实模式,保护模式,虚拟8086模式
本打算这个笔记是昨天写的,但是一提笔笔者就迷茫了,我该如何去写呢~?我如何去描述才能让大伙懂呢?因为每个知识点都有扩展,都有联系,如果真的无限的扩展下去,那就是“书"了。何况笔者能力有限,写书还是妄想~~~ 怎么办呢?我决定都采用“肯定”语句的方式来写这篇教程,既然采用肯定语句,那么这些肯定语句是如何证明的呢?那就是知识的扩展,扩展部分就在以后的章节中很快体现出来。笔者一开始迷茫就是因为这个因素:知识间联系太紧密了!!!不知道如何去下手了!
废话就少说,开始吧
1,先来稍微说下历史,好多好多年前一开始Intel公司还不是那么的强大,能力有限,导致开发了一个初级阶段的处理器:8086处理器。这个处理器是16位的,寄存器是16位,物理总线是20位,那么只能寻址2^20的地址空间。之后由于公司不断的壮大,就开发出来一个比较重要的处理器:80386.为什么说他重要呢?因为它已经是个32位的处理器,并且内部机制已经很完善,比如支持任务切换,内存分页管理,优先级。以致到现在的奔腾4,酷睿双核几乎都和80386一样,唯一的区别无非就是频率高点缓存高点(因为我们现在研究的是编程,并没有研究处理器如何去造,因此我们忽略处理器架构的什么的东西)
2,从上面的描述可以得到我们要研究的对象:8086和80386。80386处理器出来之前,大家都用的是DOS,DOS操作系统的特点是单任务,无安全保护,无分页,1M寻址,用户可以随意使用任何中断,使用任何特权指令,直接使用物理地址等等。而80386出来之后就开始支持多任务,分页管理,优先级,环0,环3,4G的寻址能力等等。以上说明一个道理,现在所谓的安全措施,内核模式还是用户模式,环0和环3的划分等等概念都是和CPU有关的。稍微举例:CPU有个CR0寄存器,大小32位,其中第16位就来控制内存的保护机制,如果第16位为“0”,那么就关闭这个保护机制,如果为“1”就打开这个保护机制。那也就是说,如果有个内存地址是写保护的,如果我必须要往里面写数据,我们只要关闭内存保护机制就OK了,很简单!不懂的人还以为很高深或者美其名曰:“某某黑客绕过了内存保护机制”,哇,听起来就好像牛逼举动,其实就几行代码。搞技术的人都信奉一句话:“技术在于你知道还是不知道,你现在知道了,OK,简单,之前你不知道,那么就感觉它很深奥”
3,还在8086时代的时候,是没有实模式这个说法的,虽然大家现在都知道8086模式就是实模式!直到80386出来之后才出现实模式这个定义。大伙仔细比较下8086和80386之间的区别再回顾下上面举的例子,你或许应该有所感悟!既然内存保护机制可以通过某个寄存器的某个位来屏蔽掉,那么内存的分页功能等等能不能关闭呢?能!!继续回到主题上来,其实研究来研究去,我们只关心这2个模式之间的特征,特征的不同带来名字的不同!顺便说下80386处理器可以来回切换这2个模式的
4,虚拟8086模式,当时80386出来的时候,DOS程序已经主宰了这个世界。从上面的介绍可知保护模式和实模式的差异十分巨大,势必导致一旦80386带来的保护模式下无法运行DOS程序的局面。这是微软和Intel公司不愿意看到的。从此虚拟8086模式诞生。你可以在windows操作系统中运行多个DOS程序并且同时运行多个真正的32位程序,比如说此时此刻你运行了3个DOS程序和5个32位程序,这8个程序支持任务切换和内存分页,其中3个DOS程序的寻址还是1M。由于分页的存在,这3个DOS程序所对应的物理内存肯定是不同的。
5,以前MSDOS系统是不分内核还是用户模式,也不会分特权指令还是非特权指令。现在问题来了,如果包含特权指令的DOS运行在windows下肯定不行。windows是这样处理的,一旦运行的时候执行特权指令就会引起一个保护异常,这个时候系统就会分析引起这个异常的指令,如果是中断指令,那么就从虚拟86任务的中断向量表中取出中断处理程序的入口地址,之后跳转;如果是像cli之类的能够危及操作系统的指令则简单的忽略。这样DOS程序就可以运行起来。
接下来介绍内存。 这节你只要了解,如果对分页模式不理解,那么下节开始介绍,笔者会把知识点联系起来!这点读者请放心
内存管理(1)
这章内容有点多,也非常的细,细到具体的函数实现。还请做好思想准备
说实在,现在的生活过的有点纠结。我是85年出生的,算起来也老大不小了,该结婚了!父母老是唠叨着让我跳槽找份收入更高的工作,这样可以改善家庭情况也为结婚做好准备。而我却不认同他们的想法,一方面我现在的工作比较闲,有空看看书,提高提高自己。另一方面如果跳槽了,那么工作就不会那么的轻松,我的驱动学习生涯估计也要终结。现实和爱好冲突之下我还是选择了爱好!
我已经好久没有写博客了,倒不是因为我放弃驱动了,我一直在学习。只是中途遇到了一些阻碍,因为在我看的书当中,我所能理解的都会很快的理解。而我所不能理解的,书里写的很模糊。庆幸的是我前不久找到了毛德操先生编写的windows内核情景分析这本书。略微的看了下感到很兴奋,这正好是我渴求的书!!!但是认真的看下去之后,感觉这本书让人头大!代码太多了,函数间扩展太多了,当你一环一环深入之后你很有可能会迷路。我曾经迷茫过,胆怯过,甚至我对自己说,干嘛要研究那么的仔细呢?不过说起来也奇怪,当你整天对着这些代码也就习惯了,代码多就多呗,看不就行乐 ~~~~ 如果感觉函数扩展的太深入,那么做好笔记,这样就不会迷路了,脑子也有条理了。OK,总之一条,我非得把这1400页的书看完不可!!
内存管理很重要,重要到将来你不管学到什么,都会碰到和它有关的函数,既然躲不了,那么就认真的理解透了。还有,现在学计算机的人群当中至少有70%的人对内存这块很是纠结,很多东西想不通为什么。比如说为什么一个进程占用4G空间呀?但是现在电脑一般就2G内存啊。这节我们就先从这基础的地方谈起。接下来的几节,我们深入都代码部分。
就内存管理这一大的知识范畴中,我们要涉及到几个目标:虚拟地址空间,物理内存(真正的内存),页文件(也就是我们常常说的虚拟内存),可执行文件(EXE文件,dll文件)。而这节我们重点谈论虚拟地址空间。因为后3个至少说不会让大伙误解。
***************************************************
每个程序运行之后都会产生一个进程,并且同时系统会分配给这个进程4G的虚拟地址空间让此进程使用。按这样算,如果系统里有10个进程,那么就必须有40G的虚拟地址空间。同时物理内存是有限的,一般的电脑也就配2G的内存。现在问题来了,如果不采取点措施,那么就单单运行一个程序也是不行的。那么换个思路来理解
虚拟地址是什么?虚拟地址是程序在运行的时候使用的地址,如果你用过反汇编软件的话,你所看到的地址就是虚拟地址(比如说0x11223344)
虚拟地址空间?虚拟地址空间就是程序在运行的时候能够使用的虚拟地址的范围。(0~0xffffffff)大小为4G。
说白了,虚拟地址就是程序眼里的地址。
如果对虚拟地址还不够明白,你可以百度搜索下相关的概念。或者说你继续看下去,看完之后你或许对它有所认识
现在引入几个名词:页目录,页表,页面
页面就不要解释了:在windows系统中大小为4K (如果有人不懂,自己百度,这个不打算扩展)
当一个进程产生之后,系统同时为它创建一个页目录,和1024个页表。页目录是个指针数组,有1024个项。页表也是个数组,每个项里记录的是具体页的信息。页目录里的指针分别指向这些页表。这样就形成了一个2维结构。(上面或许有点绕,别急,仔细看几遍)既然页目录,页表,页具有这样的关系。那么我们做个乘法运算:1024个页表*一个页表里有1024个页*页的大小为4K=4G。
哦,原来系统用页目录,页表把4G虚拟地址空间划分成以页为单位的目标来管理!
如果我要访问0x11223344(00010001001000100011001101000100)这个虚拟地址,系统首先从这个地址值中取出高10位数值【0001000100(2进制)68(10进制)】在页目录中找其第68项得到一个指向页表的指针,之后继续向后取10位数值(1000100011),经过换算得到一个十进制的数547,从得到的页表中找到第547项,得到此地址具体在哪个页里。最后还有12位的数值(1001101000100)表示在页里的偏移。
仔细的读者会发现,高10位二进制数正好表示1024,此1024对应页目录项。中间10位二进制数也表示1024,这里的1024对应的是页表里的项。最后12位二进制数表示为4K,正好对应页面的大小。
或许你开始有点感觉了,我们继续
我们还知道一件事情,程序运行的时候,系统并不是把整个程序代码和数据全部加载到内存,这是很显然的。而是用到什么才加载到内存。假如,此时此刻某某程序要用0x11223344的数据,这个时候系统就要判断,此时此刻这个数据在不在内存中,如果不在,那么就要想办法加载到内存,如果在,那么就立即访问。那么系统如何判断呢?首先系统会按上面我说的步骤定位具体的页,之后查看页的信息(上面红字表示了),这个页信息有32位。其中最低位PA_PRESENT如果是1,表明此页(虚拟页)有对应的内存页(物理内存页),那么可以直接使用(页信息有32位,当最低位为1,那么其他各位用来表示此虚拟页对应的物理内存页)。如果PA_PRESENT为0,那么至少说明此页没有对应的内存页,这个时候就要采取进一步措施---加载!
一旦如上所说,那么证明了我们所知道的常识:程序的运行并不是一下子全部加载,而是用到什么地方加载什么地方。好处显而易见,程序实际使用的物理内存很少。
不仅如此。我们还知道,windows具有进程隔离技术,也就是说一个进程的运行不会影响其他的进程,也不允许影响其他进程,一方面是稳定,另一方面是安全。
上面的设计使得这个技术成为可能,你会看到,每个进程都有属于自己的页目录和属于自己的页表。并且页表里的页都会有自己对应的物理页的映射。还是拿0x11223344为例,此时你电脑里运行了QQ,也运行了WORD,他们都有自己的进程,并且在进程眼里,他们都可以使用这个地址,不同的是此时QQ为此虚拟地址映射的物理页面编号为(10000),而WORD可能为此虚拟地址映射的物理页面标号为(1000)。这样,虽然在程序眼里他们访问的都是0x11223344,但是真正对应的物理地址就完全不一样了。也就是如此,各各进程彼此隔离。
好了,就说到这。顺便说下,以上我所介绍的只是大概,一些细节我并没有过于深入。不然大伙更是云里雾里。当然也不排除有些地方说的不是很准确,比如说页表其实并不是固定1024个,而是动态的。总之还是为了方便初学者阅读。如果这节还是不明白,还是仔细去想吧。一切靠自己去理解!!!
标签: 驱动学习内存管理 |
内存管理(2)
这节我们研究虚拟地址空间的“保留”。如果觉得拗口,那么可以说在4G地址空间里“保留”一段区域来用。此时此刻我希望读者有这方面的基础,知道“保留”是什么意思。其实笔者这个担心也没有必要,因为在应用程序编程的时候大家都应该用到过VirtualAlloc()这个函数。如果你实在不晓得保留和提交这两个术语的话也不要紧。慢慢往下看
当一个程序开始运行,系统会产生一个进程,并且为这个进程分配4G的虚拟地址空间。接下来系统会划分一个足够大的空间来映射EXE文件,并且还会分析这个EXE的PE结构,得到需要加载的DLL,并且会为每个DLL划分足够大的空间来映射这些DLL。这样一个4G的虚拟地址空间就会瓜分成一块一块的。“瓜分”这个举动你就可以理解成“保留”。而“映射”就可以理解成“提交”。
这里有人就问了,啊~~,上节不是说物理内存才可以映射到虚拟页,你现在怎么让普通的文件也影射了啊!!OK,如果你对这个问题不明白,请百度“内存映射文件”
不过我还是稍微说下。我们都知道物理内存,还有页文件(也就是虚拟内存)。并且还知道,虚拟内存的存在就是防止物理内存不足。这只是表面现象。如果说这个表面现象是完全正确的话,我们的虚拟页只能映射物理内存页或者说是虚拟内存页!那么一个程序的运行必须把整个程序一股脑的加载到虚拟内存和物理内存中,到时候如果要的数据不在内存中就到虚拟内存中找,找到之后换进内存,如果可能,还要把暂时不需要的数据换回到虚拟内存中。以上想法是好的,也不错。但是不合理!因为页文件也是有大小限制的。还有个更重要的因素就是,如果一个程序运行的时候都必须先把所有的代码和数据都加载到虚拟内存里,那么程序的启动会慢到你无法忍受。因为虚拟内存(页文件)是在硬盘里,而硬盘的读写速度比较慢。而你现在所感觉到的,程序的启动还是比较快的。这变相的证明虚拟页是可以影射普通的文件的。关于内存映射文件这个知识就说到这。如果还不清楚还是百度吧。另外还有个非常重要的常识需要记住:可执行文件在内存中的形态和在硬盘里的形态是一样的。你可以使用内存映射文件技术把一个EXE文件的一部分映射到你的地址空间里,之后通过指针来操作你的EXE文件。我后面会讲解PE文件结构,这2个技术结合起来可以制造一个像模像样的病毒了。话扯远了。回到主题上来。
我们刚才说了,一个进程要为它的EXE文件,和DLL都要保留足够大的虚拟地址空间以便映射所用。那么至少说这个进程要把这些区域记录下来。一方面用到的时候方面查找,另一方面如果你还要保留地址空间的话不能和现有的重合。这里要介绍一个结构 MADDRESS_SPACE
进程结构EPROCESS结构中有个VadRoot指针成员,指向MADDRESS_SPACE结构。
typedef struct _MADDRESS_SPACE
{
PMEMORY_AREA MEmoryAreaRoot;
ULONG LowestAddress; //最低端地址
struct _EPROCESS* Process; //所属进程
PUSHORT PageTableRefCountTable; //用来记录用户空间页表的使用情况
ULONG PageTableRefCountTableSize;
} MADDRESS_SPACE, *PMADDRESS_SPACE;
红色标注的PMEMORY_AREA也是个结构
typedef struct _MEMORY_AREA
{
PVOID StartingAddress;
PVOID EndingAddress;
struct _MEMORY_AREA *Parent;
struct _MEMORY_AREA *LeftChild;
struct _MEMORY_AREA *RightChild;
ULONG Type;
ULONG Protect;
ULONG Flags;
BOOLEAN DeleteInprogress;
ULONG PageOpCount;
union
{
struct
{
ROS_SECTION_OBJECT * Section;
ULONG ViewOffset;
PMM_SECTION_SEGMENT Segment;
BOOLEAN WriteCopyView;
LIST_ENTRY RegionListHead;
}SectionData;
struct
{
LIST_ENTRY RegionListHead;
}VirtualMemoryData;
}Data;
}MEMORY_AREA,*PMEMORY_AREA
先看MEMORY_AREA结构的土黄色部分,你会意识到这个和二叉树有关,并且还会感觉到这个结构很大可能作为一个节点而存在。红色部分再明确不过了,一个开始,一个结束,指明了一个区间。就如上面描述,一个进程为它的EXE文件保留了一个区间用来映射,这个MEMORY_AREA结构正好可以记录这样的区间。同理,进程还会为其需要的DLL文件保留区间,那么系统会生成多个MEMORY_AREA结构来分别记录每个区间的起始地址,结束地址和其他一些信息。
有了这些结构还不够,因为这些结构里面的信息将来会被用到,必须得把他们组织起来,不然找起来很不方便!微软决定把这些结构通过二叉树的形式组织在一起。并且用MADDRESS_SPACE结构里的 MEmoryAreaRoot指针指向此树的根节点。这样一来,可以通过二叉树的遍历搜索自己想要的结构。
为什么要有这样的安排呢?一方面可以知道已经保留了多少区间,另一方面如果还要保留区间的话,可以避免重合。
************************以上就是理论*****************下面是函数的介绍**********
函数的代码我就不写了,太多了,写要写半天,但是分析是少不了的。我会写明第几页的什么函数。另外,用土黄色标志的函数说明它只是过渡函数。
就目前而言我们先看看第49页 MmLocateMemoryAreaByAddress(PMADDRESS_SPACE AddressSpace,PVOID Address)
locate:查找,那么这个函数的意思是:通过地址查找MemoryArea结构。这个函数很简单,而是基本的二叉树查找方式,最后返回指向包含Address的区间所对应的MEMORYAREA结构。
第50页 MmFindGap(PMADDRESS_SPACE AddressSpace,ULONG_PTR length,ULONG_PTR Granularity,BOOLEAN TopDown)
这个函数的作用是从已经被瓜分的地址空间里找到一个“空隙”,这个空隙的至少比length大,Granularity是粒度的意思,最后一个参数表明是从高地址向低地址找还是从低向高找呢。此函数回看情况调用下面2个函数
MmFindGapBottomUp(PMADDRESS_SPACE AddressSpace,ULONG_PTR length,ULONG_PTR Granularity)
MmFindGapTopDown(PMADDRESS_SPACE AddressSpace,ULONG_PTR length,ULONG_PTR Granularity)
下一节介绍MmFindGapBottomUp()
**************************************************************************************************
标签: 内存管理驱动驱动程序驱动学习虚拟地址 |
内存管理(3)
这一节我们把注意力放在一个非常重要的函数上:它的作用是在虚拟地址空间里找出一个满足要求的一个间隙。返回值是此间隙的首地址!顺便说下,我们必须要关注返回值和函数的参数,特别是返回值!因为这个函数将来指不定就会被其他函数所调用。如果你不关注返回值的话,或许你一时会看不懂代码。还有一点。很多时候你可以省事而不去关心具体函数的实现,比如你使用win32 API函数的时候,你从来不会关心它如何去实现,你只关心它的作用。而这作用恰恰很大程度上和返回值有关。但是本着打破砂锅问到底的精神,我们硬着头皮来分析代码!此时此刻想必你已经定位到第50页这个函数。
MmFindGapBottomUp(PMADDRESS_SPACE AddressSpace,ULONG_PTR length,ULONG_PTR Granularity)
MmFindGapTopDown(PMADDRESS_SPACE AddressSpace,ULONG_PTR length,ULONG_PTR Granularity)
这两个函数原理是一样,因此拿MmFindGapBottomUp开刀~~~
先介绍几个常量:MmSystemRangeStart MAXULONG_PTR MM_VIRTMEM_GRANULARITY PAGE_SIZE
几个宏:MM_ROUND_UP MM_ROUND_DOWN CONTAINING_RECORD
其中CONTAINING_RECORD最为重要,在驱动编程中想不遇到都难。分别解释
MmSystemRangeStart:此常量的值就是0x80000000,也就是说内核空间的起始地址
MAXULONG_PTR :说实在,我还真不确定它是什么,但是从代码上分析来看,八成为0xffffffff
MM_VIRTMEM_GRANULARITY:此常量表示当前系统规定的内存粒度。默认是64KB。粒度的讲解一会就说
PAGE_SIZE:页的大小。默认是4KB
MM_ROUND_UP:此为宏,有2个参数,第一个参数是个地址值,第二个参数是粒度大小。此宏的作用是根据地址值以粒度为标准,向高地址方向对齐。靠!!!有点云里雾里了。别急,一会详细讲解
MM_ROUND_DOWN:此为宏,有2个参数,第一个参数是个地址值,第二个参数是粒度大小。此宏的作用是根据地址值以粒度为标准,向低地址方向对其。
CONTAINING_RECORD:此为宏,有3个参数。第一个参数是个地址值,第二个参数为一个结构的类型名,第三个参数为此结构里一个元素的名字。此宏的作用是得到某个结构的首地址。
我们知道在内核学习中,最让人头疼的莫过于数据结构,对数据结构的操作无非两种,一是知道数据结构的首地址之后定位到其元素。二就是知道某个元素的地址,定位其结构的首地址。关于第一种情况很简单,直接用->。你想过没有系统(严格的讲应该是编译器)怎么这么智能呢?它怎么知道如何定位?其实你的程序里已经包含了此结构的头文件,这样系统就知道偏移多少字节从而得到你需要定位的元素。同理,如果你想逆操作,那么必须告诉系统此时此刻你所针对的结构是什么类型的,并且你还要告诉它你所知道的某个元素的地址。这样系统就会算出此结构的首地址了。这也就是为什么CONTAINING_RECORD必须要这3个参数。其中第一个参数和第3个参数是对应的!!!
现在谈粒度。就虚拟地址空间而言,使用粒度这个标准是非常重要的。其他优点我也说不上,但是最主要的优点就是管理和使用的时候速度快。那么什么是粒度?举例:
比如说我的内存有100个字节。粒度为10。其中第13个字节到第55个字节这个区间为可用的。这个时候我要在这个空闲的区间里划分一个大小为22的空间。如果说没有粒度这个限制的话,你完全可以从第13个字节开始划分。而实际上你必须从第20个字节开始划。因为粒度为10!同理,当你从第20个字节开始划了之后,按道理讲会在第42个字节结束。但是由于有粒度的存在。你必须在第50个字节结束!那么这个空间的实际区域为(20~50)
这个例子介绍完之后想必读者应该对粒度有所了解。那么MM_ROUND_UP的意思是上面红色字所对应的操作,MM_ROUND_DOWN是橘黄色(我觉得更像土黄色)字所对应的操作!
好了,上面说了半天都是铺垫。回到正题上。
MmFindGapBottomUp,这个函数的目的是满足第二个参数的要求从虚拟地址空间里找到一个区间,并返回这个区间的首地址。他的流程:(按照代码的顺序)
1,先通过Address->LowestAddress的值判断是用户空间还是内核空间,如果是内核空间,那么得到其地址上限值为0xffffffff.如果是用户空间,那么上限值就是0x800000000-1。
2,之后就来查看上节讲的那棵二叉树。如果为空,那么就说明进程刚刚建立,此时虚拟地址空间还为处女地,直接返回Address->LowestAddress这个值(不过先得按粒度值换算下)
3,如果虚拟地址空间已经被瓜分过,那么就取得第一个节点,和第二个节点,分别得到节点中标注的起始地址和结束地址。之后就加加减减的算吧。如果第二个节点中取出的起始地址减去第一个节点取出的结束地址的值小于length那么就不满足要求。说明夹在这2个区间之间的空间不够大。那么继续分析第2个节点和第3个节点。。。。就这样循环下去
4,如果中途找到了,那么返回其首地址
5,当然也别忘了第一个节点前的空间,和最后一个节点后的空间。
好了。这个函数就说到这。继续下个函数
************************************************************
内存管理(4)
哎,今天真的倒霉。就在刚才,我的thinkpad电脑从桌子上掉下来了。我原本打算起来倒杯咖啡的,这下好了,拌到了网线,直接把电脑拉下来了。屏幕右边的边框断开了!好在有全面保护,明早拿去换先~~~这不本来打算睡觉的,经这么一折腾,睡不着了。一个念头闪过:最多明天工作的时候打瞌睡嘛,又不是第一次,谁怕谁哼~~~
回到正题。现在我们看下这个函数:
MmLocateMemoryAreaByRegion(PMADDRESS_SPACE AddressSpace, PVOID Address,ULONG_PTR length)
这个函数的意思是给定一个地址范围,看看哪个区间包含这个地址范围,如果能够找到,那么返回这个节点的结构的首地址。其中第2个参数和第3个参数组合起来确定一个范围。第一个参数Address_Space里的MemoryAreaRoot元素正好指向前面说的二叉树的根节点。其他代码比较简单。不作介绍~~
现在引入在我心目中最重要的函数之一:MmCreateMemoryArea(PMADDRESS_SPACE AddressSpace,ULONG Type,PVOID * BaseAddress,ULONG_PTR Length,ULONG Protect, PMEMORY_AREA * Result,BOOLEAN FixedAddress,Ulong AllocationFlags,PHYSICAL_ADDRESS BoundaryAddressMultiple)
这个函数的作用就是前面我提到过的概念---“保留”
好了,参数一大堆,我手也酸了。现在看看它的返回值:如果成功就返回STATUS_SUCESS,并且第6个参数指向新创建的节点。如果失败,那么返回的东东就五花八门了。
那么这个函数的作用就是按照参数的要求,从虚拟地址空间里找到一个大小符合要求的区间,如果成功,那么生成一个PMEMORY_AREA结构,并把这个区间的首地址和结束地址值(还有其他信息)放到生成的PMEMORY_AREA结构中。如果成功返回,那么参数Result正好指向新的PMEMORY_AREA结构的首地址。
说到这,其实已经没有什么可说的了。因为上面就是这个函数的流程!你可以看到,这些代码和函数前面已经详细的介绍。你要做的只是心静下来自己再好好看一遍代码(如果你愿意的话)
现在只剩下这句代码或许是障碍:ExAllocatePoolWithTag(NonPagedPool,sizeof(MEMORY_AREA),TAG_MAREA)
别看名字有点怪,其实它对应的win32 API就是AllocateMemory()。第一个参数可以有两种选择,一个就是我们所看到的,还有一个是PagedPool。第2个参数表示需求多大的空间,第三个参数为标志,方便调试使用。这个函数的作用就是在内存池里分配一个区域(大小由第二个参数决定)。第一个参数分2种,一种是分页的,一个是不分页的。所谓的分页表明划分的区域可以倒换到页文件中(硬盘中),不可分页就说明这块区域是常驻内存。为什么要有这两种区别呢,因为微软规定高中断级的函数一般都使用常驻内存。你可以返回到介绍IRQL这章内容,笔者曾经说过,运行在高中断级上的代码是不会被低中断级上的代码所打断。如果说负责内存和页文件之间交换数据的代码运行在较低的中断级上,而某个函数运行在较高的中断级上,那么一旦这个函数需要的空间被交换到页文件中,就会出错。因为负责内存和页文件之间交换数据的代码的中断级低的缘故而永远得不到运行!
回到开头,笔者说过MmCreateMemoryArea这个函数很重要。为什么?请读者关注下第一个参数,再关注这个参数对应结构的第3个元素。你会发现这是个EPROCESS结构。此结构很重要,它包含了当前进程几乎所有的信息!并且不管什么进程都必须调用这个函数。就是因为这点,如果你inline hook这个函数,你完全可以阻止任何进程的运行(包括杀毒软件进程)。当然人家安全工程师也不是傻瓜,这个函数估计已经被主动防御或者说已经被hook。别急,你还可以hook MmFindGapButtomUp函数。因为MmCreateMemoryArea必然会调用这个函数,并且第一个参数是一样的。
杀毒软件和病毒现在几乎都是内核级的程序,现在拼的就是谁更底层。至少说MmFindGapButtomUp比MmCreateMemoryArea更加底层。关于谁更底层这个问题靠读者自己去挖掘呢。其实主要的难点问题是构造你的inline hook。这才是大学问。以后我们一起研究。笔者现在的能力只是能够理解原理,但是自己做起来还够呛。毕竟这个东西要的是耐心和必要的知识积累
标签: 杂谈驱动程序驱动学习驱动虚拟地址内存管理 |
内存管理(5)
继续我们的学习。上节我们已经学习了MmCreateMemoryArea() ,并且用它保留了一个区间。
回忆以前讲过的知识,一个进程运行之后,他要为自己的EXE文件以及要使用的DLL文件分别保留足够的空间用来映射。至少说到目前为止我们已经学会了如何去保留一个空间。但是问题来了。
不知道读者对PE结构了解多少。如果了解,固然最好。如果不了解,那么我先给你恶补下其中一些知识以便这节的学习。
先说明,PE结构的学习是必须的。因为你不管是造病毒也罢,杀病毒也好都必须建立在熟知PE结构这个前提之上。熊猫烧香病毒为什么会改变你的图标的样子?如何实现?如何去感染一个文件?如何分析一个可执行文件?这一切的一切都基于对PE结构的熟知!很多读者要问,PE结构难学吗?不是很难,只要你肯为它牺牲点游戏和打牌时间。但是今天我并不打算深入,要不然就没完没鸟了。
那么我先来说说为什么一个EXE文件或者说SYS(驱动文件),DLL(动态库文件)都必须遵循PE结构。你想啊,你辛辛苦苦写了一个程序,图标是自己在VC++里画的,图片是自己找的,代码是自己写的,变量也是自己定义的。当你编译链接之后就会生成一个EXE文件。这一切的一切都消失了,图标,图片,代码,变量等等都看不见了!哪去呢?都封装进EXE文件里了。如果说EXE文件里面包含的图标,代码,变量都乱放的话,很显然是不行的。因此微软规定这些东西都必须按照某种结构来存放。从此PE结构诞生了
这个时候微软又在想了,比如说吧,当这个程序运行了,其图标和图片数据肯定是不可修改的(只读),代码也是不能修改的但可以执行(至少也是只读),变量还分静态,全局(可读可写),局部变量(存在于堆栈中,暂不考虑)。你可以发现,他们之间的访问属性是不相同的。既然属性不同,那么在EXE文件中就必须把这些具有不同访问属性的数据分开来。让具有某种访问属性的数据分别放入不同的“节”里。这样将来映射进虚拟地址空间的时候就有条理了。
“节”在PE结构中是个非常重要的概念。在PE结构中,数据在哪个节里只是由访问属性来控制。也就是说,如果图标或者图片为只读属性,代码也为只读属性,那么我们有理由把图标图片和代码放在同一个节当中!(上面这句我只是做个比喻,为了方便读者理解,但是我所表达的意思完全正确)。有读者会问了,到时候程序运行的时候怎么分的清哪是代码哪是图标呢?呵呵,这个问题就要牵扯N多东西出来。我只能放弃扩展下去。将来系统的介绍吧~~~
现在你应该对“节”有了些许的了解。这样就足够了!回到主题上来。先来看一个结构
typedef Struct MM_REGION
{
ULONG Type;
ULONG Protect;
ULONG Length;
LIST_ENTRY RegionListEntry;
}MM_REGION,* PMM_REGION;
这个结构代表一个“区块”,前面已经说了,系统已经在虚拟地址“空间”里保留了一个“区间”来映射EXE文件,而EXE文件又是有节的。因此系统又在“区间”里继续划分一个个的“区块”来对应EXE文件里相关的节。需要注意的是,以上的操作还只是停留在“保留”阶段,还没有真正的映射!!!有读者会问,系统怎么就知道EXE文件里各各节的大小呢?答案很简单,系统会首先分析PE结构,从中获取各各节的大小。
现在来看看MM_REGION结构的第4个元素----LIST_ENTRY。它也是个数据结构。也是很重要的东西!
下面我重点研究LIST_ENTRY。之后你会看到,就是因为LIST_ENTRY元素的存在,MM_REGION结构才有可能成为一个链表的节点被“串”起来。先不谈为什么LIST_ENTRY如此的厉害,如此的重要。我们来谈谈为什么要把这些结构串起来。
上面我已经说了,一个结构代表一个区块。我还说过,一个EXE文件里还包含N多个节。因此区块的个数就不止一个。那么为了方便查找或者其他原因,我们有必要把他们串起来。(既然要串起来,那么使用链表是再合适不过了。因此就引入了LIST_ENTRY)
这样一来,虚拟地址空间,区间,区块,页的定义我已经讲好了。请读者从头到尾连贯起来梳理一下。
*****************************下一节我们来分析下LIST_ENTRY*******************************
标签: 驱动程序内存管理 |
内存管理(6)
可以说在驱动程序编程方面,LIST_ENTRY结构是再重要不过了。几乎你到哪都能遇到他。为什么重要呢?windows系统里数据结构太多了,有联系的数据结构比比皆是,那么管理这些数据结构是就了重要的问题,而LIST_ENTRY作为其中一种手段而被广泛使用。上节的区块的“管理”就是一个活生生的例子。
先来看看结构:
typedef struct _LIST_ENTRY
{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
这个结构很简单。不过为了方便理解。我从网上COPY了一段代码。请看:
typedef struct _MYDATASTRUCT{
ULONG number;
LIST_ENTRY ListEntry; (1)
} MYDATASTRUCT, *PMYDATASTRUCT;
LIST_ENTRY linkListHead; // 链表
LIST_ENTRY使用:
VOID LinkListTest()
{
PMYDATASTRUCT pData; // 节点数据
ULONG i = 0; // 计数
LIST_ENTRY ListEntry;
KSPIN_LOCK spin_lock; // 自旋锁
KIRQL irql; // 中断级别
// 初始化
InitializeListHead(&linkListHead); (2)
KeInitializeSpinLock(&spin_lock); (3)
//向链表中插入10个元素
KdPrint(("[ProcessList] Begin insert to link list"));
KeAcquireSpinLock(&spin_lock, &irql); (4)
for (i=0 ; i<10 ; i++)
{
pData = (PMYDATASTRUCT)ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));
pData->number = i;
InsertHeadList(&linkListHead,&pData->ListEntry);(5)
}
KeReleaseSpinLock(&spin_lock, irql);(6)
//从链表中取出所有数据并显示
KdPrint(("[ProcessList] Begin remove from link list\n"));
// 锁定
KeAcquireSpinLock(&spin_lock, &irql);
while(!IsListEmpty(&linkListHead))
{
PLIST_ENTRY pEntry = RemoveTailList(&linkListHead); (7)
pData = CONTAINING_RECORD(pEntry, MYDATASTRUCT, ListEntry);
KdPrint(("%d\n",pData->number));
ExFreePool(pData); (8)
}
// 解锁
KeReleaseSpinLock(&spin_lock, irql);
}
遍历:
PLIST_ENTRY pLink=NULL;
for(pLink = linkListHead.Flink; pLink !=(PLIST_ENTRY) &linkListHead; pLink = pLink->Flink) (9)
{
MYDATASTRUCT pData= CONTAINING_RECORD(pLink,MYDATASTRUCT,ListEntry);
}
首先要感谢这位作者!!!!!并且在此申明此代码的所有权归其作者所有,我只是引用!!!
不过以上程序有几个地方是错误的。首先linkListHead不应该是局部定义,而是全局定义。其次就是加上我用蓝色写的代码
还有(9)这句有点莫名其妙。我已经改正
源代码地址:http://blog.csdn.net/bobohack/archive/2010/02/13/5308397.aspx做个比较!
具体讲解:
(1)前面笔者已经说过,如要想让一个结构有机会被串起来,那么必须包含LIST_ENTRY结构
(2)先定义一个结构实例,并用InitializeListHead初始化,此时此刻的这个结构实例作为链表头而存在
(3)初始化一个“自旋锁”。自旋锁也是驱动编程中用的最为广泛的对象。他的作用类似于应用程序编程中的线程同步对象EVENT。起到同步的作用。为了防止多个线程同时操作这个链表
(4)请求这个“锁”。一旦成功,那么其他线程再次请求的话就会导致失败。
(5)生成MYDATASTRUCT大小的空间,并且使用InsertHeadList这个函数把刚生成的MYDATASTRUCT结构挂入到LIST_ENTRY链表中。需要非常注意这个函数的2个参数!参数理解了,那么链表的结构也就理解了。
(6)操作好链表之后,进行解锁。解锁之后,其他线程就可以请求这个锁了。
(7)节点的移除
(8)此函数为 ExAllocatePoolWithTag的逆操作
(9)是个FOR语句。这句话有个非常重要的信息告诉我们。链表中最后一个节点的FLINK指向的是头节点!
关于LIST_ENTRY这方面的函数我们大都数已经涉及到了。但是还有个函数也是非常重要,下面的代码中将会遇到,那就是:InsertAfterEntry(LIST_ENTRY firstentrystruct,LIST_ENTRY lastentrystruct)
从字面上你应该能够理解这个函数的作用:把后一个节点插入到前一个节点的后面。
为什么我要提及这个函数呢?你可以发现,第一个参数并不是头节点,而是任意一个中间节点。
除此之外还有个函数:InsertTailList()也是比较重要的。此函数和InsertHeadList()用法一样。也非常好理解。我就不讲解了。
好了,和LIST_ENTRY有关的函数几乎都在这了。如果将来遇到新的相关函数,我会更新进来。如读者有所补充,还请赐教 谢谢
*************************LIST_ENTRY相关代码比较多,可以百度搜索********************************
上面链表的遍历用的是FOR语句,你还可以用WHILE语句
pLink = linkListHead.Flink
While(PLink!=linkListHead)
{
.......
.......
PLink=PLink->Flink;
}
标签: 区块函数虚拟地址区间链表驱动程序驱动驱动学习 |
内存管理(7)
前面6节的知识已经算是不少了。但是说来说去研究的还只是虚拟地址空间的“保留”。总的来说还只能算是内存管理知识框架中冰山一角。这节内容我将结束掉“保留”这部分知识的讲解。接下来我们将会研究物理内存方面的知识。有书的朋友请翻到第60页。我将从内存管理(8)开始探讨这方面的知识。
但是我看了下后面的代码,发现里面出现很多同步对象的使用,并且考虑到这方面的知识用的非常频繁以致到了不得不先拿出来重点介绍的地步了。因此我会在内存管理(8)之前先用几节的篇幅重点介绍相关的知识。
OK,回到我们今天的任务上来~~~~请大伙定位到第55页。
前面已经介绍了区块的概念。我们继续。现在关注这个函数:MmFindGegion(PVOID BaseAddress,PLIST_ENTRY RegionListHead,PVOID Address,PVOID * RegionBaseAddress)
从字面上理解,这个函数的意思就是:找到一个区块。具体点:
第一个参数作为搜索的起点,表示从哪开始搜索。需要注意的是,这个地址值不是任意的,而必须是某个区间的起点(为什么是区间的起点呢?很明显,一个区间里包含任意多个区块,并且此区间里第一个区块的首地址和此区间的首地址是重合的)
第二个参数:请回顾下MEMORY_AREA结构,你会发现这个元素。那么为什么MEMORY_AREA结构中要有个链表头呢?道理很简单,因为任何区块都属于某个区间,那么作为区间这个“母体”,必须记录下自己所包含的区块信息.现在你打算搜索某个区块,那么就必须先告诉这个函数:你打算在哪个区间里面搜!
第三个参数告诉函数,如果搜到的某个区块里包含这个地址值,那么就说明已经搜到想要的区块了。
如果函数执行成功,那么第四个参数返回区块的首地址并且函数返回值为此区块对应的MM_REGION结构的首地址 (返回值很重要)
由于函数的代码部分比较简单,笔者简要的说下其流程:
1,代码虽然简单。但是却包含了链表遍历的实现。请回到第6节。我已经把那段代码修改成正确的了!
2,请看 if(RegionBaseAddress != NULL)这句。这句的意思是验证传进来的第4个参数是不是空指针。既然说到空指针,那么空指针如何有意去实现呢 ?很简单 PVOID *p=(PVIOD)& NULL 或者 int *p ; *p=(int)NULL;(前面一种方式在驱动程序中经常看到,后面这种方式看起来就顺眼的多,你喜欢用哪个就用哪个)
这个函数就介绍到这里。继续
笔者以前说过,PE文件里节的存在而导致虚拟地址空间中区块的存在。并且笔者还说过就某个区块而言里面所有的内存的属性是完全相同的。反过来讲,如果我要修改区块中某个虚拟页的属性,那么就必须把整个区块拆分开来。(有个知识点需要注意:在内存中,就管理而言,其单位是页,而不是某个内存地址)
有个函数就负责区块的拆分,并为新的区块设置好属性。
MmSplitRegion(PMM_REGION InitialRegion,PVOID InitialBaseAddress,PVOID StartAddress,ULONG Length,ULONG NewType,ULONG NewProtect,PMADDRESS_SPACE AddressSpace,PMM_ALTER_REGION_FUNC AlterFunc)
说实在,笔者最怕的就是敲上面这些东东,哎,忍着!!
具体来说:
第一个参数:告诉函数打算拆哪个区块
第二个参数:此区块的首地址。 这2个参数可以通过MmFindRegion()函数获得
第三个参数:告诉函数打算从哪个地址开始改变
第四个参数:告诉函数范围是多大
第五个参数:新的页面类型是什么
第六个参数: 新的页面的保护属性是什么
第七个参数:这个参数将会传递到第8个参数所指向的函数中
第八个参数:这个参数是个函数指针,指向一个函数。那么指向一个什么样的函数呢?或者说指向的函数有什么样的作用呢?其作用就是改变某个页或者某几个页的属性和类型,这个函数的具体实现代码书上并没有写出来,因此我也没有办法去研究。但是我还是说下思路
1,通过CR3寄存器得到当前进程的页目录的基地址(物理内存的页的首地址。具体后面重点讲解)
2,通过函数里的StartAddress地址算出此地址存在于哪个页表
3,再算出具体在哪个页里。
4,得到具体的页的信息,就可以通过指针来修改这个页信息的32位数值。
关于页目录,页表,页的基础知识前面已经涉及到。更深入的内容下面几节中就会涉及。
函数的具体实现:
1,先生成2个MM_REGION结构。为什么呢?比如说你现在有条长长的面包,并且打算把一段用奶油抹上。这个时候出现2种情况:
(1)中间一部分抹上了奶油。这样会出现3段,由于开始的一段还是存在的,所以现在要再生成2段。
(2)从中间某个地方开始到末端全抹上了
2,选择其中一个MM_REGION结构,把传进来的第5,6参数以及长度都赋值进去。
3,把上面的这个结构加入到区块链表里。
4,调用第8个参数传进来的指针所指向的函数。
5,开始研究步骤1出现的两种情况----面包抹奶油的探讨呵呵
探讨好了之后,函数代码也就结束了。返回值是新的区块MM_REGION结构的首地址!!!!!
接下来又出现一个函数 MmAlterRegion(PMADDRESS_SPACE AddressSpace,PVOID BaseAddress,LIST_ENTRY RegionListHead,ULONG StartAddress,ULONG Length,ULONG NewType,ULONG NewProtect,PMM_ALTER_REGION_FUNC AlterFunc)
这个函数有点长,并且不是很难。只要你有足够的耐心,你完全可以看下这段代码,不过我是懒的看了。函数的作用就是修改一个区块其中某个部分的页面的属性。如果成功了,返回值是STATUS_SUCESS
**********************************虚拟地址空间的“保留”到此结束**************给点掌声,谢谢*****