分段机制

  • 在 32 位保护模式下,段寄存器就不会再直接保存段基址了,而是分成了可见的 16 位段选择子部分和不可见的 80 位高速缓冲部分,对于访问内存需要用到的一些信息,例如段的基址、限长和访问属性等都会被保存在这里的不可见部分。这一部分的内容无法人为的直接读取到,在段寄存器进行切换时会被 CPU 自动加载。首先,会解析 16 位的段选择子部分,从 TI 位获取到要使用的是哪一张表(GDT?LDT?),由于Windows没有使用LDT,所以目前保存的都是 0,再从 INDEX 获取表内元素的下标,再进行权限检查后,从中加载段的信息到不可见的高速缓存部分。
  • 在16位上是段基址,在32位上是段选择子

段选择子

  • 从下面的结构体定义可以看出,段选择子由 INDEX、TI 以及 RPL 组成,其中 RPL 全称为请求权限级别,表示当前使用什么样的权限发出请求,TI 位指定了当前查找的是 GDT(0) 还是 LDT(1),INDEX 标识的是索引,需要将 index 配合 ti 位一起使用,找到最终的 段描述符
  • 段选择子是一个16位的数值,高13位为全局描述表的索引,第三位索引GDT,LDT,低两位为请求特权级
1
2
3
4
5
6
typedef struct _SELECTOR 
{
unsigned short index: 13; // index 是存在于 GDT 或 LDT 中元素的索引
unsigned short ti: 1; // 为 0 表示 GDT 否则是 LDT,windows 始终各为 0
unsigned short rpl: 2; // 请求权限级别,表示使用什么样的权限访问
}

段描述符

  • GDT 或 LDT 中保存的就是段描述符,每一个段描述符都描述了段的一些信息,32位下,大小为 8 字节(64位)
  • 在 windbg 中使用 gdtr 寄存器可以获取段描述符的基址,gdtl 获取段描述符的限长。
  • 通过汇编指令 rgdt 获取 gdt 寄存器,还可以通过 lgdt(特权指令) 设置 gdt 寄存器的内容。
  • 描述一个段的位置和大小信息以及访问控制的状态信息,存在GDTR寄存器指向的内存中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; // 当前是系统段还是用户段
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; // 表示 limit 的单位
unsigned int base3: 8; // 段基址 [24~31]
}
  • *Base: 由 3 个部分组成,分别对应了 32 位基址的 0 ~ 15、16 ~ 23 以及 24 ~ 31 位,表示段基地址

  • P: 是切换寄存器时检查的第一个位,表示当前的段描述符是否是有效的,如果有效为 1,否则为 0

  • AVL:对于 i386 的所有处理器来说,CPU 都不会使用它,会将它留给用户(操作)进行操作

  • L: 如果当前的处理哦其处于 64 位模式,就会提供 L 位进行标识,表示当前使用的是长模式

  • *DPL:目标请求级别,描述的是想要访问当前的段描述符必须拥有的权限,权限通常要小于RPL\CPL,DPL是目标代码段的访问权限,CPL当前执行权限,RPL代码段权限

  • Limit 和 G 位:Limit 由两个部分组成,共使用了 20 位的数据表示段限长,当 G 位为 0 时,Limit 的单位就是 1Byte,当 G 位为 1 时,Limit 的单位就是 0x1000(4KB),所以Limit 的值最大可能是下面的情况

    • 若 G 为 1:假设 Limit 为 0xFFFFF,此时最终限长为 0xFFFFF × 0x1000 + 0xFFF -> 0xFFFFFFFF
    • 若 G 为 0:假设 Limit 为 0xFFFFF,此时最终限长为 0xFFFFF × 1 -> 0xFFFFF
  • D\B:用于设置默认操作数的大小和默认地址的大小。对于栈来说,通常将这一位成为 B 位,当 B 位为1时,默认对 ESP 的操作是以 4 字节计算的,例如 push 1 实际执行了 sub esp, 4 的操作。对于其它情况,该位被称作 D 位,表示默认使用的是 4 字节还是 2 字节。通过 OPCODE 指令前缀可以修改操作数和地址大小。

    当前是代码段时 D = 1采用32位寻址方式,D = 0 采用16位寻址方式

    当前是数据段是 D = 1 无论是向上拓展还是向下都是4Gb, D=0 它的拓展范围只有64kb

用户段

  • 当 S 位为 1 时,表示当前就是用户段,用户段又存在了两种状态,分别是代码段和数据段,

  • S位为0时,是系统,TYPE代表这个段的调用门,陷阱门,任务门

    • 如果 TYPE 位的最高位为 1,就表示当前是代码段,此时看到的 TYPE 整体会大于 7
      • C: 表示当前是否是一致代码段。如果但其概念是一致代码段,就表示我们可以使用低权限访问高权限的内容。如果但其概念是非一致代码段,那么要求访问者个被访问者权限必须一致 。
      • R: 当前的段是否是可读的
      • A: 表示当前的段是否被访问了,如果被访问了就为 1
    • 如果 TYPE 位的最高位为 0,就表示当前是数据段,此时看到的 TYPE 整体会小于 8
    • E: 表示当前是向上扩展(extend-up)段还是向下扩展(extend-down)段
      • W: 表示当前的段是可写的
      • A: 表示当前的段是否被访问了,如果被访问了就为 1

手动解析

  • 0008 -> 双机调试 CS = 1(INDEX) + 0(TI) + 00(RPL),通过 dq gdtr+(INDEX*8) 找到段描述符
    • 00cf9b00`0000ffff: base 表示当前的段基址为 0
    • 00cf9b00`0000ffff:limit 为 0xFFFFF,G位是C的最高位表示1,所以LIMIT 是 0xFFFFFFFF
    • 00cf9b00`0000ffff:除了 G 位,分别为 DB(1)、L(0) 和 AVL(0),只有 DB 是有意义的
    • 00cf9b00`0000ffff:9对应了 P(1) + DPL(00) + S(1),说明当前是一个有效的仅R0能访问的用户段
    • 00cf9b00`0000ffff:b大于8,说明是代码段,三个属性分别是 0(非一致) 1(可读) 1(访问过)
  • 001b -> 用户程序CS = 11(INDEX) + 0(TI) + 11(RPL),通过 dq gdtr+(INDEX*8) 找到段描述符
    • 00cffb00`0000ffff:base 表示当前的段基址为 0
    • 00cffb00`0000ffff:limit 为 0xFFFFF,G位是C的最高位表示1,所以LIMIT 是 0xFFFFFFFF
    • 00cffb00`0000ffff:除了 G 位,分别为 DB(1)、L(0) 和 AVL(0),只有 DB 是有意义的
    • 00cffb00`0000ffff:F对应了 P(1) + DPL(11) + S(1),说明当前是一个有效的需要R3能访问的用户段
    • 00cffb00`0000ffff:b大于8,说明是代码段,三个属性分别是 0(非一致) 1(可读) 1(访问过)
  • 随便找的一个数据段,对应 TI 和 INDEX 分别是 0 和 10,DPL是0表示选择子最低两位应该是0
    • 00cf9300`0000ffff:base 表示当前的段基址为 0
    • 00cf9300`0000ffff:limit 为 0xFFFFF,G位是C的最高位表示1,所以LIMIT 是 0xFFFFFFFF
    • 00cf9300`0000ffff:除了 G 位,分别为 DB(1)、L(0) 和 AVL(0),只有 DB 是有意义的
    • 00cf9300`0000ffff:9对应了 P(1) + DPL(00) + S(1),说明当前是一个有效的仅R0能访问的用户段
    • 00cf9300`0000ffff:3小于 7 是数据段,三个属性分别是 E(向上扩展) W(可写) A(访问了)

权限检查

  • 通过 lxs 和 mov xs, ax 可以切换段选择子 加载段选择符进入段寄存器的时候

    • CS寄存器只能存放代码段的选择符
    • 代码段选择符可以被加载到数据段寄存器,但是不可读的代码段选择符不能被加载进入数据段寄存器(因为数据段都是可读的)
    • 只有可读可写数据段选择符才能加载到SS寄存器(SS寄存器一定是可写的)
  • 当前接触到的三种权限:

    • CPL:特指 CS 段寄存器的最低两位,表示当前所拥有(当前执行代码)的权限
      • 当前操作系统登录用户的权限
    • RPL:指任何一个段选择子的最低两位,表示使用什么样的权限进行请求
      • 当前是否使用了管理员方式打开目标程序
    • DPL:想要切换到当前的段描述符需要什么样的权限
      • 当前的应用程序最少需要使用什么样的权限

代码间的跳转

JMP 0x20:0x004183D7 CPU怎么指向这段代码

  1. 段选择子拆分

    0x20对应二进制 0000 0000 0010 0000

    RPL = 00

    Ti = 0

    Index = 4

  2. 查表得到段描述符

    Ti = 0 所以查GDT表

    Index = 4 找到对应的段描述符

    四种情况可以跳转: 代码段,调用门,TSS任务段,任务门

  3. 权限检查

    如果是非一致代码段,要求CPL == DPL && RPL <= DPL

    如果是一致代码段,要求CPL >= DPL

  4. 加载段描述符

    通过上面的权限检查后,cpu会将段描述符加载到cs段寄存器中

  5. 代码执行

    cpu将 cs.base + Offset 的值写入eip然后执行cs:eip处的代码,段间跳转结束

调用门

1100

查看GDT表

1
2
3
4
kd> r gdtr
gdtr=80b95000
kd> r gdtl
gdtl=000003ff

跨段调用

调用提权

调用不提权

跨段调用时,一旦有权限切换,就会跟着切换栈

CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样

JMP FAR 只能跳转到同级的一致代码段,但CALL FAR可以通过调用门提权,提升CPL权限

注: SS与ESP从那里来?参见TSS段

调用门执行流程

指令格式 CALL CS:EIP(EIP是废弃的)

执行步骤:

​ 根据CS的值查GDT表,找到对应的段描述符,这个描述符是一个调用门

​ 在调用门描述符中存储另一个代码段的段选择子

​ 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <windows.h>


typedef struct _CALLGATE
{
unsigned int offset1 : 16; // 提权后执行代码的低地址
unsigned int selector : 16; // 需要提升的权限对应的选择子
unsigned int paramcount : 5; // 提权后执行的代码用到的参数
unsigned int : 3; // 保留位
unsigned int type : 4; // 对于调用门始终是 1100
unsigned int S : 1; // 对于调用门始终是 0
unsigned int DPL : 2; // 访问我需要的权限,应该写 3
unsigned int P : 1; // 必须是有效的段描述符
unsigned int offset2 : 16; // 提权后执行代码的高地址
} CALLGATE, *PCALLGATE;

// 在进行权限切换的过程中,栈也会随之改变
short r0_ss = 0;
int r0_esp = 0;

// 裸函数,只生成用户编写的代码,在其中操作高地址空间
_declspec(naked) void r0_function()
{
__asm
{
int 3
mov r0_ss, ss ; 获取 r0 权限的 ss
mov r0_esp, esp ; 获取 r0 权限的 esp
retf ; 调用门必须使用 retf 进行返回

; 实现通过调用门读取到 gdt 中的第 3
; sgdt 可以获取 gdt 的地址
; 对于中断门和陷阱门必须使用 iretd
}
}

int main()
{
// 1. 根据当前函数的位置,构建一个调用门,用于进行跳转
// offset: 0045???? ????6c20 -> 需要跳转的偏移
// selector: ???????? 0008???? (1 0 00) -> 需要切换的权限
// DPL: ????E??? ???????? -> 访问当前调用门的权限
// TYPE: ?????C?? ???????? -> 当前是一个调用门描述符
//00456BC0 00456BC0
// 0045EC00`00086BC0



// 2. 构建一个远跳的地址,在远跳中,偏移是没有意义的,给什么都哦可以
// 后面保存的是段选择子,可以用于找到调用门描述符,要求 RPL <= DPL
// - 0x004B -> 01001(INDEX) 0(TI) 11(RPL)
BYTE dest[] = { 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00 };
//char dest[6] = {};
//*(DWORD*)&dest[0] = 0x12345678;
//*(DWORD*)&dest[4] = 0x48;

// 3. 使用 call far 语句,跳转到指定的调用门中,在进行R3到R0 的转化的
// 时候 CPU 会默认的将用户 SS ESP CS IP 保存到栈中
__asm push fs
__asm call fword ptr dest;
__asm pop fs

// 4. 输出 R0 下的 esp 和 ss,如果R0代码设置断点,那么 fs 会被改变,调
// 用函数会崩溃,需要在进入门之前先进行保存
printf("%04X: %08X\n", r0_ss, r0_esp);
system("pause");

return 0;
}

将构造的门描述符填写到索引处,再将生成好到程序放到虚拟机中执行

输入指令 g 直接运行

注意,如果在返回时需要改变特权级,将会检查段寄存器DS、ES、FS、GS的内容,如果在它们之中,某个寄存器的中的选择子所指向的数据段描述符的DPL权限比返回后的CPL(CS.RPL)高,即数值上返回后的CPL>数据断描述符的DPL,处理器将会把数值0填充到相应的段寄存器。

IDT表

IDT表概述

IDT即中断描述符表,同GDT一样,IDT也是由一系列描述符组成,每个描述符占8个字节.但要注意的是,IDT表的第一个元素不是NULL

在Windbg中查看IDT表的基址和长度

1
2
3
4
kd> r idtr
idtr=80b95400
kd> r idtl
idtl=000007ff

IDT表构成

IDT表可以包含三种门描述符:

iretd 返回

中断门描述符

中断门

1110

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <windows.h>


// 在进行权限切换的过程中,栈也会随之改变
short r0_ss = 0;
int r0_esp = 0;


// 裸函数,只生成用户编写的代码,在其中操作高地址空间
_declspec(naked) void r0_function()
{
__asm
{
//int 3
mov r0_ss, ss; 获取 r0 权限的 ss
mov r0_esp, esp; 获取 r0 权限的 esp
iretd;
}
}



int main()
{
//中断门
// 1. 根据当前函数的位置,构建一个中断门,用于进行跳转
// offset: 0045???? ????6c20 -> 需要跳转的偏移
// selector: ???????? 0008???? (1 0 00) -> 需要切换的权限
// DPL: ????E??? ???????? -> 访问当前中断门的权限
// TYPE: ?????E?? ???????? -> 当前是一个中断门描述符
//制造中断,使其调用中断门描述符
//00456BC0

//0045EE0000086BC0
_asm
{
push fs
int 32
pop fs
}

printf("%04X: %08X\n", r0_ss, r0_esp);
system("pause");
return 0;
}

陷阱门描述符

1111

陷阱门与中断门的区别

中断门执行时,将IF位清零,陷阱门

任务门描述符

在调用门,中断门与陷阱门中一旦出现权限切换,那么就会有堆栈切换.而且,由于CS的CPL发送改变,也导致了SS也必须切换,切换时,会有ESP和SS(CS是由中断门或者调用门指定)这2个值从TSS(Task-state segment),任务状态段

TSS

TSS是一块内存,大小104字节

不要把TSS与任务切换联系到一起,TSS的意义就在于可以同时换掉一堆寄存器

CPU怎么找到TSS,

TR段寄存器

TSS段描述符

构造TSS段描述符:xx00e9xx`xxxx0068

TR寄存器读写

1,将TSS段描述符加载到TR寄存器

指令:LTR

说明:用LTR指令去装载 仅仅是改变TR寄存器的值96位,并没有真正改变TSS,LTR指令只能在系统层使用,加载后TSS段描述符状态为会发生改变

2,读TR寄存器

指令:STR

说明:如果用STR去读的花,只读了TR的16位,也就是选择子

分页机制

PDE与PTE

80x86映射表分两级

第一级:页目录表(PDT)

第二级:页表(PTT)

属性

P位:

​ 存在

R/W位:

​ R/W = 0 只读

​ R/W = 1 可读可写

U/S 位:

​ U/S = 0 特权用户才能访问

​ U/S = 1 普通用户特权用户都能访问

P/S 位:只对PDE有意义

​ 当ps=1时PDE直接指向物理页无PTE,低22位是页内偏移,页大小位4M

​ 当ps=0时,指向下一级页表

D位:

​ 是否被写过,写过就置为1

Windows是通过页目录

  1. 在 8086 CPU 下,我们是如何进行寻址的?

    我们使用 段寄存器:段内偏移 的形式访问一个物理地址,实际访问到的地址是:段寄存器*0x10+段内偏移,CPU所处的模式是实模式。

  2. 在 80386 系列的 CPU 下,拥有几种 CPU 模式?

    三种。分别是实模式保护模式以及虚拟8086模式。当计算机通电的时候,CPU肯定处于实模式,在进行一些初始化后,会切换到保护模式中。

  3. 在保护模式下,我们是如何进行寻址的?
    我们使用 段寄存器:段内偏移 的形式访问一个虚拟地址,此时段寄存器保存的并不是段基址,而是段选择子,通过段选择子可以找到段描述符,在进行段选择子切换的时候,CPU会将段描述符中的部分内容加载到不可见的高速缓存部分,在实际寻址时,会使用这一部分的信息。

  4. 当切换段选择子的时候,CPU会进行什么样的基本权限检查?
    CPL特指CS段的低两位,表示当前所拥有的权限、RPL指段选择子的低两位,表示当前使用的是什么权限、DPL表示想要访问指定段描述符所需要的权限,实际的权限检查公式为:MAX(CPL, RPL) <= DPL

  5. 中断门或陷阱门保存在哪里,如何触发?
    中断门和陷阱门都应该由 IDT 进行处理,构建完成后,使用 int n 触发它,在进入中断门\陷阱门后,使用 iretd 跳出到用户层。

地址的种类

  • 逻辑地址:指 段寄存器+段内偏移 表示的一个地址。
  • 线性地址:指 进程独有虚拟内存空间中 的地址。 分段机制将逻辑地址转换为线性地址。
  • 物理地址:指 实际访问到的内存条 的地址。分页机制将线性地址转换为物理地址。

WinDbg 相关指令

  • !process 0 0: 列出当前系统中的所有进程的信息
  • !process 0 0 notepad.exe: 列出指定进程的信息
  • .process 进程的 EPROCESS: 切换到目标进程中,使用它的 CR3
  • s -u 0x00000000 L0x1000000 “mmmmmwwwww”: 从指定位置开始搜索指定的字符串

虚拟地址转换(无PAE)

  • x86环境下,每一个进程都会拥有自己的 4GB 虚拟地址空间,主要由 CR3 进行区分

    • CR3寄存器保存了每一个进程独有的页目录表(PDT),页目录表是一个元素个数为 1024 的表,其中的每一项占有 4 个字节,叫做 页目录项(PDTE),页目录表项配合虚拟地址的高 10 位组成的索引可以寻址。

    • 寻址到的页目录表项,它的低12位保存的是属性位,除开属性位就是页表(PT),页表是一个元素个数为1024 的表,其中的每一项占 4 字节,描述了一个页的信息

  • 找到了页表后,配合中间的10位,寻址到最终的页(PAGE)

  • 寻址过程

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    0042d f48: 001 + 02d + f48

    kd> !dd 5bdf1000 页目录表
    #5bdf1000 45031867 02b3e867 00000000 00000000
    #5bdf1010 03393867 3929e867 02ad9867 36deb867
    #5bdf1020 023ec867 09401867 32d03867 38a74867

    kd> !dd 5bdf1000+(1*4) 页目录表项 -> 页表
    #5bdf1004 02b3e867 00000000 00000000 03393867
    #5bdf1014 3929e867 02ad9867 36deb867 023ec867

    kd> !dd 02b3e000+(4*2d) 页表 -> 页
    # 2b3e0b4 019ac867 01406867 44608867 01209867
    # 2b3e0c4 0210a867 0251e867 02825867 00924867
    # 2b3e0d4 36e29867 37d28867 36d2f867 38930867

    kd> !du 019ac000+f48 页 -> 物理地址
    # 19acf48 "1233211234567..F"


    00130bd4: 000(页目录表索引) + 130(页表索引) + bd4(页内偏移)

    kd> !dd 5bdf1000 页目录表
    #5bdf1000 45031867 02b3e867 00000000 00000000
    #5bdf1010 03393867 3929e867 02ad9867 36deb867
    #5bdf1020 023ec867 09401867 32d03867 38a74867

    kd> !dd 5bdf1000+(0*4)
    #5bdf1000 45031867 02b3e867 00000000 00000000
    #5bdf1010 03393867 3929e867 02ad9867 36deb867
    #5bdf1020 023ec867 09401867 32d03867 38a74867

    kd> !dd 45031000 + (4*130)
    #450314c0 376c5867 00000080 00000080 00000080
    #450314d0 028c7867 372c6867 00000080 00000080
    #450314e0 029c9867 01acd867 445c8867 028ce867

    kd> !du 376c5000 + bd4
    #376c5bd4 "1233211234567"


开启PAE的情况

  • 2:页目录表指针表(4) + 9:页目录表(512) + 9: 页表(512) + 12:偏移(0x1000)
  • 不管有没有开启 PAE,能够描述的页都是一样多的,但是和没有PAE的情况相比,开了的每一个项占了8字节空间,所以可以充分利用大于 4 GB的物理内存。
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
kd> !process 0 0 notepad.exe
PROCESS 87bfbc00 SessionId: 1 Cid: 029c Peb: 7ffd3000 ParentCid: 05ac
DirBase: 7f5fe560 ObjectTable: 90a553c8 HandleCount: 61.
Image: notepad.exe



kd> .process 87bfbc00
Implicit process is now 87bfbc00
WARNING: .cache forcedecodeuser is not enabled



kd> s -u 0x00000000 L0x01000000 "1234567654321"

0040e100 0031 0032 0033 0034 0035 0036 0037 0036 1.2.3.4.5.6.7.6.
01890bd4 0031 0032 0033 0034 0035 0036 0037 0036 1.2.3.4.5.6.7.6.


0040e100 转换为二进制 00 000000010 000001110 000100000000
0 2 e 100
kd> du 0040e100
0040e100 "1234567654321321"


kd> !dq 7f5fe560
#7f5fe560 00000000`57f0c801 00000000`5828d801
#7f5fe570 00000000`5838e801 00000000`5860f801
#7f5fe580 00000000`78201801 00000000`79182801
#7f5fe590 00000000`79103801 00000000`79504801
#7f5fe5a0 00000000`7815c801 00000000`77f5d801



kd> !dq 57f0c000+(2*8)
#57f0c010 00000000`58707867 00000000`5825f867
#57f0c020 00000000`00000000 00000000`00000000
#57f0c030 00000000`00000000 00000000`00000000
#57f0c040 00000000`00000000 00000000`582af867
#57f0c050 00000000`572ed867 00000000`566bb867
#57f0c060 00000000`57851867 00000000`00000000




kd> !dq 58707000 +(e*8)
#58707070 80000000`5762b867 80000000`58069867
#58707080 80000000`56aea867 80000000`570eb867
#58707090 80000000`57d6c867 80000000`5796e867
#587070a0 80000000`5686f867 80000000`57771867
#587070b0 80000000`56cf0867 80000000`57b73867
#587070c0 80000000`571f2867 80000000`57874867



kd> !du 5762b100
#5762b100 "1234567654321321"

//第二个地址
01890bd4 00 000001100 010010000 101111010100
0 c 0x90 bd4

kd> du 01890bd4
01890bd4 "1234567654321"



kd> !dq 7f5fe560
#7f5fe560 00000000`57f0c801 00000000`5828d801
#7f5fe570 00000000`5838e801 00000000`5860f801
#7f5fe580 00000000`78201801 00000000`79182801
#7f5fe590 00000000`79103801 00000000`79504801
#7f5fe5a0 00000000`7815c801 00000000`77f5d801


kd> !dq 57f0c000 + (0xc * 8)
#57f0c060 00000000`57851867 00000000`00000000
#57f0c070 00000000`5810d867 00000000`5848f867
#57f0c080 00000000`5860e867 00000000`00000000
#57f0c090 00000000`580a6867 00000000`57628867
#57f0c0a0 00000000`58329867 00000000`00000000


kd> !dq 57851000 + (0x90 * 8)
#57851480 80000000`587e6867 00000000`00000080
#57851490 00000000`00000080 00000000`00000080
#578514a0 80000000`584e8867 80000000`57ae7867
#578514b0 00000000`00000080 00000000`00000080


kd> !du 587e6000 + 0xbd4
#587e6bd4 "1234567654321"

补充内容

控制寄存器

CPU中有一系列控制寄存器用于控制和确定CPU的保护模式,CR0-CR4

CR1 保留

CR3页目录基址

CR0寄存器

31 30 29 28-19 18 17 16 15-6 5 4 3 2 1 0
PG CD NW AM WP NE ET TS EM MP PE

PE:CR0的0位是启用保护(Protection Enable)标志

​ PE = 1保护模式 PE = 0 实地址模式这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG都要置位

PG:当设置该位时即开启了分页机制。在开启这个标志之前必须已经或同时开启了PE标志

​ PG = 0 且PE = 0 处理器工作在实地址模式下

​ PG = 0 且PE = 1处理器工作在没有开启分页机制的保护模式下(没有这种操作系统)

​ PG = 1 且PE = 0在PE没有开启的情况下,无法开启PG

​ PG = 1 且PE = 1处理器工作在开启了分页机制的保护模式下(也就是我们的操作系统)

CR2寄存器

当CPU访问莫格无效页面时,会产生缺页现象,此时,CPU会将引起异常的线性地址存放在CR2中

CR4寄存器

31-11 10 9 8 7 6 5 4 3 2 1 0
保留位(置0) PCE PGE MCE PAE PSE DE TSD PVI VME

PAE/PSE:

PAE = 1 是PAE分页 PAE = 0 是普通分页

MSR寄存器

CPU中有一组寄存器,称之为特殊模块寄存器,这类寄存器数量庞大,功能各异:

​ 性能监视计数

​ 调试扩展支持

​ 机器检查

​ 功耗与温控管理

​ 特殊指令的支持

​ 处理器特性和处理器莫斯支持

每一个MSR寄存器都有它的编号可以使用下面的指令分别对其读写,一下指令只能在0环特权级才能执行

​ rdmsr: ecx 存放序号,执行完后数据存放在eax中

​ wrmsr: eax 存放数据,ecx存放序号,执行后将eax写入到相应的寄存器

总结

发明保护模式是为了进行多任务设计,避免任务之间相互干扰,保护模式实现的是通过分段和分页机制来进行的。通过设置CR0的PE标志位可以让处理器工作在保护模式下,PG位可以开启分页保护机制。

通过分段保护机制,处理器使用段寄存器中选择符(RPL和CPL)和段描述符各个字段来执行保护验证。

对于分页机制,主要用页目录和页表项中的R/W和U/S标志来实现保护操作,VirtualProtect函数就是通过修改R/W来修改分页的读写属性