对于家庭作业,我已经获得了一些c文件,并使用arm-linux-gcc编译它们(我们最终会定位gumstix板,但是对于这些练习,我们一直在使用qemu和ema)。
其中一个问题让我感到困惑 - 我们被告知:
使用arm-linux-objdump查找可执行二进制文件中main()中声明的变量的位置。
但是,这些变量是本地的,因此在运行之前不应该有地址,对吗?
我想也许我需要找到的是堆栈帧中的偏移量,实际上可以使用objdump找到它(不是我知道的)。
无论如何,对此事的任何见解将不胜感激,如果有必要,我很乐意发布源代码。
答案 0 :(得分:2)
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中输入r,在r1中输入y,在r2中输入z。然后x的主页位于fp-16,y位于fp-20,z位于fp-24。那么对one()的调用需要x和y,所以它从堆栈中拉出那些(x和y)。一个()的结果进入一个保存在fp-8的结果,这就是a的主页。等等。
函数1实际上不在地址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:ebfffffe bl 0 c:e1a04000 mov r4,r0 10:e1a01005 mov r1,r5 14:ebfffffe bl 0 18:e0800004添加r0,r0,r4 1c:e8bd4038 pop {r3,r4,r5,lr} 20:e12fff1e bx lr
编译器决定做的是通过将它们保存在堆栈中来为自己提供更多寄存器。为什么它保存r3是一个谜,但这是另一个话题......
根据调用约定输入函数r0 = x,r1 = y和r2 = z,我们可以单独留下r0和r1(再次尝试使用一个(y,x)并看看会发生什么)因为它们直接进入一个()并且永远不会再使用。调用约定说r0-r3可以被函数破坏,所以我们需要保留z以便以后将其保存在r5中。一个()的结果是r0每个调用约定,因为two()可以破坏r0-r3我们需要保存一个for for after,在调用two()之后我们还需要r0来调用两个,所以r4现在持有一个。我们在调用one之前将r保存在r5中(在r2中移动到r5),我们需要将one()的结果作为two()的第一个参数,并且它已经存在,我们需要z作为第二个所以我们需要z移动r5我们将z保存到r1,然后我们调用two()。每个调用约定的结果为two()。由于b + a = a + b来自基本数学属性,因此返回前的最终添加是r0 + r4,即b + a,结果是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
优化可以节省更多...... {/ p>
答案 1 :(得分:1)
这将取决于程序以及他们究竟想要变量的位置。问题是否需要存储哪些代码部分? .const .bss等?它需要特定的地址吗?无论哪种方式,一个好的开始是使用objdump -S flag
objdump -S myprogram > dump.txt
这很好,因为它会打印出源代码和带地址的程序集的混合。从这里开始搜索您的int main
,这应该可以帮助您入门。