ZeroOS—第2章—内存管理模块(1)

IA32内存管理结构简介

内存管理模块是硬件相关的,所以在编写内存管理模块前,我们需要了解我们编写内存管理代码所基于的硬件信息——IA32内存管理机制,这里涉及到的概念有虚拟地址、线性地址、物理地址、分段机制、分页机制和段页机制。这篇文章就对这些概念逐一进行讲解,其中的内容基本来自《IA-32卷3:系统编程指南》(需要的留言),如果有不足或者错误,还请斧正。

虚拟、线性和物理地址

这三个地址在内存寻址的过程中的先后顺序如下:虚拟地址—(分段机制)—>线性地址—(分页机制)—>物理地址,其中线性地址到物理地址的转化中,如果没有开启分页机制,那么线性地址就是物理地址,否则的话还要进过分页机制的映射,而分段机制是不能被关闭的,虚拟地址必须经过分段机制的映射才能变为线性地址。下面对三个地址的概念和它们之间的详细关系进行逐一讲解。

1.虚拟地址(逻辑地址)

虚拟地址是在代码中使用的地址,在编译生成可执行文件后,其中的每条指令、每个数据都有自己唯一的地址,这个地址就是虚拟地址。可执行文件,包括内核和用户程序,在链接的时候都会有一个起始地址,内核的起始地址(入口地址)由内核的链接脚本给出,这个地址确定了内核第一条指令的虚拟地址,其他所有的指令和数据都会根据这个起始地址重新进行确定其自身的虚拟地址,这个过程被称为重定位。这个工作由链接器根据链接脚本来完成,在根据链接脚本确定了每个节的位置后,节中的标签的位置也就确定了下来,此时链接器就会把所有引用了标签的指令,比如jmp label或mov $0,(label)指令,将label标签所对应的虚拟地址填入指令中,这样可执行文件就可以正确的寻址了。

这里要注意的是上述过程都是在程序的编译连接阶段完成的,此时程序都没有加载进内存呢,所以这个地址是给代码用的(或者说程序员)。对于IA32机器来说,虚拟地址是从0到4GB的,它是不管实际物理内存有多少的,哪怕实际物理内存只有4MB,它也能让代码在4GB的虚拟地址空间里正常运行,你就说牛批不牛批吧。那么具体是怎么实现的呢,这里我将原理大概说一下:假如你有一列10米长的火车,需要在距离为1000米的两个地点来回跑,你说这个铁路该怎么铺?那就在两个地点铺上1000米的铁路就完了呗,但是不巧的是你只有两节长度为20米的铁轨,哦吼。。。但是你还有一个效率奇高的施工队,效率有多高呢,拆一段铁路只用0.5秒,铺一段铁路也只用0.5秒,而且永不停工,永不喊累。不知道你想到没有,反正因特尔的工程师早就想到了一个奇招,先铺上40米让火车跑,当火车跑到第二节铁路时把第一节铁路拆下来铺在第二节铁路的前面,这样就能让火车只用两节20米的铁轨在1000米的距离上自由通行。我刚理解这个思想的时候觉得简直了,这是人干的事儿???你这包工头也太黑心了吧,啊,,,不是,你这想法也太巧妙了吧。

2.线性地址

如果将虚拟地址比作分段机制的原料,那么线性地址就是分段机制的产品。在分段机制根据虚拟地址算出线性地址后,分段机制的任务就完成了,这个时候需要看处理器是否开启了分页,如果没有开启分页,那么线性就是下面所说的物理地址,若果开启了分页,则交给分页机制进行处理。

线性地址是系统编程时需要了解的概念,编写用户程序是用不到这个概念。

3.物理地址

物理地址就是最后用来在物理内存中确定数据位置的,它会被放在地址总线上,用于寻址。

分段和分页机制

在详细讲解分段和分页机制前,我们先对这两个机制的关系进行一个简要的叙述,它们的关系同时也反映了在IA32架构下的内存寻址的过程。下面我们就下图进行一一讲解,如有不足,欢迎斧正。

ZeroOS—第2章—内存管理模块(1)_第1张图片

1.分段机制

分段机制中最重要的是段描述符表(下文统称段表),顾名思义其中包含的就是段描述符了,如图是段描述符的定义:

ZeroOS—第2章—内存管理模块(1)_第2张图片

属性位(G),表示该段描述符的粒度,和段的界限结合后确定段的长度,为0表示粒度为1B,最大支持段长度为1MB。为1表示粒度为4KB,最大支持段长度为4GB。

属性位(D/B),指定该段默认操作数的大小,为0表示为16位的段。为1表示为32位的段,在本课题内核中,该值为1。

属性位(AVL),这个位保留给操作系统使用。

属性位(P),该位为1表示该段存在,否则访问该段会导致异常。

属性位(DPL),描述符特权级,0最高,3最低。

属性位(S),表示描述符的类型,为0表示系统段,为1表示代码段或者数据段。

属性位(TYPE),表示段的类型,依次表示是否可执行、是否位特权级依从、是否可读、是否已访问。

通过段描述符就可以确定一个内存段的起始地址和长度,当我们访问内存时,必须确定是在哪个段描述符对应的段中进行访存的,可是段表中有那么多的段描述符,CPU是怎么确定用哪个呢,这里我们再引入一个概念——段选择子,其定义如图:

ZeroOS—第2章—内存管理模块(1)_第3张图片

其中第3至15位确定段描述符在段表中的位置,第2位确定用全局段表(GDT)还是局部段表(LDT),这两个的区别在哪呢,可以简单地理解为全局段表是可以给所有的进程使用的,而局部段表是给对应进程使用的,为什么说简单理解呢,因为目前ZOS不用LDT,内存管理的主要工作都在分页机制上,学习分段机制的目的在于尽可能的绕过分段机制。对于请求特权级会在后面说,因为还要介绍当前特权级和描述符特权级。

有了段表、段描述符和段选择子,我们就可以详细了解分段机制的运行过程了,首先需要存储这三个信息,段表的起始地址存储在GDT寄存器中,通过LGDT加载一个6字节内存区域至GDT寄存器,其中包括GDT的基地址和大小(以字节计),段选择子存储在段寄存器中(比如DS、CS),通过MOV指令即可载入段寄存器。

下面我们通过一个访问内存数据的例子(AT&T风格):movl 0x100000,%eax,这条指令将内存0x100000处的数据复制进寄存器eax,其中分段机制运行如下:CPU从ds(默认的数据段寄存器)中找到数据段描述符,检查权限(详细见特权级小节),成功后检查要访问的地址是否在数据段描述符的范围中,成功后将数据段的基地址(GRUB加载内核后这个值为0)加上要访问的地址得到线性地址:0x100000。分段机制的任务就完成了,接下来讲解分页机制的工作过程(前提是开启了分页)。

2.分页机制

分页机制是选用的,默认是关闭的,GRUB在加载完内核后也是默认关闭分页机制的,所以我们要先打开分页。这个开关叫分页标志:CRO寄存器的第31位是分页机制的标志位(PG),置1打开分页硬件,这里需要注意的是一旦PG被置位,处理器马上就会进入分页模式,所以在打开这个标志前,应该将分页机制所需的数据全被准备好。

还有一个标志位是为了使用4MB的超级页,与它相对的是4KB的普通页(暂且这么叫吧)。CR4寄存器的第4位是分页机制的页尺寸扩展标志位(PSE),置1允许使用4MB的超级页,否则只使用4KB的页。

分页机制采用了二级页表,通俗来讲就是说线性地址映射到物理地址需要经过两个页表的映射。

分页机制中首先要了解页目录(第一级),它是一个4KB连续的物理内存,其中存储着1024个页目录项,每个页目录项代表大小为4MB的物理内存,但是要注意的是每个页目录项所表示的4MB的物理内存不一定存在也不一定连续。其数据结构如图:

ZeroOS—第2章—内存管理模块(1)_第4张图片

对照英文也可以看明白大概意思,每一位的详解如下:

第31至12位确定了一个4MB页的首地址,所以地址必须是4MB对齐的,但实际上是指向了一个4KB的页表页,而不是一个4MB连续的物理页。

第11至9位保留给操作系统使用。

第8位是全局标志,若该标志和CR4中的启用全局页标志都置1,则当CR3寄存器被装载或发生任务切换时,该页在TLB中的缓存不会失效。

第7位确定页的尺寸,该标志只用于页目录表项,为0时页尺寸为4KB,为1时页尺寸为4MB。

第5位为访问标志,由硬件置位,但是需要由软件清零。

第4位是页级高速缓存禁用标志(PCD),为1时该页的高速缓存将会被禁用,即每次访问都不会访问TLB,而是直接访问内存,当在页表中映射一些映射在内存的硬件时,应该将此位置1。

第3位是写直达标志(PWT),为1时启用页表的直写高速缓存机制。

第2位是用户/管理员标志(U/S),为0时只有CPL为0的代码可以访问(就是内核代码)能访问该页。

第1位是页存在标志(P),为1表示对应的页存在,如果访问P=0的页,则会导致缺页中断。

页目录项指向一个页表(第二级),页表也是一块4KB连续的物理内存,其中存储的是页表项,页表项指向最后的物理页(页框),它的结构如图:

ZeroOS—第2章—内存管理模块(1)_第5张图片

其中每一位意义的详细解释如下:

第31至12位为物理地址的高20位。

第7位是页属性表索引(PAT)。

第6位为脏页标志(D),由处理器置位,由操作系统清零。其余位与页目录项中的意义相同。

页表项所指的物理页也是一块4KB连续的物理内存,其中存储的就是我们写入的数据,比如指令和相关数据什么的。

看到这里有没有看出来这三者的关系?是不是有一点像C语言中的数组(物理页)、数组指针(页表项)和二级数组指针(页目录项)。只不过指针中存储的全部是地址信息,而页表项和页目录项中还存储了相关属性。通过这个关系我们可以看出,这三者中只需要存储页目录就可以了,这就是CR3寄存器——页目录地址寄存器,这个寄存器保存了页目录的物理地址,该地址必须是4KB对齐的。

有了上述概念后就可以详细解释分页机制的工作过程了。其中映射4KB页的工作过程:

ZeroOS—第2章—内存管理模块(1)_第6张图片

对于使用4KB页映射的线性地址来说,该线性地址被分为三个部分,根据高十位的页目录索引找到该线性地址对应的页目录,这样就找到了线性地址对应的页表页,然后根据中间十位的页表索引找到对应的页表项,这样就找到线性地址对应的页框,最后将页框的起始地址和线性地址低12位的页内偏移相加,就得到了最后的物理地址。

映射4MB页的工作过程:

ZeroOS—第2章—内存管理模块(1)_第7张图片

对于使用4MB页的线性地址,只需要根据线性地址高十位的页目录索引找到对应的4MB页,然后将超级页的起始地址和线性地址的低二十二位相加,就得到了最后的物理地址。

IA32中的特权级详解

当前特权级(CPL)

CPL 是当前执行程序或任务的特权级.它存在 CS 段寄存器和 SS 段寄存器的第 0 位和第 1 位中。一般地,CPL 与当前指令所在代码段的特权级相等。当进程的控制流程转到一个不同特权级的代码段时,处理器就改变 CPL。当访问一致性代码段时,对 CPL 的处理会略有不同。一致性代码段可以被任何数值上等于或者大于 (特权较低) 本一致性代码段 DPL 的代码访问。同样, 当处理器访问一个与 CPL 特权级不一样的一致性代码段时, 不改变 CPL。

请求特权级(RPL)

RPL 是赋给段选择子的取代性特权级,存储在段选择子的第 0 位和第 1 位。 处理器检验 RPL 的同时也检验 CPL 来决定对段的访问是否被允许。即使请求访问的进程或任务有足够的特权(也就是说 CPL 权限够了——译注)去访问一个段,但是,如果 RPL(即指向将要去访问的这个段的段描述符的段选择子中的 RPL——译注)的特权级不够,访问会被拒绝。也就是说,如果这个段选择子的 RPL 在数值上大于 CPL,RPL 就取代 CPL,反之亦然。RPL可用来确保特权代码不代表应用程序去访问的一个段, 除非这个应用程序本身有这个段的访问特权。

描述符特权级(DPL)

DPL 是段或门的特权级。它存储在段或门的描述符的DPL 域中。一旦当前执行的代码段试图访问一个段或门时,处理器会将那个段或门的 DPL 与 CPL 以及那个段或门的选择子的 RPL(本节后面进行描述)进行比较。根据所访问的段或门的类型,对 DPL 的解释也会不一样:

1.数据段。DPL 指明访问这个段的进程或任务的特权级可以具有的最大数值。比如,如果某个数据段的 DPL 是 1,只有运行在 CPL 为 0 或 1 的进程才可以访问它。

2.非一致性代码段(不使用调用门) 。DPL 指明访问这个段的进程或任务应该具有的特权级。比如,某个非一致性代码段的 DPL 为 0,那么只有 CPL为 0 的进程才能访问它。

3.调用门。 DPL 指明能够访问这个调用门的当前进程或者任务的特权级应该具有的最大数值。 (这个访问规则同样适用数据段。 )通过调用门访问的一致性代码段和非一致性代码段。DPL 指明能访问这个段的进程或任务的特权级的最低数值。比如,一个一致性代码段的 DPL为 2,那么 CPL 为 0 或 1 的进程就不能访问它。

4.TSS。DPL 指明能够访问这个 TSS 的当前进程或者任务应该具有的特权级的最高数值。 (这个访问规则同样适用数据段。 

个人理解

上述都是IA32开发手册上的内容,对于我自己而言,我是这样理解特权级的:DPL只与CPL和RPL中数值最大(即特权级最低)的那个进行比较,如果DPL大于等于其中的最大值,那么特权级检查通过,否则禁止访问并产生一个#GP异常。对于一致性和非一致性,非一致性代码段的就是为了防止低特权级访问高特权级的数据,而一致性代码段则是为了防止高特权级访问低特权级数据。

有了这些基本概念应该就够了,下一篇我们就用这些概念再加一个简单的内存管理算法对内存管理模块进行设计。

你可能感兴趣的:(ZeroOS)