OS_lab——保护模式之GDT、 Descriptor、Selector、GDTR 及其之间关系

1. 保护模式的相关数据结构

保护模式必要的数据结构定义

•   GDT:即为 Global Descriptor Table(全局描述符表),又称段描述符表, 为保护模式下的一个数据结构。其中包含多个 descriptor,定义了段的起始地址,界限属性等。

•   Descriptor:段描述符,包含段基址,段界限,段属性。

•   Selector:选择子,其作用是表示段描述符符相对于 GDT 基址的偏移。

•   GDTRGDT 寄存器。其结构与 GDTPTR 类似,有 6 字节,前两字节为 GDT 界限,后 4 字节为 GDT 基地址。

Descriptor的定义

如下图所示,Descriptor 分别存储了段基址、段界限和相关属性。由于部分原因, 段基址与段界限被分别存储:

2. 从实模式到保护模式

pmtest1.asm代码展示

; ==========================================

; 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 g s, 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]

执行结果

从实模式到保护模式

1.  准备 GDT。

2.  用 lgdt 加载 gdtr。

3.  打开 A20。

4.  置 cr0  PE 位。

5.  跳转,进入保护模式。

jmp跳转:dword前缀

 pmtest1.asm 的第 71 行:jmp dword SelectorCode32:0 删除 dword 标识会导 致编译后偏移地址被截断,可能无法正确地跳转到指定的位置。 原因:此时操作 系统的运行环境已由实模式(16 位)转换为保护模式(32 ),但由于编译器的 限制,此段代码只能放置于 16 位汇编代码部分,因此需要 dword 标识避免 32  长度的偏移地址被截断,而发生未知错误。 测试代码的反编译源码如下所示:

GDT表的构造

Descriptor 的构造

段描述符的构造借助 pm.inc 文件来进行,分别对段基址、段界限与相关属性进行 定义,宏会根据定义将数据重组以满足规范的要求

Selector的构造

Selector 的值为段描述符位置与 GDT 表初始位置之差

GDT表的切换

由于代码在实模式将段的偏移地址存储在的 GDT 表的段基址中。 因此,在保护模 式下,程序使用事先定义好的 Selector 选择子即可实现 GDT 表的切换。

3.2. 由保护模式返回实模式

与从实模式进入保护模式的方法相反,但是由于进入实模式的相关操作需要在 16 位代码段文成,且由于高速缓存的原因,因此,由保护模式返回实模式共一下几 步:

1.  加载相关段寄存器。

2.   cr0 的 PE 位。

3.  跳转,返回 16 位代码段。

4.  设置实模式的段寄存器。

5.  关闭 A20。

6.  开中断

7.  返回实模式

执行效果展示

下图为 pmtest2.asm 代码的执行效果,可以看到,在引导入保护模式后,程序打 印了字符并回到了实模式下的 dos 系统:

4. LDT 切换

LDT (Local Descriptor Table)

LDT  GDT 相同,均为描述符表,但 LDT 表仅有部分作用范围,而 GDT 表则作用 于全局

pmtest3.asm代码片段展示

[SECTION .gdt]

...

LABEL_DESC_LDT:    Descriptor       0,        LDTLen - 1, DA_LDT     ;

LDT

...

SelectorLDT     equ LABEL_DESC_LDT      - LABEL_GDT

; END of [SECTION .gdt]

...

[SECTION .s32]; 32 位代码段. 由实模式跳入.

[BITS   32]

LABEL_SEG_CODE32:

...

; Load LDT

mov ax, SelectorLDT

lldt    ax

jmp SelectorLDTCodeA:0  ; 跳入局部任务

...

; END of [SECTION .s32]

...

; LDT

[SECTION .ldt]

ALIGN   32

LABEL_LDT:

;                             段基址       段界限      属性

LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位

LDTLen      equ $ - LABEL_LDT

; LDT 选择子

SelectorLDTCodeA    equ LABEL_LDT_DESC_CODEA    - LABEL_LDT + SA_TIL

; END of [SECTION .ldt]

LDT表的构造

1.  在 GDT 表中定义 LDT 表所对应的代码段

2.  在 LDT 表中定义对应的 LDT 

–  在 LDT 表的选择子定义上,在代码偏移量的基础上,需要额外增加 一个属性 SA_TIL,该属性将在 pm,inc 宏中存储至选择子的 TL 部分, 用于辨识该选择子是 LDT 表的选择子

LDT表的使用

1.  使用 lldt 指令加载 LDT 表所在的段

2.  使用对应 LDT 表所对应的选择子进行跳转

3.  执行代码

执行效果展示

下图为 pmtest3.asm 代码的执行效果,可以看到,程序在 LDT 表段打印输出了字 母L:

5.权限访问与段间切换

权限访问规则

 IA32 的分段机制中,特权级总共有 4 个特权级别,从高到低分别是 0、1、2、3。数字越小表示的特权级越大。

CPL

CPL 是当前执行的程序或任务的特权级。它被存储在 cs  ss 的第 0 位和第 1  上。在通常情况下,CPL 等于代码所在的段的特权级。当程序转移到不同特权级 的代码段时,处理器将改变 CPL。一致代码段可以被相同或者更低特权级的代码 访问。当处理器访问一个与 CPL 特权级不同的一致代码段时,CPL 不会被改变。

DPL

DPL 表示段或者门的特权级。它被存储在段描述符或者门描述符的 DPL 字段中。 当当前代码段试图访问一个段或者门时,DPL 将会和 CPL 以及段或门选择子的 RPL 相比较,根据段或者门类型的不同,DPL 将会被区别对待:

•   数据段:DPL 规定了可以访问此段的最低特权级

•   非一致代码段(不使用调用门的情况下):DPL 规定访问此段的特权

•   一致代码段:DPL 规定了访问此段的最高特权级

RPL

RPL 是通过段选择子的第 0 位和第 1 位表现出来。处理器通过检查 RPL  CPL  确认一个访问请求是否合法。也就是说,如果 RPL 的数字比 CPL 大(数字越大特 权级越低),那么 RPL 将会起决定性作用,反之亦然。

特权级检验测试

使用 pmtest2.asm,将数据段描述符的 DPL 与其段选择子的 RPL 进行修改: 

[SECTION .gdt]

; GDT

;

LABEL_DESC_DATA:   Descriptor 0, DataLen-1, DA_DRW+DA_DPL1 ; Data

; GDT 结束



; GDT 选择子

SelectorData   equ LABEL_DESC_DATA - LABEL_GDT + SA_RPL3

; END of [SECTION .gdt]

调试结果如下所示:

调试程序报错:load_seg_reg (DS, 0x0023): RPL & CPL must be <= DPL 证明此 处违反了特权级管理的规则

段间切换

一下几种方法可以实现不同代码段之间的转移:

   使用 jmp  call 指令

  目标操作数包含目标代码段的段选择子

–  目标操作数指向一个包含目标代码段选择子的调用门描述符。

•   使用门描述符进行转移

其中,通过 jmp  call 所能进行的代码段间转移是非常有限的,对于非一致代码 段,只能在相同特权级代码段之间转移。遇到一 致代码段也最多能从低到高,而 CPL 不会改变。 若需要进行不同特权级之间的转移,则需要即运用门描述符 进行转移。

门描述符

门描述符的结构

调用门直接定义了目标代码对应段的选择子、入口偏移地址等一系列属性,可直 接进行代码段的跳转。 门描述符有以下 4 种:

   调用门

   中断门

   陷阱门

   任务门

调用门的使用

pmtest4.asm 代码片段展示

[SECTION .gdt]

; GDT

;                            段基址,       段界限     , 属性

LABEL_DESC_CODE_DEST: Descriptor 0,SegCodeDestLen-1, DA_C+DA_32; 非一致 代码段,32

...

; 门                               目标选择子,偏移,DCount, 属性

LABEL_CALL_GATE_TEST:  Gate  SelectorCodeDest,       0,            0,

DA_386CGate+DA_DPL0

...

; GDT 选择子

SelectorCodeDest    equ LABEL_DESC_CODE_DEST    - LABEL_GDT



SelectorCallGateTest    equ LABEL_CALL_GATE_TEST    - LABEL_GDT

; END of [SECTION .gdt]

...

[SECTION .s32]; 32 位代码段. 由实模式跳入.

[BITS   32]

...

; 测试调用门(无特权级变换),将打印字母 'C'

call    SelectorCallGateTest:0

...

jmp SelectorLDTCodeA:0  ; 跳入局部任务,将打印字母 'L'。

...

; END of [SECTION .s32]

使用 pm.inc 中的 Gate 宏进行门的调用,在代码片段中,此门所指向的位置为 SelectorCodeDest:0,即 LABEL_DESC_CODE_DEST 处的代码。

执行效果展示

程序进入调用门所指向的代码段,输出了字母 C

6. 使用调用门进行特权级的变换

调用门使用的特权检验

堆栈的特权级转换——TSS

由于在进行 call-ret 跳转时,堆栈会存储跳转前的 cs:eip 地址,并在子程序结 束时返回。但是在有特权级的代码跳转时,所调用的堆栈段也会发生改变。因此, 需要使用 TSS(Tasj-State Stack)来避免这类问题。

TSS 的结构

 TSS 辅助的转移过程

1.  根据目标代码段的DPL(新的 CPL) 从 TSS 中选择应该切换至哪个 ss  esp

2.  从 TSS 中读取新的 ss  esp。在这过程中如果发现 ss、esp 或者 TSS 界限 错误都会导致无效 TSS 异常(#TS)。

3.  对 ss 描述符进行检验,如果发生错误,同样产生#TS 异常。 同样产生#TS  m。mt 接出媒设

4.  暂时性地保存当前 ss  esp 的值。

5.  加载新的 ss  esp。

6.  将刚刚保存起来的 ss  esp 的值压入新栈。

7.  从调用者堆栈中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目 由调用门中 Param Count 一项来决定。如果 Param Count  0 的话,将不 会复制参数。

8.  将当前的 cs  eip 压栈。

9.  加载调用门中指定的新的 cs  eip,开始执行被调用者过程。 

  TSS 辅助的返回过程

1.  检查保存的 cs 中的 RPL 以判断返回时是否要变换特权级。

2.  加载被调用者堆栈上的 cs  eip (此时会进行代码段描述符和选择子类型 和特权级检验)。

3.  如果 ret 指令含有参数,则增加 esp 的值以跳过参数,然后 esp 将指向被 保存过 的调用者 ss  esp 。注意 ret 的参数必须对应调用    ParamCount 的值。

4.  加载 ss  esp,切换到调用者堆栈,被调用者的 ss  esp 被丢弃。在这 里将会进行 ss 描述符、esp 以及 ss 段描述符的检验。

5.  如果 ret 指令含有参数,增加 esp 的值以跳过参数(此时已经在调用者堆栈 中)。

6.  检查 dsesfsgs 的值,如果其中哪一个寄存器指向的段的 DPL 小于 CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存 器。

使用调用门进行特权级转换

pmtest5.asm 实现了一个 ring3 特权级的代码(将在屏幕中打印数字 3);并初始  TSS,通过调用门引用 ring0 特权级的代码片段在屏幕上打印字母 C。 实验结 果如下图所示:

测试成功

1. 解决问题与动手改

1.1 GDTDescriptorSelectorGDTR 结构,及其含义是什么?他们的关联关 系如何?pm.inc 所定义的宏怎么使用?

       GDT(Global Descriptor Table 全局描述符表)又叫段描述符表,为保护模 式下的一个数据结构。其中包含多个 descriptor,定义了段的起始地址,界限属 性等,其作用是提供段式存储机制;

Descriptor 为段描述符,包含段基址、段界限、段属性;

       Selector 为选择子,可以对应一个描述符。在 pmtest1.asm 程序中,其对应 的就是描述符相对于 GDT 基址的偏移。

       GDT 是一个结构数组,包含多个 Descriptor,每个 Descriptor 都是 GDT 数组 的一个表项,存储各个段的段基址、段界限和属性。Selector 记录对应 Descriptor 相对于 GDT 基址(LABEL_GDT)的偏移、表类型(GDT/LDT)和段描述符特权。GDTR 则是记录了 GDT 表的基地址和界限。综合以上,就能够得到某个 Descriptor 的地 址。而保护模式下寻址就是先靠 GDTR 找到 GDT,而后根据 Descriptor 找到对应 段的地址,而后再加上段内偏移 offset,就获得某个线性地址。

程序中 Descriptor 由 pm.inc 中的宏 Descriptor 生成。宏的具体使用如下: 

a. 宏名 Descriptor,3 表明有三个参数,分别为段基址、界限、属性;

b. 第一行 dw 为两字节,决定了段界限;

c. 第二三行 dw  dd 确定了段基址 1、2;

d. 第四行 dw 两字节构成段属性和段界限 2;

e. 最后一行的 dw 两字节构成段基址 3。

1.2  从实模式到保护模式,关键步骤有哪些?为什么要关中断?为什么要打开 A20 地址线?从保护模式切换回实模式,又需要哪些步骤?

从实模式到保护模式步骤:

a. 准备 GDT,初始化 GDT 属性,将需要的段的描述符和选择子定义好; 

b. 初始化段;

c. 将 GDT 的物理地址填充到 GdtPtr 中,然后将填充的地址加载到寄存器 gdtr;

d. 关中断;

e. 打开 A20 地址线;

f. 置 cr0  PE 位;

g. 跳转到描述符对应段首地址,进入保护模式。

       需要关中断的原因是:保护模式下的中断处理机制和实模式下不同,如果开 启中断而对应的中断处理机制尚未完善将会出现错误。

       需要打开 A20 地址线的原因是:在 8086 CPU 中只 20 位地址总线,它的最大寻 址能力只能达到 1 MB。8086 设计当程序在访问 1 MB 以上的内存地址时,将从 0 地址开始“ 回卷 ”,也就是说当访问 1 MB  1 位时,实际访问的空间是 1 地址。 而在之后的 CPU 中,访问空间早已超过 1 MB,这就导致了不兼容。IBM 设计如过 A20 不被打开,则继续使用回滚机制,第二十个地址为是 0。如果打开 A20,则可 以正常访问 1 MB 以上的内存地址。

从实模式到保护模式步骤:

  1. 加载一个合适的描述符选择子到有关段寄存器;
  2. 跳转至 16 位代码段;
  3.  cr0  PE 位;
  4. 跳转回到实模式;
  5. 关闭 A20;
  6. 打开中断。

1.3  解释不同权限代码的切换原理,call, jmp,retf 使用场景如何,能够互换吗?

       对于 jmp 而言,长跳转和短跳转仅仅存在结果上的不同,短跳转对应段内跳 转,长跳转对应段间跳转。

       对 call 而言,由于 call 指令会影响堆栈,所以长调用和短调用会产生不同 影响,在短调用当中,call 指令会将下一条指令 eip 压栈,当遇到 ret 指令执行 时,该 eip 会被从堆栈中弹出。

       在长调用时,call 指令也会将 eip  cs 都压入栈中,遇到 retf 时会弹出 eip  cs,大致来说 call 相当于 push+jmp  ret 相当于 pop+jmp。

1.4  动手改:

 自定义添加 1  GDT 代码段、1  LDT 代码段,GDT 段内要对一个内存数据结 构写入一段字符串,然后 LDT 段内代码段功能为读取并打印该 GDT 的内容;

该功能参考 pmtest3.asm 实现,实验结果如下:

第一步:在.Data 段中修改 StrTest 内容

第二步:修改 32 味代码段,跳入 LDT 局部任务中

第三步:修改 LDT,CodeA 中的逻辑,将偏移设为 TestStr 的偏移量,最后输出到 屏幕上

 自定义 2  GDT 代码段 A、B,分属于不同特权级,功能自定义,要求实现 A-->B 的跳转,以及 B-->A 的跳转。

  • 46
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值