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

tsm1rwdh  于 2023-03-02  发布在  其他
关注(0)|答案(3)|浏览(189)

我试图更好地理解为什么在调用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“指令的地址在第一次分支调用之后被放入堆栈,但是如果LRBL调用之前被压入,那么如何将其保留在堆栈中?

ctrmrzij

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就可以了。

hxzsmxv2

hxzsmxv22#

链接寄存器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}

会很典型。

xxhby3vn

xxhby3vn3#

因为我手边有个模拟器...

.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并将返回地址放在堆栈上(以上是thumb代码,而不是arm代码,但涵盖了您的问题,因为它们在这方面的工作相同)。因此,如果您正在嵌套调用一个()调用两个()、两个()调用三个()for two()to get back to one()lr需要保存在two()中以便使用,如果不保存lr,则调用three()会更改lr,并且无法返回。
如果你的递归要使用bl(看起来像编译过的代码)的纯度,并且你想让那个函数,在我的例子测试中是factorial,能够返回到最初的调用者,那么这两个事实结合起来,就必须把lr推到堆栈上,如果你想把bl推到递归函数的顶部,外部调用者使用的相同入口点,则每个调用都将把LR添加到堆栈,并且每个返回都需要将其拉回。
如果你想做一些手工组装来修改它,而它没有调用相同的入口点,你可以去掉bl和堆栈的东西。

test:
    push {r3,lr}
test1:    
    cmp r0,#0
    beq test2
    sub r0,#1
    b test1
test2:    
    pop {r3,pc}

我甚至可以把血留在里面

test:
    push {r3,lr}
test1:    
    cmp r0,#0
    beq test2
    sub r0,#1
    bl test1
test2:    
    pop {r3,pc}

但是如果你想每次都返回,那么打破循环的方式就不同了,我手头上没有一个解决方案,它使用bl和return,但是能够在正确的时间打破循环。

相关问题