🎯 什麼情境該想到我

當你要決定「用靜態還是動態連結」,或遇到「執行期找不到 .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,各自有獨立的資料段副本,互不影響;同一行程的兩個執行緒則共享。

🔧 靜態連結體積太大時

  1. -ffunction-sections / -fdata-sections(MSVC 是 /Gy)讓每個函式/變數各自成段,連結器就能只挑用到的(p138)。代價:編譯連結變慢、段數暴增。
    — ⚠️ 書中只講了編譯器端把段切碎,沒講連結器端的死碼回收。實務上要自己補 -Wl,--gc-sections
  2. strip(或 -Wl,-s 去除所有符號/-Wl,-S 只去除錯符號)。書中說 strip 後檔案常只剩一半或更小(p272)。
  3. 極端案例作為量感:同一支程式靜態連結 glibc 約 538KB,換成書中手寫的 Mini CRT 只有 5083 bytes(p456–457)。

🧩 弱符號:讓庫可裁剪、可覆寫

__attribute__((weak)) 讓庫提供的預設實作可被使用者的強符號覆寫;弱引用則讓「少連某個 .o 程式仍可連結,只是少了功能」(p117–119)。詳見 工具-符號決議與重定位

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意

  • 動態庫的「執行期才報錯」最難查;用工具(ldd/依賴檢視)先確認依賴齊全與版本相符。
  • 不要跨庫邊界交換資源:不能在 A 庫 newmalloc 而在 B 庫 deletefree(分屬不同的堆)、不能跨庫傳 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 definedLNK4098 defaultlib conflicts

🔗 相關工具