我使用的是ARM Cortex-M4处理器。据我所知,LR
(链接寄存器)存储当前正在执行的函数的返回地址。但是,内联和/或裸功能是否会影响它?
我正致力于实施简单的多任务处理。我想编写一些代码来保存执行上下文(将R0
- R12
和LR
放到堆栈中),以便以后可以恢复。上下文保存后,我有一个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表示不会有编译器生成的序言/结尾序列。这到底是什么意思呢。裸函数是否仍会导致函数调用,是否会影响LR
?
__attribute__((naked))
void saveContext() {
asm volatile("PUSH {R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, LR}");
}
此外,出于好奇,如果某个功能同时标有always_inline
和naked
,会发生什么?这有什么不同吗?
确保函数调用不影响LR
?
答案 0 :(得分:2)
据我了解,
LR
(链接寄存器)存储当前正在执行的函数的返回地址。
不,lr
在执行bl
或blx
指令时只接收以下指令的地址。在M-class体系结构中,它还在异常进入时接收一个特殊的魔术值,当像返回地址一样使用时会触发异常返回,使异常处理程序看起来与常规函数完全相同。
输入函数后,编译器可以自由地将该值保存在其他位置,并将r14
用作另一个通用寄存器。实际上,如果它想要进行任何嵌套调用,需要来保存值。对于大多数编译器而言,任何非叶子函数都会将lr
作为序言的一部分推送到堆栈中(并且通常可以利用能够将其直接弹回到结尾的pc
中来返回)。 / p>
确保函数调用不影响
的正确方法是什么LR
?
根据定义的函数调用影响lr
- 否则它将是一个goto,而不是一个调用(当然是尾调用)。
答案 1 :(得分:1)
重新:更新。在下面留下我的旧答案,因为它在编辑之前回答了原始问题。
__attribute__((naked))
基本上存在,因此您可以在asm中编写整个函数,在asm
语句中而不是在单独的.S
文件中。编译器甚至不发出返回指令,你必须自己做。将它用于内联函数是没有意义的(就像我在下面已经回答过的那样)。
调用naked
函数将生成通常的调用序列,其中bl my_naked_function
当然将LR设置为指向bl
之后的指令。 naked
函数本质上是一个用asm编写的永不内联函数。 “prologue”和“epilogue”是保存和恢复被调用者保存的寄存器以及返回指令本身(bx lr
)的指令。
试一试,看看。很容易看到gcc的asm输出。我更改了函数名称以帮助解释发生了什么,并修复了语法(GNU C __attribute__
扩展需要加倍的parens)。
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的另一个副本,但是我们正在使用修改后的堆栈指针返回,因此调用者将会中断。除非你试图做某种二进制检测工作,否则不要乱用LR或内联函数中的堆栈。
正如@Notlikethat指出的那样,LR
可能不会保留返回地址。因此,您可能希望__builtin_return_address(0)
获取当前函数的返回地址。但是,如果您只是想保存寄存器状态,那么如果希望在此时正确地恢复执行,则应该保存/恢复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
语句,只能包含“Basic”asm(没有操作数,所以你必须自己从ABI的正确位置获取args)。内联是没有意义的,因为你不知道编译器把你的args放在哪里。
如果要内联某些asm,请不要使用naked
函数。而是使用对输入/输出参数使用正确约束的内联函数。
x86 wiki有一些很好的内联asm链接,而且它们并不是x86特有的。例如,请参阅the collection of GNU inline asm links at the end of this answer,了解如何充分利用语法让编译器在asm片段周围尽可能高效地编写代码。