4

Symbol versioning | MaskRay

 2 years ago
source link: http://maskray.me/blog/2020-11-26-symbol-versioning
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Symbol versioning

Ulrich Drepper和Eric Youngdale在1990+年借鉴Solaris symbol versioning,设计了用于glibc的GNU风格symbol versioning,意图是给shared objects提供backward compatibility:当一个shared object升级须要变更某个符号的行为时,在添加新的符号的同时,保留compatible符号兼容旧的依赖的shared objects。

下面描述表示方式,然后从assembler、链接器、ld.so几个角度描述symbol versioning行为。初次阅读时不妨跳过表示方式部分。

// 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, always 1 in practice
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 depended 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 or VER_FLG_WEAK; copied from vd_flags of the depended so
Elf64_Half vna_other; // unused
Elf64_Word vna_name; // .dynstr offset of the version name
Elf64_Word vna_next; // offset in bytes to next vernaux entry
} Elf64_Vernaux;

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中有两个符号:foofoo@v1,两者的binding一致(均为STB_LOCAK,或均为STB_WEAK,或均为STB_GLOBAL),st_other一致(visibility一致)。个人认为这个行为是设计缺陷
  • 对于.symver foo, foo@@v1
    • 如果foo未定义,assembler报错
    • 如果foo被定义,.o中有两个符号:foofoo@@v1,两者的binding和st_other一致
  • 对于.symver foo, foo@@@v1
    • 如果foo未定义,.o中有一个名为foo@v1的符号
    • 如果foo被定义,.o中有一个名为foo@@v1的符号

个人推荐:

  • 定义default-version符号时使用.symver foo, foo@@@v2,在.o中只产生foo@@v2,不产生foo
  • 定义non-default符号时在原符号名后加后缀(.symver foo_v1, foo@v1)防止和foo冲突。在.o中会同时有foo_v1foo@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可以同时满足未定义的foofoo@v1

若存在多个default version的定义(如foo@@v1 foo@@v2),触发duplicate definition error。通常一个符号有零或一个default version(@@)定义,任意个non-default version(@)定义。

(LLD的实现中,看到shared object中的foo@@v1则在符号表中同时插入foofoo@v1,因此可以满足未定义的foofoo@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(DT_VERSYM)提供。实际的version信息则存储在.gnu.version_d(DT_VERDEF)和.gnu.version_r(DT_VERNEED)中。

假如一个ld.so不支持symbol versioning(忽略DT_VERSYM,DT_VERDEF,DT_VERNEED),那么它能够继续工作,就好像所有符号都没有version一样。musl ld.so就属于此类。

Versioned symbols产生方式

对于一个符号,它获得version的可能途径:

  • 它是未定义的。该符号须要被某个shared object定义,否则GNU ld会报错
  • 它是定义的
    • 在.o中符号名形如foo@v1foo@@v1。Version v1须要被version script定义,否则报错
    • 原本unversioned,被version script的规则匹配而获得version

ld.so行为

Dynamic table中的DT_VERNEEDDT_VERNEEDNUM标识了一个shared object/可执行档需要的外部version定义,及该定义须由哪个shared object(Vernaux::vna_name)提供。
如果该Vernaux项(附属于Verneed)没有VER_FLG_WEAK标志,且目标shared object中DT_VERDEF表存在但没有定义需要的version,报错。

接下来是符号解析阶段。

  • 未定义unversioned foo可以解析到定义foofoo@@v2(v2的version index应为1(VER_NDX_GLOBAL)或2)
  • 未定义versioned foo@v1可以解析到定义foofoo@v1foo@@v1

注意(未定义versioned foo@v1解析到定义foo)这种情况是ld.so允许而链接器不允许的[link]。这提供了一种机制:在不阻碍运行时符号解析的情况下拒绝链接旧的符号。
假如某个旧版本shared object定义bar而希望在新版本废弃这个符号,可以去除bar而定义bar@compat。依赖该.so的库中的未定义bar仍可以解析,但该库无法重新链接。

LLD的实现有尚有一些不足。

# RUN: not ld.lld a.o b.o
# RUN: ld.bfd a.o b.o

//--- a.s
.symver foo, foo@@@v1
.globl foo
foo:
//--- b.s
.symver foo, foo@@@v1
call foo

去除symbol versioning

llvm-objcopy -R .gnu.version -R .gnu.version_d -R .gnu.version_r in.so out.so

执行该命令后,链接时使用out.so即可防止输出引用in.so定义的versions。

llvm-objcopy会把删除的sections在文件中的空间清零,因此这样得到的out.so不可用于运行时。
若须用于运行时,可以把dynamic table中的DT_VER*删除。

  0x000000006ffffffb (FLAGS_1)            Flags: NOW
- 0x000000006ffffffe (VERNEED) 0x8ef0
- 0x000000006fffffff (VERNEEDNUM) 5
- 0x000000006ffffff0 (VERSYM) 0x89c0
- 0x000000006ffffff9 (RELACOUNT) 1536
0x0000000000000000 (NULL) 0x0

整个方案就是:

cp in.so out.so
r2 -wqc '/x feffff6f00000000 @ section..dynamic; w0 16 @ hit0_0' a.so
llvm-objcopy -R .gnu.version -R .gnu.version_d -R .gnu.version_r out.so

GCC/Clang支持asm specifier和#pragma redefine_extname重命名一个符号。比如声明int foo() asm("foo_v1");再引用foo,.o中的符号会是foo_v1

举个例子,musl v.1.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(cosnt 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]) { ... }
  • 内部实现中“好看的”名字utimes表示time64定义;“难看的”名字__utimes_time32表示deprecated time32定义
    • 假如time32实现被其他函数调用,那么用“难看的”名字能清晰地标识出来“此处和deprecated time32定义有关”
  • Public header用asm specifier,同时作用与内部实现和对外的API
    • time32实现作为ABI而不是公开的API存在。用户若include public header,一般无法链接到__utimes_time32
    • 缺点是倘若用户自行声明utimes而不include public header,会得到deprecated time32定义。这种自行声明的方式是不推荐的
  • .o中,time32定义仍为utimes,提供ABI兼容旧程序;time64定义则为__utimes_time64

对于上述的例子,用symbol versioning来实作大概是这样:

// API header include/sys/time.h
int utimes(cosnt 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不可用@@@,因为我们希望定义一个non-default version。
根据前文对Assembler行为的讨论,不如意的地方是:定义的translation unit中,__utimes_time32这个符号也存在。链接时注意用version script localize它。

那么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都存在,不存在则在符号解析前报错

其实可能只有前两条是比较好的。
对于第一条,asm specifier的方案用约定来避免意外链接(用户不应自行链接__utimes_time32);而symbol versioning可以用ld强制。
local:有用,但假如没有version script也可以设计一个其他的类似--version-script--dynamic-list的机制提供该功能。

设计缺点:

  • .symver foo, foo@v1foo被定义时的行为:保留符号foo(链接时有个多余的符号)、binding/st_other保持同步(不方便设置不同的binding/visibility)
  • Verdaux有点多余。实践中一个Verdef只有一个Verdaux
  • Verneed/Vernaux的结构绑定了提供version定义的shared object的soname,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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK