0x00
关于原理和机制很多我直接在注释中写明了,所以就不过多阐述,更多的是总体上简单提一嘴。
0x01 为何分页
在有新进程需要内存而连续内存不够时,要么等某个进程使用内存完毕再给新进程使用,要么把某些进程使用的某些段内存先换出到硬盘,腾出一片连续内存来给新进程使用。
当然不可能采取第一种方法,不然新进程要等半天才能运行。第二种方法的思想是正确的,但是受限于段式内存管理,十分笨重,一次就要换出一整段,效率不高。并且如果内存很小甚至装不下任何一个进程的全部段,那不是啥都运行不了?
直接引用《操作系统真象还原》的原话: 问题的本质是在目前只分段的情况下,CPU 认为线性地址等 于物理地址。而线性地址是由编译器编译出来的,它本身是连续的,所以物理地址也必须要连续才行,但 我们可用的物理地址不连续。换句话说,如果线性地址连续,而物理地址可以不连续,不就解决了吗。
所以分页机制产生了。每一页不大不小 0x1000 字节。分页解除了线性地址与物理地址一一对应的关系,然后将它们的关系重新建立。通过某种映射关系,可以将线性地址映射到任意物理地址。也就是虚拟地址到物理地址的映射。这个乱序映射操作的单位就是页。如下图所示

0x02 虚拟地址空间的管理
以32位地址空间为例。
简单粗暴使用一级页表的话,也就是

也就是把 32 位虚拟地址的高 20 位划分为页表的索引,低 12 位划分为页内偏移进行一个映射。通过索引在一级页表中找到真实的物理地址的高 20 位,也就是物理页框号,在加上 12 位页内偏移就唯一确定了一个物理地址。
使用二级页表则是多划分一次。

把 32 位虚拟地址的高 10 位作为页目录表(PDE)的索引, 中间 10 位作为页表(PTE)的索引,低 12 位仍然是页内偏移。通过页目录表找到对应页表的物理地址,然后再对应页表项确定物理页的地址,加上页内偏移确定一个物理地址。
一级页表简单易懂,但是缺点是:每一个进程都要创建一个页表,而一个页表如果完全映射完所有物理内存,就需要 1M 个表项,每个表项是 4 字节,一张一级页表就要 4MB 内存。占用太大。
二级页表则可以动态的创建页表,并且每一个页表大小也只需一页也就是 0x1000 字节(4KB)。以及不提前创建好所有页表,也就是不提前分配物理页到虚拟地址空间,在程序访问某个虚拟地址时检索 PDE,如果对应的页表不存在,则现场创建一张页表,然后加入到页目录表;检索 PTE ,如果对应页面不存在,则现场分配或者替换一个物理页到页表中。这些功能在后续我们实现内核时需要实现:也就是缺页中断。
还有一个重要的问题:PDE 和 PTE 本身也处于内存中,如何通过虚拟地址访问他们本身呢? 解决方案是在 PDE 的最后一个表项放入 PDE 本身的物理地址。这样就实现了一个回环,可以通过虚拟地址访问 PDE 和 PTE 本身。
0x03 实现
还有进入保护模式前调用 BIOS 中断获取内存大小属性等信息的部分就不多赘述了,也就是调用几个 BIOS 提供的中断接口来获取内存信息。实现也就不放了。
代码注释有部分的细节机理。可能错误,请斧正。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| ; =================================== 创建内核页表和页目录表 setup_page: ; 虚拟地址32位划分 ; [10位][10位][12位] ; ↓ ↓ ↓ ; 目录 页表 页内偏移
; ==== 先把页目录表占用的空间逐字节清0 mov ecx, 0x1000 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir ; ==== 开始创建内核地址空间最低4MB的页目录项(PDE) .create_pde: mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ;页目录表本身占用0x1000大小,+0x1000就是第一个页表的物理地址 mov ebx, eax ;此处为ebx赋值, 是为.create_pte做准备, ebx为基址 or eax, PG_US_U | PG_RW_W | PG_P ;页目录项的属性RW和P位为1,US为1表示用户属性,所有特权级都可以访问 mov [PAGE_DIR_TABLE_POS + 0x0], eax ;第一个目录项 mov [PAGE_DIR_TABLE_POS + 0xc00], eax ;一个页目录项占用4字节, +0xc00对应第0x300个页目录项 ; 一个页目录项对应一张页表,一张页表大小0x1000字节, 一个页表项4字节, 共记录0x400页, 一页大小也是0x1000 ; 那么第0x300个页目录项也就对应地址 0x300*0x400*0x1000 = 0xc0000000(内核地址的起始) 开始的 4MB 内存 ; 这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表, 也就使用相同的映射方式, 映射到同一个物理地址 sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax ;使得最后一个目录项地址指向页目录表自己的地址
; ==== 开始创建内核地址空间最低1MB对应的页表项(PTE), 实际映射到物理地址最低的1MB mov ecx, 256 ;1M低端内存/每页大小4K = 256, 因为物理地址最低的4MB只使用最低的1MB内存来存放kernel, 所以内核地址对应的的页表只需创建 256 页 mov esi, 0 ;虚拟地址0x0~0x3fffff和虚拟地址0xc0000000~0xc03fffff对应的物理页,现在只用了低1MB,此时虚拟地址是等于物理地址的 mov edx, PG_US_U | PG_RW_W | PG_P ; 页表属性。注意进入循环前第一页物理地址是0起始,所以直接用属性赋值即可 .create_pte: ;创建Page Table Entry mov [ebx + esi*4], edx ;ebx为第一个页表的首地址,此前已赋值 add edx, 0x1000 ; 下一页 inc esi loop .create_pte
; ==== 创建内核地址空间其他页面的PDE (内核占用1GB, 此前只创建了最低的4MB的PDE) mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 ;此时eax为第二张页表的物理地址 or eax, PG_US_U | PG_RW_W | PG_P ;页表属性 mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 ;范围为第0x301~0x3fe的所有页目录项数量(第0x3ff项是页目录表自身的物理地址) mov esi, 769 .create_kernel_pde: mov [ebx+esi*4], eax inc esi add eax, 0x1000 ; 下一张页表 loop .create_kernel_pde ; 至此为内核地址空间1GB范围建立了页目录项 ret
|
最后 qemu 运行效果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| (qemu) xp /16wx 0x100000 0000000000100000: 0x00101027 0x00000000 0x00000000 0x00000000 0000000000100010: 0x00000000 0x00000000 0x00000000 0x00000000 0000000000100020: 0x00000000 0x00000000 0x00000000 0x00000000 0000000000100030: 0x00000000 0x00000000 0x00000000 0x00000000 (qemu) xp /16wx 0x101000 0000000000101000: 0x00000027 0x00001007 0x00002007 0x00003007 0000000000101010: 0x00004007 0x00005007 0x00006007 0x00007007 0000000000101020: 0x00008007 0x00009007 0x0000a007 0x0000b007 0000000000101030: 0x0000c007 0x0000d007 0x0000e007 0x0000f007 (qemu) info mem 0000000000000000-0000000000100000 0000000000100000 urw 00000000c0000000-00000000c0100000 0000000000100000 urw 00000000ffc00000-00000000ffc01000 0000000000001000 urw 00000000fff00000-0000000100000000 0000000000100000 urw (qemu) x/16wx 0xFFFFF000 fffff000: 0x00101027 0x00000000 0x00000000 0x00000000 fffff010: 0x00000000 0x00000000 0x00000000 0x00000000 fffff020: 0x00000000 0x00000000 0x00000000 0x00000000 fffff030: 0x00000000 0x00000000 0x00000000 0x00000000 (qemu) x/16wx 0xFFC00000 ffc00000: 0x00000027 0x00001007 0x00002007 0x00003007 ffc00010: 0x00004007 0x00005007 0x00006007 0x00007007 ffc00020: 0x00008007 0x00009007 0x0000a007 0x0000b007 ffc00030: 0x0000c007 0x0000d007 0x0000e007 0x0000f007
|
可以看到虚拟地址空间的映射完成了,并且使用虚拟地址对于 PDE 和 PTE 的访问也没问题。至此就为内核地址空间 (1GB) 都建立了 PDE,然后为其中最低的 1MB 建立好了实际虚拟地址映射,即将虚拟地址 0x000000000x000FFFFF 和 0x000000000x000FFFFF 都映射到了物理地址 0xC00000000xC00FFFFF。其余的 0xC01000000xC03FFFFF 还没有映射。