笔记参考毛德操先生所著《Windows内核情景分析》,使用代码为ReactOS 0.4.7,相比原著更新了一些数据结构帮助理解新的数据结构。
本文基于IA-32架构,假定读者已经了解IA-32架构下的MMU(具体请阅读Intel 手册)如何工作以及拥有良好的数据结构基础
一、虚拟内存的管理
进程地址空间的信息 由MMSUPPORT结构体所描述,每个EProcess结构体都会有一个结构为MMSUPPORT的Va成员,描述着这个进程的整个虚拟内存的信息,但是它并不管理这些信息。结构体成员如下
成员WorkingSetExpansionLinks把所有的MMSUPPORT串联在一起,链表头位于全局变量MmWorkingSetExpansionHead。PeakWorkingSetSize为工作集的峰值,PageFaultCount页面错误次数。
而进程一个结构为MM_AVL_TABLE的VadRoot结果管理着所有虚拟内存,进行管理的成员主要是BalancedRoot,是平衡树的根节点
NumberGenericTableElements成员描述了这个结构具体有多少个元素,NodeHint是命中的结点指针,NodeFreeHint是释放的时候指向的结点,都利于编程。
一整块地址空间,通常应该有许多的区域,这些区域由MEMORY_AREA描述,结构体如下
成员VadNode是MMVAD 虚拟地址描述符,type是AREA的类型,DeleteInProgress是代表被删除。Sement和ViewOffset将在共享内存区进行详细描述。MMVAD结构体如下
从虚拟地址描述符中可以看出虚拟描述符是一个二叉树,实际上是一颗平衡树,所以MEMORY_AREA这个结构体也是一个平衡二叉树结构,虚拟地址描述符中还描述了这个起始的虚拟页号和终止页号(如果只有一页那么这两个值相同)。
此时读者可能有问题,MM_AVL_TABLE表中BalancedRoot的类型为MMADDRESS_NODE,而AREA的结构体却和此不同,如何将AREA存入AVL_TABLE表中呢。实际上MMADDRESS_NODE结构体的定义如下
仔细观察后发现MMADDRESS_NODE正是VAD的一部分,所以包含VAD描述符的结构都能存进这个AVL_TABLE表中。
MEMORY_AREA有一个RegionListHead成员,定义为
即每一个MEMORY_AREA会管理着一系列的REGION,为什么这样设计呢,是因为一个AREA里面的地址可能有很多不同的属性,但是如果是一个REGION,里面的虚拟地址就会保持同一个页面属性。
这里介绍一下进程工作集的数据结构,我们知道进程结构体EPROCESS有一个类型为MMSUPPORT的Vm成员描述着虚拟内存,它的一个类型为PMMWSL 的VmWorkingSetList描述了工作集
FirstFree是第一个空闲项,FirstDynamic是第一个可以修剪的页面,LastEntry是最后一项,wsle是一个数组,每个数组的元素描述着一个有效的页面,UsedPageTableEntries数组成员,元素代表着这个页面被引用了多少次,这是一个数组,为什么是768个呢,我们假设一个页面是4K,乘以768就是3GB。我们知道一个进程可以是2GB,但是通过设置可以最多支持3GB,所以这也是一开始就设计好的内容。
接下来看一些函数实现来加深理解
MmLocateMemoryAreaByAddress函数参数是一个结构体MMSUPPORT地址空间的描述,第二个参数是一个地址,功能是返回第二个参数给定地址的AREA指针
首先是获取这个地址空间所属的进程,由于第一个参数赋值的时候常常是进程的Va成员,或者是内核的MiRosKernelVadRoot成员,所以如果是进程的话就可以通过Va成员在结构体的偏移来定位到EProcess得到进程结构体。这时候如果获取到了EProcess就判定是进程,如果为空,就代表是内核的请求(内核的虚拟内存空间MMSUPPORT是一个全局变量MiRosKernelVadRoot,通过这个全局变量来管理内核所有的虚拟空间)
接下来分析MiCheckForConflictingNode
这个函数参数分别是起始虚拟页号,终止页号,MM_AVL_TABLE地址,输出为MMADDRESS_NODE节点,简单来说就是给定一个范围的虚拟地址(起始虚拟页号和终止虚拟页号给定)在MM_AVL_TABLE中找出有这个地址的节点,那么就有三种情况了,一种是完全重合,一种是部分重合,最后就是整个区域中都没有这个地址。我们来看看是代码是怎么处理这种情况的。首先是通过搜索二叉树的算法开始计算,我们看一下这段代码
Else成立的条件是当这个起始虚拟页号和终止虚拟页号完全被包含在这个节点中就会返回。
如果不是完全包含或者是完全不存在,已经是遍历完毕时
如果说起始页号地址比这个节点的终止虚拟页号大,应该插入在NodeOrParent的右边,否则就是应该插入在左边。
返回到刚才分析的函数,如果调用了此函数返回了TableInsertAsRight、TableInsertAsLeft或者TableEmptyTree的话,就会返回空,代表当前并没有此地址。如果有的话,就返回这个节点
分析完了AREA寻找函数再看AREA插入函数
参数为一个地址空间MMSUPPORT和一个要插入的AREA,首先和查找一样,判断是内核还是进程,如果是进程就插入到进程的VadRoot,如果是内核就插入到内核的MiRosKernelVadRoot,没什么好分析的了,剩下的都是数据结构的内容。删除函数
MiRemoveNode同理。
MmFindGap会从VadRoot或者MiRosKernelVadRoot中遍历出一个参数能够支持的间隙并且粒度对齐大小的一块虚拟地址空间。具体的查找方法也是数据结构的内容,不予赘述。
接下来我们来分析一个由AREA进行管理的REGION链表,我们知道,REGION内所有地址安全属性一致,如果此时想要更改REGION内部部分的安全属性就必须进行切割。MmSplitRegion负责进行这样的工作
这段代码参数要求为InitialRegion是给定要改变的REGION,InitialBaseAddress是REGION的基地址(因为REGION结构中并没有保存基地址的成员),要改变属性的起始地址StartAddress,改变的长度Length,新REGION的类型NewType,新保护属性NewProtect,MMSUPPORT结构和AlterFunc函数指针,AlterFunc函数是为了让实际地址安全属性发生真的改变,是体系相关的内容,改变了REGION属性后就会回调这个函数对体系的内存属性进行修改。分离出2个REGION后这2个再进行连接。MmFindRegion函数对这个链表进行参数RegionListHead(一般是AREA的RegionListHead设定)遍历,具体的代码就不用说了,数据结构相关知识。
学习完AREA和REGION我们来看一下MmCreateMemoryArea函数,参数依次是PMMSUPPORT AddressSpace、ULONG Type,PVOID *BaseAddress, ULONG_PTR Length, ULONG Protect, PMEMORY_AREA *Result, ULONG AllocationFlags, ULONG Granularity),意思为在一个地址空间中申请一个BaseAddress为基址的,大小为Length,保护属性为Protect的一个AREA,结构保存在Result中
分段来进行阅读
如果参数表明需要静态内存,就可以到系统预先分配好的MiStaticMemoryAreas结构中直接获取,优点就是速度快,已经提前安排好,缺点就是容量有限。MiStaticMemoryAreaCount记录了下一次可用下标。
初始化后,如果没有给定基址,那么就用MmFindGap查找到一个间隙,找到后就设置根据这个间隙来设置AREA的起始Vpn和终止Vpn;
如果给定了基址就要判断之前有没有AREA已经在此,如果有的话就返回错误。没有的话就同样设置AREA的起始VPN和终止VPN,然后插入到地址空间之中。MmInsertMemoryArea已经分析过,不予赘述。
至此,我们已经看到了一个大概的虚拟内存管理的模型,首先是进程结构体的Va描述着他们自己的内存空间,由进程的VadRoot或者MiRosKernelVadRoot(内核)进行管理,VadRoot是一个平衡二叉树,里面存放着由AREA构成的节点,而每个AREA又管理着一个REGION链表,REGION里面的地址空间保证是一个属性,AREA则无此要求。
当观察源代码的时候,我们可以看到申请虚拟内存时并没有映射物理内存的代码,实际上就是如此,进程的VadRoot管理的一系列虚拟内存中不一定映射到物理内存,只有在使用的时候才会映射。
本文由看雪论坛 黑色书卷 原创 转载请注明来自看雪社区
关注看雪学院公众号:ikanxue
更多干货等着你~
http://weixin.qq.com/r/M3W7oxbE-0uArVLA9yAh (二维码自动识别)