unsigned int one ( unsigned int, unsigned int );
unsigned int two ( unsigned int, unsigned int );
unsigned int myfun ( unsigned int x, unsigned int y, unsigned int z )
{
unsigned int a,b;
a=one(x,y);
b=two(a,z);
return(a+b);
}
编译和反汇编
arm-none-eabi-gcc -c fun.c -o fun.o
arm-none-eabi-objdump -D fun.o
编译器生成的代码
00000000 <myfun>:
0: e92d4800 push {fp, lr}
4: e28db004 add fp, sp, #4
8: e24dd018 sub sp, sp, #24
c: e50b0010 str r0, [fp, #-16]
10: e50b1014 str r1, [fp, #-20]
14: e50b2018 str r2, [fp, #-24]
18: e51b0010 ldr r0, [fp, #-16]
1c: e51b1014 ldr r1, [fp, #-20]
20: ebfffffe bl 0 <one>
24: e50b0008 str r0, [fp, #-8]
28: e51b0008 ldr r0, [fp, #-8]
2c: e51b1018 ldr r1, [fp, #-24]
30: ebfffffe bl 0 <two>
34: e50b000c str r0, [fp, #-12]
38: e51b2008 ldr r2, [fp, #-8]
3c: e51b300c ldr r3, [fp, #-12]
40: e0823003 add r3, r2, r3
44: e1a00003 mov r0, r3
48: e24bd004 sub sp, fp, #4
4c: e8bd4800 pop {fp, lr}
50: e12fff1e bx lr
简短的回答是在编译时和运行时都“分配”了内存。在编译时,编译器在编译时确定堆栈帧的大小以及谁去哪里。运行时内存本身在堆栈上,这是一个动态的东西。堆栈帧在运行时从堆栈内存中获取,类似于 malloc() 和 free()。
了解调用约定会有所帮助,x 在 r0 中输入,y 在 r1 中,z 在 r2 中。那么 x 的家位于 fp-16,y 位于 fp-20,z 位于 fp-24。那么对 one() 的调用需要 x 和 y,因此它会从堆栈中提取它们(x 和 y)。one() 的结果进入 a,它保存在 fp-8 中,因此这是 a 的家。等等。
函数一实际上不在地址 0 处,这是对目标文件的反汇编,而不是链接的二进制文件。一旦一个对象与其余的对象和库链接,缺少的部分,如外部函数所在的位置,将由链接器修补,并且对 one() 和 two() 的调用将获得真实地址。(并且程序可能不会从地址 0 开始)。
我在这里作弊了一点,我知道如果没有在编译器上启用优化并且像这样一个相对简单的函数,真的没有任何堆栈帧的理由:
只需一点优化即可编译
arm-none-eabi-gcc -O1 -c fun.c -o fun.o
arm-none-eabi-objdump -D fun.o
堆栈帧消失了,局部变量保留在寄存器中。
00000000 : 0: e92d4038 推 {r3, r4, r5, lr} 4: e1a05002 mov r5, r2 8: ebffffffe bl 0 c: e1a04000 mov r4, r0 10: e1a01005 mov r1, r5 14: e080000 0 18: r0, r0, r4 1c: e8bd4038 pop {r3, r4, r5, lr} 20: e12fff1e bx lr
编译器决定做的是通过将它们保存在堆栈上来给自己更多的寄存器来使用。为什么它保存了 r3 是一个谜,但这是另一个话题......
根据调用约定输入函数 r0 = x, r1 = y 和 r2 = z,我们可以不理会 r0 和 r1(用 one(y,x) 再试一次,看看会发生什么),因为它们直接落入 one() 和不再使用。调用约定说 r0-r3 可以被函数销毁,因此我们需要保留 z 以供以后使用,因此我们将其保存在 r5 中。根据调用约定 one() 的结果是 r0,因为 two() 可以破坏 r0-r3 我们需要保存 a 以备后用,在调用 two() 之后我们也需要 r0 来调用两个,所以 r4现在持有一个。我们在调用 one 之前将 z 保存在 r5 中(在 r2 中移动到 r5),我们需要 one() 的结果作为 two() 的第一个参数,并且它已经存在,我们需要 z 作为第二个参数,所以我们将我们保存 z 的 r5 移动到 r1,然后我们调用 two()。根据调用约定的 two() 的结果。由于 b + a = a + b 来自基本数学属性,因此在返回之前的最终加法是 r0 + r4,即 b + a,结果进入 r0,r0 是用于从函数返回某些内容的寄存器,按照惯例。清理堆栈并恢复修改后的寄存器,完成。
由于 myfun() 使用 bl 调用了其他函数,bl 修改了链接寄存器 (r14),为了能够从 myfun() 返回,我们需要将链接寄存器中的值从函数的入口中保留下来最后返回 (bx lr),所以 lr 被压入堆栈。约定规定我们可以在函数中销毁 r0-r3,但不能销毁其他寄存器,因此 r4 和 r5 被压入堆栈,因为我们使用了它们。从调用约定的角度来看,为什么不需要将 r3 推入堆栈,我想知道它是否是在预期 64 位内存系统的情况下完成的,进行两次完整的 64 位写入比一次 64 位写入和一次 32 位写入便宜。但是你需要知道堆栈的对齐方式,所以这只是一个理论。没有理由在此代码中保留 r3。
现在利用这些知识并反汇编分配的代码(arm-...-objdump -D something.something)并进行相同的分析。特别是对于名为 main() 的函数与未命名为 main 的函数(我没有故意使用 main()),堆栈帧的大小可能没有意义,或者比其他函数更没有意义。在上面的非优化情况下,我们总共需要存储 6 个东西,x,y,z,a,b 和链接寄存器 6*4 = 24 字节,这导致了 sub sp, sp, #24,我需要考虑堆栈指针与帧指针的关系。我认为有一个命令行参数告诉编译器不要使用帧指针。-fomit-frame-pointer,它保存了一些指令
00000000 <myfun>:
0: e52de004 push {lr} ; (str lr, [sp, #-4]!)
4: e24dd01c sub sp, sp, #28
8: e58d000c str r0, [sp, #12]
c: e58d1008 str r1, [sp, #8]
10: e58d2004 str r2, [sp, #4]
14: e59d000c ldr r0, [sp, #12]
18: e59d1008 ldr r1, [sp, #8]
1c: ebfffffe bl 0 <one>
20: e58d0014 str r0, [sp, #20]
24: e59d0014 ldr r0, [sp, #20]
28: e59d1004 ldr r1, [sp, #4]
2c: ebfffffe bl 0 <two>
30: e58d0010 str r0, [sp, #16]
34: e59d2014 ldr r2, [sp, #20]
38: e59d3010 ldr r3, [sp, #16]
3c: e0823003 add r3, r2, r3
40: e1a00003 mov r0, r3
44: e28dd01c add sp, sp, #28
48: e49de004 pop {lr} ; (ldr lr, [sp], #4)
4c: e12fff1e bx lr
优化可以节省更多...