ARM 调用约定是否允许函数不将 LR 存储到堆栈?
Does the ARM calling convention allow a function to not store LR to the stack?
正如标题所说,我在理解 ARM 体系结构的调用约定时遇到问题。特别是,我仍然很难知道调用子程序时 LR 寄存器会发生什么。
我认为进入子程序时处理 LR 寄存器的最明显和更安全的方法是将其存储到堆栈中,但该行为未出现在文档中,因此我想到了以下示例。
我会用 C 语言编写,因为我认为用 C 语言更容易解释。假设您只有两个函数
void function_1(void){
//some code here
}
void function_2(void){
//some code here
function_1();
//some code here
}
我在 function_1
中使用 LR 寄存器的方式就像我之前说的那样,我会将它的值存储在堆栈中,但如果你仔细观察,function_1 不会调用任何其他子例程,这样就没有必要了。
是否有可能在使用 ARM 编译器时,该编译器会决定不将 LR 存储到堆栈中?
我在 infocenter
的这个网站上看到了调用标准
调用约定只定义了哪些寄存器是调用保留的和调用破坏的,以及在哪里可以找到堆栈参数。
它 100% 取决于函数如何确保其 return 地址在准备好 return 时在某处可用。处理这个问题的最简单和有效的方法是将它一直留在 LR 中,在叶函数中。 (不调用其他函数的函数:它是调用图/树中的叶子)。
实践中的编译器通常会将它留在叶函数的 LR 中,即使禁用了优化。例如,GCC 设置了一个禁用优化的帧指针,但当它知道它不需要那么多暂存寄存器而想使用 LR 时,仍然不会 store/reload LR。
否则在非叶函数中,普通编译器会通常将其存储到堆栈中,但如果他们愿意,他们可以将 R4 保存到堆栈中,mov r4, lr
,然后恢复 LR 并在它们准备好时重新加载 R4 return。
如果需要,非租用/非线程安全函数理论上可以将其 return 地址保存在静态存储中。
Source 和 GCC8.2 -O2 -mapcs-frame
output from Godbolt,强制它生成 APCS(ARM 过程调用标准)堆栈帧,即使在不需要时也是如此。 (看起来它与 -fno-omit-frame-pointer
具有类似的效果,后者在优化时默认打开。)
void function_1(void){
//some code here
}
function_1:
bx lr @ with or without -mapcs-frame
void unknown_func(void); // not visible to the compiler; can't inline
void function_2(void){
function_1(); // inlined, or IPA optimized as pure and not needing to be called.
unknown_func(); // tailcall
unknown_func();
}
function_2: @@ Without -macps-frame
push {r4, lr} @ save LR like you expected
bl unknown_func
pop {r4, lr} @ around a call
b unknown_func @ but then tailcall for the 2nd call.
或使用 APCS:
mov ip, sp
push {fp, ip, lr, pc}
sub fp, ip, #4
bl unknown_func
sub sp, fp, #12
ldm sp, {fp, sp, lr}
b unknown_func
int func3(void){
unknown_func();
return 1; // prevent tailcall
}
func3: @@ Without -macps-frame
push {r4, lr}
bl unknown_func
mov r0, #1
pop {r4, pc}
或使用 APCS:
func3:
mov ip, sp
push {fp, ip, lr, pc}
sub fp, ip, #4
bl unknown_func
mov r0, #1
ldmfd sp, {fp, sp, pc}
由于不需要 thumb 互通(使用默认编译选项),GCC 会将保存的 LR 弹出到 PC 而不是直接返回到 LR bx lr
。
将 R4 与 LR 一起压入使堆栈按 8 对齐,这是 IIRC 的默认设置。
正如标题所说,我在理解 ARM 体系结构的调用约定时遇到问题。特别是,我仍然很难知道调用子程序时 LR 寄存器会发生什么。
我认为进入子程序时处理 LR 寄存器的最明显和更安全的方法是将其存储到堆栈中,但该行为未出现在文档中,因此我想到了以下示例。
我会用 C 语言编写,因为我认为用 C 语言更容易解释。假设您只有两个函数
void function_1(void){
//some code here
}
void function_2(void){
//some code here
function_1();
//some code here
}
我在 function_1
中使用 LR 寄存器的方式就像我之前说的那样,我会将它的值存储在堆栈中,但如果你仔细观察,function_1 不会调用任何其他子例程,这样就没有必要了。
是否有可能在使用 ARM 编译器时,该编译器会决定不将 LR 存储到堆栈中?
我在 infocenter
的这个网站上看到了调用标准调用约定只定义了哪些寄存器是调用保留的和调用破坏的,以及在哪里可以找到堆栈参数。
它 100% 取决于函数如何确保其 return 地址在准备好 return 时在某处可用。处理这个问题的最简单和有效的方法是将它一直留在 LR 中,在叶函数中。 (不调用其他函数的函数:它是调用图/树中的叶子)。
实践中的编译器通常会将它留在叶函数的 LR 中,即使禁用了优化。例如,GCC 设置了一个禁用优化的帧指针,但当它知道它不需要那么多暂存寄存器而想使用 LR 时,仍然不会 store/reload LR。
否则在非叶函数中,普通编译器会通常将其存储到堆栈中,但如果他们愿意,他们可以将 R4 保存到堆栈中,mov r4, lr
,然后恢复 LR 并在它们准备好时重新加载 R4 return。
如果需要,非租用/非线程安全函数理论上可以将其 return 地址保存在静态存储中。
Source 和 GCC8.2 -O2 -mapcs-frame
output from Godbolt,强制它生成 APCS(ARM 过程调用标准)堆栈帧,即使在不需要时也是如此。 (看起来它与 -fno-omit-frame-pointer
具有类似的效果,后者在优化时默认打开。)
void function_1(void){
//some code here
}
function_1:
bx lr @ with or without -mapcs-frame
void unknown_func(void); // not visible to the compiler; can't inline
void function_2(void){
function_1(); // inlined, or IPA optimized as pure and not needing to be called.
unknown_func(); // tailcall
unknown_func();
}
function_2: @@ Without -macps-frame
push {r4, lr} @ save LR like you expected
bl unknown_func
pop {r4, lr} @ around a call
b unknown_func @ but then tailcall for the 2nd call.
或使用 APCS:
mov ip, sp
push {fp, ip, lr, pc}
sub fp, ip, #4
bl unknown_func
sub sp, fp, #12
ldm sp, {fp, sp, lr}
b unknown_func
int func3(void){
unknown_func();
return 1; // prevent tailcall
}
func3: @@ Without -macps-frame
push {r4, lr}
bl unknown_func
mov r0, #1
pop {r4, pc}
或使用 APCS:
func3:
mov ip, sp
push {fp, ip, lr, pc}
sub fp, ip, #4
bl unknown_func
mov r0, #1
ldmfd sp, {fp, sp, pc}
由于不需要 thumb 互通(使用默认编译选项),GCC 会将保存的 LR 弹出到 PC 而不是直接返回到 LR bx lr
。
将 R4 与 LR 一起压入使堆栈按 8 对齐,这是 IIRC 的默认设置。