Intel寄存器使用规范
在看反汇编程序之前,先要了解一下汇编语言中的各个寄存器的作用。
对于 Intel 架构,共有16个64位通用寄存器,各寄存器及用途如下图所示:
图中可以看到:
- %rax 作为函数返回值使用。
- %rsp 栈指针寄存器,指向栈顶,%rbp 栈基地址寄存器,保存当前帧的栈底地址。
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
- 被标识为 “miscellaneous registers” 的寄存器,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据。
- 还有一个专用的寄存器 rip - 指令指针寄存器,用于存放下一个要执行的指令地址。
什么是栈帧?
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每个未完成运行的函数占用一个独立连续区域(包含这个函数涉及的参数,局部变量,返回地址等相关信息),称为栈帧。
当调用函数时,就要压入一个新的栈帧,发起调用函数的栈帧成为调用者栈帧,被调用函数的栈帧则称为当前栈帧(rsp 和 rbp 之间的内存空间);被调用的函数运行结束后回收栈帧,回到调用者栈帧。
寄存器由谁保存?
如果一个寄存器被标识为”Caller Save”, 那么在进行子函数调用前,就需要由调用者提前保存好这些寄存器的值,保存方法通常是把寄存器的值压入堆栈中,调用者保存完成后,在被调用者(子函数)中就可以随意覆盖这些寄存器的值了。如果一个寄存被标识为“Callee Save”,那么在函数调用时,调用者就不必保存这些寄存器的值而直接进行子函数调用,进入子函数后,子函数在覆盖这些寄存器之前,需要先保存这些寄存器的值,即这些寄存器的值是由被调用者来保存和恢复的。
反汇编指令分析
以下面这段代码为例,通过命令objdump -Sd main > txt.txt
导出反汇编指令。
int Add(int *array, int n){
int t = 0;
for(int i=0; i < n; ++i) t += array[i];
return t;
}
int main() {
static int num = 10;
int a[5] = {1, 2, 3, 4, 5 };
int sum = Add(a, 5);
sum = sum + num;
return sum;
}
0000000000400710 <_Z3AddPii>:
400710: push %rbp // 将 rbp 的值压入栈中,保存上一个栈帧地址
400711: mov %rsp,%rbp // 将 rbp 赋值为 rsp,设置main函数的栈帧基址
400714: mov %rdi,-0x18(%rbp) // 将两个参数放到栈上
400718: mov %esi,-0x1c(%rbp)
40071b: movl $0x0,-0x4(%rbp) // 在栈上初始化变量 t 为 0
400722: movl $0x0,-0x8(%rbp) // 在栈上初始化变量 i 为 0
400729: jmp 400748 <_Z3AddPii+0x38> // 跳转到地址 400748 处
40072b: mov -0x8(%rbp),%eax // 将变量 i 放到 eax 中
40072e: cltq
400730: lea 0x0(,%rax,4),%rdx // rdx = rax * 4,因为 eax 是 rax 的低32位,eax的值是变量 i,所以这里 rdx = i * 4
400737:
400738: mov -0x18(%rbp),%rax // 将第一个参数数组 array 的地址放到 rax
40073c: add %rdx,%rax // 加上地址偏移 rdx
40073f: mov (%rax),%eax // 将 rax 中的地址所指向的值放到 eax 中,取数组中的元素
400741: add %eax,-0x4(%rbp) // 将变量 t 加上 eax 中的值并放在 t 上
400744: addl $0x1,-0x8(%rbp) // 将变量 i 加 1
400748: mov -0x8(%rbp),%eax // 将变量 i 放到 eax 中
40074b: cmp -0x1c(%rbp),%eax // 比较变量 i 和传递进来的第二个参数 5
40074e: jl 40072b <_Z3AddPii+0x1b> // 如果小于,则跳转到地址 40072b 处
400750: mov -0x4(%rbp),%eax // 将返回值 t 放到 eax
400753: pop %rbp // 从栈中弹出上一个栈帧的值到 rbp
400754: retq // 函数返回,retq指令会从栈中弹出返回地址到指令指针寄存器 rip 中
0000000000400755 <main>:
400755: push %rbp // 将 rbp 的值压入栈中,保存上一个栈帧地址
400756: mov %rsp,%rbp // 将 rbp 赋值为 rsp,设置main函数的栈帧基址
400759: sub $0x20,%rsp // 将 rsp 的值减去 0x20,使 rsp 下移到 rbp - 0x20 的位置,分配栈空间
40075d: movl $0x1,-0x20(%rbp) // 将 1 存放到 rbp - 0x20 的位置
400764: movl $0x2,-0x1c(%rbp) // 依次存放其他值
40076b: movl $0x3,-0x18(%rbp)
400772: movl $0x4,-0x14(%rbp)
400779: movl $0x5,-0x10(%rbp)
400780: lea -0x20(%rbp),%rax // 将 rbp - 0x20 的地址存放到 rax ,也就是数组的首地址
400784: mov $0x5,%esi // 将 0x5 放到 esi
400789: mov %rax,%rdi // 将 rax 中的值放到 rdi,两个传递参数准备就绪
40078c: callq 400710 <_Z3AddPii> // 调用 Add 函数,callq指令会先将返回地址(下一条指令执行的地址:400791)压入到栈中,然后跳转
400791: mov %eax,-0x4(%rbp) // Add 函数返回后从这里继续执行,先将函数返回值放到栈中,相当于给变量 sum 赋值
400794: mov 0x2008aa(%rip),%eax # 601044 <_ZZ4mainE3num> // 将静态变量 num 的值放到 eax
40079a: add %eax,-0x4(%rbp) // 将 eax 和变量 sum 相加,并存放到 sum 中
40079d: mov -0x4(%rbp),%eax // 将 sum 作为返回值放到 eax
4007a0: leaveq // 将 rsp 指向 rbp,然后弹出栈上保存的上一个栈帧的地址到 rbp
4007a1: retq // 函数返回,retq指令会从栈中弹出返回地址到指令指针寄存器 rip 中
函数调用栈分析
下图是上面示例程序调用 Add 函数前后的栈情况:
从汇编和栈的使用情况可以看到:
- 编译器一般是先在栈上预先分配一块空间来使用。
- 在预分配的栈空间中使用 rbp 栈帧地址加上偏移量来访问具体的地址。
- 栈使用完后没有清零。
- 有些函数调用没有分配栈空间而直接使用栈,例如上面的 Add 函数 rsp 一直没变,但是可以通过偏移量来访问没有分配的栈空间,用完即走也不需要释放。