文章目录
1 库文件的简介
库文件,其等价为压缩包二进制文件
。该文件内部通常包含不止一个目标文件(也就是二进制文件),每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块。例如,C 语言库文件提供有大量的函数(如 scanf()、printf()、strlen() 等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类(如 string 字符串类)。库文件的产生,极大的提高了程序员的开发效率。因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。并且,库文件的调用方法也很简单,以 C 语言中的 printf() 输出函数为例,程序中只需引入 <stdio.h> 头文件,即可调用 printf() 函数。
1.1 库文件与头文件的联系
首先,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分
。即所有的库文件都提供有相应的头文件作为调用它的接口,库文件是无法直接使用的,只能通过头文件间接调用。
头文件+库文件
相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。
C或C++程序从源文件到生成可执行文件需经历 4 个阶段
,分别为预处理
、编译
、汇编
和链接
。链接阶段
所要完成的工作,是将同一项目中各源文件生成的目标文件(.o文件)
和程序中用到的库文件(.a 或 .so文件)
整合为一个可执行文件
。虽然库文件明确用于链接,但编译器提供了2种
实现链接的方式,分别称为静态链接
和动态链接
。采用静态链接方式
实现链接操作的库文件称为静态链接库
;采用动态链接方式
实现链接操作的库文件称为动态链接库
。
在
Linux 发行版系统
中,静态链接库
的后缀名通常用.a
表示,动态链接库
的后缀名通常用.so
表示;
在Windows 系统
中,静态链接库
的后缀名为.lib
,动态链接库
的后缀名为.dll
。
1.2 Linux系统
GCC 编译器
生成可执行文件时,默认情况下会优先使用动态链接库
实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库
,GCC 编译器才会选择相应的静态链接库
。如果两种都没有(或者 GCC 编译器未找到),则链接失败。
在
Linux 发行版
中,静态链接库
和动态链接库
通常存放在/usr/bin
或者/bin
目录下。
1.2.1 静态链接库.a
静态链接库
实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。
使用
静态库
的优势是可移植性强,可独立运行,即生成的可执行文件不再需要任何静态库文件的支持就可以独立运行。劣势
是代码冗余、可执行文件的体积大:如果程序文件中多次调用库中的同一功能模块,则该模块代码就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。和使用动态链接库
生成的可执行文件相比,静态链接库
生成的可执行文件的体积更大。
1.2.2 动态链接库.so
动态链接库
,又称为共享链接库
。采用动态链接库
实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。
显然,这样生成的可执行文件是无法独立运行的。采用
动态链接库
生成的可执行文件运行时,GCC 编译器会将对应的动态链接库
一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库
的支持下,也可以成功运行。
采用动态链接库
的优势是生成的执行文件文件体积小,因为可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库
称为共享链接库
的原因)。劣势
是可移植性差,无法独立运行,必须借助相应的库文件。
1.3 静态链接库.a的创建与调用
1.3.1 创建静态链接库.a
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
1.3.2 调用静态链接库.a
静态链接库
的使用很简单,就是在程序的链接阶段,将静态链接库
和目标文件
一起执行链接操作,从而生成可执行文件。
以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
注意:如果 GCC 编译器
提示无法找到 libmymath.a
(因为上面这种写法只会在当前目录查找libmymath.a
),还可以使用如下方式完成链接操作。其中 -L
选项用于向GCC 编译器
指明静态链接库的存储位置
; -l
(小写的 L)选项用于指明所需静态链接库的名称
,注意这里的名称指的是 xxx
部分(不带.a
尾缀),且建议将 -l
和xxx
直接连用(即 -lxxx
),中间不需有空格。
[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.4 动态链接库.so的创建与使用
1.4.1 创建动态链接库.so
使用源文件创建动态链接库
,其基本格式如下。其中,-shared
选项用于生成动态链接库
;-fpic
(还可写成 -fPIC
)选项的功能是,令 GCC 编译器
生成动态链接库
(多个目标文件的压缩包)时,表示各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址。
这样,无论将来动态链接库
被加载到内存的什么位置,都可以正常使用。另外,动态链接库
的命令规则和静态链接库
完全相同,只不过在 Linux 发行版系统
中其后缀名用 .so
表示。
gcc -fpic -shared 源文件名... -o 动态链接库名
例如,将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
1.4.2 调用动态链接库.so
动态链接库
的使用场景,就是与项目中其它源文件或目标文件一起参与链接生成可执行文件。以 demo 项目
为例,前面我们将add.c、sub.c 和 div.c
打包到了 libmymath.so
动态链接库中,此时该项目中仅剩 main.c
源程序文件,因此执行 demo 项目
也就演变成了将main.c 和 libmymath.so
进行链接,进而生成可执行文件
。
1.4.2.1 隐式调用.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:将
动态链接库
文件移动到Linux标准库目录
下,如/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
指令(此方式仅对当前登陆用户有效)。
1.4.2.2 显式调用.so
显式调用动态链接库
的过程,类似于使用 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
,则表明最近一次操作执行成功。
[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.c
主程序,发现其中并没有引入test.h
头文件,因为显式调用动态链接库时不需要它。在使用库文件中相关函数之前,先调用dlopen()
函数打开库文件,然后通过dlsym()
函数找到相关函数,最后调用dlclose()
函数关闭库文件。
编好程序之后,通过执行如下指令,即可生成相应的可执行文件main.exe
。注意,这里需要添加 -ldl
选项,因为该程序中用到了<dlfcn.h>
头文件,-ldl
对应的动态库文件
是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
2.1 GCC找不到库文件的解决方法
2.1.1 GCC生成可执行文件时找不到库文件
程序链接阶段指明所用库文件的方式有 2 种。假设当前 mian.c 文件需要借助 libmymath.a 才能完成链接,则完成链接操作的 gcc 指令有以下 2 种写法。
[root@bogon demo]# gcc -static main.c libmymath.a -o main.exe //第1种写法
[root@bogon demo]# gcc -static main.c -lmymath -o main.exe //第2种写法
第一种写法
完成链接操作时,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.1.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)
————————————————
版权声明:本文为CSDN博主「天糊土」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/oqqHuTu12345678/article/details/125083174