内核-①保护模式

--------------------------------------保护模式-段的机制 -----------------------------------

学习保护模式的原因
学习了保护模式,可以不通过系统提供的API而直接访问高2G内存。如:读取进程,可以不使用系统API在3环读取进程信息
内核情景分析  毛德操
windows内核原理实现  潘爱民

1 论证段寄存器96位

段寄存器一共有八个:ES  CS  SS  DS  FS  GS  LDTR  TR
  
Attrbute属性:
_asm{
    mov ax,cs
    mov ds,ax
    mov dword ptr ds:[地址],eax        //---》报错,cs段不可写入
}
_asm{
    mov ax,ss
    mov ds,ax
    mov dowrd ptr ds:[地址],eax        //---》ss段可以写入
}
Base属性:
_asm{
    mov ax,fs
    mov gs,ax
    mov eax,gs:[0]        //---》[0]地址,正常情况不能读也不能写的,但是此时却可以执行成功,
    //说明真正访问的地址是fs.Base+0,相当于mov eax,ds:[0x7ffde000],这个地址是可以读的
}
Limit属性:
mov ax,fs
mov gs,ax
mov eax,gs:[0x1000]【相当于:mov eax,dword ptr ds:[0x7ffde000+0x1000]】,但是ds段的长度是8个F,fs的长度只有3个F
	点击运行,会发现报错,因为0x1000的长度已经超过了0xFFF,在fs的段里面,不能找到0x1000这个地址

2. 段寄存器的结构

段寄存器共96位,可见的只有16位,LDTR和TR段寄存器不可通过mov读写
读段寄存器的时候只能读可见的16位:mov ax,es
但是写入段寄存器的时候,是写入了96位的
段寄存器以结构体方式表现
struct SegMent
{
   WORD Selector;        //16为Selecter,表示可见的16位(段选择子)
   WORD Attributes;     //16位Atrribute,表示读写执行属性,段描述符的8~23位
   DWORD Base;              //32位Base,表示当前段的起始位置
   DWORD Limit;              //32位Limit,表示当前段的长度
}

3. 读写段寄存器

读:
段寄存器在读取的时候只能读取可见的16位
mov, ax,es
写:
    读取段寄存器时只能读取可见的16位,但是写入的时候可以写入96位,(段寄存器的值是通过段选择子来填充的)
    mov es,ax
    给的16位数是随便写的吗:当我们执行mov ds,ax的时候,CUP会查表,根据ax(段选择子)的值来决定查找GDT还是LDT,查找表的什么位置,查出多少数据 (详见段选择子)
    当写寄存器的时候,只给了16位,剩下的80位从哪里来?
        96位:
其中16位:段选择子
其中80位:根据段选择子找到8字节(64位)的段描述符填充,G为标志位,一个G可以顶12位
G=0填充0x000,G为1填充0xFFF,其他位有些是重复使用了
(详见段描述符)

4. 段描述符与段选择子

4.1 GDT表(全局描述符)

同一台计算机上的所有程序共享一个GDT表,通过kd>r gdtr得到GDT表地址当我们执行mov ds,ax的时候,CUP会查表,根据ax的值来决定查找GDT表还是LDT表,查找表的什么位置,查出多少数据
实验:查看GDT表

打开windebug,kd>窗口输输入:r gdtr【查看GDT表的位置】 r gdtl【查看GDT表的大小】 ③dq ①得到的地址
    gdtr寄存器:48位的寄存器,存放内容:①GDT这张表的位置(32位),②GDT表的大小(16位)

    得到GDT表,里面存放的是段描述,每一个段描述符是8个字节,高32位对应的是前面4字节的值,低32位对应的是后面4字节的值

④dq 地址 L40【显示更多内容】

LDT表(局部描述符)


4.2 段描述符

typedef struct _DESCRIPTOR
{
    unsigned int limit1	:16;		// 段限长 [0-15]			//--------低地址--------
    unsigned int base1	:16;		// 段基址 [0-15]
    unsigned int base2	: 8;		// 段基址 [16-23]
    unsigned int TYPE	: 4;		// 类型
    unsigned int S		: 1:		// 系统段0 \ 用户段1
    unsigned int DPL	: 2;		// 段特权级别
    unsigned int P		: 1;		// 有效位
    unsigned int limit2	: 4;		// 段限长 [16-19]
    unsigned int AVL	: 1;		// 保留
    unsigned int L		: 1;		// 保留
    unsigned int DB		: 1;		// 默认大小
    unsigned int G		: 1;		// 限长的单位
    unsigned int base3	: 8;		// 段基址 [24-31]			//--------高地址--------
} DESCRIPTOR, *PDESCRIPTOR;
GDT是一张表(GDTR是一个寄存器),里面存放的是段描述符,每一个段描述符是8个字节

4.2.1 代码段或数据段:

AVL--- 供系统软件使用
BASE--段基址

D/B---- 默认操作大小 (0 = 16-bit segment; 1 = 32-bit segment)

DPL----描述符特权级别Descriptor privilege level

G------  Granularity

LIMIT-段长度Segment Limit

P--------当前段Segment present

S--------描述符类型Descriptor type (0 = system; 1 = code or data)

TYPE--段类型Segment type
段描述符是如何填充段寄存器的:(FS例外)
   WORD Selector:由段选择子填充
   WORD Attributes:对应段描述符高4字节的8-23位,刚好16位
   DWORD Base
高8部分(24~31):段描述符高4字节的24~31位
中8部分(16~23):段描述符高4字节的00~07位
低16部分(0~15):段描述符低4字节的16~31位
   DWORD Limit
高位部分(16~19位):段描述符高4字节的16~19位
低16位部分(0~15位):段描述符低4字节的0~15位
            上面2部分总共才20位
            但是段寄存器的Limit是32位的--》看段描述符的G位
            G=0时,Limit为000FFFFF
            G=1时,Limit为FFFFFFFF
P位:通过指令将段描述符加载到寄存器的时候第一件事就是检查P位,P为0将不会再继续,P为1会继续其他检查
          P=1:段描述符有效;P=0:段描述符无效
G位:粒度,G=1:以4kb为单位,Limit为FFFFFFFF(FFFFF*4KB+FFF);
                      G=0:以字节为单位,Limit为
000FFFFF(FFFFF*1);
DLP位:DPL存储在段描述符中,规定了访问该段所需要的特权级别是什么 (段描述符第5位数拆解后的后两位)
              情况①值为0:00;情况②值为3:11
S位:当S位为1时,有以下两种情况:①代码段描述符  ②数据段描述符
          当S位为0时,有以下情况:系统段的描述符
Type域:下面分析
DB位:下面分析
4.2.2 判断数据段和代码段
练习:判断是数据段还是代码段
  1. 判断段描述符是否有效【P位】判断【S位】
    p和s为对应【P+DPL+S】也对应第5个数字,如下图9。拆分后1001,1对应P,00对应DPL,1对应S
  2. 判断具体是数据段还是代码段【Type最高位】对应第6个数字,b,拆分后1011.最高位为1,是代码段
练习:通过段选择子填充段寄存器
mov es,0x1B
①拆解段选择子:00011   0   11 :倒数第3位为0,查GDT表
②找到段描述符:GDT表中第[3]个段描述符:00cffb00`0000ffff
        高4字节00cffb00:   00000000 1100111111111011 00000000    低4字节0000ffff:    00000000 00000000 11111111 11111111
③根据段描述符查表:00cffb00`0000ffff
段寄存器
   Selector:      0x001B [RPL = 3 、TI = 0 、Index = 3]
   Attributes:   1100111111111011 = CFFB
    Base:         00+00+0000 = 00000000
    Limit:         G=1, FFFFF+FFF
④拼凑段寄存器:转成16进制:FFFFFFFF00000000CFBB001B    共96位

4.2.3 系统段 调用门描述符:

S位为0,Type为1100,这就是系统段。真正跳转的地址是:段选择子指向的段描述符的Base+跳转地址

4.2.4 Type域

当S位不同时,Tpye域有不同的意义
4.2.2.1 S位为1时,Type的意义
  • S位为1,说明当前的段寄存器时数据段或代码段,参考下图
  • 数据段(0):
    • 第[11]位为0(Type中的第一位)
    • Type的最高位为0时,Type的值一定小于8    (Tpye的二进制最大值:0111)    -->段描述符的第6位数小于8,说明是数据段
    • A:段描述符的第[8]位,A  (访问位)
      • A为0:段描述符没有加载过,没有被访问过
      • A为1:段描述符被使用过,被访问过
    • W:段描述符的第[9]位,W  (可写位)
      • W为0:不可写
      • W为1:可写
    • E:段描述符的第[10]位
      • E为0,向上拓展,表示段述符地址的有效范围从Base开始+Limit(下图红色区域)
      • E为1:向下拓展,表示段描述符地址的有效范围除了从Base开始+Limit以外的地方(下图的红色区域)
  • 代码段(1)
    • 第[11]位为1(Type中的最高位)
    • Type的第一位为1时,Type的值一定大于等于8   (Tpye的二进制最小值:1000)    -->段描述符的第6位数大于等于8,说明是代码段
    • A:段描述符的第[8]位,A  (访问位)
      • A为0:段描述符没有加载过,没有被访问过
      • A为1:段描述符被使用过,被访问过
    • R:段描述符的第[9]位,R
      • R为0:不可读
      • R为1:可读
    • C:段描述符的第[10]位,C  (一致位)
      • C=0:非一致代码段(CPL=DPL且RPL<=DPL)只能同环访问
      • C=1:一致代码段(CPL>=DPL,3环可访问)也称为共享的段 (CPL>DPL时,是3环访问,CPL=DPL时,是0环访问),低特权可以访问高特权
4.2.2.2 S位为0时,Type的意义
当S位为0时,【描述符是系统段】,S+DPL+P不等于9或F  (第5位数)
4.2.2.3 Type小结
  • 1.代码段:第5位为9或F(  1+11+1  或  1+00+1  )
    • 第6位大于8(1000)
  • 2.数据段:第5位为9或F(  1+11+1  或  1+00+1  )
    • 第6位小于8(1000)
  • 3.系统段:第5位不等于9或F
  • 4.Type 
    • 第一位=1时,代码段:
    • 1            1        1             1
    • 代码  一致   可读   被使用过
    • 第一位=0时,数据段:
    •  0             1             1          1
    • 数据  向下拓展  可写    被访问

4.2.5 D/B位

高4字节的第[22]位
  • 情况一:对CS段的影响(D位)。
    • D=1:默认采用32位寻址方式
    • D=0:默认采用16位寻址方式
    • 前缀67 改变默认寻址方式
  • 情况二:对SS段的影响(B位)。
    • B=1:隐式堆栈访问指令    (如:call,push,pop,隐式修改esp)
      • 使用32位的堆栈指针寄存器
    • B=0:隐式堆栈访问指令    (如:call,push,pop,隐式修改sp)
      • 使用16位的堆栈指针寄存器
  • 情况三:向下拓展的数据段
    • D=1:段的上线为4GB(Limit是在这个范围之内的)
      • 数据段向下拓展的范围为:0~Base  +  Base+Limit~4GB(上图红色部分)
    • D=0:段的上线为4KB(Limit是在这个范围之内的)
      • 数据段向下拓展的范围为:0~Base  +  Base+Limit~4KB(上图红色部分)

4.3 段选择子

typedef struct _SELECTOR
{
    unsigned short index: 13;		// index 存在于 GDT 或 LDT 中的下标		
    unsigned short TI 	: 1;		// TI	当前查 GDT(0) 还是 LDT(1)
    unsigned short RPL	: 2;		// RPL  请求权限级别 
} SELECTOR, *PSELECTOR;
mov ax,fs
mov ds,ax【这里的as就是一个段选择子】
段选择子是一个16位的段描述符,该描述符指向了定义该段的段描述符
001B:  0000 0000 0001 1011
低2位:请求特权级别
第3位:0时查GDT表,为1时查LDT表。(Window下一般都是GDT表)
第4~15位:指向段描述符(下标)第[高12位的值]个段描述符。(也可以这样计算:值*8+GDT表的基址)
拆分段选择子:23
0000 0000 0010 0011

4.5 段权限检查

4.5.1 区分:RPL、CPL、DPL

RPL:请求特权级别;mov ax,001B    mov ds,ax    看我们提供的001B,转成二进制后的最后2位  //RLP是针对段选择子而言的,可以自己随便写
CPL:CPU当的特权级    看CS段SS段    //当前程序CS和SS段的权限(代码执行一定会用堆栈,就一定会用SS)
DPL:访问该段所需要的特权级别    看段描述符的DPL字段  //DPL存储在段描述符中,规定了【访问该段所需要的特权级别是什么】

4.5.2 当前程序的特权级(CPL)

CS和SS中,存储的【段选择子后2位】    如:001B: 0000 0000 0001 1011    后两位为11,值为3-->当前的程序处于3环
                                                                                  0008: 0000 0000 0000 0100    后两位为00,值为0-->当前程序处于0环

4.5.3 数据段权限检查

①CPL<=DPL  (当前程序权限是否足够)    并且RPL<=DPL  (段选择子请求的权限是否足够)    (数值越大,权限越小)

4.5.4 代码段的权限检查

(假设此处是代码段,不是调用门之类的)   区分是一致代码段还是非一致代码段   
①非一致代码段要求:CPL=DPL且RPL<=DPL
②一致代码段要求:    CPL>=DPL
满足上面权限才可访问
练习:
  • kd> r gdtr
  • gdtr= 8003f 000
  • kd> dq 8003f 000
  • 得到8个段描述符:
    • 在3环能加载的数据段有哪些?
    • 在0环能加载的数据段有哪些?
    • 详细描述这下面代码的执行过程:
      • mov ax,0x23
      • mov ds,ax

5 代码间的跳转(远跳)

(远跳转 JMP FAR;段间的跳转不是调用门之类的)
段间跳转,有2种情况:①一致代码段②非一致代码段
同时修改EIP和CS的指令JMP FAR指令(段跳转):当跨段的时候,不能仅仅修改Eip,因为Eip和CS段时一起的
应该使用以下指令修改:JMP FAR/CALL FAR/RETF/INT/IRETED
跳转指令:JMP 0x20:0x004189D7
1.段选择子拆分
0x20:0000 0000 0010 0000
    RPL=00        【请求级别】
    TI=0              【选择GDT表】
    index=100=4 【第[4]个段描述符】
2.查表得到段描述符
    判断是否是代码段【S位,Type最高位】
    只有代码段、调用门、TSS任务段、任务门才可以跳转(除了代码段,其他都是系统段描述符的)
3.权限检查
    假设此处是代码段,不是调用门之类的
    区分是一致代码段还是非一致代码段
    非一致代码段要求:CPL=DPL且RPL<=DPL
    一致代码段要求:  CPL>=DPL
满足上面权限才可访问
4.加载段描述符
    通过前面的权限检查后,CPU会将段描述符加载到CS段寄存器中
5.代码执行
    CUP将CS.Base+Offset(0x4189D7)的值写入EIP,然后执行CS:EIP处的代码,段间跳转结束

5.1 OD实现远跳

使用OD实现对EIP和CS的修改完成跳转
计算出段选择子

指令:JMP FAR 段选择子:77D4EAAB
如:    JMP FAR 20:77D47AAB

要保证JMP FAR 后面的段选择子最终指向的会是代码段,自己在GDT表中插入一个代码段描述符(可以直接复制cs段的段描述符),然后用这个代码段描述符的下标计算出段选择子
修改完之后,EIP和CS都被修改了,代码也执行到77D4EAAB了,只是段选择子改之前是CS即23,改之后是计算得来的段选择子

5.2 3环跳转0环

实现3环跳转到0环的代码段
假设需要跳转段描述符是00cffb00`0000ffff,此时,是代码段,并且是非一致代码段(b=1011)
正常跳转跳不过去,因为非一致代码段权限检查要求同环,但是我们可以修改为一致代码段,尝试跳转

1.我们可以通过Windebug在GDT表中修改段描述符,只是将该段描述符的属性改成一致代码段
   ①将代码段修改成系统段(第五位改成9)-->S+DPL+P==1+00+1==1001
   ②将非一致代码修改成一致代码段(第6位改成f,大于8,且第二为也是为1,1111)
    00CFFB00 0000FFFF修改成00CF9F00 0000FFFF
    【该段寄存器可由低权限访问高权限】
2.计算出段描述符的段选择子:Index+TI+RPL,
        TI是GDT表,故TI为0
        RPL是00权限,故RPL为00
        Index是GDT表中第几个段描述符
3.然后将指令JMP FAR 段选择子:跳转地址,(段选择子是第2步得到的段选择子)

4.执行代码 JMP FAR 段选择子:跳转地址

此时,执行代码的程序只是一个应用层的程序CPL是3,但是它要跳转的代码段的DPL是00(第1步的①修改的)
如果是非一致代码段(同环才可访问),是不可以跳转的,但是我们在第1步的②中将非一致代码段修改成了一致代码段(允许低权限访问高权限的代码段)

执行跳转之后,发现可以跳转成功,EIP和CS也都修改了。说明成功由低权限跳转到跳转到了系统的非一致代码段

5.3 远跳转总结

5.3.1 JMP FAR步骤:

  1. 拆分段选择子
  2. 查找段描述符
  3. 权限检查
  4. 加载段描述符
  5. 执行代码:Base+偏移

5.3.2 一致代码段

(也就是共享的段  ,给低权限用的)
  1. 权限低的可以访问权限高的
  2. 权限高的不可以访问权限低的
  • 作用:
    • 当3环需要访问0环的数据或代码时,但是这些数据或代码即是被修改也不会对系统内核造成破坏,此时就可以使用一致代码段,这样当访问或修改时,就不需要提权,进0环,返回3环等操作了

5.3.3 非一致代码段

(也就是普通代码)
  1. 只允许同环访问,CPL必须等于DPL,3环的不能访问0环的  (保证安全)
  2. 直接对代码进行JMP或CALL操作,无论目标是一致代码段还是非一致代码段,CPL的权限都不会发生改变,如果想要提升CPL的权限,只能通过"调用门"

6. 跨段调用

6.1 跨段不提权,长调用

(指令格式:CALL CS:EIP,EIP是被废弃的)
  1. 流程:
    1. ①查表,通过CS找到段描述符
    2. ②权限匹配
    3. ③若匹配成功,将段门描述符加载到CS段
    4. ④一旦执行成功,CS(段)就被换了。但是返回的时候,还需要返回原来的CS。所以原CS也会被push进栈
  2. 跨段不提权:当前的CPL等于要跳转的段的DPL
  3. CS:段选择子,通过它查找GDT表的一个段描述符,这个段描述符必须是一个调用门,通过调用门计算出需要执行的代码地址
  4. 堆栈变化:
  1. CALL执行前:
    1. 首先压入调用者的CS进栈
  2. CALL执行后:
    1. 将返回地址压入栈,ESP-8
  3. 执行完函数的代码,执行长返回RETF
    1. 将返回地址赋值给eip
    2. 将CS赋值给CS段寄存器
    3. ESP+8
  • 小结:CALL FAR不提权调用,通过CS在GDT表中找到调用门描述符,发生改变的寄存器:ESP  EIP  CS

6.2 跨段提权,长调用

(指令格式:CALL CS:EIP,EIP是被废弃的)
  1. 流程:
    1. ①查表,通过CS找到段描述符
    2. ②权限匹配
    3. ③若匹配成功,将段门描述符加载到CS段
    4. ④一旦执行成功,CS(段)就被换了。并且权限也提升了,CS权限提升,SS的权限也必须一起提升。而返回的时候,还需要返回原来的CS。而SS变化了,就不能再使用原来的堆栈了。所以原CS和SS还有ESP也会被push进栈。
  2. 跨段并提权:当前CPL权限大于要跳转的段的DPL权限
  3. CS:段选择子,通过它查找GDT表的一个段描述符,这个段描述符必须是一个调用门,通过调用门计算出需要执行的代码地址
  4. 堆栈变化:
CALL执行后:(此处假设是从3环跳到0环)
    此时,现在的堆栈已经不是原来3环的堆栈了,现在是0环的堆栈
    ①将调用者的SS压入0环的栈
    ②将调用者的ESP压入0环的栈 
    ③将调用者的CS压入0环的栈
    ④将返回值地址压入0环的栈
堆栈发生了切换,SS发生了切换,CS发生了切换

RETF执行后
    ①返回地址出栈
    ②调用者CS出栈
    ③调用者ESP出栈
    ④调用者SS出栈

发生改变的寄存器:
    CS    CS改变。由CALL后面跟的段选择子决定变成什么
    EIP   进入到0环了,需要执行的EIP不是之前的EIP了,是0环的EIP。由CALL后面跟的段选择子指向的段描述符的Base+offset决定
    SS     CS改变,SS一定也要改变。由TSS决定变成什么
    ESP  不是同一个堆栈了,ESP会改变。由TSS决定变成什么
调用
返回

6.3 长调用总结

  1. 跨段调用时,一旦有权限的切换,就会有堆栈的切换
  2. CS的权限一旦发生改变,SS的权限也要随着改变,CS与SS的等级必须一致 (这就是为什么要把CS压入栈的原因,因为CS换了,CS换了SS也跟着换了)
  3. JMP FAR无法跳转到不同环的非一致代码段,CALL FAR可以通过调用门提权,提升CPL的权限

7. 调用门提权

7.1 调用门

S位为0,Type为1100。真正调转的地址是:段选择子指向的段描述符的Base+跳转地址
  • 调用门的DPL应该是3,否则我们的程序将没有权限访问调用门。而调用门中的段选择子则可以的权限则可以是0,也可以是3,是0的时候,就是提权了,是3则是跨段不提权。堆栈图如6.1和6.2

7.2 调用门执行步骤

CALL CS:EIP(EIP是废弃的)
执行步骤:
  1. 根据CS的值,查找GDT表,找到对应的段描述符,这是一个调用门
  2. 在调用门描述符中存储了另外一个代码段的段选择子,这个段选择子应该要指向代码段。CS最终的值就是这个段选择子
  3. 选择指向的段,段.Base+偏移地址(门描述符的高四字节的高16位和低四字节的低16位) 就是真正要执行的地址

7.3 实验:构造调用门

实现跨段并提权,指令:CALL FAR CS:EIP
准备:在虚拟机中运行代码,在物理机的windbg中修改虚拟机的GDT表,调试虚拟机的内核

7.3.1 无参数

7.3.1.1 验证堆栈图试验
  1. 构造一个调用门段描述符
    1. 1.高4字节,跳转地址的高16位
      • 0000,跳转地址暂时不知道,写0。也可以直接在mian函数中写一段代码,获取了代码地址再写入进来
      2.高四字节,P+DPL+0+Type
      • 1+11+0+1100=EC
      • P:当前描述符有效,填1
      • DPL:填11,程序是从3环跳转到0环的,当前的CPL是11,没有权限访问00的段描述符
      • 0:默认是0
      • 1100:Type字段,1100表示当前描述符是调用门
      • 0000+EC
      3.高四字节,0-7位
      • 00
      • 第一个0是5-7位,默认是0;第二个0,代表不传参数
      • 0000+EC+00
      4.低四字节,16-31位 
      • 是一个段选择子,它指向了我们真正要执行的代码段
      • 不提权:001B(段选择子后两位11,指向GDT表第[3]个段描述符,该段描述符的第五位为9或F;3环代码段,DPL为11)
      • 提权:0008(段选择子后两位00,指向GDT表第[1]个段描述符,该段描述符的第五位为9或F;0环代码段,DPL为00)
      • 0000+EC+00+0008     此时段选择子的权限是0环的
      5.低四字节,0-15位
      • 0000,偏移,目前不知道,填0。也可以直接在mian函数中写一段代码,获取了代码地址再写入进来
      • 得到调用门段描述符:0000EC00 00080000
  1. 替换GDT表的段描述符
    1. 打开windbg,进入GDT表,将第10个段描述符换成第1步获取得到的段描述符(第10个是windows没有用到的段描述符,替换掉不会蓝屏)

      1.r gdtr                                                     得到GDT表地址8003f000
      2.dq 8003f000                                          得到第10个段描述符的地址:
      3.eq 8003f048 0000EC00 `00080000     将段描述符修改成第1步得到的段描述符
      • 此时,本机的GDT表已经增加了一个段描述符,第10个就是增加的
    2. 此时,替换了之后,一旦执行CALL FAR CS:EIP起来,就会执行CS指向的偏移,也就是上面的1和5两段偏移相加的地址。不管这个地址是在高2GB还是在低2GB,只要执行起来,它就是0环的权限。如果这段代码有int3断点,又是双击调式环境下,开着windng,那么就会断在windbg的0环中。
  2. 编写代码测试
    1. //长调用的格式:CALL FAR CS:EIP(EIP是废弃的)
      //1.编写需要调用的函数
      void __declspec(naked) GetRegsiter(){
      _asm {
          int 3   //int 3是中断的意思
          retf    //注意是长调用,需要使用长返回
          }
      }
      //__declspec(naked)是告诉编译器,函数代码的汇编语言是为自己所写的,不需要编译器添加任何代码,所以结尾处一定要字节写返回
      //2.编写main函数以及格式
      int main(){
          char buff[6];
          *(dword*)buff[0]=0x12345678;
          *(word*)buff[4]=0x48;//0x48是通过在GDT表添加的段描述符的位置计算出来的            
          _asm{
          	call fword ptr[buff]
          }
          getchar();
          return 0;
      }
  3. 修复GDT表新增的段描述符的地址
    • 此时已经有了需要跳转的函数的地址,下断点,看反汇编,得到函数GerRegsiter函数的地址,并将其拆分之后重新赋值给GDT表的第10个段描述符
  4. 执行代码,验证堆栈
    • 在调用函数前下个断点,查看CS,SS,ESP,EIP,用于与call之后之后做对比,验证跨段并提权的堆栈图
    • 会在int 3的时候断在windbg,先输入g,让程序继续执行,查看CS SS EIP ESP并记录
    • 重新执行代码,断在windbg后,查看CS,SS,EIP,ESP与之前是否一致,验证堆栈图 
    • 此时,可以查看当前0环的堆栈,在windbg点击查看寄存器,查看esp,然后跳转到esp的地址,查看堆栈
    • 此时的堆栈变化与跨段提权并调用的堆栈相符,说明提成功
7.3.1.2 获取高2GB的内核内存
  • 1.构造调用门段描述符
    • 1.高4字节,跳转地址得高16位
      • 0000,跳转地址暂时不知道,写0
      2.高四字节,P+DPL+0+Type
      • 1+11+0+1100=EC
      •  P:当前描述符有效
      •  DPL:填11,程序是从3环跳转到0环的,当前的CPL是11,没有权限访问00的段描述符
      • 0:默认是0
      • 1100:Type字段,1100表示当前描述符是调用门
      •  0000+EC
      3.高四字节,0-7位
      • 00
      • 第一个0是5-7位,默认是0
      • 第二个0,代表不传参数
      • 0000+EC+00
      4.低四字节,16-31位 
      • 是一个段选择子,它指向了我们真正要执行的代码段
      • 不提权:001B(段选择子后两位11,指向GDT表第[3]个段描述符,该段描述符的第五位为9或F;3环代码段,DPL为11)
      • 提权:0008(段选择子后两位00,指向GDT表第[1]个段描述符,该段描述符的第五位为9或F;0环代码段,DPL为00)
      • 0000+EC+00+0008     此时段选择子的权限是0环的
      5.低四字节,0-15位
      • 0000,偏移,目前不知道,填0
      • 0000+EC+00+0008+0000
      6.得到调用门段描述符:0000EC00 00080000
  • 2.替换GDT表的段描述符
    • 打开windbg,进入GDT表,将第10个段描述符换成第1步获取得到的段描述符(第10个是windows没有用到的段描述符,替换掉不会蓝屏)

      1.r gdtr                                                     得到GDT表地址8003f000
      2.dq 8003f000                                          得到第10个段描述符的地址:
      3.eq 8003f048 0000EC00 `00080000     将段描述符修改成第1步得到的段描述符
      此时,本机的GDT表已经增加了一个段描述符,第10个就是增加的
  • 3.编写代码测试
  • //长调用的格式:    CALL FAR CS:EIP(EIP是废弃的)
    //1.编写需要调用的函数
    DWORD dwH2GBValue;
    BYTE GDT[6];
    void __declspec(naked) GetRegsiter()
    {
        _asm {
            pushad
            pushfd
            mov eax,0x8003f00c  //读取高2GB内存
            mov ebx,[eax]
            mov dwH2GBValue,ebx
            //sgdt GDT    //将gtdr寄存器的内容放到6字节的GDT数组中,这个指令也可以在3环执行
            popfd
            popad
            retf
        }
    }
    //2.编写main函数以及格式
    int main(){
        char buff[6];
        *(dword*)&buff[0]=0x12345678;
        *(word*)&buff[4]=0x48;//0x48是通过在GDT表添加的段描述符的位置计算出来的
        _asm{
            call fword ptr[buff]      //跨段调用并提权
         }
        printf("%x",dwH2GBValue);
        getchar();
        return 0;
    }
  • 4.修复GDT表新增的段描述符的地址
    • 此时已经有了需要跳转的函数的地址,下断点,看反汇编,得到函数GetRegsiter函数的地址,并将其拆分之后重新赋值给GDT表的第10个段描述符
  • 5.执行代码,验证读取高2G内存

7.3.2 有参数

  • 1.构造有参的调用门描述符
    • 0000+EC+03++0008+0000 (03-->传递3个参数)
  • 2.修改GDT表
    • 将新构造的调用门描述符添加到GDT表中的第10个描述符
    • eq 8003f048 0000EC03`00080000
  • 3.编写代码
  • //长调用的格式:    CALL FAR CS:EIP(EIP是废弃的)
    //1.编写需要调用的函数
    DWORD x,y,z;
    void __declspec(naked) GetRegsiter(){
        _asm {
            pushad
            pushfd
            mov eax,[esp+0x24+0x8+0x8]    
            //pushad:esp+0x20;
            //pushfd:esp+0x4
            //长调用call,esp+8
            mov dword ptr ds:[x],eax
            mov eax,[esp+0x28+0x8+0x4]
            mov dword ptr ds:[y],eax
            mov eax,[esp+0x28+0x8+0]
            mov dword ptr ds:[z],eax
            popfd
            popad
            retf 0xc   //平衡堆栈,否则会蓝屏
        }
    }
    //2.编写main函数以及格式
    int main(){
        char buff[6];
        *(dword*)&buff[0]=0x12345678;
        *(word*)&buff[4]=0x48;//0x48是通过在GDT表添加的段描述符的位置计算出来的
        _asm{
            push 1
            push 2
            push 3
            call fword ptr[buff]
        }
        printf("%x %x %x",x,y,z);
        getchar();
        return 0;
    }
  • 4.获取跳转地址,修复GDT表
    • 下断点获取函数地址,拆分后
    • 高位放GDT表中门描述符的高4字节的高16位
    • 低位放GDT表中门描述符的低4字节的低16位
  • 5.执行函数,验证输出

7.4 总结

  • 0.调用门使用步骤
    • 构造门描述符,门描述符中的段选择子是一个代码段,这个代码段是0环还是3环可以由我们自己决定(填写后两位)
    • 门描述符中的代码段的Base+偏移是我们真正要跳转的地址(地址就是函数地址,代码段提权的话,可以做0环操作)
    • 将门描述符写到GDT表
    • CALL FAR的时候,通过CALL 段选择子,在GDT表中找到门描述符,通过门描述符完成调用函数
  • 1.调用门跨段不提权时,0环堆栈只会PUSH两个参数,CS和返回地址,原CS在PUSH后会被替换。新的CS是调用门的段选择子,此时CPL就会变成调用门提供的代码段的CPL,从而完成CPL提权
  • 2.调用门跨段并提权时,0环堆栈会PUSH四个参数,CS,ESP,SS,返回地址,新的CS由调用门提供,SS和ESP由TSS提供
  • 3.通过调用门时,调用的代码是新的CS.Base+偏移,由调用门决定。但是RETF返回的时候,出栈的值可以由我们自己写,也就是说进去得到时候需要通过调用门,但是返回的时候,返回到哪里可以由我们决定,只需要修改堆栈的返回地址
  • 4.我们也可以选择进入调用门之后,不返回,自己再使用一个call调用门来返回
  • 5.有参使用调用门的时候,CALL前PUSH参数,调用完成之后平衡堆栈:retf 0xN

8. IDT表

IDT表的构成
  • 任务门描述符
  • 中断门描述符
  • 陷阱门描述符
中断门的五六位通常是ee或者8e,任务门通常是85,陷阱门通常是8f都是随着Type位的改变而改变

9. 中断门

  • 1.IDT表中存储的都是系统段描述符
  • 2.IDT表第一个元素不是NULL
  • 3.windbg查看IDT表:r idtr
  • 4.查看idt表有多大:r idtl

9.1 中断门

9.1.1 中断门段描述符

  • 高四字节
    • 高16位、低四字节的低16位与调用门一致,是代码段的偏移地址,代码段取决于低四字节16~31的段选择子
    • 0~7位,调用门可以传递参数,但中断门不可以传参数,为0 
    • Type(8~11位)为1110,12位为0,时,此时段描述符为中断门
  • 中断门会把 IF 位置 0:中断门执行的时候,CPU会将标志寄存器IF位清零,进入中断门,如果还有可屏蔽中断信号到来,CUP将不理睬。
  • 中断门堆栈图
    • 提权
      •  
      • 和调用门稍稍有点不一样的地方是,中断门提权会在堆栈中多压入一个值——EFLAGS.
    • 不提权
      • 如果不提权,意味着不会切换栈,所以也没有必要在栈中压入栈段选择子和栈顶指针
      • 调用门返回时RETF,而中断门返回是使用IRET/IRETD

9.1.2 使用中断门

  • 格式:INT 3表示:IDT表第[3]个中段描述符
    • 1.构造中断门段描述符
      • 1.高4字节,跳转地址的高16位
        • 0000,跳转地址暂时不知道,写0
        2.高四字节,P+DPL+0+Type    此时Type为1110
        • 1+11+0+1110=EE
        • P:当前描述符有效
        • DPL:填11,程序是从3环跳转到0环的,当前的CPL是11,没有权限访问00的段描述符
        • 0:S位,系统段描述符为0
        • 1110:Type字段,1110表示当前描述符是中断门
        • 0000+EE
        3.高四字节,0-7位
        • 00
        • 中断门不允许传参,所以填00
        •  0000+EE+00
        4.低四字节,16-31位 
        • 是一个段选择子,它的Base加上中断门的偏移字段,指向了我们真正要执行的代码段
        • 不提权:001B(段选择子后两位11,指向GDT表第[3]个段描述符,该段描述符的第五位为9或F;3环代码段,DPL为11)
        •  提权:0008(段选择子后两位00,指向GDT表第[1]个段描述符,该段描述符的第五位为9或F;0环代码段,DPL为00)
        •  0000+EE+00+0008     此时段选择子的权限是0环的
        5.低四字节,0-15位
        • 0000,偏移,目前不知道,填0
        • 0000+EC+00+0008+0000
        6.得到调用门段描述符:0000EE00 00080000
    • 2.替换IDT中的中断门描述符
    • 查IDT表发现,第0x20个中断描述符没有使用,
      此处试验将第0x20也就是第[32]个中断描述符替换掉
      r idtr得到8003f400
      eq 8003f500 0041EE00 000813A0
    • 3.编写代码测试
    • //长调用的格式:CALL FAR CS:EIP(EIP是废弃的)
      //1.编写需要调用的函数
      DWORD dwH2GBValue;
      BYTE GDT[6];
      void __declspec(naked) GetRegsiter(){
          _asm {
           pushad
           pushfd
           mov eax,0x8003f00c  //读取高2GB内存
           mov ebx,[eax]
           mov dwH2GBValue,ebx
           //sgdt GDT    //将gtdr寄存器的内容放到6字节的GDT数组中,这个指令也可以在3环执行
           popfd
           popad
           iret
          }
      }
      //2.编写main函数以及格式
      int main(){
          _asm
          {
              int 0x20      // 选择IDT表中的第[0x20]个中段描述符
          }
          printf("%x",dwH2GBValue);
          getchar();
          return 0;
      }
    • 4.修正IDT表的中段描述符,编译代码,查看函数地址
    • 5.执行程序,查看返回值

9.1.3 总结

  • 使用步骤
    • 1.构造中断门段描述符
    • 2.替换IDT中的中断门描述符
    • 3.编写代码测试
    • 4.修正IDT表的中段描述符,编译代码,查看函数地址
    • 5.执行程序,查看返回值
  • 中断门使用:
    • 格式:INT N
    • 通过IDT表找到第[N]个门描述符。(通过里面的段选择子可查GDT表
    • 通过门描述符的Base+偏移调用指定的地址
  • 中断门执行的时候,CPU会将标志寄存器IF位置零,进入中断门,IF位为0时,如果还有可屏蔽中断信号到来,CUP将不理睬。
  • 拓展:一旦有不可屏蔽中断信号,不管IF是什么,CUP都会马上执行
    • 例如:电源断开,CUP会使用电容的电量,马上做一些保存操作

10. 陷阱门

10.1 陷阱门段描述符

  • 高四字节
    • 高16位,低四字节的低16位与调用门一致,是代码段的偏移地址,代码段取决于低四字节16~31的段选择子
    • 0~7位,调用门可以传递参数,中断门不可以传参数,为0
    • 8~11位为1111,12位为0,时,此时段描述符为陷阱门
  • 低四字节
    • 与之前的段描述符一致 
  • 陷阱门与中断门唯一的区别
    • 中断门执行的时候,会将标志寄存器IF位清零,但是陷阱门不会
    • Type为1111,中断门是1110

11. 任务段

11.1 任务门的作用

  • 权限切换-->CS更换
  • CS更换-->SS更换
  • SS更换-->堆栈更换
  • 堆栈更换-->ESP更换
  • CS是我们指定的段选择子
  • SS和ESP就是从TSS(任务状态段)中提供的

11.2 TSS结构

  • TSS是一块内存,大小是104字节,所有寄存器都存在这里
  • TSS可以一次性的替换这一堆寄存器

11.2.1 CUP寻找TSS的方法

  • 1.TSS内存:通过TR(TaskRegister)段寄存器得到TR.Base指向了TSS在哪里,TR.Limit表明TSS的大小
  • 2.TR段寄存器:操作系统启动的时候,通过GDT表中加载TSS段描述符得到的

11.2.2 TSS段描述符

  • (TSS段描述符在GDT表中)
  • TSS段描述符的Type:
    • ①1001==9:此段描述符没有加载到TR段寄存器中
    • ②1011==B:此段描述符已经加载到TR段寄存器中
    • TSS段的Limit是104字节,高位Limit为0即段描述符G位为0

11.2.3 TR寄存器读写

    • LTR r/m16
    • 在GDT表中找到与r/m16对应的段描述符,然后将段描述符加载到TR寄存器中
    • 加载完成后,被加载的TSS段描述符状态位发生改变(9变成B)
    • LTR是特权指令,只能在0环执行,它只会修改TR段寄存器,不会修改TSS的104字节
    • STR r/m16
    • 将TR段寄存器的段选择子存储到r/m16

11.2.4 实验:修改当前环境所有寄存器

(替换一堆寄存器)
步骤:
  • 1.编写代码,并准备一份104字节的缓冲区
  • 2.构造TSS段描述符,Base指向缓冲区,Limit为104字节
  • 3.修改GDT表,增加TSS段描述符
  • 4.修改TR寄存器以及当前寄存器
  • 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段
实现:
  • 1.编写代码,并准备一份104字节的缓冲区
  • DWORD dwOK;
    DWORD dwESP;
    DWORD dwCS;
    char du[0x10];
    void __declspec(naked)func(){
       dwOK=1;
       __asm{
       mov eax,esp
       mov dwESP,eax
       mov ax,cs
       mov dwprd ptr ds:[dwCS],cs
       //跳回去的代码
       //
       //
       }
    }
    //eq 8003f0c0 0000e912`fdcc0068//构造段描述符
    int main(){
        int iCr3;
        printf("输入进程ID\n");//通过windbg!process 0 0 指令获取,选择DirBase字段
        scanf("%x",&iCr3);
        DWORD szTSS[0x68]={
            0x00000000, //link    //切换前,存储是的原来的段选择子,切换的时候CUP会自动填充
            0x00000000,//esp0    //(DWORD)bu
            0x00000000,//ss0      //0x00000000
            0x00000000,//esp1
            0x00000000,//ss1
            0x00000000,//esp2
            0x00000000,//ss2
            (DWORD )iCr3,//cr3,是一个寄存器,后面会讲
            0x00401020,//eip//要跳转的地址
            0x00000000,//eflags
            0x00000000,//eax
            0x00000000,//ecx
            0x00000000,//edx
            0x00000000,//ebx
            (DWORD)bu,//esp,一个地址,指定一个切换之后的堆栈(这里是0x10个字节的堆栈)
            0x00000000,//ebp
            0x00000000,//esi
            0x00000000,//edi
            0x00000023,//es
            0x00000008,//cs  去什么环就写什么权限的代码段,3环的话写0x1B
            0x00000010,//ss   SS必须与CS在同一个环,3环的话写0x23
            0x00000023,//ds
            0x00000030,//fs   切换到0环写0x30,切换到3环写0x0000003B
            0x00000000,//gs
            0x00000000,//ldt
            0x20ac0000//可以从操作系统直接复制出来
            };
            char buff[6];
            *(DWORD*)&buff[0]=0x12345678;
            *(WORD*)&buff[4]=0xC0;
            __asm{
                call fword ptr[buff]
            }
            printf("ok=%d ESP = %x  CS=%x \n",dwOK,dwESP,dwCS);
    }
  • 2.构造TSS段描述符,Base指向缓冲区,Limit为104字节
    • 按照下图构造,但是G位与之前构造描述符不一样,由TSS段描述符得到的TR寄存器指向的段是TSS段,TSS段的长度是以字节为单位的,之前的都是1(4KB),现在应该改成0(字节)
    • 下断点,获取iTSS数组和跳转函数的地址
    • 构造完成后,将构造好的TSS段描述符写入到空白的GDT表中
      TSS段描述符的Type:
      ①1001==9:此段描述符没有加载到TR段寄存器中
      ②1011==B:此段描述符已经加载到TR段寄存器中
      //TSS数组地址:0012 f184    TSS段的Base就是TSS数组的地址
      TSS数组的大小只有104字节,使用低四字节的低16位就足够了,Limit16~19可以设置为0了
      高四字节:
       0    0    0    0        0    0    0    0        0    0    0    0         0   0    0    0        1    1    1    0        1    0    0    1         0    0    0    1        0    0    1    0
                 0                          0                         0                           0                          e                          9                          1                           2
      低四字节:
      f184                                                   0068
      最终得到TSS段描述符:
      0000e912`f1840068

  • 3.修改GDT表,增加TSS段描述符
    • 进入GDT表,随便找到一个空白的段描述符此处是第10个
    • 填充:
r gdtr
dq 8003f000
eq 8003f0b0 0000e912`f1840068
    • 此处填充的是第10个,01001 0 11
    • 得到段选择子是0x4B,此时4B指向的段描述符是TSS段描述符
  • 4.修改TR寄存器以及当前寄存器
    • 在0环修改TR寄存器LTR r/m16
      • 只能在0换用
      • 只会修改TR寄存器的值
    • 在3环修改TR寄存器:JMP FAR或CALL FAR
      • 用 JMP 去访问一个代码段的时候
        • JMP 0x48:0X12345678 如果0x48是代码段
        • 执行后:CS-->0x48 EIP -->0X12345678
      • 用JMP去访问一个任务段的时候:
        • 如果0x48是TSS段描述符,则先取出TSS段描述符,修改TR寄存器,然后用TR.Base指向的TSS中的值修改当前的一堆寄存器
  • 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段

11.2.5 总结

  • 替换当前寄存器步骤:
    • ①构造TSS:创建104字节缓冲区,并填充里面的内容(寄存器), DirBase需要!process 0 0获取
    • ②构造TSS段描述符(Base,Limit)指向①的缓冲区
    • ③在GDT表中增加②的TSS段描述符
    • ④加载TR寄存器并修改当前寄存器
      • JMP/CALL FAR指令后面跟的若是任务段的选择子,系统会通过选择
      • 子将段选择子指向的TSS段描述符加载到TR寄存器中
      • 然后通过TR寄存器的Base找到TSS,将TSS的内容(寄存器)替换当前的寄存器
  • TSS段描述符与TR段寄存器与TSS段的关系
    • TSS段在GDT表中
    • TR寄存器通过TSS段描述符加载
    • TSS段的地址与大小是通过TR寄存器得到
  • 注意JMP和CALL的堆栈变化

12. 通过任务门访问任务段

12.1 任务门描述符

    • 灰色部分是保留部分,填0就可以
    • Type为0101的时候,是任务门
    • 低四字节的高16位:存储的是一个TSS段的段选择子,用于在GDT表中寻找TSS段描述符

12.2 任务门执行流程

  • 1.  INT N
  • 2.  查IDT表,找到任务门描述符
  • 3.  通过任务门描述符的TSS段选择子,查找GDT表,找到任务段描述符
  • 4.  将任务段描述符加载到TR寄存器中,然后使用TR寄存器Base指向的TSS中的值修改寄存器
  • 5.  IRETD返回

12.3 实验

  • 1.编写代码,并准备一份104字节的缓冲区
  • 2.构造TSS段描述符
  • 3.修改GDT表
  • 4.构造任务门描述符,写入到IDT表中
  • 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段
  • -------------------------------------------------代码---------------------------------------------------------------
  • 1.编写代码,并准备一份104字节的缓冲区
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked)func(){
        dwOK=1;
        __asm{
                mov eax,esp
                mov dwESP,eax
                mov ax,cs
                mov dwprd ptr ds:[dwCS],cs
                //跳回去的代码
                //
                //
        }
}
int main(){
        int iCr3;
        printf("输入进程ID\n");//通过windbg!process 0 0 指令获取,选择DirBase字段
        scanf("%x",&iCr3);
        DWORD szTSS[0x68]={
                0x00000000, //link    //切换前,存储是的原来的段选择子,切换的时候CUP会自动填充
                0x00000000,//esp0    //(DWORD)bu
                0x00000000,//ss0      //0x00000000
                0x00000000,//esp1
                0x00000000,//ss1
                0x00000000,//esp2
                0x00000000,//ss2
                (DWORD )iCr3,//cr3,是一个寄存器,后面会讲
                0x00401020,//eip//要跳转的地址
                0x00000000,//eflags
                0x00000000,//eax
                0x00000000,//ecx
                0x00000000,//edx
                0x00000000,//ebx
                (DWORD)bu,//esp,指定一个切换之后的堆栈
                0x00000000,//ebp
                0x00000000,//esi
                0x00000000,//edi
                0x00000023,//es
                0x00000008,//cs   0x0000001B,需要与SS在同一个环,cs08ss就10,cs1B,ss就23
                0x00000010,//ss   0x00000023
                0x00000023,//ds
                0x00000030,//fs   切换到0环写0x30,切换到3环写0x0000003B
                0x00000000,//gs
                0x00000000,//ldt
                0x20ac0000//可以从操作系统直接复制出来
        };
        __asm{
              int 0x20
        }
        printf("ok=%d ESP = %x  CS=%x \n",dwOK,dwESP,dwCS);
}
  • 2.构造TSS段描述符
    • 按照下图构造,但是G位与之前构造描述符不一样,由TSS段描述符得到的TR寄存器指向的段是TSS段,TSS段的长度是以字节为单位的,之前的都是1(4KB),现在应该改成0(字节),下断点,获取iTSS数组和跳转函数的地址,构造完成后,将构造好的TSS段描述符写入到空白的GDT表中
    • TSS段描述符的Type:
    • ①1001==9:此段描述符没有加载到TR段寄存器中
    • ②1011==B:此段描述符已经加载到TR段寄存器中
    • //TSS数组地址:0012 f184    TSS段的Base就是TSS数组的地址,通过运行代码下断点得到
    • TSS数组的大小只有104字节,使用低四字节的低16位就足够了,Limit16~19可以设置为0了
    • 高四字节:     
    •                                                  0000                                                                                                                   e912  
    • 31  30  29  28      27  26  25  24      23  22  21  20      19  18  17  16                15  14  13  12      11  10  09  08       07  06  05  04      03  02  01  00    
    •  0    0    0    0        0    0    0    0        0    0    0    0         0   0    0    0                  1    1    1    0        1    0    0    1         0    0    0    1        0    0    1    0
    •            0                          0                         0                           0                                    e                          9                          1                           2
    • 低四字节:
    • f184                                                   0068
    • 最终得到TSS段描述符:
    • 0000e912`f1840068
  • 3.修改GDT表
    • 进入GDT表,随便找到一个空白的段描述符
    • 填充:
    • r gdtr
    • dq 8003f000
    • eq 8003f0b0 0000e912`f1840068
    • 此处填充的是第10个,01001 0 11
    • 得到段选择子是0x4B,此时4B指向的段描述符是TSS段描述符
  • 4.构造任务门描述符,写入到IDT表中
  • 5.执行代码,windbg下断点,输入!process 0 0 指令获取,选择DirBase字段

12.4 总结

  • 任务门跨表了需要在IDT表和GDT表添加描述符
  • 执行流程:
    •     1.  INT N
    •     2.  查IDT表,找到任务门描述符
    •     3.  通过任务门描述符的TSS段选择子,查找GDT表,找到任务段描述符
    •     4.  将任务段描述符加载到TR寄存器中,然后使用TR寄存器Base指向的TSS中的值修改寄存器
    •     5.  IRETD返回
  • 实验步骤:
    •     1.构造TSS段描述符,TSS段描述符的Base指向TSS缓冲区,Limit为104字节
    •     2.将TSS段描述符写入GDT表
    •     3.构建任务门描述符,任务门描述符的段选择子指向GDT表的TSS段描述符
    •     4.将任务门描述符写入IDT表
    •     5.使用任务门 INT N

windbg常用指令:

r:查看寄存器
dd:查看地址内容,以dword字节分组显示
dq:查看地址内容,以8字节分组显示







--------------------------------------保护模式-页的机制 ------------------------------------

1. 10-10-12分页

1.1 为什么是10-10-12?

  • 将线性地址分成10-10-12
    • 2的10次方=1024    PDT有1024个PTT
    • 2的10次方=1024    PTT有1024个物理页
    • 2的12次方=4096    物理页有4096个字节

1.2 PDT、PDE、PTT、PTE

1.2.1 4GB内存

  • 给应用程序分配的4GB内存时虚拟的,让真正需要用到内存的时候,会通过地址转换找到物理地址

1.2.2 物理地址

  • 线性地址   有效地址   物理地址
    • mov eax,dword ptr ds:[0x12345678]
    • 有效地址:0x12345678
    • 线性地址:ds.Base+0x12345678
    • 通常情况下,ds这个段的Base都是0
    • cup会将线性地址转换成物理地址,再从物理地址中取出数据

1.2.3 设置cup分页模式

  • 10-10-12分页:C盘booit.ini中,启动项的最后一项为execute=optin时,是101012的方式
  • 2-9-9-12分页:C盘booit.ini中,启动项的最后一项为noexecute=optin时,是29912的方式

1.2.4 PDT与PTT表

  • 每个进程都有一个CR3寄存器,指向当前进程对应的PDT,Cr3是唯一一个存储物理地址的寄存器,而程序是无法直接使用物理地址的
    • 每个进程中的数据都是通过Cr3找到物理页,然后再通过物理页进行读写的
    • 通过Cr3寄存器可以实现读取目标进程的数据、写入数据到目标进程,只要将目标进程的Cr3暂时修改为本进程的Cr3即可,这样可以更加高级的完成注入(将代码写入到目标程序,比注入更高级,更加难以检测)
  • 通过!PROCESS 0 0找到对应进程的Cr3(DirBase字段)
  • windbg中,!dd可以读取CR3物理地址地址的内容,指向一个4096大小的页目录表(PDT)
  • PDT:页目录表,也叫PDT,4096大小。PDT表中成员叫页目录项(PDE),是地址,占4字节。
  • PTT:页表,也叫PTT,4096大小。PTT表中成员叫PTE,是指向物理页的地址,占4字节。
    •  PTE可以指向物理页,但是没有给PTE分配物理页的时候PTE也可以不指向物理页
    • 多个PTE可以指向同一个物理页
    •  一个PTE只能指向一个物理页
  • 系统通过PTE找到真实的物理地址,通过线性地址找到PTE。线性地址是程序内的线性地址,不同程序可能会出现相同数值的线性地址,但是指向的物理地址却很可能不相同。因此每一个程序都有自己独一无二的一套PTT页表,与线性地址一一对应。

1.2.5 实验:线性地址找物理地址

  • 1.在虚拟机设置为101012分页,重启
  • 2.在虚拟机中启动一个记事本,写上Hello World,记事本拥有自己的4GB空间,helloworld一定在这片里面
  • 3.找到HelloWorld的线性地址
    • 通过Cheat Engine工具附加记事本
    • 勾选Unicode,搜索HelloWorld
    • 修改HelloWorld,找到线性地址AA8A0
  • 4.按照101012分页的方式,找到物理地址
    • 实验工具:windbg,CE
    • 按照10-10-12的形式,将32位的线性地址分成三份
    • 000AA8A0
    • 0000000000     0010101010      8A0
    •         0                    AA             8A0
    • 每个进程都有一个CR3寄存器,Cr3是唯一一个存储物理地址的寄存器    通过!process 0 0找到对应进程的Cr3(DirBase字段),CR3指向一个4096大小的页目录表(PDT),假设找到的PDT为146a0000
      •     在PDT中,根据线性地址的第一个10,找的PDE是:PDT的地址+(10*4),PDE指向一个4069大小的页表(PTT)
      •                         !dd 146a0000+0*4          得到PTT的地址14dc0067【PTT的后3位代表属性,用的时候需要改成000】
      •     在PTT中,根据线性地址的第二个10,找的地址是:PTT的地址+(10*4),这个地址指向真正的物理页
      •                         !dd 14dc0000+AA*4     得到真正物理页的地址13fdd067【地址的后3位代表属性,用的时候需要改成000】
      •     在真正物理页中,根据线性地址的12,找的地址是:物理页的地址+(10),这个地址就是物理地址
      •                         !dd 13fdd000+8A0      得到真正要找的物理地址,物理地址里面存的就是要找的数据
      •                         !db 13fdd000+8A0      以字节方式查看,可以看得更清晰
    • 小结
      • !dd,加了一个!,意思是查看物理地址
      • PDT:页目录表,也叫PDT,4096大小。PDT表中成员叫页目录项(PDE),是地址,占4字节。
      • PTT:页表,也叫PTT,4096大小。PTT表中成员叫PTE,是指向物理页的地址,占4字节。
        • PTE可以指向物理页,但是没有给PTE分配物理页的时候PTE也可以不指向物理页
        • 多个PTE可以指向同一个物理页
        •  一个PTE只能指向一个物理页
      • 10-10-12
      • 10:按下标查找个数,第一张表存储2的10次方=1024个地址
      • 10:按下标查找个数,第二张表存储2的10次方=1024个地址
      • 12:按字节查找数据,第三张表存储2的12次方=4096个字节
      • 寻找物理地址步骤
        • 1.获取PDT                               Cr3(PDT)
        • 2.获取PDE(是一个PTT页)         Cr3+10*4
        • 3.获取PTE(是一个物理页)     PTT+10*4
        • 4.获取物理地址                       物理页+12

1.2.6 实验:对0地址进行读写

  • 正常程序的0地址都是不可写入和读取的,是因为0的线性地址为00000000
  • 通过PDT+0找到PDE
  • 通过PDE+0找到PTT
  • 通过PTT+0找到PTE---->为0,说明0的线性地址所对应的物理页地址是空的
  • 通过PTE找到内存页为空
  • 会发现,线性地址000000是没有分配物理页的,既然没有分配物理页,何来读写呢?,但是我们可以手动给0这个线性地址分配一个物理页,达到可以对0线性地址读写
  • 实验
    • 1.定义一个变量,获取变量的线性地址,并打印出控制台,然后getchar()等待
    • 2.打开windbg,通过1.2.5找到该变量线性地址的PTE对应的物理地址,并记录下来(PED和PTE的后三位是属性,最好也记录下来,若原属性是不能读写的属性,那么修改成可读写属性)
    • 3.找到0的物理地址,将0线性地址的PTE修改为第2步获取的线性地址,写入操作指令:
      • 指令格式:!ed PTE地址 第2步得到的PTE
      • 此时,0的线性地址已经分配了一个物理页,和第2步的变量共用一个物理页。
    • 4. 随意输入字符,令getchar()不再等待
      •     对0线性地址进行读写操作
      •     *(int*)0=123;
      •     int n = *(int*)0;
    • 原理:线性地址0:页目录表->页目录->页表->物理地址没有分配    将线性地址设置为可读写:那我们把物理地址分配上去即可
    • 代码
#include <iostream>
#include <windows.h>
int main() {
    int n = 100;
    printf("写入数据前n的值 = %d\n", n);
    printf("n的地址 = %p\n", &n);
    int n0 = 0;
    getchar();
    //windbg修改物理页
    *(int*)n0 = 2000;
    printf("向0地址写入数据:%d\n写入数据后n的值 = %d\n",2000, n);
    system("pause");
    return 0;
}

1.2.7 PDE、PTE属性

PDE和PTE的低3位也就是二进制的低12位是属性,0~11是属性位,12~31是页表基址,指向物理页的基址
  • P位:代表是否有效
  • R/W位:0只读,1可写
  • US位: ①U/S=0,本物理页只允许特权用户访问       ②U/S=1,本物理页普通用户也可以访问  【之前学的各种门需要00权限才能读写就是因为这个原因】,修改变量的U/S位,也可以达到让3环访问0环的目的(获取一个需要读取的0环的线性地址如0x8003f00c,更改其属性)
  • PS位:PS位只对PDE有意义,PS是PageSize的意思。当PS=1时,PDE直接指向物理页,而不是PTT页,没有PTE,此时10-10-12变成10-22,22位直接指向页内偏移,即 通过Cr3找到PDT页,PDT+(第一个10位*4)就是PDE,值就是物理页,PDE+22位的数值=物理地址。范围是4M,俗称大页。
  • A位:当前页是否被访问过,即是是访问1字节也会导致A位置为1。未访问过则为0
  • D位:当前页是否被写过,0没有被写过,1被写过
  • G位:G位为1时,TLB表不会刷新G为为1的PDE或PTT   (G为为1的都是高2GB地址,所有程序可以共享)  详情参考TLB一章
  • PWT位:(Page Wrrte Through)   PWT=1时,当将当前页的数据写入到CPU缓存的时候,要求同时将数据也写入到内存中,PWT为0则没有这个要求。
  • PCD位:(Page Cache Disable)   PCD=1时,当前页的数据禁止写入到缓存中,CPU需要读写的话,只能直接写入到内存中
    • 例如,TLB中的页,这些页已经存在TLB缓存中了,可能不需要在写入到CPU缓存了。所以他们的PCD为1。
  • PAT位:
  • PWT、PCD位前置知识-->CPU缓存
    • 1.CPU缓存是位于CPU与物理内存之间的临时存储器,它的容量比内存小得多,但是交换速度比内存快的多
    • 2.CPU缓存可以做得很大,几K,几十K,上M的都有
    • 3.CPU缓存与TLB的区别
      •     TLB:        线性地址  <---->  物理地址
      •     CPU缓存:物理地址  <---->  内容
      •     系统读取某个物理页的时候:
        •     ①查找TLB得到物理地址
        •     ②查找CPU缓存得到数据
        •     CPU缓存越大,速度越快
      • 具体细节参考Inter白皮书章节:Memory Cache Control

1.2.8 实验:实现修改只读变量

  • 物理页的属性=PDE属性&PTE属性
  • 定义一个只读类型的变量,在另一个线性地址指向与该变量相同的物理页,但是这个线性地址的PDE/PTE的R/W属性改成1可写属性,实现可写
    • 1.定义只读变量
    • 2.获取只读变量的线性地址
    • 3.通过线性地址获取PDE和PTE
    • 4.将PDE和PTE的R/W位属性改成1可写属性
    • 5.对只读变量进行赋值操作,完成对只读变量的读写
    • int main(){
          char* str ="hello world";
          printf("线性地址:%x",(DWORD)str);
         
          //windbg修改PDE和PTE属性
      
          getchar();
          DWORD dwVal=(DWORD)str;
          *(char*)dwVal="M";
          printf("修改后的值:%s\n",str);
          return 0;
      }

1.2.9 实验:实现访问高2GB内存

  • 物理页的属性=PDE属性&PTE属性
  • 确定一个高2GB的线性地址,如0x8003f00c。查看8003f00c的PDE和PTE属性,并将U/S修改为1,实现普通用户也可访问
    • 1.通过线性地址获取PDE和PTE
    • 2.将PDE和PTE的U/S位属性改成1普通用户可访问
    • 3.对线性地址进行读取操作,完成对高2GB数据的读取
    • int main(){
        
          //windbg修改PDE和PTE属性
      
          getchar();
          int Vule=0x8003f00c;
          printf("0x8003f00c的数据:%s\n",*(DWORD*)Vule);
          return 0;
      }
  • 思考
    • 如果系统要保证某个线性地址是有效的,那么必须为其填充正确的PDE与PTE,如果我们想填充PDE与PTE那么必须能够访问PDT与PTT,那么存在2个问题:
      • 1、一定已经有“人"为我们访问PDT与PTT挂好了PDE与PTE的物理页,我们只需要找到这个线性地址就可以了。但是这个地址在哪里?按照以往的知识,我们是通过访问Cr3寄存器得到PDT表的。但是程序并不能使用物理地址,也就是说Cr3寄存器对程序来说并不能使用,那怎么才能访问PDT和PTT表呢?除非有一个线性地址可以直接访问PDT和PTT【-->页目录表基址C0300000,C0300000存储的值就是PDT
      • 2、这个为我们挂好PDE与PTE的“人”是谁?

1.2.9 页目录表(PDT)基址

  • 拆分C0300000
    • 10:
      • 1100000000-->300*4=C00
    • 10:
      • 1100000000-->300*4=C00
    • 12:
      • 0
    • 通过!dd Cr3+C00得到PDE(后3位属性为置零后再加C00)
    • 通过!dd PDE+C00得到PTT(后3位属性为置零后再加C00)
    • 通过!dd PTT+0得到物理地址
    • 会发现,PTT+0就是Cr3指向的页目录表
    • 这样,系统就可以通过线性地址访问到一个目录页(PDT表),也是物理页,也是PDT表,也是PTT表(特殊的PTT表)
  • 总结
    • 1.通过0C300000找到的物理页就是PDT目录页表
    • 2.这个页目录表本身也是PTT页表
    • 3.PDT页目录表其实就是一张特殊的PTT表,这个表的每一项PDE或者说PTE指向的是其他PTT页表
    • 在windbg中,我们可以通过Cr3得到PDT目录表
    • 而在程序中,无法使用物理地址,只能通过线性地址来找到PDT表
    • 而程序就是通过C0300000这个线性地址来找到目录表(PDT)

1.2.10 页表基址(PTT)

  • 现在可以获取PDT目录表了,但是还必须结合PTT表才可做到访问PTE,才可以修改PTE的属性,这就需要访问PTT表了。那么就需要PTT表的基址
  • 线性地址C0000000,C0001000,C0002000,...
    • 验证C0000000是PTT的第一个PTE,C0001000是PTT的第二个PTE,...
      • 1. 打开windbg,!process 0 0获取Cr3
      • 2. !dd Cr3获取PDT表(里面存的都是PDE指向PTT,共有1024个)
      • 3.  ①!dd PDE1查看第一张PTT表 (后3位属性位置0)
        •      ②!dd PDE2查看第二张PTT表(后3位属性位置0)
        •      ③!dd PDE3查看第三张PTT表(后3位属性位置0)
        •      只是按照PDT表的顺序查看PTT表,没有特定找哪一个PTT表
      • 4.拆分C0000000
        • 1100 0000 00      300*4=C00
        • 0000 0000 00      0*4=0
        • 找到Cr3
        • Cr3+C00得到PTE
        • PTE+0得到物理地址
        • !dd 物理地址
        • 【发现和①找到的PTT表是一致的】
      • 5.拆分C0001000
        • 1100 0000 00      300*4=C00
        • 0000 0000 01      1*4=4
        • 找到Cr3
        • Cr3+C00得到PTE
        • PTE+4得到物理地址
        • 【发现和②找到的PTT表是一致的】

1.2.10.2 总结

1.C0000000刚好是第一个PTT表的线性地址,通过它可以找到第一个PTT表的物理地址
  C0001000刚好是第二个PTT表的线性地址,通过它可以找到第二个PTT表的物理地址
  以此类推,每隔4KB就可以访问到下一个PTT表
2.C0300000刚好是第0x300个PTT表,而第0x300个PTT表却又是PDT表
3.PTT被映射到从0xC0000000到0xC03FFFFF的4M地址空间中
    (一个PTT是大小0x1000,也就是4KB,有1024个,1024*4KB=4M)
4.之前学的PDT表其实就是第C0300000个PTT表,只是为了好学习才弄了个PDT表出来
-------------------------------------------------------------------------
公式总结:
    10-10-12
    PDI(可理解成下标):就是第一个10对应数值,PDT表第[10]个PDE
    PTI(可理解成下标):就是第二个10对应数值,PTI表第[10]个PTE
访问指定线性地址的目录表公式:
    0xC0300000          +     PDI*4
   (进入PDT目录表   +    第几个PDE)
访问指定线性地址页表公式:
    0xC00000000+PDI*4096+PTI*4
    PDI*4096表示PDT表中前[PDI]个TSS表都不是要找的PTT表,而每个表是4096的大小
-------------------------------------------------------------------------

2. 2-9-9-12分页

2.1 为什么是2-9-9-12?

  • 10-10-12分页模式最多只能寻找4GB的物理地址:1024*(1024*4096)
  • 但是4GB的物理地址不够用,为了找到更多的物理地址,只能将PTT的位数量拓展多点
  • PTT的数量由原来的12-31位变成了12-35位,多了4位
    • 2的2次方=4-->PDPT表有4个PDT表
    • 2的9次方=512-->PDT有512个PTT
    • 2的9次方=512-->PTT有512个物理页
    • 2的12次方=4096-->物理页有4096个字节

2.2 2-9-9-12寻找物理地址

2.3 PDPT、PDT、PTT

2.3.1 PDPTE

第00位:是P位,为1表示有效
09~11位:是给软件使用的
12~35位:指向PDT表的指针

2.3.2 PDE

2.3.2.1 PS为1时

  • 首先看PDT的第7位,PS位,当PS为1时,是大页
    • PS位只对PDE有意义,PS是PageSize的意思。当PS=1时,PDE直接指向物理页,而不是PTT页,没有PTE,大页的地址是21~35位决定的(15位),低21位填充0。低21位填充0,所以页的大小是2MB(10-10-12中是4MB)。
  • 寻找物理地址的时候:21~35位+21位0  一共36位,才是需要找的地址
  • 第12位:多了一个PAT(页属性表),与CUP相关,并不是所有CUP都支持,不支持就填0
  • 第9~11位:给操作系统软件使用的

2.3.2 PS位为0时

  • PS位为0时的情况:指向正常页,此时,12~35是寻找物理地址的范围,一共24位,比101012多了4位,寻找的物理地址范围:2的24次方=64GB

2.3.3 PTE

PTE中存储的是物理地址
物理页基址        :12~35位+低12位填充0
物理页具体数据:12~35位(物理页)+12(2-9-9-12中的12的值,偏移)

2.3.4 XD/NX位

有个这个XD/NE位,XD/NE位为1时,可以做到保护数据不可被执行,提高安全性

2.3.5 总结

  • 2-9-9-12模式下,变化如下:
    • ①新增了PDPT表,PDPT表存储的是一个指针,指向PDT表
    • ②Cr3指向的不再是PDT表,而是PDPT表
    • ③PPDPT,PDT,PTT表现在不再是4字节,拓展为8字节了。PTT的12~35位是页基址,比101012多了4位
    • ④PTT寻找的物理地址范围由原来的4GB(物理页寻址:2的20次方*4096)变成64GB(2的24次方*4096),但是地址空间大小还是4GB,4*512*512*4096=4GB
    • ⑤PDI和PTI由原来的10位变成了9位
    • ⑥多了一个XD/NE属性,对数据区进行保护

3. TLB

3.1 TBL是什么

  • 在10-10-12分页模式中,如果要通过线性地址读取4个字节的数据,但是实际上却是读多了8个字节:PDE四个字节、PTE四个字节
    • 极端情况:需要读取的这4个字节不在同一个物理页上,那么实际上读取的数据就更多了,要读取更多的页
  • 2-9-9-12更是如此,这样的效率是非常低的,因此,引出了TLB的概念
  • TLB
    • CUP在内部做了一个TLB表来记录这些东西,
    • 这个表在CUP内部,读取速度和寄存器一样快
      • Translation Lookaside Buffer
      •       转义        辅助       缓存
    • TLB就是用来缓存线性地址与物理页的对用地址的

3.2 TLB结构

  • ①不同的CPU这个表的大小不一样.
  • ②只要Cr3变了,说明进程切换了,线性地址的对应关系不成立了,TLB会立马刷新,一核一套TLB,但是G位为1的PDT或PTT不会刷新,会继续保留在TBL,因为G位为1的页是全局页,存储的都是高2GB的通用地址,进程间都是共享的

3.3 TLB的种类

  • 在X86体系的CPU里,一般都设有如下4组TLB:
    • 第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB);
    • 第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB);
    • 第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB);
    • 第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)

3.4 实验

3.4.1 体验TLB的存在

  • ①.给线性地址A挂上一个物理页B
  • ②.读取线性地址A的内容
  • ③.将线性地址A的物理页改成C
  • ④.读取线性地址A的内容
    • 此时A的线性地址已经修改为C了,但是③读取线性地址A的时候,读取的仍然是物理页B的地址,因为读取的是TLB的内容,CUP第一次使用线性地址的时候就将线性地址映射到TLB中了,而TLB此时又没有刷新,所以读取的是TLB之前缓存的物理页B

3.4.2 体验全局页的存在

  • ①进程A获取一个变量的线性地址
  • ②将该线性地址对应的PDT,PTT的G位修改为1
  • ③进程B访问进程A的线性地址

3.4.3 使用INVLPG指令,移除TLB中的指定对应关系

  • 执行 invlpg [0x00401020]强制刷新该线性地址对应的条目
思考:
一般TLB能保存的映射关系只有几百条,但是对于系统来说,需要使用到这么多线性地址,这里只保存了几百条有什么作用呢?是不是太少了?

3.4.4 总结

  • ①TLB中保存的是线性地址与物理地址的映射关系,只需要在第一次使用线性地址的时候加载PDE,PTE,不需要每次都加载
  • ②进程发生切换的话,TLB会刷新,G位为1页会保留下来






















③--------------------------------保护模式-中断、异常、控制寄存器--------------------------------------

1. 中断

1.1 什么是中断

  • 中断是指由CUP外部的输入输出设备(硬件)所触发的,意思就是外部设备通知CUP“有事情需要处理”
  • 中断请求的目的是希望CUP暂时停止执行当前的程序,转去执行中断请求所对应的中断处理程序(由IDT表决定)
  • 80x86有两条中断请求线:
    • 非屏蔽线 NMI       
    • 可屏蔽线 INTR

1.2 NMI-不可屏蔽请求处理

1.3 INTR-可屏蔽中断请求

1.3.1 中断管理器

  • 在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断管理器。它负责分配中断资源和管理各个中断源发出的中断请求。为了便于标识各个中断请求,中断管理器通常用IRQ(Interrupt Request)后面加上数字来表示不同的中断。
    • 比如:在Windows中 时钟中断的IRQ编号为0  也就是:IRQ0
  • 1.  IF为0时,若还有可屏蔽中断信号发送过来,CPU将不会处理,如果自己的程序执行时不希望CPU去处理这些中断,可以用CLI指令清空EFLAG寄存器中的IF位,用STI指令设置EFLAG寄存器中的IF位
  • 2.硬件中断与IDT表中的下标对应关系并非固定不变的
    • 参见:APIC(高级可编程中断控制器)
  • 3.中断是由硬件设备发起的

2. 异常

  • 异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等。
  • 常见异常处理程序
  • 缺页异常
    • 一旦发生缺页异常,CUP会执行IDT表中0xE的中断处理程序,此时由操作系统接管
      • 情况一.PDE、PTE的P位为0时
        • 假设物理页紧缺,不够用了
        • 线性地址A是有效的,但是物理页不够用了,此时线性地址A无法指向物理页,操作系统会将一个有效线性地址B所指向的物理页C的内容存储到文件中,从而达到腾出一个物理页供线性地址A使用,然后将线性地址B的指向的PDE和PTE的P位更改为0
        • 此时再次访问线性地址B时,PDE和PTE的P位为0,物理页无效,就会触发缺页异常,执行IDT表的0xE中断处理程序,由操作系统接管,操作系统首先检查PTE,当发现第0位(P位)、第10位、第11位为0,但是其他位不为0,系统就知道此时物理页被写入到文件中了,会在第1~4位中找到文件编号,这个编号指明了原来物理页的内容存储在哪个文件,操作系统会找到这个文件,把文件的内容再读到物理页上,然后将PDE/PTE的P位置为1,这样就可以正常访问物理页了
        • 操作系统通过这中方式节省了大量的物理页
      • 情况二.当PDE/PTE的属性是只读的,但程序视图写入时

3. 中断与异常的区别

  • 1.中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的.
  • 2.异常来自于CPU本身,是CPU主动产生的.
  • 3.INT N虽然被称为“软件中断”,但其本质是异常
  • 4.EFLAG的IF位对INT N无效。
  • 5.无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。

4. 控制寄存器

  • 控制寄存器用于控制和确定CPU的操作模式,一共有5个
  • Cr0:
  • Cr1:保留
  • Cr2
  • Cr3:页目录表基址,101012和29912的结构是不一样的
  • Cr4

4.1 Cr0

  • PE:
    • 用于启用保护标志,PE=1为保护模式,PE=0是实地址模式。此标志仅开启段级保护,没有启用分页机制,若要启用分页机制,PE和PG都需要=1
  • PG:
    • PG为1,说明开启了分页机制。设置PG=1之前,必须保证PE已经为1。
    • PG=0且PE=0    处理器处于实地址模式下
    • PG=0且PE=1    处理器处于没有开启分页机制的保护模式下
    • PG=1且PE=0    不存在
    • PG=1且PE=1    处理器处于开启了分页机制的保护模式下
  • WP:
    • 写保护标志。当CPL<3时,此时处于系统级
    • WP=0:当前程序可以读写任意用户级物理页,只要线性地址有效
    • WP=1:当前程序可以读取任意用户级物理页,但对于只读的物理页,不能写 

4.2 Cr2

  • 缺页异常一旦发生,CUP会将引起缺页异常的线性地址写入到Cr2中,若没有Cr2,缺页异常一旦发生,原来的线性地址就丢了

4.3 Cr4

  • PAE:
    • 当PAE=1时,当前是2-9-9-12分页模式,当PAE=0时,当前是10-10-12模式(操作系统在启动的时候会看booit.ini,将这个位设置为配置文件设置的)
  • PSE:
    • PSE位=1时,PDE的PS位才有效,PS=1才会有效,会指向大页(4MB或2MB)
    • PSE为0:时,PDE的PS位无效,不管PS=1还是PS=0,都是指向小页(4KB)
  • PSE相当于PDE的PS位的总开关

4.4 总结

  • Ce0:保护模式、实地址模式、启用分页
  • Cr1:保留
  • Cr2:缺页异常保存线性地址
  • Cr3:页目录表
  • Cr4:101012,29912,PDE的PS的总开关
posted @ 2021-01-01 10:30  三一米田  阅读(517)  评论(0)    收藏  举报