对于一项家庭作业,我得到了一些c文件,并使用arm-linux-gcc进行了编译(我们最终将针对gumstix板,但是对于这些练习,我们一直在使用qemu和ema)。

其中一个问题使我有些困惑-有人告诉我们:



但是,这些变量是局部变量,因此在运行时不应该有地址,对吗?

我在想,也许我需要找到的是堆栈框架中的偏移量,实际上可以使用objdump找到它(不是我知道如何)。

无论如何,我们将不胜感激对此事的任何见解,如有必要,我很乐意发布源代码。

最佳答案

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: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(再次尝试使用one(y,x)看看会发生什么),因为它们直接落入one()并再也不会使用了。调用约定说r0-r3可以被函数破坏,因此我们需要保留z以便以后将其保存在r5中。根据调用约定,one()的结果为r0,因为two()可以销毁r0-r3,所以我们需要保存一个for,在对two()进行调用之后,无论如何我们也需要r0来调用两个,所以r4现在持有。我们在调用a之前将z保存在r5中(r2中的r2移到r5中),我们需要将one()的结果作为two()的第一个参数,并且它已经存在,我们需要z作为第二个参数,所以我们将保存z的r5移到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个字节,这导致子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

优化可以节省更多...

关于c - 内存中的局部变量位置,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/15180309/

10-12 16:08