共享函数库

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

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

 

共享函数库中的函数是在当一个可执行程序在启动的时候被加载。如果一个共享函数库正常安装,所有的程序在重新运行的时候都可以自动加载最新的函数库中的函数。对于Linux系统还有更多可以实现的功能:

        1、升级了函数库但是仍然允许程序使用老版本的函数库。

        2、当执行某个特定程序的时候可以覆盖某个特定的库或者库中指定的函数。

        3、可以在库函数被使用的过程中修改这些函数库。

共享库的命名惯例

按照共享库的命名惯例,每个共享库有三个文件名: real name、 soname和linker name。真正的库文件(而不是符号链接)的名字是real name,包含完整的共享库版本号。例如:libcap.so.1.10、 libc-2.8.90.so等。

soname是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。例如:libcap.so.1和libcap.so.2是两个主版本号不同的libcap,有些应用程序依赖于libcap.so.1,有些应用程序依赖于libcap.so.2,但对于依赖libcap.so.1的应用程序来说,真正的库文件不管是libcap.so.1.10还是libcap.so.1.11都可以用,所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。

linker name仅在编译链接时使用, gcc的-L选项应该指定linker name所在的目录。有的linkername是库文件的一个符号链接,有的linker name是一段链接脚本。 

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

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

文件系统层次化标准FHS(Filesystem Hierarchy Standard)规定了在一个发行包中大部分的函数库文件应该安装到/usr/lib目录下,但是如果某些库是在系统启动的时候要加载的,则放到/lib目录下,而那些不是系统本身一部分的库则放到/usr/local/lib下面。

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

环境变量

各种各样的环境变量控制着一些关键的过程。例如你可以临时为你特定的程序的一次执行指定一个不同的函数库。Linux系统中,通常变量LD_LIBRARY_PATH就是可以用来指定函数库查找路径的,而且这个路径通常是在查找标准的路径之前查找。这个是很有用的,特别是在调试一个新的函数库的时候,或者在特殊的场合使用一个非标准的函数库的时候。环境变量LD_PRELOAD列出了所有共享函数库中需要优先加载的库文件,功能和/etc/ld.so.preload类似。

事实上还有更多的环境变量影响着程序的调入过程,它们的名字通常就是以LD_或者RTLD_打头。 

创建一个共享函数库

现在我们开始学习如何创建一个共享函数库。其实创建一个共享函数库非常容易。首先创建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的帮助获得更详细的说明。 

安装和使用共享函数库

一旦你定义了一个共享函数库,你还需要安装它。其实简单的方法就是拷贝你的库文件到指定的标准的目录(例如/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来执行某些意想不到的代码,而产生不安全的隐患。 

不兼容的函数库

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

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

·          输出的数据项改变了。

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

·          某些输出函数的接口改变了。

    如果你能避免这些地方,你就可以保持你的函数库在二进制代码上的兼容,或者说,你可以使得你的程序的应用二进制接口(ABI:Application Binary Interface)上兼容。

posted @ 2018-04-08 15:16  刘-皇叔  阅读(714)  评论(0编辑  收藏  举报