2026年1月更新。
1995年Solaris的link editor和ld.so引入了symbol versioning機制。 Ulrich Drepper和Eric Youngdale在1997年借鑑Solaris symbol versioning,設計了用於glibc的GNU風格symbol versioning。
gnu-gabi規範:https://sourceware.org/gnu-gabi/program-loading-and-dynamic-linking.txt#:~:text=versioning
一個shared
object更新,某個符號的行爲變更(ABI改變(如變更參數或返回值的類型)或行爲變化)時,傳統上可以bump
DT_SONAME:依賴的shared
objects必須重新編譯、鏈接才能繼續使用;如果不改變DT_SONAME,依賴的shared
objects可能悄悄地產生異常行爲。 使用symbol
versioning可以提供不改變DT_SONAME的backward
compatibility。
下面描述表示方式,然後從assembler、鏈接器、ld.so幾個角度描述symbol versioning行爲。初次閱讀時不妨跳過表示方式部分。
表示方式
在使用symbol versioning的shared
object或可執行檔中,有至多三個sections,其中.gnu.version_r和.gnu.version_d是可選的:
.gnu.version(類型SHT_GNU_versym,由DT_VERSYM指向):與.dynsym平行的表,包含N個uint16_tversion ID,每個dynamic symbol一個。.gnu.version_r(類型SHT_GNU_verneed,由DT_VERNEED/DT_VERNEEDNUM標記):描述未定義符號所需的version。.gnu.version_d(類型SHT_GNU_verdef,由DT_VERDEF/DT_VERDEFNUM標記):描述定義符號的version。
這種parallel table設計使symbol versioning成爲可選的——忽略它的ld.so實現(如musl)會把所有引用綁定到default version。
Verdef描述模塊中的version定義。每個Verdef有一個或多個Verdaux(vd_aux):第一個給出version名,後續的(如有,vda_next)給出parent
version名(用於version繼承,如v2 {} v1;)。
vd_cnt等於1加上parent
version定義數。然而實際中它不太有用——glibc和FreeBSD
rtld忽略它,ld.lld簡單地硬編碼爲1。
Verneed描述對其他shared
objects(vn_file)的version需求。每個Verneed有一個或多個Vernaux,指定所需的version名(vna_name)。
Version名不是全局唯一的。以下情況是有效的:
- 一個
Verdef定義versionv1,同時一個Verneed需要來自a.so的versionv1。 - 一個
Verneed需要來自a.so的versionv1,同時另一個Verneed需要來自b.so的versionv1。
1 | // Version definitions |
VER_FLG_WEAK
GNU ld在version
node沒有關聯符號時會在Verdef::vd_flags中設置VER_FLG_WEAK,與Solaris行爲一致。
BZ24718#c15提議"set
VER_FLG_WEAK on version reference if all symbols are weak",但被拒絕。
2026年的評論要求重新審視VER_FLG_WEAK
version
references是否應該支持可選依賴——例如,一個可執行檔對foo有weak
reference並鏈接了libA.so.1(提供foo@@v1),但運行時使用的libA.so.1缺少v1的version定義。目前這會導致rtld報錯。
Version index values
- Index
0稱爲
VER_NDX_LOCAL(名字有誤導性,應該是VER_NDX_NONE)。對於定義的符號,binding將會更改爲STB_LOCAL。 - Index
1稱爲
VER_NDX_GLOBAL。沒有特殊作用,用於unversioned符號。 - Index 2到0xffef用於用戶定義的versions。
定義的versioned符號有兩種形式:
foo@@v2,default versionfoo@v2,non-default version(hidden version)。其version ID設置了VERSYM_HIDDENbit。
未定義versioned符號只有foo@v2這一種形式。
有一個特殊情況:可執行檔中copy relocation引用的version符號。
該符號在運行時relocation處理中作爲定義,但其version
ID引用.gnu.version_r而非.gnu.version_d。 PR28158的解決方案選擇了@形式。
通常versioned符號只在shared objects中定義,但可執行檔也可以有定義的versioned符號。 (當shared object更新時保留舊符號,使其他shared objects不須重新鏈接;而可執行檔通常不提供versioned符號供其他shared objects引用。)
在.gnu.version
section(由DT_VERSYM標籤描述)中,每個符號被分配一個version
index:
- Unversioned未定義符號使用index 0。但LLD 22之前和GNU ld 2.35到2.45之間使用index 1。見https://sourceware.org/bugzilla/show_bug.cgi?id=33577#c34
- Versioned未定義符號使用index >= 2
- Unversioned定義符號使用index 1
- Versioned定義符號使用index >= 2
Index
0的定義符號是local的,不應在.dynsym或.gnu.version中有條目。
例子
readelf -V可以導出symbol versioning表。
下面輸出的.gnu.version_d section裏:
- Version index 1 (
VER_NDX_GLOBAL) is the filename (soname if shared object). TheVER_FLG_BASEflag is set. - Version index 2 is a user defined version. Its name is
LUA_5.3.
下面輸出的.gnu.version_r section裏,version index
3~10每一個都表示了一個依賴的shared
object中的version。名字GLIBC_2.2.5出現了三次,每次對應不同的shared
object。
.gnu.version表給每個.dynsym條目分配一個version
index。 一個條目(version
ID)對應.gnu.version_d中的Index:條目或.gnu.version_r中的Version:條目。
1 | % readelf -V /usr/bin/lua5.3 |
Symbol versioning in object files
這套GNU symbol versioning允許.symver
directive在.o裏標註符號的version。在.o裏符號名字面包含@或@@。
Assembler行爲
GNU as和LLVM integrated assembler提供實現。
- 對於
.symver foo, foo@v1- 如果foo未定義,.o中有一個名爲
foo@v1的符號 - 如果foo被定義,.o中有兩個符號:
foo和foo@v1,兩者的binding一致(均爲STB_LOCAL,或均爲STB_WEAK,或均爲STB_GLOBAL),st_other一致(visibility一致)。個人認爲這個行爲是設計缺陷{gas-copy}
- 如果foo未定義,.o中有一個名爲
- 對於
.symver foo, foo@@v1- 如果foo未定義,assembler報錯
- 如果foo被定義,.o中有兩個符號:
foo和foo@@v1,兩者的binding和st_other一致
- 對於
.symver foo, foo@@@v1- 如果foo未定義,.o中有一個名爲
foo@v1的符號 - 如果foo被定義,.o中有一個名爲
foo@@v1的符號
- 如果foo未定義,.o中有一個名爲
With GNU as 2.35 (PR25295) or Clang 13:
.symver foo, foo@v1, remove- 如果foo未定義,.o中有一個名爲
foo@v1的符號 - 如果foo被定義,.o中有一個名爲
foo@v1的符號 - 我推薦用這種方式定義non-default符號
- Unfortunately, in GNU as,
foocannot be used in a relocation (PR28157).
- 如果foo未定義,.o中有一個名爲
鏈接器行爲
鏈接器在讀入object files、archive files、shared objects、LTO files、linker scripts等後就進入符號解析階段。
GNU ld用indirect symbol表示versioned符號,在很多階段都有複雜的規則,這些規則都沒有文檔。 我個人得出的符號解析規則:
- 定義的
foo可以滿足未定義的foo(傳統unversioned符號規則) - 定義的
foo@v1可以滿足未定義的foo@v1 - 定義的
foo@@v1可以同時滿足未定義的foo和foo@v1
若存在多個default
version的定義(如foo@@v1 foo@@v2),觸發duplicate definition
error。通常一個符號有零或一個default
version(@@)定義,任意個non-default
version(@)定義。
ld.lld的實現中,看到shared
object中的foo@@v1則在符號表中同時插入foo和foo@v1,因此可以滿足未定義的foo和foo@v1。
鏈接器如果先看到未定義foo和foo@v1,會把它們當作兩個符號。之後看到定義的foo@@v1時,概念上應該合併foo和foo@@v1。若看到的是定義的foo@@v2,應該用foo@@v2滿足foo,而foo@v1仍是一個不同的符號。
- Combining Versions描述了這個問題
gold/symtab.cc Symbol_table::define_default_version用一個啓發式規則處理這個問題。它特殊判斷了visibility,但我感覺這個規則可能不需要也行- Before 2.36, GNU ld reported a bogus multiple definition error for
defined weak
foo@@v1and defined globalfoo@v1PR ld/26978 - Before 2.36, GNU ld had a bug that the visibility of undefined
foo@v1does not affect the output visibility offoo@@v1: PR ld/26979 - I fixed the object file side problem of ld.lld 12.0 in https://reviews.llvm.org/D92259
fooArchive files and lazy object files may still have incompatibility issues.
When ld.lld sees a defined foo@@v, it adds both
foo and foo@v1 into the symbol table, thus
foo@@v1 can resolve both undefined foo and
foo@v1. After processing all input files, a pass iterates
symbols and redirects foo@v1 to foo@@v1.
Because ld.lld treats them as separate symbols during input processing,
a defined foo@v cannot suppress the extraction of an
archive member defining foo@@v1, leading to a behavior
incompatible with GNU ld. This probably does not matter, though.
若foo和foo@v1同時被定義(在相同位置),foo會被移除。
GNU
ld有另一個奇怪的行爲:若foo和foo@v1同時被定義,foo會被移除。
我堅信這是GNU ld的問題,但maintainer拒絕了PR
ld/27210。 我在ld.lld 13.0.0中實現了類似的hack(https://reviews.llvm.org/D107235),但希望binutils能修復assembler的問題(https://sourceware.org/pipermail/binutils/2021-August/117677.html)。
ld.lld分配version
indexes的方式:首先,每個Verdef條目從2開始獲得index;然後,對於每個解析到shared
object定義的dynamic symbol,如果(file,
version)對尚未出現過,則分配一個新的Verneed/Vernaux
index。
Version script
在輸出的shared object或可執行檔中定義version必須指定version script。若所有versioned符號均爲未定義狀態則無需version script。
1 | # Make all symbols other than foo and bar local. |
Version script有三個用途:
- 定義versions
- 指定一些模式,使得匹配的、定義的、unversioned的符號具有指定的version
- Scope reduction
- 對於一個被
local:模式匹配的符號,如果它是定義的、unversioned的,那麼它的binding會被更改爲STB_LOCAL,不會導出到dynamic symbol table - 對一個定義的unversioned符號,它可以被任何version
node內的
local:模式匹配 - 對一個定義的versioned符號,它可以被相應的version
node內的
local:模式匹配。例如,foo@@v1和foo@v1都能被v1 { local: foo; };匹配
- 對於一個被
一個version script由一個anonymous version tag
({...};),或若干named version tags
(v1 {...};)組成。 如果一個anonymous version
tag和其他version tag一起使用,GNU
ld會報錯anonymous version tag cannot be combined with other version tags。
local:可以放在任意version tag裏。
如果一個定義的符號被多個version
tags匹配,如下的優先級規則適用(binutils-gdb/bfd/linker.c:find_version_for_sym):
- 第一個有exact pattern(無wildcard)的version tag獲勝
- 否則,最後一個有非
*的wildcard pattern在global:中的version tag獲勝 - 否則,最後一個有非
*的wildcard pattern在local:中的version tag獲勝 - 否則,最後一個有
*pattern的version tag獲勝
在gold和ld.lld中,規則如下:
- 第一個有exact pattern的version tag獲勝
- 否則,最後一個有非
*的wildcard pattern的version tag獲勝。如果version tag在global:和local:中都有非*的wildcard pattern,global:的獲勝 - 否則,最後一個有
*pattern的version tag獲勝。(在LLD 18之前,是第一個而非最後一個)
例如,給定v1 { local: p*;}; v2 { global: pq*;}; v3 { local: pqr*;};,對於定義的non-local符號pqrs,gold和ld.lld選擇local: pqr*,而GNU
ld選擇global: pq*。
**也是catch-all
pattern,但優先級高於*。
GNU
ld在一個pattern同時出現在global:和local:時報錯。
大多數patterns是exact的,所以gold和ld.lld迭代patterns而不是符號來改善性能。
GNU
ld和gold爲每個定義的version在.symtab和.dynsym中添加一個absolute
symbol(st_shndx=SHN_ABS)。
ld.so不需要這個符號,所以這個行爲看起來很奇怪。
在-r鏈接中,--version-script被忽略。技術上local:
version nodes可能與-r一起使用,但GNU
ld和ld.lld只是忽略--version-script。
Versioned symbol產生方式
一個未定義符號獲得version的方式:
- 名字不包含
@(沒有使用.symver):某個shared object定義了default version符號 - 名字包含
@:該符號須要被某個shared object定義,否則GNU ld會報錯;https://reviews.llvm.org/D92260之後ld.lld也會報錯
一個定義的符號獲得version的方式:
- 名字不包含
@:被version script的一個named version tag的某個pattern匹配而獲得version - 名字包含
@-shared:versionv1須要被version script定義,否則GNU ld會報錯(version node not found for symbol)-no-pie或-pie:GNU ld不需要version script即會生成version定義v1。這個行爲奇怪。
推薦用法
個人推薦:
定義default-version符號時不要用.symver。在version
script的相應version node裏指定這個符號即可。
如果你確實想用.symver,使用.symver foo, foo@@@v2使得foo不存在。如果需要binutils>=2.35或Clang>=13,.symver foo, foo@@v2, remove也可以。
定義non-default符號時在原符號名後加後綴(.symver foo_v1, foo@v1)防止和foo衝突。這會留下(通常不想要的)foo_v1。如果不從object
file中strip foo_v1,可以在version
script中用local:模式localize它。使用較新的工具鏈,可以用.symver foo_v1, foo@v1, remove
1 | cat > a.c <<e |
1 | % readelf -W --dyn-syms a.so | grep @ |
未定義的versioned符號通常是鏈接時綁定的,object
files不須要指定符號。如果確實要引用,推薦.symver foo, foo@@@v1,即使能.symver foo, foo@v1達到相同效果
多數時候,你希望未定義在鏈接時綁定到default
version。通常沒有必要指定.symver。
如果確實要引用,.symver foo_v1, foo@@@v1或.symver foo_v1, foo@v1均可。
1 | cat > b.c <<e |
未定義符號被綁定到non-default version foo@v1:
1
2% readelf -W --dyn-syms b.so | grep foo
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND foo@v1 (2)
如果省略.symver,則會被綁定到default version
foo@@v2。
爲什麼.symver xxx, foo@v1對定義的符號不好?
有兩種情況。
第一,xxx不是foo(versioned符號的unadorned name)。
這是最常見的用法。
不失一般性,我們用.symver foo_v1, foo@v1作爲例子。
如果version script沒有localize
foo_v1,我們會在.dynsym中得到foo_v1。
這個額外的符號幾乎總是不想要的。
第二,xxx是foo。
foo定義可以滿足其他TU中的unversioned引用。
如果你仔細想想,non-default version定義在TU外被使用是非常罕見的。
1 | # a.s |
如果version script包含v1 {};,輸出在GNU
ld和ld.lld>=13.0.0中只有foo@v。
輸出在gold和較舊的ld.lld中會同時有foo和foo@v1。
如果version script包含v1 { foo; };,輸出在GNU
ld、gold和ld.lld>=13.0.0中只有foo@v1。
輸出在較舊的ld.lld中會同時有foo和foo@v1。
如果version script包含v2 { foo; };,pattern會被忽略。
不幸的是沒有鏈接器對這種容易出錯的情況報警告。
有不同的行爲是不幸的。 而且第二種情況需要鏈接器內部的複雜性。
rtld行爲
Linux Standard Base Core Specification, Generic Part 描述了rtld(ld.so)的行爲。 kan在2005年給FreeBSD rtld添加了symbol versioning支持。
Dynamic
table中的DT_VERNEED和DT_VERNEEDNUM標籤界定了shared
object/可執行檔的version requirement:所需的versions和所需的shared
object名(Vernaux::vna_name)。
當載入帶有DT_VERNEEDED的object時,glibc
rtld執行一些檢查(_dl_check_all_versions)。
對於每個Vernaux條目(Verneed的輔助條目),glibc rtld檢查被引用的shared
object是否有DT_VERDEF表。 如果沒有,ld.so將此作爲graceful
degradation處理並打印no version information available (required by %s);
如果有但表中沒有定義該version,ld.so報告(如果Vernaux條目有VER_FLG_WEAK
bit)警告或(否則)錯誤。[verneed-check]
通常minor releases不會bump soname。假設libB.so依賴libA
1.3(soname爲libA.so.1)並調用1.2版本不存在的函數。如果使用PLT
lazy binding,libB.so在安裝了libA
1.2的系統上似乎還能工作,直到1.3的符號的PLT被調用。 如果不使用symbol
versioning且想解決這個問題,就得在soname裏記錄minor version
number(libA.so.1.3)。然而,bump
soname是全有或全無的:所有依賴的shared objects都需要重新鏈接。
如果使用symbol versioning,可以繼續使用soname
libA.so.1。ld.so會在使用libA
1.2時報錯,因爲libB.so需要的1.3 version不存在。
爲foo搜索定義時:
- 對於不含
DT_VERSYM的object- 可以綁定到
foo
- 可以綁定到
- 對於含
DT_VERSYM的object- 可以綁定到version
VER_NDX_GLOBAL的foo。這優先於接下來兩條規則 - 可以綁定到任一default version的
foo - 在relocation resolving階段(非dlsym/dlvsym)可以綁定到non-default
version index 2的
foo。該規則在shared object變成versioned時保持兼容性。
- 可以綁定到version
注意(未定義foo綁定到foo@v1 version index
2)是ld.so允許但鏈接器不允許的{reject-non-default}。
rtld的行爲是爲了在shared
object變成versioned時保持兼容性:最小version(index
2)的符號表示之前unversioned的符號。 如果新版本的shared
object需要廢棄unversioned
bar,可以移除bar並定義bar@compat。使用bar的庫不受影響,但禁止新鏈接到bar。
當有多個版本的foo時,dlsym(RTLD_DEFAULT, ...)返回default
version。 在glibc 2.36之前,dlsym(RTLD_NEXT, ...)返回第一個版本(BZ14932)。
這是因爲在elf/dl-sym.c:do_sym中,RTLD_NEXT分支沒有把DL_LOOKUP_RETURN_NEWEST標誌傳給dl_lookup_symbol_x。
FreeBSD沒有這個問題。
爲foo@v1搜索定義時:
- 對於不含
DT_VERSYM的object- 可以綁定到
foo。在glibc中,elf/dl-lookup.c:check_match斷言filename不匹配vn_filefilename
- 可以綁定到
- 對於含
DT_VERSYM的object- 可以綁定到
foo@v1或foo@@v1 - 在relocation resolving階段(非dlsym/dlvsym)可以綁定到version
VER_NDX_GLOBAL的foo
- 可以綁定到
假設b.so引用malloc@GLIBC_2.2.5。可執行檔因爲鏈接了malloc實現而定義unversioned
malloc。
運行時,b.so中的malloc@GLIBC_2.2.5會綁定到可執行檔。
例如,address/memory/thread sanitizers利用這個行爲:shared
objects不需要鏈接interceptors;在可執行檔中有interceptor就足夠了。
libxml2利用這個行爲移除符號的versioning,同時保持與鏈接舊版libxml2的objects的兼容性。
當versioned reference綁定到沒有symbol versioning的shared
object時,在glibc 2.41之前,elf/dl-lookup.c:check_match曾斷言filename不匹配vn_file
filename: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17echo 'void foo(); int main() { foo(); }' > a.c
echo 'v1 { foo; };' > c0.ver
echo 'void foo() {}' > c.c
sed 's/^ /\t/' > Makefile <<'eof'
.MAKE.MODE := meta curdirOk=1
CFLAGS := -fpic
LDFLAGS := -Wl,--no-as-needed
a: a.c c.so c0.so
$(LINK.c) a.c c0.so -Wl,-rpath=$$PWD -o $@
c0.so: c.c c0.ver
$(LINK.c) -shared -Wl,-soname=c.so,--version-script=c0.ver c.c -o $@
c.so: c.c
$(LINK.c) -shared -Wl,-soname=c.so -nostdlib c.c -o $@
clean:
rm -f a *.so *.o *.meta
eof1
2
3
4
5
6% bmake && ./a # glibc<2.41
./a: /tmp/d/c.so: no version information available (required by ./a)
Inconsistency detected by ld.so: dl-lookup.c: 107: check_match: Assertion `version->filename == NULL || ! _dl_name_match_p (version->filename, map)' failed!
% bmake && ./a # glibc>=2.41
./a: /tmp/d/c.so: no version information available (required by ./a)
這個glibc<2.41的檢查相當愚蠢,因爲大多數shared
objects由於對libc的versioned
references如__cxa_finalize@GLIBC_2.2.5(來自GCC
crtbeginS.o)而有DT_VERSYM。
除了這個斷言,自glibc 2.30 BZ24741起,vn_file在符號搜索中基本被忽略。
之前在relocation
resolving期間,當一個object未能提供匹配後,如果它匹配vn_file,rtld會報錯symbol %s version %s not defined in file %s with link time reference。
glibc
2.30 ld.so: Support moving versioned symbols between sonames [BZ
#24741]對weak references有附帶好處。
之前,如果b.so有versioned weak reference
foo@v1(其中v1引用c.so),當c.so缺少foo@@v1或foo@v1時,rtld會報錯symbol %s version %s not defined in file %s with link time reference——這與weak
reference語義相悖。 較新的rtld只要運行時的c.so定義了version
v1就能容忍這種情況:
1 | echo '#include <stdio.h>\nvoid fb(); int main() { fb(); puts("a"); }' > a.c |
如果c.so完全缺少所需的version,rtld仍會報fatal error:
1
2
3
4
5
6% bmake
...
% ./a
a
% LD_PRELOAD=c2.so ./a
./a: /tmp/t/v2/c2.so: version `v1' not found (required by /tmp/t/v2/b.so)
2026年的BZ24718#c19評論詢問Vernaux::vna_flags中的VER_FLG_WEAK擴展,允許rtld繼續執行(帶verbose模式警告weak version ... not found)而不是失敗。
如果a中的v1
Verneed有VER_FLG_WEAK標誌,我們將看到:
1 | % LD_PRELOAD=c2.so ./a |
例子
運行以下代碼創建a.c b.c b0.c Makefile,然後運行bmake。
1 | cat > ./a.c <<'eof' |
在glibc 2.36之前,輸出是: 1
2
3
4% ./a
foo(0) = 0, ok
foo(0) = 3, ok
foo(0) = 0, wrong
glibc中升級過的符號
When to prevent execution of new binaries with old glibc總結了何時引入新的symbol version。
注意,binutils 2.35之前的GNU
nm不顯示@或@@。
1 | nm -D /lib/x86_64-linux-gnu/libc.so.6 | \ |
在我的x86-64系統上的輸出: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31pthread_cond_broadcast @GLIBC_2.2.5 @@GLIBC_2.3.2
clock_nanosleep @@GLIBC_2.17 @GLIBC_2.2.5
_sys_siglist @@GLIBC_2.3.3 @GLIBC_2.2.5
sys_errlist @@GLIBC_2.12 @GLIBC_2.2.5 @GLIBC_2.3 @GLIBC_2.4
quick_exit @GLIBC_2.10 @@GLIBC_2.24
memcpy @@GLIBC_2.14 @GLIBC_2.2.5
regexec @GLIBC_2.2.5 @@GLIBC_2.3.4
pthread_cond_destroy @GLIBC_2.2.5 @@GLIBC_2.3.2
nftw @GLIBC_2.2.5 @@GLIBC_2.3.3
pthread_cond_timedwait @@GLIBC_2.3.2 @GLIBC_2.2.5
clock_getres @GLIBC_2.2.5 @@GLIBC_2.17
pthread_cond_signal @@GLIBC_2.3.2 @GLIBC_2.2.5
fmemopen @GLIBC_2.2.5 @@GLIBC_2.22
pthread_cond_init @GLIBC_2.2.5 @@GLIBC_2.3.2
clock_gettime @GLIBC_2.2.5 @@GLIBC_2.17
sched_setaffinity @GLIBC_2.3.3 @@GLIBC_2.3.4
glob @@GLIBC_2.27 @GLIBC_2.2.5
sys_nerr @GLIBC_2.2.5 @GLIBC_2.4 @@GLIBC_2.12 @GLIBC_2.3
_sys_errlist @GLIBC_2.3 @GLIBC_2.4 @@GLIBC_2.12 @GLIBC_2.2.5
sys_siglist @GLIBC_2.2.5 @@GLIBC_2.3.3
clock_getcpuclockid @GLIBC_2.2.5 @@GLIBC_2.17
realpath @GLIBC_2.2.5 @@GLIBC_2.3
sys_sigabbrev @GLIBC_2.2.5 @@GLIBC_2.3.3
posix_spawnp @@GLIBC_2.15 @GLIBC_2.2.5
posix_spawn @@GLIBC_2.15 @GLIBC_2.2.5
_sys_nerr @@GLIBC_2.12 @GLIBC_2.4 @GLIBC_2.3 @GLIBC_2.2.5
nftw64 @GLIBC_2.2.5 @@GLIBC_2.3.3
pthread_cond_wait @GLIBC_2.2.5 @@GLIBC_2.3.2
sched_getaffinity @GLIBC_2.3.3 @@GLIBC_2.3.4
clock_settime @GLIBC_2.2.5 @@GLIBC_2.17
glob64 @@GLIBC_2.27 @GLIBC_2.2.5
一個符號發生ABI變更時(如變更參數或返回值的類型)必須要加新version,而API行爲變更時有時也會加,比如:
realpath@@GLIBC_2.3: 之前的realpath在第二個參數爲NULL時返回EINVALmemcpy@@GLIBC_2.14BZ12518: 之前的memcpy copies forward。當年的Shockwave Flash有個memcpy downward的bug因爲memcpy採用了複雜的copy策略而觸發quick_exit@@GLIBC_2.24BZ20198: 之前的quick_exit會調用thread_local objects的destructorsglob64@@GLIBC_2.27: 之前的glob不follow dangling symlinks
去除symbol versioning
假設你想用一個有versioned references的預構建shared
object構建應用,但只能找到提供unversioned定義的shared
objects。鏈接器會報錯: 1
ld.lld: error: undefined reference to foo@v1 [--no-allow-shlib-undefined]
如診斷所示,可以添加--allow-shlib-undefined來消除錯誤。這不推薦,但構建的應用可能碰巧能工作。
對於這種情況,另一個hack的解決方案是:
1 | # 64-bit |
刪除.gnu.version後,鏈接器會認爲out.so引用的是foo而非foo@v1。
然而,llvm-objcopy會把section內容清零。在運行時,glibc
ld.so會報錯unsupported version 0 of Verneed record。
要讓glibc滿意,可以從dynamic
table中刪除DT_VER*標籤。上面的代碼片段用r2命令定位DT_VERNEED(0x6ffffffe)並重寫爲DT_NULL(DT_NULL條目停止dynamic
table的解析)。readelf -d輸出的差異大致是:
1 | 0x000000006ffffffb (FLAGS_1) Flags: NOW |
ld.lld
- 如果未定義符號沒有被shared object定義,GNU ld會報錯。ld.lld 12.0之前不會報錯(我在https://reviews.llvm.org/D92260中修復了它)。
GNU function attribute
有一個GNU function attribute可以降低爲.symver彙編指令。
這個attribute由GCC實現但Clang沒有實現。
1 | extern "C" __attribute__((symver("foo@@v2"))) void foo() {} |
不幸的是,@@@和,remove不被支持。
加上Clang沒有實現這個function
attribute的原因,我不建議使用這個特性。
評價
GCC/Clang支持asm
specifier和#pragma redefine_extname重命名一個符號。比如聲明int foo() asm("foo_v1");再引用foo,.o中的符號會是foo_v1。
舉個例子,musl v1.2.0最大的變化是32-bit架構的time64支持。musl採取了一種使用asm specifier的方案:
1 | // include/features.h |
- .o中,time32定義仍爲
utimes,提供ABI兼容舊程序;time64定義則爲__utimes_time64 - Public header用asm
specifier重定向
utimes到__utimes_time64- 缺點是倘若用戶自行聲明
utimes而不include public header,會得到deprecated time32定義。這種自行聲明的方式是不推薦的
- 缺點是倘若用戶自行聲明
- 內部實現中"好看的"名字
utimes表示time64定義;"難看的"名字__utimes_time32表示deprecated time32定義- 假如time32實現被其他函數調用,那麼用"難看的"名字能清晰地標識出來"此處和deprecated time32定義有關"
對於上述的例子,用symbol versioning來實作大概是這樣:
1 | // API header include/sys/time.h |
注意.symver不可用@@@。這個header被定義的translation
unit使用,@@@會產生一個default
version定義,而我們想要一個non-default version。
根據前文對Assembler行爲的討論,不如意的地方是:定義的translation
unit中,__utimes_time32這個符號也存在。鏈接時注意用version
script localize它。
那麼symbol versioning還有什麼意義呢?我細細琢磨:
- 在保持運行時兼容舊庫的同時拒絕鏈接舊符號。{reject-non-default}
- 不需要標註declarations。
- version定義可以延遲到鏈接時。version script提供靈活的pattern matching機制來指定versions。
- Scope reduction。可以說如果version
scripts不提供
local:,另一個類似--dynamic-list的機制可能會被開發出來。 - 對用asm specifiers重命名GCC和Clang認識的builtin functions有一些語義問題(它們不知道重命名的符號有內建語義)。見2020-10-15-intra-call-and-libc-symbol-renaming
- [verneed-check]
對於第一條,asm specifier方案用約定來防止問題(用戶應該include header);而symbol versioning可以由ld強制。
設計缺陷:
.symver foo, foo@v1{gas-copy}- Verdaux有點多餘。實踐中一個Verdef只有一個Verdaux輔助條目。
- 這可以說是個小問題,但對提供多個shared
objects的框架來說很煩人。ld.so要求"a versioned symbol is implemented in
the same shared object in which it was found at link
time",不允許在shared objects之間移動定義。所幸glibc 2.30 BZ24741放寬了這個要求,實質上忽略了
Vernaux::vna_name。
在此之前,glibc把clock_*函數從librt.so移動到libc.so用的方法是:
1 | // rt/clock-compat.c |
libc.so中定義__clock_getres和clock_getres。librt.so中用一個名爲clock_getres的ifunc引導到libc.so中的__clock_getres。