鱼C论坛

 找回密码
 立即注册
查看: 2255|回复: 0

[学习笔记] X86汇编语言-从实模式到保护模式—笔记(47)-第16章 分页机制和动态页面分配(2)

[复制链接]
发表于 2018-1-14 10:14:11 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
16.3 使内核在分页机制下工作
【16.3.1 创建内核的页目录表和页表】
页功能必须在保护模式下才能开启。
要开启页功能,必须先做好各项准备工作。
现在的情况是:内核已经加载完毕,所有的部分都已经存在于内存中。段在内存中的位置已经固定。这就要求页功能开启后,线性地址和实际物理地址的数值应该相同(因为它们在开启页功能前就已经存在)。采取的做法是:将低端1MB的内存空间特殊处理,使这一部分内存的线性地址和经过页部件转换之后的物理地址相同即可。这样做的好处是,内核不用做任何变动即可在分页机制下正常工作。
【16.3.2 任务全局空间和局部空间的页面映射】
每个任务都有自己的页目录表和页表,当任务创建时,它们一同被创建。当任务执行时,页部件使用它们访问任务自己的私有内存空间(页面)。但是,任务的页目录表和页表不能只包含任务的私有页面。如果不是这样,当任务调用内核服务时,或者换句话说,进入0特权级的全局地址空间执行时,地址转换将无法进行,因为任务的页目录和页表里没有登记内核所占用的那些物理页面。
内核就是所有任务共用的,它应当属于每个任务的全局空间。
一般来说,全局地址空间占据着任务的4GB地址空间的高2GB,对应的线性地址范围是0x80000000-0xFFFFFFFF;而局部地址空间则使用低2GB,对应的线性地址范围是0x00000000-0x7FFFFFFF。地址空间的分配必须在每个任务的页目录中体现,页目录的前半部分指向任务自己的页表;后半部分则指向内核的页表。否则的话,当转到内核中执行时,是无法完成地址转换的,因为找不到对应的目录项和页表项。
在任何任务内,在任何时候,如果段部件发出的线性地址高于等于0x80000000,指向和访问的就是全局地址空间,或者说内核。
为此,我们要修改内核自己的页目录表,甚至是内核各个段的描述符,将内核挪到虚拟地址空间的高端,也就是虚拟地址空间中,从0x80000000开始的一段连续区域。
内核程序此时已经存放在了物理内存中某段位置处,不能再移动了,我们在上段中说的“将内核挪到虚拟地址空间的高端”,是将代表内核代码位置的线性地址从低端移动到高端,而不是将已经加载到物理内存里的内核指令移到物理内存的高端,只是移动线性地址而已,就是这个意思。移动了之后,还要保证变动之后的线性地址经过转换后同样指向真正的物理地址处。要将线性地址从低端移动到高端,这就涉及到要修改页目录表PDT,需要访问它,知道它的物理地址。目前的状态是我们已经开启分页功能,在分页机制下,程序只能使用线性地址,访问内存必须先访问页目录表和页表,通过它们转换之后的地址才是能够发送到内存芯片的物理地址,我们现在就算是知道页目录表的物理地址,也没用。或者说我们现在要访问页目录表,但却还要通过页目录表、页表进行地址转换之后才能访问内存中的页目录表。这又需要页目录表中有一个目录项能指向页目录表自己。否则,访问一个并未在页目录表和页表内登记的项,会引发处理器异常中断。

下面代码第969~973行的分析:【蓝色小字体部分】
要将内核所在的那1M物理地址映射到全局线性地址空间(书中设计的要映射的全局线性地址空间起始处为0x80000000)的意思是:
用全局(高端)线性地址转换后得到的物理地址能够访问到真正的、正确的内核数据。前面的程序中,我们已经把物理内存1M以下的256个页面地址登记到了物理地址0x00021000处的页表中的前面256个页表项中,该页表现在由页目录中的第0目录项(也就是第1个目录项)指向。根据前面的规则:全局地址空间对应着线性地址0x80000000—0xFFFFFFFF的范围,局部地址空间对应着线性地址0x00000000—0x7FFFFFFF的范围。我们可以知道页目录表中第0目录项是由局部地址空间的线性地址来映射的,而此时我们是要映射到全局地址空间的线性地址上(也就是高端地址,即0x80000000以上的线性地址数值)。所以必须根据线性地址转换物理地址的规则来反推,得到一个指向真正内核所在物理地址区域的页地址数据、页表地址数据。并按照反推规则将页地址数据填入相应的页表内的相应的项内,将页表的物理地址数据填入页目录表中相应的目录项内即可。前面的程序中,我们已经设置好了页表内的项目,该页表内前面256项已经正确地指向了内核所在物理内存区域,所以我们现在只要做一步工作,就是在页目录表内某个项目(与低端线性地址不同的、处于高端线性地址范围内的页目录项)中填写包含真实物理内存1M以下页面的那个页表的物理地址,那么就能顺藤摸瓜,找到那个页表,再找到内核所处的那些页面。也就是说,建立了映射关系,也就叫映射到全局地址空间去了。
假设这是全局空间内某个高端线性地址(32位二进制形式表示):
xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
中间10位用作索引,用来寻找页表内的偏移项,即某个页面的,因为位置0x00021000处的页表已经正确指向内核所在物理内存区域所规划设计的页面,所以这里我们不用理会。低12位是页面内的偏移值,用来定位具体的指令,页表已经正确指向页面,那么这些偏移肯定也是正确的,我们就更不需要理会了。所以我们需要处理的就是页目录内的某个项目。让其项目内的数值为0x00021000,也就是指向我们已经安排好了的那个页表。32位线性地址的高10位“xxxxxxxxxx”作为页目录表内的索引值,用该索引值乘以4,得到一个值,这个就是页目录内目录项所在的偏移,按照本章的安排,这个目录项应该指向物理地址0x00021000处的页表,所以只要在这个目录项里填上0x00021000即可,这样处理器就能找到相应的页表,再继续往下访问。
现在我们来回顾一下,我们要映射的是内核区域,内核区域的大小不超过1M(因为我们只做了那么小的内核),页面的数量不超过256个,用1个页表就能全部指向完并且还有四分之三的空余。而1个页表是由页目录内的一个目录项指向,也就是说,我们要映射的内核区域(1M以下,不超过256个页面)是由页目录表内的一个目录项指向的。因此实际上我们要建立映射关系的话,只要高端线性地址的高10位能够正确地映射到该目录项就可以了。
此外,本章中,设定的是从线性地址0x80000000开始映射内核,要在高端线性地址空间映射1M的物理内存区域,那么需要映射的线性地址范围也应该是0x80000000—0x800FFFFF。我们可以看到在这个范围内,线性地址的高10位(实际上是高12位,但我们只取有用的高10位来表达)没有发生变化,这高10位代表页目录表内的索引值,它不发生变化的话,页目录表内的目录项也不会发生改变,因此我们只要把0x80000000的高10位作为索引,乘以4,作为页目录表内的偏移值,然后将偏移值所在的那个目录项的内容修改为0x00021000,那么用0x80000000开始的这1M范围内的线性地址数值来进行寻址时,页目录内的找的永远都是同一个目录项(因为永远都是用0x80000000的高10位来定位页目录的项),该目录项经我们改写后,反正就会指向物理地址0x00021000的那个页表。
最后,因为0x80000000与0x00000000的唯一不同之处仅在于最高位,1个是“1”,1个是“0”,其余部位都是相同的。所以映射之后,它们原来指令之间的偏移与现在也是一样的,也就是说页表内容是不用修改的。

如果页目录表内最后一个目录项指向当前页目录表自己,那么,无论任何时候,当线性地址的高20位是0xFFFFF时,访问的就是页目录表自己。理解:假设线性地址为0xFFFFF???,那么线性的高10位全部为“1”,也就等于0x3FF,乘以4,等于0xFFC。0xFFC这个偏移指向最后一个目录项,从中取出页表的物理地址(我们先前已经在这里填入页目录表自己的地址,因此这里实际就是页目录表的地址),这个地址指向页目录表自己,也就是说,把页目录表当成页表来访问。找到页表后,我们再次用线性地址的中间10位(也全部是1,值等于0x3FF)当成页表内的索引,乘以4,得到0xFFC,还是指向页表内的最后一项,这一项还是指向页目录表自己,这里就是页目录表自己当成页来访问了。
现在有了两个前提条件:
条件1:我们从线性地址0x80000000开始映射1M内核,而前面我们已经分析过了高10位为“1000 0000 00”的线性地址足够映射1M内核,也就是高10位值为0x200的线性地址足够映射1M内核,也就是说处理器转换地址第一步用到的索引值是不会变的,即0x200×4是不用变的,即页目录表内偏移0x800的地方存放0x00021000就可以达到访问内核的要求。【从基地址开始的偏移值不受线性地址变化的影响】
条件2:现在已经处于分页机制下,不能再用页目录表的实际物理地址0x00020000来访问页目录表,必须通过线性地址来访问页目录表自己。而当页目录表内最后一个项目指向页目录表自己时,只要线性地址的高20位为0xFFFFF,那么访问的就是页目录表自己。
综合上面这两个条件:
所以为了访问页目录表内偏移0x800的那个目录项,第969行,将高20位为0xFFFFF的线性基地址0xFFFFF000给寄存器EBX,这个0xFFFFF000指向页目录表的起始处。第970行,将要映射的0x80000000给寄存器ESI;第971行,ESI右移22位,这样只保留要映射的地址的高10位,这高10位是用来做索引值,因此它还要乘以4。所以第972行,将ESI左移2位,也就是乘以4。这样寄存器ESI的值就成了页目录表真正的偏移值了(已经不再是索引值了),第973行,将基地址0xFFFFF000加上偏移0x800,这就是我们要修改的页目录项,在其内填入第0个页表的物理地址0x00021000。
经过上面的处理,页目录表中有两个目录项指向0x00021000处的页表。第0项和偏移0x800处的那个项。第0项对应着线性地址0x00000000—0x000FFFFF,偏移0x800项对应线性地址0x80000000—0x800FFFFF。
仅仅修改页目录表是没有用的,如果段部件给出的线性地址并不在0x80000000以上,是没有用的。因此,必须修改与内核有关的段描述符,包括全局描述符表(GDT)自己的线性地址。一旦开启页功能,除页目录表和页表的地址外,其他所有地址都是线性地址,即使是在访问GDT和LDT的时候,内核就更不用说了,不可能因为它靠近硬件就能搞特殊。
因为0x80000000是一个特殊的数字,因此我们只需要访问全局描述符表(GDT),将所有描述符高字部分的最高位置“1”即可。第977~979行,先取得GDT的线性基地址,并传送到EBX寄存器,准备开始访问GDT内的段描述符。第981~986行,依次找到内核栈段、文本模式下的视频缓冲区段、公共例程段、内核数据段和内核代码段的描述符,并将每个描述符的最高位改成“1”。在这里,EBX寄存器提供了GDT的基地址:0x10、0x18、0x20等这些数提供了每个描述符在表内的偏移量;在偏移量的基础上加4,就是每个描述符的高32位。唯一没有修改的是0~4GB内存段的描述符,它本身就是为访问整个内存空间而存在的,不需要修改。
修改段描述符的时候不会对寄存器造成影响,因为段描述符所代表的段的基地址已经加载到段寄存器的高速缓存部分。所以更新GDT后,我们要显示地刷新段寄存器的内容。代码段寄存器的内容刷新一般使用转移指令完成。第992行,使用远转移指令jmp跳转到下一条指令的位置接着执行。这将导致处理器用新的段描述符选择子0x38访问GDT,从中取出修改后的内核代码段描述符,并加载到其描述符高速缓存器中。同时,这也直接导致处理器开始从内存的高端位置取指令执行。
第995~999行,重新加载段寄存器SS和DS的描述符高速缓存器,使它们的内容变成修改后的数据段描述符。注意,这些段在内存中的物理位置并没有改变。特别是栈段,因为仅仅是线性地址变了,栈在内存中的物理位置并没有发生变化,所以栈指针寄存器ESP仍指向正确的位置。段寄存器ES没有修改,因为它指向整个0~4GB内存段,内核需要有访问整个内存空间的能力。

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-12-29 15:07

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表