前言:在之前的博客介绍了进程地址空间,提到了地址空间是如何与物理内存相映射以及一些简单的运行机制,但在物理内存下,还存在与磁盘的交互,在磁盘中一个程序代码是如何存储的?又是怎么一步步加载到内存中?是谁在调度这一切?

虚拟地址与物理地址

虚拟地址是指计算机中程序所使用的地址空间,它是在程序执行过程中由程序产生的地址。在虚拟内存系统中,每个程序都有自己的虚拟地址空间,这使得每个程序都认为它是在独占地使用整个计算机内存。虚拟地址由操作系统和硬件共同管理,而程序中使用的地址都是虚拟地址。在程序执行时,虚拟地址会被映射到物理地址上。

物理地址是计算机内存中实际存储数据的地方。它是计算机内存芯片上的唯一标识位置,是硬件直接访问的地址。物理地址空间是实际存在于计算机硬件上的内存空间。操作系统通过使用内存管理单元(MMU)将虚拟地址映射到物理地址,从而实现虚拟内存的概念。

假设一个程序要访问地址0x00400000,这就是它的虚拟地址。在虚拟内存系统中,这个地址会被映射到计算机内存的另一个位置,比如物理地址0x0000A000。当程序试图访问虚拟地址0x00400000时,操作系统会通过MMU将它映射到对应的物理地址0x0000A000,从而实际上访问到了正确的内存位置。
这就是简单的虚拟与物理之间的映射,但在这之间还有些细节需要补充。

程序加载

加载前

首先需要明确的一点,程序在编译好后,就已经有地址的概念了。
而且这里的地址指的是逻辑地址。其实现在的逻辑地址和虚拟地址也没什么太大区别了。
当一个程序被存储到磁盘上时,它的内部分布通常是按照不同的段(例如代码段、数据段等)进行组织。其编址从0开始,这种组织方式通常被称为平坦模式(Flat Model)。在这个阶段,程序的不同部分被分配不同的段,每个段可以有自己的基地址。在编译阶段,程序中的符号(比如变量名、函数名等)被转换为相对于各自段基地址的偏移量,形成了虚拟地址。

加载到内存

每个程序都有一个entry,也就是入口地址。CPU通过拿到该程序的entry,就能够把程序加载到物理内存中,虽然我这里先提到的是物理内存和磁盘间的数据交互,但其实在系统内,先完成的是对虚拟地址空间的描述和组织。
当需要执行某个程序时,操作系统会负责将程序从磁盘加载到内存中。在程序的首部通常就会含有entry。有了entry,CPU就可以从entry往下顺序执行指令(例如mov,push等,这些指令也会占据物理内存空间)。
虚拟地址空间和物理内存是通过页表进行映射关联的。CPU执行某一个指令时,查询页表发现没有映射关系,就会引发缺页中断,然后去物理内存中找到该地址。这是之前就讲到过的。
不过还是在这里补充一下:

页表和缺页中断: 虚拟地址空间和物理内存之间的映射是通过页表完成的。当CPU执行某个指令时,如果发现对应的虚拟地址没有映射到物理地址,就会触发缺页中断。操作系统会处理这个中断,将对应的页面从磁盘加载到内存,更新页表,然后重新执行导致缺页中断的指令。

当执行到例如call指令时,就需要跳转到另一个地址接着执行,这个地址是编译时就形成的虚拟地址,很明显和虚拟地址空间里的虚拟地址不是同一个。
但操作系统有自己的办法,当程序从磁盘中被加载到物理内存,每行指令就有了自己的物理地址,只不过在内部存储了一个虚拟地址而已,通过其独有的物理地址,就能找到需要的虚拟地址(有时候也会是一个偏移量,下面会谈)。

动态库加载

静态库和动态库特点?

静态库在编译时被链接到程序,每个程序有独立副本;动态库在运行时加载到内存,多个程序可共享,节省内存,但需确保版本兼容性。

由上可知,静态库谈不上加载,是直接复制到程序中的,就算你删除了该静态库,先前被引入的程序也仍然可以使用该库的函数变量等,因为这些数据已经成为了该程序的一部分
而动态库不行,动态库是被多个程序所共享的。
动态库也被存储在磁盘上,当需要时,会被CPU调度进入物理内存,也同样的需要映射到一个虚拟内存地址上去。
但这里有一个问题:例如一个程序函数有printf,而这个函数依赖动态库liba,此时CPU执行到printf语句,就需要在这里通过该指令所蕴含的地址找到库里的printf函数实现。
例如该地址是0x1122,该放在虚拟内存的哪里呢?
之前讲过,代码段放在代码区,而动态库则放在共享区。而0x1122根据上文也可以推断,这是一个虚拟地址,CPU当然可以决定将这个printf实现放在0x1122,但如果这里被其他的数据占用了呢?
所以操作系统在这里并未将0x1122当做是一个虚拟地址,而是一个偏移量,也就是说,CPU将liba映射到共享区中,然后通过printf里的偏移量,与liba首地址相加(通常是相加操作),就能得到正确的虚拟地址。