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。