作者:爱写代码的刚子
时间:2023.11.21
前言:本篇博客将会介绍进程地址空间的深入理解。
继上一篇的动静态库,再次理解进程地址空间。
所以,动态库在系统中加载之后,会被所有进程共享,如何做到?(磁盘中的动态库在使用前先加载到物理内存,通过页表映射的方式映射到虚拟内存中的共享区)
结论:建立映射,从此往后,我们执行的任何代码,都是在我们的进程地址空间中进行执行。
事实:系统在运行中,一定会存在多个动态库 ——OS管理(先描述再组织),系统中所有库的加载情况,OS非常清楚
所以动态库也称为共享库,通过地址空间加自己的页表,将地址空间对应的库映射到自己的地址空间里。(如果存在全局变量,其中一个进程修改了它,则触发写实拷贝。数据一旦被多个人共享,它所在页的引用计数就会增加)
谈谈地址:
【问题】程序编译好之后,内部有地址的概念吗?
有的!(C++虚表)
程序在没有加载进内存的时候就已经被分成了很多段。(历史)
平坦模式(现在):按0~4GB将可执行程序编好,编译的时候就已经考虑了加载的问题。编译器也要考虑操作系统!
可执行程序在被编址的时候形成了**.code**(代码区)、.rdonly(只读数据区)、.data(已初始化的全局变量的一块内存区域)、.bss(存放程序中未初始化的或者初始值为0的全局变量的一块内存区域)已经是虚拟地址了,但是在可执行程序这里,程序还没有加载到内存,我们称这种地址为逻辑地址(磁盘当中我们程序形成时所对应的地址)。
【问题】:CPU怎么知道指令的地址?CPU读到的指令里面用的地址,是什么地址?
CPU内置了指令集(精简指令集和复杂指令集)
重要的指令都会有两套地址:
(1)内部采用的,加载到内存之前的逻辑地址
(2)加载到物理内存时所具备的物理地址
可执行程序在编译形成的时候,它已经在可执行程序的头部,写好了entry(入口地址),不是物理地址,是逻辑地址,在内存中就叫虚拟地址。
CPU内存在EIP寄存器/PC指针。 task_struct进程里面存在cwd工作目录,也能找到自己的可执行程序。(先加载内核数据结构)读取可执行程序时,先将入口地址load到EIP寄存器中 。(可能会存在缺页中断),(虚拟地址加载好后每一条指令都天然具备物理地址,虚拟到物理填入页表中,映射关系建立好(4kb级别)),由于每条指令有大小,于是可以通过偏移执行下一条指令。
CPU内读取到的指令,内部可能有数据,也可能有地址(函数调用,函数的地址(虚拟地址)),找到对应的虚拟地址,转成物理地址(通过MMU硬件),再指向物理内存中指令的地址。同理,若页表中找不到对应的物理地址,则会再次发生缺页中断。整个过程中,凡是我们读到的地址都是虚拟地址。(指令间:虚拟地址,找到指令:物理地址)
编译器编译考虑地址空间,进程创建时也考虑地址空间,两者协同重要的表现之一。
可执行程序在调用某一动态库的函数时,需要将动态库加载到内存并建立映射,通过页表映射到共享库。
【问题】:共享库大了,具体映射到哪里呢?这个函数是采用绝对编址吗?
动态库被加载到固定地址空间中的位置是不可能的,库可以在虚拟内存中的任意位置加载。操作系统在动态库加载到内存的时候,操作系统会记住libc.so:start(起始地址),因为操作系统要对我们的库做管理(先描述再组织),所以一个库加载到了地址空间的什么地址,操作系统也需要知道并做管理。所以之后做库函数调用的时候,一旦识别到了库函数,跳转到共享区,找到对应动态库的起始地址,通过起始地址加偏移量的方式找到该函数。所以库函数内部不采用绝对编址,只表示每个函数在库中的偏移量(lib_start+偏移量,编译代码的时候就以符号的形式进行了实例化(这一块很复杂,可深挖))
gcc在编译时,直接用偏移量进行对库中函数进行编址
【问题】:静态库为什么不谈加载,不谈与位置无关?
静态库直接将代码拷贝到程序,静态库在进行编址的时候直接按照平坦模式,0~4GB,绝对编址按线性编好,所以不谈与位置无关码,静态库的函数地址是确定的。