【问题标题】:Where is the "2+2" in this Assembly code (translated by gcc from C)此汇编代码中的“2+2”在哪里(由 gcc 从 C 翻译)
【发布时间】:2013-09-16 17:26:52
【问题描述】:

我已经编写了这个简单的 C 代码

int main()
{
    int calc = 2+2;
    return 0;
}

我想看看它在汇编中的样子,所以我使用 gcc 编译它

$ gcc -S -o asm.s test.c

结果是 ~65 行(Mac OS X 10.8.3),我只发现这些是相关的:

在这段代码中我在哪里可以找到我的2+2

编辑:

部分问题尚未解决。

如果%rbp, %rsp, %eax 是变量,在这种情况下它们会获得什么值?

【问题讨论】:

  • 编译器几乎可以肯定只是将4 直接移动到calc
  • 请注意,进一步的优化甚至可能完全消除calc,因为它没有在任何地方的函数体中引用

标签: c gcc assembly


【解决方案1】:

您得到的几乎所有代码都只是无用的堆栈操作。通过优化 (gcc -S -O2 test.c) 你会得到类似

main:
.LFB0:
    .cfi_startproc
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:

忽略以点开头或以冒号结尾的每一行:只有两个汇编指令:

    xorl %eax, %eax
    ret

它们编码return 0;。 (将寄存器与自身进行异或会将其设置为所有位为零。函数返回值根据 x86 ABI 进入寄存器 %eax。)与您的 int calc = 2+2; 相关的所有内容都已被丢弃为未使用。

如果您将代码更改为

int main(void) { return 2+2; }

你会得到

    movl $4, %eax
    ret

其中 4 来自编译器自己进行加法,而不是让生成的程序来进行加法(这称为 constant folding)。

也许更有趣的是,如果您将代码更改为

int main(int argc, char **argv) { return argc + 2; }

然后你得到

    leal    2(%rdi), %eax
    ret

它在运行时做了一些真正的工作!在 64 位 ELF ABI 中,%rdi 保存函数的第一个参数,在本例中为 argcleal 2(%rdi), %eax 是 "%eax = %edi + 2" 的 x86 汇编语言,这样做主要是因为更熟悉的 add 指令只需要两个参数,因此您不能使用它将 2 添加到 %rdi 并将导致%eax 全部在一条指令中。 (暂时忽略%rdi%edi 之间的区别。)

【讨论】:

  • 感谢优化指针以及如何评估.\:,这回答了我的一些后续问题。
【解决方案2】:

编译器确定2+2 = 4 并将其内联。常量存储在第 10 行($4)。要验证这一点,请将数学公式更改为 2+3,您将看到 $5

编辑:至于寄存器本身,%rsp 是堆栈指针,%rbp 是帧指针,%eax 是通用寄存器

【讨论】:

  • 你是对的,它确实变成了$5,但是为什么所有额外的代码呢?还有我对这些 %rbp, %rsp, %eax 值的疑问?
  • @MorganWilde %rsp 是堆栈指针,%rbp 是帧指针,%eax 是通用寄存器
【解决方案3】:

下面是汇编代码的解释:

pushq    %rbp

这会将帧指针的副本保存在堆栈上。函数本身不需要这个;它在那里,以便调试器或异常处理程序可以在堆栈上找到帧。

movq     %rsp, %rbp

这通过将帧指针设置为指向当前栈顶来开始一个新帧。同样,该功能不需要这个;维护适当的堆栈是家务。

mov      $4, -12(%rbp)

这里编译器将calc 初始化为4。这里发生了几件事。首先,编译器自己评估2+2,并在汇编代码中使用结果4。在执行程序中不进行算术运算;它是在编译器中完成的。其次,calc 已分配到帧指针下方 12 个字节的位置。 (这很有趣,因为它也在堆栈指针下方。该架构的 OS X ABI 在堆栈指针下方包含一个允许程序使用的“红色区域”,这是不寻常的。)第三,程序显然是在没有优化。我们知道,因为优化器会识别出这段代码没有任何作用和无用,所以它会删除它。

movl     $0, -8(%rbp)

此代码将 0 存储在编译器预留的位置以准备 main 的返回值。

movl     -8(%rbp), %eax
movl     %eax, -4(%rbp)

这会将数据从准备返回值的位置复制到临时处理位置。这比之前的代码更没用,强化了没有使用优化的结论。这看起来像我期望的负面优化级别的代码。

movl     -4(%rbp), %eax

这会将返回值从临时处理位置移动到返回给调用者的寄存器中。

popq      %rbp

这会恢复帧指针,从而从堆栈中删除先前推送的帧。

ret

这使程序摆脱了困境。

【讨论】:

    【解决方案4】:

    您的程序没有可观察到的行为,这意味着在一般情况下,编译器可能根本不会为它生成任何机器代码,除了一些旨在确保将零返回到调用环境的最小启动包装指令。至少将您的变量声明为volatile。或者在评估它之后打印它的值。或者从main返回。

    另请注意,在 C 语言中,2 + 2 可作为整数常量表达式。这意味着编译器不仅允许,而且实际上要求知道该表达式在编译时的结果。考虑到这一点,当编译时知道最终值(即使您完全禁用优化)时,期望编译器在运行时评估 2 + 2 会很奇怪。

    【讨论】:

    • 你说的完全正确,我不知道是这样的,即编译器在编译时进行一些计算。
    【解决方案5】:

    编译器对其进行了优化,它预先计算了答案并设置了结果。如果您想看到编译器进行添加,那么您不能让它“看到”您提供给它的常量

    如果您将这段代码全部编译为对象(gcc -O2 -c test_add.c -o test_add.o) 然后您将强制编译器生成添加代码。但操作数将是寄存器或堆栈上。

    int test_add ( int a, int b )
    {
       return(a+b);
    }
    

    然后,如果您从单独源代码 (gcc -O2 -c test.c -o test.o) 中调用它,那么您将看到两个操作数被强制进入函数。

    extern int test_add ( int, int );
    int test ( void )
    {
         return(test_add(2,2));
    }
    

    你可以反汇编这两个对象(objdump -D test.o,objdump -D test_add.o)

    当你在一个文件中做这么简单的事情时

    int main ( void )
    {
         int a,b,c;
         a=2;
         b=2;
         c=a+b;
         return(0);
    }
    

    编译器可以将您的代码优化为几个等效项之一。我在这里的例子什么都不做,数学和结果没有目的,它们没有被使用,所以它们可以简单地作为死代码删除。您的优化做到了这一点

    int main ( void )
    {
         int c;
         c=4;
         return(0);
    }
    

    但这也是对上述代码的完美优化

    int main ( void )
    {
        return(0);
    }
    

    编辑:

    calc=2+2 在哪里?

    我相信

    movl $4,-12(%rbp)
    

    是 2+2 (答案是计​​算出来的,然后简单地放在堆栈上的 calc 中。

    movl $0,-8(%rbp) 
    

    我假设是你的 return(0) 中的 0;

    两个数字相加的实际数学已被优化。

    【讨论】:

      【解决方案6】:

      我猜第 10 行,他优化了,因为所有都是常量

      【讨论】:

        猜你喜欢
        • 2021-09-27
        • 2022-11-20
        • 1970-01-01
        • 1970-01-01
        • 2021-08-16
        • 1970-01-01
        • 1970-01-01
        • 2023-01-31
        • 1970-01-01
        相关资源
        最近更新 更多