Linux程序编译链接动态库版本号的问题

摘要:
不同版本号的动态库可能会不兼容,假设程序在编译时指定动态库是某个低版本号。执行是用的一个高版本号,可能会导致无法执行。Linux上对动态库的命名採用libxxx.so.a.b.c的格式。b代表小版本号号,c代表更小的版本号号。当我们的动态库仅仅是升级一个小版本号时,我们能够让它的soname相同。採用例如以下命令编译main.c$gccmain.c-L.-lhello-omain/usr/bin/ld:cannotfind-lhello报错找不到hello动态库,在Linux下,编译时指定-lhello,链接器会去寻找libhello.so这种文件。

不同版本号的动态库可能会不兼容,假设程序在编译时指定动态库是某个低版本号。执行是用的一个高版本号,可能会导致无法执行。

Linux上对动态库的命名採用libxxx.so.a.b.c的格式。当中a代表大版本号号。b代表小版本号号,c代表更小的版本号号。我们以Linux自带的cp程序为例,通过ldd查看其依赖的动态库

 $ ldd /bin/cp                                                                                                                                                                                        
linux-vdso.so.1 =>  (0x00007ffff59df000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fb3357e0000)
librt.so.1 => /lib64/librt.so.1 (0x00007fb3355d7000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007fb3353cf000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007fb3351ca000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb334e35000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fb334c31000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb335a0d000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb334a14000)

左边是依赖的动态库名字,右边是链接指向的文件,再查看libacl.so相关的动态库

  $ ll /lib64/libacl.so*                                                                                                                                                                               
lrwxrwxrwx. 1 root root    15 1月   7 2015 /lib64/libacl.so.1 -> libacl.so.1.1.0
-rwxr-xr-x. 1 root root 31280 12月  8 2011 /lib64/libacl.so.1.1.0

我们发现libacl.so.1实际上是一个软链接,它指向的文件是libacl.so.1.1.0,命名方式符合我们上面的描写叙述。也有不按这种方式命名的,比方

$ ll /lib64/libc.so*                                                                                                                                                                                  
lrwxrwxrwx 1 root root 12 8月  12 14:18 /lib64/libc.so.6 -> libc-2.12.so

无论如何命名,仅仅要依照规定的方式来生成和使用动态库。就不会有问题。

并且我们往往是在机器A上编译程序。在机器B上执行程序,编译和执行的环境事实上是有稍微不同的。以下就说说动态库在生成和使用过程中的一些问题

动态库的编译

我们以一个简单的程序作为样例

// filename:hello.c
#include <stdio.h>

void hello(const char* name)
{
    printf("hello %s!
", name);
}

// filename:hello.h
void hello(const char* name);

採用例如以下命令进行编译

gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.1

须要注意的參数是-Wl,soname(中间没有空格),-Wl选项告诉编译器将后面的參数传递给链接器,
-soname则指定了动态库的soname(简单共享名。Short for shared object name)

如今我们生成了libhello.so.0.0.1,当我们执行ldconfig -n .命令时,当前文件夹会多一个软连接

 $ ll libhello.so.0                                                                                                                                                                                   
lrwxrwxrwx 1 handy handy 17 8月  17 14:18 libhello.so.0 -> libhello.so.0.0.1

这个软链接是如何生成的呢,并非截取libhello.so.0.0.1名字的前面部分,而是依据libhello.so.0.0.1编译时指定的-soname生成的。也就是说我们在编译动态库时通过-soname指定的名字,已经记载到了动态库的二进制数据里面。无论程序是否按libxxx.so.a.b.c格式命名,但Linux上差点儿全部动态库在编译时都指定了-soname,我们能够通过readelf工具查看soname,比方文章开头列举的两个动态库

 $ readelf -d /lib64/libacl.so.1.1.0                                                                                                                                                                   

Dynamic section at offset 0x6de8 contains 24 entries:
Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [libattr.so.1]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
0x000000000000000e (SONAME)             Library soname: [libacl.so.1]

这里省略了一部分,能够看到最后一行SONAME为libacl.so.1。所以/lib64才会有一个这种软连接

再看libc-2.12.so文件,该文件并没有採用我们说的命名方式

 $ readelf -d /lib64/libc-2.12.so                                                                                                                                                                      

Dynamic section at offset 0x18db40 contains 27 entries:
Tag        Type                         Name/Value
0x0000000000000001 (NEEDED)             Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME)             Library soname: [libc.so.6]

相同能够看到最后一行SONAME为libc.so.6。即便该动态库没有按版本号号的方式命名,但仍旧有一个软链指向该动态库,而该软链的名字就是soname指定的名字

所以关键就是这个soname,它相当于一个中间者。当我们的动态库仅仅是升级一个小版本号时,我们能够让它的soname相同。而可执行程序仅仅认soname指定的动态库,这样依赖这个动态库的可执行程序不需又一次编译就能使用新版动态库的特性

可执行程序的编译

还是以hello动态库为例,我们写一个简单的程序

// filename:main.c
#include "hello.h"

int main()
{
    hello("handy");
    return 0;
}

如今文件夹下是例如以下结构

├── hello.c
├── hello.h
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── main.c

libhello.so.0.0.1是我们编译生成的动态库,libhello.so.0是通过ldconfig生成的链接。採用例如以下命令编译main.c

 $ gcc main.c -L. -lhello -o main                                                                                                                                                                            
/usr/bin/ld: cannot find -lhello

报错找不到hello动态库,在Linux下,编译时指定-lhello,链接器会去寻找libhello.so这种文件。当前文件夹下没有这个文件,所以报错。

建立这样一个软链。文件夹结构例如以下

├── hello.c
├── hello.h
├── libhello.so -> libhello.so.0.0.1
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── main.c

让libhello.so链接指向实际的动态库文件libhello.so.0.0.1。再编译main程序

gcc main.c -L. -lhello -o main

这样可执行文件就生成了。

通过以上測试我们发现,在编译可执行程序时。链接器会去找它依赖的libxxx.so这种文件。因此必须保证libxxx.so的存在

用ldd查看其依赖的动态库

 $ ldd main                                                                                                                                                                                            
        linux-vdso.so.1 =>  (0x00007fffe23f2000)
        libhello.so.0 => not found
        libc.so.6 => /lib64/libc.so.6 (0x00007fb6cd084000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fb6cd427000)

我们发现main程序依赖的动态库名字是libhello.so.0。既不是libhello.so也不是libhello.so.0.0.1。事实上在生成main程序的过程有例如以下几步

  • 链接器通过编译命令-L. -lhello在当前文件夹查找libhello.so文件
  • 读取libhello.so链接指向的实际文件。这里是libhello.so.0.0.1
  • 读取libhello.so.0.0.1中的SONAME,这里是libhello.so.0
  • 将libhello.so.0记录到main程序的二进制数据里

也就是说libhello.so.0是已经存储到main程序的二进制数据里的,无论这个程序在哪里,通过ldd查看它依赖的动态库都是libhello.so.0

而为什么这里ldd查看main显示libhello.so.0为not found呢。由于ldd是从环境变量$LD_LIBRARY_PATH指定的路径里来查找文件的,我们指定环境变量再执行例如以下

 $ export LD_LIBRARY_PATH=. && ldd main                                                                                                                                                                
    linux-vdso.so.1 =>  (0x00007fff7bb63000)
    libhello.so.0 => ./libhello.so.0 (0x00007f2a3fd39000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f2a3f997000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2a3ff3b000)

可执行程序的执行

如今測试文件夹结果例如以下

├── hello.c
├── hello.h
├── libhello.so -> libhello.so.0.0.1
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
├── main
└── main.c

这里我们把编译环境和执行环境混在一起了。只是没关系。仅仅要我们知道当中原理。就能够将其理清楚

前面我们已经通过ldd查看了main程序依赖的动态库,并且指定了LD_LIBRARY_PATH变量,如今就能够直接执行了

 $ ./main                                                                                                                                                                                              
hello Handy!

看起来非常顺利。那么假设我们要部署执行环境,该怎么部署呢。

显然,源码是不须要的,我们仅仅须要动态库和可执行程序。这里新建一个执行文件夹。并拷贝相关文件,文件夹结构例如以下

├── libhello.so.0.0.1
└── main

这时执行会main会发现

 $ ./main                                                                                                                                                                                              
./main: error while loading shared libraries: libhello.so.0: cannot open shared object file: No such file or directory

报错说libhello.so.0文件找不到,也就是说程序执行时须要寻找的动态库文件名称事实上是动态库编译时指定的SONAME,这也和我们用ldd查看的一致。

通过ldconfig -n .建立链接,例如以下

├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── main

再执行程序,结果就会符合预期了

从以上測试看出,程序在执行时并不须要知道libxxx.so,而是须要程序本身记载的该动态库的SONAME。所以main程序的执行环境仅仅须要以上三个文件就可以

动态库版本号更新

假设动态库须要做一个小小的修改,例如以下

// filename:hello.c
#include <stdio.h>

void hello(const char* name)
{
    printf("hello %s, welcom to our world!
", name);
}

由于修改较小,我们编译动态库时仍然指定相同的soname

gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.2

将新的动态库复制到执行文件夹。此时执行文件夹结构例如以下

├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── main

此时文件夹下有两个版本号的动态库。但libhello.so.0指向的是老本版,执行ldconfig -n .后我们发现,链接指向了新版本号,例如以下

├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── main

再执行程序

 $ ./main                                                                                                                                                                                              
hello Handy, welcom to our world!

没有又一次编译就使用上了新的动态库, wonderful。

相同。假如我们的动态库有大的修改。编译动态库时指定了新的soname。例如以下

gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0

将动态库文件复制到执行文件夹。并执行ldconfig -n .,文件夹结构例如以下

├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
├── libhello.so.1 -> libhello.so.1.0.0
├── libhello.so.1.0.0
└── main

这时候发现,生成了新的链接libhello.so.1,而main程序还是使用的libhello.so.0,所以无法使用新版动态库的功能,须要又一次编译才行

最后

在实际生产环境中,程序的编译和执行往往是分开的,但仅仅要搞清楚这一系列过程中的原理,就不怕被动态库的版本号搞晕。简单来说,按例如以下方式来做

  • 编译动态库时指定-Wl,-soname,libxxx.so.a。设置soname为libxxx.so.a,生成实际的动态库文件libxxx.so.a.b.c,
  • 编译可执行程序时保证libxx.so存在。假设是软链。必须指向实际的动态库文件libxxx.so.a.b.c
  • 执行可执行文件时保证libxxx.so.a.b.c文件存在,通过ldconfig生成libxxx.so.a链接指向libxxx.so.a.b.c
  • 环境变量设置LD_LIBRARY_PATH,执行可执行程序

EOF

免责声明:文章转载自《Linux程序编译链接动态库版本号的问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇iOS 并行编程:ThreadHTB-靶机-Reddish下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

移植QT5.6到嵌入式开发板(史上最详细的QT移植教程)

目前网上的大多数 QT 移植教程还都停留在 qt4.8 版本,或者还有更老的 Qtopia ,但是目前 Qt 已经发展到最新的 5.7 版本了,我个人也已经使用了很长一段时间的 qt5.6 for windows ,本文就来介绍一下QT在嵌入式环境的搭建。   移植以到 JZ2440 为例,使用韦老大提供的 ubuntu9.10 虚拟机作为移植环境。当然,...

linux定时任务cron 安装配置

名词解释: cron是服务名称,crond是后台进程,crontab则是定制好的计划任务表。 软件包安装: 要使用cron服务,先要安装vixie-cron软件包和crontabs软件包,两个软件包作用如下: vixie-cron软件包是cron的主程序。crontabs软件包是用来安装、卸装、或列举用来驱动 cron 守护进程的表格的程序。查看是否安装了...

VSCode编译CMake工程报错c++: internal compiler error: Killed (program cc1plus)

现象 使用VSCode+CMake构建C++工程时只能编译Release版本,无法编译Debug版本。 报错提示c++: internal compiler error: Killed (program cc1plus) 解决 编译过程中内存不足 方案1:增加内存 如果在虚拟机运行Linux可以增加虚拟机的分配内存或增大swap空间 具体方法可以参考这篇文...

Linux下weblogic启动报错unable to get file lock的问题

非正常结束weblogic进程导致weblogic无法启动 由于先前服务器直接down掉了,所有进程都非正常的进行关闭了,也就导致了下次启动weblogic的时候报了以下错误:<2012-3-2 下午05时08分34秒 CST> <Info> <Management> <BEA-141281> <una...

Icinga快速安装与配置

简体中文    繁體中文 Icinga快速安装与配置 1.1. 绪论 1.2. 前提条件 1.3. 安装软件包 1.4. 创建帐户信息 1.5. 编译和安装Icinga(包括IDOUtils) 1.5.1. 编译和安装 1.6. 定制配置 1.6.1. 启用idomod event broker 模块 1.7. 数据库创建和IDOUtils 1....

Linux C 创建目录函数mkdir相关(转-清新居士)

I.Linux C 创建目录函数mkdir的mode设置问题 函数原型: #include <sys/stat.h> int mkdir(const char *path, mode_t mode); 参数: path是目录名 mode是目录权限 返回值: 返回0 表示成功, 返回 -1表示错误,并且会设置errno值。 mode模式位: mo...