使用WinDbg調試程序 編程
WinDbg是微軟發佈的一款至關優秀的源碼級(source-level)調試工具,能夠用於Kernel模式調試和用戶模式調試,還能夠調試Dump文件。 數組
WinDbg是微軟很重要的診斷調試工具: 能夠查看源代碼、設置斷點、查看變量, 查看調用堆棧及內存狀況。 服務器
調試應用程序(用戶模式 user mode) 網絡
調試操做系統及驅勱程序(內核模式 kernel mode) app
調試非託管程序(native program) dom
調試託管程序(managed program) 函數
實時調試 (JIT: Just in time) 工具
過後調試 (postmortem debugging) oop
使用WinDbg能夠解決線上.NET應用程序的以下問題: post
◆ 內存高
◆ CPU高
◆ 程序異常
◆ 程序Hang死
在生產環境下進行故障診斷時,爲了避免終止正在運行的服務或應用程序,有兩種方式能夠對正在運行的服務或應用程序的進程進行分析和調試。
1、用WinDbg等調試器直接attach到須要調試的進程,調試完畢以後再detach便可。可是這種方式有個缺點就是執行debugger命令時必須先break這個進程,執行完debug命令以後又得趕忙F5讓他繼續運 行,由於被你break住的時候意味着整個進程也已經被你掛起。另外也常常會因爲First Chance Excetpion而自動break,你得時刻留意避免長時間break整個進程。因此這樣的調試方式對時間是個很大的考驗,每每沒有充裕的時間來作仔細分析。
2、在出現問題的時候,好比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的法。
一、運行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的版本必定要正確)
二、在控制板的系統中設置一個系統變量_NT_SYMBOL_PATH 爲
SRV*c:\symbols*http://msdl.microsoft.com/download/symbols
dump文件獲取
dump文件是進程的內存鏡像。能夠把程序的執行狀態,即當時程序內存空間數據經過調試器保存到dump文件中。
一、利用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
二、使用Debug Diagnostic Tool(DebugDiag)工具獲取dump文件
下載Debug Diagnostic Tool而後進行安裝,打開該工具,Debug Diagnostic Tool能夠選擇不一樣的規則來進行dump文件。能夠根據程序崩潰時捕獲dump文件,也能夠根據性能指標來進行捕獲,如CPU太高,死鎖,HTTP響應時間過程等參數。以下圖:
也能夠找到對應的進程,經過以下方法進行捕獲。此種方式獲取的dump文件放到C:\Program Files\DebugDiag\Logs\Misc下。
三、使用.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可以獲得的最多的信息。是一種折中方案。
四、使用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工具已經獲得判斷,此步驟略。
下載後部署到本機,產生壓力:
tinyget -srv:dbg.buggybit.com -uri:/Links.aspx -loop:4000
觀察進程中w3wp進程,發現內存增加很快,大約該進程內存增加到700M時,抓取一個hang dump。
!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: //查看數組信息
3.一、運行命令!eeheap –gc查看GC堆的狀況,發現GC Heap大小爲720多兆,因此咱們重點分析託管堆的狀況。從運行結果能夠看到GC Heap中g0,g1,g2和LOH的堆狀況,以及該GC Heap中所分配的段狀況。
能夠運行!dumpheap -mt 0c3b0038 0c3b0048命令查看LOH堆中大對象的狀況。從統計結果看,LOH堆中沒有大的對象存在。同理咱們也能夠統計各個段上對象的狀況。
3.二、接下來看下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.三、過濾一下,看看10K以上大小的字符串,運行命令:!dumpheap -mt 6f021ee4 -min 10000。10K以上的有35996個。
3.四、隨便找個對象看下引用關係,運行!gcroot 36278028,結果以下:
經過結果發現,Link引用了這個字符串。並且咱們看到,link是在Finalizer Queue中的。有關Finalizer Queue能夠參考.net Finalization原理。
3.五、經過運行命令! Finalizequeue 查看Finalizer Queue隊列的狀況。
00b740fc 35987 575792 Link
一共有35987個Link對象存在於Finalizer Queue中,所以能夠斷定,Link類必定是顯示的實現了Finalize方法。
3.六、查看該方法,代碼以下:
~Link()
{
//some long running operation when cleaning up the data
Thread.Sleep(5000);
}
3.七、接下來咱們看下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.八、查看代碼
public Link(string name, string url)
{
this.name = name;
this.url.Append(url);
}
會引發垃圾回收器託管堆速度的幾個問題
一、分配太頻繁
二、預先分配空間
三、太多的引用(pointers)和根(roots)
四、太多的對象實例有很長的生命期
五、太多的定位對象實例(pinned)
六、有終結函數的對象實例
佔用更多資源
更長的生命期
兩次才能回收
垃圾回收器(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.一、產生壓力
TinyGet.exe -srv:dbg.buggybit.com -uri:/AllProducts.aspx -threads:5 -loop:1
4.二、經過Procdump抓取三個dump文件
procdump -ma -c 50% -s 2 -n 3 w3wp.exe -o d:\dump
4.三、打開這三個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.四、從上面三個輸出結果發現只有22,23,30,31號線程一值在增加。且22,23線程增加的速度較快。查看三個dump的!threadpool基本都在90%以上。
4.五、運行!threads查看當前都有哪些線程。
不知道爲何並無找到22,23線程,有30,31線程號。
4.六、切換到30線程中,運行命令~30s
4.七、查看當前線程的調用棧狀況,運行!clrstack
發現System.String.Concat方法,這是典型的字符串拼接的函數,經過調用關係發現應該是在AllProducts.Page_Load(System.Object, System.EventArgs)方法中。
4.八、查看代碼
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
那麼一個個說明吧:
!address顯示整個地址空間和使用摘要的信息
這個太長了,它會把從0-7ffefff的全打印出來,熟悉核心編程的應該知道,正常的2G用戶地址空間是這樣劃分的:0-ffff爲64K空指針區,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同樣。 |