ps:本文中图片与内容部分源自《windows内核原理与实现》
物理地址、虚拟地址、逻辑地址
物理地址
物理地址是内存中的真实地址,如果把内存看作一个很大的数组,那么物理地址就是数组的索引(index)。物理地址是一个32位或者是64位的无符号整数(unsigned int32 / unsigned int64)
虚拟地址
虚拟地址又称为“线性地址”。每一个程序都有4GB(32位程序)的虚拟地址,从0到FFFFFFFF,其中低2GB到地址是程序领空,高2GB到地址是系统领空,但是也有程序领空为3GB和系统领空为1GB的情况。虚拟地址有一套规则转化(映射)成物理地址。
后文中,页式内存管理部分使用“虚拟地址”,段式内存管理部分使用“线性地址”。
逻辑地址
逻辑地址包含两个部分:段(segment)和 偏移(offset),段部分中包含了段基地址和段空间大小 以及其他属性。偏移部分有一个偏移量,偏移量不能超过段空间的边界。逻辑地址的值就是段基地址加上偏移量。逻辑地址也有一套规则映射为物理地址,过程中会产生线性地址为中间过渡地址。但是因为绝大多数段描述符的基地址都是0,所以数值上逻辑地址=线性地址。
其他
处理器(CPU)最终需要的是物理地址。虚拟地址是为了让每个程序具有独立性(或者叫做模板性)。
前面提到每个程序都有自己独立的4GB空间,但是每个程序的虚拟地址到物理地址的映射规则中参数不一样,所以可以几乎认为一个物理地址只对应到某一个程序的一个虚拟地址,保证了程序的独立性。此外虽说有4GB的独立虚拟空间,但是实际用到的虚拟地址很少,虚拟地址映射到的物理地址也就很少,所以可以多个程序同时运行。
如图:
内存管理模式
页式内存管理
在页式内存管理中,虚拟内存与物理内存都是按页(page)来管理的,两者的页面大小相同。因此可以存在恰到好处的一一映射关系,并且虚拟内存中的连续页,映射到物理内存中可以不连续。物理内存页是动态分配给虚拟内存页的,也就是上文提到的“可以多个程序同时运行”。
因为有了按页管理的制度,那么虚拟地址就可以分为两部分:页索引 和 页内偏移。
因为有多个页,所以把页也可以分组管理,所以又存在一个页目录。所以页索引又分为:页目录索引 和 页表索引。
最后的映射流程如下图:
解释一下,CR3寄存器指向了页目录首地址,由页目录索引范围知,一共有210个页目录项(PDE,Page Directory Entry),同理有210个页表项(PTE,Page Table Entry)。
找到对应的页目录项公式:页目录项 = CR3 + 4*页目录索引 (1 int = 4 byte)
找到对应的页表项公式:页表项 = 页目录项 + 4*页表索引 (1 int = 4 byte)
找到对应的物理地址公式:物理地址 = 页表项 + 4*页内偏移(1 int = 4 byte)
如果某一虚拟地址被映射过,那么就会被缓存,第二次访问的时候就无需再转译。缓存区域被称为 地址转译快查缓冲区(TLB Translation Look-aside Buffer)。
winxp查询存在于notepad内字符串的流程
1 | kd> !process 0 0 notepad.exe |
段式内存管理
在段式内存管理中,逻辑地址到线性地址的映射关系也是由索引表和偏移实现的,此外段式内存管理借助了一些段寄存器。段寄存器确定了段选择符(段选子,以下都成为段选择符),段选择符由:段索引、表指示位、当前特权级 组成。
此外还有一个段描述符,段描述符内有:段内最大偏移,基地址,以及其他属性,总共占64位
所有段描述符都储存在描述符表中。描述符表又分为 全局描述符表(GDT,Global Descriptor Table)和 局部描述符表(LDT,Local Descriptor Table)。
到这里就可以解释段选择符的内容了:表指示位决定了所用的描述符表是 GDT 还是 LDT、段索引用于在表中寻找目标段描述符、当前特权级一般只有0或3,0表示内核特权,3表示用户特权。
通过段选择符找到了目标段描述符,其中的 段基地址 加上 最开始的偏移 就是线性地址了。
现在终于能理解汇编语言是什么意思了
1 | mov eax,dword ptr fs:[30h] # 其中fs就是段寄存器(指向段选择符) 30h 就是偏移 |
其他
两种管理模式都花费了额外的内存空间去储存一些表项,这是典型的以空间换时间的例子。
windows现在主要是用页式内存管理,但是在某特定场合,段式内存管理是不可或缺的。
段式内存管理可以很好的实现多进程内存管理,比如对于不同进程,操作系统本身的代码段和数据都是全局可见的,可以通过GDT安排访问,而进程内部的代码段和数据是局部可见的,可以通过LDT安排访问,因此每次切换进程的时候只需要切换到对应的LDT即可,实现了进程之间的空间隔离和数据共享。
书中提到:“在实际执行指令过程中,每个段寄存器内部都有一个8 byte的缓存,存放了对应于段寄存器的段描述符,如果段寄存器没有改变,则可以跳过查段描述表的步骤,直接在处理器内部计算出线性地址。” 但是并没有给出段寄存器具体的说明,百度得到段寄存器的真正结构 8 byte的缓存就是指Base 和 Limit。
1 | struct Segment{ |
而段寄存器只会在,被修改的时候,查GDT缓存信息到隐藏的80位中,使用的时候直接使用缓存而不用去查GDT