c++堆疊溢位的處理(包括遞迴)
本文背景:
在程式設計中,很多Windows或C++的記憶體函式不知道有什麼區別,更別談有效使用;根本的原因是,沒有清楚的理解作業系統的記憶體管理機制,本文企圖通過簡單的總結描述,結合例項來闡明這個機制。
本文目的:
對Windows記憶體管理機制瞭解清楚,有效的利用C++記憶體函式管理和使用記憶體。
- 6. 記憶體管理機制--堆疊 (Stack)
- 使用場合
作業系統為每個執行緒都建立一個預設堆疊,大小為1M。這個堆疊是供函式呼叫時使用,執行緒內函式裡的各種靜態變數都是從這個預設堆疊裡分配的。
- 堆疊結構
預設1M的執行緒堆疊空間的結構舉例如下,其中,基地址為0x0004 0000,剛開始時,CPU的堆疊指標暫存器儲存的是棧頂的第一個頁面地址0x0013 F000。第二頁面為保護頁面。這兩頁是已經分配物理儲存器的可用頁面。
隨著函式的呼叫,系統將需要更多的頁面,假設需要另外5頁,則給這5頁提交記憶體,刪除原來頁面的保護頁面屬性,最後一頁賦予保護頁面屬性。
當分配倒數第二頁0x0004 1000時,系統不再將保護屬性賦予它,相反,它會產生堆疊溢位異常STATUS_STACK_OVERFLOW,如果程式沒有處理它,則執行緒將退出。最後一頁始終處於保留狀態,也就是說可用堆疊數是沒有1M的,之所以不用,是防止執行緒破壞棧底下面的記憶體(通過違規訪問異常達到目的)。
當程式的函式裡分配了臨時變數時,編譯器把堆疊指標遞減相應的頁數目,堆疊指標始終都是一個頁面的整數倍。所以,當編譯器發現堆疊指標位於保護頁面之下時,會插入堆疊檢查函式,改變堆疊指標及保護頁面。這樣,當程式執行時,就會分配實體記憶體,而不會出現訪問違規。
- 使用例子
改變堆疊預設大小:
有兩個方法,一是在CreateThread()時傳一個引數進去改變;
二是通過連結命令:
#pragma comment(linker,"/STACK:102400000,1024000")
第一個值是堆疊的保留空間,第二個值是堆疊開始時提交的實體記憶體大小。本文將堆疊改變為100M。
堆疊溢位處理:
如果出現堆疊異常不處理,則導致執行緒終止;如果你只做了一般處理,內 存
結構已經處於破壞狀態,因為已經沒有保護頁面,系統沒有辦法再丟擲堆疊溢
出異常,這樣的話,當再次出現溢位時,會出現訪問違規操作
STATUS_ACCESS_VIOLATION,這是執行緒將被系統終止。解決辦法是,恢復
堆疊的保護頁面。請看以下例子:
C++程式如下:
bool handle=true;
static MEMORY_BASIC_INFORMATION mi;
LPBYTE lpPage;
//得到堆疊指標暫存器裡的值
_asm mov lpPage, esp;
// 得到當前堆疊的一些資訊
VirtualQuery(lpPage, &mi, sizeof(mi));
//輸出堆疊指標
printf("堆疊指標=%x/n",lpPage);
// 這裡是堆疊的提交大小
printf("已用堆疊大小=%d/n",mi.RegionSize);
printf("堆疊基址=%x/n",mi.AllocationBase);
for(int i=0;i<2;i++)
{
__try
{
__try
{
__try
{
cout<<"**************************"<<endl;
//如果是這樣靜態分配導致的堆疊異常,系統預設不丟擲異常,捕獲不到
//char a[1024*1024];
//動態分配棧空間,有系統呼叫Alloca實現,自動釋放
Add(1000);
//系統可以捕獲違規訪問
int * p=(int*)0xC00000000;
*p=3;
cout<<"執行結束"<<endl;
}
__except(GetExceptionCode()==STATUS_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
cout<<"Excpetion 1"<<endl;
}
}
__except(GetExceptionCode()==STATUS_STACK_OVERFLOW ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
cout<<"Exception 2"<<endl;
if(handle)
{
//做堆疊破壞狀態恢復
LPBYTE lpPage;
static SYSTEM_INFO si;
static MEMORY_BASIC_INFORMATION mi;
static DWORD dwOldProtect;
// 得到記憶體屬性
GetSystemInfo(&si);
// 得到堆疊指標
_asm mov lpPage, esp;
// 查詢堆疊資訊
VirtualQuery(lpPage, &mi, sizeof(mi));
printf("壞堆疊指標=%x/n",lpPage);
// 得到堆疊指標對應的下一頁基址
lpPage = (LPBYTE)(mi.BaseAddress)-si.dwPageSize;
printf("已用堆疊大小=%d/n",mi.RegionSize);
printf("壞堆疊基址=%x/n",mi.AllocationBase);
//釋放准保護頁面的下面所有記憶體
if (!VirtualFree(mi.AllocationBase,
(LPBYTE)lpPage - (LPBYTE)mi.AllocationBase,
MEM_DECOMMIT))
{
exit(1);
}
// 改頁面為保護頁面
if (!VirtualProtect(lpPage, si.dwPageSize,
PAGE_GUARD | PAGE_READWRITE,
&dwOldProtect))
{
exit(1);
}
}
printf("Exception handler %lX/n", _exception_code());
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
cout<<"Default handler"<<endl;
}
}
cout<<"正常執行"<<endl;
//分配空間,耗用堆疊
char c[1024*800];
printf("c[0]=%x/n",c);
printf("c[1024*800]=%x/n",&c[1024*800-1]);
}
void ThreadStack::Add(unsigned long a)
{
//深遞迴,耗堆疊
char b[1000];
if(a==0)
return;
Add(a-1);
}
程式執行結果如下:
可以看見,在執行遞迴前,堆疊已被用了800多K,這些是在編譯時就靜態決定了。它們不再佔用程序空間,因為堆疊佔用了預設的1M程序空間。分配是從棧頂到棧底的順序。
當第一次遞迴呼叫後,系統捕獲到了它的溢位異常,然後堆疊指標自動恢復到原來的指標值,並且在異常處理裡,更改了保護頁面,確保第二次遞迴呼叫時不會出現訪問違規而退出執行緒,但是,它仍然會導致堆疊溢位,需要動態的增加堆疊大小,本文沒有對這個進行研究,但是試圖通過分配另外記憶體區,改變堆疊指標,但是沒有奏效。
注意:在一個執行緒裡,全域性變數加上任何一個函式裡的臨時變數,如果超過堆疊大小,當呼叫這個函式時,都會出現堆疊溢位,這種溢位系統不會丟擲堆疊溢位異常,而直接導致執行緒退出。
對於函式1呼叫函式2,而函式n-1又呼叫函式n的巢狀呼叫,每層呼叫不算臨時變數將損失240位元組,所以預設執行緒最多有1024*(1024-2)/240=4360次呼叫。加上函式本身有變數,這個數目會大大減少