前面說的詞法分析和語法分析,確實是編譯器前端 (Front End) 最核心的兩個部分。但前端的工作還沒有結束。
編譯器各階段劃分
一個完整的編譯器通常可以分為三個部分:前端、中端 (Middle End)、后端 (Back End)。
前端 (Front End)
- 核心職責: 理解源代碼。它負責處理與源語言 (Source Language) 相關的所有事情。
- 輸入: 源代碼文件 (e.g.,
program.c
)。 - 輸出: 中間表示 (Intermediate Representation, IR)。這是一種獨立于具體硬件平臺的、類似于“通用匯編語言”的代碼表示。
- 主要工作流程:
- 詞法分析 (Lexical Analysis): 源代碼 -> Token 序列。
- 語法分析 (Syntax Analysis): Token 序列 -> 語法樹 (Syntax Tree)。
- 語義分析 (Semantic Analysis): 這是前端的第三個,也是非常重要的一個步驟。
“語義分析”屬于前端。
- 語義分析做什么?
- 語法分析只管“結構對不對”,不管“意思對不對”。比如
int a = "hello";
這句話,從語法結構上看(類型 標識符 = 字面量;),是完全正確的。 - 但從**語義(意思)**上看,它是錯誤的,因為你不能把一個字符串賦值給一個整型變量。
- 語義分析就是負責檢查這些“意思”層面的錯誤,主要包括:
- 類型檢查: 運算符兩邊的類型是否匹配?函數調用的參數類型和數量是否正確?
- 作用域分析: 變量在使用前是否已經聲明?是否存在重復定義的變量?
- 等等…
- 語義分析通常會向語法樹中添加額外的信息(比如每個節點的類型),形成一個“帶注解的語法樹”或直接生成中間表示。
- 語法分析只管“結構對不對”,不管“意思對不對”。比如
中端 (Middle End) / 優化器 (Optimizer)
- 核心職責: 優化代碼。它在一種獨立于具體機器的層面上,對代碼進行等價變換,讓它運行得更快、占用空間更小。
- 輸入: 前端生成的中間表示 (IR)。
- 輸出: 優化后的中間表示 (IR)。
- 主要工作:
- 生成中間代碼: 將語法樹(或帶注解的語法樹)轉換成一種更線性的、類似匯編的中間表示(如三地址碼)。
- 代碼優化: 這是編譯技術中最復雜、最精華的部分之一。包括但不限于:
- 刪除無用代碼 (Dead Code Elimination)
- 常量折疊 (Constant Folding): 比如把
2 + 3
在編譯時直接算成5
。 - 循環優化 (Loop Optimizations)
- 函數內聯 (Function Inlining)
所以,“生成中間代碼”和“代碼優化”屬于中端。
后端 (Back End)
- 核心職責: 生成目標代碼。它負責處理與目標機器 (Target Machine) 相關的所有事情。
- 輸入: (優化后的)中間表示 (IR)。
- 輸出: 目標機器的匯編代碼或機器碼 (e.g.,
program.s
orprogram.o
)。 - 主要工作:
- 指令選擇 (Instruction Selection): 將通用的中間代碼指令,翻譯成特定CPU的指令(比如 x86 的
mov
,add
指令)。 - 寄存器分配 (Register Allocation): 決定哪些變量應該放在CPU的高速寄存器里,哪些放在內存里。這是一個對性能至關重要的步驟。
- 指令調度 (Instruction Scheduling): 調整指令的順序以適應CPU的流水線特性,避免等待。
- 最終代碼生成: 輸出匯編代碼或二進制文件。
- 指令選擇 (Instruction Selection): 將通用的中間代碼指令,翻譯成特定CPU的指令(比如 x86 的
所以,“生成目標程序”屬于后端。
前端和后端的主要區別 (The “Why”)
這種“前-中-后”三段式的設計是現代編譯器的基石,其最大的好處是解耦 (Decoupling) 和復用 (Reuse)。
-
前端 (Source-Dependent, Target-Independent)
- 只關心源語言: C++ 的前端和 Swift 的前端完全不同。
- 不關心目標機器: C++ 的前端不在乎最終代碼是跑在 Intel CPU 上還是 ARM CPU 上。它只生成一份通用的 IR。
-
后端 (Source-Independent, Target-Dependent)
- 不關心源語言: 后端拿到的是通用的 IR,它根本不知道這份 IR 最初是由 C++ 還是 Swift 寫成的。
- 只關心目標機器: 針對 Intel x86 的后端和針對 ARM 的后端是完全不同的。
這種設計的巨大優勢:
想象一下,我們要支持 M 種編程語言(C++, Swift, Rust, …)和 N 種目標CPU架構(x86, ARM, RISC-V, …)。
- 如果沒有前后端分離: 我們需要為每一種語言和每一種CPU的組合都寫一個完整的編譯器。總共需要 M * N 個編譯器。
- 有了前后端分離: 我們只需要為每種語言寫一個前端(共 M 個),為每種CPU寫一個后端(共 N 個)。然后像搭積木一樣,把它們通過統一的中間表示 (IR) 連接起來。總共只需要 M + N 個組件。
這就是像 LLVM 這樣的現代編譯器架構如此成功的原因。Clang 是 C/C++/Objective-C 的前端,swiftc
是 Swift 的前端,它們都會生成 LLVM IR。然后,LLVM 提供了強大的中端優化器和針對各種CPU的后端,來完成剩下的工作。