5

我是一名学生,刚开始学习汇编语言。为了更好地理解它,我只是用 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

这些是什么意思?

4

2 回答 2

31

让我们分解一下:

.file   "delta.c"

编译器使用它来告诉您程序集来自的源文件。这对汇编程序没有多大意义。

.section    .rodata

这将开始一个新的部分。“rodata”是“只读数据”部分的名称。本节最终将数据写入可执行文件,该可执行文件将内存映射为只读数据。可执行映像的所有“.rodata”页面最终由加载该映像的所有进程共享。

通常,源代码中无法优化为汇编内在函数的任何“编译时常量”最终都将存储在“只读数据部分”中。

.LC0:
    .string "%d"

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

.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”或“base”寄存器用于存储函数内堆栈帧的“base”。虽然“esp”(“堆栈指针”)寄存器可以在函数内调用函数时发生变化,但“ebp”保持不变。函数的任何参数始终可以相对于“ebp”进行访问。根据 ABI 调用约定,在函数可以修改 EBP 寄存器之前,它必须保存它,以便在函数返回之前恢复原始值。

    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8

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

    movl    %esp, %ebp

GAS 使用 AT&T 语法,这与 Intel 手册使用的语法相反。这意味着“设置 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 代码中的“return 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
于 2013-07-23T05:07:56.100 回答
2

对我来说,intels 语法更容易阅读,学习如何生成 intels 语法有助于更好地理解 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;
于 2013-07-23T15:52:00.257 回答