保护模式
保护模式
什么是保护模式
x86 CPU的3个模式:实模式,保护模式,x86模式
保护模式的特点:段,页的机制,保护系统的数据结构
段寄存器
段寄存器共有8个ES CS SS DS FS GS LDTR TR
段寄存器共96位,16位可见 80位不可见
struct {
WORD Seletor; // 16位 可见
WORD Atrributes; // 16位 属性(读,写,执行) 对应段描述符中高字节中8到23位
DWORD Base; // 32位 基址 对应段描述符中全部的base
DWORD Limit; // 32位 段的长度 对应段描述符中全部的长度 G = 0 单位为字节
// G = 1 单位为4kb
}
段描述符和段选择子
GDT LDT
当类似mov ds,ax这种指令被执行的时候,CPU会查表,根据ax的值来决定查询的是GDT还是LDT,查找什么位置,查找什么数据
GDT 全局描述符表 gdtr寄存器储存表的起始地址(32位储存地址,16位储存长度)
LDT 局部描述符表 (Windows中并未使用)
段描述符
段描述符储存在GDT表中,每个段描述符长度为8个字节
P G 标志位
P = 1 描述符有效
P = 0 描述符无效
G 标志位控制limit剩下的位数
将20位的limit拓展成32位,这差的12位,也就是三个字节有G位控制
G = 0 limit高12位就填充三个000,那么最大上限就是000FFFFF
G = 1 limit高12位就填充三个000,那么最大上限就是FFFFFFFF
S 位与 TYPE 域
S = 1 代码段或数据段段描述符
A 访问位
R 可读位
C 一致位 C = 1 一致代码段 C = 0 非一致代码段
S = 0 系统段描述符
DB位
影响CS段
D = 1 32位寻址
D = 0 16位寻址
影响SS段
D = 1 隐式对战访问指令(PUSH POP CALL)使用32位堆栈指针寄存器ESP
D = 0 隐式对战访问指令(PUSH POP CALL)使用16位堆栈指针寄存器SP
影响向下拓展有效
D = 1 向下拓展有效范围为4GB
D = 0 向下拓展有效范围为64k
DPL 描述符特权级别
规定访问该段所需要的特权级别是什么
例如:mov DS, AX
如果AX指向的段的DPL = 0,而程序当前的CPL = 3, 则这行汇编无法成功执行
段选择子
16位的数
加载段描述符至段寄存器
除了mov指令,还有LES,LSS,LDS,LFS,LGS指令修改寄存器
而CS作为代码段,要修改CS必须和EIP一起修改
代码之间的跳转(段间跳转,非调用门)
同时修改CS和EIP的指令:JMP FAR / CALL FAR / RETF / INT / IRETED
执行流程:
JMP 0x20:0x004183d7
- 拆分段选择子
0x20对应0000 0000 0010 0000
RPL = 0
TI = 0
Index = 4
-
查表
TI = 0 查询GDT
Index = 4 找到对应的段描述
-
权限检查
非一致代码段:CPL DPL 且 RPL <= DPL
一致代码段:CPL >= DPL(共享段,低权限也可用)
-
加载段描述符,CPU将段描述符加载入CS段寄存器中
-
代码执行
CPU将CS.Base + Offset的值写入EIP然后执行CS:EIP
段权限检查
CPU分级:0环,1环,2环,3环(Windows 并未使用1环和2环
查看程序处于几环:CPL(Current Privilege Level)当前特权级
CS,SS中存储的段选择子的后两位
长调用和短调用
短调用
call + 立即数/寄存器/内存
压入call后指令的地址作为返回地址,然后再进行跳转
对寄存器:修改了eip和esp
对堆栈:压入返回地址
长调用(跨段不提权)
call CS:EIP(EIP是废弃的)
对寄存器:修改了eip,esp,cs
对堆栈:先压入调用者的CS,然后再压入返回地址
长调用(跨段并提权)
call CS:EIP(EIP是废弃的)
对寄存器:修改了eip,esp,cs,ss
对堆栈:堆栈切换为对应权限的堆栈,并压入调用者的ss,调用者的esp,调用者的cs,返回地址
调用门
当调用call CS:EIP(EIP是废弃的)时
- 根据cs的值查找GDT对应的段描述符,且这个描述符必须为一个调用门
- 在调用门描述符中存储的是另一个代码段的段选择子
- 选择子指向的段 段.base + offset 才是真正指向的地址
门描述符
门描述符是系统描述符中的一类
s = 0
type = 1 1 0 0
31:16这里的段选择子指向的段才是真正执行的段
中断门
系统调用
调试
IDT表
IDT表,中断描述符表。由一系列描述符组成,且均为系统描述符
IDT表由任务门描述符,中断门描述符,陷阱门描述符。
陷阱门
陷阱门和中断门的区别:中断门执行时,将IF位清零,但是陷阱门不会
IF:中断允许标志位。它用来控制8086是否允许接收外部中断请求。若IF=1,8086能响应外部中断,反之则屏蔽外部中断
任务段
相关指令 str ltr 寄存器/内存
CPU是没有线程的,线程是操作系统创建的
一个进程本质来说,在cpu看来就是一个任务
TPYE 类型 为 0 1 0 B(Busy) 1
TSS
Intel的设计思想:想通过TSS来实现任务的切换,因为利用TSS可以一次性更换很多寄存器从而实现快速的任务切换。
操作系统的设计思想:觉得TSS不够好,所以windows自己实现了一套任务切换(线程切换)。
最终作用:可以一次性替换很多寄存器,由于TSS表中有CS SS,所以可以用于提权。
TaskRegister
CPU中有一个段寄存器叫TR(TaskRegister),TR有96位,其中16位可见部分为选择子,可以找到GDT表中的一个段描述符,通过该描述符加载TR段寄存器中后80位。TR寄存器的Base指示了TSS表的位置。Limit指示了TSS表有多大。
Type:1001表示当前描述符是个TSS段描述符但没有加载到TR寄存器中。 1011表示已经加载到TR寄存器中。
读写TR寄存器
通过LTR指令来装载TR寄存器(96位)。装载后TSS段描述符中的Type-3位会发生改变。LTR指令只能在0环权限中使用。并且仅修改TR寄存器,不修改其他寄存器。
通过STR指令来读取TR寄存器,但只能读取16位,也就是可见部分(段选择子)
TSS任务段切换步骤
- 调用 call/jmp + TSS任务段选择子
- CPU将调用者的寄存器存入当前TR寄存器对应的TSS中。
- 将TSS任务段选择子加载到TR寄存器中
- 将新的TR寄存器对应的TSS任务段中的所有寄存器全部替换。
模拟任务切换(一次性切换所有寄存器)
- 准备TSS任务段(104个字节)并将对应成员赋值。CR3通过 !process 0 0 来查看
-
准备TSS段描述符。指向准备好的TSS任务段。并写入GDT表中合适的位置。(G位为0,AVL为0,因为TSS任务段是以字节为单位的。)
-
修改TR寄存器指向TSS段描述符。由于ltr指令为特权指令,因此若想在3环实现TR寄存器修改,需要使用JMP FAR或CALL FAR。
当JMP FAR后的段选择子指向一个TSS任务段描述符时,会首先将描述符装载到TR寄存器中,然后在根据TR.Base(TSS任务段)来修改当前寄存器的值。
一个问题:
JMP指令进行任务切换,TSS段中的PreviousLink不会被赋值。且NT位不变。
CALL指令进行任务切换,TSS段中的PreviousLink会被填入上一个TSS段所属TSS段描述符的段选择子。且NT位被置1.
NT位:任务嵌套位。 NT位为0时,IRET/IRETD会从堆栈中取值(中断返回)。 NT位为1时,IRET/IRETD不会再从堆栈中取值返回,而是从当前TSS的PreviousLink对应的上一个TSS中取数据进行返回。(调试模式,单步调试时,NT位会被清0,导致IRET无法从TSS中返回,造成蓝屏)
10-10-12内存分页
在程序执行的过程中,每个进程被分配到的4GB内存是假的内存,只有当进程使用到对应地址的内存时,CPU将线性地址转换为物理地址,真正的数据在物理内存里面。cpu在取数据的时候,会计算出线性地址后再转换成物理地址然后再去读取数据
物理地址
对于指令 mov eax,dword ptr ds:[0x12345678]
0x12345678为有效地址
ds.base+0x12345678为线性地址
线性地址转物理地址
例如线性地址 0x000AA8A0,10 10 12按位分割
0000 0000 00
00 1010 1010
8A0
每个进程都有一个CR3寄存器,一个核只有一个CR3寄存器。
CR3寄存器是唯一一个存储物理地址得寄存器
CR3寄存器指向第一级目录(PDE),一共4096字节,每个成员4个字节,每个成员指向另一个页
使用!dd(查看物理内存)命令而不是dd(查看线性内存)
使用时,首先利用程序的CR3找到第一级的表,第一级的值为0,即该表中的第0个成员,地址:base + 0 * 4
然后根据该成员找到第二级的表,基址为14dc0000(后三位为属性控制,使用时将其置零),最终地址:base + AA * 4
最后在物理页中找到对应的数据,地址:base + 8A0
PDE与PTE
事实上,第一级目录被称为页目录表(PDT),第二级被称为页表(PTT)
PDE:页目录表项
PTE:页表表项
也就是说,页的属性由页表属性和页目录表同时控制,只有两者均为1才有效
最后一位是12,4096大小的物理页需要12个二进制位来寻址
前面两位为10,1024个表项成员需要10个二进制位来寻址
PTE最多只能指向一个物理页,但是可以不指向物理页
同一个物理页可以同时被多个物理页指向
P位
有效位。两个P位都为1时该页有效,只要其中一个P位为0时该页无效。
R/W位
读写位,两者的P位都为1时代表该页内存可读可写,只要有一个为0代表只读不可写。(对于非VT而言,一块内存申请出来,它必然是可读的。)
US位
特权等级位,都为0时,特权用户可读写,都为1时,普通用户可读写。
特权用户: 0 1 2 环 普通用户:3环
PS位
仅对PDE有意义,PaseSize位,页大小,当PS位为0时,为4KB小页。寻址如上述正常寻址。
当PS位为1时,对应物理页为大页,PDE直接指向物理页,无PTT参与。线性地址低22位为页内偏移。
A位
Access 是否被访问过位,访问过就全部置1,否则为0。 哪怕是一个字节的地址访问也会将其对应的PDE PTE的A位置1.
D位
Dirty 脏位,是否被写过位。逻辑与A位相同。
G位
全局页,当CR3发生改变后CPU将切换TLB,若G位为1,则该页在CR3发生切换时,TLB中的数据将不被刷新。
TLB与缓存将在后续有专题讲解。
PWT位
PWT = 1在cache写入数据的时候同时写入内存中
PCD位
PWT = 1 禁止某个页写入缓存,读写时直接读写内存
PAT位
缓存相关,后续缓存章节会有说明,此处暂时不用了解。
页目录表基址与页表基址
当我们申请一块内存用于读写,系统会为这块内存挂载相应的PDE 与PTE,挂载时必然会访问PDT与PTT。但我们在程序中出现的所有地址全部为线性地址,在保护模式下我们无法对任何物理地址进行操作。想要读写PDT PTT就必须将这两个表的物理地址映射成线性地址并挂载PDE PTE。这一步是我们及系统都无法做到的。 CPU为我们贴心的将PDT PTT的物理首地址映射成了线性地址(C0300000)供操作系统访问。这就是页目录表基址和页表基址。
10-10-12
PDI-PTI-offset
PDT基址: C0300000 PTT基址:C0000000+1000×index 得出
每个PDE:C0300000+4×PDI
每个PTE:C0000000+4096×PDI+4×PTI
因此确切来说,并不存在PDT表,而是1024个PTT表被映射到从0xC0000000 到 0xC03FFFFF这4M的空间
而0xC0300000这个地址开始的PTT表,里面存储的表项并不是指向内存页,而是指向这1024个PTT表。
2-9-9-12分页(PAE 物理地址拓展)
首先,每个页的大小不变,因此页内偏移仍然是12个二进制位
为了找到更多的物理页,将原本4字节的PTE变为8字节的PTE,但是页大小不变,从1024 * 4变成 512 * 8,因此仅需9个二进制位
PDT同理,所以是9-9-12
而多出来的两位,则组成PDPI(页目录指针表)
- PDPTE
-
PDE
如果是大页的话,35~21位为物理页地址。低21位为页内偏移。每个页2MB大小。
- PTE
XD/NX位(DEP数据执行保护):
在PAE分页模式下,PDE/PTE的最高位称为XD/NX位。不可执行位,为1时,该段内存不可执行。
就是我们常见的DEP数据执行保护
TLB
当我们想读取一个线性地址的数值时,CPU会先读这个线性地址对应的PDE再读PTE再读物理内存。 这样就相当于多读了8字节的内存,若这个线性地址前两个字节和后两个字节不在同一物理页上,那么多读的字节数会更大。这点在2-9-9-12分页模式下更为明显。为了加快读取速度,提高效率,CPU内部做了一张表用来存储已读取过得线性地址和物理地址间的映射关系。这张表的读写速度与寄存器一样快速。这张表就是TLB(Translation Lookaside Buffer)
TLB种类:
一般来说一个CPU只有一套TLB,分为四组
物理页分为普通页(4KB)、大页(2MB/4MB),物理页又分为指令和数据。因此分为4种TLB
- 缓存一般页表(4KB)的指令页表缓存(Instruction-TLB)
- 缓存一般页表(4KB)的数据页表缓存(Data-TLB)
- 缓存大尺寸页表(2MB/4MB)的指令页表缓存(Instruction-TLB)
- 缓存大尺寸页表(2MB/4MB)的数据页表缓存(Data-TLB)
过CRC之shadow walker
TLB分为DataTLB和InstructionTLB,CRC校验开启时,目标代码由于会被执行,所以InstructionTLB会存一份,由于CRC线程会读取目标代码,所以DataTLB又会存一份。通过更改其中一个TLB内目标地址的映射缓存来实现读取和执行时指向的物理内存不是同一地址。达到欺骗CRC校验的效果。
但3环程序中使用的低2G地址在TLB中经常会被刷新,因此该技术显得很鸡肋。内核层高2G地址几乎不被刷新,此技术应用很强大。
过CRC之代码拷贝执行
通过层层分析,找出目标代码段上一层代码将要调用的系统API。将API后的目标代码段拷贝至自身内存。修复全局变量、IAT等数据以保证正常执行。在系统API中Hook,将EIP指向拷贝后的代码。由于CRC校验的是原始数据而我们跳回的是拷贝数据,因此可以过掉CRC。但要注意堆栈检测。
中断与异常
中断通常是由CPU外部的输入输出设备所触发的,又成为中断请求
8086CPU有两条中断线
- 非屏蔽中断线 NMI(NonMaskable Interrupt)
- 可屏蔽中断线 INTR(Interrupt Require)
非可屏蔽中断
非可屏蔽请求一旦发生,CPU必须执行对应的中断处理程序
可屏蔽中断
可屏蔽中断在CPU中由一块独立的芯片管理,称为中断控制器(APIC)其管理着每个可屏蔽中断请求对应的中断处理程序的IDT表索引。在其内部也存在一个编号,称为IRQ。每个IRQ对应IDT中的一个索号。这种对应关系不是一成不变的。
如果自己程序执行的时候,不希望CPU去处理中断,则可以用CLI指令清空eflag寄存器中的IF位,用STI指令设置eflag寄存器中的IF位
异常
异常是CPU执行指令时检测到错误,例如除零,非法访问等
INT X中断指令称为软件中断。 其本质为异常。 与中断请求无任何关系。中断请求为硬件层面的请求。异常为软件层面的产生。因此,IF位不对异常产生任何影响。
常见异常
缺页异常
- PDE/PTE :P = 0 物理页无效
- PDE/PTE 为只读但程序尝试写入
一旦发生缺页异常,CPU执行IDT表中的0xE号中断处理程序,由操作系统来接管
控制寄存器
控制寄存器用于控制和确定CPU的操作模式
Cr0寄存器
PE
Protection Enabled:保护启用位,为1时是保护模式 ,为0时是实模式。 1时仅启用段保护机制,并不一定开启分页保护模式
PG
Paging:页保护启用位(分页机制位),为1时代表启用分页保护机制。 为0时不启用分页保护机制(线性地址=物理地址)。
WP
写保护位,当WP为1时,超级特权用户(0环)不可以向用户层只读地址写入数据。
CPL< 3 时,此时为特权层,用户层地址A(US = 1)对应的页为只读页。
当WP为0时,特权层程序可以对地址A进行写的操作。
当WP为1时,特权层程序无法对地址A进行写的操作。
Cr1寄存器
保留
Cr2寄存器
当发生缺页异常时,CPU会将引起异常的线性地址存放到Cr2中
Cr4寄存器
PAE
Physical Address Extensions:物理地址扩展位。 为1时,29912分页。为0时,101012分页。
PSE
决定是否启用PS位控制
PSE = 0 页大小一律为4k
置1时,PDE的PS位才有效果。置0时,PDE的PS位作废。