Windows保护模式学习笔记(十)—— TLB

前言

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

地址解析

当我们通过一个线性地址访问一个物理页(比如:MOV EAX,[0x12345678])时,实际上CPU未必只读了4个字节。

10-10-12分页

  1. CPU先通过线性地址找到对应的PDE:4个字节
  2. CPU再通过PDE和线性地址找到PTE:4个字节
  3. 最后再通过PTE找到对应物理页:4个字节

一共访问了12个字节,如果跨页可能更多。

2-9-9-12分页

  1. 找到PDPTE:8个字节
  2. 找到PDE:8个字节
  3. 找到PTE:8个字节
  4. 最后找到物理页:4个字节

一共访问了20个字节,如果跨页可能更多。


  • 为了提高访问效率,只能对线性地址与其对应的物理地址做记录
  • CPU内部做了一张表,用来记录这些东西。它的效率和寄存器一样快,名字叫做TLB(Translation Lookaside Buffer)
  • 由于TLB的效率很快,因此它的大小不能太大,少则几十条,多则也只有上百条

思考:在一个进程的4GB空间中,有无数个线性地址,但是一个TLB最多只能记录上百条记录,那么这张表真的有意义吗?

TLB

TLB结构

TLB
ATTR:属性
在10-10-12分页模式下:ATTR = PDE属性 & PTE属性
在2-9-9-12分页模式下:ATTR = PDPTE属性 & PDE属性 & PTE属性

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

注意

  1. 不同的CPU,TLB大小不同
  2. 只要Cr3发生变化,TLB立即刷新,一核一套TLB
  3. 由于操作系统的高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中断,使虚拟机继续运行,然后继续向下运行代码

运行结果:
运行结果
实验成功!

实验总结

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

练习2:体验全局页的意义

略(待补充)

练习3:INVLPG指令的意义

略(待补充)

发布了45 篇原创文章 · 获赞 2 · 访问量 1857

猜你喜欢

转载自blog.csdn.net/qq_41988448/article/details/102736062
TLB