Linux相关概念和易错知识点(22)(ELF格式、可执行程序的加载、共享库)

目录

1.ELF格式

(1)ELF格式是什么

(2)ELF文件的划分

(3)静态链接的本质

①静态链接

②动态链接

2.可执行程序的加载

(1)指令编址

(2)逻辑地址存在的意义

(3)CPU调用指令

(4)页表的初始化

3.共享库

(1)struct vm_area_struct

(2).GOT


1.ELF格式

格式是进一步了解链接过程的刚需,所以我们要先认识ELF格式,再理解动静态库,这会清晰很多

(1)ELF格式是什么

我们写的文件都有自己的存储结构,代码和数据要怎么分配空间?哪一部分空间存哪些数据?我们的可执行程序同样是在某种格式下进行数据存储的,这个格式称为ELF格式。不仅仅是可执行程序,.o、.so也是ELF格式

(2)ELF文件的划分

下面是section各个部分的主要功能的示意图。

我们因此知道,在可执行程序中就已经包含了进程地址空间各个段的大小划分,且这些地址都是绝对的。除此之外,如磁盘上的文件的物理地址,各个section的位置都是以偏移量的形式来保存的。我们能够推导出,进程地址空间的初始化和可执行程序的生成有着直接关系。

(3)静态链接的本质

①静态链接

.o和.so都是ELF格式的文件,静态链接就是将相关.o或.so文件中相同的属性的section进行合并(堆栈段、代码段合并),一个个小的ELF格式文件合并成一个大的ELF格式文件,形成可执行文件。我们因此能够解释为什么同一个程序,在不同文件中不能定义同名变量和函数;也能够解释为什么静态链接形成的可执行文件能够独立运行而不借助任何库。

静态库.a文件是ar格式(归档文件),不是ELF格式,但我们可以将.a看作.o文件的集合。当链接时,编译器会根据使用的函数去.a里面找被使用过的.o,这些.o的section都会被合并入可执行程序中,没有使用过的.o就不会合并。.o是最小合并单位,就算使用了一个函数,整个.o都会被合并,因为ELF文件是分节存储数据的,我们没有办法进一步分割。

②动态链接

对于动态链接的可执行程序来说,当.o和.so链接时,.o并没有直接将.so里面的section合并进来,函数方法的完整代码还在.so中保存着的,只有程序运行起来之后我们才能够获取函数的方法。

2.可执行程序的加载

接下来就是在ELF文件的基础上逐渐加深对可执行程序的认识了。

(1)指令编址

当可执行程序生成时,程序的指令存到了代码段对应的section中,这部分数据长什么样呢?

我们可以直接使用objdump -S查看可执行程序、.o、.so、.a的反汇编,程序编译完成后里面的每一行指令都有自己的地址

上面显示的是每一个数据节section的每一行指令的编址,这个地址叫逻辑地址。编址方式采用平坦模式,即逻辑地址 = 起始地址 + 偏移量,起始地址为0,所以逻辑地址 = 偏移量,从0x00000000->0xFFFFFFFF表示每一行指令的偏移量。事实上这个偏移量就等于虚拟地址。在ELF格式讲解中,我们查找Section Header Table中各个section的偏移量信息就是根据这里的逻辑地址来的,在Program Header Table里面查找的各个section划分的虚拟地址及其节的大小也来源于此。在读法上,一般在内存侧叫做虚拟地址,在磁盘侧叫做逻辑地址,两者之间还是有一定区别的,毕竟逻辑地址只是在起始地址为0时和虚拟地址数值上相等。

需要注意的是,我们通过反汇编查看到的正是section里面的内容,而并非section的属性。这意味着实际的每个section里面的存储结构都是一句指令配上一个逻辑地址,形成一种一一对应的存储结构。

(2)逻辑地址存在的意义

目前我们可以看出,当编译结束后,逻辑地址就确定了,每一行指令都要分配自己的地址,因为后续CPU调用都要根据地址来定位指令。所以编译时编译器就会遇到一个大难题,如何编址?如果没有逻辑地址的概念,那么编译器会很难办,因为内存的使用都是动态变化的,没有哪一块空间是每时每刻都空闲着的,如果给这个指令编址后,当运行程序时,这个地址被占用怎么办?因此,编译器采用平坦模式编址,直接全0到全F编址,编译器不仅能给每句指令编址而不发生冲突,还能和操作系统解耦。

(3)CPU调用指令

当程序运行时,CPU将调度指令执行。其中我们着重关注虚拟地址和指令在CPU中的调用流程。

寄存器EIP(也可以叫PC)负责接收虚拟地址,IR负责存储指令,CR3负责保存页表表头的物理地址,MMU是内存管理单元,一般和CR3协作进行虚拟地址和物理地址之间的转换。当PC读到虚拟地址后,会将这个地址交给MMU,MMU和CR3协助进行虚拟地址 -> 物理地址的转换(页表)。之后系统拿着转换后的物理地址将其对应的指令加载到IR中,以供CPU执行。执行完成后,CPU继续按顺序往下读取指令,由指令来控制跳转过程。

每一句指令都需要经历虚拟地址 -> 物理地址 -> 加载指令 -> 执行这个步骤的,调用函数的本质就是call函数的地址,也是通过虚拟地址来跳转的。

这个时候,我们还面临另一个问题:CPU执行的第一条语句的虚拟地址从何而来?答案就是ELF Header

通过上述流程,我们能够体会到虚拟地址其实是操作系统、编译器、CPU三者合作的产物。

(4)页表的初始化

当程序运行起来的时候,各个section会被加载到物理内存中。我前面已经说过,section里面的存储结构是一句指令配上一个逻辑地址,加载到内存中后也是如此。因此每一条被加载到物理内存的指令 + 逻辑地址的数据刚好就形成了物理地址和逻辑地址一一对应,页表也因此被初始化。

由于section偏移量及其大小属性的存在,程序加载时不一定就要加载数据,可以先根据section的偏移量初始化页表的虚拟地址,当执行时懒加载,当然也可以分批加载。有页表不一定有数据,但只要指令进入了内存,页表一定会更新该指令的映射关系

3.共享库

共享库(动态库)有个特点,就是多个存在于堆栈段的地址之间,起始地址互不相同,同时一个库能被多个进程使用(-fPIC的作用就是生成位置无关的代码,确保库中的代码可以在内存不同的位置加载)。这样一来,如何找到、调用这些动态库就成了一个难题。不过,有了前面的知识积累,我们也能尝试了解一下共享库的加载和调用。

(1)struct vm_area_struct

在mm_struct中保存有进程地址空间,但其实我们更需要维护、管理进程地址空间的结构。struct vm_area_struct就是其中之一,它存在于mm_struct中,以链表形式将进程地址空间每个段的start和end(起止位置的虚拟地址)保存了起来。也就是说,当系统想要初始化或者调整进程地址空间的划分结构时,会实时更新struct vm_area_struct里面的信息。我们也能拿着struct vm_area_struct对进程地址空间进行增删查改。

懒加载、分批加载需要struct vm_area_struct的帮助,它会帮我们进行段的位置的记录,让指令就算没有加载进来,每个段的地址范围也能确定,保证程序能够正常启动。

(2).GOT

动态库.so、可执行程序都是ELF格式的,里面每一句指令都有自己的逻辑地址,且整个ELF共用一套编址,从全0到全F,同一个文件里每个指令的编址都独一无二,这叫统一编址。有了统一编址,每个section的偏移量、大小也都唯一确定。

内存中的库可以有很多,因此当库加载到内存中后,会被操作系统用链表管理。除此之外,这个库会被映射到堆栈段之间,同时更新vm_area_struct,为它划分一块共享区。当调用这个库里面的函数时,会采用类似call libc.so:printf这样的调用方式。其中printf的逻辑地址(偏移量)早已确定,系统只需要修改libc.so为该库的起始地址,就能定位函数了。但注意call指令是在代码段,代码段是只读的。这就意味着我们需要专门用于函数调用的协助的section,也就是.GOT(拥有读写)。

调用库函数的基本逻辑是修改库所在位置的起始地址,加上库方法本来就有的逻辑地址定位。当CPU执行指令时,实际上得到的是call .GOT的地址:下标.GOT内存的有一张表,其结构为libc.so(库的起始位置):printf(库方法的偏移量)的一一映射。当执行call命令时,由于统一编址,.GOT的地址可以通过偏移量直接得到(本来也是固定的)。之后便去.GOT里面对应下标找到函数的偏移量,再修改库所在位置的起始地址,这样libc.so:printf -> 0x1111:0x2222,函数能够正常被调用了。

需要声明的是,section中有一些值是本来就存在的,上面只是为了直观替换成了函数名。如代码段中的call .GOT的地址:下标,.GOT的地址,下标都已经在编译完成后确定,保存在硬盘里。.GOT中的libc.so:printf中的printf偏移量也已经确定,只有libc.so需要替换为实际的库的起始地址。.GOT存在的意义就是解决代码段只读的问题,利用自己可读可写的权限让系统能够修改库的起始地址,正常调用函数。整个过程称为地址重定位。

综上所述:GOT + 函数偏移量协作实现了与库的调用与其在进程地址空间的位置无关,每个ELF都有.GOT。这使得无论库在哪,操作系统都能找到它并正常调用里面的函数,当地址重定位时直接到.GOT里面修改起始地址即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值