# 前言

上一篇我们学习了调用门的原理和实验。本篇将介绍中断门与陷阱门,以及任务状态段(TSS)和任务门。

# 中断门与陷阱门

特性 中断门(Interrupt Gate) 陷阱门(Trap Gate)
功能 用于处理硬件中断和异步事件 用于处理软件异常和同步事件
IF 标志行为 自动清除 IF 标志,禁止进一步中断嵌套 不清除 IF 标志,允许中断嵌套
使用场景 处理硬件中断(如键盘、时钟) 处理系统调用或软件异常
中断响应 防止嵌套中断,确保中断处理代码不被打断 允许嵌套中断,提高系统灵活性
类型标识 Type = 0xE Type = 0xF

# 中断描述符表(IDT)

描述:中断描述符表(Interrupt Descriptor Table,IDT)与 GDT 类似,也是由一系列描述符组成的,每个描述符占 8 个字节。但要注意,IDT 表中的第一个元素不是 NULL

使用 WinDbg 查看 IDT 表的基址和大小:

IDT 表可以包含三种门描述符:

  1. 任务门描述符
  2. 中断门描述符
  3. 陷阱门描述符

# 中断门

中断门是用于处理硬件中断的机制,通过中断描述符表(IDT)定义。它允许 CPU 根据中断号跳转到对应的中断服务例程(ISR),并自动清除 IF 标志以屏蔽其他中断。中断门支持从低特权级切换到高特权级,保存执行上下文,保证中断处理安全可靠,常用于处理外部设备的中断请求。

中断门执行前后堆栈变化:

# 实验 1:构造一个中断门

一、初步构造门描述符

1
2
3
4
5
Offset in Segment 31:16 = 0x0000        ; 暂定
P = 1
DPL = 二进制:11
Segment Selector = 0x0008
Offset in Segment 15:00 = 0x0000 ; 暂定

由上述参数构造出的门描述符为: 0000EE00'00080000

二、确定 Offset in Segment

在 VC6 中执行以下代码并中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <windows.h>

DWORD dwH2GValue;

void __declspec(naked) GetH2GValue()
{
__asm
{
pushad
pushfd

mov eax, [0x8003f00c]
mov ebx, [eax] // 获取高 2G 地址的值
mov dwH2GValue, ebx

popfd
popad

iretd
}
}

void PrintH2GValue()
{
printf("%x \n", dwH2GValue);
}

int main(int argc, char* argv[])
{
__asm
{
int 0x20 // 中断门位置在 IDT[0x20]
}

PrintH2GValue();

getchar();
return 0;
}

右键进入反汇编窗口,查看 GetH2GValue 函数起始地址。

至此,门描述符最终确定为: 0040EE00'0008DE30

三、将门描述符写入 IDT 表

查看 IDT 表:

红框标注处描述符无效,将构造的描述符写入。

四、继续执行第二步的代码,成功读取高 2G 内存的值。

# 陷阱门

陷阱门是用于处理同步异常和系统调用的机制,通过中断描述符表(IDT)定义。触发陷阱时,CPU 跳转到指定的服务例程并保存上下文,但不清除 IF 标志,允许中断嵌套。陷阱门支持特权级切换,常用于处理异常(如除零、非法指令)和系统调用。

# 实验 2:构造一个陷阱门

一、初步构造门描述符

1
2
3
4
5
Offset in Segment 31:16 = 0x0000        ; 暂定
P = 1
DPL = 二进制:11
Segment Selector = 0x0008
Offset in Segment 15:00 = 0x0000 ; 暂定

由上述参数构造出的门描述符为: 0000EF00'00080000

注意:与中断门唯一的区别是 Type 域为 0xF (而非 0xE )。

二、三、四:按照实验 1 的对应步骤操作即可。

# 小结

IF 位是否会被清零是陷阱门与中断门唯一的区别

# 任务状态段(TSS)

在调用门、中断门与陷阱门中,一旦出现权限切换,就会有堆栈切换;由于 CS 的 CPL 发生改变,SS 也必须随之切换。

思考:堆栈切换时,新的 ESP 和 SS 从哪里来?

答案:从任务状态段(Task-state Segment,TSS)获取。

# TSS 的结构

TSS 是用于存储任务上下文的内存结构,大小固定为 104 字节。TSS 保存寄存器值、段选择子和各特权级的栈指针等信息,支持任务切换和从用户态到内核态的特权级转换。

CPU 通过 TR 段寄存器寻找 TSS。

# TSS 的作用

  1. Intel 的设计思想:通过使用 TSS 达到任务(线程)的切换。
  2. 操作系统的设计思想:Windows 并没有完全按照 Intel 的设计思想来做,甚至 Linux 也没有这样做。

# TSS 的本质

  1. 不要把 TSS 与 " 任务切换 " 联系到一起。
  2. TSS 的意义就在于可以同时换掉 "一堆" 寄存器

# TR 段寄存器

TR 段寄存器用于存储当前任务的 TSS 选择子,在任务切换时指向 TSS。

# TR 段寄存器的读写

TR 段寄存器的初始值由操作系统在启动时设置,通常指向操作系统为每个任务或线程创建的 TSS。

1
2
TR.Base  = TSS 起始地址
TR.Limit = TSS 大小

将 TSS 段描述符加载到 TR 寄存器:

指令: LTR

说明:

  1. LTR 指令装载时,仅改变 TR 寄存器的值(96 位),并没有真正改变 TSS。
  2. LTR 指令只能在系统层使用。
  3. 加载后 TSS 段描述符的状态位会发生改变。

读 TR 寄存器:

指令: STR

说明:只能读到 TR 的 16 位(即段选择子)。

# TSS 段描述符

TSS 段描述符是系统段描述符中的一种:

  • Type = 1001 :说明该 TSS 段描述符被加载到 TR 段寄存器中
  • Type = 1011 :说明该 TSS 段描述符被加载到 TR 段寄存器中

# TSS、TSS 段描述符、TR 段寄存器之间的关系

# 实验 1:加载自定义 TSS

一、获取必要参数

运行以下代码并在 main 函数头部设置断点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <windows.h>

DWORD dwOK;
DWORD dwESP;
DWORD dwCS;

void __declspec(naked) func()
{
dwOK = 1;
__asm
{
int 3

mov eax, esp
mov dwESP, eax
mov ax, cs
mov word ptr [dwCS], ax

// 回去的代码没写
}
}

int main(int argc, char* argv[])
{
char bu[0x10]; // 12ff70
int iCr3;

printf("input CR3:\n");
scanf("%x", &iCr3);

DWORD iTss[0x1A] = { // TSS 共 0x68 字节 = 26 个 DWORD
0x00000000, // link
0x00000000, // esp0
0x00000000, // ss0
0x00000000, // esp1
0x00000000, // ss1
0x00000000, // esp2
0x00000000, // ss2
(DWORD)iCr3, // cr3
0x0040DE30, // eip
0x00000000, // eflags
0x00000000, // eax
0x00000000, // ecx
0x00000000, // edx
0x00000000, // ebx
(DWORD)bu, // esp
0x00000000, // ebp
0x00000000, // esi
0x00000000, // edi
0x00000023, // es
0x00000008, // cs (0x0000001B)
0x00000010, // ss (0x00000023)
0x00000023, // ds
0x00000030, // fs
0x00000000, // gs
0x00000000, // ldt
0x20ac0000
};

char buff[6];
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0xC0;

__asm
{
call fword ptr[buff]
}

printf("ok=%d \t ESP=%x \t CS=%x \n", dwOK, dwESP, dwCS);

return 0;
}

二、通过反汇编窗口查看 func 函数起始地址。

三、修改代码

将地址填入 iTss 数组注释为 eip 的位置,表示 TSS 切换后 EIP 的值。

注意:代码如有修改,需要先停止程序,重新编译。

四、查看 iTss 数组所在地址,记下来备用。

五、构造 TSS 段描述符

1
2
3
4
5
6
7
             G = 0
AVL = 0
Limit = 二进制:0000
P = 1
DPL = 二进制:11
Type = 二进制:1001
Segment Limit = 0068H ; Intel 规定 G=0 时 Limit 必须 ≥ 67H

由上述参数构造出的 TSS 段描述符为: 0000E900'00000068

iTss 数组地址为 0x12FDCC ,因此 TSS 段描述符最终为: 0000E912'FDCC0068

六、将 TSS 段描述符写入 GDT 表(地址选择 0x8003f0c0 )。

若写入其他地址,则需要修改 buff 数组的后两个字节。

先不要急着运行代码,先在 WinDbg 中输入 !process 0 0 获得当前进程的 Cr3

七、解除中断,输入上一步得到的 Cr3,回车。

WinDbg 成功获得了中断信号。

查看反汇编代码,可以确定正在执行 func 函数的代码。

寄存器也都符合预期的结果,说明 TSS 切换成功。

思考:TSS 切换完成后,如何回到切换前的下一行继续执行?

# 任务门

任务门是一种特殊的中断描述符,用于触发硬件任务切换。通过任务门调用时,CPU 自动保存当前任务的上下文到当前 TSS,然后加载目标 TSS 并切换到目标任务。

特性:

  1. 任务门存在于 IDT 表
  2. 任务门中包含 TSS 段选择子
  3. 可以通过访问任务门达到切换 TSS 的目的

任务门结构:

任务门执行过程:

  1. INT N(N 为 IDT 表索引号)
  2. 系统通过用户指定的索引查找 IDT 表,找到对应的门描述符
  3. 门描述符若为任务门描述符,则根据其中的 TSS 段选择子查找 GDT 表,找到 TSS 段描述符
  4. 将 TSS 段描述符中的内容加载到 TR 段寄存器
  5. TR 段寄存器通过 Base 和 Limit 找到 TSS
  6. 使用 TSS 中的值修改寄存器
  7. IRETD 返回

# 实验 2:通过任务门切换 TSS

一、构造任务门描述符

任务门描述符结构图灰色部分默认填充为 0。

1
2
3
                   P = 1
DPL = 二进制:11
TSS Segment Selector = 0x00C3 ; TSS 段描述符选择子

由上述参数构造出的门描述符为: 0000E500'00C30000

二、将任务门描述符写入 IDT 表

三、构造 TSS 段描述符

TSS 段描述符的构造过程参照实验 1,这里不再详解,只给出测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <windows.h>

DWORD dwOK;
DWORD dwESP;
DWORD dwCS;

void __declspec(naked) func()
{
dwOK = 1;
__asm
{
mov eax, esp
mov dwESP, eax
mov ax, cs
mov word ptr [dwCS], ax
iretd
}
}

int main(int argc, char* argv[])
{
char bu[0x10];
int iCr3;

printf("input CR3:\n");
scanf("%x", &iCr3);

DWORD iTss[0x1A] = { // TSS 共 0x68 字节 = 26 个 DWORD
0x00000000, // link
0x00000000, // esp0
0x00000000, // ss0
0x00000000, // esp1
0x00000000, // ss1
0x00000000, // esp2
0x00000000, // ss2
(DWORD)iCr3, // cr3
0x00401020, // eip
0x00000000, // eflags
0x00000000, // eax
0x00000000, // ecx
0x00000000, // edx
0x00000000, // ebx
(DWORD)bu, // esp
0x00000000, // ebp
0x00000000, // esi
0x00000000, // edi
0x00000023, // es
0x00000008, // cs (0x0000001B)
0x00000010, // ss (0x00000023)
0x00000023, // ds
0x00000030, // fs
0x00000000, // gs
0x00000000, // ldt
0x20ac0000
};

__asm
{
int 0x20
}

printf("ok=%d \t ESP=%x \t CS=%x \n", dwOK, dwESP, dwCS);
getchar();
return 0;
}

四、运行代码

需要输入 Cr3(获取流程参照实验 1)。

运行后,TSS 成功切换并返回。


上一篇:保护模式(三)| 调用门
下一篇:保护模式(五)| 10-10-12 分页与 PDE/PTE