保护模式(九) 29912分页、tlb

Windows保护模式学习笔记(九)—— 2-9-9-12分页

前言

一、学习自滴水编程达人中级班课程,官网:https://bcdaren.com
二、海东老师牛逼!

要点回顾

在之前的笔记中我们已经讲解了10-10-12分页方式,在这种分页方式下物理地址最多可达4GB。但随着硬件的发展,4GB的物理地址范围已经无法满足要求
Intel在1996年就已经意识到这个问题了,所以设计了新的分页方式,也就是我们本节课要讲的2-9-9-12分页,又称为PAE(物理地址扩展)分页

10-10-12分页

原理

  1. Intel认为一张页的大小为4K是比较合理的,所以先确定页的大小为4K,4KB等于4096个字节,也就是2的12次方,所以32位的最后一部分就确定为了12位
  2. 当初的物理内存比较小,所以4个字节的PTE就够了,加上页的尺寸是4K,所以一个页能存储1024个 PTE,也就是2的10次方,第二个10也就确定了
    PTI
  3. 同理,PDT也需要10个比特位,10+10+12刚好32位

环境配置

若想将操作系统设置为10-10-12分页,只要将C:\boot.ini文件中的 noexecute 改为 execute,重启即可。具体步骤在学习10-10-12分页时已经介绍,这里不再详述。

2-9-9-12分页

描述:2-9-9-12分页(PAE,物理地址扩展)

原理

  1. 页的大小是确定的,4KB不能随便改,所以32位的最后一部分就确定为了12位
  2. 如果想增大物理内存的访问范围,就需要增大PTE,增大了多少呢?考虑对齐的因素,增加到8个字节
    由于PTE增大了,而PTT表的大小没变,依然是4KB,所以每张PTT表能放的PTE个数由原来的1024个减少到512个,512等于2的9次方,因此PTI=9
    8位PTE
  3. 由于2的9次方个PDE就能找到所有的PTT表,因此PDI=9
  4. 分配到这里时,还剩下前2位未分配
    与10-10-12不同,CR3不直接指向PDT表,而是指向一张新的表,叫做PDPT表(页目录指针表)
    PDPT表中的每一个成员叫做PDPTE(Page-Directory-Point-Table Entry,页目录指针表项),每项占8个字节
    PDPT表只有4个成员,因为2位比特位只能满足四种情况:00 01 10 11
    PDPTE

PDPTE

结构图:
PDPTE结构图
P位:第0位,有效位
Avail:这部分供操作系统软件随意使用,CPU不使用
Base Addr:指向PDT表地址,由两部分组成
第一部分:高四字节32~35位
第二部分:低4字节12~31位
这两部分加起来共24位,后12位补0
灰色部分:保留位

注意:PWT位PCD位在本篇不作描述

PDE

结构图:
2-9-9-12-PDE
PAT位:页属性表
只有当PS=1时,PAT位才是有意义的(页属性表只针对页)
具体含义这里不作介绍,感兴趣的同学可以查阅资料

注意:

  1. G位PCD位PWT位可以暂且不管。
  2. 其他属性位的含义在上面学习PDPTE时或在学习10-10-12分页的PDE与PTE时已经介绍,这里不再详述

PTE

结构图:
2-9-9-12-PTE
注意:

  1. PTE中12~35位是物理页基址,低12位补0
  2. 物理页基址+12位页内偏移指向具体数据

XD/NX标志位

描述:

  • Intel中称为XD,AMD中称为NX,即No Excetion
  • 段的属性有可读、可写和可执行
  • 页的属性有可读、可写
  • 当RET执行返回的时候,如果把堆栈里面的数据指向一段提前准备好的数据(把数据当作代码来执行,漏洞都是依赖这点,比如SQL注入也是),那么就会产生任意代码执行的后果
  • 所以,Intel就在这方面做了硬件保护,设置了一个不可执行位 – XD/NX位
  • 当XD=1时,软件产生了溢出也没有关系,即使EIP蹦到了危险的“数据区”,也是不可以执行的
  • PAE分页模式下,PDE与PTE的最高位为XD/NX位.

环境配置

若想将操作系统设置为2-9-9-12分页,只要将C:\boot.ini文件中的 execute 改为 noexecute,重启即可。具体步骤在学习10-10-12分页时已经介绍,这里不再详述。

实验:通过线性地址找到物理地址

第一步:新建一个记事本,写入"Hello World"

HelloWorld

第二步:使用Cheat Engine附加进程

CE附加进程

第三步:找到"Hello World"的线性地址

寻找线性地址
线性地址
注意:这里搜索到三个结果,需要判断哪个是缓存,哪个才是真正的线性地址

将Hello World的最后一个字符’d’改为’m’,保存
确定线性地址
线性地址最终确定为:0104FE58

第五步:将线性地址拆分为2-9-9-12四组比特组
0	1	0	4	F	E58
=
0000 0001 0000  0100 1111 E58
=
00				// 0x0
000001000		// 0x8
001001111		// 0x4F
E58				// 12个比特位刚好三个字节

     
     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
第六步:获得进程的Cr3

在WinDbg中输入命令:!process 0 0
Cr3
DirBase的值就是进程的Cr3

第六步:通过Cr3找到字符串的物理地址
第一层:PDPT表

PDPTE
线性地址前两位比特位为0,所以找下标为0的PDPTE

第二层:PDT表

PDE
乘8是因为每一个PDE占8个字节

第三层:PTT表

PTE
乘8是因为每一个PTE占8个字节

第四层:物理页

物理页
寻找物理页时就不用再乘8了

使用db命令以字符形式查看数据
物理页2
实验成功!

TLB

TLB结构

ATTR:属性
在这里插入图片描述
在10-10-12分页模式下:ATTR = PDE属性 & PTE属性
在2-9-9-12分页模式下:ATTR = PDPTE属性 & PDE属性 & PTE属性

LRU:统计信息
由于TLB的大小有限,因此当TLB被写满、又有新的地址即将写入时,TLB就会根据统计信息来判断哪些地址是不常用的,从而将不常用的记录从TLB中移除。

注意:

不同的CPU,TLB大小不同
只要Cr3发生变化,TLB立即刷新,一核一套TLB
由于操作系统的高2G映射基本不变,因此如果Cr3改了,TLB刷新的话,重建高2G以上很浪费。
所以PDE和PTE中有个G标志位(当PDE为大页时,G标志位才起作用),如果G位为1,刷新TLB时将不会刷新PDE/PTE
G位为1的页,当TLB写满时,CPU根据统计信息将不常用的地址废弃,保留最常用的地址
TLB种类
TLB在X86体系的CPU中的实际应用最早是从Intel的486CPU开始的,在X86体系的CPU中,一般都设有如下4组TLB:

第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB);
第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB);
第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB);
第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)

注意:以下练习均采用10-10-12分页模式

练习1:体验TLB的存在

第一步:运行代码
注意:在调用门(int 0x20)执行前的任意位置设置断点,并运行至断点处

#include <stdio.h>
#include <windows.h>

DWORD x, y, z;

void __declspec(naked) PageOnNull() {
	__asm
	{
		//保存现场
		push ebp
		mov ebp, esp
		sub esp, 0x100
		push ebx
		push esi
		push edi
	}

	DWORD* pPTE;			// 保存目标线性地址的 PTE 线性地址
	DWORD* pNullPTE;		// 0 地址的 PTE 线性地址
	pNullPTE = (DWORD*)0xC0000000;

	// 挂上 0x50000000 所在位置
	pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10));	
	*pNullPTE = *pPTE;

	x = *(DWORD*)0;

	// 挂上 0x60000000 所在位置
	pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10));	
	*pNullPTE = *pPTE;

	y = *(DWORD*)0;

	// 刷新 TLB 
	__asm {
		mov eax, cr3
		mov cr3, eax
	}
	
	// 再次读取 0 地址位置的数据
	z = *(DWORD*)0;



	__asm
	{
		//恢复现场
		pop edi
		pop esi
		pop ebx
		mov esp, ebp
		pop ebp
		iretd
	}
}

int main(int argc, char* argv[])
{
	DWORD* p5 = (DWORD*)VirtualAlloc((LPVOID)0x50000000, 4, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
	DWORD* p6 = (DWORD*)VirtualAlloc((LPVOID)0x60000000, 4, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

	if (p5 != (DWORD*)0x50000000 || p6 != (DWORD*)0x60000000)
	{
		printf("Error alloc!\n");
		return -1;
	}

	*p5 = 0x1234;
	*p6 = 0x5678;

	__asm
	{
		// 通过中断门提权
		int 0x20
	}

	printf("1. 读 0 地址数据:\n");
	printf("*NULL = 0x%x \n\n", x);

	printf("2. 给 0 地址重新挂上物理页\n\n");

	printf("3. 重新读取 0 地址数据:\n");
	printf("*NULL = 0x%x \n\n", y);

	printf("4. 刷新 TLB \n\n");

	printf("5. 再次读取 0 地址数据:\n");
	printf("*NULL = 0x%x \n", z);

	return 0;
}

第二步:设置中断门描述符
首先在编辑器的反汇编界面查看PageOnNull函数的首地址
在这里插入图片描述

因此确定中断门描述符:0040ee00`00081030

使用WinDbg在IDT[0x20]处写入中断门描述符
在这里插入图片描述

kd> eq 8003f500 0040ee00`00081030

第三步:继续运行程序
解除WinDbg中断,使虚拟机继续运行,然后继续向下运行代码

运行结果:在这里插入图片描述

实验总结

可以发现,在x被赋值完成后,即使0地址被挂上了新的物理页,再对y进行赋值,x和y输出的值是相同的。
但是在Cr3刷新后,0地址没有被挂上新的物理页,对z进行赋值后,z却输出了新的值。
这是因为Cr3刷新前,0地址第一次被x访问时,线性地址与物理地址的对应关系被写入了TLB中,因此在对y赋值时,TLB的记录没有被刷新,访问的还是原来的物理页

练习2:体验全局页的意义
略(待补充)

练习3:INVLPG指令的意义
略(待补充)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值