3.Linux 下制作静态库
本节课我们来使用四个文件: file1.c、file1.h、file2.c、file2.h 在 Linux 操作系统平台上 使用 GCC 编译器制作成动态库 libmylib.so。
然后将主程序 main.c 编译成可执行文件 myapp,在通过动态加载的方式运行此可执行程序。
GCC 生成动态库步骤:
- 使用 GCC 在编译和汇编阶段将源文件生成和位置无关代码目标文件 (使用
-fPIC选项)。 - 使用目标文件创建共享库
libmylib.so
生成目标文件
weimingze@mzstudio:~$ ls
file1.c file1.h file2.c file2.h
weimingze@mzstudio:~$ gcc -fPIC -c file1.c file2.c
weimingze@mzstudio:~$ ls
file1.c file1.h file1.o file2.c file2.h file2.o
weimingze@mzstudio:~$ gcc -shared -o libmylib.so file1.o file2.o
weimingze@mzstudio:~$ ls
file1.c file1.h file1.o file2.c file2.h file2.o libmylib.so
使用于位置无关的目标文件生成动态库 libmylib.so。
gcc -shared -o libmylib.so file1.o file2.o
gcc 选项说明
-fPIC选项是生成位置无关的代码,因为动态库在运行时加载的位置不固定。因此所有的变量和函数都要使用相对位置来进行计算,这就是位置无关。-shared选项就是生成共享库(即动态库)。
这样我们即生成了动态库文件 libmylib.so,合作方需要将此文件连同 两个头文件 file1.h、file2.h 一起提供给 项目发布方才能够编译成为可执行文件。
下面我们再来讲解一下如何使用动态链接库。
主程序要使用动态链接库有两种方法:
- 静态链接时加载,就是在程序启动时自动加载。
- 运行时动态加载,就是使用 dl 系列的API函数在需要时再加载。
1、动态库的静态链接时加载
我们先来讲述静态链接时加载的主程序的编译和运行过程,假设动态的库文件 libmylib.so 和头文件 file1.h和 file2.h 都存在于 mylib2 文件夹下,结构如下所示
weimingze@mzstudio:~$ tree .
.
├── main.c
└── mylib2
├── file1.h
├── file2.h
└── libmylib.so
编译和运行过程如下:
weimingze@mzstudio:~$ gcc -c main.c -I mylib2
weimingze@mzstudio:~$ gcc -o myapp main.o -L mylib2 -lmylib
weimingze@mzstudio:~$ ./myapp
./myapp: error while loading shared libraries: libmylib.so: cannot open shared object file: No such file or directory
从上述运行结果可知,主程序 myapp 运行时出错,这是因为在程序运行时需要找到 libmylib.so 所在的文件夹。也就是说主程序 myapp 启动之前,需要使用 Shell 的环境变量 LD_LIBRARY_PATH 来设置动态库的路径,方法如下:
weimingze@mzstudio:~$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./mylib2
weimingze@mzstudio:~$ ./myapp
库函数 myfunc1 被调用
库函数 myfunc2 被调用
可见在启动应用前,设置了 LD_LIBRARY_PATH 的路径为 ./mylib2,主程序能够正常运行了。
2、动态库的运行时动态加载
下面我们在来说一下如何使用 dl 系列的 API(应用程序接口)来 真正的动态加载和使用 libmylib.so 中的函数。
动态加载的主要函数
void *dlopen(const char *filename, int flags);filename 加载动态库并返回操作句柄,引用计数加1,如果动态库已经加载则只是将引用计数加1。成功返回非空值,失败返回 NULL。int dlclose(void *handle);void *dlsym(void *handle, const char *symbol);symbol参数指定名字的符号(通常是函数名),并返回符号对应的函数地址。失败返回 NULLchar *dlerror(void);修改 main.c 文件如下:
// filename: main.c
#include <stdio.h>
#include <dlfcn.h>
int main() {
void *handle; // 保存动态库的打开句柄
void (*fn1)(void); // 用于指向动态库内的函数 myfunc1
void (*fn2)(void); // 用于指向动态库内的函数 myfunc2
// 打开动态库
handle = dlopen("./mylib2/libmylib.so", RTLD_LAZY);
if (NULL == handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}
// 获取函数地址
fn1 = dlsym(handle, "myfunc1");
if (NULL == fn1) {
printf("动态库内没有找到 myfunc1函数");
goto exit_main;
}
fn2 = dlsym(handle, "myfunc2");
if (NULL == fn2) {
printf("动态库内没有找到 myfunc2函数");
goto exit_main;
}
// 4. 使用函数指针调用动态库中的函数
fn1();
fn2();
exit_main:
// 5. 关闭动态库
dlclose(handle);
return 0;
}
程序结构如下:
weimingze@mzstudio:~$ tree .
.
├── main.c
└── mylib2
└── libmylib.so
编译和运行结果如下:
weimingze@mzstudio:~$ gcc -o myapp main.c
weimingze@mzstudio:~$ ./myapp
库函数 myfunc1 被调用
库函数 myfunc2 被调用
可见,使用动态加载时,编译的时候都不依赖库的头文件,使用 dlopen 打开库文件,在使用 dlsym 定位到库函数地址,然后就可以使用函数指针来调用动态库中的函数了。
静态库和动态库对比
动态库是现代软件开发的更常见选择,特别是在大型系统中,因为它支持模块化更新和更高效的资源利用。
实验:
尝试在自己的编译环境下制作动态库文件。