进程地址空间二讲:程序是如何加载的?动态库又是如何加载的?
文章目录
- 动态库加载原理
- 再谈进程地址空间
- 程序没有加载前的地址
- ELF文件
- 程序加载后的地址
- 动态库的地址
- fPIC的原理
动态库加载原理
动态库在进程运行的时候是要被加载的(静态库不用)
动态库可以被多个进程加载使用,因此动态库又被称为共享库。
所以,动态库在系统中被加载后,会被所有进程共享。
怎么做到的呢?
实际上,动态库加载到系统中后,一般会被保存在所有进程的进程地址空间的共享区中。
看图:
得出结论:动态库在虚拟地址的地址在对应的物理内存是通过页表建立映射的,也就是说,我们执行的任何代码都是在进程地址空间中执行的。
事实上,在系统·运行中,一定会存在多个动态库,那么OS就需要将他们管理起来:先描述,再组织。
如此,系统对所有库的加载情况,是非常清楚的。
一个小细节:对于共享库中的全局变量,如果进程对该变量进行修改,会影响其他进程的该变量吗??
当然不会,共享库的数据和我们父子进程的数据是一样的,任意一个进程对其进行修改,会发生写时拷贝。
再谈进程地址空间
我们换个角度再来谈谈,什么是虚拟地址?什么是物理地址?
程序没有加载前的地址
程序在编译后,内部有地址的概念吗?
其实是有的
ELF文件
要理解编译链接的细节,我们不得不了解⼀下ELF⽂件。
有以下四种⽂件都是ELF⽂件:
可重定位⽂件(Relocatable File)
:即xxx.o目标文件。可执行文件(Executable File)
:即可执行程序。共享⽬标⽂件(Shared Object File)
:即xxx.so动态库⽂件。内核转储(core dumps)
:存放当前进程的执⾏上下⽂,⽤于dump信号触发。
⼀个ELF文件由以下四部分组成:
ELF头(ELF header)
:描述⽂件的主要特性。其位于⽂件的开始位置,它的主要⽬的是定位⽂件的其他部分。程序头表(Program header table)
:列举了所有有效的段(segments)和他们的属性。表⾥记着每个段的开始的位置和位移(offset)、⻓度,毕竟这些段,都是紧密的放在⼆进制⽂件中,需要段表的描述信息,才能把他们每个段分割开。节头表(Section header table)
:包含对节(sections)的描述。节(Section )
:ELF⽂件中的基本组成单位,包含了特定类型的数据。ELF⽂件的各种信息和数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。
最常⻅的节:代码节(.text)
:⽤于保存机器指令,是程序的主要执⾏部分。数据节(.data)
:保存已初始化的全局变量和局部静态变量。
程序编译后就是一个ELF文件了(XXX.o),ELF程序在没有被加载到内存的时候,本来就有地址,当代计算机⼯作的时候,都采⽤"平坦模式"进⾏⼯作。所以也要求ELF对⾃⼰的代码和数据进⾏统⼀编址。
“平坦模式”
由低到高顺序编址
以下是一个程序的反汇编:
左侧的就是虚拟地址,右侧对应的是指令。
CPU中有着对应的指令集,我们复杂的程序操作,对于CPU来说就是大量的一个一个简单的指令,然后快速的执行它们。
结论:可执行程序在加载到内存之前,它的代码、数据、指令、函数调用,就已经按我们的ELF的格式(地址+二进制指令集)编好了,当调用函数需要跳转时,会提供跳转地址,因为每一条代码都有自己的地址,这个地址就是虚拟地址,又称逻辑地址。
程序加载后的地址
当我们把可执行程序加载到内存中后,我们可执行程序的每条指令都天然的有了各自的物理地址。
所以,可执行程序加载到内存后,里面的指令将会有两套地址:虚拟地址和物理地址,并且它们两是一一对应的!
可执行程序的第一条指令是如何执行的呢?
我们知道,ELF文件格式是有表头的,在可执行程序中表头是entry:入口地址
,这个入口地址是什么地址?毫无疑问,是虚拟地址。
起初,可执行程序是没有加载到内存的,我们进程通过cwd和exe找到自己的可执行程序,然后将可执行程序的entry地址喂给CPU的寄存器EIP(又称pc指针),然后系统会从页表通过entry的虚拟地址试图去获取其物理地址,但此时可执行程序并没有加载到内存,所有entry没有物理地址,发生缺页中断,此时系统就会立马将可执行程序加载到内存,并将其指令一一对应的物理地址和虚拟地址去初始化页表,建立好映射关系。
所以,CPU内部读取到的指令,内部可能是数据,也可能是地址(都是是虚拟地址)。
一图胜千言:
动态库的地址
动态库的地址其实还是别具一格的,它采用的并非常规地址(绝对地址,以0地址为参照),而是采用逻辑地址(相对地址,以库头的地址为参照)。
注意:库头的地址是绝对地址。
为什么要这样设计呢??
我们知道,动态库一般是加载到共享区的,但是具体映射到哪呢?每次加载都是固定位置吗?这是不可能的!
可执行程序编译后,其中调用库函数的call指令对应的共享库中的库函数地址却是固定不变的,也就是说,如果采用绝对地址,动态库每次加载到固定位置是不可避免的。
解决方案:
但是动态库内部采用相对地址完美解决了这个问题:我们只需要知道库头的地址(随便库头存在哪个位置),我们调用库时,只需根据库头地址定位库头,然后根据相对地址这个偏移量找到目标位置即可。
所以,动态库需要让自己内部函数不要采用绝对编址,只表示每个函数在库中的偏移量即可。
fPIC的原理
以上这就是动态库链接时fPIC选项的内部原理
现在我们就知道“fPIC:产生位置无关码”是什么意思了,就是自己用偏移量对库中函数进行编址。
至于静态库:它无需加载,更无需产生位置无关码了。