x86架构下,函数执行借助于 hardware stack。为了不同模块函数能在runtime时可以互相调用,程序必须遵守共同的的Calling Convention,这也是ABI的一部分。推荐两本参考资料:

从汇编看,完成一个函数调用关键执行就是 call, pushd, leave, ret 等指令, 一个函数调用的入栈出栈大致如下:

  • caller 的 rip+1 入栈, rsp -= 8 (x64)
  • caller 的 rbp 入栈, rsp -= 8
  • callee 的 rsp 减小,开辟新栈帧, rsp -= callee实际需要栈帧大小
  • callee 的 rsp 增大,收回新栈帧, rsp 保存的 base addr 赋给rsp
  • caller 的 rbp 出栈,rsp += 8,
  • caller 的 rip+1 出栈, rsp += 8

这个可以通过 gdb 调试来直观的观察,例子见文章最后,编译 gcc -g main.c, 生成 a.out 带调试信息的程序。

gdb a.out
b main
display $rip   // 断住时自动显示指令寄存器 rip 的值
display $rbp
display $rsp
run            // 开始执行
layout asm     // 打开汇编窗口
si             // 下一条汇编 step in next instruction,遇到call进入
               // 回车一直单步,即可查看判断

下面的例子描述了Linux C程序的函数传参与调用约定 Calling Convention :

int add(int a, int b, int c, int d, int e, int f, int g, int h) {
    return a + b + c + d + e + f + g + h;
}

int main() {
    int a = 1;
    int b;
    b = add(a, 2, 3, 4, 5, 6, 7, 8);
    return 0;
}

对应的汇编代码如下, 用 gcc -S main.c 编译得到,删除了点开头的标志,默认汇编风格是 AT&T风格的。

add:
    pushq   %rbp                ; rbp -> [rsp], rsp -= 8, caller栈帧base addr入栈
    movq    %rsp, %rbp          ; rsp -> rbp, 新栈帧 base addr
    movl    %edi, -4(%rbp)      ; arg1 -> 局部变量(栈帧base addr + offset)
    movl    %esi, -8(%rbp)      ; arg2
    movl    %edx, -12(%rbp)     ; arg3
    movl    %ecx, -16(%rbp)     ; arg4
    movl    %r8d, -20(%rbp)     ; arg5
    movl    %r9d, -24(%rbp)     ; arg6
    movl    -8(%rbp), %eax      ; arg2 -> eax
    movl    -4(%rbp), %edx      ; arg1 -> edx
    addl    %eax, %edx          ; eax + edx -> edx (arg2 + arg1)
    movl    -12(%rbp), %eax     ; arg3 -> eax
    addl    %eax, %edx          ; eax + edx -> edx (arg2 + arg1 + arg3)
    movl    -16(%rbp), %eax     ; arg4 -> eax
    addl    %eax, %edx          ; eax + edx -> edx
    movl    -20(%rbp), %eax     ; arg5 -> eax
    addl    %eax, %edx          ; eax + edx -> edx
    movl    -24(%rbp), %eax     ; arg6 -> eax
    addl    %eax, %edx          ; eax + edx -> edx
    movl    16(%rbp), %eax      ; arg7 -> eax, (第7个参数在caller的栈帧里)
    addl    %eax, %edx          ; eax + edx -> edx
    movl    24(%rbp), %eax      ; arg8 -> eax, (第8个参数在caller的栈帧里)
    addl    %edx, %eax          ; edx -> eax, 返回值放在eax
    popq    %rbp                ; [rsp] -> rbp, rsp -= 8
    ret                         ; [rsp] -> rip

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $32, %rsp           ; 开辟栈32
    movl    $1, -8(%rbp)        ; 保存局部变量
    movl    -8(%rbp), %eax      ; 局部变量 -> eax
    movl    $8, 8(%rsp)         ; 第8个参数放到栈里
    movl    $7, (%rsp)          ; 第7个参数放到栈里
    movl    $6, %r9d            ; arg6 -> r9d
    movl    $5, %r8d            ; arg5 -> r8d
    movl    $4, %ecx            ; arg4 -> ecx
    movl    $3, %edx            ; arg3 -> edx
    movl    $2, %esi            ; arg2 -> esi
    movl    %eax, %edi          ; arg1 -> edi
    call    add                 ; rip++ -> [rsp], rsp -= 8, 调用完毕函数后的下一条指令入栈
    movl    %eax, -4(%rbp)      ; 存返回值由寄存器eax到局部变量
    movl    $0, %eax0 -> eax
    leave                       ; rbp -> rsp, [rsp] -> rbp, rsp += 8
    ret                         ; [rsp] -> rip
Logo

更多推荐