目录
我以前写的一篇文章中就用网吧与在宿舍自己组装电脑解释了动静态库的区别,对其概念不清楚的可以看下面这篇文章。
Linux环境基础开发工具的使用(yum、vim、gcc、g++、gdb、make/Makefile)_linux gnu make之类-CSDN博客
动静态库的基本原理
我们都知道,一堆源文件和头文件最终变为一个可执行程序需要经历以下四个步骤:
例如,用test1.c、test2.c、test3.c、test4.c以及main1.c形成可执行文件,我们需要先得到各个文件的目标文件test1.o、test2.o、test3.o、test4.o以及main1.o,然后再将这写目标文件链接起来,最终形成一个可执行程序。
如果我们在另一个项目当中也需要用到test1.c、test2.c、test3.c、test4.c和项目的main2.c或者main3.c分别形成可执行程序,那么可执行程序生成的步骤也是一样的。
但实际上,我们对于经常用到的源文件,比如上面三次都用到的test1.c、test2.c、test3.c、test4.c我们可以将它们的目标文件test1.o、test2.o、test3.o、test4.o进行打包,之后需要用到这四个目标文件时就可以之间链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。
实际上,所有库本质都是一堆目标文件(xxx.o)的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的“半成品”。
认识动静态库
在Linux下创建文件编写以下代码,并生成可执行程序。
include
int main()
{
printf("hello linux\n");
return 0;
}
这是最简单的代码,运行结果大家也都知道,就是hello linux。
在这份代码当中,我们之所以可以通过调用printf来输出hello linux,主要原因是gcc/g++编译器在生成可执行程序时,将C标准库也链接进来了,也就是我们第一行#include 。
再Linux下,我们可以通过指令,查看我们某个可执行程序所依赖的库文件
ldd 可执行程序名
这其中的libc.so.6就是该可执行程序所依赖的从C标准库库文件。剩下的两个分别是由内核提供的虚拟共享库,用于优化用户空间程序与内核之间的交互。和 Linux 系统的动态链接器,负责在程序启动时加载和链接动态库。
我们通过ls命令可以发现libc.so.6实际上只是一个软链接。 根据软连接的知识,我们得知libc.so.6这个软连接的源文件是libc-2.31.so。
但实际上libc.so.6与libc-2.31.so是在同一目录下的,下面也给出了验证。
为了进一步了解,我们可以通过file 文件名命令来查看libc-2.31.so的文件类型。
此时我们可以看到,libc-2.31.so实际上就是一个共享的目标文件库,准确来说,这还是一个动态库。
在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。
在 Linux 系统中,动态库的命名通常遵循以下规则:lib<库名>.so.<主版本号>.<次版本号>.<修订号>
这里可执行程序所依赖的libc.so.6实际上就是C动态库,更准确的来说可执行程序实际上依赖的是C动态库的一个软连接。当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字,也就是c.6。
而gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static选项。
gcc -o test_s test.c -static
此时生成的可执行程序就是静态链接的了,可以明显发现静态链接生成的可执行程序的文件大小,比动态链接生成的可执行程序的文件大小要大得多。这也符合我们的猜想,也侧向证明了test是动态链接,test_s是静态链接。
静态链接生成的可执行程序并不依赖其他库文件,此时当我们使用ldd 文件名命令查看该可执行程序所依赖的库文件时就会看到以下信息。
此外,当我们分别查看动静态链接生成的可执行程序的文件类型时,也可以看到它们分别是动态链接和静态链接的。
动静态库各自的特征
静态库
动态库
本质区别
总结
动静态库与内存
静态库的加载方式
特点:
动态库的加载方式
特点:
加载到物理内存的细节
假设我们有一个程序 myprogram,它依赖于一个库 libmylib。
如果它依赖的是一个静态库,那么在编译的时候libmylib.a 的代码会被嵌入到 myprogram 中。
运行时myprogram 的代码和 libmylib.a 的代码一起加载到内存中。如果多个程序使用 libmylib.a,每个程序都会包含一份 libmylib.a 的代码。
但如果时动态库,libmylib.so 的代码不会被嵌入到 myprogram 中,而是记录依赖关系。
运行时操作系统会加载 libmylib.so 到内存中,并映射到 myprogram 的地址空间。如果多个程序使用 libmylib.so,它们可以共享同一份 libmylib.so 的代码。
静态库的打包与使用
为了更容易理解,下面自己实现一个动静态库的打包与使用,都以下面的四个文件为例,其中两个源文件add.c和sub.c,两个头文件add.h和sub.h。
pragma once
extern int my_add(int x, int y);
include "add.h"
int my_add(int x, int y)
{
return x + y;
}
pragma once
extern int my_sub(int x, int y);
include "sub.h"
int my_sub(int x, int y)
{
return x - y;
}
打包
gcc -c add.c
gcc -c sub.c
ar命令是gnu的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包。
-r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
-c(create):建立静态库文件。
ar -rc libcal.a add.o sub.o
此外,我们可以用ar命令的-t选项和-v选项查看静态库当中的文件。
-t:列出静态库中的文件。
-v(verbose):显示详细的信息。
当我们把自己的库提供给他人使用时,通常需要提供两个关键文件夹:
因此,在这里我们可以将add.h和sub.h这两个头文件放到一个名为include的目录下,将生成的静态库文件libcal.a放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mathlib给别人使用了。
执行一下指令
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./.h mathlib/include/
cp ./.a mathlib/lib
然后,tree的目录如下
当然,我们可以将上述所要执行的命令全部写到Makefile当中,后续当我们要生成静态库以及组织头文件和库文件时就可以一步到位了,不至于每次重新生成的时候都要敲这么多命令,这也体现了Makefile的强大。
代码如下:
myLib=libcal.a
CC=gcc
$(myLib): add.o sub.o
ar -rc -o $(myLib) $^
%.o: %.c
$(CC) -c $<
.PHONY: clean
clean:
rm -f $(myLib) ./*.o
.PHONY: output
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./.h mathlib/include
cp ./.a mathlib/lib
编写Makefile后,只需一个make就能生成所有源文件对应的目标文件进而生成静态库。
一个make output就能将头文件和静态库组织起来。
使用
创建源文件main.c,编写下面这段简单的程序来尝试使用我们打包好的静态库。
include
include // 引用我们刚才打包的静态库
int main()
{
int x = 20;
int y = 10;
int z = my_add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
现在该目录下就只有main.c和我们刚才打包好的静态库。
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I:指定头文件搜索路径。
-L:指定库文件搜索路径。
-l:指明需要链接库文件路径下的哪一个库。
输入下面这段指令
gcc main.c -I./mathlib/include -L./mathlib/lib -lcal
此时就可以成功使用我们自己打包的库文件并生成可执行程序。
补充一下:
因为编译器不知道你所包含的头文件add.h在哪里,所以需要指定头文件的搜索路径。
因为头文件add.h当中只有my_add函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。
实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
-I,-L,-l这三个选项后面可以加空格,也可以不加空格。
既然编译器找不到我们的头文件和库文件,那么我们直接将头文件和库文件拷贝到系统所默认的路径下不就行了。
指令:
sudo cp mathlib/include/* /usr/include/
sudo cp mathlib/lib/libcal.a /lib64/
需要注意的是,虽然已经将头文件和库文件拷贝到系统路径下,但当我们使用gcc编译main.c生成可执行程序时,还是需要指明需要链接库文件路径下的哪一个库。
指令:
gcc main.c -lcal
因为我们使用gcc(g++)编译的是C(C++)语言,而gcc就是用来编译C(C++)程序的,所以gcc编译的时候默认就找的是C(C++)库。
当你使用自定义库(如 libcal.a)时,编译器并不知道你需要链接哪个库。即使你指定了库文件的路径(使用 -L),链接器仍然需要知道具体的库名称。所以就要使用 -l 选项指定需要链接的库名称。链接器会根据 -l 指定的名称,在库文件搜索路径中查找对应的库文件。
小提示:
实际上我们拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染。
动态库的打包与使用
打包
动态库的打包相对于静态库来说略有不同,但是大致还是一个道理都是打包。同样我们还用刚才的代码案例,四个文件。
但是这一步相对于静态库多了一个选项,要用源文件生成目标文件时需要携带-fPIC选项:
-fPIC(position independent code):产生位置无关码。
指令:
gcc -fPIC -c add.c
gcc -fPIC -c sub.c
- -fPIC 的作用
- 为什么共享库需要 -fPIC
- 静态库不需要 -fPIC
- 不加 -fPIC 的共享库
与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。
指令:
gcc -shared -o libcal.so add.o sub.o
与生成静态库时一样,为了方便别人使用,在这里我们可以将add.h和sub.h这两个头文件放到一个名为include的目录下,将生成的动态库文件libcal.so放到一个名为lib的目录下,然后将这两个目录都放到mlib下,此时就可以将mlib给别人使用了。
指令:
mkdir -p mlib/include
mkdir -p mlib/lib
cp ./.h mlib/include/
cp ./.so mlib/lib
最后tree一下,这就是我们打包好的动态库
当然,生成动态库也可以将上述所要执行的命令全部写到Makefile当中,后续当我们要生成动态库以及组织头文件和库文件时就可以一步到位了。
编写Makefile后,只需一个make就能生成所有源文件对应的目标文件进而生成动态库。
一个make output就能将头文件和动态库组织起来。
使用
我们还是用刚才使用过的main.c来演示动态库的使用。
include
include // 引用我们刚才打包的动态库
int main()
{
int x = 20;
int y = 10;
int z = my_add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
同样该目录下就只有main.c和我们刚才打包好的动态库。
说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I,-L,-l这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l选项指明需要链接的库名字来生成可执行程序,下面我们仅以第一种方法为例进行演示。
此时使用gcc编译main.c生成可执行程序时,需要用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明需要链接库文件路径下的哪一个库。
指令:
gcc main.c -I./mlib/include -L./mlib/lib -lcal
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
需要注意的是,我们在编译阶段使用-I,-L,-l这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁。但是,在编译完成后,生成的可执行程序与编译器无关。此时就编译器的工作完成后就交付给操作系统了。操作系统的动态链接器(如 ld-linux.so)负责加载和运行程序。但是操作系统就说,你光告诉了编译器,现在你又让我去工作,但是你不告诉我去哪?干什么?我给你干个迪奥啊。
同样我们可以使用 ldd 指令查看一下。也确实操作系统确实没有找到所连接的动态库。
解决该问题的方法有以下三个:
方法 1:将动态库路径添加到 LD_LIBRARY_PATH:
LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中即可。
然后正常gcc,就可以运行了。
方法 2:将动态库复制到系统的默认库路径:
运行指令:
sudo cp mlib/lib/libcal.so /usr/lib
需要注意的是,不同的系统,默认库路径不同。我的是Ubuntu
删除:
sudo rm /lib64/libcal.so
查看是否删除成功:
ls /lib64 | grep libcal.so