最近遇到了一個頗為難纏的 bug,症狀是當燒錄光碟的動作完成後,有些視窗的背景會變成黑色而不是原來的背景圖案;如果燒錄的資料量越大,這問題就越快出現;更奇怪的是這問題只會發生在安裝的版本而不是開發人員自己編譯出來的版本。

根據 debugging rules 的第二條規則 “Make it fail”,最重要的事情是在開發人員的機器上重現這個問題。我們首先嘗試將開發人員編譯的執行碼來覆蓋安裝的版本,結果問題還是會發生;反之,用安裝版本來覆蓋開發人員編譯的執行碼也不會有問題。對於這樣的結果,我們感到相當困惑,因為如果不是程式碼造成的差異,那會是什麼因素造成這樣的結果?

根據 rule 3 “Quit thinking and look”,我們決定多做一點嘗試,完整複製整個環境而不是只有執行碼。花了一番功夫,我們發現居然是執行檔 manifest 的問題,關鍵在於 manifest 中的這一段:

<dependentAssembly>
  <assemblyIdentity
    type="win32"
    name="Microsoft.Windows.Common-Controls"
    version="6.0.0.0"
    publicKeyToken="6595b64144ccf1df"
    language="*"
    processorArchitecture="x86"/>
</dependentAssembly>

這差異會導致 common control 6.0 被使用,於是我們在開發人員的電腦採用這樣的 manifest 很快就重現這症狀了。現在問題是那裡的程式碼導致這樣的錯誤?由於當初猜測是 GDI 方面的問題,我們把 Windows Task Manager 叫出來,並且追蹤程式執行時的 GDI objects 數量,才赫然發現當問題發生前的一瞬間,GDI objects 的數量會暴增至 10000。此時我們很確定這是由 GDI leak 產生的問題,但對於為什麼換用新版的 common control 會導致這個 bug 卻是沒什麼頭緒。

於是我們採用檢查問題發生點附近的程式碼,一邊單步執行一邊觀察 GDI Objects 的數量。用這樣的方法慢慢檢查,發現只要一呼叫 CStatic::SetBitmap,就會有一個 GDI leak 產生。我們終於明瞭新版的 common control 對於 STM_SETIMAGE 有不一樣的處理方式:static control 有可能複製我們傳進去的 bitmap handle,因此我們有義務把傳回來的 bitmap handle 釋放掉。

這個問題給我們的啟示是:當遇到瓶頸時,開發人員應該多觀察 (look),不要瞎猜 (quit thinking。因為根據我的經驗,問題發生的原因往往超出你的想像,真的讓你矇上的機率非常地小。