https://github.com/yyu/osfs00
实验目的:
理解 x86 架构下的段式内存管理
掌握实模式和保护模式下段式寻址的组织方式,
关键数据结构, 代码组织方式
掌握实模式与保护模式的切换
掌握特权级的概念, 以及不同特权之间的转移
实验内容:
1. 认真阅读章节资料, 掌握什么是保护模式, 弄清关键数据结构:
GDT,descriptor,selector,GDTR, 及其之间关系, 阅读
pm.inc 文件中数据结构以及含义, 写出对宏 Descriptor 的分析
2. 调试代码,/a/ 掌握从实模式到保护模式的基本方法, 画出代码
流程图, 如果代码 / a / 中, 第 71 行有 dword 前缀和没有前缀, 编
译出来的代码有区别么, 为什么, 请调试截图.
3. 调试代码,/b/, 掌握 GDT 的构造与切换, 从保护模式切换回实
模式方法
4. 调试代码,/c/, 掌握 LDT 切换
5. 调试代码,/d / 掌握一致代码段, 非一致代码段, 数据段的权限
访问规则, 掌握 CPL,DPL,RPL 之间关系, 以及段间切换的基
本方法
6. 调试代码,/e / 掌握利用调用门进行特权级变换的转移
代码对应 iso 中 chapter3
实验解决问题与课后动手改:
1. GDT,Descriptor,Selector,GDTR 结构, 及其含义是什么? 他
们的关联关系如何? pm.inc 所定义的宏怎么使用?
2. 从实模式到保护模式, 关键步骤有哪些? 为什么要关中断? 为
什么要打开 A20 地址线? 从保护模式切换回实模式, 又需要哪些
步骤?
3. 解释不同权限代码的切换原理, call, jmp,retf 使用场景如何,
能够互换吗?
4. 课后动手改:
1. 自定义添加 1 个 GDT 代码段, 1 个 LDT 代码段, GDT 段内要对一个内
存数据结构写入一段字符串, 然后 LDT 段内代码段功能为读取并打
印该 GDT 的内容;
2. 自定义 2 个 GDT 代码段 A,B, 分属于不同特权级, 功能自定义, 要
求实现 A-->B 的跳转, 以及 B-->A 的跳转.
实验环境:
VMwareWorkstationPro 15.5.0
Ubuntu 12.04.5 desktop i386 32 位
bochs 2.6.9
关键技术:
bochs 使用
实模式, 保护模式及其关键数据结构 GDT,LDT,Descriptor,Selector 等
特权级变换
实验步骤:
1. 认真阅读章节资料, 掌握什么是保护模式, 弄清关键数据结构:
GDT,descriptor,selector,GDTR, 及其之间关系, 阅读
pm.inc 文件中数据结构以及含义, 写出对宏 Descriptor 的分析
GDT 即为 Global Descriptor Table(全局描述符表)又叫段描述符表, 为保护模式下的一个数据结构. 其中包含多个 descriptor, 定义了段的起始地址, 界限属性等.
descriptor 为段描述符, 包含段基址, 段界限, 段属性. 其结构如图
Selector 为选择子, 有其数据结构. 在 pmtest1.asm 程序中, 其作用就是偏移, 对应描述符相对于 GDT 基址的偏移.
GDTR 为 GDT 寄存器. 结构与 GDTPTR 类似, 6 字节, 前两字节 GDT 界限, 后 4 字节 GDT 基地址.
四者关系:
GDT 中包含多个 descriptor,descriptor 包含段的信息, 包含段基址, 界限属性等. 多个 selector 包含对应 descriptor 相对于 GDT 的偏移, 于是 selector 发挥了类似 指向 descriptor 的作用. 而 GDTR 中包含了 GDT 基地址与界限. 四者综合就可以获得某个 descriptor 的地址. 而保护模式下寻址就先靠 GDTR 找到 GDT, 然后根据 descriptor 找到对应段的地址, 然后再加上段内偏移 offset, 就得到某个线性地址.
如图所示
对宏 Descriptor 分析:
结构如图:
共 8 字节. 从低地址开始前两字节为段界限 1, 然后三个字节为段基址 1, 然后两个字节 byte5,byte6 包含段属性以及段界限 2, 最后一字节为段基址 2. 由于历史原因, 段界限和段基址都分开存放. 程序中 descriptor 由 pm.inc 中的宏 descriptor 生成.
代码:
%macro Descriptor 3 ;macro 定义宏. 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 字节
macro 代表宏开始. 宏名 Descriptor,3 代表有三个参数.
参数 1-3 分别为段基址, 界限, 属性.
比如 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
利用宏 Descriptor 定义了基址为 0B8000H 的段 LABEL_DESC_VIDEO.
0B8000H 为显存首地址. 利用该段在屏幕中显示数据.
之后第一行 dw 为两字节. %2 & 0FFFFh, 相当于取段界限的低位, 写入这两字节.
然后 dw,dd 去段基址 1,2, 构成三字节段基址, 相当于上面结构图的段基址 1.
然后 dw 两字节构成段属性, 段界限 2.
然后 dw 两字节构成段基址 3.
其中段基址为该段起始地址, 界限为长度.
2. 调试代码,/a/ 掌握从实模式到保护模式的基本方法, 画出代码
流程图, 如果代码 / a / 中, 第 71 行有 dword 前缀和没有前缀, 编
译出来的代码有区别么, 为什么, 请调试截图.
流程图: pmtest1.asm 用文字描述如下
(1)定义 GDT [SECTION .gdt]
其中定义了一个空 descriptor, 一个 32 位代码段, 一个显存 descriptor
其中 32 位代码段只初始化了段界限, 段属性
(2)进入[SECTION .s16] 16 位代码段(实模式)
修改 GDT 值: 修改 32 位段描述符值
将 LABEL_SEG_CODE32 的物理地址 (即 [SECTION .s32] 这个段的物理地址)赋给 eax, 然后把它分成三部分赋给描述符 DESC_CODE32 中的相应位置. 由于 DESC_CODE32 的段 界限和属性已经指定, 所以至此, DESC_CODE32 的初始化全部完成.
(将段寄存器段界限段属性由符合实模式要求到符合保护模式要求)
之后赋值 gdtr 寄存器:
把 GDT 的物理地址填充到了 GdtPtr 这个 6 字节的数据结构中.
lgdt[GdtPtr] 将 GdtPtr 指示的 6 字节加载到寄存器 gdtr
之后关中断.
之后打开 A20 地址线.
修改 cr0 寄存器: PE 位置 1.
此时 cs 的值仍然是实模式下的值, 把代码段的选择子装入 cs:
jmpdword SelectorCode32:0 , 进入 32 位代码段[SECTION .s32]
(3)进入 32 位代码段[SECTION .s32]
进行屏幕显示操作.
调试代码 a:
将程序编译为. com 文件, 使用 dos 运行.(因为引导扇区只有 512 字节, 程序高于 512 字节就不方便了)
代码 a 有 dword 前缀调试:
(1)准备 freedocs.img
(2)bximage 生成 pm.img
(3)修改 bochs
重点是
- floppya: 1_44=freedos.img, status=inserted
- floppyb: 1_44=pm.img, status=inserted
- boot: a
(1)用 bochs 格式化 B 盘
Sudo bochs
在 dos format b:
(5)修改 pmtest1,org 改为 0100h, 并编译为 pmtest1.com
(6)将 pmtest1.com 复制到 pm.img
sudo mount -o loop pm.img /mnt/floppy
会出现了错误
mount point /mnt/floppy does not exist
先创建文件夹
然后
sudo losetup /dev/loop0 pm.img 创建 loop 设备, 然后操作 loop 设备, 就是对 pm.img 数据的操作了
sudo mount /dev/loop0/ /mnt/floppy loop 设备挂载到 / mnt/floppy 上
然后
sudo cp pmtest1.com /mnt/floppy/ 赋值
然后卸载
sudo umount /mnt/floppy/
之后再做一次遇到问题
解决, 卸载
另外发现了 sudo cp pmtest2.com /mnt/floppy/ 赋值并不是覆盖. 也就是说 cp 了先 cp 了 pmtest1.com, 然后不格式化(format b:), 直接 cp pmtest2.com, 那么两个程序都可以运行.
(7)在 dos 下运行 pmtest1.com
Sudo bochs
B:\pmtest1.com 运行
可见右侧出现一个红色的 P
代码 a 无 dword 前缀调试:
(1)修改 pmtest1.asm, 删掉第 71 行的 dword, 存为 pmtestd.asm, 并编译为 pmtestd.com
(2)在 dos 运行
陷入循环并且无红色的 P 在屏幕右侧
失败原因:
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, //selector16 位, dword 两字节, 高位 selector, 低位偏移 0.(因为声明了这段是 16 位代码, 所以一个字两字节)
; 并跳转到 Code32Selector:0 处
删除 dword 后只有 16 位. cs 寄存器没有正确设置, 没有跳转到 32 位代码段, 故显示失败
3. 调试代码,/b/, 掌握 GDT 的构造与切换, 从保护模式切换回实
模式方法
分析: pmtest2.asm
在前面程序的基础上, 新建一个段, 这个段以 5MB 为基址, 远远超出实模式下 1MB 的界限. 我们
先读出开始处 8 字节的内容, 然后写入一个字符串, 再从中读出 8 字节. 如果读写成功的话, 两次读出的内容应该是不同的, 而且第
二次读出的内容应该是我们写进的字符串. 字符串是保存在数据段中的, 也是新增加的.
(1)LABEL_DESC_STACK: Descriptor 为全局堆栈段 [SECTION .gs] 的 descriptor, 初始化在 [SECTION .gs] 和[SECTION.16]完成. Descriptor 属性为 DA_DRWA+DA_32,DA_32 表明是 32 位堆栈段.
(2)LABEL_DESC_DATA:Descriptor 为[SECTION .data1] ; 数据段的 descriptor, 初始化在[SECTION .data1] 完成, 其中包含了要写入的字符串
(3)LABEL_DESC_CODE32: Descriptor 为 32 位代码段(保护模式)[SECTION .s32]. 由实模式跳入.
在 [SECTION .s32] 中我们改变了 ss 和 esp(代码 3.5 第 174 行到 177 行), 这样, 在 32 位代码段中所有的堆栈操作将会在新增的 堆栈段中进行.
这个段的开头初始化了 ds,es 和 gs, 让 ds 指向新增的数据段, es 指向新增的 5MB 内存 的段, gs 指向显存(第 167 行到第 172 行). 接着显示一行字符串, 之后就开始读写大地址内存了(第 198 行到第 200 行). 由于要读 两次相同的内存, 我们把读的过程写进一个函数 TestRead, 写内存的内容也写进函数 TestWrite, 这两个函数的入口分别在第 206 行 和第 222 行. 可以看到, 在 TestRead 中还调用了 DispAL 和 DispReturn 这两个函数(第 253 行和第 286 行),DispAL 将 al 中的字节用十 六进制数形式显示出来, 字的前景色仍然是红色; DispReturn 模拟一个回车的显示, 实际上是让下一个字符显示在下一行的开头 处. 要注意的一个细节是, 在程序的整个执行过程中, edi 始终指向要显示的下一个字符的位置. 所以, 如果程序中除显示字符外 还用到 edi, 需要事先保存它的值, 以免在显示时产生混乱.
(4)保护模式中字符串寻址: 在 TestWrite 中用到一个常量 OffsetStrTest, 它的定义在代码 3.4 第 47 行. 注意, 我们用到这个字符串的时候并没有用直接标 号 StrTest, 而是又定义了一个符号 OffsetStrTest, 它等于 StrTest-$$.$$ 的含义代表当前 节 (section) 开始处的地址. 所以 StrTest-$$ 表示字符串 StrTest 相对于本节的开始处 (即 LABEL_DATA 处) 的偏移. 容易发现数据段的基址便是 LABEL_DATA 的物理地址. 于是 OffsetStrTest 既是字符串相对 LABEL_DATA 的偏移, 也是其在数据段中的偏移. 我们在保护模式下需要用到的正是这个偏移, 而不再是实模式下的地址. 前文中提到过的 section 的一点妙用指 的便是这里的 $$, 它不是没有替代品, 而是这样做思路会比较清晰. OffsetPMMessage 的情形与此类似.
(6)返回实模式
概述:
先回忆开中断: 加载寄存器, 之后关中断. 之后打开 A20 地址线. 修改 cr0 寄存器: PE 位置 1. 此时 cs 的值仍然是实模式下的值, 把代码段的选择子装入 cs(修改段界限, 段属性.)
关中断差不多就是完成上述的逆向操作:
加载一个合适的描述符选择子到有关段寄存器, 以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性, 重新设置各个段寄存器的值, 比如 cr0PE 位置 0. 恢复 sp(堆栈指针寄存器)的值, 修改段界限, 段属性, 然后关闭 A20, 打开中断, 重新回到原来的样子.
(将段寄存器段界限段属性由符合保护模式要求到符合实模式要求)
为了能从保护模式恢复实模式的寄存器, 需要先保存到系统自己的堆栈段. 在 [SECTION.16] 中完成.
- mov sp, 0100h
- ...
然后 32 位代码段的操作在自定义的堆栈段 [SECTION .STACK] 完成. 二者互不干扰, 方便了恢复.
详述:
从实模式进入保护模式时直接用一个跳转就可以了, 但是返回的时候却稍稍复杂一些. 因为在准备结束保护模式回到实模 式之前, 需要加载一个合适的描述符选择子到有关段寄存器, 以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性. 而 且, 我们不能从 32 位代码段返回实模式, 只能从 16 位代码段中返回. 这是因为无法实现从 32 位代码段返回时 cs 高速缓冲寄存器中的 属性符合实模式的要求(实模式不能改变段属性).
所以, 在这里, 我们新增一个 Normal 描述符(代码 3.4 第 15 行). 在返回实模式之前把对应选择子 SelectorNormal 加载到 ds, es 和 ss, 就是上面所说的这个原因.
LABEL_DESC_NORMAL: Descriptor 对应选择子 SelectorNormal. 对应段 [SECTION .s16code],16 位代码段. 由 32 位代码段跳入, 跳出后到实模式.
这个段是由 [SECTION .s32] 中的 jmp SelectorCode16:0 跳进来的. 开头的语句把 SelectorNormal 赋给 ds,es,fs,gs 和 ss, 完成我们刚刚提到的使命. 然后就清 cr0 的 PE 位, 接下来的跳转看上去好像不太对, 因 为段地址是 0. 其实这里只是暂时这样写罢了, 在程序的一开始处可以看到代码 3.8 中的这几句.
- 67 movax, cs
- ...
- 73 mov[LABEL_GO_BACK_TO_REAL+3], ax
mov [LABEL_GO_BACK_TO_REAL+3], ax 的作用就是为回到实模式的这个跳转指令指定正确的段地址, 这条指令的机器码如图 3.9 所示.
图 3.9 告诉我们, LABEL_GO_BACK_TO_REAL+3 恰好就是 Segment 的地址, 而第 73 行执行之前 ax 的值已经是实模式下的 cs(我们记 做 cs_real_mode)了, 所以它将把 cs 保存到 Segment 的位置, 等到 jmp 指令执行时, 它已经不再是:
jmp0:LABEL_REAL_ENTRY
而变成了:
jmpcs_real_mode:LABEL_REAL_ENTRY
它将跳转到标号 LABEL_REAL_ENTRY 处.
在跳回实模式之后, 程序重新设置各个段寄存器的值, 恢复 sp 的值, 然后关闭 A20, 打开中断, 重新回到原来的样子
144 LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
...
159 int21h ; / 回到 DOS
调试:
编译 pmtest2.asm 为 pmtest2.com
在 bochs dos 下运行
第一行为开始内存 5MB 处全是零. 然后写入了 41,42,...48, 也就是 16 进制的 A,B,C,D...H, 在代码 pmtest2.asm 中 DATA 段的写入的 str.
同时看到, 程序执行结束后不再像上一个程序那样进入死循环, 而是重新出现了 DOS 提示符. 这说明我们重新回到了实模式下
的 DOS.
4. 调试代码,/c/, 掌握 LDT 切换
分析:
LDT 与 GDT 都是描述符 table,L 代表 Local, 局部. 简单来说, LDT 是一种描述符表, 与 GDT 差不多, 只不过它的选择子的 TI 位必 须置为 1. 在运用它时, 需要先用 lldt 指令加载 ldtr,lldt 的操作数 selector 是 GDT 中用来描述 LDT 的描述符.(也就是说 LDT 相当于 GDT 中描述的一个段, 对应有特殊的寄存器 ldtr, 而该段中又有一些描述符描述一些 LDT 段, 只属于这个 LDT.)
pmtest3.asm 中增加了两个节 [SCTION .ldt][SECTION .la].(原来有 omtest2.asm 中的各个段). 其中[SCTION .ldt] 在 GDT 中有对应的 descriptor 和 selector LABEL_DESC_LDT: . 而 [SECTION .la] 是 LDT 描述的段, 在 GDT 无定义.
[SCTION .ldt]是增加的 LDT, 其中有一个 descriptor, 对应[SECTION .la].
[SECTION .la]中包含显示的字符 L, 在屏幕显示. 实现时调用了 GDT 中 的 SelectorVideo.
转换到 LDT 的过程: 先由实模式跳转到 GDT 中的 32 位代码段 [SECTION .s32](保护模式), 然后在[SECTION .s32] 中
- mov ax, SelectorLDT
- lldt ax
加载 ldtr(成为当前 LDTR),
然后. jmp SelectorLDTCodeA. 因为 SelectorLDTCodeA 的 TI 位为 1, 所以系统从当前 LDT 寻找相应描述符. 跳转到 LDT 中 descriptor 描述的段 [SECTION .la] 显示 L 后, 然后 jmp SelectorCode16:0, 跳回 GDT 中描述的 16 位代码段, 然后返回实模式. 其中 SelectorLDT 在 GDT 中定义, 指向 LDT 地址.
[SECTION .s32]第 217 行到第 220 行, 指令 lldt, 功能和 lgdt 也差不多, 负责加载 ldtr, 它的操作数是一个选择子, 这个选择子对应的就是用来描述 LDT 的那个描述符(标号 LABEL_DESC_LDT).
本例用到的 LDT 中只有一个描述符(标号 LABEL_LDT_DESC_CODEA 处), 这个描述符跟 GDT 中的描述符没什么分别. 选择子却不一样, 多出了一个属性 SA_TIL. 可以在 pm.inc 中找到它的定义:
SA_TIL EQU 4
由图 3.5 可知, SA_TIL 将选择子 SelectorLDTCodeA 的 TI 位置为 1. 实际上, 这一位便是区别 GDT 的选择子和 LDT 的选择子的关键所在. 如果 TI 被置位, 那么系统将从当前 LDT 中寻找相应描 述符. 也就是说, 当代码 3.10 中用到 SelectorLDTCodeA 时, 系统会从 LDT 中找到 LABEL_LDT_DESC_CODEA 描述符, 并跳转到相应的段中.
这个 LDT 很简单, 只有一个代码段. 我们还可以在其中增加更多的段, 比如数据段, 堆栈段等, 这样一来, 我们可以把一个单独的任务所用到的所有东西封装在一个 LDT 中.
通过几个简单的例子, 我们对 IA32 的分段机制大致已经有所了解了."保护模式" 中 "保护" 二字到底是什么含义? 在描述符中段基址和段界限定义了一个段的范围, 对超越段界限之外的地址的访问是被禁止的, 这无疑是对段的一种保护. 另外, 有点复杂的段属性作为对一个段各个方面的定义规定和限制了段的行为和性质, 从功能上来讲, 这仍然是一种保护.
调试:
编译 pmtest3.asm 为 pmtest3.com, 在 dos 运行
5. 调试代码,/d / 掌握一致代码段, 非一致代码段, 数据段的权限
访问规则, 掌握 CPL,DPL,RPL 之间关系, 以及段间切换的基
本方法
分析:
(1)特权级
在 IA32 的分段机制中, 特权级总共有 4 个特权级别, 从高到低分别是 0,1,2,3. 数字越小表示的特权级越大, 较为核心的代码和数据, 将被放在特权级较高的层级中. 处理器将用这样的机制来避免低特权级的任务在不被 允许的情况下访问位于高特权级的段. 如果处理器检测到一个访问请求是不合法的, 将会产生常规保护错误(#GP).
(2)CPL,DPL,RPL
CPL 是存寄存器如 CS 中,
RPL 是代码中根据不同段跳转而确定, 以动态刷新 CS 里的 CPL.
DPL 是在 GDT/LDT 描述符表中, 静态的.
一致代码段:
简单理解, 就是操作系统拿出来被共享的代码段, 可以被低特权级的用户直接调用访问的代码. 通常这些共享代码, 是 "不访问" 受保护的资源和某些类型异常处理. 比如一些数学计算函数库, 为纯粹的数学运算计算, 被作为一致代码段.
一致代码段的限制作用:
特权级高的程序不允许访问特权级低的数据: 核心态不允许调用用户态的数据.
特权级低的程序可以访问到特权级高的数据. 但是特权级不会改变: 用户态还是用户态.
非一致代码段:
为了避免低特权级的访问而被操作系统保护起来的系统代码.
非一致代码段的限制作用
只允许同级间访问.
绝对禁止不同级访问: 核心态不用用户态. 用户态也不使用核心态.
通常低特权代码必须通过 "门" 来实现对高特权代码的访问和调用. 不同级别代码段之间转移规则, 是通过 CPL/RPL/DPL 来校验. 先来理解这几个概念.
CPL(Current PrivilegeLevel)
CPL 是当前执行的程序或任务的特权级. 它被存储在 cs 和 ss 的第 0 位和第 1 位上. 在通常情况下, CPL 等于代码所在的段的 特权级. 当程序转移到不同特权级的代码段时, 处理器将改变 CPL.
在遇到一致代码段时, 情况稍稍有点特殊, 一致代码段可以被相同或者更低特权级的代码访问. 当处理器访问一个与 CPL 特权级不同的一致代码段时, CPL 不会被改变.
DPL(Descriptor Privilege Level)
DPL 表示段或者门的特权级. 它被存储在段描述符或者门描述符的 DPL 字段中, 正如我们先前所看到的那样. 当当前代码段试图访问一个段或者门时, DPL 将会和 CPL 以及段或门选择子的 RPL 相比较, 根据段或者门类型的不同, DPL 将会被区别 对待, 下面介绍一下各种类型的段或者门的情况.
数据段: DPL 规定了可以访问此段的最低特权级. 比如, 一个数据段的 DPL 是 1, 那么只有运行在 CPL 为 0 或者 1 的程序才有权访问它.
非一致代码段(不使用调用门的情况下):DPL 规定访问此段的特权级. 比如, 一个非一致代码段的特 权级为 0, 那么只有 CPL 为 0 的程序才可以访问它.
调用门: DPL 规定了当前执行的程序或任务可以访问此调用门的最低特权级(这与数据段的规则是一致的).
一致代码段和通过调用门访问的非一致代码段: DPL 规定了访问此段的最高特权级. 比如, 一个一致代 码段的 DPL 是 2, 那么 CPL 为 0 和 1 的程序将无法访问此段.
TSS:DPL 规定了可以访问此 TSS 的最低特权级 (这与数据段的规则是一致的).(TSS 全称 task state segment, 是在操作系统进程管理的过程中, 任务(进程) 切换时的任务现场信息.)
RPL(Requested PrivilegeLevel)
RPL 是通过段选择子的第 0 位和第 1 位表现出来的. 处理器通过检查 RPL 和 CPL 来确认一个访问请求是否合法. 即便提出访问请求的段有足够的特权级, 如果 RPL 不够也是不行的. 也就是说, 如果 RPL 的数字比 CPL 大(数字越大特权级越低), 那么 RPL 将会起决定性作用, 反之亦然.
操作系统过程往往用 RPL 来避免低特权级应用程序访问高特权级段内的数据. 当操作系统过程 (被调用过程) 从一个应用程序 (调用过程) 接收到一个选择子时, 将会把选择子的 RPL 设成调用者的特权级. 于是, 当操作系统用这个选择子 去访问相应的段时, 处理器将会用调用过程的特权级 (已经被存到 RPL 中), 而不是更高的操作系统过程的特权级(CPL) 进行特权检验. 这样, RPL 就保证了操作系统不会越俎代庖地代表一个程序去访问一个段, 除非这个程序本身是有权限的.
例子:
的数据段的选择子的 RPL 改为 3:
SelectorData equLABEL_DESC_DATA-LABEL_GDT+SA_RPL3
再运行一下, 发生了什么?
Bochs 重启了, 系统崩溃了, 在控制台你能看到这样的字样:
load_seg_reg(DS): RPL & CPL must be <= DPL
容易理解, 崩溃的原因在于我们违反了特权级的规则, 用 RPL=3 的选择子去访问 DPL=1 的段, 于是引起异常. 而我们又没有相应 的异常处理模块, 于是最为严重的情况就发生了.
(3)不同特权级代码段间转移
程序从一个代码段转移到另一个代码段之前, 目标代码段的选择子会被加载到 cs 中. 作为加载过程的一部分, 处理器将会检查描述符的界限, 类型, 特权级等内容. 如果检验成功, cs 将被加载, 程序控制将转移到新的代码段中, 从 eip 指示的位置开始执 行.
程序控制转移的发生, 可以是由指令 jmp,call,ret,sysenter,sysexit,int n 或 iret 引起的, 也可以由中断和异常机制 引起.
使用 jmp 或 call 指令可以实现下列 4 种转移:
1. 目标操作数包含目标代码段的段选择子.
2. 目标操作数指向一个包含目标代码段选择子的调用门描述符.
3. 目标操作数指向一个包含目标代码段选择子的 TSS.
4. 目标操作数指向一个任务门, 这个任务门指向一个包含目标代码段选择子的 TSS.
这 4 种方式可以看做是两大类, 一类是通过 jmp 和 call 的直接转移(上述第 1 种), 另一类是通过某个描述符的间接转移(上述 第 2,3,4 种). 下面就来分别看一下.
(4)通过 jmp 或 call 直接转移
如果目标是非一致代码段, 要求 CPL 必须等于目标段的
DPL, 同时要求 RPL 小于等于 DPL; 如果目标是一致代码段, 则要求 CPL 大于或者等于目标段的 DPL,RPL 此时不做检查. 当转移到一致
代码段中后, CPL 会被延续下来, 而不会变成目标代码段的 DPL. 也就是说, 通过 jmp 和 call 所能进行的代码段间转移是非常有限
的, 对于非一致代码段, 只能在相同特权级代码段之间转移. 遇到一致代码段也最多能从低到高, 而且 CPL 不会改变. 如果想自由
地进行不同特权级之间的转移, 显然需要其他几种方式, 即运用门描述符或者 TSS.
(5)基本的调用门进行段转移(先不涉及特权级转换, 用门特权级转换见 6./e/)
门: 门也是一种描述符, 门描述符的结构如图 3.13
可以看到, 门描述符和我们前面提到的描述符有很大不同, 它主要是定义了目标代码对应段的选择子, 入口地址的偏移和一些 属性等. 可是, 虽然这样的结构跟代码段以及数据段描述符大不相同, 我们仍然看到, 第 5 个字节 (BYTE5) 却是完全一致的, 都表 示属性. 在这个字节内, 各项内容的含义与前面提到的描述符也别无二致, 这显然是必要的, 以便识别描述符的类型. 在这里, S 位将是 0
直观来看, 一个门描述了由一个选择子和一个偏移所指定的线性地址, 程序正是通过这个地址进 行转移的. 门描述符分为 4 种:
调用门(Call gates)
中断门(Interrupt gates)
陷阱门(Trap gates)
任务门(Task gates)
其中, 中断门和陷阱门是特殊的调用门, 将会在后面提到, 我们先来介绍调用门. 在这个例子中, 我们用到调用门. 为简单起见, 先不涉及任何特权级变换, 而是先来关注它的工作方法.
在 pmtest3.asm 的基础上修改为 pmtest4.asm
增加一个代码段作为通过调用门转移的目标段
添加[SECTION .sdset]: 调用 selectvideo 在屏幕上显示 C. 因为打算用 call 指令调用将要建立的调用门, 所以, 在这段代码的结尾处调用了一个 retf 指令.
然后加入该段的 descriptor 以及 selector, 并初始化
然后添加调用门的 descriptor 以及 selector
使用宏 GATE(在 pm.inc 定义)初始化门的 descriptor
SelectorCodeDest 就是这个调用门要调用的段的 selector, 也就是我们刚刚在上面定义的段的 selector
然后就准备好了要被调用的段以及调用门
下面进行调用
Call 测试调用门后 retf, 相当于继续运行, 从 235 行开始继续.
调用门准备就绪, 它指向的位置是 SelectorCodeDest:0, 即标号 LABEL_SEG_CODE_DEST 处的代码
用一个 call 指令来使用这个调用门是个好主意 :
233 ; 测试调用门(无特权级变换), 将打印字母'C'
- 234 callSelectorCallGateTest:0
- ...
241 jmpSelectorLDTCodeA:0 ; 跳入局部任务, 将打印字母'L'.
这个 call 指令被放在进入局部任务之前, 由于我们新加的代码以指令 retf 结尾, 所以最终代码将会跳回 到 call 指令的下面继续执行. 所以, 我们最终看到的结果应该是在 pmtest3.exe 执行结果的基础上多出一个红色的字母 C.
其实调用门本质上只不过是个入口地址, 只是增加了若干的属性而已. 在我们的例子中所用到的调用门完全等同于一个地址, 我们甚至可以把使用调用门进行跳转的指令修改为跳转到调用门内指定的地址的指令:
callSelectorCodeDest:0
运行一下, 效果是完全相同的.(下面是更复杂的情况)
(6)使用调用门进行转移时特权级检验的规则.
假设我们想由代码 A 转移到代码 B, 运用一个调用门 G, 即调用门 G 中的目标选择子指向代码 B 的段. 实际上, 我们涉及了这么几个要素: CPL,RPL, 代码 B 的 DPL(记做 DPL_B), 调用门 G 的 DPL(记做 DPL_G). 根据 3.2.3.1 中提到的, A 访问 G 这个调用门时, 规则相当于访问一个数据段, 要求 CPL 和 RPL 都小于或者等于 DPL_G. 换句话说, CPL 和 RPL 需在更高的特权级上.
除了这一步要符合要求之外, 系统还将比较 CPL 和 DPL_B. 如果是一致代码段的话, 要求 DPL_B≤CPL; 如果是非一致代码段的话, call 指令和 jmp 指令又有所不同. 在用 call 指令时, 要求 DPL_B≤CPL; 在用 jmp 指令时, 只能是 DPL_B=CPL.
综上所述, 调用门使用时特权检验的规则如表所示.
也就是说, 通过调用门和 call 指令, 可以实现从低特权级到高特权级的转移, 无论目标代码段是一致的还是非一致的.
调试:
编译 pmtest4.asm 为 pmtest4.com, 在 dos 运行
在 pmtest3.asm 的基础上又多显示了 C. 是调用门调用的段的输出
6. 调试代码,/e / 掌握利用调用门进行特权级变换的转移
分析:
(1)跳转与堆栈
通过调用门和 call 指令, 可以实现从低特权级到高特权级的转移, 无论目标代码段是一致的还是非一致的. 那么如何进行高特权级向低特权级转换?
有特权级变换的转移的复杂之处, 不但在于严格的特权级检验, 还在于特权级变化的时候, 堆栈也要发生变化. 处理器的这种 机制避免了高特权级的过程由于栈空间不足而崩溃. 而且, 如果不同特权级共享同一个堆栈的话, 高特权级的程序可能因此受到有意或无意的干扰.
在我们的程序中, 指令 call DispReturn 和 call SelectorCodeDest:0 显然不同. 与在实模式下类似, 如果一个调用或跳转指 令是在段间而不是段内进行的, 那么我们称之为 "长" 的(Far jmp/call), 反之, 如果在段内则是 "短" 的(Near jmp/call). (与 Windows 不同)
那么长的和短的 jmp 或 call 有什么分别呢? 对于 jmp 而言, 仅仅是结果不同罢了, 短跳转对应段内, 而长跳转对应段间; 而 call 则稍微复杂一些, 因为 call 指令是会影响堆栈的, 长调用和短调用对堆栈的影响是不同的. 我们下面的讨论只考虑 32 位的情况.
对于短调用来说, call 指令执行时下一条指令的 eip 压栈, 到 ret 指令执行时, 这个 eip 会被从堆栈中弹出, 如图所示.
先从右向左压栈参数, 然后压栈下一条指令 eip,(从高地址到低地址压栈)eip 寄存器存储着我们 CPU 要读取指令的地址每次 CPU 执行都要先读取 eip 寄存器的值, 然后定位 eip 指向的内存地址. Esp 是当前堆栈的指针寄存器, 指向当前堆栈的底部位置.
可以看出, 调用者的 eip 被压栈, 而在此之前参数已经入栈. 图中的 " 调用者
eip" 对应 nop 指令地址. 而在函数 foo 调用最后一条指令 ret(带有参数)返回之前和之后, 堆栈的变化如图所示. 可见 esp 指向的内存中, 存放着 call 后下一条指令的地址(nop)
长调用的情况与此类似, 容易想到, 返回的时候跟调用的时候一样也是 "长" 转移, 所以返回的时候也需
要调用者的 cs, 于是 call 指令执行时被压栈的就不仅有 eip, 还应该有 cs, 如图所示.
带参数的 ret 指令执行前后的情形如图所示.
(2)通过调用门进行特权级转换
call 一个调用门也是长调用, 情况跟上面 所说的长调用差不多. 可是由于一些原因堆栈发生了切换, 也就是说, call 指令执行前后的堆栈已经 不再是同一个. 我们在堆栈 A 中压入参数和返回时地址, 等到需要使用它们的时候堆栈已经变成 B 了. Intel 提供了这样一种机制, 将堆栈 A 的诸多内容复制到堆栈 B 中, 如图所示.
事实上, 由于每一个任务最多都可能在 4 个特权级间转移, 所以, 每个任务实际上需要 4 个堆栈. 可 是, 我们只有一个 ss 和一个 esp, 那么当发生堆栈切换, 我们该从哪里获得其余堆栈的 ss 和 esp 呢? 这里涉及一样 TSS(Task-State Stack), 它是一个数据结构, 里面包含多个字段, 32 位 TSS 如图所示.
可以看出, TSS 包含很多个字段, 但是在这里, 我们只关注偏移 4 到偏移 27 的 3 个 ss 和 3 个 esp. 当发生堆栈切换时, 内层的 ss 和 esp 就是从这里取得的.
比如, 我们当前所在的是 ring3, 当转移至 ring1 时, 堆栈将被自动切换到由 ss1 和 esp1 指定的位置. 由于只是在由外层到内层 (低特权级到高特权级)切换时新堆栈才会从 TSS 中取得, 所以 TSS 中没有位于最外层的 ring3 的堆栈信息.
新堆栈的问题已经解决, 下面就是 CPU 在整个过程中所做的工作:
1. 根据目标代码段的 DPL(新的 CPL)从 TSS 中选择应该切换至哪个 ss 和 esp.
2. 从 TSS 中读取新的 ss 和 esp. 在这过程中如果发现 ss,esp 或者 TSS 界限错误都会导致无效 TSS 异常(#TS).
3. 对 ss 描述符进行检验, 如果发生错误, 同样产生 #TS 异常.
4. 暂时性地保存当前 ss 和 esp 的值.
5. 加载新的 ss 和 esp.
6. 将刚刚保存起来的 ss 和 esp 的值压入新栈.
7. 从调用者堆栈中将参数复制到被调用者堆栈 (新堆栈) 中, 复制参数的数目由调用门中 Param Count 一项来决定. 如果 Param Count 是零的话, 将不会复制参数.
8. 将当前的 cs 和 eip 压栈.
9. 加载调用门中指定的新的 cs 和 eip, 开始执行被调用者过程.
在第 7 步中, 解释了调用门中 Param Count 的作用, Param Count 只有 5 位, 也就是说, 最多只能复制 31 个参数. 如果参数多于 31 个该怎么办呢? 这时可以让其中的某个参数变成指向一 个数据结构的指针, 或者通过保存在新堆栈里的 ss 和 esp 来访问旧堆栈中的参数.
此刻结合 TSS 结构和上述步骤, 可以理解通过调用门进行由外层到内层调用的全过程. 那么, 正如 call 指令对 应 ret, 调用门也面临返回的问题. 通过长短 call 和 ret 的堆栈变化这两组对比, 我们发现, ret 基本上是 call 的反过程, 只
是带参数的 ret 指令会同时释放事先被压栈的参数.
实际上, ret 这个指令不仅可以实现短返回和长返回, 而且可以实现带有特权级变换的长返回. 由被调用者到调用者的返回过 程中, 处理器的工作包含以下步骤:
1. 检查保存的 cs 中的 RPL 以判断返回时是否要变换特权级.
2. 加载被调用者堆栈上的 cs 和 eip(此时会进行代码段描述符和选择子类型和特权级检验).
3. 如果 ret 指令含有参数, 则增加 esp 的值以跳过参数, 然后 esp 将指向被保存过的调用者 ss 和 esp. 注意, ret 的参数必须 对应调用门中的 Param Count 的值.
4. 加载 ss 和 esp, 切换到调用者堆栈, 被调用者的 ss 和 esp 被丢弃. 在这里将会进行 ss 描述符, esp 以及 ss 段描述符的检验.
5. 如果 ret 指令含有参数, 增加 esp 的值以跳过参数(此时已经在调用者堆栈中).
6. 检查 ds,es,fs,gs 的值, 如果其中哪一个寄存器指向的段的 DPL 小于 CPL(此规则不适用于一致代码段), 那么一个空描述符会被加载到该寄存器.
如图所示
综上所述, 使用调用门的过程实际上分为两个部分, 一部分是从低特权级到高特权级, 通过调用门和 call 指令来实现; 另一部
分则是从高特权级到低特权级, 通过 ret 指令来实现.
(3)进入 ring3
在 ret 指令执行前, 堆栈中应该已经准备好了目标代码段的 cs,eip, 以及 ss 和 esp, 另外, 还可能有参数. 这些可以是处理器压入栈的, 也可以由我们自己压栈. 在我们的例子中, 在 ret 前的堆栈如图 3.22 所示.
这样, 执行 ret 之后就可以转移到低特权级代码中了. 在 (pmtest4.asm) 基础上做一下修改(形成 pmtest5a.asm). 如上面的图 3.22 所示, 我们至少要添加一个 ring3 的代码段和一个 ring3 的堆栈段.
(4)pmtest5a.asm 由 ring0 到 ring3 转移
首先, 我们之前的代码都运行在 ring0!
添加一个 ring3 代码段[SECTION .ring3], 一个 ring3 堆栈段[SECTION .s3]
这个 ring3 代码段非常简单, 跟 [SECTION .la] 和[SECTION .sdest]的内容差不多, 同样是打印一个字符.
需要注意, 由于这段代码运行在 ring3, 而在其中由于要写显存而访问到了 VIDEO 段, 为了不会产生错误, 我们把 VIDEO 段的 DPL 修改为 3.
25 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW + DA_DPL3
第 392 行让程序不再继续执行. 392 jmp$
之所以这样做, 是为了先验证一下由 ring0 到 ring3 的转移是否成功. 如果屏幕上出 现红色的 3, 并且停住不动, 不再返回 DOS, 则说明转移成功.
新段对应的描述符 LABEL_DESC_CODE_RING3 的属性加上了 DA_DPL3, 让它的 DPL 变成了 3
相应选择子 SelectorCodeRing3 的 SA_RPL3 将 RPL 也设成了 3.
同时有堆栈段的 descriptor LABEL_DESC_STACK3 以及 selector SelectorStack3, 以及初始化, 在此略去.
这样, 代码段和堆栈段都已经准备好了. 让我们将 ss,esp,cs,eip 依次压栈, 并且执行 retf 指令.
- 266 pushSelectorStack3
- 267 pushTopOfStack3
- 107268 pushSelectorCodeRing3
- 269 push0
- 270 retf
此段代码放在显示完字符串 "In Protect Mode now." 后立即执行.
编译, 运行.
会看到了红色的 3 在 "In Protect Mode now." 下方显示. 在这表明我们由 ring0 到 ring3 的转移成功完成.
(5)pmtest5b.asm 在 ring3 中使用调用门
修改 pmtest4 中提到的调用门的 selectorSelectorCallGateTest 以及 descriptorLABEL_CALL_GATE_TEST: 的 DPL,RPL
然后修改 [SECTION .ring3] 代码, 在死循环前添加
callSelectorCallGateTest:0.
修改描述符和选择子是为了满足 CPL 和 RPL 都小于等于调用门 DPL 的条件.
编译运行
出现错误. 因为从低特权级到高特权级转移的时候, 需要用到 TSS.
(6)pmtest5c.asm 添加 TSS, 在 ring3 中使用调用门
因为从低特权级到高特权级转移的时候, 需要用到 TSS, 在 pmtest5c.asm 中准备一个 TSS
TSS 作为数据结构有其 descriptor LABEL_DESC_TSS,selector SelectorTSS 以及段[SECTION .TSS]. 定义及初始化见代码
可以看出, 除了 0 级堆栈之外, 其他各个字段我们都没做任何初始化. 因为在本例中, 我们只用到这一部分.
添加初始化 TSS 描述符的代码之后, TSS 就准备好了, 我们需要在特权级变换之前加载它
- callDispReturn
- movax, SelectorTSS
- ltrax
- pushSelectorStack3
- pushTopOfStack3
- pushSelectorCodeRing3
- push0
- retf
之后编译运行, 成功. 显示 call 调用门的 C 以及 ring3 段的 3.
(7)pmtest5.asm 返回实模式
到目前为止, 我们已经成功实现了两次从高特权级到低特权级以及一次从低特权级到高特权级的转移(ring0-ring3-ring-0-ring3,ring0 打印 "In protect mode", 然后到 ring3 打印 3, 然后 ring3callgate 到 ring0 打印 L, 然后返回 ring3), 最终在低特权级的代码[SECTION .ring3] 中让程序停住. 我们已经具备了在各种特权级下进行转移的能力, 并且熟悉了调用门这种典型门描述符的用法.
为了让我们的程序能够顺利地返回实模式, 我们将调用局部任务的代码加入到调用门的目标代码([SECTION .sdest]). 最后, 程序将由这里进入局部任务, 然后经由原路返回实模式.(ring3 打印 3, 调用门, 调用门打印 C, 调用局部任务 LDT 打印 L, 然后在局部任务 jmp SelectorCode16:0 返回 16 位代码段, 之后返回实模式)
346 [SECTION.sdest]; 调用门目标段
- 347 [BITS32]
- ...
- 359 movax, SelectorLDT
- 360 lldtax
- 361
362 jmpSelectorLDTCodeA:0 ; 跳入局部任务, 将打印字母'L'.
编译运行, 结果应为显示 in protect mode ,3,c,l, 然后返回实模式可以继续运行
调试:
编译为. com 文件运行
pmtest5a
pmtest5b
pmtest5c
pmtest5
7. 课后手动改:
(1)自定义添加 1 个 GDT 代码段, 1 个 LDT 代码段, GDT 段内要对一个内
存数据结构写入一段字符串, 然后 LDT 段内代码段功能为读取并打印该 GDT 的内容;
参考 pmtest3.com
修改[SECTION .data1], 修改字符串为 StrTest: db "JUST MONIKA", 0
修改[SECTION .s32]; 32 位代码段. 由实模式跳入.
改为如下, 相当于直接跳到 LDT 中的 descriptor
.........................
[SECTION .s32]; 32 位代码段. 由实模式跳入.
- [BITS 32]
- LABEL_SEG_CODE32:
- ; Load LDT
- mov ax, SelectorLDT
- lldt ax
jmp SelectorLDTCodeA:0 ; 跳入局部任务
- SegCode32Len equ $ - LABEL_SEG_CODE32
- ; END of [SECTION .s32]
- ........................
修改 LDT 中的段; CodeA (LDT, 32 位代码段)[SECTION .la]
功能改为显示 GDT 中 [SECTION .DATA] 段的字符串 StrTest
- ........................
- ; CodeA (LDT, 32 位代码段)
- [SECTION .la]
- ALIGN 32
- [BITS 32]
- LABEL_CODE_A:
- mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
; 下面显示一个字符串
mov ah, 0Ch ; 0000: 黑底 1100: 红字
- xor esi, esi
- xor edi, edi
mov esi, OffsetStrTest ; 源数据偏移
mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移. 屏幕第 10 行, 第 0 列.
- cld
- .1:
- lodsb
- test al, al
- jz .2
- mov [gs:edi], ax
- add edi, 2
- jmp .1
.2: ; 显示完毕
- jmp SelectorCode16:0
- CodeALen equ $ - LABEL_CODE_A
- ; END of [SECTION .la]
- .........................
然后编译运行. 运行时顺序为实模式跳转保护模式 [SECTION .s32], 然后[SECTION .s32] 加载 LDT 的 ldtr, 然后跳转 LDT 的 [SECTION .la] 段, 该段中先在屏幕显示 [SECTION .DATA] 段的字符串 StrTest, 然后跳回实模式
代码保存为 pmtestmy.asm, 编译为 pmtestmy.com.
编译运行
(2)自定义 2 个 GDT 代码段 A,B, 分属于不同特权级, 功能自定义, 要求实现 A-->B 的跳转, 以及 B-->A 的跳转.
参考 pmtest5, 实现了 ring0->ring3->ring0->ring3d 的跳转
二. 是书上内容的节选, 代码里有一点注释. 再翻翻书的保护模式那一章吧
x86 CPU 的基本模式: 实模式, 保护模式
- 实模式
• 地址总线宽度: 20bit
• 寄存器和数据总线宽度: 16bit
• 寻址空间是多少?
• 实模式: PA=Segment*16+Offset
- pmtest1.asm
- ; ==========================================
- ; pmtest1.asm
; 编译方法: nasm pmtest1.asm -o pmtest1.bin
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些说明
- org 07c00h
- jmp LABEL_BEGIN
[SECTION .gdt] ; 定义一个段, 段名 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 长度 :equ 相当于起个别名. S 为当前位置. s-LABEL_GDT, 就是当前位置减去. gdt 起始位置, 也就是. gdt 长度
GdtPtr dw GdtLen - 1 ; GDT 界限 .GdtPtr 也是个小的数据结构, 它有 6 字节, 前 2 字节是 GDT 的界限, 后 4 字节是 GDT 的基地址
dd 0 ; GDT 基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT ; 直观地看, 它好像是 DESC_VIDEO 这个描述符相对于 GDT 基址的偏移. 实际上有其数据结构, 其名选择子
- SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ;
- ; END of [SECTION .gdt]
- [SECTION .s16]
[BITS 16] ; 表明是 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, //selector16 位, dword 两字节, 高位 selector, 低位偏移 0.(因为声明了这段是 16 位代码, 所以一个字两字节)
; 并跳转到 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]
好了, 首先看 [SECTION .gdt] 这个段, 其中的 Descriptor 是在 pm.inc 中定义的宏(见代码 3.2). 先不要管具体的意义是什
么, 看字面我们可以知道, 这个宏表示的不是一段代码, 而是一个数据结构, 它的大小是 8 字节.
在段 [SECTION.gdt] 中并列有 3 个 Descriptor, 看上去是个结构数组, 你一定猜到了, 这个数组的名字叫做 GDT.
GdtLen 是 GDT 的长度, GdtPtr 也是个小的数据结构, 它有 6 字节, 前 2 字节是 GDT 的界限, 后 4 字节是 GDT 的基地址.
另外还定义了两个形如 SelectorXXXX 的常量, 至于是做什么用的, 我们暂且不管它.
再往下到了一个代码段,[BITS 16]明确地指明了它是一个 16 位代码段. 你会发现, 这段程序修改了一些 GDT 中的值, 然后执行
了一些不常见的指令, 最后通过 jmp 指令实现一个跳转(第 71 行). 正如代码注释中所说的, 这一句将 "真正进入保护模式". 实
际上, 它将跳转到第三个 section, 即 [SECTION .s32] 中, 这个段是 32 位的, 执行最后一小段代码. 这段代码看上去是往某个地址
处写入了 2 字节, 然后就进入了无限循环.
可以看到, 在屏幕中部右侧, 出现了一个红色的字母 "P", 然后再也不动了. 不难猜到, 程序的最后一部分代码中写入的两个字节是写进了显存中.
现在, 大致的感性认识已经有了, 但你一定有一些疑惑, 什么是 GDT? 那些看上去怪怪的指令到底在做什么? 现在我们先来总结一下, 在这个程序中, 我们了解到什么, 有哪些疑问.
我们了解到的内容如下:
程序定义了一个叫做 GDT 的数据结构.
后面的 16 位代码进行了一些与 GDT 有关的操作.
程序最后跳到 32 位代码中做了一点操作显存的工作.
我们不明就里的内容如下:
GDT 是什么? 它是干什么用的?
程序对 GDT 做了什么?
那个 jmp SelectorCode32:0 跟我们从前用过的 jmp 有什么不同?
在 IA32 下, CPU 有两种工作模式: 实模式和保护模式. 直观地看, 当我们打开自己的 PC, 开始时 CPU 是工作在实模式下的, 经过某种机制之后, 才进入保护模式. 在保护模式下, CPU 有着巨大的寻址能力, 并为强大的 32 位操作系统提供了更好的硬件保障.
我们先来回忆一下旧政策. Intel 8086 是 16 位的 CPU, 它有着 16 位的寄存器 (Register),16 位的数据总线(Data Bus) 以及 20 位的地址总线 (Address Bus) 和 1MB 的寻址能力. 一个地址是由段和偏移两部分组成的, 物理地址遵循这样的计算公式:
物理地址(Physical Address)= 段值(Segment)*16 + 偏移(Offset)
其中, 段值和偏移都是 16 位的.
从 80386 开始, Intel 家族的 CPU 进入 32 位时代. 80386 有 32 位地址线, 所以寻址空间可以达到 4GB. 所以, 单从寻址这方面说, 使用 16 位寄存器的方法已经不够用了. 这时候, 我们需要新的方法来提供更大的寻址能力. 当然, 慢慢地你能看到, 保护模式的优点不仅仅在这一个方面.
在实模式下, 16 位的寄存器需要用 "段: 偏移" 这种方法才能达到 1MB 的寻址能力, 如今我们有了 32 位寄存器, 一个寄存器就可以寻址 4GB 的空间, 是不是从此段值就被抛弃了呢? 实际上并没有, 新政策下的地址仍然用 "段: 偏移" 这样的形式来表示, 只不过保护模式下 "段" 的概念发生了根本性的变化.
实模式下, 段值还是可以看做是地址的一部分的, 段值为 XXXXh 表示以 XXXX0h 开始的一段内存.
而保护模式下, 虽然段值仍然由原来 16 位的 cs,ds 等寄存器表示, 但此时它仅仅变成了一个索引, 这个索引指向一个数据结构的一个表项, 表项中详细定义了段的起始地址, 界限, 属性等内容. 这个数据结构, 就是 GDT(实际上还可能是 LDT, 这个以后再介绍).GDT 中的表项也有一个专门的名字, 叫做描述符(Descriptor).
也就是说, GDT 的作用是用来提供段式存储机制, 这种机制是通过段寄存器和 GDT 中的描述符共同提供的. 为了全面地了解它,
我们来看一下图 3.4 所示的描述符的结构.
这个示意图表示的是代码段和数据段描述符, 此外, 描述符的种类还有系统段描述符和门描述符, 下文会有介绍.
除了 BYTE5 和 BTYE6 中的一堆属性看上去有点复杂以外, 其他三个部分倒还容易理解, 它们分别定义了一个段的基址和界限. 不过, 由于历史问题, 它们都被拆开存放.
至于那些属性, 我们暂时先不管它.
好了, 我们回头再来看看代码 3.1,Descriptor 这个宏用比较自动化的方法把段基址, 段界限和段属性安排在一个描述符中合适的位置, 有兴趣的读者可以研究这个宏的具体内容.
本例的 GDT 中共有 3 个描述符, 为方便起见, 在这里我们分别称它们为 DESC_DUMMY,DESC_CODE32 和 DESC_VIDEO.
其中 DESC_VIDEO 的段基址是 0B8000h, 顾名思义, 这个描述符指向的正是显存.
现在我们已经知道, GDT 中的每一个描述符定义一个段, 那么 cs,ds 等段寄存器是如何和这些段对应起来的呢? 你可能注意到了, 在 [SECTION.s32] 这个段中有两句代码是这样的(第 80 行和第 81 行):
- mov ax, SelectorVideo
- mov gs, ax
看上去, 段寄存器 gs 的值变成了 SelectorVideo, 我们在上文中可以看到, SelectorVideo 是这样定义的(第 25 行):
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT
直观地看, 它好像是 DESC_VIDEO 这个描述符相对于 GDT 基址的偏移. 实际上, 它有一个专门的名称, 叫做选择子(Selector), 它也不是一个偏移, 而是稍稍复杂一些, 它的结构如图 3.5 所示.
不难理解, 当 TI 和 RPL 都为零时, 选择子就变成了对应描述符相对于 GDT 基址的偏移, 就好像我们程序中那样.
看到这里, 读者肯定已经明白了第 86 行的意思, gs 值为 SelectorVideo, 它指示对应显存的描述符 DESC_VIDEO, 这条指令将把
ax 的值写入显存中偏移位 edi 的位置.
总之, 整个的寻址方式如图 3.6 所示.
注意图 3.6 中 "段: 偏移" 形式的逻辑地址 (Logical Address) 经过段机制转化成 "线性地址"(Linear Address), 而不是 "物理地址"(Physical Address),
其中的原因我们以后会提到. 在上面的程序中, 线性地址就是物理地址. 另外, 包含描述符的, 不仅可以是 GDT, 也可以是 LDT.
明白了这些, 离明白整个程序的距离已经只剩一层窗纸了. 因为只剩下 [SECTION .s16] 这一段还没有分析. 不过, 既然 [SECTION .s32] 是 32 位的程序, 并且在保护模式下执行, 那么 [SECTION .s16] 的任务一定是从实模式向保护模式跳转了. 下面我们就来看一下实模式是如何转换到保护模式的.
让我们到 [SECTION .s16] 这段, 先看一下初始化 32 位代码段描述符的这一段, 代码首先将 LABEL_SEG_CODE32 的物理地址(即
[SECTION .s32]这个段的物理地址)赋给 eax, 然后把它分成三部分赋给描述符 DESC_CODE32 中的相应位置. 由于 DESC_CODE32 的段
界限和属性已经指定, 所以至此, DESC_CODE32 的初始化全部完成.
接下来的动作把 GDT 的物理地址填充到了 GdtPtr 这个 6 字节的数据结构中, 然后执行了一条指令(第 55 行):
lgdt [GdtPtr]
这一句的作用是将 GdtPtr 指示的 6 字节加载到寄存器 gdtr,gdtr 的结构如图 3.7 所示.
pm.inc
; 描述符图示
; 图示一
;
; ------ ┏━━┳━━┓高地址
; ┃ 7 ┃ 段 ┃
; ┣━━┫ ┃
; 基
; 字节 7 ┆ ┆ ┆
; 址
; ┣━━┫ 2 ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 7 ┃ G ┃
; ┣━━──┨
; ┃ 6 ┃ D ┃
; ┣━━──┨
; ┃ 5 ┃ 0 ┃
; ┣━━──┨
; ┃ 4 ┃ AVL┃
; 字节 6 ┣━━──┨
; ┃ 3 ┃ ┃
; ┣━━┫ 段 ┃
; ┃ 2 ┃ 界 ┃
; ┣━━┫ 限 ┃
; ┃ 1 ┃ ┃
; ┣━━┫ 2 ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 7 ┃ P ┃
; ┣━━──┨
; ┃ 6 ┃ ┃
; ┣━━┫ DPL┃
; ┃ 5 ┃ ┃
; ┣━━──┨
; ┃ 4 ┃ S ┃
; 字节 5 ┣━━──┨
; ┃ 3 ┃ ┃
; ┣━━┫ T ┃
; ┃ 2 ┃ Y ┃
; ┣━━┫ P ┃
; ┃ 1 ┃ E ┃
; ┣━━┫ ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 23 ┃ ┃
; ┣━━┫ ┃
; ┃ 22 ┃ ┃
; ┣━━┫ 段 ┃
;
; 字节 ┆ ┆ 基 ┆
; 2, 3, 4
; ┣━━┫ 址 ┃
; ┃ 1 ┃ 1 ┃
; ┣━━┫ ┃
; ┃ 0 ┃ ┃
; ------ ┣━━╋━━┫
; ┃ 15 ┃ ┃
; ┣━━┫ ┃
; ┃ 14 ┃ ┃
; ┣━━┫ 段 ┃
;
; 字节 0,1┆ ┆ 界 ┆
;
; ┣━━┫ 限 ┃
; ┃ 1 ┃ 1 ┃
; ┣━━┫ ┃
; ┃ 0 ┃ ┃
; ------ ┗━━┻━━┛低地址
;
; 图示二
; 高地址................................................................................. 低地址
; | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
; |7654321076543210765432107654321076543210765432107654321076543210| <- 共 8 字节
; |--------========--------========--------========--------========|
; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
; ┃31..24┃ (见下图) ┃ 段基址(23..0) ┃ 段界限(15..0)┃
; ┃ ┃ ┃ ┃ ┃
; ┃ 基址 2┃3│2│ 1┃基址 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 ┃
; ┣━━┻━━┻━━┻━━╋━━━━━━━━━━━╋━━┻━━━━━┻━━┻━━━━━━━━━━━┫
; ┃ 3: 属性 2 ┃ 2: 段界限 2 ┃ 1: 属性 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 寄存器寻址的段 (通常是堆栈段) 的三种描述符中的意义各不相同.
; 1 在描述可执行段的描述符中, D 位决定了指令使用的地址及操作数所默认的大小.
; 1 D=1 表示默认情况下指令使用 32 位地址及 32 位或 8 位操作数, 这样的代码段也称为 32 位代码段;
; 2 D=0 表示默认情况下, 使用 16 位地址及 16 位或 8 位操作数, 这样的代码段也称为 16 位代码段, 它与 80286 兼容. 可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小.
; 2 在向下扩展数据段的描述符中, D 位决定段的上部边界.
; 1 D=1 表示段的上部界限为 4G;
; 2 D=0 表示段的上部界限为 64K, 这是为了与 80286 兼容.
; 3 在描述由 SS 寄存器寻址的段描述符中, D 位决定隐式的堆栈访问指令 (如 PUSH 和 POP 指令) 使用何种堆栈指针寄存器.
; 1 D=1 表示使用 32 位堆栈指针寄存器 ESP;
; 2 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 ;macro 定义宏. 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 字节
;
; 门
- ; usage: Gate Selector, Offset, DCount, Attr
- ; Selector: dw
- ; Offset: dd
- ; DCount: db
- ; Attr: db
- %macro Gate 4
dw (%2 & 0FFFFh) ; 偏移 1
dw %1 ; 选择子
dw (%3 & 1Fh) | ((%4 <<8) & 0FF00h) ; 属性
dw ((%2>> 16) & 0FFFFh) ; 偏移 2
%endmacro ; 共 8 字节
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
来源: http://www.bubuko.com/infodetail-3289694.html