Shared Library Versioning
相容性問題
使用 library 其中一個目標是不需要重新 compile 就能方便的升級程式的部份功能或者修正 bug。理想上,升級 library 只需要用新的檔案取代舊的。但是事情沒那麼美好,舊程式不見得能正常使用新 library,新舊程式間會有相容性問題。
依據舊程式能不能使用新 library,library 的更新分為相容更新與不相容更新。相容更新通常不修改 interface,可能只是修正 bug 或者加點新東西。不相容更新則改變了 interface,這時舊版程式無法正常使用新版 library。
library 以 binary 的形式發佈,代表 interface 是 binary 層級,也就是 ABI(Application Binary Layer),包含 call function 的 stack 結構、symbol 命名、memory 配置、參數的結構等等。
ABI 跟語言有很大關係,以 C 語言來說,只要跟 export 的 symbol(function 跟 variable)有關的修改,都會動到 ABI,例如增加 export 變數、增減 export 的 function 參數、修改 export 的資料結構(會改變 memory 配置,程式依舊配置使用新結構會踩壞記憶體)等等。至於 C++ 基本上是場災難,因為各家 compiler 甚至相同 compiler 的不同版本可能都對 C++ 的各種特性諸如 virtual function 等有不同實作方式,這會造成 ABI 不相容。C++ 有一些避免改到 interface 的原則及作法,例如 pimpl。
版號
既然有相容性問題,程式如何知道能不能使用新 library?系統必須有方法確保相容性、讓程式能正常執行,其中一個方法是以版號區分不同版本的 library,並讓程式指定它相容於哪些版本的 library。
在 Linux 系統中,shared library 的檔案命名規則為 libname.so.x.y.z
。最前面是字首 lib
,中間是 library 名稱,接著 .so
,最後跟著三個版號數字。x
表示 major versison number,y
是 minor version number,z
為 release version number。
major version number 代表 library 有重大升級,不同 major version number 的 library 是不相容的。使用舊版的程式必須要經過修改及重新 compile 才能使用新版 library。系統中需要保留舊版 library 以讓未更新至使用新版 library 的程式依然能正常執行。
minor version number 表示 library 是增量升級,也就是加一些新的 interface 並且保持原本的 symbol 在名稱與語意上皆不變,例如增加一個 function。在 major version number 相同的狀況下,高 minor version number 的 library 向後相容低 minor version number 的 library,使用舊版 library 的程式可以使用新版 library,例如使用 1.1.z
libary 的程式可以在 1.2.z
library 上正常執行。
release version number 是 library 的 bug 修正、效能改進等等,不添加新 interface 也不改動舊 interface。因此相同 major version number 以及 minor version number 的不同 release version 之間的 library 完全相容,依賴某個 release version 的程式可以使用其他任一 release version。
SO-NAME
library 有版號之後,程式如何指定使用的 library 版本?
在 Solaris 跟 Linux 裡採用 SO-NAME 命名機制,SO-NAME 是 library 版號只保留 major version number,去掉 minor version 以及 release version,例如 libfoo.so.3.2.1
的 SO-NAME 是 libfoo.so.3
。
Linux 系統會建立檔名為 SO-NAME 並指向 major version 相同、最新 minor 及 release version 的 library 實體檔案的 soft link,例如 /usr/lib/libcln.so.6
指向 /usr/lib/libcln.so.6.0.4
。這麼做的好處是系統中的程式只需要記錄 library 的 SO-NAME,再藉由 soft link 就能使用該 major version 最新的 library。
記錄 SO-NAME 提供了一些彈性,讓 library 有一定的升級空間,不會讓程式只限定於某版本的 library。同時又能知道何種狀況是大幅度的更新,系統不應該自動使用新 major version 的 library,需保留舊版 library 讓使用的程式可以正常執行。當然,話是這麼說,版號只是個規則,還是需要由開發者判斷新版是否向後相容以決定如何升版號。
ELF 在 .dynamic
以 SO-NAME 記錄所需的 shared library:
1 | $ readelf -d main |
Linux 指令 ldconfig
用來更新上述 library 的 soft link,它會掃過所有預設 shared library directory,例如 /lib
、/usr/lib
等,更新所有 soft link,將其指向目前系統中最新的 library 或為新 library 建立 soft link。安裝或更新 library 需要執行 ldconfig
。
link name
compile 時我們以 -lXXX
指定要 link 的 library,這個 XXX
稱為 link name。指定 link name 時省略了版號,compiler 會到系統相關的搜尋路徑中尋找最新版本的 XXX
library。
Symbol Versioning
SO-NAME 有升級彈性,但仍有問題──假設依賴的 library 是 libfoo.2.3.1
,SO-NAME 是 libfoo.2
,但執行環境中只有 libfoo.2.2.1
,以 SO-NAME 來看是正確的,但卻可能不相容,因為程式可能使用 libfoo.2.3.1
才有的 interface,這在 libfoo.2.2.1
中找不到,因而執行錯誤。這個問題稱為 Minor-revision Rendezvous Problem。
為解決 Minor-revision Rendezvous Problem,Solaris 2.5 發展出 symbol versioning 機制,提供 versioning 以及 scoping 機制。
Versioning
基本概念是在每個 import 及 export symbol 加上版號,例如 libfoo.1.3
要更新為 libfoo.1.4
時,就把 libfoo.1.4
增加的 symbol 加上 VERS_1.4
的標記。如此 SO-NAME 是 libfoo.1
各版 library 裡會有 VERS_1.2
、VERS_1.3
、VERS_1.4
等等擁有不同版號標記的 symbol,就能區別 symbol 的版本了。
versioning 定義了一些 symbol 集合。symbol 集合有名稱,例如 VERS_1.2
,每個集合包含一些 symbol,一個集合能包含另一個集合,例如 VERS_1.2
包含 VERS_1.1
,這種包含關係也像是繼承,VERS_1.2
繼承 VERS_1.1
的 symbol。symbol 集合由 symbol version script 指定,在 gcc
可以用 -Xlinker --version-script <script file>
指定。
versioning 機制讓 symbol 可以標上版號,build 執行檔時會記錄它需要的 symbol 及其版號。因為 symbol 只能有一個 version,在相同 major version 的 library 中,同一個 symbol 的 version 是固定的,否則會導致向後不相容。因此,執行檔執行時可以用 SO-NAME 找到系統中最新的 library,接著看它是不是有所需要版本的 symbol,如果有就能正常執行,以此解決 Minor-revision Rendezvous Problem。
但 Solaris 2.5 的 symbol versioning 有個限制:在一個 shared library 中每個 symbol 只能有一個 version,也就是如果 symbol 是 VERS_1.1
就不能再是 VERS_1.2
。這造成如果 interface 只有小修改,例如只加一個參數、依然向後相容(只是擴充,舊有功能不變),由於無法讓新版 library 標示該 symbol 為新版,只能透過增加 major version 來表示 interface 改變,有點小題大作。
Example
source code
1 | int foo(int x, int y); |
1 | int foo(int x, int y) { return (x + y); } |
1 | int foo(int x, int y); |
1 | int foo(int x, int y) { return (x + y); } |
1 |
|
1 |
|
1 | VERS_1.0 { |
1 | VERS_1.0 { |
library 版本更新的時候,可由新的 symbol 集合看出它的 interface 改動,例如上面 1.1
版繼承了 1.0
版、增加了 symbol foo2
。
1 | 1.0: |
看看 library 的 export symbol:
1 | $ readelf --dyn-syms libfoo.1.0.so |
兩個 library 的 symbol foo
後面加上 VERS
資訊。如果多個 symbol 集合裡有相同 symbol,第一次出現 symbol 的集合是該 symbol 的 version。另外,會被包含的集合必須先寫。也就是說 symbol version 的前後關係是由 version script 指定的,通常遵照數字順序寫(不然會誤導別人啦)。
執行檔可以看到使用的 symbol 版本:
1 | $ readelf --dyn-syms main1 |
修改 soft link libfoo.so
指向的 library,main1
依然可以正常使用 1.1
版的 library,但 main2
無法使用 1.0
版的 library,會跳 ./main2: ./libfoo.so: version 'VERS_1.1' not found (required by ./main2)
找不到 symbol 的 error。
Scoping
在 symbol 集合裡,local:
的設置會將原本 global 的 symbol 變成 local 的,程式以及其他 shared library 無法使用到這些 symbol。這稱為 scoping 機制,算是補強 ELF 處理 C 的 global symbol scoping,因為 ELF 把所有 global symbol 當作 export symbol,這也是為什麼 Linux 下不需要特別指定哪些 global symbol 要 export 就能 export symbol(windows 就需要)。
上面的例子裡如果把 foo.1.0.ver
裡 local: *;
拿掉,原本其他的 export symbol 就不會被設成 local:
1 | $ readelf --dyn-syms libfoo.1.0.so |
GCC 對 symbol version 的擴充
GCC 對於 symbol version 提供兩個擴充。第一個是可以指定 symbol 的 version,例如 asm(".symver foo_new, foo@VERS_1.1");
指定 foo_new
是 symbol foo
的 1.1
版。
第二個擴充是允許同個 symbol 有多個版本,這補充了 Solaris 版本機制的限制。例如上面例子裡想在 1.1 版的 foo()
增加一個參數,但希望仍保留舊版功能好能向後相容,這時候就能以指定不同版本來達到。
1 | asm(".symver foo_old, foo@VERS_1.0"); |
兩個 .symver
分別指定 foo_old()
是 foo
的版本 1.0
,foo_new()
是版本 1.1
。因為已經用 .symver
自訂 foo
這個 symbol,如果又寫 foo()
link 時會有 multiple definition
錯誤。foo@@VERS_1.1
表示 foo
預設版本是 1.1
,一個 symbol 的預設版號只能有一個,實驗看起來預設版號會影響 compile 執行檔時記錄的 symbol。由於仍然有 1.0
的 foo
,main1
用新版 library 仍能正常執行。
1 | int foo(int x, int y, int z); |
1 |
|
header 跟 library 檔一起發佈,在 compile 執行檔時讓執行檔知道有什麼 symbol,所以必須要是新版 foo
的 prototype,而且 function name 也要是 foo
,而非 foo_new
。(我想還是有別的方式可以做到同樣的事情,甚至可以刻意使用舊版 symbol,這只是我認為合理的 library 發佈以及使用方式)
1 | $ readelf --dyn-syms libfoo.1.1.so |
Semantic Versioning
由 Tom Preston-Werner 提出的 Semantic Versioning 以 spec 的形式提供了版號規範,例如每個數字代表的意義、何時該增加版號、如何增加版號以及如何區分版本的新舊等等。它定義的版號包含 MAJOR.MINOR.PATCH
三個數字,並且可以加上如 -alpha
、-beta
等 pre-release 資訊。概念與上面說的 library 版號類似,interface 有變更時需增加 major verion,增加向後相容的功能時要增加 minor version,修 bug 則增加 patch version,只是它以精準定義的方式描述這些規則。
當然,它只定義了版號本身的規則,至於「什麼改變是有向後相容?是否更改的 interface?」等問題仍然要由開發者判斷。