解码C代码的等效汇编代码

想要查看某些C代码的编译器输出(在汇编中),我在C中编写了一个简单的程序,并使用gcc生成了它的汇编文件。

代码是这样的:

#include  int main() { int i = 0; if ( i == 0 ) { printf("testing\n"); } return 0; } 

它生成的程序集在这里(只有主函数):

 _main: pushl %ebpz movl %esp, %ebp subl $24, %esp andl $-16, %esp movl $0, %eax addl $15, %eax addl $15, %eax shrl $4, %eax sall $4, %eax movl %eax, -8(%ebp) movl -8(%ebp), %eax call __alloca call ___main movl $0, -4(%ebp) cmpl $0, -4(%ebp) jne L2 movl $LC0, (%esp) call _printf L2: movl $0, %eax leave ret 

我完全无法关联C代码和汇编代码。 代码所要做的就是将0存储在寄存器中并将其与常量0进行比较并采取适当的操作。 但是大会里发生了什么?

由于main是特殊的,你可以通过在另一个函数中执行这种类型的事情来获得更好的结果(最好是在它自己的文件中没有main )。 例如:

 void foo(int x) { if (x == 0) { printf("testing\n"); } } 

assembly可能会更加清晰。 这样做还允许您使用优化进行编译,并仍然可以观察条件行为。 如果您要使用高于0的任何优化级别编译原始程序,那么可能会取消比较,因为编译器可以继续计算结果。 使用此代码部分比较对于编译器(在参数x )是隐藏的,因此编译器无法进行此优化。

多余的东西实际上是什么

 _main: pushl %ebpz movl %esp, %ebp subl $24, %esp andl $-16, %esp 

这是为当前function设置堆栈帧。 在x86中,堆栈帧是堆栈指针的值(SP,ESP或16位,32位或64位的RSP)与基指针的值(BP,EBP或RBP)之间的区域。 这应该是局部变量存在的地方,但不是真的,并且在大多数情况下显式堆栈帧是可选的。 但是,使用alloca和/或可变长度数组将需要使用它们。

这种特定的堆栈帧结构与非mainfunction不同,因为它还确保堆栈是16字节对齐的。 ESP的减法使堆栈大小增加到足以容纳局部变量,并且有效地从它减去0到15,使其与16字节对齐。 这种对齐似乎过多,除了它会强制堆栈也开始缓存对齐以及字对齐。

 movl $0, %eax addl $15, %eax addl $15, %eax shrl $4, %eax sall $4, %eax movl %eax, -8(%ebp) movl -8(%ebp), %eax call __alloca call ___main 

我不知道这一切是怎么回事。 alloca通过更改堆栈指针的值来增加堆栈帧大小。

 movl $0, -4(%ebp) cmpl $0, -4(%ebp) jne L2 movl $LC0, (%esp) call _printf L2: movl $0, %eax 

我想你知道这是做什么的。 如果没有,则只需call movl即可将字符串的地址移动到堆栈的顶部位置,以便printf可以对其进行重试。 它必须在堆栈上传递,以便printf可以使用它的地址来推断printf的其他参数的地址(如果有的话,在这种情况下不存在)。

 leave 

该指令删除前面谈到的堆栈帧。 它基本上是movl %ebp, %esp后跟popl %ebp 。 还有一个可用于构造堆栈帧的enter指令,但gcc没有使用它。 当没有显式使用堆栈帧时, EBP可以用作通用的puropose寄存器,而不是leave编译器只是将堆栈帧大小添加到堆栈指针,这将使堆栈大小减小帧大小。

 ret 

我不需要解释这个。

使用优化进行编译时

我相信你会用不同的优化级别重新编译所有这些,所以我会指出可能会发生的事情你可能会发现很奇怪。 当格式字符串不包含任何%并且没有传递其他参数时,我观察到gcc分别用putsfputs替换printffprintf 。 这是因为(出于很多原因)调用putsfputs要便宜得多,最后你仍然得到你想要的东西。

不要担心序言/后缀 – 您感兴趣的部分是:

 movl $0, -4(%ebp) cmpl $0, -4(%ebp) jne L2 movl $LC0, (%esp) call _printf L2: 

关于它如何与原始C代码相关联应该是不言而喻的。

第一部分是一些初始化代码,在您的简单示例的情况下没有任何意义。 将使用优化标志删除此代码。

最后一部分可以映射到C代码:

 movl $0, -4(%ebp) // put 0 into variable i (located at -4(%ebp)) cmpl $0, -4(%ebp) // compare variable i with value 0 jne L2 // if they are not equal, skip to after the printf call movl $LC0, (%esp) // put the address of "testing\n" at the top of the stack call _printf // do call printf L2: movl $0, %eax // return 0 (calling convention: %eax has the return code) 

好吧,其中很大一部分是与该function相关的开销。 main()只是一个像其他函数一样的函数,所以它必须在开始时将返回地址存储在堆栈中,在结尾处设置返回值等。

我建议使用GCC生成混合源代码和汇编程序,它将显示为每个源代码生成的汇编程序。

如果要将C代码与转换为它的程序集一起使用,请使用如下命令行:

 gcc -c -g -Wa,-a,-ad [other GCC options] foo.c > foo.lst 

请参阅http://www.delorie.com/djgpp/v2faq/faq8_20.html

在linux上,只需使用gcc。 在Windows下载Cygwin http://www.cygwin.com/


编辑 – 另请参阅此问题使用GCC生成可读的程序集?

和http://oprofile.sourceforge.net/doc/opannotate.html

您需要一些有关汇编语言的知识才能理解C编译器所获得的汇编。

本教程可能会有所帮助

在这里查看更多信息。 您可以使用C注释生成汇编代码,以便更好地理解。

 gcc -g -Wa,-adhls your_c_file.c > you_asm_file.s 

这应该对你有所帮助。