Linux shared library usage detail

换句话说,soname不是真实存在的文件,只是在此库中和将来调用此库的文件中保存的一个名字,在加载是去找这个名字,使用时创建一个软连接来指向真实文件,这样真实文件的版本号就可以升级了

Linux 系统,也同样面临和Window一样的问题,如何控制动态库的多个版本问题。Window之前没有处理好,为此专门有个名词来形容这个问题 “Dll hell”,其严重影响软件的升级和维护。Dll hell 是指windows 上动态库新版本覆盖旧版本,但是却不兼容老版本。常常发生在程序升级之后,动态库更新,原有程序运行不起来;或者装新软件,但是已有的软件运行不起来。 同样Linux操作系统,也有同样的问题,那么它是怎么解决的呢?

Linux 为解决这个问题,引入了一套机制,如果遵守这个机制来做,就可以避免这个问题。 但是这只事一个约定,不是强制的。但是建议遵守这个约定,否则同样也会出现 Linux 版的Dll hell 问题。 下面来介绍一个这个机制。 这个机制是通过文件名,来控制dll (shared library) 的版本。

Linux 上的Dll ,叫shared library,其有三个名字,分别又不同的目的。

第一个是共享库本身的文件名(real name),其通常包含版本号,常常是是这样: libmath.so.1.1.1234 。 lib是Linux 上的库的约定前缀,math 是共享库名子,so 是共享库的后缀名,1.1.1234的是共享库的版本号,其主版本号+小版本号+build号。主板号,代表当前动态库的版本,如果动态库的接口有变化,那么这个版本号就要加1;后面的两个版本号(小版本号 和 build 号)是告诉你详细的信息,比如为一个hot-fix 而生成的一个版本,其小版本号加1,build号也应有变化。 这个文件名包含共享库的代码。在共享库编译过程中,连接(link) 阶段,编译器将生成一个名字为real name的共享库,同时将共享库的soname写在共享库文件里的文件头里面。可以用命令 readelf -d sharelibrary 去查看。

第二个是动态库的soname( Short for shared object name),依赖该shared library的应用程序中记录了它的soname,所以应用程序加载该shared library的时候,其寻找共享库用的soname文件名。其格式为

lib + math+.so + ( major version number)

它是一个指向名字为real name的shared library的软链接。

其只包含major version number,换句话说,也就是只要其接口没有变,应用程序都可以用,不管你其后minor build version or build version。

问题来了,程序运行时怎么通过soname 找个real name? Soname 存在哪里?如果与real name 关联起来?什么时候存的?

这就是接下来要介绍的第三个共享库的名字,link name,顾名思义,就是在编译过程,link 阶段用的文件名。 其将sonmae 和real name 关联起来。

第三个名字,共享库的连接名(link name),是专门为build 阶段连接而用的名字。这个名字就是lib + math +.so ,比如libmath.so。其是不带任何版本信息的。

它同样是一个指向名字为real name的shared library的软连接。

在应用程序引用共享库时,其会用到共享库的link name。在应用程序的link阶段,其通过link名字找到real name动态库,并且把共享库的soname 提取出来,写在应用程序里记录的共享库相关信息里面。当应用程序加载时候就会通过应用程序中保存的soname 去在给定的路径下寻找名字叫做soname的共享库(其实就是指向名字为real name的shared library的软连接)。

下面通过这个代码来说明一下系统是如何做的,并且介绍系统的一些设施和工具:

代码: 

1. File libhello.c

/* hello.c - demonstrate library use. */
#include <stdio.h>
void hello(void)
{ printf("Hello, library world./n");}

2. File libhello.h

/* libhello.h - demonstrate library use. */
void hello(void);

3. File main.c

/* main.c -- demonstrate direct use of the "hello" routine */
#include "hello.h"
int main(void)
{
 hello();
return 0;
}

1.生成共享库,关联real name 和soname 。

gcc -g -Wall -fPIC -c hello.c -o hello.o

gcc -shared -W,soname,-libhello.so.0 -o libhello.so.0.0.0 hello.o

将会生成共享库libhello.so.0.0.0.

可以用系统提供的工具查看共享库的头:

readelf -d libhello.so.0.0.0 | grep libhello

ox00000000000e(SONAME) library soname: [libhello.so.0]

2.应用程序,引用共享库。

先手动生成link 名字,以被后面的程序链接时用

ln -s libhello.so.0.0.0 libhello.so.0

gcc -g -Wall -c main.c -o main.o -I.

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

查看编译出来的程序:

readelf -d main | grep libhello

ox000000000001(NEEDED) shared library: [libhello.so.0]

运行该程序,需要指定共享库的路径。 有两种办法,第一种使用环境变量“LD_LIBRARY_PATH”. 两外一种办法就是将共享库拷贝到系统目录(path 环境变量指定的其中一个目录)。

暂停! 我们还没有解决一个问题是,程序只知道soname,怎么从soname 找到共享库,即real name 文件呢? 这需要我们定义一个link文件,连接到共享库本身。

ln -s libhello.so.0.0.0 libhello.so.0

当然这个路径需要放到LD_LIBRARY_PATH环境变量中。

这样就可以运行该程序。

[Note]Linux 系统提供一个命令 ldconifg 专门为生成共享库的soname 文件,以便程序在加载时后通过soname 找到共享库。 同时该命令也为加速加载共享库,把系统的共享库放到一个缓存文件中,这样可以提高查找速度。可以用下面命令看一下系统已有的被缓存起来的共享库。

ld -p

3.共享库,小版本升级,即接口不变.

当升级小版本时,共享库的soname 是不变的,所以需要重新把soname 的那个连接文件指定新版本就可以。 调用ldconfig命令,系统会帮你做修改那个soname link文件,并把它指向新的版本呢。这时候你的应用程序就自动升级了。

4.共享库,主版本升级,即接口发生变化。

当升级主版本时,共享库的soname 就会加1.比如libhello.so.0.0.0 变为 libhello.so.1.0.0. 这时候再运行ldconfig 文件,就会发现生成两个连接 文件。

ln -s libhello.so.0—->libhello.so.0.0.0

ln -s libhello.so.1—–>libhello.so.1.0.0

尽管共享库升级,但是你的程序依旧用的是旧的共享库,并且两个之间不会相互影响。

问题是如果更新的共享库只是增加一些接口,并没有修改已有的接口,也就是向前兼容。但是这时候它的主版本号却增加1. 如果你的应用程序想调用新的共享库,该怎么办? 简单,只要手工把soname 文件修改,使其指向新的版本就可以。(这时候ldconfig 文件不会帮你做这样的事,因为这时候soname 和real name 的版本号主板本号不一致,只能手动修改)。

比如: ln -s libhello.so.0 —> libhello.so.1.0.0

但是有时候,主版本号增加,接口发生变化,可能向前不兼容。这时候再这样子修改,就会报错,“xx”方法找不到之类的错误。

总结一下,Linux 系统是通过共享库的三个不同名字,来管理共享库的多个版本。 real name 就是共享库的实际文件名字,soname 就是共享库加载时的用的文件名。在生成共享库的时候,编译器将soname 绑定到共享库的文件头里,二者关联起来。 在应用程序引用共享库时,其通过link name 来完成,link时将按照系统指定的目录去搜索link名字找到共享库,并将共享库的soname写在应用程序的头文件里。当应用程序加载共享库时,就会通过soname在系统指定的目录(path or LD_LIBRARY)去寻找共享库。

当共享库升级时,分为两种。一种是主板本不变,升级小版本和build 号。在这种情况下,系统会通过更新soname( ldconfig 来维护),来使用新的版本号。这中情况下,旧版本就没有用,可以删掉。

另外一种是主版本升级,其意味着库的接口发生变化,当然,这时候不能覆盖已有的soname。系统通过增加一个soname(ldconfig -p 里面增加一项),使得新旧版本同时存在。原有的应用程序在加载时,还是根据自己头文件的旧soname 去寻找老的库文件。

5.如果编译的时候没有指定共享库的soname,会怎么样?

这是一个trick 的地方。这样系统在生成库的时候,就没有soname放到库的头里面。从而应用程序连接时候,就把linkname 放到应用程序依赖库里面。或者换句话说就是,soname这时候不带版本号。 有时候有人直接利用这点来升级应用程序,比如,新版本的库,直接拷贝到系统目录下,就会覆盖掉已经存在的旧的库文件,直接升级。 这个给程序员很大程度的便利性,如果一步小心,就会调到类似windows的Dll hell 陷阱里面。建议不要这样做。

【Note】

1. 指定共享库加载的路径。LD_LIBRARY_PATH 优先与 path 环境变量。

2. ldd 可以查看程序,或者共享库依赖的库的路径

3. nm 查看共享库暴露的接口

4. ldconfig 可以自动生成soname 的连接文件。并提供catch 加速查找。

5.readelf 可以查看动态库的信息,比如依赖的库,本身的somae。

6. objdump 与readelf 类似。

7 ld The GUN linker

8. ld.so dynamic linker or loader

9. as the portable GNU assembley

【Reference】

http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html

幸福不是因为你拥有得多,而是由于你计较得少。

Linux shared library usage detail

相关文章:

你感兴趣的文章:

标签云: