无效的堆栈管理和寄存器分配

Ineffective stack management and registers allocation

考虑以下代码:

extern unsigned int foo(char c, char **p, unsigned int *n);

unsigned int test(const char *s, char **p, unsigned int *n) 
{
        unsigned int done = 0;

        while (*s)
                done += foo(*s++, p, n); 

        return done;
}

汇编输出:

00000000 <test>:
   0:   b5f8        push    {r3, r4, r5, r6, r7, lr}
   2:   0005        movs    r5, r0
   4:   000e        movs    r6, r1
   6:   0017        movs    r7, r2
   8:   2400        movs    r4, #0
   a:   7828        ldrb    r0, [r5, #0]
   c:   2800        cmp r0, #0
   e:   d101        bne.n   14 <test+0x14>
  10:   0020        movs    r0, r4
  12:   bdf8        pop {r3, r4, r5, r6, r7, pc}
  14:   003a        movs    r2, r7
  16:   0031        movs    r1, r6
  18:   f7ff fffe   bl  0 <foo>
  1c:   3501        adds    r5, #1
  1e:   1824        adds    r4, r4, r0
  20:   e7f3        b.n a <test+0xa>

使用 arm-none-eabi-gcc 版本编译的 C 代码:4.9.1、5.4.0、6.3.0 和 7.1.0 Linux 宿主。所有 GCC 版本的程序集输出都相同。

CFLAGS := -Os -march=armv6-m -mcpu=cortex-m0plus -mthumb

我对执行流程的理解如下:

  1. 将R3-R7 + LR入栈(完全不清楚)
  2. 将R0移动到R5(这就清楚了)
  3. 将R1移动到R6,将R2移动到R7(完全不清楚)
  4. 将 R5 解引用到 R0(这很清楚)
  5. 比较R0和0(这就清楚了)
  6. 如果 R0 != 0 转到第 14 行: - 从 R6 恢复 R1 并从 R7 恢复 R2 并调用 foo(), 如果 R0 == 0 留在第 10 行,从堆栈中恢复 R3 - R7 + PC(完全不清楚)
  7. 增加 R5(清除)
  8. 从 foo() 累积结果(清除)
  9. 分支回到a行:(清除)

我自己的大会。没有经过广泛的测试,但我绝对不需要将超过 R4 + LR 推入堆栈:

编辑:根据提供的答案,我下面的示例将失败,因为 R1 和 R2 没有通过调用 foo()

持久化
51 unsigned int __attribute__((naked)) test_asm(const char *s, char **p, unsigned int *n)
52 {
53         // r0 - *s (move ptr to r3 and dereference it to r0)
54         // r1 - **p
55         // r2 - *n
56         asm volatile(
57                 "   push {r4, lr}               \n\t"
58                 "   movs r4, #0                 \n\t"
59                 "   movs r3, r0                 \n\t"
60                 "1:                             \n\t"
61                 "   ldrb r0, [r3, #0]           \n\t"
62                 "   cmp r0, #0                  \n\t"
63                 "   beq 2f                      \n\t"
64                 "   bl foo                      \n\t"
65                 "   add r4, r4, r0              \n\t"
66                 "   add r3, #1                  \n\t"
67                 "   b 1b                        \n\t"
68                 "2:                             \n\t"
69                 "   movs r0, r4                 \n\t"
70                 "   pop {r4, pc}                \n\t"
71         );
72 }

问题:

  1. 为什么 GCC 会为这样一个微不足道的函数存储这么多寄存器?

  2. 为什么在ABI中写R0-R3是参数寄存器却压R3 并且应该是调用者保存并且应该在被调用函数中安全使用 在这种情况下 test()

  3. 为什么把R1复制到R6,R2复制到R7,而extern函数的原型差不多 与 test() 函数完美匹配。所以 R1 和 R2 已经准备好通过了 到 foo() 例程。我的理解是之前只需要取消引用 R0 调用 foo()

  1. LR 必须保存,因为 test 不是叶函数。 r5-r7 被函数用来存储跨函数调用使用的值,因为它们不是临时的,所以必须保存它们。 r3 被压入以对齐堆栈。

  2. push添加一个额外的寄存器是对齐堆栈的一种快速而紧凑的方法。

  3. r1r2 可能会被调用 foo 破坏,并且由于调用后需要最初存储在这些寄存器中的值,因此它们必须是存储在调用后仍然存在的位置。