自制操作系统学习4 进入保护模式

一、概念

1. 为什么要进保护模式

8086最大寻址范围是1M,而超过64K的内存区域访问要靠切换段基址。
当Intel的CPU发展到32位后,寻址空间达到了4G。32位下改变了寻址方式,使用名为GDT的表来管理内存,其实就是查表法,在GDT里记录每个内存段的段基址、段界限、段属性等信息。GDT里每个表项称为描述符Descriptor。
进入32位后,原先的AX,BX,CX,DX,SI,DI,SP,BP从16位扩展(Extend)到了32位,并改名EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP。
CS,DS,ES,SS这几个16位段寄存器保留,再增加FS,GS两个段寄存器。

CPU在保护模式下使用段描述符采用段描述符缓冲寄存器。在获得一个段描述符之后,后面访问相同段,就会直接访问该寄存器。

2. x86基本运行模式

1. 实地址模式

英特尔的x86系列处理器为兼容早期的8086处理器,在上电后处于这个模式。8086有20位地址线,1M的线性地址空间。

2. 保护模式

一个受保护并支持多任务的环境。大部分OS运行在这个模式下。80386有32位地址线,4G线性地址空间。 80386把4G只分为一个段,段基址0x00000000,段长0xFFFFFFFF(4G)
自制操作系统学习4 进入32位保护模式_描述符

3. 64位模式

由AMD根据速龙64处理器提出的模式,这种模式下允许处理器进入64位的内存空间,并操作64位寄存器。
多数的16位和32位处理器并不能工作在长模式下。 x86-64的实模式准确的说是像16位处理器,而x86-64的保护模式像是32位处理器。 要让芯片有64位的处理能力,就要切换到64位模式下。

4. 正在进入保护模式

5. 正在进入64位模式

3. 段描述符

在保护模式下,寻址内存的段值仍是16位的CS,DS等寄存器,但它仅是一个索引,指向了GDT表的一个描述符。段描述符是8字节(64bit),用来描述一个内存段。
自制操作系统学习4 进入32位保护模式_寄存器_02

说明:

  • G:Granularity,颗粒度标志,1表示段界限单位=4K,0表示段界限单位=1Byte
  • D/B:指明这个段中操作的长度。如果这位被设置,那么这个段是32位段,否则是16位段。
  • L:64位标志,只在IA-32e模式中使用。
  • AVL:Available,可用位,可以为操作系统软件使用。
  • P:Present,存在位,如果为0表示该段不在内存中。
  • S:为1是代码数据段,否则是系统段。
  • DPL:Descriptor Privilege Level,特权级标志。
  • TYPE:最复杂的东西,涉及到段的类型,不过可以读protect.inc里的注释来了解。

这里段基址需要32位地址表示、段界限用20位表示。段界限的单位可以是字节,也可以是4K。一个段最大值可能是自制操作系统学习4 进入32位保护模式_保护模式_03(1M)或自制操作系统学习4 进入32位保护模式_寄存器_04(4G)

自制操作系统学习4 进入32位保护模式_寄存器_05

总结:

  • 描述这些信息的内容放在内存中,称为全局描述表(Global Descriptor Table,GDT)
  • GDT的位置可以由程序员指定,放在1M以下。7c00是512字节引导区,所以可以把GDT放在0x7e00后,GDT的界限最大可以到0x17DFF (64K)。 7c00以下放BIOS数据区、中断向量表等。
  • Intel使用寄存器GDTR来保存GDT信息
  • GDTR是48位专用寄存器,0-15位是GDT边界位置,16-47位放的GDT基地址

自制操作系统学习4 进入32位保护模式_保护模式_06

二、进入保护模式的步骤:

386以后的处理器,有CR0-CR4 寄存器,用于控制模式,分页等高级功能。其中CR0寄存器的PE位表示运行状态,0是实模式、1是保护模式。
自制操作系统学习4 进入32位保护模式_描述符_07

  1. 准备GDT
  2. 创建一个6个字节的伪描述符指向GDT
  3. 用 lgdt 加载 gdtr
  4. 打开 A20
  5. 跳转,进入保护模式

其中CR0中有一位是PE位,保护模式使能位。

三、进入保护的汇编代码

​cpu32.s​

[SECTION .code]
global _start
_start:
header_start equ $$
jmp 0:kernel_start

[SECTION .gdt]
; 准备GDT 表定义
gdt_start:

gdt_null:
dd 0x0
dd 0x0

; 代码段定义
gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0

; 数据段定义
gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0

gdt_end:

; 描述符
gdt_descriptor:
dw gdt_end - gdt_start
dd gdt_start

; 代码段基址
CODE_SEG equ gdt_code - gdt_start
; 数据段基址
DATA_SEG equ gdt_data - gdt_start

; 通过中段打印字符串子程序
print:
pusha
mov ah, 14
mov bh, 0
.loop:
lodsb
cmp al, 0
je .done
int 0x10
jmp .loop
.done:
popa
ret

; 定义要打印的两个字符串
mode_16 db 'mode_16', 0
mode_32 db 'mode_32', 0

[SECTION .s16]
[bits 16]
; 程序入口
kernel_start:
; 堆栈初始化
mov ax, 0
mov ss, ax
mov sp, 0xFFFC

; 段寄存器初始化
mov ax, 0
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; 打印现在处于16位模式下
mov si, mode_16
call print

cli
; 用lgdt加载GDTR
lgdt[gdt_descriptor]
; 打开A20
mov eax, cr0
or eax, 0x1
mov cr0, eax
; 跳到保护模式
jmp CODE_SEG:b32

[bits 32]

VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f

; 32位模式下 向显存写数据
print32:
pusha
mov edx, VIDEO_MEMORY
.loop:
mov al, [ebx]
mov ah, WHITE_ON_BLACK
cmp al, 0
je .done
mov [edx], ax
add ebx, 1
add edx, 2
jmp .loop
.done:
popa
ret

; 进入32位模式
b32:
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax

mov ebp, 0x2000
mov esp, ebp

mov ebx, mode_32
call print32

jmp $

; 填充引导区
times 510-($-$$)-0x45 db 0 ; 填充剩下的空间,不知道为什么不能直接引用_start的地址,这里0x45是我临时凑出来的
dw 0xAA55

运行效果:
自制操作系统学习4 进入32位保护模式_寄存器_08

四、定义GDT数据结构

pm.inc

; 描述符图示

; 图示一
;
; ------ ┏━━┳━━┓高地址
; ┃ 7 ┃ 段 ┃
; ┣━━┫ ┃
; 基
; 字节 7 ┆ ┆ ┆
; 址
; ┣━━┫ ② ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 7 ┃ G ┃
; ┣━━╉──┨
; ┃ 6 ┃ D ┃
; ┣━━╉──┨
; ┃ 5 ┃ 0 ┃
; ┣━━╉──┨
; ┃ 4 ┃ AVL┃
; 字节 6 ┣━━╉──┨
; ┃ 3 ┃ ┃
; ┣━━┫ 段 ┃
; ┃ 2 ┃ 界 ┃
; ┣━━┫ 限 ┃
; ┃ 1 ┃ ┃
; ┣━━┫ ② ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 7 ┃ P ┃
; ┣━━╉──┨
; ┃ 6 ┃ ┃
; ┣━━┫ DPL┃
; ┃ 5 ┃ ┃
; ┣━━╉──┨
; ┃ 4 ┃ S ┃
; 字节 5 ┣━━╉──┨
; ┃ 3 ┃ ┃
; ┣━━┫ T ┃
; ┃ 2 ┃ Y ┃
; ┣━━┫ P ┃
; ┃ 1 ┃ E ┃
; ┣━━┫ ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 23 ┃ ┃
; ┣━━┫ ┃
; ┃ 22 ┃ ┃
; ┣━━┫ 段 ┃
;
; 字节 ┆ ┆ 基 ┆
; 2, 3, 4
; ┣━━┫ 址 ┃
; ┃ 1 ┃ ① ┃
; ┣━━┫ ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 15 ┃ ┃
; ┣━━┫ ┃
; ┃ 14 ┃ ┃
; ┣━━┫ 段 ┃
;
; 字节 0,1┆ ┆ 界 ┆
;
; ┣━━┫ 限 ┃
; ┃ 1 ┃ ① ┃
; ┣━━┫ ┃
; ┃ 0 ┃ ┃
; ------ ┗━━┻━━┛低地址
;


; 图示二

; 高地址………………………………………………………………………低地址

; | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
; |7654321076543210765432107654321076543210765432107654321076543210| <- 共 8 字节
; |--------========--------========--------========--------========|
; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
; ┃31..24┃ (见下图) ┃ 段基址(23..0) ┃ 段界限(15..0)┃
; ┃ ┃ ┃ ┃ ┃
; ┃ 基址2┃③│②│ ①┃基址1b│ 基址1a ┃ 段界限1 ┃
; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫
; ┃ %6 ┃ %5 ┃ %4 ┃ %3 ┃ %2 ┃ %1 ┃
; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛
; │ \_________
; │ \__________________
; │ \________________________________________________
; │ \
; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
; ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃
; ┣━━╋━━╋━━╋━━╋━━┻━━┻━━┻━━╋━━╋━━┻━━╋━━╋━━┻━━┻━━┻━━┫
; ┃ G ┃ D ┃ 0 ┃ AVL┃ 段界限 2 (19..16) ┃ P ┃ DPL ┃ S ┃ TYPE ┃
; ┣━━┻━━┻━━┻━━╋━━━━━━━━━━━╋━━┻━━━━━┻━━┻━━━━━━━━━━━┫
; ┃ ③: 属性 2 ┃ ②: 段界限 2 ┃ ①: 属性1 ┃
; ┗━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┛
; 高地址 低地址
;
;

; 说明:
;
; (1) P: 存在(Present)位。
; P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中;
; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。
;
; (2) DPL: 表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。
;
; (3) S: 说明描述符的类型。
; 对于存储段描述符而言,S=1,以区别与系统段描述符和门描述符(S=0)。
;
; (4) TYPE: 说明存储段描述符所描述的存储段的具体属性。
;
;
; 数据段类型 类型值 说明
; ----------------------------------
; 0 只读
; 1 只读、已访问
; 2 读/写
; 3 读/写、已访问
; 4 只读、向下扩展
; 5 只读、向下扩展、已访问
; 6 读/写、向下扩展
; 7 读/写、向下扩展、已访问
;
;
; 类型值 说明
; 代码段类型 ----------------------------------
; 8 只执行
; 9 只执行、已访问
; A 执行/读
; B 执行/读、已访问
; C 只执行、一致码段
; D 只执行、一致码段、已访问
; E 执行/读、一致码段
; F 执行/读、一致码段、已访问
;
;
; 系统段类型 类型编码 说明
; ----------------------------------
; 0 <未定义>
; 1 可用286TSS
; 2 LDT
; 3 忙的286TSS
; 4 286调用门
; 5 任务门
; 6 286中断门
; 7 286陷阱门
; 8 未定义
; 9 可用386TSS
; A <未定义>
; B 忙的386TSS
; C 386调用门
; D <未定义>
; E 386中断门
; F 386陷阱门
;
; (5) G: 段界限粒度(Granularity)位。
; G=0 表示界限粒度为字节;
; G=1 表示界限粒度为4K 字节。
; 注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。
;
; (6) D: D位是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同。
; ⑴ 在描述可执行段的描述符中,D位决定了指令使用的地址及操作数所默认的大小。
; ① D=1表示默认情况下指令使用32位地址及32位或8位操作数,这样的代码段也称为32位代码段;
; ② D=0 表示默认情况下,使用16位地址及16位或8位操作数,这样的代码段也称为16位代码段,它与80286兼容。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小。
; ⑵ 在向下扩展数据段的描述符中,D位决定段的上部边界。
; ① D=1表示段的上部界限为4G;
; ② D=0表示段的上部界限为64K,这是为了与80286兼容。
; ⑶ 在描述由SS寄存器寻址的段描述符中,D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器。
; ① D=1表示使用32位堆栈指针寄存器ESP;
; ② D=0表示使用16位堆栈指针寄存器SP,这与80286兼容。
;
; (7) AVL: 软件可利用位。80386对该位的使用未左规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。
;


;----------------------------------------------------------------------------
; 描述符类型值说明
; 其中:
; DA_ : Descriptor Attribute
; D : 数据段
; C : 代码段
; S : 系统段
; R : 只读
; RW : 读写
; A : 已访问
; 其它 : 可按照字面意思理解
;----------------------------------------------------------------------------
DA_32 EQU 4000h ; 32 位段

DA_DPL0 EQU 00h ; DPL = 0
DA_DPL1 EQU 20h ; DPL = 1
DA_DPL2 EQU 40h ; DPL = 2
DA_DPL3 EQU 60h ; DPL = 3
;----------------------------------------------------------------------------
; 存储段描述符类型值说明
;----------------------------------------------------------------------------
DA_DR EQU 90h ; 存在的只读数据段类型值
DA_DRW EQU 92h ; 存在的可读写数据段属性值
DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值
DA_C EQU 98h ; 存在的只执行代码段属性值
DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值
DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值
DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值
;----------------------------------------------------------------------------
; 系统段描述符类型值说明
;----------------------------------------------------------------------------
DA_LDT EQU 82h ; 局部描述符表段类型值
DA_TaskGate EQU 85h ; 任务门类型值
DA_386TSS EQU 89h ; 可用 386 任务状态段类型值
DA_386CGate EQU 8Ch ; 386 调用门类型值
DA_386IGate EQU 8Eh ; 386 中断门类型值
DA_386TGate EQU 8Fh ; 386 陷阱门类型值
;----------------------------------------------------------------------------


; 选择子图示:
; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
; ┃ 15 ┃ 14 ┃ 13 ┃ 12 ┃ 11 ┃ 10 ┃ 9 ┃ 8 ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃
; ┣━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━╋━━╋━━┻━━┫
; ┃ 描述符索引 ┃ TI ┃ RPL ┃
; ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━┻━━━━━┛
;
; RPL(Requested Privilege Level): 请求特权级,用于特权检查。
;
; TI(Table Indicator): 引用描述符表指示位
; TI=0 指示从全局描述符表GDT中读取描述符;
; TI=1 指示从局部描述符表LDT中读取描述符。
;

;----------------------------------------------------------------------------
; 选择子类型值说明
; 其中:
; SA_ : Selector Attribute

SA_RPL0 EQU 0 ; ┓
SA_RPL1 EQU 1 ; ┣ RPL
SA_RPL2 EQU 2 ; ┃
SA_RPL3 EQU 3 ; ┛

SA_TIG EQU 0 ; ┓TI
SA_TIL EQU 4 ; ┛
;----------------------------------------------------------------------------



; 宏 ------------------------------------------------------------------------------------------------------
;
; 描述符
; 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 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro ; 共 8 字节
;
; 门
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0FFFFh) ; 偏移 1 (2 字节)
dw %1 ; 选择子 (2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节)
dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节)
%endmacro ; 共 8 字节
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

cpu32.s

%include    "src/boot/pm.inc"
global _start
_start:
jmp 0:LABEL_BEGIN

[SECTION .gdt]
; GDT
; 段基址, 段界限, 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束

GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址

; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]

[SECTION .s16]
[bits 16]
; 程序入口
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0xFFFC

; 初始化 32 位代码段描述符
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

; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址

; 加载 GDTR
lgdt [GdtPtr]

; 关中断
cli

; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al

; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax

; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
; END of [SECTION .s16]

[SECTION .s32]; 32 位代码段. 由实模式跳入.
[bits 32]
; 进入32位模式
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)

mov edi, (80 * 10 + 0) * 2 ; 屏幕第 10 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax

; 到此停止
jmp $

SegCode32Len equ $ - LABEL_SEG_CODE32

; 填充引导区
times 510-($-$$)-0x7e db 0 ; 填充剩下的空间,不知道为什么不能直接引用_start的地址,这里0x7e是我临时凑出来的
dw 0xAA55

Makefile 编译脚本:

# 进入32位保护模式,加载到0x10000h
cpu32.o : $(SRC)boot/cpu32.s
$(NASM) $(ASFLAGS) -f elf32 -o $(TARGET)$@ $^
cpu32.bin : cpu32.o
$(LD) $(LDFLAGS) -Ttext 0x7c00 --oformat binary -o $(TARGET)$@ $(TARGET)$^
install_cpu32 : cpu32.bin
$(DD) if=$(TARGET)cpu32.bin of=$(TARGET)os.img conv=notrunc
run_cpu32 : install_cpu32
cd $(TOOLPATH) && bochs.bat && cd ..

目录结构:

  • src
  • boot
  • pm.inc
  • cpu32.s