令
label1
、label2
为两组指令,均以RET
指令结尾,并且label2
通过链接分支到label1
。换句话说,我们有一个如下所示的代码(为了清楚起见,我将在下文中列举一些位置):
label1:
# Some operations...
# (1)
RET
label2:
#Some operations
BL label1
#(2)
RET
我想给
label2
打电话 main
:
main:
# Some operations...
BL label2
#(3)
我希望的行为是,在
label2
中分支到 main
后,从 #(3)
继续执行。然而,这种情况并非如此。当在main
中时,我调用BL label2
,链接寄存器保存所需返回点#(3)
的地址,并且label2
被执行。然而,在label2
内部,我调用BL label1
,用#(2)#
替换链接寄存器。这使得RET
之后的#(1)
指令将执行到#(2)
,并且RET
之后的#(2)
指令再次指向#(2)
。你可以看到问题所在。
高级编程语言允许嵌套使用函数。我可以定义一个函数
f
,在其中调用一个函数 g
,每个函数都有 return
语句。所以我想要的功能必须以某种方式实现。如何顺序或嵌套调用 BL some_label
和 RET
并返回到调用 BL
的第一个位置?
我对组装很陌生,所以如果问题有点微不足道,请原谅我。
假设我理解这个问题。
您基本上标记了多个架构。所以我会选择一种架构。
简短的答案是将返回地址保存到堆栈中。
unsigned int more_fun ( unsigned int x );
unsigned int fun0 ( unsigned int x )
{
return(x+1);
}
unsigned int fun1 ( unsigned int x )
{
return(more_fun(x));
}
unsigned int fun2 ( unsigned int x )
{
return(more_fun(x)+1);
}
Disassembly of section .text:
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: e12fff1e bx lr
00000018 <fun2>:
18: e92d4010 push {r4, lr}
1c: ebfffffe bl 0 <more_fun>
20: e8bd4010 pop {r4, lr}
24: e2800001 add r0, r0, #1
28: e12fff1e bx lr
如果没有嵌套调用,则不需要保存返回地址(在该汇编语言中显示为 lr 或链接寄存器)。额外的寄存器 r4 并不是因为 r4 在这里很特殊,而是因为编译器使用的调用约定规定了 64 位对齐的堆栈,因此它们会放入其他寄存器中,这不会影响代码/约定以使其对齐。
第二个调用一个函数,所以两件事之一,在这种情况下它可以做一个尾巴?优化并完成了这个
fun1:
b more_fun
但事实并非如此。也许只有少数人知道,但我使用的是较旧的 gcc(不是最先进的),并保留了默认值,显然是 armv4t。因此,也许出于这个原因,工具链不愿意处理手臂/拇指模式切换,但使用 BL,他们会在链接器中为您放置一个单板/蹦床。
第三个我强迫它无法进行尾部优化,这类似于你通常看到的那种事情,如果手工完成,大多数人都会写。
实际上这就是您期望编译器产生的结果:
fun2:
push {r4, lr}
bl more_fun
add r0, r0, #1
pop {r4, lr}
bx lr
此堆栈帧位于函数的开头和结尾
fun2:
push {r4, lr}
...
pop {r4, lr}
bx lr
然后填写函数的内容。
相同的 gcc,但不同的 Arm 架构,随着时间的推移,添加了更多的拇指交互支持。
Disassembly of section .text:
00000000 <fun0>:
0: e2800001 add r0, r0, #1
4: e12fff1e bx lr
00000008 <fun1>:
8: eafffffe b 0 <more_fun>
0000000c <fun2>:
c: e92d4010 push {r4, lr}
10: ebfffffe bl 0 <more_fun>
14: e2800001 add r0, r0, #1
18: e8bd8010 pop {r4, pc}
哦,所以如果用手写就可以在没有堆栈帧的情况下完成,就在嵌套分支之前保留您需要保留的内容,然后在恢复您需要恢复的内容之后,所以不要在函数边缘使用堆栈帧当您浏览代码时保存和恢复。这没有什么问题,这是一种非常手工的汇编方式,而不是高级语言编译的方式。
其他架构,例如arm,具有不同的...架构...调用和返回可以例如使用堆栈而不是返回寄存器。因此,在这种情况下,就返回地址而言,您只需继续拨打电话(例如 8088/86)。您最终将需要保留其他内容以进行嵌套调用,以免除返回地址之外的内容被丢弃。这是编译语言制定/选择调用约定的地方,该调用约定作为一组规则允许以通用方式构造函数,以便您可以无限地嵌套或递归。