我试图更好地理解为什么在调用LR
指令之前按下BL
。我了解到BL
指令将分支到另一个子例程,然后再将PC恢复到BL调用之后的指令地址,但是为什么LR
在BL
被调用之前被推送?我在下面编写了用于因子计算的整个递归代码以提供上下文。 a和b都是用伪指令编写的变量。
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
推入堆栈,该指令如何保留在堆栈上?
答案 0 :(得分:4)
但是为什么在调用BL之前先推LR?
在这里您看到递归的费用。从更高级别的编码角度来看,递归看起来很简单。状态由编译器存储在堆栈帧中。只有一个LR
寄存器适合叶函数。但是,如果您具有扩展的调用链,则“ A调用B调用C调用D”,那么在LR
返回中用“ D”执行时,必须存储返回地址“ A,B和C”到“ C”。对于递归,“ A,B,C和D”都是相同的。
请参阅:ARM Link register and frame pointer。
我认为查看这些额外说明很有启发性。通常可以形成循环而不是递归,并且线性流将以相同的变量数量和更少的代码执行得更快。堆栈框架和操作对于高级语言的程序员是隐藏的。
由于“尾递归”,通常不需要框架。实际上,只有第一个对阶乘的调用才需要保存一个返回地址,而不是bl
,而只需一个简单的b
。
答案 1 :(得分:3)
链接寄存器LR
用于保存函数执行完毕后应返回的地址。 BL
指令本质上是一个“调用”;它计算下一条指令的地址,并将其插入分支之前的LR
中。相应的BX LR
(链接寄存器中保存的地址的分支)为“返回”。
但是,如果一个函数调用另一个函数,则在发出BL
指令之前,它必须将LR
的现有值保存在某个地方,否则它将被覆盖并永远丢失。将其推入堆栈是执行此操作的最简单方法。
请记住,(几乎)没有代码实际上是“独立的”。您编写的任何代码很可能都是函数的一部分,即使它是main()
,因此也必须保留链接寄存器。
您将在编译后的代码中看到的最常见的模式是,将链接寄存器压入函数顶部的堆栈,然后仅在底部再次弹出。另外,它通常只是直接弹出到程序计数器中,这会导致分支而不需要显式的BX LR
。所以像
.function
; Push working registers and LR
PUSH {r4-r8,lr}
; (rest of the function goes here)
; Pop working registers and PC for an implicit return
POP {r4-r8, pc}
会很典型。
答案 2 :(得分:1)
因为我有一个方便的模拟器...
.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang
.thumb_func
reset:
mov r0,#5
bl test
b hang
.thumb_func
hang:
swi 0xFF
b hang
test:
cmp r0,#0
bne test1
bx lr
test1:
sub r0,#1
push {r3,lr}
bl test
pop {r3,pc}
构建
08000000 <_start>:
8000000: 20001000 andcs r1, r0, r0
8000004: 08000021 stmdaeq r0, {r0, r5}
8000008: 08000029 stmdaeq r0, {r0, r3, r5}
800000c: 08000029 stmdaeq r0, {r0, r3, r5}
8000010: 08000029 stmdaeq r0, {r0, r3, r5}
8000014: 08000029 stmdaeq r0, {r0, r3, r5}
8000018: 08000029 stmdaeq r0, {r0, r3, r5}
800001c: 08000029 stmdaeq r0, {r0, r3, r5}
08000020 <reset>:
8000020: 2005 movs r0, #5
8000022: f000 f803 bl 800002c <test>
8000026: e7ff b.n 8000028 <hang>
08000028 <hang>:
8000028: dfff svc 255 ; 0xff
800002a: e7fd b.n 8000028 <hang>
0800002c <test>:
800002c: 2800 cmp r0, #0
800002e: d100 bne.n 8000032 <test1>
8000030: 4770 bx lr
08000032 <test1>:
8000032: 3801 subs r0, #1
8000034: b508 push {r3, lr}
8000036: f7ff fff9 bl 800002c <test>
800003a: bd08 pop {r3, pc}
运行它,并按执行顺序和内存访问显示反汇编。
--- 0x08000020: 0x2005 movs r0,#0x05
--- 0x08000022: 0xF000
--- 0x08000024: 0xF803 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FF8,0x0000)
write16(0x20000FFA,0x0000)
write16(0x20000FFC,0x0027)
write16(0x20000FFE,0x0800)
--- 0x08000036: 0xF7FF
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FF0,0x0000)
write16(0x20000FF2,0x0000)
write16(0x20000FF4,0x003B)
write16(0x20000FF6,0x0800)
--- 0x08000036: 0xF7FF
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FE8,0x0000)
write16(0x20000FEA,0x0000)
write16(0x20000FEC,0x003B)
write16(0x20000FEE,0x0800)
--- 0x08000036: 0xF7FF
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FE0,0x0000)
write16(0x20000FE2,0x0000)
write16(0x20000FE4,0x003B)
write16(0x20000FE6,0x0800)
--- 0x08000036: 0xF7FF
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FD8,0x0000)
write16(0x20000FDA,0x0000)
write16(0x20000FDC,0x003B)
write16(0x20000FDE,0x0800)
--- 0x08000036: 0xF7FF
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000030: 0x4770 bx r14
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FD8)=0x0000
read16(0x20000FDA)=0x0000
read16(0x20000FDC)=0x003B
read16(0x20000FDE)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FE0)=0x0000
read16(0x20000FE2)=0x0000
read16(0x20000FE4)=0x003B
read16(0x20000FE6)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FE8)=0x0000
read16(0x20000FEA)=0x0000
read16(0x20000FEC)=0x003B
read16(0x20000FEE)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FF0)=0x0000
read16(0x20000FF2)=0x0000
read16(0x20000FF4)=0x003B
read16(0x20000FF6)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FF8)=0x0000
read16(0x20000FFA)=0x0000
read16(0x20000FFC)=0x0027
read16(0x20000FFE)=0x0800
--- 0x08000026: 0xE7FF B 0x08000027
--- 0x08000028: 0xDFFF swi 0xFF
我可能会看到您的困惑,因为除最后一个地址外,所有其他人的寄信人地址都是相同的地址,我们也许可以举例说明。但是递归通常不仅仅具有返回地址,而且还具有其他一些正在更改的局部变量,在这种情况下,如果您愿意的话,我们的局部变量位于r0中,则无需在每次调用时将其保存到堆栈中。
我们第一次在重置后返回顶部bl:
write16(0x20000FFC,0x0027)
write16(0x20000FFE,0x0800)
其余时间它是相同的返回地址,但是我们需要在堆栈中使用N个,这样代码才能按编写的方式工作。
write16(0x20000FF4,0x003B)
write16(0x20000FF6,0x0800)
write16(0x20000FEC,0x003B)
write16(0x20000FEE,0x0800)
write16(0x20000FE4,0x003B)
write16(0x20000FE6,0x0800)
write16(0x20000FDC,0x003B)
write16(0x20000FDE,0x0800)
因此,当我们展开此操作时,我们现在在堆栈中有这五个地址。
read16(0x20000FDC)=0x003B
read16(0x20000FDE)=0x0800
...
read16(0x20000FFC)=0x0027
read16(0x20000FFE)=0x0800
通常,bl修改lr并将返回地址放置在堆栈上(上面的代码不是arm代码,而是涵盖了您的问题,因为它们在这方面是相同的)。因此,如果您要嵌套调用one()调用two(),two()调用three()来使two()返回one(),则lr需要保存在two()中,以便可以使用不要保存lr,则对three()的调用会更改lr,我们无法返回。
如果您的递归要使用bl(看起来像已编译的代码)来提高纯度,并且您想要该函数的一种方法(可以在我的示例测试中进行阶乘)以返回到原始调用者,则这两个事实相结合不得不将lr推入堆栈。如果要bl到递归函数的顶部,即外部调用者使用的相同入口点,则每个调用都将lr添加到堆栈中,每次返回都需要将其拉回。
如果您想进行一些手工组装以修改它,并且它不调用同一入口点,则可以摆脱bl和堆栈的内容。
test:
push {r3,lr}
test1:
cmp r0,#0
beq test2
sub r0,#1
b test1
test2:
pop {r3,pc}
甚至可以将bl留在其中
test:
push {r3,lr}
test1:
cmp r0,#0
beq test2
sub r0,#1
bl test1
test2:
pop {r3,pc}
但是如果您想每次都返回,则必须以不同的方式完成循环。我没有使用bl和return的临时解决方案,但能够在合适的时间退出循环。