您的位置:首页 > 其它

Windows 内存管理方法(二)

2015-03-12 16:14 337 查看


Windows内存原理

Windows中,我们接触的一般都是线性地址,而其并非真实存在的,而真正的物理地址是利用一段N长的数组来定位的,这样能够完成保护模式。

假设我们没有使用线性地址,那么我们可以直接访问物理地址,但是这样我们在往内存中写东西的时候操作系统更无法检查这块内存是否是可写的。这样会造成非法覆盖等,最后操作系统的内核函数可能就被覆盖了。

由于操作系统安全的需要催生了虚拟地址的应用,在CPU中有MMU(Memory
Manager Unit 内存管理单元),专门负责线性地址和物理地址之间的转化,在我们每次读写内存的时候,从CPU的结构来看都要经过ALU,ALU拿到虚拟地址以后就会通过总线传输给MMU转化成物理地址以后再把数据读入寄存器。

虚拟内存:

编程时我们面对的都是虚拟地址,win32中对于每个进程来说都拥有4G的虚拟内存(4G虚拟内存中,高2G内存属于内核部分,是所有进程共有的,低2G内存数据时进程独有的,每个进程低2G内存都不一样),但是需要注意虚拟地址并不是真正存在的,所以不构成任何资源损失,比如我们要在)X80000000的地方写“sga”的时候,操作系统就会将这个虚拟地址映射到一块物理地址A中,写这块虚拟地址就相当于写入物理地址A,但是假如我们申请一段1Kb的虚拟内存空间,并未读写,系统是不会分配任何物理内存的,只有当虚拟内存要使用的时候操作系统才会分配相应的物理空间。

虚拟内存的管理方式(通过一堆数据结构来实现内存管理)

在EPROCESS中有个数据结构如下:

内存空间:

typedef struct _MADDRESS_SPACE

{

PMEMORY_AREA MemoryAreaRoot ; //这个指针指向一颗二叉排序树,主要是这个情况下采用二叉排序树能加快内存的搜索速度

...

...

...

}MADDRESS_SPACE , *PMADDRESS_SPACE ;

段结构节点(程序段的内存分配):

typedef struct _MEMORY_AREA

{

PVOID StartingAddress ; //虚拟内存段的起始地址

PVOID EndingAddress ; //虚拟内存段的结束地址

struct _MEMORY_AREA *Parent ; //该节点的老爹

struct _MEMORY_AREA *LeftChild ; //该节点的左儿子

struct _MEMORY_AREA *RrightChild ; //该节点的左儿子

...

...

...

}MEMORY_AREA , *PMEMORY_AREA ;

这个节点内主要记录了已分配的虚拟内存空间,如果要申请虚拟内存空间就可以来此处创建一个节点,如果要删除空间同样只需要把对应节点删除,但是其中还要设计平衡二叉树的操作。我们在分配虚拟内存空间的时候系统就会找到这棵树,然后通过一定的算法便利此树,找到符合条件的内存间隙(未被分配的内存空间),创建个节点挂到这棵树上,返回起始地址就完成了。

物理内存:

物理内存的管理是基于一个数组的,win32下分页是4kb一页,假设我们物理内存有4GB,那么windows会将这4GB空间分页,分成4GB/4KB=1M页,那么每页(就是4KB)的物理空间都由一个叫PHYSICAL_PAGE的数据结构管理,这个数据结构可以看做是一个数组,这个数组有1M个元素,整好覆盖了4GB的物理地址。

物理内存的管理

在内核中有3个队列,这些队列内的元素就是PHYSICAL_PAGE结构:

A. 已分配的内存队列:存放正在被使用的内存;

B. 待清理的内存队列:存放已经被释放的内存,到那时这些内存还没有被清理(清零)

C. 空闲队列:存放可以使用的内存

系统管理流程:

1) 每隔一段时间,系统就会自动从B队列中提取队列元素进行清理,然后放入C队列中;

2) 每当释放物理内存的时候,系统将要释放的内存从A队列中提取出来,放入B队列中

3) 申请内存的时候,系统将要分配的内存从C队列中提取出来,放入A队列中

映射

主要要了解虚拟内存到物理内存的映射,在win32中,虚拟内存有32bit的地址总线,在CPU中存在个CR3寄存器,里面存放了每个进程的页目录地址

我们将转换过程分成下面几步看

1) 根据CR3(CR3中的值是物理地址)的值我们可以定位到当前进程的页目录基址,然后通过虚拟地址的高10位作偏移量来获得指定的PDE(page
directory entry),PED内容有4字节,高20位部分用来做页表基址,剩下的比特位用来实现权限控制和判断页是否在内存中之类的事,系统只要检测相应的比特位就可以实现内存的权限控制。

2) 通过PDE提供的基址加上虚拟内存中的10位(21-12)做偏移量就找到了页表PTE(page
table entry)地址,然后PTE的高20位就是物理内存的基址了(其实就是PHYSICAL_PAGE的下标),剩下的比特位同样用于访问控制。

3) 通过虚拟内存的低12位加上PTE中高20位作为基址就可以确定唯一的物理内存了。

CPU段式内存管理,逻辑地址如何转换为线性地址

存储方式(分段/分页)

分段机制是必须的,分页机制是可选的,当不使用分页的时候线性地址将直接映射为物理地址,设立分页机制的目的主要是为了实现虚拟存储(分页机制在后面介绍)。先来介绍一下分段机制,以下文字是介绍如何由逻辑地址转换为线性地址。

分段机制在保护模式中是不能被绕过得,回到我们的seg:offset地址结构,在保护模式中seg有个新名字叫做“段选择子”(seg..selector)。段选择子、GDT、LDT构成了保护模式的存储结构,GDT、LDT分别叫做全局描述符表和局部描述符表,描述符表是一个线性表(数组),表中存放的是描述符。

“描述符”是保护模式中的一个新概念,它是一个8字节的数据结构,它的作用主要是描述一个段(还有其他作用以后再说),用描述表中记录的段基址加上逻辑地址(sel:offset)的offset转换成线性地址。描述符主要包括三部分:段基址(Base)、段限制(Limit)、段属性(Attr)。一个任务会涉及多个段,每个段需要一个描述符来描述,为了便于组织管理,80386及以后处理器把描述符组织成表,即描述符表。在保护模式中存在三种描述符表
“全局描述符表”(GDT)、“局部描述符表”(LDT)和中断描述符表(IDT)(IDT在以后讨论)。

(1)全局描述符表GDT(Global
Descriptor Table)在整个系统中,全局描述符表GDT只有一张,GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

(2)段选择子(Selector)由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的,如图三①步。段选择子是一个16位的寄存器(同实模式下的段寄存器相同)

段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符(如图三①步)。然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址(如图三②步),段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。例如给出逻辑地址:21h:12345678h转换为线性地址

a. 选择子SEL=21h=0000000000100
0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1

b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

(3)局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图五

LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如图五,如果装载的是Selector
2则LDTR指向的是表LDT2。举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。

1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR

2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11
1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h

3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h

4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector
1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)

由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。

存储方式是保护模式的基础,学习他主要注意与实模式下的存储模式的对比,总的思想就是首先通过段选择子在描述符表中找到相应段的描述符,根据描述符中的段基址首先确定段的位置,再通过OFFSET加上段基址计算出线性地址。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: