从源文件到可执行文件(编译之路)

被面试官狠狠拷打的亡羊补牢罢了

编译过程

C++程序从源代码到可执行文件需要经过四个主要阶段:

  1. 预处理(Preprocess)
  2. 编译(Compilation)
  3. 汇编(Assembly)
  4. 链接(Linking)

预处理(Preprocess)

这一步由预处理器完成,对源程序中的伪指令(以#开头的指令)和特殊符号进行处理。主要工作包括:

  • 将所有的#define删除,并进行宏展开
  • 处理所有条件编译指令,如#if#ifdef#ifndef#else#elif#endif
  • 处理#include预编译指令,将被包含的头文件内容插入该位置,递归处理多重包含
  • 处理其他宏指令,包括#error#warning#line#pragma
  • 删除所有注释(C++的//,C语言的/**/),通常用空格替代
  • 添加行号和文件标识,便于调试和错误定位
  • 保留所有的#pragma编译器指令
  • 处理预定义的宏:如__DATE____FILE__

预处理后的文件通常有.i.ii(C++)扩展名,可以通过g++ -E source.cpp -o source.i命令查看预处理结果。

编译(Compilation)

这一步由编译器完成,对预处理后的文件进行分析并生成汇编代码:

  • 语法分析:在词法分析的基础上将单词序列组合成各类语法短语,如”程序”、”语句”、”表达式”等,判断源程序在结构上是否正确。

  • 语义分析:对结构正确的源程序进行上下文相关性质的审查,进行类型检查并报告错误。

  • 中间代码生成:编译器生成平台无关的中间表示(IR),如LLVM IR或GCC的GIMPLE。

  • 代码优化:对中间代码进行优化,如常量折叠、死代码消除、循环优化、内联函数、尾递归优化等。

  • 目标代码生成:将优化后的中间代码转换为特定平台的汇编代码。

编译后生成的汇编文件通常有.s扩展名,可以使用g++ -S source.cpp -o source.s命令生成。

汇编(Assembly)

由汇编器完成,将汇编代码转变成机器可执行的二进制代码,并生成目标文件。汇编器执行的具体工作包括:

  • 将汇编指令的助记符转换为操作码
  • 将标签转换为地址
  • 生成符号表和重定位信息
  • 为各段分配空间

目标文件通常采用ELF格式(Linux)或PE格式(Windows),包含:

  • 代码段(.text):存放程序指令
  • 数据段(.data):存放已初始化的全局变量和静态变量
  • BSS段(.bss):存放未初始化的全局变量和静态变量

可以使用g++ -c source.cpp -o source.o命令生成目标文件,目标文件是二进制格式,可以用objdump -d source.o查看反汇编内容。

链接(Linking)

由链接器完成,主要解决多个文件之间符号引用的问题。编译时编译器只对单个文件进行处理,如果该文件引用了其他文件中的符号(如全局变量或库函数),这些符号的地址无法确定,需要链接器将所有目标文件链接在一起才能确定最终地址。

链接器执行的具体工作:

  • **符号解析(Symbol Resolution)**:将每个符号引用与其定义匹配
  • **重定位(Relocation)**:调整代码中的地址引用,使其指向正确的最终位置
  • 处理弱符号和强符号
  • 合并各个段:将所有目标文件的相同段合并

链接分为两种方式:

  • 静态链接:库代码直接复制到可执行文件中,生成独立但较大的可执行文件
  • 动态链接:可执行文件仅包含对共享库的引用,运行时加载库,节省空间但依赖库文件

常见链接错误:

  • 未定义引用(Undefined reference)
  • 重复定义(Multiple definition)
  • 符号版本不匹配

实用命令示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 一步完成所有阶段
g++ source.cpp -o program

# 分步执行每个阶段
g++ -E source.cpp -o source.i # 预处理
g++ -S source.i -o source.s # 编译
g++ -c source.s -o source.o # 汇编
g++ source.o -o program # 链接

# 编译优化选项
g++ -O0 source.cpp -o program # 无优化(调试用)
g++ -O2 source.cpp -o program # 常用优化级别
g++ -O3 source.cpp -o program # 最高优化级别

之所以要经过预处理、编译、汇编这么一系列步骤才生成目标文件,是因为在每一阶段都有相应的优化技术,只有在各个阶段分别优化才能生成最高效的机器指令。如果直接从源程序生成目标文件,可能会失去很多代码优化的机会。

无论采用静态链接还是动态链接,最终都会生成一个可以在计算机上执行的可执行程序。