我试图更好地理解为什么在调用BL
指令之前压入LR
。我理解BL
指令在将PC恢复到BL调用之后的指令地址之前将分支到另一个子例程,但为什么在调用BL
之前压入LR
?I'我写了下面阶乘计算的完整递归代码来给予上下文。a和b都是用pseudo写的变量。
LDR RO, a
PUSH (LR)
BL factorial
STR R0, b
POP (LR)
factorial:
CMP RO, #0
MOVEQ R0, #1
MOVEQ PC, LR
MOV R3, R0
SUB R0, R0, #1
PUSH (R3, LR)
BL factorial
MUL R0, R3, R0
POP (R3, LR)
MOV PC, LR
我知道这个程序应该如何运行,但是我对堆栈中存储的地址感到困惑。显然,您希望“STR R0, b
“指令的地址在第一次分支调用之后被放入堆栈,但是如果LR
在BL
调用之前被压入,那么如何将其保留在堆栈中?
3条答案
按热度按时间ctrmrzij1#
但是为什么LR在BL被调用之前被推送呢?
这里你看到了递归的代价。从更高层次的编码Angular 看递归看起来很简单。状态由编译器存储在堆栈帧中。只有一个
LR
寄存器,这对叶函数来说很好。但是,如果你有一个扩展的调用链,“A调用B调用C调用D”,那么“A,在“D”中执行时,必须存储“B和C”,并且LR
返回到“C”。对于递归,“A、B、C和D”都相同。更多信息请参见:ARM Link register and frame pointer。
我相信看到这些额外的指令是有启发性的。通常可以形成一个循环来代替递归,线性流将执行得更快,使用相同数量的变量和更少的代码。堆栈帧和操作对高级语言的程序员是隐藏的。
由于“尾递归”,帧不需要也很常见。实际上,只有第一次调用阶乘需要保存返回地址,而不是
bl
,简单的b
就可以了。hxzsmxv22#
链接寄存器
LR
用于保存函数执行完毕后应返回的地址。BL
指令本质上是一个“调用”;它计算下一条指令的地址,并在分支之前将其插入LR
。相应的BX LR
(分支到链接寄存器中保存的地址)是“返回”。然而,如果一个函数调用另一个函数,那么在发出
BL
指令之前,它必须将LR
的现有值保存在某个地方,否则它将被覆盖并永远丢失。请记住,(几乎)没有代码实际上是“独立的”。很可能你写的任何代码都是函数的一部分,即使它是
main()
,所以链接寄存器必须被保留。在编译代码中最常见的模式是链接寄存器被推到堆栈的最顶端,然后在最底端再次弹出,此外,它经常直接弹出到程序计数器中,这导致了一个分支,而不需要显式的
BX LR
。会很典型。
xxhby3vn3#
因为我手边有个模拟器...
构建版本:
然后运行它,按执行顺序和内存访问显示反汇编:
我能理解你的困惑,因为除了最后一个,所有的返回地址都是相同的,我们可以做一个例子,但是递归通常不止有返回地址,还有一些其他的局部变量在改变,在这个例子中,我们的局部变量在r0中,如果你愿意,所以不需要每次调用都保存到栈中。
重置后我们第一次回到顶部bl:
剩下的时间是相同的返回地址,但我们需要在堆栈上有N个这样的地址,代码才能像写的那样工作。
所以当我们展开它的时候,我们现在在堆栈上有了这五个地址。
通常,bl修改lr并将返回地址放在堆栈上(以上是thumb代码,而不是arm代码,但涵盖了您的问题,因为它们在这方面的工作相同)。因此,如果您正在嵌套调用一个()调用两个()、两个()调用三个()for two()to get back to one()lr需要保存在two()中以便使用,如果不保存lr,则调用three()会更改lr,并且无法返回。
如果你的递归要使用bl(看起来像编译过的代码)的纯度,并且你想让那个函数,在我的例子测试中是factorial,能够返回到最初的调用者,那么这两个事实结合起来,就必须把lr推到堆栈上,如果你想把bl推到递归函数的顶部,外部调用者使用的相同入口点,则每个调用都将把LR添加到堆栈,并且每个返回都需要将其拉回。
如果你想做一些手工组装来修改它,而它没有调用相同的入口点,你可以去掉bl和堆栈的东西。
我甚至可以把血留在里面
但是如果你想每次都返回,那么打破循环的方式就不同了,我手头上没有一个解决方案,它使用bl和return,但是能够在正确的时间打破循环。