【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found

【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found_第1张图片

绪论

大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。

介绍

前几章,花了较大篇幅介绍了动态库链接过程原理,需要面对的场景以及解决思路,真真切切的了解到动态库的优秀之处。这就导致大量开始程序使用动态链接。随着系统中的动态库越来越多,也会出现一些问题。本文介绍动态库的发展过程中遇到的问题,以及如何解决的。以及非常好用的调试tips。

兼容性问题

共享库的开发者也会不停的更新共享库的版本,用于修正原有的bug,增加新的功能或改进性能。共享库的更新可以分为两类:

  • 兼容更新。所有的更新只是在原有的共享库基础上添加一些内容,所有原有的接口都保持不变。

  • 不兼容更新。共享库更新改变了原有的接口,使用该共享库原有接口的程序可能不能运行或运行不正常。

常见的更新方式有以下几种:

【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found_第2张图片

问题:既然随着共享库的更新,可能出现兼容性问题,那么如何区分不同的版本呢?

Linux制定了一套命名规则,如下:

libname.so.x.y.z
  • x表示主版本号不同的主版本号之间表示存在重大升级,是不兼容的
  • y表示次版本号。不同次版本号之间存在增量升级,且保持原来的符号不变。在主版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库
  • z表示发布版本号。主要是对库进行一些错误的的修正、性能的改进,并不添加任何新的接口,也不对接口进行更新。主版本号和次版本相同的情况下,发布版本号之间相关兼容。

问题:程序如何确定自己依赖哪个版本的库呢?

通过上面共享库版本可知**,主版本号和次版本号决定了共享库的接口**。理论上程序最少要知道依赖库的主版本号和次版本号。比如程序依赖libc.so.2.6,而系统中存在libc.so.2.6.1libc.so.1.1.1libc.3.2.1库,在动态链接过程中,动态链接器则会选择libc.so.2.6.1

但实际Linux 系统采用的是SO-NAME符号版本机制实现程序对特定版本的依赖。我们一起来探讨Linux的实现过程。

SO-NAME

SO-NAME其表示该动态库的真正名称。它有两个作用:

  1. 告诉程序真正依赖的动态库文件。
  2. 系统可以通过ldconfig,对动态库创建SO-NAME的软连接。注:若动态库没有soname,则不会生成对应软链接。

我们可以通过-Wl,-soname链接参数修改动态库的SO-NAME。如下示例:

//main.c
extern int func();
int main()
{
        func();
        return 0;
}

//func.c
#include
#include

int func()
{
        printf("version=1.0.0\n");
        return 0;
}

编译:

yihua@ubuntu:~/test/0110$ gcc -shared -fPIC -Wl,-soname,libfunc.so -o libfunc.so func.c
yihua@ubuntu:~/test/0110$ gcc main.c -o main -L. -lfunc
yihua@ubuntu:~/test/0110$

通过readelf -d libfunc.soldd main指令查看动态库的SO-NAMEmain程序的动态库依赖:

yihua@ubuntu:~/test/0110$ readelf -d libfunc.so

Dynamic section at offset 0xe10 contains 25 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libfunc.so.1.0.0]
...
yihua@ubuntu:~/test/0110$ ldd main
        linux-vdso.so.1 (0x00007ffd761eb000)
        libfunc.so.1.0.0 => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ae40ed000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f4ae46e0000)
yihua@ubuntu:~/test/0110$

分析:

首先动态库编译,将libfunc.soSONAME设置为libfunc.so.1.0.0

链接器在编译main时,携带参数-lfunc,则会在当前目录下查找libfunc.so,确定func符号类型。并将其SONAMElibfunc.so.1.0.0保存到main程序中。但是当main运行时,则需要加载libfunc.so.1.0.0文件,而不是找libfunc.so。由于没有libfunc.so.1.0.0文件,所以main是无法执行的。如下:

yihua@ubuntu:~/test/0110$ export LD_LIBRARY_PATH=~/test/0110/
yihua@ubuntu:~/test/0110$ ./main
./main: error while loading shared libraries: libfunc.so.1.0.0: cannot open shared object file: No such file or directory
yihua@ubuntu:~/test/0110$

注:当未指定动态库的soname时,链接器默认程序依赖的动态库与文件名相同。

创建软链接:

yihua@ubuntu:~/test/0110$ ldconfig -n .
yihua@ubuntu:~/test/0110$ ls -la
total 36
drwxrwxr-x  2 yihua yihua 4096 Jan 10 01:36 .
drwxrwxr-x 13 yihua yihua 4096 Jan  9 23:24 ..
-rw-rw-r--  1 yihua yihua   92 Jan 10 00:04 func.c
-rwxrwxr-x  1 yihua yihua 7896 Jan 10 01:20 libfunc.so
lrwxrwxrwx  1 yihua yihua   10 Jan 10 01:36 libfunc.so.1.0.0 -> libfunc.so
-rwxrwxr-x  1 yihua yihua 8288 Jan 10 01:20 main
-rw-rw-r--  1 yihua yihua   92 Jan 10 00:03 main.c
yihua@ubuntu:~/test/0110$

如上所示,创建了一个以libfunc.so的soname为名的软链接并指向libfunc.so。此时则可以运行成功。

yihua@ubuntu:~/test/0110$ ldconfig -n .
yihua@ubuntu:~/test/0110$ ./main
version=1.0.0
yihua@ubuntu:~/test/0110$

问题:这样的做法有什么好处?如何使用呢?

我们可以考虑这样的一个场景:
若程序A依赖libfunc.so.2.1.1,那么运行时,就必须要保证系统中存在libfunc.so.2.1.1文件。同理程序B依赖libfunc.so.2.2.1,则系统中也必须保存libfunc.so.2.2.1文件,这就会导致磁盘的浪费。

【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found_第3张图片

因为根据动态库版本规则而言,我们知道libfunc.so.2.2.1是向后兼容libfunc.so.2.1.1。因此我们可以这样操作:

  1. 编译环境A中将libfunc.so.2.1.1的soname设置为libfunc.so.2
  2. 编译环境B中将libfunc.so.2.1.1的soname设置为libfunc.so.2
  3. 运行环境中,仅需要保留libfunc.so.2.2.1,且其soname为libfunc.so.2,再通过ldconfig工具,创建一个软连接文件libfunc.so.2指向libfunc.so.2.2.1

【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found_第4张图片

这样既可以保证AB的正常运行,也同时节约了磁盘资源。一般情况动态库的soname仅体现主版本号。如/lib/x86_64-linux-gnu/目录下的库:

yihua@ubuntu:~/test/0110$ ls -la /lib/x86_64-linux-gnu/
total 19984
drwxr-xr-x  3 root root   16384 Oct 23 22:36 .
drwxr-xr-x 22 root root    4096 Aug 21 18:59 ..
-rwxr-xr-x  1 root root  179152 May  3  2022 ld-2.27.so
lrwxrwxrwx  1 root root      10 May  3  2022 ld-linux-x86-64.so.2 -> ld-2.27.so
lrwxrwxrwx  1 root root      15 Nov  9  2021 libacl.so.1 -> libacl.so.1.1.0
-rw-r--r--  1 root root   31232 Apr 21  2017 libacl.so.1.1.0
lrwxrwxrwx  1 root root      15 Apr  1  2019 libaio.so.1 -> libaio.so.1.0.1
-rw-r--r--  1 root root    5480 Apr  1  2019 libaio.so.1.0.1
-rw-r--r--  1 root root   14920 May  3  2022 libanl-2.27.so
lrwxrwxrwx  1 root root      14 May  3  2022 libanl.so.1 -> libanl-2.27.so
lrwxrwxrwx  1 root root      20 Jun 20  2023 libapparmor.so.1 -> libapparmor.so.1.4.2
-rw-r--r--  1 root root   64144 Jun 20  2023 libapparmor.so.1.4.2
lrwxrwxrwx  1 root root      20 Nov  9  2021 libatasmart.so.4 -> libatasmart.so.4.0.5
-rw-r--r--  1 root root   51568 May 26  2016 libatasmart.so.4.0.5
-rwxr-xr-x 2 root root 2030928 May  3  2022 /lib/x86_64-linux-gnu/libc-2.27.so
lrwxrwxrwx 1 root root      12 May  3  2022 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.27.so

其中因为历史原因,动态链接器ld-2.27.so和C语言库libc-2.27.so的共享对象文件名规则不按Linux标准的共享库命名方法,但是其原理还是一样的。

符号版本

动态链接器在进行动态链接时,可以通过soname对主版本号进行判断。比如程序依赖libfunc.so.2,系统中有libfunc.so.2-->libfunc.so.2.6.1libfunc.so.3-->libfunc.so.3.6.1,则会链接上libfunc.so.2.6.1,而不是链接到libfunc.so.3.6.1

但是这里就存在一个问题,若程序的编译环境依赖更高的次版本号libfunc.so.2.8.1时,那么同样也会在运行环境中链接上libfunc.so.2.6.1。我们知道次版本号之间只保证向后兼容,不保证向前兼容,这样就容易存在问题,比如引用了libfunc.so.2.8.1独有的符号,或新版本解决了旧版本的bug。也许程序可以继续运行,但是存在一定隐患。

为了能够将这种风险体现出来,Linux提供了一种叫做基于符号的版本机制方案,其类似于名称修饰。即给共享库中的全局符号打上一个标记。比如对应libfunc.so.2.6.1libfunc.so.2.8.1库,其soname 依然为libfunc.so.2,但是其全局符号中可以分别添加VERS_2.6VERS_2.8的标签。

这也是我们经常在交叉编译环境时,遇到的错误:

Error version `GLIBC_2.34’ not found

其原因则是程序依赖的libc版本比当前系统的要高,正确且保险的做法则是下载对应或更高次版本号的glibc源码,进行交叉编译。

环境变量

关于动态链接的两个不太常用但是非常有用的环境变量,和大家分享一下。

LD_PRELOAD

LD_PRELOAD里面指定的文件会在动态链接器按照固定规则搜索共享库之前加载,它比LD_LIBRARY_PATH指令路径共享库更加优先。无论程序是否依赖于它们,LD_PRELOAD里面指定的共享库或目标文件都会被加载。

在【程序员的自我修养09】动态链接过程的场景补充及其思考一章中,我们知道共享对象全局符号介入机制的存在。因此利用该环境变量使得我们非常方便的做到改写标准C库中的函数,而不影响其他函数,对程序的调试非常有用

LD_DEBUG

这个变量可以打开动态链接器的调试功能,打印各种有用信息,对于我们开发和调试共享库非常有用。比如:

yihua@ubuntu:~/test/0110$ LD_DEBUG=files ./main
     38378:
     38378:     WARNING: Unsupported flag value(s) of 0x8000000 in DT_FLAGS_1.
     38378:
     38378:     file=libfunc.so.1.0.0 [0];  needed by ./main [0]
     38378:     file=libfunc.so.1.0.0 [0];  generating link map
     38378:       dynamic: 0x00007f70f7c8fe10  base: 0x00007f70f7a8f000   size: 0x0000000000201030
     38378:         entry: 0x00007f70f7a8f540  phdr: 0x00007f70f7a8f040  phnum:                  7
     38378:
     38378:
     38378:     file=libc.so.6 [0];  needed by ./main [0]
     38378:     file=libc.so.6 [0];  generating link map
     38378:       dynamic: 0x00007f70f7a88b80  base: 0x00007f70f769e000   size: 0x00000000003f0ae0
     38378:         entry: 0x00007f70f76bfda0  phdr: 0x00007f70f769e040  phnum:                 10
     38378:
     38378:
     38378:     calling init: /lib/x86_64-linux-gnu/libc.so.6
     38378:
     38378:
     38378:     calling init: /home/yihua/test/0110/libfunc.so.1.0.0
     38378:
     38378:
     38378:     initialize program: ./main
     38378:
     38378:
     38378:     transferring control: ./main
     38378:
version=1.0.0
     38378:
     38378:     calling fini: ./main [0]
     38378:
     38378:
     38378:     calling fini: /home/yihua/test/0110/libfunc.so.1.0.0 [0]
     38378:
yihua@ubuntu:~/test/0110$

可知,动态链接器将整个装载过程,显示程序依赖哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址等。它还具备其它参数:

  • bindings,显示动态链接的符号绑定过程。
  • libs,显示共享库的查找过程。
  • versions,显示符号的版本依赖关系。
  • reloc,显示重定位过程。
  • symbols,显示符号表查找过程。
  • statistics,显示动态链接过程中的各种统计信息。
  • all,显示以上所有信息。

建议大家实操一下,加强印象。

总结

本文花了较大篇幅介绍了动态库如何解决和避免兼容性问题,以及分享两个分享实用的环境变量LD_PRELOADLD_DEBUG,至此关于动态库的分享算是结束了。下一章节,我们开始向《库与运行库》进军。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途。

【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found_第5张图片
【程序员的自我修养10】动态库的兼容问题——GLIBC_2.34‘ not found_第6张图片

你可能感兴趣的:(编译,链接,装载,库,java,算法,linux,动态库兼容)