Shared Libraries Manage

动态库是从程序外部加载的,未在编译时集成到程序自身当中。

情形:

error while loading shared libraries

Linux中的.so文件类似于Windows中的DLL,是动态链接库,也有人译作共享库(因so的全称为Shared Object)。当多个程序使用同一个动态链接库时,既能节约可执行文件的大小,也能减少运行时的内存占用。

目录结构

存放位置

Linux中绝大多数.so文件都存放在/lib64/usr/lib64/,对于64位和32位共存的系统,32位的动态库可能会放在/lib32/usr/lib32,完整的动态库存放路径列表可通过/etc/ld.so.conf文件配置。(如果修改了配置,需要用 /sbin/ldconfig 命令更新缓存)

应注意动态库搜寻路径并不包括当前文件夹,所以当即使可执行文件和其所需的so文件在同一文件夹,也会出现找不到so的问题,如

此时可用LD_LIBRARY_PATH环境变量做临时设置,如:

也有些so文件是在程序执行时临时加载的(如插件),它们的路径就比较灵活,只要可执行文件能找到它就行了。

程序链接的动态库

 

版本管理

动态库的版本总是个问题,如果编译时链接的库和执行时提供的不一样,难免会导致程序的执行发生诡异的错误。为解决此问题,Linux系列的做法是这样的:

首先,每个so文件有一个文件名,如libABC.so.x.y.z,这里ABC是库名称,x.y.z是文件的版本号,一般来说:

在系统中,会存在一些符号链接, 如:

其中第一个主要在使用该库开发其它程序时使用,比如gcc想连接PAM库,直接连libpam.so就行了,不必在链接时给出它的具体版本。第二个则主要用在运行时,因为前面说了第一位版本一样的库是互相兼容的,所以程序运行时只要试图连接libpam.so.0就够了,不必在意其具体的版本。ldconfig可以自动生成这些链接。

那么编译程序时gcc在链接一个so文件(如libpam.so)时,如何知道该程序运行时链接哪个文件呢(上例中是libpam.so.0)?原来产生so文件时,可以指定一个soname,一般形如libABC.so.x。人们编译可执行文件时,如果链接了某个so,存在可执行文件里的.so文件名并不是其全名,而是这个soname。比如上例中,这个soname就是libpam.so.0。回头看一下上节ldd的结果,可以印证这一现象。

有时还会看到形如libABCn.so,即版本号出现在.so前面的库文件,如libXaw6.so。此类文件一般是为开发者着想,比如GTK+ 3已经推出,但很多开发者还是想用GTK+ 2开发软件。由于编译时只连接无版本号的.so文件,就只有把版本号放在.so前面了。

 

程序动态库管理

查询程序本身是否有外部的动态库依赖

windows平台依赖查询

在windows上默认安装时没有专门的命令或工具,可通过 dumpbin.exe(需要安装了vs studio之后,它提供此)或其它第3方工具,比如 Dependencies1,此处以它为示例说明:

以mysqld.exe安装程序为例将其拖入主程序界面,即显示依赖列表:由于在当前的系统中缺少vcdist可以发现mysqld.exe在5.7版本时显示缺少MSVCR120/MSVCP120,此时程序执行会报错。而这2个dll是在[Microsoft Visual C++ Redistributable Packages for Visual Studio 2012中提供的,需要分别安装。而,另一个情况:发现mysqld.exe在8.0版本时显示依赖正常,表示在在当前的系统依赖已经存在,满足程序执行的依赖环境。

image-20230119145841969

安装依赖或从其它OS中直接提取将C:\Windows\System32\MSVCR120.dllC:\Windows\System32\MSVCP120.dll放到当前系统同样路径下,刷新Dependencies界面,即可看到依赖依赖问题处理掉了。(注意:有些情况下,可能系统通过其它的环境变量将依赖处理掉了,但Dependencies程序可能只搜索并显示了位于C:\Windows\System32\下面的,最终结果是以程序执行不报错为准。)

image-20230128142237167

另,如下图:当前系统win11中检测mysqld.exe v8.0.21则无依赖问题,表明最新的系统环境满足依赖条件。这里体现了操作系统发行版与应用程序的兼容性一致。同时期的操作系统和应用往往是开发者优先测试的。

image-20230119150135239

 

 

 

在linux对比

linux系统中可以使用ldd命令查看,比如ldd在rhel系列分支系统中在glibc-common软件包中提供的,是默认安装了的,ldd命令可以直接使用。

 

通过软件包管理查询rpm安装过程中提供的so信息

 

 

手动编译的so文件

可在编译时指定路径,或将统计完成的文件进行移动。

 

扩展

为了区分开发编译中的类似命令,这里罗列部分。

ld.so或ld-linux.so是系统级的加载器或连接程序,它可以间接运行或直接运行。其中,间接运行是指被其它动态库或程序调用,此时,无参数可以跳过,而在ELF中一般存储在程序执行过程的.interp章节中。 注:ELF可执行和可链接格式(Executable and Linkable Format,缩写为ELF),常被称为ELF格式,在计算机科学中,是一种用于执行档、目的档、共享库和核心转储的标准文件格式。 执行语法示例: /lib/ld-linux.so.* [OPTIONS] [PROGRAM [ARGUMENTS]]

加载和动态链接 从编译/链接和运行的角度看,库分为动态链接和静态链接。相应的两种不同的ELF格式映像:

1)一种是静态链接的,在装入/启动其运行时无需装入函数库映像、也无需进行动态连接。

2)另一种是动态连接,需要在装入/启动其运行时同时装入函数库映像并进行动态链接。

Linux内核既支持静态链接的ELF映像,也支持动态链接的ELF映像,GNU规定:

1)把ELF映像的装入/启动入在Linux内核中;

2)把动态链接的实现放在用户空间(glibc),并为此提供一个称为”解释器”(ld-linux.so.2)的工具软件,而解释器的装入/启动也由内核负责。

通过LD_ASSUME_KERNEL设置Linux的线程实现模型 为了实现向后兼容,许多Linux发行版支持旧的LinuxThreads实现以及新的本地POSIX线程库(NPTL)。通过设置LD_ASSUME_KERNEL环境变量,动态链接器将假定它是运行在一个特定的内核版本上。这将覆盖程序在运行时,动态链接器默认选择的线程实现(通常是NPTL),而强行使用旧的LinuxThreads实现。

/lib/tls/libc.so.6: 最低的ABI = 2.4.20 本机POSIX线程库(NPTL)

/lib/i686/libc.so.6: 最低的ABI = 2.4.1 标准的LinuxThreads,动态线程栈大小 /lib/libc.so.6

Native POSIX Threads Library by Ulrich Drepper $ /lib/i686/libc.so.6 | grep [T|t]hreads linuxthreads-0.10 by Xavier Leroy $ /lib/libc.so.6 | grep [T|t]hreads linuxthreads-0.10 by Xavier Leroy

大多数程序并不关心底层的库是哪种实现,这样即便设置 LD_ASSUME_KERNEL变量也没作用。因为库的版本不同,但提供的API相同。但也有些应用程序因某些不符合规范的操作而需要使用LinuxThreads线程实现,NPTL线程实现会导致运行失败。这种情况下就可通过 LD_ASSUME_KERNEL环境变量实现动态链接器跳过NPTL库的目的。

ldd

ldd2是一个脚本,它是对/lib/ld-linux.so.2 /lib64/ld-linux-x86-64.so.2 /libx32/ld-linux-x32.so.2的封装。ldd脚本中有详细的说明注释信息。

而上面的库也不一定都存在,比如centos7.9中以/lib64/ld-linux-x86-64.so.2存在,你可以直接使用库来代替,如下:

注意:

ldd的标准版本与glibc2一起提供。Libc5与老版本以前提供,在一些系统中还存在。在libc5版本中长选项不支持。另一方面,glibc2版本不支持-V选项,只提供等价的--version选项。

如果命令行中给定的库名字包含'/',这个程序的libc5版本将使用它作为库名字;否则它将在标准位置搜索库。运行一个当前目录下的共享库,加前缀"./"。 错误: ldd不能工作在a.out格式的共享库上。 ldd不能工作在一些非常老的a.out程序上,这些程序在支持ldd的编译器发行前已经创建。如果你在这种类型的程序上使用ldd,程序将尝试argc = 0的运行方式,其结果不可预知。

Linux 的先辈 Unix 还有一个环境变量:LD_LIBRARY_PATH 来处理非标准路经的共享库。ld.so 加载共享库的时候,也会查找这个变量所设置的路经。

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./lib export LD_LIBRARY_PATH

但是,有不少声音主张要避免使用 LD_LIBRARY_PATH 变量,尤其是作为全局变量

开发时,设置LIBRARY_PATH,以便gcc能够找到编译时需要的动态链接库。 发布时,设置LD_LIBRARY_PATH,以便程序加载运行时能够自动找到需要的动态链接库。

ld

ld命令是二进制工具集GNU Binutils3的一员,是GNU链接器,用于将目标文件与库链接为可执行程序或库文件。

ld命令支持众多链接选项,但是大部分选项很少被使用,下面是GNU ld命令接受的选项。

示例

 

ldconfig

ldconfig是一个动态链接库管理命令,其目的为了让动态链接库为系统所共享,做的是刷新动态链接库。

默认搜寻/lilb和/usr/lib,以及配置文件/etc/ld.so.conf内所列的目录下的库文件。

搜索出可共享的动态链接库,库文件的格式为:lib*.so.,进而创建出动态装入程序(ld.so)所需的连接和缓存文件。

缓存文件默认为/etc/ld.so.cache,该文件保存已排好序的动态链接库名字列表。

ldconfig通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令。

 

 

理解$LD_LIBRARY_PATH与$PATH

Windows程序在start时,同Linux一样,要将动态库load进来,Linux会查找LD_LIBRARY_PATH, Windows会查找环境变量PATH,以下是各平台的查找路径:

 

这些只是各平台会查找的其中一个路径,如Solaris就还会查找RPATH。 这不是这里要说的重点,在Linux及Unix平台上,程序在start时,如果查找到一个名字匹配,但格式不匹配的动态库,将报错,并退出,如32bit程序在LD_LIBRARY_PATH里面先找到了一个64bit的动态库,将报一个类似XXX.so: wrong ELF class: ELFCLASS64的错误,然后程序退出。 Windows平台不一样,使用Sysinternals utilities中的Procmon[Process Monitor ]工具可以看到,即使在PATH路径里面找到一个错误位数的dll,程序也会尝试将它读进来,并且从Procmon里看不到错误信息,但可以推测它load不成功,因为从Procmon里面看到程序继续查找PATH里面的其它路径,尝试去找到正常的dll,如果后面的路径有一个位数匹配的dll,程序就能load进来,成功运行起来。

附录

 

引用

 


2 https://www.man7.org/linux/man-pages/man1/ldd.1.html "[ldd - print shared object dependencies]"
3 https://www.gnu.org/software/binutils/ "[The GNU Binutils are a collection of binary tools]"