在日常分析C++軟件異常的日常工作中,大多數(shù)情況下我們都是使用Windbg去靜態(tài)分析dump文件去排查軟件異常的,今天我們就來(lái)詳細(xì)地講一下如何使用Windbg去靜態(tài)分析dump文件,以供參考。
image.png
1、概述
基本大部分軟件都內(nèi)置了異常捕獲模塊,在軟件發(fā)生閃退崩潰時(shí),都會(huì)彈出相關(guān)的提示框,比如PC版的微信在崩潰時(shí),其內(nèi)置的異常捕獲模塊會(huì)捕獲到并生成日志及dump文件,同時(shí)會(huì)彈出如下的發(fā)送錯(cuò)誤報(bào)告的提示框:
image.png
提示框的下方會(huì)自動(dòng)帶上崩潰相關(guān)的文件,其中最后一個(gè)文件就是我們要講的dump文件,點(diǎn)擊確定則會(huì)將這些文件發(fā)送到騰訊遠(yuǎn)端的服務(wù)器上。騰訊后臺(tái)的運(yùn)維人員就會(huì)收到通知,然后到服務(wù)器上將dump等文件下載下去去分析。
有些軟件可能沒(méi)有上傳崩潰日志到服務(wù)器的功能,捕捉到異常時(shí)會(huì)自動(dòng)將dump文件保存到指定的路徑中,事后可以到該路徑中取到對(duì)應(yīng)的dump文件。如果客戶機(jī)器上遇到崩潰,可以和客戶聯(lián)系,讓客戶幫忙從對(duì)應(yīng)的路徑中取來(lái)dump文件。
下面就來(lái)詳細(xì)講一下拿到dump文件之后如何使用Windbg去靜態(tài)分析dump文件。
2、靜態(tài)分析dump文件的一般步驟
用Windbg打開(kāi)dump文件,先使用.ecxr命令切換到異常的上下文,然后根據(jù)切換成功后顯示的異常類型(ExceptionCode值對(duì)應(yīng)的含義,比如0xC0000005對(duì)應(yīng)的就是AccessViolation內(nèi)存訪問(wèn)違例)、發(fā)生崩潰的那條匯編指令及相關(guān)寄存器的值。通過(guò)查看匯編指令及各寄存器的值,可以初步判斷當(dāng)前發(fā)生的是什么異常,比如C++類空指針、內(nèi)存訪問(wèn)違例。
image.png
接著,使用kn/kv/kp查看函數(shù)調(diào)用堆棧,看看是調(diào)用了什么函數(shù)觸發(fā)的異常。調(diào)用堆棧中會(huì)顯示函數(shù)所在模塊信息,一般我們需要拿到這些模塊的pdb文件,加載pdb文件后函數(shù)堆棧中才會(huì)顯示具體的函數(shù)名和行號(hào)的。我們需要使用lm命令查看這些模塊的時(shí)間戳,然后找來(lái)這些模塊對(duì)應(yīng)的pdb文件,然后將pdb文件的路徑設(shè)置到windbg中。
然后,根據(jù)函數(shù)調(diào)用堆棧中顯示的函數(shù)名及行號(hào),對(duì)照著源代碼去分析發(fā)生異常的原因。有時(shí)候?yàn)榱烁闱宄l(fā)生異常的本質(zhì),我們還需要使用IDA查看相關(guān)二進(jìn)制文件的匯編代碼,查看一下發(fā)生異常的那條匯編指令的上下文,對(duì)照著C++源碼,看看那條匯編指令為啥會(huì)出現(xiàn)異常。
3、分析實(shí)例說(shuō)明
我們?yōu)榱朔奖阏归_(kāi)講解,我們特意使用VisualStudio創(chuàng)建了一個(gè)基于MFC對(duì)話框的exe程序工程,對(duì)話框中有個(gè)名為button1的按鈕,如下所示:
image.png
我們?cè)诖税粹o的響應(yīng)中添加了一段引發(fā)崩潰的測(cè)試代碼,故意讓程序產(chǎn)生崩潰,測(cè)試然后拿到崩潰時(shí)的dump文件。
具體的測(cè)試代碼如下所示:
// 添加的一段測(cè)試代碼SHELLEXECUTEINFO *pShExeInfo = NULL;int nVal = pShExeInfo->cbSize; // 通過(guò)空指針訪問(wèn)結(jié)構(gòu)體成員,導(dǎo)致崩潰 CString strTip;strTip.Format( _T(“nVal=%d.”), nVal );AfxMessageBox( strTip );
代碼中使用到的結(jié)構(gòu)體SHELLEXECUTEINFO 定義如下:
typedef struct _SHELLEXECUTEINFOW{ DWORD cbSize; // in, required, sizeof of this structure ULONG fMask; // in, SEE_MASK_XXX values HWND hwnd; // in, optional LPCWSTR lpVerb; // in, optional when unspecified the default verb is choosen LPCWSTR lpFile; // in, either this value or lpIDList must be specified LPCWSTR lpParameters; // in, optional LPCWSTR lpDirectory; // in, optional int nShow; // in, required HINSTANCE hInstApp; // out when SEE_MASK_NOCLOSEPROCESS is specified void *lpIDList; // in, valid when SEE_MASK_IDLIST is specified, PCIDLIST_ABSOLUTE, for use with SEE_MASK_IDLIST & SEE_MASK_INVOKEIDLIST LPCWSTR lpClass; // in, valid when SEE_MASK_CLASSNAME is specified HKEY hkeyClass; // in, valid when SEE_MASK_CLASSKEY is specified DWORD dwHotKey; // in, valid when SEE_MASK_HOTKEY is specified union { HANDLE hIcon; // not used#if (NTDDI_VERSION >= NTDDI_WIN2K) HANDLE hMonitor; // in, valid when SEE_MASK_HMONITOR specified#endif // (NTDDI_VERSION >= NTDDI_WIN2K) } DUMMYUNIONNAME; HANDLE hProcess; // out, valid when SEE_MASK_NOCLOSEPROCESS specified} SHELLEXECUTEINFOW, *LPSHELLEXECUTEINFOW; #ifdef UNICODEtypedef SHELLEXECUTEINFOW SHELLEXECUTEINFO;typedef LPSHELLEXECUTEINFOW LPSHELLEXECUTEINFO;#elsetypedef SHELLEXECUTEINFOA SHELLEXECUTEINFO;typedef LPSHELLEXECUTEINFOA LPSHELLEXECUTEINFO;#endif // UNICODE
在測(cè)試代碼中定義了SHELLEXECUTEINFO結(jié)構(gòu)體指針pShExeInfo,并初始化為NULL,然后并沒(méi)有給該指針賦一個(gè)有效的結(jié)構(gòu)體對(duì)象地址,然后使用pShExeInfo訪問(wèn)結(jié)構(gòu)體的cbSize成員的內(nèi)存,因?yàn)閜ShExeInfo中的值為NULL,所以結(jié)構(gòu)體cbSize成員的內(nèi)存地址是結(jié)構(gòu)體對(duì)象起始地址的偏移,因?yàn)榻Y(jié)構(gòu)體對(duì)象地址為NULL,cbSize成員位于結(jié)構(gòu)體的首位,所以cbSize成員就是結(jié)構(gòu)體對(duì)象的首地址,就是NULL,所以就訪問(wèn)64KB小地址內(nèi)存塊的異常,引發(fā)內(nèi)存訪問(wèn)違例,導(dǎo)致程序發(fā)生崩潰閃退。
至于如何在程序中設(shè)置異常捕獲模塊去捕獲異常、自動(dòng)生成dump文件,可以嘗試使用google開(kāi)源的CrashRpt庫(kù),我們產(chǎn)品很早就有了,具體怎么獲取目前還沒(méi)有研究過(guò),大家需要的話可以到網(wǎng)上搜一下。
4、用Windbg打開(kāi)dump文件,初步分析
使用Windbg打開(kāi)dump文件(直接將dump文件拖入到Windbg),然后輸入.ecxr命令,切換到異常的上下文,就可以發(fā)生的異常類型、發(fā)生了異常崩潰的那條匯編及崩潰時(shí)的各個(gè)寄存器的值。
首先,我們查看一下發(fā)生的異常類型,比如AccessViolation內(nèi)存訪問(wèn)違例、StackOverflow線程棧溢出的異常,這樣對(duì)異常有個(gè)初步的認(rèn)知,如下:
image.png
接著就是查看發(fā)生異常的那條指令,查看指令的構(gòu)成以及崩潰時(shí)的各個(gè)寄存器的值,可能能初步估計(jì)出發(fā)生異常的原因。如果指令中訪問(wèn)了一個(gè)很小的地址或者訪問(wèn)了一個(gè)很大的地址,都會(huì)觸發(fā)內(nèi)存訪問(wèn)違例。
1)訪問(wèn)64KB小地址內(nèi)存區(qū),引發(fā)訪問(wèn)違例在Windows系統(tǒng)中,64KB以內(nèi)的內(nèi)存地址是禁止訪問(wèn)的,如果程序訪問(wèn)這個(gè)范圍內(nèi)的內(nèi)存,則會(huì)觸發(fā)內(nèi)存訪問(wèn)違例,系統(tǒng)會(huì)強(qiáng)行將進(jìn)程終止掉。2)訪問(wèn)了內(nèi)核態(tài)的大地址內(nèi)存區(qū),引發(fā)訪問(wèn)違例對(duì)于32位程序,系統(tǒng)會(huì)給進(jìn)程分配4GB的虛擬內(nèi)存,一般情況下用戶態(tài)和內(nèi)核態(tài)會(huì)各占一半,即各占2GB,我們編寫(xiě)的代碼基本都是運(yùn)行在用戶態(tài)的,用戶態(tài)的代碼時(shí)不能訪問(wèn)內(nèi)核態(tài)內(nèi)存地址的(內(nèi)核態(tài)地址是供系統(tǒng)內(nèi)核模塊使用的),如果崩潰指令中訪問(wèn)了一個(gè)很大的內(nèi)存地址,超過(guò)用戶態(tài)的地址范圍0-2GB,內(nèi)存地址大于0x8000000,則會(huì)觸發(fā)內(nèi)存違例,因?yàn)橛脩魬B(tài)的代碼是禁止訪問(wèn)內(nèi)核態(tài)內(nèi)存地址的。
如果匯編指令中使用到了ecx寄存器進(jìn)行地址計(jì)算,去訪問(wèn)計(jì)算出來(lái)的內(nèi)存地址,如果訪問(wèn)了一個(gè)小內(nèi)存地址并且ecx寄存器為0,那么這個(gè)崩潰可能是空指針引發(fā)的。在C++中,在調(diào)用C++類的成員函數(shù)時(shí)是通過(guò)ecx寄存器傳遞C++對(duì)象地址的,所以在通過(guò)C++類對(duì)象去調(diào)用虛函數(shù)(調(diào)用虛函數(shù)時(shí)的二次尋址)及通過(guò)C++對(duì)象去訪問(wèn)對(duì)象中的數(shù)據(jù)成員時(shí),都會(huì)用到ecx寄存器的。
上面故意添加的會(huì)引發(fā)崩潰的測(cè)試代碼,運(yùn)行后產(chǎn)生dump文件,我們來(lái)分析一下這個(gè)dump文件。用Windbg打開(kāi)dump文件后,輸入.excr命令,接著輸入kn命令,查看到如下的結(jié)果:
image.png
首先,這是Access Violation內(nèi)存訪問(wèn)違例的異常。其次,從崩潰的這條匯編指令來(lái)看,是訪問(wèn)了小地址0x00000000地址,這是訪問(wèn)了小于64KB小地址內(nèi)存區(qū),這個(gè)范圍的地址是禁止訪問(wèn)的,所以引發(fā)了內(nèi)存訪問(wèn)違例。從匯編指令及寄存器的值來(lái)看,看不出來(lái)什么明顯的線索。
所以接著輸入了kn命令,將函數(shù)調(diào)用堆棧打印出來(lái)。函數(shù)調(diào)用堆棧是從下往上看的,最上面一行就是最后調(diào)用的一個(gè)函數(shù),也是崩潰的那條匯編指令所在的函數(shù)。從函數(shù)調(diào)用堆棧的最后一幀調(diào)用的函數(shù)來(lái)看,程序的崩潰是發(fā)生在TestDlg.exe文件模塊中,不是其他的dll模塊。顯示的函數(shù)地址是相對(duì)TestDlg.exe文件模塊起始地址的偏移,為啥看不到模塊中具體函數(shù)名稱呢?那是因?yàn)閃indbg找不到TestDlg.exe對(duì)應(yīng)的pdb文件,pdb文件中包含對(duì)應(yīng)的二進(jìn)制文件中的函數(shù)名稱及變量等信息,Windbg加載到pdb文件才能顯示完整的函數(shù)名。
查看函數(shù)調(diào)用堆棧的命令,除了kn,還有kv和kp命令,其中kv還可以看到函數(shù)調(diào)用堆棧中調(diào)用函數(shù)時(shí)傳遞的參數(shù),如下所示:
我們需要取來(lái)pdb符號(hào)庫(kù)文件,去查看具體的函數(shù)名及行號(hào)的,這樣才好找到直接的線索的。下面就來(lái)看看如何獲取到TestDlg.exe模塊的pdb文件。
5、找到pdb文件,設(shè)置到windbg中,查看完整的函數(shù)調(diào)用堆棧
如何才能找到TestDlg.exe文件對(duì)應(yīng)的dpb文件?我們可以通過(guò)查看TestDlg.exe文件的時(shí)間戳找到文件的編譯時(shí)間,通過(guò)編譯時(shí)間找到文件對(duì)應(yīng)的pdb文件。在Windbg中輸入lm vm TestDlg命令,可以查看到TestDlg.exe文件的詳細(xì)信息,其中就包含文件的時(shí)間戳:(當(dāng)前的lm命令中使用m通配符參數(shù),所以在TestDlg后面加上了號(hào))
image.png
可以看到文件是2022年6月25日8點(diǎn)26分23秒生成的,就可以找到對(duì)應(yīng)時(shí)間點(diǎn)的pdb文件了。
一般在公司正式的項(xiàng)目中,通過(guò)自動(dòng)化軟件編譯系統(tǒng),每天都會(huì)自動(dòng)編譯軟件版本,并將軟件的安裝包及相關(guān)模塊的pdb文件保存到文件服務(wù)器中,如下所示:
這樣我們就可以根據(jù)模塊的編譯時(shí)間找到對(duì)應(yīng)版本的pdb文件了。
我們找到了TestDlg.exe對(duì)應(yīng)的pdb文件TestDlg.pdb,將其所在的路徑設(shè)置到Windbg中。點(diǎn)擊Windbg菜單欄中的File->Symbol File Path…,打開(kāi)設(shè)置pdb文件路徑的窗口,將pdb文件的路徑設(shè)置進(jìn)去,如下所示:
image.png
點(diǎn)擊OK按鈕之前,最好勾選上Reload選項(xiàng),這樣Windbg就會(huì)去自動(dòng)加載pdb文件了。但有時(shí)勾選了該選項(xiàng),好像不會(huì)自動(dòng)去加載,我們就需要使用.reload /f TestDlg.exe命令去讓W(xué)indbg強(qiáng)制去加載pdb文件(命令中必須是包含文件后綴的文件全名)。
設(shè)置完成后,我們可以再次運(yùn)行l(wèi)m vm TestDlg*命令去看看pdb文件有沒(méi)有加載進(jìn)來(lái):
image.png
如果已經(jīng)加載進(jìn)來(lái),則會(huì)在上圖中的位置顯示出已經(jīng)加載進(jìn)來(lái)的pdb文件的完整路徑。,如上所示。
加載到TestDlg.exe文件對(duì)應(yīng)的pdb文件之后,我們?cè)俅螆?zhí)行kn命令就可以包含具體的函數(shù)名及及代碼的行號(hào)信息了,如下:
image.png
我們看到了具體的函數(shù)名CTestDlgDlg::OnBnClickedButton1,還看到了對(duì)應(yīng)的代碼行號(hào)312。通過(guò)這些信息,我們就能到源代碼中找到對(duì)應(yīng)的位置了,如下所示:
image.png
是訪問(wèn)了空指針產(chǎn)生的異常。當(dāng)然上面的代碼是我們故意這樣寫(xiě)的,目的是為了構(gòu)造一個(gè)異常來(lái)詳細(xì)講解如何使用Windbg進(jìn)行動(dòng)態(tài)調(diào)試跟蹤的。
6、將C++源代碼路徑設(shè)置到Windbg中,Windbg會(huì)自動(dòng)跳轉(zhuǎn)到源代碼行號(hào)上
為了方便查看,我們可以直接在Windbg中設(shè)置C++源碼路徑,這樣Windbg會(huì)自動(dòng)跳轉(zhuǎn)到源碼對(duì)應(yīng)的位置。點(diǎn)擊Windbg菜單欄的File->Source File Path…,將源碼路徑設(shè)置進(jìn)去:
image.png
然后Windbg會(huì)自動(dòng)跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)及行號(hào)上:
image.png
然后點(diǎn)擊函數(shù)調(diào)用堆棧中每行最前面的數(shù)字超鏈接,就可以自動(dòng)切換到對(duì)應(yīng)的函數(shù)中。上圖中的函數(shù)調(diào)用堆棧中很多模塊是系統(tǒng)庫(kù)中的,比如mfc100u、User32等,這些庫(kù)是系統(tǒng)庫(kù),是沒(méi)有源碼的。我們可以點(diǎn)擊最下面的第23個(gè)鏈接,其位于我們應(yīng)用程序的模塊中,會(huì)自動(dòng)跳轉(zhuǎn)到對(duì)應(yīng)的代碼中,如下:
image.png
7、有時(shí)需要查看函數(shù)調(diào)用堆棧中函數(shù)的局部變量或C++類對(duì)象中變量的值
有時(shí)我們通過(guò)查看變量的值,找到排查問(wèn)題的線索,比如變量中值為0或者很大的異常值。這點(diǎn)我們?cè)诙啻螁?wèn)題排查中使用到,確實(shí)能找到一些線索。
可以查看函數(shù)中局部變量的值,也可以查看函數(shù)所在類對(duì)象的this指針指向的類對(duì)象中變量的值。我們要查看哪個(gè)函數(shù),就點(diǎn)擊函數(shù)調(diào)用堆棧中每一行前面的數(shù)字超鏈接,如下所示:
image.png
我們看到了局部變量pShExeInfo 的值:
struct _SHELLEXECUTEINFOW * pShExeInfo = 0x00000000
我們可以點(diǎn)擊this對(duì)象的超鏈接:
image.png
就能查看當(dāng)前函數(shù)對(duì)應(yīng)的C++類對(duì)象中成員變量的值,如下:
image.png
但有時(shí)不一定能查看變量的值,因?yàn)楫?dāng)前通過(guò)異常捕獲模塊自動(dòng)生成的dump文件一般是minidump文件,文件也就幾MB左右,不可能包含所有變量的值。所以要在minidump文件中查看變量的值,要看運(yùn)氣的,有時(shí)能查看到,有時(shí)是看不到的。這里要講一下dump文件的分類,主要分為minidump文件和全dump文件。
我們將windbg附加到進(jìn)程上使用.dump命令導(dǎo)出的dump文件,是全dump文件,全dump文件中包含了所有的信息,可以查看到所有變量的信息。另外通過(guò)任務(wù)管理器導(dǎo)出的dump文件:
image.png
也是全dump文件。全dump文件因?yàn)榘怂械男畔ⅲ詴?huì)比較大,會(huì)達(dá)到數(shù)百M(fèi)B,甚至上GB的大小。但如果通過(guò)安裝在程序的異常捕獲模塊CrashReport導(dǎo)出的dump文件就是非全dump文件,是mini dump文件,大概只有幾MB左右,因?yàn)楫惓2东@模塊捕獲到異常后,會(huì)自動(dòng)導(dǎo)出dump文件,保存到磁盤(pán)上,如果都導(dǎo)出體量很大的全dump文件,很大量消耗用戶的磁盤(pán)空間,所以我們會(huì)設(shè)置生成mini dump文件。
在異常捕獲模塊中我們是通過(guò)調(diào)用系統(tǒng)API函數(shù)MiniDumpWriteDump導(dǎo)出dump文件的,我們通過(guò)設(shè)置不同的函數(shù)調(diào)用參數(shù)去控制生成mini dump文件的。
8、有時(shí)我們需要用IDA打開(kāi)二進(jìn)制文件去查看匯編代碼上下文
有時(shí)通過(guò)函數(shù)調(diào)用堆棧中函數(shù)及行號(hào),我們很難搞清楚到底為什么會(huì)發(fā)生崩潰,這時(shí)候我們就需要回歸本源了,就需要去查看發(fā)生異常崩潰的那條匯編指令的上下文了。
匯編指令最能直觀地反映出問(wèn)題的本質(zhì),通過(guò)閱讀匯編代碼,就能搞清楚為啥會(huì)觸發(fā)崩潰了。一般我們?cè)谑褂?ecxr命令切換到發(fā)生異常的匯編指令時(shí),我們可以直接在該條匯編指令的上下文了,點(diǎn)擊菜單欄的View->Disassembly,即可打開(kāi)顯示匯編代碼的頁(yè)面,如下所示:
image.png
但直接在Windbg中查看匯編代碼,對(duì)于我們這些不精通匯編代碼的人來(lái)說(shuō),是有很大困難的,我們是大概率看不懂的。
一般需要借助IDA工具打開(kāi)二進(jìn)制文件去查看匯編代碼的,IDA在解析出匯編時(shí)會(huì)在匯編代碼中加上一些注釋,特別是在有pdb符號(hào)庫(kù)文件時(shí),會(huì)添加更多的注釋,通過(guò)這些注釋,我們就能對(duì)照了C++源代碼,就能大概讀懂匯編上下文了。如果能找到pdb符號(hào)庫(kù)文件,則需要將pdb文件放在目標(biāo)二進(jìn)制文件的同級(jí)目錄中,IDA打開(kāi)二進(jìn)制文件時(shí)會(huì)去自動(dòng)加載pdb文件。
8.1、IDA工具介紹
image.png
IDA是比利時(shí)Hex-Rays公司出品的一款交互式靜態(tài)反匯編工具。它可以直接反匯編出二進(jìn)制文件的匯編代碼,是目前軟件逆向與安全分析領(lǐng)域最好用、最強(qiáng)大的一個(gè)靜態(tài)反匯編軟件,已成為眾多軟件安全分析人員不可缺少的利器!它支持Windows、Linux等多個(gè)平臺(tái),支持Intel X84、X64、ARM、MIPS等數(shù)十種CPU指令集。
在實(shí)際工作中,我們一般使用反匯編工具IDA去打開(kāi)二進(jìn)制文件,查看二進(jìn)制文件中的匯編代碼。IDA既支持打開(kāi)Windows下的.exe、.dll等二進(jìn)制文件,也可以打開(kāi)Linux下的.bin、.so等二進(jìn)制文件。
8.2、使用IDA打開(kāi)二進(jìn)制文件
IDA安裝完成后,雙擊啟動(dòng)程序,會(huì)彈出如下的提示框:
image.png
點(diǎn)擊“New”即新建一個(gè)對(duì)象。緊接著彈出讓選擇要打開(kāi)的文件:
image.png
可以找到目標(biāo)文件的路徑,打開(kāi)目標(biāo)文件即可。也可以點(diǎn)擊取消,然后直接將文件拖到IDA中。打開(kāi)文件時(shí)會(huì)讓選擇加載文件的方式:
image.png
對(duì)于Windows庫(kù),選擇PE方式即可。接下來(lái),會(huì)彈出是否要加載pdb文件的提示框:
image.png
選擇Yes即可。前面我們說(shuō)過(guò),需要將pdb文件放置到目標(biāo)二進(jìn)制文件的同一級(jí)目錄中,這樣IDA在打開(kāi)二進(jìn)制文件時(shí)就會(huì)搜索到對(duì)應(yīng)的pdb文件,回去自動(dòng)加載pdb文件。
打開(kāi)后的效果如下:
image.png
默認(rèn)是先是Grapg View圖狀關(guān)系視圖,需要右鍵點(diǎn)擊視圖區(qū)域,在彈出的右鍵菜單中點(diǎn)擊Text View菜單項(xiàng),切換到Text View文字視圖,這樣就能看到具體的匯編代碼了:
image.png
8.3、在有pdb文件時(shí)到IDA中定位到發(fā)生異常的匯編指令的位置
Windbg中發(fā)生異常的那條匯編指令,我們需要到IDA中找到對(duì)應(yīng)的位置,然后查看目標(biāo)位置的匯編指令的上下文。在目標(biāo)二進(jìn)制文件有pdb的情況下,要在IDA中定位到發(fā)生異常的匯編指令的位置,會(huì)比較簡(jiǎn)單。
首先在Windbg中顯示的函數(shù)調(diào)用堆棧中找到發(fā)生崩潰的那條匯編指令所在的函數(shù)名CTestDlgDlg::OnBnClickedButton1,如下:
image.png
然后到IDA中,點(diǎn)擊菜單欄的Jump->Jumptofunction…,在打開(kāi)的函數(shù)列表窗口中,點(diǎn)擊下方的Search按鈕,在搜索框中輸入函數(shù)名后搜索:
image.png
在列表中找到函數(shù),雙擊之久切換到該函數(shù)的匯編代碼處:
image.png
這樣就看到該函數(shù)的代碼段地址,然后加上Windbg中顯示的偏移值:
image.png
即:
CTestDlgDlg::OnBnClickedButton1+0x32 = 0x00401A30 + 0x32 = 0x00401A62
就得到發(fā)生崩潰的那條匯編指令在當(dāng)前二進(jìn)制中的地址,因?yàn)閰R編指令就為在當(dāng)前這個(gè)函數(shù)中,鼠標(biāo)向下拉動(dòng)找到地址即可,這樣就能找到發(fā)生異常的那條匯編指令了。
其實(shí)還有個(gè)快捷操作,在計(jì)算出發(fā)生異常的那條匯編指令的地址后,按下快捷鍵G,在彈出窗口中輸入剛才的地址0x00401A62,就可以直接go到發(fā)生異常的那條匯編指令的位置,如下:
image.png
8.4、在沒(méi)有pdb文件時(shí)到IDA中定位到發(fā)生異常的匯編指令的位置
在沒(méi)有目標(biāo)二進(jìn)制文件的pdb文件的情況下,要在IDA中定位到發(fā)生異常的匯編指令的位置,會(huì)相對(duì)麻煩一點(diǎn)。
發(fā)生異常的匯編指令的地址,是程序?qū)嶋H運(yùn)行時(shí)的代碼段地址,需要在Windbg中計(jì)算出該條匯編指令的地址相對(duì)于所在模塊起始地址的偏移值,然后加上IDA中該模塊的的默認(rèn)加載地址,就能得到當(dāng)前匯編指令在IDA中的靜態(tài)地址,然后直接go過(guò)去,就能看到產(chǎn)生異常的匯編指令的上下文了。在本例中,發(fā)生異常的匯編指令的地址就是0x00401a62,如下:(發(fā)生異常的這行匯編指令最前面的那個(gè)地址,就是當(dāng)前匯編指令的地址)
image.png
從函數(shù)調(diào)用堆棧上看,當(dāng)前這條發(fā)生異常的匯編指令所在的函數(shù)為:
TestDlg!CTestDlgDlg::OnBnClickedButton1+0x32
所以這條指令所屬模塊為T(mén)estDlg.exe,所以使用lm命令查看TestDlg.exe模塊的起始地址:
image.png
TestDlg.exe模塊其實(shí)地址為0x00400000(這是系統(tǒng)將TestDlg.exe模塊加載到進(jìn)程空間中的起始地址,代碼段地址 ),所以發(fā)生異常的這條匯編指令相對(duì)于所在模塊TestDlg.exe的偏移地址為:
0x00401a62 – 0x00400000 = 0x00001a62
然后再到打開(kāi)TestDlg.exe二進(jìn)制文件的IDA中,查看該TestDlg.exe模塊默認(rèn)的加載地址:(將滾輪滾動(dòng)到最上面即可看到,注意此處是默認(rèn)預(yù)加載地址,并不是加載起來(lái)后的真正的地址)
image.png
這樣根據(jù)之前計(jì)算出來(lái)的偏移,加上TestDlg.exe模塊的默認(rèn)加載地址,就得到發(fā)生異常的那條匯編指令在IDA打開(kāi)的靜態(tài)文件中的位置:
0x00001a62 + 0x00400000 = 0x00401a62
然后到IDA中按下G快捷鍵,GO到0x00401a62地址處,即找到發(fā)生異常的那條匯編指令:
image.png
在IDA中找到發(fā)生異常的匯編指令的位置,就可以去查看其附近的匯編代碼上下文了。
8.5、閱讀匯編代碼上下文
我們?cè)陂喿x匯編代碼的上下文時(shí),一般是對(duì)照著C++源碼進(jìn)行的,然后依托匯編代碼上下文中的注釋,找到匯編代碼與C++源碼的對(duì)應(yīng)關(guān)系。在閱讀匯編代碼時(shí),要了解一些常用的匯編指令及常用寄存器的使用(比如EAX用來(lái)存放函數(shù)的返回值,ECX用來(lái)傳遞C++對(duì)象地址的),熟悉函數(shù)調(diào)用時(shí)的參數(shù)入棧、棧分布及參數(shù)尋址,了解內(nèi)存拷貝的匯編代碼實(shí)現(xiàn)、了解虛函數(shù)調(diào)用的二次尋址的過(guò)程,去啃發(fā)生異常的那條匯編指令的上下文中的匯編代碼。
匯編指令比較多,我們只需要了解一些常用的匯編指令即可,如果遇到不熟悉的匯編指令去搜索一下就可以了。此外,有一點(diǎn)需要注意的是,在Release下編譯器會(huì)對(duì)C++源碼會(huì)做優(yōu)化,部分C++源碼可能會(huì)被優(yōu)化掉,C++源碼有時(shí)不能完全和匯編代碼對(duì)應(yīng)起來(lái)的,但這基本不影響匯編代碼上下文的閱讀。