linux的共享库(动态链接库)

 

Linux共享库

原文:http://www.linux.org/docs/ldp/howto/Program-Library-HOWTO/shared-libraries.html
一个完整的教程,包括静态库、动态库、动态加载,
http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

1.共享库
共享库指的是一种在程序启动时被载入的库。当一个共享库被正确安装后,在此之后启动的程序会自动的使用最新版的库。事实上它还具有更高的复杂性和灵活性,Linux系统通过共享库的方式允许你完成以下任务:

  • 更新库并且保持哪些需要使用旧版甚至是不能向后兼容的库的程序继续使用旧版库。

  • 执行个别程序时只覆盖指定的库甚至只覆盖一个库里的部分函数。

  • 当库还在被使用时也能完成上述任务。


1.1 约定

要使共享库实现上述的几项特性,需要遵守一些约定。你必须搞明白几个不同的库名称之间的区别,特别是“soname”和“real name”(以及怎样相互影响)。另外你还必须明白他们放在整个文件系统的什么地方。

1.1.1 共享库的名字们

每个共享库都有一个特别的名字叫“soname”soname由前缀‘lib’、库名称、’.so’后面再跟一个点号和一个只要接口有变化就会递增的数字版本号(有一个例外,最低层的C库并不以前缀 lib开头)。一个全规格的soname包括一个它所在的路径名做的前缀;(这句可能翻译的有问题,原文是:A fully-qualified soname includes as a prefix the directory it’s in)在一个可运行的系统上一个全规格的soname只是简单的做城一个指向共享库真实名字(real name)的符号链接。

每个共享库还有一个真实名(real name),就是包含实际库代码的文件的文件名。真实名(real name)是在soname名后加一个点后跟一个子版本号然后再跟一个点再跟一个发布编号(release number)。最后一个点号和发布编号(release number)是可选的。子版本号和发布编号提供了让你明确知道被安装的是那个版本来设置配置文件。注意这些数字编号不是必须和库文档里描述的一致,当然保持一致会让事情跟简单些。

另外,当编译器链接库时用到的名字(我称之为链接名(linker name)),只是简单的不带任何数字版本号的soname

管理共享库的关键就是分离好这些名字。程序在内部列出他们需要的共享库时,只应该使用soname;相反,当你创建一个共享库时,你只应该使用特别的文件名(带有版本信息细节的)。当你安装一个库的新版本时,你把它装到几个特别目录中的一个然后运行 ldconfig(8)ldconfig检查已经存在的文件并创建soname作为符号链接指向真实文件名,随后设置缓冲文件 /etc/ld.so.cache(后文解释)。

ldconfig并不设置链接名(linker name)。典型的这个工作是在安装库时来完成,并且链接名只是简单的创建为指向soname或真实名(real name)的符号链接文件,我建议是指向soname,因为大多数情况下你总是希望链接时自动链接到升级后的库文件,我曾经咨询过H.J.Lu为什么ldconfig不设置链接名(linker name),他的解释是典型情况下是想让程序链接到最新的库,但是也可能在开发环境下你想让他链接到一个老(可能都是不兼容)的版本。因此,ldconfig并不臆测你的程序到底要链接那个版本,所创建链接名的工作是安装程序的责任。

因此,/usr/lib/libreadline.so.3是一个全规格的soname,这是由ldconfig设置的,指向某个真实名诸如/usr/lib/libreadline.so.3.0,另外还应该有一个链接名,/usr/lib/libreadline.so ,它指向 /usr/lib/libreadline.so.3 .

1.1.2 位置
共享库必须放在文件系统中某个位置。大多数开源软件倾向与遵从GNU标准,更详细的信息请参考info文档:info:standands#Directroy Variables.GNU标准建议当发布源码时所有的库缺省都装到/usr/local/lib下(所有的命令文件都装到/usr/local/bin下)。他们也为安装程序打算覆盖这些缺省行为时定义了一些规则。

分层文件系统(FHS)讨论了分发时什么东西应该放在什么地方(参见http://www.pathname.com/fhs)。依照FHS,大多数库应该放在/usr/lib下,但是系统启动时需要的库应该放在/lib下,而哪些不是系统部分的库应该放在 /usr/local/lib

这两个文档之间其实并没有冲突。GNU讨论的是源代码,FHS讨论的是发布版的缺省行为(谁会选择覆盖缺省的源代码,这通常是系统管理包做的事)。在实践中这样的方式工作的非常好,你下载的“最后的(可能是有BUG的)”源码通常把自己安装的local下(/usr/local)。一旦代码成熟后,包管理器就可以把它分发到标准的位置通常就是覆盖缺省的了。如果你的库要调用的程序是一个只能通过库才能调用的程序,你应该把它放到/usr/local/libexec(正式发布时变为/usr/libexec)。一个麻烦的情况是Red-Hat家族的系统没有把/usr/local/lib作为搜索库的缺省路径,关于这个问题轻看下面关于/etc/ld.so.conf的论述。另外标准库路径还应该包括针对X-Window/usr/X11R6/lib。还有 /lib/security 是专供 PAM模块的,不过他们通常都是通过DL库来加载的(也参看下面的讨论)。


1.2 库是怎么样被使用的
在基于GNU glibc库的系统上,包括所有的linux系统,启动一个ELF格式的二进制可执行文件会导致一个程序加载器自动加载并运行。在linux系统上,这个加载器的名字是/lib/ld-linux.so.X(X是版本号数字)。这个加载器负责找到并加载这个程序所需要的共享库。

/etc/ld.so.conf文件中保存这搜索目录的列表。许多Red-Hat家族的系统在/etc/ld.so.conf文件里并没有包含/usr/local/lib目录,我认为这是一个BUG,在/etc/ld.so.conf文件里加上/usr/local/lib 目录可以解决很多程序在Red-Hat家族系统上运行的问题。

如果你只是想更新一个库里的少数几个函数,但是要保留库里的其他函数,你可以在/etc/ld.so.preload 文件里输入这些覆盖库的名字(.o文件),这些“预加载”库比标准库集有更高的优先权。这些预加载文件通常用于紧急补丁,而发布版分发时通常并不包含这类文件。

每次程序启动时都搜索这些目录会导致严重的效率问题,所以通常会使用缓冲。程序ldconfig(8) 通常用来读取/etc/ld.so.conf文件,在动态链接库的目录里设置正确的符号链接(所以他们遵守标准规范),然后写一个缓冲到/etc/ld.so.cache文件中以供其他程序使用。这样大大提高里读取库的速度。这里暗示出无论是加入一个DLL,删除一个DLL或者是DLL目录集发生变化是都必须运行ldconfig。运行ldconfig通常是包管理器在安装一个库时的一个步骤。这样在程序启动时,动态加载实际上是使用了/etc/ld.so.cache然后再加载它需要的库。

顺便说下,在FreeBSD系统里这个缓冲文件用了一个不同的名字。在FreeBSD中,ELF的缓冲是/var/run/ld-elf.so.hintsa.out的缓冲是/var/run/ld.so.hints。它们也还是通过ldconfig(8)来配置的,所以这个文件名不同的问题只有在极个别的情况下才需要考虑。

1.3 环境变量
有几个环境变量可以控制整个过程。还有一些环境变量允许你重新定义这些过程。

1.3.1 LD_LIBRARY_PATH
你可以为本次执行临时指定一个不同的库。在Linux里,环境变量LD_LIBRARY_PATH是一个用冒号分隔的目录集,这个目录集在标准库目录之前被搜索。这对于调试一个新库和出于特殊目的使用非标准库来说非常有用。环境变量LD_PRELOAD列出那些要替换标准库里的部分函数的库,就是/etc/ld.so.reload做的事。所有这些都由加载器/lib/ld-linux.so实现。另外我注意到,LD_LIBRARY_PATH在大多数类Unix系统上都能工作,但并不是所有的。举个例子,在HP-UX 系统上有这个功能但是环境变量叫SH_LIBPATH,而在AIX上是通过环境变量LIBPATH(语法相同,冒号分隔目录列表)来实现的。

LD_LIBRARY_PATH对于开发和测试来说是非常方便,但是不要在安装程序里为普通用户做这样的修改。请参看 http://www.visi.com/~barr/ldpath.html 解释了为什么使用LD_LIBRARY_PATH是糟糕的。但是对于开发和测试来说它还是非常有用的,而且可以工作在那些在相反的环境下不能工作的问题。如果你不想使用环境变量LD_LIBRARY_PATH,在Linux 里你甚至可以通过直接带参数的调用程序加载器。比如,给出一个PATH来代替环境变量LD_LIBRARY_PATH然后运行给定的程序:
/lib/ld-linux.so.2 --library-path PATH EXECUTABLE
直接不带参数执行ld-linux.so.2会告诉你更多的用法。不过再次声明,在正常使用环境下不要这样使用,这只是为调试而使用的。

1.3.2 LD_DEBUG
GNU C加载器里还有一个非常有用的环境变量就是 LD_DEBUG,它会触发所有的dl*函数输出详细信息报告它正在做什么。例如:
export LD_DEBUG = files
command_to_run
当加载这些文件和库的时候,会显示给你检测到那些依赖文件以及以什么样的顺序加载了那个SO文件。设置LD_DEBUG为 “bindings”会显示符号绑定信息,把它设成“libs”会显示库搜索路径,设成“versions"会显示版本依赖关系。

LD_DEBUG设成“help”,然后运行一个程序,会列出LD_DEBUG可能的选项。再次强调,LD_DEBUG不是给通常情况下使用的,但是调试和测试是非常有用的。

1.3.3 其他环境变量
实际上还有一些环境变量可以控制加载过程。它们都是以LD_RTLD_开头,这些都是为了加载过程的低层调试或实现一些特殊的功能。它们大都没有正式文档,如果你想要了解它们,最好的方法是阅读加载器的源代码(在gcc的代码里)。

允许用户重载动态链接库的加载过程但没有实现一些特殊的措施这对setuid/setgid程序会是灾难性的。因此,在GNU加载器里(在程序启动是加载程序的其余部分),如果程序是setuid setgid那这些环境变量(或者类似的变量)将被忽略或它们能做的事情会有很大的限制。加载器通过检查程序证书来确定它们是setuid还是setgid,如果uideuid不同,或者gidegid不同,加载器会假定这个程序是setuidsetgid(或是继承于它们的)那就会对环境变量的控制链接的能做出很大的限制。如果你阅读了GNU glibc的库源代码,你就会看到这些限制。查看elf/rtld.csysdeps/generic/dl-sysdep.c。换句话说,如果你让giduid等于egideuid,然后再运行程序,这些环境变量全部会生效。另外一些类Unix系统采取稍有不同的方法但都为了同一个目的:一个setgid/setuid程序不应该受环境变量的影响。

1.4 创建共享库
创建一个共享库是很容易的。首先,用gcc -fPIC -fpic参数编译源文件生成那些要放入共享库里的目标文件。-fPIC -fpic参数用来允许生成”位置无关代码“(position independent code )。对于共享库来说”位置无关代码“是必须的。下文会解释这有什么不同。用gcc-Wl 选项传递soname-Wl选项用来向链接器传递跟在它后面的链接选项(在这个例子里就是 -soname连接器选项),注意 -Wl后面的逗号并不是笔误,并且选项里不能包含空格。创建共享库用如下的格式:
gcc -shared -Wl,-soname,your_soname /
   -o
library_name file_list library_list

这儿有个例子,创建两个目标文件(a.o b.o),然后创建共享库包含它们两个。注意这里的编译包括创建调试信息(-g)并且会警告信息全开(-Wall),这对共享库来说并不需要但我们还是建议加上,编译器生成目标文件(-c)并且包括必须的参数 -fPIC

gcc -fPIC -g -c -Wall a.c
gcc -fPIC -g -c -Wall b.c gcc -shared -Wl,-soname,libmystuff.so.1 /    -o libmystuff.so.1.0.1 a.o b.o -lc



这里有几个要点要注意:

  • Don't strip the resulting library,并且不要使用编译选项 -fomit-frame-pointer除非你真的必须这样做,这样做生成的库可以工作,但是调试器将对他无能为力。

 

  • 要用-fPIC -fpic来生成代码。无论是用-fPIC 还是 -fpic生成的代码都是目标依赖的。-fPIC总是可以工作的,只是产生出来的代码比-fpic稍大点,(记住这个很容易,PIC是大写的,所以产生的代码就大一点),-fpic生成的代码更小一点更快一点,但是它是平台依赖的,比如全局可见符号的数量和代码的尺寸。当你创建共享库时连接器会告诉你这个选项是否合适。在不确定的情况下,我总是用-fPIC,因为它总是可以工作 。

 

  • 在某些情况下,调用gcc创建目标文件时还需要加上一个选项-Wl,-export-dynamic。通常,动态符号表只包括那些被动态目标使用到的符号。使用这个选项当创建ELF文件时会把所有的符号都加入动态符号表里。(查看ld(1)可以获得更多的信息)。当存在反向依赖关系时你需要使用这个选项,也就是说当加载一个库时存在一个未决符号并且按约定它是由加载这个库的程序来定义的。为了使反向依赖关系能够工作,主程序必须让它的符号也是动态的。注意如果你只工作在linux系统上你可以使用-rdynamic来代替-Wl,-export-dynamic,但是根据ELF文档的说明,在非linux系统的gcc-rdynamic不一定能正常工作。



在开发过程中,在修改一个同时被很多其他程序在使用的库时还有一些潜在的问题,并且你不想让其他程序使用这个开发版的库,只有你在测试的程序可以使用它。有一个链接选项你可以用就是ld的”rpath”,这是在编译主程序时来设定动态库的搜索路径的,对于gcc你可以这样做:
-Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)
如果你在生成库的客户端程序时使用这个参数,就不必再为设置LD_LIBRARY_PATH时要确保不要产生冲突而烦恼,也不必考虑用其他手段来隐藏库文件。

1.5 安装和使用共享库
一旦你创建好了库,就要考虑安装它了。最简单的方法就是简单的把它拷贝到标准库目录下比如/usr/lib,然后运行idconfig(8)
首先你需要在某处创建共享库,然后你需要设置一些必要的符号链接,特别是soname到真实名(real name)的链接(最好是从一个无版本号的soname就是一个以.so结尾的soname,给那些对版本没有特别要求的用户),最简单的方法就是运行:

ldconfig -n directory_with_shared_libraries



最后,当你编译程序时,你要告诉连接器任何你需要的静态和动态链接库,用 -l -L 选项来完成这个工作。

如果你不想把库安装在标准目录里,或者你没有权利修改/usr/lib,那么你就要用另外的方法了。在这中情况下,你需要把它安装到某处,然后给你的程序提供足够的信息让它能找到库。这儿有几种办法来做这件事。在简单情况下你可以使用 gcc -L。如果你只有一个特别的程序要用到你这个放在非标准目录里的库你也可以用 rpath 的方法(上文讲到过的)。你还可以使用环境变量来控制这件事,特别是你可以用LD_LIBRARY_PATH,就是由冒号分隔的目录列表来指明在标准目录之前先搜索哪些目录。如果你用bash,你可以象这样来调用my_program

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH  my_program



如果你只是想覆盖几个函数,你可以创建一个包含这几个新函数的目标文件,然后设置LD_PRELOAD来实现它,这个目标文件里的函数只是覆盖了原来的库里的函数,而其他函数继续使用。

通常你可以毫无顾虑的更新一个库。如果一个接口发生了改变,库的开发者通常会改变soname。这样,多个版本的库可以并存在同一个系统里,并且每个程序都会链接到正确的库上。然而,如果一个相同soname的升级库导致一个程序不能运行,你可以强制它使用老版本库,只要把老ia版本的库拷到某个地方,把这个不能运行的程序改个名(给它加个.orig表示它是老的),然后你创建一个包装(Wrapper)脚本用来重置要用的库并调用改过名后的老程序,你可以把这个老版本的库放到某个特殊的区域,如果你喜欢,你可以通过数字编号的方法让多个版本的库在同一个目录里。这个包装脚本看上去象下面这样:

  #!/bin/sh
  export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH
  exec /usr/bin/my_program.orig $*

当你写自己的程序时请别使用这种方法;请尽量确保你的库是向后兼容的或者当有不兼容的改变时请递增你的soname的版本号部分。这个方法只是为了解决一些很糟糕的问题的应急办法。

 

ldd(1)程序可以列出一个程序使用了哪些共享库。举个例子,你可以键入下面命令来看 ls 命令都使用了哪些共享库。

 

ldd /bin/ls

 

通常你可以看到一个被依赖的soname的列表,每个soname后跟一个它实际指向的目录,特别的在所有的情况下你都至少可以看到下面两条:

  • /lib/ld-linux.so.N(这里N1或以上的数字,通常至少是2),这是一个加载其他所有库的库。

  • libc.so.N(这里N6或更高),这是C库,甚至其他语言也倾向于使用C库(至少在实现它们自己的库时),所以大多数程序至少包含这个。

 

注意:不要在你不信任的程序上运行ldd,在ldd手册里有明确的说明,对于ELF对象ldd是通过设置一个特殊的环境变量LD_TRACE_LOADED_OBJECT然后运行这个程序,这使得这个不受信任的程序有可能可以执行任意代码(取代简单的输出一些ldd信息的代码) 。因此,出于安全的角度,不要用ldd来检查一个不信任的程序。

  1.  
    1.  

1.6不兼容库

当一个库和旧版的存在着二进制级的不兼容就需要改变它的soname,在C里,有四个原因会产生二进制兼容性问题:

  1. 函数的行为发生了变化因此它的地址会发生变化

  2. 引出的数据成员发生里变化(有个例外,如果结构只是在库内部分配内存,那在结构的尾部添加成员是可以的)

  3. 一个引出的函数被删除了

  4. 一个引出的函数的接口发生了变化。

如果你避免了这些情况,你就可以保持你的库是二进制兼容的。换句话说,如果你避免了上述问题,你的库就是ABIApplication Binary Interface)兼容的。比如,你可以加一个新函数,但是并不删除旧函数。你可以给结构添加成员,但是你必须确信旧的客户程序对结构成员的变化并不敏感并且只能在尾部添加成员,只有库(而不是客户程序)才能为结构分配内存,添加的成员对客户程序来说可以忽略的(或者由库来操纵它)。注意,如果客户把结构用在了数组里,那你不能扩展这个结构。

对于C++(包括那些支持模板或迟后编连技术的语言),情况就变得复杂的多了。上面的四条要遵循外,还有很多其他注意事项。所有这些要注意的就一个原因就是编译后的代码里由很多隐性代码,如果你不了解典型的C++实现方法那这些相互依赖关系不是那么显而易见的。严格的说,它们并不是什么新问题,它只是C++编译后的调用代码可能会让你很吃惊。下面这张列表(可能还不完整)列出了要保持二进制兼容在C++里你不能做的事,摘自 Troll Tech's Technical FAQ :

  1. 在虚函数里添加执行父类虚函数的调用(除非调用旧的实现时对旧的二进制级是安全的),因为编译器是在编译时(而非链接时)计算SuperClass::VirtualFunction的地址的。

  2. 添加或删除虚函数,因为这会改变每一个子类的虚表(vtbl)的大小和布局。

  3. 改变那些能被inline函数访问的数据成员的数据类型或删除这些数据成员。

  4. 改变类的继承关系,除非是添加新的叶子类(指最底层的子类)。

  5. 添加或删除私有数据成员,因为这会改变每个子类的大小和布局。

  6. 删除公共或保护的成员函数除非它们是inline的。

  7. 把公共和保护的成员函数改成inline的。

  8. 改变inline函数做的事,除非老版的仍可以工作。

  9. 在一个跨平台的程序里改变一个成员函数的存取权限(公共、保护、私有),因为有些编译器会把存取权限添加到函数名里。

 

有鉴于这张长列表,用C++开发库需要非常仔细的计划否则可能造成库的兼容性问题。所幸,在类Unix系统,包括linux你可以同一时刻装载多个版本的库,只要你的磁盘空间够,那些需要旧版库的旧程序都能运行。

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值