编译过程

以C语言为例,编译成可执行文件一共要经历:预处理=>狭义编译=>汇编=>链接,最终成为可执行文件。

预处理

输入源代码文件以及其包含的头文件,由预处理器执行展开宏定义、处理,条件编译指令、将包含的头文件直接插入到指令位置,删除注释。

输出纯净的、宏已展开、注释已删除、头文件已包含的中间代码文件,通常为.i

编写一个简单的程序:

#include <stdio.h>
#define STRING "Program"

int
main() {
  // 打印一些内容
  printf("Hello world!\n");
  printf("%s\n", STRING);
  return 0;
}

使用gcc的-E选项生成中间代码:

gcc exam.c -E -o ./exam.i

他生成了非常长的代码

可以验证预处理器是直接把头文件替换进来的。而代码中类似# 5 "exam.c"的标记用于记录源代码的位置信息,便于调试器和错误诊断程序追踪代码位置。

编译(狭义编译)

词法分析

输入预处理后的源代码文件,有编译器执行词法分析,将字符流分解成有意义的词素(Token),例如:

int sum = a + b;

分解成int, sum, =, a, +, b等Token。

语法分析

根据语言的语法规则,将Token序列组合成抽象语法树(Abstract Syntax Tree),简称AST,表达了代码的结构和层次关系。

语义分析

检查程序的语义是否正确,例如变量是否声明、类型是否匹配等。

中间代码生成

将AST转换成一种独立于CPU架构的中间表示形式(intermediate Representation),即IR。常见的IR有三地址码、LLVM IR、Java字节码等,为了优化和转移成多种目标机器码。

优化

对IR进行处理,目的是在不改变程序行为的前提下,提高代码效率。包括:

  • 常量折叠:如计算3 + 58
  • 死代码消除:移除永远不会执行到的代码。
  • 循环优化:如循环展开。
  • 函数内联。
  • 寄存器分配:决定哪些变量存储在高速的CPU寄存器中。

输出优化后的中间代码。

汇编

输入IR,或直接由编译器生成的汇编代码。由汇编器as执行。将汇编指令一对一地转换成特定CPU架构的机器操作指令。处理伪汇编指令,如定义数据段.data,定义代码段.text,分配存储空间space等。最后解析符号、如函数名、变量名等,生成符号表。

输出目标文件,如.o,.obj,包含机器指令、全局变量、静态变量的初始值等、符号表、重定位信息。

链接

输入一个或多个.o文件夹+库文件(静态.a/.lib,动态.so/.dll)。由链接器进行符号解析、重定位、库处理,最输出可执行文件或库文件。


CPU如何识别指令

CPU执行程序的循环称为Fetch-Decode-Execute Cycle(取指-译码-执行周期)。

取指

CPU内部有一个寄存器叫程序计数器(Program Counter, PC),它保存着下一条要执行的指令的内存地址,CPU将PC中的地址发送到地址总线,内存控制器根据地址总线上的地址,找到对应的内存单元,将其存储的指令通过数据总线送回CPU。取回来的指令被放入指令寄存器IR。PC的值自动增加,指向下一条指令的地址。

译码指令

CPU的控制单元读取IR中的指令,控制单元包含一指令译码器,译码器分析指令 操作码部分,操作码唯一表示CPU应该执行什么操作,如ADD MOV JMP等。

根据操作码,译码器决定:

  • 操作的性质(算术、逻辑、数据传输、跳转等)。
  • 操作需要多少个操作数。
  • 操作数存放位置(寄存器、内部地址指令本身中的立即数)

译码器激活执行该操作所需要的CPU内部电路通路和控制信号,结果决定了下一个阶段(执行)需要做什么。

执行指令

CPU的算术逻辑单元(Arithmetic Logic Unit, ALU)或去他功能单元(FPU MMU)根据译码器产生的控制信号执行实际操作。

操作数可能从寄存器文件中读取,或者从内存中加载,ALU执行加减与或移位等操作,如果指令是JMP/CALL/RET/分支指令等跳转指令,可能会修改PC的值,从而改变下一条指令的位置。计算结果可能写回寄存器,或者通过数据总线写会内存。

上述的操作循环以极高的速度(GHz级别)不断重复,构成了CPU运行程序基础。

编译后的指令在物理层面上是什么

答:内存中的电荷状态

程序被操作系统加载到计算机的RAM中,RAM由无数的存储单元(通常是电容)组成,每一个单元可以存储一个bit的信息。

高电平电荷(通常代表1)或低电平电荷(通常代表0)的状态,就表示一个二进制位。

每条指令由多个bit组成。指令序列在RAM中是一系列连续存储单元的电荷状态。当CPU取指令时,PC寄存器的值,也就是一组触发器的电平状态被送到地址总线,也就是一组物理导线。

地址总线上的电平信号激活内存控制器和特定的存储单元。

被选中的内存单元的电荷状态被读出,转换为相应的电平信号,通过数据总线传回CPU。

这些电平信号进入CPU的指令寄存器IR,也是一组触发器的电平状态。

一组一组电平状态激活着CPU中的组件,从而执行指令。