链路寄存器 (LR) 是否受内联函数或裸函数的影响

Is the Link Register (LR) affected by inline or naked functions?

本文关键字:函数 影响 寄存器 LR 是否 链路      更新时间:2023-10-16

我正在使用ARM Cortex-M4处理器。据我了解,LR(链接寄存器)存储当前执行函数的返回地址。但是,内联和/或裸函数会影响它吗?

我正在努力实现简单的多任务处理。我想编写一些代码来保存执行上下文(将R0 - R12LR到堆栈),以便以后可以恢复。保存上下文后,我有一个SVC,以便内核可以安排另一个任务。当它决定再次调度当前任务时,它将恢复堆栈并执行BX LR。我问这个问题是因为我想BX LR跳到正确的地方。

假设我使用arm-none-eabi-g++并且我不关心可移植性。

例如,如果我有以下带有 always_inline 属性的代码,由于编译器将内联它,那么生成的机器代码中就不会有函数调用,因此LR不受影响,对吧?

__attribute__((always_inline))
inline void Task::saveContext() {
    asm volatile("PUSH {R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, LR}");
}

然后,还有 naked 属性,其文档说它不会有编译器生成的序言/尾声序列。这到底是什么意思。裸函数是否仍会导致函数调用,是否会影响LR

__attribute__((naked))
void saveContext() {
    asm volatile("PUSH {R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, LR}");
}

另外,出于好奇,如果一个函数同时标有always_inlinenaked会发生什么?这有什么不同吗?

哪种方法可以确保函数调用不会影响LR

据我了解,LR(链接寄存器)存储当前执行函数的返回地址。

不,lr只是在执行blblx指令时接收以下指令的地址。在 M 类架构中,它还会在异常输入时接收一个特殊的魔术值,当像返回地址一样使用时会触发异常返回,使异常处理程序看起来与常规函数完全相同。

输入函数后,编译器可以自由地将该值保存在其他地方,并将r14用作另一个通用寄存器。实际上,如果它想要进行任何嵌套调用,则需要将值保存在某个地方。对于大多数编译器,任何非叶函数都会lr作为序言的一部分推送到堆栈(并且通常利用能够在尾声中将其直接弹出回pc以返回)。

确保函数调用不影响LR的正确方法是什么?

根据定义,函数调用会影响lr - 否则它将是 goto,而不是调用(当然,尽管有尾部调用)。

re: update. 在下面留下我的旧答案,因为它回答了编辑前的原始问题。

__attribute__((naked))基本上存在,因此您可以在 asm 中编写整个函数,asm语句中,而不是在单独的.S文件中。 编译器甚至不发出返回指令,您必须自己执行此操作。 将其用于内联函数是没有意义的(就像我在下面已经回答的那样)。

调用naked函数将生成通常的调用序列,带有bl my_naked_function,这当然将LR设置为指向bl之后的指令。 naked函数本质上是您在 asm 中编写的永不内联函数。 "序言"和"尾声"是保存和恢复被叫方保存的寄存器的指令,以及返回指令本身(bx lr)。

<小时 />

试试看。 很容易看一下 gcc 的 asm 输出。 我更改了您的函数名称以帮助解释正在发生的事情,并修复了语法(GNU C __attribute__扩展需要双倍的参数)。

extern void extfunc(void);
__attribute__((always_inline))
inline void break_the_stack() {   asm volatile("PUSH LR");   }
__attribute__((naked))
void myFunc() {
    asm volatile("PUSH {r3, LR}nt"  // keep the stack aligned for our callee by pushing a dummy register along with LR
                 "bl extfuncnt"
                 "pop {r3, PC}"
                );
}

int foo_simple(void) {
  extfunc();
  return 0;
}
int foo_using_inline(void) {
  break_the_stack();
  extfunc();
  return 0;
}

带有 gcc 4.8.2 -O2 的 asm 输出用于 ARM(我认为默认是拇指目标)。

myFunc():            # I followed the compiler's foo_simple example for this
        PUSH {r3, LR}
        bl extfunc
        pop {r3, PC}
foo_simple():
        push    {r3, lr}
        bl      extfunc()
        movs    r0, #0
        pop     {r3, pc}
foo_using_inline():
        push    {r3, lr}
        PUSH LR
        bl      extfunc()
        movs    r0, #0
        pop     {r3, pc}

额外的推送LR意味着我们将错误的数据弹出到PC中。 在这种情况下,也许是 LR 的另一个副本,但我们返回的是一个修改后的堆栈指针,因此调用方将中断。 不要弄乱内联函数中的 LR 或堆栈,除非您尝试做某种二进制检测的事情。

<小时 />

re: 注释: 如果你只想设置一个 C 变量 = LR:

正如@Notlikethat指出的那样,LR可能不持有退货地址。 因此,您可能希望__builtin_return_address(0)获取当前函数的返回地址。 但是,如果您只是尝试保存寄存器状态,那么如果您希望此时正确恢复执行,则应保存/恢复该函数在 LR 中的任何内容:

#define get_lr(lr_val)  asm ("mov %0, lr" : "=r" (lr_val))

这可能需要volatile,以防止它在全程序优化期间被提升到调用树上。

这会导致额外的 mov 指令,而理想的顺序可能是存储 lr,而不是先复制到另一个 reg。 由于 ARM 对 reg-reg 移动与存储到内存使用不同的指令,因此不能只对输出操作数使用 rm 约束来为编译器提供该选项。

你可以把它包装在一个内联函数中。 宏中的 GNU C 语句表达式也可以工作,但内联函数应该没问题:

__attribute__((always_inline)) void* current_lr(void) {  // This should work correctly when inlined, or just use the macro
  void* lr;
  get_lr(lr);
  return lr;
}

供参考:ARM中的SP(堆栈)和LR是什么?

<小时 />

naked always_inline函数没有用。

文档说naked函数只能包含asm语句,并且只能包含"基本"asm(没有操作数,因此您必须自己从正确的位置获取 ABI 的参数)。 内联毫无意义,因为您不知道编译器将参数放在哪里。

如果要内联某些 asm,请不要使用 naked 函数。 相反,请使用对输入/输出参数使用正确约束的内联函数。

x86 wiki 有一些很好的内联 asm 链接,它们并不都是特定于 x86 的。 例如,请参阅本答案末尾的 GNU 内联 asm 链接集合,以获取有关如何充分利用语法让编译器围绕您的 asm 片段制作尽可能高效的代码的示例。