🎯 什麼情境該想到我
當你在新機器編譯、拿到舊機器執行卻報 version 'GLIBC_2.xx' not found;當你要決定「升級這個 .so 會不會炸掉線上服務」;當你自己要發佈一個函式庫、不知道該怎麼命名與打包;或當兩個相依都要同一個庫的不同版本時。
⚙️ 怎麼用
一、版本號的語義(libname.so.x.y.z)
| 位置 | 名稱 | 語義 |
|---|---|---|
| x | 主版本號 | 重大升級,不同主版本完全不相容。舊程式必須改寫重編,或系統保留舊版 |
| y | 次版本號 | 增量升級,只加新介面、不改舊的。高次版本向後相容低次版本 |
| z | 發佈版本號 | 只修 bug/改效能,完全相容,雙向可換 |
二、SO-NAME:程式依賴的是介面,不是檔名
SO-NAME = 檔名去掉次版本號與發佈版本號,只保留主版本號。
libfoo.so.2.6.1 → SO-NAME 是 libfoo.so.2(p258)。
系統為每個庫建一個叫 SO-NAME 的軟連結,指向該主版本下最新的實體檔;連結時寫進 .dynamic 的 DT_NEEDED 記的是 SO-NAME 而非完整檔名。所以增量升級只要換檔案 + 更新軟連結,不必保留一大堆舊版本。
「总之,SO-NAME 表示一个库的接口,接口不向后兼容,SO-NAME 就发生变化,这是基本的原则。」(p260)
建庫時務必給 -soname:
gcc -shared -fPIC -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 foo.c沒指定 -soname 的庫根本沒有 SO-NAME,ldconfig 對它完全無效(p270)——這是「檔案明明拷過去了卻還是找不到」的常見死因。
(例外:Glibc 自己不守這規則,用 libc-x.y.z.so 命名,但 SO-NAME 仍是 libc.so.6。)
三、什麼時候該升主版本號?ABI 相容性判準
書中直接列了表(p256):
| 動作 | 相容? |
|---|---|
| 增加一個導出符號 | ✅ 相容 |
| 刪除導出符號 | ❌ 不相容 |
| 增加或減少函式參數 | ❌ 不相容 |
| 改變被導出用到的 struct 記憶體佈局 | ❌ 不相容 |
| 改變函式語義(同名同簽名但行為變了) | ❌ 不相容 |
| 只修 bug、不改語義 | ✅ 相容 |
「要破坏一个共享库的 ABI 十分容易,要保持 ABI 的兼容却十分困难。」(p256)
C++ 更難:虛表佈局、模板實例化、多重繼承、名稱修飾、例外機制各家實作不同,標準沒有規範 ABI。書中給了 7 條注意事項後直接下結論:
「最重要的是,不要改变接口的任何部分或干脆不要使用 C++ 作为共享库接口!」(p257)
書中借《COM 本質論》的 StringFind.DLL 案例說明有多脆:只加一個私有成員變數,主程式 new 出來的還是舊版的 4 bytes,而庫裡的建構函式認為物件有 8 bytes——直接寫進不屬於自己的記憶體(p298–300)。
四、符號版本:SO-NAME 解決不了的那一半
SO-NAME 只比對主版本。在高次版本庫上編譯的程式拿到低次版本庫的機器上跑,動態連結器認為 SO-NAME 一致就放行,結果跑到一半才發現缺符號——書中叫它次版本號交會問題(Minor-revision Rendezvous Problem)(p261)。
解法是給每個導入/導出符號附一個版本標籤(GLIBC_2.5 這種)。關鍵設計:連結時記錄的不是庫當時的最新版本,而是這支程式實際用到的最小夠用版本;執行時比對,不滿足就直接拒絕啟動,而不是跑到一半炸掉(p261–262)。
./main: ./lib.so: version 'VERS_1.2' not found (required by ./main)
這就是現代最常見的 version GLIBC_2.34 not found 的完整解釋——也直接說明了為什麼常見對策是「在最舊的目標系統上編譯」、manylinux、musl、或靜態連結。
GCC 在 Solaris 方案上加了兩個擴充(p264–265):asm(".symver ...") 內嵌指令;以及允許同名符號在同一個庫裡多版本共存(printf@VERS_1.1 與 printf@VERS_1.2 各指向不同實作),這樣連改語義都不必動主版本號。
自己的庫要用的話:
gcc -shared -fPIC lib.c -Xlinker --version-script lib.ver -o lib.soversion script 裡的 local: *; 是範圍機制(Scoping),把其餘全域符號降級為區域符號——等於補上 C 語言缺乏的可見性控制。
五、找不到 .so 時的排查順序
動態連結器的查找順序是(p266–268):
LD_LIBRARY_PATH/etc/ld.so.cache- 預設目錄(
/lib、/usr/lib)
ld.so.cache 由 ldconfig 產生:遍歷預設目錄與 /etc/ld.so.conf 指定的目錄,建立/更新每個庫的 SO-NAME 軟連結,並收集成查找專用的快取檔。
→ 安裝或更新任何共享庫之後都必須跑 ldconfig。 沒有 root 權限時用 ldconfig -n <dir> 自己建軟連結。
FHS 三個目錄的分工:/lib 系統關鍵、/usr/lib 開發用、/usr/local/lib 第三方。
六、版本衝突的四種解法譜系
書中在 DLL HELL 一節整理的四條路,概念完全通用(p301–304):
- 靜態連結 —— 終極解法,代價是放棄動態連結的一切好處。
- 防覆蓋 —— 阻止未授權程式覆蓋系統庫(Windows File Protection)。
- 每個應用自帶一份依賴 —— 載入時先找自己目錄再找系統目錄。
- 強識別 + 多版本共存 —— Windows 的 Side-by-Side:用 name + version + processorArchitecture + publicKeyToken 組成「強檔名」,同名不同版本在
WinSxS下共存,靠 manifest 指定版本。
DLL HELL 的三種成因也值得記(p301):安裝程式用舊版覆蓋新版(最常見)、新版行為無意改變、新版引入新 bug。
🧪 我實際套用的紀錄
- 2026-08-02:(待填)
⚠️ 注意
- 「完全向後相容」實務上做不到——這是 DLL HELL 的第二種成因,也是為什麼「只是修個 bug」的升級照樣會出事。
- 跨庫邊界不要交換資源:不能在 A 庫裡
new/malloc而在 B 庫裡delete/free(分屬不同的堆)、不能跨庫傳FILE*、不能共享 locale。書中原話:「如果不违反上述规则,可能会使程序发生莫名其妙的错误并且很难发现」(p375)。這條在今天的 Rust/C++ FFI、Python C extension 依然是頭號地雷。 - 容器不是免死金牌。把
libfoo.so.1.2.3拷進映像檔卻沒建 SO-NAME 軟連結、或忘了跑ldconfig,一樣找不到。 - 第四節那套「強識別 + 多版本共存」,就是今天
node_modules巢狀安裝、Go module 的 semantic import versioning、乃至容器化走的同一條路——問題沒有變,只是換了包裝。
🔗 相關工具
- 工具-動態連結與位址無關程式碼 —— 上游:符號怎麼被解析、為什麼會有全域符號介入這種撞名問題
- 工具-靜態庫與動態庫 —— 決策層:讀完這張卡的成本,回去重新評估要不要用動態連結
- 工具-資料編碼與演進 —— 同構問題:那張講資料格式的前後相容,這張講二進位介面的前後相容,判準思路一致