x64 内核(三)IA-32e 分段研究

😄

1 中断

从中断源来看,中断分为两类:

  1. 外部(硬件)产生的中断。
  2. 软件产生的中断(使用 INT n 指令)。

1.1 外部中断

外部中断通过处理器上的引脚或本地 APIC 来接收。引脚分为两类:INTR、NMI。

  • 通过 INTR 引脚或 APIC 传递给处理器的外部中断为可屏蔽中断。

    • INTR 引脚:0~255 中断向量号。
    • 本地 APIC:16~255 中断向量号。
  • 通过 NMI 引脚产生的外部中断为不可屏蔽中断。

    • NMI 引脚:使用 2 中断向量号。
    • 软件中断 INT n 指令产生的,也是不可屏蔽中断。

不可屏蔽中断说明:

  1. 只要从 NMI 引脚产生的中断,都会使用 2 号中断向量指向的处理例程来进行处理。
  2. INTR 引脚发出的中断向量号为 2 的中断,虽然可以调用 NMI 中断处理例程,但并不是真正的 NMI 中断。

1.2 软件中断

在程序中,可以使用 INT n 指令来产生一个从软件内部产生的中断,叫做软件中断。n 的取值范围 0~255 。

特别说明:

  1. int 2 ,可以通过中断向量 2 来调用 NMI 中断处理例程,但是不会激活处理器的 NMI-handling hardware。
  2. 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
2
3
4
5
6
7
8
pushfd
bts dvord [esp], 8 ; eflags.TF = 1
popfd ;更新 eflags 寄存器
mov eax, 1 ;test 1
mov eax, 2 ;test 2
mov eax, 3 ;test 3
mov eax, 4 ;test 4
mov eax, 5 ;test 5

42.png

正常代码执行过程中,每次指令执行结束,处理器都会将 RF = 0。但是 iret 指令会将 RF = 1。

单步调试的过程如下:

  1. RF = 0,TF = 1。mov eax, 1 指令执行完之后产生指令断点触发 #DB 异常。CPU 将 eflags 压栈、将 TF = 0、PF = 0、NT = 0、VM = 0。
  2. iret 执行之后前,将栈中 eflags.RF = 1,iret 执行时将其弹出到 eflags 中。此时 RF = 1,TF = 1(TF=1是因为保存在堆栈的eflags.TF=1)。
  3. 由于 RF = 1,TF = 1。所以 mov eax, 2 会得到执行,该指令执行完后 CPU 将 RF = 0,然后重复上面这个过程。

参考《x86/x64体系探索及编程5.3》。

如果在指令边界(在指令执行之间)有多个事件挂起,则处理器会以可预测的顺序为它们提供服务。表 6-2 显示了事件源类之间的优先级。

43.png

44.png

这个也很有用,比如说 eflags.if = 0 时,并不会阻止中断和异常的生成(只是暂时屏蔽掉),新生成的中断和异常将被挂起,这些被挂起的事件(中断和异常事件)有优先级,优先级高的在IF = 1后优先被处理(挂起的事件并不是只使用一个队列)。

2 异常

处理器接收的 3 个异常来源:

  1. 程序执行产生的错误。
  2. 软件生成的异常(int n 指令)。
  3. 机器检查异常(Machine-check)。

异常分类:

1.png

中断/异常向量号 助记符 类型 描述 来源
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

2.png

3 中断门/陷阱门

IDT表用来将中断向量和中断/异常处理例程进行关联。IDT表有以下特征:

  1. 类似于 GDT 表,每个条目为 8 字节,但是 IDT 表中不包含调用门(中断门、任务门、陷阱门)。
  2. 调用门、中断门、陷阱门描述符中的段选择子都为代码段选择子。
  3. 不像 GDT 表,IDT 表的下标 0 的条目不是空的,它是对应于 INT 0 的。
  4. 中断向量为 0-256,所以 IDT 表中最多有 256 项。
  5. IDT 表的地址是随机的,可以使用 IDTR 寄存器来定位 IDT 表。
  6. IDTR 寄存器类似于 GDTR 寄存器。特权指令 LIDT 将一个数加载到 IDTR 寄存器(仅在R0使用),SIDT 指令读取 IDTR 寄存器的值(SIDT可以在3环使用)。
  7. 中断向量 INT n的 n 为 IDT 表的索引。

3.1 IA-32 中断门/陷阱门

中断描述符表

IA-32 中断描述符表包含以下三种门描述符(不包含调用门):

  • 中断门
  • 陷阱门
  • 任务门

中断门、陷阱门描述符与调用门描述符非常相似,它们都包含一个远指针(Selector:Offset),处理器使用该远指针找到代码段中的中断/异常处理例程。

45.png

门处理过程

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

INT_N转换为服务过程.png

堆栈切换

关于段的切换:

  1. CS0:来自于中断门/陷阱门描述符的 CS 段选择子。
  2. SS0、ESP0:来自于 TSS。

中断门/陷阱门类似于调用门,有同级别权限和不同级别的中断门/陷阱门,关于堆栈:

  1. 如果不发生权限切换:堆栈中压入 EFLAGS、CS、EIP、ErrrCode(注意只有8、17号需要压入)。
  2. 如果发生权限切换:R0 堆栈中压入:SS3、ESP3、EFLAGS3、CS3、EIP3、ErrrCode。
  3. 中断门/陷阱门没有参数,调用门有参数。

如果不发生权限切换,则不会将 SS、ESP 压入堆栈。

46.png

门返回

中断门/陷阱门的返回:

  • 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 中断门/陷阱门发生了很多变化:

  1. IA-32e 下系统段描述符(TSS、LDT)、门描述符都是 16 字节 128 位。
  2. 中断堆栈 push 都是 8 字节。
  3. 原 SS、RSP 被无条件压入堆栈(IA-32下发生权限切换才会压入SS3、ESP3)。
  4. 如果 CPL 发生改变,则将新的 SS = NULL。
  5. 引入 IST 表(Interrupt Stack Table)。

中断描述符表

同 IA-32 下一样,

  1. IDT 表的地址是随机的,可以使用 IDTR 寄存器来定位 IDT 表。
  2. 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 表在内存中的起始线性地址。

26.png

IA-32e 取消了任务门,中断描述符表包含以下三种两种门描述符(不包含调用门、任务门):

  • 中断门
  • 陷阱门

门描述符都是 16 字节 128 位,它们都包含一个远指针(Selector:Offset),处理器使用该远指针找到代码段中的中断/异常处理例程。远指针 Offset 为 64 位。

IA-32e 中断门/陷阱门描述符一样。

47.png

中断门引用的目标代码段必须是 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。如下图:

7.png

关于段、堆栈的切换:

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

48.png

中断门/陷阱门门返回使用 iretq 指令,该指令会强制从堆栈中弹出原先的 SS 和 RSP。

中断堆栈表(Interrupt Stack Table, IST)

在 Legacy 模式下, IDT 中的任务门会提供一个良好的堆栈环境,所以有的中断产生以后会使用任务门来找到对应的中断服务例程。

但是,IA-32e 模式不支持旧的任务切换机制,也不支持任务门。所以 IA-32e 模式下就提供一种新的堆栈切换机制,以达到使特定中断(NMl、双重故障、机器检查)对应的中断服务例程始终在已知良好堆栈上执行。这种机制就是中断堆栈表(Interrupt Stack Table, IST)。

  1. IST 位于 TSS 中,仅可在 IA-32e 模式下使用。
  2. 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(任务状态段):

49.png

注意查看 IST 域:

51.png

不难发现,大部分中断门描述符的 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
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
kd> !idt
Dumping IDT: fffff8024de97000

00: fffff8024ba19100 nt!KiDivideErrorFaultShadow
01: fffff8024ba19180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF8024DE9B9D0
02: fffff8024ba19240 nt!KiNmiInterruptShadow Stack = 0xFFFFF8024DE9B7D0
03: fffff8024ba192c0 nt!KiBreakpointTrapShadow
04: fffff8024ba19340 nt!KiOverflowTrapShadow
05: fffff8024ba193c0 nt!KiBoundFaultShadow
06: fffff8024ba19440 nt!KiInvalidOpcodeFaultShadow
07: fffff8024ba194c0 nt!KiNpxNotAvailableFaultShadow
08: fffff8024ba19540 nt!KiDoubleFaultAbortShadow Stack = 0xFFFFF8024DE9B3D0
09: fffff8024ba195c0 nt!KiNpxSegmentOverrunAbortShadow
0a: fffff8024ba19640 nt!KiInvalidTssFaultShadow
0b: fffff8024ba196c0 nt!KiSegmentNotPresentFaultShadow
0c: fffff8024ba19740 nt!KiStackFaultShadow
0d: fffff8024ba197c0 nt!KiGeneralProtectionFaultShadow
0e: fffff8024ba19840 nt!KiPageFaultShadow
10: fffff8024ba198c0 nt!KiFloatingErrorFaultShadow
11: fffff8024ba19940 nt!KiAlignmentFaultShadow
12: fffff8024ba199c0 nt!KiMcheckAbortShadow Stack = 0xFFFFF8024DE9B5D0
13: fffff8024ba19ac0 nt!KiXmmExceptionShadow
14: fffff8024ba19b40 nt!KiVirtualizationExceptionShadow
15: fffff8024ba19bc0 nt!KiControlProtectionFaultShadow
1f: fffff8024ba19c40 nt!KiApcInterruptShadow
20: fffff8024ba19cc0 nt!KiSwInterruptShadow
29: fffff8024ba19d40 nt!KiRaiseSecurityCheckFailureShadow
2c: fffff8024ba19dc0 nt!KiRaiseAssertionShadow
2d: fffff8024ba19e40 nt!KiDebugServiceTrapShadow
2f: fffff8024ba19f40 nt!KiDpcInterruptShadow
30: fffff8024ba19fc0 nt!KiHvInterruptShadow
31: fffff8024ba1a040 nt!KiVmbusInterrupt0Shadow
32: fffff8024ba1a0c0 nt!KiVmbusInterrupt1Shadow
33: fffff8024ba1a140 nt!KiVmbusInterrupt2Shadow
34: fffff8024ba1a1c0 nt!KiVmbusInterrupt3Shadow
35: fffff8024ba1a468 nt!HalpInterruptCmciService (KINTERRUPT fffff8024bcf9fc0)

3 TSS(IA-32e模式)

在 64 位模式下,不支持任务切换,也不存在任务门,但 TSS 描述符仍然存在。TSS 属于系统描述符,为 16 字节 128 位。

系统描述符(段描述符S = 0)分为两类:系统段描述符、门描述符。

  1. 系统段描述符:LDT、TSS。
  2. 门描述符:调用门、中断门、陷阱门。

64 位模式下 LDT、TSS 段描述符结构如下:

50.png

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 位。

22.png

关于 TR 寄存器:

  1. TR 可见的 16 位指向的 GDT 表中 TSS 段描述符(IA-32中将 TSS 段描述符加载到 TR 不可见部分)。
  2. TSS 段描述符的 Bsae 指向 TSS 段(TSS内存块结构)在线性地址空间的位置。
  3. IA-32e 下,TSS 的作用主要就是用来存储中断门/陷阱门堆栈切换时的 RSP,因为 IA-32e 不存在任务切换。

读写 TR 寄存器的指令:

  • LTR:特权指令。
  • STR:在任何权限下使用(CR4.UMIP=1,则只有在 CPL=0 时才能执行 STR)。
1
2
LTR AX;	// 将 AX 加载到 TR 可见的 16 位 
STR AX; // 将 TR 可见的 16 位保存到 AX 寄存器

使用 TR 寄存器找到 TSS 在内存中的位置:

  1. 读取 TR 及 GDT 表的地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    kd> 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 段描述符。

  2. 从 TSS 段描述符中提取 base,为 fffff802'4de98000,其为 TSS 内存块的起始线性地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    kd> 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 异常:

  1. 通过 JMP、CALL、INT n、INT3、INTO、INT1 、中断门、陷阱门进入 TSS段/任务门,会产生 #GP 异常。
  2. 使用 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(任务状态段):

49.png

4 SEMP、SEAP