【问题标题】:What does this assembly language code mean?这个汇编语言代码是什么意思?
【发布时间】:2013-07-22 17:56:39
【问题描述】:

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

这些是什么意思?

【问题讨论】:

  • 哪一部分给你带来了麻烦?我们无法解释每一行,如果你处于那个水平,你需要从阅读一本书开始,而不是直接跳入难以理解的东西。告诉我们您了解哪些部分以及您不了解哪些部分。
  • 您的示例中有几个主要概念在逐行解释指令的过程中会丢失。如果您对组装说明知之甚少或根本不了解,您应该从基础开始获取一本书或一些在线资料。一旦您熟悉了指令的工作原理,就可以涵盖管理堆栈帧、寄存器/内存和函数调用约定等更大的概念。
  • 其实我对汇编语言了解不多,只知道一些mov,add之类的。我最好选择你的意见。
  • 可能只是堆栈帧操作的东西让你难过。从 movl $4, 28(%esp) 开始,它应该看起来很熟悉(与源代码比较时)。
  • 您可以通过以下命令获得一个显示组合 C 和汇编语言的列表文件:gcc -c -g -Wa,-a,-ad [other GCC options] foo.c &gt; foo.lst 组合列表将在 foo.lst 中。 Source

标签: assembly x86


【解决方案1】:

让我们分解一下:

.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

【讨论】:

  • 当然,在现代操作系统上,每个可执行文件(大致上)都是一个共享对象。所以rodata 不仅在使用共享库的不同程序之间共享,如果多个实例碰巧同时运行,它也在同一程序的不同实例之间共享。查看 John Levine 关于链接和加载主题的精美书籍以获取更多详细信息。 iecc.com/linker
  • 是的。我知道。谢谢。我应该用我的语言更准确。感谢您指出。
  • 如果您想知道生成的所有程序集由哪些高级源代码负责,您可以随时使用以下命令进行检查:gcc -Wa,-adhln=delta.lst -g delta.c
  • 编译时常量的措辞被“优化为程序集内在函数”留下了一些不足之处。 “内在”具有特定的技术含义(C 中类似函数的东西,如 _mm_popcnt_u32),在这里不合适。不过,我没有任何完全准确的好主意。一些常量将在指令流中作为立即数结束,而常量传播会将其他常量转换为已删除的分支或完全展开的小循环。因此,仅仅说“对于不编译为立即数的常量”是不准确的。
  • .cfi_* 噪音是堆栈展开信息,由异常处理程序和调试器使用。即使没有-g,它也会生成,并且不会被strip 删除。异常处理程序需要能够通过使用-fomit-frame-pointer(默认为-O2)编译的函数展开堆栈。请注意,每次%esp 更改时都有一个 .cfi 指令,以及指示哪个寄存器在哪个点保存的指令。但是对于阅读 asm 以了解它的作用的人类来说,它们只是噪音。 stackoverflow.com/questions/38552116/…
【解决方案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;

【讨论】:

  • 是的,更多的教科书使用英特尔格式。我开始以英特尔格式进行汇编编程(25 年前)。今天,我发现 AT&T 格式更容易阅读,除了一种情况:疯狂的 386 间接寻址模式。无论如何,[ecx*2+12]12(,ecx,2) 更容易阅读。
  • 啊,这很有趣,是 AT&T 的风格更有效率,还是你最终比 intel 更关注他们的语法?也许随着时间的推移,我也会开始更喜欢 AT&T 的方式......
  • 我发现它更有效率,而且更容易阅读。 DWORD PTRs 会在一段时间后以 COBOL 的方式与您联系,而 x86-64 只会让情况变得更糟。
  • .global 通常来自 nasm 还是 Intel 语法?我总是将 .global 与 nasm 一起使用(在 Intel 模式下;它甚至支持 at&t 吗?无论如何我都不会使用它),如果我正在练习 at&t 语法,我会将 GAS 与 .globl 一起使用.无论汇编程序如何,我是否应该始终将.global 与英特尔一起使用,将.globl 与 at&t 一起使用?为了最大的便携性?
  • @RastaJedi:NASM 和 GAS 使用完全不同的指令,尽管当 GAS 处于 .intex_syntax noprefix 模式时它们支持相似的助记符和语法。 NASM 的global 指令没有.。显然,您应该始终在 NASM 中使用global symbol_name。对于 GAS,请使用 .globl,因为这就是 gcc 的作用。 IDK 如果它甚至支持.global
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多