2007-03-03

[转]linux库知识,静态库和动态库

引言:
在xmeeting中,关于usb手柄部分,采用动态库调用方式,下面翻译一篇David A. Wheeler写的文章。文章就如何创建和使用静态库,共享库以及动如何动态装载库进行了论述。内容纲要如下:
1.概述
2.静态库
3.共享库
3.1 约定
3.2 使用
3.3 环境变量
3.4 创建共享库
3.5 安装与使用
3.6 兼容性
4.动态加载
4.1 dlopen()
4.2 dlerror()
4.3 dlsym()
4.4 dlclose()
4.5 示例
5.辅助知识
5.1 nm命令
5.2 库的构建与析构函数
5.3 脚本
5.4 版本
5.5 GNU libtool
5.6 去除符号空间
5.7 外部执行体
5.8 C++ 与 C
5.9 加速C++初始化
5.10 Linux标准
1.概述
本文就如何在Linux系统中运用GNU工具创建和使用程序库进行论述。所谓"程序库",简单说,就是包含了数据和执行码的文件。其不能单独执行,可以作 为其它执行程序的一部分,来完成执行功能。库的存在,可以使得程序模块化,可以加快程序的再编译,可以实现代码重用,可以使得程序便于升级。程序库可分三 类:静态库,共享库和动态加载库。

静态库,是在执行程序运行前就已经加入到执行码中,在物理上成为执行程序的一部分;共享库,是在执行程序启动时加载到执行程序中,可以被多个执行程序共享 使用。动态加载库,其实并不是一种真正的库类型,应该是一种库的使用技术,应用程序可以在运行过程中随时加载和使用库。
建议库开发人员创建共享库,比较明显的优势在于库是独立的,便于维护和更新;而静态库的更新比较麻烦,一般不做推荐。然而,它们又各有优点,后面会讲到。在C++编程中,要使用动态加载技术,需要参考文章"C++ dlopen MINI-Howto"。
文章中讲述的执行程序和库都采用ELF(Executable and Linking Format)格式,尽管GNU GCC 工具可以处理其它格式,但不在本文的讨论范围。本文可以在 http://www.dwheeler.com/program-libraryhttp://www.linuxdoc.org 找到。

2.静态库
静态库可以认为是一些目标代码的集合。按照习惯,一般以".a"做为文件后缀名。使用ar(archiver)命令可以创建静态库。因为共享库有着更大的优势,静态库已经不被经常使用。但静态库使用简单,仍有使用的余地,并会一直存在。

静态库在应用程序生成时,可以不必再编译,节省再编译时间。但在编译器越来越快的今天,这一点似乎已不重要。如果其他开发人员要使用你的代码,而你又不想 给其源码,提供静态库是一种选择。从理论上讲,应用程序使用了静态库,要比使用动态加载库速度快1-5%,但由于莫名的原因,实际上可能并非如此。由此看 来,除了使用方便外,静态库可能并非一种好的选择。

要创建一个静态库,或要将目标代码加入到已经存在的静态库中,可以使用以下命令:
ar rcs my_libraty.a file1.o file2.o
以上表示要把目标码file1.o和file2.o加入到静态库my_library.a中。若my_library.a不存在,会自动创建。

静态库创建成功后,需要连接到应用程序中来使用。如果你使用gcc(1)来产生执行程序,需要利用-l选项来指定静态库。更多信息,查看gcc使用手册。

在使用gcc时,要注意其参数的顺序。-l是连接器选项,一定要放在被编译的文件名称之后;若放在文件名称之前,你会连接失败,并会出现莫名其妙的错误。这一点切记。

你也可以直接使用连接器ld(1),使用其选项-l或-L。但最好使用gcc(1),因ld(1)的接口有可能会有变化。

3.共享库
共享库是在程序启动时被装载。当一个应用程序装载了一个共享库后,其它应用程序仍可以装载同一个共享库。基于linux的使用方法,共享库还有其它灵活的而又精妙的特性:
更新库并不影响应用程序使用旧的,非向后兼容的版本;
在执行特定程序时,可以覆盖整个库或更新库中的特定函数;
以上操作不会影响已经运行的程序,他们仍会使用已经装载的库。

3.1约定
要想共享库具有以上特性,一些约定需要遵守。你需要掌握共享库名称之间的区别,特别是搜名(soname)和实名(realname)之间的区别和关系;你还需要知道共享库在文件系统的位置。
3.1.1名称
每个共享库都有一个特定的搜名(soname),其组成如下:
lib + 库名 + .so + . + version
| | |_______________|
前缀 库名 后缀
在文件系统中,搜名是一个指向实名的符号联结。

每个共享库也有一个实名,其真正包含有库的代码,组成如下:
搜名 + . + 子版本号 + . + 发布号
最后的句点和发布号是可选项。

另外,共享库还有一个名称,一般用于编译连接,称为连名(linker name),它可以被看作是没有任何版本号的搜名。

看下面的例子:
lrwxrwxrwx 1 root root libpng.so -> libpng12.so
lrwxrwxrwx 1 root root libpng.so.2 -> libpng.so.2.1.0.12
-rw-r--r-- 1 root root libpng.so.2.1.0.12
在以上信息中, libpng.so.2.1.0.12是共享库的实名(real name),libpng.so.2是共享库搜名(soname),libpng.so 则是连接名(linker name),用于编译连接。

3.2共享库的装载
在所有基于GNU glibc的系统(当然包括Linux)中,在启动一个ELF二进制执行程序时,一个特殊的程序"程序装载器"会被自动装载并运行。在linux中,这个 程序装载器就是/lib/ld-linux.so.X(X是版本号)。它会查找并装载应用程序所依赖的所有共享库。
被搜索的目录保存在/etc/ls.so.conf文件中,但一般/usr/local/lib并不在搜索之列,至少debian是这样。这似乎是一个系统失误,只好自己加上了。
当然,如果程序的每次启动,都要去搜索一番,势必效率不堪忍受。Linux系统已经考虑这一点,对共享库采用了缓存管理。ldconfig就是实现这一功 能的工具,其缺省读取/etc/ld.so.conf文件,对所有共享库按照一定规范建立符号连接,然后将信息写入/etc/ld.so.cache。 /etc/ld.so.cache的存在大大加快了程序的启动速度。

3.3创建共享库
共享库的创建比较简单,基本有两步。首先使用-fPIC或-fpic创建目标文件,PIC或pic表示位置无关代码,然后就可以使用以下格式创建共享库了:
gcc -share _Wl,-soname,your_soname -o library_name file_list library_list
下面是使用a.c和b.c创建库的示例:
gcc -fPIC -g -c -Wall a.c
gcc -fPIC -g -c -Wall b.c
gcc -share -Wl,-soname, libmyab.so.1 -o libmyab.so.1.0.1 a.o b.o -lc
-g表示带有调试信息,-Wall表示产生警告信息。
几个需要注意的地方:
(1)不推荐使用strip处理共享库,最好不要使用-fomit-frame-pointer编译选项
(2)-fPIC和-fpic都可以产生目标独立代码,一般采用-fPIC,尽管其产生的目标文件可能会大些;-fpic产生的代码小,执行速度快,但可能有平台依赖限制。
(3)一般情况下,-Wall,-soname,your_soname编译选项是需要的。当然,-share选项更不能丢。

4 动态加载库
DL技术可以允许应用程序在运行过程的任何时候去加载和使用指定的库。这一技术在插件的实现上很实用。动态加载库这一概念并不是着眼于库的文件格式,而是 指使用方式。存在着一组接口函数,使得应用程序可以采用DL技术。下面对这些接口函数逐一介绍,在最后给出应用示例。

4.1 dlopen
函数原型:void *dlopen(const char *libname,int flag);
功能描述:dlopen必须在dlerror,dlsym和dlclose之前调用,表示要将库装载到内存,准备使用。如果要装载的库依赖于其它库,必须 首先装载依赖库。如果dlopen操作失败,返回NULL值;如果库已经被装载过,则dlopen会返回同样的句柄。
参数中的libname一般是库的全路径,这样dlopen会直接装载该文件;如果只是指定了库名称,在dlopen会按照下面的机制去搜寻:
(1)根据环境变量LD_LIBRARY_PATH查找
(2)根据/etc/ld.so.cache查找
(3)查找依次在/lib和/usr/lib目录查找。
flag参数表示处理未定义函数的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暂时不去处理未定义函数,先把库装载到内 存,等用到没定义的函数再说;RTLD_NOW表示马上检查是否存在未定义的函数,若存在,则dlopen以失败告终。

4.2 dlerror
函数原型:char *dlerror(void);
功能描述:dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息,返回NULL表示无错误。dlerror在返回错误信息的同时,也会清除错误信息。

4.3 dlsym
函数原型:void *dlsym(void *handle,const char *symbol);
功能描述:在dlopen之后,库被装载到内存。dlsym可以获得指定函数(symbol)在内存中的位置(指针)。如果找不到指定函数,则dlsym会返回NULL值。但判断函数是否存在最好的方法是使用dlerror函数,下面是示例:
dlerror();/*清除错误信息*/
function = dlsym(handle,"function_name");
if((error=dlerror()) != NULL)
{
/*错误处理*/
}
else
{
/*找到函数*/
}

4.4 dlclose
函数原型:int dlclose(void *);
功能描述:将已经装载的库句柄减一,如果句柄减至零,则该库会被卸载。如果存在析构函数,则在dlclose之后,析构函数会被调用。

4.5动态加载库示例
#include
#include
#include

int main(int argc,char **argv)
{
void *handle;
double (*cosine)(double);
char *error;

handle = dlopen("/lib/libm.so.6",RTLD_LAZY);
if(!handle)
{
printf("%s\n",dlerror());
exit(1);
}

printf("opened /lib/libm.so.6\n");

cosine = dlsym(handle,"cos");
if((error = dlerror()) != NULL)
{
printf("%s\n",error);
dlclose(handle);
printf("after error,closed /lib/libm.so.6\n");
exit(1);
}

printf("%f\n",(*cosine)(2.0));

dlclose(handle);
printf("closed /lib/libm.so.6\n");

return 0;
}
编译:gcc -o test test.c -ldl。在这个例子中,/lib/libm.so.6是动态加载库,而/usr/lib/libdl.so则是共享库。


5.相关知识
5.1 nm命令
nm(1)命令可以报告库的符号列表,对于查看库的相关信息是一个不错的工具。具体使用查看帮助文档。示例:
$nm -D libavcodec-0.4.7.so | grep 263
结果如下:
00109d40 T h263_encode_mb
00105f94 T h263_encode_picture_header
001a85a0 D h263_encoder
001162d0 T h263_get_picture_format
0010a7b4 T h263_pred_motion
00106df8 T h263_send_video_packet
001ab180 D h263i_decoder
001a85e0 D h263p_encoder
00115c68 T intel_h263_decode_picture_header
……………
其中,T表示正常代码段,D表示初始化数据段

5.2库的构建与析构函数
关于构建与析构函数,一般不需要自己去编程实现。如果你一定要自己做,下面是函数原型:
void __attribute__ ((constructor)) my_init(void);
void __attribute__ ((destructor)) my_fini(void);
在编译共享库时,不能使用"-nonstartfiles"或"-nostdlib"选项,否则,构建与析构函数将不能正常执行(除非你采取一定措施)。

5.3脚本共享库
linux中,共享库可以是脚本形式,当然需要专门的脚本语言。/usr/lib/libc.so是一个典型的例子,内容如下:
/* GNU ld script
Use the shared library, but some functions are only in
the static library, so try that secondarily. */
OUTPUT_FORMAT(elf32-i386)
GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a )

5.4 版本脚本(略)
5.5 GNU libtool(略)

5.6除去记号信息
共享库中的记号信息多为调试之用,但占用了磁盘空间。如果你的库是为嵌入式系统所用,最好去掉记号信息。一种方法,利用strip(1)命令,使用方法查 看其帮助文档;另一种方法,使用GNU LD选项-s或-S,例如"-Wl -s"或"-Wl -S"。-S仅除去调试记号信息;-s除去所有记号信息。

5.7编译优化
有一篇文章写的不错"Whirlwind tutorial On Creating really teensy ELF Executables For Linux"。这篇文章中可以说把程序的代码优化到了极点。在我们实际的应用中,可能并需要那些技巧,但通过此文,我们可以更多的了解ELF。

5.8 C++与C
要使得你编写的共享库能同时被C和C++程序使用,库的头文件需要使用"extern C"预定义,下面是一个例子:
#ifndef LIB_HELLO_H
#define LIB_HELLO_H

#ifdef __cplusplus
extern "C"
{
#endif

.....头文件代码

#ifdef __cplusplus
}
#endif

#endif

5.9关于C++程序的启动速度
C++应用程序的启动速度是比较慢的。我一直使用firefox,感受颇深。有人认为这是因主函数启动之前的代码重定位所导致。有一篇文章"making C++ ready for the desktop"(by Waldo Bastian)对这问题作了分析。我读了一下,理解不是很深刻。

5.10 Linux Standard Base(LSB)
LSB是一个项目,致力于制订和推动一系列标准,尽力提高不同Linux发布版本之间的兼容性,从而为应用程序的开发提供一致性的接口。关于linux标准项目的详细信息,可查阅网站www.linuxbase.org


No comments: