在C、C++语言中,将源代码文件转换成可执行文件涉及多个步骤,主要包括预处理、编译、汇编和链接。
源代码(Source code)文件
源代码文件包含了一系列人类可读的计算机语言指令。在 C 语言规范中,源代码文件通常以 .c
为拓展名;而在 C++ 语言规范中,源代码文件通常以 .cpp
为拓展名。
预处理(Preprocessing)
预处理是编译过程的第一步。在这个过程中,预处理器处理源代码文件中以 #
开头的指令。这些指令包括头文件 #include
、宏定义 #define
替换、条件编译 #ifdef
等。预处理的结果是一个 “拓展源代码” 文件,通常以 .i
为拓展名。具体来说,“拓展源代码” 是在原始源代码的基础上,展开所有的宏,插入所有头文件的内容,处理所有的条件编译。
假设有如下的C++源代码文件 “main.cpp” :
|
|
预处理后的代码将不包含 “#include” 和 “#define” 指令,而是包含了 “iostream” 的全部内容并替换 “PI” 的值:
|
|
编译(Compilation)
编译器将预处理后的代码转换为目标平台的汇编语言。这一步涉及语法分析、语义分析与优化等。编译器输出的是汇编代码,通常以 .s
为拓展名,这些汇编指令是平台相关的,表示如何在特定的硬件上执行程序。编译后的汇编代码可能看起来像这样(以x86平台为例):
|
|
汇编(Assembly)
汇编器将汇编代码转换为机器代码,即二进制指令,这些指令可以由计算机的 CPU 直接执行。汇编器生成的是对象文件(Object file),通常以 .o
(Unix/Linux系统)或 .obj
(Windows系统)为拓展名。对象文件包含了编译后的代码的机器语言版本,但这些代码还未进行地址绑定。对象文件是二进制文件,通常不可读,但它包含了函数和数据的二进制表示。
为什么汇编阶段不完成地址绑定? 在汇编阶段,代码被转换成机器可执行的指令。然而,指令中引用的函数和全局变量的具体内存地址通常尚未确定。这些引用被暂时标记为“待定”,直到链接阶段才会被解析和绑定。出现这种情况的原因如下:
- 多模块程序:一个大型程序通常由多个源代码文件组成。经过 “预处理、编译、汇编” 这三个步骤后,每个源代码文件会被独立地转换成对象文件。在这个过程中,汇编器仅能处理当前模块中的符号(如函数、全局变量等),而无法得知其他模块中定义的符号的具体地址。因此,在汇编时,这些跨模块的引用会被标记为待定。
- 库连接:程序可能依赖于多个外部库,这些库在编译和汇编时也是被独立处理的。因此,库函数的具体内存地址在汇编阶段是未知的,汇编器无法将这些地址填入生成的指令中。这些库函数的地址同样会在链接阶段由链接器解析和绑定。
对于上述问题,汇编器和链接器通过重定位解决这一问题:
- 重定位记录:汇编器生成的对象文件中包括机器指令和一些符号表,其中符号表记录了未解析的符号及其引用位置。对于同一模块内定义和使用的符号(例如局部变量),汇编器可以直接将地址填入机器指令中;而对于跨模块引用的符号(例如其他源文件或库中的函数和全局变量),汇编器会生成 “重定位表”,标记这些符号为未解析,并记录它们在目标文件中的位置。
- 链接:在链接阶段,链接器将所有对象文件和库文件合并成一个可执行文件。链接器首先扫描所有目标文件的符号表,构建全局符号表(Global Symbol Table),该表包含了所有模块中定义的符号及其相对地址。接着,链接器通过重定位表,将未解析的符号地址替换为它们在最终可执行文件中的实际地址。链接器将所有模块的代码和数据段重定位到最终的内存地址空间中,这样每个符号都能正确地被引用。
链接(Linking)
链接器处理一个或多个对象文件,解决外部符号引用,可能还会链接运行时库等。链接器将所有对象文件及所需的库文件集合在一起,生成最终的可执行文件(在Windows上是 .exe
,在Unix/Linux上通常没有扩展名)。链接器生成的可执行文件包含了所有必要的程序代码、数据和运行时库的引用,这些都已经是准备好可以被操作系统加载和执行的格式。最终的可执行文件实现了特定平台上的 “一次编译,多次运行”。