Skip to content

c calling convention

· xieyuheng

指令珠串与局部变量卡片 ​

在学习 forth 时,我们用珠串来理解函数调用机制。

  (op)
  (op)
[ (op) ] - [ (op) ] - [ (op) ]
  (op)       (op)       (op)
             (op)

C 的函数调用机制,也可以用珠串来想象。 但是与 forth 不同的是:

  • 传递参数用参数寄存器,传递返回值用 rax。
  • 每个珠串中的珠子是指令,而不是 forth 中的线串码地址。
  • 每一串珠子带有局部变量卡片。
  (op)
  (op)
[ (op) ·----· ] - [ (op) ·----· ] - [ (op) ·----· ]
  (op) | v1 |       (op) | v1 |       (op) | v1 |
       | v2 |       (op) | v2 |            ·----·
       ·----·            | v3 |
                         ·----·

每次珠子在移动的时候,可以修改卡片中局部变量的值。

卡片在内存中的具体结构是:

  • 栈向小地址方向生长。
  • 卡片中前两个位置可以看作是表头中保存的元数据, 用来连接到之前的卡片(前一个 rbp 指针), 以及连接到之前的珠子(返回地址)。
内存说明
[rbp + 8]返回地址
[rbp]前一个 rbp 指针
-----------------------------
[rbp - 8]variable 1
[rbp - 16]variable 2
......
[rsp]variable n

构建局部变量卡片所需要用到的 .prolog 和 .epilog 代码:

  • 预留 N bytes 空间,来保存局部变量。
  • 由于函数调用之前,rsp 必须 16 byte 对齐, 因此下面的例子中,N 必须 16 byte 对齐。
asm
.prolog:
  push rbp
  mov rbp, rsp
  sub rsp, N

  <body>

.epilog:
  add rsp, N
  pop rbp
  ret

通用寄存器分类 ​

通用寄存器分为两类。

  • 临时寄存器:rax rcx rdx rsi rdi r8 r9 r10 r11

    包括了:

    • 参数寄存器:rdi rsi rdx rcx r8 r9
    • 返回值寄存器:rax
  • 长期寄存器:rsp rbp rbx r12 r13 r14 r15

    包括了:

    • 栈指针寄存器:rsp
    • 栈基寄存器:rbp

caller 和 callee 的职责分配:

  • caller:

    • 假设 callee 不会修改长期寄存器。

    • 假设 callee 会修改临时寄存器。 因此在调用 callee 之前,如果有 live 的变量被分配到了临时寄存器中, 就需要先保存这些寄存器,在调用结束时恢复。

      • 由于在函数调用之前 rsp 必须 16 byte 对齐, 所以如果保存临时寄存器之后没有对齐, 就需要 sub rsp, 8 以保证对齐, 在调用结束时恢复。
  • callee:

    • 假设 caller 不会用到临时寄存器,可以随意使用临时寄存器。

    • 假设 caller 用到了所有长期寄存器。 因此如果需要使用长期寄存器, 就先在 .prolog 中保存长期寄存器, 再在 .epilog 中恢复。

      • 由于在函数调用之前 rsp 必须 16 byte 对齐, 所以在 .prolog 中保存寄存器要在 sub rsp, N 之前, 利用 N 来做 16 byte 对齐时, 要考虑到已经保存了的长期寄存器数量的。 之后在用 [rbp + X] 来引用局部变量时, 需要首跨过所保存的长期寄存器。