1. 什么是库

库文件一般就是编译好的二进制文件 , 用于在链接阶段同目标代码一起生成可执行文件 , 或者运行可执行文件的时候被加载 , 以便调用库文件中的某段代码。库文件无法直接执行 , 因为它的源代码中没有入口主函数 , 而只是一些函数模块的定义和实现 , 所以无法直接执行。程序库使程序更加模块化 , 重新编译更快 , 更新更容易。

  • 说起库 , 对于软件开发人员来说都不陌生 , 而且应该是必须掌握的一项技术。在windows平台和linux平台下都大量存在着很多库。因此库文件是为了方便升级、维护或二次开发 , 而发布的一组可以单独与应用程序在编译时或运行时链接的二进制可重定位目标码文件。

  • 实际开发中我们所编写的程序需要依赖很多基础的底层库 , 因此库的存在有很大的意义 , 避免每次编码都要从头开始

  • 本质上库是可执行代码的二进制形式 , 这个文件可以在编译时由编译器直接链接到可执行程序中 , 也可以在运行时根据需要动态加载到内存中

  • WindowsLinux系统的本质不同 , 所以这两个系统库的格式不同 , 同样也是不兼容的 , 本文不讲Windows下的库 , 我们只关注Linux下的程序库

  • 例如我们常用的标准C/C++库、Qt库、GTK库等

2. 库的种类

为了便于理解 , 将程序库可以分为三种类型:静态库、共享库和动态加载(DL)库

2.1 静态库

Linux下静态库以.a结尾的库文件

  • 静态库实际上是一些目标文件的集合 , 在生成可执行文件阶段进行链接。Linux下编译源码时源文件经过编译生成.o的目标文件 , .o的目标文件在链接阶段经过链接生成可执行程序。所以在链接阶段可以链接.o的目标文件 , 也可以把所有.o的目标文件进行打包 , 统一进行链接 , 因此打包.o文件生成的文件 , 就是静态库

  • 静态库只在程序链接阶段被链接使用 , 链接器会将程序中使用到代码段和数据段从库文件中拷贝进来。当链接完成并生成可执行程序后 , 在程序执行阶段就不需要静态库了。因为使用静态库的应用程序需要拷贝所用到的代码段、数据段等 , 所以链接静态库生成的可执行程序会增大。当多个程序连接相同的静态库时 , 运行时所占用内存空间较大 , 但是由于程序运行的时候不再动态加载静态库 , 所以速度相比于共享库会快一些。

2.2 共享库

Linux下共享库以.so结尾的库文件

共享库在程序链接的时候不会像静态库那样从库中拷贝使用的代码段和数据段到生成的可执行程序中 , 而只是做相应的标记 , 在程序开始执行时 , 动态地加载所需的库。因此 , 可执行程序在运行的时候需要共享库的支持。调用共享库的可执行程序比静态库链接出来的可执行程序要小 , 当多个程序调用共享库时 , 运行时所占用内存空间比静态库的方式要小。

2.2.1 共享库命名

Linux系统中我们经常看到同一个共享库还有软连接文件指向共享库。一般来说一个共享库有三个名字 : sonamereal-namelinker-name

  • soname是一个软连接 , 用来区分版本的名字 , 如果real-name文件存在的话 , 它是指向real-name的软链接文件 , 名称的形式一般是lib*.so.X.Y(其中X , Y就是代表版本号) , 每当接口改变时它都会递增。在工作系统上 , 完全限定的 soname 只是指向共享库“真实姓名”的符号链接

  • real-name每个共享库还有一个“真实名称” , 即包含实际库代码的文件名。真实姓名在soname上加上一个小数点、一个小号、另一个小数点和发布号。最后期间和版本号是可选的。次要编号和版本号通过让你确切知道安装了哪些版本的库来支持配置控制。请注意 , 这些数字可能与文档中用于描述库的数字不同

  • linker-name是传递给连接器的名字 , 应用程序调用时用于链接的搜索 , 一般它可能就是指向soname的连接 , 名称的形式一般是lib*.so。换句话说 , 它只是没有任何版本号的soname

Linux系统上这样做的目的主要是系统中允许不同版本的库文件共存 , 一般在命名库文件的时候通常与soname相同

image-202508052218

2.2.2 如何装载共享库

  • ldconfig命令 , 在Linux中 , 运行一个可执行程序时 , 程序装载器会被自动装载并运行。这个程序装载器就是/lib/ld-linux.so.X(X是版本号)。该加载程序依次查找并加载该程序使用的所有其他共享库。被搜索的目录保存在/etc/ls.so.conf文件中 , 但如果某个所使用的库的路径不在搜索之内 , 手动添加上。为了避免程序每次启动都搜索一边 , Linux系统对共享库采用了缓存管理之ldconfig工具 , 其默认读取/etc/ld.so.conf文件 , 对所有共享库按照一定规范建立符号连接 , 然后将信息写入/etc/ld.so.cache。每次搜索的时候实际是通过ld.so.cache这个缓存文件进行搜索 , /etc/ld.so.cache的存在大大加快了程序的启动速度。每次修改ld.so.conf文件之后 , 运行ldconfig命令便把信息更新到缓存文件中。

  • 环境变量 , 可以通过设置环境变量LD_LIBRARY_PATH来设置ld的装载路径。这样装载器就会首先搜索该变量的目录 , 然后搜索默认目录。

  • 传参数 , 如果您不想设置LD_LIBRARY_PATH环境变量 , 在 Linux 上可以直接调用程序加载器并向其传递参数。例如 , 以下将使用给定的PATH而不是环境变量LD_LIBRARY_PATH的内容 , 并运行给定的可执行文件 :

    /lib/ld-linux.so.2 --library-path 可执行路径
  • Linux系统上或嵌入式Linux系统上装载库一般通过下面三种方式 :

    1. 拷贝库到默认的库搜索路径/usr/lib

    2. 设置环境变量LD_LIBRARY_PATH,在其中添加库的路径

    3. 修改配置文件/etc/ld.so.conf加入库所在的路径 , 并刷新缓存ldconfig

2.3 动态加载库

动态加载库(dynamically loaded (DL) libraries)是指在程序运行过程中加载的函数库。而不是像共享库一样在程序启动的时候加载。在Linux中 , 动态库的文件格式跟共享库没有区别 , 主要区别在于共享库是程序启动时加载 , 而动态加载库是运行的过程中加载。可以理解为动态加载库是共享库的另一种调用方式。DL对于实现程序模块化很有用处 , 因为它可以让程序在运行时进行模块升级。

动态加载库如何实现

Linux系统中 , 实现动态加载库的调用 , 有一个用于打开库、查找符号、处理错误和关闭库的APIC程序需要包含头文件<dlfcn.h>才能使用这些API

3. 三种库对比

特点

静态库

静态链接库在程序编译时会被链接到目标代码中 , 目标程序运行时将不再需要库 , 移植方便 , 但是体积较大 , 因为所有相关的库内容都被链接合成一个可执行文件 , 这样导致可执行文件的体积较大

共享库

动态库在程序编译时并不会被链接到目标代码中 , 而是在程序运行时才被载入 , 因为可执行文件体积较小。有了动态库 , 程序的升级会相对比较简单 , 只需要替换动态库的文件 , 而不需要替换可执行文件

动态加载库

动态库的文件格式跟共享库没有区别 , 主要区别在于共享库是程序启动时加载 , 而动态加载库是运行的过程中加载。可以理解为动态加载库是共享库的另一种调用方式

4. 相关命令

4.1 ldconfig

主要是更新动态链接库, 在默认搜寻目录/lib/usr/lib以及动态库配置文件/etc/ld.so.conf内所列的目录下 , 搜索出可共享的动态链接库 ( 格式如lib*.so* ) ,进而创建出动态装入程序(ld.so)所需的连接和缓存文件 , 缓存文件默认为/etc/ld.so.cache , 此文件保存已排好序的动态链接库名字列表。linux下的共享库机制采用了类似高速缓存机制 , 将库信息保存在/etc/ld.so.cache , 程序连接的时候首先从这个文件里查找 , 然后再到ld.so.conf的路径中查找。为了让动态链接库为系统所共享 , 需运行动态链接库的管理命令ldconfig , 此执行程序存放在/sbin目录下。

选项或参数

说明

-v

显示详细的信息 , 包括扫描的目录、动态链接库的名称、版本和依赖关系等

-n

只扫描指定的目录 , 不扫描/etc/ld.so.conf文件中的目录

-N

不创建或更新缓存文件 , 只显示动态链接库的信息

-X

不扫描动态链接库 , 只创建或更新缓存文件

-f <文件名>

使用指定的文件作为配置文件 , 而不是/etc/ld.so.conf文件

-C <文件名>

使用指定的文件作为缓存文件 , 而不是/etc/ld.so.cache文件

-r <根目录>

使用指定的根目录作为基准 , 扫描根目录下的动态链接库和配置文件

-l

显示当前缓存文件中的动态链接库的列表

-p

显示当前缓存文件中的动态链接库的路径和版本信息

-V

显示ldconfig命令的版本信息

4.2 ldd

用来查看程序运行所需的共享库,常用来解决程序因缺少某个库文件而不能运行的一些问题。

  • 首先ldd不是一个可执行程序 , 而只是一个shell脚本;

  • ldd能够显示可执行模块的dependency , 其原理是通过设置一系列的环境变量 , 如 : LD_TRACE_LOADED_OBJECTSLD_WARNLD_BIND_NOWLD_LIBRARY_VERSIONLD_VERBOSE等。当LD_TRACE_LOADED_OBJECTS环境变量不为空时 , 任何可执行程序在运行时 , 它都会只显示模块的dependency , 而程序并不真正执行。可以在shell终端测试一下 :

    • export LD_TRACE_LOADED_OBJECTS=1

    • 再执行任何的程序 , 如ls等 , 看看程序的运行结果

[root@localhost ~]#  export LD_TRACE_LOADED_OBJECTS=0
[root@localhost ~]# ls
        linux-vdso.so.1 =>  (0x00007ffeda27d000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f4d952ae000)
        libcap.so.2 => /lib64/libcap.so.2 (0x00007f4d950a9000)
        libacl.so.1 => /lib64/libacl.so.1 (0x00007f4d94ea0000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f4d94ad2000)
        libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f4d94870000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f4d9466c000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4d954d5000)
        libattr.so.1 => /lib64/libattr.so.1 (0x00007f4d94467000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f4d9424b000)
[root@localhost ~]#  export LD_TRACE_LOADED_OBJECTS=1
[root@localhost ~]# ls
        linux-vdso.so.1 =>  (0x00007ffe8f68c000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fdda8983000)
        libcap.so.2 => /lib64/libcap.so.2 (0x00007fdda877e000)
        libacl.so.1 => /lib64/libacl.so.1 (0x00007fdda8575000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fdda81a7000)
        libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fdda7f45000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fdda7d41000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fdda8baa000)
        libattr.so.1 => /lib64/libattr.so.1 (0x00007fdda7b3c000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fdda7920000)
[root@localhost ~]# unset LD_TRACE_LOADED_OBJECTS
[root@localhost ~]# ls
anaconda-ks.cfg  generated.iso  ks.cfg  test
​
  • ldd显示可执行模块的dependency的工作原理 , 其实质是通过ld-linux.so ( elf动态库的装载器 ) 来实现的。我们知道 , ld-linux.so模块会先于executable模块程序工作 , 并获得控制权 , 因此当上述的那些环境变量被设置时 , ld-linux.so选择了显示可执行模块的dependency

  • 实际上可以直接执行ld-linux.so模块 , 如 : /lib/ld-linux.so.2 --list program ( 这相当于ldd program ) 。

[root@localhost ~]# /lib64/ld-linux-x86-64.so.2 --list /bin/ls
        linux-vdso.so.1 =>  (0x00007ffd6b3a7000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007ff7efe20000)
        libcap.so.2 => /lib64/libcap.so.2 (0x00007ff7efc1b000)
        libacl.so.1 => /lib64/libacl.so.1 (0x00007ff7efa12000)
        libc.so.6 => /lib64/libc.so.6 (0x00007ff7ef644000)
        libpcre.so.1 => /lib64/libpcre.so.1 (0x00007ff7ef3e2000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007ff7ef1de000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff7f0047000)
        libattr.so.1 => /lib64/libattr.so.1 (0x00007ff7eefd9000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007ff7eedbd000)

4.3 patchelf

4.3.1 基础

PatchELF 是一个用于修改现有 ELF 可执行文件和库的简单实用程序

ELF :可执行与可链接格式(Executable and Linkable Format),常被称为 ELF 格式。

既然它能够更改可执行文件,那么我们就可以直接将可执行文件需要加载的 glibc 库的路径修改为我们刚才安装的路径。

这样就可以在既不需要添加环境变量,也不需要手动加载临时变量的情况下使用。

用以下命令安装:

# Ubuntu/Debian
sudo apt install patchelf
# CentOS/Fedora
sudo yum install patchelf

参数

描述

--set-interpreter FILENAME

设置动态库解析器

--page-size SIZE

设置页大小

--print-interpreter

打印解析器

--print-soname

打印 DT_SONAME

--set-soname SONAME

设置 DT_SONAME

--set-rpath RPATH

设置 RPATH

--remove-rpath

删除 RPATH

--shrink-rpath

收缩 RPATH

--allowed-rpath-prefixes PREFIXES

添加允许的 RPATH 前缀

--print-rpath

打印 RPATH

--force-rpath

强制使用 RPATH

--add-needed LIBRARY

添加需要的动态库

--remove-needed LIBRARY

删除需要的动态库

--replace-needed LIBRARY NEW_LIBRARY

替换动态库

--print-needed

打印需要的动态库

--no-default-lib

不链接默认的动态库

--output FILE

输出文件

--debug

输出调试信息

--version

打印版本信息

patchelf 的主要功能与动态库解析器、RPATH 以及动态库有关。

4.3.2 使用方式

  • 更改可执行文件的动态库解析器

    $ patchelf --set-interpreter /lib/my-ld-linux.so.2 my-program
  • 更改可执行文件和库的RPATH

    $ patchelf --set-rpath /opt/my-libs/lib:/other-libs my-program
  • 收缩可执行文件和库的RPATH

    $ patchelf --shrink-rpath my-program
  • 设置运行时路径

    patchelf --set-rpath /usr/local/lib some_executable

    这将设置some_executable的运行时路径为/usr/local/lib

  • 添加运行时路径

    patchelf --add-rpath /opt/lib some_executable

    这将在some_executable的运行时路径中添加/opt/lib

该命令会删除可执行文件中所有不包含 DT_NEEDED 字段指定的库的路径。

例如:

一个可执行文件引用一个库 libfoo.so,它的 RPATH/lib:/usr/lib:/foo/lib,而 libfoo.so 只能在 /foo/lib 中找到,那么新的 RPATH 将是 /foo/lib

其中 RPATH 指定的是可执行文件的动态链接库的搜索路径

  • 删除动态库上声明的依赖项(DT_NEEDED),可多次使用

    $ patchelf --remove-needed libfoo.so.1 my-program
  • 添加动态库上声明的依赖项(DT_NEEDED),可多次使用

    $ patchelf --add-needed libfoo.so.1 my-program
  • 替换动态库声明的依赖项(DT_NEEDED),可多次使用

    $ patchelf --replace-needed liboriginal.so.1 libreplacement.so.1 my-program
  • 更改动态库的 SONAME

    $ patchelf --set-soname libnewname.so.3.4.5 path/to/libmylibrary.so.1.2.3

4.3.3 示例

我有一个 msi 分析的可执行文件 msisensor-blood

在终端执行时,出现错误

$ ./msisensor-blood
./msisensor-blood: /lib64/libm.so.6: version `GLIBC_2.23' not found (required by ./msisensor-blood)
./msisensor-blood: /lib64/libc.so.6: version `GLIBC_2.15' not found (required by ./msisensor-blood)
./msisensor-blood: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by ./msisensor-blood)

从输出信息可以看出,需要两个库

  • glibc-2.15libc.so.6

  • glibc-2.23libm.so.6

首先, 我们用 ldd 命令列出其动态库依赖关系

./msisensor-blood: /lib64/libm.so.6: version `GLIBC_2.23' not found (required by ./msisensor-blood)
./msisensor-blood: /lib64/libc.so.6: version `GLIBC_2.15' not found (required by ./msisensor-blood)
./msisensor-blood: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by ./msisensor-blood)
        linux-vdso.so.1 =>  (0x00007ffc75beb000)
        libz.so.1 => /home/dengxs/software/zlib-1.2.11/lib/libz.so.1 (0x00007f7594351000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00000033dee00000)
        libstdc++.so.6 => /home/dengxs/software/anaconda3/lib/libstdc++.so.6 (0x00007f75941c4000)
        libm.so.6 => /lib64/libm.so.6 (0x00000033de600000)
        libgomp.so.1 => /home/dengxs/software/anaconda3/lib/libgomp.so.1 (0x00007f7594196000)
        libgcc_s.so.1 => /home/dengxs/software/anaconda3/lib/libgcc_s.so.1 (0x00007f7594182000)
        libc.so.6 => /lib64/libc.so.6 (0x00000033de200000)
        /lib64/ld-linux-x86-64.so.2 (0x00000033dde00000)
        librt.so.1 => /lib64/librt.so.1 (0x00000033df200000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00000033dea00000)

OK!就是把下面两个动态库替换掉

libm.so.6 => /lib64/libm.so.6
...
libc.so.6 => /lib64/libc.so.6

更换 libm.so.6 的路径

patchelf --replace-needed libm.so.6 /home/dengxs/software/glibc-2.23/lib/libm.so.6 msisensor-blood 

更换 libc.so.6 的路径

patchelf --replace-needed libc.so.6 /share/software/glibc/2.15/lib/libc.so.6 msisensor-blood

再看下动态库列表

$ ldd msisensor-blood
        linux-vdso.so.1 =>  (0x00007ffde0199000)
        libz.so.1 => /home/dengxs/software/zlib-1.2.11/lib/libz.so.1 (0x00007f6a786df000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00000033dee00000)
        libstdc++.so.6 => /home/dengxs/software/anaconda3/lib/libstdc++.so.6 (0x00007f6a78552000)
        /home/dengxs/software/glibc-2.23/lib/libm.so.6 (0x00007f6a7844b000)
        libgomp.so.1 => /home/dengxs/software/anaconda3/lib/libgomp.so.1 (0x00007f6a7841d000)
        libgcc_s.so.1 => /home/dengxs/software/anaconda3/lib/libgcc_s.so.1 (0x00007f6a78409000)
        /share/software/glibc/2.15/lib/libc.so.6 (0x00007f6a78062000)
        /lib64/ld-linux-x86-64.so.2 (0x00000033dde00000)
        librt.so.1 => /lib64/librt.so.1 (0x00000033df200000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00000033dea00000)

OK,已经替换成功了

接下去看看能不能直接运行

$ ./msisensor-blood 
​
​
Program: msisensor-blood (homopolymer and miscrosatelite analysis using cfDNA bam files)
Version: v0.1
Author: Beifang Niu && Kai Ye
​
Usage:   msisensor-blood <command> [options]
​
Key commands:
​
 scan            scan homopolymers and miscrosatelites
 msi             msi scoring


参考链接

Linux库详解

找不到动态链接库?


熊熊