我写了这个简单的 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
是变量,在这种情况下它们获得什么值?
你得到的几乎所有代码都只是无用的堆栈操作。优化 ( gcc -S -O2 test.c
) 你会得到类似的东西
main:
.LFB0:
.cfi_startproc
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
忽略以点开头或以冒号结尾的每一行:只有两个汇编指令:
xorl %eax, %eax
ret
他们编码return 0;
. (将寄存器与自身进行异或会将其设置为所有位为零。函数返回值%eax
根据 x86 ABI 进入寄存器。)与您有关的所有int calc = 2+2;
内容都已被丢弃为未使用。
如果您将代码更改为
int main(void) { return 2+2; }
你会得到
movl $4, %eax
ret
其中 4 来自编译器自己做加法,而不是让生成的程序做它(这称为常量折叠)。
也许更有趣的是,如果您将代码更改为
int main(int argc, char **argv) { return argc + 2; }
然后你得到
leal 2(%rdi), %eax
ret
这是在运行时做一些真正的工作!在 64 位 ELF ABI 中,%rdi
保存函数的第一个参数,argc
在这种情况下。 leal 2(%rdi), %eax
"" 是 x86 汇编语言,%eax = %edi + 2
这样做主要是因为更熟悉的add
指令只需要两个参数,所以你不能用它来加 2%rdi
并将结果%eax
全部放在一条指令中。%rdi
(暂时忽略和之间的区别%edi
。)
编译器确定2+2 = 4
并内联它。常量存储在第 10 行(第 10 行$4
)。要验证这一点,请将数学更改为2+3
,您将看到$5
编辑:至于寄存器本身,%rsp
是堆栈指针,%rbp
是帧指针,%eax
是通用寄存器
您的程序没有可观察到的行为,这意味着在一般情况下,编译器可能根本不会为它生成任何机器代码,除了一些旨在确保将零返回到调用环境的最小启动包装指令。至少将您的变量声明为volatile
. 或者在评估它之后打印它的值。或从main
.
另请注意,在 C 语言中,2 + 2
有资格作为整数常量表达式。这意味着不仅允许编译器,而且实际上还需要在编译时知道该表达式的结果。考虑到这一点,期望编译器2 + 2
在编译时知道最终值(即使您完全禁用优化)时在运行时进行评估是很奇怪的。
下面是对汇编代码的解释:
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
这使程序摆脱了痛苦。
编译器对其进行了优化,它预先计算了答案并设置了结果。如果您想看到编译器进行添加,那么您不能让它“看到”您提供给它的常量
如果您将此代码全部编译为对象(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;
将两个数字相加的实际数学进行了优化。
我猜第 10 行,他优化了,因为一切都是常数