深刻理解动静态库
要知道,如果我们的代码中使用到了静态库中的函数,那么main文件就需要和静态库进行连接,而静态库有很多,因此,在制作完成静态库之后,我们利用静态库编译main文件的时候,也要让编译器知道要链接的库在哪儿。当然,虚拟地址空间中也有分区,即规定,那一片区域是被用来分配给谁,所以每个vm_area_struct都属于其中一个分区,而映射动态库的vm_area_struct属于共享区。第一部分安装是为了让
库,说白了,就是已经写好的函数的代码,我们制作、使用库就是为了复用其中的代码
代码变成可执行程序
在讲解动静态库之前,我们最好回顾一下代码变成可执行程序的整个过程

这是单个文件经历的过程,可以发现链接库的行为应该是发生在“链接阶段。
静态库
静态库的制作

要打包的文件是.o文件,数量可以是多个
静态库的安装
要知道,如果我们的代码中使用到了静态库中的函数,那么main文件就需要和静态库进行连接,而静态库有很多,因此,在制作完成静态库之后,我们利用静态库编译main文件的时候,也要让编译器知道要链接的库在哪儿。
gcc或者g++编译器有默认的库头文件和库文件查找路径,我们可以在命令行中执行一些指令查看默认的查找路径(查一查AI就知道这些命令了):


所谓的静态库安装,就是将静态库的头文件和静态库拷贝或者移动到这些默认路径下。让编译器能够找到他们并使用。
为什么要在把头文件也安装进去,换句话说,为什么使用库的main文件还要包含库的头文件,直接链接不就好了?
- 实际上,main文件在编译的时候需要通过头文件来检查有没有语法错误,包括但不限于,你是否正确使用了库函数,有没有传错参数等。毕竟,头文件中有库函数的声明。
安装静态库的过程即是:

不过呢,gcc/g++编译器也提供给我们一些选项,让我们可以不用安装库而是编译时直接指定路径:

- -L选项指定库路径,.表示当前目录
- -I选项指定头文件路径,.表示当前目录
静态库的使用
如上文所示:

在编译的时候指定要使用的库就好了,唯一注意的是-l后面跟的不是库的名字(libadd.a)而是去掉lib和.a后的名字,即add。(-l是用来指定库的选项)
特别注意
静态库之所以被称为静态库,是因为静态库的本质就是在编译时将把静态库中的函数拷贝到main文件中,因此main文件才可以使用静态库中的函数。只要编译好了,main文件就是独立的,不再依赖静态库,这与我们下面所说的动态库有本质区别。
动态库
动态库的制作

唯一需要注意的就是动态库的命名格式:lib+name+.so
动态库的安装
与静态库不同,动态库的安装是分为两部分的:
- 第一部分安装是为了让编译器利用动态库编译main文件的时候能找到动态库的位置,这一点和静态库的安装完全一致,下面我们就不再多说。
- 第二部分安装则是为了让程序在跑起来的时候也能按照默认路径找到动态库并使用,因为使用动态库的main文件不会把动态库内容拷贝到自己的文件中,而是使用了一种运行时调用的机制。下面讲述的就是如何进行这种安装。
如果我们只实行第一部分安装会导致的结果:
- 编译能过,但是运行程序的时候,程序找不到动态库

下面我们讲述几种安装动态库以使程序运行时也可以找到库的方式:
- 把动态库安装到程序运行时的默认查找路径下:

- 在默认查找路径下建立一个动态库的软链接(其实和第一种差不多):

- 还可以设置环境变量,OS默认也会在环境变量LD_LIBRARY_PATH指明的路径下查找:

当然这只是暂时导入环境变量,要想永久修改环境变量需要修改环境变量导入相关的配置文件:
- 最后一种方法就是直接把动态库安装到系统路径下,所有程序都能在这儿找到动态库:
我们在ld.so.conf.d下创建一个.conf文件,然后把路径写入即可。
动态库的使用
使用的话,和静态库一样
特别注意

动态库的加载
这部分主要是想简略地说明一下可执行程序以及动态库是如何加载并被使用的
可执行文件的格式:ELF(Executeable and Linkable Format)
.o文件,库(.o文件的集合),可执行文件(链接后的.o文件)中的信息不是随便存放的,而是具有一定的格式:
- 现在我们可以明白了,所谓的可执行文件和库,不过是把若干个.o文件按照ELF格式拼接起来
可执行程序的加载

如上图所示:
- 可执行文件里的文本和数据等内容本身就有地址(可以叫做虚拟地址),从0开始编址(毕竟,指令需要操作数的地址)。
- 当文件被加载到内存后,这个可执行文件的信息可以用来初始化虚拟地址空间——mm_struct(例如用.text的起始虚拟地址是0x0,大小是10,那么虚拟地址空间中0-10的空间就被分配给.text的内容,这个空间分配的实体是vm_area_struct,下面会提到)。
- 由于文件中的文本和数据有了真实地址,因此我们可以通过文本以及数据的虚拟地址和真实地址的映射关系构建页表。
-
准备好后,系统首先把ELF Header 中的程序开始位置取出来,放在cpu的PC寄存器中,这个指针指向的是可执行文件需要执行的第一条命令(是虚拟地址)。
-
然后通过CR3(存放的是页表表头的地址)+MMU(硬件单元)将PC里的虚拟地址转换为实际地址,读取实际地址里存放的命令到cpu的EIP里,并把PC变为PC+EIP里面的命令的长度,继而开始执行这条命令(不难发现进入CPU的是虚拟地址,但是从CPU出来的都是真实地址)。
-
每执行一条指令后就从PC指向的位置取出下一条指令并更新PC的内容,这样程序就运转起来了。
动态库的加载
动态库同样需要加载到内存上来供可执行程序使用。当然,为了保证动态库可以灵活加载,动态库也是用虚拟地址编址+页表映射。这就需要在进程的虚拟地址空间中为这个库分配虚拟空间并在页表上建立映射。

vm_area_struct是分配虚拟空间的实体,start,end规定了加载的可执行程序或者库分配的虚拟空间的起始和结束位置。当然,虚拟地址空间中也有分区,即规定,那一片区域是被用来分配给谁,所以每个vm_area_struct都属于其中一个分区,而映射动态库的vm_area_struct属于共享区。
但动态库的虚拟地址也是从0x0开始编址,那么就要直接给他分配0x0-0xx的虚拟空间,可是可执行程序的文本和数据占用了这部分空间,这就发生地址空间分配冲突,除此之外,这样分配也不够灵活。所以给库分配地址空间用的不是本来库文件中的地址,而是给它分配共享区(堆区和栈区之间)中随意一块未被使用的空间。
这貌似没问题,我们只要构建页表的时候用新的虚拟地址(而不是0x0为首的虚拟地址)就可以,可真的是这样吗?
- 函数调用时用的虚拟地址是编译时就确定的,调用指令一般会被编成这样call 0x0(库起始地址)+ xx(函数的偏移量)。因此一但函数调用发生,程序就会跳转到代码区而不是共享区。
解决这个问题的方法也很容易想到:
- 当我们加载库的时候直接修改代码区的指令为call 库在共享区的起始虚拟地址 + xx。但这又引发一个问题,代码区是不可修改的!
不兜圈子,实际的解决方案是GOT,我们只需要像下面这样做:
- GOT是文件中的一个section区,里面存放的是一张表,表里存放的是库的起始虚拟地址和库中每个函数相对于库的偏移量:

- 编译时把函数调用执令编为call GOT的首地址 + 偏移量(这就是为什么要在编译形成.o文件的时候要加-fPIC!!!)
- 把GOT加载到内存里的可修改的区域,当库在进程中的虚拟地址确定下来后,就可以把表中的库的起始地址(libc.so)都改成真实的虚拟地址。
这样不需要改变代码区,就可以灵活的使用虚拟地址来调用库函数了。
GOT+偏移量 = 与地址无关(库调用与库在虚拟地址的那个地方无关)。
更多推荐


所有评论(0)