57

我编写空程序来惹恼stackoverflow编码器,不是。我只是在探索 gnu 工具链。

现在以下内容对我来说可能太深了,但为了继续空程序传奇,我已经开始检查 C 编译器的输出,即 GNU 所消耗的东西。

gcc version 4.4.0 (TDM-1 mingw32)

测试.c:

int main()
{
    return 0;
}

gcc -S 测试.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: 回到栈中保存的指令地址

感谢您的帮助!

4

5 回答 5

57

.文件“test.c”

以 . 开头的命令。是对汇编器的指令。这只是说这是“file.c”,该信息可以导出到exe的调试信息中。

.def ___main; .scl 2; . 类型 32; .endef

.def 指令定义了一个调试符号。scl 2 表示存储类 2(外部存储类)。type 32 表示这个 sumbol 是一个函数。这些数字将由 pe-coff exe 格式定义

___main 是一个调用的函数,负责处理 gcc 需要的引导(它将执行诸如运行 c++ 静态初始化程序和其他所需的内务管理之类的事情)。

.text

开始一个文本部分 - 代码在这里。

.globl _main

将 _main 符号定义为全局符号,这将使其对链接器和链接的其他模块可见。

.def        _main;  .scl    2;      .type   32;     .endef

与 _main 相同,创建调试符号,说明 _main 是一个函数。这可以由调试器使用。

_主要的:

开始一个新标签(它会结束一个地址)。上面的 .globl 指令使该地址对其他实体可见。

pushl       %ebp

将旧的帧指针(ebp 寄存器)保存在堆栈中(以便在此函数结束时将其放回原处)

movl        %esp, %ebp

将堆栈指针移动到 ebp 寄存器。ebp 通常被称为帧指针,它指向当前“帧”内的栈顶值(通常是函数),(通过 ebp 引用栈上的变量可以帮助调试器)

和 $-16, %esp

并将堆栈与 fffffff0 有效地对齐在 16 字节边界上。访问堆栈上的对齐值比未对齐时快得多。所有这些前面的指令几乎都是标准的功能序言。

call        ___main

调用 ___main 函数来初始化 gcc 需要的东西。call 会将当前指令指针压入栈中,并跳转到 ___main 的地址

movl        $0, %eax

将 0 移至 eax 寄存器,(返回 0 中的 0;) eax 寄存器用于保存 stdcall 调用约定的函数返回值。

离开

离开指令几乎是简写

movl     ebp,esp
popl     ebp

即它“撤消”在函数开始时所做的事情——将帧指针和堆栈恢复到以前的状态。

ret

返回给调用此函数的人。它将从堆栈中弹出指令指针(相应的调用指令将放置在那里)并跳转到那里。

于 2009-08-22T23:34:12.277 回答
12

这里概述了一个非常相似的练习: http ://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

您已经了解了大部分内容——我将做一些额外的笔记来强调和补充。

__main是 GNU 标准库中的一个子程序,负责各种启动初始化。这对于 C 程序不是绝对必要的,但在 C 代码与 C++ 链接的情况下是必需的。

_main是你的主要子程序。由于_main__main都是代码位置,它们具有相同的存储类和类型。.scl我还没有挖掘出定义.type。您可以通过定义一些全局变量来获得一些启发。

前三个指令是建立一个堆栈帧,这是一个子程序工作存储的技术术语——大部分是局部变量和临时变量。推送ebp保存调用者堆栈帧的基础。放入我们espebp堆栈框架的基础。将andl堆栈帧对齐到 16 字节边界,以防堆栈上的任何局部变量需要 16 字节对齐(对于 x86 SIMD 指令需要对齐,但对齐确实加速了普通类型,例如ints 和floats.

此时,您通常希望esp在内存中向下移动,为局部变量分配堆栈空间。你main没有,所以 gcc 不会打扰。

对主入口点的调用__main是特殊的,通常不会出现在子例程中。

其余的如你所料。寄存器eax是在二进制规范中放置整数返回码的地方。 leave撤消堆栈帧并ret返回给调用者。在这种情况下,调用者是低级 C 运行时,它将执行额外的魔法(如调用atexit()函数、设置进程的退出代码并要求操作系统终止进程。

于 2009-08-22T22:22:33.933 回答
5

关于那个andl $-16,%esp

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

因此它将屏蔽 ESP 的最后 4 位(顺便说一句:2**4 等于 16)并保留所有其他位(无论目标系统是 32 位还是 64 位)。

于 2009-08-22T22:20:27.683 回答
4

进一步andl $-16,%esp来说,这是可行的,因为将低位设置为零将始终%esp 向下调整值,并且堆栈在 x86 上向下增长。

于 2009-08-23T04:31:34.380 回答
2

我没有所有答案,但我可以解释我所知道的。

ebp函数使用它来存储esp其流动期间的初始状态,对传递给函数的参数在哪里以及它自己的局部变量在哪里的引用。函数做的第一件事是保存给定ebpdoing的状态pushl %ebp,这对调用的函数至关重要,然后用它自己当前的堆栈位置espdoing替换它movl %esp, %ebp。此时将最后 4 位归零ebp是 GCC 特定的,我不知道为什么这个编译器会这样做。不做也行。现在终于开始做生意了,call ___main__main 是谁?我也不知道...也许更多的 GCC 特定程序,最后你 main() 做的唯一一件事,将返回值设置为 0movl $0, %eaxleave这与做movl %ebp, %esp; popl %ebp恢复ebp状态,然后ret完成。ret从该点弹出eip并继续线程流,无论它在哪里(作为它的 main(),这个 ret 可能会导致一些处理程序结束的内核过程)。

其中大部分都是关于管理堆栈的。前段时间我写了一篇关于如何使用堆栈的详细教程,解释为什么要制作所有这些东西会很有用。但它的葡萄牙语...

于 2009-08-22T22:00:19.657 回答