Windows内核实验:数据TLB(DTLB)

Windows内核实验:数据TLB(DTLB)

TLB(Translation Lookaside Buffer)是从虚拟地址到物理地址转换的缓存(一级一级查页表太耗费时间了),TLB命中后就不会再去访问物理内存去查找,对于tlb是对我们完全透明的(一核一套TLB),没有一条指令去看tlb的内容,调试器也不行

所以实验时一定要保证执行的指令是在一个页上,即不造成页面异常,不然页面异常会较大的影响tlb里的内容,对实验现象造成不可解释的结果

实验现象

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define PDE(x) (DWORD64 *) (((x >> 21) << 3) + 0xc0600000)
#define PTE(x) (DWORD64 *) (((x >> 12) << 3) + 0xc0000000)
DWORD g_var;
DWORD64 g_pte;
#pragma section("new_page",read,write)
__declspec(allocate("new_page")) DWORD page1[1024];//0x0041b000
__declspec(allocate("new_page")) DWORD page2[1024];//0x0041c000
void __declspec(naked) IdtEntry() {//裸函数不会帮我们生成栈帧,单纯一个call
__asm {
mov eax,ds:[0x0041c000]//再次确保page2的页面在tlb里,没有被换出
}
g_pte= *PTE(0x0041c000);
*PTE(0x0041c000) = *PTE(0x0041b000);
g_var = page2[0];
*PTE(0x0041c000) = g_pte;//换回原来的页表项,避免double free 造成的蓝屏
__asm{
iretd
}
}
void interrupt() {
page1[0] = 1;
page2[0] = 2;//挂载物理内存的同时把虚拟内存放到tlb里
__asm {
int 0x20
}
}
int main() {
if (0x401040 != IdtEntry) {
printf("Idtentry address wrong");
system("pause");
exit(-1);
}
//eq 8003f500 0040ee00`00081040
interrupt();
printf("g_var:%d\n",g_var);
system("pause");
}
g_var:2
请按任意键继续. . .

如果没有tlb,我们把page2的页表项变为page1的页表项,g_var = page2[0];就是把page1的数据放入g_var,打印出1

但是有了tlb,此时虽然页表换了但是tlb没有刷新,在访问page2虚拟内存时直接在tlb中命中了原来的物理地址,所以依旧打印出了2

在进程结束时,操作系统会回收虚拟内存映射的物理内存,这个时候如果不把page2的页表项换回来,就会造成page1的double free,从而蓝屏

刷新tlb

TLB的刷新(使tlb无效,感觉像清除)通常发生在以下情况下:

  1. 上下文切换:当操作系统切换进程时,TLB可能会被刷新,以确保新进程的虚拟地址能够正确地映射到物理地址。
  2. 内存管理单元(MMU)失效:如果操作系统更改了页表或地址映射方案,它可能会通知处理器刷新TLB,以便更新地址映射。
  3. 强制刷新:在某些特定的情况下,操作系统或处理器可能需要强制刷新TLB,以确保一致性和正确性。invlpg addr指令(Invalidate Page)可以无视g(global)标志位将该虚拟地址的TLB项标记为无效,下次会访问会更新tlb

所以我们把cr3重新装入就会刷新TLB

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define PDE(x) (DWORD64 *) (((x >> 21) << 3) + 0xc0600000)
#define PTE(x) (DWORD64 *) (((x >> 12) << 3) + 0xc0000000)
DWORD g_var;
DWORD64 g_pte;
#pragma section("new_page",read,write)
__declspec(allocate("new_page")) DWORD page1[1024];//0x0041b000
__declspec(allocate("new_page")) DWORD page2[1024];//0x0041c000
void __declspec(naked) IdtEntry() {//裸函数不会帮我们生成栈帧,单纯一个call
__asm {
mov eax, ds: [0x0041c000]//再次确保page2的页面在tlb里,没有被换出
}
g_pte = *PTE(0x0041c000);
*PTE(0x0041c000) = *PTE(0x0041b000);
__asm {
mov eax,cr3
mov cr3,eax//刷新TLB
}
g_var = page2[0];
*PTE(0x0041c000) = g_pte;//换回原来的页表项,避免double free 造成的蓝屏
__asm {
iretd
}
}
void interrupt() {
page1[0] = 1;
page2[0] = 2;//挂载物理内存的同时把虚拟内存放到tlb里
__asm {
int 0x20
}
}
int main() {
if (0x401040 != IdtEntry) {
printf("Idtentry address wrong");
system("pause");
exit(-1);
}
//eq 8003f500 0040ee00`00081040
interrupt();
printf("g_var:%d\n", g_var);
system("pause");
}
g_var:1
请按任意键继续. . .

G位,进一步提升性能

页目录项和页表项的第8位是G(gloabal)位

image-20230603122904700

在tlb里会记录这个G位,如果是全局页,在刷新tlb(除了强制(invlpg)时,不会刷新带有G位的tlb项

这样做是因为内核中的多数数据是进程共享的,每次切换进程时刷新这些没必要刷新的内存浪费性能

kd> !process 0 0 test.exe
Failed to get VadRoot
PROCESS 8137ada0 SessionId: 0 Cid: 0430 Peb: 7ffdb000 ParentCid: 04f0
DirBase: 090c0360 ObjectTable: e24d1a98 HandleCount: 15.
Image: test.exe

kd> .process /i 8137ada0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
80528bdc cc int 3
kd> !pte 0x0041b000
VA 0041b000
PDE at C0600010 PTE at C00020D8
contains 000000000FE30067 contains 8000000000938067
pfn fe30 ---DA--UWEV pfn 938 ---DA--UW-V

kd> !pte gdtr
VA 8003f000
PDE at C0602000 PTE at C04001F8
contains 0000000000B0A163 contains 000000000003F163
pfn b0a -G-DA--KWEV pfn 3f -G-DA--KWEV

可以看到内核共享数据gdtr是有G属性的,但是3环数据就没有

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define PDE(x) (DWORD64 *) (((x >> 21) << 3) + 0xc0600000)
#define PTE(x) (DWORD64 *) (((x >> 12) << 3) + 0xc0000000)
DWORD g_var;
DWORD64 g_pte;
#pragma section("new_page",read,write)
__declspec(allocate("new_page")) DWORD page1[1024];//0x0041b000
__declspec(allocate("new_page")) DWORD page2[1024];//0x0041c000
void __declspec(naked) IdtEntry() {//裸函数不会帮我们生成栈帧,单纯一个call
g_pte = *PTE(0x0041c000);
*PTE(0x0041c000) = *PTE(0x0041b000) | 0x100;//把虚拟地址的页表项改为Global
__asm {
mov eax,cr3
mov cr3,eax
}//刷新tlb
__asm mov eax, ds: [0x0041c000]//确保虚拟地址tlb里的g位置1
*PTE(0x0041c000) = g_pte;//换回原来的页表项
__asm {
mov eax, cr3
mov cr3, eax
}//刷新tlb,但是由于我们的page2 G位被置1,所以不会被刷新,page2还是指向page1物理页
//__asm invlpg ds:[0x0041c000] //强制刷新,无视g位
g_var = page2[0];
__asm {
iretd
}
}
void interrupt() {
page1[0] = 1;
page2[0] = 2;//挂载物理内存的同时把虚拟内存放到tlb里
__asm {
int 0x20
}
}
int main() {
if (0x401040 != IdtEntry) {
printf("Idtentry address wrong");
system("pause");
exit(-1);
}
//eq 8003f500 0040ee00`00081040
interrupt();
printf("g_var:%d\n", g_var);
system("pause");
}
g_var:1
请按任意键继续. . .