1. 保护模式的相关数据结构
保护模式必要的数据结构定义
• GDT:即为 Global Descriptor Table(全局描述符表),又称段描述符表, 为保护模式下的一个数据结构。其中包含多个 descriptor,定义了段的起始地址,界限属性等。
• Descriptor:段描述符,包含段基址,段界限,段属性。
• Selector:选择子,其作用是表示段描述符符相对于 GDT 基址的偏移。
• GDTR:GDT 寄存器。其结构与 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. 检查 ds、es、fs、gs 的值,如果其中哪一个寄存器指向的段的 DPL 小于 CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存 器。
使用调用门进行特权级转换
pmtest5.asm 实现了一个 ring3 特权级的代码(将在屏幕中打印数字 3);并初始 化 TSS,通过调用门引用 ring0 特权级的代码片段在屏幕上打印字母 C。 实验结 果如下图所示:
测试成功
1. 解决问题与动手改
1.1 GDT、Descriptor、Selector、GDTR 结构,及其含义是什么?他们的关联关 系如何?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.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 的跳转。