1.库文件简介
库文件是一个包含了编译后代码、数据的文件,用于与程序其他代码连编,它可以使得程序模块化、编译速度更快,并且易于更新。库文件分为三种(实质为两种,在随后两句话有解释 ):静态库(在程序之前就已经装载进其中了)、共享库(在程序启动之时加载进去,在程序直接共享)、动态加载库(dynamically loaded,DL)(在程序运行中任何时候都可以被加载进程序中使用,事实上DL并非是一个完全不同的库类型,共享库可以用作DL而被动态加载(静态库在Linux貌似无法用dlopen加载) 。注意有些人使用dynamically linked libraries (DLLs)来指代共享库,有些人使用DLL这个词来形容任何可以被用作DL的库文件,这个请区分对待。
在具体使用中,我们应该多使用共享库,这使得用户可以独立于使用该库文件的程序而更新库。DL的确非常有用,但有时候我们可能并不需要那些灵活性,而对于静态库,由于更新起来实在费劲,我们一般不使用。
2.静态库的建立
静 态库就是一堆普通的目标文件(object file),习惯上静态库以.a为后缀,这是使用ar命令生成的。静态库允许用户不用重新编译代码就可以链接程序,以节省重新编译的时间,其实这个时间已 经在强大的机器配置和快速的编译器中显得微不足道了,这个常常用来提供程序而不是源代码。速度上,静态ELF(Executable and Linking Format)库文件比共享库或者动态加载库快1%-5%,但实际上常常因为其他因素而并不一定快。
我们写主文件prog.c:
1: #include 2: void ctest1(int *); 3: void ctest2(int *); 4: 5: int main() 6: { 7: int x; 8: ctest1(&x); 9: printf("Valx=%d/n ",x); 10: 11: return 0; 12: } 13:然后写这两个函数的实现:
ctest1.c
1: void ctest1(int *i) 2: { 3: *i=5; 4: }ctest2.c
1: void ctest2(int *i) 2: { 3: *i=100; 4: }我们首先编译这两个函数实现的源文件:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ gcc -Wall -c ctest1.c ctest2.c gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ls ctest1.c ctest1.o ctest2.c ctest2.o prog.c
然后创建静态库libctest.a:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ar -cvq libctest.a ctest1.o ctest2.o
a - ctest1.o a - ctest2.o
我们查看一下这个库中的文件:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ar -t libctest.a ctest1.o ctest2.o
此时我们可以编译我们的程序了,注意-l选项,后边的参数是去掉lib和.a的部分,并且需要放在要编译的文件名之后,否则会报错。:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ gcc -o test prog.c -L./ –lctest gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ls ctest1.c ctest1.o ctest2.c ctest2.o libctest.a prog.c test gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ./test Valx=5
3.共享库的建立
共享库是在程序启动时加载的库文件。当共享库加载完毕后所有启动的起来的程序都将使用新的共享库。在创建共享库之前,还需要了解一些知识:
命名规则: 每一个共享库都有一个soname,一般都形如libname.so.versionNumber,其中versionNumber 每当接口发生改变时都要增加,一个完全的soname的前缀应该是它所在目录,在一个实际系统中,一个完整的soname只是共享库文件的real name的符号链接。程序运行时在内部列出所需的共享库时使用的就是soname。 每一个共享库也有一个real name,这是包含实际代码的文件名,real name使用soname为前缀,并且在后边添加一些信息,一般都形如soname.MinorNumber.ReleaseNumber。 最后的releaseNumber可有可无。这个是生成共享库时实际文件的名称。 同时,在编译器要求使用一个共享库时使用的名字称为linker name,一般都是去掉版本号的soname,用于gcc中-lname这样的选项的编译。 这几个名字的关系:你在创建实际库文件中指定libreadline.so.3.0为real name ,并且使用符号链接创建soname ->libreadline.so.3和linker name-> /usr/lib/libreadline.so。 放置位置: GNU标准推荐将所有默认的库安装在/usr/local/lib,这指的是开发者源代码默认的位置。 FHS指出大多数的库文件应该放在/usr/lib,而启动所需的库则应该放在/lib中,而非系统库应该放在/usr/local/lib。这指的是发行版默认的位置,这两个标准并没有矛盾。共享库的主要有三个步骤:
创建目标代码。 创建库。 使用符号链接创建默认版本的共享库(可选)。现在我们举个例子来说明,首先我们编译源代码,使用-fPIC选项生成共享库所需的位置独立代码(position-independent code (PIC)):
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -Wall -fPIC -c *.c gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls ctest1.c ctest1.o ctest2.c ctest2.o prog.c prog.o
然后我们创建库文件:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -shared -Wl,-soname,libctest.so.1 -o libctest.so.1.0 *.o gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls ctest1.c ctest1.o ctest2.c ctest2.o libctest.so.1.0 prog.c prog.o
-shared选项指明生成共享目标文件,-W1(注意是小写L而不是一)指明传入链接器的参数,在此我们设定了该库的soname 为libctest.so.1,-o则指明了生成的目标库文件为libctest.so.1.0(这个就是real name )。
最后创建所需的符号链接:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo mv libctest.so.1.0 /usr/local/lib/libctest.so.1.0 gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ln -sf /usr/local/lib/libctest.so.1.0 /usr/local/lib/libctest.so.1 gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ln -sf /usr/local/lib/libctest.so.1.0 /usr/local/lib/libctest.so
创建的libctest.so就是上面所谓linker name,用于编译时-lctest选项。
创建的libctest.so.1就是soname,我们在上边说过程序在运行时需要这个名字的符号链接。
此时我们的共享库就建好了,接着我们编译程序:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -Wall -L/usr/local/lib prog.c -lctest -o prog gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls ctest1.c ctest1.o ctest2.c ctest2.o prog prog.c prog.o
我们编译完毕,该库并不会包含在可执行文件中,只有在执行时来会动态加载进来。我们可以通过ldd列出一个可执行程序所有的依赖,在我的系统中还找不到/usr/local/bin的路径:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ldd prog linux-gate.so.1 => (0x00a5c000) libctest.so.1 => not found libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00a6f000) /lib/ld-linux.so.2 (0x00451000)
此时,运行会报找不到库的错误:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ./prog ./prog: error while loading shared libraries: libctest.so.1: cannot open shared object file: No such file or directory
我们可以将所需库的路径加入到系统路径中,有三种方法可以完成:
A.在/etc/ld.so.conf中加入所在路径,然后执行ldconfig配置链接器运行时绑定配置。你也可以创建一个文件,将路径写入,然后使用ldconfig –f filename将配置写入。 B.修改LD_LIBRARY_PATH 环境变量(Linux下,AIX下为LIBPATH),在其中添加路径。若你直接在.bashrc文件中配置则重启后不失效,否则在shell中设置重启后失效。我们使用A方法中的-f选项:
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ vi libctest.conf gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ldconfig -f libctest.conf gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ./prog Valx=5 gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ldd prog linux-gate.so.1 => (0x00f6f000) libctest.so.1 => /usr/local/lib/libctest.so.1 (0x005d9000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00718000) /lib/ld-linux.so.2 (0x001e6000)
其中libctest.conf中写入路径:/usr/local/lib。程序运行正常。
4.动态加载库的使用
动态加载库是在非程序启动时动态加载进入程序的库,这对于实现插件或动态模块有很大的帮助。在Linux中,动态加载库的形式并不特殊,它使用上述两种程序库,使用提供的API在程序运行时动态加载。注意,在不同平台上动态加载库的API并不相同,所以可能会有移植问题出现。
我们可以通过nm命令先查看一下我们创建的库里面有哪些symbol(可以理解为函数方法)供我们使用:
gnuhpc@gnuhpc-desktop:~/MyCode/lib$ nm /usr/local/lib/libctest.so 00001f18 a _DYNAMIC 00001ff4 a _GLOBAL_OFFSET_TABLE_ w _Jv_RegisterClasses 00001f08 d __CTOR_END__ 00001f04 d __CTOR_LIST__ 00001f10 d __DTOR_END__ 00001f0c d __DTOR_LIST__ 000005a0 r __FRAME_END__ 00001f14 d __JCR_END__ 00001f14 d __JCR_LIST__ 00002014 A __bss_start w __cxa_finalize@@GLIBC_2.1.3 00000540 t __do_global_ctors_aux 00000420 t __do_global_dtors_aux 00002010 d __dso_handle w __gmon_start__ 000004d7 t __i686.get_pc_thunk.bx 00002014 A _edata 0000201c A _end 00000578 T _fini 000003a0 T _init 00002014 b completed.7021 000004dc T ctest1 000004ec T ctest2 00002018 b dtor_idx.7023 000004a0 t frame_dummy 000004fc T main U printf@@GLIBC_2.0
这个命令对静态库和共享库都支持,第二列为symbol类型,小写字母表示符号是本地的,大写字母表示符号是全局(外部)的,几个常见的字母含义如下:T为代码段普通定义,D为已初始化数据段,B为未初始化数据段,U为未定义(用到该符号但是没有在该库中定义)。
我们创建ctest.h:
1: #ifndef CTEST_H 2: #define CTEST_H 3: 4: #ifdef __cplusplus 5: extern "C " { 6: #endif 7: 8: void ctest1(int *); 9: void ctest2(int *); 10: 11: #ifdef __cplusplus 12: } 13: #endif 14: 15: #endif这里使用extern C是为了使得该库既可以用于C语言又可以用于C++。
我们动态加载库进来:progdl.c
1: #include 2: #include 3: #include "ctest.h " 4: 5: int main(int argc, char **argv) 6: { 7: void *lib_handle; 8: double (*fn)(int *); 9: int x; 10: char *error; 11: 12: lib_handle = dlopen("/usr/local/lib/libctest.so ", RTLD_LAZY); 13: if (!lib_handle) 14: { 15: fprintf(stderr, "%s/n ", dlerror()); 16: exit(1); 17: } 18: 19: fn = dlsym(lib_handle, "ctest1 "); 20: if ((error = dlerror()) != NULL) 21: { 22: fprintf(stderr, "%s/n ", error); 23: exit(1); 24: } 25: 26: (*fn)(&x); 27: printf("Valx=%d/n ",x); 28: 29: dlclose(lib_handle); 30: return 0; 31: }里面的方法解释如下:
void * dlopen(const char *filename, int flag); 若filename为绝对路 径,那么dlopen就会试图打开它而不搜索相关路径,否则就现在环境变量LD_LIBRARY_PATH处搜索,然后在/etc /ld.so.cache以及/lib和/usr/lib搜索。flag我们只解释两个常用的选项:若为RTLD_LAZY则表示在动态库执行时解决未定 义符号问题,而RTLD_NOW则表示在dlopen返回前解决未定义符号问题。当你调试时你应该用RTLD_NOW,这个时候若存在未解决的引用程序还 可以继续进行。另外,RTLD_NOW选项可能会使打开库的这个操作稍微慢一点,但是以后寻找函数时就会快一点。注意,若程序库相互依赖则应该按依赖顺序 依次载入,比如X依赖Y,那么要先载入Y然后再载入X。返回的是一个句柄,若失败则返回null. char *dlerror(void); 报告任何上一次对加载库操作的错误。两次调用期间若有操作错误则第二次会报告, 否则第二次则返回null——它报告完错误就等待下一个错误的发生,上一次错误的情况一旦报告就不再提及。 void *dlsym(void *handle, const char *symbol); 寻找对应symbol的函数方法,handle就是dlopen返回的句柄。一般如下使用: 1: dlerror(); /* clear error code */ 2: s = (actual_type) dlsym(handle, symbol_being_searched_for); 3: if ((err = dlerror()) != NULL) { 4: /* handle error, the symbol wasn't found */ 5: } else { 6: /* symbol found, its value is in s */ 7: } int dlclose(void *handle); 关闭一个动态加载库。当一个动态库被加载多次时,你需要用同样次数dlclose该动态库才可以deallocated.我们编译该代码gcc -g -rdynamic -o progdl progdl.c -ldl,即可得到可执行文件(其中-g选项是为了gdb调试所用),其中的库为动态加载后又关闭的。我们使用gdb看一下代码:
(gdb) b main Breakpoint 1 at 0x804878d: file progdl.c, line 12. (gdb) r Starting program: /home/gnuhpc/MyCode/lib/dynamic/progdl
Breakpoint 1, main (argc=1, argv=0xbffff4a4) at progdl.c:12 12 lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY); (gdb) f #0 main (argc=1, argv=0xbffff4a4) at progdl.c:12 12 lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY); (gdb) s 13 if (!lib_handle) (gdb) n 19 fn = dlsym(lib_handle, "ctest1"); (gdb) 20 if ((error = dlerror()) != NULL) (gdb) 26 (*fn)(&x); (gdb) 27 printf("Valx=%d/n",x); (gdb) p x $1 = 5 (gdb) p fn $2 = (double (*)(int *)) 0x28c4dc
可以看到fn获得了ctest1的地址。
