Windows(x86)頁表與虛擬空間之我見

Windows(x86)頁表與虛擬空間之我見

來自專欄看雪論壇7 人贊了文章

運行環境:

主機系統:Windows 10 x64

目標系統:Windows XP sp3 x86

工具軟體:

虛擬機:Virtual Box

編譯器:Visual Studio 2017

調試器:Windbg、OllyDBG

參考書籍:

《Windows 內核設計思想》

《Windows 核心編程》

《軟體調試》

參考博文:

《OS 學習筆記》

《詳解Windows內存分頁機制》

本文引用了以上書籍、文章的部分觀點,算是一篇學習筆記。在此感謝各位作者!

頁表機制準確的說是CPU實現的特性,由OS加以利用。網上有很多陳述頁表機制的文章,但以Linux居多,Windows偏少,且很多都是理論層面借圖表的闡述,或許對於科班出身的人士來說是小菜一碟,可對我等自學愛好者而言卻經常是一臉懵逼的感嘆!因此,萌發了動手實踐的想法,經過一翻折騰,總算小有心得,就此記錄於網際,希望本文能給和我一樣的愛好者以幫助。本文可能存在很多歪解,請各位看官多多斧正!

本文試圖解決的疑問:

  • 頁有多少個?
  • 頁每個進程都有嗎?
  • 頁多大?
  • 頁放在哪裡?
  • 頁如何查看?
  • 頁如何修改?
  • 頁如何隔離進程?
  • 頁和虛擬地址的關係?
  • 頁在何時被使用?

從test.exe開始:

// test.exe 源碼#include <iostream> int main(){ std::cin.get (); // 等待做手腳 int* p = NULL; *p = 0x89abcdef ; // 向0指針寫入數據 std::cout << "Hello nullptr!
"; // 改編自經典:) return 0;}

代碼很簡單不出大問題的話,編譯運行敲回車直接崩潰QAQ。在默認設置的Win10(17134)cmd下可正常運行和退出,但沒有列印「Hello nullptr」,在XP下報錯終止,錯誤代碼0xC0000005(訪問違規),可見Win10在異常處理上有所調整(記得《Windows核心編程》有提過在Vista之後即如此)。

有C/C++基礎的碼友一定記得一條原則,0x00000000為空指針,設計思想很簡單,例如調用malloc返回0表示失敗,如果返回0指針是可用的,那錯誤用什麼表示呢?

我的運行結果是正常的,我承認我搞事情了,嘿嘿!

在動手完成空指針寫入數據之前先大致了解一些基本情況,在Windbg枚舉進程。

kd> !process 0 0**** NT ACTIVE PROCESS DUMP ****PROCESS 8a0e59c8 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000 DirBase: 00039000 ObjectTable: e1000d10 HandleCount: 240. Image: System PROCESS 89be5020 SessionId: 0 Cid: 05d8 Peb: 7ffd6000 ParentCid: 05e4 DirBase: 19f3c000 ObjectTable: e1e27cc8 HandleCount: 35. Image: cmd.exe PROCESS 89ba8318 SessionId: 0 Cid: 0578 Peb: 7ffde000 ParentCid: 0268 DirBase: 19f97000 ObjectTable: e1b8ef08 HandleCount: 82. Image: taskmgr.exe PROCESS 89f52c10 SessionId: 0 Cid: 07c0 Peb: 7ffde000 ParentCid: 05e4 DirBase: 1d42f000 ObjectTable: e1ba5700 HandleCount: 77. Image: OllyICE.exe PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...

信息經過刪減,留下了幾個比較熟悉的進程,文中類似信息也會將無關清除,清除部分會用「...」表示,不再單獨註明。

這些進程分別是:taskmgr.exe任務管理器、cmd.exe控制台、System系統進程、OllyICE.exe調試器、test.exe測試程序。

每個進程的DirBase項的16進位數可以發現以下特徵:

  1. 各進程不同;
  2. 可被4096整除;
  3. 以000結尾。

如果你的顯示結果與特徵不符,那是另外一種模式PAE,本文不做解釋。從特徵上分析,可被4096整除說明是4K對齊;各進程不相同說明每個進程的起始頁不同,因此得以隔離進程;000結尾說明PDE沒有屬性,PDE為何物見下文。

了解了進程信息之後,再來看看進程的地址、佔用空間大小等信息,著重觀察test.exe。

在任務管理器窗口紅色框部分,是各進程使用物理內存佔用大小,單位Kb,這些數都可以被4整除,所得商即該程序使用的內存頁數。

當test.exe在前台時(最大化)使用了193(772K/4)頁內存,在後台時(最小化)使用9(36/4)頁,由此可見當程序非活動狀態或需求非常少時,物理內存會歸還給OS。從test.exe代碼可知,程序阻塞在cin.get ()調用,基本上沒什麼運行需求。(切換的窗口是cmd,因test運行於此)

OD窗口紅色框部分,是test.exe的邏輯地址和空間大小信息,此空間大小是程序自身可能用到的大小,粗略合計約1000(4M/4K)頁,通過OD的簡要信息,可獲知地址處都存放了什麼,如主棧、PE頭、.text代碼段等。這裡的空間大小基本是固定的。

經過以上分析得到一個結果,程序虛擬空間遠大於物理佔用,實際上程序在運行時,不會將所有內容都放入物理內存,僅將當前運行所需要的代碼、數據放入物理頁。

有了這些線索,我們通過實驗來進一步印證。利用PROCESS項的16進位數將test.exe切換至當前環境。默認當前環境是System進程,所以查詢顯示信息將是System進程的。

kd> !process 0 0...PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...kd> .process /i /p 89bfc470kd> gkd> r cr3cr3=24231000

使用r指令查看了cr3寄存器,顯示的信息與DirBase項一致,實際上OS在後台不斷的自動切換cr3,讓每個進程都得到一點運行時間。

實驗所需的知識點簡單介紹一下(以下僅是小頁面,大頁面請翻閱書籍):每個進程都有一頁(4K)存放一個頁目錄(Page Directory Entry,PDE),PDE佔4K空間,分成1024項,每項4B,每項描述一個頁表(Page Table Entr,PTE),PTE的高20位是基址域,低12位是屬性域;PTE也佔4K,同樣可分成1024項,每項4B,每項描述一頁(Page),Page的高20位是基址域,低12位是屬性域。

根據上述可得

1024(PTE 個數)*1024(Page 個數)*4096(Page 大小)=4G(總空間);

1024(PTE 個數)*1024(Page個數)*4(描述項大小)=4M(總佔用);

1024(PTE個數)*1024(Page個數)=1M(總頁數);

實際總佔用僅是理論值,一般程序不可能達到4M,下面實驗會得到證實。

查看test.exe的PDE,我將PDE分成兩部分,前512項和後512項分別進行分析,因PDE管理4G虛擬空間,但其高2G(後512項)是內核態使用,剩餘低2G(前512項)由用戶態使用(PAE模式用戶態擁有更多的空間)。

kd> !process 0 0...PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...kd> !dd 24231000 l200#24231000 24766067 245e0067 00000000 00000000...#242317c0 00000000 00000000 247cf067 00000000...#242317f0 00000000 2459a067 00000000 2451e067...

PDE前512項中只有5個非空PTE,前面說過1個PTE佔用4K物理內存,含1024個Page描述,也就是說1個PTE維護4M(1024*4096)空間,因此可認為OS給test.exe分配了20M(4*5)虛擬空間。

我把以上數據和OD的邏輯地址整理成表進行對比。

我相信你已經找到了相似特徵,邏輯地址與PTE是映射關係。更準確的說邏輯地址值是一個三段式數據結構,關於該結構我用一段代碼來說明。

// demo.exe 源碼#include <iostream> int main(){ struct { ULONG offset : 12; ULONG pteIndex : 10; ULONG pdeIndex : 10; } va { 0 }; using namespace std; while (true) { cin >> hex >> *(ULONG*)&va; cout << hex << "邏輯地址:0x" << *(ULONG*)&va << endl << "PDE索引:0x" << va.pdeIndex << " PDE物理偏移:0x" << va.pdeIndex * 4 << endl << "PTE索引:0x" << va.pteIndex << " PTE物理偏移:0x" << va.pteIndex * 4 << endl << "頁內偏移:0x" << va.offset << endl; } return 0;}

PDE可通過cr3得到,PDE+偏移即可找到PTE描述,PTE+偏移又可找到Page描述,Page+頁內偏移可指向具體數據,這就是用邏輯地址搜索頁表的過程。

進一步查看PTE索引1(第二個PTE)的內容:

kd> !process 0 0...PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...kd> !dd 24231000#24231000 24766067 245e0067 00000000 00000000...kd> !dd 245e0000 l400#245e0000 24430025 2456c025 2436d025 243ee025#245e0010 242af025 24470025 24271025 244f2025#245e0020 242b3025 242f4025 24375025 242b6025#245e0030 23fb7025 242b8025 240f9025 2423a025#245e0040 243fb025 241bc025 241bd025 242be025#245e0050 242bf025 24540025 243c1025 24502025#245e0060 24543025 248c4025 24705025 27da8025#245e0070 2459e025 24647025 00000000 24708025#245e0080 24349025 00000000 2444a025 2454b025#245e0090 2450c025 00000000 00000000 00000000#245e00a0 24362025 24523025 24664025 2465f067#245e00b0 24660067 24625025 00000000 00000000...

截取部分中共41項非空Page描述(省略的都是空Page),第二個PTE管理的4M空間範圍是0x00400000~0x007FFFFF,回顧OD截圖在這4M範圍內的大小合計是46頁,這裡相差5個頁,原因是什麼我不清楚,猜測可能是軟體之間的誤差,有知道的請告知!

PTE和Page的低12位是屬性域,數據上對應的如025、067等,屬性域其中一位描述了數據是在物理內存還是在硬碟的虛擬內存,OS通過屬性域控制內存,PTE可控制4M,Page可控制4K。更多屬性的內容請翻閱書籍。

在OD截圖可知0x00401000是.text段的起始,從demo程序的顯示結果獲知該邏輯地址PTE的索引1偏移4,現在來看該Page內容:

kd> !process 0 0...PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...kd> !dd 24231000#24231000 24766067 245e0067 00000000 00000000...kd> !dd 245e0000#245e0000 24430025 2456c025 2436d025 243ee025...kd> !db 2456c000#2456c000 b9 a0 c1 42 00 e8 24 2b-00 00 68 29 b5 41 00 e8#2456c010 d2 25 00 00 59 c3 68 33-b5 41 00 e8 c6 25 00 00#2456c020 59 c3 68 3d b5 41 00 e8-ba 25 00 00 59 c3 6a 01#2456c030 6a 00 68 a8 c2 42 00 b9-00 c3 42 00 e8 82 33 00#2456c040 00 68 47 b5 41 00 e8 9b-25 00 00 59 c3 56 57 6a#2456c050 00 e8 ee b9 00 00 59 bf-a8 c2 42 00 8b f0 8b cf#2456c060 e8 ca 33 00 00 6a 00 56-8b cf c7 05 a8 c2 42 00#2456c070 18 ce 41 00 e8 98 37 00-00 68 51 b5 41 00 e8 63

或許有人會問這些數據是什麼?我們來看一張截圖,看完之後自然豁然開朗……!

所謂的頁內偏移就是以上數據頁位元組的位置,Page[0]=b9,Page[1]=a0……Page[15]=e8,偏移的範圍是0~4095(4K頁內)。

上文書說到:「令貴妃魏瓔珞為救五阿哥永琪……」。咦!好像是走錯片場了……,好吧,書接上文……

前面分析了PDE前512項,基本清楚了含蓋的內容,現在來分析PDE的後512項,我將test.exe的後512項PTE與taskmgr、cmd、System、OllyICE等4個進程的後512項PTE進行了對比,發現這些PTE只有2項不同,其它完全一致,共有414項非空PTE(含2項不同),按每項管理4M來計算(414-2)*4=1648M,可認為這些空間是所有進程共享的。

不同項和偏移如下:

kd> !process 0 0**** NT ACTIVE PROCESS DUMP ****PROCESS 8a0e59c8 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000 DirBase: 00039000 ObjectTable: e1000d10 HandleCount: 240. Image: System PROCESS 89be5020 SessionId: 0 Cid: 05d8 Peb: 7ffd6000 ParentCid: 05e4 DirBase: 19f3c000 ObjectTable: e1e27cc8 HandleCount: 35. Image: cmd.exe PROCESS 89ba8318 SessionId: 0 Cid: 0578 Peb: 7ffde000 ParentCid: 0268 DirBase: 19f97000 ObjectTable: e1b8ef08 HandleCount: 82. Image: taskmgr.exe PROCESS 89f52c10 SessionId: 0 Cid: 07c0 Peb: 7ffde000 ParentCid: 05e4 DirBase: 1d42f000 ObjectTable: e1ba5700 HandleCount: 77. Image: OllyICE.exe PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...kd> !dd 00039000+c00 l2# 39c00 00039067 0a640063kd> !dd 19f3c000+c00 l2#19f3cc00 19f3c063 19ffd063kd> !dd 19f97000+c00 l2#19f97c00 19f97063 19f58063kd> !dd 1d42f000+c00 l2#1d42fc00 1d42f063 1d5f0063kd> !dd 24231000+c00 l2#24231c00 24231063 244b2063

從數據上看不同的第一個PTE基址域都指向了自己,而屬性域指明了該PTE歸內核態空間所有,第二個不同PTE沒有深挖。如有知者請告知。將這個位置轉換成邏輯地址是0xC0000000,可以想見PTE是由內核態空間保存的。

現在來說下test.exe向0地址寫入數據的方法,首先將0x00000000邏輯地址分解成三段式結構,通過所得結果找到對應頁一探究竟。

kd> !process 0 0...PROCESS 89bfc470 SessionId: 0 Cid: 01b8 Peb: 7ffde000 ParentCid: 05d8 DirBase: 24231000 ObjectTable: e1d764b8 HandleCount: 7. Image: test.exe...// 修改之前kd> !dd 24231000 #24231000 24766067 245e0067 00000000 00000000...// 原因是沒有分配頁kd> !dd 24766000#24766000 00000000 00000000 00000000 00000000...// 修改kd> !ed 24766000 29e3c067 // 修改之後kd> !dd 24766000#24766000 29e3c067 00000000 00000000 00000000

或許有人會問「29e3c067」是哪裡來的,《OS 學習筆記》的作者是將主棧的描述掛到此處,但我認為主棧涉及局部變數、調用參數和返回地址等,修改可能造成程序報錯,因此我的方法是用OD的插件分配一頁堆空間,並將堆邏輯地址解析成三段式找到對應的Page描述將其掛到此處,「29e3c067」就是該堆頁的描述。

總結:

test.exe分析結果統計,1個PDE,5個PTE,佔6個4K物理頁,後512項那些PTE或複製或映射,總之這些都不屬於當前進程,5個PTE所描述的是20M虛擬空間,程序僅用4M,20M是5120頁,4M是1024頁,急需運行的193頁放入物理內存。

原文作者: khristian (看雪ID)

原文鏈接:bbs.pediy.com/thread-24

轉載請註明:轉自看雪論壇

看雪推薦:

1、[原創] PEDIY-JD CTF 2018 Windows CrackMe 題目及設計思路

2、[原創]覺醒之戰Ⅰ:洞察HW程序員的腦洞

3、[原創]幾種常見的注入姿勢

4、[翻譯]StaDynA:解決Android APP安全分析中的動態代碼更新問題

5、[原創]新手——win32程序的半生(CreateProcess)

推薦閱讀:

命令行中四個常見命令的使用
我們的國產操作系統在哪裡 你知道的這些操作系統其實都是國外的
不用虛擬機,直接在Windows上安裝Linux
計算機論文精選-20180630
為什麼 KaiOS 能超越 iOS 成為印度第二大移動操作系統?

TAG:x86 | MicrosoftWindows | 操作系統 |