共享库,即程序启动时动态加载的库文件。合理地使用共享库,可以有效地实现代码的重用。
假设我有一个库文件 vmath,用来实现简单的四则运算:
/* vmath.h */ #ifndef VMATH_H #define VMATH_H extern int vm_add(int, int); extern int vm_sub(int, int); extern int vm_mul(int, int); extern int vm_div(int, int); #endif
各函数的具体实现如下:
/* vmath.c */ #include "vmath.h" int vm_add(int op1, int op2) { return op1 + op2; } int vm_sub(int op1, int op2) { return op1 - op2; } int vm_mul(int op1, int op2) { return op1 * op2; } int vm_div(int op1, int op2) { int res; if (op2 != 0) { res = op1 / op2; } return res; }
都是很简单的代码,就不多解释了。现在我有一个程序,想借用这个 vmath 库来实现简单的四则运算:
/* vm_test.c */ #include <stdio.h> #include "vmath.h" int main(int argc, char *argv[]) { int op1, op2; if (argc != 3) { fprintf(stderr, "Usage: %s <op1> <op2>\n", argv[0]); return -1; } sscanf(argv[1], "%d", &op1); sscanf(argv[2], "%d", &op2); printf("Operand #1: %d\n", op1); printf("Operand #2: %d\n", op2); printf("Addition: %d\n", vm_add(op1, op2)); printf("Subtraction: %d\n", vm_sub(op1, op2)); printf("Multiplication: %d\n", vm_mul(op1, op2)); printf("Division: %d\n", vm_div(op1, op2)); printf("Fin\n"); return 0; }
我们看到程序头部嵌入了 vmath 的头文件 vmath.h。程序读取 main 函数的两个参数:argv[1] 和 argv[2],将他们转换为 int,并保存进入 op1 和 op2 内,接着通过调用 vmath 的函数,依次计算他们的和、差、积和商。
那我们希望 vmath 是以共享库的形式存在,则编译步骤较之静态库,也会略有不同,首先对 vmath.c 进行编译:
gcc -c -fPIC vmath.c -o vmath.o
编译器毫无悬念地生成了 vmath.o 对象文件,接下去我们需要将 .o 文件转成 .so 文件,s 即 shared:
gcc -shared -Wl,-soname,libvmath.so.1 vmath.o -o libvmath.so.1.0.0
当前目录下瞬间多出一个文件 libvmath.so.1.0.0,即我们的共享库文件,搞定啦!么么哒!这是不可能的……
要看懂上面这条命令,我们需要首先弄明白共享库的三个名字:
共享库名(Soname):顾名思义,就是库的名称,标准格式为 lib + <库名> + .so + .<主版本号>。以我们的共享库 vmath 为例,那它的 soname 便是 libvmath.so.1。在系统中,soname 通常是以链接(Symbolic link)的形式存在,指向该共享库的真实名;
真实名(Real name):即真正包含代码的文件名,标准格式为 <Soname> + .<次版本号> + .<发行号>。共享库 vmath 的真实名则为 libvmath.so.1.0.0;
连接名(Linker name):即编译器在程序连接阶段,请求共享库时使用的名称,通常为不带任何版本号的 soname,vmath 的连接名则为 libvmath.so。连接名也是以链接的形式存在,指向该共享库的 soname。
现在我们回过头来看之前的那条编译器指令,其作用是将 vmath.o 生成一个 soname 为 libvmath.so.1 的共享库 libvmath.so.1.0.0。
现在我们已经获得我们的共享库 libvmath.so.1.0.0,接下去就是要为其添加 Soname 和 linker name:
ln -sf libvmath.so.1.0.0 libvmath.so.1 ln -sf libvmath.so.1 libvmath.so
前者生成一个指向 libvmath.so.1.0.0 的链接 libvmath.so.1,即共享库的 soname,后者生成一个指向 libvmath.so.1 的链接 libvmath.so,即共享库的 linker name。
万事俱备,接下去就剩下对测试程序 vm_test.c 的编译了:
gcc -c vm_test.c -o vm_test.o gcc -L. -lvmath vm_test.o -o vm_test
-L 指定共享库所在的路径(path),此处为当前目录。-l 指定所需的共享库,编译器读取到 -lvmath,便会自动在指定的路径内寻找 libvmath.so。就这么妥妥地编译好了,接下去试运行一下:
./vm_test 12 3 ./vm_test: error while loading shared libraries: libvmath.so.1: cannot open shared object file: No such file or directory
你妹!显然 vm_test 在启动之际,未能找到共享库。之前我们只是在 gcc 连接阶段指定共享库的所在和名称,但是最终生成的程序本身,并不知道共享库的相关信息。为了程序在启动时,能顺利找到共享库,我们可以采取一系列办法。
第一种方法,我们可以通过环境变量 LD_LIBRARY_PATH 来指定共享库的所在位置:
export LD_LIBRARY_PATH=/home/vesontio/devel/lib:$LD_LIBRARY_PATH ./vm_test 12 3 Operand #1: 12 Operand #2: 3 Addition: 15 Subtraction: 9 Multiplication: 36 Division: 4 Fin
我这里用 export 将共享库所在的路径保存进环境变量 LD_LIBRARY_PATH,这里我们假设共享库文件 libvmath.so.1.0.0 和它的 soname 都保存在 /home/vesontio/devel/lib 下面。然后尝试运行 vm_test,妥妥的。
第二种方法是使用 rpath,即 runtime search path,是被硬生生写入可执行文件的路径,帮助程序在运行时寻找所需的库文件。rpath 需要在编译生成可执行文件时被指定,我们重新生成 vm_test:
gcc -L. -lvmath -Wl,-rpath=/home/vesontio/devel/lib vm_test.o -o vm_test unset LD_LIBRARY_PATH ./vm_test 12 3 Operand #1: 12 Operand #2: 3 Addition: 15 Subtraction: 9 Multiplication: 36 Division: 4 Fin
我们重新生成了 vm_test,命令参数唯一的区别就是用 -rpath 指定了共享库所在的路径。在试运行前,我们故意去掉环境变量 LD_LIBRARY_PATH,但是程序还是依旧华丽丽地运行了,因为共享库的路径已经被写死在可执行文件里了。所以 vm_test 不再需要借助 LD_LIBRARY_PATH 来寻找 libvmath.so.1 了。
ldconfig 是 Linux 系统下共享库的管理工具,可以帮助系统内的应用程序搜索所需的共享库。ldconfig 默认会搜索标准的共享库路径,包括 /lib 和 /usr/lib,但是如果你的共享库被放在了其他地方,那就需要手动修改 ldconfig 的配置文件,即 /etc/ld.so.conf:
su vim /etc/ld.so.conf
修改 ldconfig 的配置文件,你需要管理员 root 权限,用 su 登录之后,将我们的共享库的路径写入 ld.so.conf 内,然后刷新一下 ldconfig 的缓存:
ldconfig
现在再去运行一下 vm_test:
./vm_test 12 3 Operand #1: 12 Operand #2: 3 Addition: 15 Subtraction: 9 Multiplication: 36 Division: 4 Fin
又妥了!但是记住:ldconfig 只会帮助程序在运行时寻找共享库,但是在 gcc 的连接阶段,还是得手动用 -L 指定共享库的路径,并且用 -l 指定所需共享库。
以上三种方法都可以实现共享库的调用,至于哪种方法更合适,那就看开发人员自己的需求,根据实际情况来决定。