Windows下c++程序崩潰問題定位
Windows下c++程序崩潰問題定位主要依賴於代碼編譯過程中生成的調試信息文件,比如.map和.pdb。
本文以前幾天項目中出現的崩潰問題當做具體案例來分析。具體原因是配置了某個數據文件A,由於A文件是在線程T1中去加載的,T2線程中去讀取緩存內容,這兩個線程同時運行,沒有進行異步控制,導致轉代碼線程先執行時,取緩存為空,同時有個函數邏輯錯誤,導致memcpy的目的地址為空,且對空指針進行訪問了,程序崩潰。
本文首先通過Windbg工具結合pdb文件來分析崩潰的原因,然后自己制造一個崩潰來驗證WinDbg的調試結果,最后再講解如何通過map文件以及系統崩潰日志計算崩潰偏移地址直接定位代碼行數。
一,WinDbg調試
在主程序中加入了dump文件生成邏輯后,還原當時現場場景,程序崩潰,在HqIssue同級目錄的Dump文件夾下生成了20181226_093530.dmp文件。在自己的D盤上新建一個文件夾dump_test,將20181226_093530.dmp,A.pdb,B.pdb,B.exe,A.map放於文件夾內(其他pdb也可以加入進來,在你不確定到底崩潰在程序哪個插件中的時候;B.exe取決於如果程序崩潰還可以在windbg中繼續調試的時候,可以加進來;A.map文件保存了地址信息)。
打開WinDbg工具,我們首先需要配置Symbol path,Source path以及Image path
1) Symbol path
這是pdb文件路徑,包括當前dump調試對應的工程pdb文件,以及系統api對應的pdb文件。前者在上面第一點中的dump_test文件夾中,后者需要配置
SRVD:\mysymbolhttp://msdl.microsoft.com/download/symbols
其中D:\mysymbol為下載系統pdb文件存放在本機的路徑。
在view-commond中輸入 .reload命令,加載pdb符號
2) Source path
這是代碼路徑,此代碼需要和pdb文件版本保持一致。
3) Image path
Exe程序路徑,一般用於死循環時,在WinDbg中繼續調試。
拖拽20181226_093530.dmp文件到WinDbg界面中,然后打開Debug-Modules查看我們需要的模塊是否已經全部加載上來了(至少要保證系統pdb和程序中重要的pdb被加載進來)。
輸入!analyze –v命令,這個命令是最簡單也是最實用的的調試命令,命令執行需要一些時間,稍等一下,出現堆棧結果,在我的WinDbg上調試結果為:
由於源碼,pdb完全匹配,所以可以直接在FAULTING_SOURCE_CODE中顯示錯誤的代碼。memcpy拷貝出錯,剛好錯誤也就是上面我們說的pData == NULL,並且pNode->m_pFieldPoint == NULL。
如果沒有下面的FAULTING_SOURCE_CODE,只有STACK_TEXT呢,這種情況又分為兩種:
1) STACK_TEXT的堆棧信息后面帶有代碼行數的,我們可以直接定位到行數
2) STACK_TEXT的堆棧信息后面什么都沒有帶,只有一個偏移地址。
下面我們着重對第2)中情況進行分析
從上圖中我們可以得出兩個信息:
1)0ae7c1ec 8a4810 mov cl,byte ptr [eax+10h]
程序崩潰的地址為0x0ae7c1ec
2)程序崩潰在hq_shanghai_binary 組件的CCheckDBF::BCLXTimeCall中。
我們首先找到hq_shanghai_binary.dll加載到主程序中后在主程序中的地址空間,輸入命令lm
可以看到hq_shanghai_binary.dll的地址基址為0x0ae70000,用0x0ae7c1ec – 0x0ae70000得到崩潰處在dll中的虛擬地址為:0xc1ec。但是對於dll或者exe來說,它們的地址空間都是有一個最佳裝載地址的,也就是說它們存儲數據的地方並不是從0x0ae70000基址開始,會加上一個偏移量再開始存儲數據,所以我們需要找到這個最佳裝載基址,公式為:
虛擬地址 = pe頭文件大小 + 最佳裝載地址 +相對虛擬地址
打開hq_shanghai_binary.map文件,可以在文件頭部看到:
Pe頭文件大小為0x10000000,然后再在當前map中找任何一個函數地址進行計算出當前dll的最佳裝載地址,此處推薦全局搜索main(不管對於exe還是dll,都會存在main/dllmain)來進行計算最佳裝載地址。
1004ff19 = 10000000 + 最佳裝載地址 + 0004ef19 -》最佳裝載地址 = 0x1000
所以崩潰處的相對偏移地址為:0xc1ec – 0x1000 = 0xb1ec
由於崩潰處在CheckDbf.cpp文件中,所以在hq_shanghai_binary.map中搜索CheckDbf.cpp
在這個段中查找與0xb1ec最接近的地址,找到為:
0xb1ec剛好在上圖標紅的兩個地址之間,前面的2290,2291就是行號,那么就說明問題出錯的地方在CheckDBF.cpp文件的2290行。
上面的地址偏移計算過程可以解決我們兩個疑惑:
1) WinDbg為什么能夠精准定位到程序崩潰處:因為pdb文件中是包含了文件名和代碼行號信息,以及各個變量,函數的地址信息,當根據偏移地址找到行號后,再通過我們配置的Source path,直接將代碼展示在FAULTING_SOURCE_CODE區域。
2) 通過完全匹配的map文件我們也可以直接定位到程序崩潰處:當我們的應用程序內部沒有捕捉程序崩潰的機制時,windows默認會有一個崩潰捕捉處理,也即是我們常見的windows報錯框,根據崩潰的模塊地址,以及崩潰處地址,也是通過同樣的地址計算,來定位代碼行數,下面會介紹到。
二,地址偏移計算驗證
現在我們來創造一個崩潰驗證上面分析的根據地址去找尋崩潰錯誤行數的方法,在HqShanghaiBinary.cpp文件的TimeCall函數入口處加入兩行代碼,制造崩潰,編譯代碼,生成最新的pdb文件和map文件,然后運行程序,崩潰,在dump文件夾下找到dump文件。
具體打開WinDbg分析dump的步驟不再描述,可以參照上面。輸入!analyze – v后如圖所示:
當然,它這里已經告訴我們行數了,718行,剛好是我們崩潰開始的地方。然后我們再看崩潰發生的地址為:0x0b01610e。
0b01610e 8908 mov dword ptr [eax],ecx
輸入lm命令,查看當前dll在主程序中的基址,為0x0afe0000
計算出:崩潰處偏移地址 + dll中最佳裝載地址 = 0x0b01610e - 0x0afe0000 = 0x3610e。
再打開hq_shanghai_binary.map文件,以dllmain函數的地址為例求出dll最佳裝載地址為:
dll最佳裝載地址 = 0x1004e049 – 0x0004d049 – 10000000 = 0x1000
所以崩潰處偏移地址 = 0x3610e – 0x1000 = 0x3510e
然后再到hq_shanghai_binary.map中找到崩潰所在文件HqShanghaiBinary.cpp區域行號和偏移地址的映射關系:
崩潰偏移地址剛好在0x00035100 至 0x0003511c中間,所以崩潰代碼所在行數為718-724之間,再回頭看我們設置崩潰的地方,也剛好是這個區域,完美定位!
三,通過map文件定位程序崩潰代碼行數
還是以(二)中的崩潰為例,現在我們屏蔽掉代碼中的崩潰捕捉,不生成.pdb文件(如果程序自己捕捉了崩潰,操作系統就不會進行默認的崩潰處理)運行程序,程序崩潰,出現下圖所示信息:
打開WER7FDF.tmp.WERInternalMetadata.xml文件,我們主要看第二個xml節點:
這里面包含了崩潰的地址信息,Parameter7就是Exception code,崩潰處地址,且是在崩潰模塊中的偏移地址。
崩潰處偏移地址 + dll中最佳裝載地址 = 0x3610e。
在此不再重述最佳裝載地址的計算,參考(二)中計算可得最佳裝載地址 = 0x1000,所以崩潰處偏移地址 = 0x3510e,到map文件中查找對應行數在718-724之間。結果和WinDbg定位出來的崩潰代碼行數是一樣的。
至此,碰到絕大部分的崩潰性問題,都可以通過使用WinDbg來分析dump幫助我們定位代碼崩潰處,前提是pdb和dump文件匹配。
附problem signatures節點中前面幾個屬性的順序和含義:
Problem Event Name: APPCRASH //問題事件名稱
Application Name: acad.exe //應用名稱
Application Version: 24.0.55.0 //應用版本號
Application Timestamp: 498ff0e7 //應用時間戳
Fault Module Name: StackHash_bade //故障模塊名稱
Fault Module Version: 6.1.7600.16385 //故障模塊版本號
Fault Module Timestamp: 4a5be02b //故障模塊時間戳
Exception Code: c0000374 //異常代碼
Exception Offset: 00000000000c6cd2 //異常偏移地址
from:https://blog.csdn.net/bajianxiaofendui/article/details/85303089
