第19章 分页机制和动态页面分配

第19章 分页机制和动态页面分配

内存置换:每个段描述符有A(Access)位,每当访问一个段时,处理器将其置为1。A位的清零由操作系统定时进行,借此可以统计段的访问频度。当内存不够用的时候,它可以将那些较少访问的段换出到磁盘上,以腾出空间来给马上要运行的段使用。

为什么要有分页功能:每个段的长度不定,在分配内存时,随着段在内存中的调入和调出,内存空间会变得支离破碎。

  • 当一个较大的段要调入内存时,虽然内存里确实还有剩余可用空间,但找不到一个长度足够的连续空间来存放这个段。
  • 如果内存中的空闲区域远远大于要加载的段,则虽然分配会成功,但太过于浪费。

分页功能:分页功能从总体上说,是用长度固定的页来代替长度不一定的段,借此解决因段长度不同而带来的内存空间管理问题。

该章的内容感觉比前面的章节都难,除了分页机制本身的知识,在书中还涉及到了一些技巧:如访问页目录表、页表自身,如果分页基础知识没有了解,越往后看会越懵逼。

在阅读的过程中,一开始比较懵,但后来掌握一个方法后就容易理解多了:就是多从线性地址转成物理地址的角度去思考。

因为书中很多的地方要通过物理地址构造线性地址,比如页目录表的物理地址是0x21000,构造的线性地址是0xFFFFF000,反向思考有点难,但是正向从线性地址0xFFFFF000转成物理地址0x21000进行思考并尝试自己计算,就很容易理解了。

分页机制概述

简单的分页模型

分段机制:每个程序或任务都有自己的段,这些段都用段描述符定义。当要访问内存时,就用段地址加上偏移量,段部件就会输出一个线性地址。在单纯的分段模式下,线性地址就是物理地址。

image

全局空间范围:全局空间源于可以在全局描述符表GDT中定义8192个段描述符,而且假定每个段描述符对应一个4GB的段,最大可以达到32TB。

为什么是4GB的段?
因为段界限是20位,最大2^20字节。如果粒度位G位为1,则表示以4KB为单位。
所以:总大小 = 2^20 4KB = 2^20 * 2^12 = 4GB

局部空间范围:局部空间范围就是LDT中定义的8192个段描述,和GDT类似,也是32TB。

分页机制:任务的全局空间和局部空间的总内存范围为64TB,但是实际物理内存才4GB。实际实现上是通过内存置换实现的。随着段在内存和硬盘之间的调入调出,内存空间会变得支离破碎,所以引入分页机制解决这个问题。

为了支持分页功能,从32位处理器开始内置了页管理部件,简称页部件,而且页部件是可以选择开启和关闭的。

页部件工作原理图:

image

页的划分:将物理内存分成大小相同的页,页的最小单位是4KB,也就是4096字节,用十六进制数表示就是0x1000。

  • 第1个页的物理地址是0x00000000;
  • 第2个页的物理地址是0x00001000;
  • 第3个页的物理地址是0x00002000;
  • ……
  • 最后一个页的物理地址是0xFFFFF000。

4GB内存总共可以划分为0x100000(1048576)个页。

image

虚拟内存:引入并开启了页功能之后,物理内存是按照页来访问的,不是按照段来访问的,不能再用于定义段。此时,我们就需要假想一个新的4GB内存空间,并在这个假想的内存中分段,再将这个段拆分并映射到物理内存中的页。在这里,假想的内存空间也叫虚拟的内存空间,我们称之为虚拟内存。

每个任务的32TB全局空间和32TB局部空间不再是映到4GB物理内存,而是映射到4GB虚拟内存。值得强调的是,开启分页后,所有任务的地址空间是强制分离的。所以,也就等于为每个任务都提供了4GB的虚拟内存。即,要求每个任务都有自己独立的4GB虚拟内存,而不是共用同一个4GB虚拟内存。

虚拟内存的全局和局部空间:从容量上说,每个任务的虚拟内存和真实的物理内存一样大。对于只能访问4GB物理内存的计算机系统来说,每个任务的虚拟内存也是4GB,线性地址范围是0到0xFFFFFFFF。因为每个任务是由全局部分和私有部分组成的,所以,这个4GB的虚拟内存空间也被分成两部分:

  • 从线性地址0到0x7FFFFFFF的这2GB,属于任务的私有内存空间;
  • 从线性地址0x80000000到0xFFFFFFFF的这2GB,属于任务的全局内存空间。

所有任务共享一个全局部分,每个任务都有自己独立的2GB虚拟内存空间。

image

程序映射到虚拟内存:所谓映射,就是计算每个段在虚拟内存中的位置和长度,并创建和安装它们的描述符。

  • 内核的段来自内核自己的可执行文件,而且会映射到虚拟内存的高2GB。
  • 任务私有部分的段来自它自己的可执行文件,而且映射到虚拟内存的低2GB。

当一个程序加载时,操作系统既要在虚拟内存中分配段空间,又要在物理内存中分配相应的页面。

  • 第一个步骤是寻找虚拟内存中空闲的段空间。
  • 将虚拟内存中的段按4KB拆分,将虚拟内存加载到物理内存。

image

将程序映射到虚拟内存的主要工作是规划所有段在内存中的布局和位置,并根据这些信息来创建段描述符,和未开启分页的方式是一样的。

虚拟内存映射到物理内存:程序映射到虚拟内存,那么虚拟内存是怎么映射到物理内存的呢?

1)引例:创建一个数据段描述符,描述符中的基地址是0x002000c0,段的界限值为0x2007,粒度是字节,段是向上扩展的。

mov edx,[1008] ;从段内1008偏移的位置取一个双字

分析开启分页和未开启分页的情况:

  • 未开启分页:0x002000c0 + 0x1008 = 0x002010c8 ,这个就是物理内存地址。
  • 开启分页:0x002000c0 + 0x1008 = 0x002010c8,这个是虚拟内存地址,

2)给虚拟内存也分页:将虚拟内存页也进行分页,每页就有一个索引号。
由于页地址必须是0x1000的整数倍,所以这个段跨越了三个页面。

  • 地址为0x00200000(索引号:0x00200)的页;
  • 地址为0x00201000(索引号:0x00201)的页;
  • 地址为0x00202000(索引号:0x00202)的页。

这个段从第0x00200页面内偏移为0xc0的地方开始,在第0x00202页面内偏移为0xc7的地方结束。

image

段部件发出的每个线性地址,它的前五个数字后面加三个0,就是页地址,后面三个数字就是页内偏移。

image

3)虚拟内存页地址对应和物理内存页地址:当一个程序加载时,操作系统既要在虚拟内存分配段空间,又要在物理内存分配相应的页面。假设这三个页面的物理地址分别是0x00002000、0x00004000和0x00007000。则有如下对应关系:

image

图例如下:

image

4)记录虚拟内存页和物理内存页映射关系:页部件只需要将线性地址拆分成页地址和页内偏移,再将页地址修改为真实的页地址即可。那么,页部件又是如何知道某个线性地址对应于物理内存中的哪个页呢?这肯定需要在分配物理页的时候记录这两者之间的对应关系。

既然线性地址的前五个数字,也就是前20位对应着一个物理页地址的前20位,那么,我们在物理内存中建立一个表格,然后用线性地址的前20位作为索引访问这个表格。

  • 索引号0x00000对应表格的第1行;
  • 索引号0x00001对应表格的第2行;
  • 索引号0xFFFFF对应表格的第0x100000行;

所以,这个表格一共有1048576行。每一行都是一个表项,表项的内容是物理页地址的前20位(或者理解为第N页)。每个表项占用4个字节,访问这个表时需要将索引号乘以4。

image

加载程序时,首先在虚拟内存中规划和安排段并创建段描述符。这个段需要三个虚拟页,虚拟页地址的前20位分别是00200、00201和00202。
还需要在物理内存中分配物理页,我们假设分配的物理页分别是0x00002、0x00004和0x00007。

操作系统用线性地址的前20位作为索引,访问表格,将对应的物理页地址写入对应的表项。写入时,只写入物理页地址的前20位。

  • 索引为00200的表项,其内容是00002;
  • 索引为00201的表项,其内容是00004;
  • 索引为00202的表项,其内容为00007。

每个表项占4字节,因此,访问这个表的时候,需要将索引号乘以4以得到表内的偏移量。

image

每个表项4字节,共0x100000项,总大小=0x100000*2^2=4MB

5)页部件将线性地址转换为物理地址

  • 段部件发出的线性地址是0x002010c8;
  • 页部件取出线性地址的前20位作为索引号,索引号是0x00201;
  • 索引号乘以4从表格中获取物理页地址的前20位(物理内存页号),就是0x00004;
  • 将物理页地址的前20位乘以0x1000,再加上线性地址的后3位(0x0c8,页内偏移),就是物理地址0x000040c8

image

6)页映射表:页部件在将线性地址转换为物理地址时,需要查表,我们姑且称之为页映射表。操作系统所要做的,就是寻找空闲页面,把它分配给需要的段,并在页映射表内登记这个页。

每个任务都可以有自己的页映射表,物理内存始终只有一个,物理内存中的页可能分属于不同的任务。当多个任务共用一个物理内存页的时候,操作系统可以将暂时不用的页暂存到磁盘,调入马上就要使用的页,通过这种手段来实现分页内存管理。

image

页目录、页表和页

为什么用层次化的分页结构:页映射表一共有1048576个表项,表的索引从0到0xFFFFF。每个表项都是32位的,共4字节,用来存储物理页地址。这个表的总大小是4MB。在二十世纪八十年代,一台80386计算机的主流配置是拥有2MB内存。这点内存空间,连这个页映射表都存放不了,更何况,每个任务都有一个这样的页映射表。

1048576就是十六进制0x100000,总共这么多0x100000表项,就是0xFFFFF+1。
表的总大小:0x100000 * 4Byte = 0x400000Byte = 4MB
速记:210=1KB,220=1MB,相当于上面呢数字左移20位,单位就转为MB。

虚拟内存的低一半属于私有部分,高一半对应着内核。私有部分所占用的物理页地址登记在页映射表的低一半,全局部分占用的物理页登记在页映射表的高一半。不管页映射表的高一半有几个表项,为了访问它们,低一半的表项都必须存在,即使它们并不对应着实际的物理页。

因为这个原因,显然,页映射表从一开始就必须完全定义,而且不可避免地要占用4MB内存空间。为了解决这个问题,同时又不会浪费宝贵的内存空间,处理器设计了层次化的分页结构。

层次化的分页结构:分页结构层次化的主要手段是不采用单一的映射表,而是页目录表和页表。

image

1)页目录表:将1048576个页归拢到1024个页表之后,接着,再用一个表来指向1024个页表,这就是页目录表(Page DirectoryTable, PDT),和页表一样,页目录项的长度为4字节,填写的是页表的物理地址,共指向1024个表页,所以页目录表的大小是4KB,正好是一个标准页的长度。
2)页目录基址寄存器:每个任务都有自己的页目录表和页表。在处理器内部,有一个控制寄存器CR3,存放着当前任务的页目录表的物理地址,故又叫作页目录基址寄存器(Page Directory Base Register, PDBR)。

每个任务都有自己的任务状态段(TSS),它是任务的标志性结构,存放了和任务相关的各种数据,其中就包括了CR3寄存器域,存放了任务自己的页目录表物理地址。当任务切换时,处理器切换到新任务开始执行,而CR3的内容也被更新,以指向新任务的页目录表。相应的,页目录又指向一个个的页表,这就使得每个任务都只在自己的地址空间内运行。

页目录和页表也是普通的页,混迹于全部的物理页中。它们和普通页的不同之处仅仅在于功能不一样。当任务撤销之后,它们和任务所占用的普通页一样会被回收,并分配给其他任务。

image

到这里层次化的分页结构好像也没有解决内存不够的问题?
看完本章才了解到,这么页表实际上实在申请内存的时候才检查是否存在,不会提前创建。所以就能节省空间。

地址变换的具体过程

引例:假设某个任务加载后,在4GB虚拟地址空间里创建了一个段,段的起始地址为0x00800000,段界限值为0x5000,字节粒度。

当该任务执行时,段寄存器DS指向该段。假设执行如下指令:

mov edx,[0x1050] ;线性地址为0x00801050
;二进制:0000 0000 1000 0000 0001 0000 0101 0000

线性地址转化成物理地址:在处理器内部,页部件将段部件送来的32位线性地址截成3段,分别是:

  • 高10位:是页目录的索引;
  • 中间10位:是页表的索引;
  • 低12位:则作为页内偏移来用。

image

检测点:在分页模式下,某程序运行时,段部件发出一个线性地址0x0C005032访问内存数据。如果该线性地址对应的物理页是0x0000A000,页表的物理地址是0x00003000,那么,操作系统在此程序开始运行前,是如何安排与该线性地址相关的页目录项和页表项的?

参考上面页部件把线性地址转化为物理地址的逻辑,就很容易做了,可以参考下图,红色部分是题目给出的数据。

image

本章代码清单

本章代码实现的功能和第18章的一样,只是采用了分页的机制。

image

使内核在分页机制下工作

创建内核的页目录表和页表

引导程序:引导程序和第15章用的一样,主引导程序已经创建了内核的大部分要素:全局描述符表(GDT)、公共例程段、内核数据段、内核代码段、内核栈,还包括一个用于访问全部4GB内存空间的段。

主引导程序和内核的布局基本没什么变化,内存布局图如下:

image

初始化中断系统:进入内核之后,首先初始化中断系统,创建中断描述符表IDT并安装中断门。

分页前准备

1)准备页目录和页表:内核是在开启分页功能之前就已经加载了,所以开启分页后需要保证之前段部件输出的线性地址等于开启分页后页部件输出的物理地址就可以了。

例如:GDT在内存的基地址是0x00007E00,开启分页前段部件输出的线性地址就是物理地址;开启页功能之后,它还在那个内存位置,这就要求页部件输出的物理地址和段部件输出的线性地址相同。

书中的内核示例只有1MB,1个页表有1024页,1个页4KB,总共可以管理4MB内存,所以只需要一个页目录和页表就可以了。另外页目录和每个页表都必须占用一个自然页,也就是说,它们的物理地址的低12位必须全是零。

书中将页目录放在0x00020000起始的物理页,页表放在0x00021000起始的物理页。图示如下:

image

2)页目录项和页表项的组成:因为页表和页的物理地址要求4KB对齐,就是低12位都为0,页目录和页表中只保存了页表或者页物理地址的高20位;另外12位可以另作它用。

image

  • AVL位被处理器忽略,软件可以使用。
  • G(Global)是全局位。用来指示该表项所指向的页是否为全局性质的(比如,属于所有任务共有的内核部分)。如果页是全局的,那么,它将在高速缓存中一直保存(也就意味着地址转换速度会很快)。因为页高速缓存容量有限,只能存放频繁使用的那些表项。而且,当因任务切换等原因改变寄存器CR3的内容时,整个页高速缓存的内容都会刷新。
  • PAT(Page Attribute Table)页属性表支持位。此位涉及更复杂的分页系统,和页高速缓存有关,可以不予理会,在普通的4KB分页机制中,处理器建议将其置“0”。
  • D(Dirty)是脏位。该位由处理器固件设置,用来指示此表项所指向的页是否写过数据。
  • A(Accessed)是访问位。该位由处理器固件设置,用来指示此表项所指向的页是否被访问过。这一位很有用,可以被操作系统用来监视页的使用频率,当内存空间紧张时,用以将较少使用的页换出到磁盘,同时将其P位清零。然后,将释放的页分配给马上就要运行的程序,以实现虚拟内存管理功能。
  • PCD(Page-level Cache Disable)是页级高速缓存禁止位,用来间接决定该表项所指向的那个页是否使用高速缓存策略。
  • PWT(Page-level Write-Through)是页级通写位,和高速缓存有关。
  • US(User/Supervisor)是用户/管理位。为“1”时,允许所有特权级别的程序访问;为“0”时,只允许特权级别为0、1和2的程序访问,特权级别为3的程序不能访问。
  • RW(Read/Write)是读/写位。为“0”时表示这样的页只能读取,为“1”时,可读可写。
  • P(Present)是存在位,为“1”时,表示页表或者页位于内存中。否则,表示页表或者页不在内存中,必须先予以创建,或者从磁盘调入内存后方可使用。

3)页目录项清0:代码非常容易理解。将页目录清零的原因,主要是使所有目录项的P位为“0”。目录项用于定位对应的页表,如果其P位是“0”,表明该页表并不在内存中,在地址变换时将引发处理器异常中断。

         ;页目录表清零
         mov ecx,1024                       ;1024个目录项
         mov ebx,0x00020000                 ;页目录的物理地址
         xor esi,esi ; esi从0开始
  .b1:
         mov dword [es:ebx+esi],0x00000000  ;页目录表项清零
         add esi,4   ;每个目录项长度为4个字节
         loop .b1

4)登记页目录表的物理地址:将页目录表的物理地址登记在它自己的最后一个目录项内。

;在页目录内创建指向页目录自己的目录项,00020是页目录表项所在物理地址的高位。
mov dword [es:ebx+4092],0x00020003 ;登记在最后一个目录项,p=1内存中,RW=1可读可写
;0000 0000 0000 0010 0000 0000 0000 0101

注意,这将浪费一个页目录表项,同时使得最高端的4MB内存无法访问(0xFFC00000~0xFFFFFFFF)。不过,即使不浪费,一般的软件也不会涉足这个区域。

为什么会浪费一个呢?
一个页目录表项指向一个页表,页面有1024个项,每个项指向4KB的内存,所以为4096KB=4MB。

5)在页目录表中登记页表的位置:修改页目录内第1个目录项的内容,使其指向页表,页表的物理地址是0x00021000,该页位于内存中,可读可写,但不允许特权级别为3的程序和任务访问。

;在页目录内创建与线性地址0x00000000(第一项)对应的目录项
mov dword [es:ebx+0],0x00021003    ;写入目录项(页表的物理地址和属性)

6)初始化页表:将内存低端1MB所包含的那些页的物理地址按顺序一个一个地填写到页表中。

内核占用着内存的低端1MB,线性地址范围是0x00000000~0x000FFFFF,共256个4KB页,占用了页目录表的第1个目录项,以及该目录项下属页表的前256个页表项。

参考图示如下:

image

高20位:每个页表项的高20位参考如下:

image

低12位:每个页表项的低12位属性为003,表示p=1内存中,RW=1可读可写。

代码如下:

;创建与上面那个目录项相对应的页表,初始化页表项
     mov ebx,0x00021000                 ;页表的物理地址
     xor eax,eax                        ;起始页的物理地址
     xor esi,esi                  ;esi用作循环计数
.b2:
     mov edx,eax
     or edx,0x00000003            ;p=1内存中,RW=1可读可写
     mov [es:ebx+esi*4],edx       ;登记页的物理地址
     add eax,0x1000               ;下一个相邻页的物理地址
     inc esi
     cmp esi,256                  ;仅低端1MB内存对应的页才是有效的
     jl .b2

如此一来,这部分内存的线性地址就和物理地址一样了。

这句话可以用GDT的位置这个例子理解:GDT在内存的基地址是0x00007E00,开启页功能页部件处理这个线性地址,输出的物理地址还是0x00007E00。参考如下示例:

image

7)处理其他页表项:处理其他页表项,使它们的内容全为零,即无效表项。

.b3:                                      ;其余的页表项置为无效
     mov dword [es:ebx+esi*4],0x00000000
     inc esi
     cmp esi,1024
     jl .b3

8)CR3指向页目录表基址:将页目录表的物理基地址传送到控制寄存器CR3,也就是页目录表基地址寄存器PDBR。

;令CR3寄存器指向页目录,并正式开启页功能
mov eax,0x00020000                 ;PCD=PWT=0
mov cr3,eax

由于页目录表必须位于一个自然页内,故其物理基地址的低12位是全零,只登记它的高20位即可。低12位,除了PCD和PWT位,都没有使用。

image

控制寄存器的一些用法:

mov cr0~cr7,r32 ;从32位通用寄存器传送到控制寄存器
mov r32,cr0-cr7 ;从控制寄存器传送到32位通用寄存器

9)开启页功能:控制寄存器CR0的最高位,也就是位31,是PG(Page)位,用于开启或者关闭页功能。

image

mov eax,cr0
or eax,0x80000000     ;高位置1
mov cr0,eax           ;开启分页机制

任务全局空间和局部空间的页面映射

任务的全局空间和局部空间:每个任务都有自己独立的4GB虚拟地址空间,包含两个部分:局部空间和全局空间。

  • 局部地址空间使用低2GB,对应的线性地址范围是0x00000000~0x7FFFFFFF;
  • 全局地址空间使用高2GB,对应的线性地址范围是0x80000000~0xFFFFFFFF。

在任何任务内,在任何时候,如果段部件发出的线性地址高于或等于0x80000000,指向和访问的就是全局地址空间,或者说内核。

为此,我们要修改内核自己的页目录表,甚至是内核各个段的描述符,将内核挪到虚拟地址空间的高端,也就是虚拟地址空间中,从0x80000000开始的一段连续区域。

image

内核映射到高2GB的方法:在内核原先的地址上增加0x80000000。

  • 系统核心栈原来是从线性地址0x00007c00往下延伸,现在映射到了0x80007c00;
  • 主引导程序原先是从线性地址0x00007c00开始的,现在映射到0x80007c00,当然这个现在没有用了,可以不管;
  • 全局描述符表GDT原先是从线性地址0x00007e00开始的,现在映射到0x80007e00;
  • 中断描述符表IDT原先是从线性地址0x0001f 000开始的,现在映射到0x8001f 000;
  • 系统核心部分原先是从线性地址0x00040000开始的,映射后的线性地址是x80040000;
  • 文本显示缓冲区从线性地址0x000b8000开始的,映射后的线性地址0x800b8000。

映射到高端地址后的系统核心虚拟内存布局:

image

这是虚拟内存的布局,而不是真实的物理内存,内核现在是位于物理内存的低端1MB,我们只是将它映射到这个高端,只是让段部件发出的线性地址高于0x80000000。经页部件转换后,访问的还是原先那些页面。

登记内核映射到高2GB的页目录项

1)登记的页目录项位置:将内核映射到虚拟内存的高端之后,其线性地址范围是0x80000000~0x800FFFFF。这个范围内的地址,其二进制形式的高10位都是1000000000,即十六进制的0x200,乘以4之后是0x800,去访问页目录表。所以,我们需要在页目录表内偏移为0x800的地方填写一个目录项,用来转换这些地址。

2)登记的页目录项内容:因为内核占用的物理页没有改变,内核的数据和代码仍然在原来的页内,而这些页登记在原先的页表中,所以,这个目录项也指向原先的页表。即这个页目录项的内容也是0x00021003。

如此一来,在内核的页目录表中,有两个目录项是指向同一个页表的:

  • 第1个(0x000),线性地址范围:0x00000000~0x000FFFFF;
  • 第801个(0x800),线性地址范围:0x80000000~0x800FFFFF;

当段部件发出的线性地址是上面两套地址时,都用于访问同一段物理内存,也就内核。

3)代码实现

;在页目录内创建与线性地址0x80000000对应的目录项
;mov ebx,0xfffff000                 ;页目录自己的线性地址
;mov esi,0x80000000                 ;映射的起始地址
;shr esi,22                         ;线性地址的高10位是目录索引
;shl esi,2
;mov dword [es:ebx+esi],0x00021003  ;写入目录项(页表的物理地址和属性)
                                    ;目标单元的线性地址为0xFFFFF200
mov dword [es:0xfffff800], 0x00021003

3.1)定位页表:首先,处理器的页部件用CR3定位页目录表。段部件发出的线性地址是0xFFFFF800,如下图所示:

image

页部件先取出其二进制形式的高10位,这10位的值是十六进制的0x3FF。页部件将它乘以4,得到页目录表内的偏移0xFFC,访问页目录表,取出页表的物理地址。

这个页目录项值为0x00020003,简单的说还是页目录表自己,相当于把页目录表当成页表来用。

3.2)定位页表中的具体项:页部件取出线性地址的中间10比特,将它的数值乘以4,作为偏移量,去访问页表(还是页目录表)。

中间10位的值是0x3FF,乘以4之后是0xFFC,从页表中取出页的物理地址,值为0x00020003,实际上还是页目录表自己。也就是说,最终要访问的物理页,其实是页目录表。

image

3.3)定位物理页中偏移:处理器从这里取出页的物理地址(其实就是页目录表的物理地址),加上线性地址的低12位作为页内偏移量。

低12位的值是0x800,要访问的页是页目录表自身,所以,最终要访问的目标是在页目录表内偏移为0x800的那个双字。

image

从以上可以看出,如果页目录表的最后一个目录项指向当前页目录表自己,那么,无论任何时候,当线性地址的高20位是0xFFFFF时,访问的就是页目录表自己。

修改后页目录表内有两个目录项000和800都指向同一个页表,如下图所示:

image

修改内核相关的段描述符:仅仅修改页目录表是没有用的,如果段部件给出的线性地址并不在0x80000000以上,是没有用的。因此,必须修改与内核有关的段描述符,包括全局描述符表(GDT)自己的线性地址。

;将GDT中的段描述符映射到线性地址0x80000000
sgdt [pgdt] ;将gdtr中的值保存到内存中(pgdt标号处)。

mov ebx,[pgdt+2]  ;ebx表示gdt的起始基地址。

or dword [es:ebx+0x10+4],0x80000000;处理保护模式下初始代码段描述符
or dword [es:ebx+0x18+4],0x80000000;处理内核的栈段描述符
or dword [es:ebx+0x20+4],0x80000000;处理显示缓冲区描述符
or dword [es:ebx+0x28+4],0x80000000;处理公共例程段描述符
or dword [es:ebx+0x30+4],0x80000000;处理内核数据段描述符
or dword [es:ebx+0x38+4],0x80000000;处理内核代码段描述符

add dword [pgdt+2],0x80000000      ;GDTR也用的是线性地址

lgdt [pgdt]

开启分页模式后,为什么直接es:ebx可以访问到GDT?
因为前面已经做了映射处理,线性地址的低1MB和物理的低1MB一样。

又为什么要+4?
+4表示高32位,将高32位的最高位置1即可。

修改中断描述符表:修改中断描述符表寄存器IDTR,毕竟中断描述符表已经被映射到虚拟内存的高端了。

;修改IDTR,将中断描述符表映射到线性地址高端
sidt [pidt]
add dword [pidt+2],0x80000000      ;IDTR也用的是线性地址
lidt [pidt]

刷新段寄存器的高速缓存:段寄存器实际上由段选择器和描述符高速缓存器组成。当取指令和执行指令时,或者访问内存中的数据时,处理器不会每次都重新加载段寄存器,而是使用CS、SS、DS、ES、FS和GS描述符高速缓存器中的内容。

所以当改变了GDT的基地址,或者修改了段描述符之后,这些修改不会立即反映到段寄存器的描述符高速缓存器,对程序的运行没有任何影响。

但是,当执行一个段间转移指令,或者往段寄存器里加载一个新的段描述符选择子时,处理器将会访问GDT或者LDT,并刷新段寄存器描述符高速缓存器的内容。因此,为了使处理器转移到内存的高端位置执行,需要显式地刷新段寄存器的内容。

1)代码段CS:CS的刷新一般使用转移指令完成。

jmp core_code_seg_sel:flush ;刷新段寄存器CS,启用高端线性地址

2)栈段SS和数据段DS:重新加载段寄存器SS和DS的描述符高速缓存器,使它们的内容变成修改后的数据段描述符。

mov eax,core_stack_seg_sel
mov ss,eax

mov eax,core_data_seg_sel
mov ds,eax

显示消息开启分页功能:显示一条消息,已经开启了分页功能,而且内核已经被映射到线性地址0x80000000以上。

mov ebx,message_1
call sys_routine_seg_sel:put_string

安装调用门:安装供用户程序使用的调用门,并显示安装成功的消息。

  ;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
     mov edi,salt                       ;C-SALT表的起始位置 
     mov ecx,salt_items                 ;C-SALT表的条目数量 
.g0:
     push ecx   
     mov eax,[edi+256]                  ;该条目入口点的32位偏移地址 
     mov bx,[edi+260]                   ;该条目入口点的段选择子 
     mov cx,1_11_0_1100_000_00000B      ;特权级3的调用门(3以上的特权级才
                                        ;允许访问),0个参数(因为用寄存器
                                        ;传递参数,而没有用栈) 
     call sys_routine_seg_sel:make_gate_descriptor
     call sys_routine_seg_sel:set_up_gdt_descriptor
     mov [edi+260],cx                   ;将返回的门描述符选择子回填
     add edi,salt_item_len              ;指向下一个C-SALT条目 
     pop ecx
     loop .g0

     ;对门进行测试 
     mov ebx,message_2
     call far [salt_1+256]              ;通过门显示信息(偏移量将被忽略) 

创建内核任务

内核的虚拟内存分配

内核控制块地址:内核任务的任务控制块位于物理地址0x0001F800,在中断描述符表IDT的上面。我们现在已经将内核整体映射到虚拟内存的高端,所以这个任务控制块在映射之后的线性地址是0x8001F800

mov ecx,core_lin_tcb_addr          ;移至高端之后的内核任务TCB线性地址

;core_lin_tcb_addr在文件顶部有定义了
core_lin_tcb_addr     equ  0x8001f800
                                ;内核任务TCB的高端线性地址

将任务的状态置为忙

mov word [es:ecx+0x04],0xffff      ;任务的状态为“忙”

登记内核可用于分配的起始线性地址

mov dword [es:ecx+0x46],core_lin_alloc_at
                                ;登记内核中可用于分配的起始线性地址
;core_lin_alloc_at定义               
core_lin_alloc_at     equ  0x80100000
                                ;内核中可用于分配的起始线性地址
                                ;即1MB开始的位置,1MB已经被内核占用了

TCB的后面新增了一个成分,它就是偏移为0x46的双字,用来保存下一个可用于分配的线性地址,所以在本章中,任务控制块TCB的长度增加到0x4A。

image

新任务的内存分配一般是从自己的4GB虚拟内存的起始处分配的。所以,任务创建时,任务控制块TCB的下一个可用于分配的线性地址通常是0。

举个例子来说,如果需要分配1MB内存,那么,在这个任务的虚拟内存空间里,占用的线性地址范围是0到0xFFFFF。分配了线性地址范围后,还需要在物理内存中分配足够的物理页,然后在页目录和页表中登记线性地址与页地址的对应关系。如此一来,就可以用分配的线性地址来访问物理内存了。

因为刚才已经分配了1MB内存,线性地址范围是0到0xFFFFF,所以,下一个可用于分配的线性地址是0x100000,需要登记在任务控制块TCB中。

在内核任务中分配内存,是从虚拟内存空间的高端进行的。这样做的原因很简单,内核是所有任务共有的,它占据了每个任务地址空间的高端,低端是每个任务私有的。所以,在内核中分配内存,只能从虚拟内存的高端开始。

内核任务TCB追加到任务控制块链:在创建了内核任务的控制块后,调用例程 append_to_tcb_link 加入到任务控制块链表中。

call append_to_tcb_link         ;将内核任务的TCB添加到TCB链中

一旦将任务控制块加入链表中,它就会参与任务切换,而这个时候修改链表是危险的,所以必须在链表完全修改前,禁止硬件中断,从而禁止任务切换。

append_to_tcb_link:             ;在TCB链上追加任务控制块
                                ;输入:ECX=TCB线性基地址
    cli    ;关中断
    ...
    sti    ;开中断
    ret

创建任务状态段TSS:创建了内核任务的任务控制块之后,接下来要创建内核任务的任务状态段TSS,这是处理器的要求,对于任何一个任务来说,任务状态段都是不可或缺的。创建任务状态段TSS所需要的内存是动态分配的,而且必须在内核的虚拟内存空间里分配。

;为内核任务的TSS分配内存空间。所有TSS必须创建在内核空间
mov ecx,104                        ;为该任务的TSS分配内存,TSS长度104
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx              ;在内核TCB中保存TSS基地址

ECX指定需要分配的内存数量,104是TSS的基本长度。每个任务都有自己独立的4GB虚拟内存空间,所以,是在当前任务自己的虚拟内存空间里分配的。

1)allocate_memory例程:搜索当前任务,并在当前任务的地址空间中分配内存。

  • 进入例程时,要求用ECX指定需要分配的字节数;
  • 例程返回时,用ECX返回所分配内存的起始线性地址,这个线性地址是当前任务自己的虚拟内存空间里的线性地址。

因为是在当前任务的虚拟内存空间里分配的,所以,需要搜索任务控制块链表,找到当前任务的任务控制块,从中取得可用于分配的起始线性地址,然后从这个地址开始分配。

allocate_memory:            ;在当前任务的地址空间中分配内存
                            ;输入:ECX=希望分配的字节数
                            ;输出:ECX=起始线性地址 
     push eax
     push ebx

     push ds

     ;得到TCB链表首节点的线性地址
     mov eax,core_data_seg_sel          ;ds为内核数据段,主要是为了获取tcb链首节点地址
     mov ds,eax

     mov eax,[tcb_chain]                ;EAX=tcb首节点的线性地址

     mov ebx,mem_0_4_gb_seg_sel         ;ds切换为4GB段
     mov ds,ebx

     ;搜索状态为忙(当前任务)的节点
.s0:
     cmp word [eax+0x04],0xffff         ;tcb的0x04保存任务状态信息
     jz .s1                    ;找到忙的节点,EAX=节点的线性地址
     mov eax,[eax]             ;下一个TCB线性地址
     jmp .s0

     ;开始分配内存
.s1:
     mov ebx,eax        ;将忙节点的线性地址传送到ebx
     call sys_routine_seg_sel:task_alloc_memory

     pop ds

     pop ebx
     pop eax

     retf

之所以搜索忙状态的任务,因为当前任务一定是忙状态的。而分配虚拟内存也是在当前任务的虚拟内存里分配的。

2)task_alloc_memory例程:在指定任务的虚拟内存空间中分配内存。

  • 参数:EBX指定任务控制块TCB的线性地址;ECX指定所要分配的字节数;
  • 返回:ECX已分配的起始线性地址。

代码我加了一些注释:

task_alloc_memory:        ;在指定任务的虚拟内存空间中分配内存
                          ;输入:EBX=任务控制块TCB的线性地址
                          ;      ECX=希望分配的字节数
                          ;输出:ECX=已分配的起始线性地址
     push eax

     push ds

     push ebx                           ;to A

     ;获得本次内存分配的起始线性地址
     mov ax,mem_0_4_gb_seg_sel          ;指向4GB数据段
     mov ds,ax

     mov ebx,[ebx+0x46]                 ;获得本次分配的起始线性地址
     mov eax,ebx
     add ecx,ebx                        ;本次分配,最后一个字节之后的线性地址
                                        ;下次分配使用的起始线性地址
     push ecx                           ;to B,先保存起来,返回的时候用

     ;为请求的内存分配页
     and ebx,0xfffff000               ;低12位是页内偏移,将线性地址的低12位清0
     and ecx,0xfffff000
.next:
     call sys_routine_seg_sel:alloc_inst_a_page
                                        ;安装当前线性地址所在的页
     add ebx,0x1000                     ;+4096
     cmp ebx,ecx                        ;判断分配的内存是否够了
     jle .next                          ;没有分配够则进行下一次循环

     ;将用于下一次分配的线性地址强制按4字节对齐
     pop ecx                            ;B,下次分配使用的起始线性地址

     test ecx,0x00000003                ;线性地址是4字节对齐的吗?
     jz .algn                           ;是,直接返回
     add ecx,4                          ;否,强制按4字节对齐
     and ecx,0xfffffffc

.algn:
     pop ebx                            ;A

     mov [ebx+0x46],ecx                 ;将下次分配可用的线性地址回存到TCB中
     mov ecx,eax        ;ecx需要返回本次分配的起始地址,这个存储在eax中。

     pop ds

     pop eax

     retf

下一次分配的地址为什么一定要按4个字节对齐?
书中并没有介绍为什么要按4个字节对齐,但是一般按一定字节对齐,系统在处理的时候可以逐段获取,比较方便。就像物理内存分页每页4KB,即便有所浪费,但是操作方便。
4字节对齐起始就是 ceil(n/4),有余数那么n/4的整数部分+1,没有余数就是n/4本身。

3)alloc_inst_a_page例程:分配一个页,并安装在当前活动的层级分页结构中。

  • 参数:EBX=页的线性地址

3.1)检测线性地址对应的页表是否存在:思路上就是根据线性地址从从页目录表找到对应的页目录项,然后通过页目录项的值(最后一位)就可以判断页表是否存在。

;检查该线性地址所对应的页表是否存在
mov esi,ebx                        ;复制到esi,避免破坏ebx的值    
and esi,0xffc00000                 ;清除页表索引和页内偏移部分
shr esi,20                         ;将页目录索引乘以4作为页内偏移,先右移22位,再左移2位,就是右移20位
or esi,0xfffff000                  ;页目录自身的线性地址+表内偏移,就是对应页目录项

test dword [esi],0x00000001        ;[esi]页目录项值。P位是否为“1”。检查该线性地址是
jnz .b1                            ;否已经有对应的页表,存在则直接跳转到b1

3.2)创建页表并登记到页目录表:根据第一步的判断,如果页表不存在,就要创建页表。然后登记到页目录表。

;创建并安装该线性地址所对应的页表
call allocate_a_4k_page            ;分配一个页做为页表
or eax,0x00000007                  ;设置页表属性,7=0111,
mov [esi],eax                      ;写入页目录表

代码的一些说明:

  • allocate_a_4k_page分配1个物理页,例程返回时将返回EAX表示页的物理地址(类似0x00001000,0x00002000,…这样的物理地址)。
  • 设置页表属性中7=0111,含义:
    • US=1,特权级别为3的程序也可以访问;
    • RW=1,页是可读可写的;
    • P=1,页已经位于内存中,可以使用。
  • esi为页表登记在页目录项的地址。

为什么特权级别为3?
这个例程是通用的,既要用于为内核分配页面,也用于为3特权级的用户任务分配页面。3特权级的用户任务要求所分配页面的U/S位是1,不然的话无法访问。

3.3)页表项全部清零:创建了页表之后,它应当是空白的,需要将全部页表项清零。

关键的一个问题是要访问页表,把页表当成普通页来访问。代码不容易理解,我整理了一下思路,了解下面的思路就容易理解代码了。

正常页部件转换线性地址的思路:

  • CR3中基地址 + 线性地址高10位*4作为偏移地址,访问对应的页目录项。
  • 页目录项中高20位基地址 + 线性地址中10位*4作为偏移地址,访问对应页表项。
  • 页表项中高20位基地址 + 线性地址低12位*4作为偏移地址,访问对应物理页。

现在要访问的物理页是页表,页表的基地址是记录在页目录表中的,所以只要把页目录表当成正常步骤第二步的页表就可以了。知道这个思路,代码就容易理解了。

     ;清空当前页表
     mov eax,ebx               ;将页线性地址复制到eax
     and eax,0xffc00000        ;页线性地址的高12位就是在页目录表中的偏移
     shr eax,10                
     or eax,0xffc00000         ;高10位都为1,在页目录表中的偏移就是最大的页目录项,这项是指向页目录表。
     mov ecx,1024
.cls0:
     mov dword [es:eax],0x00000000 ;一个页表项占用4个字节,写入双字
     add eax,4                 ;继续下一个页表项处理
     loop .cls0

;转换后的线性地址eax格式如下:
; 1111 1111 11xx xxxx xxxx 0000 0000 0000
;   前面10位指向页目录表的最后一项,最后一项还是指向页目录表;
;   中间10位是原页线性地址的高10位,是页表基址信息登记在页目录表中的偏移位置;
;   低12位为0,表示从页表的偏移位置0开始访问。
;这个线性地址通过页部件转换后的物理地址就是页表的物理起始地址。

3.4)检查该线性地址对应的页(页表项)是否存在

.b1:
     ;检查该线性地址对应的页表项(页)是否存在
     mov esi,ebx                        ;ebx表示页线性地址
     and esi,0xfffff000                 ;清除页内偏移部分
     shr esi,10                         ;将页目录索引变成页表索引,页表索引乘以4作为页内偏移
     or esi,0xffc00000                  ;得到该线性地址对应的页表项

     test dword [esi],0x00000001        ;P位是否为“1”。检查该线性地址是
     jnz .b2                            ;否已经有对应的页
     
;转换后的线性地址esi格式如下:
; 1111 1111 11xx xxxx xxxx nnnn nnnn nn00
;   前面10位(都是1)指向页目录表的最后一项,最后一项还是指向页目录表;
;   中间10位(10个x)是原页线性地址的高10位,是页表基址信息登记在页目录表中的偏移位置;
;   低12位(10个n和2个0),
;     10个n,是原页线性地址的中10位,是物理页信息登记在页表中的偏移位置;
;     2个0,中10位定位到页表中的偏移位置要乘以4,就是左移两位,所以有两个0。
;这个线性地址通过页部件转换后的物理地址就是页表中对应的页表项的物理起始地址。

3.5)创建并安装该线性地址所对应的页:如果对应的页不存在,则创建并安装。

call allocate_a_4k_page            ;分配一个页,这才是要安装的页
or eax,0x00000007                  ;设置页属性
mov [esi],eax                      ;写入页表

;allocate_a_4k_page返回eax为物理页的起始物理地址

页面位映射串和空闲页的查找

页面位映射串:假如有4GB内存,每页4KB,可以分为1048576页,每页的是否使用如果用1字节来记录,那么就需要1MB的空间。如果每页用1比特来记录,那么就只需要128KB的空间。

总页数计算:4GB / 4KB = 1048576 页
每页1字节记录页是否使用占用空间计算:1048576 * 1字节 = 1MB
每页1比特记录页是否使用占用空间计算:1048576 * 1比特 = 128KB(8比特=1字节)

每页1比特记录页是否使用图示如下:

image

2MB的内存页映射位串:书中假定只有2MB的物理内存可用,2MB的内存,可分为512个页,需要512比特的位串,总共64字节。

声明了标号page_bit_map,并初始化了64字节的数据。

page_bit_map    db  0xff,0xff,0xff,0xff,0xff,0xff,0x55,0x55
                db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
                db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
                db  0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
                db  0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
                db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
                db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
                db  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00

前32字节的值都是0xFF,对应着最低端1MB内存的哪些页整体上划归内核使用了。没有被内核占用的部分多数也被外围硬件占用了,比如ROM-BIOS。

内存映像就是这张图:

image

在物理地址0x00030000~0x00040000之间,是一段较为连续的空闲区,共64KB,可划分为16个页,页的物理地址为0x00030000~0x00040000,就对应着这两字节。本来,这两字节都应当是0x00,以表明是可以分配的空闲页。不过,为了表明大的、连续的线性地址空间不必对应着连续的页,我们有意将空闲的页在物理上分开,因为0x55的二进制形式是01010101。同样的做法也出现在后面的64个页中。

这里两个0x55字节是指 page_bit_map 第一行的最后两个字节。

allocate_a_4k_page例程:过程allocate_a_4k_page是怎么搜索页映射位串并分配页的。

allocate_a_4k_page:                         ;分配一个4KB的页
                                            ;输入:无
                                            ;输出:EAX=页的物理地址
     push ebx
     push ecx
     push edx
     push ds

     mov eax,core_data_seg_sel ;指向内核数据段
     mov ds,eax

     xor eax,eax ;从头开始搜索位串,查找空闲的页,就是第一个为0的比特。
.b1:
     bts [page_bit_map],eax ;对应的比特位置1
     jnc .b2                ;如果为0,表示page_bit_map中的比特位为0,即可用。
     inc eax                ;不为0,eax+1,表示寻找下一个比特位。
     cmp eax,page_map_len*8 ;判断是否越界了。page_map_len是字节,所以要乘以8
     jl .b1                 ;没有越界继续判断下一个比特位

     mov ebx,message_3      ;没有找到,打印一个消息,然后停机
     call sys_routine_seg_sel:put_string
     hlt                                ;没有可以分配的页,停机

.b2:                        ;寻找到空闲的页了,其中eax表示第几页
     shl eax,12             ;左移12位,相当于乘以2^12=4096(0x1000),就是转换成物理地址
     pop ds
     pop edx
     pop ecx
     pop ebx

     ret

bts指令:bts(Bit Test and Set)指令测试位串中的某比特,用该比特的值设置EFLAGS寄存器的CF标志,然后将该比特置“1”。它最基本的两种格式为:

bts r/m16,r16
bts r/m32,r32

image

总结内存分配的全过程:总结一下内存分配的全过程。

内存分配的一个特定场景是当前任务为自己分配内存,这要在当前任务的虚拟地址空间里分配,要分配和占用当前任务自己的一部分线性地址范围。

  • 为当前任务分配内存需要调用例程allocate_memory,当前任务实际上是状态为忙的任务,所以这个例程的工作是从任务控制块TCB中寻找状态为忙的节点,也就是获得当前任务的任务控制块TCB。
  • 紧接着,例程allocate_memory用任务控制块的线性地址作为参数,调用另一个例程task_alloc_memory。
  • 例程task_alloc_memory访问指定的任务控制块TCB,取出并确定本次内存分配的线性地址范围,然后调用例程alloc_inst_a_page。
  • 例程alloc_inst_a_page对当前任务的页目录和页表进行检查,看是否存在与线性地址对应的条目。
    • 对应的页目录项、页表项及物理页都已经存在,所以本次内存分配就可以提前结束而不需要做任何实际的内存分配工作。
    • 如果相关的表项不存在,那么,就需要先调用例程allocate_a_4k_page,在物理内存中查找并返回空闲的物理页,然后,例程alloc_inst_a_page在页目录和页表中创建条目,登记线性地址与物理页的对应关系。

至此,内存分配的工作就完成了。

内核任务的确立

在内核TCB中保存TSS基地址:内存分配之后,用ECX返回本次分配的起始线性地址。将这个线性地址保存到内核任务的任务控制块TCB中,这是为以后访问任务控制块及执行任务切换做准备。登记的位置是任务控制块内偏移为0x14的地方。

mov [es:esi+0x14],ecx              ;在内核TCB中保存TSS基地址

TSS中设置相应的数据:在程序管理器的TSS中设置必要的项目。

mov word [es:ecx+0],0              ;反向链=0
mov eax,cr3
mov dword [es:ecx+28],eax          ;登记CR3(PDBR),从这里恢复页目录表基地址
mov word [es:ecx+96],0             ;没有LDT。处理器允许没有LDT的任务。
mov word [es:ecx+100],0            ;T=0
mov word [es:ecx+102],103          ;没有I/O位图。0特权级事实上不需要。
                                ;不需要0、1、2特权级堆栈。0特级不
                                ;会向低特权级转移控制。

创建TSS描述符并安装:创建TSS描述符,并安装到GDT中:

mov eax,ecx                        ;TSS的起始线性地址
mov ebx,103                        ;段长度(界限)
mov ecx,0x00008900                 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov word [es:esi+0x18],cx          ;登记TSS选择子到TCB

把CX中的TSS描述符选择子加载到任务寄存器TR:任务寄存器TR所指向的任务是当前任务,还要把CX中的TSS描述符选择子加载到任务寄存器TR。

;任务寄存器TR中的内容是任务存在的标志,该内容也决定了当前任务是谁。
;下面的指令为当前正在执行的0特权级任务“程序管理器”后补手续(TSS)。
ltr cx

打印信息:打印内核任务已经创建好的信息。

;现在可认为“程序管理器”任务正执行中
mov ebx,core_msg1
call sys_routine_seg_sel:put_string

;core_msg1定义
core_msg1        db  '  Core task created.',0x0d,0x0a,0

用户任务的创建和切换

用户任务的虚拟内存分配策略

创建TCB:每个任务都有自己的任务控制块。

mov ecx,0x4a              ;新的任务控制块时74字节,十六进制表示是0x4a
call sys_routine_seg_sel:allocate_memory ;分配内存,ECX返回本次分配的起始线性地址
mov word [es:ecx+0x04],0           ;任务状态:就绪
mov dword [es:ecx+0x46],0          ;任务内可用于分配的初始线性地址

push dword 50                      ;用户程序位于逻辑50扇区
push ecx                           ;压入任务控制块起始线性地址 
call load_relocate_program         ;完成用户任务的整个创建过程
call append_to_tcb_link            ;将此TCB添加到TCB链中

load_relocate_program:例程load_relocate_program来完成用户任务的整个创建过程。

通过栈传递两个参数:

  • 用户程序在硬盘上的起始逻辑扇区号(这里是逻辑50扇区);
  • 任务控制块的起始线性地址(保存在ECX中)。

创建用户任务的过程包含下面两个方面:

  • 分配内存,内存分配包括创建用户任务自己的页目录表和页表,并分配对应的物理页。
  • 把用户程序从硬盘上读出,写入分配来的内存。

创建用户任务思路:先在内核任务的低2GB空间里给用户程序分配内存,然后读入用户程序,最后将内核任务的页目录表复制一份,作为被创建的那个用户任务的页目录表

为什么这么做呢?书中也做了详细的说明。

因为当前任务是内核任务,使用的是内核自己的页目录表和页表,不可能通过用户任务的页目录表和页表进行地址转换。

每个任务都有自己独立的页目录表和页表,里面记录了线性地址与物理页的对应关系。对每个任务来说,4GB虚拟内存里的每个线性地址,都有一个物理内存中的页与之对应,这种对应关系记录在每个任务自己的页目录表和页表中。

因此,即使每个任务都发出相同的线性地址,但由于这些线性地址对应着不同的物理页,所以最终访问的是不同的物理页。

image

  • 任务A是需要访问内核服务的,页目录表的高一半,也就是对应于2~4GB的这一半,指向内核的页表。如此一来,访问高端内存时,访问的就是内核所占用的页面。访问低端内存时,访问的是任务A自己的页面;
  • 任务B和A类似;
  • 内核任务作为一个独立的任务,也有自己独立的4GB虚拟内存空间。

在内核任务的虚拟内存里,低2GB是空的,所以在页目录表中这一半也是空的,也没有使用。那么在创建用户任务时,我们可以先在内核任务的低2GB空间里分配内存。

用户任务的虚拟地址空间分配

小节内容是对 load_relocate_program 进行具体的分析。

清空内核任务页目录表的低2G空间:之所以要清理是因为可能之前分配过其他用户任务的虚拟地址。

    ;清空当前页目录的前半部分(对应低2GB的局部地址空间)
    mov ebx,0xfffff000  ;页目录表的线性地址
    xor esi,esi         ;页目录项从0开始
.clsp:
    mov dword [es:ebx+esi*4],0x00000000 ;每个页目录项4字节。
    inc esi             ;继续下一个页目录项
    cmp esi,512         ;和512对比,是否已经到一半了。页目录项总共1024个      
    jl .clsp            ;小于512,就继续清理。

刷新TLB:开启页功能时,处理器的页部件要使用页目录表和页表把线性地址转换成物理地址,而访问页目录表和页表是相当费时间的。因此,把页表项预先存放到处理器中,可以加快地址转换速度。

为此,处理器专门构造了一个特殊的高速缓存装置,叫作转换速查缓冲器(Translation Lookaside Buffer, TLB)。

TLB的结构如下:

image

这是TLB的结构,它很像一个表格,这个表格有很多行,每行又分成两大部分,第一部分是标记,其内容为线性地址的高20位;第二部分是页表数据,包括属性、访问权和页物理地址的高20位。

处理器仅仅缓存那些P位是“1”的页表项,而且,TLB的工作和寄存器CR3的PCD位和PWT位无关,不受这两位的影响。另外,对于页表项的修改不会同时反映到TLB中

TLB的内容(条目)是软件不可直接访问的,所以你不能直接更改或者刷新它的内容,但有其他办法来刷新。比如,将寄存器CR3的内容读出,再原样写入,这样就会使得TLB中的所有条目失效。当然,这是比较直接的做法。当任务切换时,因为要从新任务的TSS中加载CR3,也会隐式地导致TLB中的所有条目无效。

注意,上述方法对于那些标记为全局(G位为“1”)的页表项来说无效,不起作用。被设置为全局的页表项意味着它应该始终被缓存在TLB中。

TLB其实就是缓存页部件转换后的结果。

代码实现:

mov ebx,cr3 ;刷新TLB(Translate Lookaside Buffer)
mov cr3,ebx

创建用户任务的LDT

取得TCB的基地址:后续任务的相关信息都需要写入TCB,所以需要先获取到TCB的基地址。

mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址,任务控制块在第11个位置。

;为什么是11*4?
;调用过程前,push了两个参数:
;  PUSH 逻辑扇区号
;  PUSH 任务控制块基地址
;执行过程中:
;  pushad ;8个通用寄存器
;  push ds ;1个
;  push es ;1个
从下往上数,任务控制块基地址就是在第11个。

创建LDT:申请创建LDT所需要的内存。

;以下申请创建LDT所需要的内存
mov ebx,esi                        ;ebx是task_alloc_memory的参数
mov ecx,160                        ;允许安装20个LDT描述符
call sys_routine_seg_sel:task_alloc_memory ;分配内存
mov [es:esi+0x0c],ecx              ;登记LDT基地址到TCB中
mov word [es:esi+0x0a],0xffff      ;登记LDT初始的界限到TCB中

;LDT界限值:因为还没有安装任何描述符,所以LDT的长度为0,
;  用0减去1,保留16位的结果就是0xFFFF。

task_alloc_memory和上面内核申请TSS内存时的功能是一致的。

用户程序的加载

DS指向内核数据段:首先要从硬盘读取用户程序。段寄存器DS指向内核数据段。

mov eax,core_data_seg_sel
mov ds,eax                         ;切换DS到内核数据段

;为什么通过ds可以访问4G内存段?
;  内核数据段依旧是选择子,然而GDTR中的地址已经切换为线性地址了,
;  包括GDT中的各描述符内容。

读取第一个扇区:从硬盘读取用户程序第一个扇区,主要是为了得到用户程序大小。

mov eax,[ebp+12*4]                 ;从堆栈中取出用户程序起始扇区号 
mov ebx,core_buf                   ;读取程序头部数据,ebx存储段内偏移量    
call sys_routine_seg_sel:read_hard_disk_0

;程序读取出来后存储在core_buf定义的缓冲区

计算用户程序多大:计算用户程序有多大(512字节对齐,按扇区读,每扇区是512字节),申请需要的内存。

;这里是512字节对齐
mov eax,[core_buf]                 ;程序尺寸,用户程序的前面4个字节记录用户程序大小
mov ebx,eax
and ebx,0xfffffe00                 ;使之512字节对齐(能被512整除的数低 
add ebx,512                        ;9位都为0 
test eax,0x000001ff                ;程序的大小正好是512的倍数吗? 
cmovnz eax,ebx                     ;不是。使用凑整的结果

mov ecx,eax                        ;实际需要申请的内存数量
mov ebx,esi                        ;任务控制块的起始地址
call sys_routine_seg_sel:task_alloc_memory
mov [es:esi+0x06],ecx              ;登记程序加载基地址到TCB中

计算总扇区数:计算总扇区数,因为硬盘是按扇区读取数据的。

mov ebx,ecx                        ;ebx -> 申请到的内存首地址
xor edx,edx
mov ecx,512
div ecx                            ;eax/ecx,eax存储了程序总大小(512字节对齐)
mov ecx,eax                        ;总扇区数 

从硬盘逐个扇区读取数据

mov eax,mem_0_4_gb_seg_sel         ;切换DS到0-4GB的段
mov ds,eax

mov eax,[ebp+12*4]                 ;起始扇区号,栈段的低12个位置就是起始扇区号
.b1:
    call sys_routine_seg_sel:read_hard_disk_0
    inc eax
    loop .b1                           ;循环读,直到读完整个用户程序

创建描述符:创建每个段的描述符,并安装在用户任务自己的局部描述符表LDT中。

获取程序加载基地址:

mov edi,[es:esi+0x06]              ;获得程序加载基地址

;这个很简单,就是从TCB中获取程序加载的基地址。

安装程序头部段、代码段、数据段和栈段描述符:

;建立程序头部段描述符
mov eax,edi                        ;程序头部起始线性地址
mov ebx,[edi+0x04]                 ;段长度
dec ebx                            ;段界限
mov ecx,0x0040f200                 ;字节粒度的数据段描述符,特权级3 
call sys_routine_seg_sel:make_seg_descriptor

;安装头部段描述符到LDT中 
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt

or cx,0000_0000_0000_0011B         ;设置选择子的特权级为3
mov [es:esi+0x44],cx               ;登记程序头部段选择子到TCB 
mov [edi+0x04],cx                  ;和头部内 

;建立程序代码段描述符
mov eax,edi
add eax,[edi+0x0c]                 ;代码起始线性地址
mov ebx,[edi+0x10]                 ;段长度
dec ebx                            ;段界限
mov ecx,0x0040f800                 ;字节粒度的代码段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B         ;设置选择子的特权级为3
mov [edi+0x0c],cx                  ;登记代码段选择子到头部

;建立程序数据段描述符
mov eax,edi
add eax,[edi+0x14]                 ;数据段起始线性地址
mov ebx,[edi+0x18]                 ;段长度
dec ebx                            ;段界限 
mov ecx,0x0040f200                 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B         ;设置选择子的特权级为3
mov [edi+0x14],cx                  ;登记数据段选择子到头部

;建立程序堆栈段描述符
mov eax,edi
add eax,[edi+0x1c]                 ;数据段起始线性地址
mov ebx,[edi+0x20]                 ;段长度
dec ebx                            ;段界限
mov ecx,0x0040f200                 ;字节粒度的堆栈段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B         ;设置选择子的特权级为3
mov [edi+0x1c],cx                  ;登记堆栈段选择子到头部

重定位U-SALT并复制页目录表

如何访问用户程序头部段:U-SALT的定位是在程序头部段,那么就需要访问用户程序头部段,之前没有分页的时候是通过用户头部段选择子进行访问,先在还可以吗?

答案是不可以,因为这些段描述符位于局部描述符表LDT。只有当处理器的局部描述符表寄存器LDTR指向这个LDT时,才可以访问这些段。而当前任务是内核任务,内核任务是没有LDT段的。

所以只能通过4GB段访问用户程序头部。

mov eax,mem_0_4_gb_seg_sel      ;这里和前一章不同,头部段描述符
mov es,eax                      ;已安装,但还没有生效,故只能通
                                ;过4GB段访问用户程序头部

新接口malloc:在内核的符号地址检索表中,添加了一个新的接口名字malloc,它实际上对应着例程allocate_memory。allocate_memory用于当前任务为自己分配内存,而且是在当前任务的虚拟内存空间里分配的,是在当前任务自己的页目录表和页表中登记相关信息。

创建三个特权级的栈:创建调用门所使用的三个特权级的栈,每个栈的长度是4KB。

;创建0特权级栈
mov ecx,0                          ;以4KB为单位的栈段界限值
mov [es:esi+0x1a],ecx              ;登记0特权级栈界限到TCB
inc ecx
shl ecx,12                         ;乘以4096,得到段大小
push ecx
mov ebx,esi
call sys_routine_seg_sel:task_alloc_memory
mov [es:esi+0x1e],ecx              ;登记0特权级栈基地址到TCB
mov eax,ecx
mov ebx,[es:esi+0x1a]              ;段长度(界限)
mov ecx,0x00c09200                 ;4KB粒度,读写,特权级0
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt
;or cx,0000_0000_0000_0000          ;设置选择子的特权级为0
mov [es:esi+0x22],cx               ;登记0特权级堆栈选择子到TCB
pop dword [es:esi+0x24]            ;登记0特权级堆栈初始ESP到TCB

;创建1特权级堆栈
mov ecx,0
mov [es:esi+0x28],ecx              ;登记1特权级堆栈尺寸到TCB
inc ecx
shl ecx,12                         ;乘以4096,得到段大小
push ecx
mov ebx,esi
call sys_routine_seg_sel:task_alloc_memory
mov [es:esi+0x2c],ecx              ;登记1特权级堆栈基地址到TCB
mov eax,ecx
mov ebx,[es:esi+0x28]              ;段长度(界限)
mov ecx,0x00c0b200                 ;4KB粒度,读写,特权级1
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0001          ;设置选择子的特权级为1
mov [es:esi+0x30],cx               ;登记1特权级堆栈选择子到TCB
pop dword [es:esi+0x32]            ;登记1特权级堆栈初始ESP到TCB

;创建2特权级堆栈
mov ecx,0
mov [es:esi+0x36],ecx              ;登记2特权级堆栈尺寸到TCB
inc ecx
shl ecx,12                         ;乘以4096,得到段大小
push ecx
mov ebx,esi
call sys_routine_seg_sel:task_alloc_memory
mov [es:esi+0x3a],ecx              ;登记2特权级堆栈基地址到TCB
mov eax,ecx
mov ebx,[es:esi+0x36]              ;段长度(界限)
mov ecx,0x00c0d200                 ;4KB粒度,读写,特权级2
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi                        ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0010          ;设置选择子的特权级为2
mov [es:esi+0x3e],cx               ;登记2特权级堆栈选择子到TCB
pop dword [es:esi+0x40]            ;登记2特权级堆栈初始ESP到TCB

创建LDT自己的描述符并安装在全局描述符表GDT中

;在GDT中登记LDT描述符
mov eax,[es:esi+0x0c]              ;LDT的起始线性地址
movzx ebx,word [es:esi+0x0a]       ;LDT段界限
mov ecx,0x00008200                 ;LDT描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x10],cx               ;登记LDT选择子到TCB中

创建用户任务的任务状态段TSS:创建用户任务的任务状态段TSS并填写必要的内容。

;创建用户程序的TSS
mov ecx,104                        ;tss的基本尺寸
mov [es:esi+0x12],cx              
dec word [es:esi+0x12]             ;登记TSS界限值到TCB 
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx              ;登记TSS基地址到TCB

;登记基本的TSS表格内容
mov word [es:ecx+0],0              ;反向链=0

mov edx,[es:esi+0x24]              ;登记0特权级堆栈初始ESP
mov [es:ecx+4],edx                 ;到TSS中

mov dx,[es:esi+0x22]               ;登记0特权级堆栈段选择子
mov [es:ecx+8],dx                  ;到TSS中

mov edx,[es:esi+0x32]              ;登记1特权级堆栈初始ESP
mov [es:ecx+12],edx                ;到TSS中

mov dx,[es:esi+0x30]               ;登记1特权级堆栈段选择子
mov [es:ecx+16],dx                 ;到TSS中

mov edx,[es:esi+0x40]              ;登记2特权级堆栈初始ESP
mov [es:ecx+20],edx                ;到TSS中

mov dx,[es:esi+0x3e]               ;登记2特权级堆栈段选择子
mov [es:ecx+24],dx                 ;到TSS中

mov dx,[es:esi+0x10]               ;登记任务的LDT选择子
mov [es:ecx+96],dx                 ;到TSS中

mov dx,[es:esi+0x12]               ;登记任务的I/O位图偏移
mov [es:ecx+102],dx                ;到TSS中

mov word [es:ecx+100],0            ;T=0

;访问用户程序头部,获取数据填充TSS 
mov ebx,[ebp+11*4]                 ;从堆栈中取得TCB的基地址
mov edi,[es:ebx+0x06]              ;用户程序加载的基地址 

mov edx,[es:edi+0x08]              ;登记程序入口点(EIP)
mov [es:ecx+32],edx                ;到TSS

mov dx,[es:edi+0x0c]               ;登记程序代码段(CS)选择子
mov [es:ecx+76],dx                 ;到TSS中

mov dx,[es:edi+0x1c]               ;登记程序堆栈段(SS)选择子
mov [es:ecx+80],dx                 ;到TSS中

mov edx,[es:edi+0x20]              ;堆栈的高端线性地址
mov [es:ecx+56],edx                ;填写TSS的ESP域

mov dx,[es:edi+0x04]               ;登记程序数据段(DS)选择子
mov word [es:ecx+84],dx            ;到TSS中。注意,它指向程序头部段

mov word [es:ecx+72],0             ;TSS中的ES=0

mov word [es:ecx+88],0             ;TSS中的FS=0

mov word [es:ecx+92],0             ;TSS中的GS=0

创建TSS描述符,并登记在全局描述符表

;在GDT中登记TSS描述符
mov eax,[es:esi+0x14]              ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12]       ;段长度(界限)
mov ecx,0x00008900                 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x18],cx               ;登记TSS选择子到TCB

复制内核任务的页目录表:通过create_copy_cur_pdir例程将内核任务的页目录表复制一份,作为用户任务的页目录表。

复制完成后,将用户任务的页目录表的物理地址记录到TSS的CR3域,将来切换任务的时候可以从TSS中恢复新任务的页目录表指针。

;创建用户任务的页目录
;注意!页的分配和使用是由页位图决定的,可以不占用线性地址空间
call sys_routine_seg_sel:create_copy_cur_pdir
mov ebx,[es:esi+0x14]              ;从TCB中获取TSS的线性地址
mov dword [es:ebx+28],eax          ;填写TSS的CR3(PDBR)域
create_copy_cur_pdir例程:用于实现页目录表的复制。
create_copy_cur_pdir: ;创建新页目录,并复制当前页目录内容
                      ;输入:无
                      ;输出:EAX=新页目录的物理地址
    push ds
    push es
    push esi
    push edi
    push ebx
    push ecx
    
    mov ebx,mem_0_4_gb_seg_sel ;复制有一个源和目的:DS为源;ES位目的。
    mov ds,ebx
    mov es,ebx

    call allocate_a_4k_page    ;分配1个4k的物理页,EAX是这个页面的物理地址
    mov ebx,eax                ;
    or ebx,0x00000007          ;设置页的属性
    mov [0xfffffff8],ebx       ;登记在倒数第二个目录项,倒数第一个是:0xfffffffc
                               ;  即在页目录表的偏移是0xFF8
    invlpg [0xfffffff8]        ;上面物理地址的线性地址就是0xfffffff8,此指令用于刷新

    mov esi,0xfffff000                 ;ESI->当前页目录的线性地址
    mov edi,0xffffe000                 ;EDI->新页目录的线性地址
    mov ecx,1024                       ;ECX=要复制的目录项数
    cld
    repe movsd                         ;依次复制2个字,即4个字节,一个表项

    pop ecx
    pop ebx
    pop edi
    pop esi
    pop es
    pop ds

    retf

1)为什么 0xFFFFFFF8 是倒数第2个目录项?

答:因为每个页目录项是4字节

  • 最后一项是 0xFFFFFFFC ~ 0xFFFFFFFF 共4个字节;
  • 倒数第二项就是 0xFFFFFFF8 ~ 0xFFFFFFFB 共4个字节;
  • 依次类推…

2)为什么申请的物理页线性地址是0xFFFFFFF8?

答:根据线性地址转换成物理地址的规则进行计算:

  • 高10位:0x3FF * 4 = 0xFFC,页目录表偏移0xFFC处指向页目录表;
  • 中10位:0x3FF * 4 = 0xFFC,页目录表偏移0xFFC处指向页目录表;
  • 低12位:0xFF8,页目录表偏移0xFF8处指向这个物理页的地址。

3)invlpg指令:invlpg:Invalidate TLB Entry,用来刷新处理器的转换速查缓冲器TLB。

invlpg [0xfffffff8]

0xFFFFFFF8是当前页目录表内的倒数第2个目录项的线性地址,每次都用它来指向新任务的页目录表。当任务A创建完毕后,它指向任务A的页目录表;当任务B创建时,它依然指向任务A的页目录表。虽然我们刚才已经改写了它,使它指向新任务的页目录表,但这个更改只在内存中有效,还没有反映到TLB中。如果不刷新TLB中的这个条目,那么,后面的所有操作,都是针对前一个任务的页目录表进行的。

4)为什么当前页目录的线性地址是0xFFFFF000?

答:根据线性地址转换成物理地址的规则进行计算:

  • 高10位:0x3FF * 4 = 0xFFC,页目录表偏移0xFFC处指向页目录表;
  • 中10位:0x3FF * 4 = 0xFFC,页目录表偏移0xFFC处指向页目录表;
  • 低12位:0x000,页目录表的第1项指向页目录表。

书上写的是0xFFFFFFF0,应该是笔误了。

5)为什么新创建的页目录表的线性地址是0xFFFFE000?

答:根据线性地址转换成物理地址的规则进行计算:

  • 高10位:0x3FF * 4 = 0xFFC,页目录表偏移0xFFC处指向页目录表;
  • 中10位:0x3FE * 4 = 0xFF8,页目录表偏移0xFF8处指向新创建的页目录表;
  • 低12位:0x000,从第1项开始逐项复制。

切换到用户任务执行

将任务加入到TCB链:完成任务的加载后,将其加入到TCB链。

call append_to_tcb_link            ;将此TCB添加到TCB链中

任务切换:任务切换是自动进行的,由一个实时时钟信号驱动。时钟芯片每秒钟发出一个中断信号,中断发生时,处理器执行我们设置好的中断处理过程,而这个中断处理过程就用于执行任务切换。

此时TCB链有两个任务,任务切换随时可能发生。

再创建1个用户任务:多创建1个任务,方便查看多个任务切换。

;可以创建更多的任务,例如:
mov ecx,0x4a
call sys_routine_seg_sel:allocate_memory
mov word [es:ecx+0x04],0           ;任务状态:空闲
mov dword [es:ecx+0x46],0          ;任务内可用于分配的初始线性地址

push dword 100                     ;用户程序位于逻辑100扇区
push ecx                           ;压入任务控制块起始线性地址

call load_relocate_program
call append_to_tcb_link            ;将此TCB添加到TCB链中

程序的编译、执行和调试

本章程序的编译和运行方法

开始执行本章程序:

  • 第15章的mbr.bin写入虚拟硬盘的逻辑0扇区;
  • 本章核心core.bin写入虚拟硬盘的逻辑1扇区;
  • 第18章的第1个用户程序bin写入虚拟硬盘的逻辑50扇区;
  • 第18章的第2个用户程序bin写入虚拟硬盘的逻辑100扇区;

image

查看CR3寄存器的内容

creg:查看CR3寄存器的内容。

image

上图执行的了以下指令后的控制寄存器状态:

mov eax,0x00020000 ;PCD=PWT=0
mov cr3,eax

mov eax,cr0
or eax,0x80000000
mov cr0,eax ;开启分页机制

由于已经处于分页模式下(必须要先处于保护模式),所以CR0的PE位和PG位都已处于置位状态,控制寄存器CR3中的内容是当前任务页目录的物理地址,即,0x20000,PCD=0,页级缓存禁用是关闭的(即,允许页级缓存);PWT=0,页级通写被禁用。

调试过程中发现问题:执行到 设置8259A中断控制器 这块,一直有一行代码会崩溃。后面注释这个代码就可以了。然后发现即使没有 设置8259A中断控制器 ,在VirtualBox也能正常跑起来。

image

小技巧:调试这个一开始不太容易,要一直往下执行,然后中间很多循环,执行到哪里也不清楚,通过图上所示的信息和编译的.lst文件就可以知道进行到哪里了。

image

查看线性地址对应的物理页信息

page:进入到分页模式,可以用page命令查看线性地址到物理页的映射信息。

命令格式:page 线性地址

image

  • PDE表示页目录表,存储了对应的页目录项,页目录项为0x21003,即页表物理地址是0x21000;
  • PTE表示页表,存储了对应的页表项,页表项为0x7003,即页物理地址是0x7000。

也就是说与线性地址0x7e08相对应的页是0x7000。

查看当前任务的页表信息

info tab:可以查看当前任务的页表,显示线性地址和物理地址(页)的全部映射关系。

image

  • cr3:表示当前指向的页目录表物理起始地址,当前页目录表物理起始地址为0x20000;
  • 第二行:0x00000000 ~ 0x000FFFFF 对应 物理地址 0x00000000 ~ 0x000FFFFF,一开始进入分页模式的时候,建立了低端1M内存空间的一一映射,使线性地址和物理地址相同。
  • 第二行:为了用线性地址来修改页表的内容,我们把页表当成普通的页,把页目录表当成页表来用。在这种情况下,页表的线性首地址是0xFFC00000。因此,0xFFC00000~0xFFC00FFF这段4KB的线性地址区间对应的是页表的实际物理地址0x00021000~0x00021FFF。
    • 这个线性地址高10位:0x3FF * 4 = 0xFFC,还是指向页目录表;
    • 这个线性地址中10位:0x000 * 4 = 0x000,页目录表第一项指向页表;
    • 这个线性地址低12位:表内偏移,页表第1项(0x000)到第0x1000项(0xFFF)。
  • 第三行:为了用线性地址访问和修改页目录表自己,页目录表的最后一个目录项,登记的是页目录表自己的物理地址。因此,页目录表的线性地址是0xFFFFF000。即,0xFFFFF000~0xFFFFFFFF这段4KB线性地址区间对应着实际的物理地址区间0x00020000~x00020FFF。

使用线性(虚拟)地址调试程序

x:使用x指令可以查看一个线性地址单元里的内容。

命令格式:x 线性地址

image

  • x 0x40000:因为低端1MB的线性地址和物理地址相同,所以这个查看的内容实际就是内核大小;
  • x 0x80040000:内核映射到高端2GB~4GB的地址,所以这个指令结果和0x40000是一样的;

查看页目录表第一项:通过页目录表的线性地址即可查看,页目录表的线性地址有如下3个。

  • 内核页目录表的线性地址和物理地址是0x20000;
  • 映射到高端后:内核页目录表的线性地址是0x80020000;
  • 页目录表的最后一个表项指向当前页目录表自身,所以还有一个线性地址0xFFFFF000。

image

用线性地址设置断点:lb或者vb指令。

完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值