C语言的hello world程序汇编输出的每一行是什么意思?

44

我运行了gcc -S:

int main()
{
printf ("Hello world!");
}

我得到了这段汇编代码:

        .file   "test.c"
        .section        .rodata
.LC0:
        .string "Hello world!"
        .text
.globl main
        .type   main, @function
main:
        leal    4(%esp), %ecx
        andl    $-16, %esp
        pushl   -4(%ecx)
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %ecx
        subl    $20, %esp
        movl    $.LC0, (%esp)
        call    printf
        addl    $20, %esp
        popl    %ecx
        popl    %ebp
        leal    -4(%ecx), %esp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
        .section        .note.GNU-stack,"",@progbits

我很想了解这个输出的含义。是否有人可以提供一些指导,帮助理解这个输出?或者如果有人能在每行/每组行旁边标注注释,解释它们的作用,那就太好了。

3个回答

81

下面是具体步骤:

        .file   "test.c"

原始源文件名称(用于调试器)。

        .section        .rodata
.LC0:
        .string "Hello world!"

在“.rodata”段中包含了一个以零结尾的字符串(“ro”代表“只读”:应用程序可以读取数据,但对它进行任何写入尝试都会触发异常)。

        .text

现在,我们将东西写入“ .text”部分,这是代码所在的位置。

.globl main
        .type   main, @function
main:

我们定义了一个名为“main”的函数,并使其在全局可见(其他对象文件将能够调用它)。

        leal    4(%esp), %ecx

我们将值4+%esp(其中%esp是栈指针)存储在寄存器%ecx中。

        andl    $-16, %esp

为了使内存访问地址是16的倍数,%esp 被轻微修改。 对于一些数据类型(对应于C语言中的doublelong double浮点格式),当内存访问在16的倍数地址上时,性能更好。 这在这里并不是真正必要的,但如果没有使用优化标志(例如-O2 ...),编译器往往会产生相当多的通用无用代码(即在某些情况下可能有用但在这里不是)。

        pushl   -4(%ecx)

这个有点奇怪:此时,地址-4(%ecx)处的单词是在andl操作之前位于堆栈顶部的单词。 代码检索该单词(顺便说一句,它应该是返回地址),并再次将其推送。 这样有点类似于从具有16字节对齐堆栈的函数调用获取的内容。 我猜这个push是参数复制序列的遗留物。 由于函数已经调整了堆栈指针,因此必须复制函数参数,这些参数可以通过旧的堆栈指针值访问。 在这里,除了函数返回地址外,没有其他参数。 请注意,此单词不会被使用(再次强调,这是没有进行优化的代码)。

        pushl   %ebp
        movl    %esp, %ebp

这是标准的函数序言:我们保存%ebp(因为我们即将修改它),然后设置%ebp指向堆栈帧。此后,%ebp将用于访问函数参数,使%esp再次可用。(是的,没有参数,所以对该函数无用。)

        pushl   %ecx

我们保存了 %ecx 寄存器(在函数退出时我们需要它,以恢复 andl 执行之前的 %esp 值)。

        subl    $20, %esp

我们在栈上保留了32个字节(记住栈是向下增长的)。这些空间将用于存储printf()函数的参数(这是过度设计,因为只有一个参数,它将使用4个字节[即指针大小])。

        movl    $.LC0, (%esp)
        call    printf

我们“推送”参数给 printf() 函数(即确保 %esp 指向一个包含参数的单词,这里是常量字符串在 rodata 部分中的地址 $.LC0)。然后我们调用 printf() 函数。

        addl    $20, %esp

printf() 返回时,我们会释放为参数分配的空间。这个 addl 与上面的 subl 相互抵消。

        popl    %ecx

我们恢复了%ecx寄存器(它被压在上面);printf()可能已经修改了它(调用约定描述函数可以在退出时不还原哪些寄存器;%ecx是其中一个寄存器)。

        popl    %ebp

函数结尾:这将恢复%ebp(对应于上面的pushl %ebp)。

        leal    -4(%ecx), %esp

我们将%esp恢复到其初始值。该操作码的效果是将值%ecx-4存储在%esp中。%ecx在第一个函数操作码中设置。这将取消对%esp的任何更改,包括andl

        ret

函数退出。

        .size   main, .-main

这将设置main()函数的大小:在汇编过程中的任何时刻,“.”是“我们当前添加内容的地址”的别名。如果在此处添加了另一条指令,它将放在由“.”指定的地址上。因此,在此处,“.-main”是函数main()代码的确切大小。 .size指令指示汇编器将该信息写入对象文件。

        .ident  "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"

GCC喜欢留下其操作的痕迹。这个字符串最终会成为目标文件中的一种注释。连接器将删除它。

        .section        .note.GNU-stack,"",@progbits

一段特殊的代码区域,在这里GCC写入的代码可以适用于一个非可执行堆栈。这是正常情况。可执行堆栈只在某些特殊用途中需要(不是标准C语言)。在现代处理器上,内核可以创建一个非可执行堆栈(堆栈上的数据如果被尝试作为代码执行将触发异常)。对于某些人来说,这被视为“安全功能”,因为把代码放在堆栈上是利用缓冲区溢出的常见方式。通过这个代码区域,可执行文件将被标记为“兼容非可执行堆栈”,内核会愉快地提供这样的服务。


1
subl $20, %esp 我们在堆栈上保留了32字节(请记住,堆栈向“下”增长)。 难道不应该是20字节而不是32字节吗? - Ragtime
1
@Ragtime 不是的,因为$20是用16进制表示的(16进制的20等于32)。 - Karambit

31

以下是对@Thomas Pornin回答的补充内容:

  • .LC0 代表局部常量,例如字符串字面值。
  • .LFB0 代表局部函数的开始位置。
  • .LFE0 代表局部函数的结束位置。

这些标签的后缀是一个数字,从0开始递增。

这是gcc汇编器的规定约定。


4
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ecx
    subl    $20, %esp

这些指令在你的C程序中不进行比较,它们总是在每个函数的开头执行(但这取决于编译器/平台)。

    movl    $.LC0, (%esp)
    call    printf

这个代码块对应于你的printf()调用。第一条指令将它的参数(一个指向“hello world”的指针)放到堆栈上,然后调用函数。

    addl    $20, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret

这些指令与第一个块相反,它们是一些堆栈操作。总是被执行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接