在C、C++语言中,将源代码文件转换成可执行文件涉及多个步骤,主要包括预处理、编译、汇编和链接。


源代码(Source code)文件

源代码文件包含了一系列人类可读的计算机语言指令。在 C 语言规范中,源代码文件通常以 .c 为拓展名;而在 C++ 语言规范中,源代码文件通常以 .cpp 为拓展名。


预处理(Preprocessing)

预处理是编译过程的第一步。在这个过程中,预处理器处理源代码文件中以 # 开头的指令。这些指令包括头文件 #include、宏定义 #define 替换、条件编译 #ifdef 等。预处理的结果是一个 “拓展源代码” 文件,通常以 .i 为拓展名。具体来说,“拓展源代码” 是在原始源代码的基础上,展开所有的宏,插入所有头文件的内容,处理所有的条件编译。

假设有如下的C++源代码文件 “main.cpp” :

1
2
3
4
5
6
7
8
// main.cpp
#include <iostream>
#define PI 3.1415926

int main() {
    std::cout << "PI value is " << PI << std::endl;
    return 0;
}

预处理后的代码将不包含 “#include” 和 “#define” 指令,而是包含了 “iostream” 的全部内容并替换 “PI” 的值:

1
2
3
4
5
6
7
// main.i
// iostream 内容展开
// 例如 std::ostream, std::cout等的定义
int main() {
    std::cout << "PI value is " << 3.1415926 << std::endl;
    return 0;
}

编译(Compilation)

编译器将预处理后的代码转换为目标平台的汇编语言。这一步涉及语法分析、语义分析与优化等。编译器输出的是汇编代码,通常以 .s 为拓展名,这些汇编指令是平台相关的,表示如何在特定的硬件上执行程序。编译后的汇编代码可能看起来像这样(以x86平台为例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.section .rodata 
.LC0: 
    .string "PI value is %f\n" 
.text 
.globl main 
main: 
    pushq %rbp 
    movq %rsp, %rbp 
    subq $16, %rsp 
    movss $0x40490fdb, -4(%rbp) // PI value as float 
    movq $LC0, %rdi 
    leaq -4(%rbp), %rsi 
    call printf 
    movl $0, %eax 
    leave 
    ret

汇编(Assembly)

汇编器将汇编代码转换为机器代码,即二进制指令,这些指令可以由计算机的 CPU 直接执行。汇编器生成的是对象文件(Object file),通常以 .o(Unix/Linux系统)或 .obj(Windows系统)为拓展名。对象文件包含了编译后的代码的机器语言版本,但这些代码还未进行地址绑定。对象文件是二进制文件,通常不可读,但它包含了函数和数据的二进制表示。

为什么汇编阶段不完成地址绑定? 在汇编阶段,代码被转换成机器可执行的指令。然而,指令中引用的函数和全局变量的具体内存地址通常尚未确定。这些引用被暂时标记为“待定”,直到链接阶段才会被解析和绑定。出现这种情况的原因如下:

  • 多模块程序:一个大型程序通常由多个源代码文件组成。经过 “预处理、编译、汇编” 这三个步骤后,每个源代码文件会被独立地转换成对象文件。在这个过程中,汇编器仅能处理当前模块中的符号(如函数、全局变量等),而无法得知其他模块中定义的符号的具体地址。因此,在汇编时,这些跨模块的引用会被标记为待定。
  • 库连接:程序可能依赖于多个外部库,这些库在编译和汇编时也是被独立处理的。因此,库函数的具体内存地址在汇编阶段是未知的,汇编器无法将这些地址填入生成的指令中。这些库函数的地址同样会在链接阶段由链接器解析和绑定。

对于上述问题,汇编器和链接器通过重定位解决这一问题:

  • 重定位记录:汇编器生成的对象文件中包括机器指令和一些符号表,其中符号表记录了未解析的符号及其引用位置。对于同一模块内定义和使用的符号(例如局部变量),汇编器可以直接将地址填入机器指令中;而对于跨模块引用的符号(例如其他源文件或库中的函数和全局变量),汇编器会生成 “重定位表”,标记这些符号为未解析,并记录它们在目标文件中的位置。
  • 链接:在链接阶段,链接器将所有对象文件和库文件合并成一个可执行文件。链接器首先扫描所有目标文件的符号表,构建全局符号表(Global Symbol Table),该表包含了所有模块中定义的符号及其相对地址。接着,链接器通过重定位表,将未解析的符号地址替换为它们在最终可执行文件中的实际地址。链接器将所有模块的代码和数据段重定位到最终的内存地址空间中,这样每个符号都能正确地被引用。

链接(Linking)

链接器处理一个或多个对象文件,解决外部符号引用,可能还会链接运行时库等。链接器将所有对象文件及所需的库文件集合在一起,生成最终的可执行文件(在Windows上是 .exe,在Unix/Linux上通常没有扩展名)。链接器生成的可执行文件包含了所有必要的程序代码、数据和运行时库的引用,这些都已经是准备好可以被操作系统加载和执行的格式。最终的可执行文件实现了特定平台上的 “一次编译,多次运行”。