手写 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
; =================================================================================; ATA 相关; =================================================================================
ATA_PRIMARY_BASE equ 0x1F0
; =================================================================================; 分页相关常量(节选,后文 enable_page 中使用); =================================================================================PAGE_SIZE equ 0x1000 ; 标准页大小 (4KB)
PAGE_DIR_PHYISCAL_ADDR equ 0x100000 + PAGE_SIZE ; 页目录物理地址 0x101000PAGE_DIR_PHYSICAL_ADDR equ PAGE_DIR_PHYISCAL_ADDR ; 拼写纠正别名PAGE_DIR_VIRTUAL_ADDR equ 0xC0701000 ; 页目录虚拟地址PAGE_TABLES_VIRTUAL_ADDR_START equ 0xC0400000 ; 页表区虚拟起点
; 页表条目标志位(与上文 PDE/PTE 位布局对应)PTE_PRESENT equ 1 << 0 ; 位0 存在位PTE_WRITE equ 1 << 1 ; 位1 写允许PTE_USER equ 1 << 2 ; 位2 用户访问PTE_PWT equ 1 << 3 ; 位3 页级写通过PTE_PCD equ 1 << 4 ; 位4 页级缓存禁用PTE_ACCESSED equ 1 << 5 ; 位5 访问位PTE_DIRTY equ 1 << 6 ; 位6 脏位PTE_PAT equ 1 << 7 ; 位7 页属性表PTE_GLOBAL equ 1 << 8 ; 位8 全局页
; =================================================================================; CR0 位掩码(节选); =================================================================================CR0_PE equ 1 << 0 ; Protection EnableCR0_PG equ 1 << 31 ; PaGing
; =================================================================================; 内核空间常量(节选); =================================================================================KERNEL_SPACE_START equ 0xC0000000
; =================================================================================; 结束; =================================================================================进入保护模式后,实模式下方便的 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 模式); cdecl: [ebp+8]=LBA, [ebp+12]=count, [ebp+16]=buf; 每次读 1 个扇区,循环 count 次; ======================================================================read_disk: push ebp mov ebp, esp pushad
mov esi, [ebp + 8] ; LBA 起始扇区号 mov edi, [ebp + 12] ; 扇区数量 mov ebx, [ebp + 16] ; 目标缓冲区地址
.next_sector: ; -------------------------------------- ; 写入扇区数量(每次读1个) ; -------------------------------------- mov dx, ATA_PRIMARY_BASE + 2 mov al, 1 out dx, al
; -------------------------------------- ; 写入 LBA 地址(低 24 位) ; -------------------------------------- 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 ; 主盘 + LBA 模式 mov dx, ATA_PRIMARY_BASE + 6 out dx, al
; -------------------------------------- ; 发送读命令 READ SECTOR (0x20) ; -------------------------------------- mov dx, ATA_PRIMARY_BASE + 7 mov al, 0x20 out dx, al
.wait_ready: in al, dx ; 状态寄存器 test al, 0x80 ; BSY=1 ? 忙碌中? jnz .wait_ready test al, 0x08 ; DRQ=1 ? 数据准备好? jz .wait_ready
; ----------------------- 读一个扇区 ----------------------- mov dx, ATA_PRIMARY_BASE + 0 ; 数据端口 mov ecx, 256 ; 每扇区 256 words = 512B.read_word: in ax, dx mov [ebx], ax add ebx, 2 loop .read_word
; ----------------------- 准备下一个扇区 ----------------------- inc esi ; LBA++ dec edi ; 扇区数-- jnz .next_sector
.done: popad 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),定位页内的具体字节。
| 位范围 | 字段 | 宽度 |
|---|---|---|
| 31 - 22 | 页目录索引(PDI) | 10 bit |
| 21 - 12 | 页表索引(PTI) | 10 bit |
| 11 - 0 | 页内偏移(Offset) | 12 bit |
页目录和页表都是 4 字节的结构,1024 个页目录项中都有 1024 项页表,所以一共仅需 1024 × 1024 × 4 = 4MB 的物理内存空间即可存储 4GB 虚拟地址 → 物理地址的映射关系:

页目录项(PDE,4KB 页):
| 位数 | 名称 | 作用 |
|---|---|---|
| 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 | 保留 | 4KB PDE 保留位,应为 0 |
| 7 | PS(Page Size) | 0 表示指向页表,1 表示 4MB 大页 |
| 8 | G(Global) | 全局页(开启 PGE 时 TLB 不失效) |
| 9–11 | OS 可用 | 供操作系统自由使用 |
| 12–31 | 页表基址高 20 位 | 指向页表的物理地址(4KB 对齐) |
页表项(PTE):
| 位数 | 名称 | 作用 |
|---|---|---|
| 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 | D(Dirty) | 是否被写过(仅对可写页有效) |
| 7 | PAT(Page Attribute Table) | 页属性表索引(无 PAT 时作保留位) |
| 8 | G(Global) | 全局页(开启 PGE 时 TLB 不失效) |
| 9–11 | 可用位 | 供操作系统自由使用 |
| 12–31 | 页框基址高 20 位 | 指向实际物理页地址(4KB 对齐) |
PDE 与 PTE 的低 8 位布局很相似,区别主要在第 6、7 位:PDE 第 6 位保留、第 7 位是 PS(决定指向页表还是大页),PTE 第 6 位是 D(脏位)、第 7 位是 PAT。bootmacros.inc 中的 PTE_GLOBAL(1<<8)等标志位就是按上表第 8 位定义的。
当开启虚拟内存分页后,CPU 会从虚拟地址进行取指/取数,经历以下流程:
要开启分页,首先需要在物理地址空间中划分出 4MB 空间用来存储映射关系,然后拿出一个 4KB 物理帧(frame)作为页目录。再为内核创建所需的页表项,最后把划分给页目录的物理帧作为页目录基址载入 CR3,以及把 CR0.PG 置 1 即可开启分页。
页目录规划
1MB 以下的空间已经被实模式规划征用,所以用来存储页目录的物理帧可以从 0x100000 开始。
-
选择使用
0x100000后面的第一个帧存储第一个page table,它用来映射 0 ~ 1MB 低内存 -
第二个帧即
0x100000 + 4KB用来存储页目录
-
PDE 索引按虚拟地址高 10 位划分:PDE[0] ~ PDE[767] 对应 0x00000000 ~ 0xBFFFFFFF 的用户空间,PDE[768] ~ PDE[1023] 对应 0xC0000000 ~ 0xFFFFFFFF 的内核空间。其中 PDE[0] 与 PDE[768] 指向同一张低 1MB 页表,使得内核空间起始处仍然能看到低 1MB 物理内存(包括显存、bootloader 数据等)
-
为了方便内核能操作和修改页目录以及页表项,选择将 PDE[769] 自映射到页目录本身所在的
0x101000帧,这样页目录自己也被当成一张页表来用,它管理的恰好是全部 1024 张页表本身,共 4MB。这部分虚拟地址空间为0xC0400000 ~ 0xC0800000,其中页目录所在的虚拟地址为0xC0701000 -
PDE[770] ~ PDE[1023] 共 254 项依次映射内核其余的页表,覆盖
KERNEL_SPACE_START + 0x0800000起的内核代码与数据空间
开启分页
page tables 都准备就绪后,就可以打开 paging 了:
; 启用分页,并调整段寄存器与 GDT 到高地址空间enable_page: sgdt [gdt_descriptor]
; 调整视频段描述符的基址 ; +2 表示描述符表中的 base 偏移 mov ebx, [gdt_descriptor + 2] ; 0x18 表示第三个GDT, 也就是视频段 ; +4 是为了修改到基址的高位部分 or dword [ebx + 0x18 + 4], KERNEL_SPACE_START
; 移动 GDT 到大于 KERNEL_SPACE_START 的位置 ; +2 表示描述符表中的 base 偏移 add dword [gdt_descriptor + 2], KERNEL_SPACE_START
; 将栈移动到内核空间 KERNEL_SPACE_START mov eax, [esp] add esp, KERNEL_SPACE_START mov [esp], eax
; 设置页目录地址到 CR3 寄存器 mov eax, PAGE_DIR_PHYISCAL_ADDR mov cr3, eax
; 启动分页 mov eax, cr0 or eax, CR0_PG mov cr0, eax
; 在分页模式下重新加载 GDT lgdt [gdt_descriptor]
; 在分页模式下重新设置视频段 mov ax, SELECTOR_VIDEO mov gs, ax
ret这里有两个容易遗漏的步骤。开启分页后,GDT 中段描述符的基址仍然指向低地址空间,需要用 lgdt 重新加载已经调整到 KERNEL_SPACE_START 之上的 GDT;同时 gs 段寄存器缓存了旧的视频段基址,需要重新加载 SELECTOR_VIDEO 让其指向高地址空间中的视频段。少了这两步,分页开启后立刻取指或写显存就会访问到错误的地址。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






