GCC在x86,win32上的空程序的汇编输出

时间:2009-08-22 21:28:18

标签: c gcc x86 gas tdm-mingw

我编写空程序来惹恼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:返回保存在堆栈中的指令地址

感谢您的帮助!

5 个答案:

答案 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

  • 32位:十六进制中的-16等于十六进制表示中的0xfffffff0
  • 64位:十六进制中的-16等于十六进制表示中的0xfffffffffffffff0

因此它将掩盖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 ___mainmovl $0, %eax将返回值设置为0,这与执行{相同{1}}恢复leave州,然后movl %ebp, %esp; popl %ebp完成。 ebp弹出ret并继续从该点开始的线程流,无论它在哪里(因为它的main(),这个ret可能会导致一些处理程序结束的内核程序)。

大部分都是关于管理堆栈。我写了一篇关于如何使用堆栈的详细教程,解释为什么所有这些都是有用的。但它用葡萄牙语......