Link 寄存器 (LR) 是否受内联或裸函数影响?

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

我使用的是 ARM Cortex-M4 处理器。据我了解,LR(link寄存器)存储的是当前执行函数的return地址。但是,内联 and/or 裸函数会影响它吗?

我正在努力实现简单的多任务处理。我想写一些代码来保存执行上下文(将 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属性whose documentation说它不会有编译器生成的prologue/epilogue序列。这到底是什么意思呢。裸函数是否仍会导致函数调用并影响 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 的正确方法是什么?

回复:更新。在下面留下我的旧答案,因为它在编辑之前回答了原始问题。

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

调用 naked 函数将生成通常的调用序列,带有 bl my_naked_function,这当然会将 LR 设置为指向 bl 之后的指令。 naked 函数本质上是您在 asm 中编写的永不内联函数。 “prologue”和“epilogue”是保存和恢复被调用者保存的寄存器的指令,以及 return 指令本身 (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}\n\t"  // keep the stack aligned for our callee by pushing a dummy register along with LR
                 "bl extfunc\n\t"
                 "pop {r3, PC}"
                );
}


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

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

asm output with gcc 4.8.2 -O2 for 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 的另一个副本,但我们 return 使用修改后的堆栈指针,因此调用者会中断。不要乱用内联函数中的 LR 或堆栈,除非您正在尝试执行某种二进制检测操作。


回复:评论:如果你只想设置一个 C 变量 = LR:

正如@Notlikethat 指出的那样,LR 可能不包含 return 地址。所以你可能希望 __builtin_return_address(0) 得到当前函数的 return 地址。但是,如果您只是想保存寄存器状态,那么如果您希望在此时正确恢复执行,那么您应该 save/restore LR 中的任何函数:

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

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

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

You could wrap this inside an inline function。宏中的 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;
}

供参考:What are SP (stack) and LR in ARM?


一个naked always_inline函数没有用。

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

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

wiki has some good inline asm links, and they're not all specific to x86. For example, see 有关如何充分利用语法的示例,让编译器围绕您的 asm 片段生成尽可能高效的代码。

As far as I understand, the LR (link register) stores the return address of the currently executing function.

不,lr 只是在执行 blblx 指令时接收以下指令的地址。在 M-class 架构中,它在异常入口时也接收到一个特殊的 magic 值,当像 return 地址一样使用时会触发异常 return ,使异常处理程序看起来完全一样作为常规函数。

一旦函数被输入,编译器就可以自由地将这个值保存在别处并将r14用作另一个通用寄存器。实际上,如果要进行任何嵌套调用,它 需要 将值保存在某处。对于大多数编译器,任何非叶函数都会将 lr 压入堆栈作为序言的一部分(并且经常利用能够直接将其弹出到 pc 的结尾到 return).

Which is the correct way to ensure that a function call does not affect the LR?

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