Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

共享库及符号的版本控制实践 #16

Open
chenpengcong opened this issue Jun 20, 2018 · 0 comments
Open

共享库及符号的版本控制实践 #16

chenpengcong opened this issue Jun 20, 2018 · 0 comments

Comments

@chenpengcong
Copy link
Owner

首先了解下共享库的命名约定

共享库的命名规则:libname.so.x.y.z

  • name: 库的名字
  • x: 主版本号(Major Version Number)
  • y: 次版本号(Minor Version Number)
  • z: 发布版本号(Release Vresion Number)

主版本号表示库的重大升级,不同主版本号之间不兼容,旧的接口符号进行了改动
次版本号表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变
发布版本号表示库的一些错误的修正,性能的改进等,并不添加任何新的接口,也不对旧的接口进行修改

共享库的编译和运行过程涉及到3个名称: real name, sonamelinker name

  • real name: 编译出来的库文件的真实名字。比如libfoo.so.1.0.0
  • soname: 描述共享库功能的一个逻辑名称(logic name)。命名规则为libname.so.x,只保留了主版本号,常用于提供版本向后兼容性信息。比如程序a在编译时使用了共享库libx.1.0.0,而现在系统中只包含了libx.1.1.0,那么程序a会根据它所依赖的soname,即libx.1,去使用比编译时更高版本的库libx.1.2.0,因为该库是兼容1.1.0版本的
  • linker name: 链接器使用该名称去寻找共享库,一般不包含版本号。比如使用链接选项-lfoo,那么链接器就会去寻找名称为libfoo.so的文件,libfoo.so就是linker name

通常情况下,linker name软链接到soname, soname软链接到real name

这三个名称会在下面实际例子中体现各自的作用

接下来以一个实际的例子来演示共享库的版本控制实践

say.c

#include "stdio.h"
#include "say.h"
void say_hello()
{
    printf("hello\n");
}

say.h

void say_hello();

main.c

#include "say.h"
int main()
{
    say_hello();
    return 0;    
}

共享库的编译

$ gcc -c -fPIC say.c
$ ld -shared -soname libsay.so.1 -o libsay.so.1.0.0 -lc say.o

这里指定libsay.so.1.0.0的soname为libsay.so.1,soname可以通过查看.dynamic段确认

如果链接时不指定soname那么就没有查看不到SONAME

$ readelf -d libsay.so.1.0.0 
Dynamic section at offset 0xec0 contains 15 entries:
  Tag Type Name/Value
 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
 0x000000000000000e (SONAME) Library soname: [libsay.so.1]
 0x0000000000000004 (HASH) 0x158
 0x0000000000000005 (STRTAB) 0x218
 0x0000000000000006 (SYMTAB) 0x188
 0x000000000000000a (STRSZ) 74 (bytes)
 0x000000000000000b (SYMENT) 24 (bytes)
 0x0000000000000003 (PLTGOT) 0x201000
 0x0000000000000002 (PLTRELSZ) 24 (bytes)
 0x0000000000000014 (PLTREL) RELA
 0x0000000000000017 (JMPREL) 0x290
 0x000000006ffffffe (VERNEED) 0x270
 0x000000006fffffff (VERNEEDNUM) 1
 0x000000006ffffff0 (VERSYM) 0x262
 0x0000000000000000 (NULL) 0x0

共享库的安装和使用

libsay.so.1.0.0库使用者拿到.so和.h文件后,执行以下命令生成可执行程序main

$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ gcc main.c -L. -lsay -o main

ldconfig -n .会生成一个软链接libsay.so.1链接到libsay.so.1.0.0,如果有多个版本,那么该软链接会指向最libsay.so.1.x.y中的最新版本

第二条指令$ ln -sf libsay.so.1 libsay.so是创建一个名为libsay.so软链接,指向libsay.so.1。为什么需要创建libsay.so呢,因为链接时-lsay会去搜索libsay.so,也就是linker name,否则会报找不到库的错。

查看最终生成的可执行程序的依赖信息

$ ldd main
    linux-vdso.so.1 (0x00007fff71525000)
    libsay.so.1 => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff5c8729000)
    /lib64/ld-linux-x86-64.so.2 (0x00007ff5c8cca000)

可以看到执行程序main记录的是共享库的soname,而不是real name或linker name(也可以通过readelf -d查看依赖的共享库),所以装载时会去找名称为soname的库

由于动态链接器查找共享库默认是从/lib/,/usr/lib和/ld.so.conf文件指定的路径中寻找,而我们没有把libsay.so.1拷贝到其中某个路径下,所以输出结果中libsay.so.1显示not found,这里将so库所在路径添加到环境变量LD_LIBRARY_PATH后即可显示,这里我的so文件和可执行文件在同一级目录下,所以执行如下命令

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
$ ldd main
    linux-vdso.so.1 (0x00007fff4c7e2000)
    libsay.so.1 (0x00007fa52b270000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa52aed1000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa52b674000)

共享库的版本升级

当共享库更新版本时,共享库提供者还是按照上面步骤进行编译生成so,而使用者只需要重新执行$ ldconfig -n .就可以让soname指向最新版本

soname机制存在的问题

如下面示例

假设现在代码如下

say.c

#include "stdio.h"
#include "say.h"
void say_hello_v1()
{
    printf("hello(v1)\n");
}

say.h

void say_hello_v1();

进行编译链接,生成libsay.so.1.0.0

然后我增加了了一个say_hello_v2接口,现在代码如下

say.c

#include "stdio.h"
#include "say.h"
void say_hello_v1()
{
    printf("hello(v1)\n");
}

void say_hello_v2()
{
    printf("hello(v2)\n");
}

say.h

void say_hello_v1();
void say_hello_v2();

进行编译链接,生成libsay.so.1.1.0

假设此时用户B在编译可执行程序main的时候,所在的编译环境使用的是libsay.so.1.1.0,且程序使用了say_hello_v2符号,但是main程序的运行环境的机器上却使用了libsay.so.1.0.0,由于程序在进行动态链接时soname只判断主版本号,因此可以正常装载1.0.0版本的库,但是可能造成的后果就是

  • mian程序的执行流程没走到调用say_hello_v2的地方,程序没报错,正常运行
  • main程序的执行流程走到了调用say_hello_v2的地方,程序报找不到符号的错symbol lookup error: ./main: undefined symbol: say_hello_v2,崩溃

很明显这种运行时出现未知行为我们是不允许的,解决这种问题可以通过使用**基于符号的版本控制机制(symbol versioning)**来解决。

基于符号的版本控制

基于符号的版本控制实际上就是让每个导入和导出的符号都有一个相关联的版本

我们为say.c中的接口符号指定版本
say.c

#include "stdio.h"
#include "say.h"

asm(".symver say_hello_v1, say_hello@@VERS_1.0.0");
void say_hello_v1()
{
    printf("hello(v1)\n");
}

say.h

void say_hello();

编写符号版本脚本say.ver

VERS_1.0.0 {
    global:
        say_hello;
    local:
        *;
};

编译链接,生成libsay.so.1.0.0

$ gcc -c -fPIC say.c
$ ld -shared say.o -soname libsay.so.1 --version-script say.ver -lc -o libsay.so.1.0.0

接下来修改say.c添加say_hello_v2接口,并修改符号版本脚本,最后生成libsay.so.1.1.0

#include "stdio.h"
#include "say.h"
asm(".symver say_hello_v1, say_hello@VERS_1.0.0");
void say_hello_v1()
{
    printf("hello(v1)\n");
}
asm(".symver say_hello_v2, say_hello@@VERS_1.1.0");
void say_hello_v2()
{
    printf("hello(v2)\n");
}

say.ver

VERS_1.0.0 {
    global:
        say_hello;
    local:
        *;
};

VERS_1.1.0 {
}VERS_1.0.0;

编译链接

$ gcc -c -fPIC say.c
$ ld -shared say.o -soname libsay.so.1 --version-script say.ver -lc -o libsay.so.1.1.0

接下来编写main.c
main.c

#include "stdio.h"
#include "say.h"
int main()
{
    printf("running...\n");
    say_hello();
    return 0;
}

把main.c和libsay.so.1.1.0在放在同级目录,生成依赖libsay.so.1.1.0的可执行程序main

$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ gcc main.c -L. -lsay -o main

把libsay.so.1.1.0和main拷贝到同一目录下,运行:

$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
$ ./main 
running...
hello(v2)

正常。

接下来把libsay.so.1.0.0和main拷贝到同一目录下,运行

$ ldconfig -n .
$ ln -sf libsay.so.1 libsay.so
$ ./main 
./main: libsay.so.1: version `VERS_1.1.0' not found (required by ./main)

可以看到main程序直接被阻止运行,而不是等到运行时才崩溃,避免了上面提到的运行时出现symbol lookup error的错误。

参考
Shared objects for the object disoriented!
《程序员的自我修养》
soname

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant