Windows内核(1)保护模式
1. 保护模式简介
1.1. x86CPU的三种运行模式
- 实模式
- 保护模式
- 保护操作系统的数据结构如GDT、IDT
- 保护操作系统的寄存器
- 保障多任务的并发进行
- 现代操作系统大多采用保护模式
- 虚拟8086模式
CPU刚启动的时候处于实模式,如果CPU开启了保护模式功能,CPU会在之后切换到保护模式
1.2. x86CPU的等级环
x86CPU存在4个等级环0-3
- 0环为特权环
- 0、1、2环为系统环
- 3环为用户环
1.3. 保护模式的核心机制
段机制和页机制是保护模式的基础,也是实现现代操作系统的基础。只有深刻掌握和理解CPU的段页机制,才能掌握操作系统底层的一些原理。注意,段机制和页机制是CPU这个硬件提供的功能,并不是操作系统提供的,操作系统只是利用了CPU的这个功能
1.4. 学习原则
- x64是x86的拓展,x86是x64的基础
- 学完后可以不通过驱动来进入内核
- 学完后可以不通过操作系统的API来实现相应功能
1.5. 实验环境搭建
- 虚拟机内搭建Windows XP系统(配置成单核,否则后面的实验可能会出错),安装VC6和OllyDbg
- 宿主机内安装Windbg,配置Symbols
- 宿主机与虚拟机配置双机调试
2. 段机制
2.1. 段寄存器
mov eax,dword ptr ds:[0x401000],这里读取到eax的值实际位于地址ds.base + 0x401000处。总共存在CS、SS、DS、ES、FS、GS、LDTR、TR共八个段寄存器
2.1.1. 段寄存器读写
2.1.1.1. 读段寄存器
- CS、SS、DS、ES、FS、GS:
mov ax,es,只能读取可见的段选择子 - LDTR:
SLDT - TR:
str ax,str word ptr ds:[buff]
2.1.1.2. 写段寄存器
- CS:JMP FAR、CALL FAR、RETF、INT、IRETED指令(详见后续章节)
- SS、DS、ES、FS、GS:
mov es,ax,写段寄存器一次性写96位,其它64位由CPU填充- LES、LSS、LDS、LFS、LGS指令,
les ecx,fword ptr ds:[buffer],高两个字节给段寄存器es,低四个字节给ecx
- LDTR:
LLDT - TR:
ltr ax,ltr word ptr ds:[buff],这个指令只能在0环执行,加载后TSS段描述符的Busy位会发生变化
2.1.2. 段寄存器结构
段寄存器为96位(只有16位可见),由以下四部分构成:
- 段选择子,16位,可见,段的选择子
- 段属性,16位,不可见,指示段是否可读、可写、可执行
- 段基址,32位,不可见,段的起始地址
- 段限长,32位,不可见,段的长度
此表为实验中的实例表:
| 段寄存器 | 段选择子 | 段属性 | 段基址 | 段限长 |
|---|---|---|---|---|
| CS | 001B | 可读可执行 | 0x00000000 | 0xFFFFFFFF |
| SS | 0023 | 可读可写 | 0x00000000 | 0xFFFFFFFF |
| DS | 0023 | 可读可写 | 0x00000000 | 0xFFFFFFFF |
| ES | 0023 | 可读可写 | 0x00000000 | 0xFFFFFFFF |
| FS | 003B | 可读可写 | 0x7FFDE000 | 0xFFF |
| GS | - | - | - | - |
- 不同的操作系统,段选择子FS的段基址可能不同
- Windows操作系统未使用GS段寄存器
- 在做寄存器读写实验的时候,尽量不要使用DS段寄存器,这个寄存器在操作系统生成的代码中被使用,可能会导致程序崩溃
2.1.3. 段选择子
| 比特位 | 15-3 | 2 | 1-0 |
|---|---|---|---|
| 结构 | index | TI | RPL |
- index:段选择子索引
- TI(Table Indicator):0代表查GDT表,1代表查IDT表
- RPL(Requested Privilege Level):请求特权级别
2.2. 段选择子选择了什么:段描述符
2.2.1. 段描述符在哪儿:GDT、LDT、IDT
- GDT:全局描述符表
- gdtr寄存器:32位,存有GDT表的地址
- gdtl寄存器:16位,存有GDT表的长度(单位为字节)
- LDT:局部描述符表(Windows中并未使用该结构)
- IDT:中断描述符表
- idtr寄存器:32位,存有IDT表的地址
- idtl寄存器:16位,存有IDT表的长度(单位为字节)
sgdt fword ptr ds:[buffer]汇编指令可以读取gdtr和gdtl寄存器,高四字节存储gdtr,低二字节存储gdtl(如果CR4.UMIP = 0,SGDT可以在3环执行)
2.2.2. 段描述符结构(8字节)
| 比特位 | 31-24 | 23 | 22 | 21 | 20 | 19-16 | 15 | 14-13 | 12 | 11-8 | 7-0 | 31-16 | 15-0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 结构 | Base 31-24 | G | D/B | 0 | AVL | SL 19-16 | P | DPL | S | Type | Base 23-16 | Base 15-0 | SL 15-0 |
- Base(Base Address):段的基地址
- G(Granularity):指示段限长的单位,1表示4KB,0表示字节
- D/B(Default operation size):1表示32位段,0表示16位段
- CS段(段加载到CS段寄存器中时):1表示默认用32位寻址方式,0表示默认用16位寻址方式(可以用67这个opcode强制更改寻址方式为16位)
- SS段(段加载到SS段寄存器中时):影响PUSH、POP、CALL等隐式堆栈操作指令,1表示使用ESP指针,0表示使用SP指针
- 向下拓展的数据段:1表示段上限为4GB,0表示段上限为64KB
- AVL(Available for use by system software):可用位,提供给用户或者应用程序使用
- SL(Segment Limit):段的限长
- P(Segment Present):1表示段描述符有效,0表示段描述符无效(CPU检查的第一步)
- DPL(Descriptor Privilege Level):描述符特权级别,Windows只使用了3和0
- S(Descriptor type):1表示代码或数据段,0表示系统段(CPU检查的第二步)
- Type(Segment type):段类型,代码或数据段和系统段的解析结果不同,详见下一小节
注意:FS对应的段描述符比较特殊,查分后与段寄存器中的值不符合,与操作系统(线程)相关
2.2.3. 段描述符Type结构
2.2.3.1. 代码或数据段
| 比特位 | 11 | 10 | 9 | 8 |
|---|---|---|---|---|
| 数据段结构 | 0 | E | W | A |
| 代码段结构 | 1 | C | R | A |
- E:1表示向下拓展(0->base和base+limit->段上限有效),0表示向上拓展(base->base+limit有效)
- W:1表示可读可写,0表示只读
- A:指示该位被操作系统最后一次清零后,该段是否被访问过(CPU将该段置入段寄存器中),1表示已经被访问过,0表示未被访问过
- C:1表示一致代码段(系统拿出来的共享段),低权限可以访问高权限但是特权级别不变,高权限不能访问低权限,0表示非一致代码段,禁止不同级别互相访问
- R:1表示可读,0表示不可读
2.2.3.2. 系统段
| 比特位 | 11 | 10 | 9 | 8 |
|---|---|---|---|---|
| 保留 | 0 | 0 | 0 | 0 |
| 16位TSS(可用) | 0 | 0 | 0 | 1 |
| LDT | 0 | 0 | 1 | 0 |
| 16位TSS(Busy) | 0 | 0 | 1 | 1 |
| 16位调用门 | 0 | 1 | 0 | 0 |
| 任务门 | 0 | 1 | 0 | 1 |
| 16位中断门 | 0 | 1 | 1 | 0 |
| 16位陷阱门 | 0 | 1 | 1 | 1 |
| 保留 | 1 | 0 | 0 | 0 |
| 32位TSS(可用) | 1 | 0 | 0 | 1 |
| 保留 | 1 | 0 | 1 | 0 |
| 32位TSS(Busy) | 1 | 0 | 1 | 1 |
| 32位调用门 | 1 | 1 | 0 | 0 |
| 保留 | 1 | 1 | 0 | 1 |
| 32位中断门 | 1 | 1 | 1 | 0 |
| 32位陷阱门 | 1 | 1 | 1 | 1 |
2.3. 位于GDT表中的系统段
2.3.1. 调用门
| 比特位 | 31-16 | 15 | 14-13 | 12 | 11-8 | 7-5 | 4-0 | 31-16 | 15-0 |
|---|---|---|---|---|---|---|---|---|---|
| 结构 | Offset in Segment 31-16 | P | DPL | S(0) | Type(1100) | 000 | Param Count | Segment Selector | Offset in Segment 15-0 |
- Param Count:参数个数
注意:Windows并未使用调用门
2.3.2. 任务段(Task-state Segment, TSS, 任务状态段)
2.3.2.1. TSS的结构
TSS是一块大小为104字节的内存块,存储了一堆寄存器,TSS的作用就在于可以替换一堆寄存器。Intel设计TSS初衷用于线程切换,但是Windows和Linux的线程切换均采用堆栈来保存寄存器而非TSS。Windows将TSS用于跨段跳转提权时,获取高权限的SS和ESP。
| 31-15比特位 | 15-0比特位 | 偏移字节 |
|---|---|---|
| I/O Map Base Address | Reserved(0位单独为T) | 100 |
| Reserved | LDT Segment Selector | 96 |
| Reserved | GS | 92 |
| Reserved | FS | 88 |
| Reserved | DS | 84 |
| Reserved | SS | 80 |
| Reserved | CS | 76 |
| Reserved | ES | 72 |
| EDI | < | 68 |
| ESI | < | 64 |
| EBP | < | 60 |
| ESP | < | 56 |
| EBX | < | 52 |
| EDX | < | 48 |
| ECX | < | 44 |
| EAX | < | 40 |
| EFLAGS | < | 36 |
| EIP | < | 32 |
| CR3(PDBR) | < | 28 |
| Reserved | SS2 | 24 |
| ESP2 | < | 20 |
| Reserved | SS1 | 16 |
| ESP1 | < | 12 |
| Reserved | SS0 | 8 |
| ESP0 | < | 4 |
| Reserved | Previous Task Link | 0 |
- I/O Map Base Address:IO许可位图基地址,当用户态进程试图通过in或out指令访问一个I/O端口时,CPU需要访问存放在TSS中的I/O许可位图以检查该进程是否有访问端口的权利。步骤如下:
- 检查eflags寄存器中的2位IOPL字段,如果字段的值为3,控制单元就执行I/O指令。否则,执行下一个检查。
- 访问tr寄存器以确定当前的TSS和相应的I/O许可权位图。
- 检查I/O指令中指定的I/O端口在I/O许可权位图中对应的位,如果该位清,这条指令就执行,否则控制单元产生一个异常。
2.3.2.2. 如何找到TSS:TR寄存器
TR(Task Register)寄存器存储了TSS段描述符的段选择子
2.3.2.3. TSS段描述符结构
| 比特位 | 31-24 | 23 | 22-21 | 20 | 19-16 | 15 | 14-13 | 12 | 11-8 | 7-0 | 31-16 | 15-0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 结构 | Base 31-24 | G | 00 | AVL | Limit 19-16 | P | DPL | S(0) | Type(10B1) | Base 23-16 | Base 15-0 | Limit 15-0 |
- Type中的B:1表示Busy(已被加载至TR寄存器),0表示Available
2.3.2.4. 修改TR寄存器
- 0环下的
LTR指令
2.4. 位于IDT表中的系统段
2.4.1. 中断门
| 比特位 | 31-16 | 15 | 14-13 | 12 | 11-8 | 7-5 | 4-0 | 31-16 | 15-0 |
|---|---|---|---|---|---|---|---|---|---|
| 结构 | Offset 31-16 | P | DPL | S(0) | Type(D110) | 000 | 00000 | Segment Selector | Offset 15-0 |
- Type中的D:1表示32位门,0表示16位门
2.4.2. 陷阱门
| 比特位 | 31-16 | 15 | 14-13 | 12 | 11-8 | 7-5 | 4-0 | 31-16 | 15-0 |
|---|---|---|---|---|---|---|---|---|---|
| 结构 | Offset 31-16 | P | DPL | S(0) | Type(D111) | 000 | 00000 | Segment Selector | Offset 15-0 |
- Type中的D:1表示32位门,0表示16位门
2.4.3. 任务门
| 比特位 | 31-16 | 15 | 14-13 | 12 | 11-8 | 7-0 | 31-16 | 15-0 |
|---|---|---|---|---|---|---|---|---|
| 结构 | Reserved | P | DPL | S(0) | Type(0101) | Reserved | TSS Segment Selector | Reserved |
2.5. 跨段访问
2.5.1. 跨段时的权限检查
2.5.1.1. 三类特权级别
- CPL(Current Privilege Level):当前特权级别,见CS、SS段寄存器中的值的最后两个比特位,CS、SS段寄存器中的值的最后两个比特位永远相同
- RPL(Requested Privilege Level):请求特权级别,位于段选择子中,指用什么权限去访问一个段,可以进行附加限制,防止误操作
- DPL(Descriptor Privilege Level):描述符特权级别,位于段描述符结构中,指访问这个段需要的权限级别
- 数据段:DPL规定了可以访问此段的最低特权级。比如,一个数据段的DPL是1,那么只有运行在CPL为0或者1的程序才有权访问它
- 非一致代码段(不使用调用门的情况下):DPL规定访问此段的特权级。比如一个非一致代码段的特权级为0,那么只有CPL为0的程序才可以访问它
- 调用门:DPL规定了当前执行的程序或任务可以访问此调用门的最低特权级(这与数据段的规则是一致的)
- 一致代码段和通过调用门访问的非一致代码段:DPL规定了访问此段的最高特权级。比如,一个一致代码段的DPL是2,那么CPL为0和1的程序将无法访问此段
2.5.1.2. 权限检查与变化
- 数据段:CPL <= DPL,RPL <= DPL,访问后CPL不变
- 直接跳转、直接调用代码段(jmp far、call far)
- 非一致代码段:CPL == DPL,RPL <= DPL,跳转后CPL=DPL,实际不变
- 一致代码段:CPL >= DPL,跳转后CPL不变
- 调用门:调用门描述符DPL为DPL_Date,调用门中包含的代码段描述符DPL为DPL_Code
- 一致代码段:CPL <= DPL_Gate,RPL <= DPL_Gate,DPL_Code <= CPL,调用后CPL = DPL_Code
- 非一致代码段:
- call调用门:CPL <= DPL_Gate,RPL <= DPL_Gate,DPL_Code <= CPL,调用后CPL = DPL_Code
- jmp调用门:CPL <= DPL_Gate,RPL <= DPL_Gate,DPL_Code = CPL,调用后CPL = DPL_Code
- 跨段中断(中断门):CPL <= DPL
2.5.2. 直接跳转、直接调用代码段(jmp far、call far)
JMP far 0x1b:0x00401000、call far 0x1b:0x00401000,这里0x20指向代码段
2.5.2.1. 流程
- 拆分段选择子,确定index、TI和RPL,定位段描述符
- 权限检查
- 加载段描述符到段寄存器
- 修改EIP为CS.Base + Offset
2.5.2.2. 堆栈变化(无论怎样都不会提权,所以不涉及堆栈切换)
2.5.2.2.1. jmp far
堆栈无变化
2.5.2.2.2. call far(retf)
- 压入参数(如果有)
- 压入CS
- 压入返回地址
2.5.3. 调用门
CALL far CS:EIP或者call far fword ptr ds:[buff],这里的EIP并不使用,调用的地址由CS指向的调用门决定
2.5.3.1. 流程
- 定位调用门描述符,并取出调用门中的代码段描述符
- 根据代码段描述符的DPL,从TSS中读取新的SS、ESP,并检查合法性
- 暂时性保存当前即原SS、ESP
- 切换堆栈,压入原SS、ESP
- 从原堆栈复制参数至新堆栈
- 压入当前即原CS、EIP
- 加载段描述符到段寄存器
- 修改EIP为Base+调用门的Offset in Segment
- 在用ret返回时,会检查ds、es、fs、gs的值,如果其中哪一个寄存器指向的段的DPL小于CPL(此规则不适合于一致代码段),那么一个空描述符会被加载到该寄存器中
2.5.3.2. Call时候的堆栈变化
2.5.3.2.1. 跨段不提权调用(不涉及堆栈切换)
- 压栈参数(如果有)
- 压栈3环CS
- 压栈3环返回地址
2.5.3.2.2. 跨段提权调用(涉及堆栈切换,0环栈)
- 3环栈压栈参数(如果有)
- 0环栈压栈3环SS
- 0环栈压栈3环ESP
- 0环栈压栈参数(如果有)
- 0环栈压栈3环CS
- 0环栈压栈3环返回地址
2.5.4. 任务段
2.5.4.1. JMP任务段
JMP 0x48:0x12345678,这里0x48为任务段
- 把当前所有寄存器(TSS结构中有的那些寄存器)的值填写到当前TR段寄存器指向的TSS中
- 修改TR寄存器
- 使用TR.Base指向的TSS的值修改当前的寄存器
2.5.4.2. CALL任务段
call 0x48:0x12345678,这里0x48为任务段
流程和JMP任务段基本一样,唯一的区别是会往前堆栈里面压入一个返回地址
2.5.5. 中断门
int 20h
2.5.5.1. 流程
- 使用中断号为索引找到IDT表中的中断门描述符
- 从中断门描述符中取出中断门中的段选择子
- 根据中断门中的段选择子找到段描述符,得到Base即基地址
- 加载段描述符到段寄存器
- 修改EIP为Base+中断门的Offset in Segment
2.5.5.2. 堆栈变化(中断门无法传递参数)
2.5.5.2.1. 不提权
- 压栈调用者CS
- 压栈调用者EFLAG
- 压栈调用者返回地址
2.5.5.2.2. 提权
- 压栈调用者SS
- 压栈调用者ESP
- 压栈调用者EFLAG
- 压栈调用者CS
- 压栈调用者返回地址
2.5.6. 陷阱门
注意:陷阱门的机制和中断门基本相同,仅有一点不同,中断门在执行的时候会将EFlag的IF标志位清0,导致CPU不响应可屏蔽中断,但是陷阱门不会
2.5.7. 任务门
提供通过IDT表来访问任务段的方式,中断号找到任务门之后,会获取一个任务段的段选择子,之后的步骤和跳转任务段相同
3. 页机制
3.1. 四个地址类型
mov eax,ds:[0x12345678]
- Virtual Address:虚拟地址,也称为逻辑地址(Logical Address),这里的0x12345678就是虚拟地址
- Liner Address:线性地址,这里的ds.Base + 0x12345678就是线性地址,每个进程有独立的4GB线性地址空间
- Physical Address:物理地址,线性地址经过页机制转换后就是物理地址
- Board Address:板卡地址,物理地址经过硬件转换形成板卡地址(硬件层面,与操作系统无关)
3.2. 分页机制:每页4KB
为了能够方便地使用内存,CPU把4GB线性地址空间划分成了2的20次方个内存页,每一页的大小都是0x1000字节也就是4KB,物理地址空间也按照4KB大小进行划分。
3.3. 线性地址->物理地址:从CR3寄存器开始
CR3寄存器里面存储的是物理地址(唯一一个存放物理地址的寄存器).每个进程都有一个独立的CR3寄存器值,CR3寄存器指向页目录表(4字节 * 1024项),页目录表中每个表项指向页表(4字节 * 1024项),页表中每个表项指向物理页。
3.4. 第一种分页方式:10-10-12分页
3.4.1. 设置10-10-12分页
编辑C:\boot.ini文件,将noexecute改成execute,然后重启
3.4.2. 线性地址->物理地址的转换步骤
- 将32位线性地址分割为10-10-12,第一个10为页目录索引(PDI),第二个10为页表索引(PTI),12为物理地址偏移
- PDT基地址(页目录表基地址) = CR3
- PTT基地址(页表的基地址) = PDI*4(每项4字节,得到页目录项即PDE)+ PDT基地址
- 物理页的地址 = PTI*4(每项4字节,得到页表项项即PTE)+ PTT基地址
- 物理地址 = 物理页的地址 + 物理地址偏移
物理页一页为4KB(10-10-12中还存在一种4MB的大页),PDT中有1024个PDE,PTT中有1024个PTE。PDE、PTE可以为空,也可以多个PTE指向同一个物理页(这些PTE的属性可以不同,这样可以实现对于同一片地址空间,操作系统可读可写而用户只读)。0地址不能访问就是因为0对应的PTE为空。
3.4.3. PDE、PTE结构
物理页属性= PDE属性 & PTE属性
| 比特位 | 31-12 | 11-9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| PDE | 页表基址 | 有效位 | G | PS | 0 | A | PCD | PWT | U/S | R/W | P |
| PTE | 页表基址 | 有效位 | G | PAT | D | A | PCD | PWT | U/S | R/W | P |
- 有效位:
- P:1代表有效,0代表无效
- R/W:1代表可读可写,0代表只读
- U/S(User/System):1代表可以被用户(3环)访问,0代表只能被系统(0、1、2环)访问(高2G内存保护的实现方式)
- PS(Page Size):只针对PDE有效,1代表页大小为4MB即大页,且PDE直接指向物理页,0代表页大小为4KB即小页,且PDE指向PTT
- A:指示对应物理页是否被访问过(访问一个字节也算被访问过),1代表被访问过,0代表没被访问过
- D(Dirty):脏位,指示对应物理页是否被写过,1代表写过,0代表没被写过,可用于页面置换
- G(Global):TLB刷新位,指示刷新TLB时刷不刷新该项,1代表刷新TLB时不刷新该项,0代表刷新TLB时刷新该项(对于PDE,PS=1时G位有效)
- PAT:无意义
- PWT(Page Write Through):1代表写Cache的时候需要同时写到内存中,0代表写内存时机由缓存控制器决定
- PCD(Page Cache Disable):1代表禁止将某个页写入缓存,直接操作内存(比如TLB的存在使得有些页可以不用Cache)
3.4.4. 通过线性地址访问PDT基地址、PTT基地址
- 0xC0300000(线性地址)指向PDT基地址,PDT占用地址空间4KB,如果要找PDE,
0xC0300000 + PDI*4即可 - 0xC0000000(线性地址)指向第一个PTT基地址,所有的PTT在线性地址上连续,占用0xC0000000-0xC0400000地址空间共4MB,如果要找PTE,
0xC0000000 + PDI * 4096 + PTI * 4即可
0xC0300000根据拆分,PDI为0x300,该PDE指向的PTT为PDT表自身,即这里的PDT表也扮演着PTT表的角色;PTI也是0x300,该PTE指向的物理页为PTT表自身,即这里的PDT表=PTT表=物理页。
3.4.5. 一些细节
- 高2G空间有一些大页,4MB大小
- 不同进程低2G空间几乎不同
- 不同进程高2G空间几乎相同(除了页表等一些地方)
- Windows中,进程低2G空间的前后64KB没有使用(0-00010000;7FFF0000-7FFFFFFF),是因为要用于调试
- 进程B的PDT、PTT是由创建进程B的进程A来填充的,填充完切换CR3即可
3.5. 第二种分页方式:2-9-9-12分页(PAE分页,物理地址拓展分页)
3.5.1. 为什么要有PAE分页
10-10-12分页只有4GB的物理地址寻找范围,不满足内存大小的发展要求
3.5.2. 如何开启PAE分页
编辑C:\boot.ini文件,将execute改成noexecute,然后重启,如果查看我的电脑->属性显示有物理地址拓展,那么说明修改成功
3.5.3. 为什么是2-9-9-12分页
页的大小需要保持为4KB不变(性能原因,AMD为2KB),所以需要通过增大PTE的长度(4字节->8字节)的方式来增大物理地址的寻找范围,同时因为页的大小不变,所以页项减少(1024项->512项),所以页项索引变成9位,最后物理地址寻找范围达到64GB(单进程其实可用空间仍为4GB,因为线性地址只有32位,但是多进程可用空间就会扩大)
3.5.4. 线性地址->物理地址的转换步骤
- 将32位线性地址分割为2-9-9-12,第一个2为页目录指针表索引(PDPI),第二个9为页目录索引(PDI),第三个9为页表索引(PTI),12为物理地址偏移
- PDPT基地址(页目录指针表,8字节*4项,基地址) = CR3
- PDT基地址(页目录表基地址) = PDPI*8(每项8字节,得到页目录项即PDPE)+ PDPT基地址
- PTT基地址(页表的基地址) = PDI*8(每项8字节,得到页目录项即PDE)+ PDT基地址
- 物理页的地址 = PTI*4(每项4字节,得到页表项项即PTE)+ PTT基地址
- 物理地址 = 物理页的地址 + 物理地址偏移
3.5.5. PDPE、PDE、PTE结构
物理页属性= PDPE属性 & PDE属性 & PTE属性
3.5.5.1. PDPE
| 比特位 | 63-36 | 35-12 | 11-9 | 8-5 | 4 | 3 | 2-1 | 0 |
|---|---|---|---|---|---|---|---|---|
| PDPE | Reserved | 基地址 | 有效位 | Reserved | PCD | PWT | Reserved | 1 |
3.5.5.2. PDE
| 比特位 | 63 | 62-36 | 35-21 | 20-13 | 12 | 11-9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PDE(2MB大页) | XD | Reserved | 基地址 | Reserved | PAT | Avail | G | ps(1) | D | A | PCD | PWT | U/S | R/W | P |
| PDE(4KB小页) | XD | Reserved | 基地址 | < | < | Avail | G(0) | ps(0) | D(0) | A | PCD | PWT | U/S | R/W | P |
- XD:AMD中称为NX位,1代表该页不可执行,0代表该页可以执行
3.5.5.3. PTE
| 比特位 | 63 | 62-36 | 35-12 | 11-9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PTE | XD | Reserved | 基地址 | Avail | G | 0 | D | A | PCD | PWT | U/S | R/W | P |
3.5.5.4. PAE分页下通过线性地址访问PDT、PTT:页目录基址、页表基址
- pPDE = (int*)(0xc0600000 + ((addr >> 18) & 0x3ff8))
- pPTE = (int*)(0xc0000000 + ((addr >> 9) & 0x7ffff8))
3.6. TLB(快表)
3.6.1. TLB是什么以及为什么要有TLB
通过线性地址访问物理页需要进行多层转换(如果读取的多个字节跨页会更麻烦),效率很低,所以CPU做了一个线性地址-物理页的缓存表来提高效率,称为TLB(Translation Lookaside Buffer)
3.6.2. TLB的结构
| 比特位 | ||||
|---|---|---|---|---|
| 结构 | Liner Address(线性地址) | Physical Address(物理地址) | Attribute | LRU(统计) |
- Attribute(属性):PDE & PTE(PDPE & PDE & PTE)
- 对于不同的CPU,TLB的大小不同。TLB越大,缓存效果越好,执行速度越快
- 只要CR3发生改变,TLB立即刷新
- 每个CPU核心单独有一个TLB
- 当TLB满了,根据统计值,不常用的地址将会被替代
3.6.3. TLB分类
- 缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB)
- 缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB)
- 缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB)
- 缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)
3.6.4. 强制刷新TLB:invlpg
invlpg 0x400000,invlpg指令可以无视G属性位强制刷新TLB中某个线性地址表项
4. 中断与异常
4.1. 中断
中断(中断请求)通常由CPU外部的输入输出设备(硬件)发起,供外部设备通知CPU,希望CPU暂时停止执行当前执行程序,转而处理中断请求所对应的中断例程(中断例程地址位于IDT表中)
4.1.1. 不可屏蔽中断
非屏蔽中断(NMI):NonMaskable Interrupt,不可屏蔽请求对应的中断例程固定位于0x2处,不可屏蔽中断不受EFLAG中IF位的影响,CPU必须立即响应,比如说断电。
4.1.2. 可屏蔽中断
可屏蔽中断(INTR):Interrupt Require,在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器。它负责分配中断资源和管理各个中断源发出的中断请求。为了便于标识各个中断请求,中断管理器通常用IRQ(Interrupt Request) 后面加上数字来表示不同的中断(例如时钟中断为IRQ0,在设备管理器的设备属性->资源中可以查看对应编号)。
4.1.2.1. 可屏蔽中断类型
| 中断号 | IRQ | 中断类型 |
|---|---|---|
| 0x30 | IRQ0 | 时钟中断 |
| 0x31-0x3F | IRQ1-IRQ15 | 其他硬件设备的中断 |
硬件中断与IDT表中的对应关系并非固定不变的,参见APIC(高级可编程中断控制器)。
4.1.2.2. 如何屏蔽可屏蔽中断:EFLAG.IF
当EFLAG.IF=1的时候,表示CPU当前可以接受可屏蔽中断并进行处理,当EFLAG.IF=0的时候,任何可屏蔽中断都将会被CPU忽略。cli置位0,sti置位1。在单核模式下运行多线程程序时,可以关闭中断请求,从而不响应可屏蔽中断,这种方法可以实现原子操作。不过多核的话这种方法就失效了,需要采取其它手段。
4.2. 异常
异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等。异常例程也位于IDT表中。
4.2.1. 一些异常类型
| 异常类型 | 中断号 |
|---|---|
| 页错误 | 0xE |
| 段错误 | 0xD |
| 除零错误 | 0x0 |
| 双重错误 | 0x8 |
4.3. 异常与中断的区别
- 中断来自于外部设备,产生于硬件,CPU被动接受
- 异常来源于CPU内部,产生于软件,它是由CPU主动产生的
注意:INT N虽然被称为软件中断,但是它来源于CPU内部,其本质是异常,EFLAG的IF位对INT N无效
5. 控制寄存器
用于控制CPU的操作模式,有五个控制寄存器CR0-CR5
- CR0
- CR1:保留
- CR2:存有32位缺页线性地址。当CPU访问某个无效页面时,会产生缺页异常,此时CPU将产生缺页异常的线性地址(原来的EIP)存放在CR2中
- CR3:页目录表基址(两种分页模式下结构不同)
- CR4
5.1. CR0寄存器
| 比特位 | 31 | 30 | 29 | 16 | 0 |
|---|---|---|---|---|---|
| 属性 | PG | CD | NW | WP | PE |
- PG:1代表开启页机制(PE位必须为1才可以置位为1),0代表未开启页机制
- PE(Protection Enable):1代表开启保护模式,启用段机制(页机制需要看PG位),0代表CPU工作在实模式下
- WP(Write Protect):禁止特权环向用户环只读页面写数据
5.2. CR4寄存器
| 比特位 | 5 | 4 |
|---|---|---|
| 属性 | PAE | PSE |
- PAE:1代表2-9-9-12分页,0代表10-10-12分页
- PSE:1代表PS位有意义(大页大小为4MB、2MB),0代表PS位无意义(大页大小为4KB)
6. 参考资料
注:以下参考资料为系列文章的参考资料,并不局限于本篇文章
- 操作系统修炼指南——保护模式
- 内核程序文件和IDA分析文件:Windows内核.7z