拼一个自己的操作系统 SnailOS 0.03的实现
拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-CSDN下载
操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-CSDN下载
SnailOS0.00-SnailOS0.00-其它文档类资源-CSDN下载
挑战分页机制
大家看到了吧,标题咱们用了“挑战”二字,可见分页机制的难度。这么说吧,有一个关键的地方,大家不得不把脑子转它个九九二百五十转才能反应过来。什么,有不信邪的。那咱们就骑驴看唱本吧。
intel的处理器早在80386的时候就开始支持分页的内存管理模式了。内存的分页机制显然不是可有可无的,它存在的主要目的是为了实现虚拟存储技术。无论是操作系统还是应用程序,在运行时,它们都迫不及待地获得足够多的内存。在仅分段下,这显然是不可能的,也就是说,操作系统在为应用程序分配内存时,必须遵循互不干涉,有限合作的原则。虽然在操作系统做起来,段与段之间分配相同的物理地址空间没有任何不妥,可是如果真的这样做了,此应用程序可是不会关照彼应用程序的,没有良好的软件管理机制必然造成相互拆台的局面。所以一般的,在分段机制下,多个应用程序同时运行时,段的地址空间不但不能够重合的,而且由于物理地址的有限,所分配的尺度也是非常有限的。即使在现在物理内存很大的情况下,采用分段模型,倘若操作系统分配了四分之一的空间,而应用程序就只能在剩下的四分之三空间中竞争使用了。
在这种情况下,分页机制应运而生,一举解决了这一棘手问题,它为每个程序准备了4G的虚拟地址空间,当然也包括操作系统本身。在该机制下的掩护下,每个程序都认为自己有这么大的内存空间,可以随意使用4G内的虚拟地址,只要该地址通过分页机制被映射到了实际的物理内存就完全没有问题。那么如果没有映射到实际的物理内存是否就意味着该程序要“芭比Q”了呢?答案当然是否定的,操作系统通过实现处理页异常的代码还可以将实际的数据从其他存储设备中换入内存中,并恢复该应用程序的执行。那么说分段就不能换入吗?不是的,分段也有这种能力,不过分段是不可能有4G这么大的虚拟内存的,在分段下如果内存被某个程序占有了,其他程序就要受气了。
接下来要为大家介绍一下分页机制实现原理了。第一个问题是什么是页。我们知道32位处理器,可以访问的物理内存大小是4G。在这种极限的情况下,如果我们按照4k容量为单位,把内存分成大小均等顺序排列的块的话,可以分出1M个这样的块。我们在这里就可以把每个块称为一个内存页,因为有了编号,所以任意两个页是有区别的,即是页的索引不同。这跟内存的编号是一样的,任何两个物理内存都不可能有相同的编号。在分页下,物理地址和虚拟地址的页都是分别这样来划分的。
第二个问题是,物理地址和虚拟地址都分了页后,就是建立虚拟页和物理页的对应关系了。显然虚拟内存的任何一页可以对应物理内存的任何一页,只要建立了对应关系就行。换个方式来说,我们的应用程序很大程度上是希望内存是连续的,不要东一榔头西一镐,因为这样对编程来说实现最简单、最有利。而操作系统说,放心好了,分配内存的事情归我管,背后的事情不用你管,就是给你4G的内存,你用起来也是一点不零散。比如,当应用程序想要用8M的内存时,从虚拟内存的角度说,根本就是从某个固定页开始的吗,分吧不在话下。但从操作系统角度看,你是大方了,我怎么办,这零零散散的内存没有一块够8M吗,还好我操作系统也不是吃素的,让我用个好策略给你凑凑乎乎拼个8M的空间吧,成熟的内存管理系统,在这种极端的情况,一定能有个好的策略来实现。我刚才说的东西,其实要表达的中心意思是不管是系统程序还是应用程序在受到分段和分页机制控制下,对内存的访问总是看到虚拟地址,它们根本就看不到物理地址,而且也没必要看到物理地址。因此你可以认为编译和链接软件在完成一系列工作后,为我们的操作系统代码生成了合适的虚拟地址。
第三个问题是,在80x86中,建立这种映射关系的是通过页表来完成的,而且是两级的页表,上层的页表叫做页目录表,下一层的叫做页表,页表的表项中存入的是某一虚拟内存页对应的物理页基地址。页目录表项中存放的是页表的物理基地址。页表和页目录表用几乎一样格式的表项,长度为4字节,每个页表和页目录表的大小都对齐和占用一个自然页。下面我们来仔细的看一下页目录项和页表项,如下图:
【P】:存在位。为1表示页表或者页位于内存中。否则,表示不在内存中,必须先予以创建或者从磁盘调入内存后方可使用。
【R/W】:读写标志。为1表示页面可以被读写,为0表示只读。当处理器运行在0、1、2特权级时,此位不起作用。页目录中的这个位对其所映射的所有页面起作用。
【U/S】:用户/超级用户标志。为1时,允许所有特权级别的程序访问;为0时,仅允许特权级为0、1、2的程序访问。页目录中的这个位对其所映射的所有页面起作用。
【PWT】:Page级的Write-Through标志位。为1时使用Write-Through的Cache类型;为0时使用Write-Back的Cache类型。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。
【PCD】:Page级的Cache Disable标志位。为1时,物理页面是不能被Cache的;为0时允许Cache。当CR0.CD=1时,此标志被忽略。
【A】:访问位。该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位。这个位可以被操作系统用来监视页的使用频率。
【D】:脏位。该位由处理器固件设置,用来指示此表项所指向的页是否写过数据。
【PS】:Page Size位。为0时,页的大小是4KB;为1时,页的大小是4MB(for normal 32-bit addressing )或者2MB(if extended physical addressing is enabled)。在页目录项中该位置0。
【G】:全局位。如果页是全局的,那么它将在高速缓存中一直保存。当CR4.PGE=1时,可以设置此位为1,指示Page是全局Page,在CR3被更新时,TLB内的全局Page不会被刷新。
【AVL】:被处理器忽略,软件可以使用。
第12-31位是我们真正关心的页表或者页的物理基地址。也就是通过它,虚拟地址被转化位真正的物理地址。
说了这么多,无非是从理论上对分页机制有了个大概的了解,还是让我们用实践来代替这些头痛的理论吧。
首先我们在system.asm中加入了void enter_page(void)函数。
【system.asm 节选】
(上面省略)
global _enter_page
align 16
_enter_page: ; void enter_page(void);
mov eax, 0x8000
mov cr3, eax
mov eax, cr0
; or eax, 0x80000000
; 在nasm中我们可以用这样的二进制形式来表示数据,
; 对于像置位或者清零这样的操作是不是很直观呀!
or eax, 1000_0000_0000_0000_0000_0000_0000_0000b
mov cr0, eax
jmp 0x08 : .1
.1:
ret
上面这段程序,就是从分页机制切换到分段机制的标准代码了。我们真理简单的说说高大上的分页机制是怎么来的吧。第一步没有在这个汇编程序中体现,它是用C来完成的,它就是简单的设置页目录表和一些页表,一会儿我们再讲。这里是从第二步开始的,也就是在CR3中存入页目录表的基地址。CR3就叫做页目录表的基地址寄存器了,它类似于全局描述符表寄存器,毕竟这个第一次出现的地址还是要找个特殊的东西存储,随时备用好的。而CR3和CR0一样,都是不能用立即数或内存直接存入数据的,需要用到eax(也不一定非要是这个累加器)来帮助存入页目录表的基地址。接下来的第三步是在前两部做好准备的基础上,真正的开启分页机制。这其实也是非常简单了,CR0的最高位就是分页机制的开启与否的标志位,通过把该位置1,分页机制立即开启。注意了它可不管你前面是否准备了页目录表还有页表,也不管你是否在CR3中存储了页目录表的基地址。
下面我们来看一下memory.c文件,看看究竟是怎样设置页目录表和页表的,当然这里还附赠了memory.h文件。
【memory.h】
// memory.h 创建者:至强 创建时间:2022年8月
#ifndef __MEMORY_H
#define __MEMORY_H
void page_init(void);
extern void enter_page(void);
#endif
【memory.c】
// memory.c 创建者:至强 创建时间:2022年8月
#include "memory.h"
void page_init(void) {
// 内核页目录表基地址一拍脑袋确定为0x8000。
unsigned int* pde_base = (unsigned int*)0x8000;
// 内核固定页表基地址确定为0x8000 + 4096,
// 初步计划占有8页的,共8 * 4096大小的内存,
// 从而内核可以直接访问的内存为8 * 4M,
// 这是因为一个页表可以映射4M的内存。
unsigned int* pte_base = (unsigned int*)(
(unsigned int)pde_base + 1 * 4096);
// 显存页表基地址放在内核固定页表的后面,
// 物理地址为0x8000 + 1 * 4096 + 8 * 4096。
unsigned int* video_pte_base = (unsigned int*)
((unsigned int)pte_base + 8 * 4096);
// 清空页目录表。
int i;
for(i = 0; i < 1024; i++) {
pde_base[i] = 0;
}
// 内核占用物理地址前32M,对等映射到虚拟地址前32M,
// 这里首先对页目录表建立映射关系,将页表的物理地址
// 和页目录表项的属性写入到页目录表项中。
for(i = 0; i < 8; i++) {
pde_base[i] = (unsigned int)pte_base + i * 4096 + 0x07;
}
// 显存采取对等映射,因此0xe0000000也映射到物理地址
// 的0xe0000000。在这里我们映射16M的显存。
for(i = 0; i < 4; i++) {
pde_base[0xe0000000 / 0x400000 + i] =
(unsigned int)video_pte_base + i * 4096 + 0x07;
}
//从0开始计算的页目录表第1022项是页目录表的物理地址藏身处,
//因此要在分页机制下访问页目录表,即是访问指针
//(unsigned int*)0xffbfe000。
pde_base[1022] = 0x8000 + 0x07;
// 建立对等映射前32M的页表,它紧跟在页目录表后边。
for(i = 0; i < 8 * 1024; i++) {
pte_base[i] = 0x00000000 + i * 4096 + 0x07;
}
// 建立显存对等映射的页表,它紧跟在内核页表后边,是16M。
for(i = 0; i < 4 * 1024; i++) {
video_pte_base[i] = 0xe0000000 + i * 4096 + 0x07;
}
enter_page();
}
下面让我们来用virtual box的调试功能看一下,到底分页机制是否成功开启。
VBoxDbg> info cpumguest
Guest CPUM (VCPU 0) state:
eax=00000023 ebx=001491b0 ecx=00000007 edx=00000000 esi=00000000 edi=00000000
eip=00101070 esp=00100fc8 ebp=00100ff0 iopl=0 nv up di pl zr na pe nc
cs=0008 ss=0010 ds=0010 es=0010 fs=0010 gs=0010 tr=0000 eflags=00000002
cr0=80000011 cr2=00000000 cr3=00008000 cr4=00000000 gdtr=00006000:07ff ldtr=0000
VBoxDbg> dpdg
%%0000000000008000 (index 0x0):
000 %0000000000000000: 00009027 4kb phys=00009000 p w u a avl=00
001 %0000000000400000: 0000a007 4kb phys=0000a000 p w u na avl=00
002 %0000000000800000: 0000b007 4kb phys=0000b000 p w u na avl=00
003 %0000000000c00000: 0000c007 4kb phys=0000c000 p w u na avl=00
004 %0000000001000000: 0000d007 4kb phys=0000d000 p w u na avl=00
005 %0000000001400000: 0000e007 4kb phys=0000e000 p w u na avl=00
006 %0000000001800000: 0000f007 4kb phys=0000f000 p w u na avl=00
007 %0000000001c00000: 00010007 4kb phys=00010000 p w u na avl=00
008 %0000000002000000: 00000000 4kb phys=00000000 np r s na avl=00
009 %0000000002400000: 00000000 4kb phys=00000000 np r s na avl=00
00a %0000000002800000: 00000000 4kb phys=00000000 np r s na avl=00
VBoxDbg> dpdg 380
%%0000000000008e00 (index 0x380):
380 %00000000e0000000: 00011027 4kb phys=00011000 p w u a avl=00
381 %00000000e0400000: 00012007 4kb phys=00012000 p w u na avl=00
382 %00000000e0800000: 00013007 4kb phys=00013000 p w u na avl=00
383 %00000000e0c00000: 00014007 4kb phys=00014000 p w u na avl=00
384 %00000000e1000000: 00000000 4kb phys=00000000 np r s na avl=00
385 %00000000e1400000: 00000000 4kb phys=00000000 np r s na avl=00
386 %00000000e1800000: 00000000 4kb phys=00000000 np r s na avl=00
387 %00000000e1c00000: 00000000 4kb phys=00000000 np r s na avl=00
388 %00000000e2000000: 00000000 4kb phys=00000000 np r s na avl=00
389 %00000000e2400000: 00000000 4kb phys=00000000 np r s na avl=00
38a %00000000e2800000: 00000000 4kb phys=00000000 np r s na avl=00
VBoxDbg> dpdg 3fe
%%0000000000008ff8 (index 0x3fe):
3fe %00000000ff800000: 00008007 4kb phys=00008000 p w u na avl=00
3ff %00000000ffc00000: 00000000 4kb phys=00000000 np r s na avl=00
400 %0000000100000000: 00000007 4kb phys=00000000 p w u na avl=00
401 %0000000100400000: 00001007 4kb phys=00001000 p w u na avl=00
402 %0000000100800000: 00002007 4kb phys=00002000 p w u na avl=00
403 %0000000100c00000: 00003007 4kb phys=00003000 p w u na avl=00
404 %0000000101000000: 00004007 4kb phys=00004000 p w u na avl=00
405 %0000000101400000: 00005007 4kb phys=00005000 p w u na avl=00
406 %0000000101800000: 00006027 4kb phys=00006000 p w u a avl=00
407 %0000000101c00000: 00007007 4kb phys=00007000 p w u na avl=00
408 %0000000102000000: 00008007 4kb phys=00008000 p w u na avl=00
VBoxDbg> dptg 0
%%0000000000009000 (base %0000000000000000 / index 0x0):
000 %0000000000000000: 00000007 4kb phys=00000000 p w u na nd avl=00
001 %0000000000001000: 00001007 4kb phys=00001000 p w u na nd avl=00
002 %0000000000002000: 00002007 4kb phys=00002000 p w u na nd avl=00
003 %0000000000003000: 00003007 4kb phys=00003000 p w u na nd avl=00
004 %0000000000004000: 00004007 4kb phys=00004000 p w u na nd avl=00
005 %0000000000005000: 00005007 4kb phys=00005000 p w u na nd avl=00
006 %0000000000006000: 00006027 4kb phys=00006000 p w u a nd avl=00
007 %0000000000007000: 00007007 4kb phys=00007000 p w u na nd avl=00
008 %0000000000008000: 00008007 4kb phys=00008000 p w u na nd avl=00
009 %0000000000009000: 00009007 4kb phys=00009000 p w u na nd avl=00
00a %000000000000a000: 0000a007 4kb phys=0000a000 p w u na nd avl=00
VBoxDbg> dptg e0000000
%%0000000000011000 (base %00000000e0000000 / index 0x0):
000 %00000000e0000000: e0000067 4kb phys=e0000000 p w u a d avl=00
001 %00000000e0001000: e0001067 4kb phys=e0001000 p w u a d avl=00
002 %00000000e0002000: e0002067 4kb phys=e0002000 p w u a d avl=00
003 %00000000e0003000: e0003067 4kb phys=e0003000 p w u a d avl=00
004 %00000000e0004000: e0004067 4kb phys=e0004000 p w u a d avl=00
005 %00000000e0005000: e0005067 4kb phys=e0005000 p w u a d avl=00
006 %00000000e0006000: e0006067 4kb phys=e0006000 p w u a d avl=00
007 %00000000e0007000: e0007067 4kb phys=e0007000 p w u a d avl=00
008 %00000000e0008000: e0008067 4kb phys=e0008000 p w u a d avl=00
009 %00000000e0009000: e0009067 4kb phys=e0009000 p w u a d avl=00
00a %00000000e000a000: e000a067 4kb phys=e000a000 p w u a d avl=00
VBoxDbg> dptg ffbfe000
%%0000000000008ff8 (base %00000000ffbfe000 / index 0x3fe):
3fe %00000000ffbfe000: 00008007 4kb phys=00008000 p w u na nd avl=00
3ff %00000000ffbff000: 00000000 4kb phys=00000000 np r s na nd avl=00
400 %00000000ffc00000: 00000007 4kb phys=00000000 p w u na nd avl=00
401 %00000000ffc01000: 00001007 4kb phys=00001000 p w u na nd avl=00
402 %00000000ffc02000: 00002007 4kb phys=00002000 p w u na nd avl=00
403 %00000000ffc03000: 00003007 4kb phys=00003000 p w u na nd avl=00
404 %00000000ffc04000: 00004007 4kb phys=00004000 p w u na nd avl=00
405 %00000000ffc05000: 00005007 4kb phys=00005000 p w u na nd avl=00
406 %00000000ffc06000: 00006027 4kb phys=00006000 p w u a nd avl=00
407 %00000000ffc07000: 00007007 4kb phys=00007000 p w u na nd avl=00
408 %00000000ffc08000: 00008007 4kb phys=00008000 p w u na nd avl=00
VBoxDbg>
看到上面的调试信息我们是不是有些望而生畏的感觉,是呀看似有些“无厘头”吗,不过仔细看来还是很贴心的。调试命令info cpumguest是显示虚拟机处理器状态的,我们要先找到CR0是0x80000011,最高位是1,说明已经成功的开启了分页机制,再看CR3是0x00008000,正是我们有意设置的页目录表的基地址。dpdg和dptg分别可以按页目录表和页表的形式显示我们的分页机制产生的映射关系,只不过页目录表需要提供的是下标,而页表提供的是虚拟地址。我还是简单的说一下吧。第一栏是页目录表或者页表的下标,二栏是虚拟地址,三栏是表项的实际值,四栏是单页尺寸,五栏是对应的物理地址,后面是各个属性位。
前面的内容其实忘了告诉大家了,grub是默认的把内核加载到物理地址1M处的。在我们系统引导的时候,grub在切换到保护模式的时候它也同时必须设置最基本的段地址,当然段地址都是从0开始了。上一章我们又重新设置了段,当然是全无新意,只不过是把描述符表的地址和描述符的顺序改变了而已。到了分页机制后,我们并不想挪动内核,也无意改变映射关系,所以我们将虚拟内存的前32M,完全重合的映射到物理内存的前32M上,作为我们内核的固定内存。由于显存的物理地址在0xe0000000处,所以如果不想改变显存地址的话,我们也要将地址重合的映射进去。在这里我们映射了16M。非常值得说的问题是,我们把页目录表的倒数第二项的物理地址设置成了页目录表的物理地址。之所以设置成这样,是因为,这才是我们开启分页机制的灵魂所在,有了它,我们将来在建立页映射关系时将如鱼得水。
下面我们就该讲脑子转“九九二百五十”转,也可能还是迷迷糊糊的如何问题。它实质其实就是在分页模式下如何访问页目录表和页表的问题。也可能聪明的你会说,那还不简单,页目录表不就在物理地址的0x8000处吗,页表不就在0x9000处吗?是的,我们也是的确把虚拟地址和物理地址对等的映射了,在内核的条件下,我们的确可以直接向这里写入相应的表项,完成不一样的映射关系的建立。但是,我要告诉你,这完全无用,因为这些映射关系我们在建立页机制的初始化时已经完全安排的妥妥的,从今以后根本不会也不能做任何改变了。它的更进一步的意思是说,我们不能安于现状,为了满足内存动态分配的目标,我们的内核中内存管理模块,要能够实时的建立和删除映射关系,最后也就是反应在要能够方便的操作页目录表和页表。并且在用户程序(进程)方面,页目录表和页表都是自身独有的,即是说都是动态分配和销毁的,到那时唯有通过,这个十分绕脑的方法才能顺利完成映射。因此,上我们必须在此处痛下决心,好好学习、天天向上才好。好了,下面我们来分享出这些宏还有函数,让大家见识一下什么叫绕脑。
【memory.h 节选】
(上面省略)
// 虚拟地址在pde中的下标
#define pde_idx(vaddr) (((vaddr) & 0xffc00000) >> 22)
// 虚拟地址在pte中的下标
#define pte_idx(vaddr) (((vaddr) & 0x003ff000) >> 12)
(中间省略)
unsigned int pde_ptr(unsigned int vaddr);
unsigned int pte_ptr(unsigned int vaddr);
(下面省略)
【memory.c 节选】
(上面省略)
unsigned int pde_ptr(unsigned int vaddr) {
return (unsigned int)(0xffbfe000 + (pde_idx(vaddr) << 2));
}
unsigned int pte_ptr(unsigned int vaddr) {
return (unsigned int)(0xff800000 +
((vaddr & 0xffc00000) >> 10)
+ (pte_idx(vaddr) << 2));
}
(下面省略)
上面的两个宏,如果知道分页机制中虚拟地址到物理地址的转化过程,理解起来是完全没有问题的。
我们看0xffc00000的二进制形式,就一目了然了。
1111_1111_1100_0000_0000_0000_0000_0000b,我们看到高10位的全为1的16进制是0xffc00000,不是我们可能想象的0xfff00000,而中间10位全为1的16进制是0x003ff000,也不是我们认为的0x00fff000。如果你仔细看,上述两个值加起来正好是0xfffff000。这样一来,进行“与”操作然后再相应的“移位”后得到的正是在页目录表或页表中的索引值(下标),注意这里不是偏移值。
下面的两个函数就让我们慢慢讲来吧。为什么0xffbfe000的值是访问页目录表的首地址呢?让我们来分析一下吧。首先来说,我们的页目录表的首地址是存放在cr3中的,而通过上面的设置以及调试我们知道,他的物理地址是0x00008000,因为是对等映射,所以,虚拟地址0x00008000其实也是页目录表的首地址,这一点大家认为应该没有问题吧。按照我们在页目录表中安排的“间谍”也就是从0开始数的第1022项,它的物理地址是0x00008000。怎么会这么巧呢?难道真是巧合。这时候我们就要看处理器是怎么想的了。这时候请允许我们先看一下0xffbfe000是个什么东西了。我们这个是的把它拆成0xff800000 + 0x003fe000的形式,为什么呢,大家看明白了吧,高10位中间10位拆出来,就跟我们直接看到的又差头了。按照处理器的想法,因为现在cr3中是0x00008000,所以它去那里找页目录表开始,同时去索引0xff800000那里找页目录表项,用以确定一个页表从哪来开始。可是如果真把0xff800000看成0xff8索引找的到页目录表项吗?显然时找不到的,因为一个页表仅仅有1024项,而0xff8比这个要大得多,直接看当然时找不到的。我们可以这样理解,我们要的仅仅时高10位,所以这里必须右移22位才能找到索引值。如果我们乖乖的移位的话,根本就不会云里雾里了,甚至都不用上面的拆分了。移位后我们发现所得的索引正是1022项(注意所以从0算起)。开悟了吧,这里是不是正好放的是数值为0x00008000的地址。到了现在处理器转换完成了吗?没有啊同学们,它刚刚只是找到了页表的物理地址。刚才我们说了也是页目录表的地址,没办法再接着关注中间10位吧,中间10位是要访问的页表的索引,对中间的数值我们还是乖乖的移位吧,这样不至于被坑。还好还好这回是0x3fe,不移位也不会被坑了。也是1022项,这回的转换终于找到了所在页的首地址。仍然是页目录表的基地址,哈哈,转一圈回到原点了。
现在来看我们的函数,前面0xffbfe000不用说了吧,用了它我们就能够顺利的访问内核的页目录表了。原因就是他是分页模式下,页目录的一个特殊的虚拟地址。当然我们也可以照这个样子映射多个特殊地址,只要你不怕浪费虚拟内存就行,要知道在页目录表中加入1项就减少4M虚拟内存。而且你没想到吧,我们的应用程序有自己的页目录表和页表,并且也把它定义成指向自己的页目录表,在分配虚拟内存是就也是一样的套路了。
该函数的最后一部分是形成一个完整地址的最后一步,即是处理器结合上面的转化自动的合并低12位偏移值产生一个物理地址。因为是访问页目录表,目的肯定是确定映射关系,所以如果以前没有确定过,这一回我们要写入数值,也就是找到某个要映射物理内存的地址的页目录项。前面的那个宏正是确定虚拟地址的页目录表项的索引的操作,这时在左移2位正是地址的偏移值。
函数unsigned int pte_ptr(unsigned int vaddr) 的作用是通过给定的虚拟地址得到该虚拟地址对应的页表项的虚拟地址。上面的话说着就绕嘴,听起来更一头雾水吧。我们知道一个虚拟地址是通过c程序中的指针指出的,而在汇编中是通过各种标号指出的,所以我们程序中使用的地址都是虚拟地址。当程序运行时,虚拟地址必定要对应一个实际的物理地址,而这种对应关系是处理器自动完成的。这个过程就是通过两级页表将虚拟地址转换成物理地址的过程。而一旦指定了对应关系后,转换的过程我们的程序根本不在意,也没有必要知道,只要它能够正确的运行就好了。然而操作系统中内存管理的单元却不能这样“耍大鞋”,动态分配内存时,内存管理单元必须要找到没有被使用的物理内存页面,并且把该页面的地址,写入到虚拟地址对应的页表项中,这样才能完成映射,是处理器访问该虚拟内存时不出现缺页异常。这时候问题就来了,动态分配内存的管理单元,在分配内存是很有可能连虚拟内存对应的页表还没有分配内存啊。因此在这种情况下,先为没有分配页表的虚拟地址分配页表的物理页面,并且不明确的和任何虚拟地址建立对应关系。有同学说这样不就悲催了吗?是啊,如果是按照这个思路来,就算想破了脑袋恐怕也是没有帮助啊。我们还是来看程序吧。看到0xff800000是不是想到了什么了,对吗,他不是对应虚拟地址的什么来的(还是请大家自己推算吧),反正是倒数第二个4M的虚拟地址空间。为什么这么说呢,因为一个页目录表项对应4M的内存呀,而且正好是转换关系中的页目录项。看来偷偷的访问虚拟地址对应页表项的重任要落在这4M的虚拟内存上了。再看中间的部分(vaddr & 0xffc00000) >> 10,竟然是虚拟地址的页目录索引,大家可别忘了,该目录项中存放的是正是虚拟地址对应的页表的物理地址啊。右移10位和上面的0xff800000的页目录表索引拼出来一个什么呢?正是一个能访问虚拟地址对应页表的虚拟地址。下面的就不用说了吧,它是得到了在该页表中的偏移量。
可惜的是我们现在还没有动态内存分配的管理单元,没法做实验呀。大家不要着急了,即使没有我们也能造出来一个,小试牛刀的东西,毕竟闭门造车是笔者的大本领吗!
我们的内核仅仅是到了虚拟内存的32M以下,因此32M以后的虚拟内存就是没有使用的,让我们在kernel.c中加入如下语句看看会在屏幕上看到什么结果吧。
printf_("%x %x", pde_ptr(0x2000000), pte_ptr(0x2000000));
果然为我们生成了两个奇奇怪怪的虚拟地址,看到头一个大家有想法了吧,正是当前内核页目录表的第8项(从0算起)。第二个虚拟地址就别细究了,反正是在倒数第二个4M内的某个页面中了。现在我们是不是就能访问这两个虚拟地址了呢?这可不行啊,还没有映射关系吗。那么,就让大家来看一下,在未做映射的情况下,使用该虚拟地址的后果吧!下面是kernel.c中新增大代码了。
【kernel.c 节选】
(上面省略)
printf_("%x %x\n", pde_ptr(0x2000000), pte_ptr(0x2000000));
/*
*((unsigned int*)pde_ptr(0x2000000)) = 0x2000000 + 0x07;
*((unsigned int*)pte_ptr(0x2000000)) = 0x2000000 + 0x07;
// *((unsigned int*)pte_ptr(0x2000000)) = 0x2001000 + 0x07;
for(i = 1; i < 1024; i++) {
((unsigned int*)0x2000000)[i] = 0;
}
// asm("mov %cr3, %eax;mov %eax, %cr3");
*/
printf_("%x %x\n", ((unsigned int*)0x2000000)[0],
((unsigned int*)0x2000000)[1]);
(下面省略)
通过上面的代码和截图大家可以看到,我们把构造映射关系的代码注释掉了,当此情况下,由于目前系统没有处理页异常的相关代码,因此就造成了虚拟机的宕机。
而当我们拿掉部分注释,运行虚拟机就不会出现任何问题。代码如下:
【kernel.c 节选】
(上面省略)
printf_("%x %x\n", pde_ptr(0x2000000), pte_ptr(0x2000000));
// 虚拟地址的页目录表项写入物理地址0x2000000 + 0x07,也就
// 是页表的首地址在0x2000000处。
*((unsigned int*)pde_ptr(0x2000000)) = 0x2000000 + 0x07;
// 页表项的物理地址也写入0x2000000 + 0x07,也就是最终该
// 虚拟地址映射到物理地址一样的地方。
*((unsigned int*)pte_ptr(0x2000000)) = 0x2000000 + 0x07;
// *((unsigned int*)pte_ptr(0x2000000)) = 0x2001000 + 0x07;
// 由于该地址已经映射了实际的物理内存,所以访问时不会出现
// 任何问题。需要注意的是,由于这里是页表第一项(从0开始)
// 是映射关系,所以如果把第一项也清零了,也会出现宕机的情况。
// 所以我们从第二项开始清零。
for(i = 1; i < 1024; i++) {
((unsigned int*)0x2000000)[i] = 0;
}
// 当第一项清零的情况下,如果不加入此句是不会宕机的。是不是
// 非常的奇怪。这是因为只有执行了下面一句,处理器才按照内存
// 中实际的页表建立映射关系。不要忘了处理器还有自己的缓存。
// 下面的汇编语句是不是觉得有些怪怪的,是啊,他是AT&T的格式
// 格式的非扩展的嵌入式汇编语句,我们在实际的编程中很少会
// 使用,因为这次是实验场景,再这里就请允许笔者偷个懒吧。
// 主要是函数调用得到形式要切换到汇编文件中。注意了大伙,
// AT&T格式的汇编,源操作数和目标操作数的顺序是相反的。
// asm("mov %cr3, %eax;mov %eax, %cr3");
printf_("%x %x\n", ((unsigned int*)0x2000000)[0],
((unsigned int*)0x2000000)[1]); (下面省略)
通过上面的截图大家可以看到,在信息区的第二行打印出了,虚拟地址0x2000000第一个4字节和第二个四字节的内容。第一个4字节大家熟悉吧。它正是我们的页表的第一项(数值上稍有不同)。
上面的注释虽然粗糙了一点,也还是能够说明笔者要讲的问题的。大家可以尝试者让上面的语句形成各种组合,看看会不会出现宕机的情况。从而更深入地了解,分页机制中的诀窍部分。下面我们通过virtual box的调试信息看看,是否和程序中的设置相符。
VBoxDbg> info cpumguest
Guest CPUM (VCPU 0) state:
eax=0000000e ebx=ff808000 ecx=00000007 edx=00008000 esi=00000000 edi=00000000
eip=001010f0 esp=00100fc8 ebp=00100ff0 iopl=0 nv up di pl zr ac pe nc
cs=0008 ss=0010 ds=0010 es=0010 fs=0010 gs=0010 tr=0000 eflags=00000012
cr0=80000011 cr2=00000000 cr3=00008000 cr4=00000000 gdtr=00006000:07ff ldtr=0000
VBoxDbg> dpdg
%%0000000000008000 (index 0x0):
000 %0000000000000000: 00009027 4kb phys=00009000 p w u a avl=00
001 %0000000000400000: 0000a007 4kb phys=0000a000 p w u na avl=00
002 %0000000000800000: 0000b007 4kb phys=0000b000 p w u na avl=00
003 %0000000000c00000: 0000c007 4kb phys=0000c000 p w u na avl=00
004 %0000000001000000: 0000d007 4kb phys=0000d000 p w u na avl=00
005 %0000000001400000: 0000e007 4kb phys=0000e000 p w u na avl=00
006 %0000000001800000: 0000f007 4kb phys=0000f000 p w u na avl=00
007 %0000000001c00000: 00010007 4kb phys=00010000 p w u na avl=00
008 %0000000002000000: 02000067 4kb phys=02000000 p w u a 6 avl=00
009 %0000000002400000: 00000000 4kb phys=00000000 np r s na avl=00
00a %0000000002800000: 00000000 4kb phys=00000000 np r s na avl=00
VBoxDbg> dptg 0x2000000
%%0000000002000000 (base %0000000002000000 / index 0x0):
000 %0000000002000000: 02000067 4kb phys=02000000 p w u a d avl=00
001 %0000000002001000: 00000000 4kb phys=00000000 np r s na nd avl=00
002 %0000000002002000: 00000000 4kb phys=00000000 np r s na nd avl=00
003 %0000000002003000: 00000000 4kb phys=00000000 np r s na nd avl=00
004 %0000000002004000: 00000000 4kb phys=00000000 np r s na nd avl=00
005 %0000000002005000: 00000000 4kb phys=00000000 np r s na nd avl=00
006 %0000000002006000: 00000000 4kb phys=00000000 np r s na nd avl=00
007 %0000000002007000: 00000000 4kb phys=00000000 np r s na nd avl=00
008 %0000000002008000: 00000000 4kb phys=00000000 np r s na nd avl=00
009 %0000000002009000: 00000000 4kb phys=00000000 np r s na nd avl=00
00a %000000000200a000: 00000000 4kb phys=00000000 np r s na nd avl=00
VBoxDbg>
为此,笔者专门把调试信息中发生变化的部分加入了下划线。可以看到为什么会出现目录项和页表项数值与我们设置的数值不一样的情况呢?通过翻看上面页表项和页目录表项的详细说明,原来当该页面被访问后,处理器会自动对已访问位和脏页位置位。