Linux共享库、静态库、动态库详解

1. 介绍
html

        使用GNU的工具咱们如何在Linux下建立本身的程序函数库?一个“程序函数库”简单的说就是一个文件包含了一些编译好的代码和数据,这些编译好的代码和数据能够在过后供其余的程序使用。程序函数库可使整个程序更加模块化,更容易从新编译,并且更方便升级。  linux

程序函数库可分为3种类型:静态函数库(static libraries)、共享函数库(shared libraries)、动态加载函数库(dynamically loaded libraries): 程序员

一、静态函数库,是在程序执行前就加入到目标程序中去了 ;windows

二、动态函数库同共享函数库是一个东西(在linux上叫共享对象库, 文件后缀是.so ,windows上叫动态加载函数库, 文件后缀是.dll)数组

 

Linux中命名系统中共享库的规则

2. 静态函数库
缓存

        静态函数库实际上就是简单的一个普通的目标文件的集合,通常来讲习惯用“.a”做为文件的后缀。能够用ar这个程序来产生静态函数库文件。Ar是archiver的缩写。静态函数库如今已经不在像之前用得那么多了,主要是共享函数库与之相比较有不少的优点的缘由。慢慢地,你们都喜欢使用共享函数库了。不过,在一些场所静态函数库仍然在使用,一来是保持一些与之前某些程序的兼容,二来它描述起来也比较简单。 安全

        静态库函数容许程序员把程序link起来而不用从新编译代码,节省了从新编译代码的时间。不过,在今天这么快速的计算机面前,通常的程序的从新编译也花费不了多少时间,因此这个优点已经不是像它之前那么明显了。静态函数库对开发者来讲仍是颇有用的,例如你想把本身提供的函数给别人使用,可是又想对函数的源代码进行保密,你就能够给别人提供一个静态函数库文件。理论上说,使用ELF格式的静态库函数生成的代码能够比使用共享函数库(或者动态函数库)的程序运行速度上快一些,大概1-5%。 bash

建立一个静态函数库文件,或者往一个已经存在地静态函数库文件添加新的目标代码,能够用下面的命令: app

        ar rcs my_library.a file1.o file2.o 模块化

这个例子中是把目标代码file1.o和file2.o加入到my_library.a这个函数库文件中,若是my_library.a不存在则建立一个新的文件。在用ar命令建立静态库函数的时候,还有其余一些能够选择的参数,能够参加ar的使用帮助。这里再也不赘述。

一旦你建立了一个静态函数库,你可使用它了。你能够把它做为你编译和链接过程当中的一部分用来生成你的可执行代码。若是你用gcc来编译产生可执行代码的话,你能够用“-l”参数来指定这个库函数。你也能够用ld来作,使用它的“-l”和“-L”参数选项。具体用法能够参考info:gcc。 

 

3. 共享函数库

共享函数库中的函数是在当一个可执行程序在启动的时候被加载。若是一个共享函数库正常安装,全部的程序在从新运行的时候均可以自动加载最新的函数库中的函数。对于Linux系统还有更多能够实现的功能: 
        一、升级了函数库可是仍然容许程序使用老版本的函数库。
        二、当执行某个特定程序的时候能够覆盖某个特定的库或者库中指定的函数。
        三、能够在库函数被使用的过程当中修改这些函数库。

3.1. 一些约定
若是你要编写的共享函数库支持全部有用的特性,你在编写的过程当中必须遵循一系列约定。你必须理解库的不一样的名字间的区别,例如它的“soname”和“real name”之间的区别和它们是如何相互做用的。你一样还要知道你应该把这些库函数放在你文件系统的什么位置等等。下面咱们具体看看这些问题。 

3.1.1. 共享库的命名

每一个共享函数库都有个特殊的名字,称做“soname”。soname名字命名必须以“lib”做为前缀,而后是函数库的名字,而后是“.so”,最后是版本号信息。不过有个特例,就是很是底层的C库函数都不是以lib开头这样命名的。
    每一个共享函数库都有一个真正的名字(“real name”),它是包含真正库函数代码的文件。真名有一个主版本号,和一个发行版本号。最后一个发行版本号是可选的,能够没有。主版本号和发行版本号使你能够知道你究竟是安装了什么版本的库函数。另外,还有一个名字是编译器编译的时候须要的函数库的名字,这个名字就是简单的soname名字,而不包含任何版本号信息。

管理共享函数库的关键是区分好这些名字。当可执行程序须要在本身的程序中列出这些他们须要的共享库函数的时候,它只要用soname就能够了;反过来,当你要建立一个新的共享函数库的时候,你要指定一个特定的文件名,其中包含很细节的版本信息。当你安装一个新版本的函数库的时候,你只要先将这些函数库文件拷贝到一些特定的目录中,运行ldconfig这个实用就能够。ldconfig检查已经存在的库文件,而后建立soname的符号连接到真正的函数库,同时设置/etc/ld.so.cache这个缓冲文件。这个咱们稍后再讨论。

ldconfig并不设置连接的名字,一般的作法是在安装过程当中完成这个连接名字的创建,通常来讲这个符号连接就简单的指向最新的soname或者最新版本的函数库文件。最好把这个符号连接指向soname,由于一般当你升级你的库函数后,你就能够自动使用新版本的函数库类。

咱们来举例看看:/usr/lib/libreadline.so.3 是一个彻底的完整的soname,ldconfig能够设置一个符号连接到其余某个真正的函数库文件,例如是/usr/lib/libreadline.so.3.0。同时还必须有一个连接名字,例如 /usr/lib/libreadline.so就是一个符号连接指向/usr/lib/libreadline.so.3。

3.1.2. 文件系统中函数库文件的位置

共享函数库文件必须放在一些特定的目录里,这样经过系统的环境变量设置,应用程序才能正确的使用这些函数库。大部分的源码开发的程序都遵循GNU的一些标准,咱们能够看info帮助文件得到相信的说明,info信息的位置是:info:standards#Directory_Variables。GNU标准建议全部的函数库文件都放在/usr/local/lib目录下,并且建议命令可执行程序都放在/usr/local/bin目录下。这都是一些习惯问题,能够改变的。 

文件系统层次化标准FHS(Filesystem Hierarchy Standard)(http://www.pathname.com/fhs)规定了在一个发行包中大部分的函数库文件应该安装到/usr/lib目录下,可是若是某些库是在系统启动的时候要加载的,则放到/lib目录下,而那些不是系统自己一部分的库则放到/usr/local/lib下面。 

上面两个路径的不一样并无本质的冲突。GNU提出的标准主要对于开发者开发源码的,而FHS的建议则是针对发行版本的路径的。具体的位置信息能够看/etc/ld.so.conf里面的配置信息。

3.2. 这些函数库如何使用

在基于GNU glibc的系统里,包括全部的linux系统,启动一个ELF格式的二进制可执行文件会自动启动和运行一个program loader。对于Linux系统,这个loader的名字是/lib/ld-linux.so.X(X是版本号)。这个loader启动后,反过来就会load全部的其余本程序要使用的共享函数库。

到底在哪些目录里查找共享函数库呢?这些定义缺省的是放在/etc/ld.so.conf文件里面,咱们能够修改这个文件,加入咱们本身的一些特殊的路径要求。大多数RedHat系列的发行包的/etc/ld.so.conf文件里面不包括/usr/local/lib这个目录,若是没有这个目录的话,咱们能够修改/etc/ld.so.conf,本身手动加上这个条目。

若是你想覆盖某个库中的一些函数,用本身的函数替换它们,同时保留该库中其余的函数的话,你能够在 /etc/ld.so.preload中加入你想要替换的库(.o结尾的文件),这些preloading的库函数将有优先加载的权利。

当程序启动的时候搜索全部的目录显然会效率很低,因而Linux系统实际上用的是一个高速缓冲的作法。ldconfig缺省状况下读出/etc/ld.so.conf相关信息,而后设置适当地符号连接,而后写一个cache到 /etc/ld.so.cache这个文件中,而这个/etc/ld.so.cache则能够被其余程序有效的使用了。这样的作法能够大大提升访问函数库的速度。这就要求每次新增长一个动态加载的函数库的时候,就要运行ldconfig来更新这个cache,若是要删除某个函数库,或者某个函数库的路径修改了,都要从新运行ldconfig来更新这个cache。一般的一些包管理器在安装一个新的函数库的时候就要运行ldconfig。 

另外,FreeBSD使用cache的文件不同。FreeBSD的ELF cache是/var/run/ld-elf.so.hints,而a.out的cache则是/var/run/ld.so.hints。它们一样是经过ldconfig来更新。

3.3. 环境变量

各类各样的环境变量控制着一些关键的过程。例如你能够临时为你特定的程序的一次执行指定一个不一样的函数库。Linux系统中,一般变量LD_LIBRARY_PATH就是能够用来指定函数库查找路径的,并且这个路径一般是在查找标准的路径以前查找。这个是颇有用的,特别是在调试一个新的函数库的时候,或者在特殊的场合使用一个非标准的函数库的时候。环境变量LD_PRELOAD列出了全部共享函数库中须要优先加载的库文件,功能和/etc/ld.so.preload相似。这些都是有/lib/ld-linux.so这个loader来实现的。值得一提的是,LD_LIBRARY_PATH能够在大部分的UNIX-linke系统下正常起做用,可是并不是全部的系统下均可以使用,例如HP-UX系统下,就是用SHLIB_PATH这个变量,而在AIX下则使用LIBPATH这个变量。

LD_LIBRARY_PATH在开发和调试过程当中常常大量使用,可是不该该被一个普通用户在安装过程当中被安装程序修改,你们能够去参考http://www.visi.com/~barr/ldpath.html,这里有一个文档专门介绍为何不使用LD_LIBRARY_PATH这个变量。

事实上还有更多的环境变量影响着程序的调入过程,它们的名字一般就是以LD_或者RTLD_打头。大部分这些环境变量的使用的文档都是不全,一般搞得人头昏眼花的,若是要真正弄清楚它们的用法,最好去读loader的源码(也就是gcc的一部分)。

容许用户控制动态连接函数库将涉及到setuid/setgid这个函数,若是特殊的功能须要的话。所以,GNU loader一般限制或者忽略用户对这些变量使用setuid和setgid。若是loader经过判断程序的相关环境变量判断程序的是否使用了setuid或者setgid,若是uid和euid不一样,或者gid和egid部同样,那么loader就假定程序已经使用了setuid或者setgid,而后就大大的限制器控制这个老连接的权限。若是阅读GNU glibc的库函数源码,就能够清楚地看到这一点。特别的咱们能够看elf/rtld.c和sysdeps/generic/dl-sysdep.c这两个文件。这就意味着若是你使得uid和gid与euid和egid分别相等,而后调用一个程序,那么这些变量就能够彻底起效。

3.4. 建立一个共享函数库

如今咱们开始学习如何建立一个共享函数库。其实建立一个共享函数库很是容易。首先建立object文件,这个文件将加入经过gcc –fPIC参数命令加入到共享函数库里面。PIC的意思是“位置无关代码”(Position Independent Code)。下面是一个标准的格式:

        gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list

下面再给一个例子,它建立两个object文件(a.o和b.o),而后建立一个包含a.o和b.o的共享函数库。例子中”-g”和“-Wall”参数不是必须的。

        gcc -fPIC -g -c -Wall a.c

        gcc -fPIC -g -c -Wall b.c

        gcc -shared -Wl,-soname,liblusterstuff.so.1 -o liblusterstuff.so.1.0.1 a.o b.o -lc

下面是一些须要注意的地方:

不用使用-fomit-frame-pointer这个编译参数除非你不得不这样。虽然使用了这个参数得到的函数库仍然可使用,可是这使得调试程序几乎没有用,没法跟踪调试。

使用-fPIC来产生代码,而不是-fpic。

某些状况下,使用gcc 来生成object文件,须要使用“-Wl,-export-dynamic”这个选项参数。 

一般,动态函数库的符号表里面包含了这些动态的对象的符号。这个选项在建立ELF格式的文件时候,会将全部的符号加入到动态符号表中。能够参考ld的帮助得到更详细的说明。

3.5. 安装和使用共享函数库

一旦你定义了一个共享函数库,你还须要安装它。其实简单的方法就是拷贝你的库文件到指定的标准的目录(例如/usr/lib),而后运行ldconfig。

若是你没有权限去作这件事情,例如你不能修改/usr/lib目录,那么你就只好经过修改你的环境变量来实现这些函数库的使用了。首先,你须要建立这些共享函数库;而后,设置一些必须得符号连接,特别是从soname到真正的函数库文件的符号连接,简单的方法就是运行ldconfig:

        ldconfig -n directory_with_shared_libraries 
而后你就能够设置你的LD_LIBRARY_PATH这个环境变量,它是一个以逗号分隔的路径的集合,这个能够用来指明共享函数库的搜索路径。例如,使用bash,就能够这样来启动一个程序my_program:

        LD_LIBRARY_PATH=$LD_LIBRARY_PATH my_program

若是你须要的是重载部分函数,则你就须要建立一个包含须要重载的函数的object文件,而后设置LD_PRELOAD环境变量。

一般你能够很方便的升级你的函数库,若是某个API改变了,建立库的程序会改变soname。然而,若是一个函数升级了某个函数库而保持了原来的soname,你能够强行将老版本的函数库拷贝到某个位置,而后从新命名这个文件(例如使用原来的名字,而后后面加.orig后缀),而后建立一个小的“wrapper”脚原本设置这个库函数和相关的东西。例以下面的例子:

        #!/bin/sh export LD_LIBRARY_PATH=/usr/local/my_lib,$LD_LIBRARY_PATH

        exec /usr/bin/my_program.orig $*

咱们能够经过运行ldd来看某个程序使用的共享函数库。例如你能够看ls这个实用工具使用的函数库:

        ldd /bin/ls

        libtermcap.so.2 => /lib/libtermcap.so.2 (0x4001c000)

        libc.so.6 => /lib/libc.so.6 (0x40020000)

        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)  
一般我么能够看到一个soname的列表,包括路径。在全部的状况下,你都至少能够看到两个库:

·                   /lib/ld-linux.so.N(N是1或者更大,通常至少2)。这是这个用于加载其余全部的共享库的库。

·                    libc.so.N(N应该大于或者等于6)。这是C语言函数库。

值得一提的是,不要在对你不信任的程序运行ldd命令。在ldd的manual里面写得很清楚,ldd是经过设置某些特殊的环境变量(例如,对于ELF对象,设置LD_TRACE_LOADED_OBJECTS),而后运行这个程序。这样就有可能使得某地程序可能使得ldd来执行某些意想不到的代码,而产生不安全的隐患。

3.6. 不兼容的函数库

若是一个新版的函数库要和老版本的二进制的库不兼容,则soname须要改变。对于C语言,一共有4个基本的理由使得它们在二进制代码上很难兼容:

一个函数的行文改变了,这样它就可能与最开始的定义不相符合。

·          输出的数据项改变了。

·          某些输出的函数删除了。

·          某些输出函数的接口改变了。
    若是你能避免这些地方,你就能够保持你的函数库在二进制代码上的兼容,或者说,你可使得你的程序的应用二进制接口(ABI:Application Binary Interface)上兼容。

 

4. 动态加载的函数库Dynamically Loaded (DL) Libraries

 

动态加载的函数库Dynamically loaded (DL) libraries是一类函数库,它能够在程序运行过程当中的任什么时候间加载。它们特别适合在函数中加载一些模块和plugin扩展模块的场合,由于它能够在当程序须要某个plugin模块时才动态的加载。例如,Pluggable Authentication Modules(PAM)系统就是用动态加载函数库来使得管理员能够配置和从新配置身份验证信息。

Linux系统下,DL函数库与其余函数库在格式上没有特殊的区别,咱们前面提到过,它们建立的时候是标准的object格式。主要的区别就是这些函数库不是在程序连接的时候或者启动的时候加载,而是经过一个API来打开一个函数库,寻找符号表,处理错误和关闭函数库。一般C语言环境下,须要包含这个头文件。 
        Linux中使用的函数和Solaris中同样,都是dlpoen() API。固然不是全部的平台都使用一样的接口,例如HP-UX使用shl_load()机制,而Windows平台用另外的其余的调用接口。若是你的目的是使得你的代码有很强的移植性,你应该使用一些wrapping函数库,这样的wrapping函数库隐藏不一样的平台的接口区别。一种方法是使用glibc函数库中的对动态加载模块的支持,它使用一些潜在的动态加载函数库界面使得它们能够夸平台使用。具体能够参考http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html. 另一个方法是使用libltdl,是GNU libtool的一部分,能够进一步参考CORBA相关资料。  

4.1. dlopen()
dlopen函数打开一个函数库而后为后面的使用作准备。C语言原形是:

        void * dlopen(const char *filename, int flag);

若是文件名filename是以“/”开头,也就是使用绝对路径,那么dlopne就直接使用它,而不去查找某些环境变量或者系统设置的函数库所在的目录了。不然dlopen()就会按照下面的次序查找函数库文件:
       1. 环境变量LD_LIBRARY指明的路径。

2. /etc/ld.so.cache中的函数库列表。

3. /lib目录,而后/usr/lib。不过一些很老的a.out的loader则是采用相反的次序,也就是先查 /usr/lib,而后是/lib。
    dlopen()函数中,参数flag的值必须是RTLD_LAZY或者RTLD_NOW,RTLD_LAZY的意思是resolve undefined symbols as code from the dynamic library is executed,而RTLD_NOW的含义是resolve all undefined symbols before dlopen() returns and fail if this cannot be done'。
    若是有好几个函数库,它们之间有一些依赖关系的话,例如X依赖Y,那么你就要先加载那些被依赖的函数。例如先加载Y,而后加载X。

    dlopen()函数的返回值是一个句柄,而后后面的函数就经过使用这个句柄来作进一步的操做。若是打开失败dlopen()就返回一个NULL。若是一个函数库被屡次打开,它会返回一样的句柄。 
    若是一个函数库里面有一个输出的函数名字为_init,那么_init就会在dlopen()这个函数返回前被执行。咱们能够利用这个函数在个人函数库里面作一些初始化的工做。咱们后面会继续讨论这个问题的。  
4.2. dlerror()

经过调用dlerror()函数,咱们能够得到最后一次调用dlopen(),dlsym(),或者dlclose()的错误信息。 
4.3. dlsym()

若是你加载了一个DL函数库而不去使用固然是不可能的了,使用一个DL函数库的最主要的一个函数就是dlsym(),这个函数在一个已经打开的函数库里面查找给定的符号。这个函数以下定义:

        void * dlsym(void *handle, char *symbol);

函数中的参数handle就是由dlopen打开后返回的句柄,symbol是一个以NIL结尾的字符串。若是dlsym()函数没有找到须要查找的symbol,则返回NULL。若是你知道某个symbol的值不多是NULL或者0,那么就很好,你就能够根据这个返回结果判断查找的symbol是否存在了;不过,若是某个symbol的值就是NULL,那么这个判断就有问题了。标准的判断方法是先调用dlerror(),清除之前可能存在的错误,而后调用dlsym()来访问一个symbol,而后再调用dlerror()来判断是否出现了错误。一个典型的过程以下:

[cpp]  view plain  copy
 
 print?
  1. dlerror();      /*clear error code */  
  2. s = (actual_type)dlsym(handle, symbol_being_searched_for);  
  3. if((error = dlerror()) != NULL){  
  4.     /* handle error, the symbol wasn't found */  
  5. else {  
  6.     /* symbol found, its value is in s */  
  7. }  

4.4. dlclose()

dlopen()函数的反过程就是dlclose()函数,dlclose()函数用力关闭一个DL函数库。Dl函数库维持一个资源利用的计数器,当调用dlclose的时候,就把这个计数器的计数减一,若是计数器为0,则真正的释放掉。真正释放的时候,若是函数库里面有_fini()这个函数,则自动调用_fini()这个函数,作一些必要的处理。Dlclose()返回0表示成功,其余非0值表示错误。

4.5. DL Library Example

下面是一个例子。例子中调入math函数库,而后打印2.0的余弦函数值。例子中每次都检查是否出错。应该是个不错的范例:

[cpp]  view plain  copy
 
 print?
  1. int main(int argc, char *argv){  
  2.         void *handle;  
  3.         char *error;  
  4.           
  5.         double (*cosine )(double);  
  6.         handle = dlopen("/lib/libm.so.6", RTLD_LAZY);  
  7.         if(!handle){  
  8.             fputs(dlerror(), stderr);  
  9.              exit(1);  
  10.         }  
  11.           
  12.         cosine = dlsym(handle, "cos");  
  13.         if((error = dlerror()) != NULL){  
  14.             fputs(error, stderr);  
  15.             exit(1);  
  16.         }  
  17.           
  18.         printf("%f", (*cosine)(2, 0));  
  19.           
  20.         dlclose(handle);  
  21.           
  22.         return 0;  
  23. }  

若是这个程序名字叫foo.c,那么用下面的命令来编译:

        gcc -o foo foo.c –ldl

 

共享库

共享库是程序启动时加载的库。共享库安装正确后,全部启动的程序将自动使用新的共享库。它实际上比这更灵活和复杂,由于Linux使用的方法容许您:

 

  • 更新库而且仍然支持但愿使用这些库的旧版,非后向兼容版本的程序;

  • 在执行特定程序时,重写特定库或甚至库中的特定函数。

  • 在程序使用现有库运行时执行全部这些操做。

 

3.1。约定

对于共享库来支持全部这些所需的属性,必须遵循许多约定和准则。您须要了解图书馆名称之间的区别,特别是“soname”和“实名”(以及它们的相互做用)。您还须要了解它们应该放在文件系统中的位置。

3.1.1。共享库名称

每一个共享库都有一个名为“soname”的特殊名称。soname具备前缀``lib'',库的名称,短语“.so”,后跟一个句点和一个版本号,每当界面改变时都会递增(做为一个特殊的例外,级别C库不以“lib”开头)。一个彻底合格的soname包含做为前缀的目录; 在一个工做系统上,一个彻底合格的soname只是一个与共享库的“真实姓名”的符号连接。

每一个共享库还有一个“实名”,它是包含实际库代码的文件名。真正的名字增长了一个时期,次要号码,另外一个时期和发行号码。最后一个期间和发行号码是可选的。次要号码和发行号码经过让您准确知道安装了哪些版本的库,来支持配置控制。请注意,这些数字可能与用于在文档中描述库的数字不一样,尽管这样作更容易。

另外,编译器在请求库时使用的名称(我将其称为“连接器名称”),这只是没有任何版本号的soname。

管理共享库的关键是这些名称的分离。程序在内部列出他们须要的共享库时,应该只列出他们须要的soname。相反,建立共享库时,只能建立具备特定文件名的库(具备更详细的版本信息)。当您安装新版本的库时,将其安装在几个特殊目录之一中,而后运行程序ldconfig(8)。ldconfig检查现有文件,并将声名建立为真实名称的符号连接,以及设置缓存文件/etc/ld.so.cache(稍后描述)。

ldconfig不设置连接器名称; 一般这是在库安装期间完成的,连接器名称简单地建立为“最新”的soname或最新的真实名称的符号连接。我建议将连接器名称做为与soname的符号连接,由于在大多数状况下,若是您更新库,那么您但愿在连接时自动使用它。我问HJ Lu为何ldconfig不会自动设置连接器名称。他的解释基本上是你可能想使用最新版本的库来运行代码,可是可能须要  开发 连接到旧的(可能不兼容的)库。所以,ldconfig不会对您但愿程序连接的任何假设,所以安装程序必须特别修改符号连接以更新连接器将用于库。

所以,/  usr  /lib/libreadline.so.3是一个彻底限定的soname,其中ldconfig将被设置为与/usr/lib/libreadline.so.3.0之类的一些真实名称的符号连接  还应该有一个连接器名称  /usr/lib/libreadline.so  ,它能够是引用/usr/lib/libreadline.so.3的符号连接  

3.1.2。文件系统放置

共享库必须位于文件系统的某个位置。大多数开源软件每每遵循GNU标准; 有关更多信息,请参阅info:standards#Directory_Variables上的信息文件文档  GNU标准建议默认安装/ usr / local / lib中的全部库,当分发源代码(全部命令都应该进入/ usr / local / bin)时。它们还定义了覆盖这些默认值和调用安装例程的约定。

文件系统层次标准(FHS)讨论了在分发中应该去哪里(请参阅  http://www.pathname.com/fhs)。根据FHS,大多数库应该安装在/ usr / lib中,但启动所需的库应该在/ lib中,不属于系统的库应该在/ usr / local / lib中。

这两个文件之间没有真正的冲突; GNU标准建议开发人员使用默认的源代码,而FHS则建议分销商使用默认值(一般经过系统的软件包管理系统来选择覆盖源代码默认值)。在实践中,这很好地工做:您下载的“最新”(多是buggy!)源代码自动安装在“本地”目录(/ usr / local),一旦该代码已经成熟,软件包管理器能够轻松地覆盖默认值,以将代码放置在标准的发行版中。请注意,若是您的库调用只能经过库调用的程序,则应将这些程序放在/ usr / local / libexec(在/ usr / libexec中)。一个复杂的状况是,Red Hat派生的系统在搜索库时默认不包括/ usr / local / lib; 请参阅下面关于/etc/ld.so.conf的讨论。其余标准库位置包括用于X-windows的/ usr / X11R6 / lib。请注意,/ lib / security用于PAM模块,但一般会做为DL库加载(下面也将讨论)。

3.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。我认为这是一个错误,并在/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.hints,a.out缓存为/var/run/ld.so.hints。这些仍然由ldconfig(8)更新,因此这个位置的差别只能在几个异乎寻常的状况下重要。

3.3。环境变量

各类环境变量能够控制此过程,而且有一些环境变量容许您覆盖此过程。

3.3.1。LD_LIBRARY_PATH

您能够临时替换不一样的库进行此特定执行。在Linux中,环境变量LD_LIBRARY_PATH是一个冒号分隔的目录库,首先要在库文件的标准目录集以前进行搜索; 当调试新库或为特殊目的使用非标准库时,这很是有用。环境变量LD_PRELOAD列出了覆盖标准集的函数的共享库,就像/etc/ld.so.preload同样。这些由加载器/lib/ld-linux.so实现。我应该注意,虽然LD_LIBRARY_PATH适用于许多类Unix系统,但它并不适用; 例如,此功能在HP-UX上可用,但做为环境变量SHLIB_PATH,在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  - 文件路径路径可执行

只需执行ld-linux.so而不使用参数便可提供更多的使用帮助,可是再一次不要使用它来进行正常使用 - 这些都是用于调试的。

3.3.2。LD_DEBUG

GNU C加载器中的另外一个有用的环境变量是LD_DEBUG。这会触发dl *函数,以便他们提供关于他们正在作什么的至关详细的信息。例如:

  导出LD_DEBUG =文件
  command_to_run

在处理库时显示文件和库的处理,告诉您哪些依赖关系被检测到,哪些SO以什么顺序加载。将LD_DEBUG设置为“bindings”显示有关符号绑定的信息,将其设置为“libs”,显示库搜索路径,并将ti设置为“`versions”显示版本依赖。

将LD_DEBUG设置为“帮助”,而后尝试运行程序将列出可能的选项。再次,LD_DEBUG不适用于正常使用,但在调试和测试时能够方便。

3.3.3。其余环境变量

实际上还有一些控制加载过程的其余环境变量; 他们的名字以LD_或RTLD_开头。大多数其余的是用于低级别的加载程序调试或用于实现专门的功能。他们大多没有文件证实; 若是您须要了解它们,了解它们的最佳方式是读取装载器的源代码(gcc的一部分)。

若是不采起特殊措施,容许用户控制动态连接的库对于setuid / setgid程序将是灾难性的。所以,在GNU加载程序(程序启动时加载程序的其他部分)中,若是程序为setuid或setgid,那么这些变量(和其余相似的变量)将被忽略或受到很大的限制。加载程序经过检查程序的凭据来肯定程序是否被setuid或setgid; 若是uid和euid不一样,或者gid和egid不一样,那么加载器会假定程序是setuid / setgid(或者从一个降低的),所以极大地限制了其控制连接的能力。若是您阅读GNU glibc库源代码,能够看到这一点; 特别看到文件elf / rtld.c和sysdeps / generic / dl-sysdep.c。这意味着若是你使uid和gid等于euid和egid,而后调用一个程序,这些变量就会有效果。其余类Unix系统处理不一样的状况,但出于一样的缘由:setuid / setgid程序不该该受到环境变量集的不当影响。

3.4。建立共享库

建立共享库很容易。首先,使用gcc -fPIC或-fpic标志建立将进入共享库的对象文件。-fPIC和-fpic选项能够实现“位置独立代码”生成,这是共享库的一个要求; 见下文的差别。您使用-Wl gcc选项传递soname。-Wl选项将选项传递给连接器(在这种状况下为-soname连接器选项) - -Wl以后的逗号不是打字错误,而且您不能在选项中包含未转义的空格。而后使用如下格式建立共享库:

gcc -shared -Wl,-soname,your_soname \
    -o library_name  file_list  library_list

这是一个例子,它建立两个对象文件(ao和bo),而后建立一个包含它们的共享库。请注意,此编译包括调试信息(-g),并将生成警告(-Wall),这些共享库不是必需的,但建议使用。编译生成对象文件(使用-c),并包含所需的-fPIC选项:

gcc -fPIC -g -c -Wall ac
gcc -fPIC -g -c -Wall bc
gcc -shared -Wl,-soname,libmystuff.so.1 \
    -o libmystuff.so.1.0.1 ao bo -lc

这里有几点值得注意:

 

  • 不要剥离生成的库,而且不要使用编译器选项-fomit-frame-pointer,除非你真的必须。生成的库将工做,但这些操做使调试器大多没有用。

  • 使用-fPIC或-fpic生成代码。是否使用-fPIC或-fpic生成代码是依赖于目标的。-fPIC选项始终有效,可是可能产生比-fpic更大的代码(请记住,这是PIC在更大的状况下,所以可能产生更大量的代码)。使用-fpic选项一般会生成更小更快的代码,但会有平台相关的限制,例如全局可见符号的数量或代码的大小。连接器将告诉您,建立共享库时是否适合。若是有疑问,我选择-fPIC,由于它老是有效。

  • 在某些状况下,调用gcc来建立对象文件也须要包含“-Wl,-export-dynamic”选项。一般,动态符号表仅包含动态对象使用的符号。此选项(建立ELF文件时)将全部符号添加到动态符号表(有关详细信息,请参阅ld(1))。当有“反向相关性”时,您须要使用此选项,即,DL库具备未解决的符号,按照惯例,必须在要加载这些库的程序中定义它们。对于“反向相关性”工做,主程序必须使其符号动态可用。请注意,若是您只使用Linux系统,则可使用“-rdynamic”而不是“-Wl,export-dynamic”,但根据ELF文档,“-rdynamic”

 

在开发过程当中,修改也被许多其余程序使用的库的潜在问题 - 您不但愿其余程序使用“开发”库,只是您正在测试的特定应用程序。您可能使用的一个连接选项是ld的“rpath”选项,它指定正在编译的特定程序的运行时库搜索路径。从gcc,您能够经过这样指定来调用rpath选项:

 -Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)

若是您在构建库客户机程序时使用此选项,则不须要再打扰LD_LIBRARY_PATH(下文将介绍),除了确保它不冲突,或者使用其余技术来隐藏库。

3.5。安装和使用共享库

建立共享库后,您须要安装它。简单的方法是将库复制到标准目录(例如/ usr / lib)中,并运行ldconfig(8)。

首先,您须要在某个地方建立共享库。而后,您将须要设置必要的符号连接,特别是从soname到真实名称的连接(以及从无版本的soname,即以“.so”结尾的soname)为用户谁没有指定版本)。最简单的方法是运行:

ldconfig -n directory_with_shared_libraries

最后,当你编译你的程序时,你须要告诉连接器你正在使用的任何静态和共享库。使用-l和-L选项。

若是您不能或不想在标准位置安装库(例如,您没有权限修改/ usr / lib),则须要更改方法。在这种状况下,您须要将其安装在某个地方,而后为您的程序提供足够的信息,以便程序能够找到库...而且有几种方法能够作到这一点。您能够在简单的状况下使用gcc的-L标志。您可使用“rpath”方法(如上所述),特别是若是您只有一个特定的程序将库放置在“非标准”位置。您也可使用环境变量来控制事物。特别是,您能够设置LD_LIBRARY_PATH,这是一个冒号分隔的目录列表,用于在一般的位置以前搜索共享库。若是你使用bash,

LD_LIBRARY_PATH =。:$ LD_LIBRARY_PATH my_program

若是要仅覆盖几个选定的函数,能够经过建立一个覆盖目标的文件并设置LD_PRELOAD来实现; 此对象文件中的函数将仅覆盖这些函数(留下其余函数)。

一般你能够不须要更新库; 若是有API更改,则库建立者应该更改soname。这样,多个库能够在单个系统上,并为每一个程序选择正确的库。可是,若是一个程序中断更新到保持相同soname的库,您能够强制它使用旧的库版本经过将旧的库复制到某个地方,重命名该程序(好比说旧的名称加上“.orig ''),而后建立一个小的“包装器”脚本,该脚本重置库以使用并调用真实(重命名)程序。您能够将旧图书馆放在本身的特殊区域,若是您愿意,尽管编号约定容许多个版本生活在同一目录中。包装脚本可能看起来像这样:

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

编写本身的程序时请不要依赖这个; 尝试确保您的库向后兼容,或者您​​每次进行不兼容的更改时都会在soname中增长版本号。这只是处理最坏状况问题的“紧急”方法。

您可使用ldd(1)查看程序使用的共享库列表。因此,例如,您能够经过键入如下方式查看ls使用的共享库:

  ldd / bin / ls

通常来讲,您将看到依赖的声名的列表,以及这些名称解析的目录。在几乎全部状况下,您至少有两个依赖关系:

 

  • /lib/ld-linux.so.N(其中N为1或更多,一般至少为2)。这是加载全部其余库的库。

  • libc.so.N(N为6以上)。这是C库。即便是其余语言也倾向于使用C库(至少要实现本身的库),因此大多数程序至少包括这个库。

请注意:千万   不能   对你不信任的程序运行LDD。如ldd(1)手册中明确指出的,ldd经过设置特殊环境变量(对于ELF对象,LD_TRACE_LOADED_OBJECTS),而后执行程序(在某些状况下)工做。不可信程序可能强制ldd用户运行任意代码(而不是简单地显示ldd信息)。因此,为了安全起见,不要在不信任的程序上使用ldd来执行。

 

3.6。不兼容的库

当新版本的库与旧版本的二进制不兼容时,soname须要更改。在C中,图书馆将再也不是二进制兼容的四个基本缘由:

 

  1. 函数的行为发生变化,使其再也不符合其原始规范,

  2. 导出的数据项更改(例外:将可选项添加到结构的末尾是能够的,只要这些结构只在库中分配)。

  3. 导出的功能被删除。

  4. 导出功能的界面发生变化。

 

若是能够避免这些缘由,可使您的库二进制兼容。换句话说,若是您避免此类更改,您能够保持您的应用程序二进制接口(ABI)兼容。例如,您可能须要添加新功能,但不要删除旧功能。您能够向结构中添加项目,但只有经过将项目添加到结构的末尾才能确保旧程序不会对这些更改敏感,只容许库(而不是应用程序)分配结构,使额外的项目可选(或将库填充到其中),等等。注意 - 若是用户在数组中使用它们,您可能没法展开结构。

对于C ++(以及支持编译模板和/或编译调度方法的其余语言),状况更加棘手。全部上述问题都适用,还有更多问题。缘由是一些信息在编译代码中被实现为“在封面下”,致使依赖关系,若是您不知道如何一般实现C ++,这可能并不明显。严格来讲,它们不是“新”的问题,只是编译的C ++代码以可能令您惊讶的方式调用它们。如下是您不能在C ++中执行的(多是不完整的)列表,并保留二进制兼容性,如  Troll Tech的技术常见问题报告

 

  1. 添加虚拟函数的从新实现(除非它对于旧的二进制文件调用原始实现是安全的),由于编译器在编译时评估SuperClass :: virtualFunction()调用(而不是连接时)。

  2. 添加或删除虚拟成员函数,由于这会改变每一个子类的vtbl的大小和布局。

  3. 更改任何数据成员的类型或移动可经过内联成员函数访问的任何数据成员。

  4. 更改类层次结构,除了添加新的树叶。

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

  6. 删除公共或受保护的成员函数,除非它们是内联的。

  7. 公开或保护成员函数内联。

  8. 更改内联函数的做用,除非旧版本继续工做。

  9. 在便携式程序中更改为员函数的访问权限(即公共,受保护或私有),由于一些编译器将访问权限转换为函数名称。

 

给定这个冗长的列表,特别是C ++库的开发人员必须计划更多的偶尔更新破坏二进制兼容性。幸运的是,在类Unix系统(包括Linux)上,您能够同时加载多个版本的库,因此当有一些磁盘空间损失时,用户仍然能够运行须要旧库的“旧”程序。

相关文章
相关标签/搜索