windows進程中的記憶體結構

來源:互聯網
上載者:User

在閱讀本文之前,如果你連堆棧是什麼多不知道的話,請先閱讀文章後面的基礎知識。 

接觸過編程的人都知道,進階語言都能通過變數名來訪問記憶體中的資料。那麼這些變數在記憶體中是如何存放的呢?程式又是如何使用這些變數的呢?下面就會對此進行深入的討論。下文中的C語言代碼如沒有特別聲明,預設都使用VC編譯的release版。 

首先,來瞭解一下 C 語言的變數是如何在記憶體分部的。C 語言有全域變數(Global)、本地變數(Local),靜態變數(Static)、寄存器變數(Regeister)。每種變數都有不同的分配方式。先來看下面這段代碼: 

#include <stdio.h> 

int g1=0, g2=0, g3=0; 

int main() 

static int s1=0, s2=0, s3=0; 
int v1=0, v2=0, v3=0; 

//列印出各個變數的記憶體位址 

printf("0x%08x"n",&v1); //列印各本地變數的記憶體位址 
printf("0x%08x"n",&v2); 
printf("0x%08x"n"n",&v3); 
printf("0x%08x"n",&g1); //列印各全域變數的記憶體位址 
printf("0x%08x"n",&g2); 
printf("0x%08x"n"n",&g3); 
printf("0x%08x"n",&s1); //列印各靜態變數的記憶體位址 
printf("0x%08x"n",&s2); 
printf("0x%08x"n"n",&s3); 
return 0; 

編譯後的執行結果是: 

0x0012ff78 
0x0012ff7c 
0x0012ff80 

0x004068d0 
0x004068d4 
0x004068d8 

0x004068dc 
0x004068e0 
0x004068e4 

輸 出的結果就是變數的記憶體位址。其中v1,v2,v3是本地變數,g1,g2,g3是全域變數,s1,s2,s3是靜態變數。你可以看到這些變數在記憶體是連 續分布的,但是本地變數和全域變數分配的記憶體位址差了十萬八千裡,而全域變數和靜態變數分配的記憶體是連續的。這是因為本地變數和全域/靜態變數是分配在不 同類型的記憶體地區中的結果。對於一個進程的記憶體空間而言,可以在邏輯上分成3個部份:代碼區,待用資料區和動態資料區。動態資料區一般就是“堆棧”。“棧 (stack)”和“堆(heap)”是兩種不同的動態資料區,棧是一種線性結構,堆是一種鏈式結構。進程的每個線程都有私人的“棧”,所以每個線程雖然 代碼一樣,但本地變數的資料都是互不干擾。一個堆棧可以通過“基地址”和“棧頂”地址來描述。全域變數和靜態變數分配在待用資料區,本地變數分配在動態數 據區,即堆棧中。程式通過堆棧的基地址和位移量來訪問本地變數。 

├———————┤低端記憶體地區 
│ …… │ 
├———————┤ 
│ 動態資料區 │ 
├———————┤ 
│ …… │ 
├———————┤ 
│ 代碼區 │ 
├———————┤ 
│ 待用資料區 │ 
├———————┤ 
│ …… │ 
├———————┤高端記憶體地區 

堆 棧是一個先進後出的資料結構,棧頂地址總是小於等於棧的基地址。我們可以先瞭解一下函數調用的過程,以便對堆棧在程式中的作用有更深入的瞭解。不同的語言 有不同的函數調用規定,這些因素有參數的壓入規則和堆棧的平衡。windows API的調用規則和ANSI C的函數調用規則是不一樣的,前者由被調函 數調整堆棧,後者由調用者調整堆棧。兩者通過“__stdcall”和“__cdecl”首碼區分。先看下面這段代碼: 

#include <stdio.h> 

void __stdcall func(int param1,int param2,int param3) 

int var1=param1; 
int var2=param2; 
int var3=param3; 
printf("0x%08x"n",¶m1); //列印出各個變數的記憶體位址 
printf("0x%08x"n",¶m2); 
printf("0x%08x"n"n",¶m3); 
printf("0x%08x"n",&var1); 
printf("0x%08x"n",&var2); 
printf("0x%08x"n"n",&var3); 
return; 

int main() 

func(1,2,3); 
return 0; 

編譯後的執行結果是: 

0x0012ff78 
0x0012ff7c 
0x0012ff80 

0x0012ff68 
0x0012ff6c 
0x0012ff70 

├———————┤<—函數執行時的棧頂(ESP)、低端記憶體地區 
│ …… │ 
├———————┤ 
│ var 1 │ 
├———————┤ 
│ var 2 │ 
├———————┤ 
│ var 3 │ 
├———————┤ 
│ RET │ 
├———————┤<—“__cdecl”函數返回後的棧頂(ESP) 
│ parameter 1 │ 
├———————┤ 
│ parameter 2 │ 
├———————┤ 
│ parameter 3 │ 
├———————┤<—“__stdcall”函數返回後的棧頂(ESP) 
│ …… │ 
├———————┤<—棧底(基地址 EBP)、高端記憶體地區 

上 圖就是函數調用過程中堆棧的樣子了。首先,三個參數以從又到左的次序壓入堆棧,先壓“param3”,再壓“param2”,最後壓入“param1”; 然後壓入函數的返回地址(RET),接著跳轉到函數地址接著執行(這裡要補充一點,介紹UNIX下的緩衝溢出原理的文章中都提到在壓入RET後,繼續壓入 當前EBP,然後用當前ESP代替EBP。然而,有一篇介紹windows下函數調用的文章中說,在windows下的函數調用也有這一步驟,但根據我的 實際調試,並未發現這一步,這還可以從param3和var1之間只有4位元組的間隙這點看出來);第三步,將棧頂(ESP)減去一個數,為本地變數分配內 存空間,上例中是減去12位元組(ESP=ESP-3*4,每個int變數佔用4個位元組);接著就初始化本地變數的記憶體空間。由於“__stdcall”調 用由被調函數調整堆棧,所以在函數返回前要恢複堆棧,先回收本地變數佔用的記憶體(ESP=ESP+3*4),然後取出返回地址,填入EIP寄存器,回收先 前壓入參數佔用的記憶體(ESP=ESP+3*4),繼續執行調用者的代碼。參見下列彙編代碼: 

;--------------func 函數的彙編代碼------------------- 

:00401000 83EC0C sub esp, 0000000C //建立本地變數的記憶體空間 
:00401003 8B442410 mov eax, dword ptr [esp+10] 
:00401007 8B4C2414 mov ecx, dword ptr [esp+14] 
:0040100B 8B542418 mov edx, dword ptr [esp+18] 
:0040100F 89442400 mov dword ptr [esp], eax 
:00401013 8D442410 lea eax, dword ptr [esp+10] 
:00401017 894C2404 mov dword ptr [esp+04], ecx 

……………………(省略若干代碼) 

:00401075 83C43C add esp, 0000003C ;恢複堆棧,回收本地變數的記憶體空間 
:00401078 C3 ret 000C ;函數返回,恢複參數佔用的記憶體空間 
;如果是“__cdecl”的話,這裡是“ret”,堆棧將由調用者恢複 

;-------------------函數結束------------------------- 

;--------------主程式調用func函數的代碼-------------- 

:00401080 6A03 push 00000003 //壓入參數param3 
:00401082 6A02 push 00000002 //壓入參數param2 
:00401084 6A01 push 00000001 //壓入參數param1 
:00401086 E875FFFFFF call 00401000 //調用func函數 
;如果是“__cdecl”的話,將在這裡恢複堆棧,“add esp, 0000000C” 

聰明的讀者看到這裡,差不多就明白緩衝溢出的原理了。先來看下面的代碼: 

#include <stdio.h> 
#include <string.h> 

void __stdcall func() 

char lpBuff[8]=""0"; 
strcat(lpBuff,"AAAAAAAAAAA"); 
return; 

int main() 

func(); 
return 0; 

編 譯後執行一下回怎麼樣?哈,“"0x00414141"指令引用的"0x00000000"記憶體。該記憶體不能為"read"。”,“非法操作”嘍! "41"就是"A"的16進位的ASCII碼了,那明顯就是strcat這句出的問題了。"lpBuff"的大小隻有8位元組,算進結尾的"0,那 strcat最多隻能寫入7個"A",但程式實際寫入了11個"A"外加1個"0。再來看看上面那幅圖,多出來的4個位元組正好覆蓋了RET的所在的記憶體空 間,導致函數返回到一個錯誤的記憶體位址,執行了錯誤的指令。如果能精心構造這個字串,使它分成三部分,前一部份僅僅是填充的無意義資料以達到溢出的目 的,接著是一個覆蓋RET的資料,緊接著是一段shellcode,那隻要著個RET地址能指向這段shellcode的第一個指令,那函數返回時就能執 行shellcode了。但是軟體的不同版本和不同的運行環境都可能影響這段shellcode在記憶體中的位置,那麼要構造這個RET是十分困難的。一般 都在RET和shellcode之間填充大量的NOP指令,使得exploit有更強的通用性。 

├———————┤<—低端記憶體地區 
│ …… │ 
├———————┤<—由exploit填入資料的開始 
│ │ 
│ buffer │<—填入無用的資料 
│ │ 
├———————┤ 
│ RET │<—指向shellcode,或NOP指令的範圍 
├———————┤ 
│ NOP │ 
│ …… │<—填入的NOP指令,是RET可指向的範圍 
│ NOP │ 
├———————┤ 
│ │ 
│ shellcode │ 
│ │ 
├———————┤<—由exploit填入資料的結束 
│ …… │ 
├———————┤<—高端記憶體地區 

windows下的動態資料除了可存放在棧中,還可以存放在堆中。瞭解C++的朋友都知道,C++可以使用new關鍵字來動態分配記憶體。來看下面的C++代碼: 

#include <stdio.h> 
#include <iostream.h> 
#include <windows.h> 

void func() 

char *buffer=new char[128]; 
char bufflocal[128]; 
static char buffstatic[128]; 
printf("0x%08x"n",buffer); //列印堆中變數的記憶體位址 
printf("0x%08x"n",bufflocal); //列印本地變數的記憶體位址 
printf("0x%08x"n",buffstatic); //列印靜態變數的記憶體位址 

void main() 

func(); 
return; 

程式執行結果為: 

0x004107d0 
0x0012ff04 
0x004068c0 

可以發現用new關鍵字分配的記憶體即不在棧中,也不在待用資料區。VC編譯器是通過windows下的“堆(heap)”來實現new關鍵字的記憶體動態分配。在講“堆”之前,先來瞭解一下和“堆”有關的幾個API函數: 

HeapAlloc 在堆中申請記憶體空間 
HeapCreate 建立一個新的堆對象 
HeapDestroy 銷毀一個堆對象 
HeapFree 釋放申請的記憶體 
HeapWalk 枚舉堆對象的所有記憶體塊 
GetProcessHeap 取得進程的預設堆對象 
GetProcessHeaps 取得進程所有的堆對象 
LocalAlloc 
GlobalAlloc 

當進程初始化時,系統會自動為進程建立一個預設堆,這個堆預設所佔記憶體的大小為1M。堆對象由系統進行管理,它在記憶體中以鏈式結構存在。通過下面的代碼可以通過堆動態申請記憶體空間: 

HANDLE hHeap=GetProcessHeap(); 
char *buff=HeapAlloc(hHeap,0,8); 

其中hHeap是堆對象的控制代碼,buff是指向申請的記憶體空間的地址。那這個hHeap究竟是什麼呢?它的值有什麼意義嗎?看看下面這段代碼吧: 

#pragma comment(linker,"/entry:main") //定義程式的入口 
#include <windows.h> 

_CRTIMP int (__cdecl *printf)(const char *, ...); //定義STL函數printf 
/*--------------------------------------------------------------------------- 
寫到這裡,我們順便來複習一下前面所講的知識: 
(*注)printf函數是C語言的標準函數庫中函數,VC的標準函數庫由msvcrt.dll模組實現。 
由 函數定義可見,printf的參數個數是可變的,函數內部無法預Crowdsourced Security Testing道調用者壓入的參數個數,函數只能通過分析第一個參數字串的格式來獲得壓入參數的信 息,由於這裡參數的個數是動態,所以必須由調用者來平衡堆棧,這裡便使用了__cdecl調用規則。BTW,Windows系統的API函數基本上是 __stdcall調用形式,只有一個API例外,那就是wsprintf,它使用__cdecl調用規則,同printf函數一樣,這是由於它的參數個 數是可變的緣故。 
---------------------------------------------------------------------------*/ 
void main() 

HANDLE hHeap=GetProcessHeap(); 
char *buff=HeapAlloc(hHeap,0,0x10); 
char *buff2=HeapAlloc(hHeap,0,0x10); 
HMODULE hMsvcrt=LoadLibrary("msvcrt.dll"); 
printf=(void *)GetProcAddress(hMsvcrt,"printf"); 
printf("0x%08x"n",hHeap); 
printf("0x%08x"n",buff); 
printf("0x%08x"n"n",buff2); 

執行結果為: 

0x00130000 
0x00133100 
0x00133118 

hHeap 的值怎麼和那個buff的值那麼接近呢?其實hHeap這個控制代碼就是指向HEAP首部的地址。在進程的使用者區存著一個叫PEB(進程環境塊)的結構,這個 結構中存放著一些有關進程的重要訊息,其中在PEB首地址位移0x18處存放的ProcessHeap就是進程預設堆的地址,而位移0x90處存放了指向 進程所有堆的地址清單的指標。windows有很多API都使用進程的預設堆來存放動態資料,如windows 2000下的所有ANSI版本的函數都是 在預設堆中申請記憶體來轉換ANSI字串到Unicode字串的。對一個堆的訪問是順序進行的,同一時刻只能有一個線程訪問堆中的資料,當多個線程同時 有訪問要求時,只能排隊等待,這樣便造成程式執行效率下降。

 

最後來說說記憶體中的資料對齊。所位元據對齊,是指資料所在的記憶體位址必須是該 資料長度的整數倍,DWORD資料的記憶體起始地址能被4除盡,WORD資料的記憶體起始地址能被2除盡,x86 CPU能直接存取對齊的資料,當他試圖訪問 一個未對齊的資料時,會在內部進行一系列的調整,這些調整對於程式來說是透明的,但是會降低運行速度,所以編譯器在編譯器時會盡量保證資料對齊。同樣一 段代碼,我們來看看用VC、Dev-C++和lcc三個不同編譯器編譯出來的程式的執行結果: 

#include <stdio.h> 

int main() 

int a; 
char b; 
int c; 
printf("0x%08x"n",&a); 
printf("0x%08x"n",&b); 
printf("0x%08x"n",&c); 
return 0; 

這是用VC編譯後的執行結果: 
0x0012ff7c 
0x0012ff7b 
0x0012ff80 
變數在記憶體中的順序:b(1位元組)-a(4位元組)-c(4位元組)。 

這是用Dev-C++編譯後的執行結果: 
0x0022ff7c 
0x0022ff7b 
0x0022ff74 
變數在記憶體中的順序:c(4位元組)-中間相隔3位元組-b(佔1位元組)-a(4位元組)。 

這是用lcc編譯後的執行結果: 
0x0012ff6c 
0x0012ff6b 
0x0012ff64 
變數在記憶體中的順序:同上。 

三個編譯器都做到了資料對齊,但是後兩個編譯器顯然沒VC“聰明”,讓一個char佔了4位元組,浪費記憶體哦。 

基礎知識: 
堆 棧是一種簡單的資料結構,是一種只允許在其一端進行插入或刪除的線性表。允許插入或刪除操作的一端稱為棧頂,另一端稱為棧底,對堆棧的插入和刪除操作被稱 為入棧和出棧。有一組CPU指令可以實現對進程的記憶體實現堆棧訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。CPU的ESP寄存器存放 當前線程的棧頂指標,EBP寄存器中儲存當前線程的棧底指標。CPU的EIP寄存器存放下一個CPU指令存放的記憶體位址,當CPU執行完當前的指令後,從 EIP寄存器中讀取下一條指令的記憶體位址,然後繼續執行。 

參考:《Windows下的HEAP溢出及其利用》by: isno 
《windows核心編程》by: Jeffrey Richter 

摘要: 討論常見的堆效能問題以及如何防範它們。(共 9 頁)

前言
您 是否是動態分配的 C/C++ 對象忠實且幸運的使用者?您是否在模組間的往返通訊中頻繁地使用了“自動化”?您的程式是否因堆分配而運行起來很慢?不僅僅 您遇到這樣的問題。幾乎所有項目遲早都會遇到堆問題。大家都想說,“My Code真正好,只是堆太慢”。那隻是部分正確。更深入理解堆及其用法、以及會發生什 麼問題,是很有用的。

什麼是堆?
(如果您已經知道什麼是堆,可以跳到“什麼是常見的堆效能問題?”部分)

在程式中,使用堆來動態分配和釋放對象。在下列情況下,調用堆操作: 

事先不知道程式所需對象的數量和大小。

對象太大而不適合堆棧分配程式。
堆使用了在運行時分配給代碼和堆棧的記憶體之外的部分記憶體。給出了堆分配程式的不同層。

GlobalAlloc/GlobalFree:Microsoft Win32 堆調用,這些調用直接與每個進程的預設堆進行對話。

LocalAlloc/LocalFree:Win32 堆調用(為了與 Microsoft Windows NT 相容),這些調用直接與每個進程的預設堆進行對話。

COM 的 IMalloc 分配程式(或 CoTaskMemAlloc / CoTaskMemFree):函數使用每個進程的預設堆。自動化程式使用“元件物件模型 (COM)”的分配程式,而申請的程式使用每個進程堆。

C/C ++ 運行時 (CRT) 分配程式:提供了 malloc() 和 free() 以及 new 和 delete 操作符。如  Microsoft Visual Basic 和 Java 等語言也提供了新的操作符並使用垃圾收集來代替堆。CRT 建立自己的私人堆,駐留在  Win32 堆的頂部。

Windows NT 中,Win32 堆是 Windows NT 運行時分配程式周圍的薄層。所有 API 轉寄它們的請求給 NTDLL。

Windows NT 運行時分配程式提供 Windows NT 內的核心堆分配程式。它由具有 128 個大小從 8 到 1,024 位元組的空閑列表的前端分配程式組成。後端分配程式使用虛擬記憶體來保留和提交頁。

在圖表的底部是“虛擬記憶體分配程式”,作業系統使用它來保留和提交頁。所有分配程式使用虛擬記憶體進行資料的存取。

分配和釋放塊不就那麼簡單嗎?為何花費這麼長時間?

堆實現的注意事項
傳 統上,作業系統和執行階段程式庫是與堆的實現共存的。在一個進程的開始,作業系統建立一個預設堆,叫做“進程堆”。如果沒有其他堆可使用,則塊的分配使用“進程 堆”。語言運行時也能在進程內建立單獨的堆。(例如,C 運行時建立它自己的堆。)除這些專用的堆外,應用程式或許多已載入的動態連結程式庫 (DLL) 之 一可以建立和使用單獨的堆。Win32 提供一整套 API 來建立和使用私人堆。有關堆函數(英文)的詳盡指導,請參見 MSDN。

當應用程式或 DLL 建立私人堆時,這些堆存在於進程空間,並且在進程內是可訪問的。從給定堆分配的資料將在同一個堆上釋放。(不能從一個堆分配而在另一個堆釋放。)

在所有虛擬記憶體系統中,堆駐留在作業系統的“虛擬記憶體管理器”的頂部。語言運行時堆也駐留在虛擬記憶體頂部。某些情況下,這些堆是作業系統堆中的層,而語言運行時堆則通過大塊的分配來執行自己的記憶體管理。不使用作業系統堆,而使用虛擬記憶體函數更利於堆的分配和塊的使用。

典 型的堆實現由前、後端分配程式組成。前端分配程式維持固定大小塊的空閑列表。對於一次分配調用,堆嘗試從前端列表找到一個自由塊。如果失敗,堆被迫從後端 (保留和提交虛擬記憶體)分配一個大塊來滿足請求。通用的實現有每塊分配的開銷,這將耗費執行循環,也減少了可使用的儲存空間。

Knowledge Base  文章 Q10758,“用 calloc() 和 malloc() 管理記憶體” (搜尋文章編號), 包含了有關這些主題的更多背景知識。另外,有關堆 實現和設計的詳細討論也可在下列著作中找到:“Dynamic Storage Allocation:  A Survey and Critical Review”,作者 Paul R. Wilson、Mark S. Johnstone、 Michael Neely 和 David Boles; “International Workshop on Memory Management”, 作者 Kinross, Scotland, UK,  1995 年 9 月(http://www.cs.utexas.edu/users/oops/papers.html)(英文)。

Windows NT  的實現(Windows NT 版本 4.0 和更新版本) 使用了 127 個大小從 8 到 1,024 位元組的 8 位元組對齊塊空閑列表和一個“大 塊”列表。“大塊”列表(空閑列表[0]) 儲存大於 1,024 位元組的塊。空閑列表容納了用雙向鏈錶鏈接在一起的對象。預設情況下,“進程堆”執行收 集操作。(收集是將相鄰空閑塊合并成一個大塊的操作。)收集耗費了額外的周期,但減少了堆塊的內部片段。

單一全域鎖保護堆,防止多線程式的使用。(請參見“Server Performance and Scalability Killers”中的第一個注意事項, George Reilly 所著,在 “MSDN Online Web Workshop”上(網站:http://msdn.microsoft.com/workshop/server/iis/tencom.asp(英文)。)單一全域鎖本質上是用來保護堆資料結構,防止跨多線程的隨機存取。若堆操作太頻繁,單一全域鎖會對效能有不利的影響。

什麼是常見的堆效能問題?
以下是您使用堆時會遇到的最常見問題: 

分配操作造成的速度減慢。光分配就耗費很長時間。最可能導致運行速度減慢原因是空閑列表沒有塊,所以運行時分配程式碼會耗費周期尋找較大的空閑塊,或從後端分配程式分配新塊。

釋放操作造成的速度減慢。釋放操作耗費較多周期,主要是啟用了收集操作。收集期間,每個釋放操作“尋找”它的相鄰塊,取出它們並構造成較大塊,然後再把此較大塊插入空閑列表。在尋找期間,記憶體可能會隨機碰到,從而導致快取不能命中,效能降低。

堆 競爭造成的速度減慢。當兩個或多個線程同時訪問資料,而且一個線程繼續進行之前必須等待另一個線程完成時就發生競爭。競爭總是導致麻煩;這也是目前多處理 器系統遇到的最大問題。當大量使用記憶體塊的應用程式或 DLL 以多線程方式運行(或運行於多處理器系統上)時將導致速度減慢。單一鎖定的使用—常用的解 決方案—意味著使用堆的所有操作是序列化的。當等待鎖定時序列化會引起線程切換上下文。可以想象交叉路口閃爍的紅燈處走走停停導致的速度減慢。 
競爭通常會導致線程和進程的環境切換。環境切換的開銷是很大的,但開銷更大的是資料從處理器快取中丟失,以及後來線程複活時的資料重建。

堆 破壞造成的速度減慢。造成堆破壞的原因是應用程式對堆塊的不正確使用。通常情形包括釋放已釋放的堆塊或使用已釋放的堆塊,以及塊的越界重寫等明顯問題。 (破壞不在本文討論範圍之內。有關記憶體重寫和泄漏等其他細節,請參見 Microsoft Visual C++(R) 調試文檔 。)

頻繁的分配和重分配造成的速度減慢。這是使用指令碼語言時非常普遍的現象。如字串被反覆分配,隨重分配增長和釋放。不要這樣做,如果可能,盡量分配大字串和使用緩衝區。另一種方法就是盡量少用串連操作。
競爭是在分配和釋放操作中導致速度減慢的問題。理想情況下,希望使用沒有競爭和快速分配/釋放的堆。可惜,現在還沒有這樣的通用堆,也許將來會有。

在所有的伺服器系統中(如 IIS、MSProxy、DatabaseStacks、網路伺服器、 Exchange 和其他), 堆鎖定實在是個大瓶頸。處理器數越多,競爭就越會惡化。

盡量減少堆的使用
現在您明白使用堆時存在的問題了,難道您不想擁有能解決這些問題的超級魔棒嗎?我可希望有。但沒有魔法能使堆運行加快—因此不要期望在產品出貨之前的最後一星期能夠大為改觀。如果提前規劃堆策略,情況將會大大好轉。調整使用堆的方法,減少對堆的操作是提高效能的良方。

如何減少使用堆操作?通過利用資料結構內的位置可減少堆操作的次數。請考慮下列執行個體:

struct ObjectA {
   // objectA 的資料 
}

struct ObjectB {
   // objectB 的資料 
}

// 同時使用 objectA 和 objectB

//
// 使用指標 
//
struct ObjectB {
   struct ObjectA * pObjA;
   // objectB 的資料 
}

//
// 使用嵌入
//
struct ObjectB {
   struct ObjectA pObjA;
   // objectB 的資料 
}

//
// 集合 – 在另一對象內使用 objectA 和 objectB
//

struct ObjectX {
   struct ObjectA  objA;
   struct ObjectB  objB;
}

避免使用指標關聯兩個資料結構。如果使用指標關聯兩個資料結構,前面執行個體中的對象 A 和 B 將被分別分配和釋放。這會增加額外開銷—我們要避免這種做法。

把帶指標的子物件嵌入父物件。當對象中有指標時,則意味著對象中有動態元素(百分之八十)和沒有引用的新位置。嵌入增加了位置從而減少了進一步分配/釋放的需求。這將提高應用程式的效能。

合并小對象形成大對象(彙總)。彙總減少分配和釋放的塊的數量。如果有幾個開發人員,各自開發設計的不同部分,則最終會有許多小對象需要合并。整合的挑戰就是要找到正確的彙總邊界。

內 聯緩衝區能夠滿足百分之八十的需要(aka 80-20 規則)。個別情況下,需要記憶體緩衝區來儲存字串/位元據,但事先不知道總位元組數。估計並內 聯一個大小能滿足百分之八十需要的緩衝區。對剩餘的百分之二十,可以分配一個新的緩衝區和指向這個緩衝區的指標。這樣,就減少分配和釋放調用並增加資料的 位置空間,從根本上提高代碼的效能。

在塊中指派至(塊化)。塊化是以組的方式一次分配多個對象的方法。如果對列表的項連續跟蹤, 例如對一個 {名稱,值} 對的列表,有兩種選擇:選擇一是為每一個“名稱-值”對分配一個節點;選擇二是分配一個能容納(如五個)“名稱-值”對的結 構。例如,一般情況下,如果儲存四對,就可減少節點的數量,如果需要額外的空間數量,則使用附加的鏈表指標。 
塊化是友好的處理器快取,特別是對於 L1-快取,因為它提供了增加的位置 —不用說對於塊分配,很多資料區塊會在同一個虛擬頁中。

正確使用 _amblksiz。C 運行時 (CRT) 有它的自訂前端分配程式,該分配程式從後端(Win32 堆)分配大小為 _amblksiz 的塊。將 _amblksiz 設定為較高的值能潛在地減少對後端的調用次數。這隻對廣泛使用 CRT 的程式適用。
使用上述技術將獲得的好處會因物件類型、大小及工作量而有所不同。但總能在效能和可升縮性方面有所收穫。另一方面,代碼會有點特殊,但如果經過深思熟慮,代碼還是很容易管理的。

其他提高效能的技術
下面是一些提高速度的技術: 

使用 Windows NT5 堆 
由於幾個同事的努力和辛勤工作,1998 年初 Microsoft Windows(R) 2000 中有了幾個重大改進:

改進了堆代碼內的鎖定。堆代碼對每堆一個鎖。全域鎖保護堆資料結構,防止多線程式的使用。但不幸的是,在高通訊量的情況下,堆仍受困於全域鎖,導致高競爭和低效能。Windows 2000 中,鎖內代碼的臨界區將競爭的可能性減到最小,從而提高了延展性。

使 用 “Lookaside”列表。堆資料結構對塊的所有空閑項使用了大小在 8 到 1,024 位元組(以 8-位元組遞增)的快速快取。快速快取 最初保護在全域鎖內。現在,使用 lookaside 列表來訪問這些快速快取空閑列表。這些列表不要求鎖定,而是使用 64 位的互鎖操作,因此提 高了效能。

內部資料結構演算法也得到改進。
這些改進避免了對分配快取的需求,但不排除其他的最佳化。使用  Windows NT5 堆評估您的代碼;它對小於 1,024 位元組 (1 KB) 的塊(來自前端分配程式的塊)是最佳的。GlobalAlloc () 和 LocalAlloc() 建立在同一堆上,是存取每個進程堆的通用機制。如果希望獲得高的局部效能,則使用 Heap(R) API 來存取 每個進程堆,或為分配操作建立自己的堆。如果需要對大塊操作,也可以直接使用 VirtualAlloc() / VirtualFree() 操作。

上 述改進已在 Windows 2000 beta 2 和 Windows NT 4.0 SP4 中使用。改進後,堆鎖的競爭率顯著降低。這使所有  Win32 堆的直接使用者受益。CRT 堆建立於 Win32 堆的頂部,但它使用自己的小塊堆,因而不能從 Windows NT 改進中受益。 (Visual C++ 版本 6.0 也有改進的堆分配程式。)

使用分配快取 
分配快取允許快取分配的塊,以便將來重用。這能夠減少對進程堆(或全域堆)的分配/釋放調用的次數,也允許最大限度的重用曾經分配的塊。另外,分配快取允許收集統計資訊,以便較好地理解對象在較高層次上的使用。

典 型地,自訂堆分配程式在進程堆的頂部實現。自訂堆分配程式與系統堆的行為很相似。主要的差別是它在進程堆的頂部為分配的對象提供快取。快取設 計成一套固定大小(如 32 位元組、64 位元組、128 位元組等)。這一個很好的策略,但這種自訂堆分配程式丟失與分配和釋放的對象相關的“語義信 息”。 

與自訂堆分配程式相反,“分配快取”作為每類分配快取來實現。除能夠提供自訂堆分配程式的所有好處之外,它們還能夠保 留大量語義資訊。每個分配快取處理常式與一個目標二進位對象關聯。它能夠使用一套參數進行初始化,這些參數表示並發層級、對象大小和保持在空閑列表中 的元素的數量等。分配快取處理常式對象維持自己的私人空閑實體集區(不超過指定的閥值)並使用私人保護鎖。合在一起,分配快取和私人鎖減少了與主系 統堆的通訊量,因而提供了增加的並發、最大限度的重用和較高的延展性。

需要使用清理程式來定期檢查所有分配快取處理常式的活動情況並回收未用的資源。如果發現沒有活動,將釋放指派至的池,從而提高效能。

可以審核每個分配/釋放活動。第一級資訊包括對象、分配和釋放調用的總數。通過查看它們的統計資訊可以得出各個對象之間的語義關係。利用以上介紹的許多技術之一,這種關係可以用來減少記憶體配置。

分配快取也起到了調試助手的作用,協助您跟蹤沒有完全清除的對象數量。通過查看動態堆棧返回蹤跡和除沒有清除的對象之外的簽名,甚至能夠找到確切的失敗的調用者。

MP 堆 
MP  堆是對多處理器友好的分布式分配的程式包,在 Win32 SDK(Windows NT 4.0 和更新版本)中可以得到。最初由 JVert 實現, 此處堆抽象建立在 Win32 堆程式包的頂部。MP 堆建立多個 Win32 堆,並試圖將分配調用分布到不同堆,以減少在所有單一鎖上的競爭。

本 程式包是好的步驟 —一種改進的 MP-友好的自訂堆分配程式。但是,它不提供語義資訊和缺乏統計功能。通常將 MP 堆作為 SDK 庫來使用。如果 使用這個 SDK 建立可重用組件,您將大大受益。但是,如果在每個 DLL 中建立這個 SDK 庫,將增加工作設定。

重新思考演算法和資料結構 
要 在多處理器機器上伸縮,則演算法、實現、資料結構和硬體必須動態伸縮。請看最經常分配和釋放的資料結構。試問,“我能用不同的資料結構完成此工作嗎?”例 如,如果在應用程式初始化時載入了唯讀項的列表,這個列表不必是線性連結的列表。如果是動態分配的數組就非常好。動態分配的數組將減少記憶體中的堆塊和碎 片,從而增強效能。

減少需要的小對象的數量減少堆分配程式的負載。例如,我們在伺服器的關鍵處理路徑上使用五個不同的對象,每個對象單獨分配和釋放。一起快取這些對象,把堆調用從五個減少到一個,顯著減少了堆的負載,特別當每秒鐘處理 1,000 個以上的請求時。

如果大量使用“Automation”結構,請考慮從主線代碼中刪除“Automation BSTR”,或至少避免重複的 BSTR 操作。(BSTR 串連導致過多的重分配和分配/釋放操作。)

摘要
對所有平台往往都存在堆實現,因此有巨大的開銷。每個單獨代碼都有特定的要求,但設計能採用本文討論的基本理論來減少堆之間的相互作用。 

評價您的代碼中堆的使用。

改進您的代碼,以使用較少的堆調用:分析關鍵路徑和固定資料結構。

在實現自訂的封裝程式之前使用量化堆調用成本的方法。

如果對效能不滿意,請要求 OS 組改進堆。更多這類請求意味著對改進堆的更多關注。

要求 C 運行時組針對 OS 所提供的堆製作小巧的分配封裝程式。隨著 OS 堆的改進,C 運行時堆調用的成本將減小。

作業系統(Windows NT 家族)正在不斷改進堆。請隨時關注和利用這些改進。
Murali Krishnan  是 Internet Information Server (IIS) 組的首席軟體設計工程師。從 1.0 版本開始他就設計 IIS,並成功發行 了 1.0 版本到 4.0 版本。Murali 組織並領導 IIS 效能組三年 (1995-1998), 從一開始就影響 IIS 效能。他擁有威 斯康星州 Madison 大學的 M.S.和印度 Anna 大學的 B.S.。工作之外,他喜歡閱讀、打排球和家庭烹飪。

http://community.csdn.net/Expert/FAQ/FAQ_Index.asp?id=172835
我在學習對象的生存方式的時候見到一種是在堆棧(stack)之中,如下  
CObject  object;  
還有一種是在堆(heap)中  如下  
CObject*  pobject=new  CObject();  
 
請問  
(1)這兩種方式有什麼區別?  
(2)堆棧與堆有什麼區別??  
 
 
---------------------------------------------------------------  
 
1)  about  stack,  system  will  allocate  memory  to  the  instance  of  object  automatically,  and  to  the
 heap,  you  must  allocate  memory  to  the  instance  of  object  with  new  or  malloc  manually.  
2)  when  function  ends,  system  will  automatically  free  the  memory  area  of  stack,  but  to  the 
heap,  you  must  free  the  memory  area  manually  with  free  or  delete,  else  it  will  result  in  memory
leak.  
3)棧記憶體配置運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。  
4)堆上分配的記憶體可以有我們自己決定,使用非常靈活。  
---------------------------------------------------------------

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

Tags Index: