目录
1、介绍动静态库
1、静态库:
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库,因此使用静态库生成的可执行程序的大小一般比较大。
- 静态链接是将需要的代码拷贝到程序当中
优点:
- 使用静态库生成可执行程序后,该可执行程序就可以独自运行,不再需要库了。
缺点:
- 使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码。
2、动态库:
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
优点:
- 节省磁盘空间,且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存当中不会存在重复代码。
缺点:
- 必须依赖动态库,否则无法运行。
3、示例:
#include<stdio.h> int main() { printf("hello Linux!\n"); return 0; }
这份代码我们可以通过printf输出hello Linux,注意原因是gcc编译器在生成可执行程序时,把C标准库也链接进来了,在Linux下,我们可以通过ldd文件名来查看一个可执行程序所依赖的库文件:
上述的libc.so.6就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6实际上是一个软连接:
实际上该软连接的源文件libc-2.17.so和libc.so.6在同一个目录下,为了进一步了解,我们可以通过file文件名命令查看libc-2.17.so的文件类型:
此时会发现,libc-2.17.so实际上就是一个共享的目标文件库,准确来说,这是一个动态库。
- 在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
- 在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。
这里可执行程序所依赖的libc.so.6实际上就是C动态库,当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字,如上库名就是c库。
而gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static选项:
[xzy@ecs-333953 date23]$ gcc -o myfile-s myfile.c -static
此时生成的可执行程序就是静态链接的了,可以明显发现静态链接生成的可执行程序的文件大小,比动态链接生成的可执行程序的文件大小要大得多。静态链接生成的可执行程序并不依赖其他库文件,此时当我们使用ldd 文件名命令查看该可执行程序所依赖的库文件时就会看到以下信息:
此外,当我们分别查看动静态链接生成的可执行程序的文件类型时,也可以看到它们分别是动态链接和静态链接的:
2、生成动静态库(设计)
下面演示动静态库的打包和使用时,都以下面的四个文件示例,其中两个头文件mymath.h和myprint.h,两个源文件mymath.c和myprint.c。
mymath.h的代码如下:
#pragma once #include<stdio.h> #include<assert.h> //[from, to] —》累加 —》result —》return extern int addToVal(int from, int to);
mymath.c的代码如下:
#include"mymath.h" int addToVal(int from, int to) { assert(from <= to); int result = 0; for (int i = from; i <= to; i++) { result += i; } return result; }
myprint.h的代码如下:
#pragma once #include<stdio.h> #include<time.h> //获取时间 extern void Print(const char* msg);
myprint.c的代码如下:
#include"myprint.h" void Print(const char* msg) { printf("%s : %lld\n", msg, (long long)time(NULL)); }
下面将演示打包和使用。
静态库的打包
下面就把这四个文件打包生成一个静态库:
第一步:让所有的源文件翻译成对应的.o文件
- 这里我们使用gcc -c命令把源文件生成对应的.o文件,这一步操作我们放在Makefile文件中:
至此,来看这样一个问题:链接就是把所有的.o文件链接形成一个可执行程序!可如果我把我的所有.o文件给别人,别人能链接使用吗?
答案是可以的,测试如下:
- 我在当前目录下创建一个test目录,并cd到test目录下,利用gcc -c把上级目录的两个源文件生成为.o目标文件到此test目录下:
- 现在自己创建一个test.c文件,内部代码如下:
#include"../myprint.h" int main() { Print("hello world"); return 0; }
- 现在需要把别人的.o文件和我自己写的.c文件结合生成可执行程序,执行如下命令:
- 综上,把我的.o文件给别人,别人可以链接使用。上述把三个.o文件拷贝合起来形成可执行程序就是一个静态链接的过程,但是上述操作很麻烦,.o文件太多了,用起来不方便,因此我们可以把.o文件打包生成一个静态库。下面转接第二部操作。
第二步:使用ar命令将所有目标文件打包为静态库
ar 命令是 gnu 的归档工具,常用于将目标文件打包为静态库,下面我们使用 ar 命令的 -r 选项和 -c 选项进行打包。
- -r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
- -c(create):建立静态库文件。
Makefile文件的实现如下:
libmymath.a:mymath.o myprint.o ar - rc libmymath.a mymath.o myprint.o mymath.o : mymath.c gcc - c mymath.c - o mymath.o - std = c99 myprint.o : myprint.c gcc - c myprint.c - o myprint.o .PHONY : clean clean : rm - f *.o *.a
此时就生成了静态库libmymath.a:
此时的libmymath.a静态库就相当于是 .c 源文件翻译成 .o 文件,并将这些.o文件打包集于一体的静态库。此外,我们可以使用 ar 命令的 -t 和 -v选项查看静态库中的文件。
- -t:列出静态库中的文件。
- -v(verbose):显示详细的信息。
[xzy@ecs-333953 mklib]$ ar -tv libmymath.a
第三步:将头文件和生成的静态库组织起来(发布)
- 当我们把自己的库给别人用的时候,实际上需要给别人两个文件夹,一个文件夹下面放的是一堆头文件的集合,另一个文件夹下面放的是所有的库文件。
- 因此,这里我们可以将 mymath.h 和 myprint.h 这俩头文件放到一个名为 include 的目录下,将生成的静态库文件 libmymath.a 放到一个名为 lib 的目录下,然后将这两个目录都放到 lib-static 下,此时就可以将 lib-static 给别人使用了。
Makefile实现如下:
libmymath.a : mymath.o myprint.o ar - rc libmymath.a mymath.o myprint.o mymath.o : mymath.c gcc - c mymath.c - o mymath.o - std = c99 myprint.o : myprint.c gcc - c myprint.c - o myprint.o .PHONY : static static : mkdir - p lib - static / lib mkdir - p lib - static / include cp * .a lib - static / lib cp * .h lib - static / include .PHONY : clean clean : rm - rf *.o *.a lib - static
- 测试如下:
- 此时会发现,成功将头文件和生成的动态库组织起来了,并可以将其给别人使用了,我们使用tree命令查看其内部相关信息:
- 至此,打包生成一个静态库已经完成,并可以将其发布给人使用了。
动态库的打包
动态库的打包相对于静态库来说有一点差别,但大致相同,我们还是利用这四个文件进行打包演示:
第一步:让所有源文件翻译成对应的.o文件 + 打包为动态库
此时用源文件生成目标文件时需要携带 -fPIC 选项:
- -fPIC(position independent code):产生位置无关码。
说明:
- -fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
- 如果不加-fPIC选项,则加载.so文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so文件代码段和数据段内存映射的位置。
- 不加-fPIC编译出来的.so是要在加载时根据加载到的位置再次重定位的,因为它里面的代码BBS位置无关代码。如果该.so文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码副本(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。
- 我们总是用-fPIC来生成.so,但从来不用-fPIC来生成.a。但是.so一样可以不用-fPIC选项进行编译,只是这样的.so必须要在加载到用户程序的地址空间时重定向所有表目。
生成好对应的 .o 目标文件,下面就需要将其打包为动态库了,与生成静态库不同的是,我们不再使用ar命令,而是使用gcc的-shared选项即可。
Makefile总体实现如下:
libmymath.so : mymath.o myprint.o gcc - shared - o libmymath.so mymath.o myprint.o mymath.o : mymath.c gcc - fPIC - c mymath.c - o mymath.o - std = c99 myprint.o : myprint.c gcc - fPIC - c myprint.c - o myprint.o .PHONY : clean clean : rm - f * .o * .so
- 此时就形成了动态库,下面执行第二步交互的过程:
第二步:将头文件和生成的动态库组织起来(发布)
- 与生成静态库时一样,为了方便别人使用,我们将 mymath.h 和 myprint.h 这俩头文件放到一个名为 include 的目录下,将生成的动态库文件 libmymath.so 放到一个名为 lib 的目录下,然后将这两个目录都放到 lib-dyl 下,此时就可以将 lib-dyl 给别人使用了。
Makefile内部实现如下:
libmymath.so : mymath.o myprint.o gcc - shared - o libmymath.so mymath.o myprint.o mymath.o : mymath.c gcc - fPIC - c mymath.c - o mymath.o - std = c99 myprint.o : myprint.c gcc - fPIC - c myprint.c - o myprint.o .PHONY : dyl dyl : mkdir - p lib - dyl / lib mkdir - p lib - dyl / include cp * .so lib - dyl / lib cp * .h lib - dyl / include .PHONY:clean clean : rm - rf *.o *.so lib-dyl
- 此时会发现,成功将头文件和生成的动态库组织起来了,并可以将其给别人使用了,我们使用tree命令查看其内部相关信息:
动静态库同时生成
如果我想让动静态库同时生成,只需要对Makefile做修改即可:
.PHONY:all all : libmymath.so libmymath.a libmymath.so : mymath.o myprint.o gcc - shared - o libmymath.so mymath.o myprint.o mymath.o : mymath.c gcc - fPIC - c mymath.c - o mymath.o - std = c99 myprint.o : myprint.c gcc - fPIC - c myprint.c - o myprint.o libmymath.a : mymath_s.o myprint_s.o ar - rc libmymath.a mymath_s.o myprint_s.o mymath_s.o : mymath.c gcc - c mymath.c - o mymath_s.o - std = c99 myprint_s.o : myprint.c gcc - c myprint.c - o myprint_s.o .PHONY : lib lib : mkdir - p lib - static / lib mkdir - p lib - static / include cp * .a lib - static / lib cp * .h lib - static / include mkdir - p lib - dyl / lib mkdir - p lib - dyl / include cp * .so lib - dyl / lib cp * .h lib - dyl / include .PHONY:clean clean : rm - rf * .o * .a * .so lib
这样就同时生成了lib-dyl的动态库和lib-static的静态库。
3、使用动静态库
静态库的使用
现在我test目录下就有我刚才打包号的静态库和一个新的mytest.c文件:
我们在mytest.c文件中输入以下程序来测试静态库:
#include"mymath.h" #include"myprint.h" int main() { int start = 0; int end = 100; int result = addToVal(start, end); printf("result: %d\n", result); Print("hello world"); return 0; }
如果针对上述程序直接进行编译,会报错:
报错的原因在于头文件的路径找不到,在解决此问题前,我们来看下头文件的搜索路径相关知识点:
- #include< >编译程序会先到标准函数库中调用文件。
- #include“ ”编译程序会先从当前目录中调用文件。
首先,我们自己写的这个静态库的头文件一定不在系统当中,其次,也不在当前路径下,所以找不到头文件的路径,自然会报错。解决办法如下:
①:将自己的头文件和库文件,拷贝到系统路径下即可!
- 系统当中的头文件一般都在 /usr/include/ 路径下,库在 /lib64/ 路径下。所以我们可以把自己的头文件和库文件拷贝到系统路径下(库的安装):
- 拷贝到系统路径后再次编译,发现还是报错,不过这就与之前不同了,现在是链接错误。 因为,这是第三方库(在这之前,我们用的都是c/c++的库,gcc/g++默认就认识c/c++的库),而链接第三方库必须要指定第三方库的名称。只需要在gcc编译时带上选项即可。
gcc -l(小写L) + 指明要链接的第三方库的名称:
- 库的名称就是去掉前缀lib,再去掉后缀.a
- 注意:不推荐使用这种方法,因为此法会污染系统的头文件和库。测试完后,一定要将该头文件和库删除(卸载):
②:指定搜索路径
这里同样需要用到gcc的选项(I和L):
- gcc -I(大写i)+ 头文件搜索路径
- gcc -L + 库搜索路径
[xzy@ecs-333953 test]$ gcc mytest.c -o mytest -I ./lib-static/include/
我们使用 gcc -I(大写i)选项指定了头文件的搜索路径,所以它变成了链接错误,因此我们加上 -L 选项,手动指定库文件搜索路径。
[xzy@ecs-333953 test]$ gcc mytest.c -o mytest -I ./lib-static/include/ -L ./lib-static/lib/
但是,我们发现依旧是错误的,因为我们仅仅指定了库的路径,而没有指定是这个库路径中的哪个库。因此我们又需要用到 -l(小写L),去指定链接的库。
[xzy@ecs-333953 test]$ gcc mytest.c -o mytest -I ./lib-static/include/ -L ./lib-static/lib/ -lmymath
综上,我们来总结下上述gcc三个选项对应的作用:
- -I(大写i):指定头文件搜索路径。
- -L:指定库文件搜索路径。
- -l(小写L):指明需要链接库文件路径下的哪一个库。
- -I,-L,-l这三个选项后面可以加空格,也可以不加空格。
如上,才是真正的使用了这个静态库。
动态库的使用
现在我test目录下就有我刚才打包号的动态库和一个新的mytest.c文件:
我们在mytest.c文件中还是输入以下程序来测试动态库:
#include"mymath.h" #include"myprint.h" int main() { int start = 0; int end = 100; int result = addToVal(start, end); printf("result: %d\n", result); Print("hello world"); return 0; }
说明一下,使用动态库的方法与上文使用静态库的方法一样,我们可以将头文件和库文件拷贝到系统目录下,然后仅使用 -l 选项指明需要链接的库名字来生成可执行程序,也可以使用 -I(大写i),-L,-l(小写L)这三个选项来生成可执行程序。下载我们用第二种方式做演示:
- 此时使用gcc编译mytest.c生成可执行程序时,需要用 -I(大写i)选项指定头文件搜索路径,用 -L 选项指定库文件搜索路径,最后使用 -l(小写L)选项指明需要链接库文件路径下的哪一个库。
[xzy@ecs-333953 test]$ gcc mytest.c -o mytest -I lib-dyl/include/ -L lib-dyl/lib/ -lmymath
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
需要注意的是,我们使用
-I
,-L
,-l
这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd
命令进行查看。
- 此时可以看到,可执行程序所依赖的动态库是没有被找到的。因为-I,-L,-l都是告诉编译器gcc信息的,然后形成了可执行程序,形成了之后,就与gcc没有关系了,这时运行该可执行程序时,就不知道该进程的库在哪里了,所以无法运行。
- 而静态库,在形成了可执行程序之后,已经把需要的代码拷贝进了我的代码中。这样运行时,就不需要依赖别的库了。(不需要运行时查找)
问:为什么动态库会找不到进程的库?
- 我形成的动态库libmymath.o,这本来就是一个文件,我自己的可执行程序被加载到内存时有一部分代码是需要跳转到库中运行的,所以必须要将你的库加载到内存中,再通过页表把库映射到堆栈之间(共享区),在自己的地址空间中,可以执行所有的代码(自己的和库的)
- 此时又创建了一个进程,它的代码也有一部分是用到库的,所以把库通过页表映射到自己的共享区里头,类似的,可以执行所有的代码(自己的和库的),此时系统里的库只有1份,如果是静态链接,每个进程都有一份代码,把库的代码编进自己的代码当中,所以库中的代码就会有n份
总结:
- 程序和动态库是分开加载的。动态库在运行期间,可以被多个进程所共享,所以也叫做共享库。动态库在加载的时候,需要先找到在哪里,就要先告诉操作系统路径在哪里,而操作系统只会去默认路径找,但是找不到,就需要我们告诉操作系统动态库在哪里。
解决的方式只需要让进程找到动态库即可,解决方式如下:
法①:将动态库拷贝到系统路径下/lib64
- 既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。
[xzy@ecs-333953 test]$ sudo cp lib-dyl/lib/libmymath.so /lib64
- 此时程序能够正常运行,不过不推荐这种方法,因为会污染系统头文件和库,所以测试完后要及时删除:
法②:通过导入环境变量的方式(LD_LIBRARY_PATH)
- 程序运行的时候,会在环境变量中查找自己需要的动态库路径 ---- LD_LIBRARY_PATH,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中即可。
[xzy@ecs-333953 test]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xzy/dir/date23/mklib/test/lib-dyl/lib/
- 此时我们再用 ldd 命令查看该可执行程序就会发现,系统现在可以找到该可执行程序所依赖的动态库了。
- 现在就可以正常运行该可执行程序了:
- 注意:如果关掉了xshell,再次进来时,导入的环境变量就会消失,需要重新导入了。
法③:系统配置文件
- 我们可以通过配置
/etc/ld.so.conf.d/
的方式解决该问题,/etc/ld.so.conf.d/
路径下存放的全部都是以.conf为后缀的配置文件:
- 这个路径表明的是系统里面,如果自定义了动态库,系统在扫描系统的路径时,除了在系统对应的库路径扫描对应的库之外,还会一个一个的去读取里面的配置文件,然后根据配置文件里的内容去找到对应的动态库。这个配置文件的内部起始就是一个路径:
- 我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。所以首先在此路径下(
/etc/ld.so.conf.d/
)创建一个以.conf为后缀的文件:
- 下面使用如下指令进入此创建的新文件:
[xzy@ecs-333953 test]$ sudo vim /etc/ld.so.conf.d/my-file.conf
- 下面将我动态库后缀名为.so的路径拷贝进此文件当中:
- 但此时我们用ldd命令查看可执行程序时,发现系统还是没有找到该可执行程序所依赖的动态库:
- 此时我们需要使用 ldconfig 命令将配置文件更新一下,将这个配置文件加载到内存空间里,更新之后系统就可以找到该可执行程序所依赖的动态库了。
- 此时再运行此可执行程序就可以正常跑了:
- 并且就算关闭了xshell再重新打开,也依旧不会消失。不用了就将其删掉:
法④:在系统中建立软链接
- 如上就是在系统中建立软链接的过程,建立好后,我们使用 ldd 命令查看该可执行程序就会发现,系统现在可以找到该可执行程序所依赖的动态库了。并且程序也可以正常运行:
- 一旦我们建立了软链接,那么我们重新编译代码的时候,只需要 -I(大写i)和 -l(小写L)即可找到对应的库。
[xzy@ecs-333953 test]$ gcc mytest.c -I lib-dyl/include/ -lmymath