以下内容源于C语言中文网相关内容的学习整理,如有侵权请告知删除。
一、库文件的简介
库文件,其等价为压缩包文件。该文件内部通常包含不止一个目标文件(也就是二进制文件),每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块。例如,C 语言库文件提供有大量的函数(如 scanf()、printf()、strlen() 等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类(如 string 字符串类)。库文件的产生,极大的提高了程序员的开发效率。因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。并且,库文件的调用方法也很简单,以 C 语言中的 printf() 输出函数为例,程序中只需引入 <stdio.h> 头文件,即可调用 printf() 函数。
调用库文件为什么还要牵扯到头文件呢?首先,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。即所有的库文件都提供有相应的头文件作为调用它的接口,库文件是无法直接使用的,只能通过头文件间接调用。头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。
二、静态链接库与动态链接库的简介
C或C++程序从源文件到生成可执行文件需经历 4 个阶段,分别为预处理、编译、汇编和链接。链接阶段所要完成的工作,是将同一项目中各源文件生成的目标文件和程序中用到的库文件整合为一个可执行文件。虽然库文件明确用于链接,但编译器提供了2种实现链接的方式,分别称为静态链接和动态链接。采用静态链接方式实现链接操作的库文件称为静态链接库;采用动态链接方式实现链接操作的库文件称为动态链接库。
在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示,动态链接库的后缀名通常用 .so 表示;在 Windows 系统中,静态链接库文件的后缀名为 .lib,动态链接库的后缀名为 .dll。
GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。
在 Linux 发行版中,静态链接库和动态链接库通常存放在 /usr/bin 或者 /bin 目录下。
1、静态链接库
静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。
使用静态库文件的优势是可移植性强,可独立运行,即生成的可执行文件不再需要任何静态库文件的支持就可以独立运行。劣势是代码冗余、可执行文件的体积大:如果程序文件中多次调用库中的同一功能模块,则该模块代码就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。
2、动态链接库
动态链接库,又称为共享链接库。采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。
显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。
采用动态链接库的优势是生成的执行文件文件体积小,因为可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。劣势是可移植性差,无法独立运行,必须借助相应的库文件。
三、静态链接库的创建与使用
1、静态链接库的创建
比如有如下项目。为了隐藏加法、减法、除法的具体实现,可以将add.c、sub.c 以及 div.c 这 3 个源文件加工成静态链接库。并且根据实际需要,我们可以将它们集体压缩到一个静态链接库中,也可以各自压缩成一个静态链接库。
demo项目
├─ headers
│ └─ test.h
└─ sources
├─ add.c
├─ sub.c
├─ div.c
└─ main.c
将源文件打包为静态链接库的过程很简单,只需经历以下 2 个步骤:
(1)将所有指定的源文件,都编译成相应的目标文件。
[root@bogon demo]# gcc -c sub.c add.c div.c
[root@bogon demo]# ls
add.c add.o div.c div.o main.c sub.c sub.o test.h
(2)使用 ar 压缩指令,将生成的目标文件打包成静态链接库。该指令的基本格式如下。
ar rcs 静态链接库名称 目标文件1 目标文件2 ...
注意,静态链接库不能随意起名,需遵循“libxxx.a”这样的命名规则,其中xxx 指我们为该库起的名字。比如 Linux 系统自带的一些静态链接库名称为 libc.a、libgcc.a、libm.a,它们的名称分别为 c库、gcc库 和 m库。
[root@bogon demo]# ar rcs libmymath.a add.o sub.o div.o
[root@bogon demo]# ls
add.c add.o div.c div.o libmymath.a main.c sub.c sub.o test.h
2、静态链接库的使用
静态链接库的使用很简单,就是在程序的链接阶段,将静态链接库和目标文件一起执行链接操作,从而生成可执行文件。
以 demo 项目为例,首先我们将 main.c 文件编译为目标文件:
[root@bogon demo]# gcc -c main.c
[root@bogon demo]# ls
add.c div.c libmymath.a main.o sub.c
test.h add.o div.o main.c sub.o
在此基础上,我们可以直接执行如下命令,即可完成链接操作,默认生成可执行文件a.out(或者利用-o选项生成其他命名文件)。-static 选项表示强制 GCC 编译器使用静态链接库。
[root@bogon demo]# gcc -static main.o libmymath.a
[root@bogon demo]# ls
add.c a.out div.o main.c sub.c test.h
add.o div.c libmymath.a main.o sub.o
注意,上面这种写法只会在当前目录查找libmymath.a静态链接库,如果 GCC 编译器提示无法找到 libmymath.a,还可以使用如下方式完成链接操作。其中 -L 选项用于向 GCC 编译器指明静态链接库的存储位置; -l(小写的 L)选项用于指明所需静态链接库的名称。这里的名称指的是 xxx 部分,建议将 -l 和 xxx 直接连用(即 -lxxx),中间不需有空格;-L与路径之间可以有空格,也可以没有空格。
[root@bogon demo]# gcc main.o -static -L/root/demo/ -lmymath
[root@bogon demo]# ls
add.c a.out div.o main.c sub.c test.h
add.o div.c libmymath.a main.o sub.o
四、动态链接库的创建与使用
1、动态链接库的创建
使用源文件创建动态链接库,其基本格式如下。
gcc -fpic -shared 众多源文件名 -o 动态链接库名
- 选项-fpic(或者 -fPIC),表示让 GCC 编译器生成动态链接库(多个目标文件的压缩包)时,各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址,这样无论将来链接库被加载到内存的什么位置,都可以正常使用。
- 选项-shared,表示生成动态链接库。
- 动态链接库的名字规则与静态链接库完全相同,只不过其后缀是.so。
例如将add.c、sub.c 和 div.c 这 3 个源文件生成一个动态链接库:
[root@bogon demo]# ls
add.c div.c main.c sub.c test.h
[root@bogon demo]# gcc -fpic -shared add.c sub.c div.c -o libmymath.so
[root@bogon demo]# ls
add.c div.c libmymath.so main.c sub.c test.h
2、动态链接库的隐式调用
动态链接库的使用场景,就是与项目中其它源文件或目标文件一起参与链接生成可执行文件。以 demo 项目为例,前面我们将 add.c、sub.c 和 div.c 打包到了 libmymath.so 动态链接库中,此时该项目中仅剩 main.c 源程序文件,因此执行 demo 项目也就演变成了将 main.c 和 libmymath.so 进行链接,进而生成可执行文件。
执行如下指令,即意味着隐式调用动态链接库(隐式调用,意思是将动态链接库和其它源程序文件或者目标文件一起参与链接),生成可执行文件main.exe。
[root@bogon demo]# gcc main.c libmymath.so -o main.exe
[root@bogon demo]# ls
add.c div.c libmymath.so main.c main.exe sub.c test.h
main.exe 通常无法直接执行,因为执行过程中无法找到 libmymath.so 这个动态链接库。
[root@bogon demo]# ./main.exe
./a.out: error while loading shared libraries: libd.so: cannot open shared object file: No such file or directory
通过执行ldd main.exe
指令,可以查看当前文件在执行时需要用到的所有动态链接库,以及各个库文件的存储位置。可以看到main.exe 文件的执行需要 4 个动态链接库的支持,其中就包括 libmymath.so,但该文件无法找到,因此 main.exe 执行会失败。
[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fff423ff000)
libmymath.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00000037e2c00000)
/lib64/ld-linux-x86-64.so.2 (0x00000037e2800000)
运行由动态链接库生成的可执行文件时,必须确保程序在运行时可以找到这个动态链接库。常用的解决方案有以下几种。
(1)将链接库文件移动到标准库目录下(例如 /usr/lib、/usr/lib64、/lib、/lib64)。
(2)在终端输入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx
,其中 xxx 为动态链接库文件的绝对存储路径(此方式仅在当前终端有效,关闭终端后无效)。
(3)修改~/.bashrc 或~/.bash_profile 文件,即在文件最后一行添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx
(xxx 为动态库文件的绝对存储路径)。保存之后,执行source .bashrc
指令(此方式仅对当前登陆用户有效)。
3、动态链接库的显式调用
显式调用动态链接库的过程,类似于使用 malloc() 和 free()(C++ 中使用 new 和 delete)管理动态内存空间,需要时就申请,不需要时就将占用的资源释放。显式调用,意思是在编写代码时,利用某些函数申请调用库函数以及最后关闭与释放库函数资源。由此可见,显式调用动态链接库对内存的使用更加合理。显式调用动态链接库,更常应用于一些大型项目中。
和隐式调用动态链接库不同,在 C/C++ 程序中显式调用动态链接库时,无需引入和动态链接库相关的头文件。但与此同时,程序中需要引入另一个头文件,即#include <dlfcn.h>,因为要显式调用动态链接库,需要使用该头文件提供的一些函数。
(1)类似于读写文件前必须先打开文件,要想显式调用某个动态链接库提供的资源,首先要做的就是打开该库文件。打开库文件,其本质就是将库文件装载到内存中,为后续使用做准备。打开动态库文件需要借助 dlopen() 函数,其语法格式为:void *dlopen (const char *filename, int flag);
- filename 参数用于表明目标库文件的存储位置和库名;如果用户提供的是以 / 开头,即以绝对路径表示的文件名,则函数会前往该路径下查找库文件;反之,如果用户仅提供文件名,则该函数会依次前往 LD_LIBRARY_PATH 环境变量指定的目录、/etc/ld.so.cache 文件中指定的录、/usr/lib、/usr/lib64、/lib、/lib64 等默认搜索路径中查找。
- flag 参数的值有 2 种。RTLD_NOW:将库文件中所有的资源都载入内存。RTLD_LAZY:暂时不降库文件中的资源载入内存,使用时才载入。
(2)借助 dlsym() 函数可以获得指定函数在内存中的位置,其语法格式为:void *dlsym(void *handle, char *symbol);
- hanle 参数表示指向已打开库文件的指针;symbol 参数用于指定目标函数的函数名。
- 如果 dlsym() 函数成功找到指定函数,会返回一个指向该函数的指针;反之如果查找失败,函数会返回 NULL。
(3)和 dlopen() 相对地,借助 dlclose() 函数可以关闭已打开的动态链接库。该函数的语法格式如下:int dlclose (void *handle);
- 其中,handle 表示已打开的库文件指针。当函数返回 0 时,表示函数操作成功;反之,函数执行失败。
- 注意,调用 dlclose() 函数并不一定会将目标库彻底释放,它只会是目标库的引用计数减 1,当引用计数减为 0 时,库文件所占用的资源才会被彻底释放。
(4)dlerror() 函数可以获得最近一次 dlopen()、dlsym() 或者 dlclose() 函数操作失败的错误信息。该函数的语法格式如下:const char *dlerror(void);可以看到,该函数不需要传递任何参数。同时,如果函数返回 NULL,则表明最近一次操作执行成功。
这里举一个例子说明,代码如下所示。分析main.c 主程序,发现其中并没有引入 test.h 头文件,因为显式调用动态链接库时不需要它。在使用库文件中相关函数之前,先调用 dlopen() 函数打开库文件,然后通过 dlsym() 函数找到相关函数,最后调用 dlclose() 函数关闭库文件。
[root@bogon demo]# cat main.c
#include <stdio.h>
#include <dlfcn.h>
int main()
{
int m,n;
//打开库文件
void* handler = dlopen("libmymath.so",RTLD_LAZY);
if(dlerror() != NULL){
printf("%s",dlerror());
}
//获取库文件中的 add() 函数
int(*add)(int,int)=dlsym(handler,"add");
if(dlerror()!=NULL){
printf("%s",dlerror());
}
//获取库文件中的 sub() 函数
int(*sub)(int,int)=dlsym(handler,"sub");
if(dlerror()!=NULL){
printf("%s",dlerror());
}
//获取库文件中的 div() 函数
int(*div)(int,int)=dlsym(handler,"div");
if(dlerror()!=NULL){
printf("%s",dlerror());
}
//使用库文件中的函数实现相关功能
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
//关闭库文件
dlclose(handler);
return 0;
}
编好程序之后,通过执行如下指令,即可生成相应的可执行文件main.exe。注意,这里需要添加 -ldl 选项,因为该程序中用到了<dlfcn.h>
头文件,对应的动态库文件是libdl.so
,gcc 命令在编译 main.c 时必须用-ldl
指明这个库文件。
[root@bogon demo]# gcc main.c -ldl -o main.exe
[root@bogon demo]# ls
add.c div.c libmymath.so main.c main.exe sub.c test.h
五、GCC找不到库文件的解决方法
1、GCC生成可执行文件时找不到静态库文件
假设当前 mian.c 文件需要借助 libmymath.a 才能完成链接,则完成链接操作的 gcc 指令有以下 2 种写法。
[root@bogon demo]# gcc -static main.c libmymath.a -o main.exe #第一种写法
[root@bogon demo]# gcc -static main.c -lmymath -o main.exe #第二种写法
第一种写法完成链接操作时,GCC 编译器只会在当前目录中查找 libmymath.a 静态链接库。如果使用第一种方法完成链接操作,但 GCC 编译器提示找不到所需库文件,表明所用库文件并未存储在当前路径下,解决方案就是手动找到库文件并将其移至当前路径,然后重新执行链接操作。
第二种写法(使用 -l(小写的 L)选项指明要查找的静态库的文件名),GCC 编译器会按照如下顺序,依次到指定目录中查找所需库文件:
(1)如果 gcc 指令使用 -L 选项指定了查找路径,则 GCC 编译器会优先选择去该路径下查找所需要的库文件;
(2)再到 Linux 系统中 LIBRARY_PATH 环境变量指定的路径中搜索需要的库文件;
(3)最后到 GCC 编译器默认的搜索路径(比如/lib、/lib64、/usr/lib、/usr/lib64、/usr/local/lib、/usr/local/lib64 等,不同系统环境略有差异)中查找。
如果使用的是第二种方法,也遇到了 GCC 编译器提示未找到所需库文件,表明库文件的存储路径不对,解决方案有以下 3 种:
(1)手动找到该库文件,并在 gcc 指令中用 -L 选项明确指明其存储路径。比如 libmymath.a 静态库文件存储在 /usr 目录下,则完成链接操作的 gcc 指令应为gcc -static main.c -L/usr -lmymath -o main.exe
;
(2)将库文件的存储路径添加到 LIBRARY_PATH 环境变量中。仍以库文件存储在 /usr 目录下,则通过执行export LIBRARY_PATH=$LIBRARY_PATH:/usr
指令,即可将 /usr 目录添加到该环境变量中(此方式仅在当前命令行窗口中有效);
(3)将库文件移动到 GCC 编译器默认的搜索路径中。
2、GCC运行可执行文件时找不到动态库文件
当 GCC 编译器运行可执行文件时,会按照如下的路径顺序搜索所需的动态库文件:
(1)如果在生成可执行文件时,用户使用了-Wl,-rpath=dir
(其中 dir 表示要查找的具体路径,如果查找路径有多个,中间用 : 冒号分隔)选项指定动态库的搜索路径,则运行该文件时 GCC 会首先到指定的路径中查找所需的库文件;
(2)GCC 编译器会前往 LD_LIBRARY_PATH 环境变量指明的路径中查找所需的动态库文件;
(3)GCC 编译器会前往 /ect/ld.so.conf 文件中指定的搜索路径查找动态库文件;
(4)GCC 编译器会前往默认的搜索路径中(例如 /lib、/lib64、/usr/lib、/usr/lib64 等)中查找所需的动态库文件。
由上面的描述可知,即便将动态库文件和可执行文件放在同一目录下,当 GCC 编译器运行可执行文件时,也可能提示“找不到动态库”,因为动态库文件的当前路径并不在默认的搜索路径范围内。
因此,对于 GCC 运行可执行文件时提示找不到动态库文件的问题,常用的解决方法如下。
(1)将动态库文件的存储路径,添加到 LD_LIBRARY_PATH 环境变量中。假设动态库文件存储在 /usr 目录中,执行export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr
指令,即可实现此目的(此方式仅在当前命令行窗口中有效);
(2)修改动态库文件的存储路径,即将其移动至 GCC 编译器默认的搜索路径中。
(3)修改~/.bashrc 或 ~/.bash_profile 文件,即在文件最后一行添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx
(xxx 为动态库文件的绝对存储路径)。保存之后,执行 source .bashrc 指令(此方式仅对当前登陆用户有效)。
GCC 编译器提供有 ldd 指令,借助该指令,我们可以明确知道某个可执行文件需要哪些动态库文件做支撑、这些动态库文件是否已经找到、各个动态库文件的具体存储路径等信息。注意,如果某个动态库文件未找到,则 => 后面会显示 not found,表明 GCC 编译器无法找到该动态库,此时该可执行文件将无法执行。
以main.exe 可执行文件为例,执行如下 ldd 指令:
[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fff06fb3000)
libmymath.so => /lib64/libmymath.so (0x00007f65b2a62000)
libc.so.6 => /lib64/libc.so.6 (0x00000037e2c00000)
/lib64/ld-linux-x86-64.so.2 (0x00000037e2800000)