🎯 什麼情境該想到我
當你要決定「用靜態還是動態連結」,或遇到「執行期找不到 .so/.dll、版本衝突」時。
⚙️ 怎麼用(依需求選)
- 靜態庫(.a/.lib):連結時把庫的程式碼打進執行檔。
- ✅ 部署簡單(單一檔、無外部依賴)、版本鎖定。
- ❌ 執行檔大、庫更新要重新編譯整個程式、多程式共用時記憶體重複。
- 動態庫(.so/.dll):執行期才載入、多程式共享。
- ✅ 執行檔小、庫可獨立更新、記憶體共享。
- ❌ 部署要帶對版本、易遇「依賴地獄/版本衝突」、執行期才炸。
取捨:要部署省事/版本可控 → 靜態;要省空間/可熱更新庫 → 動態。容器時代常用靜態或把動態依賴一起打包。
📦 先搞清楚「庫」到底是什麼
「库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。」(PDF p77)
.a 就是 ar 把一堆 .o 打包再加索引而已:ar -t libfoo.a 列出內容、ar -x 全部解出來(Windows 用 lib /LIST)。書中量測 libc.a 約 2.8 MB、含約 1400 個目標檔(p142–144)。
關鍵:連結器連靜態庫的粒度是「目標檔」,不是「函式」。 用到 printf() 就整個 printf.o 被拉進來——這就是為什麼執行庫刻意做成一個函式一個 .o,把「連進來卻沒用到」的浪費壓到最小(p147–148)。
相依還是遞迴的:printf.o 依賴 stdout(在 stdio.o)與 vfprintf(在 vfprintf.o),書中示範手動連結必然失敗(p144–145)。所以 gcc -static --verbose 揭露的真實連結行長這樣(p145–147):
crt1.o crti.o crtbeginT.o your.o --start-group -lgcc -lgcc_eh -lc --end-group crtend.o crtn.o
這串 crt*.o 是什麼、順序為什麼不能亂,見 工具-程式啟動與執行庫。
💰 動態庫的真實成本(取捨表通常漏掉的那半)
| 成本 | 細節 |
|---|---|
| 執行速度 | 跨模組資料與函式都要先定位 GOT 再間接定址。書中兩個口徑:整體損失「大约在 5% 以下」(p209);另一處「靜態比動態快約 1%~5%」(p225) |
| 啟動時間 | 啟動時要做符號查找與重定位。.so 越多、導入符號越多,啟動越慢;延遲綁定只是把它推遲,沒有消滅 |
| PIC 的取捨 | 不加 -fPIC 只加 -shared → 走裝載時重定位,跑得快但程式碼段無法跨行程共享,動態連結最大的優勢就沒了(p214–215)。判斷指令:readelf -d x.so | grep TEXTREL,有輸出就不是 PIC(p222) |
| 錯誤延後 | 未定義符號可能在啟動時才爆,甚至延遲綁定下第一次呼叫該函式時才爆 |
反過來,動態庫的收益也不只是「省磁碟」——還包括減少實體頁面的換入換出、提高 CPU 快取命中率(p206–207)。
一條常被誤解的語義,值得單記(p222–224):
兩個行程用同一個
.so,各自有獨立的資料段副本,互不影響;同一行程的兩個執行緒則共享。
🔧 靜態連結體積太大時
-ffunction-sections/-fdata-sections(MSVC 是/Gy)讓每個函式/變數各自成段,連結器就能只挑用到的(p138)。代價:編譯連結變慢、段數暴增。
— ⚠️ 書中只講了編譯器端把段切碎,沒講連結器端的死碼回收。實務上要自己補-Wl,--gc-sections。strip(或-Wl,-s去除所有符號/-Wl,-S只去除錯符號)。書中說 strip 後檔案常只剩一半或更小(p272)。- 極端案例作為量感:同一支程式靜態連結 glibc 約 538KB,換成書中手寫的 Mini CRT 只有 5083 bytes(p456–457)。
🧩 弱符號:讓庫可裁剪、可覆寫
__attribute__((weak)) 讓庫提供的預設實作可被使用者的強符號覆寫;弱引用則讓「少連某個 .o 程式仍可連結,只是少了功能」(p117–119)。詳見 工具-符號決議與重定位。
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意
- 動態庫的「執行期才報錯」最難查;用工具(ldd/依賴檢視)先確認依賴齊全與版本相符。
- 不要跨庫邊界交換資源:不能在 A 庫
new/malloc而在 B 庫delete/free(分屬不同的堆)、不能跨庫傳FILE*、不能共享 locale。「如果不违反上述规则,可能会使程序发生莫名其妙的错误并且很难发现」(p374–375)。這條在今天的 Rust/C++ FFI、Python C extension 依然是頭號地雷。 - C++ 不適合當共享庫介面。 修飾規則、物件佈局、虛表、模板實例化、例外機制全算 ABI,標準又沒規範——換編譯器甚至換版本就連不起來。書中結論是「不要改变接口的任何部分或干脆不要使用 C++ 作为共享库接口」(p257)。要跨邊界就用純虛介面 +
extern "C"工廠函式。 - 靜態連結不只是體積差別。 書中給了一個硬證據:Windows 上靜態連結 CRT(
/MT)沒有DllMain,用CreateThread建執行緒時_tiddata無人釋放 → 每建一個執行緒漏一份,是教科書級的緩慢記憶體洩漏;動態連結 CRT(/MD)因為有DllMain就不會(p381–382)。解法是一律用_beginthread(ex)/_endthread(ex)。 - 一個工程裡所有目標檔與 DLL 應使用同一版本的 CRT(p374–375)。混用會出現
LNK2005 already defined或LNK4098 defaultlib conflicts。
🔗 相關工具
- 工具-共享庫版本與相容性 —— 選了動態庫之後,版本怎麼命名、什麼時候該升主版本、
GLIBC_2.xx not found怎麼解 - 工具-動態連結與位址無關程式碼 —— 動態庫執行期到底做了什麼:PIC、GOT/PLT、全域符號介入
- 工具-編譯連結與裝載 —— 上游:庫在哪一階段被連進來
- 工具-符號決議與重定位 —— 庫裡的符號怎麼被挑出來、連結順序為什麼重要
- 工具-資料編碼與演進 —— 同構問題:資料格式的前後相容 vs 二進位介面的前後相容