x86 32 位保护模式笔记
概念
32 位保护模式
32 位保护模式(后面统一简称保护模式)是区别于 16 位 CPU 任何程序可以访问任何内存空间的模式(自保护模式出来后,16 位 CPU 运行的模式被称为实模式),在这种模式下所有对内存的访问都要通过一个段选择子的东西,而要被访问的内存也必须先在一个叫做 GDT(全局描述符表) 的东西中定义,这个段选择子实质上就是全局描述符表中某个描述符的索引,通过段选择子找到对应的描述符,然后通过描述符里的记录的相关段的信息再去访问对应的内存。32 保护模式下的段不再像实模式下的段一样需要是以 16 的整数倍为起始地址了,它可从任何地址开始,所以 32 位保护模式下的段就可以理解为描述符描描述的内存中的某一连续空间。全局描述符表又通过一个叫 GDTR(全局描述符表寄存器) 的东西里的内容来定义。
相比于实模式,32 位保护模式的数据总线、地址总线的长度都是 32 位。通用寄存器也扩展到了 32 位:EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP,而且依然可以通过 AX、AL、AH 来访问 EAH 的低 16 位、低 8 位以及第 8 到 16 位。但段寄存器依然保持 16 位,此外新增了两个段寄存器 FS、GS。
GDT
顾名思义,如中断向量表类似,GDT 是一个表,每个表项是一个描述内存中某块区域的段描述符,用 64 位来描述内存段的属性,包括:
- 基址(32 位)
- 大小(20 位)
- 类型(1 位,可用于区分系统段和用户段)
- 子类(4 位,比如用户段又分代码段、数据段)
- 描述符特权级 DPL(2 位,0,1,2,3,越小说明级别最高)
- 是否在内存(1 位)
- AVL(1 位,硬件没用上)
- L,是否是 64 位代码段(1 位,保留位)
- D/B,兼容 286 的保护模式(1 位)
- 段大小单位(1 位,分为 1B 和 4KB)
32 位基址覆盖了 4GB 的内存空间。20 位段大小以及对应段大小的单位可以为 4KB 表明一个段最大可以是 4GB。
GDTR
用于描述 GDT,GDTR 为 48 位,包含:
- 全局描述符表线性基址(32 位),全局描述符表在内存中的起始地址
- 全局描述符表的边界(16 位),也即定义全局描述符表的大小,最大为 64KB,最多可以装下 64KB/64bit=8192 个描述符
段选择子
如上所言,段选择子本质是一个索引,包含 16 位的信息,由以下三部分组成:
- 索引(13 位,因为 GDT 最多 8192 个描述符)
- TI (1 位,标识是 GDT 的索引还是 LDT 的索引)
- 请求特权级 RPL(2 位,与 DPL 对应,用于决定通过的选择子能否访问对应的段,如果 RPL 不大于对应段的 DPL 说明可以访问)
32 位保护模式下内存的访问
32 位的地址线以及段描述符中的 32 位基址表明 32 位保护模式下 CPU 无需再像实模式那样要通过 16 位段地址乘以 16 再加 16 位偏移地址才能访问完整内存空间了,只需通过一个 32 位的寄存器就可以访问内存的任意位置。但保护模式下对内存的访问依然是通过段寄存器加偏移地址的方式,实际物理地址=段选择子对应的描述符中的段基址+偏移地址。比如处理器执行 mov byte [es:1], 1 指令时访问内存中对应段空间的流程如下:
- 将待访问段对应的段选择子赋值到 es 中
- 根据段选择子以及借助 GDTR 找到对应段描述符(假设 GDT 已经建立)
- 拿到段描述符中的段基址然后与 1 相加,得到对应的内存地址
- 将 1 存储到对应的内存中
GDT 的建立
保护模式下的内存访问都必须通过段选择子,而通过段选择子找到对应的段描述符又必须访问 GDT,也就是说最初的 GDT 肯定不是在保护模式下建立的,因为在那个模式下建立 GDT 本身就是访问内存的行为,即意味着建立 GDT 依赖一个已经存在的 GDT,所以只能是在实模式下先建立一个 GDT,才能保证进入保护模式后处理器得已访问内存。
进入保护模式
x86 中保护模式是通过将一个叫做 cr0 的寄存器的最低位置为 1 打开的。但肯定不能是简简单单的做这一步操作就进入保护模式,如前所言,进入保护模式前起码要建立一个 GDT,否则保护模式内存访问都会出错。实际上一个简单的进入保护模式的流程包含:
- 创建 GDT
- GDTR 赋值
- 打开 A20 gate
- 打开 cr0 寄存器中保护模式对应的开关
- 清空实模式下的流水线
一个简单的进入保护模式的 nasm 汇编代码如下:
boot.inc 文件,定义一常量
START_ADDR equ 0x7c00
STRING_END_FLAG equ 0x0a
MBR_CODE_SIZE equ 446
; inital related values
INITIAL_BASE_ADDR equ 0x7e00
LOADER_START_SECTOR equ 0x2
; GTD related values
GTD_BASE_SEG_ADDR equ 0x9000
bootloader.asm 文件,编译后写在 MBR 中
%include "boot.inc"
section Initial vstart=START_ADDR
initialize_regs:
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, START_ADDR
print_info1:
mov si, INFO1
call printString
call loadInitial
print_info2:
mov si, INFO2
call printString
jmp INITIAL_BASE_ADDR:0
jmp end
; 偏移地址存在 si 里,字符串以 0x0a 结尾
printString:
push ax
push si
.printChar:
mov al, [si]
mov ah, 0x0e
int 0x10
cmp byte [si], STRING_END_FLAG
jz .return
inc si
jnz .printChar
.return:
pop si
pop ax
ret
loadInitial:
push ax
push bx
push cx
push dx
push ds
push si
mov dx, 0x1f2
mov al, 1
out dx, al
inc dx
mov al, LOADER_START_SECTOR ; initail 写在磁盘 2 号扇区
out dx, al
inc dx
mov al, 0
out dx, al
inc dx
mov al, 0
out dx, al
inc dx
mov al, 0xe0
out dx, al
inc dx
mov al, 0x20
out dx, al
.wait:
in al, dx
and al, 0x88
cmp al, 0x08
jnz .wait
mov dx, 0x1f0
mov cx, 256 ;复制一个扇区
mov si, 0
.load:
in ax, dx
mov bx, ax
mov ax, INITIAL_BASE_ADDR ; 加载到段地址 7e00h 处
mov ds, ax
mov [si], bx
add si, 2
loop .load
pop si
pop ds
pop dx
pop cx
pop bx
pop ax
ret
error_end:
mov si, ERROR_INFO
call printString
end:
jmp end
INFO1 db 'Start booting...', 0x0d, 0x0a
INFO2 db 'Load initial...', 0x0d, 0x0a
ERROR_INFO db 'Error 0, oops!', 0x0d, 0x0a
times 510-($-$$) db 0
db 0x55, 0xaa
init.asm 文件,编译后写在 2 扇区,被 Bootloader 加载到内存中执行,进入保护模式
%include "boot.inc"
section initial vstart=0
mov ax, INITIAL_BASE_ADDR
mov ds, ax
jmp enable_protection_mode
enable_protection_mode:
mov ax, GTD_BASE_SEG_ADDR ; GDT 从地址 0x9000:0 开始存放
mov ds, ax
;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [0x00], 0x00
mov dword [0x04], 0x00
;创建#1描述符,保护模式下的代码段描述符,代码段基地址为当前物理地址 7e000h+
mov dword [0x08], 0xe00001ff
mov dword [0x0c], 0x00409807
;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
mov dword [0x10], 0x8000ffff
mov dword [0x14], 0x0040920b
;初始化描述符表寄存器GDTR
mov ax, INITIAL_BASE_ADDR
mov ds, ax
mov word [gdt_size], 31 ;描述符表的界限(总字节数减一)
lgdt [gdt_size]
;先取出 0x92 端口的数据
in al, 0x92 ;南桥芯片内的端口
;与 0000_0010B 进行或操作用于打开 A20 Gate,关闭地址回绕
or al, 0000_0010B
;再将新的数据写回 0x92 端口
out 0x92, al ;打开 A20 Gate
cli ;保护模式下中断机制尚未建立,应
;禁止中断
mov eax, cr0 ;取出 cr0 寄存器
or eax, 1 ;设置 PE 位
mov cr0, eax ;写回 cr0 寄存器
;以下进入保护模式
;此时已经在保护模式中,所以下面的代码中的 0x008 已经不像在
;实模式下那样是段偏移量了,而是段选择子
jmp dword 0x0008:flush ;16位的描述符选择子:32位偏移
;清流水线并串行化处理器
[bits 32]
flush:
mov cx, 00000000000_10_000B ;加载数据段选择子(0x10)
mov ds, cx
;以下在屏幕上显示"Protect mode OK."
mov byte [24*160+0x00],'P'
mov byte [24*160+0x02],'r'
mov byte [24*160+0x04],'o'
mov byte [24*160+0x06],'t'
mov byte [24*160+0x08],'e'
mov byte [24*160+0x0a],'c'
mov byte [24*160+0x0c],'t'
mov byte [24*160+0x0e],' '
mov byte [24*160+0x10],'m'
mov byte [24*160+0x12],'o'
mov byte [24*160+0x14],'d'
mov byte [24*160+0x16],'e'
mov byte [24*160+0x18],' '
mov byte [24*160+0x1a],'O'
mov byte [24*160+0x1c],'K'
end: jmp end
gdt_size dw 0
gdt_base dd GTD_BASE_SEG_ADDR << 4 ; GDT 物理地址
其他
nasm 中的 section.xx.start 和 标号地址的意义
-
section.xx.start:表示 asm 源文件被编译成二进制文件后 xx 段的第一个数据(没有也无妨)在文件中的字节偏移数,比如一个 asm 文件包含两个段,第一个 data 段编译成二进制数据后占据 32 字节大小,那么第二个段 code 对应的 section.code.start 就为 32,不管段声明中的 vstart 为何值。但是这个值会受段声明中的 align 的值的影响。
-
标号值:表示 asm 源文件被编译成二进制文件后标号之后的第一个数据(没有也无妨)在文件中的字节偏移数 - section.所在段.start + vstart(未指定时默认为 section.所在段.start)