3.1 程序的机器级表示
编写main.c
和mstore.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;
}
long mul2(long, long);
void mulstore(long x, long y, long *dest) {
long t = mul2(x, y);
*dest = t;
}
x -> rdi
y -> rsi
dest -> rdx
两个文件一起编译生成可执行文件prog
gcc -Og -o prog main.c mstore.c
汇编
也可以编译单个文件,生成汇编文件mstore.s
gcc -Og -S mstore.c
删除部分无关代码
mulstore:
pushq %rbx
movq %rdx, %rbx
call mul2@PLT
movq %rax, (%rbx)
popq %rbx
ret
pushq
意思是将寄存器rbx
的值压如程序栈进行保存
补充寄存器的知识:
在Intel x86-64的处理器中包含16个通用目的寄存器,这些寄存器用来存放整数数据和指针,都是以%r
开头
为什么需要对寄存器进行压栈保存,因为在函数中修改了全局的东西,当函数调用完成之后要恢复原样
调用者保存寄存器和被调用者保存寄存器
函数A称为调用者(caller),函数B称为被调用者(callee),寄存器rbx
在函数B中被修改了,逻辑上rbx
的内容在调用函数B前后应该保持一致,解决这个问题有2个策略
- 一个是函数A在调用函数B之前,提前保存寄存器
rbx
的内容,执行完函数B之后,再恢复rbx
的内容,这个策略就称为调用者保存- 一个是函数B在使用寄存器
rbx
之前,先保存寄存器的内容,在函数B返回之前,先恢复rbx
原来的内容,称之为被调用者保存
不同的寄存器采取不同的策略,具体如图所示
寄存器rbx
被定义为被调用者保存寄存器,pushq
就是用来保存rbx
的内容,在函数返回之前,使用了pop
指令,恢复rbx
的内容
movq %rdx, %rbx
第二行汇编的含义是将寄存器rdx
的内容复制到寄存器rbx
根据寄存器用法的定义,函数mulstore
的三个参数分别保存在rdi, rsi, rdx
中,这条指令执行完,寄存器rbx
的内容与寄存器rdx
一致,都是dest
指针所指向的内存的地址
movq
指令的后缀q
表示数据的大小,由于早期的机器是16位,后来才扩展到32位,因此,用字word
表示16位的数据类型,32位称为双字,64位称为四字
图中给出了C语言的基本数据类型对应的汇编后缀表示
大多数GCC生成的汇编指令都有一个字符后缀来表示操作数的大小,例如,数据传送指令就有4个变种
- movb: Move byte, 传送8位
- movw: Move word, 传送16位
- movl: Move double word, 传送32位
- movq: Move quad word, 传送64位
call mul2@PLT
call
指令表示函数调用,返回值会保存在寄存器rax
中,因此,寄存器rax
中保存了x
和y
的乘积结果
movq %rax, (%rbx)
这条指令将寄存器rax
的值送到内存中,内存的地址就存放在寄存器rbx
中
popq %rbx
恢复寄存器rbx
的值
反汇编
将编译选项-S
替换为-c
,就可以生成对应的机器码目标文件mstore.o
gcc -Og -c mstore.c
该文件是二进制文件无法直接查看,要用到反汇编工具objdump
,将机器代码反汇编成汇编代码
objdump -d mstore.o
mstore.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <mulstore>:
0: 53 push %rbx
1: 48 89 d3 mov %rdx,%rbx
4: e8 00 00 00 00 callq 9 <mulstore+0x9>
9: 48 89 03 mov %rax,(%rbx)
c: 5b pop %rbx
d: c3 retq
通过对比反汇编得到的汇编代码与编译器直接生成的汇编代码,可以看到有细微的差异,反汇编代码省略了后缀q
,但在call
和ret
指令后添加了后缀q
,由于q
只是表示大小指示符,大多数情况下是可以省略的