前言
不管是在 Windows 下开发,还是在 Linux 下开发,我们都会经常性的使用一些库文件,这些库文件的特点就是,我们可以看到接口的原型并通过这些接口来调用这个函数的功能,但是我们无法查看这个功能的实现。这就是库文件,功能提供者把头文件和库文件发布给我们,我们便可以根据头文件中的接口去调用库文件中实现的功能,并且对我们隐藏了功能实现的源码和细节。其实,链接库(动态库和静态库)并没有什么神秘的,本文就带你一起解开链接库的神秘面纱,让大家自己也可以动手制作动态库与静态库。
一、什么是链接库
有时候我们对外提供功能的时候,可能不希望对方看到源码,我们就可以制作成库文件,把库文件和头文件给到对方就可以达到提供功能又不暴露源码的目的。链接库就是指将库文件编译后打包为一个二进制文件,这些二进制文件会在程序调用的时候加载到内存中。实际上,一个或多个源文件编译为目标文件后,这个文件中所引用的外部的符号需要通过链接来找到这部分缺失的地址。而链接的方式又分为两种,如果是在生成可执行文件之前就已经把所有的链接操作完成了,这种链接称为静态链接,这种库文件称为静态链接库;如果是在程序执行的时候才进行链接,这种称为动态链接,对应的库文件称为动态链接库。也正因为如此,使用静态库时生成的可执行文件是可以独立运行的,因为他不再需要外部的内容,而动态库编译生成的可执行文件就无法单独运行,因为他在运行时,才会去链接所引用的外部地址。
1. 静态链接库
静态库会直接加载到代码段,他和所有的目标文件一起链接成可执行文件,生成可执行文件后可以独立运行。但是,正因为静态库会直接加载到内存的代码段,可执行文件的内部都拷贝了所有目标文件和静态库的指令和数据,编译生成的可执行文件会比较大。并且,如果整个系统中有多个链接统一静态库的可执行文件时,每个可执行文件都要拷贝一份静态库的指令和数据,这就造成了空间浪费,因为他们拷贝的数据都是同样的内容。最后,如果一旦静态库文件有代码更新,就需要重新编译链接重新生成整个可执行文件,更新升级麻烦。在 Linux 系统中,静态链接库文件的名称通常为 libxxx .a,在 Windows 系统中,静态链接库文件的后缀名为 .lib。
2. 动态链接库
其实,动态库这个称呼本身是对 Windows 平台上动态链接所用的库文件的一种称呼,在 Linux 下,一般称为共享库。动态库是在运行时加载到内存的共享库段,这样,如果很多程序都要用到静态库的时候,就会节省大量内存,因为它不像静态库那样加载到代码段,而是是在运行时载入内存的共享库段,当多个程序要用到同一个动态库时,所有程序可以共享这个共享库段的指令和数据。动态链接的实现是这样的,在编译时首先由静态链接器将所有的目标文件链接为一个可执行文件,等到程序运行时会将要用到的动态库加载到内存的共享库段,由动态链接器完成可执行文件和动态库文件的链接工作,可以理解为按需载入内存(在需要用到的时候,才会载入内存)。动态库大大方便了程序的升级和更改,只要用新的动态库文件替换旧的动态库文件即可,在运行时,会自动连接新的库文件。但是正因为动态库运行时载入的这个特点,使用动态库的可执行文件在运行时,会略慢一些,但整体来说,运行速度的性能损失,远远小于内存节省带来的收益。在Linux系统中,动态链接库的名称通常为 libxxx.so,在 Windows 系统中,动态链接库的后缀名为 .dll。GCC 编译器在生成可执行文件时,默认会优先使用动态链接库完成链接,如果当前系统环境中没有程序文件所需要的动态链接库,GCC 便会选择静态链接库进行静态链接。如果两种库文件都没有找到,则链接失败。
3. 库文件与头文件
我们在发布库文件的同时,要将库文件和头文件一起发布,头文件中存储了变量、函数或者类等这些功能模块的声明部分,库文件中存储了各模块具体的实现部分。也就是说,头文件中定义了调用库文件中功能模块的接口。头文件的存在也实现了这样一种功能,当我们对外提供功能时,可以通过库文件来隐藏源码实现,功能的使用方只需要根据头文件所提供的接口来调用功能模块即可。
4. 库文件的引用
当我们使用 GCC 编译和链接程序时,GCC 默认会链接 libc.a 或者 libc.so这两个标准库,但是对于其他的库(非标准库、第三方库等),就需要手动去添加链接库。通过 GCC -l 选项来指定库名,直接在 -l 后面加库名即可。( -l 是小写的 L ) 正常情况下,我们指定了要使用的库名时,GCC 会自动在标准库目录中搜索文件,例如在CentOS中是 /usr/lib 目录。但是,如果想链接位于其它目录中的库,比如说我们自己建的库,或者我们要引用别人提供的库,就需要在编译时显示指定库的路径。指定方法有三种: ① 像指定普通头文件的路径一样,为 GCC 显示指定该库文件的完整路径与文件名 -I /目录名 。 ② 通过 GCC 的 -L 选项,为GCC增加搜索目录,可以使用多个 -L 选项,或者在一个选项内使用冒号 : 分割来指定多个搜索路径。 ③ 把库文件所在的目录加到环境变量 LIBRARYPATH 中。
二、自己动手制作静态链接库
准备工作,共准备4个文件,目录结构如下
my_print.h 文件内容如下
代码语言:javascript复制#ifndef _TEST_H
#define _TEST_H
#include "stdio.h"
#include "stdlib.h"
void print_array_int(int* array, int len);
void print_array_char(char* array, int len);
void print_array_string(char* array[], int len);
void print_hello();
#endif
my_print.c 文件内容如下
代码语言:javascript复制#include "my_print.h"
void print_array_int(int* array, int len)
{
int i = 0;
printf("array: ");
for(i = 0; i < len; i )
{
printf("%d ", array[i]);
}
printf("n");
}
void print_array_char(char* array, int len)
{
i = 0;
printf("array: ");
for(i = 0; i < len; i )
{
printf("%c ", array[i]);
}
printf("n");
}
void print_array_string(char* array[], int len)
{
int i = 0;
printf("array: ");
for(i = 0; i < len; i )
{
printf("%s ", array[i]);
}
printf("n");
}
main.c 文件内容如下
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include "my_print.h"
int main()
{
int a1[10];
int i = 0;
for(i = 0; i < 10; i )
{
a1[i] = i 1;
}
print_array_int(a1, 10);
print_hello();
return 0;
}
print_hello.c 文件内容如下
代码语言:javascript复制#include "my_print.h"
void print_hello()
{
printf("hello Linux ...n");
}
1. 静态库的制作
① 首先把所有 .c 文件编译为 .o 文件
代码语言:javascript复制gcc -c *.c -I ../header/
一定要从 .o 文件去生成 .a 文件,否则即使你生成了库,在使用时也可能出现未知的错误。 ② 将 .o 文件打包为 .a 文件 这一步需要使用 ar 工具来完成。ar 工具是用来创建, 修改和提取档案的工具,ar 是 archive 的缩写。归档是指将多个文件(或一个文件)放到单个文件中,不进行其他额外的操作。也就是说,归档并不等同于压缩,压缩会使文件的大小减小。
代码语言:javascript复制ar rcs libmprint.a *.o
- 参数 r :在库中插入模块(替换)。当插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在,ar 显示一个错误消息,并不替换其他同名模块。默认的情况下,新的成员增加在库的结尾处,可以使用其他任选项来改变增加的位置。
- 参数 c :创建一个库。不管库是否存在,都将创建。
- 参数 s :创建目标文件索引,这在创建较大的库时能加快时间。如果不需要创建索引,可改成大写 S 参数。如果 .a 文件缺少索引,可以使用 ranlib 命令添加)。
一般我们命令静态库的时候是这样的,前面是 lib 中间加上我们自己给静态库文件的名称,后面加后缀 .a ,也就是 libxxx.a ,实际上, xxx 才是静态库文件的名称。
可以查看一下生成的 libmprint.a 的内容,一堆乱码
我们可以用 nm 命令查看库文件的信息,可以看到它是由 my_print.o 和 print_hello.o 生成的。
2. 静态库的使用
静态库的使用方法
代码语言:javascript复制gcc main.c -o exe -lmyprint -L ../lib/ -I ../header/
首先对上面的命令进行分解一下,-lmyprint 用于指定链接库的名字,我们上面生成了一个名为 libmprint.a 的静态库,把前缀后缀去掉留下中间的名字即可;-L …/lib/ 指定静态库的搜索目录,不指定的会,编译器只会去默认的链接库搜索路径寻找;-I …/header/ 这个就比较熟悉了,指定头文件的路径。
我们再发布的时候,只需要把 header 和 lib 发布给别人即可
三、自己动手制作动态链接库
1. 动态库的制作
① 编译生成与位置无关的 .o 文件 动态共享库是在运行的时候才加载的,它加载到内存的共享库段,用完后就释放,所以要编译为与位置无关的。这里要用到GCC 的 -fPIC 选项参数,该选项表示编译为位置独立的代码,如果不用这个选项的话编译后的代码是位置相关的,所以动态载入的时候会通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。在静态链接库中,可执行文件在链接时就知道每一行代码、每一个变量会被放到线性地址空间的什么位置,因此这些地址可以都作为常数写到代码里面。对于动态库,只有加载的时候才知道。也就是说,动态链接库要编译为与位置无关的代码,这样只有在运行时才直到代码的位置。而静态库是在生成可执行文件之前,就直接把代码加载到内存的代码段了,代码的位置等信息也就已经知道了,所以不需要生成与位置无关的代码。
代码语言:javascript复制gcc -fPIC -c *.c -I ../header/
② 将 .o 文件打包 这里要用到 GCC 的 -shared 选项,该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),如果不用该标志的话外部程序将无法连接。 可以在动态库文件名后面加版本号 libxxx.so.主版本号.副版本号 ,比如 libmdprint.so.1.1 表示我这是 1.1 版本的动态库,文件后缀后面的数字就是版本号的意思。
代码语言:javascript复制gcc -shared *.o -o libmdrint.so
2. 动态库的使用
(1)加载动态库
使用动态库的命令和使用静态库的命令一样
代码语言:javascript复制gcc main.c -o exe -lmdrint -L ../lib/ -I ../header/
(2)“加载共享库出错”的解决方法
这个是时候,虽然我们已经生成了可执行文件,但是当我们运行可执行文件的时候,可能会出现这样的错误
我们可以通过命令 ldd 来查看一下可执行文件的链接情况
在我们自己引入的库 libmdrint.so 一栏显示 “not found” ,在默认情况下,我们自己引入的动态库是无法使用的,需要进行一些配置。这里提供以下几种解决方法。
① 在系统库路径下建立软链接
前面我们已经介绍了,GCC 会默认去系统库路径下搜索库文件,所以只要我们把自己的库文件放到这个目录下就可以了,系统库的路径是 /usr/lib 或 /lib 。但是我们实际操作的时候,可能无法把文件拷贝到这两个路径下,我们可以在系统库路径下建立一个软链接来指向我们的库文件(软链接相关知识请参考本人 Linux 专栏文章《【Linux王者之路基础篇:基本命令与基础知识】Linux常用shell命令(及相关知识)详解与用法演示》)
代码语言:javascript复制sudo ln -s /home/qq/dm/dm_lib/lib/libmdrint.so /usr/lib/libmdrint.so
这里第一个路径也就是我们自己的动态库文件所在目录,一定要用绝对路径(如果使用相对路径,会找不到该路径),第二个路径是系统库的路径。
② 配置环境变量
实际上, ld 链接器在寻找库路径的时候,都是通过一个环境变量 LD_LIBRARY_PATH 来寻找的,我们可以打印看一下这个环境变量
我们可以通过 export 命令来给环境变量增加一个路径,只需要把自己的路径加进去即可,命令如下
代码语言:javascript复制export LD_LIBRARY_PATH=/home/qq/dm/dm_lib/lib/:$LD_LIBRARY_PATH
我们知道在 shell 下直接把绝对路径加到环境变量中,只是一种临时的环境变量,当关机再次开机的时候,就会失效,如果想要永久有效,可以修改配置文件。我们可以把上面这条命令放到 .bashrc 文件中。 在我的 Linux 专栏文章中,已经不只一次提到 .bashrc 这个配置文件了,其实 Linux 每次开机都会执行这个配置文件,我们实现环境变量永久有效的原理就是,Linux 每次开机都会执行 .bashrc 配置文件,而配置文件中包含设置环境变量的命令,所以每次开机都会配一下环境变量,这样就达到了环境变量永久有效的目的。.bashrc 配置文件在家目录下
代码语言:javascript复制vim ~/.bashrc
③ 修改 ld.so 加载器的配置文件 ld.so.conf
这里先介绍几个概念:
- ld链接器 :gcc 可以在编译周期传递参数,指定需要链接的库文件,生命周期是在 compile-time。
- ld.so动态链接器/加载器,程序运行的时候会根据指定的路径去加载指定的库,生命周期是在 run-time。
- ld.so加载器及配置文件ld.so.conf:指定动态链接库的搜索路径(也可以通过配置环境变量$LD_LIBRARY_PATH 来指定程序运行时的动态库.so文件的搜索路径)。使用 ldconfig 就可以将 ld.so.conf 中的指定目录的库文件加载到内存中,并记录在/etc/ld.so.cache文件中。配置文件 /etc/ld.so.conf 记录了编译时使用的动态链接库的路径,在默认情况下,编译器只会使用 /lib 和 /usr/lib 这两个目录下的库文件,通过这个配置文件可以增加我们自己的动态库文件搜索路径。
使用 vim 编辑器打开配置文件,把路径放进去
代码语言:javascript复制sudo vim /etc/ld.so.conf
放入路径之后,还要在 shell 下执行一条命令,使刚才的配置生效
代码语言:javascript复制sudo ldconfig -v
不管使用上面三种方法的哪种,都可以使动态库正常加载到可执行文件中,我们再次使用 ldd 命令查看 exe 文件,可以看到,我们的动态库 libmdrint.so 后面由原来的 “not found” 变成了一个路径,这说明加载器现在可以找到动态库的路径了,可执行文件 exe 也就可以运行了。
总结
学完本文,是不是觉得动态库和静态库也没什么嘛,不过如此,我们大家都可以自己动手去做。那么如果想学习到更多 Linux 下编程的知识,请关注我的 Linux 专栏,查阅其他文章,谢谢大家的支持。