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

67

我已经有一段时间没有编写ARM汇编了,对细节有点生疏。如果我从ARM调用C函数,我只需要担心保存R0-R3和LR寄存器,是吗?

如果C函数使用其他寄存器,那么是否需要负责将这些寄存器保存到堆栈并在必要时恢复它们?换句话说,编译器会为C函数生成代码来完成这些操作。

例如,如果我在汇编函数中使用R10寄存器,我不必将其值推送到堆栈或内存中,并在C调用后弹出/恢复它,是吗?

这是针对arm-eabi-gcc 4.3.0的。


1
这里有一个可能会有帮助的外部链接。APCS intro,特别是一些关于register使用的不同名称 - artless noise
6个回答

86

这取决于您正在编译的平台的ABI。在Linux上,有两种ARM ABI;旧的和新的。据我所知,新的(EABI)实际上是ARM的AAPCS。完整的EABI定义目前在ARM的infocenter上

来自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”的同义词:什么是callee和caller保存的寄存器?
在进行函数调用时,可以假设r4-r11中的值(除了可能的r9)在调用后仍然存在(call-preserved),但对于r0-r3(call-clobbered / volatile),不是这样。

1
您可以从此页面下载整个ABI规范及其支持文档和示例代码的ZIP归档文件。ZIP归档文件:http://infocenter.arm.com/help/topic/com.arm.doc.ihi0036b/bsabi.zip - jww
3
我认为更容易记住,如果你要使用r4-r11寄存器,就需要保存和恢复它们,这就是它们被称为被调用者保存的原因。 - user339222
1
Alex的评论有些令人困惑,因为它是从被调用者的角度来看的。这里讨论的问题是从调用者的角度来看的。调用者在调用C函数时不需要保存r4-r11寄存器。C函数(被调用者)将保存这些寄存器。 另外,为什么没有人澄清调用者是否需要保存r9?我相信对于arm-eabi-gcc工具链,r9也是被调用者保存的。谁能指出一个解决r9问题的信息来源? - Sven
1
总结一下:在调用C函数时,需要保存寄存器r0-r3、r12(以及可能的r9)。根据我的经验,gcc在函数内部使用r12作为临时寄存器,因此即使不使用arm/thumb交互,它也不是被调用者保存的。在交互的情况下,链接器将生成粘合代码,如果arm函数调用thumb函数,则使用r12。 - Sven
@jxgn 编译器在寄存器分配器无法一次性将所有变量放入寄存器时,会使用栈的一部分来将变量“溢出”到内存中,通过加载和存储操作将变量放入寄存器或内存中。优秀的编译器会动态地改变变量在寄存器中的分配情况,特别是在循环中,以达到更好的效果。 - undefined
显示剩余4条评论

32

32位ARM调用约定由AAPCS指定

来自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指定

通用寄存器部分指定需要保留哪些寄存器。

  • r0-r7 是参数/结果寄存器
  • r9-r15 是临时寄存器
  • r19-r28 是被调用者保存的寄存器。
  • 所有其他寄存器(r8r16-r18r29r30SP)都有特殊含义,有些可能被视为临时寄存器。

SIMD和浮点寄存器指定了Neon和浮点寄存器。


1
AAPCS64链接已经失效,这里是新链接 - swineone

20

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

  • SP 堆栈指针
  • r30 LR 链接寄存器
  • r29 FP 帧指针
  • r19…r28 被调用者保存的寄存器
  • r18 平台寄存器(如果需要);否则为临时寄存器。
  • r17 IP1 第二个内部过程调用临时寄存器(可供调用嵌板和PLT代码使用);在其他时间可能被用作临时寄存器。
  • r16 IP0 第一个内部过程调用临时寄存器(可供调用嵌板和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位;保留更大值的责任由调用方承担。


10
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)。 该命令将在标准输出上打印生成的汇编代码。它可能看起来像这样:

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开关)可能会影响结果。

2
你的分析是错误的;lr 作为 pc 弹出以更快地返回。你 r9 问题的答案在 APCS 中。在这个文档中,它被称为 静态基址,而 可重入代码与不可重入代码 部分是相关的。APCS 支持多种配置,但 gcc 通常是没有 堆栈限制可重入。特别地,在某些 APCS 变体中,sb/r9sl/r10 有专门的角色。在其他变体中,它们可能被用作被调用者保存的寄存器。 - artless noise
有关pclr的详细信息,请参见ARM链接寄存器和帧指针r12也称为ip,可以在序言尾声期间使用。它是一个易失性寄存器。这对于解析调用堆栈/帧的例程非常重要。 - artless noise
1
“lr”被恢复到“pc”是正确的,但不能期望“lr”的值本身被恢复。我不认为这有什么问题。该值最终出现在不是“lr”的寄存器中对于“lr”是否被恢复的问题完全无关。您是正确的,随着“-mabi”选项的更改,将恢复和未恢复的寄存器集可能会发生变化。 - Sven
现在,我正在为ARM7TDMI编写一个嵌套中断的汇编包装器。调用C代码时是否保留lr的值很重要,因为汇编包装器必须将lr的值恢复到先前的值。因此,了解lr是否被调用者保存并不是那么假设性的问题。 - Sven
2
这正是我正在寻找的——一种查找由我项目使用的特定编译器设置保留的寄存器的方法。谢谢! - TonyK
显示剩余4条评论

4

0

在Cortex M3体系结构中,函数调用和中断至少有一个区别。

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接