保護模式篇——TLB與CPU快取
- 2021 年 10 月 23 日
- 筆記
- 羽夏看Win系統內核
寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?上一節教程學會了嗎?上一節課的練習做了嗎?沒有的話就不要繼續了。
🔒 華麗的分割線 🔒
練習及參考
本次答案均為參考,可以與我的答案不一致,但必須成功通過。
2️⃣ 自己實驗有和沒有DEP保護的程式,看看效果。
🔒 點擊查看答案 🔒
這題目有一個坑,接下來請我娓娓道來。
首先說一句:你設置作業系統的DEP保護所有程式有效了嗎?(具體啟用查看上一節教程,選中為除下列選定程式之外的所有程式和服務啟用DEP,直接點確定),不然不起效果,白皮書原話:
If IA32_EFER.NXE = 1, execute-disable (if 1, instruction fetches are not allowed from the 2-MByte region controlled by this entry; see Section 4.6); otherwise, reserved (must be 0).
也就是說IA32_EFER的NXE位為0,就算最高位是1也是無盡於是。我們更改這個選項就是使它為1,這個就是是坑。好了,根據我的程式碼先運行一下:
發現報錯了,這個和10-10-12分頁的不同之處。然後把PAGE_READWRITE改為PAGE_EXECUTE_READWRITE,然後再運行:
這次運行成功,既然它們的頁的首地址都是一樣的,我們來看看它們的PDE或者PTE哪裡不同,先是PAGE_READWRITE的:
PROCESS 8947dda0 SessionId: 0 Cid: 0690 Peb: 7ffde000 ParentCid: 0204
DirBase: 13b00380 ObjectTable: e1702740 HandleCount: 18.
Image: mytest.exe
kd> !dq 13b00380
#13b00380 00000000`4ac31001 00000000`4a972001
#13b00390 00000000`4ac33001 00000000`4aa70001
#13b003a0 00000000`baf903c0 00000000`4a8a3001
#13b003b0 00000000`4a6a4001 00000000`4a5a1001
#13b003c0 00000000`baf90360 00000000`4a1a6001
#13b003d0 00000000`4a227001 00000000`4a264001
#13b003e0 00000000`baf90400 00000000`00000000
#13b003f0 00000000`00000000 00000000`00000000
kd> !dq 4ac31000
#4ac31000 00000000`4a92a067 00000000`4aa13067
#4ac31010 00000000`4aaa9067 00000000`4ab2f067
#4ac31020 00000000`00000000 00000000`00000000
#4ac31030 00000000`00000000 00000000`00000000
#4ac31040 00000000`00000000 00000000`00000000
#4ac31050 00000000`00000000 00000000`00000000
#4ac31060 00000000`00000000 00000000`00000000
#4ac31070 00000000`00000000 00000000`00000000
kd> !dq 4aa13000+1d0*8
#4aa13e80 80000000`4adc7067 00000000`00000000
#4aa13e90 00000000`00000000 00000000`00000000
#4aa13ea0 00000000`00000000 00000000`00000000
#4aa13eb0 00000000`00000000 00000000`00000000
#4aa13ec0 00000000`00000000 00000000`00000000
#4aa13ed0 00000000`00000000 00000000`00000000
#4aa13ee0 00000000`00000000 00000000`00000000
#4aa13ef0 00000000`00000000 00000000`00000000
然後是PAGE_EXECUTE_READWRITE:
PROCESS 895c8da0 SessionId: 0 Cid: 068c Peb: 7ffdf000 ParentCid: 0204
DirBase: 13b00300 ObjectTable: e168ea78 HandleCount: 18.
Image: mytest.exe
kd> !dq 13b00300
#13b00300 00000000`4711c001 00000000`472dd001
#13b00310 00000000`4735e001 00000000`4741b001
#13b00320 00000000`319e5001 00000000`31ae6001
#13b00330 00000000`31b27001 00000000`31b64001
#13b00340 00000000`37e87001 00000000`37c88001
#13b00350 00000000`37d89001 00000000`37c46001
#13b00360 00000000`baf903a0 00000000`46a2a001
#13b00370 00000000`46a6b001 00000000`46ba8001
kd> !dq 4711c000
#4711c000 00000000`475d6067 00000000`472bf067
#4711c010 00000000`47715067 00000000`4751b067
#4711c020 00000000`00000000 00000000`00000000
#4711c030 00000000`00000000 00000000`00000000
#4711c040 00000000`00000000 00000000`00000000
#4711c050 00000000`00000000 00000000`00000000
#4711c060 00000000`00000000 00000000`00000000
#4711c070 00000000`00000000 00000000`00000000
kd> !dq 472bf000+1d0*8
#472bfe80 00000000`476b3067 00000000`00000000
#472bfe90 00000000`00000000 00000000`00000000
#472bfea0 00000000`00000000 00000000`00000000
#472bfeb0 00000000`00000000 00000000`00000000
#472bfec0 00000000`00000000 00000000`00000000
#472bfed0 00000000`00000000 00000000`00000000
#472bfee0 00000000`00000000 00000000`00000000
#472bfef0 00000000`00000000 00000000`00000000
可以看出PTE的最高位置為1,導致這個頁被認為是數據,不是程式碼,故導致記憶體訪問錯誤,如果我們在訪問之前改了它,那麼就能訪問它嗎?結果是可以的,我就不贅述了。
🔒 點擊查看程式碼 🔒
#include "stdafx.h"
#include <iostream>
#include <windows.h>
char shellcode[]={
0x6A,0x00, //PUSH 0
0x6A,0x00, //PUSH 0
0x6A, 0x00 , //PUSH 0
0x6A ,0x00 , //PUSH 0
0xB8, 0x00 ,0x00 ,0x00, 0x00 , //MOV EAX,0000
0xFF, 0xD0 , //CALL EAX
0xC3 //RET
};
int main(int argc, char* argv[])
{
int msgbox=(int)MessageBoxW;
*(int*)&shellcode[9]=msgbox;
LPVOID scaddr = VirtualAlloc(NULL,1024,MEM_COMMIT,PAGE_READWRITE);
memcpy(scaddr,shellcode,sizeof(shellcode));
printf("Shellcode物理頁:%p\n",scaddr);
_asm
{
mov eax,scaddr;
call eax;
}
VirtualFree(scaddr,0,MEM_RELEASE);
system("pause");
return 0;
}
TLB
CPU通過頁的方式對物理記憶體進行了,需要通過某種運算方式才能訪問真正的記憶體。但是,如果頻繁訪問某一個線性地址,每次都得通過推算到真正的物理地址然後進行讀寫操作,是不是挺浪費效率的?Intel就考慮到性能的問題,提供了TLB這一個機制,提供快取提高讀寫效率。
TLB的全稱為Translation Lookaside Buffer,它的結構如下:
| LA(線性地址) | PA(物理地址) | ATTR(屬性) | LRU(統計) |
|---|---|---|---|
| 0x81010111 | …… | …… | 1 |
對於TLB,給出如下說明:
1. ATTR(屬性):屬性是PDPE PDE PTE三個屬性AND起來的. 如果是10-10-12分頁就是PDE和PTE。
2. 不同的CPU這個表的大小不一樣。
3. 只要Cr3變了,TLB立馬刷新,一核一套TLB。
學到現在我們知道,作業系統的高2G映射基本不變,如果Cr3改了,TLB刷新重建高2G以上很浪費。所以PDE和PTE中有個G標誌位,如果G位為1刷新TLB時將不會刷新PDE/PTE的G位為1的頁,當TLB滿了,根據統計資訊將不常用的地址廢棄,最近最常用的保留。
TLB有不同的種類,用於不同的快取目的,它在X86體系里的實際應用最早是從Intel的486CPU開始的,在X86體系的CPU裡邊,一般都設有如下4組TLB:
第一組:快取一般頁表(4K位元組頁面)的指令頁錶快取:Instruction-TLB
第二組:快取一般頁表(4K位元組頁面)的數據頁錶快取:Data-TLB
第三組:快取大尺寸頁表(2M/4M位元組頁面)的指令頁錶快取:Instruction-TLB
第四組:快取大尺寸頁表(2M/4M位元組頁面)的數據頁錶快取:Data-TLB
CPU快取
CPU快取是位於CPU與物理記憶體之間的臨時存儲器,它的容量比記憶體小的多但是交換速度卻比記憶體要快得多。它可以做的很大,但不是TLB,它們有很大的不同。TLB存的是線性地址與物理地址的對應關係,CPU快取存的是物理地址與內容對應關係。
更多的細節請參考白皮書的Chapter 11 Memory Cache Control,本篇教程主要是針對內核安全層面,就不再贅述了。
PWT 與 PCD
PWT全稱為Page Write Through,PWT = 1時,寫Cache的時候也要將數據寫入記憶體中。
PCD全稱為Page Cache Disable,PCD = 1時,禁止某個頁寫入快取,直接寫記憶體。比如,做頁表用的頁,已經存儲在TLB中了,可能不需要再快取了。
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習很少,請保質保量的完成。
1️⃣ 在10-10-12分頁模式下體會TLB的存在。要求:通過程式碼掛物理頁,不能通過Windbg掛。原物理頁掛完寫入值讀取,然後把地址換物理頁繼續讀取,看看值是否發生變化。然後改頁屬性為全局頁,重複上述操作。然後用INVLPG指令之後再看看值是否變化。
下一篇
保護模式篇——中斷與異常和控制暫存器

