2012年6月8日 星期五

有效分析 VC++ Debugger 丟出的 Heap Corruption 警告

最近一次進行專案 refactoring 之後發現使用 VC++ Debugger 進行程式除錯時, 有時 Debugger 會丟出 Heap Corruption 警示. 具體來說是跳出如下視窗:



同時 Visual Studio 的 Output window 中會出現:

"HEAP: Free Heap block xxxxxxxx modified at xxxxxxxx after it was freed"

的警告語句. 但這樣的 Debugger Break 只會發生在進行除錯時, 若是直接運行程式則不會有影響. 仔細做了一下功課之後發現這個 Debugger Break 的發生的原因是: 某塊曾經被 malloc/new 出來的 heap block, 在經過 free/delete 釋放之後, 其內容又被改動到.

一般來說, 只要是 OS 分配給你的 heap block, 一定都位於可讀寫的 virtual page 裡面. 因此就算是被釋放掉的區塊內容受到竄改, 實際運作時也不會觸發任何異常. 不過以程式寫作的觀點來看, 區塊已被釋放掉但內容卻受到竄改, 這高度暗示了程式中有 dangling pointer 的存在, 必需即早正視和排除. Windows 的 Debug CRT 函式庫為了讓 programmer 提早注意到這個問題, 在釋放 heap block 時會多動一點手腳: 在釋放前先把這個 heap block 裡面的內容全部填上一些制式的 pattern. 下次透過 malloc/new 獲得這些曾被釋放過的 heap block 時, 檢查看看裡面是否還保有那樣的 pattern, 藉以斷定內容是否被竄改過(*1), 若有就會觸發 Break.

正因這樣的警示只會在 Debugger 配合 Debug CRT 使用的情況下才會出現, 也許有些 programmer 對此抱持姑息的心態. 但 dangling pointer 就像是一顆不知啥時會爆炸的炸彈, 所以最好是一發現就盡全力去排除掉. 但在排除上會遇到一個問題: 在 Debugger Break 的那個中斷點上, Debug CRT 只能提出內容被竄改的結論, 但卻無法告訴你是哪行代碼在什麼時候竄改的. 根據這樣的情資一樣很難推估問題點. 網上一陣訪查之後發現很多人推薦使用微軟的一個 gflags 除錯工具來開啟 full heap validation 的功能. 實際試用過之後真得馬上幫我找出了問題點. 真是一個很神奇的工具. 整理心得如下:

  1. gflags 是 Debugging Tools for Windows 套件其中的一隻工具程式, 而 Debugging Tools for Windows 目前只能靠安裝 Windows SDK for Windows 8 Release Preview 來安裝. 沒有單獨的安裝程式.
  2. 安裝好了之後 gflags.exe 預設是位於 C:\Program Files\Windows Kits\8.0\Debuggers\x86 之下, 直接點擊執行會帶出 GUI 界面, 它也支援命令列模式. 使用命令列模式時建議用系統管理員身份來執行命令列, 不然每次執行 gflags 指令時, 顯示結果都會出現在另外開啟的命令列中, 馬上就消失.
  3. 簡易用法 - 針對目標程式開啟 full heap validation 功能:
    gflags /p /enable xxxx.exe /full
    關閉 validation:
    gflags /p /disable xxxx.exe 
    開關後原程式不需要重編或做任何改動. 作業系統只要一偵測到有 xxxx.exe 這個執行檔在運作, 就會對它套用特別的 allocation 程序. 更多用法請參考這篇文章這篇文章.
  4. gflags 支援的命令列參數說明

根據這篇 msdn 文章的解釋, gflags 開啟的 full heap validation 原理是把所有 malloc/new 獲得的 heap block 都獨立安排在一個 virtual page 的最後面, 與邊界貼齊. 在該 virtual page 之後馬上安插一個不可存取的 page. 當 heap block 被釋放後, 也保證馬上將原 page 設定為不可讀寫. 若是有 dangling pointer 意圖存取曾被釋放過尚未被領用、或超過原 allocation 長度以後的內容, 馬上會觸發 page fault, 強制程式結束. 這樣就能第一時間發現問題點. 若 dangling pointer 的存取形態不是一般的向後超出, 而是向前超出怎麼辦? 這時可以參考命令列參數中的 /backwards 功能, 把 heap block 內容改成向前貼齊一個 page 的邊界. 由這樣的原理我們可以想像, 這樣的 heap blocks 會以非常鬆散的方式排列在記憶體中(每個 page 只放一個 allocation 單位). 所以當程式運作於 full heap validation 模式時, 會比原本多佔用數十倍的記憶體.

*1: Debug CRT 還會在其它一些特定時候填寫制式 pattern 到 heap 或 stack 中試圖引起程式設計師的注意, 可參考這篇文章.

沒有留言:

張貼留言