ARM 过程调用标准 APCS 以及堆栈使用

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 对数据的访问速度。


  1. https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/using-the-stack-in-aarch32-and-aarch64 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章