APCS
ARM 过程调用标准 (ARM Procedure Call Standard)。除此之外,还有一种说法, ARM Application Procedure Call Standard (AAPCS)。
APCS 定义了:
- 对寄存器使用的限制。
- 使用栈的惯例。
- 在函数调用之间传递/返回参数。
- 可以被"回溯"的基于栈的结构的格式,用来提供从失败点到程序入口函数的列表,包括函数参数。
有了这个约定,编译器生成代码时才有参照,我们使用内联汇编或者汇编编写代码时也才有了参考,不至于各行其是,引起程序异常。
arm
默认的情况下,这些寄存器只是叫做 r0,r1,...,r14
等,在汇编器的支持下大写也是可以接受的,而 APCS 对其起了不同的别名。
r0-r15 and R0-R15
a1-a4 (argument, result, or scratch registers, synonyms for r0 to r3)
v1-v8 (variable registers, r4 to r11)
sb and SB (static base, r9)
ip and IP (intra-procedure-call scratch register, r12)
sp and SP (stack pointer, r13)
lr and LR (link register, r14)
pc and PC (program counter, r15).
aarch641
接下来看下 aarch64 的函数调用过程标准。
int funcB(int, int);
int funcC(int, int);
int funcA(int a, int b) {
int ret = funcB(a, b);
return ret;
}
int funcB(int a, int b) {
return funcC(a, b);
}
int funcC(int a, int b) {
int c = a + b;
return c;
}
int main(int argc, char *argv[])
{
return 0;
}
A->B->C。从函数是否调用了其他函数的角度看,可分为叶子函数、非叶子函数:
叶子函数:函数内部没有调用其他函数了,例如上面的 funcC。
非叶子函数:函数内部还调用了其他的函数,例如上面的 funcA、funcB。
首先认识一下寄存器。
name | number | mark |
---|---|---|
fp | x29 | 存着函数调用栈栈底,栈帧寄存器。 |
lr | x30 | 存着函数返回地址 |
sp | x31 | 存着函数调用栈栈顶,堆栈指针寄存器 |
以下是 O0
编译的结果。
0000000000400524 <funcA>:
400524: a9bd7bfd stp x29, x30, [sp, #-48]!
400528: 910003fd mov x29, sp
40052c: b9001fa0 str w0, [x29, #28]
400530: b9001ba1 str w1, [x29, #24]
400534: b9401ba1 ldr w1, [x29, #24]
400538: b9401fa0 ldr w0, [x29, #28]
40053c: 94000005 bl 400550 <funcB> // call the funcB, return address in the lr
400540: b9002fa0 str w0, [x29, #44]
400544: b9402fa0 ldr w0, [x29, #44]
400548: a8c37bfd ldp x29, x30, [sp], #48
40054c: d65f03c0 ret
0000000000400550 <funcB>:
400550: a9be7bfd stp x29, x30, [sp, #-32]! // 保护现场,fp & lr. sp 下移 32 bytes
400554: 910003fd mov x29, sp // fp = sp
400558: b9001fa0 str w0, [x29, #28] // prepare the parameters
40055c: b9001ba1 str w1, [x29, #24] // Use fp to locate the para, why not sp ?
400560: b9401ba1 ldr w1, [x29, #24]
400564: b9401fa0 ldr w0, [x29, #28]
400568: 94000003 bl 400574 <funcC> // call the funcC
40056c: a8c27bfd ldp x29, x30, [sp], #32 // restore the fp & lr, and sp 上移动 32 bytes,回收栈空间
400570: d65f03c0 ret
0000000000400574 <funcC>:
400574: d10083ff sub sp, sp, #0x20 // sp 下移 32 bytes。这里没有再存 lr & fp
400578: b9000fe0 str w0, [sp, #12] // 原因是 funcC 是叶子函数,里面没有 bl 指令等涉及更改 lr 的指令
40057c: b9000be1 str w1, [sp, #8] // 且下面的指令均使用 sp 去寻址,也没有存 fp。
400580: b9400fe1 ldr w1, [sp, #12]
400584: b9400be0 ldr w0, [sp, #8]
400588: 0b000020 add w0, w1, w0
40058c: b9001fe0 str w0, [sp, #28]
400590: b9401fe0 ldr w0, [sp, #28]
400594: 910083ff add sp, sp, #0x20 // restore the sp, 回收栈空间
400598: d65f03c0 ret
arm
处理器的栈是满降序栈,由高地址往低地址增长,sp
总是指向在当前栈桢中使用的最低地址。aarch64
架构开辟栈空间是 16 字节的倍数,这是 aarch64
对栈对齐的要求,同时也许有助于充分利用 cache
的宽度,加快 CPU 对数据的访问速度。
https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/using-the-stack-in-aarch32-and-aarch64 ↩︎