windows 保护模式

参考 英特尔开发手册
CPU 的模式
x86 CPU 的三个主要模式分别是实模式(Real Mode)、保护模式(Protected Mode)和虚拟8086模式(Virtual 8086 Mode)。它们各自的功能和特点如下:
实模式(Real Mode)
- 简介 :实模式是x86架构最早的工作模式,与8086 CPU的工作方式相同。
- 内存管理 :在实模式下,CPU可以访问1MB的内存。内存地址是通过段寄存器和偏移地址组合来形成的,总地址空间为20位。
- 特点 :
- 没有内存保护机制,即程序可以访问所有内存区域,包括操作系统内核和其他程序的数据。
- 没有多任务支持,所有程序运行在同一个地址空间中。
- 简单的地址计算:物理地址 = 段寄存器 * 16 + 偏移地址。
- 用途 :实模式主要用于早期的DOS操作系统和初始系统引导过程。
保护模式(Protected Mode)
- 简介 :保护模式是为了提供更高级的内存管理和多任务处理而设计的,是现代操作系统(如Windows、Linux)运行的基础模式。
- 内存管理 :保护模式下,CPU可以访问4GB的内存地址空间,使用32位地址。引入了分页(Paging)和分段(Segmentation)机制。
- 特点 :
- 支持内存保护,防止一个程序访问另一个程序的内存区域。
- 支持硬件级别的多任务处理,通过任务状态段(TSS)进行任务切换。
- 支持虚拟内存,通过分页机制,可以使用物理内存之外的存储空间。
- 提供特权级别(Privilege Levels),通常有四个环(Ring 0到Ring 3),用于控制访问权限和隔离内核与用户程序。
- 用途 :保护模式用于运行复杂的多任务操作系统,如Windows、Linux等。
虚拟8086模式(Virtual 8086 Mode)
- 简介 :虚拟8086模式是保护模式的一部分,允许在保护模式下运行实模式应用程序。它引入了一种机制,使得保护模式操作系统可以运行多个8086虚拟机。
- 内存管理 :每个虚拟8086模式下的任务都有自己独立的1MB地址空间,模拟实模式的内存管理。
- 特点 :
- 兼容性:允许在现代操作系统上运行旧的实模式程序。
- 隔离:每个虚拟8086任务被隔离在自己的内存空间内,受保护模式的内存保护机制保护。
- 性能:在保护模式下,通过硬件支持的虚拟化技术,可以高效地执行实模式代码。
- 用途 :虚拟8086模式用于在保护模式操作系统(如Windows NT系列)中运行DOS程序。
保护模式下的地址
在保护模式下,x86 CPU通过段式内存管理和分页机制来转换和管理逻辑地址、虚拟地址和物理地址。
三种地址的转换具体如下图所示:
逻辑地址(Logical Address)
- 组成 :逻辑地址由段选择子(Segment Selector)和段内偏移(Offset)组成。
- 格式 :
Logical Address = Segment Selector:Offset
- 作用 :逻辑地址是程序员在代码中使用的地址形式。
- 段选择子决定了使用哪个段描述符
- 偏移量则指示在该段内的具体位置。
虚拟地址(Virtual Address)
- 生成 :通过段选择子从段描述符表(GDT或LDT)中找到对应的段描述符,利用段描述符中的基址(Base Address)加上偏移量(即逻辑地址中的段内偏移)得到虚拟地址。
- 格式 :
Virtual Address = Base Address (from Segment Descriptor) + Offset
- 作用 :虚拟地址是CPU在段内的线性地址,它可以直接被分页单元使用以进一步转换为物理地址。
物理地址(Physical Address)
- 生成 :虚拟地址通过分页机制(如果启用)被转换成物理地址。分页机制使用页目录(Page Directory)和页表(Page Table)来完成这个转换。
- 作用 :物理地址是内存芯片上的实际地址,是最终用于存储器访问的地址。
段式内存管理
段式内存管理(Segmentation)是x86架构保护模式下的内存管理机制之一。它通过将内存划分为多个段,每个段都有独立的基址、大小和访问权限,以便对内存进行更加灵活和安全的管理。
不过在 32 位下由于访问范围为 4GB,因此通常设置基址为 0 。
段寄存器(Segment Registers)
段寄存器用于保存段选择子,指示当前正在使用的段。x86 CPU中有多个段寄存器,每个寄存器对应不同类型的内存段。
主要段寄存器
32 位 Windows(x86)段寄存器:
寄存器 | Ring 3 (用户态) | Ring 0 (内核态) | 说明 |
---|---|---|---|
CS | 0x1B | 0x08 | 代码段 |
DS | 0x23 | 0x10 | 数据段 |
SS | 0x23 | 0x10 | 栈段 |
ES | 0x23 | 0x10 | 额外数据段(一般等于 DS) |
FS | 0x3B | 0x30 | 用户态: TEB ,内核态: KPCR |
GS | 0x00 | 0x00 | 通常未使用 |
64 位 Windows(x64)段寄存器:
寄存器 | Ring 3 (用户态) | Ring 0 (内核态) | 说明 |
---|---|---|---|
CS | 0x33 | 0x10 | 代码段 |
DS | 0x00 | 0x00 | 被忽略,平坦段 |
SS | 0x00 | 0x00 | 被忽略,平坦段 |
ES | 0x00 | 0x00 | 被忽略,平坦段 |
FS | 0x00 (或用于 WOW64) | 0x00 | 兼容层用,64 位下一般不用 |
GS | 0x53 (或配置值) | 0x10 | 用户态: TEB ,内核态: KPCR |
段寄存器结构
在x86保护模式下,每个段寄存器有一个16位的可见部分(段选择子)和一个80位的隐含部分(包含段基址、界限和属性)。所以,段寄存器的总长度是96位,其中只有16位是可见的。当然不可见的部分其实对我们来说是透明的,因为这一部分在段寄存器中的属性是从段描述符中加载出来,以提高内存访问速度。
为了更准确地描述段寄存器,可以使用如下的结构体:
1 | struct SegmentRegister { |
Selector(段选择子) :段寄存器中唯一可见的部分,它包含段选择子的索引、TI(表指示位)和RPL(请求特权级别)。Selector用于在GDT或LDT中查找段描述符。
- 索引(Index) :段描述符在描述符表(GDT 或 LDT)中的索引。
- TI(Table Indicator) :指示使用全局描述符表(GDT)还是局部描述符表(LDT)。
- 0:使用 GDT。
- 1:使用 LDT。
- RPL(Requested Privilege Level) :请求的特权级别,用于权限检查。
Base(段基址) :段的起始地址,由段描述符中的 Base 字段组成。Base 字段在段描述符中分为三部分(低、中、高),合并后形成完整的 32 位基址。
Limit(段大小) :段的界限,由段描述符中的 Limit 字段组成。Limit 字段也分为低 16 位和高 4 位,高 4 位包含在段描述符的高字节中。Limit 通常表示段的大小。
Attributes(段属性) :段的属性和类型信息,包括段的类型(代码段、数据段)、DPL(描述符特权级别)、存在位(P)、扩展向下位(D/B)、可访问位(A)等。
段寄存器读写
数据段寄存器
数据段寄存器读写通常是通过寄存器完成的,例如 mov ds, ax
,mov ax, es
等。
另外 push
和 pop
这种栈操作指令同样可以读写数据段寄存器,例如 push fs
,pop fs
等,在开发中我们通常利用这个方法来保存段寄存器环境。
注意
ss
寄存器也能通过 mov
指令修改,但是 ss
的 RPL 要保持不变。因为 ss
的 RPL 和 cs
的 RPL 同为 CPL,段权限管理要求 cs
和 ss
的 RPL 要始终保持相同,而仅通过 mov
指令修改 ss
的 RPL 显然不符合这一规定。
代码段寄存器
cs
寄存器不能通过 mov
和 pop
指令修改(不过可以使用 push cs
和 mov ax, cs
这种指令来读取 cs
寄存器的值),因此我们必须通过跨段跳转指令来修改 cs
寄存器。 例如 jmp fword
和 call fword
指令:
1 | jmp far :offset |
然而在 MSVC(尤其是 32 位 x86 平台)中的内联汇编在语法上不支持这种直接立即数跳转:
1 | jmp far 0x1234:0x5678 |
因此我们需要使用 call fword ptr [mem]
指令来代替:
1 | struct FarPointer { |
对于 call fword ptr
和 call far
指令,如果 CPL 不变则会在向栈中依次压入调用者的 cs 和返回地址(CPL 变化的情况比较复杂,具体见调用门部分)。
因此我们返回时需要通过 retf
指令返回。如果调用前在栈上压入了参数,那么我们可以通过 retf [参数的字节总数]
来平衡堆栈。
为了避免编译器在函数代码中生成堆栈帧(push ebp
/mov ebp, esp
…),我们需要借助裸函数(naked function)精确控制函数的入口和返回指令。
1 | __declspec(naked) void func() { |
除此之外中断指令 int
以及对应的返回指令 iret
同样会修改 cs
寄存器,具体同样见后面的分析。
段描述符表(Descriptor Table)
段描述符表种类
段描述符表是存储段描述符的结构,定义了各个段的属性。x86 架构中有两种主要的段描述符表:全局描述符表(GDT)和局部描述符表(LDT)。
全局描述符表(GDT, Global Descriptor Table)
- 作用 :全局描述符表用于定义系统范围内的段,包括代码段、数据段和系统段(如任务状态段TSS)。
- 存储位置 :GDT存储在内存中的一个固定位置,其基址和界限由 GDTR(GDT Register)寄存器保存。
- 访问方式 :通过段选择子中的 TI 位(Table Indicator)为 0 来选择 GDT 。
- 相关汇编指令 :
lgdt [mem]
→ 加载 GDTsgdt [mem]
→ 存储 GDT(SGDT 可以在 Ring 3 执行,常用于探测系统)
关于 GDT 有如下常用的调试命令:
命令 | 功能说明 |
---|---|
r gdtr /r gdtl |
查看 GDT 基地址和长度 |
!gdt |
列出当前处理器的 GDT 表项 |
!gdt <n> |
查看某个段选择子的详细描述符 |
dg <selector> [count] |
解析段选择子 selector 开始的 count 个段描述符 |
~<n> |
切换到第 n 个处理器上下文 |
局部描述符表(LDT, Local Descriptor Table)
作用 :局部描述符表用于定义特定任务或进程的段,通常用于多任务环境中。
存储位置 :LDT 也存储在内存中的一个固定位置,其基址和界限由 LDTR(LDT Register)寄存器保存。
访问方式 :通过段选择子中的 TI 位为 1 来选择 LDT 。
相关汇编指令 :
lldt reg/mem
→ 加载 LDTRsldt reg/mem
→ 存储 LDTR(Ring 3 可执行)str reg/mem
→ 存储任务寄存器(可用于 TSS 调试)
关于 LDT 有如下常用的调试命令:
命令 | 功能说明 |
---|---|
r ldtr /r ldtl |
查看 LDT 基地址和长度 |
段描述符(Segment Descriptor)
段描述符是描述段属性的结构体,存储在GDT或LDT中。每个段描述符占用8个字节,包含段的基址、界限和属性等信息。
一个段描述符的结构如下:
基址(Base Address) :段的起始地址,由 8 位高位基址(
Base 31:24
)、8 位中位基址(Base 23:16
)和 16 位低位基址(Base Address 15:00
)组成,共32位。界限(Limit) :段的大小,由 16 位低位界限(
Limit 15:0
)和 4 位高位界限(Limit 19:16
)组成,共 20 位。P(Present) :段存在位,1 表示段存在。段描述符加载时,首先看 P 位是否为 1 。
DPL(Descriptor Privilege Level) :描述符的特权级别,共 2 位。
S(Descriptor Type),Type :
S
描述符类型,Type
根据S
的不同含义不同。S
为 0 表示系统段,此时Type
描述系统段的类型(如调用门,中断门,陷阱门等),含义如下表所示。S
为 1 表示代码段或数据段,此时Type
描述段的具体类型(如代码段、数据段)和访问权限,含义如下表所示。
另外
Type
对应代码段和数据段时分别引入了一个新概念:- 一致代码段(Conforming Code Segment)
- 当
Type
的 11 位为 1 时段描述符表示的是代码段,此时如果C
位(10 位)为 1 则表示为一致代码段。 - 一致代码段是指当 CPU 执行跨特权级的代码段切换(比如调用更高权限的代码段)时,不需要特权级检查,允许直接进入。
- CPU 不会在段切换时更改 CPL(因为一致代码段特权级不敏感)。
- 当
- 向下拓展数据段(Expand-Down Data Segment)
- 当
Type
的 11 位为 0 时段描述符表示的是数据段,此时如果E
位(10 位)为 1 则表示为向下拓展。 - 正常情况下的我们见到的数据段都是向上拓展的,即有效偏移范围是从
0
到Limit
。 - 对于向下拓展的数据段,有效偏移范围是从
Limit + 1
到最大偏移值(例如,0xFFFF
或0xFFFFFFFF
,取决于段的大小)。由于Base + Limit
会溢出,因此实际的有效范围是[Base, Base + Limit)
之外的范围。
- 当
G位(Granularity) :决定界限单位。
- 0 表示段寄存器的
Limit
元素单位为字节,即最大范围为 0x000FFFFF(1MB)。 - 1 表示段寄存器的
Limit
元素单位为 4KB ,即最大范围为 0xFFFFFFFF(4GB)。
- 0 表示段寄存器的
D/B 位(Default Operand Size / Big) :决定了默认的操作数和地址的大小,以及堆栈指针的大小。
注意
这里需要与
Type
位配合才能生效。Type
位决定了段描述符是代码段还是数据段,另外还决定了一些特性如向下拓展是否生效。- 对于代码段:
- 0 :默认操作数和地址大小为 16 位。例如
push
指令只能压 2 字节。 - 1 :默认操作数和地址大小为 32 位。
- 0 :默认操作数和地址大小为 16 位。例如
- 对于堆栈段:
- 0 :使用 16 位堆栈指针。
- 1 :使用 32 位堆栈指针。
- 对于数据段:
- 0 :向下拓展。
- 1 :向上拓展。
- 对于代码段:
L 位(64-bit code segment) :仅适用于 IA-32e 模式(64 位模式)的代码段。
- 0 :表示这是一个 16 位或 32 位代码段,具体取决于
D/B
位。 - 1 :表示这是一个 64 位代码段,忽略
D/B
位。
- 0 :表示这是一个 16 位或 32 位代码段,具体取决于
AVL 位(Available for use by system software) :供操作系统或其他系统软件使用,用于特定用途,例如标记段的状态,未被硬件使用。
段权限检测
权限类型
Ring Model 是一种 CPU 权限级别(Privilege Level) 的分层架构,主要用于操作系统中处理权限隔离和保护。它通过硬件机制,限制不同权限级别的代码对系统资源的访问,从而提高系统的安全性和稳定性。
Ring Model 的分层通常分为 4 个等级,编号从 0 到 3:Ring 0
, Ring 1
, Ring 2
, Ring 3
。
CPU 的权限等级主要有段机制来维护,具体来说段机制中有下面几个权限类型:
- CPL(Current Privilege Level) :表示当前程序的运行权限级别,由 CS 和 SS 的 RPL(低两位)决定,并且 CS 和 SS 的低两位一定相等(因为权限变化要求 CS 和 SS 同时修改,并且 CS 和 SS 必须变成相同是值)。
- RPL(Requested Privilege Level) :请求访问段时指定的权限级别。通常由访存时使用的段寄存器的段选择子决定。
- DPL(Descriptor Privilege Level) :段描述符中定义的段权限级别。
权限规则
这里仅考虑常见的非一致代码段。
权限检测规则
- 对于数据段:仅检测 DPL 与 CPL 之间的大小关系,当且仅当 DPL 在数值上大于等于 CPL(CPL ≤ DPL)时允许。例如当 CPL = DPL = 3 时,即使 RPL = 0 也允许访问。
- 对于堆栈段和代码段:当且仅当 CPL = RPL = DPL 时允许访问。因为堆栈段在数据段的基础上本身 RPL 就是 CPL,因此我们只需要知道这里比数据段严格,必须要求 CPL = DPL。
- 对于系统段:CPL = RPL ≤ 门描述符的 DPL
权限修改规则
cs
的权限一旦改变,ss
的权限也要随着改变,cs
与ss
的等级必须一样。int
,call far
这一类的指令只能通过系统段提权或者不改变权限。jmp far
这一类指令不改变权限,除了 TSS(可以同时影响到cs
和ss
)。retf
,iretd
这一类指令只能同级跳转或者降权。
调用门
在 x86 保护模式下,调用门(Call Gate)是一种特殊的系统段描述符,存在于 GDT(全局描述符表)或 LDT(局部描述符表) 中。它的作用是:允许安全地从一个代码段转移到另一个代码段,通常伴随特权级切换。
门描述符
- 偏移地址(Offset in Segment) :目标代码段中被调用过程的入口的逻辑地址,分为 16 位的低位偏移(
Offset 15:00
)和 16 位的高位偏移(Offset 31:16
),需要与段选择子指向的段描述符结合才能得到入口的线性地址。 - 段选择子(Segment Selector) :调用门要跳转到的目标代码段的选择子,用于在 GDT 或 LDT 中查找目标代码段的描述符。
- 参数计数(Param Count) :堆栈切换(权限改变)时需要从旧特权级堆栈复制到新特权级堆栈的参数个数,单位为字(16 位)或双字(32 位),具体取决于调用门的类型(位数)。
- 类型(Type) :4 位,表示调用门的类型。对于 32 位调用门,值为
1100
(0xC);对于 16 位调用门,值为0100
(0x4)。 - 系统段标志(S) :1 位,固定为 0,表示调用门是系统段,而不是普通的代码段或数据段。
- 描述符特权级(DPL) :2 位,表示访问调用门时所需的最低特权级,用于特权检查。
- 段存在标志(P) :1 位,表示调用门描述符是否存在。若为 0,访问该调用门会触发 #NP(Not Present)异常。
过程分析
调用过程
当我们通过 call/jmp far
或者 call/jmp fword ptr
指令调用调用门:
根据指令中的段选择子找到调用门。(指令中的地址没有用到,最终跳转到的地址由调用门的门描述符中的
Offset
字段决定)进行权限检查:
- 指令中的 RPL 不参与整个过程。
- CPL 在数值上小于等于调用门 DPL。
- 调用门中代码段选择子的 RPL 在数值上小于等于对应的代码段的的 DPL(貌似不用满足,并且跳过去之后 CPL 还是设置为代码段的 DPL)。
- 如果是
jmp
则调用门的段描述符对应的代码段的 DPL 等于 CPL。(jmp
通过调用门提不了权) - 如果是
call
则调用门的段描述符对应的代码段的 DPL 在数值上小于等于 CPL。
判断调用门是否提权:
如果没有权限变化则等价于普通的跨段跳转,只不过这里跳转的地址不是直接从操作数获取的。
如果权限发生变化则需要进行栈切换:
从 TSS 中获取新的
ss
和esp
更新对应的寄存器,期间会检测ss
的权限以及 TSS 是否合法。将原有的
ss
,esp
,参数(从原本的栈中拷贝,拷贝长度参考调用门描述符中的参数计数(Param Count)字段),cs
,返回地址依次压入新的堆栈。注意
原有的堆栈中只有参数,没有压入返回地址之类的东西。
根据门描述符中表示的代码段还有调用门入口点更新
cs
,eip
。
至此我们完成的 cs
和栈的切换,跳转指调用门指定的代码开始执行。
注意
如果我们调试提权后的代码,则返回用户态之后会发生崩溃。
这是因为 int 3
对应的中断处理函数 _KiTrap03
会将 fs
寄存器修改为 0x30(这也是为什么我们下断点调试时发现 fs
的值发生变化),而由于我们通过调用门提权后处于 0 环权限,_KiTrap03
并不会恢复 fs
寄存器,因此我们在返回 3 环时需要手动还原 fs
寄存器。
返回过程
如果我们通过 call
长调用指令进行调用门跳转则需要通过 retf
指令返回。
注意
如果调用门规定了参数格式,则我们必须使用 retf [参数的字节总数]
来返回。
注意,retf
后面跟的是参数的字节数,而调用门的参数计数(Param Count)字段描述的是参数的个数。
具体过程为:
进行权限检查 :根据栈中存放的
cs
寄存器判断是否发生权限(CPL)变化:如果没有发生权限变化则为普通的跨段跳转,不会发生栈切换。
如果发生权限变化则:
- 只能是降权,即在数值上栈中保存的
cs
的 RPL 要大于 CPL。 - 对栈中保存的
ss
进行权限检查,要求返回后ss
和cs
的 DPL 要相等。
- 只能是降权,即在数值上栈中保存的
恢复寄存器 :
- 根据中保存的
eip
,cs
恢复eip
和cs
寄存器。 - 如果降权则要在普通的跨段跳转返回的基础上根据栈中保存的
esp
和ss
恢复堆栈。栈中保存的esp
和ss
的位置需要根据retf
后面跟的参数字节数定位。
- 根据中保存的
平衡堆栈 :
如果权限不变则根据
retf
后面跟的参数字节数平衡掉压入的参数,另外还要平衡掉跨段跳转时压入的返回地址和cs
。如果是降权则需要在原本的堆栈中根据
retf
后面跟的参数字节数平衡掉压入的参数。注意
内核堆栈每次都是通过 TSS 的
esp
赋值,因此不需要平衡内核堆栈。
中断
什么是中断
中断(Interrupt)是计算机系统中一种异步事件通知机制,用于在处理器正在执行任务时,打断当前执行流程,让处理器去响应更紧急或更重要的事件。
中断机制为计算机系统提供了一种对事件的实时响应方式,同时保证处理器资源的高效利用。
按照来源划分,中断可以分为硬件中断和软件中断 :
硬件中断 :来自外部设备(如键盘、鼠标、定时器、网卡)发出的中断信号。如:键盘按键、鼠标移动、时钟滴答。
软件中断 :由程序通过指令(如
int
)显式触发,通常用于系统调用。
按目的划分,中断可以分为可屏蔽中断(IRQ),不可屏蔽中断(NMI)和异常(Exception) :
可屏蔽中断(IRQ) :可以被禁止(如设置
IF=0
,EFLAGS
寄存器第 10 位)。大多数硬件中断都属于这一类。不可屏蔽中断(NMI) :非常紧急,不能被屏蔽,比如内存校验出错。
异常(Exception) :由 CPU 自身执行错误或特定条件触发,如除 0、段错误、页错误等。属于“内部中断”。
中断描述符表(IDT,Interrupt Descriptor Table)
与调用门不同,中断门的门描述符位于存储在 IDT(中断描述符表) 中。这是因为中断描述符要以“中断号”为索引,用于快速响应异步事件或异常情况,强调处理器控制流程的跳转,因此需要单开一个表存储。
中断向量表(IDT)最多有 256 项(编号 0–255),每一项都对应一个中断/异常,中断号就是中断向量表的下标。其中中断号 0–31(共 32 个)被 CPU 保留,用于异常处理,操作系统不得更改其语义。
中断号 | 异常简称 | 异常名称 | 类型 |
---|---|---|---|
0 | #DE | Divide Error(除 0 错误) | Fault |
1 | #DB | Debug(调试异常) | Fault/Trap |
2 | — | Non-Maskable Interrupt(NMI) | Interrupt |
3 | #BP | Breakpoint(断点) | Trap |
4 | #OF | Overflow(溢出) | Trap |
5 | #BR | BOUND Range Exceeded(数组越界) | Fault |
6 | #UD | Invalid Opcode(非法/未定义指令) | Fault |
7 | #NM | Device Not Available(协处理器不可用) | Fault |
8 | #DF | Double Fault(双重故障) | Abort |
9 | — | Coprocessor Segment Overrun(协处理器段溢出) | Fault(已废弃) |
10 | #TS | Invalid TSS(任务状态段无效) | Fault |
11 | #NP | Segment Not Present(段不存在) | Fault |
12 | #SS | Stack Segment Fault(栈段错误) | Fault |
13 | #GP | General Protection Fault(一般保护异常) | Fault |
14 | #PF | Page Fault(页错误) | Fault |
15 | — | Reserved(保留) | — |
16 | #MF | x87 Floating-Point Error(浮点错误) | Fault |
17 | #AC | Alignment Check(对齐检查) | Fault |
18 | #MC | Machine Check(机器检查) | Abort |
19 | #XM/#XF | SIMD Floating-Point Exception | Fault |
20–31 | — | Reserved(保留) | — |
中断号 32–255:可由操作系统自由使用,每一项都是一个“中断描述符”,可为以下几种类型:
描述符类型 | 用途 |
---|---|
中断门(Interrupt Gate) | 常用于处理硬件中断,自动清除 IF 位,防止嵌套 |
陷阱门(Trap Gate) | 常用于异常和调试,不清除 IF |
任务门(Task Gate) | 切换到另一个任务(TSS),操作系统不使用该功能 |
关于 IDT 有如下常用的调试命令:
命令 | 功能说明 |
---|---|
r idtr /r idtl |
查看 IDT 基地址和长度 |
!idt |
查看当前处理器 IDT 内容 |
!idt <n> |
查看中断号 <n> 的处理函数 |
~<n> |
切换到第 n 个处理器上下文 |
中断门
中断门(Interrupt Gate)是 x86 架构中用于处理中断(硬件/软件中断)和异常的一种特殊机制。它是一种描述符,存储在 IDT(中断描述符表) 中,用于指向中断/异常处理程序的入口地址和相关属性。
中断门的作用是:当发生中断或异常时,CPU 通过中断门自动跳转到对应的处理函数,并进行必要的权限切换、堆栈切换等。
门描述符
- 偏移地址(Offset) :目标代码段中中断处理程序入口的逻辑地址,分为 16 位的低位偏移(
Offset 15:00
)和 16 位的高位偏移(Offset 31:16
),需要与段选择子指向的段描述符结合才能得到入口的线性地址。 - 段选择子(Segment Selector) :中断门要跳转到的目标代码段的选择子,用于在 GDT 或 LDT 中查找中断处理程序所在的段描述符。
- 类型(Type) :4 位,表示门描述符的类型。对于 32 位中断门,值为
1110
(0xE);表示该门在触发时会自动清除 EFLAGS 中的 IF 位,从而禁止中断嵌套。 - 系统段标志(S) :1 位,固定为 0,表示中断门是系统段,而不是普通的代码段或数据段。
- 描述符特权级(DPL) :2 位,表示访问中断门所需的最低特权级。在中断由软件触发(如 INT 指令)时进行特权检查;若 CPL > DPL,则会触发 #GP 异常。
- 段存在标志(P) :1 位,表示中断门描述符是否存在。若为 0,访问该中断门会触发 #NP(Not Present)异常。
过程分析
调用过程
当我们通过 int n
或指令触发中断:
根据中断向量在 IDT 中查找中断门描述符。(指令中的操作数
n
作为索引,找到对应的门描述符;跳转地址由中断门中的Offset
和Segment Selector
字段决定)进行权限检查:
- 对于硬件中断 :不进行权限检查,中断处理程序直接执行。
- 对于软件中断(如 INT n) :
- CPL(当前特权级)必须小于等于中断门的 DPL。
- 如果 CPL > DPL,则触发 #GP 异常。
判断是否发生权限变化(提权) :
如果中断门的目标代码段的 DPL < CPL,则发生提权,需要进行堆栈切换。
- 从 TSS 中查找对应的
SS
和ESP
,加载为新堆栈。 - 将当前栈的
SS
、ESP
、EFLAGS
、CS
和EIP
依次压入新堆栈中,形成完整的返回上下文。 - 若中断号是
INT n
且带有错误码,则错误码也一并压入堆栈(CPU 固定的几项,我们自己注册的没有)。
- 从 TSS 中查找对应的
如果没有权限变化(例如从 Ring 0 调用 Ring 0 中断门)则不会切换堆栈,仅将
EFLAGS
、CS
、EIP
压入当前堆栈。
修改
EFLAGS
寄存器 :中断门会清空EFLAGS
寄存器中的下面几个标志位:
位名 | 含义 | EFLAGS 位位置 | 修改行为 / 说明 |
---|---|---|---|
IF (Interrupt Flag) | 中断允许标志:控制是否允许 CPU 响应可屏蔽中断(IRQ) | 位 9 | 当通过中断门进入中断处理程序时,CPU 会自动将 IF = 0 ,即暂时屏蔽其他中断,防止当前中断被新的中断打断。等中断处理结束后通过 IRET 恢复原 EFLAGS ,IF 才可能再次为 1。这个行为只发生在中断门,陷阱门不会清除 IF,允许中断嵌套。 |
TF (Trap Flag) | 单步调试标志:控制是否在每条指令后触发调试异常(#DB) | 位 8 | 进入中断处理程序前,CPU 会自动将 TF 清 0,禁止单步调试在中断处理流程中生效,避免调试器每条指令都触发中断,造成混乱甚至不可预期行为。恢复时,IRET 会将 TF 恢复为中断前的状态。 |
NT (Nested Task) | 嵌套任务标志:用于任务切换机制(与 TSS 和任务门相关) | 位 14 | NT = 1 表示当前是嵌套任务。中断门触发时,即便不是 Task Gate 形式,也会清除 NT 位为 0,防止任务嵌套链在中断中干扰任务切换控制流。(如果是嵌套任务则 iretd 指令会根据任务段返回) |
VM (Virtual 8086 Mode) | 虚拟 8086 模式标志:控制是否在 V8086 模拟环境中运行 16 位实模式代码 | 位 17 | 如果 CPU 当前处于 VM = 1 (虚拟 8086 模式),当通过中断门执行 INT n 指令时,CPU 会自动清除 VM ,退出 V86 模式,进入保护模式(Ring 0),以便执行保护模式下的中断处理程序。这个过程伴随着堆栈切换、权限检查和返回机制,支持从实模式模拟环境跳转到内核态中断处理逻辑。 |
跳转到中断处理程序:
- 设置新的
CS:EIP
为门描述符中指定的目标。 - 开始执行中断处理程序。
- 设置新的
返回过程
在中断处理程序结束时通过 iret
( 32 位 iretd
,64 位 iretq
)指令返回,具体过程为:
- 进行权限检查:
- 从栈中弹出
EIP
、CS
和EFLAGS
。 - 如果发生过权限变化(即从高权限跳转到低权限):
- 同时还需要从栈中恢复
SS
和ESP
。 - 返回后会将 CPL 设置为
CS
描述符中的 DPL。
- 同时还需要从栈中恢复
- 从栈中弹出
- 恢复上下文:
- 设置
CS:EIP
为返回地址。 - 恢复原
EFLAGS
。 - 如果发生过堆栈切换,则也恢复
SS
和ESP
,切回用户堆栈。
- 设置
- 完成权限恢复与返回:
- 中断处理完毕,程序继续从中断发生处继续执行。
注意
- 若中断期间修改了段寄存器(如
fs
),在返回到用户态前应手动还原。 iretd
不能被retf 4
代替,因为发送权限变化的时候retf 4
还会平衡掉用户态栈中压入的参数,而int
指令只在内核态栈中压入EFLAGS
,因此会导致堆栈不平衡。
陷阱门
陷阱门是一种门描述符,用于定义某个中断或异常发生时,处理器该跳转到哪段代码继续执行。它和中断门类似,都存在于 IDT(Interrupt Descriptor Table) 中,但行为略有不同,主要用于不会频繁嵌套、需要精细控制响应时机的异常或调试场景。
门描述符
陷阱门描述符与中断门描述符格式基本一致,唯一区别就是 Type
字段不同,由 1110
(0xE)变为了 1111
(0xF)。
过程分析
与中断门完全一致,唯一的不同点在于陷阱门不会清空 EFLAGS
的 IF
标志位。
任务门
任务门(Task Gate)是一种特殊的系统段描述符,用于在 x86 保护模式下实现任务切换(Task Switch),其本质作用是指向一个任务(TSS),以便通过它进行硬件级别的任务切换。
然而现代操作系统几乎完全不用任务门(Task Gate)和硬件任务切换机制,无论是 Windows、Linux、macOS 还是虚拟化内核,都采用软件实现的任务调度和上下文切换机制。这是因为:
- 控制粒度太粗、太死板。
- 一旦使用任务门 → 切换的是整个 TSS + CR3 + 所有段寄存器 + GPR + EFLAGS + EIP。OS 不能只保存/恢复一部分,比如只切换线程状态,不切换地址空间。而软件调度可以精细控制切换内容(比如 lazy FPU context switching、延迟页表切换等)。
- 性能开销大。使用任务门意味着 CPU 要自动执行一整套验证、保存、加载流程,而软件调度只需要保存需要的部分上下文,用
mov
,push
,pop
就行,效率更高。 - 异常可恢复性差。硬件任务切换由 CPU 自动执行的一整套流程,如果在任务切换“中间过程”如果出错,则当前处于模糊状态,OS 没办法清楚知道到底处在哪个任务上下文。最终会导致:陷入不可恢复状态 → Triple Fault(第三次错误) → CPU 复位重启,分析调试困难。
- 不利高并发场景。TSS 原生只支持一个任务状态。TSS 的 busy 位、防嵌套规则等,使得任务链和多线程管理变得复杂且危险,经常可能因为并发问题触发异常导致系统崩溃。Linux、Windows 等直接为每个 CPU 定义自己的调度器和线程控制块(TCB),避免使用硬件机制。
- 兼容性差。硬件任务切换机制使得 OS 必须与 x86 的 GDT、IDT、TSS、任务门、段保护等高度绑定,导致 OS 无法跨架构通用,也难以模块化维护。
因此现代操作系统中任务门的主要作用为:
TSS 的 ESP0 用于内核栈切换 :现代操作系统都需要为每个 CPU 设置一个 TSS,仅为了提供 Ring 3 → Ring 0 的栈切换支持。在中断门或系统调用触发从用户态切换到内核态时,CPU 自动从当前 CPU 的 TSS 中加载
SS0
/ESP0
作为临时新栈顶。双重错误(#DF)异常处理 :Intel 推荐用 任务门 描述符来设置 IDT 中的第 8 号向量(#DF),这样就可以防止中断嵌套导致的堆栈损坏引发 Triple Fault。我们在调试这类异常的时候可以根据这一性质找到出现异常的堆栈。
“Some exception handlers, such as the double-fault exception handler, are typically set up with a task gate in the IDT. This enables the processor to switch to a known-good TSS and clean stack.”
— SDM Vol. 3, Section 6.15 “Exception and Interrupt Handling”
任务段描述符(TSS Descriptor)
任务段(TSS)描述符的定义如下:
注意
根据《Intel SDM(System Developer’s Manual)》中的明确规定:
“TSS descriptors may only be placed in the GDT; they cannot be placed in an LDT or the IDT.”
任务段(TSS)描述符只能出现在 GDT(全局描述符表) 中,不能放在 LDT(局部描述符表)里。
段基址(Base Address) :任务段(TSS)在内存中的线性地址,由三部分组成:
Base 15:00
、Base 23:16
和Base 31:24
,共同构成 32 位的段基址,用于定位 TSS 数据结构的起始地址。段限长(Segment Limit) :指定任务段的长度,通常为
0x67
(即 104 字节),分为低 16 位(Limit 15:00
)和高 4 位(Limit 19:16
)。如果G
位为 0,则按字节为单位解释该限长。类型(Type) :4 位,表示描述符的具体类型。对于 32 位可用任务段:
- 类型值为
1001
(0x9)说明该TSS段描述符未被加载到TR段寄存器中。 - 类型值为
1011
(0xB)说明该TSS段描述符已被加载到TR段寄存器中。
- 类型值为
系统段标志(S) :1 位,必须固定为 0,表示该描述符是一个系统段,而非普通的代码段或数据段。TSS、LDT、门描述符等都属于系统段。
描述符特权级(DPL) :2 位,表示对该任务段的访问权限要求。在通过任务门或软件指令(如
CALL
,JMP
)触发任务切换时,当前特权级(CPL)必须 ≤ DPL,否则会触发 #GP(General Protection)异常。段存在标志(P) :1 位,表示该任务段是否有效。若为 0,则尝试访问此任务段会触发 #NP(Not Present)异常;为 1 时表示段存在且可以使用。
可用位(AVL) :1 位,由操作系统自由使用,处理器不进行解释。常用于系统内部标记用途,例如多处理器状态跟踪或调试标志。
默认操作数大小(D/B) :1 位,对于任务段必须设置为 0,否则行为未定义。该位在代码段中表示默认指令大小(16/32 位),但在 TSS 描述符中无实际意义。
粒度(G) :1 位,控制段限长的单位。当为 0 时,段限长以字节为单位解释(TSS 推荐);若为 1,则以 4KB 页为单位(不建议用于 TSS)。
任务寄存器(TR,Task Register)
TR(Task Register,任务寄存器)是 x86 处理器中的一个系统段寄存器,用于引用当前正在使用的任务状态段(TSS)。
TR 寄存器会保存当前活动 TSS 的 段选择子(32 位下默认是 28),并缓存当前 TSS 的 基地址 和 限长。
TR
寄存器可以通过 LTR
和 STR
指令进行读写,不过这两个指令都是 特权指令,只能在特权级 0(CPL = 0)下执行。
STR r16/m16
:从TR
读取当前任务段选择子。LTR r/m16
:向TR
加载任务段选择子。
任务段(TSS,Task State Segment)
任务段(TSS,Task State Segment)是 x86 架构中为支持多任务操作系统设计的一个特殊机制,它定义了一段特殊的内存结构,用于保存任务(线程/进程)的运行上下文和特权级切换信息。
虽然现代操作系统(如 Windows 和 Linux)不再使用“硬件任务切换”(TSS 最初的主要目的),但 TSS 在 中断处理、安全控制、特权切换时仍然是必不可少的组件。
Previous Task Link :上一个任务字段。如果使用任务切换(如使用
jmp TSS selector
),这里保存前一个任务的 TSS 选择子。由于 Windows 基本不使用硬件任务切换(而是用软件方式),因此此字段一般无实际用途。栈切换相关字段 :
- ESP0 / SS0 : Windows 在处理系统调用(如
sysenter
或int 0x2e
)和异常(如页错误)时依赖这两个字段快速切换内核栈。当 CPU 从用户态(CPL=3)切换到内核态(CPL=0)时,会自动加载这两个字段的值作为内核栈的临时基地址(SS:ESP
)。 - ESP1 / SS1,ESP2 / SS2 :较少用,仅在三层栈(Ring1、Ring2)情况下切换,Windows 不使用,这些字段保留。
- ESP0 / SS0 : Windows 在处理系统调用(如
CR3(PDBR) :存储页目录基地址寄存器(CR3),用于指向进程的页表。如果用任务切换机制,CPU 会自动加载 CR3 切换页表。
通用寄存器和段寄存器 :这些是当通过任务切换机制(
CALL/JMP TSS
)切换任务时自动保存的内容。- 通用寄存器 :EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP
- 段寄存器 :ES, CS, SS, DS, FS, GS
- 其他 :EIP、EFLAGS
LDT Segment Selector :指定当前任务使用的 LDT(Local Descriptor Table)。
T(Trap)位 :如果置 1,则在任务切换到此 TSS 时触发调试异常(#DB)
I/O Map Base Address :指定 TSS 后面的 I/O 权限位图偏移,用于控制任务是否可以访问某些 I/O 端口,某些驱动或 VM 技术中使用。
SSP(Shadow Stack Pointer) :当启用 Intel CET(Control-flow Enforcement Technology)时,记录 Shadow Stack 的指针。Windows 10+ 在启用 CET 保护(比如 MS Edge 浏览器)时会使用它。
Intel CET(Control-flow Enforcement Technology,控制流强制技术)是 Intel 推出的一个 硬件级安全特性。CET 有两个核心组件:
- Shadow Stack(影子栈) :CPU 维护一个只写入返回地址的“只读”影子栈。每次函数调用(
CALL
)时,返回地址会同时写入正常栈和影子栈。当执行RET
时,CPU 会比对影子栈的返回地址,不一致则触发异常。 - Indirect Branch Tracking(IBT) : 限制间接跳转(
JMP
、CALL
到寄存器/内存)只能跳转到有效的目标地址。要跳转的目标必须以特殊指令(如ENDBR32
或ENDBR64
)开头,否则触发异常。
- Shadow Stack(影子栈) :CPU 维护一个只写入返回地址的“只读”影子栈。每次函数调用(
ntkrpamp!_KTSS
是 Windows 内核中用于描述任务状态段(TSS,Task State Segment)的结构体,主要用于 x86 架构下的特权级堆栈切换、I/O 端口访问控制等功能。
虽然现代 Windows 系统主要采用软件方式进行任务切换,但仍为每个处理器维护一个 TSS 结构,特别是在处理特定异常(如双重故障)时,硬件任务切换机制仍可能被触发。
1 | //0x20ac bytes (sizeof) |
注意
Windows 默认不使用 ESP1
/ SS1
和 ESP2
/ SS2
等特权级备用栈(Ring 1 和 Ring 2 的栈),所以在它所定义的 _KTSS
结构体中,这些字段会被统一省略或标为保留(Reserved1
,NotUsed1
)。
任务门描述符(Task Gate Descriptor)
段选择子(TSS Segment Selector) :表示该任务门对应的目标任务段(TSS)。该字段占据描述符的第 0–15 位,其值应为 GDT 中某个 TSS 段描述符的选择子。当任务门被触发时,处理器会使用该选择子来访问并加载目标 TSS,实现硬件任务切换。
类型(Type) :4 位,位于描述符的第 40–43 位,用于标识该描述符的具体类型。对于任务门而言,该字段的值为
0101
(即 0x5),表示“32 位任务门(Task Gate)”。该类型值要求处理器在使用该描述符时执行硬件任务切换流程。系统段标志(S) :1 位,位于第 44 位,必须固定为 0,表示该描述符是一个 系统段,而非普通的数据段或代码段。所有任务门、中断门、陷阱门、TSS 和 LDT 描述符都属于系统段。
描述符特权级(DPL) :2 位,位于第 45–46 位,表示访问该任务门所需的最小权限级别。在尝试通过
CALL
,JMP
, 或INT
指令使用该任务门时,如果当前特权级(CPL)高于该值,将会触发#GP
(General Protection)异常。该字段用于控制访问粒度,例如可限制用户态程序不可使用该门。段存在标志(P) :1 位,位于第 47 位,用于表示该任务门描述符是否有效。当该位为 0 时,尝试访问该任务门会导致处理器抛出
#NP
(Segment Not Present)异常。为 1 时表示描述符存在且可使用。
调用过程(任务切换机制)
“任务门调用”是间接的任务切换方式,通过门找到 TSS;“任务段描述符调用”是直接调用 TSS 来切换任务。它们最终都会导致 TSS 被加载和任务切换,但路径和机制不同。
比较点 | 任务段描述符调用 | 任务门调用 |
---|---|---|
调用方式 | CALL TSS_selector 或 JMP TSS_selector (选择子直接指向 TSS 描述符) |
CALL , JMP , 或 INT 到一个任务门(描述符中包含 TSS 选择子) |
位置 | TSS 必须在 GDT 中 | 任务门可以在 GDT、LDT、IDT |
是否间接 | ❌ 否,直接引用 TSS | ✅ 是,门中保存 TSS 的选择子 |
可用于中断 | ❌ 否,不能嵌入到 IDT | ✅ 是,任务门可以作为中断门放入 IDT(任务型中断) |
权限检查点 | 直接检查 TSS 描述符的 DPL | 先检查门的 DPL,再检查 TSS 描述符的 DPL |
用途 | 通常用于显式调度(早期OS设计) | 可用于中断/异常自动触发任务切换(支持隔离的 handler) |
效率与灵活性 | 较低 | 稍好,但仍落后于软件上下文切换 |
现代操作系统使用情况 | ❌ 弃用 | ❌ 弃用(包括 Windows/Linux) |
任务段描述符调用过程
使用 CALL TSS_selector
或 JMP TSS_selector
指令,其中选择子(Selector)指向 GDT 中的任务段描述符(TSS Descriptor)。
权限检查 :
max(CPL,RPL) ≤ DPL
,否则#GP(selector)
存在检查 :P=0 →
#NP(selector)
类型检查 :Busy TSS 调用 →
#GP(selector)
段限长检查 :limit < 0x67 →
#GP(selector)
加载新任务
将当前任务的上下文(寄存器、EIP、EFLAGS、段寄存器等)保存到当前 TSS;
设置 TR(Task Register)指向新的 TSS;
目标 TSS 类型字段自动由 0x9 → 0xB,表示忙状态。
载入新任务状态 :CR3、LDTR、EFLAGS、EIP、通用/段寄存器
对
CALL
:设置EFLAGS.NT=1
并把当前 TSS 段选择子写到新TSS.Backlink
;对JMP
不设置。因此如果想用JMP
进行任务切换直接iretd
返回会蓝屏。When a CALL instruction, an interrupt, or an exception causes a task switch:
the processor copies the segment selector for the current TSS to the previous task link field (Backlink) of the TSS for the new task;
it then sets EFLAGS.NT = 1.当一个 CALL 指令、中断或异常 触发任务切换时:
处理器将当前 TSS 的段选择子写入新任务的 TSS 的 Backlink 字段,并设置 EFLAGS.NT = 1。When a JMP instruction causes a task switch, the new task is not nested.
The previous task link field is not used and EFLAGS.NT = 0.当一个 JMP 指令 引发任务切换时,新任务不被视为嵌套任务,不会使用 backlink,EFLAGS.NT = 0。
任务门调用过程
权限检查 对任务门(同样比较 CPL/RPL 与 DPL)
取出任务门中的 TSS Selector
后续步骤与直接调用任务段相同。
返回过程
当执行
iretd
指令的时候,CPU 发现EFLAGS.NT=1
,识别为任务返回。任务状态段(TSS)中的“前一个任务链接字段”(Previous Task Link,有时也称为“backlink”)以及 EFLAGS 寄存器中的 NT 标志,用于将控制流返回到前一个任务。
当
EFLAGS.NT = 1
时,表示当前正在执行的任务是嵌套在另一个任务之内。
注意
如果我们调试任务切换的代码,由于
int 3
中断会将EFLAGS.NT
为置 0,导致返回的时候是通过堆栈返回,造成蓝屏。读取当前 TSS 的 Backlink → 找到前一个 TSS 描述符。
对当前任务段执行与调用流程相同的有效性检查(
P=1
,limit≥0x67
,Type=Busy
)保存当前任务状态到当前 TSS,因为 IRET 任务返回也属于任务切换。
清除当前 TSS Busy 位。
加载前一任务 TSS 内容 → 寄存器、EIP、EFLAGS。
注意
如果 分页未启用(即
CR0.PG = 0
),那么虽然 TSS 中的 CR3 字段仍会被读出来,但不会写入 CR3 寄存器。在 Intel 开发文档原文如下:If paging is not enabled, a PDBR value is read from the new task’s TSS, but it is not loaded into CR3.
写 TR = 前一 TSS selector,恢复其 Busy 状态。
清除
EFLAGS.NT
跳回前一任务继续执行
页式内存管理
Intel CPU 支持下面几种分页模式:
分页模式 | 地址宽度 | 页目录结构 | 页大小 | 是否支持 NX(DEP) |
---|---|---|---|---|
非 PAE 分页 | 32-bit | 2 级(10+10+12) CR3 → PDE → PTE |
4 KB | ❌ 否 |
PAE 分页 | 36-bit(最多64GB) | 3 级(2+9+9+12) CR3 → PDPT → PDE → PTE |
4 KB, 2 MB | ✅ 是 |
x64 长模式分页 | 48-bit(理论) | 4 级(9+9+9+9+12) CR3 → PML4 → PDPT → PDE → PTE |
4 KB, 2 MB, 1 GB | ✅ 是 |
页式内存管理由 CPU 和操作系统共同维护:
- CPU 主要负责地址转换过程。CPU 在执行指令时,如果涉及内存访问(如
mov eax, [esp+0x10]
),就会自动触发地址翻译流程(虚拟 → 线性 → 物理)。 - 操作系统主要负责页表结构维护。操作系统负责在内存中创建、更新和删除页表各层的表项,以决定每个虚拟地址该映射到哪个物理地址。
有时候 CPU 还要与操作系统紧密配合,比如如果 CPU 发现访问的地址页表中标记为不存在(P=0),则触发 #PF 缺页异常,此时需要由操作系统进行缺页处理或报错终止程序。
环境配置
Windows XP 32 位
Windows XP 32 位按照是否支持 PAE 和是否支持多处理器分为 4 个内核。
文件名 | 支持 PAE | 支持 SMP(多处理器) | 用途与说明 |
---|---|---|---|
ntoskrnl.exe |
❌ 否 | ❌ 否 | 标准 单处理器,非 PAE 模式使用 |
ntkrnlpa.exe |
✅ 是 | ❌ 否 | 单处理器,启用 PAE 的内核 |
ntkrnlmp.exe |
❌ 否 | ✅ 是 | 多处理器 SMP,非 PAE 模式使用 |
ntkrpamp.exe |
✅ 是 | ✅ 是 | 多处理器 SMP,启用 PAE 的完整内核(PAE + MP 全支持) |
我们通过 boot.ini
指定 /PAE
参数,并可通过 /KERNEL=xxx.exe
可以显式指定要加载的内核映像。
boot.ini
是 Windows NT 系列(如 Windows XP、2000、Server 2003)中用于控制启动引导参数的配置文件。它属于 NT Loader(ntldr)引导机制的一部分
1 | [boot loader] |
启动时你将看到两个选项:
Windows XP (PAE)
会启用 PAE 分页,可能加载ntkrnlpa.exe
/ntkrpamp.exe
Windows XP (No PAE)
会强制关闭 PAE,使用ntoskrnl.exe
/ntkrnlmp.exe
Windows Vista / 7 32 位
Windows Vista / 7 32 位后不再区分 mp
和 up
,统一支持 SMP。也不再使用 ntkrnlmp.exe
等文件。并且这个版本之后不再使用 boot.ini
而是使用 bcdedit
修改 PAE 状态。
文件名 | 支持 PAE | 支持多核 (SMP) | 说明 |
---|---|---|---|
ntoskrnl.exe |
❌ 否 | ✅ 是 | 默认内核映像,未启用 PAE 时使用 |
ntkrnlpa.exe |
✅ 是 | ✅ 是 | 启用 PAE 时使用(通过 bcdedit /set pae ForceEnable ) |
从 Windows Vista 开始(包括 Windows 7/8/10/11)就已经彻底废除了 boot.ini
,转而使用新的引导配置机制 BCD(Boot Configuration Data)。该机制通过 bcdedit
命令进行配置,具体步骤为:
复制当前默认启动配置
1
bcdedit /copy {current} /d "Windows 7 - No PAE"
- 这会复制当前启动项,生成一个新 GUID(比如
{3e9a0...}
)。 - 新启动项会出现在启动菜单中,名字叫 “Windows 7 - No PAE”。
正常情况下系统返回类似下面这段输出:
1
The entry was successfully copied to {3e9a0123-xxxx-xxxx}.
- 这会复制当前启动项,生成一个新 GUID(比如
给这个新启动项设置关闭 PAE 参数
1
bcdedit /set {3e9a0123-xxxx-xxxx} pae ForceDisable
再次开机时会看到:
1 | Windows 7 |
- 第一个启用 PAE,可能支持 DEP、安全保护机制;
- 第二个禁用 PAE,用于兼容旧驱动或调试。
Windows 64 位
Windows 7 / 10 / 11 等 64 位系统使用长模式分页,ntoskrnl.exe
已合并所有功能模块,不再有 PAE / 非 PAE 区分,也不再需要多个版本。
文件名 | 支持 PAE | 支持多核 (SMP) | 说明 |
---|---|---|---|
ntoskrnl.exe |
✅ 是 | ✅ 是 | 唯一内核映像,默认启用 PAE + SMP + NX(DEP)等 |
控制寄存器
CR0
~CR4
是 x86 架构中的 控制寄存器(Control Registers),它们是 CPU 控制系统级特性和行为的关键寄存器。这些寄存器 只能在特权级 0(Ring 0)下访问,用于开启分页、保护模式、中断响应、缓存控制等功能。
名称 | 状态 | 主要功能 |
---|---|---|
CR0 |
使用中 | 启用保护模式、分页机制、缓存控制、对齐检查、写保护等 |
CR1 |
保留 | 未使用,Intel 架构中始终保留 |
CR2 |
使用中 | 保存最近一次页错误异常(#PF)的线性地址 |
CR3 |
使用中 | 页目录基地址(或 PAE 下页目录指针表地址),分页的起点 |
CR4 |
使用中 | 启用各种扩展功能,如 PAE、大页、PGE、SMEP、SMAP 等 |
CR8 |
64 位专用 | 控制 中断优先级(Task Priority Level),用于高级中断屏蔽 |
CR0
CR0 控制的是处理器的基本操作模式,特别是保护模式、分页机制、缓存和协处理器等。其关键位如下:
位号 | 名称 | 含义 & 功能 |
---|---|---|
31 | PG | 📌 Paging Enable :启用分页机制。需要 PE=1 (保护模式)才能生效。 |
30 | CD | 📌 Cache Disable :禁止 CPU 缓存,设为 1 时强制所有访问绕过缓存(需配合 NW=0 )。 |
29 | NW | 📌 Not Write-through :当 CD=0 时决定写回/直写策略(Write-back/Write-through)。 |
18 | AM | Alignment Mask :启用对对齐错误的检测。需 EFLAGS.AC=1 、CPL=3 且当前处于保护模式或 V8086 模式。 |
16 | WP | 📌 Write Protect :设置为 1 时,内核模式代码无法写入只读页(即使 U/S=0 )。常用于 Copy-On-Write(COW)。 |
5 | NE | Numeric Error :启用内部机制报告 x87 浮点错误;为 0 时使用 PC 式 FERR# 引脚。 |
4 | ET | Extension Type(已弃用):在现代 CPU 中固定为 1。 |
3 | TS | Task Switched :任务切换后由 CPU 设置,用于延迟保存 FPU/MMX/SSE 状态。与 EM/MP 配合控制 FPU 异常。 |
2 | EM | Emulation :启用浮点指令模拟(禁用 FPU、MMX、SSE 指令,抛出 #NM / #UD)。 |
1 | MP | Monitor Coprocessor :配合 TS 控制 WAIT/FWAIT 指令是否触发 #NM。 |
0 | PE | 📌 Protection Enable :启用保护模式。仅此位不足以开启分页,需同时 PG=1 才行。 |
CR2
内容 | 用途 |
---|---|
Page Fault Linear Address | 仅当发生页错误(#PF)时,由处理器自动写入导致错误的线性地址。调试和异常处理中使用频繁。 |
CR3
仅在分页启用(CR0.PG=1
)时生效,用于指示页目录(或 PDPTE)的物理地址:
位号 | 名称 | 功能 |
---|---|---|
31:12 | Page Directory Base | 页目录/页目录指针表的物理地址高位。必须 4KB 对齐,低 12 位为 0。 |
4 | PCD | Page-level Cache Disable:控制页表访问的缓存行为。为 1 则关闭缓存。 |
3 | PWT | Page-level Write-Through:控制页表访问的写策略。 |
2:0, 11:5, 63:32 | - | 保留/忽略位,在 PAE 或 IA-32e 模式下含义不同。 |
注意:在 PAE/64 位模式下,CR3
不直接索引 PDE/PTE,而是用于定位 PDPTE 表。
CR4
提供了分页、虚拟化、异常处理、SIMD 等新机制的开关,位数最多,关键位如下:
位号 | 名称 | 功能简述 |
---|---|---|
0 | VME | Virtual-8086 Mode Extensions(虚拟中断处理) |
1 | PVI | Protected-mode Virtual Interrupts,支持虚拟 8086 中断 |
2 | TSD | 限制 RDTSC 指令只能在 CPL=0 执行 |
3 | DE | 禁用对 DR4/DR5 寄存器的访问(生成 #UD),否则 CPU 会将对 DR4 和 DR5 的访问重定向到 DR6 和 DR7 ,这是为了兼容旧软件(早期 IA-32 处理器中曾使用 DR4/DR5) |
4 | PSE | 📌 启用 4MB 大页(非 PAE 下) |
5 | PAE | 📌 启用物理地址扩展(使用 PDPTE 层、36 位物理地址) |
6 | MCE | 启用机器检查异常(Machine Check Exception) |
7 | PGE | 📌 启用全局页(带 G 标志的页不因 CR3 更新而清空 TLB) |
8 | PCE | 用户态也能执行 RDPMC 指令 |
9 | OSFXSR | OS 支持 FXSAVE/FXRSTOR ,支持 SSE/SSE2 |
10 | OSXMMEXCPT | OS 支持 SSE 异常处理(#XM) |
11 | UMIP | 用户模式禁止访问 SGDT/SIDT/STR/SMSW/SLDT |
12 | LA57 | 启用 5 级分页,支持 57 位线性地址 |
13 | VMXE | 启用虚拟化扩展(VMX) |
14 | SMXE | 启用安全模式扩展(SMX) |
16 | FSGSBASE | 允许直接读写 FS/GS 基址 |
17 | PCIDE | 启用进程上下文标识(不刷新 TLB 切换上下文) |
18 | OSXSAVE | OS 支持 XSAVE/XRSTOR/XGETBV/XSETBV |
19 | KL | 启用 AES Key Locker |
20 | SMEP | Supervisor Mode Execution Protection(防止内核执行用户页) |
21 | SMAP | Supervisor Mode Access Protection(防止内核访问用户页) |
22 | PKE | 启用用户页保护键(Protection Key)机制 |
23 | CET | 启用控制流强制(CET)技术 |
24 | PKS | 启用 supervisor 页面的保护键机制 |
CR8
位号 | 名称 | 功能 |
---|---|---|
3:0 | TPL | Task Priority Level:中断优先级阈值;0 表示允许所有中断,15 表示屏蔽所有中断。仅在 IA-32e 模式(64 位)使用。 |
非 PAE 分页
在 32 位系统中,如果 CR0.PG = 1
(启用分页)且 CR4.PAE = 0
(未启用 PAE)则为非 PAE 分页模式,也就是我们常说的 10-10-12 分页模式。
页表结构
在不考虑大页的情况下,非 PAE 分页模式的页表结构如下图所示。
该模式通过 2 级页表结构,将 32 位的线性地址映射到 最多 4GB 的物理地址空间。
名称 | 层级 | 数量与结构 | 每项大小 / 总大小 | 作用说明 |
---|---|---|---|---|
页目录表(Page Directory) | 一级结构 | 每进程 1 个,含 1024 个页目录项(PDE) | 每项 4 字节,共 4KB | 每项控制 4MB 线性空间,可指向页表或直接映射一个 4MB 大页(需 CR4.PSE=1 ) |
页表(Page Table) | 二级结构 | 最多 1024 个,每表含 1024 个页表项(PTE) | 每项 4 字节,共 4KB | 每项映射一个 4KB 物理页框,控制 4KB 线性空间。由 PDE 间接索引获得 |
地址转换过程
非 PAE 分页模式的线性地址到物理地址的转换过程如下图所示:
线性地址由高到低划分为 10 位 PDE 索引、10 位 PTE 索引 和 12 位页内偏移,其中 10,10,12 恰好对应:
- 一个页目录表包含 个 PDE。
- 一个页表包含 个 PTE。
- 一个内存页大小为 字节。
例如假设线性地址为 0x12345678
,则具体的转换过程如下:
字段 | 值 | 含义 |
---|---|---|
PDE Index | 0x48 | CR3 指向页目录 → 第 0x48 项 PDE |
PTE Index | 0xD1 | PDE 指向的页表 → 第 0xD1 项 PTE |
Offset | 0x678 | 页内偏移,定位物理页中的位置 |
页目录自映射
另外,为了方便操作系统在内核态访问并维护自身的页表结构,Windows 会设置页目录表中 第 0x301
项(769 项) 的 页目录项 PDE 指向页目录表自身的物理地址,实现所谓的 页目录自映射(Page Directory Self-Mapping)机制。这样,整个页目录表和所有页表都可以通过一组固定的线性地址访问。
页目录表的线性地址为:
0x300 << (12 + 10) | 0x300 << 12 = 0xC0300000
第
N
个 PDE 的线性地址为:0xC0300000 + (N - 1) * 4
(后面加的是第N
个 PDE 在页目录表中的偏移)第 0x301 个 PDE 存放着页目录表的物理地址,对应线性地址为:
0xC0300000 + 0x300 * 4 = 0xC0300c00
提示
因此我们可以从
0xC0300c00
地址读取到页目录表的物理地址。线性地址
p
对应的 PDE 的线性地址为:0xC0300000 | ((p >> (12 + 10)) * 4) = 0xC0300000 | p >> 20 & 0xFFC
第一个 PDE 指向的页表(如果有)基址对应的线性地址为:
0x300 << (12 + 10) = 0xC0000000
第
N
个 PDE 指向的页表(如果有)基址对应的线性地址为:0xC0000000 | (N - 1) << 12 = 0xC0000000 + (N - 1) * 0x1000
第
N
个 PDE 指向的页表的第M
个 PTE的线性地址为:0xC0000000 | (N - 1) << 12 | (M - 1) * 4 = 0xC0000000 + (N - 1) * 0x1000 + (M - 1) * 4
线性地址
p
对应的 PTE 的线性地址为:0xC0000000 | ((p >> 12) * 4) = 0xC0000000 | p >> 10 & 0x3FFFFC
提示
这个公式计算 PTE 的 PTE 时会找到指向 PTE 所在页表的 PDE 的线性地址;同理在计算 PDE 的 PTE 时会找到
PDE[0x300]
的线性地址。
页表属性
在非 PAE 分页模式下,页表相关属性如下:
其中 PDE(页目录项)主要有两种类型,取决于 PDE 的第 7 位(PS 位) 的取值,这两种 PDE 的结构稍有不同。
页大小 | PS 位 | 解释方式 | PDE 指向的对象 | 后续结构 |
---|---|---|---|---|
4KB 页 | 0 | 二级页表结构 | 指向一个页表(PTE 数组) | 需要查找 PTE |
4MB 页 | 1 | 一级页表结构(大页) | 直接映射一个4MB 页框 | 不查 PTE,直接定位物理页 |
CR3
页目录基地址(Address of page directory) :CR3 的 Bits[31:12] 存储的是页目录的 物理地址的高 20 位(即第 12~31 位)。因为页目录必须 4KB 对齐,因此其低 12 位始终为 0,不会在 CR3 中存储。另外
Bits[63:32]
仅在 64 位模式下使用,在普通 32 位分页中忽略。页级写通标志(PWT, Page-level Write Through) :位于 CR3 的 第 3 位,指定访问页目录时所用内存类型是否为直写缓存策略(Write-Through)。仅在启用页表缓存机制时生效,属于 间接影响。
直写缓存策略(Write-Through)指的是每次 CPU 写数据到缓存时,也会立即写入主内存,也就是说主内存始终是最新数据。
页级缓存禁止(PCD, Page-level Cache Disable) :位于 CR3 的 第 4 位,指定是否对页目录表所在的内存区域禁用缓存。如果设为 1,则 CR3 所指向的整个页目录将不会被缓存,即CR3 所指向的页目录表所在内存区域,不会进入 CPU 的缓存系统(如 L1/L2/L3 cache),每次访问都直接访问内存(DRAM)。
提示
在调试的时候,我们可以通过 !process 0 0
来获取页目录基地址。
1 | kd> !process 0 0 |
其中 DirBase
的值 a9024000
就是页目录基地址的物理地址。
PDE
首先对于 4KB 页对应的 PDE,该页目录项的结构如下:
存在位(P, Present) :表示该 PDE 是否有效,这里需要设置为 1 表示有效。
- 1:该 PDE 有效,处理器可使用;
- 0:该 PDE 无效效,访问相关内存地址会触发 #PF(Page Fault)。
读/写标志(R/W, Read/Write) :控制该页是否可写。
- 0:只读;
- 1:可读可写(实际受
CR0.WP
影响)。
用户/特权标志(U/S, User/Supervisor) :控制访问权限级别。
- 0:仅 CPL 0~2(内核)可访问;
- 1:所有特权级(包括用户态)可访问。
页级直写缓存策略(PWT, Page-level Write Through) :控制缓存策略。
- 1:Write-Through(写同时写入主存和缓存);
- 0:Write-Back(只写缓存,主存延迟刷新)。
页级缓存禁止(PCD, Page-level Cache Disable) :控制是否禁用该页表所在区域的缓存。
- 1:禁止缓存,每次访问都直接访问主内存;
- 0:允许缓存。
访问位(A, Accessed) :由处理器在访问该 PDE(即页表)时自动置 1。OS 可用来统计页面活跃度、用于换页算法。
页大小标志(PS, Page Size) :指定 PDE 是映射页表还是映射 4MB 大页。
- 0:PDE 指向一个 页表(常见);
- 1:PDE 直接映射一个 4MB 大页(需
CR4.PSE=1
)。
页表物理基地址(Page Table Base Address) :用于指定页表的 物理地址的高 20 位,表示该 PDE 所对应页表的实际位置。因为页表是 4KB 对齐,低 12 位为 0,因此未在 PDE 中存储,实际页表地址为
PDE[31:12] << 12
。
而对于 4MB PDE,由于是直接记录物理页的地址,因此比 4KB PTE 多了一些 PTE 相关的属性。另外就是由于从 10-10-12
变成了 10-22
,也就是说指向的物理页大小为 字节,基址关于 对齐,因此只需要 32 - 22 = 10
比特来记录物理页的物理地址。
4MB PDE 的各字段分布如下:
相对于 4KB PDE 有变化的属性如下:
脏位(D, Dirty) :当该页被写入过时,自动由 CPU 置
1
,用于操作系统判断是否需要回写磁盘。页面大小(PS, Page Size) :固定为
1
,表示这是一个 4MB 大页项,而非指向页表(4KB 页面)。全局页(G, Global) :为
1
时表示该页为全局页(不因 CR3 切换被 TLB 清除),仅当CR4.PGE=1
有效。PAT 位(Page Attribute Table) :CPU 用来选择内存缓存类型的一个控制位,它和 CR0、CR3 中的 PCD/PWT 配合,最终决定某一页(或一块内存)以什么样的缓存策略访问。
页框基址(Page Frame Address) :存储 4MB 页起始的物理地址高 10 位。低 22 位默认为 0(因为对齐到 4MB)。另外如果 支持 PSE-36 机制 并且 处理器支持的最大物理地址宽度为 36,则页框基址可以存储 14 位,可以映射 64 GB 的物理地址。
PTE
前面两种 PDT 基本包含了 PTE 的所有属性,PTE 主要变化就是在 4MB PDT 的基础上把 PAT
字段移动到了 PS
字段上(PTE 不需要 PS
字段)。
PAE 分页
在 32 位处理器中,若设置 CR0.PG = 1
(启用分页)且 CR4.PAE = 1
(启用物理地址扩展),则启用 PAE 分页模式,也就是我们常说的 2-9-9-12 分页模式。此时每个进程的线性地址空间仍为 4GB,但可访问的物理地址上限扩展到 64GB(36 位地址)。
页表结构
PAE 模式采用 3 级页表结构。在不考虑大页的情况下,PAE 分页模式的页表结构如下图所示。
名称 | 层级 | 数量与结构 | 每项大小 / 总大小 | 作用说明 |
---|---|---|---|---|
页目录指针表(PDPT) | 顶层结构 | 每进程 1 个,含 4 项 | 每项 8 字节,共 32B | 每项指向一个页目录,控制 1GB 线性空间 |
页目录表(Page Directory) | 一级结构 | 最多 4 个,每表含 512 项 PDE | 每项 8 字节,共 4KB | 每项指向一个页表,或直接映射 2MB 大页(需 PS=1) |
页表(Page Table) | 二级结构 | 最多 512 个,每表含 512 项 PTE | 每项 8 字节,共 4KB | 每项映射 4KB 物理页,提供最小的线性地址映射单元 |
地址转换过程
PAE 分页模式的线性地址到物理地址的转换过程如下图所示:
PAE 模式下将 32 位线性地址按如下方式拆分,即 2-9-9-12 分层结构。
Bits[31:30]
(2 位):选择 PDPT 中的页目录项(最多 4 项)Bits[29:21]
(9 位):选择页目录中的 PDE(最多 512 项)Bits[20:12]
(9 位):选择页表中的 PTE(最多 512 项)Bits[11:0]
(12 位):页内偏移(4KB)
如果是大页模式,则线性地址低 21 比特作为 2MB 大小物理页的页内偏移。
页目录自映射
PAE 分页模式下的页目录自映射(Page Directory Self-Mapping)机制也发生变化。由于 PDPTE 代替了 CR3 的功能,因此需要由第 4 个页目录表实现页目录自映射机制。该页目录表的前 4 项分别指向 4 个页目录表,因此在线性地址空间中,4 个页目录表以及对应映射的页表分布如下:
根据页目录自映射的特性可知,对应一个线性地址 p
:
- 对应的 PTE 的线性地址为:
0xC0000000 | ((p >> 12) * 8) = 0xC0000000 | (p >> 9 & 0x7FFFF8)
。(p >> 12) * 8
中右移 12 是在计算页框编号,也就是第几个 PTE;乘 8 是在计算页框号对应的 PTE 的偏移。这种计算方法是从页表分布上推导出来的。(p >> 12) * 8 = p >> 9 & 0x7FFFF8
,而右移 9 使得 32 位地址中最高 2 位放在2-9-9-12
中第一个 “9” 的范围内,从另一个角度印证了只需要第 3 个页目录表的前 4 个 PDE 就可以实现页目录自映射机制。
- 对应的 PDE 的线性地址为:
0xC0600000 | ((p >> (12 + 9)) * 8) = 0xC0600000 | p >> 18 & 0x3FF8
。
提示
- PAE 分页模式物理地址寻址增大不是因为分页层数增多,而是因为每一个页表项从 4 字节增加到 8 字节,导致物理地址字段长度增大。
- 新增页目录指针表(PDPT)的原因是每一个页表项从 4 字节增加到 8 字节,导致索引范围从 1024(10 bits)减小到 512(9 bits),空出了 2 bits。
- 页目录指针表(PDPT)的作用是代替非 PAE 模式的 CR3,从而避免利用页目录自映射(Page Directory Self-Mapping)机制从固定线性地址读取到 CR3 存储的值。
页表属性
CR3
在开启 PAE 之后,CR3 寄存器的字段发生了一些变化。
CR3 在 PAE 模式下,其 Bits[31:5] 存放 PDPTE 表的物理地址高位(因 32 字节对齐,所以低 5 位为 0)。
提示
在调试时我们可以通过
!process 0 0
输出的DirBase
是关于 0x20 还是 0x1000 对齐快速判断出当前是否是 PAE 分页模式。将 CR3 中的
PWT
和PCD
字段转移到了 PWPTE 中。
PDPTE
PDPTE 中主要承担了非 PAE 分页中的 CR3 的功能,唯一的区别就是 PDPTE 比 CR3 多了一个 P
标志位表示该 PDPTE 是否有效。
另外就是由于物理地址增大到 36 bits,因此页目录表地址字段增大到 36 - 12 = 24 bits。
PDE
对于 PS
位为 0 的 PDE,各字段分布基本和非 PAE 分页模式一致,只不过页表地址字段从原来的 20 bits 增加到 24 bits。
另外 PAE 模式的 PDE 新增了一个 XD
字段,即 Execute Disable(不可执行页)位。如果启用了 IA32_EFER.NXE
(No eXecute Enable),则可以使用该位禁用页的代码执行,用于实现 DEP(数据执行保护) 等安全特性。如果 NXE
没启用,该位为 保留位(必须为 0)。
IA32_EFER.NXE
是 Intel x86 架构中一个控制位,用于 开启“不可执行页”功能(Execute Disable,XD),位于IA32_EFER
(扩展功能使能寄存器) 的 第 11 位。
IA32_EFER
是一个寄存器,更准确地说,它是一个 Model-Specific Register(MSR,模型特定寄存器)。因为这个寄存器是 Intel 专门定义的一类控制寄存器,不像通用寄存器那样在汇编中直接访问。要通过RDMSR
和WRMSR
指令来读写。
对于 PS
位为 1 的 PDE,各字段分布基本和非 PAE 分页模式同样基本一致。不同点是由于大页的大小有 4MB 减半到 2MB,因此地址字段的起始位从 22 减小到 21。
PTE
PTE 除了内存页物理地址字段长度增加外基本无变化。
缓存
分页不仅仅是地址映射,它与 CPU 的缓存体系(L1/L2/L3 Cache)与 TLB(Translation Lookaside Buffer) 紧密配合,高效完成地址转换与缓存一致性管理。
flowchart TD %% ───── 1. 发起访问 ───── subgraph LA["线性地址"] A0["发起内存访问
(CPU 指令)"] end A0 --> B1[TLB 查询] %% ───── 2. TLB 分支 ───── B1 -->|命中| C0[得到物理页基址
+ 权限/属性
Write/Exec 检查] B1 -->|未命中| PWalk["Page Walk
(CR3→PDPTE→PDE→PTE)"] %% ---- Page Walk 子流程 ---- subgraph Page_Table["页表遍历 (硬件完成)"] PWalk --> P1[PDPTE
① 定位 1 GB 区域] P1 --> P2[PDE
② 定位 2 MB 区域
或继续页表] P2 --> P3[PTE
③ 定位 4 KB 页框] P3 --> PT_Exit[得到物理页基址] end PT_Exit --> UpdateTLB["写入/更新 TLB
(缓存映射)"] UpdateTLB --> C0 %% ---- 3. 权限检查 ---- C0 -->|权限允许| L1[L1 Cache 查询] C0 -->|权限不允许| PageFault[#PF → Page-Fault Handler] %% ---- 写时复制(COW) ---- PageFault -->|写时复制触发| DoCOW[COW:分配新页+复制+更新页表] %% 触发 invlpg,刷本虚拟页的 TLB 项 DoCOW -.invLPG(TLB flush).-> L1 %% 非法访问 PageFault -->|非法访问等| Kill[进程终止 / 抛异常] %% ---- 4. Cache → DRAM ---- L1 -->|命中| Return[返回数据] L1 -->|未命中| L2[L2 Cache 查询] L2 -->|命中| Return L2 -->|未命中| L3["L3 (LLC) 查询"] L3 -->|命中| Return L3 -->|未命中| DRAM[主存 DRAM 访问] DRAM --> FillCache["加载到 L1
(并按一致性写入 L2/L3)"] FillCache --> Return %% ---- 样式:仅热点节点上色(不含 DRAM) ---- classDef hot fill:#d5f5d5,stroke:#2c6a21; class C0,L1,L2,L3,FillCache,Return hot;
CPU 缓存层级结构(L1 / L2 / L3)
CPU Cache 是 CPU 内部用于缓存主存(DRAM)数据的硬件单元,用于提升访问频繁数据的速度。主要分为多级:
缓存级别 | 延迟(大致) | 每核独立? | 存什么? | 特点 |
---|---|---|---|---|
L1 Cache | 1~3 cycle | ✅ 是 | 指令 / 数据 | 最快 |
L2 Cache | 10~20 cycle | ✅ 是 | L1 的后备 | 大 |
L3 Cache | 40~70 cycle | ⛔ 否,共享 | 所有数据(共享页表) | 更大更慢 |
这些缓存用于存储「最近访问的内存数据」,而数据来自「物理地址」,也就是说 CPU Cache 缓存的是“物理地址 → 数据”的映射。
缓存类型
内存的 缓存类型(Cache Type) 决定了 CPU 如何对某块物理内存进行缓存操作,包括:是否缓存、以何种策略缓存、如何写入内存等。缓存类型不仅影响性能,还关系到设备访问的一致性和可靠性(例如访问显存、MMIO 寄存器时必须禁止缓存)。
PAT 编码 | MTRR 编码 | 名称(缩写) | 中文释义 | 特征说明 |
---|---|---|---|---|
00h |
06h |
Write-Back (WB) | 写回缓存 | 默认策略。读写均使用缓存(L1/L2/L3);写操作只更新缓存,延迟刷新主存(由缓存一致性机制控制)。性能最佳,支持预取、读写合并、乱序执行等,适用于普通主存数据访问。 |
01h |
04h |
Write-Through (WT) | 直写缓存 | 写操作同时写入缓存和主存,保证主存数据始终最新;读操作仍可命中缓存。适用于对主存一致性要求高的区域,如共享内存。写性能低于 WB。 |
02h |
07h |
Uncached Minus (UC-) | 弱无缓存 | Intel 专有类型。行为类似 UC,但允许有限度的预取和请求合并。部分 CPU 可能仍使用缓存机制优化访问。适合性能敏感但对一致性要求略低的设备地址映射区域。 |
03h |
00h |
Uncacheable (UC) | 不可缓存 | 完全禁用缓存 :所有访问直接访问主存,不使用缓存、不支持预取、合并、乱序。适用于 MMIO、设备寄存器、显存帧缓冲等 对时序要求严格的场景。 |
04h |
01h |
Write-Combining (WC) | 写合并 | 不缓存读操作,但写操作可合并多个写入请求后批量写入主存。适用于图像缓冲区、视频帧缓冲、DMA 区域等大吞吐场景。写性能提升显著,但不保证一致性。 |
05h |
05h |
Write-Protected (WP) | 只读缓存 | 只读可缓存,但所有写操作必须访问主存(写不命中缓存)。适用于代码段或受保护数据页,防止意外修改或提升写安全性。 |
07h |
00h |
UC(冗余编码) | 与 UC 等效 | 与 03h 完全等价,行为与 UC 一致,出于兼容性考虑保留此编码。部分 BIOS/固件使用该编码表示 UC。 |
我们常见的几种内存的缓存机制如下:
区域 | 推荐类型 | 原因 |
---|---|---|
显卡显存(MMIO) | UC / WC |
避免缓存污染 / 提高写效率 |
页表 / CR3 | WT / UC |
保证一致性 |
普通内核代码页 | WB |
高效指令读取 |
DMA 缓冲区 | UC / WC |
避免设备与 CPU 缓存不一致 |
用户态栈 / 堆 | WB |
高频读写,性能关键 |
缓存类型的配置机制
缓存类型是 多个机制共同决定的结果,包括:
来源 | 描述 |
---|---|
页表项的 PCD / PWT / PAT 位 | 控制页级别的缓存策略 |
IA32_PAT 寄存器 | 定义 PAT 编码与缓存类型的映射 |
MTRR(Memory Type Range Register) | 控制物理地址范围的默认缓存类型 |
固定类型内存(如设备寄存器) | 固定为 UC(不可缓存)或 WC(写合并)等类型 |
每个页表项(PDE/PTE)都有这三位控制缓存:
位名 | 含义 | 取值 1 的效果 |
---|---|---|
PWT |
Page-level Write-Through | 采用 Write-Through 策略 |
PCD |
Page-level Cache Disable | 禁用缓存(Uncached) |
PAT |
Page Attribute Table | 用于选择 PAT 中的映射项(解释见下) |
这三位组成一个 3 位索引(b2b1b0 = PAT,PWT,PCD),共可表示 8 种编码(0~7),再由 IA32_PAT
映射到缓存类型。
编号(索引) | 对应组合(PAT:PWT:PCD) | 默认缓存类型(如 IA32 手册) |
---|---|---|
0 | 0:0:0 | Write-Back (WB) |
1 | 0:0:1 | Write-Through (WT) |
2 | 0:1:0 | Uncached Minus (UC-) |
3 | 0:1:1 | Uncached (UC) |
4 | 1:0:0 | Write-Combining (WC) |
5 | 1:0:1 | Write-Protected (WP) |
6 | 1:1:0 | 保留 |
7 | 1:1:1 | UC(冗余) |
最终使用哪种类型由 CPU 按一定优先级解析这些机制的组合。
1 | 1. 固定类型区域(如 ROM/显存)? |
TLB(Translation Lookaside Buffer)
TLB 是一种「地址映射的缓存」,专门缓存页表翻译(虚拟 → 物理)。
TLB 属性
通常 TLB 中的每一项包含如下属性:
LA
:线性地址(Linear Address)PA
:物理地址(Physical Address)ATTR
:权限与属性字段(如 R/W, U/S, PWT, PCD, PAT 等)- 非 PAE(
10-10-12
)下:ATTR = PDE 属性 & PTE 属性
- PAE(或 4级页表,如
9-9-9-12
)下:ATTR = PDPTE 属性 & PDE 属性 & PTE 属性
- 非 PAE(
LRU
:最近最少使用(Least Recently Used)统计信息,TLB替换策略之一
这些都是标准硬件 TLB 中会缓存的内容,尤其现代 CPU 会把 PTE/PDE/PDPTE 的属性字段也缓存进去,加快访问判断速度,避免再次查页表。
当 TLB 未命中时 → 需要查完整页表 → 增加延迟 → 加载结果再写入 TLB。
TLB 类型
在典型的 x86 CPU(从 Intel 486 起)中,为了提高虚拟地址到物理地址转换的效率,CPU 内部通常设置多组 TLB 缓存,按用途和页大小分为:
- Instruction TLB for 4KB pages :用于缓存指令取值所需的 4KB 页映射;
- Data TLB for 4KB pages :用于缓存数据访问所需的 4KB 页映射;
- Instruction TLB for large pages (2MB/4MB) :用于缓存代码段中的大页映射;
- Data TLB for large pages (2MB/4MB) :用于缓存数据段中的大页映射;
在现代 CPU 架构中,上述 TLB 有可能是多级的(L1/L2),并支持更多页大小(如 1GB),具体组织方式视 CPU 实现而定。
TLB 控制位
标志位 | 寄存器 | 作用 |
---|---|---|
CR4.PGE | CR4[7] | 开启全局页(Global Page)支持;全局页不因 CR3 切换被刷新 |
PDE/PTE.G | 页表项 bit 8 | 与 CR4.PGE 配合,标记页项为全局页,避免频繁清除 |
CR4.PCIDE | CR4[17] | 启用 PCID 支持,配合 CR3 实现不清 TLB 的上下文切换 |
TLB 刷新方式
TLB(Translation Lookaside Buffer)是一个 缓存虚拟地址到物理地址映射 的结构(即页表的副本)。当页表发生变更后,TLB 中的内容可能过时,这时候就必须刷新 TLB,防止地址转换错误。
常见的 TLB 刷新方式如下:
写入
CR3
(页目录基地址)寄存器 是最常见的刷新方式,不论 CR3 的值是否真的改变,CR3 寄存器写入都会导致 TLB 被清空(传统 x86)。不过这种方式性能损耗较大,因为所有地址映射都要重新查页表。如果禁用CR4.PGE
,G 页也会被清除。1
2mov eax, cr3
mov cr3, eax ; 触发完整 TLB flush局部刷新(invlpg 指令)只刷新一个页(以页为单位),但是可以无视
G
标志位强制刷新。1
invlpg [0x8048000] ; 使虚拟地址 0x8048000 的 TLB 项失效
- Title: windows 保护模式
- Author: sky123
- Created at : 2022-09-28 11:45:14
- Updated at : 2025-07-01 10:19:52
- Link: https://skyi23.github.io/2022/09/28/windows 保护模式/
- License: This work is licensed under CC BY-NC-SA 4.0.