【问题标题】:loRecursion Example in ARM AssemblyloRecursion ARM 汇编中的示例
【发布时间】:2017-10-20 23:02:57
【问题描述】:

谁能给我一个示例,说明如何仅使用此处列出的说明(对于 visUAL)在 ARM 汇编中进行递归?

我正在尝试为类做一个递归斐波那契和阶乘函数。我知道递归是一个调用函数的函数,但我不知道如何在 ARM 中模拟它。

https://salmanarif.bitbucket.io/visual/supported_instructions.html

如果链接失效,我正在使用 visUAL,我只能使用以下说明:

  • 电影
  • MVN
  • ADR
  • LDR
  • 添加
  • ADC
  • 订阅
  • SBC
  • RSB
  • RSC
  • EOR
  • BIC
  • ORR
  • LSL
  • LSR
  • ASR
  • ROR
  • RRX
  • CMP
  • CMN
  • 尖沙咀
  • TEQ
  • LDR
  • LDM
  • STM
  • 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

【问题讨论】:

  • 模拟呼叫自己? BL yourself。保存并恢复它周围的LR。简单的。显示您正在尝试执行的操作的伪代码(或 C),以及 what you attempted 以及您遇到的问题。
  • 用 C 语言编写一个非常简单的递归示例,总共五行左右。编译和反汇编,看看产生了什么。如果您遵循 C EABI,那么您可以简单地编写代码而无需这样做,只需将其敲出即可。
  • @Jester 对不起,我已经添加了到目前为止所做的事情。我不知道 C 或 Python 3 以外的任何高级语言。
  • @old_timer 抱歉,我不会 C。我只知道 Python 3。
  • 在调试器中单步执行您的代码,这样您就可以看到寄存器值是如何变化的。您的 stmdb / ldmia 指令看起来正确,推送一些 regs + LR 并弹出相同的 regs + PC 以返回。不过,我认为您的返回值不正确。我认为您正在更新您破坏的计数器。评论你的代码;我不知道你想要它如何工作,只是它没有。 (但实际上,使用调试器单步执行应该可以解决问题。)

标签: recursion assembly arm cortex-m3 cortex-m


【解决方案1】:

您需要使用堆栈、STMDB 和 LDMIA 指令。在具有“统一”符号的真实 ARM 工具上,它们还具有助记符 PUSH 和 POP。

斐波那契和阶乘不是很好的例子,因为它们“不需要”递归。但让我们假装他们这样做。我会选择斐波那契,因为你没有 MUL 指令!?你想做这样的事情:

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 的 R0 终止。当然,这段代码效率很低,因为它重复调用 FIB 以获得相同的值。

需要 STM,因此您可以使用寄存器 r4、r5,因为另一个函数调用可以更改 r0-r3 和 LR。推LR和弹出PC就像B LR。如果您正在调用 C 代码,您应该推送偶数个寄存器以保持 SP 64 位对齐(我们实际上不需要这样做;忽略 R6)。

【讨论】:

  • 斐波那契和阶乘也一直困扰着我作为递归示例。尤其是在 asm 中,很容易错过重点而不是实际保存/恢复您的本地人,而是最终实现了一个常规循环,最后有一堆返回。 (尤其是在 x86 上,callret 已经使用了堆栈,不像 RISC 机器在非叶函数中手动保存 LR。)真正让我烦恼的是递归显然效率较低,尤其是对于 Fib。 IMO 二叉树遍历是一个更好的例子,除非你需要一个数据结构。
  • @PeterCordes 现在我试图想出一个很好的简单递归函数,它确实需要堆栈上下文并且不能针对迭代或尾调用进行优化。我还想不出...
  • 谢谢!我已经按照你说的做了,但我还是很迷茫。我不知道我做错了什么,但我认为这与 STMDB 和 LDMIA 指令有关。我以前从未对堆栈做过任何事情,所以这对我来说是全新的。
  • @NickHollinghurst:按顺序遍历树仍然是我的首选。无需调用print 函数,只需按顺序将元素附加到数组即可。作为家庭作业,树可以作为静态常量数据提供(dd 指令)。
  • 非数据结构替代方案:The Ackermann function(计算模 2^32 我猜 :)。我不支持我的可计算性理论,但它不是“原始递归”。我认为这意味着你不能轻易地将它变成一个循环。这个iterative implementation 使用堆栈数据结构,因此递归实现可能同样简单,或者实际上更简单。
【解决方案2】:

其他一些递归函数:

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    

第一个加载的是将这个值加载到 r3 中。

调用约定说 r0 将包含进入函数时的第一个参数(有例外,但在这种情况下确实如此)。

所以我们从内存中获取z,r0已经有参数x所以我们将x添加到z并保存到内存中

编译器出于谁知道性能原因而进行了无序比较,所写的 add 和 str 不要修改标志,这样就可以了,

如果 x 不等于 0,它会分支到 28,执行 so(x-1) 调用 从内存中读回 r3(调用约定说 r0-r3 是 volatile 一个函数,您可以随意修改它们而不必保留它们,因此我们在 r3 中的 z 版本可能已被破坏,但 r4 被任何被调用者保留,因此我们将 z 读回 r3。我们将 r4 和返回地址从堆栈中弹出,我们用 z 准备返回寄存器 r0 并执行返回。

如果 x 等于 0(18 上的 bne 失败,我们运行 1c,然后是 20,然后是 24)然后我们将 z(r3 版本)复制到 r0 中,这是用于根据调用约定从该函数返回的寄存器这个编译器(武器推荐)。并返回。

链接器会将z的地址填充到偏移量0x40,这是一个对象而不是最终二进制文件...

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    

这里的重点不是欺骗和使用编译器,这里的重点是递归函数没有什么神奇之处,如果你遵循调用约定或任何你喜欢的术语,当然不会。

例如

如果你有参数 r0 是第一个,r1 第二个,直到 r3(如果它们合适,让你的代码这样,并且你有四个或更少的参数) 如果适合,则返回值在 r0 中 您需要将 lr 推入堆栈,因为您将调用另一个函数 r4 on up 如果您需要修改它们,如果您想要一些本地存储,或者通过相应地修改堆栈指针(或执行 push/stms)来使用堆栈。您可以看到 gcc 将寄存器中的内容保存到堆栈中,然后在函数期间使用寄存器,至少有几个局部变量值得,除此之外,它需要大量敲击堆栈,sp 相对。 当您执行递归调用时,您会像根据调用约定的任何其他普通函数一样执行此操作,如果您需要在调用之前保存 r0-r3,则在寄存器 r4 或更高版本或堆栈中执行此操作,在函数返回。您可以看到将要在函数调用之前和之后保留的值放在寄存器 r4 或更高版本中更容易。 编译器可以在分支之前完成 r0 的比较,这样更容易阅读。同样可以在 pop 之前完成 mov 到 r0 的返回值

我没有放参数,所以我这里的 gcc 版本似乎是 armv4t,如果我要求更新一点的东西

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 可能是 30 的 beq (mov r0,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.

堆栈就像一个 dixie 杯架,可能在您的时代之前。一个杯架,您可以在其中拉下一个杯子,然后将下一个和其余杯子留在架子中,您可以将其中一个推回那里。

所以堆栈是函数的临时存储,在杯子上写入一个数据项,然后将其推入持有者(从调用者保存 r4)写入另一项并将其推入持有者(lr,返回地址来自呼叫者)。我们在这里每个函数只使用了两个项目,所以每个函数我可以将两个杯子推入支架中,每次调用该函数我都会获得两个新的唯一存储位置来存储此本地信息。当我退出该功能时,我将两个杯子从支架中拉出并使用它们的值(并丢弃它们)。这在某种程度上是递归的关键,堆栈为每次调用提供了新的本地存储,与之前对同一函数的调用分开,如果没有其他需要返回地址(尽管确实做了一些更简单的递归示例,当优化足够聪明,基本上可以循环使用)。

ldr rd,[rn] 认为他在说那个地址的项目,所以读取 rn 中地址的内存并将该值保存在 rd 中。

str rd,[rn] 一个搞砸的 arm 指令,其余的第一个参数是等号的左侧(加 r1,r2,r3 r1 = r2 + r3, ldr r1,[r4] r1 = [ r4]) 这个是后向 [rn] = rd 将 rd 中的值存储到地址 r4 所描述的内存位置,一级间接。

stmdb sp!,表示在执行任何操作之前将堆栈指针递减 4 字节乘以列表中的寄存器数量,然后将第一个编号最小的寄存器写入 [sp+0],然后写入 [sp+4] 和所以最后一个将比 sp 的起始值少四。这 !表示函数以 sp 为递减值结束。除了堆栈推送和弹出之外,您还可以使用 ldm/stm。就像 memcpy,但那是另一回事了...

所有这些都在您应该已经拥有的 infocenter.arm.com 的 arm 文档中(arm 架构参考手册,如果您还没有阅读 armv5 是首选的第一个)。

【讨论】:

  • 谢谢,但这包括我无法使用的多个说明。你说的很多事情都超出了我的想象。
  • 不,你是 100% 干净的,所有这些指令都在你的列表中,stm,ldr add,cmp b[cond],sub,bl,mov,ldm。对于 bx lr,您只需将其替换为列表中的 mov pc, lr。
  • @RidiculousName:push 和 pop 是 stmdbldmia 的别名,使用 SP!andeq 是一个 AND 指令,它只是将条件执行位设置为 EQ 条件而不是始终为真。 (另外,它并没有真正运行,它只是对全零数据/填充的反汇编)。
  • andeq 不是指令,它是被反汇编的数据。把它们删掉
  • 我提到 push 和 pop 是伪指令。
猜你喜欢
  • 1970-01-01
  • 2013-09-21
  • 1970-01-01
  • 1970-01-01
  • 2021-04-23
  • 2017-07-19
  • 2012-08-12
  • 2013-08-18
  • 1970-01-01
相关资源
最近更新 更多