分类: C/C++
2011-04-23 13:40:00
大体上库的存在,有两方面的原因,一是代码的复用,二是声明和实现的分离。将功能相近的使用模块封装成库,使代码的复用、管理和分发变得简单了许多,例如著名的开源图形库ncurses,你可以自行编译,更可以直接使用已经编译好的现成的库文件。另外,由于库是二进制文件,某种意义上讲,将功能的实现部分隐藏了起来,这就为商业代码的保护提供了一种方式。
库文件按照链接方式和时机,可以分为动态库和静态库,下面分别介绍它们在Linux环境中的创建和使用方法。
静态库是指在程序的链接阶段,其中被用到的代码会被直接链接到可执行文件中的库。静态链接的可执行程序包含了其所需的全部库函数:所有库函数都连接到程序中。 这类程序是完整的,其运行不需要外部库的支持。 静态链接程序的优点之一是其安装之前不需要做环境准备工作 。
Linux中,静态库的扩展名通常为.a,它仅仅是一些目标文件(.o)的归档(archive)或者说打包,另外,为了链接时能够快速地定位其中的符号(函数、变量名等),静态库还会包含一个对其中符号的索引。
创建静态库的过程十分简单,除编译所必须的工具之外,要用到的命令只有两个ar和ranlib。ar可以将各个目标文件进行归档,ranlib对ar生成的归档文件(即静态库文件)进行索引。假设现在有这样几个源文件:plus.c, sub.c及相应的头文件,另外还有一个用来调用库文件中函数的主文件main.c,它们的内容分别是:
plus.c,
1 2 3 4 5 6 | #include "plus.h" int plus(int a, int b) { return a + b; } |
sub.c,
1 2 3 4 5 6 | #include "sub.h" int sub(int a, int b) { return a - b; } |
main.c,
1 2 3 4 5 6 7 8 9 10 11 | #include |
下面将plus.c和sub.c编译,制作成静态库libmath.a,依次执行:
1 2 3 | $ cc -c plus.c sub.c $ ar rc libmath.a plus.o sub.o $ ranlib libmath.a |
现在我们的libmath.a静态库已经制作完成,其中使用了ar命令的两个选项,c表示若库文件不存在则创建之,r表示库文件中若已经存在某个目标文件,且较旧,则执行替换。使用这个静态库更加简单:
1 2 3 | $ cc main.c -L. -lmath -o main $ ./main 3 |
动态库和静态库相似,也是各个目标文件的集合。但相比静态链接的程序,动态链接可执行程序要小得多:这类程序运行时需要外部共享 函数库的支持,因此好像并不完整。除了程序体小之外,动态链接允许程序包指定必须的库,而不必将库装入程序包内。动态链接技术还允许多个运行中的程序共享一个库,这样就不会出现同一代码的多份拷贝共占内存的情况了。由于这些原因,当前多数程序采用动态链接技术。
在Linux中的扩展名通常为.so。但在链接时,并不会被链接到可执行文件中,而是在执行时(需要时)由操作系统的动态加载模块动态地加载到内存,并链接到可执行文件地址空间的相应位置。
动态链接库的创建也分为编译和”归档”两个阶段,但不同的是在这两个阶段需要使用一些不同的命令选项。首先,需要将源文件编译成一种成为位置无关码(PIC: Position Independent Code)的目标文件,这种代码可以被加入到内存的任何位置却不需要加载器对其进行重定位,关于这种格式可以参考《链接器与加载器》和《程序员的自我修养–链接装载与库》中较为详尽的描述。接下来需要将这些位置无关码“归档”为.so文件。整个过程只需一个工具即可,即gcc。还是上面的源文件,执行以下命令:
1 2 | $ cc -c -fpic plus.c sub.c $ cc -shared -o libmath.so *.o |
这样,包含plus.o和sub.o的动态库文件libmath.so就创建完成了。其中cc(gcc的符号链接)命令的-fpic或-fPIC选项使之创建位置无关的目标文件,使用-shared选项可以创建最终的动态库文件。
使用动态库文件有两种方法,一种是让操作系统的动态加载模块(如ld-linux.so.2)在执行时加载动态库。另一种是在代码中使用dl库动态加载库文件。
先介绍下第一种方法。使用这种方法需要在编译可执行文件时指明库名及其路径(对于自己的编写的动态库而言):
1 | $ cc main.c -L. -lmath -o main |
这时可执行文件main就被创建了,上面的命令并没有将libmath.so中相应的目标代码链接到main中(你可以对比一下这里的main和静态链接的main的大小),只是在库中查找和确认main.c中用到的符号而已。但通常这个main现在还无法正常执行,这涉及到系统查找动态库的路径问题,系统通常只在某些指定的目录(标准路径)下查找所需的库文件,若在标准路径中无法找到所需的库,则会到环境变量LD_LIBRARY_PATH(如果存在的话)指定的目录下查找,若仍无法找到就会报错。因为libmath.so在main的当前目录中,而当前目录通常并不在标准路径之列。为了使libmath.so能够被找到和加载,你可以把它放到标准路径中,但更好的方法是将其所在目录加入到LD_LIBRARY_PATH变量中。执行下面的命令:
1 2 3 4 | $ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. # 添加当前目录 $ export LD_LIBRARY_PATH # 将环境变量导出,使其在子shell中可用 $ ./main 3 |
下面介绍使用dl库加载动态库,dl库中函数很少很简练,看main.c代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include |
main.c中,首先使用dlopen打开需要的动态库,其中参数RTLD_LAZY指明仅当需要调用该库时才进行加载。dlopen返回一个句柄,dlsym使用该句柄和符号来取得相应函数的地址,这里使用int (*)(int, int)函数指针来接收plus函数的地址。接下来使用得到的函数指针调用相应的函数,最后通过dlclose函数来关闭句柄。编译这个程序需要使用dl动态链接库,因此需要使用gcc的-ldl选项:
1 2 3 | $ cc main.c -ldl -o main $ ./main 3 |
最后附上libmath.a和libmath.so文件的内容,使用objdump的-d选项查看的:
libmath.a,
In archive libmath.a: plus.o: file format elf32-i386 Disassembly of section .text: 00000000: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 8b 45 0c mov 0xc(%ebp),%eax 6: 8b 55 08 mov 0x8(%ebp),%edx 9: 8d 04 02 lea (%edx,%eax,1),%eax c: 5d pop %ebp d: c3 ret sub.o: file format elf32-i386 Disassembly of section .text: 00000000 : 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 8b 45 0c mov 0xc(%ebp),%eax 6: 8b 55 08 mov 0x8(%ebp),%edx 9: 89 d1 mov %edx,%ecx b: 29 c1 sub %eax,%ecx d: 89 c8 mov %ecx,%eax f: 5d pop %ebp 10: c3 ret
libmath.so:
libmath.so: file format elf32-i386 Disassembly of section .init: 00000324 <_init>: 324: 55 push %ebp 325: 89 e5 mov %esp,%ebp 327: 53 push %ebx 328: 83 ec 04 sub $0x4,%esp ...... 0000044c: 44c: 55 push %ebp 44d: 89 e5 mov %esp,%ebp 44f: 8b 45 0c mov 0xc(%ebp),%eax 452: 8b 55 08 mov 0x8(%ebp),%edx 455: 8d 04 02 lea (%edx,%eax,1),%eax 458: 5d pop %ebp 459: c3 ret 45a: 90 nop 45b: 90 nop 0000045c : 45c: 55 push %ebp 45d: 89 e5 mov %esp,%ebp 45f: 8b 45 0c mov 0xc(%ebp),%eax 462: 8b 55 08 mov 0x8(%ebp),%edx 465: 89 d1 mov %edx,%ecx 467: 29 c1 sub %eax,%ecx 469: 89 c8 mov %ecx,%eax 46b: 5d pop %ebp 46c: c3 ret 46d: 90 nop 46e: 90 nop 46f: 90 nop ...... Disassembly of section .fini: 000004a8 <_fini>: 4a8: 55 push %ebp 4a9: 89 e5 mov %esp,%ebp 4ab: 53 push %ebx 4ac: 83 ec 04 sub $0x4,%esp 4af: e8 00 00 00 00 call 4b4 <_fini+0xc> 4b4: 5b pop %ebx 4b5: 81 c3 40 1b 00 00 add $0x1b40,%ebx 4bb: e8 d0 fe ff ff call 390 <__do_global_dtors_aux> 4c0: 59 pop %ecx 4c1: 5b pop %ebx 4c2: c9 leave 4c3: c3 ret
可见,相对.a,.so有很多额外的代码段,其中比较重要的是<_init>段和<_fini>段。它们分别进行一些前期和后期处理工作,例如<_init>通常在dlopen返回之前执行一些.so库中的一些初始化工作(C++中就可能是全局构造或者静态对象的构造函数)。
转载自: