Windows保护模式学习笔记

Windows保护模式学习笔记

最近复现游戏安全赛题的时候频频遇到内核编程中的问题,不是反调试过不去就是自己编写的驱动总是蓝屏,加上腾讯实习一面的时候问有关这方面的东西,回答的时候也是支支吾吾记得不是很清楚,想到是之前学习windows保护模式只是粗略的看一眼的原因,实在是感觉概念太多,生涩难啃,不管是Q神的博客还是火哥内核讲的,总是看个好几遍还是看不进去,索性就跳过了这一部分,现在想静下心来好好把这部分之前落的啃完并总结输出出来加深印象

Windows 保护模式学习笔记(一) —— 段选择子&段描述符

在最开始的时候我很困惑,为什么要谈到保护模式,要讲段和页?其实如果引入x86段页的由来的话,可能就更容易理解一点,首先x86还在在十六位的时候,由于那会没有段页的概念,自然也没有当前的虚拟内存和物理内存的说法,所以程序都运行在绝对的物理地址上,这样的话应用模块和内核模块就没有明显的区分,在应用层也能访问到内核层的地址,那对一个操作系统来讲那是毁灭性的。所以在80386CPU出现的时候就引入了段页的概念,段主要用来做权限的划分页主要用来构造一个虚拟内存空间,可以让物理地址对用户不可见,保护内核空间安全

0x01 段选择子

image-20241012084156549

在最开始学习汇编指令的时候,主要用到的寄存器有DS,SS,CS,ES,FS,主要作用是用来寻址,通过ds:[0x1234]等指令来访问内存,并告诉我们
$$
线性地址 = 段地址*0x10 + 逻辑地址
$$
但是根据我们调试会发现段寄存器的值往往不代表段地址,那么段地址是从哪来的呢?段权限又是哪来的呢?这就需要引入段选择子的概念了,段选择子其实就是我们所看到的段寄存器的值002B,0023等,以下是它的结构

image-20241012084040373

在段选择子我们可以找到三个值index、TI、RPL,RPL指的是请求特权级别,本质上只能降权访问某个段,没办法从这提权,用处不是很大,index好理解,索引嘛,那是什么的索引呢?这就还要引入GDT和IDT两个表的概念

0x02 GDT表和LDT表

GDT(Global Descriptor Table,全局描述符表)和LDT(Local Descriptor Table,局部描述符表)是在x86架构中使用的两种描述符表,用于定义内存段的属性和访问权限。GDT是全局性的描述符表,它存储了系统中所有任务共享的段描述符。GDT在系统启动时被加载到GDTR(GDT寄存器),全局有效,可由所有任务和进程共享。它适用于操作系统内核代码、全局共享的库和驱动程序等。

LDT是每个任务(进程)独有的描述符表,它存储了每个任务独有的段描述符。每个任务最多只能有一个自己的LDT,用于定义任务私有的内存段属性。但是因为在IA-32架构下,一个时间点只能有一个任务,所以LDT的寄存器LDTR全局也只能有一个,在多任务操作系统中,CPU需要在不同的任务之间切换,当切换发生时,CPU会保存当前任务的状态,并加载新任务的状态。这个状态包括段寄存器,如CS、DS、SS等,以及LDTR。这样,每个任务都有自己的执行环境。

GDT和LDT都可以通过系统段寄存器的值来获取

1
2
r gdtr	
r ldtr

0x03 段描述符

有了GDT或者LDT表和index之后,每一个段选择子就会对应一个段描述符,描述段权限的属性正是在段描述符里,这个其实才是呈现段寄存器主要功能的结构,段寄存器正是由段选择子和段描述符所构成,通过段选择子的索引找到段描述符,从而就可以找到诸如基址和当前权限等属性。

image-20241012090553228

解释一下这几个属性的概念:

  1. Segment Limit:这个是描述段界限的属性,在描述符里由两块Limit组合而成,当用ds:[0x1234]访问内存的时候,0x1234一定要在段界限之内,如果Segment Limit只有0x1000,那么程序就会产生报错,如果一定要访问这个内存的话,就需要跨段访问,这个后面会有。

  2. BaseAddress:这个是描述段基址的属性,32位下是4字节的大小,在描述符里是由三块Base组合而成。

  3. Type:这个是描述段类型的属性,每一位的意义分别代表着代码段/数据段,可/不可执行,可/不可写,可/不可读

1586953-20220204192318064-466670764
4. S:这个是段描述符的类型,用来区分是否为系统段,系统段包括但不限于以下几种。

- **GDT(Global Descriptor Table)**:全局描述符表,存储全局可用的段描述符。

- **LDT(Local Descriptor Table)**:局部描述符表,用于存储特定于某个任务或进程的段描述符。

- **TSS(Task State Segment)**:任务状态段,用于存储任务切换时需要保存和恢复的状态信息。

- **IDT(Interrupt Descriptor Table)**:中断描述符表,用于存储中断和异常处理程序的入口点。
  1. DPL:这个是段描述符当前的权限级别,分别有0,1,2,3四个值,0代表零环权限,3代表三环权限,后续提权需要用到这个属性。需要注意的是CPL指的是CS段的DPL,也就是说CS段的DPL代表进程的特权级,而数据段的DPL就算属于0环权限也并非提权了,提权的本质是代码段的提权。
  2. P:这个是段描述符是否可用的属性,如果为0则该段描述符不可用,访问该段时会发生报错
  3. AVL:留给操作系统自由使用的位,具体使用方式取决于操作系统的实现。没什么用……
  4. D/B:这个是段描述符用于指明段中指令引用有效地址和操作数的默认长度, 主要是用于提供向后兼容性,让32位系统可以兼容16位系统。
  5. G:这个是段描述符的粒度,主要作用是确定段限长(Limit)字段的单位和解释方式。如果G为0,这意味着段的最大长度可以从1字节到1兆字节(1MB);如果G位1,段限长(Limit)字段以4KB为单位。这允许段的最大长度从4KB到4GB。

0x04 实验验证

这里验证一下Base,首先通过在GDT表的0x4B的位置粘贴一份ds段描述符,然后通过将base值加1,看看value2是否还会等于0x100,

image-20241012195927004

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stdafx.h"
#include <Windows.h>

int value = 0x100;
int _tmain(int argc, _TCHAR* argv[])
{
int value2 = 0;
__asm
{
mov ax,0x4b;
mov ds,ax;
mov eax,dword ptr ds:[value];
mov dword ptr ss:[value2],eax;
mov ax,es;
mov ds,ax;
}
printf("value2 = %x\r\n",value2);
system("pause");
return 0;
}

程序原本结果应为0x100的,但是由于ds段的基址为1,ss段基址为0,所以value2得到的值只有0x01,前面的1为内存里的垃圾值。

image-20241012200221408

Windows 保护模式学习笔记(二) —— 调用门&中断门&陷阱门

前面讲到了段是权限的划分,在三环下没办法直接通过ds:[0xxxxxxx]寻址来直接访问内核内存,那么操作系统又是怎么通过修改段权限进入内核态执行内核代码呢?这就需要引入调用门,中断门,陷阱门这个三个门的概念。调用门、中断门和陷阱门是操作系统和硬件体系结构中的重要机制,它们被设计出来是为了提供安全、有效的特权级转换和异常处理。

先给出一张图,这个图描述了当段描述符的S属性为0,即该段为系统段时,type的值会对应不同的含义,比如说S = 0,type = 0x0c时,该段描述符指向的是调用门。

image-20241013010129927

0x01 调用门

设计目的:实现从低特权级(如用户态)到高特权级(如内核态)的受控调用。

  • 安全性:通过调用门,系统可以安全地从用户态切换到内核态,防止直接跳转到内核代码的任意位置,从而保护内核的完整性。
  • 控制转移:调用门允许用户程序在执行特定的系统调用时,进入内核态执行预定义的内核例程。
  • 参数检查:调用门可以提供参数检查机制,以确保传递给内核例程的数据是合法的。

那怎么进入这个门里面呢?这就需要用到前面的段描述符的属性DPL了,只有当CS.DPL == SS.DPL == 调用门.DPL,且S == 0,Type == 0x0c时才会进入调用门之中,后面好理解,是调用门的特征属性,前面的CS.DPL为什么要等于调用门.DPL呢?我们不是要提权么,不是要让DPL变成0么?

这个就需要理解“门”的概念,门是通往另一个空间的方式,并不代表另一个空间,它是操作系统提供的一种跨入零环的工具,里面包含着去往零环的通道,只有满足特定条件,也就是找到这个门,才能进入零环。那么条件是什么呢?三环进零环的的时候,首先你得是三环的段吧,也就是说DPL == CPL == 0x03,其次就是Type和S属性,满足这些你就能进入调用门找到零环的段选择子(Segment Selector)和 段偏移,从而进入到零环空间。

img

代码实现

首先将0x48处的段描述符修改成符合调用门的值,比如这里需要跳进test()里读取内核内存,而test的地址为0x00401000,那么调用门的值就应该为:0040ec00`00081000,修改gdt表后运行即可提权。

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
#include "stdafx.h"
#include <Windows.h>

int xxx;
void _declspec(naked)test()
{
__asm
{
mov eax,dword ptr ds:[0x8404c9dc]; // nt!NtOpenProcess地址
mov xxx,eax;
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{

char bufcode []={0,0,0,0,0x48,0};
printf("function addr = %x\r\n",test);
__asm
{
call fword ptr bufcode;
}
printf("%X\n",xxx);
system("pause");
return 0;
}

堆栈变化

在跨段调用提权的时候,不仅是CS段发生了改变,其实SS段也发生了改变,进入内核空间后会有一个新的栈来供程序使用,这个新的SS段描述符往往在CS段描述符的索引的下一位。下面是调用门前后的堆栈变化实验结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
//进入调用门之前
EAX = 00000018 EBX = 7FFD5000 ECX = 7CCE2466 EDX = 6AF81408 ESI = 0012FE50 EDI = 0012FF30 EIP = 0040106C ESP = 0012FE50 EBP = 0012FF30 EFL = 00000246
CS = 001B DS = 0023 ES = 0023 SS = 0023 FS = 003B GS = 0000
//进入调用门之后
eax=00000018 ebx=7ffd5000 ecx=7cce2466 edx=6af81408 esi=0012fe50 edi=0012ff30
eip=00401000 esp=9a7d3ca0 ebp=0012ff30 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000246
//进入调用门的栈
kd> dd esp
9a7d3ca0 0040106f 0000001b 0012fe50 00000023
9a7d3cb0 00000000 00000000 00000000 00000000
9a7d3cc0 0000027f 00000000 00000000 00000000
9a7d3cd0 00000000 00000000 00001f80 0000ffff

可以看到进入内核前向栈压入了四个值,先后依次是ss、esp、cs、eip,这也就是为什么在test函数时用RETF来返回,因为执行RETF指令时,处理器从堆栈中弹出两个值:返回地址(通常是指令指针IP或EIP)和代码段选择子(CS)。这两个值一起决定了返回的确切位置,即返回的代码段和段内偏移地址。

0x02 中断门

设计目的:处理硬件中断。

  • 硬件中断响应:当硬件设备(如键盘、网卡等)发出中断信号时,中断门允许处理器立即停止当前执行的任务,转而处理中断服务例程(ISR)。
  • 特权级提升:中断门可以提升特权级(通常从用户态提升到内核态),以确保中断服务例程在内核态下运行,能够访问所有系统资源。
  • 原子性和保护:中断门确保中断处理在受保护的环境中进行,防止中断处理例程被其他中断打断。

中断门和调用门的结构类似,只是Type在32位下变成了1110。触发方式和堆栈变化也发生了一点变化,中断门依赖int 0xxx来触发,0xxx的值是idt表的索引,跟调用门的call far 0xxx:[0x1234]类似。

代码实现

和调用门一样的先设置idt表索引为32的值为:0040ee00`00081000,运行代码即可提权。

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
#include "stdafx.h"
#include <Windows.h>

int xxx = 0;
void _declspec(naked)test()
{
__asm
{
int 3;
mov eax,dword ptr ds:[0x8404c9dc];
mov xxx,eax;
iretd;
}
}

int _tmain(int argc, _TCHAR* argv[])
{

printf("function addr = %x\r\n",test);
__asm
{
push fs;
int 32;
pop fs;
}
printf("%X\n",xxx);
system("pause");
return 0;
}

稍微需要解释一下的就是IRETDpush fs\pop fs这两条指令,首先是IRETD,它和IRET类似,都会从堆栈中弹出EIPCSEFLAGS、如果导致了特权级变化,还会弹出ESPSS,区别在于IRETD是用于32位保护模式的,IRET是用于16位实模式或保护模式的。push fspop fs则是为了保存fs寄存器的值,在中断的时候由于使用了FS寄存器,需要显式保存和恢复FS寄存器的值。

堆栈变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
EAX = 00000018 EBX = 7FFD8000 ECX = 3A416E83 EDX = 661F1408 ESI = 0012FE50 EDI = 0012FF30 EIP = 0040106E ESP = 0012FE4C EBP = 0012FF30 EFL = 00000246 

CS = 001B DS = 0023 ES = 0023 SS = 0023 FS = 003B GS = 0000


kd> r
eax=00000018 ebx=7ffd8000 ecx=3a416e83 edx=661f1408 esi=0012fe50 edi=0012ff30
eip=00401000 esp=a600fc9c ebp=0012ff30 iopl=0 nv up di pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000046


kd> dd esp
a600fc9c 00401070 0000001b 00000246 0012fe4c
a600fcac 00000023 00000000 00000000 00000000
a600fcbc 00000000 0000027f 00000000 00000000
a600fccc 00000000 00000000 00000000 00001f80
a600fcdc 0000ffff 00000000 00000000 00000000
a600fcec 00000000 00000000 00000000 00000000
a600fcfc 00000000 00000000 00000000 00000000
a600fd0c 00000000 00000000 00000000 00000000

可以看到进入内核前向栈压入了四个值,先后依次是ss、esp、eflag,cs、eip

0x03 陷阱门

设计目的:处理软件生成的异常和陷阱。

  • 软件异常处理:陷阱门用于处理由程序执行引发的异常(如除零错误、无效操作码等)或软件中断(如系统调用)。
  • 一致的异常处理机制:通过陷阱门,操作系统可以提供一致的异常处理机制,确保在特定的异常发生时,系统可以转移到适当的处理代码。
  • 调试支持:陷阱门可以用于调试目的,例如处理断点和单步执行。

陷阱门的类型码为0111,陷阱门和中断门极其相似,区别仅仅在于中断门在执行时会清楚EFLAGS寄存器的IF标志位,而陷阱门不会,这意味着在中断服务程序期间,其他可屏蔽中断将不会处理,从而防止中断嵌套,而陷阱门不会改变IF标志位,允许中断嵌套发生,所以陷阱门更适合处理软件引起的异常(除零等),而中断门更适合处理硬件引起的中断。这里就不做实验演示了,只需要和中断门执行的时候对比一下IF标志位即可。

Windows 保护模式学习笔记(三) —— 任务状态段&任务门

在之前我们通过三种门来提权进入到零环的时候,会提前将esp等寄存器的值压栈以保存返回地址和维护栈帧,但是其他寄存器的值呢?为什么没有保存?并且在我们进入零环的时候,会有新的esp,这个新的esp的值又是从哪里来的?

0x01 任务状态段

这就需要引入TSS(Task State Segment)的概念,我们先看一下TSS的结构

image-20241015112107862

可以看到TSS里保存着一个任务运行时CPU需要的环境,即各个寄存器,这里包括了ESP1,ESP2,ESP0等,指的是各个特权级别下的栈指针(ESP0, ESP1, ESP2)和栈段(SS0, SS1, SS2)。当发生中断时,如果需要切换到不同的特权级别,处理器会从当前任务的TSS中读取新的栈段和栈指针,并将它们加载到SS和ESP寄存器中。这样,当中断处理程序开始执行时,它将使用正确的栈。此外,TSS还保存了其他寄存器的值,如EAX, ECX, EDX, EBX, EBP, ESI, EDI,以及EFLAGS、EIP等。在任务切换时,这些寄存器的值会被保存在TSS中,以便任务恢复时能够继续执行。

任务状态段存储在哪呢?

TSS本质上是一个内存块,在x86架构中,TSS的位置是通过任务寄存器(Task Register,TR)来访问的。TR寄存器存储了TSS的段选择子,这个选择子指向全局描述符表(GDT)中的一个条目。这个GDT条目包含了TSS的段描述符,而段描述符中包含了TSS在物理内存中的基地址和限长等信息。

所以我们可以通过TSS段描述符来找到TSS内存块,下面是TSS段描述符的结构

image-20241015123417074

Type中的B为0时,表示该TSS段描述符未被加载到TR段寄存器中,Type中的B为1时,表示该TSS段描述符已经被加载到TR段寄存器中(全局只有一个TR寄存器,但是有多个TSS)

0x02 任务门

任务门被设计出来是为了实现多任务操作系统的任务切换功能,前面提到的TSS切换就可以通过执行任务门来实现,也就是说任务门提供了一个方式去改变TSS,从而让操作系统执行一个新的任务。那么任务门是怎么找到新的TSS的呢?下面是任务门描述符结构体

Task_Gate_Descriptor

在结构体中我们可以看到描述符里含有一个TSS段描述符的选择子,这就是任务切换的时候能找到新的TSS段描述符的原因,即通过任务门来访问到新的TSS。

P位:任务门的P位指示该门 是否有效 ,p=0时,不允许使用此门实施任务切换;

任务门是怎么触发的呢?

在中断门的情况下,当执行中断指令(如int)时,处理器会查找中断描述符表(IDT)中的相应门描述符。如果这个门描述符指向一个任务门,那么处理器会使用任务门中提供的TSS选择子来找到新的TSS。然后,处理器会将当前任务的状态保存到当前TSS中,并从新的TSS中加载状态,以准备执行新任务。这个过程通常涉及到IRETIRETD指令来完成返回操作,这些指令会从堆栈中恢复SS、ESP、EFLAGS、CS和EIP的值,以便返回到被中断的任务。

Windows 保护模式学习笔记(四) —— 10-10-12分页&页目录表

在保护模式学习的开始,我们提到了80386CPU时引入段页的概念,并顺便提了页的一个主要作用,可以用来构造一个虚拟内存空间,让物理地址对用户不可见,保护内核空间安全。那么页是怎么实现这个功能的呢?接下来我们要解析Windows是怎么通过页,实现内核地址空间和用户地址空间的隔离,并且通过实验利用windbg从某个进程的虚拟地址找到对应的真实物理地址。

0x01 10-10-12分页

为了实现内存保护和扩展功能,Windows设计了很多种分页模式,包括两级分页,四级分页,五级分页等等,四级分页和五级分页多用于64位系统,这个以后会学到。先介绍一下32位系统下的10-10-12分页模式,包括:

  • 页目录(Page Directory):包含多个页目录项(PDE),每个项指向一个页表。
  • 页表(Page Table):包含多个页表项(PTE),每个项指向一个物理页面。

在10-10-12分页模式下对应的PDE数量是2^10个,PTE的数量也是2^10个,这也就是为什么32位下的程序拥有4G的虚拟内存,一页是4K(2^12)大小。

用图片更容易解释这两者的关系

PhysicalPage

图中描述了通过CR3->PDT(PDE)->PTT(PTE)->物理页,找到最终的物理内存地址,涉及了一个寄存器和三个偏移量——通过CR3寄存器找到PDT表,通过PDT的第一个偏移找到页目录PDE,然后再通过PTT的第二个偏移找到页表项PTE,最后通过第三个偏移(物理页内偏移)找到物理地址。

这三个偏移量怎么来的?

在10-10-12分页模式下,32位的地址被划分为三个区域,每个区域对应不同的偏移量,比如说0x4012345的线性地址就会被划分为0x400,0x123,0x456三个区域,对应的分别是PDE的索引,PTE的索引,以及物理页内偏移。

转换过程:

1
2
3
4
5
40123456
0100 0000 0001 0010 0011 0100 0101 0110
0100000000:0x400 -- 10
0100100011:0x123 -- 10
010001010110:0x456 -- 12

CR3寄存器指的是什么?

CR3寄存器是一种控制寄存器,用于存储页目录表的物理地址是相对于进程的概念,每一个进程拥有一个CR3寄存器。在x86架构的操作系统中,分页机制被用来将虚拟地址映射到物理地址。为了实现这种映射,需要使用页表和页目录表来管理地址转换。

CR3寄存器存储了页目录表的物理地址,通过改变CR3寄存器的值,可以实现不同的虚拟地址空间之间的切换。当处理器执行访问内存的指令时,它会将虚拟地址发送给内存管理单元(MMU)。MMU会根据当前CR3寄存器中存储的页目录表地址进行地址转换,将虚拟地址转换为物理地址,并完成内存的访问

在windbg里通过!process 0 0得到进程的DirBase是进程页目录表的物理地址,也是CR3寄存器的值

0x02 页表项属性

对于PTE和PDE,每个表项的属性如下

PDE和PTE属性

可以看到,不管是PDE还是PTE,后面的12位表示的是改页表/页的属性,前面的20位则代表页表/页基址,比如我们要解析某个PDE时,举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
PDE = 0x71282867
baseAddress = 0x71282000
0x867 -- 1000 0110 0111
avail:100 -- 留给软件使用后
G:0 -- 忽略
PS:0 -- 页大小
RESERVERD:1 -- 保留
A:1 -- 被执行过
PCD:0 -- 当PCD位设置为1时,表示禁止将该页写入缓存,所有的写操作都会直接写入物理内存,当PCD位设置为0时,表示允许将该页写入缓存,这是一种默认设置
PWT:0 -- 跟缓存相关,当PWT位设置为1时,表示写操作会同时更新缓存和物理内存,当PWT位设置为0时,表示写操作仅更新缓存
U/S:1 -- 用户还是管理员可执行的内存
R/W:1 -- 可不可写,1可写
P:1 -- 有效位

PTE也同理。

0x03 页目录表基址

由于应用层CR3寄存器的不可见性,我们不能直接访问CR3寄存器获取页目录表基址,从而无法直接访问和修改物理内存。但是我们可以通过某种方式从线性地址访问到页目录表基址,比如说在线性地址C0300000进行10-10-2转换时是这样的

1
2
3
4
0xC0300000
1:0x300
2:0x300
3:0x000

然后访问该线性地址指向的PDT表

image-20241015233423699

发现线性地址C0300000指向的PDT表和进程实际的PDT表完全相同。所以,我们可以通过线性地址C0300000来访问真实的页目录表,即得公式:目录表项 = 线性地址C0300000 + index*4

0x04 页表基址

同理,直接给出结论:页表项 = 线性地址C0000000 + index*4

Windows 保护模式学习笔记(五) —— 2-9-9-12分页&TLB

由于10-10-12分页模式下的一个进程能获得的虚拟地址范围最多是4G,随着软件的迭代更新,4G的内存已经无法满足需求,于是需要设计新的分页模式来满足更大的内存需求,便有了2-9-9-12分页模式,又被称为PAE(物理地址扩展)分页。

0x01 2-9-9-12分页

开启方式

CR4中的物理地址扩展(PAE)标志可以开启PAE机制, 将物理地址从32位扩展至36位。处理器提供额外的4个地址线引脚来容纳这额外的地址位。 为了能使用这个选项,必须设置如下的标志:

  • CR0寄存器中的PG标志(位 31)-开启分页

  • CR4寄存器中的PAE标志(位5)置位,开启PAE分页机制

页表等结构的变化

当开启PAE分页机制时,处理器支持两种尺寸的页:4KB和2MB(2MB == 4KB*(2^9))。当使用32位寻址时,这两种尺寸的页都能够使用同一个页表集来寻址(也就是说,一个页目录项可以指向一个2MB的页, 也可以指向一个页表,这个页表的表项指向4KB的页)。要支持36位的物理地址,分页的数据结构需要做如下的变化:

  • 页表项将变为64位以适应36位物理地址。每个4KB页的页目录和页表也就可以有最多 512个表项了。

  • 线性地址变换的层次中,一个叫做页目录指针表(PDPT)的新表将被加入。这个表有4个64位的表项(PDPTE)。在线性变换的层次中,这个表在页目录之上。随着物理地址扩展机制的开启, 处理器支持4个页目录。

  • 寄存器CR3(PDPR)中20位的页目录基地址被27位 的页目录指针表基地址所替代(此时,寄存器CR3叫做PDPTR)。这个域给出了页目录指针表基地址的高27位, 这就迫使页目录指针表的地址是32字节对齐的。

  • 线性地址变换允许将32位的线性地址映射到更大的物理地址空间中。

PDPTR

0x02 线性地址变换(4KB页)

可以从CR3寄存器的前27位拿下我们的PDPT基址,也就是是!process 0 0得到的进程DirBase,从这个依次得到PDPTE->PDE->PTE->Offset,对应的图示如下。

PAE下的线性地址转换

0x03 线性地址变换(2MB页)

这个就是将PTE和offset合成一个Offset,让Offset占用12+9=21位,然后少了一个映射过程,过程图示如下。

PAE下的线性地址转换(2MB)

0x04 页表项属性

和10-10-12的页表项属性大差不差,无非是拓宽成64位,然后将前28位保留,12-35位作为下一个表的基址,后面接着属性位…

image-20241023020923360

页目录项中的页尺寸标志(位7),可以判断该表项指向一个页表还是指向一个2MB的页。当 该标志被清零时,表项指向一个页表;当该标志被置位时,表项指向一个2MB的页。上图中展示的是4KB,所以置0。这个标志使得4KB和2MB的页在一个页表集合中混用。

0x05 TLBS

处理器将最近使用过的页目录和页表项存放在on-chip的缓存中,这种缓存叫做转换后备缓冲区或者TLBs。大多数分页操作都是使用TLBs中的内容来进行的。仅当请求页时,进行地址转换所需的信息并不在TLBs中时,才需要为访问内存中的页目录和页表而花费总线周期。

每当装载CR3寄存器时,所有的(非全局的)TLB都自动失效(除非某个页或者页表项的G标志被置位),有两种方式来装载CR3寄存器:

  • 显示的,使用MOV指令,比如: MOV CR3,EAX 在这里EAX寄存器包含有一个适当的页目录基址。

  • 隐式的,通过执行任务切换来自动改变CR3寄存器的内容。

也可以用INVLPG指令使某个在TLB中的页表项失效的。

Windows 保护模式学习笔记(六) —— 控制寄存器&&系统指令总汇

0x01 功能

控制寄存器(CR0、CR1、CR2、CR3和CR4)决定了处理器的运行模式和当前正在执行的任务的特征,具体如下:

  1. CR0-包含系统控制标志,这些标志控制着处理器的运行模式和状态。
  2. CR1-保留
  3. CR2-包含缺页的线性地址(引起缺页的线性地址)
  4. CR3-包含了页目录的基地址和二个标志(PCD和PWT)。该寄存器也被称为页目录基地址寄 存器(PDBR)。页目录基地址只有高20位确定,低12位是0,所以页目录地址必须是页边界对齐的(4K字节)。PCD和PWT标志控制着页目录在处理器内部数据缓冲区的缓存(它们不控制TLB页目录信息的缓存)。当使用物理地址扩展时,CR3寄存器包含了页目录指针表的基地址。
  5. CR4-包含了一组标志,这些标志启用了架构方面的几个扩展,并指明了系统对某些处理器支持的能力。这个控制寄存器可以通过用MOV指令“从寄存器读或者写到寄存器”的方式进行读取或者装载(修改)。在保护模式下,MOV指令允许读取或者装载控制寄存器(在0级特权下)。这个限制意味着应用程序或者操作系统过程(运行在1、2、3级特权下)不能读取或者装载控制寄存器。装载控制寄存器时,保留位应该保持以前读取的值。

0x02 属性

image-20241023024602565

各个标志位对应的属性如下:

CR0(控制寄存器 0)

  1. PE(保护启用,位 0)
    • 0: 实模式
    • 1: 保护模式
  2. MP(监视协处理器,位 1)
    • 0: WAIT 指令没有影响
    • 1: WAIT 指令会检查 CR0 中的 TS 标志位
  3. EM(仿真,位 2)
    • 0: 启用 FPU(浮点运算单元)
    • 1: 禁用 FPU,并在软件中模拟 FPU
  4. TS(任务切换,位 3)
    • 0: 没有任务切换
    • 1: 表示任务切换发生
  5. ET(扩展类型,位 4)
    • 0: 处理器是 80287 协处理器
    • 1: 处理器是 80387 或以上的协处理器
  6. NE(数值错误,位 5)
    • 0: 通过 I/O 地址 0xF0 检查 FPU 错误
    • 1: 通过内部信号检查 FPU 错误
  7. WP(写保护,位 16)
    • 0: 超级用户可以写入只读页面
    • 1: 禁止写入只读页面
  8. AM(对齐检查,位 18)
    • 0: 关闭对齐检查
    • 1: 启用对齐检查(仅在 CR4.AC 和 EFLAGS.AC 设置时生效)
  9. NW(不写通,位 29)
    • 0: 写通缓存
    • 1: 非写通缓存
  10. CD(缓存禁用,位 30)
    • 0: 启用缓存
    • 1: 禁用缓存
  11. PG(分页,位 31)
    • 0: 关闭分页
    • 1: 启用分页

CR3(控制寄存器 3)

CR3 通常用于保存页目录基地址寄存器(PDBR),它包含了页目录的物理地址。在启用分页模式时,CR3 用于内存地址转换。

  1. PCD(页目录缓存禁用,位 4)
    • 0: 启用页目录缓存
    • 1: 禁用页目录缓存
  2. PWT(页目录写通,位 3)
    • 0: 非写通缓存
    • 1: 写通缓存

CR4(控制寄存器 4)

  1. VME(虚拟 8086 模式扩展,位 0)
    • 0: 禁用虚拟 8086 模式扩展
    • 1: 启用虚拟 8086 模式扩展
  2. PVI(保护模式虚拟中断,位 1)
    • 0: 禁用保护模式虚拟中断
    • 1: 启用保护模式虚拟中断
  3. TSD(时间戳禁用,位 2)
    • 0: RDTSC 指令在所有特权级可用
    • 1: RDTSC 指令仅在特权级 0 可用
  4. DE(调试扩展,位 3)
    • 0: 禁用调试寄存器的扩展功能
    • 1: 启用调试寄存器的扩展功能
  5. PSE(页面大小扩展,位 4)
    • 0: 禁用 4MB 页
    • 1: 启用 4MB 页
  6. PAE(物理地址扩展,位 5)
    • 0: 禁用物理地址扩展
    • 1: 启用物理地址扩展
  7. MCE(机器检查异常,位 6)
    • 0: 禁用机器检查异常
    • 1: 启用机器检查异常
  8. PGE(全局分页,位 7)
    • 0: 禁用全局分页
    • 1: 启用全局分页
  9. PCE(性能监视计数器启用,位 8)
    • 0: 禁用 RDPMC 指令
    • 1: 启用 RDPMC 指令
  10. OSFXSR(操作系统支持 FXSAVE 和 FXRSTOR,位 9)
    • 0: 禁用 FXSAVE 和 FXRSTOR 指令
    • 1: 启用 FXSAVE 和 FXRSTOR 指令
  11. OSXMMEXCPT(操作系统支持未掩码的 SIMD 浮点异常,位 10)
    • 0: 禁用未掩码的 SIMD 浮点异常
    • 1: 启用未掩码的 SIMD 浮点异常
  12. UMIP(用户模式指令预防,位 11)
    • 0: 禁用 UMIP
    • 1: 启用 UMIP
  13. VMXE(虚拟机扩展启用,位 13)
    • 0: 禁用 VMX 操作
    • 1: 启用 VMX 操作
  14. SMXE(安全模式扩展启用,位 14)
    • 0: 禁用 SMX 操作
    • 1: 启用 SMX 操作
  15. FSGSBASE(启用 RDFSBASE,WRFSBASE,RDGSBASE,WRGSBASE 指令,位 16)
    • 0: 禁用这些指令
    • 1: 启用这些指令
  16. PCIDE(进程上下文标识启用,位 17)
    • 0: 禁用 PCID
    • 1: 启用 PCID
  17. OSXSAVE(操作系统支持 XSAVE/XRSTOR,位 18)
    • 0: 禁用 XSAVE 和 XRSTOR 指令
    • 1: 启用 XSAVE 和 XRSTOR 指令
  18. SMEP(超级用户模式执行保护,位 20)
    • 0: 禁用 SMEP
    • 1: 启用 SMEP
  19. SMAP(超级用户模式访问保护,位 21)
    • 0: 禁用 SMAP
    • 1: 启用 SMAP
  20. PKE(保护密钥启用,位 22)
    • 0: 禁用保护密钥
    • 1: 启用保护密钥

0x03 系统指令总汇

image-20241023030353868

这些指令只能在CPL为1或者2时运行,三环下只能通过CR4中的TSD和PCE标志访问这些指令。

完结撒花!!!

参考文献

《INTEL开发手册卷3(中文版).pdf》

Bilibili火哥内核6期

Q神的博客

X86_64 CR3控制寄存器详解

页目录表基址

页表基址