C/C++的编译过程基本相同,主要包括预处理、编译、汇编和链接四个阶段。下面通过gcc演示
1.预处理
预处理阶段是编译过程的第一步,主要处理源代码中的预处理器指令。这个阶段主要完成以下任务:
-
文件包含:
处理 #include 指令,将被包含文件的内容直接插入到当前文件中。例如:#include <stdio.h> 会被替换为 stdio.h 的实际内容 -
宏展开:
展开所有的宏定义,处理 #define 指令定义的宏。例如:#define MAX 100 会将代码中所有的 MAX 替换为 100 -
条件编译:
处理条件编译指令,如 #if, #ifdef, #ifndef, #else, #elif, #endif,根据条件选择性地包含或排除某些代码段 -
删除注释:
移除代码中的所有注释,包括单行注释 (//) 和多行注释 (/* */) -
行控制:
处理 #line 指令,可以修改编译器的行号计数和当前文件名 -
错误和警告:
处理 #error 和 #warning 指令,生成编译错误或警告信息 -
特殊符号处理:
处理预定义的特殊符号,如 FILE, LINE, DATE, TIME 等这些符号会被替换为相应的值 -
头文件保护:
处理头文件中的include保护符(通常使用 #ifndef, #define, #endif),防止同一个头文件被多次包含 -
Pragma 指令:
处理 #pragma 指令,这些指令通常用于控制编译器的行为 -
三元字符组替换:
将某些特殊的字符序列替换为单个字符。例如:"??" 可能被替换为 "^" -
连接相邻的字符串字面量:
将源代码中相邻的字符串字面量合并为一个
- 输入文件: .cpp, .cxx, .cc 等
- 输出文件: .i
- 例如: myfile.cpp -> myfile.i
g++ -E myfile.cpp > myfile.i
2.编译
编译阶段是将预处理后的源代码转换为汇编代码的过程。
-
词法分析:将源代码分解成一系列的标记,识别关键字、标识符、常量、运算符等
-
语法分析:根据语言的语法规则,将标记组织成抽象语法树检查代码是否符合语言的语法规则
-
语义分析:检查代码的语义正确性,类型检查,变量声明和使用检查,函数调用参数检查
-
中间代码生成:将AST转换为中间表示,如三地址码或四元式这种表示更接近机器码,但仍然与具体的机器架构无关
-
代码优化:对中间代码进行优化,以提高效率,常见优化包括:常量折叠、死代码消除、循环优化、内联展开等
-
目标代码生成:将优化后的中间代码转换为目标机器的汇编代码,这一步骤与具体的目标架构相关
-
符号表管理:维护一个符号表,记录变量、函数等标识符的信息。用于类型检查、作用域分析等
-
错误处理和诊断:检测并报告编译错误,生成警告信息
-
调试信息生成:如果启用了调试选项,生成调试信息(如行号对应关系)
-
特定语言特性处理:对于C++,还包括模板实例化、名称修饰(name mangling)等
-
内存布局决策:决定变量和数据结构在内存中的布局
-
寄存器分配:决定哪些变量放在寄存器中,哪些放在内存中
输出:
- 编译阶段的最终输出通常是汇编代码(.s文件)
g++ -S myfile.i > myfile.s
3.汇编
汇编阶段是将编译器生成的汇编代码转换为机器码的过程。
-
指令转换:
将汇编指令转换为对应的机器码,每条汇编指令通常对应一个或多个机器指令 -
符号解析:
将汇编代码中的符号(如标签、变量名)转换为内存地址,对于外部符号(如来自其他模块的函数),生成重定位信息 -
地址分配:
为代码和数据分配内存地址,这些地址可能是相对的,需要在链接阶段进行最终调整 -
生成目标文件:
创建目标文件(.o 或 .obj),包含机器码、数据和元数据,目标文件通常包含多个段(section),如代码段、数据段、只读数据段等 -
生成重定位表:
记录需要在链接阶段进行地址调整的位置 -
生成符号表:
创建一个符号表,列出所有定义和引用的符号,包括全局符号、局部符号、外部符号等 -
常量池处理:
将常量数据放入特定的段中 -
调试信息处理:
如果启用了调试选项,将调试信息嵌入到目标文件中 -
错误检查:
检查汇编代码中的语法错误,验证指令的合法性 -
架构特定优化:
可能进行一些特定于目标架构的优化 -
生成元数据:
包括目标文件格式信息、版本信息等
输出:
- 汇编阶段的输出是目标文件(.o 或 .obj)
g++ -c myfile.s > myfile.o
链接
链接阶段是编译过程的最后一步,它将多个目标文件和库文件组合在一起,生成最终的可执行文件或共享库。
-
符号解析:
解析所有外部符号引用,将每个符号引用与其定义匹配,检查是否有未解析的符号 -
地址和空间分配:
为每个段分配内存地址,合并相同类型的段,确定每个符号的最终内存地址 -
重定位:
调整代码和数据中的地址引用,用符号的实际地址替换占位符地址 -
库链接:
链接静态库(.a 或 .lib 文件),为动态库(.so 或 .dll 文件)添加必要的信息 -
生成启动代码:
添加程序入口点代码(如 main 函数的调用准备),添加初始化代码(如全局对象的构造函数调用) -
解决名称修饰:
特别是在C++中,处理由于函数重载等特性导致的复杂符号名 -
处理弱符号:
解决多重定义的弱符号 -
生成导出和导入表:
对于动态链接,生成必要的导出和导入信息 -
优化:
可能进行一些全局优化,如删除未使用的代码和数据 -
生成元数据:
添加程序头、节头等元数据,包含调试信息 -
错误检查:
检查符号冲突,验证所有必需的符号都已解析 -
生成可执行文件:
创建最终的可执行文件或共享库,设置正确的文件格式
输出:
- 链接阶段的输出通常是可执行文件(在Unix/Linux系统上没有扩展名,在Windows上是.exe)或共享库(.so 或 .dll)
g++ myfile.o -o myfile