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; }