本文背景:
在編程中,很多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次調用。加上函數本身有變量,這個數目會大大減少
