源碼 (.c) │ 預處理 (cpp) ▼ 預處理輸出 (.i) │ 編譯 (cc1) ▼ 匯編代碼 (.s) │ 匯編 (as) ▼ 目標文件 (.o) │ 鏈接 (ld) ▼ 可執行文件 (a.out / 指定名)
預處理器(cpp)處理以 # 開頭的指令,產出純 C 代碼(無宏、無條件編譯、無頭文件引用):
#define PI 3.14 → 所有 PI 替換為 3.14(字符串常量內不替換)#ifdef DEBUG / #if / #elif / #endif 根據宏定義保留/刪除代碼塊#include <stdio.h> (系統路徑) / #include "my.h" (當前目錄優先) → 將頭文件內容原位展開__FILE__、__LINE__、__func__、__DATE__、__TIME__、__STDC__ 等替換為上下文值// 和 /* */GCC 命令:gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i
編譯器前端(cc1)將預處理輸出翻譯為目標架構的匯編代碼:
GCC 命令:gcc -S hello.i -o hello.s (輸出 .s) 或 gcc -S hello.c -o hello.s (隱式預處理)
匯編器將助記符翻譯為機器碼,生成 ELF/COFF/Mach-O 格式目標文件:
mov eax, 1 → B8 01 00 00 00.text:只讀代碼段(指令).data:已初始化全局/靜態變量.bss:未初始化全局/靜態變量(僅記錄大小).rodata:只讀數據(字符串常量、const 變量)GCC 命令:gcc -c hello.s -o hello.o 或 gcc -c hello.c -o hello.o (一鍵完成前三階段)
鏈接器將多個目標文件、庫文件、啟動代碼合併為可執行文件:
.a,ar 打包的 .o 集合)提取被引用的目標文件ld-linux.so、dyld)加載共享庫(.so/.dll/.dylib),完成符號綁定.text 合併),計算最終虛擬地址 (VMA)GCC 命令:gcc hello.o -o hello (隱式鏈接 libc、啟動文件 crt1.o 等)
| 階段 | GCC 選項 | 輸出後綴 | 對應工具 |
|---|---|---|---|
| 預處理 | -E | .i | cpp |
| 編譯 | -S | .s | cc1 |
| 匯編 | -c | .o | as |
| 鏈接 | (無) | a.out / -o | ld |
# 查看預處理展開 gcc -E -P hello.c | head -20 # 查看匯編代碼 (Intel 語法) gcc -S -masm=intel hello.c -o hello.s # 查看目標文件符號表 nm hello.o | grep -E 'T|D|B|U' # 查看動態依賴 ldd hello # 完整構建並查看鏈接腳本 gcc -Wl,--verbose hello.c -o hello 2>&1 | grep -A5 "GNU ld script"
| 現象 | 階段 | 原因 | 對策 |
|---|---|---|---|
| 宏未展開/重複定義 | 預處理 | 宏名衝突、頭文件缺少護衛 | #pragma once / #ifndef 護衛、-D 命令行定義 |
| 語法錯誤/類型不匹配 | 編譯 | 代碼錯誤、頭文件版本不一致 | 讀取錯誤行、檢查 -I 路徑順序 |
| 未定義引用 | 鏈接 | 缺少目標文件/庫、庫順序錯誤 | 補充 -l、調整庫順序(依賴在後)、-Wl,--no-as-needed |
| 多重定義 | 鏈接 | 頭文件定義變量/函數、靜態庫重複 | 頭文件只聲明、定義放 .c、static inline |
| 運行時找不到共享庫 | 加載 | LD_LIBRARY_PATH 缺失、RPATH 未設置 | export LD_LIBRARY_PATH、-Wl,-rpath,$ORIGIN/../lib |
-O0:無優化,調試首選-O2:發布默認,平衡體積速度-O3:激進優化(向量化、循環展開),可能增大體積-Os:優化體積(嵌入式首選)-flto:鏈接時優化,跨文件內聯、全局分析,需鏈接器支持(gold/lld)-fPIC:位置無關代碼,構建共享庫必需理解四大階段的輸入輸出、工具鏈分工、產物結構,是從「會寫代碼」進化為「懂構建系統」的關鍵一步。下次遇到離奇構建錯誤,請先問自己:這發生在哪一階段?