管理 Linux 系统中的带版本的共享库

前言

之前自己在编译共享库的时候一直就把生成的共享库直接命名成 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 ,同时其他用户程序要想用新版的共享库,也需要重新编译来链接新版的共享库才行。

猜你喜欢

转载自blog.csdn.net/qq_21746331/article/details/120333736