CXD Linux Engineer

objdump反汇编代码阅读

2019-12-12

Intel寄存器使用规范

在看反汇编程序之前,先要了解一下汇编语言中的各个寄存器的作用。 对于 Intel 架构,共有16个64位通用寄存器,各寄存器及用途如下图所示:
register

图中可以看到:

  1. %rax 作为函数返回值使用。
  2. %rsp 栈指针寄存器,指向栈顶,%rbp 栈基地址寄存器,保存当前帧的栈底地址。
  3. %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
  4. 被标识为 “miscellaneous registers” 的寄存器,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据。
  5. 还有一个专用的寄存器 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 函数前后的栈情况:
stack

从汇编和栈的使用情况可以看到:

  1. 编译器一般是先在栈上预先分配一块空间来使用。
  2. 在预分配的栈空间中使用 rbp 栈帧地址加上偏移量来访问具体的地址。
  3. 栈使用完后没有清零。
  4. 有些函数调用没有分配栈空间而直接使用栈,例如上面的 Add 函数 rsp 一直没变,但是可以通过偏移量来访问没有分配的栈空间,用完即走也不需要释放。

参考

x86-64 下函数调用及栈帧原理
x86指令速查表


下一篇 数据库概念

Comments

Content