x86系列处理器上的内存分页
- 硬件层直接支持内存分页机制
- 默认情况下并没有打开分页机制
- 分页机制开启后,使用二级页表对内存进行管理
启用分页机制步骤
- 创建页目录表及页表
- 将页目录表首地址写入控制寄存器 cr3
- 寄存器 cr0 的 PG 位置 1
启动分页机制代码实现
- 找到以前的代码 loader.asm,在这个代码的基础上做更改
- 回顾代码的框架
boot.asm 跳转到 loader.asm,并打印 “Welcome to KOS.” | CODE16_START ; 实模式,打印 “Loader...” | CODE32_START ; 进入保护模式,打印 “Enter protection”
- 先上改动后的完整代码 loader.asm
- 为了程序的美观,我们把开启分页机制的工作都封装成了一个函数 setup_page
- 先来看一下开启分页机制的第 2 步和第 3 步,这两步比较简单
; 将页目录表首地址写入控制寄存器 cr3 mov eax, PAGE_DIR_BASE mov cr3, eax ; 寄存器 cr0 的 PG 位置 1 mov eax, cr0 or eax, 0x80000000 mov cr0, eax
- 下面我们就实现页目录表及页表的创建
- 知识点准备
- 页目录占用 1 内存页(可访问 1024 个子页表)
- 单个子页表占用 1 内存页(可访问 1024 个页面)
- 页面起始地址按 4K 字节对齐(4096 的整数倍)
- 分页后可访问的虚拟地址空间为:4K * (1024 * 1024) = 4G
- 首先,我们脑子中要有页表的整体样子,如下图(由下往上看)。PDE:页目录项;PTE:页表项
- 从图中可以看出,为了让页表和更紧凑一些,页目录后面紧跟着页表,其实这不是必须的
- 页目录表和页表都存在于物理内存之中,它们自身也要有个“安身”的地方,我们就把页目录表放在物理地址 0x100000 处(),页目录本身占 4KB,所以第一个页表的物理地址是 0x101000
- 第一个页目录项(忽略属性)PDE0 的值为 0x101000,第二个页目录项(忽略属性)PDE1 的值为 0x102000,依次类推...
- 第一个页表项(忽略属性)PTE0 的值为 0x0(第一个页表我们就让它指向实际物理内存 0x0 处),第二个页表录项(忽略属性)PTE1 的值为 0x1000,依次类推... 不过这在图中没有显示出来
- 从 PDE0 找到 PTE0,然后再从 PTE0 找到实际内存 0x0;从 PDE1 找到 PTE1,然后再从 PTE1 找到实际内存 0x1000;从 PDE2 找到 PTE2,然后再从 PTE2 找到实际内存 0x2000。依次类推...
- 在实现创建页目录表和页表代码之前,我们先学习一个汇编小知识
; 传送指令 stosb / stosw / stosd
- 把 al / ax / eax 中的值存储到 es:edi 指向的内存单元中
- 同时 edi 的值根据方向标志自增或自减(cld:自增 / std:自减)
- 举例:
mov ax, 0 mov es, ax ; es = 0 mov edi, 0 ; edi = 0 mov eax, 0xFF cld stosd ; 程序执行到这里后 ; 地址 0:0 所指向的内存被赋值 0xFF ; edi = 0x04 ; eax 为 4 字节,顾 edi 自增 4 字节
- 由于我们是在保护模式下开启分页机制,所以把页目录表和页表也当成一种数据段处理,于是段描述符相关操作来一遍
; 增加页目录表和页表的段描述符和选择子 PAGE_DIR_DESC : Descriptor PAGE_DIR_BASE, 4095, DA_DRW + DA_32 ; 子页表我们把颗粒度 G 设为 1,则内存单位就变为了 4K ,于是子页表内存数据段总大小 = (1023+1)*4K PAGE_TAB_DESC : Descriptor PAGE_TAB_BASE, 1023, DA_DRW + DA_32 + DA_LIMIT_4K PAGE_DIR_SELECTOR equ (0x0005 << 3) + SA_RPL0 + SA_TIG PAGE_TAB_SELECTOR equ (0x0006 << 3) + SA_RPL0 + SA_TIG
- 就剩下最后的页目录表和页表实现代码实现了,用 C 语言来理解就是循环给页目录表和页表这两个数组填充每一个元素
; 创建页目录表 mov ax, PAGE_DIR_SELECTOR mov es, ax mov edi, 0 ; es:edi -> PAGE_DIR_SELECTOR:0 mov cx, 1024 ; 循环 1024 次,页目录共 1024 项 mov eax, PAGE_TAB_BASE + PG_P + PG_US_U + PG_RW_W ; eax = 第一个子页表的地址和属性 cld ; edi 自增 .creat_pde: ; 创建页目录项 stosd ; 把 eax 中的值写入 [es:edi] 指向的内存中 add eax, 4096 ; eax = eax + 4K loop .creat_pde
; 创建页表 mov ax, PAGE_TAB_SELECTOR mov es, ax mov edi, 0 ; es:edi -> PAGE_TAB_SELECTOR:0 mov ecx, 1024*1024 ; 循环 1024*1024 次,共 1024 个页表,每个页表有 1024 个页表项 mov eax, PG_P + PG_US_U + PG_RW_W ; 只有属性,没有基地址,因为第一个页表项指向的就是物理地址 0x0 处 cld .creat_pte: ; 创建页表项 stosd ; 把 eax 中的值写入 [es:edi] 指向的内存中 add eax, 4096 ; eax = eax + 4K loop .creat_pte
发现
- 我们来改动一下代码
; 原代码 call setup_page 开启分页机制后直接就死循环了 mov ebp, msg2Offset mov bl, 0x0F ; 打印属性,黑底白字 ; 坐标 (0, 2) mov dl, 0x00 mov dh, 0x02 call print_str_32 call setup_page jmp $
- 改动后
; 改动后,先 call setup_page 开启分页机制,再打印"Enter protection" call setup_page mov ebp, msg2Offset mov bl, 0x0F ; 打印属性,黑底白字 ; 坐标 (0, 2) mov dl, 0x00 mov dh, 0x02 call print_str_32 jmp $
- 编译执行,发现程序居然跟改动前一样,按道理开启分页机制后,映射后的地址不应该改变,导致开启分页机制后的程序无法执行吗?
- 猜测:我们的分页机制没有开启成功?
- 验证一下,把 add eax, 4096 注释掉,破坏页表的创建
.creat_pte: ; 创建页表项 stosd ; 把 eax 中的值写入 [es:edi] 指向的内存中 ; add eax, 4096 ; eax = eax + 4K loop .creat_pte
- 编译运行一下,发现 "Enter protection" 未打印出来,程序崩溃了,感觉分页机制好像是开启了的
- 这到底是什么原因呢?
- 以一个具体地址 0x00804ABC,分析其映射后的地址变成了多少。我们把 0x00804ABC 转成二进制 100000000100101010111100,其中低 12 位的值为:0xABC,中 10 位的值为:0x4,高 10 位的值为:0x2。我们的第一个子页表对应的实际物理地址 0x0 处,0x00804ABC 映射后的实际物理地址为:4096
*
(1024*
0x02 + 0x04) + 0xABC = 0x00804ABC。映射前后的地址居然是一样的,自己可以再随便换个地址映射一下看看是不是一样。 - 让我们来反汇编后断点调试证明
- make 之后反汇编一下
ndisasm -o 0x900 loader.bin > loader.txt
- 找到程序最后死循环处(jmp $):0xA14,使用 “b 0xA14”在 0xA14 处打断点,执行 “c” 命令后程序运行到断点处,根据页目录表首地址 0x100000 和 高 10 位的值 0x2,于是我们找到 0x00804ABC 地址对应的页目录项实际对应的物理地址为 0x100000 + 2
*
4 = 0x100008,用 “xp 0x100008”命令 查看 0x100008 地址处的值为 0x00103007,其中低 12 位是属性,高 20 位为对应的子页表首地址,于是我们再根据中 10 位的值 0x4 计算出对应的子页表中的页表项地址为 0x103000 + 4*
4 = 0x103010,然后我们使用 “xp 0x103010” 命令查看 0x103010 地址处的值,发现其值为 0x00804007,去掉低 12 位的属性,高 20 位的地址为 0x00804000,最后我们再加上偏移量 0xABC 得到最终映射的物理地址为 0x00804ABC
<bochs:3> xp 0x100008 0x00100008 <bogus+ 0>: 0x00103007 <bochs:4> xp 0x103010 0x00103010 <bogus+ 0>: 0x00804007
- 当前的分页方式使得:任意虚拟地址都被直接映射为物理地址,因此,setup_page 调用的时间并不影响程序的执行结果
- 由此可以看出,我们目前构建的页目录和页表其实是一种非常简单的方式,简单到直接将虚拟地址映射成物理地址