手写 Loader,开启保护模式与虚拟内存
Loader 从 MBR 手中接过接力棒后,便可以为加载和运行现代内核做准备了,即实现以下任务和能力:
- 初始化 GDT 并进入保护模式,突破实模式 1MB 内存空间的牢笼,这是加载现代内核的前提
- 初始化 kernel 页目录和页表,开启分页功能(虚拟内存),解锁现代内核虚拟内存管理的能力
- 加载并进入内核
在 Loader 中进入保护模式
上一章实现了打印 Loader 字符串的程序,现在需要将其改造成进入保护模式再打印 Loader 的程序。在 CPU 工作模式 - 实模式/保护模式以及长模式 的保护模式一节中,我们了解到段描述符是一个 8 字节(64 位)的结构体,存储段的基地址、段限长(大小)以及访问权限等信息。进入保护模式其实就是定义系统全局的数据段与代码段,然后通过 lgdt 指令加载它,最后将 CR0 寄存器的第 0 位置 1 即可:
bits 16org 0x8000
; this is how constants are definedVIDEO_MEMORY equ 0xb8000WHITE_ON_BLACK equ 0x0f ; the color byte for each character
mov bp, 0x7c00 ; set the stack mov sp, bp
; 清空屏幕 call cls
mov bx, MSG_REAL_MODE call print ; This will be written after the BIOS messages
call switch_to_pm jmp $ ; this will actually never be executed
gdt_start: ; don't remove the labels, they're needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte
; GDT for code segment. base = 0x00000000, length = 0xfffff; for flags, refer to os-dev.pdf document, page 36gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31
; GDT for data segment. base and length identical to code segment; some flags changed, again, refer to os-dev.pdfgdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0
gdt_end:
; GDT descriptorgdt_descriptor: dw gdt_end - gdt_start - 1 ; size (16 bit), always one less of its true size dd gdt_start ; address (32 bit)
; define some constants for later useCODE_SEG equ gdt_code - gdt_startDATA_SEG equ gdt_data - gdt_start
MSG_REAL_MODE db "Started in 16-bit real mode", 0MSG_PROT_MODE db "Loaded 32-bit protected mode", 0
switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment
print: pusha
; keep this in mind:; while (string[i] != 0) { print string[i]; i++ }
; the comparison for string end (null byte)print_start: mov al, [bx] ; 'bx' is the base address for the string cmp al, 0 je done
; the part where we print with the BIOS help mov ah, 0x0e int 0x10 ; 'al' already contains the char
; increment pointer and do next loop add bx, 1 jmp print_start
done: popa ret
print_nl: pusha
mov ah, 0x0e mov al, 0x0a ; newline char int 0x10 mov al, 0x0d ; carriage return int 0x10
popa ret
; 清屏,并将屏幕设置为黑底白字cls: mov ax, 0x0600 ; ah=06h(滚动窗口功能), al=00h(清空整个窗口) mov bx, 0x0700 ; bh=07h(空白行的显示属性: 黑底白字) mov cx, 0 ; ch=00h(左上角行号=0), cl=00h(左上角列号=0) mov dx, 184fh ; dh=18h(右下角行号=24), dl=4fh(右下角列号=79) int 10h ; 调用bios中断
; 重置光标到 (0,0) mov ah, 0x02 xor dx, dx ; DH=0, DL=0 mov bh, 0 int 0x10 ret
[bits 32]init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax
mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp
call BEGIN_PM ; 7. Call a well-known label with useful code
BEGIN_PM: ; after the switch we will get here mov ebx, MSG_PROT_MODE call print_string_pm ; Note that this will be written at the top left corner jmp $
print_string_pm: pusha mov edx, VIDEO_MEMORY
print_string_pm_loop: mov al, [ebx] ; [ebx] is the address of our character mov ah, WHITE_ON_BLACK
cmp al, 0 ; check if end of string je print_string_pm_done
mov [edx], ax ; store character + attribute in video memory add ebx, 1 ; next char add edx, 2 ; next video memory position
jmp print_string_pm_loop
print_string_pm_done: popa ret将上面的代码编译并通过 dd 插入到 loader.vhd 后运行,可以观察到下面的结果:

整理代码
无论是进入保护模式时定义的 GDT,还是开启分页需要定义的页表,都涉及大量数据结构与常量。为了避免这些干扰项,使代码逻辑更加清晰,可以将这些定义放在**包含文件(.inc)**中,在主代码文件中使用 %include 包含进来。
首先定义包含文件 bootmacros.inc:
; -----------------------------------------------------------------------; bootmacros.inc — Boot constants, GDT macros, selectors, and paging helpers; NASM syntax; -----------------------------------------------------------------------
; =================================================================================; 内存布局常量; =================================================================================MBR_BASE_ADDR equ 0x7C00 ; BIOS加载MBR的物理地址LOADER_BASE_ADDR equ 0x8000 ; 引导程序加载地址LOADER_START_SECTOR equ 1 ; 引导程序起始扇区
; =================================================================================; 视频内存常量; =================================================================================VIDEO_MEMORY equ 0xB8000 ; 文本模式视频内存地址WHITE_ON_BLACK equ 0x0F ; 黑底白字属性字节
; ================================================================================; GDT 结构宏定义; ================================================================================; 每个 GDT 描述符占 8 字节,总结构如下(按低地址到高地址):;; 31 16 15 0; +------------------------------------+------------------------------------+; | Base[15:0] | Limit[15:0] |; +------------------------------------+------------------------------------+; | Base[23:16] | Access Byte | Flags | Limit[19:16] | Base[31:24] |; +------------------------------------+------------------------------------+;; Access Byte (第 5 字节)控制段的类型、存在、特权等;; Flags (第 6 字节高 4 位)控制段粒度、操作数大小、64 位模式等。;; ------------------------------------------------------------------------------; Intel 手册:Descriptor Type Fields; Access Byte (bits 8..15) → 控制"段类型"; Flags (bits 20..23) → 控制"段属性"; ================================================================================
; ================================================================================; [1] Access Byte 位布局(8 bits); ================================================================================; bit | 名称 | 说明; ----+------+--------------------------------------------------------------; 7 | P | 段存在位 (1=有效); 6-5 | DPL | 特权级别(0=最高, 3=最低); 4 | S | 描述符类型 (1=代码/数据, 0=系统段); 3-0 | TYPE | 段类型(取决于S=1或S=0)
; ------------------------------------------------------------------------------; [Access Byte] 位定义(保持互斥且可组合); ------------------------------------------------------------------------------
; (bit 7)ACCESS_P equ 0x80 ; P=1, 段存在于内存
; (bits 6-5) 特权级(DPL)ACCESS_DPL0 equ 0x00 ; CPL=0(内核)ACCESS_DPL1 equ 0x20 ; CPL=1ACCESS_DPL2 equ 0x40 ; CPL=2ACCESS_DPL3 equ 0x60 ; CPL=3(用户)
; (bit 4)ACCESS_S_CODEDATA equ 0x10 ; S=1, 普通代码/数据段ACCESS_S_SYSTEM equ 0x00 ; S=0, 系统段 (TSS, LDT等)
; (bits 3-0) TYPE 字段; --- 数据段类型(S=1 且 TYPE[3]=0)---ACCESS_DATA_RD equ 0x00 ; 只读ACCESS_DATA_RDA equ 0x01 ; 只读, 已访问ACCESS_DATA_RW equ 0x02 ; 可读写ACCESS_DATA_RWA equ 0x03 ; 可读写, 已访问ACCESS_DATA_EXP equ 0x04 ; 向下扩展,只读ACCESS_DATA_EXPW equ 0x06 ; 向下扩展,可写
; --- 代码段类型(S=1 且 TYPE[3]=1)---ACCESS_CODE_X equ 0x08 ; 只执行ACCESS_CODE_XR equ 0x0A ; 可执行+可读(最常用)ACCESS_CODE_XC equ 0x0C ; 一致代码段(低特权可执行)ACCESS_CODE_XRC equ 0x0E ; 可执行+可读+一致
; ------------------------------------------------------------------------------; 常用组合(可直接使用); ------------------------------------------------------------------------------ACCESS_CODE32_KERNEL equ ACCESS_P | ACCESS_DPL0 | ACCESS_S_CODEDATA | ACCESS_CODE_XRACCESS_DATA32_KERNEL equ ACCESS_P | ACCESS_DPL0 | ACCESS_S_CODEDATA | ACCESS_DATA_RWACCESS_CODE32_USER equ ACCESS_P | ACCESS_DPL3 | ACCESS_S_CODEDATA | ACCESS_CODE_XRACCESS_DATA32_USER equ ACCESS_P | ACCESS_DPL3 | ACCESS_S_CODEDATA | ACCESS_DATA_RW
; ================================================================================; [2] Flags 位布局(高 4 位 bits 20..23); ================================================================================; bit | 名称 | 说明; ----+------+--------------------------------------------------------------; 7-4| Flags高4位 | G / D / L / AVL; 3-0| Limit高4位 | 段界限高4位(由 GDT_ENTRY 宏补充);; Flags字段高4位(即 descriptor 第6字节的高半字节):; 7 6 5 4; G D L AVL;; ------------------------------------------------------------------------------; [Flags] 位定义(高4位部分); ------------------------------------------------------------------------------
FLAGS_G_4K equ 0x8 ; G=1, 粒度=4KBFLAGS_G_BYTE equ 0x0 ; G=0, 粒度=1B
FLAGS_D_32 equ 0x4 ; D=1, 32位默认操作数大小FLAGS_D_16 equ 0x0 ; D=0, 16位默认操作数大小
FLAGS_L_64 equ 0x2 ; L=1, 64位代码段(仅 IA-32e 模式)FLAGS_L_32 equ 0x0 ; L=0, 32位段
FLAGS_AVL equ 0x1 ; AVL=1, 软件可用位(一般置0)
; ------------------------------------------------------------------------------; 常用组合; ------------------------------------------------------------------------------FLAGS_4K_32BIT equ FLAGS_G_4K | FLAGS_D_32 | FLAGS_L_32FLAGS_4K_64BIT equ FLAGS_G_4K | FLAGS_L_64FLAGS_BYTE_16BIT equ FLAGS_G_BYTE | FLAGS_D_16
; ================================================================================; [3] 通用常量; ================================================================================DESC_BASE_ZERO equ 0x00000000DESC_LIMIT_MAX equ 0xFFFFF ; 当 G=4K 时, 表示 4GB 空间
; ================================================================================; [4] GDT Entry 构造宏; ================================================================================; GDT_ENTRY base, limit, access_byte, flags_nibble; 输出: 共6条指令,生成完整8字节描述符; ------------------------------------------------------------------------------%macro GDT_ENTRY 4 dw (%2 & 0xFFFF) ; Limit[15:0] dw (%1 & 0xFFFF) ; Base[15:0] db ((%1 >> 16) & 0xFF) ; Base[23:16] db %3 ; Access Byte db (((%2 >> 16) & 0x0F) | ((%4 & 0x0F) << 4)) ; Flags高4位 | Limit高4位 db ((%1 >> 24) & 0xFF) ; Base[31:24]%endmacro
; ================================================================================; [5] 标准 GDT 定义(最小可运行); ================================================================================%macro DEFINE_STANDARD_GDT 0gdt_start: ; Null Descriptor(必须存在) GDT_ENTRY 0, 0, 0, 0
; 内核代码段: base=0, limit=4GB, 32位, 粒度4K GDT_ENTRY DESC_BASE_ZERO, DESC_LIMIT_MAX, ACCESS_CODE32_KERNEL, FLAGS_4K_32BIT
; 内核数据段 GDT_ENTRY DESC_BASE_ZERO, DESC_LIMIT_MAX, ACCESS_DATA32_KERNEL, FLAGS_4K_32BIT
; 视频段 (0xB8000, limit约4KB) GDT_ENTRY 0x000B8000, 0x07FF, ACCESS_DATA32_KERNEL, FLAGS_G_BYTE
gdt_end:
gdt_descriptor: dw gdt_end - gdt_start - 1 dd gdt_start%endmacro
; ================================================================================; [6] 段选择子定义; ================================================================================RPL0 equ 0RPL3 equ 3TI_GDT equ 0TI_LDT equ 4
SELECTOR_NULL equ (0 << 3) + TI_GDT + RPL0SELECTOR_CODE32 equ (1 << 3) + TI_GDT + RPL0SELECTOR_DATA32 equ (2 << 3) + TI_GDT + RPL0SELECTOR_VIDEO equ (3 << 3) + TI_GDT + RPL0SELECTOR_UCODE32 equ (4 << 3) + TI_GDT + RPL3SELECTOR_UDATA32 equ (5 << 3) + TI_GDT + RPL3
; =================================================================================; 栈设置宏; =================================================================================; 将栈指针设置到MBR_BASE_ADDR; 注意: 只有512字节的栈空间,使用时需谨慎;; 使用示例:; SET_STACK_AT_MBR%macro SET_STACK_AT_MBR 0 xor ax, ax mov ss, ax mov sp, MBR_BASE_ADDR%endmacro
%macro SET_STACK_AT_PM 0 mov esp, 0x00090000 mov ebp, esp%endmacro; =================================================================================; 结束; =================================================================================进入保护模式后,实模式下方便的 BIOS 中断便不可再用,所以需要在保护模式下实现一些实用功能:
- 文本打印
- 光标控制
- 磁盘读写
- …
将它们实现到 pm_utils.S 中,同样通过 include 指令加载:
; -----------------------------------------------------------------------; pm_utils.S — Protected mode utility library (fixed & optimized version); NASM syntax, 32-bit protected-mode helpers; -----------------------------------------------------------------------
%define SCREEN_WIDTH 80%define SCREEN_HEIGHT 25
%define ATA_PRIMARY_BASE 0x1F0%define ATA_PRIMARY_CONTROL 0x3F6
%define WHITE_ON_BLACK 0x0F
section .datacursor_row: dd 0cursor_col: dd 0
section .text
; ======================================================================; 光标管理函数 (cdecl); ======================================================================
set_cursor_pos: ; [esp+4] = row, [esp+8] = col mov eax, [esp + 4] mov [cursor_row], eax mov eax, [esp + 8] mov [cursor_col], eax ret
get_cursor_pos: mov eax, [cursor_row] mov edx, [cursor_col] ret
move_cursor_forward: ; [esp+4] = count push ebp mov ebp, esp mov eax, [ebp + 8] ; count mov ecx, [cursor_row] mov edx, [cursor_col] add edx, eax
.fixup: cmp edx, SCREEN_WIDTH jl .store sub edx, SCREEN_WIDTH inc ecx jmp .fixup.store: mov [cursor_row], ecx mov [cursor_col], edx pop ebp ret
cursor_newline: ; 行 +1,列 = 0,防止越界(不自动滚屏) mov eax, [cursor_row] inc eax cmp eax, SCREEN_HEIGHT jb .ok mov eax, SCREEN_HEIGHT - 1.ok: mov [cursor_row], eax mov dword [cursor_col], 0 ret
; ======================================================================; 硬件光标; ======================================================================
set_hardware_cursor: push ebp mov ebp, esp push eax push ebx push ecx push edx
mov eax, [ebp + 8] ; row mov ecx, SCREEN_WIDTH xor edx, edx mul ecx add eax, [ebp + 12] ; + col
mov dx, 0x3D4 mov bx, ax ; bx = cell index (word)
mov al, 0x0E out dx, al inc dx mov al, bh out dx, al
dec dx mov al, 0x0F out dx, al inc dx mov al, bl out dx, al
pop edx pop ecx pop ebx pop eax pop ebp ret
sync_hardware_cursor: ; 使用当前软光标同步硬件光标 mov eax, [cursor_row] mov edx, [cursor_col] push edx push eax call set_hardware_cursor add esp, 8 ret
; ======================================================================; 屏幕控制; ======================================================================
clear_screen: push ebx push ecx push edi push esi
mov ecx, SCREEN_WIDTH * SCREEN_HEIGHT mov bx, 0x0720 ; word: attr + ' ' xor edi, edi mov esi, ecx
.clear_loop: mov [gs:edi], bx add edi, 2 dec esi jnz .clear_loop
pop esi pop edi pop ecx pop ebx
mov dword [cursor_row], 0 mov dword [cursor_col], 0
push 0 push 0 call set_hardware_cursor add esp, 8 ret
; ======================================================================; 打印功能; ======================================================================
; get_vram_addr - 返回字节偏移到 eax(字节偏移,用于 [gs:eax] 访问)get_vram_addr: mov eax, [cursor_row] mov ecx, SCREEN_WIDTH xor edx, edx mul ecx add eax, [cursor_col] shl eax, 1 ; bytes ret
print_char: push ebp mov ebp, esp push edi push eax push ebx push ecx push edx
; 取参数(只用低字节) mov al, byte [ebp + 8] ; char mov bl, byte [ebp + 12] ; attr
; 把 char/attr 合成 word,保存到 BX,避免被后续计算覆盖 mov ah, bl ; AH = attr, AL = char mov bx, ax ; BX = AX(=char+attr)
; 计算显存偏移 (用 EAX 重新计算,不影响 BX) mov eax, [cursor_row] mov ecx, SCREEN_WIDTH xor edx, edx mul ecx add eax, [cursor_col] shl eax, 1 mov edi, eax
; 写显存 mov ax, bx mov [gs:edi], ax
; 更新光标 inc dword [cursor_col] cmp dword [cursor_col], SCREEN_WIDTH jl .done mov dword [cursor_col], 0 inc dword [cursor_row] cmp dword [cursor_row], SCREEN_HEIGHT jl .done mov dword [cursor_row], SCREEN_HEIGHT - 1
.done: pop edx pop ecx pop ebx pop eax pop edi pop ebp ret
print_string: push ebp mov ebp, esp push esi push edi push ebx
mov esi, [ebp + 8] ; string mov bl, [ebp + 12] ; attr
mov eax, [cursor_row] mov ecx, SCREEN_WIDTH xor edx, edx mul ecx ; eax = row * SCREEN_WIDTH add eax, [cursor_col] ; eax = 偏移(单位:字符)
mov edi, eax shl edi, 1 ; 每个字符 2 字节 mov ah, bl ; attr 在 ah
.print_loop: lodsb test al, al jz .done cmp al, 10 ; '\n' je .newline
mov [gs:edi], ax add edi, 2
; 更新 cursor_col inc dword [cursor_col] cmp dword [cursor_col], SCREEN_WIDTH jl .print_loop
.newline: mov dword [cursor_col], 0 inc dword [cursor_row] mov eax, [cursor_row] mov ecx, SCREEN_WIDTH xor edx, edx mul ecx shl eax, 1 mov edi, eax jmp .print_loop
.done: pop ebx pop edi pop esi pop ebp ret
println: push ebp mov ebp, esp
; 取出参数 mov eax, [ebp + 8] ; string mov edx, [ebp + 12] ; color
; 正确传递给 print_string push edx push eax call print_string add esp, 8
call cursor_newline call sync_hardware_cursor
pop ebp ret
; ======================================================================; 打印十六进制/十六进制32; ======================================================================
; print_hex8: [esp+4]=value(byte), [esp+8]=attr(byte)print_hex8: push ebp mov ebp, esp push eax push ebx push ecx push edx
mov al, byte [ebp + 8] ; value (low 8 bits) mov bl, byte [ebp + 12] ; attr mov dl, al ; backup original byte into DL
; ---- high nibble ---- mov cl, dl shr cl, 4 and cl, 0x0F cmp cl, 9 jbe .h_digit add cl, 'A' - 10 jmp .h_done.h_digit: add cl, '0'.h_done: movzx eax, cl ; zero-extend ASCII char into EAX movzx ecx, bl ; zero-extend attr into ECX push ecx push eax call print_char add esp, 8
; ---- low nibble ---- mov cl, dl and cl, 0x0F cmp cl, 9 jbe .l_digit add cl, 'A' - 10 jmp .l_done.l_digit: add cl, '0'.l_done: movzx eax, cl movzx ecx, bl push ecx push eax call print_char add esp, 8
pop edx pop ecx pop ebx pop eax pop ebp ret
; print_hex32: [esp+4]=value(dword), [esp+8]=attr(byte)print_hex32: push ebp mov ebp, esp push eax push ebx push ecx push edx push esi push edi
mov eax, [ebp + 8] ; value mov bl, byte [ebp + 12] ; attr mov ecx, 8
.hloop: mov edx, eax shr edx, 28 and dl, 0x0F
cmp dl, 9 jbe .digit add dl, 'A' - 10 jmp .out.digit: add dl, '0'.out: movzx esi, dl movzx edi, bl push edi push esi call print_char add esp, 8
shl eax, 4 loop .hloop
pop edi pop esi pop edx pop ecx pop ebx pop eax pop ebp ret
; ======================================================================; 内存操作(标准 prologue/epilogue); ======================================================================
memcpy: ; [esp+4] = dest, [esp+8] = src, [esp+12] = n push ebp mov ebp, esp push esi push edi push ecx
mov edi, [ebp + 8] mov esi, [ebp + 12] mov ecx, [ebp + 16] cld rep movsb
pop ecx pop edi pop esi pop ebp ret
memset: ; [esp+4]=dest, [esp+8]=val (byte), [esp+12]=n push ebp mov ebp, esp push edi push ecx
mov edi, [ebp + 8] ; dest mov al, [ebp + 12] ; val (byte) mov ecx, [ebp + 16] ; count cld rep stosb
pop ecx pop edi pop ebp ret
; ======================================================================; ATA 磁盘读取 (PIO 模式); 约定: eax = start LBA (28-bit), ebx = buf, ecx = count; 返回: eax = 实际读取扇区数; ======================================================================
read_disk: push ebp mov ebp, esp push eax push ebx push ecx push edx push esi push edi
mov esi, eax ; LBA mov edi, ecx ; sector count ; ebx = buffer
mov al, cl mov dx, ATA_PRIMARY_BASE + 2 out dx, al
mov eax, esi mov dx, ATA_PRIMARY_BASE + 3 out dx, al shr eax, 8 mov dx, ATA_PRIMARY_BASE + 4 out dx, al shr eax, 8 mov dx, ATA_PRIMARY_BASE + 5 out dx, al shr eax, 8 and al, 0x0F or al, 0xE0 mov dx, ATA_PRIMARY_BASE + 6 out dx, al
mov dx, ATA_PRIMARY_BASE + 7 mov al, 0x20 out dx, al
.wait_ready: in al, dx test al, 0x08 jz .wait_ready
mov ecx, edi mov dx, ATA_PRIMARY_BASE
.read_loop: push ecx mov ecx, 256.read_sector: in ax, dx mov [ebx], ax add ebx, 2 loop .read_sector pop ecx loop .read_loop
mov eax, edi
pop edi pop esi pop edx pop ecx pop ebx pop eax pop ebp ret这样 loader.S 即可简化为:
[bits 16]org 0x8000
%include "../common/bootmacros.inc"
_start: jmp loader_start
; 定义 GDTDEFINE_STANDARD_GDT
loader_start: cli xor ax, ax mov ds, ax mov es, ax mov fs, ax mov gs, ax
SET_STACK_AT_MBR
call cls mov bx, MSG_REAL_MODE call print_bios
; 启用A20地址线 in al, 0x92 or al, 0000_0010b out 0x92, al
; 加载GDT并进入保护模式 lgdt [gdt_descriptor] ; 设置 PE mov eax, cr0 or eax, 1 mov cr0, eax
; 远跳刷新CS jmp SELECTOR_CODE32:init_pm
; ------------------------------; BIOS文本输出; ------------------------------print_bios: pusha.loop: mov al, [bx] test al, al jz .done mov ah, 0x0e int 0x10 inc bx jmp .loop.done: popa ret
cls: mov ax, 0x0600 mov bx, 0x0700 mov cx, 0 mov dx, 184Fh int 0x10 mov ah, 0x02 xor dx, dx xor bh, bh int 0x10 ret
MSG_REAL_MODE db "Started in 16-bit real mode (BIOS)",0MSG_PROT_MODE db "Now in 32-bit protected mode (direct video)",0
; ------------------------------; 保护模式初始化; ------------------------------[bits 32]
%include "../common/lib/pm_utils.S"
init_pm: mov ax, SELECTOR_DATA32 mov ds, ax mov es, ax mov fs, ax mov ss, ax mov ax, SELECTOR_VIDEO mov gs, ax
; 切换栈 SET_STACK_AT_PM
call clear_screen
; 打印保护模式信息 push 0x0F push MSG_PROT_MODE call println add esp, 8
cli.hang: hlt jmp .hang开启虚拟内存分页
虚拟内存技术简介与使用
虚拟内存技术把线性地址的内存空间按固定大小(4 KiB/2 MiB/1 GiB)切成页,然后把物理内存也切成同样大小的页框。用多级页表 + MMU 完成虚拟页号 → 物理页框号的映射,缺页时由 OS 动态换入/换出。
在 32 位环境下,32 位的地址可以表达 4GB 的虚拟内存空间。开启 4KiB 分页后,CPU 会把虚拟地址分为三部分:
# 前 10 位:页目录索引(Page Directory Index, PDI),可定位2^10=1024项页目录
# 中间 10 位:页表索引(Page Table Index, PTI),可定位2^10=1024项页表
# 最后 12 位:页内偏移(Page Offset)
+----------------+--------------+---------------+| Page Directory | Page Table | Offset || 10 bits | 10 bits | 12 bits |+----------------+--------------+---------------+页目录和页表都是 4 字节的结构,1024 个页目录项中都有 1024 项页表,所以一共仅需 1024 × 1024 × 4 = 4MB 的物理内存空间即可存储 4GB 虚拟地址 → 物理地址的映射关系:

页目录结构:
| 位数 | 名称 | 作用 |
|---|---|---|
| 0 | P(Present) | 该页表是否存在 |
| 1 | R/W(Read/Write) | 可读写权限 |
| 2 | U/S(User/Supervisor) | 用户态/内核态访问权限 |
| 3 | PWT(Page Write Through) | 控制缓存策略 |
| 4 | PCD(Page Cache Disable) | 控制是否缓存该页表 |
| 5 | A(Accessed) | 是否被访问过 |
| 6 | Reserved | 通常为 0 |
| 7 | PS(Page Size) | 为 0 表示这是页表项(而不是大页) |
| 8–11 | 保留 | 系统保留 |
| 12–31 | 页表物理地址高 20 位 | 指向页表的物理地址(对齐 4KB) |
页表结构:
| 位数 | 名称 | 作用 |
|---|---|---|
| 0 | P(Present) | 该页是否映射 |
| 1 | R/W(Read/Write) | 是否可写 |
| 2 | U/S(User/Supervisor) | 用户态/内核态权限 |
| 3–4 | 缓存控制 | 与 PDE 相似 |
| 5 | A(Accessed) | 是否访问过 |
| 6 | D(Dirty) | 是否被写过(仅对可写页有效) |
| 7–11 | 保留/扩展位 | 例如全局页(global page)等扩展位 |
| 12–31 | 页框物理地址 | 指向实际物理页的高 20 位地址 |
当开启虚拟内存分页后,CPU 会从虚拟地址进行取指/取数,经历以下流程:
- 先从 MMU 中查询 TLB,如果命中则直接得到物理地址
- 若 TLB Miss,则会查询 2 级页表,首先从 CR3 中获取页目录的物理基址
- 然后通过页目录的物理基址 + VA 中高 10 位的页目录索引 × 4 得到页目录项索引的物理地址,从该地址中的页目录结构的高 20 位中得到页表项物理基址(PDE = CR3 + 页目录索引 × 4)
- 由页表项物理基址 + VA 中 10 位的页表索引 × 4 得到页表项的物理地址,从该地址中的页表结构的高 20 位中能得到物理页框基址(PTE = 页表基址 + 页表索引 × 4)
- 最后由物理页框基址 + VA 中低 12 位的页内偏移即可得到物理地址,同时会将这个映射关系(虚拟页号 → 物理页框)写入 TLB 缓存
要开启分页,首先需要在物理地址空间中划分出 4MB 空间用来存储映射关系,然后拿出一个 4KB 物理帧(frame)作为页目录。再为内核创建所需的页表项,最后把划分给页目录的物理帧作为页目录基址载入 CR3,以及把 CR0.PG 置 1 即可开启分页。
页目录规划
1MB 以下的空间已经被实模式规划征用,所以用来存储页目录的物理帧可以从 0x100000 开始。
-
选择使用
0x100000后面的第一个帧存储第一个page table,它用来映射 0 ~ 1MB 低内存 -
第二个帧即
0x100000 + 4KB用来存储页目录
-
pde[0:768] 用来存储用户空间映射的页表项,pde[768:1024] 用来存储内核空间的页表项,其中 pde[0] 和 pde[768] 指向同一个页表,也就是最特殊的用来映射低 1MB 空间的那张
-
为了方便内核能操作和修改页目录以及页表项,选择将 pde[769] 指向
0x100000 + 4KB的 page directory 本身,这样页目录本身也相当于一张页表,它所管理的正好是 1024 张 page tables 本身,一共 4MB。它所在的虚拟地址空间为0xC0400000 ~ 0xC0800000,其中页目录所在的虚拟地址为0xC0701000
开启分页
page tables 都准备就绪后,就可以打开 paging 了:
enable_page: sgdt [gdt_ptr]
; move the video segment to > 0xC0000000 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xC0000000
; move gdt to > 0xC0000000 add dword [gdt_ptr + 2], 0xC0000000
; move stack to > 0xC0000000 mov eax, [esp] add esp, 0xc0000000 mov [esp], eax
; set page directory address to cr3 register mov eax, PAGE_DIR_PHYSICAL_ADDR mov cr3, eax
; enable paging on cr0 register mov eax, cr0 or eax, 0x80000000 mov cr0, eax支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






