针对同一动态组件的不同版本链接和加载。
一、概念
DLLHELL字面意思是DLL"灾难",是因为com组件(动态库)升级导致的程序不能运行的情况。
缘由
有三种可能的诱因引起了DLLHell的发生:
一是由使用旧版本的DLL取代原先一个新版本的DLL而引发的。这个缘由最普遍,是Windows9X用户一般遇见的DLL错误之一。
二是由新版DLL中的函数无意发生改变而导致。虽然在设计DLL时侯应当向上兼容,但是要保证DLL完全向下兼容却是不能的。
三是由新版DLL的安装引入一个新的Bug。
二、linux下的解决方案——命名规范
Linux上的Dll,叫sharedlibrary。Linux系统面临和Window一样的问题,怎样控制动态库的多个版本问题。为解决这个问题,Linux为解决这个问题,引入了一套命名机制,假如遵循这个机制来做,就可以避开这个问题。并且这只事一个约定,不是强制的。并且建议遵循这个约定,否则同样也会出现Linux版的Dllhell问题。
RealName
首先是共享库本身的文件名:共享库的命名必须如libname.so.x.y.z最上面使用前缀”lib”,中间是库的名子和后缀”.so”,最后三个数字是版本号。x是主版本号(MajorVersionNumber),y是次版本号(MinorVersionNumber),z是发布版本号(ReleaseVersionNumber)。
主版本号(不兼容):重大升级,不同主版本的库之间的库是不兼容的。所以假如要保证向后兼容就不能删掉旧的动态库的版本。
次版本号(向上兼容):增量升级,降低一些新的插口但保留原有插口。高次版本号的库向后兼容低次版本号的库。
发布版本号(互相兼容):库的一些例如错误更改、性能改进等,不添加新插口,也不修改插口。主版本号和次版本号相同的前提下,不同发布版本之间完全兼容。
SO-NAME
严格遵循上述规定,确实能防止动态库因为版本冲突的问题,而且读者可能有疑惑:在程序加载或运行的时侯,动态链接器是怎样晓得程序依赖什么库,怎么选择库的不同版本?
Solaris和Linux等采用SO-NAME(Shortforsharedobjectname)的命名机制来记录共享库的依赖关系。每位共享库都有一个对应的“SO-NAME”(共享库文件名去除次版本号和发布版本号)。例如一个共享库名为libtest.so.3.8.2,这么它的SO-NAME就是libtest.so.3。
在Linux系统中,系统会为每位共享库所在的目录创建一个跟SO-NAME相同的而且指向它的软联接(SymbolLink)。这个软联接会指向目录中主版本号相同、次版本号和发布版本号最新的共享库。也就是说,例如目录中有两个共享库版本分别为:/lib/libtest.so.3.8.2和/lib/libtest.so.3.7.5,么软联接/lib/libtest.so.3指向/lib/libtest.so.3.8.2。
构建以SO-NAME为名子的软联接的目的是,致使所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的SO-NAME,而不须要使用详尽版本号。在编译生产ELF文件时侯linux系统入门学习,假如文件A依赖于文件B,这么A的链接文件中的”.dynamic”段中会有DT_NEED类型的数组,数组的值就是B的SO-NAME。这样当动态链接器进行共享库依赖文件查找时,都会根据系统中各类共享库目录中的SO-NAME软联接手动定向到最新兼容版本的共享库。
★readelf-dsharelibrary可以查看so-name
★Linux提供了一个工具——ldconfig,当系统中安装或更新一个共享库时,须要运行这个工具,它会遍历默认所有共享库目录,例如/lib,/usr/lib等,之后更新所有的软链接,使他们指向最新共享库。
LinkName
当我们在编译器里使用共享库的时侯,如用GCC的“-l”参数链接共享库libtXXX.so.3.8.1,只须要在编译器命令行指定-lXXX即可,省略了前缀和版本信息。编译器会按照当前环境,在系统中的相关路径(常常由-L参数指定)查找最新版本的XXX库。这个XXX就是共享库的“链接名”。不同类型的库可能有相同的链接名,例如C语言运行库有静态版本(libc.a)也动态版本(libc.so.x.y.z)的区别,假如在链接时使用参数”-lc”,这么联接器都会按照输出文件的情况(动态/静态)来选择合适版本的库。eg.ld使用“-static”参数时吗,”-lc”会查找libc.a;假如使用“-Bdynamic”(默认),会查找最新版本的libc.so.x.y.z。
更详尽可以参见
代码:
1.Filelibhello.c
/* hello.c - demonstrate library use. */
#include
void hello(void)
{ printf("Hello, library world./n");}
2.Filelibhello.h
/* libhello.h - demonstrate library use. */
void hello(void);
3.Filemain.c
/* main.c -- demonstrate direct use of the "hello" routine */
#include "hello.h"
int main(void)
{
hello();
return 0;
}
1.生成共享库,关联realname和soname。
gcc-g-Wall-fPIC-chello.c-ohello.o
gcc-shared-W,soname,-libhello.so.0-olibhello.so.0.0.0hello.o
将会生成共享库libhello.so.0.0.0.
可以用系统提供的工具查看共享库的头:
readelf-dlibhello.so.0.0.0|greplibhello
oxe(SONAME)librarysoname:[libhello.so.0]
2.应用程序,引用共享库。
先手动生成link名子,以被旁边的程序链接时用
ln-slibhello.so.0.0.0libhello.so.0
gcc-g-Wall-cmain.c-omain.o-I.
gcc-omainmain.o-lhello-L.
(这儿我会出问题。由于:执行gcc-omainmain.o-lhello-L.命令的时侯linux 版本,默认会找libhello.so这个文件,而且似乎没有,直接出错。我又执行:
ln-slibhello.so.0.0.0libhello.so以后,才可以,到如今,没明白为何?)
查看编译下来的程序:
readelf-dmain|greplibhello
ox1(NEEDED)sharedlibrary:[libhello.so.0]
运行该程序,须要指定共享库的路径。有两种办法,第一种使用环境变量“LD_LIBRARY_PATH”.两外一种办法就是将共享库拷贝到系统目录(path环境变量指定的其中一个目录)。
暂停!我们还没有解决一个问题是,程序只晓得soname,如何从soname找到共享库,即realname文件呢?这须要我们定义一个link文件,联接到共享库本身。
ln-slibhello.so.0.0.0libhello.so.0
其实这个路径须要放在LD_LIBRARY_PATH环境变量中。
这样就可以运行该程序。
[Note]Linux系统提供一个命令ldconifg专门为生成共享库的soname文件,便于程序在加载时后通过soname找到共享库。同时该命令也为加速加载共享库,把系统的共享库放在一个缓存文件中,这样可以提升查找速率。可以用下边命令看一下系统已有的被缓存上去的共享库。
ld-p
运行程序:
./main这样,直接运行是不行了。动态库必须在运行的时侯,也指定路径。所以,请将.so文件,复制到/lib或则/usr/lib下,之后执行ldconfig/lib命令就OK了。
(
ldconfig是一个动态链接库管理命令
为了让动态链接库为系统所共享,还需运行动态链接库的管理命令--ldconfig
ldconfig命令的用途,主要是在默认搜救目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库(格式如前介绍,lib*.so*),从而创建出动态放入程序(ld.so)所需的联接和缓存文件.缓存文件默认为/etc/ld.so.cache,此文件保存已排好序的动态链接库名子列表.
ldconfig一般在系统启动时运行,而当用户安装了一个新的动态链接库时,就须要手工运行这个命令.)
3.共享库,小版本升级,即插口不变.
当升级小版本时,共享库的soname是不变的,所以须要重新把soname的那种联接文件指定新版本就可以。调用ldconfig命令,系统会帮你做更改那种sonamelink文件,并把它指向新的版本呢。这时侯你的应用程序就手动升级了。
4.共享库,主版本升级,即插口发生变化。
当升级主版本时,共享库的soname都会加1.例如libhello.so.0.0.0变为libhello.so.1.0.0.这时侯再运行ldconfig文件,都会发觉生成两个联接文件。
ln-slibhello.so.0---->libhello.so.0.0.0
ln-slibhello.so.1----->libhello.so.1.0.0
虽然共享库升级,而且你的程序仍然用的是旧的共享库,但是两个之间不会互相影响。
问题是假如更新的共享库只是降低一些插口,并没有更改已有的插口,也就是往前兼容。并且这时侯它的主版本号却降低1.假如你的应用程序想调用新的共享库,该如何办?简单,只要手工把soname文件更改,使其指向新的版本就可以。(这时侯ldconfig文件不会帮你做这样的事,由于这时侯soname和realname的版本号显卡本号不一致,只能自动更改)。
例如:ln-slibhello.so.0--->libhello.so.1.0.0
并且有时侯,主版本号降低,插口发生变化,可能往前不兼容。这时侯再这样子更改,都会报错,“xx”方法找不到之类的错误。
总结一下,Linux系统是通过共享库的三个不同名字,来管理共享库的多个版本。realname就是共享库的实际文件名子,soname就是共享库加载时的用的文件名。在生成共享库的时侯linux 获取动态库版本信息,编译器将soname绑定到共享库的文件头里,两者关联上去。在应用程序引用共享库时,其通过linkname来完成,link时将根据系统指定的目录去搜索link名子找到共享库,并将共享库的soname写在应用程序的头文件里。当应用程序加载共享库时,都会通过soname在系统指定的目录(pathorLD_LIBRARY)去找寻共享库。
当共享库升级时,分为两种。一种是显卡本不变,升级小版本和build号。在这些情况下,系统会通过更新soname(ldconfig来维护),来使用新的版本号。这中情况下,旧版本就没有用linux 获取动态库版本信息,可以删除。
另外一种是主版本升级,其意味着库的插口发生变化,其实,这时侯不能覆盖已有的soname。系统通过降低一个soname(ldconfig-p上面降低一项),致使新旧版本同时存在。原有的应用程序在加载时,还是按照自己头文件的旧soname去找寻老的库文件。
5.假如编译的时侯没有指定,共享库的soname,会如何样?
这是一个trick的地方。第一系统将会在生成库的时侯,就没有soname放在库的头上面。因而应用程序联接时侯,就把linkname放在应用程序依赖库上面。或则换句话说就是,soname这时侯不带版本号。有时侯有人直接借助这点来升级应用程序,例如,新版本的库,直接拷贝到系统目录下,才会覆盖掉早已存在的旧的库文件,直接升级。这个给程序员很大程度的便利性,假如一步当心,都会调到类似windows的Dllhell圈套上面。建议不要这样做。
【Note】
1.指定共享库加载的路径。LD_LIBRARY_PATH优先于path环境变量。
2.ldd可以查看程序,或则共享库依赖的库的路径
3.nm查看共享库曝露的插口
4.ldconfig可以手动生成soname的联接文件。并提供catch加速查找。
5.readelf可以查看动态库的信息,例如依赖的库,本身的soname。
6.objdump与readelf类似。
7ldTheGUNlinker
8.ld.sodynamiclinkerorloader
9.astheportableGNUassembley
【Reference】