3-段选择子与段描述符结构

看完了上一篇,我已经猜到了你一脸懵逼的表情。什么段寄存器,什么段选择子,什么段描述符,你这讲的都是啥啊!!!

0. 回顾

先来回顾一下,上一篇讲解了段寄存器,它是 CPU 中的一块存储数据的地方,共有 96 位。然后我们只能看得见其中的 16 位,剩下的 80 位是被隐藏起来的。本篇要讲的就是这 80 位的来源。

1. 段描述符和段选择子

我想 C 语言你应该是学了很久了,那么我来定义一个数组。

// 一个 QWORD 是一个 8 字节的整数
QWORD gdt[1024];

很明显,这是一个能容纳 1024 个元素的数组。现在我来定义:

  • gdt 数组中的每个元素都是一个段描述符
  • 数组的索引号是段选择子
  • 这个 gdt 数组被称为 gdt 表

只不过……,只不过这个段选择子,可能不会直接就表示成你想要的索引号,0就是0,5就是5,它稍微有些区别。

另外,段描述符,就是一个 8 字节的整数,可是这个整数,包含的信息量有点大。后面我们要做的,就是破译这个整数。

既然如此,后文自然是重点解析段选择子和段描述符,打通通往操作系统之路。

2. 段描述符与段选择子的结构

2.1 段选择子结构

段选择子就是一个数字,一共有16位,结构如下:

|   1   |     0    |  字节
|7654321076543 2 10|  比特
|-------------|-|--|  占位
|    INDEX    |T|R |  含义
|             |I|P |
|             | |L |
  • INDEX:在GDT数组或LDT数组的索引号
  • TI:Table Indicator,这个值为0表示查找GDT,1则查找LDT
  • RPL:请求特权级。以什么样的权限去访问段。

2.2 段描述符结构

|   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 |
  • BASE: 段基址,由上图中的两部分(BASE 31-24 和 BASE 23-0)组成
  • G:LIMIT的单位,该位 0 表示单位是字节,1表示单位是 4KB
  • D/B: 该位为 0 表示这是一个 16 位的段,1 表示这是一个 32 位段
  • AVL: 该位是用户位,可以被用户自由使用
  • LIMIT: 段的界限,单位由 G 位决定。数值上(经过单位换算后的值)等于段的长度(字节)- 1。
  • P: 段存在位,该位为 0 表示该段不存在,为 1 表示存在。
  • DPL:段权限
  • S: 该位为 1 表示这是一个数据段或者代码段。为 0 表示这是一个系统段(比如调用门,中断门等)
  • TYPE: 根据 S 位的结果,再次对段类型进行细分。

段描述符安装在 GDT 或者 LDT 数组中,可以在 WinDbg 中使用命令 r gdtr来查看 GDT 基址在哪里。

有关段描述符属性的具体细节,后文陆续给出。

Windows 操作系统并没有使用 LDT 数组。所以后面查的表基本上是 GDT。

3. 实验——查看段描述符

  • 在 WinDbg 中调试 xp

3-段选择子与段描述符结构_第1张图片

图1 点这个按钮,中断到 WinDbg

3-段选择子与段描述符结构_第2张图片

图2 查看 GDT 数组

r gdtr 查看 GDT 表的基址,使用 dq 表示以 8 字节单元显示内存单元的数据。命令 r 表示显示寄存器的值,gdtr 也是 CPU 中的一个寄存器,它保存了 GDT 的基址。

4. 修改段寄存器

  • 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 中进行。

5. 如何把段描述符填充到段选择子

在第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
  1. 0x1B
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
  1. 0x23
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 了。

5.1 如何分析 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

  • limit 简算法

如果 G = 0,把段描述符中的 20 bit LIMIT取出来,比如 0x003ff,然后在前面补 0 至32bit,即 limit = 0x000003ff.
如果 G=1,把段描述符中的 20 bit LIMIT取出来,比如 0x003ff,然后在后面补 f 至 32bit, 即 LIMIT = 0x003fffff

6. 总结

本篇主要讲解了段寄存器 中的数据来源。上篇实验中,给出了几个实验,当时我们只是把另一个段寄存器中的数据读入到寄存器 ax(16bit),然后把 ax 代入到了 ds,可是 ax 明明只有 16 位啊,而 ds 有 96 位。

CPU必然在背后帮我们做了一些事情,它从 GDT 表中取出对应的段描述符,经过分析后自动的填写的了段寄存器中。这个过程,是需要大家深刻理解和掌握的。

你可能感兴趣的:(OS,学习笔记,OS修炼指南之保护模式)