1. 程式人生 > >定位window程式Crash常用工具和方法

定位window程式Crash常用工具和方法

一、引言
  任何程式正確則只有一種結果,但是錯誤卻有千萬種,而眾多的錯誤有些是可容忍,有些則是致命的,如除零錯誤、堆疊溢位、記憶體越界等導致程式Crash。由於很多錯誤並不是發生在開發工作者除錯階段,而是在使用者或測試工作者使用階段;這就需要相關程式碼維護工作者對於程式異常捕獲收集現場資訊。
  當收集相關資訊後,如何定位這些錯誤是的極為講究的過程,工具和方法使用得當則可事半功倍,反之事倍功半,所謂工欲善其事,必先利其器,本文將講述一些定位Windows客戶端程式Crash的工具(當然這些工具不僅僅只能定位Crash問題,同時本文不詳細描述工具的詳細使用幫助)和方法,對定位相關問題,提供一些借鑑意義,使之能夠少走彎路,快速定位問題、解決問題。

二、符號檔案
  發生Crash的時機並不總存在於開發機器,即時發生在開發機器上面也不一定存在於除錯狀態下;當Crash不是發生在除錯狀態下,如果需要迅速搭建一個除錯環境,這離不開符號檔案。
  符號檔案(Symbol Files)是一個數據信息檔案,它包含了應用程式二進位制檔案(比如:EXE、DLL等)除錯資訊和專案狀態資訊,使用這些資訊可以對程式的除錯配置進行增量連結,最終生成的可執行檔案在執行時並不需要這個符號檔案,但你的程式中所有的變數資訊都記錄在這個檔案中。
  在 Windows 系統中,符號檔案以 .PDB(程式資料庫Program Database)為副檔名,比如:每個 Windows 作業系統下有一個 GDI32.dll 檔案,編譯器在編譯該 DLL 的時候會產生一個 GDI32.pdb 檔案,一旦你擁有了這個 PDB 檔案,那麼便可以用它來除錯並跟蹤到 GDI32.dll 內部。該檔案和二進位制檔案的編譯版本密切相關,比如修改了 DLL 的輸出函式(哪怕是沒有何人修改),再編譯該 DLL,那麼原先的 PDB 檔案就過時了,不能再用老的 PDB 檔案來做除錯工作,而必須使用最新的 PDB 檔案版本。

  如何生成符號檔案呢,下面以VC6為例,說明如何生成一份PDB檔案。
  首先在選單“Project”->子選單“Project Settings”的選項卡“C/C++”中,將“Debug info”設定為“Program Database”


  其次在選單“Project”->子選單“Project Settings”的選項卡“Link”中,將“Genrate debug info”勾選


  然後在選單“Project”->子選單“Project Settings”的選項卡“Link”中,將“Category”選擇為“Customize”,指定PDB檔案輸出路徑和名稱


  最後儲存好相應的PDB檔案

三、dump檔案的獲取方法
  對於定位Crash問題,特別是非除錯環境下的Crash問題,這就需要採取一些方法用來獲取異常退出是的dump資訊,常見方法如下
  1. 使用SetUnhandledExceptionFilter函式,設定最高一級的異常處理函式,當程式出現任何未處理的異常,都會觸發你設定的函式裡,然後在異常處理函式中獲取程式異常時的呼叫堆疊、記憶體資訊、執行緒資訊等。
  2. 使用WinDBG工具,當程式執行後,使用WinDBG工具Attach這個程序,當程式異常時,則被WinDBG捕獲,使用工具相關命令匯出dump檔案或直接除錯。
  3. 當程式執行後,使用指令碼解釋程式Script.exe執行WinDBG下面的adplus.vbs指令碼,當程式異常退出後,則會在WinDBG目錄下面生成dump檔案。
四、WinDBG
  相對於Windows程式設計師來說,掌握WinDBG(Debugging Tools for Windows)工具的使用,可大大提升客戶端開發工作者定位問題能力。
  WinDBG是微軟公司提供的免費的Windows平臺的除錯工具。適用於Win2000以上的操作系,WinDBG可以在沒有安裝開發環境的機器上快速搭建一個除錯環境。
  當開發工作者收集到一份Crash的dump檔案後,並獲取到相應符號檔案(即按照上述描述方法生成的pdb檔案)使用WinDBG開啟相應dump檔案,並在選單“File”->“Symbols File Path”中設定好pdb檔案所在的目錄


  此時即刻得到下面類似的介面,然後在紅圈內輸入除錯命令,如.ecxr、Kb之類命令則可以檢視Crash時的Call Stack、記憶體資訊等,如下面程式定位該局cout << 0x7fff / I 出現Crash


  然後檢視相應記憶體資訊


  發現是I = 0,出現除0錯誤,導致程式Crash
五、AQTime
  AQTime是AutomatedQA的產品,主要運用於效能分析和記憶體除錯等。通過AQTime不僅可以得知其專案中確實存在bug和瓶頸,而且會知道具體到哪個模組、類、執行緒、程式碼行出了問題,從而快速消除錯誤。
  本文不涉及使用AQTime檢測程式效能問題,主要談及對Crash檢測問題。
  1. 將生成的pdb檔案與即將執行的exe檔案放置於同一個目錄下面
  2. 啟動AQTime,同時新建一個Module,將所需測試的exe加入Module


  3. 然後點選選單“Run”->“Run”,如果程式出現異常,則可在Event View視窗中則可檢視到發生異常時的記錄,如下例子表現為程式遞迴調用出現Bug,發生堆疊溢位問題


六、Application Verifier
  Application Verifier 是微軟的程式碼驗證工具,被設計為檢測程式執行記憶體錯誤和其他嚴重的安全隱患,可以找出在正常程式程式碼檢測中難以察覺的錯誤;該工具的使用也非常簡單:執行程式讀入程式碼即可,很快就能生成一個log,相關人員根據log分析相關程式碼即可。
  1. 使用者將需要測試的例項新增Application Verifier,並點選儲存按鈕


  2. 此時執行相應程序,如果程式發生異常,則點選Application Verifier的選單“View”->“Log”,檢視是否有發生錯誤的log日誌


  3. 檢視發生錯誤的日誌,如發生Crash例子顯示錯誤資訊如下

Access violation exception.

3cc - Invalid address causing the exception

40350c - Code address executing the invalid access

12fcac - Exception record

12fcc8 - Context record

vrfcore!VfCoreRedirectedStopMessage+81

kernel32!UnhandledExceptionFilter+fb

Ex!_XcptFilter+2e

Ex!mainCRTStartup+10f

kernel32!RegisterWaitForInputIdle+49


根據log日誌提供的12fcac - Exception record和12fcc8 - Context record資訊,結合map檔案,則可發現程式發生越界訪問
void main()
{
  getch();
  int arrData[1000];
  for (short i = 0; i <= sizeof(arrData)/ sizeof(arrData[0]); ++i)
  {
    arrData = i;
  }
  cout << arrData[1000];
}
七、Purify
  IBM Rational Purify是由IBM開發一款用於對C/C++源程式中存在的記憶體問題進行勘察和分析,並且提供了有關的例項以便讀者在實際操作中作為參考的工具。
  程式執行時的分析可以採用多種方法。Purify使用了具有專利的目的碼插入技術(OCI:Object Code Insertion)。她在程式的目的碼中插入了特殊的指令用來檢查記憶體的狀態和使用情況。這樣做的好處是不需要修改原始碼,只需要重新編譯就可以對程式進行分析。
如下例子
   #include 
   using namespace std;
   int main(){
      char* str1="four";
      char* str2=new char[4];
      char* str3=str2;
      cout<<str2< 
      strcpy(str2,str1);
      cout<<str2<<endl;
      delete str2;
      str2[0]+=2;
      delete str3;
   }
  當編譯完成後,並生成pdb,將pdb放置於應用程式同級目錄,然後將需要測試的程式新增入Purify,點選“Run”,程式在執行過程中,可以在右側視窗中不斷看見相應資訊輸出,相關人員檢視相關輸出是否是有問題。


  如上述例子,則可發現發生陣列越界訪問。
八、BoundChecker
  BoundChecker工具對於大多數VC開發工作者來說均不陌生,該工具採用目的碼插入技術,主要應用於檢測記憶體、資源等洩漏和記憶體操作操作等問題,該工具主要針對開發者使用,開發者在使用階段使用BoundChecker編譯,程式碼並執行,檢查程式碼是否存在隱患導致程式出現執行異常或執行時錯誤。
  使用BoundChecker編譯並在Debug模式下執行則可發現一些異常Errors或記憶體的錯誤操作。


  該工具存在一定的侷限性,主要只能使用在開發除錯階段。
九、過載記憶體分配釋放符
  程式發生異常其中很大肯能是因為記憶體的錯誤訪問或使用所致,總的說來,與記憶體有關的問題可以分成兩類:記憶體訪問錯誤和記憶體使用錯誤。記憶體訪問錯誤包括錯誤地讀取記憶體和錯誤地寫記憶體。錯誤地讀取記憶體可能讓你的模組返回意想不到的結果,從而導致後續的模組執行異常。錯誤地寫記憶體可能導致系統崩潰。記憶體使用方面的錯誤主要是指申請的記憶體沒有正確釋放,從而使程式執行逐漸減慢,直至停止。這方面的錯誤由於表現比較慢很難被人工察覺。程式也許運行了很久才會耗淨資源,發生問題,對於這類問題開發工作者可以過載開發工具提供的記憶體分配釋放操作,加入一些定位手段,用於捕捉記憶體有關問題,如下例程所示:
過載new操作
void * operator new (unsigned int size) 
{
       //建立的頁面個數
       size += sizeof(unsigned int);
       int page_num = (int) ( (size + 4096 - 1) / 4096 );
       //偏移量
       unsigned int offset = page_num * 4096 - size + sizeof(unsigned int);
       //在記憶體塊後面建立一個額外的保護頁面,並且將頁面的屬性設定為不可讀寫
       unsigned int allocsize = (page_num + 1) * 4096;
       void *p = VirtualAlloc(NULL, allocsize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
       *((unsigned int *)p) = allocsize;
       //定位最後一個保護頁面的地址
       void *pchecker = (char *)p + page_num * 4096;
       //設定最後一頁為不可讀寫
       DWORD old_value;
       VirtualProtect(pchecker,4096,PAGE_NOACCESS,&old_value);
       //OutputDebugString("my new ") ;
       return (char *)p + offset;
}
void main()
{
       int * p = new int[10];
       p[10] = 2;
       delete [] p;
}
  分配陣列時在陣列尾部多分配一段記憶體,並將該段記憶體設定為不可訪問,當陣列訪問越界,則程式執行到p[10] = 2語句時,將發生錯誤,如果完善一下new的過載,則可以知道發生異常訪問所在的檔案和位置。
  另外如果發生記憶體洩漏,也可根據分配和釋放的記錄檢查是否發生記憶體洩漏。
  上述方法也存在一定侷限性,只能運用於開發除錯階段。
十、規範日誌
  程式Crash極可能是我們自身業務執行邏輯出現問題,不是按照想象的方式執行,導致資料發生同步問題而導致程式異常問題,如
Class::FuncA()
{
      Lock();
      OutputDebugString(“Class::FuncA: 修改M_oVar之前”);
      OperationA(M_oVar);
      OutputDebugString(“Class::FuncA: 修改M_oVar之後”);
      Unlock();
}
Class::FuncB()
{
      Lock();
      OutputDebugString(“Class::FuncB: 修改M_oVar之前”);
      OperationB(M_oVar);
      OutputDebugString(“Class::FuncB: 修改M_oVar之後”);
      Unlock();
}
Class::FuncC()
{
      Lock();
      OutputDebugString(“Class::FuncC: 修改M_oVar之前”);
      OperationB(M_oVar);
      OutputDebugString(“Class::FuncC: 修改M_oVar之後”);
      Unlock();
}
  其中主執行緒
Mainthread()
{
      Class::FuncA();
Class::FuncB();
}
  工作執行緒
Workthread()
{
      Class::FuncC();
}
  如果我們本來要求AB需要同時執行,也就是需要執行緒安全訪問執行,表面上看AB兩個函式都是執行緒安全的,但是日誌卻會出現
Class::FuncA: 修改M_oVar之前
Class::FuncA: 修改M_oVar之後
Class::FuncC: 修改M_oVar之前
Class::FuncC: 修改M_oVar之前
Class::FuncB: 修改M_oVar之前
Class::FuncB: 修改M_oVar之後
與我們想象的執行順序
Class::FuncA: 修改M_oVar之前
Class::FuncA: 修改M_oVar之後
Class::FuncB: 修改M_oVar之前
Class::FuncB: 修改M_oVar之前
Class::FuncC: 修改M_oVar之前
Class::FuncC: 修改M_oVar之後
  完全不一致,通過完善的日誌即可看出工作流程是否,可以方便定位發生錯誤問題。
  要想通過日誌分析流程正確否,則需要規範程式碼中的日誌格式和內容,比如日誌中需要包含輸出時間,當前執行緒號,所屬模組號/類名稱,函式名稱,當前關鍵變數的值等資訊。
十一、程式碼回溯
  當無法從當前程式碼發現是否存在問題時,可以從最近版本中找出一個不存在相應問題和存在相應問題的兩個相鄰版本,通過比較程式碼,檢查是那塊程式碼引入,這樣縮小範圍後可重點排查。
  該方法簡單易行,但是如果相鄰兩個版本改動幅度較大,則變為不可實施。
十二、功能/模組遮蔽
  如果功能/模組分配合理,則可將一塊塊模組或功能遮蔽,確定是何種模組或功能出現問題,類似程式碼回溯,重點突出,可該方法同樣簡單易行,但是如果模組或功能耦合比較嚴重,則可實施性不強。
十三、模組替代
  當懷疑某個模組存在問題,但是又無法定位或不方便定位,此時可以採用另外的具有同樣功能的模組來檢查驗證猜想是否正確。
十四、專家諮詢
  漢高祖曾經說過:夫運籌帷幄之中,決勝千里之外,吾不如子房;鎮國家,撫百姓,給餉饋,不絕糧道,吾不如蕭何;連百萬之眾,戰必勝,攻必取,吾不如韓信。三者皆人傑,吾能用之,此吾所以取天下者也。項羽有一范增而不用,此所以為我所禽也。
  上述古人語在當今同樣有意義,能夠善於利用周邊資源,包括諮詢周邊專家,,快速定位解決問題同樣值得可取,這一點可能是眾多開發工作者所不願取的,合適時機、合適方法、放下文無第一,武無第二的心態,求教經驗豐富的同事,同樣可以事半功倍。
  例如前段時間,旋風經常在STL的string類中的分配釋放記憶體是不定時間不定位置Crash,後諮詢相關專家,很快發現了VC6提供string類的缺陷所致問題。
十五、PCLnt
  程式碼編寫者如果書寫不夠規範、可讀性差,這樣非常容易引入BUG,程式碼編寫除錯完成後,可以使用當前成熟的工具PCLnt檢查程式碼是否存在變數沒有賦初值,訪問是否越界;由於程式碼是靜態檢查,只能起到輔助作用,不能發現執行時錯誤資訊。


十六、程式碼Review
  程式碼開發工作者開發功能由於很大程度是黑盒測試(我們公司可能絕大部分產品如此),這樣很多問題就依賴開發人員的水平了,但是團隊人員多了之後,水平參差不齊,再加上不斷的開發任務,很多隱藏的BUG可能就成為一顆定時炸彈,如linux前段時間發現了一個存在10多年的BUG。
  為了保證程式碼質量,相同專案組應定期Review程式碼,特別是出現Crash後,應集中人力,安排專門的時間,大規模Review程式碼,儘可能將Bug現形。
總結
  程式出現Bug乃至Crash,均不可怕,可怕是不能找到快速的定位手段,上述也只是就旋風定位Crash時使用到的一些手段,希望能夠對以後的定位所有幫助。
  總之,最重要的還是不斷熟悉自身所維護程式碼,閱讀高手所寫程式碼,同時不斷充電檢視資料,勤修內功,儘可能使自己免於充當Bug製造者。
</str2<<endl;
</str2<