x64 内核(三)IA-32e 分段研究
😄
1 中断
从中断源来看,中断分为两类:
- 外部(硬件)产生的中断。
- 软件产生的中断(使用
INT n指令)。
1.1 外部中断
外部中断通过处理器上的引脚或本地 APIC 来接收。引脚分为两类:INTR、NMI。
通过
INTR引脚或 APIC 传递给处理器的外部中断为可屏蔽中断。INTR引脚:0~255中断向量号。- 本地 APIC:
16~255中断向量号。
通过
NMI引脚产生的外部中断为不可屏蔽中断。NMI引脚:使用2中断向量号。- 软件中断
INT n指令产生的,也是不可屏蔽中断。
不可屏蔽中断说明:
- 只要从
NMI引脚产生的中断,都会使用2号中断向量指向的处理例程来进行处理。 INTR引脚发出的中断向量号为2的中断,虽然可以调用NMI中断处理例程,但并不是真正的 NMI 中断。
1.2 软件中断
在程序中,可以使用 INT n 指令来产生一个从软件内部产生的中断,叫做软件中断。n 的取值范围 0~255 。
特别说明:
int 2,可以通过中断向量2来调用NMI中断处理例程,但是不会激活处理器的 NMI-handling hardware。INT n指令产生的中断不会被eflags.IF位屏蔽。
注意:通过 INT n 指令,产生的是中断,它也用来在软件中模拟一个异常,这样就会使该中断和异常处理程序关联起来。
手册6.4.2提到,在架构上已经给很多异常进行了定义,如
0~31号,如果软件使用INT n指令中的n位于这个区间,产生中断时,并不会向堆栈中压入错误码Error。同样地6.8.1提到,如果通过INTR引脚来调用一个异常处理程序,也不会向堆栈中压入错误码Error。因为这些都是中断,不是真正意义上的异常。
1.3 中断的开启和屏蔽
屏蔽可屏蔽的中断
eflags.IF 标志位用来屏蔽可屏蔽的中断:
eflags.IF = 0,使用cli指令,屏蔽可屏蔽的中断。eflags.IF = 1,使用sti指令,响应中断。
屏蔽单步调试
当我们 eflags.TF = 1 ,时就是设置了指令断点,会产生调试异常。它与 eflags.RF 配合起来使用才能达到单步调试的效果。
- TF = 1,产生指令断点,触发调试异常。
- TF = 0,不产生指令断点。
- RF = 1,屏蔽指令断点产生调试异常。
- RF = 0,指令断点可产生调试异常。
1 | pushfd |

正常代码执行过程中,每次指令执行结束,处理器都会将 RF = 0。但是 iret 指令会将 RF = 1。
单步调试的过程如下:
- RF = 0,TF = 1。
mov eax, 1指令执行完之后产生指令断点触发 #DB 异常。CPU 将 eflags 压栈、将 TF = 0、PF = 0、NT = 0、VM = 0。 iret执行之后前,将栈中 eflags.RF = 1,iret执行时将其弹出到 eflags 中。此时 RF = 1,TF = 1(TF=1是因为保存在堆栈的eflags.TF=1)。- 由于 RF = 1,TF = 1。所以
mov eax, 2会得到执行,该指令执行完后 CPU 将 RF = 0,然后重复上面这个过程。
参考《x86/x64体系探索及编程5.3》。
如果在指令边界(在指令执行之间)有多个事件挂起,则处理器会以可预测的顺序为它们提供服务。表 6-2 显示了事件源类之间的优先级。


这个也很有用,比如说 eflags.if = 0 时,并不会阻止中断和异常的生成(只是暂时屏蔽掉),新生成的中断和异常将被挂起,这些被挂起的事件(中断和异常事件)有优先级,优先级高的在IF = 1后优先被处理(挂起的事件并不是只使用一个队列)。
2 异常
处理器接收的 3 个异常来源:
- 程序执行产生的错误。
- 软件生成的异常(int n 指令)。
- 机器检查异常(Machine-check)。
异常分类:

| 中断/异常向量号 | 助记符 | 类型 | 描述 | 来源 |
|---|---|---|---|---|
| 0 | #DE | 错误 | 除零错误 | DIV 和 IDIV指令 |
| 1 | #DB | 错误/陷阱 | 调试异常,用于软件调试(eflags.TF = 1单步调试) | 任何代码或数据引用 |
| 2 | / | 中断 | NMI中断 | 不可屏蔽的外部中断 |
| 3 | #BP | 陷阱 | 断点 | INT 3 指令 |
| 4 | #OF | 陷阱 | 溢出 | INTO 指令 |
| 5 | #BR | 错误 | 数组越界 | BOUND 指令 |
| 6 | #UD | 错误 | 无效指令(没有定义的指令) | UD2 指令(奔腾 Pro CPU 引入此指令)或任何保留的指令 |
| 7 | #NM | 错误 | 数学协处理器不存在或不可用 | 浮点或 WAIT/FWAIT 指令 |
| 8 | #DF | 中止 | 双重错误 (Double Fault) | 任何可能产生异常的指令、不可屏蔽中断或可屏蔽中断 |
| 9 | #MF | 错误 | 向协处理器传送操作数时检测到页错误(Page Faults)或段不存在,自从 486 把数学协处理器集成到 CPU 内部后,本异常便保留不用 | 浮点指令 |
| 10 | #TS | 错误 | 无效 TSS | 任务切换或访问 TSS |
| 11 | #NP | 错误 | 段不存在 | 加载段寄存器或访问系统段 |
| 12 | #SS | 错误 | 栈段错误 | 栈操作或加载 SS 寄存器 |
| 13 | #GP | 错误 | 通用保护(GP)异常,如果一个操作违反了保护模式下的规定,而且该情况不属于其他异常,则 CPU 便产生通用保护异常,很多时候也被翻译为一般保护异常 | 任何内存引用和保护性检查 |
| 14 | #PF | 错误 | 页错误 | 任何内存引用 |
| 15 | 保留 | / | / | / |
| 16 | #MF | 错误 | 浮点错误 | 浮点或 WAIT/FWAIT 指令 |
| 17 | #AC | 错误 | 对齐检查 | 对内存中数据的引用(486CPU 引入) |
| 18 | #MC | 中止 | 机器检查(Machine Check) | 错误代码和来源与型号有关(奔腾CPU 引入) |
| 19 | #XF | 错误 | SIMD 浮点异常 | SIMD 浮点指令(奔腾I CPU 引入) |
| 20~31 | 保留 | / | / | / |
| 32~255 | 用户定义中断 | 中断 | 可屏蔽中断 | 来自INTR的外部中断或 INT n指令 |
手册2A,int 3指令可为:0xcc/0xcd 3

3 中断门/陷阱门
IDT表用来将中断向量和中断/异常处理例程进行关联。IDT表有以下特征:
- 类似于 GDT 表,每个条目为 8 字节,但是 IDT 表中不包含调用门(中断门、任务门、陷阱门)。
- 调用门、中断门、陷阱门描述符中的段选择子都为代码段选择子。
- 不像 GDT 表,IDT 表的下标
0的条目不是空的,它是对应于INT 0的。 - 中断向量为
0-256,所以 IDT 表中最多有 256 项。 - IDT 表的地址是随机的,可以使用 IDTR 寄存器来定位 IDT 表。
- IDTR 寄存器类似于 GDTR 寄存器。特权指令
LIDT将一个数加载到 IDTR 寄存器(仅在R0使用),SIDT 指令读取 IDTR 寄存器的值(SIDT可以在3环使用)。 - 中断向量
INT n的n为 IDT 表的索引。
3.1 IA-32 中断门/陷阱门
中断描述符表
IA-32 中断描述符表包含以下三种门描述符(不包含调用门):
- 中断门
- 陷阱门
- 任务门
中断门、陷阱门描述符与调用门描述符非常相似,它们都包含一个远指针(Selector:Offset),处理器使用该远指针找到代码段中的中断/异常处理例程。

门处理过程
在软件中可以使用 INT n 指令来触发,然后过程如下。

堆栈切换
关于段的切换:
- CS0:来自于中断门/陷阱门描述符的 CS 段选择子。
- SS0、ESP0:来自于 TSS。
中断门/陷阱门类似于调用门,有同级别权限和不同级别的中断门/陷阱门,关于堆栈:
- 如果不发生权限切换:堆栈中压入 EFLAGS、CS、EIP、ErrrCode(注意只有8、17号需要压入)。
- 如果发生权限切换:R0 堆栈中压入:SS3、ESP3、EFLAGS3、CS3、EIP3、ErrrCode。
- 中断门/陷阱门没有参数,调用门有参数。
如果不发生权限切换,则不会将 SS、ESP 压入堆栈。

门返回
中断门/陷阱门的返回:
- 16位:
iret - 32位:
iretd - 64位:
iretq
调用门的返回:retf。
门权限检查
中断门/陷阱门是通过 INT n 触发的,不存在 RPL,所以不会对 RPL 进行权限检查。
中断门/陷阱门:CPL <= 门的DPL,代码段描述符 DPL = 0。
- 仅当使用
INT n、INT3或INTO指令产生异常或中断时,处理器才会检查中断或陷阱门的 DPL。 - 对于硬件产生的中断/处理器检测到的异常,处理器会忽略中断和陷阱门的 DPL。
注意:int3(0xCC) 和 int 3(0xCD 0x03) 在机器码上不同,后者需要进行门权限检查。
EFLAGS 的位
中断门/陷阱门下,处理器会先将 EFLAGS 存储在被调用程序堆栈中,然后将 TF、VM、RF、NT 位清 0。iret 时将堆栈的 eflags 弹出到 EFLAGS 寄存器。(清除 TF 标志可防止指令跟踪影响中断响应并确保没有单步异常将被传递。)
注意:中断门和陷阱门之间的唯一区别是:中断门会 EFLAGS.IF = 0 来屏蔽可屏蔽的中断。当通过中断门访问异常或中断处理程序时,处理器清除 IF 标志以防止其他中断干扰。
3.2 IA-32e 中断门/陷阱门
IA-32e 中断门/陷阱门发生了很多变化:
- IA-32e 下系统段描述符(TSS、LDT)、门描述符都是 16 字节 128 位。
- 中断堆栈
push都是 8 字节。 - 原 SS、RSP 被无条件压入堆栈(IA-32下发生权限切换才会压入SS3、ESP3)。
- 如果 CPL 发生改变,则将新的
SS = NULL。 - 引入 IST 表(Interrupt Stack Table)。
中断描述符表
同 IA-32 下一样,
- IDT 表的地址是随机的,可以使用 IDTR 寄存器来定位 IDT 表。
- IDTR 寄存器类似于 GDTR 寄存器。特权指令
LIDT将一个数加载到 IDTR 寄存器(仅在R0使用),SIDT 指令读取 IDTR 寄存器的值(SIDT可以在3环使用)。
IDTR 和 GDTR 扩展到 80 位:
注意:GDT、IDT 不被视为段,它只是用来描述段描述符。段描述符寄存器分为两部分:base、limit。(不是段寄存器的Base和Limit,注意区分段寄存器和段描述符寄存器)
- IA-32:GDTR、IDTR 寄存器为 48 位,低 16 位表明 GDT、IDTR 表的字节总数,高 32 位为 GDT、IDT 表在内存中的起始线性地址。
- IA-32e:GDTR、IDTR 寄存器为 80 位,低 16 位表明 GDT、IDTR 表的字节总数,高 64 位为 GDT、IDT 表在内存中的起始线性地址。

IA-32e 取消了任务门,中断描述符表包含以下三种两种门描述符(不包含调用门、任务门):
- 中断门
- 陷阱门
门描述符都是 16 字节 128 位,它们都包含一个远指针(Selector:Offset),处理器使用该远指针找到代码段中的中断/异常处理例程。远指针 Offset 为 64 位。
IA-32e 中断门/陷阱门描述符一样。

中断门引用的目标代码段必须是 64 位代码段 (CS.L = 1,CS.D = 0)。如果目标不是 64 位码段,则会生成一般保护异常 (#GP),并将 IDT 向量号报告为错误码。
- Type = 0xE
00007FF6 1A1310C0
0001 0 0 00
00000000`00007FF6
1A13EE00`001010C0
堆栈切换
注意:中断门中并不是必须要向堆栈中压入 ErrorCode,可以看到 8、17 号中断需要向新的堆栈中压入 ErrorCode = 0,其他中断无需预留该空间,栈顶为 RIP3。如下图:

关于段、堆栈的切换:
- CS0:来自于中断门/陷阱门描述符的 CS 段选择子。
- SS:在 IA-32e 下,不管权限是否发生切换,新的强制为 SS = NULL。
- RSP0:从 TSS 中获取。
- 堆栈切换时,新的堆栈必须 16 字节对齐。新的堆栈中会压入 SS3、RSP3、RFLAGS3、CS3、RIP3、ErrrCode。而且旧的 SS、RSP 是无条件压栈的,不管是否权限发生改变。(IA-32下仅权限发生改变时会压入旧的SS、ESP)
- IDT 门描述符的 IST 域决定着新的 RSP 取值(具体看下一节)。

中断门/陷阱门门返回使用 iretq 指令,该指令会强制从堆栈中弹出原先的 SS 和 RSP。
中断堆栈表(Interrupt Stack Table, IST)
在 Legacy 模式下, IDT 中的任务门会提供一个良好的堆栈环境,所以有的中断产生以后会使用任务门来找到对应的中断服务例程。
但是,IA-32e 模式不支持旧的任务切换机制,也不支持任务门。所以 IA-32e 模式下就提供一种新的堆栈切换机制,以达到使特定中断(NMl、双重故障、机器检查)对应的中断服务例程始终在已知良好堆栈上执行。这种机制就是中断堆栈表(Interrupt Stack Table, IST)。
- IST 位于 TSS 中,仅可在 IA-32e 模式下使用。
- IDT 表中 IST 域(bits-32,33,34)不为零的条目说明支持 IST 机制。IST 域有 3 bits,但是仅有 7 种值,IST = 0 保留。
查看 IDT 表的每个门描述符时;
IST = 0,堆栈切换时,新的RSP = RSP0。IST != 0,堆栈切换时,新的RSP = TSS.ISTn(n取值为1~7)。
IA-32e 模式下的 TSS(任务状态段):

注意查看 IST 域:

不难发现,大部分中断门描述符的 IST 域都是 0,只有少数是有值的。
上图的 int 1/2/8/18 都是有 IST 值的,跟 Intel 手册(6.14.5)说的一样。
- 1 号 #DB 用于调试异常,用于软件调试(eflags.TF = 1单步调试)。
- 2 号 NMI 中断。
- 8 号 #DF 双重错误 (Double Fault)。
- 18 号 #MC 机器检查(Machine Check)。
如下查看这几个中断号对应的处理函数
1 | kd> !idt |
3 TSS(IA-32e模式)
在 64 位模式下,不支持任务切换,也不存在任务门,但 TSS 描述符仍然存在。TSS 属于系统描述符,为 16 字节 128 位。
系统描述符(段描述符S = 0)分为两类:系统段描述符、门描述符。
- 系统段描述符:LDT、TSS。
- 门描述符:调用门、中断门、陷阱门。
64 位模式下 LDT、TSS 段描述符结构如下:

3.1 TR 寄存器
在 IA-32e 下 TR 寄存器和段寄存器 CS、DS、ES、FS、GS、SS 段寄存器一样都是 128 位。
段寄存器分为可见部分(段选择子)和隐藏部分:
- IA-32:共 96 位
- 可见部分:16 位
- 隐藏部分:80 位
- IA-32e:共 128 位
- 可见部分:16 位
- 隐藏部分:112 位
变化是 IA-32e 下段基址变为 64 位。

关于 TR 寄存器:
- TR 可见的 16 位指向的 GDT 表中 TSS 段描述符(IA-32中将 TSS 段描述符加载到 TR 不可见部分)。
- TSS 段描述符的
Bsae指向 TSS 段(TSS内存块结构)在线性地址空间的位置。 - IA-32e 下,TSS 的作用主要就是用来存储中断门/陷阱门堆栈切换时的 RSP,因为 IA-32e 不存在任务切换。
读写 TR 寄存器的指令:
LTR:特权指令。STR:在任何权限下使用(CR4.UMIP=1,则只有在 CPL=0 时才能执行 STR)。
1 | LTR AX; // 将 AX 加载到 TR 可见的 16 位 |
使用 TR 寄存器找到 TSS 在内存中的位置:
读取 TR 及 GDT 表的地址。
1
2
3
4
5
6
7
8
9
10kd> r tr
tr=0040 //01000 0 00,Index = 8
kd> r gdtr
gdtr=fffff8024de99fb0
kd> dq gdtr
fffff802`4de99fb0 00000000`00000000 00000000`00000000
fffff802`4de99fc0 00209b00`00000000 00409300`00000000
fffff802`4de99fd0 00cffb00`0000ffff 00cff300`0000ffff
fffff802`4de99fe0 0020fb00`00000000 00000000`00000000
fffff802`4de99ff0 4d008be9`80000067 00000000`fffff802得到段描述符为
4d008be9'80000067 00000000'fffff802,为 TSS 段描述符。从 TSS 段描述符中提取 base,为
fffff802'4de98000,其为 TSS 内存块的起始线性地址。1
2
3
4
5
6
7
8
9
10kd> dt _KTSS64 fffff802`4de98000
win32k!_KTSS64
+0x000 Reserved0 : 0
+0x004 Rsp0 : 0xfffff802`4de9b200
+0x00c Rsp1 : 0
+0x014 Rsp2 : 0
+0x01c Ist : [8] 0
+0x05c Reserved1 : 0
+0x064 Reserved2 : 0
+0x066 IoMapBase : 0x68
3.2 64-bit 模式任务管理
IA-32e 模式下不支持任务切换,更是取消了任务门(本来 32 位 Windows 也没使用任务管理,而是使用线程技术)。
在 64-bit 模式下,如果使用任务切换/任务门,会产生 #GP 异常:
- 通过
JMP、CALL、INT n、INT3、INTO、INT1、中断门、陷阱门进入 TSS段/任务门,会产生 #GP 异常。 - 使用
iret指令时如果eflags.NT = 1(任务嵌套)时,会产生 #GP 异常。
虽然 64-bit 模式下不支持硬件任务切换,但是仍然存在着 TSS(任务状态段),此时 TSS 里面存的信息和任务切换机制完全没有关系。
TSS 保存以下信息:
- RSP0,1,2:特权级别为 0,1,2 的 RSP 指针(
canonical forms表示0环高16位0xFFFF,3环高16位0x0000)。 - IST1~7:保存特殊中断下的 RSP。
- I/O 位图基址:是一个表示 I/O 权限的 16 位偏移量(The 16-bit offset to the I/O permission bit map from the 64-bit TSS base.)。
IA-32e 模式下的 TSS(任务状态段):
