理解空main()的转换为汇编
有人可以解释GCC正在为这段代码做些什么吗? 什么是初始化? 原始代码是:
#include int main() { }
它被翻译成:
.file "test1.c" .def ___main; .scl 2; .type 32; .endef .text .globl _main .def _main; .scl 2; .type 32; .endef _main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp movl $0, %eax addl $15, %eax addl $15, %eax shrl $4, %eax sall $4, %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax call __alloca call ___main leave ret
如果编译器/汇编大师通过解释堆栈,寄存器和段初始化来启动我,我将不胜感激。 我无法从代码中删除头部或尾部。
编辑:我正在使用gcc 3.4.5。 命令行参数是gcc -S test1.c
谢谢你,kunjaan。
我应该在我的所有评论前面说,我仍在学习。
我会忽略部分初始化。 关于部分初始化的解释以及基本上我覆盖的其他所有内容都可以在这里找到: http : //en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax
ebp 寄存器是堆栈帧基指针,因此是BP。 它存储指向当前堆栈开头的指针。
esp寄存器是堆栈指针。 它保存堆栈顶部的内存位置。 每次我们在堆栈上推送某些内容时,esp都会更新,以便始终指向堆栈顶部的地址。
所以ebp指向基础,esp指向顶部。 所以堆栈看起来像:
esp -----> 000a3 fa 000a4 21 000a5 66 000a6 23 esb -----> 000a7 54
如果你在堆栈上推e4,这就是:
esp -----> 000a2 e4 000a3 fa 000a4 21 000a5 66 000a6 23 esb -----> 000a7 54
请注意,堆栈会向较低的地址增长,这一事实在下面非常重要。
前两个步骤称为过程序言或更常见的函数序言 ,它们准备堆栈以供局部变量使用。 请参阅底部的程序prolog引用。
在步骤1中,我们通过调用pushl%ebp将指针保存到堆栈上的旧堆栈帧。 由于main是第一个被调用的函数,我不知道%ebp的前一个值是什么。
第2步,我们正在输入一个新的堆栈帧,因为我们正在输入一个新函数(main)。 因此,我们必须设置一个新的堆栈帧基指针。 我们使用esp中的值作为堆栈帧的开头。
步骤3.在堆栈上分配8个字节的空间。 正如我们上面提到的,堆栈向低地址增长,因此,减去8,将堆栈顶部移动8个字节。
第4步; 分配堆栈,我发现了不同的意见。 我真的不确定这是做什么的。 我怀疑这是为了允许在堆栈上分配大指令(SIMD),
http://gcc.gnu.org/ml/gcc/2008-01/msg00282.html
此代码“和”ESP与0xFFFF0000,使堆栈与下一个最低16字节边界对齐。 对Mingw源代码的检查表明,这可能是出现在“_main”例程中的SIMD指令,它只在对齐的地址上运行。 由于我们的例程不包含SIMD指令,因此不需要此行。
http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax
步骤5到11似乎对我没有任何意义。 我在谷歌上找不到任何解释。 能够真正了解这些东西的人能否提供更深入的了解。 我听说有传言称这些东西用于C的exception处理。
步骤5,将主0的返回值存储在eax中。
步骤6和7我们在未知原因的情况下将15 in hex添加到eax中。 eax = 01111 + 01111 = 11110
步骤8我们将eax 4位向右移位。 eax = 00001因为最后一位是00001 |的末尾 111。
步骤9我们将eax 4位向左移位,eax = 10000。
步骤10和11将堆栈中前4个分配字节中的值移动到eax中,然后将其从eax移回。
步骤12和13设置c库。
我们已经达到了function结局 。 也就是说,函数的一部分将堆栈指针esp和ebp返回到调用此函数之前的状态。
步骤14,将设置esp保留为ebp的值,将堆栈顶部移动到main调用之前的地址。 然后它将ebp设置为指向我们在步骤1中保存在堆栈顶部的地址。
可以使用以下说明替换休假:
mov %ebp, %esp pop %ebp
步骤15,返回并退出该function。
1. pushl %ebp 2. movl %esp, %ebp 3. subl $8, %esp 4. andl $-16, %esp 5. movl $0, %eax 6. addl $15, %eax 7. addl $15, %eax 8. shrl $4, %eax 9. sall $4, %eax 10. movl %eax, -4(%ebp) 11. movl -4(%ebp), %eax 12. call __alloca 13. call ___main 14. leave 15. ret
程序Prolog:
函数必须做的第一件事就是程序prolog。 它首先用指令pushl%ebp保存当前的基指针(ebp)(记住ebp是用于访问函数参数和局部变量的寄存器)。 现在它使用指令movl%esp,%ebp将堆栈指针(esp)复制到基指针(ebp)。 这允许您从基指针访问函数参数作为索引。 局部变量总是从ebp中减去,例如-4(%ebp)或(%ebp)-4对于第一个局部变量,返回值始终为4(%ebp)或(%ebp)+4,每个参数或参数为N * 4 + 4(%ebp),例如第一个参数为8(%ebp),而旧ebp为(%ebp)。
http://www.milw0rm.com/papers/52
存在一个非常好的堆栈溢出线程,它回答了大部分问题。 为什么我的gcc输出中有额外的说明?
有关x86机器代码指令的详细参考,请访问: http : //programminggroundup.blogspot.com/2007/01/appendix-b-common-x86-instructions.html
这个讲座包含以下使用的一些想法: http : //csc.colstate.edu/bosworth/cpsc5155/Y2006_TheFall/MySlides/CPSC5155_L23.htm
以下是回答您的问题的另一种看法: http : //www.phiral.net/linuxasmone.htm
这些来源都没有解释一切。
这是由GCC编译的简单main()
函数的一个很好的逐步细分,有很多详细信息: GAS语法(维基百科)
对于您粘贴的代码,说明细分如下:
- 前四条指令(pushl through andl):设置一个新的堆栈帧
- 接下来的五条指令(movl to sall):为eax生成一个奇怪的值,它将成为返回值(我不知道它是如何决定这样做的)
- 接下来的两条指令(均为movl):将计算出的返回值存储在堆栈上的临时变量中
- 接下来的两条指令(都调用):调用C库的init函数
-
leave
指令:撕下堆栈框架 -
ret
指令:返回调用者(外部运行时函数,或者调用程序的内核函数)
嗯,不太了解GAS,我在英特尔组装上有点生疏,但它看起来像初始化主要的堆栈框架。
如果你看一下,__ main是某种宏,必须执行初始化。 然后,当main的主体为空时,它调用leave指令,返回调用main的函数。
来自http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax#.22hello.s.22_line-by-line :
此行声明“_main”标签,标记从启动代码调用的位置。
pushl %ebp movl %esp, %ebp subl $8, %esp
这些行保存EBP在堆栈上的值,然后将ESP的值移动到EBP中,然后从ESP中减去8。 每个操作码末尾的“l”表示我们想要使用与“长”(32位)操作数一起使用的操作码版本;
andl $-16, %esp
此代码“和”ESP与0xFFFF0000,使堆栈与下一个最低16字节边界对齐。 (使用simd指令时必要,在这里没用)
movl $0, %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax
此代码将零移至EAX,然后将EAX移动到内存位置EBP-4,该位置位于我们在过程开始时在堆栈上保留的临时空间中。 然后它将内存位置EBP-4移回EAX; 显然,这不是优化代码。
call __alloca call ___main
这些函数是C库设置的一部分。 由于我们在C库中调用函数,我们可能需要这些函数。 它们执行的确切操作取决于平台和安装的GNU工具的版本。
这是一个有用的链接。
知道你正在使用的gcc版本和libc是什么真的很有帮助。 看起来你有一个非常古老的gcc版本或一个奇怪的平台或两者兼而有之。 发生的事情是调用约定有些奇怪。 我可以告诉你一些事情:
根据约定将帧指针保存在堆栈上:
pushl %ebp movl %esp, %ebp
在框架的旧端为空间腾出空间,并将堆栈指针向下舍入到4的倍数(为什么需要这个我不知道):
subl $8, %esp andl $-16, %esp
通过疯狂的歌舞,准备从main
回归1:
movl $0, %eax addl $15, %eax addl $15, %eax shrl $4, %eax sall $4, %eax movl %eax, -4(%ebp) movl -4(%ebp), %eax
恢复使用alloca
(GNU-ism)分配的任何内存:
call __alloca
向libc宣布main
正在退出(更多GNU-ism):
call ___main
恢复帧和堆栈指针:
leave
返回:
ret
以下是在Debian Linux上使用gcc 4.3编译完全相同的源代码时会发生的情况:
.file "main.c" .text .p2align 4,,15 .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (Debian 4.3.2-1.1) 4.3.2" .section .note.GNU-stack,"",@progbits
我这样打破了它:
告诉调试器和其他工具源文件:
.file "main.c"
代码在文本部分:
.text
甘拜下风:
.p2align 4,,15
main
是一个导出函数:
.globl main .type main, @function
main
的切入点:
main:
抓住返回地址,将堆栈对齐到4字节地址,然后再次保存返回地址(为什么我不能说):
leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx)
使用标准约定保存帧指针:
pushl %ebp movl %esp, %ebp
难以置信的疯狂:
pushl %ecx popl %ecx
恢复帧指针和堆栈指针:
popl %ebp leal -4(%ecx), %esp
返回:
ret
有关调试器的更多信息?:
.size main, .-main .ident "GCC: (Debian 4.3.2-1.1) 4.3.2" .section .note.GNU-stack,"",@progbits
顺便说一句, main
是特殊和神奇的; 当我编译
int f(void) { return 17; }
我得到一些更健全的东西:
.file "fc" .text .p2align 4,,15 .globl f .type f, @function f: pushl %ebp movl $17, %eax movl %esp, %ebp popl %ebp ret .size f, .-f .ident "GCC: (Debian 4.3.2-1.1) 4.3.2" .section .note.GNU-stack,"",@progbits
仍然有大量的装饰,我们仍然保存帧指针,移动它,并恢复它,这是完全没有意义的,但其余的代码是有道理的。
看起来GCC似乎可以编辑main()
以包含CRT初始化代码。 我刚刚确认我从MinGW GCC 3.4.5获得与源文本完全相同的汇编列表。
我使用的命令行是:
gcc -S emptymain.c
有趣的是,如果我将函数的名称更改为qqq()
而不是main()
,我会得到以下程序集:
.file“emptymain.c” 。文本 .globl _qqq .def _qqq; .scl 2; .type 32; .endef伪 _qqq: pushl%ebp movl%esp,%ebp popl%ebp RET
这对于没有启用优化的空函数更有意义。