函数调用约定(Calling Convention)

x86-64 System V ABI(Linux/macOS)调用约定。

调用过程

    movq $10, %rdi        # 第 1 个参数
    movq $20, %rsi        # 第 2 个参数
    call add_numbers      # 压入返回地址并跳转到函数
    # 返回后继续执行,返回值在 %rax

call 把下一条指令的地址压栈后跳转;ret 弹出该地址到 %rip

函数序言与尾声(prologue / epilogue)

add_numbers:
    pushq %rbp            # 保存旧帧指针
    movq  %rsp, %rbp      # 建立新栈帧
    # ... 函数体 ...
    popq  %rbp            # 恢复帧指针
    ret                   # 返回(弹出返回地址到 rip)

leave 等价于 movq %rbp, %rsp + popq %rbp,用于省略帧指针场景之外的快速收尾。(历史指令 enter 已废弃,不要使用。)

现代代码常省略帧指针以优化(-fomit-frame-pointer),用 %rsp 直接寻址:

func:
    subq $16, %rsp        # 为局部变量预留栈空间
    # ...
    addq $16, %rsp
    ret

参数与返回值

  • 整数参数:%rdi %rsi %rdx %rcx %r8 %r9,第 7 个起入栈(调用者从右向左压,函数入口处第 7 个参数在 8(%rsp))。
  • 浮点参数:%xmm0%xmm7,第 9 个起入栈。
  • 返回值:整数在 %rax(128 位值用 %rax:%rdx 两寄存器);浮点在 %xmm0
  • 返回大结构体(>16 字节或含构造/析构):调用者传隐藏指针作为第 1 个参数(%rdi),函数把结果写入该地址并返回该指针。

栈对齐要求

call 指令执行前,%rsp 必须 16 字节对齐。由于 call 压入 8 字节返回地址,进入函数时 %rsp ≡ 8 (mod 16),所以函数内通常先 push 一个寄存器或 subq $8k, %rsp 恢复 16 对齐,再调其他函数。变长参数函数(printf 等)尤其严格要求对齐。

func:                   # 入口 rsp % 16 == 8
    pushq %rbp          # 压 8 字节后恢复 16 对齐
    movq  %rsp, %rbp
    subq  $16, %rsp     # 局部变量空间(仍保持 16 对齐)
    call  other         # OK,此时 rsp 仍 16 对齐
    # ... 函数体 ...
    leave
    ret

红色区域(Red Zone)

System V ABI 允许叶子函数(不再调用其他函数的函数)直接使用 %rsp 下方 128 字节的区域,信号处理程序不会破坏它——因此叶子函数可省去 subq

leaf:                   # 不调用任何函数
    movq %rdi, -8(%rsp) # 直接用 rsp 下方的红色区域,无需调整 rsp
    movq -8(%rsp), %rax
    ret

变长参数函数(printf 等)

调用变参函数前,%al 必须保存使用的向量寄存器个数(0–8)。普通整数调用设为 0:

    movl $42, %esi
    leaq fmt(%rip), %rdi
    xorl %eax, %eax     # al = 0:未用向量寄存器
    call printf@PLT

示例:两数相加

# int add(int a, int b) { return a + b; }
.globl add
add:
    movl %edi, %eax       # 第一参数 -> eax(32 位参数用 edi/esi)
    addl %esi, %eax       # eax += 第二参数
    ret

栈帧内访问参数(用帧指针时)

    pushq %rbp
    movq  %rsp, %rbp
    movl  16(%rbp), %eax  # 取第 7 个参数(前 6 个在寄存器;8=返回地址,16=旧 rbp 之上)

标准帧指针布局(pushq %rbp + movq %rsp,%rbp 之后):

        高地址
+--------------------+
| 第 8 个参数 ...     |   24(%rbp) 及以上
+--------------------+
| 第 7 个参数         |   16(%rbp)
+--------------------+
| 返回地址            |   8(%rbp)
+--------------------+
| 旧的 %rbp          |   0(%rbp)   <-- %rbp 指向这里
+--------------------+
| 局部变量 1          |   -8(%rbp)
| 局部变量 2          |   -16(%rbp)
+--------------------+   <-- %rsp(保持 16 字节对齐)
        低地址

ret 的立即数形式 ret $n 在返回时额外把 %rsp 加 n 字节,用于清理调用者压栈的参数(32 位 cdecl/stdcall 时代常见,64 位 System V 已不需要)。

调用者/被调用者保存

调用前须保存 caller-saved 寄存器(若之后还用其值);被调用函数须保证 callee-saved 寄存器(%rbx %rbp %r12–%r15)返回时不变——用 push/pop 或栈上保存。注意浮点/向量寄存器 %xmm0%xmm15 在 System V 下全部是 caller-saved,被调用函数可随意使用而无需恢复。

    pushq %rbx            # rbx 是被调用者保存,函数内要用先保存
    # ... 使用 rbx ...
    popq  %rbx
    ret

综合示例:点积(参数 + 局部保存 + 循环)

# long dot(long *a, long *b, long n);  rdi=a  rsi=b  rdx=n
.globl dot
dot:
    pushq %rbx            # 保存 callee-saved
    xorl  %eax, %eax      # sum = 0
    xorl  %ecx, %ecx      # i = 0
.Lloop:
    cmpq  %rdx, %rcx
    jge   .Ldone          # i >= n 结束
    movq  (%rdi,%rcx,8), %rbx
    imulq (%rsi,%rcx,8), %rbx
    addq  %rbx, %rax      # sum += a[i]*b[i]
    incq  %rcx
    jmp   .Lloop
.Ldone:
    popq  %rbx
    ret

另:尾调用优化——若函数最后一步只是调用另一函数并原样返回其值,可用 jmp 代替 call+ret,省一层栈。

在启用 CET/IBT 的系统上反汇编时,函数入口常看到 endbr64(间接跳转落点标记,64 位);它是控制流完整性检查的一部分,手工写汇编时不加也能运行(除非内核强制开启 IBT)。