1. 程式人生 > >c++堆疊溢位的處理(包括遞迴)

c++堆疊溢位的處理(包括遞迴)

本文背景:

在程式設計中,很多Windows或C++的記憶體函式不知道有什麼區別,更別談有效使用;根本的原因是,沒有清楚的理解作業系統的記憶體管理機制,本文企圖通過簡單的總結描述,結合例項來闡明這個機制。

本文目的:

對Windows記憶體管理機制瞭解清楚,有效的利用C++記憶體函式管理和使用記憶體。

  1. 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次呼叫。加上函式本身有變數,這個數目會大大減少