这段汇编语言代码是什么意思?

6

我是一名学生,刚开始学习汇编语言。为了更好地理解它,我写了一个简短的C程序并将其转换为汇编语言。然而令我惊讶的是,我一点也没理解。

代码如下:

#include<stdio.h>

int main()
{
    int n;
    n=4;
    printf("%d",n);
    return 0;
}

相应的汇编语言为:

.file   "delta.c"
    .section    .rodata
.LC0:
    .string "%d"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    movl    $4, 28(%esp)
    movl    $.LC0, %eax
    movl    28(%esp), %edx
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section    .note.GNU-stack,"",@progbits

这些是什么意思?

11
你有什么部分不理解吗?我们不能解释每一个句子,如果你处于这个水平,需要先阅读一本书,而不是直接跳进难以理解的内容。告诉我们哪些部分你理解了,哪些部分你不理解。 - Gilles 'SO- stop being evil'
5
你的例子中有几个重要概念,如果逐行解释指令,这些概念可能会被忽略。如果你对汇编指令没有或很少了解,可以找一本书或一些基础教材进行学习。一旦你熟悉了指令的工作方式,就可以学习更大的概念,如管理堆栈框架、寄存器/内存和函数调用惯例。 - lurker
其实我对汇编语言不是很了解,只知道一些 mov、add 等指令。我最好选择你的意见。 - Nithin Jose
可能只是栈帧操作让你感到沮丧。从movl $4, 28(%esp)开始,它应该看起来非常熟悉(与源代码进行比较)。 - Brian Knoblauch
如果你想给自己一个好处,就从MIPS汇编语言开始吧。 - Raphael
显示剩余3条评论
2个回答

32

让我们分解一下:

.file   "delta.c"

编译器使用此信息告知你汇编代码来自哪个源文件。对于汇编器而言,这并不重要。
.section    .rodata

这是一个新章节的开始。"rodata"是指"只读数据"部分的名称。该部分最终会将数据写入可执行文件,并映射到只读数据中。可执行镜像的所有".rodata"页面最终会被加载镜像的所有进程共享。

通常情况下,源代码中任何无法优化为汇编内核函数的"编译时常量"最终都会存储在"只读数据"部分中。

.LC0:
    .string "%d"

.LC0"部分是一个标签。它提供了一个符号名称,引用文件中它之后出现的字节。在这种情况下,“LC0”表示字符串“%d”。GNU汇编器使用以“L”开头的标签作为“局部标签”的约定。这具有技术含义,主要是对编写编译器和链接器的人感兴趣。在这种情况下,编译器用它来引用特定对象文件中私有的符号。在这种情况下,它代表一个字符串常量。

.text

这是一个新的章节。"text" 章节是存储可执行代码的目标文件中的章节。

.globl  main

".global"指令告诉汇编器将其后面的标签添加到生成的目标文件“导出”的标签列表中。这基本上意味着“这是一个应该对链接器可见的符号”。例如,在“C”中的“非静态”函数可以被任何声明(或包含)兼容函数原型的c文件调用。这就是为什么您可以#include stdio.h,然后调用printf的原因。当编译任何非静态C函数时,编译器会生成一个声明指向函数开头的全局标签的汇编代码。与不应链接的字符串字面值相对比,它们是“局部”符号。
.type   main, @function

我不确定GAS(GNU汇编器)如何处理“.type”指令。然而,这个指令告诉汇编器,“main”标签是可执行代码,而不是数据。

main:

这定义了你“main”函数的入口点。
.LFB0:

这是一个“本地标签”,它指向函数的起始位置。

    .cfi_startproc

这是一个“调用帧信息”指令。它指示汇编器发出dwarf格式的调试信息。

    pushl   %ebp

这是汇编代码中“前奏”的标准部分。它保存了当前的“ebp”寄存器值。“ebp”或“基址”寄存器用于在函数内存储堆栈帧的“基址”。而“esp”(“堆栈指针”)寄存器可能会在函数内调用其他函数时发生变化,但“ebp”保持不变。任何函数参数都可以相对于“ebp”访问。按照ABI调用约定,在函数修改EBP寄存器之前,必须先保存它,以便在函数返回之前恢复原始值。

    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8

我还没有详细调查过这些内容,但我相信它们与DWARF调试信息相关。

    movl    %esp, %ebp

GAS使用AT&T语法,与英特尔手册使用的语法相反。这意味着“将ebp设置为esp”。这基本上为函数的其余部分建立了“基指针”。

    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp

这也是函数结尾的一部分。它会对齐堆栈指针,并从中减去足够的空间以容纳函数中的所有本地变量。

    movl    $4, 28(%esp)

这将32位整数常量4加载到堆栈帧中的一个插槽中。

    movl    $.LC0, %eax

这将"%d"字符串常量加载到eax中。
    movl    28(%esp), %edx

这将从栈中偏移量为28的位置加载存储的值"4"到edx寄存器。很可能你的代码是在关闭优化的情况下编译的。

    movl    %edx, 4(%esp)

接着将数值4移动到堆栈中,在调用printf时,它需要在正确的位置。

    movl    %eax, (%esp)

这将把字符串“%d”加载到调用printf时需要的堆栈位置。
    call    printf

这个调用了printf。

    movl    $0, %eax

这将eax设置为0。考虑到接下来的指令是“leave”和“ret”,这相当于在C代码中返回0。 EAX寄存器用于保存函数的返回值。

    leave

这个指令清理调用框架。它将ESP设置回EBP,然后从修改后的堆栈指针中弹出EBP。与下一个指令一样,这是函数结尾部分的一部分。

    .cfi_restore 5
    .cfi_def_cfa 4, 4

这是更多关于DWARF的东西

    ret

这是实际的返回指令,它从函数中返回。

    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section    .note.GNU-stack,"",@progbits

2
当然,在现代操作系统上,每个可执行文件(在第一近似下)都是共享对象。因此,“rodata”不仅在使用共享库的不同程序之间共享,而且在同一程序的多个实例之间共享,如果多个实例恰好同时运行。有关链接和加载的更多详细信息,请查看John Levine的精美著作。http://www.iecc.com/linker/ - Pseudonym
是的,我知道这个。谢谢。我应该在我的语言上更加精确。感谢您指出这一点。 - Scott Wisniewski
1
如果您想知道高级源代码生成的所有汇编代码的责任,您可以始终使用以下命令进行检查:gcc -Wa,-adhln = delta.lst -g delta.c - Puttaraju
编译时常量被称为“优化为汇编内置函数”,这种说法并不太合适。“内置函数”有一个特定的技术含义(在C语言中类似于函数的东西,比如_mm_popcnt_u32),在这里并不适用。虽然我没有完全准确的好主意,但有些常量最终会成为指令流中的立即数,而常量传播则会将其他常量转换为已删除的分支或完全展开的小循环。因此,仅仅说“对于不能编译为立即数的常量”是不准确的。 - Peter Cordes
.cfi_* 噪音是堆栈展开信息,被异常处理程序和调试器使用。即使没有 -g,它也会生成,并且不会被 strip 删除。对于异常处理程序能够通过使用 -fomit-frame-pointer(在 -O2 时为默认值)编译的函数展开堆栈,这是必需的。请注意,每次 %esp 改变时都有一个 .cfi 指令,以及指示哪个寄存器在哪个点上保存的指令。但对于阅读汇编以查看其功能的人来说,它们只是噪音。https://dev59.com/n1kT5IYBdhLWcg3wefUn - Peter Cordes

2

对我而言,英特尔的语法更易于阅读。学习如何生成英特尔语法有助于更好地理解C程序;

gcc -S -masm=intel file.c

在Windows中,你的C程序变成了:
    .file   "file.c"
    .intel_syntax noprefix
    .def    ___main;    .scl    2;  .type   32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "%d\0"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB13:
    .cfi_startproc
    push    ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    mov ebp, esp
    .cfi_def_cfa_register 5
    and esp, -16
    sub esp, 32
    call    ___main
    mov DWORD PTR [esp+28], 4
    mov eax, DWORD PTR [esp+28]
    mov DWORD PTR [esp+4], eax
    mov DWORD PTR [esp], OFFSET FLAT:LC0
    call    _printf
    mov eax, 0
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE13:
    .ident  "GCC: (rev2, Built by MinGW-builds project) 4.8.1"
    .def    _printf;    .scl    2;  .type   32; .endef

编译器选项在Ubuntu和Windows上应该相同。

除了疯狂的标签外,这更像是我在教科书中读到的汇编语言。

这是一个观察它的方式;

    call    ___main

    mov DWORD PTR [esp+28], 4  
    mov eax, DWORD PTR [esp+28]              ; int n = 4;

    mov DWORD PTR [esp+4], eax 
    mov DWORD PTR [esp], OFFSET FLAT:LC0
    call    _printf                          ; printf("%d",n);

    mov eax, 0
    leave                                    ; return 0;

1
是的,更多的教科书使用Intel格式。我在25年前开始学习汇编语言时就是用Intel格式的。今天,除了一个例外情况:疯狂的386间接寻址模式,我发现AT&T格式更容易阅读。无论如何,[ecx*2+12]12(,ecx,2)更容易阅读。 - Pseudonym
哦,那很有趣,是因为AT&T的风格更具生产力,还是你只是更多地接触了他们的语法而已?也许在未来,我也会开始更喜欢AT&T的方式。 - James
我发现这样更有效率,而且通常更容易阅读。DWORD PTR 会让你感觉像在写 COBOL 一样,而 x86-64 只会让情况变得更糟。 - Pseudonym
“.global” 是来自 nasm 还是 Intel 语法?我总是在 nasm 中使用“ .global”(在 Intel 模式下;它是否支持 AT&T?如果支持,我也不会使用它),如果我练习 AT&T 语法,我会使用 GAS 和“ .globl”。无论使用哪种汇编器,我都应该始终使用“ .global”与 Intel 一起使用,“ .globl”与 AT&T 一起使用吗?以实现最大的可移植性? - RastaJedi
@RastaJedi:NASM和GAS使用完全不同的指令,尽管当GAS处于.intex_syntax noprefix模式时,它们支持类似的助记符和语法。 NASM的“global”指令没有“.”。 显然,在NASM中应始终使用global symbol_name。 对于GAS,请使用.globl,因为这是gcc所做的。 我不知道它是否甚至支持.global - Peter Cordes
是的,我是指 global,不好意思。但是 GAS 支持 .global,我有时候也会用到它。很好,我只使用每个变量一个修饰符。 - RastaJedi

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