分段机制
- 在 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 | typedef struct _SELECTOR |
段描述符
- GDT 或 LDT 中保存的就是段描述符,每一个段描述符都描述了段的一些信息,32位下,大小为 8 字节(64位)
- 在 windbg 中使用 gdtr 寄存器可以获取段描述符的基址,gdtl 获取段描述符的限长。
- 通过汇编指令 rgdt 获取 gdt 寄存器,还可以通过 lgdt(特权指令) 设置 gdt 寄存器的内容。
- 描述一个段的位置和大小信息以及访问控制的状态信息,存在GDTR寄存器指向的内存中
1 | typedef struct _DESCRIPTOR |
*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)段
- 如果 TYPE 位的最高位为 1,就表示当前是代码段,此时看到的 TYPE 整体会大于 7
- 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:想要切换到当前的段描述符需要什么样的权限
- 当前的应用程序最少需要使用什么样的权限
- CPL:特指 CS 段寄存器的最低两位,表示当前所拥有(当前执行代码)的权限
代码间的跳转
JMP 0x20:0x004183D7 CPU怎么指向这段代码
段选择子拆分
0x20对应二进制 0000 0000 0010 0000
RPL = 00
Ti = 0
Index = 4
查表得到段描述符
Ti = 0 所以查GDT表
Index = 4 找到对应的段描述符
四种情况可以跳转: 代码段,调用门,TSS任务段,任务门
权限检查
如果是非一致代码段,要求CPL == DPL && RPL <= DPL
如果是一致代码段,要求CPL >= DPL
加载段描述符
通过上面的权限检查后,cpu会将段描述符加载到cs段寄存器中
代码执行
cpu将 cs.base + Offset 的值写入eip然后执行cs:eip处的代码,段间跳转结束
调用门

1100
查看GDT表
1 | kd> r gdtr |
跨段调用
调用提权

调用不提权

跨段调用时,一旦有权限切换,就会跟着切换栈
CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样
JMP FAR 只能跳转到同级的一致代码段,但CALL FAR可以通过调用门提权,提升CPL权限
注: SS与ESP从那里来?参见TSS段
调用门执行流程
指令格式 CALL CS:EIP(EIP是废弃的)
执行步骤:
根据CS的值查GDT表,找到对应的段描述符,这个描述符是一个调用门
在调用门描述符中存储另一个代码段的段选择子
选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址
1 |
|
将构造的门描述符填写到索引处,再将生成好到程序放到虚拟机中执行

输入指令 g 直接运行

注意,如果在返回时需要改变特权级,将会检查段寄存器DS、ES、FS、GS的内容,如果在它们之中,某个寄存器的中的选择子所指向的数据段描述符的DPL权限比返回后的CPL(CS.RPL)高,即数值上返回后的CPL>数据断描述符的DPL,处理器将会把数值0填充到相应的段寄存器。
IDT表
IDT表概述
IDT即中断描述符表,同GDT一样,IDT也是由一系列描述符组成,每个描述符占8个字节.但要注意的是,IDT表的第一个元素不是NULL
在Windbg中查看IDT表的基址和长度
1 | kd> r idtr |
IDT表构成
IDT表可以包含三种门描述符:
iretd 返回
中断门描述符
中断门
1110

1 |
|

陷阱门描述符
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是通过页目录
在 8086 CPU 下,我们是如何进行寻址的?
我们使用 段寄存器:段内偏移 的形式访问一个物理地址,实际访问到的地址是:段寄存器*0x10+段内偏移,CPU所处的模式是实模式。
在 80386 系列的 CPU 下,拥有几种 CPU 模式?
三种。分别是实模式、保护模式以及虚拟8086模式。当计算机通电的时候,CPU肯定处于实模式,在进行一些初始化后,会切换到保护模式中。
在保护模式下,我们是如何进行寻址的?
我们使用 段寄存器:段内偏移 的形式访问一个虚拟地址,此时段寄存器保存的并不是段基址,而是段选择子,通过段选择子可以找到段描述符,在进行段选择子切换的时候,CPU会将段描述符中的部分内容加载到不可见的高速缓存部分,在实际寻址时,会使用这一部分的信息。当切换段选择子的时候,CPU会进行什么样的基本权限检查?
CPL特指CS段的低两位,表示当前所拥有的权限、RPL指段选择子的低两位,表示当前使用的是什么权限、DPL表示想要访问指定段描述符所需要的权限,实际的权限检查公式为:MAX(CPL, RPL) <= DPL中断门或陷阱门保存在哪里,如何触发?
中断门和陷阱门都应该由 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
410042d 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 | kd> !process 0 0 notepad.exe |
补充内容
控制寄存器
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来修改分页的读写属性