標籤:
前言
在開始這篇文章之前我們先來講講如何從高度最佳化的Release版的Dump中找到正確的異常上下文地址,並手動恢複異常發生的第一現場。
1. 什麼是異常上下文
簡單來說,在windows體系的作業系統裡面,每個線程都有自己的線程上下文來儲存需要的資訊,其中包括當前寄存器的值。我們這裡需要找到的異常上下文就是當異常發生的時候,異常分發器幫我們儲存的當前寄存器的值(也叫寄存器上下文),利用這些資訊結合彙編代碼的分析,我們就可以進一步找到發生異常的原因。關於寄存器上下文結構體的定義我們可以在winnt.h找到,這裡就不一一解釋了。
2. Windbg 中的“.cxr + Context Address”命令可以協助我們恢複異常發生時候寄存器的一手資料,那麼我們只需要找到這個內容相關的地址就可以交由這個命令來處理了。
3. 如何找到儲存內容相關的地址?
當我們使用windbg開啟dump file的時候,我們都會看到類似下面的呼叫堆疊模式:
這個正式異常分發器在協助我們儲存上下文,我們看看RtlDispatchException的函式宣告,這裡使用”x”命令:
大家已經發現了,這個函數的第二個參數正是我們要找的上下文地址。
4. 有了這個上下文地址,我們就可以使用”.cxr ”命令進行恢複了。這裡第二個參數是00c2e168, 執行”.cxr 00c2e168”命令。
5. 這個值真的對嗎?恢複了異常發生時的上下文,讓我們再看看呼叫堆疊的變化
這個堆棧和寄存器的的值看起來太詭異了,這不禁讓我們對上面的地址產生了懷疑!
6. 想想看,這個dump檔案來自於Release版本的App,編譯器會在產生release版的可執行檔中進行大量的最佳化,這種情況下的函數調用會最大程度地使用我們的寄存器來傳遞參數,而不是靠@esp+0x?來進行。想到了這裡,讓我們再次回到RtlDispatchException去查看一下。
7. 使用”.frame + index”跳轉到呼叫堆疊中指定的棧幀,並使用”dv”命令查看本地變數
這次我們清晰的看到了寄存器r6和r9裡面的內容,正是代碼最佳化的結果。編譯器最佳化了代碼使用r6, r9兩個寄存器來代替“棧操作”進行了傳參。
8. 再次使用”.cxr” 和”k”,這次我們就看到了真正的異常發生第一現場
原來是ldr指令企圖載入一個空地址引發了遭難。
現在我們已經學會了如何在高度最佳化的release版本中分辨出正確的上下文地址,接下來我們繼續尋找程式奔潰的原因。
定位異常上下文
1. 我們找到了真正的異常上下文,先看來看一下這個呼叫堆疊。
2. 讓我們來看看FontFileReference::ReleaseFragment到底幹了些什麼,使用命令”uf 模組名字!函數名字“來反編譯這個函數。
3. 再次查看這個函數對應的棧幀裡面的內容來輔助我們理解上面的彙編代碼,”.frame + frame index”
4. 使用命令”dt 模組名字!類名字”來查看FontFileReference的記憶體布局
分析和推測
1. 從呼叫堆疊可以看到,下面的語句觸發了異常,這裡的指令正在執行載入寄存器的操作,load r3寄存器裡面存的地址,但是r3=0,我們不禁要問,r3為什麼會是0,而這個r3裡面又應該是什麼呢?
2. 重點分析寄存器,r3。這裡給大家分享個看彙編指令的心得,其實我們並不用每次都一行一行的來分析,簡單的方法就是找到關鍵的指令!在這個case裡面,我把下面的指令列為關鍵指令:
為什麼要把這幾行列為關鍵指令呢?這是因為這裡出現了”r0, #0x10”和blx。這裡的”r0, #0x10”是”r0 + 0x10“的意思,”blx” 是調用子函數的意思。再結合FontFileReference這個類的結構,我們可以合理的猜測到r0裡面的地址應該指向了this指標,那麼r0 + 0x10就是FontFileReference這個類裡的成員變數”stream_”。
我們已經知道了r0裡面的是this指標,r3裡面的是指向類型為ComPtr<IDWriteFontFileStream>的COM對象stream_,那麼接下來r3+0x10又是什麼呢,關於IDWriteFontFileStream的定義可以在dwrite.h中查到也可以使用”dt /v”顯示出來。
我們知道從IUnknown繼承下來的COM介面前三個函數一定是QueryInterfacy,Release和AddRef,那麼r3+0x10就是ReleaseFileFragment這個函數了。布局大致如下:
在這裡需要解釋一下為什麼我們要進行“猜測”而不是直接調用”dt this”命令來直接查看當前的這個FontFileReference對象呢?因為這個dump是release版本產生的,很多的資訊都已經無效了:
3. 經過上面的分析我們就知道了這段彙編代碼的主要工作就是調用” stream_->ReleaseFileFragement”。對比”!analyze –v” 命令給出的資訊可以驗證我們的猜測並得出結論,正是 stream_這個變數為空白導致了異常。再來看看這個線程堆裡面還有什麼有用的資訊:
分析和定位我們的原始碼
棧裡面殘留的資訊也把問題指向了Font,FontFamily相關的內容,從native的callstack中我們沒法定位到我們的代碼,這裡使用”!CLRStack”命令來看看我們CLR的代碼棧裡面的調用資訊:
紅線的代碼就是我們自己的代碼了,好了開啟代碼查看一下:
從這段代碼看我們在動態建立TextBlock的時候確實沒有指定FontFamily這個屬性。程式員在開發代碼和測試的時候這個異常並沒有出現,從我們下載下來的excel檔案來看,這個異常只出現在WP8.0平台,30天內導致的閃退累計次數是4000次。這個次數相比我們APP的活躍度來說非常小。所以我們可以推斷,這個bug應該是系統的問題,很可能是stream_這個COM對象的ref count增減操作在沒有指定FontFamily的一些特殊情況下發生的,它導致了stream_被提前釋放掉了。既然是系統的問題,那我們能做什麼呢?首先,我們可以指定一個FontFamily給控制項TextBlock;其次,在CreateWideTileBackgroundImage這個函數裡面的Render調用上添加try catch來捕獲這個“System.ExecutionEngineException“。
小結
雖然有些時候問題是由系統引起的,但是通過分析我們還是可以採取一定的措施在我們的代碼裡面來處理的。
Windows Phone App的dump檔案執行個體分析- System.ExecutionEngineException