ARM:为什么我需要在函数调用时推/弹出两个寄存器



我明白我需要在函数调用开始时推送链接寄存器,并在返回之前将该值弹出到程序计数器,以便执行可以从函数调用之前的位置携带一个。

我不明白的是为什么大多数人通过在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_startva_arg为例)

当然这都是关于公共接口的,如果你正在制作一个没有额外链接的静态可执行文件,你可以在4字节处对齐堆栈

他们称之为"虚拟寄存器"的原因是什么?为什么不简单地push{lr}和pop{pc}呢?他们说这是为了保持堆栈8字节对齐,但堆栈不是4字节对齐吗?

堆栈只需要4字节对齐;但是如果数据总线是64位宽(就像在许多现代arm上一样),那么将其保持在8字节对齐会更有效。然后,例如,如果调用一个函数需要对两个寄存器进行堆栈,则可以在单个64位写入中完成,而不是在两个32位写入中完成。

更新:显然这不仅仅是为了效率;正如评论中所指出的,这是官方过程调用标准的要求。

如果您的目标是较旧的32位arm,那么额外的堆叠寄存器可能会略微降低性能。

哪个寄存器是"ip"(即r7或其他什么?)

r12。例如,请参阅此处查看过程调用标准使用的寄存器别名的完整集合。

因为您希望在执行函数后存储和恢复它们。在函数入口,它保存iplr寄存器(命名为prolog)。在完成函数后,它分配两个(epilog):

pc <- lr
ip <- old_ip

编辑

寄存器r12也称为IP,用作过程内部调用的刮擦寄存器,另见

约定是被调用函数可以更改ip,r0-r3,因此您必须根据调用约定恢复它们

EDIT2: 为什么我们可能希望堆栈在ARM

上对齐为8

如果堆栈不是8字节对齐的,则可以使用LDRD和STRD(加载和存储双字)根据目标器和配置,导致对齐故障使用。

请注意,我们在X86上也有同样的问题,在Mac OS上我们有16个字节对齐

最新更新