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

来源:互联网 发布:js元素节点举例 编辑:程序博客网 时间:2024/04/30 20:15

第三章 保护模式

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

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 3dw%2 & 0FFFFh; 段界限1dw%1 & 0FFFFh; 段基址1db(%1 >> 16) & 0FFh; 段基址2dw((%2 >> 8) & 0F00h) | (%3 & 0F0FFh); 属性1 + 段界限2 + 属性2db(%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 结束
段选择子

Selector1equLABEL_DESC_1- LABEL_GDTSelector2equLABEL_DESC_2- LABEL_GDT
这就是他们的真正实现

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

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

; ==========================================; pmtest1.asm; 编译方法:nasm pmtest1.asm -o pmtest1.bin; ==========================================%include"pm.inc"; 常量, 宏, 以及一些说明org07c00hjmpLABEL_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 结束GdtLenequ$ - LABEL_GDT; GDT长度GdtPtrdwGdtLen - 1; GDT界限dd0; GDT基地址; GDT 选择子SelectorCode32equLABEL_DESC_CODE32- LABEL_GDTSelectorVideoequLABEL_DESC_VIDEO- LABEL_GDT; END of [SECTION .gdt][SECTION .s16][BITS16]LABEL_BEGIN:movax, csmovds, axmoves, axmovss, axmovsp, 0100h; 初始化 32 位代码段描述符xoreax, eaxmovax, csshleax, 4addeax, LABEL_SEG_CODE32movword [LABEL_DESC_CODE32 + 2], axshreax, 16movbyte [LABEL_DESC_CODE32 + 4], almovbyte [LABEL_DESC_CODE32 + 7], ah; 为加载 GDTR 作准备xoreax, eaxmovax, dsshleax, 4addeax, LABEL_GDT; eax <- gdt 基地址movdword [GdtPtr + 2], eax; [GdtPtr + 2] <- gdt 基地址; 加载 GDTRlgdt[GdtPtr]; 关中断cli; 打开地址线A20inal, 92horal, 00000010bout92h, al; 准备切换到保护模式moveax, cr0oreax, 1movcr0, eax; 真正进入保护模式jmpdword SelectorCode32:0; 执行这一句会把 SelectorCode32 装入 cs,; 并跳转到 Code32Selector:0  处; END of [SECTION .s16][SECTION .s32]; 32 位代码段. 由实模式跳入.[BITS32]LABEL_SEG_CODE32:movax, SelectorVideomovgs, ax; 视频段选择子(目的)movedi, (80 * 11 + 79) * 2; 屏幕第 11 行, 第 79 列。movah, 0Ch; 0000: 黑底    1100: 红字moval, 'P'mov[gs:edi], ax; 到此停止jmp$SegCode32Lenequ$ - 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
在跳转之前,把这个段描述符赋给段寄存器

movax, SelectorNormalmovds, axmoves, axmovfs, axmovgs, axmovss, 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也要做啊!

0 0
原创粉丝点击