看完了上一篇,我已经猜到了你一脸懵逼的表情。什么段寄存器,什么段选择子,什么段描述符,你这讲的都是啥啊!!!
先来回顾一下,上一篇讲解了段寄存器,它是 CPU 中的一块存储数据的地方,共有 96 位。然后我们只能看得见其中的 16 位,剩下的 80 位是被隐藏起来的。本篇要讲的就是这 80 位的来源。
我想 C 语言你应该是学了很久了,那么我来定义一个数组。
// 一个 QWORD 是一个 8 字节的整数
QWORD gdt[1024];
很明显,这是一个能容纳 1024 个元素的数组。现在我来定义:
- gdt 数组中的每个元素都是一个段描述符
- 数组的索引号是段选择子
- 这个 gdt 数组被称为 gdt 表
只不过……,只不过这个段选择子,可能不会直接就表示成你想要的索引号,0就是0,5就是5,它稍微有些区别。
另外,段描述符,就是一个 8 字节的整数,可是这个整数,包含的信息量有点大。后面我们要做的,就是破译这个整数。
既然如此,后文自然是重点解析段选择子和段描述符,打通通往操作系统之路。
段选择子就是一个数字,一共有16位,结构如下:
| 1 | 0 | 字节
|7654321076543 2 10| 比特
|-------------|-|--| 占位
| INDEX |T|R | 含义
| |I|P |
| | |L |
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 字节
|76543210|7 6 5 4 3210 |7 65 4 3210|76543210|76543210|76543210|76543210|76543210| 比特
|--------|-|-|-|-|---- |-|--|-|----|--------|--------|--------|--------|--------| 占位
| BASE |G|D|0|A|LIMIT|P|D |S|TYPE|<------- BASE 23-0 ------>|<-- LIMIT 15-0 ->| 含义
| 31-24 | |/| |V|19-16| |P |
|B| |L| | |L |
段描述符安装在 GDT 或者 LDT 数组中,可以在 WinDbg 中使用命令 r gdtr
来查看 GDT 基址在哪里。
有关段描述符属性的具体细节,后文陆续给出。
Windows 操作系统并没有使用 LDT 数组。所以后面查的表基本上是 GDT。
r gdtr
查看 GDT 表的基址,使用 dq
表示以 8 字节单元显示内存单元的数据。命令 r
表示显示寄存器的值,gdtr 也是 CPU 中的一个寄存器,它保存了 GDT 的基址。
mov
指令修改段寄存器例1:
mov ax, 0x20
mov ds, ax
例2:
mov ax, 0x10
mov ds, ax
上面这两个例子直接在 OD 中写会比较方便。具体操作方法是先用 OD 随意打开一个 exe 文件,然后双击第一行,就可以改代码了。执行的时候,选择单步执行,快捷键是 F8. 注意,是虚拟机里的 OD。以后的实验也是。如果你还不习惯 OD,上面的代码也可以放到 VC6.0 中,注意,使用
__asm {}
把汇编代码包围起来。
lds, les, lfs, lgs, lss
除了可以使用 mov
指令修改段寄存器,也可以使用les、lds
等指令修改段寄存器。
代码:
int main() {
char buffer[6] = {0};
__asm {
// 高 2 字节加载到 ES 寄存器,低 4 字节复制到 ecx 寄存器。fword 表示 6 字节。
// LDS/LSS/LFS/LGS 用法是类似的。没有LCS指令,要修改CS,需要使用其它指令,这里就不给出。
// 这行指令是有坑的,不一定可以执行成功,取决于buffer中的值。
les ecx, fword ptr ds:[buffer]
}
return 0;
}
上面的代码,可以在虚拟机的 VC 6.0 中进行。
在第1篇中讲到过,段寄存器一共有 96 位,其中 16 可见部分来源于段选择子的索引部分。剩下 80 位来源于 GDT 表。
下面来分析一下,如何把 0x1B 、0x23 这两个选择子对应的描述符填充到段寄存器。
做这个练习的时候,先不要问这些字段是什么含义,只要把这些字段的值查出来就行了。
原始数据:
|--地址--|-------------16进制值---------------|
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
0x1B = 0000 0000 0001 1011b
索引号:0000 0000 0001 1= 3 (查找gdt[3])
RPL: 11b = 3
TI: 0 (查找 GDT 表)
查找到的 GDT 描述符为:gdt[3] = 00cffb00`0000ffff
段寄存器结构:
selector = 0x001B
attribute = 0xcffb (G = 1 DB = 1 P = 1 DPL = 3 S = 1 TYPE = 1011(非一致代码段,可读已访问过))
base = 0x00000000
limit = 0xffffffff
0x23 = 0000 0000 0010 0011b
索引号:0000 0000 0010 0 = 4
TI: 0 (查找 GDT 表)
RPL: 11b = 3
查找到的 GDT 描述符为:gdt[4] = 00cff300`0000ffff
段寄存器结构:
selector = 0x23
attribute = 0xcff3 (G = 1 DB = 1 P = 1 DPL = 3 S = 1 TYPE = 0011(可读可写向上扩展的数据段))
base = 0x00000000
limit = 0xffffffff
这里最麻烦的应该是分析 limit 了。
limit 的含义是这个段的大小。实际上这么说的点不准确。limit 应该描述为,段大小再减去1字节。(这里的 limit 是换算后的 limit)。后面我用大写的 LIMIT 表示段描述符中的 20bit LIMIT。
如果粒度 G=0,LIMIT= 0x3ff
,这意味着该段的大小是 0x3ff+1=0x400
字节。如果 G=1
,那意味着该段的大小是(0x3ff+1)*4KB=0x400000
字节,所以换算后的 limit = 0x400000-1=0x003fffff
.
再举个例子。LIMIT=0xfffff, G=1
,则该段的大小是 (0xfffff+1)*4KB=0x100000*0x1000=0x100000000
字节,所以换算后的 limit=0x100000000-1=0xffffffff
如果 G = 0,把段描述符中的 20 bit LIMIT取出来,比如
0x003ff
,然后在前面补0
至32bit,即limit = 0x000003ff
.
如果 G=1,把段描述符中的 20 bit LIMIT取出来,比如0x003ff
,然后在后面补f
至 32bit, 即LIMIT = 0x003fffff
本篇主要讲解了段寄存器 中的数据来源。上篇实验中,给出了几个实验,当时我们只是把另一个段寄存器中的数据读入到寄存器 ax(16bit),然后把 ax 代入到了 ds,可是 ax 明明只有 16 位啊,而 ds 有 96 位。
CPU必然在背后帮我们做了一些事情,它从 GDT 表中取出对应的段描述符,经过分析后自动的填写的了段寄存器中。这个过程,是需要大家深刻理解和掌握的。