Windows内核—保护模式
本文只是个人的学习笔记,作为个人复习自用,侵删!!!
1、段机制概述
段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的内存块单元。
2、段机制基础——段寄存器
2.1 段
每个段由3部分参数定义:段基地址、段限长和段属性
需要8个字节(64bit)存储这些信息。
2.2 段寄存器
要想知道内存单元是如何组织的,就得先来认识段寄存器。
段寄存器其实因内存的分段管理机制而起,计算机需要对内存分段,以分配给不同的程序使用。如 2.1 所述的只需要64位存储这些信息,而段寄存器只有16位,因此段寄存器中只能存储段选择符,再由段号映射到内存的GDT表中,读取段的信息。
8个96位的段寄存器
1、CS 代码段寄存器
存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。
2、DS 数据段寄存器
指出当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。
3、SS 堆栈段寄存器
指出当前堆栈的底部地址,即存放堆栈段的段基址。
4、ES 附加段寄存器
指出当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段。
5、FS 标志寄存器
在ring0中指向KPCR,在ring3中的程序,fs:[0] 指向TEB
6、GS 全局段寄存器
用于管理线程特定的内存
7、LDTR 段选择子
记录局部描述符表的起始位置,LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这个一个选择子。
LDTR可以在程序中随时改变,通过使用lldt指令。
8、TR 任务寄存器
用于寻址一个特殊的任务状态段(TSS),TSS包含当前执行任务的重要信息。
TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。
它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。
当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。
当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。
段寄存器的结构
段寄存器总共有96位,其中80位是不可见的,16位是可见的,这就是为啥我们在OD中只能看到16位的原因。
Ø读段寄存器
比如:mov ax, es 只能读16位的可见部分
读写LDTR的指令为:SLDT/LLDT
读写TR的指令为:STR/LTR
Ø写段寄存器
如:MOV DS,AX 写时是写96位
3、段寄存器——属性探测
3.1 段寄存器成员
段寄存器 | Selector 16位 | Attribute 16位 | Base 32位 | Limit 32位 |
---|---|---|---|---|
ES | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
CS | 001B | 可读、可执行 | 0 | 0xFFFFFFFF |
SS | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
DS | 0023 | 可读、可写 | 0 | 0xFFFFFFFF |
FS | 003B | 可读、可写 | 0x7FFDE000 | 0xFFF |
GS | - | - | - | - |
3.2 探测Attribute
int var = 0;
__asm
{
mov ax,ss //cs不行: cs是可读 可执行 但不可写
mov ds,ax
mov dword ptr ds:[var],eax
}
3.3 探测Base
int var = 1;
__asm
{
mov ax,fs
mov gs,ax
mov eax,gs:[0] //0地址不能读也不能写,这里对0地址进行读的操作是gs.base+0=
mov dword ptr ds:[var],eax
//mov edx,dword ptr ds:[0x7FFDF000]
}
3.4 探测Limit属性是否存在
int var = 1;
__asm
{
mov ax,fs
mov gs,ax
mov eax,gs:[0x1000]
// 访问的地址相当于下面这行注释的代码 但DS的Limit是0xFFFFFFFF
// mov eax,dword ptr ds:[0x7FFDF000+0x1000]
mov dword ptr ds:[var],eax
}
写段寄存器的时候只有16位,还有另外的80位从哪来?
64位段描述符
段描述符共有64位,如何扩充到80位?
P位
如果P=1段描述符有效,P=0无效
回顾一下这个段寄存器的结构,理清如何实现由64位扩充到80位的。
WORD Selector; //16位,已确定
WORD Atrribute; //16位
DWORD Base; //32位 ,3部分组成
DWORD Limit; //32位
Selector已经确定,主要是理清楚 身下的80位的对应关系
这里重点关注Limit,Limit最大为FFFFF,当G位为0时,Limit为000FFFFF;当G为1时,Limit为FFFFF000。
备注:为什么Limit最大为FFFFF呢?
我们来看看低4字节的015位,高4字节的1619位,总共为5字节,最大为5个F,即FFFFF。
由此就解释了,如何由64位扩充到80位。
4、段权限检查
CPU分为4级,我们主要用到的就是Ring3和Ring0。
4.1 如何查看程序在几环
通过CS、SS中存储的段选择子后2位可知当前所属的特权级(CPL)
4.2 如何进行权限检查
DPL存出在段描述符中,规定了访问该段所需要的特权级是什么
当CPL>DPL时,不允许访问。
RPL请求特权级别,是针对端选择子而言的,每个段的选择子都有自己的RPL。
数据段的权限检查
当CPL<=DPL & RPL<=DPL 时,即可实现数据段的访问。
有了CPL为何还要RPL,有这样一种情况:本可用“读 写”的权限去打开一个文件,但为了避免出错,有些时候我们使用“只读”的权限去打开。
5、代码跨段跳转流程
5.1 代码间的跳转(段间跳转,非调用门之类的)
两种情况:1、要跳转的段是一致代码段还是非一致代码段
2、同时修改CS与EIP的指令
如:JMP FAR 、CALL FAR、RETF、INT、IRETED
执行流程:
1、段选择子拆分
2、查表得到段描述符
3、权限检查
4、加载段描述符
5、代码执行
例子:CPU如何执行 JMP 0x20:0x004183D7这个指令?
1、拆分 0x20的段选择子
0x20 = 0000 0000 0010 0000
RPL = 00 TI=0 Index=100
2、查表得到段描述符
TI=0 => 查GDT表
Index=4 => 找到对应的段描述符
3、权限检查
如果是非一致代码段,要求:CPL==DPL & RPL<=DPL
如果是一致代码段,要求:CPL>=DPL
4、加载段描述符
通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中。
5、代码执行
CPU将CS.Base+Offset的值写入EIP,然后执行CS:EIP处的代码,段间跳转结束。
总结
对于一致代码段(共享的段):
1、特权级高的程序不允许访问特权级低的数据,核心态不允许访问用户态的数据
2、特权级低的程序可以访问到特权级高的数据,但特权级不会改变,用户态还是用户态
对于普通代码段(非一致代码段):
1、只允许同级访问
2、绝对禁止不同级别的访问
5.2 跨段调用
前面我们通过JMP FAR实现了段间的跳转,如果要实现跨段的调用就必须用CALL FAR,实现长调用。
下面我们来分析一下CALL FAR(长调用)和CALL(段调用)指令的区别:
1、短调用
CALL指令执行前后堆栈的变化如下:在跳转前,先将当前的ESP压栈,进入到另一函数的堆栈领空,通过RET跳到CALL指令执行前的ESP,如此来实现短调用。这个过程中,发生改变的寄存器有 ESP EIP。
2、长调用(跨段不提权)
形如 CALL CS:EIP 执行前后的堆栈图如下:
长调用会先将调用者CS压栈,再将返回地址压栈,ESP+8
3、长调用(跨段并提权)
形如 CALL CS:EIP 执行前后的堆栈图如下:
随着权限的变化堆栈会发生切换。
总结:
1)跨段调用时,一旦有权限切换,就会切换堆栈
2)CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样。
3)JMP FAR只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL权限。
6、调用门
6.1 调用门的执行流程
指令格式:CALL CS:EIP(EIP是废弃的)
执行步骤:
1)根据CS的值查GDT表,找到对应的段描述符,这个描述符是一个调用门。
2)在调用门描述符中存储另一个代码段的段选择子。
3)选择子指向的段,段.Base+偏移地址=真正要执行的地址。
6.2 门描述符
在保护模式下,中断描述符表(IDT)中的每个表项由8个字节组成,其中的每个表项叫做门描述符。当中断发生时,必须先访问这些门,判断是否能够"开门",需要对执行的指令进行权限检查,符合设定的权限约束才可以进入相应的处理程序。
符号解析:
1、type字段用于标识门的类型,1100表示调用门。
2、P位位有效位,值为0时,调用这样的门会导致处理器产生异常。
3、DPL字段指明调用门的特权级,从而指定通过调用门访问特定过程所要求的的特权级。
6.3 构造一个调用门(无参 提权)
0040EC00 000810D0
6.4 代码测试
步骤一:代码测试,并观察堆栈与寄存器的变化.
#include <windows.h>
#include<stdio.h>
void __declspec(naked) GetRegister()
{
__asm
{
int 3
retf
}
}
/***
通过中断的方法进入内核
观察CS、SS的变化
观察ESP的值由低2G到高2G
****/
int main()
{
char buff[6];
*(DWORD*) &buff[0] = 0x12345678;
*(WORD*) &buff[4] = 0x48;
__asm
{
call fword ptr[buff]
}
// getchar();
return 0;
}
记录执行前的寄存器值:
CS SS ESP
如何查看当前程序的CPL DPL
中断到内核,进入0环。
步骤二:在测试代码中加入特权指令并读取高2G内存.
7、中断门
7.1 什么是中断门?
Windows没有调用门,但是在系统调用和调试时使用了中断门。
中断描述符的结构:
中断门通常是以 ----EE00 0008----的形式出现,第5个16进制数不一定是额,如果DPL=0,那第5个16进制数就是8。 0008是段选择子,而左右两侧的占位符是要跳转的地址,具体的跳转地址如何查找,见下面的实验部分。
查看中断描述符表(IDT)的基址和长度:
r idtr
r idtl
IDT表可以包含3种门描述符:
1、任务门描述符
2、中断描述符
3、陷阱门描述符
中断门提权
入口函数
int g_high2G; // 在中断门中读取高2G内存保存进来。
int g_eflagsBefore; // 保存进入中断门前的 EFLAGS 寄存器。
int g_eflagsAfter; // 保存进入中断门里的 EFLAGS 寄存器。
int g_eax; // 待会在中断门里要用到 eax ,先把旧的保存到这里。
__declspec(naked) void func() {
__asm {
/*
此时栈结构(当你执行 int 指令的时候,CPU 会自动在0环栈中压入这些值):
| eip3 | <- esp0
| cs3 |
|eflags|
| esp3 |
| ss3 |
*/
mov g_eax, eax
// 保存当前 eflags
pushfd
pop g_eflagsAfter
// 保存原始 eflags
mov eax, [esp+0x08]
mov g_eflagsBefore, eax
// 读取 8003f500 处的值
mov eax, ds:[0x8003f500]
mov g_high2G, eax
// 恢复 eax
mov eax, g_eax
// 中断门返回
iretd
}
}
构造中断门描述符
在IDE中通过查看反汇编代码,查找fun函数的起始地址,通过断点调试查看反汇编代码定位到fun函数,可以看到地址是00401550 ,根据中断门描述符构造规则,查找函数函数偏移地址:
0: kd> r idtr
idtr=8003f400
0: kd> dq 8003f400 L30
8003f400 80548e00`000831a0 80548e00`0008331c
8003f410 00008500`0058113e 8054ee00`00083730
8003f420 8054ee00`000838b0 80548e00`00083a10
8003f430 80548e00`00083b84 80548e00`000841fc
8003f440 00008500`00501198 80548e00`00084600
8003f450 80548e00`00084720 80548e00`00084860
8003f460 80548e00`00084ac0 80548e00`00084dac
8003f470 80548e00`000854a8 80548e00`000857e0
8003f480 80548e00`00085900 80548e00`00085a3c
8003f490 80548500`00a057e0 80548e00`00085ba4
8003f4a0 80548e00`000857e0 80548e00`000857e0
8003f4b0 80548e00`000857e0 80548e00`000857e0
8003f4c0 80548e00`000857e0 80548e00`000857e0
8003f4d0 80548e00`000857e0 80548e00`000857e0
8003f4e0 80548e00`000857e0 80548e00`000857e0
8003f4f0 80548e00`000857e0 806e8e00`0008710c
8003f500 00000000`00080000 00000000`00080000
0040EE00 00081550
EE00是中断门特征,0008是中断门跨入0008描述的代码,0040是函数地址的前4个字节,1550是函数地址的后4个字节。
执行指令修改描述符
eq 8003f500 0040ee00`00081550
执行中断门描述符后
0: kd> eq 8003f500 0040ee00`00081550
0: kd> dq 8003f400 L30
8003f400 80548e00`000831a0 80548e00`0008331c
8003f410 00008500`0058113e 8054ee00`00083730
8003f420 8054ee00`000838b0 80548e00`00083a10
8003f430 80548e00`00083b84 80548e00`000841fc
8003f440 00008500`00501198 80548e00`00084600
8003f450 80548e00`00084720 80548e00`00084860
8003f460 80548e00`00084ac0 80548e00`00084dac
8003f470 80548e00`000854a8 80548e00`000857e0
8003f480 80548e00`00085900 80548e00`00085a3c
8003f490 80548500`00a057e0 80548e00`00085ba4
8003f4a0 80548e00`000857e0 80548e00`000857e0
8003f4b0 80548e00`000857e0 80548e00`000857e0
8003f4c0 80548e00`000857e0 80548e00`000857e0
8003f4d0 80548e00`000857e0 80548e00`000857e0
8003f4e0 80548e00`000857e0 80548e00`000857e0
8003f4f0 80548e00`000857e0 806e8e00`0008710c
8003f500 0040ee00`00081550 00000000`00080000
中断门对堆栈的影响
1、提权切换栈,会比调用门多切换一个堆栈
2、不提权,不会切换堆栈,没有必要在栈中压入栈段选择子和栈顶指针。
3、使用INT进入中断门
使用 INT [index]汇编进入各种类型的门,通过INT指令进入中断门,会影响栈,如果从3环进入0环,必须要实现:
-
切换到0环的栈
-
在0环栈压入ss3,esp3,eflags3,cs3,eip3
(注:上面所指的3表示的3环,即ss3为3环的栈,esp3 3环的栈顶指针,eflags3 ,cs3 3环的代码段选择子,eip3 3环的返回地址)
8、陷阱门
陷阱门描述符的结构
通常陷阱门是以 ----ef00 0008---- 的形式出现,第5个16进制数不一定是e,当DPL=0时,第5个16进制数就是8。0008是段选择子,而左右两侧的占位符是要跳转的地址。
对比陷阱门和中断门的描述符结构,我们发现除了type域的第3位不一样之外,其他的都相同。
9、任务状态段 TSS
在调用门、中断门、陷阱门中,一旦出现权限切换,就会有堆栈的切换,而且由于CS、CPL的改变,也引起了SS的切换。SS切换时,就会有新的ESP和SS,那就这两个值是从哪里来的呢?
TSS的内存结构图如下:
大小:104字节
TSS的本质:不要把TSS与任务切换联系到一起,TSS的意义就在于可以同时切换掉一堆寄存器。
CPU是如何找到TSS的呢?
CPU通过TR寄存器来确定TSS的位置,TR是一个段寄存器,大小为96位,存放时描述了TSS段的相关信息,如TSS段的基址、大小和属性。
可以通过ltr指令+TSS段描述符的选择子来加载TSS段。由于该指令时特权指令,所以只能在特权级为0的情况下使用。
TSS段描述符
当 S=0,TYPE=1001 / TYPE=1011,表示这是一个TSS段描述符。
当TSS段没被加载进TR寄存器时,TYPE=1001,一旦TSS被加载进TR寄存器,TYPE=1011.
那么,TSS有什么作用呢?
1、保存Ring0、Ring1、Ring2的栈段选择子和栈顶指针。
在跨段提权时,需要切换栈,CPU会通过TR寄存器找到TSS,取出其中的SS0和ESP0复制到SS和ESP寄存器中。
(注:上面SS0、ESP0中的0表示Ring0)
2、一次切换一堆寄存器
TSS存储了不同特权级下的SS、ESP、CS、SS等,可以通过call/jmp + TSS段选择子 指令一次性把这些值加载到CPU'对应的寄存器中,同时旧值保存在旧的TSS中。
一个GDT表可以存放多个TSS描述符,即内存中可以同时存在多个TSS,TR总是指向当前使用中的TSS,当执行 call/jmp + TSS段选择子时,CPU执行:
- 把当前所有寄存器的值填写到当前TR寄存器指向的TSS中
- 把新的TSS段选择子执行的段描述符加载到TR段寄存器中
- 把新的TSS段中的值覆盖到当前所有的寄存器中。
以上就是简单的段机制入门笔记,细节还可以自己去深究,重点参考Inter白皮书。
10、分页机制
前面做的各种内核实验都卡死,很是郁闷,准备还是先从理论上先过一篇,然后再来深究细节。
本节我们来了解一下页的机制。
10.1 3个概念
先来看个例子: mov eax,dword ptr ds:[0x12345678]
-
逻辑地址:也叫有效地址,C语言中指针里存的地址就是有效地址。这里0x12345678就是这个地址。
-
线性地址:线性地址=ds.base+逻辑地址,这里 ds.Base+0x12345678 就是这个地址。
-
物理地址:通过某种映射的方法,把线性地址映射到真实的物理内存条上的某个地址。
每个进程都有一个CR3寄存器,CR3指向一个物理页,一共4096个字节。
10.2 页
页是intel CPU提供的一种机制,页其实就是有固定大小的内存块。
CPU提供了两种大小的页,一种为4KB(212字节,也就是0x1000字节),这种机制将4GB的地址空间划分成了220 (1MB)个固定大小的内存块。
页的范围:
第0页 0x00000000~0x00000fff
第1页 0x00001000~0x00001fff
第2^20 -1 页 0xfffff000~0xffffffff
10.3 如何实现线性页到物理页的映射?
1、页表
CPU把物理地址空间分成若干个页面,每个页面大小为4KB,每个页面都有一个编号,假设物理内存为4GB,那么编号就是从0~2^20 - 1(0x000000xfffff);假设物理内存只有1MB,那么编号就是从02^8 - 1(0x00000~0x000ff)
只要知道了某个页面页面编号,就一定能计算出小页面的真是物理地址。比如:
0号物理页的物理地址就是 0x00000000,
1号物理页的物理地址就是 0x00001000,
0xff号物理页的物理地址就是 0x0000ff00。
计算方法:页号*4KB
那么CPU是如何找到这些页面的呢?
其实方法就是CPU和物理内存都要将页面编号保存起来,接下来看看CPU和物理内存是如何保存这些页号的:
-
CPU用32bit来保存一个页面编号,高20bit保存编号,低12位保存页面属性。
这些保存编号的页成为页表,页表中的每个元素占4字节,一个页面可以保存1024个元素。
2、一级页表、二级页表、普通物理页
一级页表:保存页表索引号的页表,也叫页目录表
二级页表:保存普通物理页索引号的页表
PDE:page directory entry页目录表项
PTE:page table entry 页表项
3、10-10-12分页
线性地址保存的就是页号,一个32位的线性地址,结构如下:
举个栗子:假设线性地址为 0x12345678
0x12345678 转为二进制 0001 0010 0011 0100 0101 0110 0111 1000
做10-10-12的拆分:0001 0010 00 = 0x48
11 01 00 01 01 = 0x345
0110 0111 1000 = 0x678
即:第一个页表索引号是0x48,第二个页表的索引号是 0x345,最后一个页内偏移是 0x678。
假设:一级页表的基址为 page_dir_tables,根据索引查找第一个页表索引号的值 page_dir_tables[0x48],假设page_dir_tables[0x48]=0x12FFFFFF,取高20位二级页表编号 0x12FFF,则物理页的基址为0x12FFF000。则page_tables=0x12FFF000.
再通过page_tables[0x345]=0x21991067的值。求得页编号 0x21991,则页基址为 0x21991000.
找到物理页基址0x21991000,但是这是个普通物理页,需要通过普通物理页+页内偏移,定位到最终的物理页,即:0x21991000+0x678=0x21991678.
4、2-9-9-12分页
10-10-12分页方式下的物理地址最多可达4GB,但是4GB的物理地址范围已经无法满足要求,所以Intel又衍生出2-9-9-12分页方式,成为PAE(物理地址拓展)分页。
为什么是10-10-12???
- 首先确定了页大小为4KB,所以后面的12是确定的。
- 当物理内存比较小时,4个字节的PTE就够了,因为页的尺寸是4K,所以一个页能存储1024(2^10)个PTE,所以第2个10也就确定了。
- 剩下的10为PDI
这样就有了10-10-12的分页方式。
那为什么是2-9-9-12???
-
首先确定了页大小为4KB,所以后面的12是确定的。
-
如果要增大物理内存的访问范围,就需要增大PTE。那具体增大多少呢?
考虑对齐因素,将PTE增加到8字节。这样一来,一个页就能存储512(2^9)个PTE,所以PTI为9
-
同理PDI也是2^9,在4GB内存空间,32位的寻址空间中,还差 2=32-9-9-12 就称为PDPI。
10.4 2-9-9-12的分页结构
PDPTE(page directory point table entry,页目录指针表项),每项占8个字节
如何开启PAE模式?
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /execute=optin /fastdetect
execute :为10-10-12分页
noexecute :为2-9-9-12分页
举个栗子:假设线性地址为 0x00423024
0x12345678 转为二进制 0000 0000 0100 0010 0011 后12位不分解
做2-9-9-12的拆分:00
00 0000 010 =0x2
0 0010 0011 =0x23
即:第一个页表索引号是0x48,第二个页表的索引号是 0x345,最后一个页内偏移是 0x678。
假设:一级页表的基址为 page_dir_tables,根据索引查找第一个页表索引号的值 page_dir_tables[0x48],假设page_dir_tables[0x48]=0x12FFFFFF,取高20位二级页表编号 0x12FFF,则物理页的基址为0x12FFF000。则page_tables=0x12FFF000.
再通过page_tables[0x345]=0x21991067的值。求得页编号 0x21991,则页基址为 0x21991000.
找到物理页基址0x21991000,但是这是个普通物理页,需要通过普通物理页+页内偏移,定位到最终的物理页,即:0x21991000+0x678=0x21991678.
11、TLB
TLB为块表,那么块表是怎么来的呢?
我们用熟悉的10-10-12分页来举个栗子:
当采用10-10-12分页方式时,CPU需要访问一个int类型的变量,需要执行以下操作:
1、读取4字节的PDE
2、读取4字节的PTE
3、读取4字节的物理内存
那如果在2-9-9-12分页方式下,会执行以下操作:
1、读取8字节的PDPTE
2、读取8字节的PDE
3、读取8字节的PTE
4、读取4字节的物理内存
在10-10-12分页模式下,CPU每次要访问额外的访问的8字节内存数据才能读取到数据,而在PAE分页模式下,需要额外的访问24字节内存数据才能读取到数据。
对于需要频繁进行内存访问的CPU而言,这样的访问方式的效率是很低的,由此产生TLB。
TLB作为一种中间结构,能够避免过多的内存访问,使得CPU不经由页目录表和页表,直接就能把线性地址映射成物理地址。
到底TLB的结构是怎样的,使其能够将线性地址映射成物理地址?
TLB是一个寄存器,存储了线性地址对应的物理地址,以及页属性和统计。
当CPU要访问某个地址时,先访问TLB,若TLB中不存在这个地址,再去访问PDE或PTE,同时将这个线性地址保存到TLB中。
当TLB满时,根据LRU统计的值,把不经常使用的记录删除,添加新值。
12、中断
12.1 什么是中断?
中断通常是由CPU外部的IO设备所触发的,供外部设备通知CPU有事情需要处理,因此也叫中断请求。
中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理历程。
80x86有两个中断请求线:
1、非屏蔽中断线,称NMI
2、可屏蔽中断线,称INTR
12.2 非可屏蔽中断如何处理?
当非可屏蔽中断产生时,CPU在执行完当前指令后hi进入中断处理程序
非可屏蔽中断不受EFLAG寄存器中IF位的影响,一旦发生,CPU必须处理
非可屏蔽中断处理程序位于IDT表中的2号位置。
12.3 可屏蔽中断
可屏蔽中断由中断控制器(专门的芯片)来管理,它负责分配中断资源和管理各个中断源发出的中断请求。为了便于标识各个中断请求,中断管理器常用IRQ后面+数字来表示不同的中断,如:IRQ0表示Windows中的时钟中断。
当可屏蔽中断发生时,如何处理呢?
1、若自己的程序执行时,不希望CPU去处理这些中断,可以用CLI指令清空EFLAG寄存器中的IF位,用STI指令设置EFLAG寄存器中的IF位。
2、硬件中断与IDT表中的对应关系并非固定不变
12.4 异常
异常通常是CPU在执行指令时检测到的某些错误,如:除0异常、访问无效页面等。
中断与异常的区别:
1、中断来自于外部设备,是中断源发起的,CPU是被动的。
2、异常来自于CPU本身,是CPU主动产生的。
3、INT N 虽然被称为软中断,但本质是异常。EFLAG的IF位对INT N无效。
那是如何处理异常的呢?
无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。
常见的异常处理程序如下:
我们来重点关注一下缺页异常的产生:
比如:1、当PDE/PTE的P=0时,
2、当PDT/PTE的属性为只读但程序试图写入时。
一旦发生缺页异常,CPU会执行IDT表中的0xE号中断处理程序,由OS来接管。
13、控制寄存器
控制寄存器用于控制和确定CPU的操作模式。
13.1 有哪些控制寄存器?
Cr0 Cr1 Cr2 Cr3 Cr4
Cr1 保留
Cr3 页目录表基址
13.2 Cr0寄存器
说明:
1、PE:Cr0的为0是启用保护标志
PE=1保护模式
PE=0实地址模式
这个标志仅开启段级保护,而并没有启动分页机制,若启用分页机制,那么PE和PG标志都要置位。
2、PG:当设置该为时,即开启了分页机制,在开启这个标志之前必须已经或者同时开启PE标志。
PG=0且PE=0 处理器工作在实地址模式下
PG=0且PE=1 处理器工作在没有开启分页机制的保护模式下
PG=1且PE=0 在PE没有开启的情况下 无法开启PG
PG=1且PE=1 处理器工作在开启了分页机制的保护模式下
3、WP:对于Intel 80486或以上的CPU,Cr0的位16是写保护(Write Proctect)标志。当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作。
当CPL<3时,若WP=0可以读写任意用户级物理页,只要线性地址有效。
若WP=1可以读取任意用户级物理页,但对于只读的物理页,则不能写。
13.3 Cr2寄存器
当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在Cr2中。
13.4 Cr4寄存器
PAE/PSE说明:
PAE=1是2-9-9-12分页
PAE=0是10-10-12分页
PSE=1:
10-10-12 PS=1 4M页
PS=0 4K页
2-9-9-12 PS=1 2M页
PS=0 4K页
PSE=0:
10-10-12 PS=1 4K页
PS=0 4K页
2-9-9-12 PS=1 4K页
PS=0 4K页
14、PWT/PCD属性
PWT:Page Write Through
PWT=1时,写Cache的时也要讲数据写入内存中
PCD:Page Cache Disable
PCD=1时,禁止某个页写入缓存,直接写内存。
比如,做页表用的页,已经存储在TLB中了,可能不需要再缓存了。