指令珠串与局部变量卡片
在学习 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]来引用局部变量时, 需要首跨过所保存的长期寄存器。
- 由于在函数调用之前