我明白我需要在函数调用开始时推送链接寄存器,并在返回之前将该值弹出到程序计数器,以便执行可以从函数调用之前的位置携带一个。
我不明白的是为什么大多数人通过在push/pop中添加一个额外的寄存器来做到这一点。例如:
push {ip, lr}
...
pop {ip, pc}
例如,下面是ARM中的Hello World,由ARM官方博客提供:
.syntax unified
@ --------------------------------
.global main
main:
@ Stack the return address (lr) in addition to a dummy register (ip) to
@ keep the stack 8-byte aligned.
push {ip, lr}
@ Load the argument and perform the call. This is like 'printf("...")' in C.
ldr r0, =message
bl printf
@ Exit from 'main'. This is like 'return 0' in C.
mov r0, #0 @ Return 0.
@ Pop the dummy ip to reverse our alignment fix, and pop the original lr
@ value directly into pc — the Program Counter — to return.
pop {ip, pc}
@ --------------------------------
@ Data for the printf calls. The GNU assembler's ".asciz" directive
@ automatically adds a NULL character termination.
message:
.asciz "Hello, world.n"
问题1:他们称之为"虚拟寄存器"的原因是什么?为什么不简单地push{lr}和pop{pc}呢?他们说这是为了保持堆栈8字节对齐,但堆栈不是4字节对齐吗?
问题2 :什么是注册"知识产权"(即,r7或什么?)
8字节对齐是符合AAPCS的对象之间互操作性的要求。
ARM有一个关于这个主题的建议说明:
ARM®架构的ABI咨询说明- SP必须在符合aapcs的函数的入口上对齐8字节
文章提到了使用8字节对齐的两个原因对齐错误或不可预测的行为。(硬件/架构相关原因- LDRD/STRD可能导致对齐错误或在ARMv7以外的架构上显示不可预测的行为)
应用程序失败。
(编译器-运行时假设差异,以
va_start
和va_arg
为例)当然这都是关于公共接口的,如果你正在制作一个没有额外链接的静态可执行文件,你可以在4字节处对齐堆栈
他们称之为"虚拟寄存器"的原因是什么?为什么不简单地push{lr}和pop{pc}呢?他们说这是为了保持堆栈8字节对齐,但堆栈不是4字节对齐吗?
堆栈只需要4字节对齐;但是如果数据总线是64位宽(就像在许多现代arm上一样),那么将其保持在8字节对齐会更有效。然后,例如,如果调用一个函数需要对两个寄存器进行堆栈,则可以在单个64位写入中完成,而不是在两个32位写入中完成。
更新:显然这不仅仅是为了效率;正如评论中所指出的,这是官方过程调用标准的要求。
如果您的目标是较旧的32位arm,那么额外的堆叠寄存器可能会略微降低性能。
哪个寄存器是"ip"(即r7或其他什么?)
r12
。例如,请参阅此处查看过程调用标准使用的寄存器别名的完整集合。
因为您希望在执行函数后存储和恢复它们。在函数入口,它保存ip
和lr
寄存器(命名为prolog
)。在完成函数后,它分配两个(epilog
):
pc <- lr
ip <- old_ip
编辑
寄存器r12
也称为IP
,用作过程内部调用的刮擦寄存器,另见
约定是被调用函数可以更改ip,r0-r3
,因此您必须根据调用约定恢复它们
EDIT2: 为什么我们可能希望堆栈在ARM
上对齐为8如果堆栈不是8字节对齐的,则可以使用LDRD和STRD(加载和存储双字)根据目标器和配置,导致对齐故障使用。
请注意,我们在X86上也有同样的问题,在Mac OS上我们有16个字节对齐