1.动态链接库概念:
动态链接库又叫做动态链接共享库,共享库是一个目标模块,再运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来,这个过程称为动态链接,是由一个叫做动态连接器的程序来执行的。
共享库也成为共享目标(shared object),在Unix系统中通常用.so后缀来表示,在Windows中是.dll。
2.与位置无关的代码(PIC):
我们可以编译库代码,使得不需要链接器修改库代码就可以在任何地址加载和执行这些代码,这样的代码叫做与位置无关的代码。
下面这张图是CSAPP中的图,很好地说明了共享库是如何工作的:
在这张图上面,我们最关心的过程在最后:动态连接器,从这张图上,我们可以看到,加载共享库的过程是在程序运行中完成的,而不是编译的时候把共享库中的内容拷贝到相应的空间,这个就是动态链接库与静态链接库的区别。动态链接库可以提供更大的灵活性:按照用户的需求来调用相应的函数或者访问相应的变量。但是使用动态链接库比使用静态链接库要慢一些【用时间换空间】
3.从应用程序中加载动态链接库:
那么我们如何从程序中显式地去加载动态链接库呢?在Linux环境下面,我们可以使用系统给我们提供的几个接口来实现这个需求:
/*
@成功返回指向句柄的指针,错误则为NULL
*/
#include <dlfcn.h>
void* dlopen(const char*filename, int flag);
这个函数用来打开后缀名为.so的文件,filename可以是绝对地址或者相对地址。flag 的取值有三个:
RTLD_GLOBAL:这个选项是用以前带此选项打开的库来解析fine那么中的外部符号。
RTLD_NOW:这个选项告诉链接器立即解析对外部符号的引用。
RTLD_LAZY:这个选项告诉链接器推迟符号解析知道执行来自库中的代码。
这三个参数后面两者必取一个,第一个可选。
/*
成功则返回指向符号的指针,出错则返回NULL
*/
#include <dlfcn.h>
void* dlsym(void* handle, char* symbol);
handle参数是第一个函数dlopen所返回的指针,symbol参数是定义在共享库里面的变量,函数的命名。如果在当前共享库里面找不到symbol字符串所标明的符号,那么就返回NULL。
/*
成功返回0,出错返回-1
*/
#include <dlfcn.h>
int dlclose(void* handle)
这个函数用来关闭对共享库的使用,但是前提是没有其他的共享库在使用这个共享库。
/*
如果前面三个函数调用失败,那么则返回错误消息,如果调用成功,则返回NULL
*/
#include <dlfcn.h>
const char* dlerror(void);
这个函数是用来错误解析的,这个函数在实际的编写代码中是必须的,它可以让我们知道每一个函数的调用结果,并且进行出错处理。
4.应用:
1)分发软件:参照微软windows的dll的做法,每当软件需要更新时,就发布一个dll,供用户下载。
2)web服务器:动态生成web页面内容,个性化的web页面等。
3)在线更新:我们可以在服务器运行的时候无需停止服务器,就可以更新已经存在的函数,以及添加新的函数。
5.实践:
我们考虑这么一个情景:一个游戏服务器正在线上运行,但是突然发现了一个业务逻辑错误,在这种情况下,选择直接关服是不明智的,因为这会造成用户数据的丢失,并且影响用户游戏的体验,所以我们不能直接重启服务器来修改这个BUG。那么如何在不让用户掉线,并且不丢失用户数据的情况下修改这个BUG呢,这个时候我们就需要把用户的数据和处理业务逻辑的代码制作成为两个共享库,让数据,逻辑代码跟主程分开。
我们需要两个源文件:libdata.c libtext.c,这两个源文件一个用来保存数据,一个用来保存模拟的代码逻辑。
libdata.c
int num = 100;
libtext.c
int printNum(int num) {
printf("Num is : %d", num--);
return num - 1;
}
main.c
#include <stdio.h>
#include <signal.h>
#include <dlfcn.h>
#include <unistd.h>
typedef struct {
void* textHandler;
void* dataHandler;
} MyHandler;
typedef enum {
DATA = 0,
CODE = 1,
} so_type_e;
void* Dlopen(const char* filename, int flag, so_type_e type) { //dlopen 包装函数
char* error = NULL;
void* result = NULL;
if (type) { //根据type来加载不同的.SO
result = dlopen(filename, flag);
} else {
result = dlopen(filename, flag);
}
if ((error = dlerror()) != NULL) { //出错
printf("The error is : %s\n", error);
return NULL;
}
return result;
}
void* Dlsym(void* handle, const char* symbol) { //加载对应共享库中的内容, 包装函数
char* error = NULL;
void* result = dlsym(handle, symbol);
if((error = dlerror()) != NULL) {
printf("The error is : %s\n", error);
return NULL;
}
return result;
}
MyHandler handler;
int (*printNum)(int);
void reload_text(int sig) { //注册的信号处理函数,信号为SIGQUIT
char* error = NULL;
printf("Start to reload\n");
dlclose(handler.textHandler);
handler.textHandler = NULL;
handler.textHandler = dlopen("./text.so", RTLD_NOW);
int (*currentPtr)(int) = printNum;
printNum = dlsym(handler.textHandler, "printNum");
if ((error = dlerror()) != NULL) {
printf("The error is : %s\n", error);
printNum = currentPtr;
return ;
}
}
int main(int argc, char** argv) {
handler.dataHandler = Dlopen("./data.so", RTLD_NOW, DATA); //取得两个.so的句柄
handler.textHandler = Dlopen("./text.so", RTLD_NOW, CODE);
int num = *(int*) Dlsym(handler.dataHandler, "num"); //获得定义在data里面的变量
printNum = Dlsym(handler.textHandler, "printNum"); //获得定义在text里面的函数
struct sigaction newAction, oldAction;
newAction.sa_handler = reload_text; //注册处理函数
sigaddset(&newAction.sa_mask, SIGQUIT); //添加需要处理的信号
//newAction.sa_flags = SA_RESETHAND | SA_NODEFER;
newAction.sa_flags = 0;
sigaction(SIGQUIT, &newAction, &oldAction); //监听信号的发生
while (1) {
num = printNum(num);
sleep(2);
}
return 0;
}
对于最开始的两个文件,我们使用下面的命令来编译这两个文件成为共享库:
gcc -shared -fPIC -o text.so libtext.c
gcc -shared -fPIC -o data.so libdata.c
其中的 -fPIC参数是生成位置无关的代码, -shared 是生成动态库
对于main.c文件,我们需要下面这条命令来编译:
gcc -ldl path/to/the/libdl.a main.c
其中的 -ldl参数是指定链接器去使用libdl.a这个静态库文件,必须要加上这个参数,否则上面dl开头的函数会出现未定义的错误。
上面这个命令可能也会出现错误,可能是跟环境或者编译器有关系。如果上面的那条命令出错的话,可以试下下面这条命令:
gcc main.c -ldl path/to/the/libdl.a
编译通过之后,直接运行这个程序
./a.out
接下来我们打开libtext.c这个文件,把代码修改如下:
int printNum(int num) {
printf("Num is : %d", num--);
return num;
}
再按照编译库文件的命令编译此.c文件,生成新的.so文件,然后我们通过终端向刚才的程序发一个SIGQUIT信号:Ctrl + \ ,最后我们可以得到类似于下面这样的结果:
从图中可以看到,我们修改了代码的逻辑部分,但是数据部分在重新加载新的so文件的前后都是不变的(不会被覆盖) 这样就达到了在保持main函数不结束,并且数据不变的情况下修改逻辑这个需求。