我编写空程序来惹恼stackoverflow程序员的地狱,不是。我正在探索gnu工具链。
现在以下内容可能对我来说太深了,但是为了继续执行空程序传奇,我已经开始检查C编译器的输出,即GNU消耗的东西。
gcc version 4.4.0 (TDM-1 mingw32)
test.c的:
int main()
{
return 0;
}
gcc -S test.c
.file "test.c"
.def ___main; .scl 2; .type 32; .endef
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
call ___main
movl $0, %eax
leave
ret
你能解释一下这里发生了什么吗?这是我努力理解它。我使用了as
手册和我的最小x86 ASM知识:
.file "test.c"
是逻辑文件名的指令。.def
:根据文档“开始定义符号名称的调试信息”。什么是符号(函数名称/变量?)以及什么样的调试信息?.scl
:文档说“存储类可以标记符号是静态还是外部”。这是我从C中知道的静态和外部吗?什么是'2'?.type
:将参数“存储为符号表条目的类型属性”,我不知道。.endef
:没问题。.text
:现在这是有问题的,它似乎是一个叫做部分的东西,我已经读过它的代码所在,但文档并没有告诉我太多。.globl
“使符号对ld可见。”,手册非常明确。_main:
这可能是我的主要功能的起始地址(?)pushl_
:一个长(32位)推送,将EBP放在堆栈上movl
:32位移动。伪C:EBP = ESP;
andl
:逻辑与。 Pseudo-C:ESP = -16 & ESP
,我真的不明白这是什么意思。call
:将IP推送到堆栈(因此被调用的过程可以找回其路径)并继续__main
所在的位置。 (什么是__main?)movl
:这个零必须是我在代码末尾返回的常量。 MOV将此零置于EAX中。leave
:在ENTER指令(?)之后恢复堆栈。为什么?ret
:返回保存在堆栈中的指令地址感谢您的帮助!
答案 0 :(得分:55)
.file“test.c”
以。开头的命令。是汇编程序的指令。这只是说这是“file.c”,该信息可以导出到exe的调试信息。
.def ___main; .scl 2; .type 32; .endef伪指令
.def指令定义调试符号。 scl 2表示存储类2(外部存储类).type 32表示此sumbol是一个函数。这些数字将由pe-coff exe-format
定义___ main是一个被调用的函数,用于处理gcc需要的引导(它将执行诸如运行c ++静态初始化器和其他所需的内务处理之类的事情)。
.text
开始一个文本部分 - 代码就在这里。
.globl _main
将_main符号定义为global,这将使链接器和链接的其他模块可见。
.def _main; .scl 2; .type 32; .endef
与_main相同,创建调试符号,表明_main是一个函数。这可以由调试器使用。
_main:
开始一个新标签(它最终会有一个地址)。上面的.globl指令使这个地址对其他实体可见。
pushl %ebp
将旧帧指针(ebp寄存器)保存在堆栈上(因此当此函数结束时可以将其放回原位)
movl %esp, %ebp
将堆栈指针移动到ebp寄存器。 ebp通常被称为帧指针,它指向当前“帧”(通常是函数)内堆栈值的顶部,(通过ebp引用堆栈上的变量可以帮助调试器)
andl $ -16,%esp
使用fffffff0对堆栈进行修改,有效地将其与16字节边界对齐。访问堆栈上的对齐值比未对齐时快得多。所有这些前面的说明几乎都是标准的功能序言。
call ___main
调用___main函数,它将初始化gcc需要的东西。调用将推送堆栈上的当前指令指针并跳转到___ main
的地址movl $0, %eax
将0移至eax寄存器,(0返回0;)eax寄存器用于保存stdcall调用约定的函数返回值。
假
离开指令几乎是
的简写movl ebp,esp popl ebp
即。它“取消”在函数开始时完成的操作 - 将帧指针和堆栈恢复到以前的状态。
保留
返回调用此函数的人。它将从堆栈中弹出指令指针(相应的调用指令将放置在那里)并跳转到那里。
答案 1 :(得分:12)
这里列出了一个非常类似的练习:http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax
你已经找到了大部分内容 - 我只会为重点和补充做出补充说明。
__main
是GNU标准库中的子程序,负责各种启动初始化。它对于C程序来说并不是绝对必要的,但是只要C代码与C ++链接就是必需的。
_main
是您的主要子程序。由于_main
和__main
都是代码位置,因此它们具有相同的存储类和类型。我还没有挖出.scl
和.type
的定义。您可以通过定义一些全局变量来获得一些启发。
前三条指令是设置堆栈帧,这是子程序的工作存储的技术术语 - 大部分是本地和临时变量。按ebp
可保存调用者堆栈帧的基础。将esp
放入ebp
会设置堆栈框架的基础。 andl
将堆栈帧对齐到16字节边界,以防堆栈上的任何局部变量需要16字节对齐(对于x86 SIMD指令需要对齐,但对齐确实加速普通类型,如{{1 s和int
s。
此时,您通常希望float
在内存中向下移动,以便为局部变量分配堆栈空间。你的esp
没有,所以gcc不会打扰。
对main
的调用对主入口点是特殊的,通常不会出现在子例程中。
其余的就像你猜测的那样。寄存器__main
是在二进制规范中放置整数返回码的地方。 eax
取消堆栈帧,leave
返回给调用者。在这种情况下,调用者是低级C运行时,它将执行额外的魔术(如调用ret
函数,设置进程的退出代码并要求操作系统终止进程。
答案 2 :(得分:5)
关于和l $ -16,%esp
因此它将掩盖ESP的最后4位(btw:2 ** 4等于16)并保留所有其他位(无论目标系统是32位还是64位)。
答案 3 :(得分:4)
除了andl $-16,%esp
之外,这也有效,因为将低位设置为零将始终将值%esp
向下调整,并且堆栈在x86上向下增长。
答案 4 :(得分:2)
我没有所有的答案,但我可以解释我所知道的。
函数使用 ebp
在其流中存储esp
的初始状态,引用传递给函数的参数在哪里以及它自己的局部变量在哪里。函数的第一件事就是保存执行ebp
的给定pushl %ebp
的状态,它对于调用函数至关重要,而不是用它自己当前的堆栈位置替换它{{1做esp
。此时将movl %esp, %ebp
的最后4位归零是GCC特定的,我不知道为什么这个编译器会这样做。如果不这样做就可以。现在终于我们开始营业,ebp
,谁是__main?我不知道......也许更多GCC特定的程序,最后你的main()做的唯一的事情是,使用call ___main
和movl $0, %eax
将返回值设置为0,这与执行{相同{1}}恢复leave
州,然后movl %ebp, %esp; popl %ebp
完成。 ebp
弹出ret
并继续从该点开始的线程流,无论它在哪里(因为它的main(),这个ret可能会导致一些处理程序结束的内核程序)。
大部分都是关于管理堆栈。我写了一篇关于如何使用堆栈的详细教程,解释为什么所有这些都是有用的。但它用葡萄牙语......