构建Linux程序库和使用Linux程序库 [转]
在本文里,我们将探索与Linux的程序库有关的知识。首先,我们考察静态库的基本知识,并介绍如何使用ar命令来建立静态库。然后,我们将学习共享库方面的知识,并讲述可以动态加载的共享库的有关内容。
一、什么是程序库
通俗的讲,一个程序库就是目标程序文件的一个集合。如果某些目标文件提供了解决一个特定问题的所需功能,我们就可以把这些目标文件归并为一个程序库,从而让应用开发者更易于访问这些目标文件,省得到处去找。
对于静态库,我们可以用实用程序ar来建立。当应用程序开发人员利用程序库进行程序的编译和连接时,程序库中为应用程序所需的那些元件就会集成到最终生成的的可执行程序中。之后,因为程序库已经融入应用程序的映像之中,成为它密不可分的一部分了,所以对应用程序来说,已经没什么外部的程序库可言了。
共享程序库(或者动态程序库)也会连接到一个应用程序的映像上,不过需要两个不同的步骤。第一步发生在构建应用程序之时,链接程序检查是否在应用程序或者程序库内部找到了构建应用程序所需的全部符号(函数名或变量名)。第二步发生在运行时,动态加载器把所需的共享库载入内存,然后动态地把它链接到应用程序的映像之中。注意,这里与静态程序库不同,这次并没有把共享程序库中的所需元件放入应用程序的映像之中。很明显,这样生成的应用程序映像较小,因为共享程序库和应用程序的映像是相互独立的,如下图所示。
图1 静态库示意图
图2 动态库示意图
虽然共享库能够节约内存,但是这是有代价的——必须在运行时解析程序库。很明显,要想弄清需要哪些库,然后寻找这些库并将其载入内存肯定是需要一定时间的。
本文中,我们会建立两个程序库,一个静态库和一个动态库,并以各自的方式应用于程序之中,以此亲身体验两者之间的区别。
二、静态库的创建和使用
相对于动态链接库,静态库要简单一些,它被静态的链接到应用程序的映像之中。这意味着,映像一旦建好,外部程序库的有无对映像的执行将毫无影响,因为所需的部分已经放进程序二进制映像了。
下面我们来演示如何用一组源文件来构造一个程序库。我们建立的程序库是用来封装GNU/Linux的随机函数的,这样我们的库就可以对外提供随机数生成器了。现在看一下我们的程序库为应用程序提供的接口(API),我们将其放在头文件randapi.h中,如下所示:
//randapi.h,我们的程序库的接口 #ifndef __RAND_API_H extern void initRand( void ); #endif /* __RAND_API_H */ |
我们的应用程序接口由三个函数构成,第一个函数是initrand(),这是一个初始化函数,它的任务是为使用程序库做好必要的准备,在调用所有其他随机函数之前,必须首先调用这个初始化函数。第二个函数getSRand()的作用是随机返回一个浮点数,其值介于0.0到1.0之间。最后一个函数是getRand(x),它返回一个随机整数,其值介于0到(x-1)之间。
在文件initrand.c中,放的是初始化函数initrand()的实现代码,这个函数使用当前时间作为种子值来初始化随机数生成程序。代码如下所示:
//initrand.c,初始化函数initrand()的源代码 //initRand()用于初始化随机数生成器 void initRand() |
文件randapi.c是我们最后一个实现API的文件,它也提供了一个随机数函数,源代码如下所示:
//randapi.c,随机数函数的API实现 randvalue = ((float)rand() / (float)RAND_MAX); return randvalue; //getRand()返回一个介于0~(max-1)之间的整数 randvalue = (int)((float)max * rand() / (RAND_MAX+1.0)); return randvalue; |
这就是我们的API了,注意,initapi.c和randapi.c的函数原型都放在了同一个头文件中,即randapi.h.当然,我们完全可以在单个文件中来实现这些功能,但是为了示范程序库的建立,我们故意将它们放到不同的文件中。
现在,我们在来看一下使用这些API的测试程序,该程序将使用我们的程序库提供的应用编程接口进行工作。这个应用程序通过计算随机数函数返回的的平均值来快速检验API,因为平均值应该在随机数范围的中间附近。该程序的代码如下所示:
//test.c,测试我们的程序库的API的应用程序。 #define ITERATIONS 1000000L int main() fsum += getSRand(); |
通过下列命令,可以编译所有源文件并将其综合成单个映像:
$ gcc initapi.c randapi.c test.c -o test |
$ ./test getRand() Average 4 $ |
我们看到,结果和预期的一样,产生的随机数的平均值正好在随机数范围的中间值附近。然而,我们想要的可不是把所有源代码编译成单个映像,而是建立一个随机数函数库。别急,我们现在就开始使用ar实用程序来达到此目。您可以通过下面的命令,在获得最终的二进制映像的同时,还会生成我们的第一个静态库。
$ gcc -c -Wall initapi.c $ gcc -c -Wall randapi.c $ ar -cru libmyrand.a initapi.o randapi.o $ |
这里,我们首先使用gcc编译了两个源文件initapi.c和randapi.c,其中-c选项是告诉gcc仅编译而不链接,并开启所有警告。接下来,我们使用ar命令来生成咱们的程序库libmyrand.a.其中cru选项是创建或者添加存档时的标准设置,c选项表示要建立静态库,如果静态库早已存在,则忽略该选项。选项r的作用是让ar替换静态库中的现有目标,当然是它们业已存在的话。
最后,选项u的作用是为保险起见,只有当新生成的目标文件比存档中原有的目标文件要新时才替换同名的目标文件。
如今,我们已经得到了一个名为libmyrand.a的新文件,它就是我们想要的静态库。该静态库中存有两个目标程序,即initapi.o和randapi.o.那么,我们如何利用这个静态库来构建应用程序呢?别急,继续往下看。方法很简单,如下所示:
$ gcc test.c -L. -lmyrand -o test
$ ./test |
这里,我们首先使用gcc来编译test.c,然后利用libmyrand.a连接目标程序test.o,这样就得到了可执行文件。选项-L.的作用是告诉gcc,程序库在当前子目录中。(这里的点号。表示目录)。
注意,我们也可以为程序库指定具体的子目录,如-L/usr/mylibs.选项-L用来标识要用的程序库。还要留意的是,这里的myrand并不是我们的程序库的名称,我们的程序库的名称是libmyrand.a.使用-L时,系统会替我们在指定的名称的前后分别加上lib和。a,因此,如果我们在此规定-ltest的话,系统将查找名为libtest.a的程序库。
我们已经了解了创建程序库以及使用它来构建应用程序的方法,现在让我们继续探讨一下ar程序的用法。我们可以通过-t选项来调查静态库中到底包含了哪些内容,如下所示:
$ ar -t libmyrand.a initapi.o randapi.o $ |
如果需要的话,我们还可以删除静态库中的目标程序,为此需要用到-d选项,如下所示:
$ ar -d libmyrand.a initapi.o $ ar -t libmyrand.a randapi.o |
这里要引起注意的是,使用上述命令ar执行删除任务失败时,它是不会通知我们的,要想查看出错信息,需要添加-v选项,如下所示:
$ ar -d libmyrand.a initapi.o $ ar -dv libmyrand.a initapi.o No member named ‘initapi.o’ $ |
在前一种情况下,如果我们试图删除目标文件initapi.o,尽管该静态库中并没有这个文件,但是却没有产生任何错误消息。在第二种情况下,我们添加v选项后就得到了相应的错误信息。我们不仅可以从静态库删除目标文件,还可以通过-x选项来从中提取目标文件,如下所示:
$ ar -xv libmyrand.a initapi.o |
选项-x和-v结合使用后,我们能够得到更多的信息。提供在相应子目录中键入ls命令,我们就能看到实用程序ar提取的文件。这里我们要注意的是,我们同时还列出了提取文件后的静态库的内容,我们发现initapi.o仍然在那里,也就是说,提取选项实际上并没有删除静态库中的目标文件,而只是复制了一份而已。要想删除静态库中的目标文件,必须使用删除选项-d.
三、共享库的创建和使用
现在,我们开始介绍共享库,为了省劲,我们依然使用前面的测试程序。首先,我们用initapi.o和randapi.o这两个目标文件来建立一个共享库。与静态库相比,在为共享库编译源代码时有些不同之处。
与使用静态库的情形不同,使用共享库时,程序库并没有放进应用程序的可执行文件中,所以共享库的代码应该使用相对寻址方式,比如通过全局偏移表(GOT)寻址。加载共享库时,加载器会自动地解析所有GOT地址。编译源文件时,为了让生成的目标文件具有位置无关性,我们需要使用gcc的PIC选项:
$ gcc -fPIC -c initapi.c $ gcc -fPIC -c randapi.c |
这样得到的两个目标文件的代码就具有位置无关性。我们可以使用带有-shared标志的gcc命令来创建一个共享库,这个标志的作用是告诉gcc要建立的是一个共享库:
$ gcc -shared initapi.o randapi.o -o libmyrand.so |
我们利用-o规定两个目标模块作为共享库输出。注意,这里我们使用。so后缀来指出该文件是一个共享库。为了使用新建的共享库,即共享目标文件来创建应用程序,我们需要连接共享库中的所需元素,这一点与静态库是一致的:
$ gcc test.c -L. -lmyrand -o test |
要想知道新的二进制映像依赖于哪些共享库,可以使用ldd命令,该命令会打印出某个应用程序所依赖的共享库。举例来说:
$ ldd test libmyrand.so => not found libc.so.6 => /lib/tls/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) $ |
命令ldd能确定出我们的测试程序需要用到哪些共享库。其中libc.so.6是标准C库,ld-linux.so.2是动态链接器/装载器。注意,这里显示没有发现libmyrand.so,这是因为虽然该文件存在于应用程序的目录中,但是我们必须显式指出。我们可以通过环境变量LD_LIBRARY_PATH来完成此任务。给出我们的共享库的位置之后,再次使用ldd命令:
$ export LD_LIBRARY_PATH=./ $ ldd test libmyrand.so => ./libmyrand.so (0x40017000) libc.so.6 => /lib/tls/libc.so.6 (0x42000000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) $ |
我们显式说明了共享库位于当前目录(。/)之后,再次运行ldd命令,它现在终于找到所需共享库了。如果我们不这样做就急着执行我们的应用程序的话,将会收到一条错误消息,指出找不到所需的共享库,如下所示:
$ ./test ./test: error while loading shared libraries: libmyrand.so: cannot find shared object file: No such file or directory. $ |
四、动态库的创建和使用
我们最后要介绍的是一种动态加载的共享库,或称为动态链接库。它的特点是在应用程序运行过程中,什么时候需要了才装入内存,而不是像共享库那样当应用程序启动时就把程序库装入内存。为此,我们还要像前面那样来构建共享的目标文件,如下所示:
$ gcc -fPIC -c initapi.c $ gcc -fPIC -c randapi.c $ gcc -shared initapi.o randapi.o -o libmyrand.so $ su - <provide your root password> $ cp libmyrand.so /usr/local/lib $ exit |
在这里,我们将我们的共享库放到一个公共位置,即/usr/local/lib目录。就像我们所看到的那样,静态库和共享库通常跟应用程序的二进制文件放置在同一目录下,与之不同,动态链接库则一般放在/usr/local/lib目录中。这个库跟原来的共享库在功能上是等价的,只是应用程序使用它们的方式有所区别。需要说明的是,为了把我们的程序库复制到/usr/local/lib中,需要root权限。为此,可以首先使用su命令变成root用户。
现在,我们已经重建了自己的共享库,接下来就是我们的测试程序如何动态使用它了。为此,我们必须对测试程序访问API的方式做一些修改。然后,我们再来看一下如何构建使用动态链接库的程序。更新后的测试程序代码如下所示:
//用于动态链接库的测试程序 #define ITERATIONS 1000000L int main() void (*initRand_d)(void); //打开动态库的句柄 //检查对initRand()函数的访问情况 |
与之前的代码相比,您可能觉得这里的有些费解,但是只要弄懂了DL API的工作方式,一切问题就迎刃而解了。我们这里的真实意图是,先使用dlopen打开共享目标文件,然后通过dlsym让一个局部函数指针指向共享目标文件中的函数。这使得我们可以之后从应用程序中调用该函数。做完这些后,使用dlclose关闭共享库,清除所有引用,即释放为该接口分配的所有内存。
为了能够使用这些DL API,我们需要在应用程序中包含头文件dlfcn.h.使用动态库的第一步是用dlopen来打开它,其中代码为:
handle = dlopen( "/usr/local/lib/libmyrand.so", RTLD_LAZY ); |
我们不仅要规定要使用的程序库(本例为/usr/local/lib/libmyrand.so),除此之外还得规定一个标志。我们这里可能用到的标志有两个,RTLD_LAZY和RTLD_NOW,如果规定RTLD_LAZY,那么就会在将来用到时才解析引用;如果规定RTLD_NOW标志的话,就会在装入程序库时就解析各种引用。如果函数dlopen返回的是一个opaque句柄,说明程序库已经打开了。需要注意的是,如果出现错误的话,我们可以使用dlerror函数向stdout或者stderr输出错误信息。此外,如果必要的话,我们可以在共享库中创建一个名为_init的函数,这样通过dlopen打开我们的共享库时,就会调用该函数,以进行初始化。因为dlopen函数总是在返回到调用者之前调用这个_init函数。
下面,让我们看看是如何引用库中的函数的,或者说如何通过函数名来调用库函数的。先看一下下面的代码:
initRand_d = dlsym( handle, "initRand" ); err = dlerror(); if (err != NULL) { fputs( err, stderr ); exit(-1); } |
由此代码片断可以看出,该过程是很简单的。API函数dlsym会在我们的共享库中搜索定义的函数,就本例而言要找的是初始化函数initrand.如果找到了,会返回一个void *指针,并将其放进一个局部函数指针变量中,这样就可以调用这个函数来进行实际的初始化工作了。我们自动地检验错误状态,如果返回错误字符串,那么会发出该字符串并退出应用程序。这就是发现我们想要调用的共享库函数的过程。获取initrand函数指针之后,我们又得到getSRand以及getRand的函数指针。
我们的测试程序基本没变,只是没有直接调用函数,而是使用指针到函数的接口来间接调用它们。我们看到,虽然动态载入的接口在灵活性方面有所提高,但是在性能上却稍微有所损失。
在新测试程序中,最后一步是关闭程序库,这是通过API函数dlclose来进行的。如果该API发现没有其他用户再使用该共享库的话,它就会将其卸载。
与dlopen相似,dlclose也提供了一种机制,共享目标文件可以通过该机制来导出一个完成例程,以便当在调用API函数dlclose的时候该完成例程。开发人员只需为共享库添加一个称为_fini的函数,dlclose将在返回之前调用_fini函数。
看到了吧,虽然需要少量的改动,但是应用程序使用动态载入的共享库之后,不仅带来了更大的灵活性,而且还可以显著的节约内存(参见前面的图1和图2)。需要注意的是,当应用程序启动时,并不要求所有的动态函数对它都是可见的。相反,只有在程序运行到需要这些函数的时候,才会加载动态库,在此之前,程序一直不需要它们。
动态加载的程序库API非常简单,为完整起见我们在此简单加以介绍:
void *dlopen( const char *filename, int flag ); const char *dlerror( void ); void *dlsym( void *handle, char *symbol ); int dlclose( void *handle ); |
五、结束语
在本文中,我们探讨了各种程序库的创建和使用方法。我们首先介绍了静态库,然后讨论共享库,最后讲解了动态加载的程序库,同时,我们还以源码的形式演示了使用ar命令和gcc创建程序库的方法,并用一个示例程序对它们的使用做出了演示。
最后要说明的是,每一个库应当对应于一个特定的问题。如果某个函数不是专门用来解决给定问题的,我们就应该将其排除在该程序库之外,并考虑将其放入其他的程序库。这一点希望读者要引起注意。