使用WinDbg調試程序


使用WinDbg調試程序

 

WinDbg是微軟發布的一款相當優秀的源碼級(source-level)調試工具,可以用於Kernel模式調試和用戶模式調試,還可以調試Dump文件。

WinDbg是微軟很重要的診斷調試工具: 可以查看源代碼、設置斷點、查看變量, 查看調用堆棧及內存情況。

 調試應用程序(用戶模式 user mode)

 調試操作系統及驅勱程序(內核模式 kernel mode)

 調試非托管程序(native program)

 調試托管程序(managed program)

 實時調試 (JIT: Just in time)

 事后調試 (postmortem debugging)

使用WinDbg可以解決線上.NET應用程序的如下問題:

◆ 內存高

◆ CPU高

◆ 程序異常

◆ 程序Hang死

 

在生產環境下進行故障診斷時,為了不終止正在運行的服務或應用程序,有兩種方式可以對正在運行的服務或應用程序的進程進行分析和調試。

一、用WinDbg等調試器直接attach到需要調試的進程,調試完畢之后再detach即可。但是這種方式有個缺點就是執行debugger命令時必須先break這個進程,執行完debug命令之后又得趕緊F5讓他繼續運 行,因為被你break住的時候意味着整個進程也已經被你掛起。另外也經常會由於First Chance Excetpion而自動break,你得時刻留意避免長時間break整個進程。所以這樣的調試方式對時間是個很大的考驗,往往沒有充裕的時間來做仔細分析。

二、在出現問題的時候,比如CPU持續長時間100%,內存突然暴漲等非正常情況下,通過對服務進程snapshot抓取一個dump文件,完成dump之后先deatch,讓進程繼續運行。然后用windbg等工具來分析這個抓取到的dump 文件。所以我們一般采用這種方式來進行調試排錯。

 

設置符號文件目錄

符號文件包含了相關二進制文件的調試信息以.pdb戒.dbg為擴展名。WinDbg使用符號文件來確定調用棧,堆及其他重要信息。

配置WinDbg的符號文件路徑

WinDbg符號文件路徑搜索的兩個位置:環境變量中的_NT_SYMBOL_PATH設置及WinDbg中的"symblos file path";

設置srv*x:/symbols_folder*http://msdl.microsoft.com/download/symbols 路徑是保證我們能快速正確使用windbg的法。

1、運行WinDbg->File->Symbol File Path->按照下面的方法設置_NT_SYMBOL_PATH變量:

在彈出的框中輸入"C:\ Symbols; SRV*C:\MyLocalSymbols*http://msdl.microsoft.com/download/symbols"(按照這樣設置,WinDbg將先從本地文件夾C:\ Symbols中查找Symbol,如果找不到,則自動從MS的Symbol Server上下載Symbols)。另一種做法是從這個Symbol下載地址中http://www.microsoft.com/whdc/devtools/debugging/symbolpkg.mspx,下載相應操作系統所需要的完整的Symbol安裝包,並進行安裝,例如我將其安裝在D:\WINDOWS\Symbols,在該框中輸入"D:\WINDOWS\Symbols"。(這里要注意下載的Symbols的版本一定要正確

2、在控制板的系統中設置一個系統變量_NT_SYMBOL_PATH 為

SRV*c:\symbols*http://msdl.microsoft.com/download/symbols

 

dump文件獲取

dump文件是進程的內存鏡像。可以把程序的執行狀態,即當時程序內存空間數據通過調試器保存到dump文件中。

1、利用WinDbg里的adplus來獲取dump文件

Adplus.vbs 是一個Visual Basic Script 文件,Adplus 主要用來生成內存轉儲文件 (dump file),內存轉儲文件適用於不能實時調試的情況下。在WinDbg安裝目錄里可以找到adplus.vbs,使用adplus.vbs生成dump文件,

adplus -hang -o d:\dump -p 1234

其中hang表示附加到進程,如果是crash,則為目標進程崩潰的時候抓取,-o后面的參數表示dump文件存到位置,-p后面的數字為進程的PID,也可以是-pn后面跟進程名稱,如:adplus.vbs -hang -pn ConsoleWindbg.exe -o D:\dump

2、使用Debug Diagnostic Tool(DebugDiag)工具獲取dump文件

下載Debug Diagnostic Tool然后進行安裝,打開該工具,Debug Diagnostic Tool可以選擇不同的規則來進行dump文件。可以根據程序崩潰時捕獲dump文件,也可以根據性能指標來進行捕獲,如CPU過高,死鎖,HTTP響應時間過程等參數。如下圖:

也可以找到對應的進程,通過如下方法進行捕獲。此種方式獲取的dump文件放到C:\Program Files\DebugDiag\Logs\Misc下。

3、使用.dump命令

1) 打開WinDBG—>File—>Attach to a Process,然后選擇將之要進行捕獲的進程。如我們這里要對ConsoleWindbg.exe進程產生dump文件。選擇后如圖:

2)在上圖紅色區域的輸入框內輸入產生dump 文件的命令 .dump 可以選擇不同的參數來生成不同類型的dump文件。

選項(1): /m

命令行示例.dump /m D:/dump/myapp.dmp

注解: 缺省選項,生成標准的minidump, 轉儲文件通常較小,便於在網絡上通過郵件或其他方式傳輸。 這種文件的信息量較少,只包含系統信息、加載的模塊(DLL)信息、 進程信息和線程信息。

選項(2): /ma

命令行示例.dump /ma D:/dump/myapp.dmp

注解: 帶有盡量多選項的minidump(包括完整的內存內容、句柄、未加載的模塊,等等),文件很大,但如果條件允許(本機調試,局域網環境), 推薦使用這中dump。

 選項(3):/mFhutwd

命令行示例.dump /mFhutwd D:/dump/myapp.dmp

注解:帶有數據段、非共享的讀/寫內存頁和其他有用的信息的minidump。包含了通過minidump能夠得到的最多的信息。是一種折中方案。

4、使用ProcDump工具

Procdump是一個輕量級的命令行工具, 它的主要目的是監控應用程序的CPU異常動向, 並在此異常時生成crash dump文件, 供研發人員和管理員確定問題發生的原因。你還可以把它作為生成dump的工具使用在其他的腳本中。有了它, 就完全不需要在同一台服務器上使用諸如32位系統上的Debug Diag 1.1或是64位系統上的ADPlus了。

Procdump下載:http://technet.microsoft.com/en-us/sysinternals/dd996900

procdump -ma -c 50% -s 3 -n 2 5844 (Process Name or PID) -o c:\dumpfile

-ma 生成full dump, 即包括進程的所有內存. 默認的dump格式包括線程和句柄信息。

-c 在CPU使用率到達這個閥值的時候, 生成dump文件。

-s CPU閥值必須持續多少秒才抓取dump文件。

-n 在該工具退出之前要抓取多少個dump文件。

-o dump文件保存目錄。

 

技術術語

GC Heap:用於存儲對象實例,受 GC 管理

Loader Heap:分為 High-Frequency Heap 、 Low-Frequency Heap 和 Stub Heap ,不同的 heap 又存儲不同的信息。 Loader Heap 中最重要的信息是元數據 (MetaData) 相關的信息,也就是 Type 對象,每個 Type 對象在 Loader Heap 上體現為一個 Method Table , Method Table 中記錄了存儲的元數據信息,如基類型、靜態字段、實現的接口、所有的方法等。 Loader Heap 的生命周期為從 AppDomain 創建到卸載。

MethodTable: 我們知道每種type可以有多個instance,每個instance,其每個field可以享有獨立的space,而對於type的method提供一個公共的method入口地址。也就是說不管多少個相同類型的instance,其都指向了同一個同一的函數入口地址。在這個函數入口地址描述表中記錄了各個函數的入口地址。而MethodTable就有點類似的作用。不過所有Assembly都是自描述的,因此我們可以從MethodTable中,可以知道相應的instance。因此通過相應的debug命令!dumpheap -mt MTAddress可以知道在MethodTable中相關聯的所有instance了。

Finalization 原理

 

通過WinDbg分析dump文件

通過上面步驟,我們生成了dump文件,接下來我們就可以使用WinDbg工具對生成的dump文件進行分析。

案例:

建立控制台應用程序,代碼如下:

namespace ConsoleWindbg

{

class Program

{

private static List<User> list =new List<User>();

static void Main(string[] args)

{

MemeryLeakProc();

Console.ReadLine();

}

 

private static void MemeryLeakProc()

{

string str = "aaa";

while (true)

{

for (int i = 0; i < 100 * 1024; i++)

{

str += "bbb" + i;

User u = new User();

u.Age = i;

u.Name = "UserName" + i;

list.Add(u);

}

Thread.Sleep(1000);

}

}

 

}

 

public class User

{

public int Age { set; get; }

public string Name { set; get; }

}

}

編譯,運行,按照上面的步驟產生dump文件。然后使用WinDbg打開dump文件。

紅色標注區域顯示了dump文件獲取的一些環境信息,如:當前系統信息,程序運行時間,符號文件的路徑等。

WinDbg調試托管程序時需用SOS擴展(SOS.dll), SOS 調試擴展(SOS.dll) 通過提供有關內部公共語言運行時(CLR) 環境的信息,幫助您在WinDbg.exe 調試器和Visual Studio 中調試托管程序。SOS.dll安裝在.Net Framewok 目錄底下C:\Windows\Microsoft.NET\Framework\vx.x.xxxxx。WinDbg調用SOS.dll的語法:

SOS.dll 在.Net Framewok 目錄底下,在WinDbg的命令行輸入:

.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll

拷貝SOS.dll 到windbg目錄底下,注意拷貝的Framework版本必須和你要調試的目標程序所使用的版本一致,否則調試信息就不能正確顯示出來。如果你同時工作在兩個版本的Framework的話,可以SOS文件重命名為SOS<version>.dll或者直接將它們放入不同的文件夾下面。可以使用如下命令:

.load sos.dll

.loadby sos mscorwks  [.Net 3.5版本及以下]

     檢查SOS.dll是否已經裝載

    .chain

內存過高問題

內存過高問題初判定,內存泄漏可以通過以下兩種方式通過性能監視器來基本判斷屬於那種類型的內存泄露。

A、非托管程序的症狀 (Perfmon工具)

Process\Private Bytes 增加

.NET CLR Memory\# Bytes in all heaps不增加

B、托管程序的症狀 (Perfmon工具)

Process\Private Bytes增加

.NET CLR Memory\# Bytes in all heaps也增加

本例屬於托管程序症狀,通過Perfmon工具已經得到判斷,此步驟略。

  1. 測試程序

    BuggyBits(http://blogs.msdn.com/cfs-filesystemfile.ashx/__key/communityserver-components-postattachments/00-07-43-14-54/BuggyBits.zip

    下載后部署到本機,產生壓力:

    tinyget -srv:dbg.buggybit.com -uri:/Links.aspx -loop:4000

    觀察進程中w3wp進程,發現內存增長很快,大約該進程內存增長到700M時,抓取一個hang dump。

  1. 與內存操作相關命令

    !eeheap –gc:正是查看GC堆得命令,查看GC堆上的內存占用是多大

    !eeheap –loader:正是查看Loader堆得命令

    !dumpheap –stat:就是GC堆上的統計,就看看GC堆上存活的對象是那些

    !dumpheap -mt <<MethodTable address>>:查看該地址上的對象

    !dumpheap –type:通過 type 參數查看內存中指定類型的對象

    !gcroot +對象地址:這個命令就可以得到這個對象的"根"

    !objsize +對象地址:查看對象占用多大的內存

    !name2ee TestClass.exe TestClass.Program.test ://顯示test方法相關的地址

    !dumpmt -md 00976d48 ://得到類的成員函數詳細信息

    !dumpmt:找到相關MethodTable處的相關信息。

    !dumpmd:根據MethodDesc找到相關模塊信息,比如MethodTable.

    !dumpdomain:顯示所有域里的程序集,或者根據參數獲取指定域。

    !dumpil 00973028:// 顯示這個方法被編譯器編譯之后的IL代碼

    !dumpobj(do) 012a3904: //顯示一個對象的具體內容,看對象里面有什么,值是什么

    !dumpmodule 1ee30010:查看某個模塊的詳細信息

    !DumpArray: //查看數組信息

  2. 分析過程

3.1、運行命令!eeheap –gc查看GC堆的情況,發現GC Heap大小為720多兆,所以我們重點分析托管堆的情況。從運行結果可以看到GC Heap中g0,g1,g2和LOH的堆情況,以及該GC Heap中所分配的段情況。

可以運行!dumpheap -mt 0c3b0038 0c3b0048命令查看LOH堆中大對象的情況。從統計結果看,LOH堆中沒有大的對象存在。同理我們也可以統計各個段上對象的情況。

3.2、接下來看下heap中對象的一些情況,運行命令!dumpheap –stat。統計堆上所有對象的情況。統計項包括MT(Method Table),Count對象個數,TotalSize對象所占用的大小。Count與TotalSize按照升序統計。

最終我們發現,System.Char[]占用內存最多,大概720M,同時有36088個System.Char[]對象。

這里我們做個推理,通過!dumpheap –stat統計到的System.Char[]的個數,應該與在3.1中顯示的各個段中System.Char[]個數之和相等。即如果對3.1中統計到的各個段進行!dumpheap –stat <startAddress> <endAddress>統計,各個段中統計到的System.Char[]個數之和應該與3.2中統計到的結果相同,通過驗證發現,結論正確。

3.3、過濾一下,看看10K以上大小的字符串,運行命令:!dumpheap -mt 6f021ee4 -min 10000。10K以上的有35996個。

3.4、隨便找個對象看下引用關系,運行!gcroot 36278028,結果如下:

通過結果發現,Link引用了這個字符串。而且我們看到,link是在Finalizer Queue中的。有關Finalizer Queue可以參考.net Finalization原理。

3.5、通過運行命令! Finalizequeue 查看Finalizer Queue隊列的情況。

00b740fc 35987 575792 Link

一共有35987個Link對象存在於Finalizer Queue中,因此可以判定,Link類一定是顯示的實現了Finalize方法。

3.6、查看該方法,代碼如下:

  ~Link()

  {

  //some long running operation when cleaning up the data

  Thread.Sleep(5000);

}

3.7、接下來我們看下Link對象的結構,可以通過3.4步驟中運行出來的結果找到對應那個Link對象的地址,通過運行命令!do 36277ffc 來查看,當然也可以通過找到Link對象的MT,通過查看!dumpheap –mt <MTAddress>上的所有Link對象,找到其中一個地址,在通過!do <address>來查看。

據此發現,Link應該有url和name兩個屬性。通過!objsize 362a66ec查看url對象的大小為20k,且是StringBuilder類型的。

sizeof(362a66ec) = 20040 ( 0x4e48) bytes (System.Text.StringBuilder)

3.8、查看代碼

public Link(string name, string url)

  {

  this.name = name;

  this.url.Append(url);

  }

會引起垃圾回收器托管堆速度的幾個問題

 1、分配太頻繁

 2、預先分配空間

 3、太多的引用(pointers)和根(roots)

 4、太多的對象實例有很長的生命期

 5、太多的定位對象實例(pinned)

6、有終結函數的對象實例

         占用更多資源

         更長的生命期

         兩次才能回收

         垃圾回收器(GC)只有一個線程來運行終結函數

         有時這個線程會很慢戒堵塞(blocked)

CPU/異常操作相關命令

查看引起CPU過高命令比如:

!threadpool:查看線程池CPU使用量,我認為WEB的比如iis應用程序池進程w3wp如果CPU使用過高,那查看線程池命令肯定看的出來過高,這個是我自己的理解,c/s的就不一定了。

!threads:查看所有托管線程情況

!clrstack:到具體某個線程后,本線程托管代碼的調用棧情況

~* e !clrstack:所有線程托管代碼的調用棧情況

!runaway:查看線程占用CPU時間,可以從中找到哪個線程占用時間更高。

~number s:number為具體哪個線程的ID。

!dumpstackobjects(!dso):本線程調用棧所有對象實例

!dumpdomain:顯示所有域里的程序集,或者根據參數獲取指定域。

!savemodule:根據具體程序集地址,把當前程序集的代碼生成到指定文件

!PrintException:顯示在當前線程上引發的最后一個異常錯誤信息

!StopOnException:在指定異常錯誤信息停止運行

!VerifyHeap:檢查垃圾回收器堆中是否有損壞跡象,並顯示找到任何錯誤

!SyncBlk –all:顯示所有SyncBlock 結構情況

 

4.1、產生壓力

TinyGet.exe -srv:dbg.buggybit.com -uri:/AllProducts.aspx -threads:5 -loop:1

4.2、通過Procdump抓取三個dump文件

procdump -ma -c 50% -s 2 -n 3 w3wp.exe -o d:\dump

4.3、打開這三個dump,加載sos之后,分別查看!runaway的輸出。

第一個dump輸出:

0:023> !runaway

User Mode Time

Thread Time

23:14c8 0 days 0:00:06.817

22:1b74 0 days 0:00:06.084

31:ba8 0 days 0:00:02.823

30:680 0 days 0:00:02.823

33:25c 0 days 0:00:00.280

35:13c8 0 days 0:00:00.218

第二個dump輸出:

0:023> !runaway

User Mode Time

Thread Time

23:14c8 0 days 0:00:12.792

22:1b74 0 days 0:00:11.918

31:ba8 0 days 0:00:04.009

30:680 0 days 0:00:03.712

35:13c8 0 days 0:00:01.965

33:25c 0 days 0:00:01.887

34:14e4 0 days 0:00:00.514    

第三個dump輸出:

0:023> !runaway

User Mode Time

Thread Time

23:14c8 0 days 0:00:18.969

22:1b74 0 days 0:00:17.160

31:ba8 0 days 0:00:05.382

30:680 0 days 0:00:04.804

35:13c8 0 days 0:00:03.151

33:25c 0 days 0:00:02.792

34:14e4 0 days 0:00:01.185

4.4、從上面三個輸出結果發現只有22,23,30,31號線程一值在增長。且22,23線程增長的速度較快。查看三個dump的!threadpool基本都在90%以上。

4.5、運行!threads查看當前都有哪些線程。

不知道為什么並沒有找到22,23線程,有30,31線程號。

4.6、切換到30線程中,運行命令~30s

4.7、查看當前線程的調用棧情況,運行!clrstack

發現System.String.Concat方法,這是典型的字符串拼接的函數,通過調用關系發現應該是在AllProducts.Page_Load(System.Object, System.EventArgs)方法中。

4.8、查看代碼

protected void Page_Load(object sender, EventArgs e)

{

DataTable dt = ((DataLayer)Application["DataLayer"]).GetAllProducts();

string ProductsTable = "<table><tr><td><B>Product ID</B></td><td><B>Product Name</B></td><td><B>Description</B></td></tr>";

 

foreach (DataRow dr in dt.Rows)

{

ProductsTable += "<tr><td>" + dr[0] + "</td><td>" + dr[1] + "</td><td>" + dr[2] + "</td></tr>" ;

}

ProductsTable += "</table>";

tblProducts.Text = ProductsTable;

}

這里面有一個循環的方法,然后針對輸出的DataTable,進行了大量的String.Concat操作。

 

 

 

 

 

1!address

!address 擴展顯示目標進程或目標機使用的內存信息。

這個學習起來比較簡單:我們直接使用!address -?就可以找到它的使用說明:

給個例子:

[cpp] view plaincopy

  1. 0:001> !address -?  
  2. !address                 - prints information on the entire address space  
  3. !address -?              - prints this help  
  4. !address <address>       - prints available information about the region  
  5.                            of the address space containing this address  
  6. !address -summary        - prints only summary information  
  7. !address -RegionUsageXXX - fiters the output limiting the dispaly to one  
  8.                            of the following types:  
  9.   RegionUsageIsVAD            - `busy` region that could be charcterized better  
  10.                                  this includes Virtual-Alloc-ed blocks, SBH heap,  
  11.                                  memory from custom allocators, etc  
  12.   RegionUsageFree             - availalble (neither committed nor reserved) region  
  13.   RegionUsageImage            - region used by mapped images of binaries  
  14.   RegionUsageStack            - stack of threads  
  15.   RegionUsageTeb              - TEB of threads   
  16.   RegionUsageHeap             - region in used by a heap  
  17.   RegionUsagePageHeap         - region in use by full page-heap  
  18.   RegionUsagePeb              - PEB of the process  
  19.   RegionUsageProcessParametrs - parameters of the process  
  20.   RegionUsageEnvironmentBlock - environment block  


那么一個個說明吧:

!address顯示整個地址空間和使用摘要的信息

這個太長了,它會把從0-7ffefff的全打印出來,熟悉核心編程的應該知道,正常的2G用戶地址空間是這樣划分的:0-ffff64K空指針區,1000-7ffeffff為用戶模式分區

之后64K為禁入分區,之后就是內核模式分區,要看它們的信息,需要用到以下的表,

Filter 

顯示的內存區域

RegionUsageIsVAD

"busy" 區域。包括所有虛擬分配塊、SBH堆、自定義內存分配器(custom allocators)的內存、以及地址空間中所有屬於其他分類的內存塊。

RegionUsageFree

目標的虛擬地址空間中所有可用內存。包括所有非提交(committed)和非保留(reserved)的內存。

RegionUsageImage

用來映射二進制映像的內存區域。

RegionUsageStack

用作目標進程的線程的堆棧的內存區域。

RegionUsageTeb

用作目標進程中所有線程的線程環境塊(TEB)的內存區域。

RegionUsageHeap

用作目標進程的堆的內存區域。

RegionUsagePageHeap

用作目標進程的整頁堆(full-page heap)的內存區域。

RegionUsagePeb

目標進程的進程環境塊(PEB)的內存區域。

RegionUsageProcessParametrs

用作目標進程啟動參數的內存區域。

RegionUsageEnvironmentBlock

用作目標進程的環境塊的內存區域。

下面這些Filter值按照內存類型來指定內存。

Filter 

顯示的內存類型

MEM_IMAGE

映射的文件屬於可執行映像一部分的內存。

MEM_MAPPED

映射的文件不屬於可執行映像一部分的內存。這種內存包含哪些從頁面文件映射的內存。

MEM_PRIVATE

私有的(即不和其他進程共享)並且未用來映射任何文件的內存。

下面的Filter 值按照狀態來指定內存:

Filter 

顯示的內存狀態

MEM_COMMIT

當前已提交給目標使用的所有內存。已經在物理內存或者頁面文件中為這些內存分配了物理的存儲空間。

MEM_RESERVE

所有為目標以后的使用保留的內存。這種內存還沒有分配物理上的存儲空間。

MEM_FREE

目標虛擬地址空間中所有可用內存。包括所有未提交並且未保留的內存。該Filter 值和RegionUsageFree一樣。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM