線程堆棧是如何增加的

咱們知道每一個線程初始堆棧的默認空間是1M, 咱們能夠在VC編譯的Linker項裏進行設置,該值會被編譯進最終的PE可執行文件中。線程堆棧內存包括commit部分和reserver部分,咱們上面說的1M實際上指reserve部分,系統爲了節約內存,並不會把全部reserve的1M都提交物理內存(commit), 因此初始只是提交部份內存。
 
咱們能夠隨便找一個程序,經過WinDbg進行驗證:!address -f:stack
  BaseAddr EndAddr+1 RgnSize Type State Protect Usage
---------------------------------------------------------------------------------------------
   90000 184000 f4000 MEM_PRIVATE MEM_RESERVE Stack [~0; 16d8.13ec]
  184000 185000 1000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE|PAGE_GUARD Stack [~0; 16d8.13ec]
  185000 190000 b000 MEM_PRIVATE MEM_COMMIT PAGE_READWRITE Stack [~0; 16d8.13ec]
 
能夠看到一個線程的堆棧分3部分:0xB000字節的MEM_COMMIT內存,0x1000字節的MEM_COMMIT & PAGE_GUARD內存,還有0xF4000字節的MEM_RESERVE內存,總共是0xB000+0x1000+0xf4000 = 0x100000 = 1M
 
經過實驗,咱們能夠看到線程堆棧只提交(commit)了一部份內存,大部份內存是reserve的,如今的問題是堆棧在增加的過程當中,它是如何提交(commit)內存的? 咱們知道,咱們在函數中申明一個N字節大小的局部變量,它就是在線程的堆棧中申請的空間(實際上只須要ESP-N)就能夠了。咱們若是觀察過函數的反彙編代碼,會注意到它沒有commit內存相關的代碼。 那麼究竟最終它是如何commit那些reserve的內存的呢? 
 
曾經面試被問到這個問題, 由於沒有看過相關的書籍, 沒回答出來...
 
最近思考這個問題, 終於在張銀奎的<< 軟件調試>>裏找到了答案:

系統在提交棧空間時會故意多提交一個頁面,稱這個頁面爲棧保護頁面(Stack Guard Page), 這點咱們能夠在上面WinDbg的實驗中驗證。棧保護頁面具備特殊的PAGE_GUARD屬性,當具備如此屬性的內存頁被訪問時,CPU會產生頁錯誤並開始執行系統的內存管理函數,當內存管理函數檢測到PAGE_GUARD屬性後,會清除對應頁面的PAGE_GUARD屬性,而後調用一個名爲MiCheckForUserStackOverflow的系統函數,這個函數會從當前線程的TEB中讀取用戶態棧的基本信息並檢查致使異常的地址,若是致使異常的被訪問地址不屬於棧空間範圍,則返回STATUS_GUARD_PAGE_VIOLATION,不然MiCheckForUserStackOverflow函數會計算棧中是否還有足夠的剩餘空間能夠建立一個新的棧保護頁面。若是有,則調用ZwAllocateVirtualMemory從保留的空間中在提交一個具備PAGE_GUARD屬性的內存頁。新的棧保護頁與原來的緊鄰,通過這樣的操做後,棧的保護頁向低地址方向平移了一位,棧的可用空間增大了一個頁面的大小,這即是所謂的棧空間自動增加。
 
棧溢出是指當提交的棧空間再被用完,棧保護頁又被訪問時,系統便會重複以上過程,直到當棧保護頁距離保留空間的最後一個頁面只剩一個頁面的空間時,MiCheckForUserStackOverflow函數會提交倒數第二個頁面,但再也不設置PAGE_GUARD屬性,由於最後一個頁面永遠保留不可訪問,因此這時棧增加到它的最大極限,爲了讓應用程序知道棧將用完,MiCheckForUserStackOverflow函數返回STATUS_STACK_OVERFLOW,觸發棧溢出異常。
 
最後感概技術深了能夠再深,從C++編譯器到CRT運行庫, 再到操做系統, 從用戶態到內核和驅動, 最後到硬件, 原理背後還有原理, 真正能掌握全部細節的又有幾人呢?
相關文章
相關標籤/搜索