R7 和 R11 与 Link 寄存器在 ARM 架构中的关系 (thumb/arm) 调用约定

R7 and R11 relation with Link Register in ARM architecture (thumb/arm) calling convention

我在看 gcc 生成的 arm 汇编代码,我注意到 GCC 编译了一个函数,代码如下:

   0x00010504 <+0>: push    {r7, lr}
   0x00010506 <+2>: sub sp, #24
   0x00010508 <+4>: add r7, sp, #0
   0x0001050a <+6>: str r0, [r7, #4]
=> 0x0001050c <+8>: mov r3, lr
   0x0001050e <+10>:    mov r1, r3
   0x00010510 <+12>:    movw    r0, #1664   ; 0x680
   0x00010514 <+16>:    movt    r0, #1
   0x00010518 <+20>:    blx 0x10378 <printf@plt>
   0x0001051c <+24>:    add.w   r3, r7, #12
   0x00010520 <+28>:    mov r0, r3
   0x00010522 <+30>:    blx 0x10384 <gets@plt>
   0x00010526 <+34>:    mov r3, lr
   0x00010528 <+36>:    mov r1, r3
   0x0001052a <+38>:    movw    r0, #1728   ; 0x6c0
   0x0001052e <+42>:    movt    r0, #1
   0x00010532 <+46>:    blx 0x10378 <printf@plt>
   0x00010536 <+50>:    adds    r7, #24
   0x00010538 <+52>:    mov sp, r7
   0x0001053a <+54>:    pop {r7, pc}

我感兴趣的是,我看到 GCC 使用 R7 将值弹出到 PC 而不是 LR。我在 R11 上看到了类似的东西。编译器将 r11 和 LR 压入堆栈,然后将 R11 弹出到 PC。 LR 不应充当 return 地址而不是 R7 或 R11。为什么要在这里使用 R7(Thumb 模式下的帧指针)? 如果您查看 apple ios 调用约定,它甚至会有所不同。它使用其他寄存器(例如 r4 到 r7)来控制 PC 到 return。不应该用LR吗?

或者我遗漏了什么?

另一个问题是,看起来 LR、R11 或 R7 值从来都不是 return 地址的立即值。但是指向包含 return 地址的堆栈的指针。是吗?

另一件奇怪的事情是编译器不会对函数结尾做同样的事情。例如,它可能不是使用 pop 到 PC,而是使用 bx LR,但是为什么呢?

首先,他们可能希望让堆栈在 64 位边界上对齐。

R7 对于帧指针来说比任何更大的都好,因为大多数指令不支持寄存器 r8 到 r15。我得看看我会假设有特殊的 pc 和 sp offset load/store 指令那么为什么 r7 会被烧掉?

不确定您要问的所有问题,根据经验,您可以按 lr 但可以按 pop pc,我认为这等同于 bx lr,但是您必须针对每个体系结构查找它,因为对于某些您不能使用 pop 切换模式.在这种情况下,它似乎假设并且不使用 pop r3 bx r3 之类的东西来刻录额外的指令。实际上,要做到这一点可能需要两条额外的指令 pop r7、pop r3、bx r3。

所以可能是这样一种情况,一个编译器被告知正在使用什么架构,并且可以假设 pop pc 是安全的,而另一个则不太确定。再次必须阅读各种体系结构的 arm 体系结构文档,以了解哪些指令可用于更改模式,哪些不能。也许如果您使用 gnu 浏览各种架构类型,它可能会改变它的方式 returns.

编辑

unsigned int morefun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int x, unsigned int y )
{
    x+=1;
    return(morefun(x,y+2)+3);
}
arm-none-eabi-gcc -O2 -mthumb -c so.c -o so.o
arm-none-eabi-objdump -D so.o 
00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   3102        adds    r1, #2
   4:   3001        adds    r0, #1
   6:   f7ff fffe   bl  0 <morefun>
   a:   3003        adds    r0, #3
   c:   bc10        pop {r4}
   e:   bc02        pop {r1}
  10:   4708        bx  r1
  12:   46c0        nop         ; (mov r8, r8)

arm-none-eabi-gcc -O2 -mthumb -mcpu=cortex-m3 -march=armv7-m -c so.c -o so.o
arm-none-eabi-objdump -D so.o 
00000000 <fun>:
   0:   b508        push    {r3, lr}
   2:   3102        adds    r1, #2
   4:   3001        adds    r0, #1
   6:   f7ff fffe   bl  0 <morefun>
   a:   3003        adds    r0, #3
   c:   bd08        pop {r3, pc}
   e:   bf00        nop

在没有 mcpu 的情况下使用那个 march 会给出相同的结果(不会将 lr 弹出到 r1 到 bx)。

march=armv5t 稍作改动

00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   3102        adds    r1, #2
   4:   3001        adds    r0, #1
   6:   f7ff fffe   bl  0 <morefun>
   a:   3003        adds    r0, #3
   c:   bd10        pop {r4, pc}
   e:   46c0        nop         ; (mov r8, r8)

armv4t 按预期执行 pop 和 bx 操作。

armv6-m 给出了 armv5t 给出的结果。

gcc 版本 6.1.0 使用 --target=arm-none-eabi 构建,没有任何其他 arm 说明符。

很可能当 OP 问我是否理解正确时,他们可能看到了三个指令 pop pop bx 而不是单个 pop {rx,pc}。或者至少一个编译器与另一个编译器不同。提到了 Apple IOS,因此它可能默认使用比无处不在的东西更重的核心。他们的 gcc 像我的一样默认在任何地方工作(包括原始的 ARMv4T)而不是在除原始版本之外的任何地方工作。我假设如果您添加一些命令行选项,您将看到 gcc 编译器的行为与我所演示的不同。

请注意,在这些示例中没有使用 r3 和 r4,为什么要保留它们呢?这可能是我提到的在堆栈上保持 64 位对齐的第一件事。如果对于所有 thumb 变体解决方案,如果您在弹出之间获得中断,则中断处理程序正在处理未对齐的堆栈。由于 r4 无论如何都是一次性的,因此他们可以分别弹出 r1 和 r2 或 r2 和 r3,然后弹出 bx r2 或 bx r3,而不是没有对齐并保存指令的那一刻。哦好吧...