All about symbol versioning
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。
// Version definitions
typedef struct {
Elf64_Half vd_version; // version: 1
Elf64_Half vd_flags; // VER_FLG_BASE (index 1) or 0 (index != 1)
Elf64_Half vd_ndx; // version index
Elf64_Half vd_cnt; // number of associated aux entries, one plus number of parent version definitions
Elf64_Word vd_hash; // SysV hash of the version name
Elf64_Word vd_aux; // offset in bytes to the verdaux array
Elf64_Word vd_next; // offset in bytes to the next verdef entry
} Elf64_Verdef;
typedef struct {
Elf64_Word vda_name; // version name
Elf64_Word vda_next; // offset in bytes to the next verdaux entry
} Elf64_Verdaux;
// Version needs
typedef struct {
Elf64_Half vn_version; // version: 1
Elf64_Half vn_cnt; // number of associated aux entries
Elf64_Word vn_file; // .dynstr offset of the needed filename
Elf64_Word vn_aux; // offset in bytes to vernaux array
Elf64_Word vn_next; // offset in bytes to next verneed entry
} Elf64_Verneed;
typedef struct {
Elf64_Word vna_hash; // SysV hash of vna_name
Elf64_Half vna_flags; // usually 0; copied from vd_flags of the needed so
Elf64_Half vna_other; // `Version:` in readelf -V output
Elf64_Word vna_name; // .dynstr offset of the version name
Elf64_Word vna_next; // offset in bytes to next vernaux entry
} Elf64_Vernaux;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:条目。
% readelf -V /usr/bin/lua5.3
Version symbols section '.gnu.version' contains 248 entries:
Addr: 0x0000000000002af4 Offset: 0x002af4 Link: 5 (.dynsym)
000: 0 (*local*) 3 (GLIBC_2.3) 4 (GLIBC_2.2.5) 4 (GLIBC_2.2.5)
004: 5 (GLIBC_2.3.4) 4 (GLIBC_2.2.5) 4 (GLIBC_2.2.5) 4 (GLIBC_2.2.5)
...
Version definition section '.gnu.version_d' contains 2 entries:
Addr: 0x0000000000002ce8 Offset: 0x002ce8 Link: 6 (.dynstr)
000000: Rev: 1 Flags: BASE Index: 1 Cnt: 1 Name: lua5.3
0x001c: Rev: 1 Flags: none Index: 2 Cnt: 1 Name: LUA_5.3
Version needs section '.gnu.version_r' contains 3 entries:
Addr: 0x0000000000002d20 Offset: 0x002d20 Link: 6 (.dynstr)
000000: Version: 1 File: libdl.so.2 Cnt: 1
0x0010: Name: GLIBC_2.2.5 Flags: none Version: 9
0x0020: Version: 1 File: libm.so.6 Cnt: 1
0x0030: Name: GLIBC_2.2.5 Flags: none Version: 6
0x0040: Version: 1 File: libc.so.6 Cnt: 6
0x0050: Name: GLIBC_2.11 Flags: none Version: 10
0x0060: Name: GLIBC_2.14 Flags: none Version: 8
0x0070: Name: GLIBC_2.4 Flags: none Version: 7
0x0080: Name: GLIBC_2.3.4 Flags: none Version: 5
0x0090: Name: GLIBC_2.2.5 Flags: none Version: 4
0x00a0: Name: GLIBC_2.3 Flags: none Version: 3Symbol 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。
# Make all symbols other than foo and bar local.
{ global: foo; bar; local: *; };
# Assign version FBSD_1.0 to malloc and version FBSD_1.3 to mallocx,
# and make internal local.
FBSD_1.0 { malloc; local: internal; };
FBSD_1.3 { mallocx; };
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
cat > a.c <<e
__asm__(".symver foo_v1, foo@v1, remove");
void foo_v1() {}
void foo() {}
e
cat > a.ver <<e
v1 {};
v2 { foo; };
e
cc -fpic a.c -shared -Wl,--version-script=a.ver -o a.so% readelf -W --dyn-syms a.so | grep @
5: 0000000000001630 7 FUNC GLOBAL DEFAULT 11 foo@@v2
6: 0000000000001629 7 FUNC GLOBAL DEFAULT 11 foo@v1
未定义的versioned符号通常是链接时绑定的,object files不须要指定符号。如果确实要引用,推荐.symver foo, foo@@@v1,即使能.symver foo, foo@v1达到相同效果
多数时候,你希望未定义在链接时绑定到default version。通常没有必要指定.symver。
如果确实要引用,.symver foo_v1, foo@@@v1或.symver foo_v1, foo@v1均可。
cat > b.c <<e
__asm__(".symver foo_v1, foo@v1"); // foo@@@v1 and foo@v1, remove work as well
void foo_v1();
void bar() {
foo_v1();
}
e
cc -fpic b.c ./a.so -shared -o b.so
未定义符号被绑定到non-default version foo@v1:
% 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外被使用是非常罕见的。
# a.s
.symver foo, foo@v1
.globl foo
foo:
# b.s
# 如果打算使用non-default version符号,应该引用foo@v1而不是foo
如果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:
echo '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
eof% 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就能容忍这种情况:
echo '#include <stdio.h>\nvoid fb(); int main() { fb(); puts("a"); }' > a.c
echo '__attribute__((weak)) void foo(); void fb() { if (foo) foo(); }' > b.c
echo 'v1 { foo; };' > c-link.ver
echo '#include <stdio.h>\nvoid foo() { puts("foo"); }' > c.c
echo 'v1 { };' > c.ver
echo 'v2 { };' > c2.ver
sed 's/^ /\t/' > Makefile <<'eof'
.MAKE.MODE := meta curdirOk=1
CFLAGS := -fpic
a: a.c b.so c-link.so c.so c2.so
$(LINK.c) a.c b.so -Wl,-rpath=$$PWD -o $@
b.so: b.c c-link.so
$(LINK.c) -shared -Wl,--no-as-needed $> -Wl,-rpath=$$PWD -o $@
c-link.so: c.c c.ver
$(LINK.c) -shared -Wl,-soname=c.so,--version-script=c-link.ver c.c -o $@
c.so: c.c c.ver
$(LINK.c) -shared -Wl,-soname=c.so,--version-script=c.ver -Dfoo=foo1 c.c -o $@
c2.so: c.c c2.ver
$(LINK.c) -shared -Wl,-soname=c.so,--version-script=c2.ver -Dfoo=foo1 c.c -o $@
clean:
rm -f a *.so *.o *.meta
eof
如果c.so完全缺少所需的version,rtld仍会报fatal error:
% 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标志,我们将看到:
% LD_PRELOAD=c2.so ./a
./a: /tmp/t/v2/c2.so: weak version `v1' not found (required by /tmp/t/v2/b.so)
a例子
运行以下代码创建a.c b.c b0.c Makefile,然后运行bmake。
cat > ./a.c <<'eof'
#define _GNU_SOURCE // workaround before glibc 2.36 (commit 748df8126ac69e68e0b94e236ea3c2e11b1176cb)
#include <dlfcn.h>
#include <stdio.h>
int foo(int a);
int main(void) {
int res = foo(0);
printf ("foo(0) = %d, %s\n", res, res == 0 ? "ok" : "wrong");
/* Resolve to foo@@v3 in b.so, instead of foo@v1 or foo@v2. */
int (*fp)(int) = dlsym(RTLD_DEFAULT, "foo");
res = fp (0);
printf ("foo(0) = %d, %s\n", res, res == 3 ? "ok" : "wrong");
/* Resolve to foo@@v3 in b.so, instead of foo@v1 or foo@v2. */
fp = dlsym(RTLD_NEXT, "foo");
res = fp(0);
printf ("foo(0) = %d, %s\n", res, res == 3 ? "ok" : "wrong");
}
eof
echo 'int foo(int a) { return -1; }' > ./b0.c
cat > ./b.c <<'eof'
int foo_va(int a) { return 0; } asm(".symver foo_va, foo@va, remove");
int foo_v1(int a) { return 1; } asm(".symver foo_v1, foo@v1, remove");
int foo_v2(int a) { return 2; } asm(".symver foo_v2, foo@v2, remove");
int foo(int a) { return 3; } asm(".symver foo, foo@@@v3");
eof
echo 'va {}; v1 {} va; v2 {} v1; v3 {} v2;' > ./b.ver
sed 's/^ /\t/' > ./Makefile <<'eof'
.MAKE.MODE := meta curdirOk=1
CFLAGS = -fpic -g
a: a.o b0.so b.so
$(CC) -Wl,--no-as-needed a.o b0.so -ldl -Wl,-rpath=$$PWD -o $@
b0.so: b0.o
$(CC) $> -shared -Wl,--soname=b.so -o $@
b.so: b.o
$(CC) $> -shared -Wl,--soname=b.so,--version-script=b.ver -o $@
clean:
rm -f a *.so *.o *.meta
eof
在glibc 2.36之前,输出是:
% ./a
foo(0) = 0, ok
foo(0) = 3, ok
foo(0) = 0, wrong
从2.36开始,最后一行是正确的。
glibc中升级过的符号
When to prevent execution of new binaries with old glibc总结了何时引入新的symbol version。
注意,binutils 2.35之前的GNU nm不显示@或@@。
nm -D /lib/x86_64-linux-gnu/libc.so.6 | \
awk '$2!="U" {i=index($3,"@"); if(i){v=substr($3,i); $3=substr($3,1,i-1); m[$3]=m[$3]" "v}} \
END {for(f in m)if(m[f]~/@.+@/)print f, m[f]}'
在我的x86-64系统上的输出:
pthread_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。链接器会报错:
ld.lld: error: undefined reference to foo@v1 [--no-allow-shlib-undefined]
如诊断所示,可以添加--allow-shlib-undefined来消除错误。这不推荐,但构建的应用可能碰巧能工作。
对于这种情况,另一个hack的解决方案是:
# 64-bit
cp in.so out.so
rizin -wqc '/x feffff6f00000000 @ section..dynamic; w0 16 @ hit0_0' out.so
llvm-objcopy -R .gnu.version out.so
# 32-bit
cp in.so out.so
rizin -wqc '/x feffff6f @ section..dynamic; w0 8 @ hit0_0' out.so
llvm-objcopy -R .gnu.version out.so
删除.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输出的差异大致是:
0x000000006ffffffb (FLAGS_1) Flags: NOW
- 0x000000006ffffffe (VERNEED) 0x8ef0
- 0x000000006fffffff (VERNEEDNUM) 5
- 0x000000006ffffff0 (VERSYM) 0x89c0
- 0x000000006ffffff9 (RELACOUNT) 1536
0x0000000000000000 (NULL) 0x0ld.lld
- 如果未定义符号没有被shared object定义,GNU ld会报错。ld.lld 12.0之前不会报错(我在https://reviews.llvm.org/D92260中修复了它)。
GNU function attribute
有一个GNU function attribute可以降低为.symver汇编指令。
这个attribute由GCC实现但Clang没有实现。
extern "C" __attribute__((symver("foo@@v2"))) void foo() {}
extern "C" __attribute__((symver("foo@v1"))) void foo_v1() {}
不幸的是,@@@和,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的方案:
// include/features.h
#define __REDIR(x,y) __typeof__(x) x __asm__(#y)
// API header include/sys/time.h
int utimes(const char *, const struct timeval [2]);
__REDIR(utimes, __utimes_time64);
// Implementation src/linux/utimes.c
int utimes(const char *path, const struct timeval times[2]) { ... }
// Internal header compat/time32/time32.h
int __utimes_time32() __asm__("utimes");
// Compat implementation compat/time32/utimes_time32.c
int __utimes_time32(const char *path, const struct timeval32 times32[2]) { ... }
- .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来实作大概是这样:
// API header include/sys/time.h
int utimes(const char *, const struct timeval [2]);
// Implementation src/linux/utimes.c
int utimes(const char *path, const struct timeval times[2]) { ... }
// Internal header compat/time32/time32.h
// Probably __asm__(".symver __utimes_time32, utimes@time32, rename"); if supported
__asm__(".symver __utimes_time32, utimes@time32");
// Implementation compat/time32/utimes_time32.c
int __utimes_time32(const char *path, const struct timeval32 times32[2])
{
...
}
注意.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用的方法是:
// rt/clock-compat.c
__typeof(clock_getres) *clock_getres_ifunc(void) asm("clock_getres");
__typeof(clock_getres) *clock_getres_ifunc(void) { return &__clock_getres; }
libc.so中定义__clock_getres和clock_getres。librt.so中用一个名为clock_getres的ifunc引导到libc.so中的__clock_getres。