我是一名学生,刚开始学习汇编语言。为了更好地理解它,我只是在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
这些是什么意思?
答案 0 :(得分:27)
让我们分解一下:
.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
这是一个“调用帧信息”指令。它指示汇编程序发出矮调格式的调试信息。
pushl %ebp
这是汇编代码中函数“prologue”的标准部分。它保存了“ebp”寄存器的当前值。 “ebp”或“base”寄存器用于在函数内存储堆栈帧的“基础”。 “esp”(“堆栈指针”)寄存器可以在函数内调用函数时发生变化,而“ebp”保持不变。始终可以相对于“ebp”访问函数的任何参数。通过ABI调用约定,在函数可以修改EBP寄存器之前,它必须保存它,以便在函数返回之前恢复原始值。
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
我没有详细研究过这些,但我认为它们与DWARF调试信息有关。
movl %esp, %ebp
GAS使用AT&amp; T语法,这与英特尔手册使用的语法相反。这意味着“设置ebp等于esp”。这基本上为函数的其余部分建立了“基指针”。
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
这也是该功能的epilouge的一部分。这会对齐堆栈指针,然后从中减去足够的空间来容纳函数的所有本地。
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
答案 1 :(得分: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;