[系列]OS学习-自己写操作系统(4)- 保护模式

第三章 保护模式

非常困难、内容非常多的一章。这一章的学习流程大致:

1.从实模式到保护模式的跳转。熟悉保护模式下寻址流程。GDT的实现,段选择子的实现,熟悉GDT中段描述符的格式,尤其是属性一项。

2.在GDT中,添加一个LDT。

3.从保护模式如何跳转回实模式?

4.CPL,RPL,DPL的关系,深入理解“保护”二字的意义

5.从0优先级,跳转到3优先级,体会“门”、TSS的详细意义

6.开启分页机制,熟悉页目录表基址寄存器cr3,以及页目录表-PDE,页表-PTE,以及32位CPU为什么最大寻址4GB。


下面就每一点说说自己的学习体会:

1.从实模式到保护模式的跳转,熟悉保护模式下寻址流程。GDT的实现,段选择子的实现。

这在上一篇博客中详细说明了。

要弄清楚跳转,就要弄清实模式和保护模式下的区别。

1.实模式下,用CS:IP的方式获得指令地址,然后取指,执行。保护模式下,CS中存放的是“代码段段选择子”,IP继续存放偏移量。所以跳转时一定要把CS里放进“代码段段选择子”。怎么放呢?

使用 jmp dowrd 代码段段选择子:00000000,就可以更改CS中的内容了。

2.可是,CPU怎么知道jmp语句执行时,是保护模式还是实模式?更改32位的cr0寄存器(0号控制寄存器),因此,jmp之前,一定要这样做:

MOV EAX, CR0

OR EAX,1

MOV CR0,EAX

当CR0的最低位为0——CPU在实模式下工作

当CR0的最低位为1——CPU在保护模式下工作

3.在Jmp 段选择子:0这句话之前,应该先把段选择子和对应的段描述符的内容填好。怎么填?看了这段代码,你会一目了然:

段描述符的宏实现:

	%macro Descriptor 3
		dw	%2 & 0FFFFh				; 段界限1
		dw	%1 & 0FFFFh				; 段基址1
		db	(%1 >> 16) & 0FFh			; 段基址2
		dw	((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)	; 属性1 + 段界限2 + 属性2
		db	(%1 >> 24) & 0FFh			; 段基址3
	%endmacro ; 共 8 字节
GDT的实现:建立了一个很普通的section,里面放着至关重要的GDT

	[SECTION .gdt]
	; GDT
	;                              段基址,       	 段界限,   属性
	LABEL_GDT:	 Descriptor         0,                0, 0          ; 空描述符
	LABEL_DESC_1:    Descriptor         0, 		 0FFFFH, 如何如何   ; 非一致代码段
	LABEL_DESC_2:    Descriptor   0B8000h,           0FFFFH, 如何如何   ; 显存首地址
	; GDT 结束
段选择子

	Selector1		equ	LABEL_DESC_1	- LABEL_GDT
	Selector2		equ	LABEL_DESC_2	- LABEL_GDT
这就是他们的真正实现

4.冷门知识:A20地址线的问题/跳转之前要关中断/段描述符的填充方法

把《Orange's》书中,完整的代码贴出来。关键的部分看懂了,别的部分多看几次就知道啦。

; ==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================

%include	"pm.inc"	; 常量, 宏, 以及一些说明

org	07c00h
	jmp	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, 0100h

	; 初始化 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]

LABEL_SEG_CODE32:
	mov	ax, SelectorVideo
	mov	gs, ax			; 视频段选择子(目的)

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

	; 到此停止
	jmp	$

SegCode32Len	equ	$ - LABEL_SEG_CODE32
; END of [SECTION .s32]

2.添加一个LDT

在有了GDT之后,为什么还要LDT?一个是global(全局的),一个是local(本地的)

我的理解:GDT是给系统进程/内核这样的“重要”进程使用的;如果所有的进程都是用同一描述符表,一定会造成管理的混乱。所以,每个进程应该有自己的描述符表,即GDT。


3.如何从保护模式跳回实模式?

要学会跳回实模式,就必须要透彻懂得实模式和保护模式的区别。大概可以概括为以下几点:

1.段寄存器中放的是段选择子,不再是段基址。

2.A20地址线打开了

3.cr0的最后一位变成1了

4.一旦跳到保护模式之后,就找不到以前的CS(代码段寄存器)了。(这个问题非常难解决,下面谈)

5.实模式下,不能设置段属性(在此不得不提,每一个段寄存器其实都是96位的,下面谈)

只要把这些问题逐一解决就好了。

谈几个疑难事件

1.不能从32位代码段跳回实模式,只能从16位代码段跳回去,为什么?

不信的同学可以试试,这句话是真的。原因在下个问题中解决。

2.技术细节:如果想跳回16位代码段,需要在GDT中多写一个"默认"描述符,为什么?

在此不得不提,段寄存器(CS/ES/DS/SS)本质上都是96位的,然而我们只能控制其中的16位,即WORD Selector;这部分,剩下的80位,是CPU自动将对应段描述符中的内容载入进去的。载入的目的是,加快运行速度,不必每次都到GDT中去寻找。

Struct Segment

{

   WORD  Selector;        16位段选择子(实模式下的段基址)

   WORD  Attribute;        16位属性

   DWORD Base;            32位段基地址(保护模式下的基地址)

   DWORD Limit;             32位段界限

};

我们在保护模式下,会出于各种各样的目的,为不同的段设置不同的段属性。比如有的段Limit可能是0FFFFFFH,有的段可能是一致/非一致代码段,有的段可能优先级是1/2/3级(优先级默认0级,实模式下都是0级),我们就改变了CS/ES/DS/SS寄存器中其余80位的内容。如果我们在16位代码段中,不管不顾,直接跳回实模式,那就可能会把错误的设置带回实模式——而实模式下是不能改变段属性/段界限/段基址的。

所以要写一个默认的段描述符 

LABEL_DESC_NORMAL:  Descriptor 0(实模式下默认段基址), 0ffffh(实模式下默认段界限), DA_DRW(实模式下默认段属性)                 ; Normal 描述符 段界限是64K
在跳转之前,把这个段描述符赋给段寄存器

mov	ax, SelectorNormal
	mov	ds, ax
	mov	es, ax
	mov	fs, ax
	mov	gs, ax
	mov	ss, ax

然而我们这样做,不能改变CS寄存器中后80位的内容:mov cs,ax是不被允许的,能改变cs的只有jmp和call指令。所以,我们必须要从32位保护模式代码段中跳入一个16位代码段中,再从这个16位代码段中跳回实模式。

4.CPL,RPL,DPL的关系,深入理解“保护”二字的意义

保护模式的麻烦——特权级校验,终于来了。在实模式下,畅快访问所有段的日子已经一去不复返了。这部分只是庞杂冗余,决心做一次大的整理。

优先级(Privilege Level):保护模式下,每一个段都有一个优先级(不仅仅代码段有优先级,数据段,堆栈段都有)。共分ring0 ring1 ring2 ring3四个等级,其中0级优先级最高,3级优先级最低。内核代码段一般是0级,用户代码一般是3级。

一致代码段和非一致代码段 找到一篇好的博客,可以看看。-

CPL:Current Privilege Level,当前优先级。通常等于当前所在代码段的DPL。

DPL:Descriptor Privilege Level,描述符优先级。存储在段描述符中。每一个段对应一个段描述符,每一个段描述符都有一个DPL,这个DPL即代表这个段的优先级。

RPL:Requested Privilege Level,请求优先级。存储在段选择子中。

每次访问其他段(包括跳转到其它代码段和访问数据段和使用堆栈段),CPU都会把CPL、RPL和目标段DPL比较。比较的策略随着情况不同而变化。如果校验通过,就可以访问,如果校验不通过,就会引发错误。

Gate:即门,门描述符的简称。分为调用门/中断门/陷阱门/任务门。

问题一:校验的策略究竟有多少种花样?

在不调用门的情况下,如果目标代码段是:

一致代码段,校验规则:CPL>=目标段的DPL,RPL不要求。

非一致代码段,校验规则:CPL==目标段DPL,RPL<=目标段DPL。

调用门时:(盗图一张)

使用调用门时,校验分为2大部分:CPL/RPL和调用门的DPL比较;CPL/RPL和目标段的DPL比较

借此可以实现从低优先级到高优先级的转换。

问题二:为什么有了CPL,还要有RPL?感觉很没用啊。

这个问题请看转载的这篇博客……

问题三:当Jmp指令执行的时候,究竟发生了什么?

jmp大致可以分为2大类:

1.jmp  选择子:偏移量被称为直接转移,丝毫不绕弯。直接转移时,CPL是不会变的。

2.jmp 包含选择子的谜之事物:偏移量间接转移,选择子被包裹起来

选择子被什么包裹起来了?大致分为3类:

1.包含目标代码段选择子的call gate descriptor

2.包含目标代码段选择子的TSS(Task State Segment 任务状态段)

3.任务门,这个门指向一个TSS,TSS中有着选择子。(相当于第2种情况外边又包了一层任务门)

目标代码段的selector会被加载到cs中。在加载过程中进行段界限/类型/权限校验,如果校验成功,cs加载。

问题四:为什么要有门(Gate)这种打破规则的东西?

只通过jmp/call这样的直接转移,CPL是不会变的——活动范围实在太小了。为了扩大活动范围又不失安全性,创立了门。

在面对问题五之前,我想先介绍一个小技巧:实模式所有段都在最高优先级0,我们如果想实验从低优先级爬到高优先级,就要先想办法把自己降落下去。——使用ret(返回)指令

问题五:特权级发生变化时,堆栈会发生变换是怎么一回事?

为了防止不同特权级,有不同的堆栈。因为优先级有4个,所以堆栈也有对应的4个。

与此同时,TSS也要做啊!

你可能感兴趣的:([系列]OS学习-自己写操作系统(4)- 保护模式)