地址解析
当我们通过一个线性地址访问一个物理页(比如: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的效率很快,因此它的大小不能太大,少则十几条,多则也只有上百条,具体和CPU型号有关。
在手册第三卷里的4.10.2 Translation Lookaside Buffers (TLBs)能查看详细解释
引入TLB的目的是什么?
上面的地址解析已经详细说明了,就是为了减少到内存查页表的次数!
在一个进程的4GB空间中,有无数个线性地址,但是一个TLB最多只能记录上百条记录,那么这张表真的有意义吗?
有,一个小页大小为4k,一个大页的大小为2M/4M,这已经是非常能提高CPU寻址的效率了
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);
TLB衍生的攻防技术
Shadow-Walker
这个技术已经成为了历史,而且问题多多,具体可看
https://www.blackhat.com/docs/us-14/materials/us-14-Torrey-MoRE-Shadow-Walker-The-Progression-Of-TLB-Splitting-On-x86.pdf
https://bbs.pediy.com/thread-194933.htm
https://blog.can.ac/2018/04/26/splitting-data-from-code-forgotten-x86-feature-segmentation-n/
实验
下面做几个小实验来验证TLB
一.验证TLB的存在
找到两个有效的线性地址
然后构建调用门,修改GDT表项
上面的步骤之前有说过,这里不再重复
最后编译如下程序
#include <iostream>
#include <windows.h>
DWORD g_tmpValue=0;
void __declspec(naked) proc()//0x00401000
{
__asm
{
//给0地址挂物理页
mov dword ptr ds : [0xc0000000], 0x0d3cd867;//867是页属性 注意
//给0地址挂上的物理页赋值
mov dword ptr ds : [0], 0x12345678;
//将0地址的物理页改成其他的物理页
mov dword ptr ds : [0xc0000000], 0x0270a867;//867是页属性 注意
//再次读取线性地址
mov eax, dword ptr ds : [0];
mov g_tmpValue, eax;
retf;
}
}
int main(int argc, char* argv[])
{
//调用门
char buff[6] = { 0 };
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48;
__asm
{
call fword ptr[buff]
}
printf("g_tmpValue=%x\n", g_tmpValue);
getchar();
return 0;
}
成功读出之前挂上的物理页的内容,证明了TLB的存在
二.修改cr3后是否会导致处理器自动刷新非全局页的TLB表项
编译如下代码
#include <iostream>
#include <windows.h>
DWORD g_tmpValue=0;
void __declspec(naked) proc()//0x00401000
{
__asm
{
//给0地址挂物理页
mov dword ptr ds : [0xc0000000], 0x0d3cd867;//867是页属性 注意
//给0地址挂上的物理页赋值
mov dword ptr ds : [0], 0x12345678;
//将0地址的物理页改成其他的物理页
mov dword ptr ds : [0xc0000000], 0x0270a867;//867是页属性 注意
//修改cr3
mov eax, cr3;//如果属性为967 修改cr3也不会刷新 因为G位为全局位
mov cr3, eax;
//再次读取线性地址
mov eax, dword ptr ds : [0];
mov g_tmpValue, eax;
retf;
}
}
int main(int argc, char* argv[])
{
//调用门
char buff[6] = { 0 };
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48;
__asm
{
call fword ptr[buff]
}
printf("g_tmpValue=%x\n", g_tmpValue);
getchar();
proc();
return 0;
}
执行
并没有读出我们之前赋值的0x12345678
说明切换了cr3会导致刷新非全局页的TLB表项
我们再把代码稍作更改,把我们给0地址挂上的第一个物理页0x0d3cd867
,改为0x0d3cd967
,修改了G位
再次编译运行
成功读出之前挂上的物理页的内容,证明了切换cr3只会导致非全局页的TLB表项刷新
三.INVLPG指令的意义
上面我们已经证明了全局页在TLB表项是不会被自动刷新的,那么我们想要删除这个全局页在TLB中的表项该怎么办呢?
编译如下程序
#include <iostream>
#include <windows.h>
DWORD g_tmpValue=0;
void __declspec(naked) proc()//0x00401000
{
__asm
{
//给0地址挂物理页
mov dword ptr ds : [0xc0000000], 0x0d3cd967;//867是页属性 注意
//给0地址挂上的物理页赋值
mov dword ptr ds : [0], 0x12345678;
//将0地址的物理页改成其他的物理页
mov dword ptr ds : [0xc0000000], 0x0270a867;//867是页属性 注意
//删除全局页
INVLPG dword ptr ds : [0];
//修改cr3
mov eax, cr3;//如果属性为967 修改cr3也不会刷新 因为G位为全局位
mov cr3, eax;
//再次读取线性地址
mov eax, dword ptr ds : [0];
mov g_tmpValue, eax;
retf;
}
}
int main(int argc, char* argv[])
{
//调用门
char buff[6] = { 0 };
*(DWORD*)&buff[0] = 0x12345678;
*(WORD*)&buff[4] = 0x48;
__asm
{
call fword ptr[buff]
}
printf("g_tmpValue=%x\n", g_tmpValue);
getchar();
return 0;
}
运行
可以看到,并没有成功读出我们之前赋值的内容,我们已经成功删除了全局页
手册中对INVLPG
指令的解释如下,在第二卷中
感兴趣的可以详细的测试一下