windows記憶體結構概述(轉)
13.1 Windows的虛擬地址空間安排
13.1.1虛擬地址空間的分割槽(即虛擬地址空間佈局)
程序的地址空間劃分
分割槽 |
x86 32位 Windows |
3GB使用者模式下的x86 32位Windows |
X64 64位 Windows |
IA-64 64位 Windows |
空指標賦值區 |
0x0000 0000 0x0000 FFFF |
0x0000 0000 0x0000 FFFF |
0x00000000 00000000 0x00000000 0000FFFF |
0x00000000 00000000 0x00000000 0000FFFF |
使用者模式分割槽 |
0x0001 0000 0x7FFE FFFF |
0x0001 0000 0xBFFE FFFF |
0x00000000 00010000 0x000007FF FFFEFFFF |
0x00000000 00010000 0x000006FB FFFEFFFF |
64KB禁入分割槽 |
0x7FFF 0000 0x7FFF FFFF |
0xBFFF 0000 0xBFFF FFFF |
0x000007FF FFFF0000 0x000007FF FFFFFFFF |
0x000006FB FFFF0000 0x000006FB FFFFFFFF |
核心模式 |
0x8000 0000 0xFFFF FFFF |
0xC000 0000 0xFFFF FFFF |
0x00000800 00000000 0xFFFFFFFF FFFFFFFF |
0x000006FC 00000000 0xFFFFFFFF FFFFFFFF |
(1)空指標賦值分割槽
①為幫助程式設計師捕獲對空指標的賦值,當執行緒試圖讀取或寫入這一分割槽的記憶體地址,就會引發訪問違規
②沒有任何辦法可以讓我們分配到位於這一地址區間的虛擬記憶體。
(2)使用者模式分割槽
①程序地址空間的駐地。對於應用程式來說,大部分資料都儲存在這一分割槽。
②32位下,預設為2GB大小。開啟/3GB開關時,可擴大到3GB空間,但同時核心空間縮小為1GB)
【x86 Windows下獲得更大的使用者模式分割槽】——修改Windows啟動配置資料(Boot Configuration Data,BCD)
①執行BCDEdit.exe
②bcdedit /set IncreaseUserVa 3072,就可以為程序保留3GB使用者模式地址空間,IncreaseUserVa可接受的最小值為2048,即預設的2GB。取消的話:bcdedit /deletevalue IncreaseUserVa。
③為了讓應用程式可以訪問2GB以上的地址空間(特別地,早期的應用程式是不允許這樣做的)。在連結時,可以開啟/LARGEADDRESSAWARE連結開關。
【在64位Windows下得到2GB使用者模式分割槽】將32位應用程式移植到64位環境下
①因大量使用32位指標開發程式,僅重新編譯程式會導致指標截斷錯誤和不正確的記憶體訪問。但可以讓應用程式在地址空間沙箱(Address space sandbox)中執行,這也是預設的情況,系統能夠保證高33位都為0的64地址截斷為32位,這樣程序可用的地址空間就被限制在最底部的2GB中。
②當執行64位應用程式時,預設下系統會保留使用者模式地址空間中在2GB以下(即最底部的2GB),這就是所謂的地址空間沙箱。這空間對於大多數的應用程式來說是足夠的。
③為了讓64位應用程式能夠訪問整個使用者地址空間,必須指定/LARGEADDRESSAWARE連結器開關來連結應用程式。
(3)核心模式分割槽
作業系統程式碼的駐地。與執行緒排程、記憶體管理 、檔案系統支援、網路支援以及裝置驅動程式相關的程式碼都載入到這個分割槽中。該分割槽中的所有程式碼和資料都為所有程序共有,但這些程式碼和資料都是被保護起來的,如果試圖在這分割槽的某個記憶體地址讀取或寫入資料時,會引發訪問違規。
13.1.2 Windows記憶體安排(時間上的安排)
(1)每個應用程式都有自己的4GB定址空間。該空間可存放作業系統、系統DLL和使用者DLL程式碼,它們之中有各種函式供應用程式呼叫。再除去其他的一些空間,餘下的是應用程式的程式碼、資料和可以分配的地址空間。
(2)不同應用程式的線性地址空間是隔離的。雖然它們在實體記憶體中同時存在,但在某個程式所屬的時間片中,其他應用程式的程式碼和資料沒有被對映到可定址的線性地址中,所以是不可訪問的。從程式設計的角度看,程式可供使用的4GB的定址空間,而且這個空間是“私有的”
(3)DLL程式沒有自己的“私有”的空間。它們總是被對映到其他應用程式的地址空間中,當做其他應用程式的一部分執行。原因很簡單,如果它不和其他程式同屬一個地址空間,應用程式就不能呼叫它。
(4)作業系統和系統DLL的程式碼需要供每個應用程式呼叫,所以在所有的時間片中都必須被對映;
(5)使用者程式只在自己所屬的時間片內被對映。使用者DLL則有選擇地被對映。如程式B和C都呼叫了xxx.dll,那麼實體記憶體中xxx.dll(注意在記憶體中已經存在了!)的程式碼在圖中的時間片2和n中被對映,其他時間片就不需要被對映。(當然實體記憶體中只需要一份xxx.dll的程式碼)。
13.2 地址空間中的區域
(1)預定地址空間中的一塊區域(預訂:VirtualAlloc、釋放:VirtualFree)
①起始地址:分配粒度(一般是64K)的整數倍。(注意:分配粒度與CPU平臺有關,同時系統自己預訂的區域的起始地址不一定非得是64KB的整數倍,如系統為程序環境塊(PEB)和執行緒環境塊(TEB)預定的區域地址就可能不是64KB的整數倍,但區域的大小仍是系統頁面大小的整數倍。應用程式自己預訂的區域,)
②預定空間的區域的大小:系統頁面大小的整數倍(x86和x64的頁面大小為4KB,I64系統使用的頁面大小為8KB)
(2)將預訂區域提交物理儲存器
①提交時,可以只提交區域的一部分。如預訂64KB空間大小,但可以只提交第2、第4兩個頁面(同樣是呼叫VirtualAlloc函式,但傳入的是MEM_COMMIT型別的引數)。
②撤消提交:VirtualFree,並傳入MEM_DECOMMIT
13.3 物理儲存器和頁交換檔案
(1)虛擬記憶體的實現:當應用程式呼叫VirtualAlloc函式將預訂的空間區域提交物理儲存器(實體記憶體或頁交換檔案)時,該空間實際上仍然不是從實體記憶體而是頁交換檔案中分配得到的,以後當訪問該空間時,會因資料並不存在於實體記憶體而發生訪問“頁面錯誤”,從而引發作業系統利用異常處理機制將虛擬地址空間真正對映到對應的實體記憶體中,如下圖所示。
(2)記憶體對映檔案:把硬碟上的檔案映像(如一個.exe或DLL檔案)作為虛擬記憶體的一部分(注意是檔案對映,而不是頁交換檔案)。當用戶要執行一個可執行檔案時,系統會開啟應用程式對應的.exe檔案並計算出應用程式的程式碼和資料的大小。然後預訂一塊地址空間,並註明與該區域相關的儲存場所是.exe檔案本身,而不是頁交換檔案。這樣做可以將.exe的實際內容用作程式預訂的地址空間區域,不僅載入程式速度快,而且可避免將為每個程式檔案的程式碼和資料複製到頁交換檔案而造成頁交換檔案過於龐大和臃腫。
13.4 頁面保護屬性
保護屬性 |
描述 |
PAGE_NOACCESS |
不可訪問。試圖讀取、寫入或執行頁面中的資料(程式碼)時將引發訪問違規。 |
PAGE_READONLY |
只讀。試圖寫入頁面或執行頁面中的程式碼將引發訪問違規 |
PAGE_READWRITE |
讀寫屬性。試圖執行頁面中的程式碼將引發訪問違規 。 |
PAGE_EXECUTE |
可執行屬性。試圖讀取或寫入頁面將引發訪問違規。 |
PAGE_EXECUTE_READ |
可讀、可執行。讀圖寫入頁面將引發訪問違規。 |
PAGE_EXECUTE_READWRITE |
可讀可寫可執行。對頁面的任何操作都不會引發訪問違規 |
PAGE_WRITECOPY |
①寫時複製。試圖執行頁面中的程式碼將引發訪問違規。 ②試圖寫入頁面將使系統為程序單獨建立一份該頁面私有副本(以頁交換檔案為後備儲存器) |
PAGE_EXECUTE_WRITECOPY |
對頁面執行任何操作都不會引發訪問違規。試圖寫入頁面將使系統為程序單獨建立一份該頁面私有副本(以頁交換檔案為後備儲存器) |
★注意:如果Windows啟用了資料執行保護(Data Execution Protection,DEP),當CPU試圖執行某個頁面中的程式碼,而該頁面又沒有PAGE_EXECUTE_*保護屬性,那麼CPU會丟擲訪問違規異常。(DEP開啟方法:我的電腦→右鍵“屬性”→高階系統設定→效能→設定→資料執行保護,選中“僅為基本Windows程式和服務啟用DEP”)
13.4.1 寫時複製
(1)寫時複製屬性的作用:節省記憶體和頁交換檔案的使用
Windows提供一種機制,允許兩個或兩個以上的程序共享一塊儲存器。如10個記事本程序正在執行,所有的程序會共享應用程式的內碼表和資料頁。當只讀或執行時,這種共享儲存頁的方式極大地提高了效能。但當某個例項寫入一個儲存頁時,就要求給共享的儲存頁指定寫時複製屬性,這樣在對映地址空間時,系統會計算有多少可寫頁面,然後從頁交換檔案中分配空間來容納這些可寫頁面,在程式真正寫入的時候,就儲存在頁交換檔案中。
(2)寫入共享頁面時,系統介入的操作
①系統在記憶體中找到一個空閒頁面。注意,該空閒頁的後備頁面來自頁交換檔案。它是系統最初將模組對映到程序的地址空間時分配的。由於是第1次對映時就分配了所需的頁交換檔案空間。所以這步不可能失敗。
②系統將要修改的頁面內容複製到第1步找到的空閒頁面,然後給這些空閒頁面指定PAGE_READWRITE或PAGE_EXECUTE_READWRITE屬性。(注意系統不會修改原始頁面的保護屬性和資料)
③然後系統更新程序的頁面表,這樣,原來的虛擬地址現在就對應到記憶體中一個新的頁面了。以後程序就可以訪問它自己的副本了。
(3)在預訂地址空間或提交物理儲存器時,不能使用PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY保護屬性,否則VirtualAlloc會失敗,GetLastError將返回ERROR_INVALID_PARAMETER。
13.4.2 一些特殊的訪問保護屬性標誌
保護屬性 |
描述 |
PAGE_NOCACHE |
禁止對己提交的頁面進行快取。該標誌的目的是為了讓需要操控記憶體緩衝區的驅動程式開發人員使用。一般不建議用將這標誌用於除此以外的其他用途。 |
PAGE_WRITECOMBINE |
允許把單個裝置的多次寫操作組合在一起,以提高效能。也是給驅動程式開發人員用的。 |
PAGE_GUARD |
使應用程式能夠在頁面中的任何一個位元組被寫入時得到通知。 |
13.5 例項分析
13.5.1 各區域分析
……
……
(1)基地址:
①從0x0000 0000開始,到0x7FFE 0000+ FFFF結束。
②幾乎所有的非空閒區域的基地址都是64KB的整數倍(這是由系統地址空間的分配粒度決定的)。如果不是64KB的整數倍,這意味著該區域是由作業系統以程序名義分配的。
(2)區域型別
型別 |
描述 |
Free(空閒) |
區域的虛擬地址沒有任何後備儲存器。該地址空間尚未預訂,應用程式可以從基地址開始預訂,也可以從空閒區域內的任何地方開始預訂區域 |
Private(私有) |
區域的虛擬地址以系統的頁交換檔案為後備儲存器 |
Image(映像) |
一開始以映像檔案(如exe或DLL)為後備儲存器,但以後不一定以映像檔案為後備儲存器(如程式寫入映像檔案中一個全域性變數,那麼寫時複製會改用頁交換檔案來作為後備儲存器)(對映檔案可理解為exe或dll檔案) |
Mapped(己對映) |
一開始以記憶體對映檔案為後備儲存器,此後不一定以記憶體映像檔案為後備儲存器。(如記憶體對映檔案可能會使用寫時複製保護屬性。任何寫操作會使對應的頁面改用頁交換檔案來作為後備儲存器) |
★注意:對於每個區域整體而言,該區域的型別是推測出來的(除空閒外),詳細見13.5.2節《區域內部》的內容。
(3)區域預訂的位元組數
①始終是CPU頁面大小的整數倍(對於x86為4位元組,即4096的倍數)
②為了節省磁碟空間,連結器會盡可能對對PE檔案進行壓縮,所以磁碟上的檔案大小與對映到記憶體所需要的位元組數是有差異的。
(4)預訂區域內部的塊的數量(block)
①塊是一些連續的頁面,這些頁面具有相同的保護屬性,並以相同型別的物理儲存器為後備儲存器。對閒置頁面來說,由於不可能將儲存器撥給他們,該值始終為0。
②每個區域最大能容納的塊的數量為:區域大小/頁面大小,即當每個頁面都是一個不同的塊時,這裡塊的數量最多。
(5)區域的保護屬性:
①E=execute,R=read,W=Write,C=copy on write。如果區域沒有顯示任保護屬性,表示該區域沒有任何訪問保護。閒置區域沒有與之相關聯的保護屬性。
②PAGE_GAUARD和PAGE_ONCACHE標誌對地址空間沒有意義,這些標誌只有當用於物理儲存時才有意義。
③如果同時給區域和物理儲存器指定了保護屬性,那麼以後者為準。(見區域內部一節的分析)
13.5.2 區域內部——以0x767F000所在區域為例(本例中用來裝載User32.dll的區域)
基地址 |
型別 |
大小 |
塊數 |
保護屬性 |
描述 |
… |
… |
… |
… |
… |
… |
767F0000 767F0000 767F1000 7685A000 7685B000 7685C000 |
映像 映像 映像 對映 映像 映像 |
647168 4096 430080 4096 4096 40960 |
5 |
ERWC -R—(只讀) ER—(可執行,可讀) -RW—(可讀可寫) -RWC- -R-— |
C:\Windows\system32\USER32.dll //提交了105個頁面(430080/4096) |
7688E0000 |
空閒 |
8192 |
|||
… |
… |
… |
… |
… |
… |
(1)第1列顯示的是具有相同狀態和保護屬性的一組頁面的地址。如第1組只讀,第2組可執行可讀,第3組可讀可寫。
(2)第2列塊的型別,即以何種型別的物理儲存器為後備儲存器。Private、Mapped、Image分別表示以頁交換檔案、記憶體對映檔案和載入的Exe(或Dll)檔案為後備儲存器。但Free和Reserved表示該塊沒有後備物理儲存器。
(3)第3列:塊的大小。一個區域中所有的塊都是連續的,不會存在任何的間隙。
(4)第4列:所預訂區域內部中塊的數量
(5)第5列:塊的頁保護屬性:一個塊的保護屬性會優先於所屬區域的保護屬性。(注意:PAGE_GUARD、PAGE_NOCACHE、PAGE_WRITECOMBINE保護屬性只能用於塊(即物理儲存器,不能用於區域)。(注意:區域可以理解為預訂的地址空間,塊可以理解為在這個預訂的地址空間中進一步細分出來的更小的一片地址空間)。
13.6 資料對齊的重要性
(1)資料對齊:將資料的地址 % 資料大小 = 0時的資料是對齊的。
(2)x86CPU對錯位資料的處理
①EFLAGS暫存器的AC標誌位(AlignmentCheck)為0時,CPU自動執行必要的操作來訪問錯位資料)
②AC標誌位為1時,如果試圖訪問錯位資料,CPU會觸發INT 17H中斷。(對於x86版本的Windows從來不變為AC標誌位(即永遠為0),因此x86處理器上執行應用程式,絕對不會發生資料錯位的異常,但IA-64CPU處理器不能自己處理資料錯誤的錯誤,因此當訪問錯位資料時,會丟擲一個EXECPTION_DATATYPE_MISALIGNMENT異常,我們通用SetErrorMode函式並傳為SEM_NOALIGNMENTFAULTEXCEPT標誌,讓系統自動修正資料錯位的錯誤。(注意傳入這個標誌會影響程序中所有的執行緒,而且這個錯誤模式會被程序的子程序繼承)
(3)編譯器對錯位資料的處理
①IA-64版本的VC/C++編譯器支援__unaligned關鍵字
如DWORD dw = *(__unaligned DWORD*)pvDataBuffer;
②x86版本的VC/C++編譯器:不支援__nnaligned關鍵字,所以這個關鍵字在x86版本的編譯器下會報錯。
③鑑於編譯器對__unaligned有不同的支援,為程式碼的通用性,建議用UNALIGNED和UNLIGNED64巨集來替換__unaligned。
#if defined(_M_MRX000) || defined(_M_ALPHA) || defined(_M_PPC) ||defined(_M_IA64) || defined(_M_AMD64)
#define ALIGNMENT_MACHINE
#define UNALIGNED __unaligned
#if defined(_WIN64)
#define UNALIGNED64 __unaligned
#else
#define UNALIGNED64
#endif
#else
#undef ALIGNMENT_MACHINE
#define UNALIGNED
#define UNALIGNED64
#endif
【AlignOf程式】記憶體對齊演示程式
#include <windows.h>
#include <tchar.h>
#include <locale.h>
//在MSVC中,一般使用#progma pack來指定記憶體對齊:
#pragma pack(show) //以警告資訊的形式顯示當前位元組對齊的值(在編譯輸出框顯示)
//預設的8位元組對齊
struct BYTE1{
char ch1;
int i1;
};
#pragma pack(push)
#pragma pack(1)
#pragma pack(show)
struct BYTE2{
char ch2;
int i2;
};
#pragma pack(pop)
//微軟的__declspec(align(#)),其#的內容可以是預編譯巨集,但不能是編譯期數值
struct __declspec(align(1)) BYTE3{
char ch3;
int i3;
};
VOID AlignTest(PVOID pvDataBuffer){
char *pc = (PCHAR)pvDataBuffer;
pc++; //指向第2個位元組
//未對齊方式訪問:將第2-5個位元組當成DWORD來看待,此時記憶體沒對齊,
//因為DWORD的起始地址而是4的倍數
DWORD dwUnAligned = *(DWORD*)(pc);
_tprintf(_T("dwUnAligned=0x%08X\n"), dwUnAligned);
//用對齊方式訪問,效率更高
DWORD dwAligned = *(UNALIGNED DWORD*)pc;//*(DWORD*)pc;
_tprintf(_T("dwAligned =0x%08X\n"), dwAligned);
}
int _tmain(){
char c[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
AlignTest((PVOID)c);
//記憶體對齊
size_t sz1 = sizeof(BYTE1);
size_t sz2 = sizeof(BYTE2);
size_t sz3 = sizeof(BYTE2);
_tprintf(_T("sizeof(BYTE1)==%d\n"), sz1);
_tprintf(_T("sizeof(BYTE2)==%d\n"), sz2);
_tprintf(_T("sizeof(BYTE3)==%d\n"), sz3);
//MSVC使用__alignof獲得結構體中最大成員變數的對齊大小,即結構體的對齊大小
sz1 = __alignof(BYTE1); //最大成員為i1,對齊大小應為4
sz2 = __alignof(BYTE2); //最大成員為ch2,對齊大小應為1
sz3 = __alignof(BYTE3); //最大成員為i3,對齊大小應為4
_tprintf(_T("__alignof(BYTE1)==%d\n"), sz1);
_tprintf(_T("__alignof(BYTE2)==%d\n"), sz2);
_tprintf(_T("__alignof(BYTE3)==%d\n"), sz3);
return 0;