无效的堆栈管理和寄存器分配
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
我对执行流程的理解如下:
- 将R3-R7 + LR入栈(完全不清楚)
- 将R0移动到R5(这就清楚了)
- 将R1移动到R6,将R2移动到R7(完全不清楚)
- 将 R5 解引用到 R0(这很清楚)
- 比较R0和0(这就清楚了)
- 如果 R0 != 0 转到第 14 行: - 从 R6 恢复 R1 并从 R7 恢复 R2 并调用 foo(),
如果 R0 == 0 留在第 10 行,从堆栈中恢复 R3 - R7 + PC(完全不清楚)
- 增加 R5(清除)
- 从 foo() 累积结果(清除)
- 分支回到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 }
问题:
为什么 GCC 会为这样一个微不足道的函数存储这么多寄存器?
为什么在ABI中写R0-R3是参数寄存器却压R3
并且应该是调用者保存并且应该在被调用函数中安全使用
在这种情况下 test()
为什么把R1复制到R6,R2复制到R7,而extern函数的原型差不多
与 test() 函数完美匹配。所以 R1 和 R2 已经准备好通过了
到 foo() 例程。我的理解是之前只需要取消引用 R0
调用 foo()
LR 必须保存,因为 test
不是叶函数。 r5-r7
被函数用来存储跨函数调用使用的值,因为它们不是临时的,所以必须保存它们。 r3
被压入以对齐堆栈。
向push
添加一个额外的寄存器是对齐堆栈的一种快速而紧凑的方法。
r1
和 r2
可能会被调用 foo
破坏,并且由于调用后需要最初存储在这些寄存器中的值,因此它们必须是存储在调用后仍然存在的位置。
考虑以下代码:
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
我对执行流程的理解如下:
- 将R3-R7 + LR入栈(完全不清楚)
- 将R0移动到R5(这就清楚了)
- 将R1移动到R6,将R2移动到R7(完全不清楚)
- 将 R5 解引用到 R0(这很清楚)
- 比较R0和0(这就清楚了)
- 如果 R0 != 0 转到第 14 行: - 从 R6 恢复 R1 并从 R7 恢复 R2 并调用 foo(), 如果 R0 == 0 留在第 10 行,从堆栈中恢复 R3 - R7 + PC(完全不清楚)
- 增加 R5(清除)
- 从 foo() 累积结果(清除)
- 分支回到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 }
问题:
为什么 GCC 会为这样一个微不足道的函数存储这么多寄存器?
为什么在ABI中写R0-R3是参数寄存器却压R3 并且应该是调用者保存并且应该在被调用函数中安全使用 在这种情况下 test()
为什么把R1复制到R6,R2复制到R7,而extern函数的原型差不多 与 test() 函数完美匹配。所以 R1 和 R2 已经准备好通过了 到 foo() 例程。我的理解是之前只需要取消引用 R0 调用 foo()
LR 必须保存,因为
test
不是叶函数。r5-r7
被函数用来存储跨函数调用使用的值,因为它们不是临时的,所以必须保存它们。r3
被压入以对齐堆栈。向
push
添加一个额外的寄存器是对齐堆栈的一种快速而紧凑的方法。r1
和r2
可能会被调用foo
破坏,并且由于调用后需要最初存储在这些寄存器中的值,因此它们必须是存储在调用后仍然存在的位置。