1

还是看实际例子更直接,对于这样一份 C 代码:

int add (int a, int b) {
    return a + b;
}

int main (void) {
    int a = 10;
    int b = 20;
    int c = add(a, b);
    return c;
}

先使用 gcc 编译

[dou@localhost ~ 0 ]$ gcc -g -O0 hello.c -o hello

然后使用 objdump 生成 Intel 风格的汇编代码
只摘取其中结果中最重要的汇编代码,# 之后的内容为手动加的注释

[dou@localhost ~ 0 ]$ objdump -M intel -j .text -d hello

00000000004004ed <add>:
  4004ed:       55                      push   rbp          # 将 rbp 寄存器的值压入栈
  4004ee:       48 89 e5                mov    rbp,rsp      # 将 rsp 寄存器的值 移动到 rbp 寄存器,栈底(rbp)移动到原来的栈顶的位置(rsp)
  4004f1:       89 7d fc                mov    DWORD PTR [rbp-0x4],edi  # 将 edi 寄存器的值(参数 a),移动到 -0x4(相对于 rbp 的
地址)
  4004f4:       89 75 f8                mov    DWORD PTR [rbp-0x8],esi  # 将 esi 寄存器的值(参数 b),移动到 -0x8(相对于 rbp 的
地址)
  4004f7:       8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]  # 将 -0x8 的值移动到 eax
  4004fa:       8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]  # 将 -0x4 的值移动到 edx
  4004fd:       01 d0                   add    eax,edx                  # eax += edx // a + b;
  4004ff:       5d                      pop    rbp                      # 从栈顶弹出一个值,放到 rbp 里
  400500:       c3                      ret                             # 从栈顶弹出一个值,放到 rip 里,也就是相当于 pop rip

0000000000400501 <main>:
  400501:       55                      push   rbp                      # 将 rbp 压入栈
  400502:       48 89 e5                mov    rbp,rsp                  # 将 rsp 寄存器的值 移动到 rbp 寄存器,栈底(rbp)移动到原来的栈顶的位置(rsp)
  400505:       48 83 ec 10             sub    rsp,0x10                 # rsp -= 0x10,栈顶向下生长高度 0x10
  400509:       c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa  # 将整数 0xa 移动到 -0x4(相对于 rbp)  // a = 10
  400510:       c7 45 f8 14 00 00 00    mov    DWORD PTR [rbp-0x8],0x14 # 将整数 0x14 移动到 -0x8(相对于 rbp) // b = 20
  400517:       8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]  # 将 -0x8 移动到 edx
  40051a:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]  # 将 -0x4 移动到 eax
  40051d:       89 d6                   mov    esi,edx                  # esi = edx; 为第一个参数寄存器赋值
  40051f:       89 c7                   mov    edi,eax                  # edi = eax; 为第二个参数寄存器赋值
  400521:       e8 c7 ff ff ff          call   4004ed <add>             # 调用函数 add  // add(a, b)
  400526:       89 45 f4                mov    DWORD PTR [rbp-0xc],eax  # 将 eax 移动到 -0xc; 返回值入栈
  400529:       8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]  # 将 -0xc 移动到 eax; 准备 main 函数返回值
  40052c:       c9                      leave                           # 相当于mov rsp, rbp; + pop rbp; 将 rbp 和 rsp 回退到
上一帧
  40052d:       c3                      ret                             # 从栈顶弹出一个值,放到 rip 里,也就是相当于 pop rip
  40052e:       66 90                   xchg   ax,ax                    # nop

这里有几个知识点

0> 函数返回值寄存器

函数调用的返回值,会放入 rax 寄存器。

1> 函数参数寄存器

当函数参数少于 6 个的时候,参数从左到右依次放入:rdi, rsi, rdx, rcx, r8, r9
当大于 6 个参数时,剩余的参数从右边往左一次压入栈(取参数的时候,依次弹出,就是自然从左到右的顺序)

2> 函数调用的栈操作

call 相当于 push rip; + jump [address];
ret 相当于 pop rip;

3> callee-saved vs caller-saved

r12, r13, r14, r15, rbx, rbp 是 callee-saved 寄存器。
r10, r11,函数参数、返回值寄存器,都是 caller-saved 寄存器。

rsp 寄存器有一点点特殊,但是严格意义上也属于 callee-saved 寄存器。


doujiang24
209 声望1k 粉丝

Core developer of OpenResty.