hello,各位小伙伴,本篇文章跟大家一起学习《Linux:ELF加载_动态库_进程通信》,感谢大家对我上一篇的支持,如有什么问题,还请多多指教 !
如果本篇文章对你有帮助,还请各位点点赞!!!
话不多说,开始正题:
文章目录
- 可执行程序格式
- 重谈地址空间 ---- 可执行程序,加载
- CPU在执行的时候,使用的是什么地址?
- 重谈区域划分
- 动态库加载的理解
- 真实做法
可执行程序格式
下图是可执行程序main
:
下图是可执行程序的格式ELF
格式:
main
的text、data、bss……
就是上图的Section
不仅仅是我们的可执行程序是这个格式,我们的库(动静态库)、目标文件(.o
文件)都是这样的格式的!
.o
文件的Section
都有自己的text、data……
动静态库实际上就是所有的.o
文件链接起来,所谓的链接其实就是将我们一个个相同属性的Section
进行合并!
合并最终变成一个大的ELF
,就是可执行程序
对于ELF Header
是ELF
表头,Program Header Table optional
是程序的表头,Section
等等也有自己的大小,每一个ELF
都不一样,那是如何分配好空间的呢?
对于任何一个文件,文件的内容就是一个巨大的“一维数组”,标识文件任何一个区域,采用偏移量+大小的方式,所以我们只需要知道偏移量就可以了,大小在文件属性里有
重谈地址空间 ---- 可执行程序,加载
mm_struct
是由谁来初始化的?- 可执行程序有没有地址?
先谈第二个问题,可执行程序是有地址的,我们反汇编一下:
objdump -d main
可以看到每一条指令都是有地址的,再加上每一条指令都有自己的长度,所以下一条指令的地址减去上一条指令的地址,就是上一条指令的长度!从另一个角度来讲:当前地址+当前指令长度 = 下一条指令的地址
这种地址叫做逻辑地址(逻辑地址 = 起始地址 + 偏移量),在ELF
,没有加载到内存时就已经形成的了,并且都是按照[0000……00,FFFF……FF]进行编址,没错,这实际上就是虚拟地址
所以,编译器编译,就已经形成了虚拟地址
那么第一个问题也就解决了,mm_struct
的初始化是从磁盘中的可执行程序ELF
中得来的!
在CPU里有一个指针叫做pc
指针,指向的是下一条指令的地址,那么CPU如何获取main
函数的地址的呢?
实际上main
函数什么都交代了,连程序入口地址也说明了
CPU在执行的时候,使用的是什么地址?
答案是:虚拟地址
没错,CPU也是读取虚拟地址的,那是怎么实现的?
在磁盘里存放着可执行程序的ELF
格式,虚拟地址加上指令:
加载到物理内存:
物理内存也有地址,也就是物理地址,这样物理地址有了、虚拟地址也有了,页表就完善了:
这样CPU读到虚拟地址再通过MMU硬件
和CR3
进行查表,就可以实现虚拟内存到物理内存的转换!!
对于MMU硬件
是通过硬件电路进行查表的,CR3
指向的是页表的起始地址,这里的起始地址是物理地址!
在CPU里还有一个EIP
,是用来读取物理地址的指令的:
当pc
将虚拟地址给CPU查表后,得到物理地址,通过EIP
读取并且解析该物理地址上的指令长度,那么pc
就可以在原先虚拟地址加上指令长度得到下一条指令的地址,如此循环往复,程序就跑起来了!
总结:
虚拟地址空间是操作系统,CPU,编译器共同协作下的产物!!
所以为什么要有虚拟地址和虚拟空间?
有了之后,编译器就再也不用考虑物理地址了,操作系统就能够以线性角度去看待程序,实现了操作系统与物理内存的解耦!
重谈区域划分
为什么mm_struct
能够帮助我们形成虚拟内存,但是,当数据虚拟内存中时,是怎么控制的?任意加载吗?很明显不可能,这样虚拟空间会非常乱!
其实mm_struct
并非这么简单,里面有着一个结构体struct vm_area_struct
,这个结构体里存放着struct vm_area_struct*
指针,用来指向下一个struct vm_area_struct
,该结构体里存放着start、end
用来实现空间的开辟,实际上这是用链表的形式来维护的,从struct vm_area_struct*
指针就可以得出结论
所以啊,假设你加载一个动态库,只需要创建一个struct vm_area_struct
节点维护即可,MMU硬件
所谓的查表,实际上就是查找你所寻找的在哪一个节点
动态库加载的理解
对于库,我们也可以认为是基地址 + 偏移量
那么库里的方法本质就是偏移量!
动态库加载到内存中也是需要被操作系统管理的,也是先描述再组织!同样是在mm_struct
里进行管理,通过页表进行映射关系
将正文执行到库里的某个方法的时候call libc.so:printf
,直接将libc.so
虚拟地址转换为物理地址,此时printf
就会转变为偏移量0xXXX
而不再是printf
问题来了,代码区不是只读的吗?
真实做法
对于动态库,ELF
里还有一张表叫做.GOT
:
这张表加载到物理内存里,正文代码区只需要看其物理地址,也就是说正文区实际上放的就是物理地址,为并非虚拟地址,物理地址存放着.GOT
这张表,这就能够转到虚拟地址,然后在mm_struct
共享区找到方法,就不用改正文代码区
.GOT
+ 库方法偏移量 = 与地址无关