ARM Link 寄存器 - 非叶子程序

ARM Link register - non-leaf subroutine

我想知道 Link 寄存器在 ARM CPU 中的什么地方使用。据我了解,它正在存储 return 函数地址。但是每个return地址是在函数调用后都到这个寄存器还是只与叶子程序实现有关?它是如何在必须使用堆栈(用于存储数据或其他 return 地址)的函数中执行的 - LR 是否仍以任何方式在这里使用?

return 地址是一个参数,它对 C 和其他高级语言隐藏,但在汇编和机器代码中可见。

在 ARM 上,被调用函数(被调用者)依赖于在 lr 寄存器中传递的 return 地址;这是根据调用约定的规范 - 因此使用标准 ARM 调用约定 的调用者必须 将 return 地址放在那里,在 lr,满足这个参数传递要求和期望。

标准调用约定旨在让调用者可以正确地调用被调用者 — 只知道被调用者的函数签名。因此,除了参数和 return 值之外,调用者从知道被调用者的实现细节中抽象出来。这意味着只要签名未修改,函数就可以进化(用于错误修复或其他)而无需重新访问(或重新编译)调用者。

函数是否为叶函数是其内部实现的一个方面,在函数签名中不可见。因此,调用者不知道(也不应该知道)被调用者是否是叶节点,或者在某些错误修复版本控制期间函数是否从叶节点更改为非叶节点。

函数对堆栈的使用也是函数签名中未捕获的内部实现细节,因此不会影响函数的调用方式,以及 return 地址值传递和预期的位置。

所以,传递 return 地址真的只有一种方法。

需要使用 lr 的被调用者(因为他们正在调用其他函数或者可能只是想使用该寄存器)将需要保留作为参数提供给他们的 return 地址值由他们的来电者使用,以便以后 return 给他们(假设他们想要 return)。

使用 lr 的函数实现(因此保留其中的值供以后使用)不必将保留的 return 地址值恢复到 lr寄存器(调用约定不要求将 return 地址传回给调用者)因此有时会恢复 lr 寄存器然后使用,但其他时候在 ARM 上,return 地址是直接从堆栈弹出到程序计数器,绕过 lr,即不恢复 lr 寄存器。


您可以创建自己的调用约定,将 return 地址传递到不同的位置,即在不同的寄存器中,或者将其压入堆栈!

有些语言确实与标准调用约定不同(在一些小方面),但仍然支持标准调用约定,因为它们与 C 风格函数的互操作性。

硬件设计为支持将 return 地址收集到 lr 寄存器中,同时进行将控制权转移给被调用方的调用,全部在一条指令中完成,因此避免这样做是愚蠢的那。硬件也没有提供其他特别有效的方法来捕获 return 地址,并且没有真正的理由这样做。

BL instruction

Operation
  if ConditionPassed(cond) then
  LR = address of the instruction after the branch instruction
  PC = PC + (SignExtend(signed_immed_24) << 2)

Usage
  The BL instruction is used to perform a subroutine call. The return
  from subroutine is achieved by copying the LR to the PC. Typically, 
  this is done by one of the following methods:
  - Executing a BX R14 instruction.
  - Executing a MOV PC,R14 instruction.

更新的 ARM 继续允许 pop {lr} 和其他...

我好像很清楚LR的用法是什么

您也可以轻松地自己尝试一下:

unsigned int more_fun ( unsigned int );
unsigned int fun0 ( unsigned int x )
{
    return(x+1);
}
unsigned int fun1 ( unsigned int x )
{
    return(more_fun(x)+1);
}
unsigned int fun2 ( unsigned int x )
{
    return(more_fun(x));
}
unsigned int fun3 ( unsigned int x )
{
    return(3);
}

00000000 <fun0>:
   0:   e2800001    add r0, r0, #1
   4:   e12fff1e    bx  lr

00000008 <fun1>:
   8:   e92d4010    push    {r4, lr}
   c:   ebfffffe    bl  0 <more_fun>
  10:   e8bd4010    pop {r4, lr}
  14:   e2800001    add r0, r0, #1
  18:   e12fff1e    bx  lr

0000001c <fun2>:
  1c:   e92d4010    push    {r4, lr}
  20:   ebfffffe    bl  0 <more_fun>
  24:   e8bd4010    pop {r4, lr}
  28:   e12fff1e    bx  lr

0000002c <fun3>:
  2c:   e3a00003    mov r0, #3
  30:   e12fff1e    bx  lr

因为,如文档所述,bl 修改了 link 寄存器。为了从非叶函数中 return,您需要为该调用保留 link 寄存器,即 return 地址。所以你把它压入堆栈。该编译器的约定要求堆栈 64 位对齐,因此添加 r4 寄存器只是为了促进对齐,否则此处不涉及 r4。

你可以在叶函数中看到它没有使用堆栈,因为它没有理由这样做,link 寄存器在函数期间没有被修改,在这种情况下函数太简单了由于其他原因需要堆栈。如果您需要堆栈并成为叶函数,优化器将不需要将 lr 放在堆栈上,但如果出于对齐原因它需要另一个寄存器,谁知道他们可以自由使用 r14 以及许多其他寄存器。

现在如果我们强制堆栈上的东西(非叶)

unsigned int new_fun ( unsigned int, unsigned int );
unsigned int fun4 ( unsigned int x, unsigned int y)
{
    return(new_fun(x,y)+y);
}

00000034 <fun4>:
  34:   e92d4010    push    {r4, lr}
  38:   e1a04001    mov r4, r1
  3c:   ebfffffe    bl  0 <new_fun>
  40:   e0800004    add r0, r0, r4
  44:   e8bd4010    pop {r4, lr}
  48:   e12fff1e    bx  lr

lr 必须在堆栈上,因为 bl 用于调用下一个函数。在这种情况下,按照惯例,他们选择使用 r4 来保存 y 变量(在 r1 中),以便它可以在嵌套调用的 return 之后使用。由于只需要保留两个寄存器,并且符合堆栈对齐规则,因此保存了 r4 和 lr,在这种情况下两者都被使用(r4 不仅仅是对齐堆栈)。

不确定额外的 return 地址是什么意思。也许你在想,当每个函数调用时,堆栈上有一个 return 地址来保存那个地址,这是真的,但你真的只需要一次查看一个函数,这就是调用约定。在这种情况下,对于这种架构,理想情况下使用 bl 来进行函数调用(正如另一个答案中指出的那样,它们 没有 ,但不这样做是愚蠢的)这意味着 lr 是为每次调用子程序进行修改,结果调用函数将其 return 地址丢失给其调用者,因此它需要以某种方式在本地保留它。正如我们在 fun 4 中看到的那样,从技术上讲,他们可以例如:

fun2:
 push {r4, r5}
 mov r5,lr
 bl 0 <more_fun>
 mov r1,r5
 pop {r4, r5}
 bx r1

并没有真正将 lr 保存在堆栈上。比我为您构建的 ARM 更新的 ARM 会看到这个

00000008 <fun1>:
   8:   e92d4010    push    {r4, lr}
   c:   ebfffffe    bl  0 <more_fun>
  10:   e2800001    add r0, r0, #1
  14:   e8bd8010    pop {r4, pc}

00000018 <fun2>:
  18:   eafffffe    b   0 <more_fun>

lr 的内容在堆栈上(lr 本身当然是一个寄存器,它不能“在堆栈上”,但是在 armv4t 之后你可以弹出到 pc 并在 arm 和 thumb 之间切换模式(其中之前只有 bx 可以用于拇指交互。

还要注意 fun2 的尾部优化。这意味着 fun2 甚至没有将 return 地址压入堆栈。

如果您查看 arm 文档,lr 的使用方式似乎很明显。然后考虑编译器将如何实现标准功能,然后他们可能会进行哪些优化。当然,您可以尝试一下,看看某些编译器实际生成了什么。