mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1667 字
5 分钟
手写 Loader:开启保护模式与分页虚拟内存
2021-04-01

手写 Loader,开启保护模式与虚拟内存#

Loader 从 MBR 手中接过接力棒后,便可以为加载和运行现代内核做准备了,即实现以下任务和能力:

  • 初始化 GDT 并进入保护模式,突破实模式 1MB 内存空间的牢笼,这是加载现代内核的前提
  • 初始化 kernel 页目录和页表,开启分页功能(虚拟内存),解锁现代内核虚拟内存管理的能力
  • 加载并进入内核

在 Loader 中进入保护模式#

上一章实现了打印 Loader 字符串的程序,现在需要将其改造成进入保护模式再打印 Loader 的程序。在 CPU 工作模式 - 实模式/保护模式以及长模式 的保护模式一节中,我们了解到段描述符是一个 8 字节(64 位)的结构体,存储段的基地址、段限长(大小)以及访问权限等信息。进入保护模式其实就是定义系统全局的数据段与代码段,然后通过 lgdt 指令加载它,最后将 CR0 寄存器的第 0 位置 1 即可:

bits 16
org 0x8000
; this is how constants are defined
VIDEO_MEMORY equ 0xb8000
WHITE_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 36
gdt_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.pdf
gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:
; GDT descriptor
gdt_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 use
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
MSG_REAL_MODE db "Started in 16-bit real mode", 0
MSG_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 后运行,可以观察到下面的结果: pm-mode

整理代码#

无论是进入保护模式时定义的 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=1
ACCESS_DPL2 equ 0x40 ; CPL=2
ACCESS_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_XR
ACCESS_DATA32_KERNEL equ ACCESS_P | ACCESS_DPL0 | ACCESS_S_CODEDATA | ACCESS_DATA_RW
ACCESS_CODE32_USER equ ACCESS_P | ACCESS_DPL3 | ACCESS_S_CODEDATA | ACCESS_CODE_XR
ACCESS_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, 粒度=4KB
FLAGS_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_32
FLAGS_4K_64BIT equ FLAGS_G_4K | FLAGS_L_64
FLAGS_BYTE_16BIT equ FLAGS_G_BYTE | FLAGS_D_16
; ================================================================================
; [3] 通用常量
; ================================================================================
DESC_BASE_ZERO equ 0x00000000
DESC_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 0
gdt_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 0
RPL3 equ 3
TI_GDT equ 0
TI_LDT equ 4
SELECTOR_NULL equ (0 << 3) + TI_GDT + RPL0
SELECTOR_CODE32 equ (1 << 3) + TI_GDT + RPL0
SELECTOR_DATA32 equ (2 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (3 << 3) + TI_GDT + RPL0
SELECTOR_UCODE32 equ (4 << 3) + TI_GDT + RPL3
SELECTOR_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 .data
cursor_row: dd 0
cursor_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
; 定义 GDT
DEFINE_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)",0
MSG_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 虚拟地址 → 物理地址的映射关系: paging

页目录结构:

位数名称作用
0P(Present)该页表是否存在
1R/W(Read/Write)可读写权限
2U/S(User/Supervisor)用户态/内核态访问权限
3PWT(Page Write Through)控制缓存策略
4PCD(Page Cache Disable)控制是否缓存该页表
5A(Accessed)是否被访问过
6Reserved通常为 0
7PS(Page Size)为 0 表示这是页表项(而不是大页)
8–11保留系统保留
12–31页表物理地址高 20 位指向页表的物理地址(对齐 4KB)

页表结构:

位数名称作用
0P(Present)该页是否映射
1R/W(Read/Write)是否可写
2U/S(User/Supervisor)用户态/内核态权限
3–4缓存控制与 PDE 相似
5A(Accessed)是否访问过
6D(Dirty)是否被写过(仅对可写页有效)
7–11保留/扩展位例如全局页(global page)等扩展位
12–31页框物理地址指向实际物理页的高 20 位地址

当开启虚拟内存分页后,CPU 会从虚拟地址进行取指/取数,经历以下流程:

  1. 先从 MMU 中查询 TLB,如果命中则直接得到物理地址
  2. 若 TLB Miss,则会查询 2 级页表,首先从 CR3 中获取页目录的物理基址
  3. 然后通过页目录的物理基址 + VA 中高 10 位的页目录索引 × 4 得到页目录项索引的物理地址,从该地址中的页目录结构的高 20 位中得到页表项物理基址(PDE = CR3 + 页目录索引 × 4
  4. 由页表项物理基址 + VA 中 10 位的页表索引 × 4 得到页表项的物理地址,从该地址中的页表结构的高 20 位中能得到物理页框基址(PTE = 页表基址 + 页表索引 × 4
  5. 最后由物理页框基址 + VA 中低 12 位的页内偏移即可得到物理地址,同时会将这个映射关系(虚拟页号 → 物理页框)写入 TLB 缓存

要开启分页,首先需要在物理地址空间中划分出 4MB 空间用来存储映射关系,然后拿出一个 4KB 物理帧(frame)作为页目录。再为内核创建所需的页表项,最后把划分给页目录的物理帧作为页目录基址载入 CR3,以及把 CR0.PG 置 1 即可开启分页。

页目录规划#

1MB 以下的空间已经被实模式规划征用,所以用来存储页目录的物理帧可以从 0x100000 开始。

  • 选择使用 0x100000 后面的第一个帧存储第一个 page table,它用来映射 0 ~ 1MB 低内存

  • 第二个帧即 0x100000 + 4KB 用来存储页目录 page-lanning

  • 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

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

手写 Loader:开启保护模式与分页虚拟内存
https://blog.souloss.com/posts/os/protected-mode-and-paging/
作者
Souloss
发布于
2021-04-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时