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次調用。加上函數本身有變量,這個數目會大大減少


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM