3.Linux 下制作静态库

本节课我们来使用四个文件: file1.cfile1.hfile2.cfile2.h 在 Linux 操作系统平台上 使用 GCC 编译器制作成动态库 libmylib.so

然后将主程序 main.c 编译成可执行文件 myapp,在通过动态加载的方式运行此可执行程序。

GCC 生成动态库步骤:

  1. 使用 GCC 在编译和汇编阶段将源文件生成和位置无关代码目标文件 (使用 -fPIC 选项)。
  2. 使用目标文件创建共享库 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 选项说明

这样我们即生成了动态库文件 libmylib.so,合作方需要将此文件连同 两个头文件 file1.hfile2.h 一起提供给 项目发布方才能够编译成为可执行文件。

下面我们再来讲解一下如何使用动态链接库。

主程序要使用动态链接库有两种方法:

  1. 静态链接时加载,就是在程序启动时自动加载。
  2. 运行时动态加载,就是使用 dl 系列的API函数在需要时再加载。

1、动态库的静态链接时加载

我们先来讲述静态链接时加载的主程序的编译和运行过程,假设动态的库文件 libmylib.so 和头文件 file1.hfile2.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);
减少动态库的引用计数,如果计数达到0 则卸载动态库(真正释放动态库)。成功返回 0,失败返回非零值。
void *dlsym(void *handle, const char *symbol);
在动态库中找到用 symbol参数指定名字的符号(通常是函数名),并返回符号对应的函数地址。失败返回 NULL
char *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 定位到库函数地址,然后就可以使用函数指针来调用动态库中的函数了。

静态库和动态库对比

特性
静态库
动态库
链接时机
编译时
运行时
文件大小
可执行文件较大
可执行文件较小
内存占用
每个程序独立占用
多个程序可共享
更新
需重新编译
只需替换库文件
依赖
无运行时依赖
需要库文件存在于系统中

动态库是现代软件开发的更常见选择,特别是在大型系统中,因为它支持模块化更新和更高效的资源利用。

实验:

尝试在自己的编译环境下制作动态库文件。