想要查看某些C代码的编译器输出(在汇编中),我在C中编写了一个简单的程序,并使用gcc生成了它的汇编文件。
代码是这样的:
#include <stdio.h>
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进行比较并采取适当的措施。但是大会上发生了什么?
答案 0 :(得分:6)
由于main
很特别,你可以通过在另一个函数中执行这种类型的事情来获得更好的结果(最好是在它自己的文件中没有main
)。例如:
void foo(int x) {
if (x == 0) {
printf("testing\n");
}
}
作为装配可能会更清楚。这样做还允许您使用优化进行编译,并仍然可以观察条件行为。如果您要使用高于0的任何优化级别编译原始程序,它可能会取消比较,因为编译器可以继续并计算结果。使用此代码,部分比较对编译器是隐藏的(在参数x
中),因此编译器无法进行此优化。
_main:
pushl %ebpz
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
这是为当前功能设置堆栈帧。在x86中,堆栈帧是堆栈指针的值(SP,ESP或16位,32位或64位的RSP)与基指针的值(BP,EBP或RBP)之间的区域。这应该是局部变量存在的地方,但不是真的,并且在大多数情况下显式堆栈帧是可选的。但是,alloca
和/或可变长度数组的使用需要使用它们。
这种特殊的堆栈帧结构与非main
函数不同,因为它还确保堆栈是16字节对齐的。 ESP的减法使堆栈大小增加到足以容纳局部变量,andl
有效地从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
我想你知道这是做什么的。如果没有,movl
只是call
正在将{1}}的字符串地址移动到堆栈的顶部位置,以便printf可以对其进行重新审核。它必须在堆栈上传递,以便printf可以使用它的地址来推断printf的其他参数的地址(如果有的话,在这种情况下不存在)。
leave
该指令删除前面谈到的堆栈帧。它基本上是movl %ebp, %esp
,然后是popl %ebp
。还有一个enter
指令可用于构造堆栈帧,但gcc没有使用它。当没有显式使用堆栈帧时,EBP
可以用作一般的puropose寄存器而不是leave
编译器只会将堆栈帧大小添加到堆栈指针,这将减少堆栈大小按帧大小。
ret
我不需要解释这个。
我相信你会用不同的优化级别重新编译所有这些,所以我会指出可能会发生的事情你可能会发现很奇怪。当格式字符串不包含任何gcc
时,我发现printf
分别用fprintf
和puts
取代fputs
和%
没有传递额外的参数。这是因为(出于多种原因)调用puts
和fputs
要便宜得多,最后你仍然可以获得你想要的内容。
答案 1 :(得分:3)
不要担心序言/后缀 - 您感兴趣的部分是:
movl $0, -4(%ebp)
cmpl $0, -4(%ebp)
jne L2
movl $LC0, (%esp)
call _printf
L2:
这与原始C代码的相关性应该是不言而喻的。
答案 2 :(得分:2)
第一部分是一些初始化代码,在你的简单例子中没有任何意义。此代码将通过优化标记删除。
最后一部分可以映射到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)
答案 3 :(得分:2)
嗯,其中很大一部分是与该功能相关的开销。 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/
修改 - 另请参阅此问题Using GCC to produce readable assembly?
答案 4 :(得分:1)
您需要一些有关汇编语言的知识才能理解C编译器所获得的汇编。
此tutorial可能会有所帮助
答案 5 :(得分:1)