前言
最近在写一个小型的http服务器,为了能够提供网页的动态内容,开始接触linux静态库和动态库的概念。之后又看了《深入理解计算机系统》的第七章–链接,在此对于linux静态库和动态库做一个总结。
为什么需要库
定义
程序一般需要经过预处理,编译,汇编和链接将我们编写ASII文本程序变成可执行的目标文件(.out)。然而在实际开发中,有许多模块是反复复用的,如printf函数,scanf函数等,故所有的编译系统都提供一种机制,将所有的目标模块打包成库,作为链接的输入,供开发者调用链接。
静态库
静态库的名字一般为libxxx.a, 静态库把相关的函数编译成独立的目标模块(.a),然后封装成一个单独的静态库文件。在链接时,将静态库嵌入到可执行目标文件中,链接器将只拷贝被程序引用的目标模块。生成静态库文件过程中的Linux命令为:gcc -c pro1.c pro2.c; ar rcs libxxx.a pro1.o pro2.o
.静态库的优点是编译后的执行程序不需要外部函数库的支持,但是如果静态库更新了,那么我们就得重新编译程序,并且几乎每个C程序都会用到标准IO函数,如printf和scanf,那么在运行时,这些函数的代码会被复制到每个运行进程的文本段中,在运行多进程的系统上,这是对稀缺的存储器系统资源的浪费。
动态库
基于静态库的缺点,动态库(共享库)应运而生。动态库是一个目标模块(.so),在运行时可以被加载到任意的存储器地址,并在一个存储器中的程序链接起来。从文件系统角度来说,所用引用该动态库的可执行目标文件都共享这个.so文件的代码和数据,而不是像静态库那样将内容嵌入到引用它们的可执行目标文件中。从存储器系统角度来说,一个共享库的.text节的一个副本可以被不同正在运行的进程共享。
使用静态库
首先我们编写两个程序pro1.c 和pro2.c,代码如下:
//pro1.c
#include <stdio.h>
void print1()
{
printf("This is the first lib!\n");
}
//pro2.c
#include <stdio.h>
void print2()
{
printf("This is the second lib!\n");
}
然后我们编译这两个程序,并使用AR工具,生成.libpro.a文件
[yateslaw@study link]$ gcc -c pro1.c pro2.c
[yateslaw@study link]$ ar rcs libpro.a pro1.o pro2.o
之后再编写一个main.c
int main()
{
print1();
print2();
return 0;
}
最后我们编译和链接main.o 和libpro.a,运行main程序
[yateslaw@study link]$ gcc -c main.c
[yateslaw@study link]$ gcc -o main main.o ./libpro.a
[yateslaw@study link]$ ./main
This is the first lib!
This is the second lib!
使用动态库
生成.so文件
首先编写add.c 和mul.c程序
//add.c
void add(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]+y[i];
}
//mult.c
void mult(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]*y[i];
}
生成.so文件Linux命令
[yateslaw@study dynamic]$ gcc -shared -fPIC -o libpro.so add.c mult.c
其中-fPIC选项指示编译器生成与位置无关的代码,-shared选项指示连接器创建一个共享的目标文件。
加载和链接共享库的相关函数
dlopen函数
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
返回:若成功返回指向句柄的指针,若出错则为NULL
dlopen函数加载和链接共享库filename。flag参数可以为RTLD_NOW,告诉链接器立即解析对外部符号的引用,而RTLD_LAZY则告诉链接器推迟符号解析直到执行来自库中的代码。RTLD_GLOBAL标志则表示上面两值任一都可以。
dlsym函数
#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);
返回:若成功返回指向符号的指针,若出错则为NULL
dlsym函数的输入是一个指向前面已经打开共享库的句柄和一个符号名字,如果该符号存在,就返回符号的地址,否则返回NULL。
dlclose函数:卸载共享库
#include <dlfcn.h>
int dlclose(void *handle);
返回:若成功返回0,若出错则为-1
dlerror函数
#include <dlfcn.h>
const char *dlerror(void);
返回:如果前面对dlopen,dlsym,dlclose调用失败,则为错误消息,如果前面调用成功,则为NULL
讲完上面的函数以后,下面我们就利用libpro.so共享库来编写我们的main程序:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int x[2]={1,2};
int y[2]={3,4};
int z[2];
int main()
{
void *handle;
void (*add)(int *,int *,int *,int);
char *error;
handle=dlopen("./libpro.so",RTLD_LAZY);
if(!handle)
{
fprintf(stderr,"%s\n",dlerror());
exit(1);
}
add=dlsym(handle,"add");
if((error=dlerror())!=NULL)
{
fprintf(stderr,"%s\n",dlerror());
exit(1);
}
add(x,y,z,2);
printf("z=[%d %d]\n",z[0],z[1]);
if(dlclose(handle)<0)
{
fprintf(stderr,"%s\n",dlerror());
exit(1);
}
return 0;
}
编译main.c程序并链接动态库libpro.so ,运行后结果如下:
[yateslaw@study dynamic]$ gcc -rdynamic -O2 -o main main.c -ldl
[yateslaw@study dynamic]$ ./main
z=[4 6]
总结比较动态库和静态库
- 多个目标文件可以被链接到一个单独的静态库,静态库被链接后直接嵌入到可执行文件中。这样有两个缺点,首先系统的存储器资源被浪费了,因为如果多个程序链接了同一个库,那么每一个生成的可执行文件都会有库的一个副本。其次,当需要更新静态库文件时,整个程序需要重新编译。
- 动态库的出现弥补了静态库的缺点。动态库是在程序运行时被链接的,在链接时.so文件的代码和数据节是没有拷贝到可执行目标文件的,反之,链接器只是拷贝了一些重定位和符号表的信息,使得运行时可以解析.so文件代码和数据的引用。当我们需要升级动态库时,只需使用新的.so文件替换原来的旧文件即可。
参考文献:
《深入理解计算机系统 第七章-链接》