前言
之前自己在编译共享库的时候一直就把生成的共享库直接命名成 libxxx.so 的形式,最近遇到需要进行共享库版本管理的问题,发现之前对于 Linux 系统对共享库管理的方式的掌握成都已经不再够用了,所以接下来记录 Linux 系统中管理共享库的解决方法。
0x1 约定俗称的命名方式
在 Linux 上对共享库的命名采用 libxxx.so.a.b.c 的格式,其中 a 代表主版本号,b 代表次版本号,c 代表发布版本号,其中发布版本号一般是可选的。而因此动态库就有了三种名字:
- linker name:顾名思义,这个名字是链接器链接共享库所用到的名字,其格式为 libxxx.so ,也就是说其不带任何版本号。在编译选项中通过 -lxxx 来指定依赖库,链接器就会去指定好的路径中搜索 libxxx.so,作为链接使用。
- soname:soname 是一个很重要的名字,其格式为 libxxx.so.a,也就是在 linker name 后面加上主版本号,其具体作用我们后续再讨论。
- real name:顾名思义,这就是共享库的真实名称,传统意义上来说,一个共享库的真实名称应该是 libxxx.so.a.b.c 的格式,但实际上并不一定。
我们可以查看 Linux 中的一些库文件。比如说 c 语言的标准库:
可以发现其 soname 仅仅是一个软链接,指向真正的标准库文件 libc-2.31.so,这就是其 real name,而其 real name 也不是完全符合命名格式,所以说,real name 也可以不符合上述的命名方式。
再看看管道库:
可以发现这个共享库就满足了上述的命名格式。
0x2 查看可执行文件依赖的共享库
通过 ldd
命令可以查看可执行文件以来的共享库。
可以看到常用的 ls
命令依赖的共享库,且其名称都是用的 soname !!!! 这里埋下一个伏笔,想想为什么用的都是 soname ? 等文章末尾再做出解答。
0x3 创建有版本号的共享库
假设我们要将以下函数创建成共享库:
/// shared.c
#include <stdio.h>
void versionControl()
{
printf("Now, the version of shared is 0.0.1\n");
}
/// shared.h
void versionCOntrol();
通过一个用户程序来调用它:
#include <stdio.h>
#include "shared.h"
int main(void)
{
versionControl();
return 0;
}
采用如下命令进行编译:
gcc shared.c -fPIC -shared -Wl,-soname,libshared.so.0 -o libshared.so.0.0.1
可以看到生成了一个共享库 libshared.so.0.0.1 。
注意这条编译命令其中的参数 -Wl,-soname,libshared.so.0
,这个命令就是告诉链接器,指定所生成的共享库的 soname。其中,soname 是直接被保存在共享库的二进制文件中的。可以查看如下:
这样,通过 ldconfig
命令则可以为刚刚编译出来的共享库生成一个软链接,这条软链接正是从 soname 指向 real name。
[注] 在 Linux 中编译共享库的时候一般都会指定其 soname ,在编译好后用
ldconfig
命令为其生成软链接和刷新缓存文件 /etc/ld.so.cache(加上 -n 选项表示只处理当前指定的目录,而且不刷新缓存)。这个软链接的生成就是依赖已经保存到共享库中的 soname,而不仅仅是简单的截断共享库名。
0x4 使用共享库
现在共享库被创建出来了,我们可以编译 main.c 来使用共享库。
发现报了链接错误,无法找到链接库。这是因为在链接的时候寻找的库的 linker name,也就是 libshared.so,这自然无法找到了。所以我们可以创建一个软链接,让 linker name 指向 soname ,再编译,就可以编译成功了,如下:
但此时还不能运行 main 程序,因为装载程序无法找到共享库。用 ldd
查看其依赖的共享库,发现 libshared.so.0 没有找到。
这是因为装载程序搜索的路径不包含当期的文件夹,因为前面用的 ldconfig
命令加上了 -n 选项,所以没有刷新缓存。这里只需要用 ldconfig
命令刷一下缓存,将当前的目录也配置到告诉缓存就行。
执行成功,mian 输出了我们的版本号为 0.0.1。而 main 所依赖的共享库用的都是 soname,这就说明了在编译的时候,编译器就将 soname 记录到了可执行文件中。
0x5 共享库的更新
假设我们需要将库添加一个功能升级一个版本,但这个新版本是与之前版本兼容的,修改库里面的输出版本信息的函数如下:
#include <stdio.h>
void versionControl()
{
printf("Now, the version of shared is 0.0.2\n");
}
然后重新编译共享库,因为仍然是兼容的,所以不改变 soname 的名称。并且用 ldconfig
命令更新 soname 到 real name 的软链接。
可以看到生成了一个新版的共享库 libshared.so.0.0.2,并且 soname 也指向了他,所以在不重新编译 main 目标的情况下,运行它发现输出了新版的版本信息,也就是用了新版的共享库!!
假设我们对库有一次比较大的变更,导致新版本不再兼容了,这就需要修改 soname 了,由于可执行文件中保留的还是之前版本的 soname,所以原本的可执行文件是无法使用新版的共享库的,这时候就需要对可执行文件进行重新编译,链接到新版的共享库。
0x6 总结
到目前为止,我们就大概明白了 Linux 系统下管理共享库的方向,可以总结出一些要点如下:
- 首先就是 soname,这是很重要的名称!它就像一个桥梁,不仅仅在共享库中会指定它(在编译的时候加入编译选项),而且在使用共享库的可执行文件中也会指定它(在编译的时候编译器会顺着linker name 找到 real name,然后从共享库中取出 soname),通过
ldd
命令查看可执行文件的依赖库,共享库显示的都是 soname 的名称。 - 正是因为 soname 的桥梁作用,使得共享库的小更新(不影响版本兼容性)不需要重新编译可执行文件,只需要在更新共享库后用
ldconfig
命令重新自动生成 soname 到 新版本 real name 的软链接,原来的可执行文件即可以自动加载新版的共享库。 - 如果共享库的版本发生重大更新,导致了不兼容性,则需要指定新的 soname,原来可执行文件想要用到新的共享库,也必须重新编译,这是因为原来编译的可执行文件中保存了旧的 soname 的索引,这个索引是装载程序为其装载共享库的依据。
- 在我们进行共享库升级管理的时候,如果更新不影响与之前版本的兼容性,就仅仅更新次版本号和发布版本号,用
ldconfig
命令重新生成软链接即可。而如果更新影响了兼容性,则需要更新主版本号,改变库的 soname ,同时其他用户程序要想用新版的共享库,也需要重新编译来链接新版的共享库才行。