assembly ARM汇编中的loRecursion示例

envsm3lx  于 2023-08-06  发布在  其他
关注(0)|答案(2)|浏览(121)

有人能给予我一个例子,说明如何递归将在ARM大会只列出了这里的指令(为视觉)?
我试图做一个递归的斐波那契和阶乘函数的类。我知道递归是一个调用函数的函数,但我不知道如何在ARM中模拟它。
https://salmanarif.bitbucket.io/visual/supported_instructions.html
如果链接不起作用,我使用visUAL,这些是我可以使用的唯一指令:

  • MOV
  • MVN
  • ADR
  • LDR
  • 添加
  • ADC
  • SUB
  • SBC
  • RSB
  • RSC
  • 提高采收率
  • BIC
  • ORR
  • LSL
  • LSR
  • ASR
  • ROR
  • RRX
  • CMP
  • CMN
  • TST
  • TEQ
  • LDR
  • LDM
  • 扫描隧道显微镜
  • B
  • BL
  • 灌装
  • 结束

这不会为R4加载旧的值,所以R4只是在每次函数调用自身时加倍。

;VisUAL initializess all registers to 0 except for R13/SP, which is -16777216

    MOV     R4, #0
    MOV     R5, #1

    MOV     r0, #4

    MOV     LR, #16             ;tells program to move to 4th instruction

FIB

    STMDB   SP!, {R4-R6, LR}    ;Stores necessary values on stack (PUSH command)
    LDR     R4, [SP]            ;Loads older value for R4 from memory
    ADD     R4, R4, R5          ;Adds R5 to R4
    STR     R4, [SP], #8        ;stores current value for R4 to memory
    MOV     R5, R4              ;Makes R5 = R4

    CMP     R4, #144            ;If R4 >= 144:
    BGE     POP                 ;Branch to POP

    MOV     PC, LR              ;Moves to STMDB(PUSH) statement

POP
    LDMIA   SP!, {R4-R6, LR}    ;Pops registers off stack
    END                         ;ends program

字符串

muk1a3rh

muk1a3rh1#

您需要使用堆栈、STMDB和LDMIA指令。在真实的的ARM工具上,使用“统一”符号,它们也有助记符PUSH和POP。
Fibonnaci和factorial不是很好的例子,因为它们不“需要”递归。但让我们假装他们知道。我会选择Fibonacci,因为你没有穆尔指令!?您要执行以下操作:

START
   MOV R0, #6
   BL FIB
   END ; pseudo-instruction to make your simulator terminate

FIB                                 ; int fib(int i) {
   STMDB SP!, {R4,R5,R6,LR}         ;   int n, tmp;
   MOV R4, R0                       ;   n = i;
   CMP R0, #2                       ;   if (i <= 2) {
   MOV R0, #1                       ;     return 1;
   BLE FIB_END                      ;   }
   SUB R0, R4, #2                   ;   i = n-2;
   BL FIB                           ;   i = fib(i);
   MOV R5, R0                       ;   tmp = i;
   SUB R0, R4, #1                   ;   i = n-1;
   BL FIB                           ;   i = fib(i);
   ADD R0, R0, R5                   ;   i = i + tmp;
FIB_END                             ;   return i;
   LDMIA SP!, {R4,R5,R6,PC}         ;  }

字符串
它应该以包含fib(6)== 8的R 0终止。当然,这段代码效率很低,因为它重复调用FIB获取相同的值。
STM是必需的,因此您可以使用寄存器r4、r5,因为另一个函数调用可以更改r 0-r3和LR。推LR和弹出PC就像B LR。如果你调用C代码,你应该压入偶数个寄存器来保持SP 64位对齐(这里我们不需要这么做;忽略R6)。

yhxst69z

yhxst69z2#

一些其他递归函数:

unsigned int so ( unsigned int x )
{
    static unsigned int z=0;
    z+=x;
    if(x==0) return(z);
    so(x-1);
    return(z);
}

字符串
构建/拆卸

arm-none-eabi-gcc -O2 -c Desktop/so.c -o so.o
arm-none-eabi-objdump -D so.o

00000000 <so>:
   0:   e92d4010    push    {r4, lr}
   4:   e59f4034    ldr r4, [pc, #52]   ; 40 <so+0x40>
   8:   e5943000    ldr r3, [r4]
   c:   e3500000    cmp r0, #0
  10:   e0803003    add r3, r0, r3
  14:   e5843000    str r3, [r4]
  18:   1a000002    bne 28 <so+0x28>
  1c:   e1a00003    mov r0, r3
  20:   e8bd4010    pop {r4, lr}
  24:   e12fff1e    bx  lr
  28:   e2400001    sub r0, r0, #1
  2c:   ebfffffe    bl  0 <so>
  30:   e5943000    ldr r3, [r4]
  34:   e8bd4010    pop {r4, lr}
  38:   e1a00003    mov r0, r3
  3c:   e12fff1e    bx  lr
  40:   00000000


如果你不理解它,那么它是值得的。让一个工具帮你做这件事是作弊吗?
push是stm的伪指令,pop是ldm的伪指令,所以你可以使用它们。
我使用了一个静态局部变量,我称之为局部全局变量,它位于.data中,而不在堆栈中(在本例中是.bss,因为我将其设置为零)

Disassembly of section .bss:

00000000 <z.4099>:
   0:   00000000


第一个TO加载将该值加载到R3中。
调用约定规定r 0将包含进入函数的第一个参数(有例外,但在这种情况下是正确的)。
所以我们从内存中获取z,r 0已经有了参数x,所以我们把x加到z上,并保存到内存中
编译器不按顺序进行比较,因为谁知道性能原因,add和str写的不修改标志,所以没关系,
如果x不等于零,它分支到28,这就执行了so(x-1)调用,从内存中读回r3(调用约定说r 0-r3是volatile的,你可以随意修改它们,而不必保留它们,所以我们在r3中的z版本可能已经被破坏了,但r4被任何被调用者保留了,所以我们把z读回r3。我们将r4和返回地址从堆栈中取出,我们用z准备返回寄存器r 0并进行返回。
如果x等于零(18上的bne失败,我们运行1c,然后20,然后24),那么我们将z(r3版本)复制到r 0中,r 0是用于根据该编译器使用的调用约定(arms推荐)从该函数返回的寄存器。并返回。
链接器将把z的地址填充到偏移量0x 40,这是一个对象,而不是最终的二进制...

arm-none-eabi-ld -Ttext=0x1000 -Tbss=0x2000 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000001000
arm-none-eabi-objdump -D so.elf

so.elf:     file format elf32-littlearm

Disassembly of section .text:

00001000 <so>:
    1000:   e92d4010    push    {r4, lr}
    1004:   e59f4034    ldr r4, [pc, #52]   ; 1040 <so+0x40>
    1008:   e5943000    ldr r3, [r4]
    100c:   e3500000    cmp r0, #0
    1010:   e0803003    add r3, r0, r3
    1014:   e5843000    str r3, [r4]
    1018:   1a000002    bne 1028 <so+0x28>
    101c:   e1a00003    mov r0, r3
    1020:   e8bd4010    pop {r4, lr}
    1024:   e12fff1e    bx  lr
    1028:   e2400001    sub r0, r0, #1
    102c:   ebfffff3    bl  1000 <so>
    1030:   e5943000    ldr r3, [r4]
    1034:   e8bd4010    pop {r4, lr}
    1038:   e1a00003    mov r0, r3
    103c:   e12fff1e    bx  lr
    1040:   00002000    

Disassembly of section .bss:

00002000 <z.4099>:
    2000:   00000000


这里的重点不是欺骗和使用编译器,这里的重点是递归函数没有什么神奇之处,当然不是如果你遵循调用约定或任何你喜欢的术语。
例如
如果你有参数r 0是第一个,r1是第二个,直到r3(如果它们合适,让你的代码这样做,你有四个或更少的参数)返回值是在r 0如果它适合你需要推lr在堆栈上,因为你将调用另一个函数r4在uppreserve如果你需要修改它们,如果你想要一些本地存储,或者通过相应地修改堆栈指针来使用堆栈(或者执行push/stms)。你可以看到gcc将寄存器中的内容保存到堆栈中,然后在函数期间使用寄存器,至少有几个局部变量值,除此之外,它需要在堆栈上进行多次操作,sp相对。当你进行递归调用时,你按照调用约定像任何其他正常函数一样进行,如果你需要在调用之前保存r 0-r3,那么在寄存器r4或更高的寄存器中或在堆栈上这样做,在函数返回后恢复。你可以看到,把你想在函数调用之前和之后保存的值放在寄存器r4或更高的寄存器中会更容易。编译器可以在分支之前完成r 0的比较,这样读起来更容易。同样,在弹出之前,也可以将返回值mov到r 0
我没有输入参数,所以如果我要求更新一些的话,我的gcc版本看起来是armv 4 t。

arm-none-eabi-gcc -O2 -c -mcpu=mpcore Desktop/so.c -o so.o
arm-none-eabi-objdump -D so.o

so.o:     file format elf32-littlearm

Disassembly of section .text:

00000000 <so>:
   0:   e92d4010    push    {r4, lr}
   4:   e59f402c    ldr r4, [pc, #44]   ; 38 <so+0x38>
   8:   e3500000    cmp r0, #0
   c:   e5943000    ldr r3, [r4]
  10:   e0803003    add r3, r0, r3
  14:   e5843000    str r3, [r4]
  18:   1a000001    bne 24 <so+0x24>
  1c:   e1a00003    mov r0, r3
  20:   e8bd8010    pop {r4, pc}
  24:   e2400001    sub r0, r0, #1
  28:   ebfffffe    bl  0 <so>
  2c:   e5943000    ldr r3, [r4]
  30:   e1a00003    mov r0, r3
  34:   e8bd8010    pop {r4, pc}
  38:   00000000


你可以看到回报读起来容易一点
尽管错过了优化,但是它可以执行LDR_R0,[R4]并保存指令。或者使尾端保持原样,且BNE可以是BEq到30(movr 0,r3; pop{r4,pc}并共享退出。
可读性更强一点

so:
    push    {r4, lr}
    @ z += x
    ldr r4, zptr
    ldr r3, [r4]
    add r3, r0, r3
    str r3, [r4]
    @ if x==0 return z
    cmp r0, #0
    beq l30
    @ so(x - 1)
    sub r0, r0, #1
    bl so
    ldr r3, [r4]
l30:
    @ return z
    mov r0, r3
    pop {r4, pc}
zptr: .word z

.section .bss
z: .word 0

arm-none-eabi-as so.s -o so.o
arm-none-eabi-objdump -D so.o

so.o:     file format elf32-littlearm

Disassembly of section .text:

00000000 <so>:
   0:   e92d4010    push    {r4, lr}  (stmdb)
   4:   e59f4024    ldr r4, [pc, #36]   ; 30 <zptr>
   8:   e5943000    ldr r3, [r4]
   c:   e0803003    add r3, r0, r3
  10:   e5843000    str r3, [r4]
  14:   e3500000    cmp r0, #0
  18:   0a000002    beq 28 <l30>
  1c:   e2400001    sub r0, r0, #1
  20:   ebfffff6    bl  0 <so>
  24:   e5943000    ldr r3, [r4]

00000028 <l30>:
  28:   e1a00003    mov r0, r3
  2c:   e8bd8010    pop {r4, pc}  (ldmia)

00000030 <zptr>:
  30:   00000000    

Disassembly of section .bss:

00000000 <z>:
   0:   00000000


编辑
让我们来看看最后一个。

push {r4,lr}  which is a pseudo instruction for stmdb sp!,{r4,lr}

Lr is the r14 which is the return address look at the bl instruction
branch and link, so we branch to some address but lr (link register) is 
set to the return address, the instruction after the bl.  So when main or some other function calls so(4);  lets assume so is at address 0x1000 so the program counter, r15, pc gets 0x1000, lr will get the value of the instruction after the caller so lets say that is 0x708.  Lets also assume the stack pointer during this first call to so() from main is at 0x8000, and lets say that .bss is at 0x2000 so z lives at address 0x2000 (which also means the value at 0x1030, zptr is 0x2000.

We enter the function for the first time with r0 (x) = 4.

When you read the arm docs for stmdb sp!,{r4,lr} it decrements before (db)  so sp on entry this time is 0x8000 so it decrements for the two items to 0x7FF8, the first item in the list is written there so

0x7FF8 = r4 from main
0x7FFC = 9x 0x708 return address to main

the ! means sp stays modified so sp-0x7ff8

then ldr r4,zptr  r4 = 0x2000
ldr r3,[r4] this is an indirect load so what is at address r4 is read to 
put in r3 so r3 = [0x2000] = 0x0000 at this point  the z variable.

z+=x;  add r3,r0,r3  r3 = r0 + r3 = 4 + 0 = 4
str r3,[r4]  [r4] = r3, [0x2000] = r3 write 4 to 0x2000

cmp r0,#0   4 != 0

beq to 28 nope, not equal so no branch

sub r0,r0,#1   r0 = 4 - 1 = 3

bl so  so this is so(3); pc = 0x1000 lr = 0x1024

so now we enter so for the second time with r0 = 3

stmdb sp!,{r4,lr}

0x7FF0 = r4 (saving from so(4) call but we dont care its value even though we know it)
0x7FF4 = lr from so(4) = 0x1024
sp=0x7FF0
ldr r4,zptr r4 = 0x2000
ldr r3,[r4] r3 = [0x2000] = 4
add r3,r0,r3  r3 = 3 + 4 = 7
str r3,[r4]  write 7 to 0x2000
cmp r0,#0 3 != 0
beq 0x1028 not equal so dont branch
sub r0,r0,#1   r0 = 3-1 = 2
bl so  pc=0x1000 lr=0x1024

so(2)

stmdb sp!,{r4,lr}
0x7FE8 = r4 from caller, just save it
0x7FEC = lr from caller, 0x1024
sp=0x7FE8
ldr r4,zprt  r4=0x2000
ldr r3,[r4]  r3 = read 7 from 0x2000
add r3,r0,r3  r3 = 2 + 7 = 9
str r3,[r4]  write 9 to 0x2000
cmp r0,#0  2 != 0
beq 0x1028  not equal so dont branch
sub r0,r0,#1  r0 = 2 - 1 = 1
bl 0x1000 pc=0x1000 lr=0x1024

so(1)

stmdb sp!,{r4,lr}
0x7FE0 = save r4
0x7FE4 = lr = 0x1024
sp=0x7FE0
ldr r4,zptr r4=0x2000
ldr r3,[r4]  r3 = read 9 from 0x2000
add r3,r0,r3  r3 = 1 + 9 = 10
str r3,[r4]  write 10 to 0x2000
cmp r0,#0  1 != 0
beq 0x1028  not equal so dont branch
sub r0,r0,#1  r0 = 1 - 1 = 0
bl 0x1000  pc=0x1000 lr=0x1024

so(0)

stmdb sp!,{r4,lr}
0x7FD8 = r4
0x7FDC = lr = 0x1024
sp = 0x7FD8
ldr r4,zptr  r4 = 0x2000
ldr r3,[r4]  r3 = read 10 from 0x2000
add r3,r0,r3  r3 = 0 + 10 = 10
str r0,[r4]  write 10 to 0x2000
cmp r0,#0  0 = 0  so it matches
beq 0x1028 it is equal so we finally take this branch
mov r0,r3  r0 = 10
ldmia sp!,{r4,pc}
increment after
r4 = [sp+0] = [0x7FD8] restore r4 from caller
pc = [sp+4] = [0x7FDC] = 0x1024
sp += 8 = 0x7FE0
(branch to 0x1024)(return from so(0) to so(1))
ldr r3,[r4]  read 10 from 0x2000
mov r0,r3  r0 = 10
ldmia sp!,{r4,pc}
r4 = [sp+0] = [0x7FE0] restore r4 from caller
pc = [sp+4] = [0x7FE4] = 0x1024
sp += 8 = 0x7FE8
(branch to 0x1024)(return from so(1) to so(2))
ldr r3,[r4]  read 10 from 0x2000
mov r0,r3  r0 = 10
ldmia sp!,{r4,pc}
r4 = [sp+0] = [0x7FE8] restore r4 from caller
pc = [sp+4] = [0x7FEC] = 0x1024
sp += 8 = 0x7FF0
(branch to 0x1024)(return from so(2) to so(3))
ldr r3,[r4]  read 10 from 0x2000
mov r0,r3  r0 = 10
ldmia sp!,{r4,pc}
r4 = [sp+0] = [0x7FF0] restore r4 from caller
pc = [sp+4] = [0x7FF4] = 0x1024
sp += 8 = 0x7FF8
(branch to 0x1024)(return from so(3) to so(4))
ldr r3,[r4]  read 10 from 0x2000
mov r0,r3  r0 = 10
ldmia sp!,{r4,pc}
r4 = [sp+0] = [0x7FF8] restore r4 from caller (main()'s r4)
pc = [sp+4] = [0x7FFC] = 0x708
sp += 8 = 0x8000
(branch to 0x708)(return from so(4) to main())

and we are done.


一个堆栈就像一个迪克西杯保持器人,这可能是在你的时间。一个杯保持器你把一个杯子拉下来,下一个和其余的杯子留在杯架里,你可以把一个推回那里。
所以堆栈是函数的临时存储,在cup上写入一个数据项,然后将其推到保持器中(从调用者保存r4)写入另一个项并将其推到holder中(lr,从调用者返回地址)。我们在这里每个函数只使用了两个项目,所以每个函数我可以把两个杯子推到保持器上,每个函数的调用我得到两个新的和唯一的存储位置来存储这些本地信息。当我退出函数时,我将两个杯子从保持器中拉出来,并使用它们的值(并丢弃它们)。这在某种程度上是递归的关键,堆栈为每个调用提供了新的本地存储,与之前对同一函数的调用分开,如果没有其他的话,你需要一个返回地址(尽管做了一些更简单的递归例子,当优化时没有足够聪明地从它中产生一个循环)。
ldr_rd,[rn]把括号看作是在那个地址处的项,因此读取rn中的地址处的存储器并将那个值保存在rd中。
str rd,[rn]一个混乱的arm指令作为其余的第一个参数是左边的equals(add r1,r2,r3 r1 = r2 + r3,ldr r1,[r4] r1 = [r4])这一个是backward [rn] = rd将rd中的值存储到地址r4所描述的内存位置,一级间接。

stmdb sp!,意味着在做任何事情之前递减堆栈指针4字节乘以列表中寄存器的数量,然后将第一个最低编号的寄存器写入[sp+0],然后写入[sp+4],依此类推,最后一个将比sp的起始值小4。!表示函数结束时sp为递减后的值。你可以使用ldm/stm来完成除了栈的push和pops之外的事情。就像memcpy,但那是另一个故事。。
所有这些都可以在infocenter.arm.com上找到arm文档,你应该已经有了(arm架构参考手册,如果你还没有读过的话,armv 5是首选的第一本)。

相关问题