在 ARM C 调用约定中要保存哪些寄存器?

Posted

技术标签:

【中文标题】在 ARM C 调用约定中要保存哪些寄存器?【英文标题】:What registers to save in the ARM C calling convention? 【发布时间】:2010-09-20 15:47:25 【问题描述】:

自从我上次编写 arm assembler 以来已经有一段时间了,我对细节有点生疏了。如果我从 arm 调用一个 C 函数,我只需要担心保存 r0-r3 和 lr,对吗?

如果 C 函数使用任何其他寄存器,它是否负责将这些寄存器保存在堆栈上并恢复它们?换句话说,编译器会为 C 函数生成代码来执行此操作。

例如,如果我在汇编函数中使用 r10,我不必将其值压入堆栈或内存,并在 C 调用后弹出/恢复它,对吗?

这适用于 arm-eabi-gcc 4.3.0。

【问题讨论】:

这是一个可能有用的外部链接。 APCS intro,尤其是一些different names 供register 使用。 【参考方案1】:

32 位 ARM 调用约定由 AAPCS 指定

来自the AAPCS,§5.1.1 核心寄存器: r0-r3 是参数和暂存寄存器; r0-r1也是结果寄存器 r4-r8 是被调用者保存寄存器 r9 可能是被调用者保存寄存器,也可能不是(在 AAPCS 的某些变体中,它是一个特殊寄存器) r10-r11 是被调用者保存寄存器 r12-r15 是特殊寄存器

来自 AAPCS,§5.1.2.1 VFP 寄存器使用约定:

s16–s31 (d8–d15, q4–q7) 必须保留 s0–s15 (d0–d7, q0–q3)d16–d31 (q8–q15) 不需要保留

原帖:arm-to-c-calling-convention-neon-registers-to-save

64 位 ARM 调用约定由 AAPCS64 指定

General-purpose Registers 部分指定需要保留哪些寄存器。 r0-r7 是参数/结果寄存器 r9-r15 是临时寄存器 r19-r28 是被调用者保存的寄存器。 所有其他(r8r16-r18r29r30 , SP) 有特殊含义,有些可能被视为临时寄存器。

SIMD and Floating-Point Registers 指定 Neon 和浮点寄存器。

【讨论】:

【参考方案2】:

这取决于您正在编译的平台的ABI。在 Linux 上,有两个 ARM ABI;旧的和新的。 AFAIK,新的(EABI)实际上是ARM的AAPCS。完整的 EABI 定义目前已上线 here on ARM's infocenter。

来自the AAPCS, §5.1.1:

r0-r3 是参数和暂存寄存器; r0-r1也是结果寄存器 r4-r8 是被调用者保存寄存器 r9 可能是被调用者保存寄存器,也可能不是(在 AAPCS 的某些变体中,它是一个特殊寄存器) r10-r11 是被调用者保存寄存器 r12-r15 是特殊寄存器

被调用者必须保存被调用者保存寄存器(与调用者保存寄存器相反,调用者保存寄存器);所以,如果这是你正在使用的ABI,你不必在调用另一个函数之前保存r10(另一个函数负责保存它)。

编辑:您使用的编译器没有区别;特别是 gcc 可以为几个不同的 ABI 配置,甚至可以在命令行上进行更改。查看它生成的序言/尾声代码并没有那么有用,因为它是为每个函数量身定制的并且编译器可以使用其他方式来保存寄存器(例如,将其保存在函数)。


术语:“callee-save”是“non-volatile”或“call-preserved”的同义词:What are callee and caller saved registers? 在进行函数调用时,您可以假设 r4-r11(可能除了 r9)中的值在(调用保留)之后仍然存在,但对于 r0-r3(调用破坏/易失)则不存在。

【讨论】:

谢谢,这似乎敲响了一些钟声。我认为您列表中的第一个“r0-r4”是一个错字,对吧? +1(可能是最好的答案,除非有彻底的转变) “您可以从该页面下载整个 ABI 规范及其支持文档和示例代码作为 ZIP 存档。”压缩存档:infocenter.arm.com/help/topic/com.arm.doc.ihi0036b/bsabi.zip 我认为更容易记住你必须保存和恢复r4-r11,以防你要使用它们;这就是他们被调用者保存的原因。 Alex 的评论令人困惑,因为它是从被调用者的角度来看的。这里讨论的问题是从调用者的角度来看的。调用者在调用 C 函数时不需要保存 r4-r11。 C 函数(被调用者)将保存这些寄存器。另外,为什么没有人澄清 r9 是否需要由调用者保存?我相信对于 arm-eabi-gcc 工具链,r9 也是被调用者保存的。谁能指出解决 r9 问题的信息来源? 总结一下:调用C函数时,需要保存寄存器r0-r3、r12(可能还有r9)。根据我的经验,gcc 使用 r12 作为函数内部的临时寄存器,因此即使不使用 arm/thumb-interworking,它也不会被调用者保存。在互通的情况下,如果 arm 函数调用 thumb 函数,链接器将生成使用 r12 的胶水代码。【参考方案3】:

至少在 Cortex M3 架构上,函数调用和中断也有所不同。

如果发生中断,它将自动将 R0-R3、R12、LR、PC 压入堆栈,并在从 IRQ 返回时自动 POP。如果您在 IRQ 例程中使用其他寄存器,则必须手动将它们推送/弹出到堆栈中。

我不认为这种自动 PUSH 和 POP 是针对函数调用(跳转指令)进行的。如果约定说 R0-R3 只能用作参数、结果或暂存寄存器,则无需在函数调用之前存储它们,因为在函数返回之后不应再使用任何值。但是与在中断中一样,如果您在函数中使用它们,则必须存储所有其他 CPU 寄存器。

【讨论】:

【参考方案4】:

对于 64 位 ARM,A64(来自 ARM 64 位架构的过程调用标准)

有 31 个 64 位通用(整数)寄存器对 A64 指令集可见;这些标记为 r0-r30。在 64 位上下文中,这些寄存器通常使用名称 x0-x30 来引用;在 32 位上下文中,寄存器是使用 w0-w30 指定的。此外,堆栈指针寄存器 SP 可以与有限数量的指令一起使用。

SP 堆栈指针 r30 LR 链接寄存器 r29 FP 帧指针 r19…r28 被调用者保存的寄存器 r18 平台注册(如果需要);否则为临时寄存器。 r17 IP1 第二个intra-procedure-call临时寄存器(可以用 通过调用单板和PLT代码);在其他时候可以用作 临时注册。 r16 IP0 第一个intra-procedure-call暂存寄存器(可以被调用使用 单板和PLT代码);在其他时候可以用作 临时注册。 r9…r15 临时寄存器 r8 间接结果位置寄存器 r0…r7 参数/结果寄存器

前八个寄存器,r0-r7,用于将参数值传递到子程序并从函数返回结果值。它们也可用于在例程中保存中间值(但通常仅在子例程调用之间)。

寄存器 r16 (IP0)r17 (IP1) 可以被链接器用作例程和它调用的任何子例程之间的临时寄存器。它们也可以在例程中用于保存子例程调用之间的中间值。

寄存器r18的作用是平台特定的。如果平台 ABI 需要一个专用的通用寄存器来承载过程间状态(例如,线程上下文),那么它应该为此目的使用该寄存器。如果平台 ABI 没有这样的要求,那么它应该使用 r18 作为额外的临时寄存器。平台 ABI 规范必须记录此寄存器的使用情况。

SIMD

ARM 64 位架构还有另外 32 个寄存器,v0-v31,可用于 SIMD 和浮点运算。寄存器的准确名称会随着访问的大小而改变。

注意:与 AArch32 不同,在 AArch64 中,SIMD 和浮点寄存器的 128 位和 64 位视图不会在更窄的视图中与多个寄存器重叠,所以 q1 , d1 和 s1 都指向寄存器组中的同一个条目。

前八个寄存器,v0-v7,用于将参数值传递给子例程并从函数返回结果值。它们也可用于在例程中保存中间值(但通常仅在子例程调用之间)。

寄存器 v8-v15 必须由被调用者跨子例程调用保存;剩余的寄存器(v0-v7, v16-v31)不需要保留(或者应该由调用者保留)。此外,v8-v15 中存储的每个值只需要保留低 64 位;保持较大的值是调用者的责任。

【讨论】:

【参考方案5】:

CesarB 和 Pavel 的回答提供了 AAPCS 的引用,但未解决的问题仍然存在。被调用者是否保存 r9? r12呢? r14呢?此外,答案非常笼统,并没有按照要求专门针对 arm-eabi 工具链。这里有一个实用的方法来找出哪些寄存器是被调用者保存的,哪些不是。

以下 C 代码包含一个内联汇编块,声称修改寄存器 r0-r12 和 r14。编译器将生成代码以保存 ABI 所需的寄存器。

void foo() 
  asm volatile ( "nop" : : : "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r14");

使用命令行arm-eabi-gcc-4.7 -O2 -S -o - foo.c 并为您的平台添加开关(例如-mcpu=arm7tdmi)。 该命令将在 STDOUT 上打印生成的汇编代码。它可能看起来像这样:

foo:
    stmfd   sp!, r4, r5, r6, r7, r8, r9, sl, fp, lr
    nop
    ldmfd   sp!, r4, r5, r6, r7, r8, r9, sl, fp, lr
    bx  lr

请注意,编译器生成的代码会保存并恢复 r4-r11。编译器不保存 r0-r3、r12。它恢复 r14(别名 lr)纯属偶然,因为我从经验中知道退出代码也可能将保存的 lr 加载到 r0 中,然后执行“bx r0”而不是“bx lr”。通过添加-mcpu=arm7tdmi -mno-thumb-interwork 或使用-mcpu=cortex-m4 -mthumb,我们获得了稍微不同的汇编代码,如下所示:

foo:
    stmfd   sp!, r4, r5, r6, r7, r8, r9, sl, fp, lr
    nop
    ldmfd   sp!, r4, r5, r6, r7, r8, r9, sl, fp, pc

同样,r4-r11 被保存和恢复。但是 r14(别名 lr)没有恢复。

总结一下:

r0-r3 不是被调用者保存 r4-r11 被调用者保存 r12(别名 ip)不是被调用者保存 r13(别名 sp)被调用者保存 r14(别名 lr)不是被调用者保存的 r15(别名 pc)是程序计数器,在函数调用之前设置为 lr 的值

这至少适用于 arm-eabi-gcc 的默认值。有一些命令行开关(特别是 -mabi 开关)可能会影响结果。

【讨论】:

您的分析不正确lr弹出pc,以便更快地返回。您的r9 问题的答案在APCS 中。在本文档中称为静态库Reentrant vs Non-Reentrant Code部分是相对的。 APCS 支持多种配置,但gcc 通常是re-entrant 没有堆栈限制。特别是,在 APCS 的某些变体中,sb/r9sl/r10 有专门的角色。在其他变体中,它们可以用作被调用者保存的寄存器 有关pclr 的详细信息,请参阅ARM link and frame pointer。 r12 也称为ip,可以在prologueepilogue 中使用。它是一个 volatile 寄存器。这对于解析调用堆栈/帧的例程很重要。 我对@9​​87654340@ 的分析在什么意义上不正确?我想你误读了我。无论如何,我展示了第二个汇编代码 sn-p,因为第一个看起来像 lr 被保存。然而,我认为不是。是的,在第二个 sn-p 中,lr 弹出为 pc 作为一种更快的返回方式,我没有解释这一点,但呈现第二个 sn-p 的重点是它表明 lr 是未保存被调用者。 确实lr恢复为pc。但这不是真的,可以期望lr 本身的值被恢复。我不明白这怎么可能是错误的。该值最终出现在不是lr 的寄存器中与lr 是否恢复的问题完全无关。您是对的,恢复和未恢复的寄存器集可能会随着-mabi 选项的变化而变化。 这正是我一直在寻找的——一种找出哪些寄存器被我用于项目的特定编译器设置保留的方法。谢谢!

以上是关于在 ARM C 调用约定中要保存哪些寄存器?的主要内容,如果未能解决你的问题,请参考以下文章

C++ Visual Studio 运行时错误

2018/10/03-函数调用约定cdeclstdcallfastcall- 《恶意代码分析实战》

调用约定

在函数调用之前保存 XMM 寄存器

C语言函数调用完整过程

rdi 和 rsi 调用者保存还是被调用者保存的寄存器?