Windows保护模式学习笔记(十)—— TLB
前言
一、学习自
滴水编程达人
中级班课程,官网:https://bcdaren.com
二、海东老师牛逼!
地址解析
当我们通过一个线性地址访问一个物理页(比如:MOV EAX,[0x12345678]
)时,实际上CPU未必只读了4个字节。
10-10-12分页
- CPU先通过线性地址找到对应的PDE:4个字节
- CPU再通过PDE和线性地址找到PTE:4个字节
- 最后再通过PTE找到对应物理页:4个字节
一共访问了12个字节,如果跨页可能更多。
2-9-9-12分页
- 找到PDPTE:8个字节
- 找到PDE:8个字节
- 找到PTE:8个字节
- 最后找到物理页:4个字节
一共访问了20个字节,如果跨页可能更多。
- 为了提高访问效率,只能对线性地址与其对应的物理地址做记录。
- CPU内部做了一张表,用来记录这些东西。它的效率和寄存器一样快,名字叫做TLB(Translation Lookaside Buffer)。
- 由于TLB的效率很快,因此它的大小不能太大,少则几十条,多则也只有上百条。
思考:在一个进程的4GB空间中,有无数个线性地址,但是一个TLB最多只能记录上百条记录,那么这张表真的有意义吗?
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函数的首地址
因此确定中断门描述符:0040
ee00`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指令的意义
略(待补充)