Ulrich Drepper和Eric Youngdale在1990+年借鉴Solaris symbol versioning,设计了用于glibc的GNU风格symbol versioning,意图是给shared objects提供backward compatibility:当一个shared object升级须要变更某个符号的行为时,在添加新的符号的同时,保留compatible符号兼容旧的依赖的shared objects。
下面描述version index的取值,然后从assembler、链接器、ld.so几个角度描述symbol versioning行为。
Version index values
Index 0称为VER_NDX_LOCAL
,标记一个定义的符号的binding应改变为STB_LOCAL
。
Index 1称为VER_NDX_GLOBAL
,没有特殊作用,相当于一个unversioned符号。
Index 2到0xffef用于其他versions。
定义的versioned符号有两种形式:
foo@@v2
,称为default version。只有定义的符号可以具有这种形式foo@v2
,称为non-default version,也叫hidden version,其version id设置了VERSYM_HIDDEN
bit
通常只在shared object中定义versioned符号,但可执行档也是可以获得versioned符号的。
(一个shared object更新保留旧符号使其他shared objects不须重新链接,而可执行档通常不提供versioned符号供其他shared objects引用。)
未定义符号只有foo@v2
这一种形式。
Assembler行为
GNU as和LLVM integrated assembler提供实现。
- 对于
.symver foo, foo@v1
- 如果foo未定义,.o中有一个名为
foo@v1
的符号 - 如果foo被定义,.o中有两个符号:
foo
和foo@v1
,两者的binding一致(均为STB_LOCAK
,或均为STB_WEAK
,或均为STB_GLOBAL
),st_other
一致(visibility一致)
- 如果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中有一个名为
个人推荐:
- 定义default-version符号时使用
.symver foo, foo@@@v2
,在.o中只产生foo@@v2
,不产生foo
- 定义non-default符号时在原符号名后加后缀(
.symver foo_v1, foo@v1
)防止和foo
冲突。在.o中会同时有foo_v1
和foo@v1
。目前没有便捷方法去除(通常不想要的)foo_v1
,一般在指定version script时注意把foo_v1
设置为local - 未定义的versioned符号通常是链接时绑定的,object files不须要指定符号。如果确实要引用,推荐
.symver foo, foo@@@v1
,即使能.symver foo, foo@v1
达到相同效果
在.o中,@
是实际出现在symbol table中的。
链接器行为
链接器在读入object files、archive files、shared objects、LTO files、linker scripts等后就进入符号解析阶段。符号解析规则:
- 定义的
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(@
)定义。
(LLD的实现中,看到shared object中的foo@@v1
则在符号表中同时插入foo
和foo@v1
,因此可以满足未定义的foo
和foo@v1
。)
(GNU ld用indirect symbol表示versioned符号,在很多阶段都有复杂的规则。)
在输出的shared object或可执行档中定义version必须指定version script。若所有versioned符号均为未定义状态则无需version script。
Version script有三个用途:
- 定义versions
- 指定一些模式,使得匹配的、定义的、unversioned的符号具有指定的version
- Local version:
local:
可以改变匹配的、定义的、unversioned的符号的binding为STB_LOCAL
,不会导出到dynamic symbol table
在shared objects和可执行档中,对于static symbol table,@
直接出现在符号名中;对于dynamic symbol table,version index信息由一个parallel table .gnu.version
提供。实际的version信息则存储在.gnu.version_d
和.gnu.version_r
中。
Versioned symbols产生方式
对于一个符号,它获得version的可能途径:
- 它是未定义的。该符号须要被某个shared object定义,否则GNU ld会报错
- 它是定义的
- 在.o中符号名形如
foo@v1
或foo@@v1
。Versionv1
须要被version script定义,否则报错 - 原本unversioned,被version script的规则匹配而获得version
- 在.o中符号名形如
ld.so行为
Dynamic table中的DT_VERNEED
和DT_VERNEEDNUM
标识了一个shared object/可执行档需要的外部version定义,及该定义须由哪个soname提供。
如果目标soname包含DT_VERDEF
表但不包含需要的version,且该verneed项不是VER_FLG_WEAK
则报错。
接下来是符号解析阶段。
- 未定义unversioned
foo
可以解析到定义foo
或foo@@v2
(v2的version index应为1(VER_NDX_GLOBAL
)或2) - 未定义versioned
foo@v1
可以解析到定义foo
或foo@v1
或foo@@v1
注意(未定义versioned foo@v1
解析到定义foo
)这种情况是ld.so允许而链接器不允许的。这提供了一种机制:在不阻碍运行时符号解析的情况下拒绝链接旧的符号。
假如某个旧版本shared object定义bar
而希望在新版本废弃这个符号,可以去除bar
而定义bar@compat
。依赖该.so的库中的未定义bar
仍可以解析,但该库无法重新链接。
LLD
LLD的实现有尚有一些不足。
1 | # RUN: not ld.lld a.o b.o |
评价
GCC/Clang支持asm specifier和#pragma redefine_extname
重命名一个符号。比如声明int foo() asm("foo_v1");
再引用foo
,.o中的符号会是foo_v1
。
那么symbol versioning还有什么意义呢?我细细琢磨,有如下优点:
- 在不阻碍运行时符号解析的情况下拒绝链接旧的符号
- version定义可以延迟决定到链接时。链接时的version script提供灵活的pattern matching机制指定versions
- version script中的
local:
可以使符号获得值为VER_NDX_LOCAL
的version,具有把weak/global符号变成local的效果 - 对编译器认识的builtin functions,在GCC/Clang的实现里重命名有一些语义上的问题(符号foo含有内建语义X)2020-10-15-intra-call-and-libc-symbol-renaming
- 对于一个.so,ld.so检查是否所有
DT_VERNEED
需要的versions都存在,不存在则在符号解析前报错
其实可能只有前两条是比较好的。local:
有用,但假如没有version script也可以设计一个其他的类似--version-script
或--dynamic-list
的机制提供该功能。