您现在的位置是:首页 >其他 >【CSAPP 3.3~3.4】x86-64访问数据网站首页其他
【CSAPP 3.3~3.4】x86-64访问数据
文章目录
1. 数据格式
在汇编语言层面,Intel用术语字word表示16位数据类型,双字double words表示32位数据类型,四字quad words表示64位数据类型。
这里字的概念和字长不一样,要注意区分。
下表给出了x86-64环境(64位机器+64位编译)下C语言的基本数据类型表示。
C声明 | Intel数据类型 | 汇编代码后缀 | 字节大小 |
|---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char * | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
此外还有不太常用的数据类型,如long long、long double。
大多数gcc生成的汇编代码指令都有一个字符的后缀,表面操作数的大小。例如:传送字节movb、传送字movw、传送双字movl、传送四字movq。
用后缀l表示双字,因为32位数被看成是长字long word。汇编代码也使用后缀l表示8字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。
2. 访问信息
一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针,它们的名字都以%r开头。

最初的8086中有8个16位寄存器,即%ax ~ %sp。扩展到IA32架构时,为了兼容旧架构,这些寄存器的标号扩展为%eax ~ %esp。扩展到64位时,原来的8个寄存器标号为%rax ~ %rsp,此外还增加了8个新的寄存器%r8 ~ %r15。
指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。字节级操作可以访问最低的字节,16位操作可以访问最低的2个字节,32位操作可以访问最低的4个字节,64位操作可以访问整个寄存器。
对于操作小于8字节数据的指令,寄存器中剩下的字节会怎样,有以下规则:
- 仅操作最低的
1字节或2字节时,寄存器中其他的字节不变。 - 仅操作低
4字节时,高4字节会被置0。
在常见的程序里,不同的寄存器扮演不同的角色。如%rsp用来指明运行时栈的结束位置。
有一组标准的编程规范控制着如何使用寄存器来管理栈、传递函数参数、存储函数的返回值,存储局部和临时数据。
2.1. 操作数指示符
大多数指令有一个或多个操作数,指示执行一个操作要使用的源数据值、目的位置。源数据可以以常数形式给出,或从寄存器或内存中读出。目的位置可以是寄存器或内存。因此操作数的类型有三种:
- 立即数,用来表示常数值。在
ATT格式的汇编代码中,立即数的书写方式是$后面跟一个标准C表示法表示的整数,如$-577、$0xff、$010等。不同的指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。 - 寄存器,表示某个寄存器的内容。使用
16个寄存器的低1、2、4或8字节作为操作数。我们使用 r a r_a ra表示任意寄存器a,使用 R [ r a ] R[r_a] R[ra]表示该寄存器的值。这里将寄存器集合看成数组R,寄存器标号是数组下标。 - 内存引用,它会根据计算出来的地址访问某个内存位置。将内存看成一个很大的字节数组,我们用符号
M
b
[
A
d
d
r
]
M_b[Addr]
Mb[Addr]表示对存储在内存中从地址
Addr开始的b的字节的访问。通常省略下标b。
| 类型 | 格式 | 操作数值 |
|---|---|---|
| 立即数 | $ I m m m Immm Immm | I m m m Immm Immm |
| 寄存器 | r a r_a ra | R [ r a ] R[r_a] R[ra] |
| 存储器 | I m m Imm Imm | M [ I m m ] M[Imm] M[Imm] |
| 存储器 | ( r a r_a ra) | M [ R [ r a ] ] M[R[r_a]] M[R[ra]] |
| 存储器 | I m m ( r b ) Imm(r_b) Imm(rb) | M [ I m m + R [ r b ] ] M[Imm+R[r_b]] M[Imm+R[rb]] |
| 存储器 | ( r b , r i ) (r_b, r_i) (rb,ri) | M [ R [ r b ] + R [ r i ] ] M[R[r_b]+R[r_i]] M[R[rb]+R[ri]] |
| 存储器 | I m m ( r b , r t ) Imm(r_b, r_t) Imm(rb,rt) | M [ I m m + R [ r b ] + R [ r i ] M[Imm+R[r_b]+R[r_i] M[Imm+R[rb]+R[ri]] |
| 存储器 | ( , r i , s ) (,r_i,s) (,ri,s) | M [ R [ r i ] ∗ s ] M[R[r_i]*s] M[R[ri]∗s] |
| 存储器 | I m m ( , r i , s ) Imm(,r_i,s) Imm(,ri,s) | M [ I m m + R [ r i ] ∗ s ] M[Imm+R[r_i]*s] M[Imm+R[ri]∗s] |
| 存储器 | ( r b , r i , s ) (r_b, r_i, s) (rb,ri,s) | M [ R [ r b ] + R [ r i ] ∗ s ] M[R[r_b]+R[r_i]*s] M[R[rb]+R[ri]∗s] |
| 存储器 | I m m ( r b , r i , s ) Imm(r_b, r_i, s) Imm(rb,ri,s) | M [ I m m + R [ r b ] + R [ r i ] ∗ s ] M[Imm+R[r_b]+R[r_i]*s] M[Imm+R[rb]+R[ri]∗s] |
2.1.1. 练习
假设下面的值存放在指明的内存地址和寄存器中。
| 地址 | 值 |
|---|---|
| 0x100 | 0xFF |
| 0x104 | 0xAB |
| 0x108 | 0x13 |
| 0x10c | 0x11 |
| 寄存器 | 值 |
|---|---|
| %rax | 0x100 |
| %rcx | 0x1 |
| %rdx | 0x3 |
填写下表,给出操作数的值:
| 操作数 | 值 |
|---|---|
| %rax | 0x100 |
| 0x104 | 0xAB |
| $0x108 | 0x108 |
| (%rax) | 0xFF |
| 4(%rax) | 0xAB |
| 9(%rax, %rdx) | 0x11 |
| 260(%rcx, %rdx) | 0x13 |
| 0xFC(, %rcx, 4) | 0xFF |
| (%rax, %rdx, 4) | 0x11 |
2.2. 数据传送指令
最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。下表列出来的是最简单的数据传送指令——MOV类,由四条指令组成:movb、movw、movl和movq,这些指令都执行同样的操作,区别在于所操作的数据大小不同。
| 指令 | 效果 | 描述 |
|---|---|---|
| MOV S,D | S → o →D | 传送 |
| movb | 传送字节 | |
| movw | 传送字 | |
| movl | 传送双字 | |
| movq | 传送四字 | |
| movabsq I,R | I → o →R | 传送绝对的四字 |
源操作数指定的值是一个立即数,存储在寄存器或内存中;目的操作数指定一个位置,要么是一个寄存器,要么是一个内存地址。x86-64中两个操作数不能都指向内存位置。寄存器操作数可以是16个寄存器有标号部分中的任意一个,寄存器部分的大小必须与指令最后一个字符(b、w、l或q)指定的大小匹配。通常情况下,MOV指令只会更新目的操作数指定的那些寄存器字节或内存位置。唯一例外的是movl指令以寄存器作为目的操作数时,它会把寄存器的高4字节设置为0。
下面的MOV指令示例给出了源和目的类型的5种可能的组合。第一个是源操作数,第二个是目的操作数。
movl $0x4050, %eax
movw %bp, %sp
movb (%rdi, %rcx), %al
movb $-17, (%rsp)
movq %rax, -12(%rbp)
movabsq指令是处理64位立即数的。常规的movq指令只能以表示为32位补码数字的立即数作为源操作数,然后把这个值符号扩展到64位,放到目的位置。movabsq指令能够以任意64位立即数作为源操作数,且目的操作数只能是寄存器。
在将较小的源值复制到较大的目的时,需要使用MOVZ和MOVS做数据扩展。MOVZ类中的指令把目的中剩余的字节填充为0,MOVS类中的指令通过符号扩展填充。每条指令名字的最后两个字符分别表示源大小和目的大小。如:
movzbw S, R // 将做了零扩展的字节传送到字
movsbw S, R // 将做了符号扩展的字节传送到字
cltq // 把%eax符号扩展到%rax
MOVZ和MOVS只能以寄存器或内存地址作为源,以寄存器作为目的。
movl %ebx, %rax中movl默认做了4字节传送到8字节的零扩展,等价于movzlq %ebx, %rax。
2.2.1. 练习1
对于下面汇编代码的每一行,根据操作数,确定适当的指令后缀。
movl %eax, (%rsp)
movw (%rax), %dx
movb $0xFF, %bl
movb (%rsp, %rdx, 4), %dl
movq (%rdx), %rax
movw %dx, (%rax)
2.2.2. 练习2
请解释一下如下的每行代码分别有什么问题。
movb $0xF, (%ebx) // x86-64环境下,内存地址是64位而不是32位,应该使用(%rbx)而不是(%ebx)
movl %rax, (%rsp) // 传送8字节数据应该使用movq
movw (%rax), 4(%rsp) // 两个操作数不能都为内存地址
movb %al, %sl // 不存在寄存器%sl
movq %rax, $0x123 // 立即数不能作为目的操作数
movl %eax, %rdx // 目的操作数大小大于源操作数大小,逻辑上应该使用`movzlq`或`movslq`
movb %si, 8(%rbp) // %si是2字节大小,应该用movw
不存在
movzlq这个指令,movzlq %eax, %rdx等价于movl %eax, %edx,%rdx的高4字节会被自动置0。
2.3. 数据传送示例
有如下C代码,exchange.c
long exchange(long *xp, long y)
{
long x = *xp;
*xp = y;
return x;
}
使用gcc产生的汇编代码:
[root@localhost2 3]# gcc -Og -S exchange.c
[root@localhost2 3]# cat exchange.s
.file "exchange.c"
.text
.globl exchange
.type exchange, @function
exchange:
.LFB0:
.cfi_startproc
movq (%rdi), %rax
movq %rsi, (%rdi)
ret
.cfi_endproc
.LFE0:
.size exchange, .-exchange
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits
核心部分代码是:
exchange:
movq (%rdi), %rax // long x = *xp;
movq %rsi, (%rdi) // *xp = y;
ret // return x;
函数调用的参数通过寄存器传递,第一个参数xp保存在%rdi,第二个参数y保存在%rsi。%rax保存函数的返回值。
可以看到C语言中所谓的“指针”其实就是地址,引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。像x这样的局部变量通常保存在寄存器,而不是内存中。CPU访问寄存器比访问内存快得多。如果程序中有对x取地址的操作&x,那一定要在内存中为x分配空间。
2.3.1. 练习
假设变量sp和dp的声明如下:
src_t *sp;
dest_t *dp;
这里的src_t和dest_t是用typedef声明的数据类型。我们想使用适当的数据传送指令来实现下面的操作:
*dp = (dest_t)*sp;
假设sp和dp的值分别存储在寄存器%rdi和%rsi中。对于下表中每个表项,给出实现指定数据传送的两条指令。其中第一条指令应该从内存中读数,做适当的转换,并设置寄存器%rax的适当部分。第二条指令要把%rax的适当部分写到内存,在这两种情况中,寄存器的部分可以是%rax、%eax、%ax或%al。
当执行强制类型转换既涉及大小变化又涉及C语言中符号变化时,操作应该先改变大小。
| src_t | dest_t | 指令 |
| long | long | movq (%rdi), %rax |
| movq %rax, (%rsi) | ||
| char | int | movsbl (%rdi), %eax |
| movl %eax, (%rsi) | ||
| char | unsigned | movsbl (%rdi), %eax |
| movl %eax, (%rsi) | ||
| unsigned char | long | movzbl (%rdi), %eax |
| movq %rax, (%rsi) | ||
| int | char | movl (%rdi), %eax |
| movb %al, (%rsi) | ||
| unsigned | unsigned char | movl (%rdi), %eax |
| movb %al, (%rsi) | ||
| char | short | movsbw (%rdi), %ax |
| movw %ax, (%rsi) |
数据扩展时,第一条指令读源数据并扩展,第二条指令存放数据。(带扩展的mov指令只能以寄存器为目的操作数)
数据截断时,第一条指令读完整的源数据,第二条指令获取低位字节。
数据截断时,能不能把截断操作放在第一条指令,即从内存中只读低几个字节的数据?跟上述方案有啥区别?
2.3.2. 练习
已知某个函数的原型如下:
void decode1(long *xp, long *yp, long *zp);
它的编码代码表示如下:
decode1:
movq (%rdi), %r8
movq (%rsi), %rcx
movq (%rdx), %rax
movq %r8, (%rsi)
movq %rcx, (%rdx)
movq %rax, (%rdi)
ret
其中xp、yp、zp分别存储在%rdi、%rsi和%rdx中。
请写出等价于上面汇编代码的decode1的C代码。
void decode1(long *xp, long *yp, long *zp)
{
long x = *xp;
long y = *yp;
long z = *zp;
*yp = x;
*zp = y;
*xp = z;
}
2.4. 压入和弹出栈数据
栈是一种数据结构,可以添加或者删除值,不过要遵循“后进先出”的原则。通过push操作把数据压入栈中,通过pop操作删除数据。它具有一个属性:弹出的值永远是最近被压入而且仍然在栈中的值。栈可以实现为一个数组,总是从数组的一端插入和删除元素,这一端被称为栈顶。
| 指令 | 效果 | 描述 |
|---|---|---|
| pushq S | R[%rsp] - 8 → o → R[%rsp]; S → o → M[R[%rsp]] | 将四字压入栈 |
| pop D | M[R[%rsp]] → o → D; R[%rsp] + 8 → o → R[%rsp] | 将四字弹出栈 |
在x86-64中,程序栈存放在内存中某个区域,栈向下增长,栈顶元素的地址是所有栈中元素地址中最低的。栈指针%rsp保存着栈顶元素的地址。
pushq %rbp等价于
subq $8, %rsp
movq %rbp, (%rsp)
popq %rax等价于
movq (%rsp), %rax
add $8, %rsp
pop时不会主动清理原来栈顶位置的数据。





U8W/U8W-Mini使用与常见问题解决
QT多线程的5种用法,通过使用线程解决UI主界面的耗时操作代码,防止界面卡死。...
stm32使用HAL库配置串口中断收发数据(保姆级教程)
分享几个国内免费的ChatGPT镜像网址(亲测有效)
Allegro16.6差分等长设置及走线总结