经过一系列的讲解,我们从启动扇区一直加载到了分段。
分段让操作系统具备了对内存的保护能力,通过描述符表、选择子的多级跳转,让每一段内存都增加了一系列属性,从而可以实现读、写、执行等权限以及为不同程序赋予不同特权的保护功能。 在此前的文章中,我们已经提到,通过 LDT 来解决进程间内存独立的问题,其代价是寄存器的反复加载,这对于 CPU 来说是一件较为耗时的操作,于是,80386 开始,Intel 引入了内存分页功能,相比于 LDT,更为灵活高效,因此 LDT 已经基本不会被使用了。 那么,分页究竟是一种什么样的机制,又是如何实现的呢?本文我们就来一探究竟。
随着 80286 保护机制的引入,让多个程序共用 CPU、内存来执行成为了可能,虽然 CPU 可以通过反复的保存现场并切换完成多个进程的并发执行,但昂贵而又容量有限的内存成为了最大的限制,虽然 32 位的地址总线提供了 4GB 内存的寻址能力,但程序的运行受限于实际的内存容量,同时,系统在启动时又很难预先定义每个进程究竟要分配多大的段空间来满足每一个应用程序的需要。 此时,操作系统迫切需要一种类似 CPU 任务切换的机制来对内存进行切换,可以想到,这需要从两个方面来进行考虑:
经过上述的离散化与虚拟化,分页机制就这样诞生了。 从 80386 开始,内存被分为 4KB 固定大小的“页”,他们在需要使用时载入内存,不需要使用时可以被置换到磁盘上,由分页机制将程序持有的固定的线性地址动态映射到物理地址上。
如图所示,在 80X86 的软硬件设计中,实现了两级页表。 第一级页表 Root page table 被称为“页目录表”,总计占用 4KB 空间,每个表项占用 4 字节,共计 1024 个表项,因此通过线性地址的最高 10bits 可以索引每一个表项,每个表项简称“PDE”(Page Directory Entry) 第二级页表是直接保存物理页基地址的列表,他同样有 1024 个表项,每个表项 4 字节,通过线性地址的中间 10bits 来进行索引。 线性地址剩余的 12bits 用来索引最终指向的页面的 4KB 内存。
PDE 与 PTE 的结构非常相似:
此前,我们介绍了 CPU 的控制寄存器。 进军保护模式
硬件控制开关寄存器 cr0 的部分字段如下图所示:
这里重点介绍 PG 位、WP 位与 CD 位:
PG 位就是是否开启分页的标志,当 PG 位被置为 1,则开启分页模式,上述一系列机制开始生效。
WP 位是内核写保护位,当 WP 位为 0,那么当 CPL 为 0、1、2(系统级)的程序去访问 U/S 位为 1 的内存页时,不再校验页的 R/W 位,系统级程序对所有用户级页面均具有读写权限。 如果 WP 位为 1,那么系统级程序访问用户级内存时,仍然要校验用户级内存的 R/W 位是只读还是读写权限。
CD 位是页表缓冲位,用来标识是否开启 CPU 页表缓冲。 所谓的“页表缓冲”简称为“TLB”(translation lookaside buffer),指的是 CPU 内的一块缓冲区,用来缓存经常访问的页目录和页表项,从而加快访问页目录与页表的时间。 如果 CD 位为 0,即不开启 CPU 页表缓冲,那么 PDE 与 PTE 中的 PWT 位、PCD 位与 G 位也将不起作用。
接下来我们就来实战开启内存分页机制。 经过上述讲解,我们已经对分页机制了解的十分清楚了,那么,如何在我们已有的分段代码基础上实现分页机制呢? 为了以最快速度上手实战,我们这里不考虑线性地址与物理地址的映射关系,直接取线性地址 = 物理地址,同时假设内存地址空间足够容纳所有页面,并且页、页表在内存中均连续。
PageDirBase equ 200000h ; 页目录开始地址: 2M
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, 92h ; Page Directory,可读写
这里我们直接选择内存 2M 地址作为页目录表的起始地址,且页目录表大小为 4KB。
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
PageTblBase equ 201000h ; 页表开始地址: 2M+4K
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, 8092h ; Page Tables,段界限为 1023 * 4096 字节
页表位于页目录起始地址后 4KB 位置,同样为可读写数据段,但此处,我们置位了 GDT 描述符的 G 位,表示段界限的单位为 4096 字节。
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
接下来我们就要填充上面定义的 4096 字节的页目录中的每一个表项 PDE。
; 初始化页目录
mov ax, SelectorPageDir ; 此段首地址为 PageDirBase
mov es, ax
mov ecx, 1024 ; 共 1K 个表项
xor edi, edi
xor eax, eax
mov eax, PageTblBase | 7 ; 用户级、存在于内存、可读写
.filter_pde:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .filter_pde
这里通过 stosd 指令与 loop 指令,将 eax 的内容循环填充到 4KB 大小的页目录表内。
这两个指令我们之前已经有过很多使用:
; 初始化所有页表 (1K 个, 4M 内存空间)
mov ax, SelectorPageTbl ; 此段首地址为 PageTblBase
mov es, ax
mov ecx, 1024 * 1024 ; 共 1M 个页表项, 也即有 1M 个页
xor edi, edi
xor eax, eax
mov eax, 7 ; 用户级、存在于内存、可读写
.filter_pte:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .filter_pte
; 设置页目录表起始地址
mov eax, PageDirBase
mov cr3, eax
; 开启分页机制
mov eax, cr0
or eax, 80000000h
mov cr0, eax
mov eax, cr0
and eax, 7FFFFFFEh ; PE=0, PG=0
mov cr0, eax
; ---------------- 内存段描述符宏 -------------
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro
PageDirBase equ 200000h ; 页目录开始地址: 2M
PageTblBase equ 201000h ; 页表开始地址: 2M+4K
; ------------ DOS 加载初始内存地址 -----------
org 0100h
jmp LABEL_BEGIN
; ------------------- GDT ---------------------
[SECTION .gdt]
; GDT
; 段基址, 段界限, 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, 92h ; Normal 描述符
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, 92h ; Page Directory,可读写
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, 8092h ; Page Tables,段界限为 1023 * 4096 字节
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, 4098h ; 非一致代码段
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, 98h ; 非一致代码段, 用于跳回 16 BITS 模式
LABEL_DESC_DATA: Descriptor 0, DataLen-1, 92h ; 可读写数据段,界限 64KB
LABEL_DESC_STACK: Descriptor 0, TopOfStack, 4093h ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, 92h ; 显存首地址
; ------------------ END OF GDT ----------------
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; ------------------ GDT 选择子 -----------------
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; --------------- END OF 段选择子 ----------------
[SECTION .data1] ; 数据段
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
BootMessage: db "Hello World my OS, techlog.cn!", 0
OffsetBootMessage equ BootMessage - $$
DataLen equ $ - LABEL_DATA
; 全局堆栈段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
; 初始化段基址寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp
; 初始化 16 位代码段描述符
mov ax, cs
movzx eax, ax
shl eax, 4
add eax, LABEL_SEG_CODE16
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化非一致代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32 ; 计算非一致代码段基地址物理地址
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 初始化数据段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
; 初始化堆栈段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
; 准备加载 GDTR
xor eax, eax ; 清空 eax 寄存器
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; 计算出 GDT 基地址的物理地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关闭硬件中断
cli
; 打开 A20 地址总线
in al, 92h
or al, 00000010b
out 92h, al
; 置位 PE 标志位,打开保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 跳转进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
; 从保护模式跳回到实模式
LABEL_REAL_ENTRY:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode]
; 关闭 A20 地址线
in al, 92h
and al, 0fdh
out 92h, al
; 打开硬件中断
sti
; 触发 BIOS int 21h 中断,回到实地址模式
mov ax, 4c00h
int 21h
[SECTION .s32] ; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
call SetupPaging
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorVideo
mov gs, ax ; 赋值视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
xor edi, edi
mov edi, 80 * 2 * 2 ; 屏幕第 2 行, 第 0 列
xor esi, esi
mov esi, OffsetBootMessage
call DisplayString
jmp SelectorCode16:0
; ---------------------- 分页机制启动 ---------------------------
SetupPaging:
; 为简化处理, 所有线性地址对应相等的物理地址.
; 初始化页目录
mov ax, SelectorPageDir ; 此段首地址为 PageDirBase
mov es, ax
mov ecx, 1024 ; 共 1K 个表项
xor edi, edi
xor eax, eax
mov eax, PageTblBase | 7 ; 用户级、存在于内存、可读写
.filter_pde:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .filter_pde
; 初始化所有页表 (1K 个, 4M 内存空间)
mov ax, SelectorPageTbl ; 此段首地址为 PageTblBase
mov es, ax
mov ecx, 1024 * 1024 ; 共 1M 个页表项, 也即有 1M 个页
xor edi, edi
xor eax, eax
mov eax, 7 ; 用户级、存在于内存、可读写
.filter_pte:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .filter_pte
; 设置页目录表起始地址
mov eax, PageDirBase
mov cr3, eax
; 开启分页机制
mov eax, cr0
or eax, 80000000h
mov cr0, eax
ret
; ------------------------- 打印字符串 -------------------------
DisplayString:
push eax
mov ah, 8Ch ; 0000: 黑底 1100: 红字
cld
.loop_label:
lodsb
test al, al
jz .over_print
mov [gs:edi], ax
add edi, 2
jmp .loop_label
.over_print:
pop eax
ret
SegCode32Len equ $ - LABEL_SEG_CODE32
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and eax, 7FFFFFFEh ; PE=0, PG=0
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp word 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]
https://en.wikipedia.org/wiki/Paging。 https://slide-finder.com/view/IA32-Paging-Scheme-Introduction.268455.html。 https://en.wikipedia.org/wiki/Translation\_lookaside\_buffer。 https://en.wikipedia.org/wiki/Memory\_type\_range\_register。 https://en.wikipedia.org/wiki/Control\_register。