静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候不再需要静态库。
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
动静态库的本质就是可执行程序的“半成品”
我们知道,程序的编译要经过四个阶段,预处理,编译,汇编,链接。在汇编之后就会生成对应的二进制文件,这个时候它的后缀是为".o",比如test.c文件,在编译之后会生成test.o文件。 在汇编之后生成的二进制文件还是不可执行的,要想执行它,必须再经过链接阶段。
对于频繁用到的许多源文件,我们可以先将它们生成对于的二进制目标文件,然后进行打包,这样做的好处是,之后需要用到这些文件的时候就可以直接链接这个包中的目标文件即可。打包之后,这些目标文件就可以称为一个库。
库的本质就是一堆二进制目标文件的集合,库的文件中不包含主函数而是包含了大量写好的方法以供调用。因此,我们可以通俗地说,动静态库是可执行程序的半成品。
我们平时写代码的时候,就常常用到了库。下面我们用C语言调用C库写一份最简单的代码。
在这串代码中,我们用printf函数打印"hello world!",我们用到的printf函数就是C语言的库函数,包含的头文件stdio.h其实也就是C语言的一个库。gcc编译器在生成可执行程序的时候,将C标准库链接进来了!平时,我们写程序的时候基本都用到了系统的库,也就是说,库无处不在!
我们将这份代码生成可执行程序
在命令行中,我们可以使用ldd命令查看可执行程序所依赖的动态链接库,ldd其实也就是 List Dynamic Dependencies 的缩写。
下面我们使用ldd命令查看刚才生成的可执行程序。
第一行和第三行我们暂时不考虑,我们就说第二行。
第二行表示我们在生成这个可执行程序的时候依赖"libc.so.6" 动态库,然后右边是它的路径"/lib64/libc.so.6" 。
在Linux中,以 .so 为后缀的是动态库,以 .a 为后缀的是静态库
去掉一个动静态库的前缀lib,和后缀 .so 或者 .a 以及后面的版本号,得到的就是这个库的名字。
比如libc.so.6,去掉前缀和后缀,得到的就是c,及c标准库。
gcc和g++编译器默认都是动态链接的,如果要进行静态链接,需要在编译时加上选项 -static
从这里也可以看到,静态链接生成的可执行程序比动态链接大的多。这是因为,静态链接是将所有的依赖库的代码和数据都复制到可执行文件中,而动态链接是在运行的时候从系统中加载所需的共享库。
ldd命令是查看依赖的动态库的,如果没有依赖动态库,就什么也查不到。
我们可以使用file命令来查看文件类型,进而可以看到可执行程序是静态链接还是动态链接的。
静态库是程序在编译阶段将库的代码和数据复制到可执行文件当中的,生成的可执行程序在运行的时候不再依赖库文件,但是生成的可执行程序非常大。
静态库的优点
使用静态库生成的可执行程序,不再依赖库了,它可以独自运行。
静态库的缺点
使用静态库可能会造成大量占用内存的问题,如果多个程序使用了相同的静态库的话,每一个程序都要在内存中加载一份相同的静态库代码,这样的话,内存中就存在了大量的重复代码!
程序如果依赖动态库,它会在程序运行的时候才去链接对应的动态库代码,如果多个程序使用同一份动态库,那么它们只用在内存中加载一份动态库代码。一个与动态库链接的可执行程序仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个代码。
在可执行程序运行之前,操作系统会将外部函数的代码由磁盘加载到内存中,这个过程就是动态链接。操作系统采用虚拟地址空间的机制允许物理内存中的一份动态库被多个进程使用,节省了内存空间和磁盘空间。
动态库的优点
节省磁盘和内存空间。可执行程序在磁盘上时,没有直接复制库的代码到文件中,节省磁盘空间。在内存中时,多个进程共享同一份代码,节省内存空间。平时我们生成的可执行程序一般都采用的动态链接。
动态库的缺点
必须依赖动态库,否则无法运行
下面我们尝试对一些目标文件进行打包和使用,其中用到了两个源文件add.c和sub.c以及两个头文件add.h和sub.h。
第一步:将所有的源文件生成目标文件
第二步:使用ar命令将目标文件打包为静态库
ar命令是Linux中用于创建和管理静态库的工具,下面我们要用到-r和-c选项其中 -r 选项表示如果静态库中的目标文件有更新,则用心的目标文件替换旧的目标文件,-c选项表示建立静态库文件。
将静态库和头文件组织起来
当我们将自己写好的库给别人时,实际上要给别人两个目录,一个目录存放头文件,一个目录存放库文件。
我们可以将头文件放在include目录下,将库文件放在lib目录下,然后就可以给别人用了。
g)
我们将打包好的目录拷贝到提前创建好的lib_test目录下,以便演示静态库是如何使用的。
下面写一段用到这个库的代码。
如果直接进行编译,当然是不行的,找不到我们刚才自己写的库文件和头文件。
如果用gcc编译的话,我们既要指明头文件的路径,也要指明库文件的路径,且要告诉gcc我们用的是指定路径下的哪一个库。但是头文件不用指明,原因是要用到哪个头文件在代码中就可以体现了,但是用到哪个库编译器是不知道的!
注意:我们在使用库的时候要去掉前缀和后缀!上面我们写的库文件去掉前缀和后缀之后就为mymath
使用库的另外一种方法,将库文件和头文件拷贝到系统路径下
我们在使用自己写的库的时候怎么这么麻烦?又是找头文件又是找库文件的,为什么我们使用系统自带的库文件时就不用?原因是gcc内置了系统自带库文件和头文件的路径,使用系统自带的库文件时,gcc会自己去指定路径下寻找库文件和头文件。
那么,我们将自己写的库文件和头文件拷到指定路径下,不也不用那么麻烦了吗?
需要注意的是,虽然不用指明库的路径,但是仍然需要指明库的名称。
为什么之前使用gcc编译的时候没有指明过名字?我们用的是gcc编译器,gcc就是编译C语言的,gcc编译的时候会默认去找C库,但是我们如果链接自己的库,编译器是不知道链接那个的,所以我们仍然需要 -l 选项来指明链接哪一个库。
其实,我们拷贝库文件和头文件到系统路径下的过程,就是按照库的过程。将库文件和头文件从系统路径下删除的过程,就是卸载的过程。最好不要将自己写的库文件和头文件放在系统路径下,否则会造成系统文件的污染。
第一步:生成二进制目标文件
如果要使用动态库,在生成二进制目标文件的时候一个加上 -fPIC选项,表示产生位置无关码的意思。
-fPIC用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确地执行,这正是共享库所要求的,共享库被加载时,在内存的位置是不固定的。
将目标文件打包为动态库
在打包动态库时,我们用的就不是ar命令了,ar命令是专门用来打包静态库的。打包动态库,我们用的还是gcc编译器,在使用时加上 -shared 即可。
将头文件和库文件组织起来
为了方便使用,我们也可以将上面打包的动态库文件和头文件组织起来。
和上面演示静态库的使用一样,我们将打包好的目录拷贝到提前创建好的lib_test目录下,以便演示动态库是如何使用的。
代码也还是同一份代码
编译依赖动态库的文件的方法和静态库是一样的,我们可以使用三个选项来进行编译。
当前,我们也可以将头文件和库文件拷贝到系统目录下,然后仅指明需要链接的库名字就能生成可执行程序了。
下面以第一种方法为例进行演示。
下面执行程序,会出现下面的报错。
这为啥就会报错呢?
我们虽然告诉了gcc我们要链接的文件在哪里,但是当可执行程序生成之后就与编译器没有关系了,也就是说,操作系统找不到该可执行程序所依赖的动态库在哪里。
我们使用ldd命令之后会出现这样的信息,表明这个库此时就是找不到的。
解决该问题的方法有三个
方法一:拷贝.so文件到系统共享路径下
在链接的时候,是操作系统去找要链接的库文件的,既然系统找不到我们的库文件,那么我们可以直接将库文件拷贝到共享的库路径下,这样系统就能够找到对应的库文件了。
方法二:更改LD_LIBRARY_PATH
LD_LIBRARY_PATH变量中存放了程序运行时查找库的路径,我们将动态库所在路径添加到这个变量中即可。
方法三:配置/etc/ld.so.conf.d/
/exc/ld.so.conf.d/ 路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后会在每个路径下查找所需要的库。我们若是将自己库文件的路径也放在该路径下,系统就能够找到我们的库文件了。
首先将库文件所在目录的路径存入一个以.conf为后缀的文件当中。
然后将该文件拷贝到上面所说的路径中
最后使用ldconfig命令将配置文件更新一些
这样程序就可以运行了。