页式管理是重中之重!
在段式管理下操作系统的运作出现了很多问题,因为段的长度不定,在分配内存时,可能会发生内存中的空闲区域小于要加载的段,或者空闲区域远远大于要加载的段,这样一通分来分去最后会导致剩下一些内存碎片,也就是可以的内存还有但是都很小而且地址空间不连续,导致无法再继续利用了。为了解决这个问题,从80386 处理器开始,引入了分页机制。
概述:
分页功能从总体上说,是用长度固定的页来代替长度不一定的段, 来解决因段长度不同而带来的内存空间管理问题。
因此,第1 个页的物理地址是0x00000000,第2 个页的物理地 址是0x00001000,第3 个页的物理地址是0x00002000,……,最后一个 页 的 物 理 地 址 是 0xFFFFF000 。 这 样 , 可 以 将 4GB 内 存 划 分 为 1048576(0x100000)个页。
段管理机制对于Intel 处理器来说是最基本的,任何时候都无法关闭。即使启用页管理功能,分段机制依然是起作用的,段部件也依然工作,所以当开启页式管理时,intel 实际上采用的是段页式管理。
段页式管理机制的主要功能:当一个程序要加载调用时操作系统先在虚拟内存中按照段分配空间,然后再根据页大小将虚拟内存地址中的段映射到物理地址空间中。
(注:通常在intel中开启页式管理后线性地址就和线性虚拟内存地址是一样的,这个是个叫法问题,为了方便,然后线性物理内存地址就通常叫物理地址)
段页式管理中的任务执行流程:
前面阐述过段式管理中的任务执行流程,其实就比他多了个按页的内存映射,然后最后实际内存安装页为单位而不是段为单位来执行。
首先将可执行文件的内容按照段映射到虚拟内存中,然后再按照页映射到物理内存中。最后CPU再通过物理内存进行实际性的操作。(如果要用到其他文件就将其加载到数据段中进行处理就行了)。
简单的段页式模型:
假设在某个任务里操作系统给该任务的某个段分配了一段虚拟内存空间,基地址为0x00200000,长度为8200 字节:
页的最小尺寸是4KB,也就是4096 字节。因此,8200 字节的段, 需要占用3 个页面,所以操作系统就将其映射到三个空闲的物理内存地址页面:
这里的任务以段的方式映射到虚拟内存中采用的是段式管理,然后将段按照页分割再映射到内存页上采用的是页式管理,两个合成段页式管理。
为了根据线性地址 找到页的物理地址,操作系统必须维护一张表,把线性地址转换成物理地址。
因为有1048576 个页,所以转换表也有1048576项。这是个一维表格,每个表项占4 字节,存储的内容为页的物理地址。
因为页的标准尺寸是4KB,所以将线性地址的低12用来作为页的偏移地址,将高20位用来作为在映射表中的索引,假设映射表如下:
然后这个时候我们执行指令:
mov edx,[0x2002]
这条指令首先会通过段部件将其转换为线性地址:
;访问内存 ds:0x2002的值然后赋值给edx
;这里的ds前面给出了基地址0x00200000
;所以就是访问虚拟地址为 0x00200000:0x2002的值
;也就是虚拟地址为:0x00202002的值
然后通过页部件转化为物理地址:
; 0x00202002可分为:0x00202 002
; 其中高20位内容为0x00202,低12位内容为0x002
; 将高20位的值0x00202 * 4 = 0x00808得到映射表中的索引
; 然后通过映射表得到页物理地址的起始地址0x00007000
; 然后再通过偏移0x002,得到物理地址:0x00007002
最后对0x00007002物理地址取四字节内容赋值给edx。
(注:映射表是操作系统自己来处理,自己来控制映射表中的内容,无需程序员操心)。
每个任务都有一个自己的映射表,所以不需要考虑有多对一产生冲突的情况:
但是就算这样处理了,当任务很多的时候4GB的物理内存页不够用,这个时候就和段式内存管理一样,操作系统会利用硬盘来作为缓冲区,进行页面置换,将暂时不用的页放到硬盘中,然后要用的时候再置换回来。
80386的分页机制(经典分页机制):
第一个支持分页内存管理模式的Intel 处理器是80386,虽然该CPU比较古老了,但是后面的CPU都会兼容和参考该CPU的内容,比如80386的分页机制。
重点概念:页目录表(Page Directory Table,PDT)、页表(Page Table),页目录项(Page Directory Entry PDE)和页表项(Page Table Entry PTE)
前面那个段页式模型中的页式管理只能拿来参考,因为一个页映射表有 0x4*0xFFFFF = 4MB的大小,而且每个任务都有这么多个,这样来说对于内存的消耗太大了,并且在实践中并没有任务会真正的用到页映射表中的所有内容,所以采用了页目录表和页表来代替页映射表。
80386CPU采用了一个4KB大小的页表来存放1024个页的地址和信息合成的结构体叫做页表项,每个页表项有4个字节,用来存放页的物理内存和属性,一个4KB大小的页表正好和页的大小相等,所以可以很好的保存到页里,由于一个任务最多对应0xFFFFF个页,所以最多可以有1024个页表。
然后采用了一个4KB大小的页目录表来存放1024个页表的地址和属性结合成的结构体叫做页目录项,每个页目录项的大小又是4字节,所以一个页目录表也有1024*4 = 4KB字节,也正好对应了一个标准页的大小。
(其实就是数组的概念)
页表和页目录表的最大大小为4KB+4KB*1024 = 4KB+4MB,虽然看起来比页映射表大,但是页表和页目录表也是用页来管理和正常的页一样,也会被回收和重新分配。
至于页目录和页表的内容是如何添加的,这就是操作系统来处理的了。
这样的分页结构体体系是每个任务都有的,所以每个任务都有自己的页表和页目录表,其中在CPU内部有一个CR3寄存器,来专门存放页目录表的基地址。
而每个任务都有自己的TSS段,TSS段中就有CR3字段,所以就可以通过任务的tss得到CR3再通过CR3来得到页表和页目录表再得到物理内存。
当任务切换时,处理器切换到新任务开 始执行,而CR3 寄存器的内容也被更新,以指向新任务的页目录位置。 相应地,页目录又指向一个个的页表,这就使得每个任务都只在自己的地址空间内运行。
页目录表(Page Directory Table,PDT)、页表(Page Table),页目录项(Page Directory Entry PDE)和页表项(Page Table Entry PTE) 总结
PDT和PT其实就是个数组用来存放PDE和PTE。
页目录项(Page Directory Entry,PDE)包含页表的地址和属性。
页表中存放的内容叫做页表项(Page Table Entry PTE)包含页的地址和属性。
在intel中的PDE和PTE结构:
从这里可以看出来通过页表项和页目录
PTE结构体:
PDE结构体:
80386 地址变化的具体过程:
前面我们理清楚了80386的页式管理,这里讲解如何通过页表页目录表,来进行页式管理的内存地址转换得到物理地址。
假如某个任务加载后,操作系统根据它的实际情况,在其4GB 虚拟 地址空间里创建了一个段,段的起始地址为0x00800000,段界限值为 0x5000,当该任务执行时,段寄存器DS 指向该段,并且执行了下面一条指令:
mov edx,[0x1050]
通过段部件得到线性地址为: 0x00801050
(段式管理这里就不继续详述了,如果没有页式管理这个地址就是物理地址了。)
页部件处理流程:
;1 页部件首先将段部件送来的32位线性地址截成高10,间的10位和低12位
;高10 位是页目录的索引,中间10 位是页表索引,低12 位则作为页内偏移来用
;2 然后将通过高10位*4得到页目录表的索引和通过CR3寄存器中的页目录表首地址相加得到页目录项
;3 对页目录项的前12位清零得到页表基地址
;4 通过中10位*4得到页表的偏移地址,然后和页表基地址相加得到页表项PTE
;5 对页表项的前12位清零得到页的基地址
;6 通过低12位得到页的偏移地址,然后将偏移地址和页的基地址相加得到物理地址
在Windbg中实践:
Windows采用了很多中的分页机制,如果想要查看经典的80386分页类似模式需要在32位系统下进行以下设置:
bcdedit /set {current} nx AlwaysOff
bcdedit /set {current} PAE ForceDisable
(注: 重启后才生效)
首先我写了一个很简单的程序:
然后放到虚拟机中运行,并采用双机调试。
首先采用ollydbg来查看,并定位到缓存内容:
然后运行到这个int 3断点处,注意不要直接用ollydbg来运行,因为会先给od来捕获这个断点。
然后查看该字符串的线性地址:
线性地址为:009520F8
然后用Windbg断下来。接着就开始实践分析了:
先找到我们的进程的页目录表基地址:
!process 0 0
PROCESS 8809f860 SessionId: 1 Cid: 0d04 Peb: 7ffd5000 ParentCid: 0d3c
DirBase: 1e0a1000 ObjectTable: a710a1e8 HandleCount: 8.
Image: ApplicationTest1.exe
在操作系统中并没有使用TSS来进行任务切换,所以并不能采用查看tss然后得到cr寄存器这样的办法来处理,只能在进程中查看cr3寄存器的值或者支持采用Windbg中提供的DirBase,两个值是一样的。
然后开始通过虚拟地址查找到物理地址:
1 解析线性地址:
kd> .formats 009520F8
Evaluate expression:
Hex: 009520f8
Decimal: 9773304
Octal: 00045220370
Binary: 00000000 10010101 00100000 11111000
Chars: .. .
Time: Fri Apr 24 10:48:24 1970
Float: low 1.36953e-038 high 0
Double: 4.82865e-317
得到二进制为:
Binary: 00000000 10010101 00100000 11111000
高10位为: 0000000010 ==0x2
中10位为: 0101010010 ==0x152
低12位为: 000011111000 ==0xF8
2 将高10位的值0x2*4和页目录表基地址1e0a1000相加得到页目录项:
kd> !dd 1e0a1000+0x2*4
#1e0a1008 2cc5b867 00000000 00000000 00000000
#1e0a1018 00000000 00000000 00000000 00000000
#1e0a1028 00000000 00000000 00000000 00000000
3 将页表项2cc5b867的前12位清零得到页表首地址:2cc5b000,
4 通过页表首地址+偏移值0x152*0x4得到页表项:
;查看页表中对应的页表项:
kd> !dd 2cc5b000+0x152*0x4
#2cc5b548 1fd95025 2426a867 3cd1e025 00000000
5 将页目录项1fd95025的前12位清零得到页表地址:1fd95000,
6 然后通过页表地址+偏移值查看物理内存
kd> !db 1fd95000+0xF8
#1fd950f8 53 6e 61 69 6c 47 6f 00-00 00 00 00 00 00 00 00 SnailGo.........
#1fd95108 dc fe 31 62 00 00 00 00-02 00 00 00 5a 00 00 00 ..1b........Z...
#1fd95118 5c 22 00 00 5c 14 00 00-00 00 00 00 dc fe 31 62 \"..\.........1b
#1fd95128 00 00 00 00 0c 00 00 00-14 00 00 00 b8 22 00 00 ............."..
#1fd95138 b8 14 00 00 00 00 00 00-dc fe 31 62 00 00 00 00 ..........1b....
#1fd95148 0d 00 00 00 6c 02 00 00-cc 22 00 00 cc 14 00 00 ....l...."......
#1fd95158 00 00 00 00 dc fe 31 62-00 00 00 00 0e 00 00 00 ......1b........
#1fd95168 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................