VC中捕捉StackOverflow異常

    程序中棧溢出通常會導致進程直接關閉。本文分析Windows中棧增長和溢出過程,並提供一種捕捉棧溢出的方法,作爲調試程序的參考。

棧大小

    VC中,棧大小默認爲1M,但可以在編譯的時候或在創建線程的時候指定其他值。

棧增長和溢出

   棧底地址在線程生命期內是常量,棧頂地址保存在SP(ESP、RSP)寄存器。棧地址從高向低增長,因此棧增長,對應着SP寄存器減小。

   1M空間只是預留進程地址空間(Reserve的過程),默認沒有分配物理內存。隨着程序對棧的使用,操作系統逐步爲地址分配物理內存(commit的過程)。x86平臺下,每次commit一個頁面,也就是4K大小的物理內存。1M的地址空間可以對應256個頁面。

   正常情況下,假設已經爲棧分配了N個物理頁面。只要N<254,第1到N個頁面就是可讀寫的。第N+1個頁面也會分配物理內存,但是其保護位是PAGE_GUARD,不可讀寫。現在棧繼續增長,需要N+1個頁面的內存,當程序試圖讀寫第N+1個頁面的時候,由於PAGE_GUARD屬性,發生STATUS_GUARD_PAGE_VIOLATION。操作系統捕捉該異常,把第N+1個頁面的保護屬性修改爲可讀寫。同時爲第N+2個頁面地址分配物理內存,設置保護屬性爲PAGE_GUARD。

   在N=254的時候,如果棧繼續增長,則會觸發第255個頁面的STATUS_GUARD_PAGE_VIOLATION異常,操作系統捕捉該異常,把第255個頁面設置爲可讀寫,然後觸發棧溢出異常(EXCEPTION_STACK_OVERFLOW)。

   N=254與N<254不同點是

  1. N=254時,不爲第256個頁面分配物理內存。實際上系統永遠不會爲第256個頁面地址分配物理內存,第256個頁面永遠只是Reserve狀態。
  2. N=254時,觸發EXCEPTION_STACK_OVERFLOW。

棧溢出捕捉和恢復

   EXCEPTION_STACK_OVERFLOW不是C++異常,只能用操作系統提供的結構化異常(Structured Exception Handling,SEH)措施來處理。如下所示:

__try{
		// 導致棧溢出的操作
	}__except((GetExceptionCode() == EXCEPTION_STACK_OVERFLOW) ? 
				EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
	{
		int reset = _resetstkoflw();
		if(reset)
			std::cout << "棧恢復成功。遞歸次數=" << count << std::endl;
		else
			std::cout << "棧恢復失敗。" << count << std::endl;
	}

   注意到代碼中,在捕捉到棧溢出異常後,調用了_resetstkoflw函數。_resetstkoflw是VC運行庫提供的函數,作用是恢復棧內存中相應頁的PAGE_GUARD屬性。如果不執行_resetstkoflw,本次溢出異常看似捕捉到了,程序也可以繼續運行。但是由於棧中的頁面不存在PAGE_GUARD屬性,如果棧下一次溢出,訪問第256個頁面的時候,由於第256個頁面沒有分配物理內存, 直接拋出ACCESS_VIOLATION異常,而不是EXCEPTION_STACK_OVERFLOW。

   _resetstkoflw是有返回值的。如果_resetstkoflw執行的時候,棧又不夠用,則返回0。此時無法恢復棧到正常狀態。SetThreadStackGuarantee函數可以要求系統在拋出EXCEPTION_STACK_OVERFLOW異常的時候,保證仍然有指定大小的空閒區可以用。SetThreadStackGuarantee實際上減少了程序實際可用棧的大小。

 

完整代碼

#include"stdafx.h"
#include<iostream>
#include<Windows.h>

int count = 0;

void recursive(){
     char dummy[1024];
     ++count;
     recursive();
}

 

void testRecursive(){

     __try{
         recursive();
     }__except((GetExceptionCode() == EXCEPTION_STACK_OVERFLOW) ?
                   EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
     {
         int reset = _resetstkoflw();
         if(reset)
              std::cout << "棧恢復成功。遞歸次數=" << count << std::endl;
         else
              std::cout << "棧恢復失敗。" << count << std::endl;
     }

}

 
int_tmain(intargc, _TCHAR* argv[])
{
     std::cout << GetProcessId(GetCurrentProcess()) << std::endl;
 
     ULONG stkLimit = 1024*4;// 4K
     if(!SetThreadStackGuarantee(&stkLimit)){
         std::cout << "SetThreadStackGuarantee失敗" << std::endl;
         return -1;
     }


     // 第一次棧溢出
     testRecursive();

     // 第二次棧溢出
     testRecursive();

     std::cout << "測試結束" << std::endl;

     bool tmp = std::cin >> tmp;
     return 0;
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章