在ARM汇编中的BL指令之前推送LR

时间:2018-12-05 05:38:35

标签: assembly arm stack branch

我试图更好地理解为什么在调用LR指令之前按下BL。我了解到BL指令将分支到另一个子例程,然后再将PC恢复到BL调用之后的指令地址,但是为什么LRBL被调用之前被推送?我在下面编写了用于因子计算的整个递归代码以提供上下文。 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推入堆栈,该指令如何保留在堆栈上?

3 个答案:

答案 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的临时解决方案,但能够在合适的时间退出循环。