首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

從零手寫 C 語言編譯器:最小可行編譯器實作全紀錄

     編輯:關於C語言

從零手寫 C 語言編譯器:最小可行編譯器實作全紀錄

動手寫一個編譯器,是理解計算機如何工作的最佳途徑。遵循 Abdulaziz Ghuloum 論文《An Incremental Approach to Compiler Construction》「從最小可用子集開始,逐步擴展」的哲學,本文記錄從 int main() { return 2; } 這個最簡單程式開始,一步步構建 babyc 編譯器的完整過程:詞法分析、語法分析、代碼生成、組裝鏈接、執行驗證。

階段 0:目標程式

// return_two.c
int main() {
    return 2;
}

階段 1:詞法分析

使用 Flex 定義 Token:

/* lexer.l */
%{
#include "parser.h"
%}

"{"     return '{';
"}"     return '}';
"("     return '(';
")"     return ')';
";"     return ';';
[0-9]+  { yylval = atoi(yytext); return NUMBER; }
"return" return RETURN;
"int"   return TYPE;
"main"  return IDENTIFIER;
[ \t\n]  ;  // 忽略空白
.       { fprintf(stderr, "Unknown char: %s\n", yytext); }

階段 2:語法分析

使用 Bison 定義文法:

/* parser.y */
%{
#include <stdio.h>
#include <stdlib.h>
void yyerror(const char *s) { fprintf(stderr, "%s\n", s); exit(1); }
%}

%token NUMBER RETURN TYPE IDENTIFIER

%start program

%%
program: function ;
function: TYPE IDENTIFIER '(' ')' '{' expression '}' ;
expression: RETURN NUMBER ';' ;
%%

階段 3:代碼生成(x86 32 位組合語言)

/* 目標輸出 out.s */
.text
.global _start
_start:
    movl $2, %ebx    # 返回值放入 %ebx (sys_exit 參數)
    movl $1, %eax    # 系統呼叫號 sys_exit = 1
    int $0x80        # 觸發中斷進入核心

階段 4:整合編譯器主程式

/* compiler.c 簡化版 */
int main(int argc, char **argv) {
    FILE *src = fopen(argv[1], "r");
    yyin = src;
    yyparse();  // 語法分析並觸發代碼生成動作
    fclose(src);
    return 0;
}

階段 5:編譯、組裝、鏈接、執行驗證

# 1. 編譯編譯器本身
flex lexer.l
bison -d parser.y
gcc lex.yy.c parser.tab.c compiler.c -o babyc

# 2. 使用 babyc 編譯目標程式
./babyc return_two.c
# 輸出 out.s

# 3. 組裝為目標檔 (32 位)
as out.s -o out.o --32

# 4. 鏈接為可執行檔
ld -m elf_i386 -s -o out out.o

# 5. 執行並檢查返回碼
./out
echo $?  # 輸出 2 → 成功!

擴展路線圖:從 babyc 到完整子集

步驟新增特性學習重點修改點
1任意返回值 return 42;立即數編碼、寄存器分配代碼生成
2一元運算 return ~1;運算符優先級、AST 節點語法分析、代碼生成
3變數定義與賦值符號表、棧幀佈局、記憶體存取詞法/語法/代碼生成
4二元運算 + - * /表達式樹、暫存器分配語法分析、代碼生成
5if 條件判斷控制流圖、標籤生成、跳轉指令語法分析、代碼生成
6while 迴圈迴圈標籤、條件跳轉語法分析、代碼生成
7函數呼叫呼叫慣例、參數傳遞、返回位址代碼生成、運行時
8陣列/結構體記憶體佈局、位移計算符號表、代碼生成

關鍵架構決策

  • 前端/後端分離:中間表示 (IR) 讓前端語言無關、後端機器無關。
  • AST 而非直接生成:抽象語法樹支援多輪優化、多目標代碼生成。
  • SSA 形式:靜態單賦值形式簡化資料流分析與優化。
  • 寄存器分配:圖著色演算法、線性掃描、Spill 處理。
  • 最佳化管線:常數傳播、死代碼消除、循環不變量外提、函數內聯。

推薦學習資源

  1. 論文:《An Incremental Approach to Compiler Construction》- Abdulaziz Ghuloum
  2. 書籍:《Crafting Interpreters》- Bob Nystrom (免費線上)
  3. 書籍:《Engineering a Compiler》- Cooper & Torczon
  4. 專案:《Writing a C Compiler》- Nora Sandler (網站/書籍)
  5. 工具:LLVC 開發套件、Cranelift、QBE (簡潔後端)

為什麼要寫編譯器?

  • 徹底理解原始碼如何變成機器指令
  • 掌握詞法、語法、語義、優化、代碼生成全流程
  • 建立對程式語言設計、型別系統、運行時機制的深層直覺
  • 能閱讀、修改、擴展現有編譯器(GCC、LLVM、Rustc 等)
  • 培養系統級工程思維:模組化、測試驅動、漸進式構建

不要害怕底層細節。從 return 2; 開始,每一步都能在幾分鐘內看到實際輸出。這條路徑雖長,但每一步都扎實、可驗證、可擴展。祝你編譯器之旅愉快!

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved