目录
前言
1.库的文件名
2.库的制作
对于动态库:
对于静态库:
3.库文件的查找
4.库的加载与使用
对于动态库:
对于静态库:
可执行程序分段:
程序的编址于动态库:
总结
前言
在刚开始学习C语言代码的时候,我们需要引入一个名为stdio.h的头文件,在头文件中为我们一些读写方法的声明,我们可以根据这些声明来使用接口实现对应的功能。但我们要知道,光有函数方法的声明是无法正常使用一个函数,还需要函数的具体定义,函数的定义就写在对应的库文件中。在本文中我们将介绍如何制作一个库,以及动静态库的区别。
1.库的文件名
在Linux中,无论是动态库还是静态库其文件名的前缀都是lib而后紧跟库的名称,最后用.so/.a表示该库是动态库还是静态库
lib[库名字].so //动态库
lib[库名字].a //静态库
举个例子,对于第一行中红色方框框住的库是一个动态库,库的名字是atomic
对于第二行中红色方框框住的库是一个静态库,库的名字是gcov
2.库的制作
//生成一个静态库
ar -rc [生成库的文件名] [依赖的文件]
//生成一个动态库
gcc -c -fPIC [依赖文件的源文件] //将库源文件编译成.o文件
gcc -shared [生成库的文件名] [依赖的文件] //链接库文件与自己编写的主函数文件
说明:
-fPIC选项:生成位置无关代码,位置无关是指当程序引用添加该选项生成的库文件时,不会因为该库文件加载到内存中的位置不同而影响库的功能。这使得多个程序可以同时引用该库,且平台可移植性更好
-shared选项:使用该选项生成的库文件有着即时更改的性质,意思是当库升级或由其他变动的时候无需将使用该库的程序重新编译,只需要将库重新生成即可。这样可以减少因库中有错误而重新编译生成可执行文件的时间。
简单实现两个数的加减操作的库:
这里分四个文件来进行演示,分别是Add函数定义的C++文件、Sub函数定义的C++文件、包含Add与Sub函数声明的头文件、包含主函数的文件。
对于动态库:
在制作库前,先复习这样一个问题:如何编译生成一个可执行文件。
首先我们要去掉注释,展开头文件、编译源文件生成.o文件、链接编译后的相关.o文件。也就是说我们使用的库其实也需要和我们写的程序一同完成上述的这些过程,但是在我们的本地IDE上所有的库本质上都是一个个完成编译后的.o文件集成的包,所以当我们自己写完的程序完成编译后,链接器直接链接链接库和我们的程序对象文件(.o文件)即可。
所以,如果我们想制作一个动态库,在完成对库源文件的编辑后,就必须要将我们自己写的库先编译成.o文件,然后与我们的主函数源文件一同编译链接即可。
说明:
对于静态库:
相比于动态库,静态库的制作显得更加简便,只需要使用ar工具进行编译打包即可,具体操作如图6上半部分所展示。
值得一提的是:
当库的名字一样时,当动静态库同时存在时,链接器会优先使用动态库,具体验证如图6。在图6上半部分中,只有静态库,那么此时链接器链接静态库,链接后的可执行程序文件属性在终端中显示。在图6下半,让动静态库同时存在,此时再次链接库,此时链接器使用动态库链接生成可执行文件,使用ldd命令可以查看可执行程序链接的动态库,在图6下半中,可以看到所连接的库名称。使用file命令可以查看可执行文件的链接方式。可以看到两个文件都是动态连接的。
当使用gcc命令编译链接程序入口文件时,可以添加“-static”选项,让链接器使用静态链接的方式链接库,此时优先链接静态库。如果不加该选项,那么链接器就是用动态链接的方式,优先链接动态库,若在存在动态库的前提下,选择链接静态库,那么会发生报错。
(以上结论基于Ubuntu20.04)
3.库文件的查找
在第二部分中,编译链接一个库的选项过于复杂,为什么我们使用语言的库就不需要这么大费周章的显示路径,告诉链接器要链接的库文件名呢?
这是因为,链接链接文件时会从系统的头文件默认保存下查找所需的头文件,而后从系统的库文件默认保存路径下查找库,我们自己写的库的头文件与库文件都不在这个对应的路径下,所以需要为链接器指明。此外,由于我们自己编写的库都会被认为成是一个外部库,对于链接外部库的可执行文件必须要指明链接库的名称。
通常情况下我们有四种方式来简化我们的链接指令:
①将我们自己编写的外部库放到系统存放库的文件夹中
②将我们自己编写外部库的路径设置进环境变量中
③利用软硬链接生成库入口文件,将该文件设置到系统存放库的文件夹中
④将自己编写的库路径信息内置到配置文件中
实现①:
在Ubuntu20.04下,存放库文件的路径是/lib,存放库文件的头文件的路径是/usr/include。在库路径下放置我们的库文件,在头文件目录下放置头文件。
实现②:
如果没有对应的LD_LIBRART_PATH环境变量则自己创建一个,如图9写入环境变量是内存级写入,虚拟机或云服务器关机后将会被恢复,如果想不被恢复就必须将环境变量设置进文件。
关于如何创建一个环境变量并将其设置进文件,请参考【Linux中的环境变量】
实现③:
关于软硬链接有需要的读者可以阅读【Linux文件系统(下)】
实现④:
4.库的加载与使用
对于动态库:
首先我们要明确一点的是,库也是文件,那么既然是文件就需要遵守文件系统的管理规则。库在没有被加载时会像其他文件一样被保存在磁盘上,(假设文件系统是ext系列文件系统),文件系统会为库分配内存块,inode表等。当库被链接,也就是库有读需求的时候,操作系统查看该库与inode的绑定关系,而后向磁盘发出访问申请,申请成功后,磁盘中的数据将会被加载到物理内存中,在物理内存中数据将以指针的指向的形式被指向,之后与进程地址空间中的共享区填充页表映关系。
这样库就可以被进程使用了,在该进程中的可执行性程序就可以对该库进行链接使用。
说明:
动态库与普通文件加载的不同之处:
①加载目的:
动态库:通常是要被多个文件进行只读使用的,因此动态库在底层设计时就被允许了多文件的访问与链接,来减少库在内存中重复加载,提高了内存的利用效率。
普通文件:通常是为了特定程序使用的,而且普通文件的读写需求是不确定的。因此多个程序对同一文件有读写需求就需要拷贝多个副本。
②加载方式:
动态库:通常需要操作系统使用对应的接口,将加载到内存中的库进行符号解析、代码重定位等操作。
普通文件:则不需要进行上述操作
③内存管理:
动态库:在一个进程中往往会有加载多个库,操作系统要对这些加载的库进行管理,记录这些库的使用情况。
普通文件:在内存中的管理往往都是由他所对应的程序完成的
④文件更新:
动态库:被更改时,正在运行的程序可能会因库中接口或库中逻辑的变化受到影响而发生崩溃。
普通文件:通常在内存中由一个副本,程序对文件的操作都是对副本的操作,只要不强制更新的文件文件副本与磁盘上的文件内容同步,程序仍然可以使用更新后的文件。
对于静态库:
静态库在编译链接后直接拷贝在程序的内部中,不同于动态库复杂的加载过程,它随着程序一同从磁盘中加载。这里就不过多介绍了。
可执行程序分段:
size [可执行程序名]
使用上述命令可以查看可执行程序各个段的大小
text(代码段):用来存储代码的区域,通常为只读状态。
data(数据段):用来存储已初始化的全局变量和静态变量。
bss:用来存储未初始化的全局变量和静态变量,这些变量在程序加载到内存时,会自动初始化为0。
dec、hex:分别是前三个段总大小的十进制表示和十六进制表示。
将程序中的数据进行分段存储可以提高程序的运行效率:
比如,当一个数据量庞大的程序要运行时,为了程序尽可能快的开始运行,操作系统会将先用到的数据加载到内存,暂时未用到的数据延后加载。
此外,代码分段还可以更好的提高程序的安全性:
比如,如果有对代码段进行改写的行为,那么这种行为将会被禁止,此外还可以对特定段进行加密,进而提升程序的安全性。
程序的编址于动态库:
对于任何可执行程序,实际上都已经被“地址化”了。
将可执行程序反汇编,可以看到,我们的程序中的每一条指令都被“地址化”,在可执行程序中,没有变量,只有寄存器和地址,当CPU执行我们的程序时,实际上就是至上而下的遍历地址并更改寄存器。对于动态库,库的内容会加载到该可执行程序所在的进程地址空间中的共享区,在反汇编文件中,我们可以得到每一个函数调用的地址,当程序要调用库中定义的函数时,CPU会在寄存器的帮助下,跳转到共享区中对应的部分,当CPU执行完函数体内的内容后,又会在寄存器的帮助下跳转回导致跳转的下一条指令所在的地址。