reentrancy 是 function 可以在執行過程中被中斷,在上一次執行還沒完成前可以再次進入這個 function 執行而不會有問題,也就是第二次進入 function 執行結束後,第一次的執行仍能正確完成。中斷可以是 jump 或 call,也可以是系統的 interrupt 或 signal。
reentrancy 是 single thread 環境下就有的概念。像是被系統 interrupt 中斷時 interrupt handler 是不是能夠再 call 剛剛執行到一半的 function?反過來說,interrupt handler 能 call 的只有 reentrant function。另外,recursive function 一般會是 reentrancy,但如果它使用了 global 變數就不一定。
reentrancy function 可以是 thread-safe 但不一定是 thread-safe。thread-safe function 可以是 reentrancy 也可以不是(繞口令時間)。function 是 thread-safe 但不是 reentrancy 的例子:
1 2 3 4 5 6
function foo() { mutex_lock(); //blah... mutex_unlock(); }
計算是一連串更新一個或多個巨大 data structure 中的 element 時,如果這些更新是彼此獨立的,則可以切割 data structure 並將不同部份分給不同 thread 處理,進而達到並行化(concurrent)。切割 data structure 並交給 thread 處理的方法稱為 data decomposition。
如何將 data structure 切割成多個連續區域(稱為 chunk)取決於 data structure 的 type,例如常見的 array 會依照 dimension 來切割。
在 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 2 3 4 5
$ readelf -d main Dynamic section at offset 0x868 contains 26 entries: Tag Type Name/Value ... 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
基本概念是在每個 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 改變,有點小題大作。
Symbol table '.dynsym' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000000004b0 0 SECTION LOCAL DEFAULT 10 2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3) 7: 0000000000000600 20 FUNC GLOBAL DEFAULT 12 foo@@VERS_1.0 8: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERS_1.0
$ readelf --dyn-syms libfoo.1.1.so
Symbol table '.dynsym' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000528 0 SECTION LOCAL DEFAULT 10 2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (4) 7: 0000000000000680 20 FUNC GLOBAL DEFAULT 12 foo@@VERS_1.0 8: 0000000000000694 14 FUNC GLOBAL DEFAULT 12 foo2@@VERS_1.1 9: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERS_1.0 10: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERS_1.1
兩個 library 的 symbol foo 後面加上 VERS 資訊。如果多個 symbol 集合裡有相同 symbol,第一次出現 symbol 的集合是該 symbol 的 version。另外,會被包含的集合必須先寫。也就是說 symbol version 的前後關係是由 version script 指定的,通常遵照數字順序寫(不然會誤導別人啦)。
Symbol table '.dynsym' contains 8 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo@VERS_1.0 (2) 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (3) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (3) 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 7: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
$ readelf --dyn-syms main2
Symbol table '.dynsym' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo2@VERS_1.1 (2) 2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo@VERS_1.0 (3) 4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (4) 5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (4) 6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 7: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 8: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
修改 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$ readelf --dyn-syms libfoo.1.0.so
Symbol table '.dynsym' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000570 0 SECTION LOCAL DEFAULT 10 2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3) 7: 00000000002009b8 0 NOTYPE GLOBAL DEFAULT 22 _edata 8: 00000000000006c0 20 FUNC GLOBAL DEFAULT 12 foo@@VERS_1.0 9: 00000000002009c0 0 NOTYPE GLOBAL DEFAULT 23 _end 10: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERS_1.0 11: 00000000002009b8 0 NOTYPE GLOBAL DEFAULT 23 __bss_start 12: 0000000000000570 0 FUNC GLOBAL DEFAULT 10 _init 13: 00000000000006d4 0 FUNC GLOBAL DEFAULT 13 _fini
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() 增加一個參數,但希望仍保留舊版功能好能向後相容,這時候就能以指定不同版本來達到。
header 跟 library 檔一起發佈,在 compile 執行檔時讓執行檔知道有什麼 symbol,所以必須要是新版 foo 的 prototype,而且 function name 也要是 foo,而非 foo_new。(我想還是有別的方式可以做到同樣的事情,甚至可以刻意使用舊版 symbol,這只是我認為合理的 library 發佈以及使用方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
$ readelf --dyn-syms libfoo.1.1.so
Symbol table '.dynsym' contains 12 entries: Num: Value Size Type Bind Vis Ndx Name 7: 0000000000000690 20 FUNC GLOBAL DEFAULT 12 foo@VERS_1.0 8: 00000000000006a4 28 FUNC GLOBAL DEFAULT 12 foo@@VERS_1.1 9: 00000000000006c0 14 FUNC GLOBAL DEFAULT 12 foo2@@VERS_1.1 10: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERS_1.0 11: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS VERS_1.1
$ readelf --dyn-syms main2
Symbol table '.dynsym' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo2@VERS_1.1 (2) 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo@VERS_1.1 (2)
Semantic Versioning
由 Tom Preston-Werner 提出的 Semantic Versioning 以 spec 的形式提供了版號規範,例如每個數字代表的意義、何時該增加版號、如何增加版號以及如何區分版本的新舊等等。它定義的版號包含 MAJOR.MINOR.PATCH 三個數字,並且可以加上如 -alpha、-beta 等 pre-release 資訊。概念與上面說的 library 版號類似,interface 有變更時需增加 major verion,增加向後相容的功能時要增加 minor version,修 bug 則增加 patch version,只是它以精準定義的方式描述這些規則。
透過 .dynamic 裡的資訊,linker 可以知道自己的 relocation table 以及 symbol table,能對自己進行 relocation。linker 做完針對自己的 relocation 後,才能開始使用 global 變數、call function 等等。
啟動完成後,dynamic linker 將自己的 symbol 以及 executable file 的 symbol 合併到 global symbol table。接著開始從 executable file 的 .dynamic 得知依賴哪些 shared library,一一打開 shared library 並將之 load 到 memory 中、建立 mapping,並且將 shared library 的 symbol 合併到 global symbol table,再搜尋 shared library 的 dependency。如此 traverse 整棵 shared library dependency tree 之後,即 load 完所需的 shared library。通常會以 BFS traverse dependency tree。
symbol 的部份,ELF 以 dynamic symbol table 保存模組間 import 及 export symbol 的關係,位於 .dynsym,作用以及記錄的資訊跟 .symtab 差不多,但只記錄給其他模組使用的 symbol 以及使用其他模組的 symbol。
traverse dependency tree 的順序跟 symbol 的優先度有關──當兩個 shared library 有相同的 symbol 時要使用誰的?在 Linux 中會優先使用先 load 的 symbol。也就是說,shared library 的 load 順序會影響 symbol 的優先度,而 link 時指定的 library 順序會影響 load 的順序。
$ gdb ./main Reading symbols from ./main...done. (gdb) b main Breakpoint 1 at 0x40067a: file main.c, line 5. (gdb) b bar Breakpoint 2 at 0x400550 (gdb) b foo Function "foo" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 3 (foo) pending. (gdb) r Starting program: /home/cjw/source/dynamic-link/relocate/main
Breakpoint 1, main () at main.c:5 5 bar(2, 3, 10);
這種問題的解法之一是 unit test,用充分的測試保護那個 module,就能確保之後的修改比較不會改壞原本好的東西。但是那個 project 還沒裝 unit test framework,我也不熟,而且有點時間壓力,我認為先做出一個堪用的版本比架起 unit test framework 重要。
$ gcc -shared foo.o -o foo.so /usr/bin/ld: foo.o: relocation R_X86_64_32S against `sum' can not be used when making a shared object; recompile with -fPIC
Relocation section '.rela.text' at offset 0x240 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 00000000000d 000300000002 R_X86_64_PC32 0000000000000000 .data - 8 000000000011 000a0000000b R_X86_64_32S 0000000000000000 sum + 0
Relocation section '.rela.eh_frame' at offset 0x270 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
$ readelf -r foo.o.pic
Relocation section '.rela.text' at offset 0x278 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 00000000000d 000b00000009 R_X86_64_GOTPCREL 0000000000000000 sum - 4 000000000014 000300000002 R_X86_64_PC32 0000000000000000 .data - 4
Relocation section '.rela.eh_frame' at offset 0x2a8 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
程式裡可能有很多 function 在執行過程中不會或很少被用到,例如錯誤處理跟少用的功能。一開始執行就 link 所有 library 裡的 function 顯然有點浪費,畢竟可能花時間 link 了卻沒用到。如果等到 function 第一次被使用時才 bind symbol(找 symbol、relocate 等等)可以加快程式啟動的速度,這個方法稱為 lazy binding。