3.1 程序的机器级表示
现有两个源文件:
main.c
#include<stdio.h>
void mulstore(long, long, long*);
int main()
{
long d;
mulstore(2, 3, &d);
printf("2 * 3 --> %ld\n", d);
return 0;
}
long mult2(long a, long b) {
long s = a * b;
return s;
}
mstore.c
long mult2(long, long);
void mulstore(long x, long y, long* dest) {
long t = mult2(x, y);
*dest = t;
}
执行指令
gcc -Og -o prog main.c mstore.c
其中-o prog 表示将main.c 和mstore.c 编译后得到的可执行文件的文件名设置为prog ,-Og 是用来告诉gcc 编译器生成符合原始C代码整体结构的机器代码。实际项目中可能会使用-O1 或-O2 (人称吸氧)等编译优化
执行指令
gcc -Og -S mstore.c
将获得mstore.c 对应的汇编文件mstore.s
这其中,以. 开头的行都是指导汇编器和链接器工作的伪指令,在查看时可以忽略,得到如下汇编
其中pushq %rbx 表示将寄存器rbx 的值压入程序栈进行保存。 这里引入寄存器的北京知识,在Intel x86-64的处理器中包含16个通用目的寄存器: 这16个寄存器用来存储数据和指针。 其中分为调用者保存寄存器和被调用者保存寄存器 这里是func_A调用func_B,所以func_A是调用者,func_B是被调用者。因为func_B中修改了寄存器%rbx ,而func_A在调用func_B前后也使用了寄存器%rbx ,因此需要保证在调用func_B前后,func_A使用%rbx 的值应该是一致的。
第一种:在func_A调用func_B前,提前保存%rbx 的值,然后再调用结束后再将提前保存的值重恢复到%rbx 中,这称为调用者保存。 第二种:在func_B调用%rbx 前,先保存%rbx 的值,在调用结束后,返回前,再恢复%rbx 的值,这称为被调用者保存。
每个寄存器的保存策略不一定相同。
其中调用者寄存器:
%rbx, %rbp, %r12, %r13, %r14, %r15
被调用者寄存器:
%r10, %r11
%rax
%rdi, %rsi, %rdx, %rcx, %r8, %r9
pushq 就是用来保存% rbx 的值,在函数返回前,使用popq 来恢复%rbx 的值。
movq 就是将%rdx 的值复制到%rbx 中 这条指令执行后,%rbx 的内容和%rdx 的内容一致,都是dest 指针所指向的内存地址
根据寄存器用法定义 函数multistore的三个参数分别保存在%rdi,%rsi,%rdx 中
这里的pushq , movq 的后缀q 都表示数据的大小。早期机器是16位,后来才扩展到32位。Intel用字(word)来表示16位的数据类型,32 位的数据类型称为双字,64位的数据类型称为4字。 其中: b -> byte w -> word l -> long word(double word) q ->quad word
call mult2@PLT 的返回值保存到%rax 中。 movq %rax (%rbx) 是指将%rax 的值送到%rbx 所指向的内存地址处 ret 表示函数返回即return
对于从源文件生成机器代码文件 执行gcc -Og -c mstore.c ,即可得到机器代码文件mstore.o
借反汇编工具objdump 可以将机器代码文件反汇编成汇编文件
objdump -d mstore.o
3.2 寄存器与数据传送指令
寄存器
这是上述所说的保存寄存器
调用者保存寄存器(Callee Saved)
%rbx, %rbp, %r12, %r13, %r14, %r15
被调用者保存寄存器(Caller Saved)
%r10, %r11
%rax 保存函数返回值
%rdi, %rsi, %rdx, %rcx, %r8, %r9 传递函数参数
%rsp 用于保存程序栈结束位置
指令
操作码
movq, addq, subq, xorq, ret 等
操作数
- 立即数(Immediate)
在AT&T格式的汇编中,立即数以$ 符号开头,后面跟一个整数,这个整数需要满足标准C语言的定义,如$8 。 - 寄存器(Register)
在64位处理器上,64,32,16,8位的寄存器都可以作为操作数,如%rdx 寄存器带了小括号的情况,表示内存引用,如(%rdx) - 内存引用(Memory Reference)
将内存抽象成一个字节数组,当需要从内存中存取数据时,需要获取目的地址的起始地址addr 和数据长度b
mov指令
其中源操作数可以为立即数,寄存器和内存引用,而目的寄存器只能为寄存器或内存引用。同时x86-64位规定,源操作数和目的操作数不能同时为内存引用,因此如果需要将内存地址A的内容赋值给内存地址B,需要进行两次mov 操作,第一次mov A register ,第二次mov register B 。
使用movb,movw, movl, movq 是与其寄存器的位数对应的,b -> 8, w ->16, l -> 32, q ->64 。
当movq 指令的源操作数是立即数时,该立即数只能是32位的补码表示。对该数进行符号位扩展(Signed extended),将64位数传送到目的位置。 当立即数是64位时,使用指令movabsq ,该指令的源操作数可以是任意的64位整数,目的操作数只能是寄存器。
当使用movl 指令且其目的操作数是寄存器时,会将该寄存器的高4字节设置为全0。x86-64位处理器规定,任何对寄存器生成的32位值的指令都会把该寄存器的高位部分设置为0
当源操作数的数位小于目的操作数,需要进行零扩展或符号位扩展
零扩展
z表示zero零扩展
b, w, l, q分别表示8,16,32,64位
movzbw
movzbl
movzbq
movzwl
movzwq
movl代替了movzlq的功能,所以不需要movzlq
符号位扩展
s表示signed 符号位扩展
b, w, l, q分别表示8,16,32,64位
movsbw
movsbl
movsbq
movswl
movswq
movslq
cltq = movslq %eax, %rax
|