编译过程四个阶段:
-
预处理(Preprocessing):
- 宏替换:预处理器根据
#define
指令替换宏名为宏定义的内容。 - 文件包含:预处理器处理
#include
指令,插入被包含文件的内容。 - 条件编译:预处理器根据
#if
、#ifdef
等条件指令决定哪些代码应该被编译,哪些应该被忽略。 - 注释移除:预处理器删除代码中的注释,使得它们不会被编译器处理。
- 输出:经过预处理后的文件通常具有
.i
后缀,内容是纯 C 代码,已处理所有宏和包含文件。
- 宏替换:预处理器根据
-
编译(Compilation):
- 语法检查:编译器分析源代码,检查其语法是否正确。
- 生成中间代码:编译器将源代码转换为中间表示(IR,Intermediate Representation),这种表示形式独立于目标机器。
- 代码优化:编译器优化中间代码,使其更高效,例如消除冗余代码、进行常量折叠等。
- 生成汇编代码:编译器将优化后的中间代码转换为汇编代码,这些代码是针对特定架构的指令集编写的。
- 输出:汇编代码通常存储在
.s
文件中。
-
汇编(Assembly):
- 翻译成机器码:汇编器将汇编代码翻译为机器码,也就是处理器可以直接执行的二进制指令。
- 生成目标文件:汇编器生成的目标文件是一个包含二进制代码和数据的文件,这些文件通常带有
.o
或.obj
扩展名。 - 输出:目标文件包含了可执行代码的片段,但还不能独立执行,因为它们可能依赖于其他代码或库。
-
链接(Linking):
- 符号解析:链接器将不同目标文件中的符号(如函数名、变量名)解析为具体的内存地址。
- 库链接:链接器将程序需要的库函数(如标准库函数)链接到最终的可执行文件中。
- 重定位:链接器调整目标文件中代码和数据的内存地址,以确保它们在最终的可执行文件中位于正确的位置。
- 生成二进制文件:最终生成的二进制文件通常采用 ELF(Executable and Linkable Format)格式,在 UNIX 系统中使用广泛。
ELF 文件包含的主要部分:
-
.text 段:
- 存放程序的机器指令,也称为代码段。CPU 从这里读取指令并执行。
-
.data 段:
- 存放已初始化的全局变量。这些变量在程序启动时会加载到内存中,并保留在程序运行期间。
-
.bss 段:
- 存放未初始化的全局变量。这些变量在加载时被初始化为零,不占用文件空间,但在程序运行时会占用内存。
-
符号表:
- 存放程序中的函数名、变量名等符号信息。符号表在调试或链接过程中使用,但不包含在最终的可执行文件中。
-
重定位信息:
- 在程序链接过程中,链接器使用重定位信息将符号地址调整到正确的内存位置。
反汇编 ELF 文件
ELF 文件包含二进制数据和指令,无法直接作为文本文件查看。因此,需要使用反汇编工具将其中的机器码转换回汇编代码,这种过程称为反汇编。
使用 objdump -D
进行反汇编
objdump
是一个用于显示二进制文件信息的工具,可以用来反汇编 ELF 文件。
objdump -D hello
这个命令将 hello
可执行文件中的所有段(包括 .text
段)反汇编并输出为人类可读的汇编代码。
-D
选项:表示反汇编整个文件中的所有代码段,而不仅仅是.text
段。- 输出说明:
- 地址:每条汇编指令对应的内存地址。
- 机器码:汇编指令对应的二进制机器码。
- 汇编指令:与机器码对应的汇编语言指令。
具体链接: objdump命令介绍-CSDN博客