本文通过一个简单的例子,介绍linux环境下动态库编程的基本做法。
基本概念
库从本质上来说是一种可执行代码的二进制格式,可以被载入到内存中执行。库分为静态库和动态库两种。
在Linux下,静态库的名字一般为libxxx.a,其中xxx为库的名字,使用静态库时,整个库的所有数据都会被整合到目标代码中,因而文件比较大,执行时不再需要外部库的支持,如果库函数有变化,程序必须重新编译。
动态库的名字一般是libxxx.M.N.so,其中xxx为库名,M是主版本号,N是副版本号,版本号是可选的,但名字必须要有。动态库在编译时并没有被编进目标代码中,程序执行到相关函数时才调用库中的相应函数,因此可执行文件较小,程序运行环境必须提供相应的动态库。动态库的修改不影响程序,因而升级比较方便。
对于静态库,链接器会找出程序需要的函数,将它们拷到执行文件,由于这种拷贝是完整的,一旦链接成功,静态库也就不再需要了。对于动态库,则会在程序内打个标记指明程序运行时,要先载入这个库。由于动态库节省空间,在链接时会优先去链动态库,即如果同时存在静态库和动态库,不特别指定的话,将与动态库链接。
制作动态库
为方便演示,这里做个简单的加减法库,代码包括add.h,add.c,sub.h,sub.c共4个文件。
// === add.h ===
#ifndef _ADD_H_
#define _ADD_H_
int add(int, int);
#endif
// === add.c ===
int add(int a, int b)
{
return a + b;
}
// === sub.h ===
#ifndef _SUB_H_
#define _SUB_H_
int sub(int, int);
#endif
// === sub.c ===
int sub(int a, int b)
{
return a - b;
}
通过以下命令生成动态库文件。
gcc -o libtest.so add.c sub.c -fPIC -shared -g -Wall -O0
可以用nm -D libtest.so
查看导出了哪些接口,可以看到,add和sub接口已正常导出,可以使用。
$ nm -D libtest.so
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000201020 B __bss_start
w __cxa_finalize
w __gmon_start__
0000000000201020 D _edata
0000000000201028 B _end
00000000000005c0 T _fini
0000000000000480 T _init
000000000000059a T add
00000000000005ae T sub
静态链接动态库
动态库有了,下面写个程序来调用动态库,假设文件为call.c,代码如下。
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main()
{
int x, y;
while (~scanf("%d%d", &x, &y))
{
printf("add=%d, sub=%d\n", add(x,y), sub(x,y));
}
return 0;
}
运行以下命令进行编译链接,其中-L参数指定需要链接的so库的位置,-l参数接库名指定链接哪个库。
gcc -o call call.c -L. -ltest
当然,默认情况下call程序还不能运行,因为它找不到libtest.so库,可以用ldd call
看下它静态链接了哪些库。
$ ldd call
linux-vdso.so.1 (0x00007fffe7acc000)
libtest.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd695350000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd695943000)
可以看到,libtest.so库确实没找到,因为编译时能找到so并不代表运行时就能找到,编译时如果so文件不在系统默认的lib目录下,则需要用-L
参数指定位置;而运行时加载so可以通过多种方式指定,按先后顺序依次如下:
- 环境变量LD_LIBRARY_PATH
- 链接时-rpath指定的共享库查找路径
- ldconfig配置指定的路径
- /lib, /usr/lib目录
关于ldconfig多说一句,在Linux系统中,动态链接库的配置文件一般在/etc/ld.so.conf文件内,该文件会包含/etc/ld.so.conf.d目录。当修改了ld.so.conf或者/ld.so.conf.d目录下的文件,或者往该目录下拷贝了新的动态库时,要执行ldconfig
命令,它负责搜索/lib, /usr/lib以及/etc/ld.so.conf中所列的目录下可用的动态链接库文件到缓存/etc/ld.so.cache中。执行ldconfig
时,如果带了目录,则表示在缓存/etc/ld.so.cache中追加指定目录下的共享库;如果是单独运行,则它只会搜索/lib, /usr/lib和/etc/ld.so.conf列出的目录,重建/etc/ld.so.cache。
在本例中,可以通过多种方式让程序能够跑起来。
- 改环境变量:
export LD_LIBRARY_PATH=.
- 运行
ldconfig $(pwd)
,需root权限 - 改编译指令,追加参数
-Wl,-rpath=.
- 将libtest.so拷贝到/lib或/usr/lib目录下。
动态链接动态库
Linux提供了一组api用于动态加载so文件。
#include <dlfcn.h>
void* dlopen(const char *filename, int flag);
char* dlerror(void);
void* dlsym(void *handle, const char *symbol);
int dlclose(void *handle);
说明:
- filename如果不以/开头,则为相对路径,将按照以下顺序查找。
(1)环境变量LD_LIBRARY
(2)/etc/ld.so.cache
(3)/lib, /usr/lib - flag表示在什么时候解决未定义的符号调用,可取RTLD_LAZY和RTLD_NOW。
下面是个动态链接的例子。
#include <stdio.h>
#include <dlfcn.h>
typedef int (*Func)(int, int);
int main()
{
void *handle = dlopen("libtest.so", RTLD_LAZY);
if (dlerror()) perror("dlopen");
Func fnadd = dlsym(handle, "add");
if (dlerror()) perror("load add");
Func fnsub = dlsym(handle, "sub");
if (dlerror()) perror("load sub");
int x, y;
while (~scanf("%d%d", &x, &y))
printf("%d %d\n", fnadd(x, y), fnsub(x, y));
dlclose(handle);
return 0;
}
注意,如果采用动态加载方式,编译时须指定-rdynamic -ldl选项。
gcc -o call call.c -rdynamic -ldl