什么是栈的字节对齐?

栈的字节对齐,实际是指栈顶指针必须是某字节的整数倍

AAPCS栈使用规约:

在ARM上编程,但凡涉及到调用,就需要遵循一套规约 AAPCS:《Procedure Call Standard for the ARM Architecture》。

这套规约里面对栈使用的约定如下:

  • 5.2.1.1 Universal stack constraints At all times the following basic constraints must hold: Stack-limit < SP <= stack-base. The stack pointer must lie within the extent of the stack. SP mod 4 = 0. The stack must at all times be aligned to a word boundary. A process may only access (for reading or writing) the closed interval of the entire stack delimited by [SP, stack-base – 1] (where SP is the value of register r13). Note : This implies that instructions of the following form can fail to satisfy the stack discipline constraints, even when reg points within the extent of the stack. ldmxx reg, {…, sp, …} // reg != sp If execution of the instruction is interrupted after sp has been loaded, the stack extent will not be restored, so restarting the instruction might violate the third constraint.
  • 5.2.1.2 Stack constraints at a public interface. The stack must also conform to the following constraint at a public interface: SP mod 8 = 0. The stack must be double-word aligned.

可以看到,规约规定:栈任何时候都必须 4B 对齐,在调用入口必须 8B 对齐。 在这个约定里,栈的4字节对齐确实得任何时候都遵守,而且你想不遵守都难,因为SP的最后两位是硬件上保持0的。而对于8字节对齐,这就需要码农和编译器配合着来。需要说明的一点是,8字节对齐即使不遵守,一些情况下也没问题,只要主调和被调用例程两边把堆栈使用、传参、返回等处理好就行,也就是说两边有自己的一套约定就行。但是有时候,主调这边在调用严格遵守 AAPCS 的函数时,没有将栈保持在8字节对齐上,那就会出问题!

如何编程?

编程时,对于AAPCS栈使用约定的遵守,总的来说就两条:

  1. 汇编文件中需要我们亲自动手来保证遵守AAPCS栈使用约定; (特别注意每次从汇编进入 C 的世界时,要保证汇编部分的代码在调用 C 接口时栈是8字节对齐的,不要疏忽了,因为C编译器可不负责调整。C编译器说你得送给我的SP就是8字节对齐的,我才能保证接下来的C部分没有结束之前,遵守AAPCS栈使用约定)
  2. 在 C 文件中,由编译器来处理;

PRESERVE8 伪指令

在芯片的启动代码中,这个伪指令并非必不可少,可以不要这个伪指令。但是有了这两个伪指令(ALIGN 和 PRESERVE8),可以在确保遵守 AAPCS 的道路上加一道保险,使得AAPCS栈使用约定的遵守在实际编程时变得稍微容易点。

当在段定义头(即: AREA 伪指令的相关代码)当中使用 ALIGN=? 时,ALIGN属性的作用为设定该代码段或数据段的首址的对齐位置(例如: ALIGN=3就表示,该段首址将被安排在2^3=8字节对齐处)。需要注意的是,除了 AREA 的 ALIGN 属性,还有一个同名的 ALIGN指令,ALIGN指令使用在段内部的,用来调整 ALIGN指令下一条命令或数据的对齐位置。

PRESERVE8伪指令并不会对栈进行任何修改。 PRESERVE8伪指令的使用有3种方法,分别如下:(其中1、2的用法是等价的)

  1. 1. PRESERVE8
  2. 2. PRESERVE8 {TRUE}
  3. 3. PRESERVE8 {FALSE}

如果不写,那么由编译器来决定在编译过程中将汇编文件标识为PRES8属性还是~PRES8属性(也即加还是不加该伪指令),但经过实验,发现编译器在加不加这条伪指令上表现的并不完全可靠。。。

PRESERVE8 伪指令起什么作用呢?

加上 PRESERVE8 {TRUE},还是 PRESERVE8 {FALSE}?

如果你想要告诉汇编器说:“在我这个汇编文件中保证栈的8字节对齐,我这个文件对栈的任何时刻的任何操作都是8字节对齐的”,那么你就把PRESERVE8伪指令用在汇编文件中,用以向汇编器通知前面你的保证内容。汇编器就知道你这个汇编文件是8字节对齐靠谱选手,将该文件标识为PRES8属性,然后如果在你这个汇编中调用了标示了需要8字节对齐属性的文件中的函数,连接的时候就不会报错。但是假如你把这个汇编文件标示为PRESERVE8 {FALSE},然后你又在这个文件中调用了标示了需要8字节对齐属性的文件中的函数,连接时就会给出错误信息。

那么什么是标示了需要8字节对齐属性的文件呢?如果你的某个汇编文件,某些操作一定要栈8字节对齐才行,那么你就需要使用REQUIRE8伪指令来通知汇编器将该文件标识为REQ8属性,然后这个文件就是所谓的“标示了需要8字节对齐属性的文件”。

在文件较多,文件之间调用由繁多的情况下,通过PRESERVE8 和 REQUIRE8 的配合,就能够在连接期间由编译器检查出我们写代码时不小心造成的破坏8字节对齐模块对需要8字节对齐模块的调用(经过实验发现,汇编之间是给出警告,汇编调用 C 则是给出错误,由于C文件中并不能直接用 REQUIRE8 ,所以我猜编译器将C文件都通通标识为REQ8属性了,所以才会出错)。

REQUIRE8的用法同PRESERVE8。