共享库版本

兼容性

共享库的更新可能兼容可能不兼容。

以前还以为这种库提供的接口只是属于API层面,现在看看其实这些接口属于ABI层面。因为这些库(以ELF文件形式存在的动态链接文件)其中的一些操作,比如参数传递方式、虚函数表、多重继承等都给出了明确定义,但这些东西在库的使用者电脑上可能是完全不同的,这就可能导致兼容性问题。

其实想想啊,如果不是以共享库的形式,而是以源代码的形式提供给用户,是不是接口就只是在API层面,就不用考虑ABI层面的兼容问题了呢?

共享库命名方式

  • 主版本,不同号码,可以不兼容
  • 次版本,高级的可以兼容低级的,一般是增加一些接口
  • 发布版本号,一般是修改库的错误,性能,不提供新接口

不遵守规则的库,比如 /lib/x86_64-linux-gnu/libc-2.31.so

SO-NAME

Linux使用一种SO-NAME的方式来记录共享库的依赖关系。SO-NAME就是去掉第二个和第三个版本号剩下的部分。系统会为每个共享库在他的目录创建一个和SO-NAME相同的软链接指向该库。标准格式:libfoo.so.2.6.1

这些库这么喜欢标新立异吗

1
2
3
4
5
6
7
8
9
lrwxrwxrwx 1 root root      10 5月   1  2024 ld-linux-x86-64.so.2 -> ld-2.31.so
lrwxrwxrwx 1 root root 14 5月 1 2024 libanl.so.1 -> libanl-2.31.so
lrwxrwxrwx 1 root root 17 2月 18 2020 libaudit.so.1 -> libaudit.so.1.0.0
lrwxrwxrwx 1 root root 17 4月 9 2024 libblkid.so.1 -> libblkid.so.1.1.0
lrwxrwxrwx 1 root root 18 4月 27 2021 libbrlapi.so.0.6 -> libbrlapi.so.0.6.6
lrwxrwxrwx 1 root root 18 3月 4 2020 libbrlapi.so.0.7 -> libbrlapi.so.0.7.0
lrwxrwxrwx 1 root root 23 5月 1 2024 libBrokenLocale.so.1 -> libBrokenLocale-2.31.so
lrwxrwxrwx 1 root root 15 9月 5 2019 libbz2.so.1 -> libbz2.so.1.0.4
lrwxrwxrwx 1 root root 15 9月 5 2019 libbz2.so.1.0 -> libbz2.so.1.0.4

这个SO-NAME其实会指向目录中主版本号相同、次版本号和发布版本号最新的共享库。这使得以来某个共享库的模块在编译链接运行时,都使用共享库的SO-NAME, 而不需要详细的版本号。

之前讲过,动态链接文件的.dynaamic有DT_NEED类型的字段,如果值为B,则代表依赖于B,运行时需要将B加载到内存。但如果直接存储B的全称,不太方便维护库(只依赖于某个具体版本的库,变一个数字都不行),所以改为存储共享库的SO-NAME, 使用SO-NAME保证了兼容性,同时也能指向最新的共享库,可以避免维护太多库。

Linux提供了ldconfig这个工具,当安装或更新共享库时,他会遍历所有的默认共享库目录如/lib, /usr/lib, 然后更新所有的软链接,使其指向新的共享库。

符号版本

前面的SO-NAME有一个问题,那就是只判断主版本号。如果一个程序依赖于1.8.1版本的库,但是一个电脑中只有1.1.1版本的库,那么因为SO-NAME指向了这个1.1.1的库,程序表面上能正常运行。但如果程序使用了1.8.1存在的函数接口但这些接口在1.1.1不存在,就会出错。这是次版本交会问题

基于符号的版本控制

给每个新添加的符号也增加个版本标记,如果链接发时现机器上的中符号的版本过低,会报错

共享库存放路径

许多Linux在内的操作系统都遵循FHS标准.一个系统有三个存放动态库的地方:

共享库查找过程

动态连接的ELF启动过程中,当系统调用返回时会返回到动态链接器继续执行,动态链接器会根据.dynamic段中的地址进行查找库.

  • 如果DT_NEED保存的绝对地址,那么就找这个地址
  • 如果是相对地址,那么动态链接器会在/lib, /usr/lib/etc/ld.so.conf 配置文件指定的目录中查找共享库

如果每次执行时都需要动态链接器去找这些文件,会费耗时,所以系统有个名为ldconfig的程序,他会

  • 及时更新,创建,删除SO-NAME
  • 收集SO-NAME,并存放到/etc/ld.so.cache中,这个文件非常适合查找.(如果在这个文件中没找到才会遍历文件夹进行寻找)

所以理论上往系统的共享库目录下添加、删除、更新任何一个共享库,都应该执行ldconfig,来更新SO-NAME 和 ld.so.cache.

环境变量

每个进程会有一个从父进程继承来的环境变量(环境变量存放在每个进程中,每个进程可以有不同的变量)。BASH进程启动时,也会从一些文件中加载全局变量,比如:

  1. 全局环境变量:这些环境变量对系统中的所有用户和所有进程都是可用的。它们在 /etc/profile/etc/environment 或其他系统级别的配置文件中设置。
  2. 用户环境变量:这些环境变量只对特定的用户可用,通常在用户的家目录下的 .bash_profile.bashrc.profile 文件中设置。
  3. 局部环境变量:这些环境变量只在特定的进程或脚本中有效,可以通过在命令行中使用 export 命令来设置。

通常要让应用程序在包含某些路径的环境变量下执行,不是直接设置这些应用程序的环境变量,而是在bash进程中先把环境变量设定为期望的值,然后再bash中启动进程。(这样应用程序会继承父进程即bash进程的环境变量,巧妙)

LD_LIBRARY_PATH

改变共享库查找路径最简单的方式就是修改这个变量,这个路径有若干个路径组成,之间有冒号隔开。

动态链接器会按照如下路径查找共享对象:(如果是绝对地址,那么直接访问就行了)

  • LD_LIBRARY_PATH指定的路径
  • ldconfig维护的/etc/ld.so.cache指定的路径
  • 默认共享库目录,比如/usr/lib,/lib

LD_DEBUG

这个变量可以打开动态链接器的调试功能,动态链接器在运行时会打印很多调试信息。比如将LD_DEBUG设置为files,然后运行一个动态连接的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
moveai415:~/cpp/effective_cpp$ LD_DEBUG=files ./ab
2805656:
2805656: file=libc.so.6 [0]; needed by ./ab [0]
2805656: file=libc.so.6 [0]; generating link map
2805656: dynamic: 0x00007fa3da898b80 base: 0x00007fa3da6ad000 size: 0x00000000001f1660
2805656: entry: 0x00007fa3da6d11c0 phdr: 0x00007fa3da6ad040 phnum: 14
2805656:
2805656:
2805656: calling init: /lib/x86_64-linux-gnu/libc.so.6
2805656:
2805656:
2805656: initialize program: ./ab
2805656:
2805656:
2805656: transferring control: ./ab
2805656:
2805656:
2805656: calling fini: ./ab [0]
2805656:

共享库的创建和安装

共享库的创建

前面用-shared, -fPIC创建过共享对象,共享库的创建也非常类似。最关键的一点是需要给共享库指定一个soname,可以向链接器传递-soname my_soname来指定(不然ldconfig对该共享库没有效果),如果要是有GCC来编译,那么前面还需要加一个-W1

gcc -shared -W1,-soname, my_soname -o library_name source_files library_files

  • 要想测试一下共享库,又不影响其他程序的运行,可以设置待测试进程bash进程LD_LIBRARY_PATH, 也可以使用连接器的-rpath选项来指定共享库的查找路径。
  • 假设一个进程由一个主模块和其他共享模块组成,默认情况下,链接器在生成可执行文件时,只会将主模块中被其他共享模块使用到的符号放到动态符号表。如果程序使用dlopen动态加载模块,可能导致没法访问主模块的符号(按道理说,链接时不会去检查符号吗),可以使用-export-dynamic参数来导出所有的全局符号。

清除符号信息

可以用strip来删除共享库或者可执行文件中的所有符号和调试信息。但还是非保留需要动态链接相关的符号。

1
2
3
4
5
删除前:
Symbol table '.dynsym' contains 7 entries:
Symbol table '.symtab' contains 66 entries:
删除后:
Symbol table '.dynsym' contains 7 entries:

共享库的安装

  • 最简单的方法:将库复制到标准的共享库目录,然后运行ldconfig即可,但这需要root权限。

  • 稍复杂的方法:

    • 建立SO-NAME软链接(通过ldconfig -n your_directory 来建立)
    • 然后告诉编译器和程序如何查找该库(-L, -l参数以及-rpath参数以及LD_LIBRARY_PATH)。

共享库构造以及析构函数

定义如下的构造/析构函数,可以在共享库加载时(main之前)被执行构造函数,可以在main执行完毕后执行析构函数。