理解空main()的转换为汇编

时间:2009-05-05 03:07:28

标签: c gcc compiler-construction assembly

有人可以解释一下GCC正在为这段代码做些什么吗?什么是初始化?原始代码是:

#include <stdio.h>
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。

5 个答案:

答案 0 :(得分:14)

我应该在我的所有评论前面说,我仍然在学习。

我会忽略部分初始化。部分初始化的解释基本上我覆盖的其他所有内容都可以在这里找到: http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

ebp registerstack frame基指针,因此是BP。它存储指向当前堆栈开头的指针。

esp寄存器是堆栈指针。它保存堆栈顶部的内存位置。每次我们在堆栈上推送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

请注意,堆栈会向较低的地址增长,这一点在下面非常重要。

前两个步骤称为过程序言,或者更常见的是function prolog,它们准备堆栈以供局部变量使用。请参阅底部的程序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的源代码   揭示这可能是针对SIMD的   出现在“_main”中的说明   例程,仅在对齐的情况下运行   地址。因为我们的例程没有   包含SIMD指令,此行   是不必要的。

http://en.wikibooks.org/wiki/X86_Assembly/GAS_Syntax

步骤5到11似乎对我没有任何意义。我在谷歌上找不到任何解释。真正了解这些东西的人能否提供更深入的了解。我听说过这些东西用于C的异常处理。

步骤5,将主0的返回值存储在eax中。

步骤6和7我们在未知原因的情况下将十六进制加到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 epilogue。也就是说,函数的一部分将堆栈指针,esp和ebp返回到调用此函数之前的状态。

步骤14,将esp设置为ebp的值,将堆栈顶部移动到main调用之前的地址。然后它将ebp设置为指向我们在步骤1中保存在堆栈顶部的地址。

可以用以下说明替换:

mov  %ebp, %esp
pop  %ebp

步骤15,返回并退出该功能。

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:

  

功能必须做的第一件事   被称为程序序言。它   首先保存当前的基指针   (ebp)指令pushl%ebp   (记住ebp是用于的寄存器   访问函数参数和   局部变量)。现在它复制了   堆栈指针(esp)到基地   指针(ebp)与指令   movl%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

存在一个非常好的堆栈溢出线程,它回答了大部分问题。 Why are there extra instructions in my gcc output?

可以在此处找到有关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

这些资料都没有解释一切。

答案 1 :(得分:9)

以下是GCC编译的简单main()函数的逐步细分,包含大量详细信息:GAS Syntax (Wikipedia)

对于您粘贴的代码,说明细分如下:

  • 前四条指令(pushl through andl):设置一个新的堆栈框架
  • 接下来的五条指令(movl to sall):为eax生成一个奇怪的值,它将成为返回值(我不知道它是如何决定这样做的)
  • 接下来的两条指令(均为movl):将计算出的返回值存储在堆栈上的临时变量中
  • 接下来的两条指令(两个调用):调用C库初始化函数
  • leave指令:拆掉堆栈框架
  • ret指令:返回调用者(外部运行时函数,或者调用程序的内核函数)

答案 2 :(得分:4)

好吧,不太了解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工具的版本。

这是一个有用的链接。

http://unixwiz.net/techtips/win32-callconv-asm.html

答案 3 :(得分:4)

知道你正在使用的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   "f.c"
        .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

仍然有大量的装饰,我们仍然保存帧指针,移动它,并恢复它,这是完全没有意义的,但其余的代码是有道理的。

答案 4 :(得分:1)

看起来GCC似乎可以编辑main()以包含CRT初始化代码。我刚刚确认我从MinGW GCC 3.4.5获得与源文本完全相同的汇编列表。

我使用的命令行是:

gcc -S emptymain.c

有趣的是,如果我将函数的名称更改为qqq()而不是main(),我会得到以下程序集:

        .file   "emptymain.c"
        .text
.globl _qqq
        .def    _qqq;      .scl    2;      .type   32;     .endef
_qqq:
        pushl   %ebp
        movl    %esp, %ebp
        popl    %ebp
        ret

对于未启用优化的空函数更有意义。