Linux动态链接库.so文件的创建与使用

李大钊英勇就义94周年Linux动态链接库.so⽂件的创建与使⽤
1. 介绍
使⽤GNU的⼯具我们如何在Linux下创建⾃⼰的程序函数库?⼀个“程序函数库”简单的说就是⼀个⽂件包含了⼀些编译好的代码和数据,这些编译好的代码和数据可以在事后供其他的程序使⽤。程序函数库可以使整个程序更加模块化,更容易重新编译,⽽且更⽅便升级。
程序函数库可分为3种类型:静态函数库(static libraries)、共享函数库(shared libraries)、动态加载函数库(dynamically loaded libraries):
1、静态函数库,是在程序执⾏前就加⼊到⽬标程序中去了;
2、共享函数库,则是在程序启动的时候加载到程序中,它可以被不同的程序共享;动态加载函数库则可以在程序运⾏的任何时候动态的加载。
3、动态函数库,并⾮另外⼀种库函数格式,区别是动态加载函数库是如何被程序员使⽤的。
2. 静态函数库
静态函数库实际上就是简单的⼀个普通的⽬标⽂件的集合,⼀般来说习惯⽤“.a”作为⽂件的后缀。可以⽤ar这个程序来产⽣静态函数库⽂件。Ar是archiver的缩写。静态函数库现在已经不在像以前⽤得那么多了,主要是共享函数库与之相⽐较有很多的优势的原因。慢慢地,⼤家都喜欢使⽤共享函数库了。不过,在⼀些场所静态函数库仍然在使⽤,⼀来是保持⼀些与以前某些程序的兼容,⼆来它描述起来也⽐较简单。
静态库函数允许程序员把程序link起来⽽不⽤重新编译代码,节省了重新编译代码的时间。不过,在今天这么快速的计算机⾯前,⼀般的程序的重新编译也花费不了多少时间,所以这个优势已经不是像它以前那么明显了。静态函数库对开发者来说还是很有⽤的,例如你想把⾃⼰提供的函数给别⼈使⽤,但是⼜想对函数的源代码进⾏保密,你就可以给别⼈提供⼀个静态函数库⽂件。理论上说,使⽤ELF格式的静态库函数⽣成的代码可以⽐使⽤共享函数库(或者动态函数库)的程序运⾏速度上快⼀些,⼤概1-5%。
创建⼀个静态函数库⽂件,或者往⼀个已经存在地静态函数库⽂件添加新的⽬标代码,可以⽤下⾯的命令:
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。
THE BEST YEARS OF OUR LIVES
3. 共享函数库
共享函数库中的函数是在当⼀个可执⾏程序在启动的时候被加载。如果⼀个共享函数库正常安装,所有的程序在重新运⾏的时候都可以⾃动加载最新的函数库中的函数。对于Linux系统还有更多可以实现的功能:
1、升级了函数库但是仍然允许程序使⽤⽼版本的函数库。
2、当执⾏某个特定程序的时候可以覆盖某个特定的库或者库中指定的函数。
3、可以在库函数被使⽤的过程中修改这些函数库。
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)()规定了在⼀个发⾏包中⼤部分的函数库⽂件应该安装到/usr/lib⽬录下,但是如果某些库是在系统启动的时候要加载的,则放到/lib⽬录下,⽽那些不是系统本⾝⼀部分的库则放到/usr/local/lib下⾯。
上⾯两个路径的不同并没有本质的冲突。GNU提出的标准主要对于开发者开发源码的,⽽FHS的建议则是针对发⾏版本的路径的。具体的位置信息可以看/etc/f⾥⾯的配置信息。
3.2. 这些函数库如何使⽤
在基于GNU glibc的系统⾥,包括所有的linux系统,启动⼀个ELF格式的⼆进制可执⾏⽂件会⾃动启动和运⾏⼀个program loader。对于Linux系统,这个loader的名字是/lib/ld-linux.so.X(X是版本号)。这个loader启动后,反过来就会load所有的其他本程序要使⽤的共享函数库。
到底在哪些⽬录⾥查共享函数库呢?这些定义缺省的是放在/etc/f⽂件⾥⾯,我们可以修改这个⽂件,加⼊我们⾃⼰的⼀些特殊的路径要求。⼤多数RedHat系列的发⾏包的/etc/f⽂件⾥⾯不包括/usr/local/lib这个⽬录,如果没有这个⽬录的话,我们可以修
改/etc/f,⾃⼰⼿动加上这个条⽬。
如果你想覆盖某个库中的⼀些函数,⽤⾃⼰的函数替换它们,同时保留该库中其他的函数的话,你可以在 /etc/ld.so.preload中加⼊你想要替换的库(.o结尾的⽂件),这些preloading的库函数将有优先加载的权利。
当程序启动的时候搜索所有的⽬录显然会效率很低,于是Linux系统实际上⽤的是⼀个⾼速缓冲的做法。ldconfig缺省情况下读
出/etc/f相关信息,然后设置适当地符号链接,然后写⼀个cache到 /etc/ld.so.cache这个⽂件中,⽽这个/etc/ld.so.cache则可以被其他程序有效的使⽤了。这样的做法可以⼤⼤提⾼访问函数库的速度。这就要求每次新增加⼀个动态加载的函数库的时候,就要运⾏ldconfig 来更新这个cache,如果要删除某个函数库,或者某个函数库的路径修改了,都要重新运⾏ldconfig来更新这个cache。通常的⼀些包管理器在安装⼀个新的函数库的时候就要运⾏ldconfig。
2011江西高考作文另外,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在开发和调试过程中经常⼤量使⽤,但是不应该被⼀个普通⽤户在安装过程中被安装程序修改,⼤家可以去参考,这⾥有⼀个⽂档专门介绍为什么不使⽤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/ig $*
我们可以通过运⾏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函数库中的对动态加载模块的⽀持,它使⽤⼀些潜在的动态加载函数库界⾯使得它们可以夸平台使⽤。具体可以参考./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是否存在了;不过,如果某个sym
bol的值就是NULL,那么这个判断就有问题了。标准的判断⽅法是先调⽤dlerror(),清除以前可能存在的错误,然后调⽤dlsym()来访问⼀个symbol,然后再调⽤dlerror()来判断是否出现了错误。⼀个典型的过程如下:
4.4. dlclose()
dlopen()函数的反过程就是dlclose()函数,dlclose()函数⽤⼒关闭⼀个DL函数库。Dl函数库维持⼀个资源利⽤的计数器,当调⽤dlclose的时候,就把这个计数器的计数减⼀,如果计数器为0,则真正的释放掉。真正释放的时候,如果函数库⾥⾯有_fini()这个函数,则⾃动调⽤_fini()这个函数,做⼀些必要的处理。Dlclose()返回0表⽰成功,其他⾮0值表⽰错误。
4.5. DL Library Example
下⾯是⼀个例⼦。例⼦中调⼊math函数库,然后打印2.0的余弦函数值。例⼦中每次都检查是否出错。应该是个不错的范例:
如果这个程序名字叫foo.c,那么⽤下⾯的命令来编译:
gcc -o foo foo.c –ldl

本文发布于:2024-09-21 16:25:13,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/671811.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:函数库   程序   共享   加载
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议