.NET應用程序調試—原理、工具、方法

閱讀目錄:緩存

  • 1.背景介紹
  • 2.基本原理(Windows調試工具箱、.NET調試擴展SOS.DLL、SOSEX.DLL)
    • 2.1.Windows調試工具箱
    • 2.2..NET調試擴展包,SOS.DLL、SOSEX.DLL
    • 2.3.調試系統的基本流程及架構(.NETDAC概念、mscordacwks.dll)
    • 2.4.VisualStudio中集成擴展調試(更加細粒度的調試程序)
  • 3.調試程序類型(客戶端程序、服務端程序)
  • 4.調試方式及場景
    • 4.1.本機調試(Attach Process,調試器啓動)
    • 4.2.不中斷調試或者稱過後調試(對Dump文件進行調試)
  • 5.通常調試步驟
    • 5.1.設置符號文件(公有符號、私有符號)
    • 5.2.加載.NET程序擴展調試包(SOS.DLL、SOSEX.DLL)
    • 5.3.調試的三種命令類型(標準命令、元命令、擴展命令)
  • 6.調試擴展的幾個比較經常使用的命令(SOS.DLL、SOSEX.DLL)
  • 7.簡單示例,常見的線上兩類問題
    • 7.1.內存問題(內存偏高,內存溢出)
    • 7.2.線程問題(CPU太高,線程死鎖)
  • 8.獲取Dump文件時的重要注意事項
  • 9.總結

1.背景介紹

隨着應用程序的複雜度不斷上升,要想將好的設計思想穩定的落實到線上,咱們須要具有解決問題的能力。須要具有對運行時的錯誤進行定位且快速的解決它的能力。本篇文章我將分享一下我對.NET應用程序調試方面的學習和使用總結。服務器

其實對調試程序的使用是不難的,關鍵是知道它的調試原理才行,由於調試一個程序或者dump文件,都須要瞭解必定的.NET調試的原理才行,好比你在附加到進程調試時在執行某個SOS擴展命令是須要切換到指定線程上的,而調試dump文件就不須要,可是對Dump文件的分析有些SOS擴展命令是不能用的,相似這樣的問題,一旦出現你就一頭霧水,因此花點時間學習一下原理是有必要的。架構

2.基本原理(Windows調試工具箱、.NET調試擴展SOS.DLL、SOSEX.DLL)

在Windows平臺上調試應用程序首選Windows調試工具箱,該工具箱包含了一套專門用來針對Windows進行不少複雜場景調試所須要的工具和組件。須要注意的是此工具箱是針對於非託管.NET平臺用的,意思就是說此工具箱的全部工具和組件默認是不可以進行.NET應用程序調試的,只能用來對原生Windows程序進行調試。併發

那麼.NET平臺也並非有本身一套專用的調試工具箱,畢竟.NET仍是屬於Windows平臺的,因此很大部分的運行時原理仍是基於Windows的,要想在原生的調試器中對.NET這個具備虛擬運行時程序進行調試就須要專門的翻譯器纔可以執行。SOS.DLL、SOSEX.DLL這兩個就是用來對.NET程序在Windows調試工具中起到翻譯做用的調試器擴展。簡單講就是,這兩個組件是.NET項目組專門開發出來用來對.NET應用程序進行方便調試用的,固然不用這兩個擴展也能調試.NET程序,只不過就會很困難,會被不少細節束縛住。有了這個調試擴展以後,咱們就可讓原生Windows調試器正確的翻譯出.NET相關概念。app

圖1:(Windows調試工具執行流程)框架

全部對.NET程序發起的調試會話都要通過.NET調試擴展組件進行翻譯才行,也就是要使用.NET調試擴展的調試命令來調試.NET程序。上圖中,咱們若是要想調試.NET程序就須要將.NET調試擴展組件加載到Windows調試工具中去,而後才能方便在Windows調試工具中使用。ide

2.1.Windows調試工具箱

Windows調試工具箱中包含了不少調試工具,都是用來輔助於咱們進行方便調試用的。Windows調試工具箱分爲兩個執行版本,X8六、X64這兩個版本是專門用來分析不一樣的運行時環境的,若是你的分析環境是32位的你就須要使用X86的版本,同理,若是是用64位的環境就須要使用X64的版本。工具

下載地址爲:http://www.microsoft.com/whdc/devtools/debugging/default.aspx性能

記住選擇你須要的版本,建議你兩個版本都下載,由於你隨時須要針對Dump文件進行分析,而Dump文件是隨時都有多是兩個版本。學習

Windows工具箱中的默認使用WinDbg.exe做爲調試首選,它是一個GUI程序。

圖2:(默認的Windows調試工具,WinDbg)

安裝事後的菜單中就只有WinDbg做爲調試選擇。

這裏須要注意的是,當你啓動了WinDbg以後要留意程序的名字和標題,由於當你存在兩個版本的WinDbg時會容易搞錯,在調試時會有各類奇怪的問題出現,當你找了半天以後結果發現是由於用錯了版本,那就正的無語了。

圖3:(注意運行WinDbg的環境版本)

WinDbg是默認的調試工具,可是在工具箱中還有幾個控制檯調試工具,他們行必之下比較輕量簡單,有些任務比較好執行,在配合cmd使用會很方便,好比工具箱中的tlist.exe用來查看進程信息的小工具就很是方便。

圖4:(方便查看進程ID)

這樣咱們就能夠很方便的attach到一個指定的進程進行調試。

Windows調試工具箱中有不少其餘的工具,須要用的話可使用cmd切換到當前安裝的目錄下:C:\Program Files\Debugging Tools for Windows (x86),或者你直接到工具的安裝目錄運行也行,這就看此工具是否是支持手動無參數啓動了。

2.2..NET調試擴展包,SOS.DLL、SOSEX.DLL

.NET調試擴展包分爲兩個,一個是SOS.DLL,該擴展包是.NET平臺的一部分,屬於官方版本。而SOSEX.DLL是微軟的一名叫「Steve Johnson」軟件工程師開發,屬於我的維護的,用來加強SOS.DLL功能的,在SOSEX.DLL有不少功能比較強大的擴展命令。

下載地址爲:

32位:http://www.stevestechspot.com/downloads/sosex_32.zip

64位:http://www.stevestechspot.com/downloads/sosex_64.zip

具體的幫助文檔能夠查看該工程師的博客來了解詳情。這兩個版本用來調試不一樣環境的程序的,若是你的程序是運行在32位環境下,就用32位的SOSEX,同理,用在64位下就用64位SOSEX。

而SOS.DLL擴展包是跟着.NETFramework一塊兒安裝的,地址位於:C:\Windows\Microsoft.NET\Framework\v4.0.30319。若是你是64位系統的話地址就是:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319。在這兩個地址下面均可以找到SOS.dll文件,不一樣的目錄下對應於調試不一樣機器類型的.NET程序。

有了這兩個擴展包以後就能夠在WinDbg中對.NET程序進行分析了,具體使用咱們後面會介紹。

2.3.調試系統的基本流程及架構(.NETDAC概念、mscordacwks.dll)

有一個很重要的原理我以爲頗有必要講一下,就是.NETDAC概念。

其實.NETDAC也就是.NET Data Access .NET數據訪問層,這個是專門用來提供給SOS.DLL\SOSEXDLL或者其餘調試擴展包使用的,全部的調試擴展組件必須經過這個DAC才能訪問到.NET運行時的數據,因此在初次使用SOS的時候會常常遇見加載錯誤的mscordacwks.dll文件,此文件就是DAC的物理文件。

這個文件和SOS擴展文件同樣,都有這不一樣的版本,當加載不一樣類型的.NET程序時會使用到不一樣版本的mscordacwks.dll文件,固然大部分狀況下此文件時自動加載的,只有出現你分析的文件與生成調試文件的環境不一致時纔會出現頭疼的問題。

圖5:(mscordacwks.dll位置)

當你知道這個組件是工做於此位置時,當出現跟它相關的錯誤提示時你就不須要擔憂了,無非就是文件加載的位置或者版本不匹配而已。

調試器會話、調試器注入線程

還有一點我以爲也頗有必要介紹的就是有關調試器如何調試.NET程序的,當咱們在使用調試器啓動被調試程序或者將調試器附加到被調試進程時,其實調試器會注入一些線程到.NET程序中,讓調試線程與.NET程序本來的線程在一個.NET執行環境中,這樣的目的是可以起到最.NET程序在執行時的控制,好比中斷執行,設置斷點。當咱們須要執行某些跟線程上下文相關的擴展命令時就須要切換到正確的線程上去。

圖6:(調試器注入線程)

此時,調試器使用一個注入線程將.NET程序在執行時中斷,原理就是經過發送線程中斷命令來達到控制目標線程,那麼首先要可以與原線程通信才行,因此須要注入託管線程。(注意:注入的線程不必定就是託管.NET線程,嚴重它最好的方法就是查看全部全部的進程內線程和全部託管線程,對比一下就知道了。),其實這個ID爲3的線程是調試器會話線程。

圖7:(切換到原託管線程)

咱們經過~0s命令切換到咱們須要調試的原託管線程中,好比,在執行!ClrStack命令時,就須要切換到當前線程上執行。

咱們須要驗證它是不是注入了託管線程仍是非託管線程。

圖8:(託管線程列表)

使用!Threads命令能夠查看進程內全部的託管線程,僅僅是託管線程,此命令是沒法查看非託管線程的,接下來咱們使用另一個命令來查看全部的線程。

圖9:(全部的執行時線程)

這樣咱們就能夠判斷出,調試器使用了ID位7的做爲目前的調試會話線程。知道這些背後的原理很重要,當你在執行某個調試命令時你就會發現此命令是否須要在.NET線程中執行,仍是說能夠在調試器會話線程中執行,通常dump類的命令都是能夠遠程執行的,也就是說在調試器會話中執行,當須要跟蹤.NET線程內部過程時就須要切換到.NET線程上去執行。

2.4.VisualStudio中集成擴展調試(更加細粒度的調試程序)

SOS擴展也是能夠和VisualStudio進行集成的,這樣真的方便了咱們調試一些性能要求比較高的程序,當程序運行一段時間後咱們用VS附加到進程,而後查看一些重要的對象數據,可是此時咱們看不到.NET運行時的一些數據,好比:對象的代齡,託管堆的大小,線程池的任務等。經過集成SOS擴展會讓咱們對程序的運行時有了一個更加方便的跟蹤。

圖10:(打開本地代碼調試)

設置斷點,而後在」即時窗口「(調試->窗口->即時)中加載擴展SOS.DLL。

圖11:(在VisualStudio2012中加載SOS.dll擴展)

這樣的便利性大大提升咱們在調試程序內存方面、線程方面的好處,咱們能夠適當的作壓力測試,而後Attach process,執行SOS擴展命名來查看內存問題,當須要調試程序邏輯時在單步調式C#代碼,一箭雙鵰。

3.調試程序類型(客戶端程序、服務端程序)

.NET程序主要分爲兩類,一類是客戶端程序,另外一類是服務端程序。對於這兩類程序來講前者調試時基本上能夠經過附加進程的方式進行調試,而對於服務端程序則不行,由於服務程序一般是運行在一個複雜的線上環境中,咱們沒有任何權限或機會去接觸,此時是經過獲取進程的dump文件來進行分析。

客戶端程序也大概分爲控制檯、Winform兩種,服務端程序都是基於ASP.NET框架,宿主與IIS進程中。

4.調試方式及場景

針對不一樣類型的程序及場景須要使用不一樣的方式進行調試,客戶端程序中的控制檯程序基本上能夠經過在調試器中啓動的方式進行調試。若是是GUI程序則須要附加進程方式。服務端程序若是在條件容許下也是可使用附加進程的方式進行調試的,可是這通常不太可能,由於一旦附加進程將block住全部的線程活動。

4.1.本機調試(Attach Process,調試器啓動)

本機調試能夠直接在調試器中啓動程序,WinDbg打開後,在文件中有一個Open Executable,能夠打開一個可執行文件。若是是使用NTSD控制檯調試器,則須要在NTSD後面跟上程序的執行路徑。

圖12:(ntsd.exe打開調試程序)

一樣,在WinDbg中也有一個附加進程的選項,NTSD也是同樣,操做起來都比較簡單,須要注意的是當你對進程進行附加時要清楚此進程是多少位的,而後你須要選擇正確的調試器進行調試。

4.2.不中斷調試或者稱過後調試(對Dump文件進行調試)

在不可以對被調試程序直接調試時咱們就須要此程序的進程鏡像文件,此鏡像文件就是進程在某一個時刻的快照,經過分析這個快照,咱們也是能夠定位出問題的。首先咱們須要使用適當的工具來獲取進程的dump文件,操做系統自己的任務管理器就有這個功能,dump文件的存放位置默認在用戶信息臨時文件下面,好比:XXX\Users\Administrator\AppData\Local\Temp,獲取完dump文件後任務管理器會有提示路徑的。

圖13:(使用任務管理器獲取dump文件)

圖14:

使用任務管理器獲取dump文件當然很方便,可是有一個問題就是若是當前機器是64位的,而且你的進程是以32位方式運行的,那麼此時你獲取出來的dump文件是64位的,當你經過32位的調試器沒法進行分析,甚至會有各類其餘的問題,這些問題就是由於獲取dump文件的機器環境和你預想的不一致。這個時候咱們但願可以經過很明瞭的方式來獲取dump文件,就是經過調試器來獲取dump文件。

經過調試器來獲取dump文件有不少好處,能夠設置不少選項,包括只獲取進程的哪部分鏡像數據等。

先經過tlist.exe查看全部進程列表,會有一個進程ID號,有了ID號才能進行獲取。

圖15:(tlist、ntsd 進入到指定進程中)

進入到ntsd調試器中,而後使用.dump/mf d:\order.dmp 命令獲取dump文件到D盤。

圖16:(使用NTSD.exe獲取dump文件)

此時咱們就成功的獲取到了dump文件。

經過調試器獲取dump文件比較穩定可靠,由於機器運行環境的不一樣,經過任務管理器獲取的dump文件會存在一些沒法預知的問題,你並不清楚,當前任務管理器是使用哪一個版本的環境輸出調試信息的。

有了dump文件以後就是經過調試工具打開就好了,WinDbg就有一個菜單專門打開dump文件的,Open Crash Dump。使用ntsd須要使用命令ntsd -z d:\order.dmp。

5.通常調試步驟

知道了調試的一些原理和工具以後咱們來看一下調試的基本步驟,這些步驟都具體是指的什麼意思,有哪些好處。

5.1.設置符號文件(公有符號、私有符號)

設置符號文件的目的是爲了可以在調試器中正確的對應到源代碼的位置和一些元數據信息。符號文件都是*.pdb文件名。符號文件分爲公有和私有兩種,公有的都是公司公開出去用於幫助調試用的,而私有的是公司內部使用的,爲何要區分公有和私有,是爲了防止逆向工程。

圖17:(設置符號文件路徑)

首先經過.sympath d:,設置了符號路徑爲D盤,而後又使用.symfix+ d:,是設置私有符號路徑,而且使用d盤爲緩存路徑。在最後一個紅線中咱們能看出來。

爲何使用.symfix 時要帶上一個+號,實際上是告訴調試器咱們是多加一個符號位置,而不是覆蓋原有符號位置。

設置好了兩個符號位置後須要使用.reload命令來從新加載模塊,這樣調試器纔會去符號位置去加載這些符號。

圖18:(加載的符號文件)

調試器會自動的將公有符號下載到你剛纔設置的緩存目錄中。

5.2.加載.NET程序擴展調試包(SOS.DLL、SOSEX.DLL)

對.NET程序分析固然是須要加載SOS擴展了。加載SOS擴展有兩個命令可使用,第一個是.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll,.load命令是要給出sos.dll絕對路徑的。第二個是.loadby sos modulename,.loadby 命令是能夠根據已經加載的模塊名稱來加載SOS.dll擴展。使用第一個命令有一個問題就是,咱們須要人工的判斷當前環境究竟是須要什麼版本的SOS擴展,而使用.loadby是能夠根據已經加載的模塊來自動的查找對應的SOS擴展。

0:000> .load C:\Windows\Microsoft.NET\Framework\v4.0.30319\SOS.dll

0:000> .loadby sos.dll clrjit

使用.loadby 命令很容易的就能夠加載SOS擴展,而不須要本身去判斷當前程序是.NET什麼版本的。

5.3.調試的三種命令類型(標準命令、元命令、擴展命令)

在使用調試器調試程序時,所要使用的命令主要分爲三類。

第一類是標準命令,就是不帶任何符號開始的命令,好比:pb、lmvm。這一類命令是全部Windows調試工具箱中的調試工具通用的,無論你是使用ntsd仍是winDbg均可以。

第二類命令是元命令,就是使用"."號開始的命令,這一類命令並非在全部調試工具中通用的。第三類是擴展命令,擴展命令就是各個調試器擴展出來的命令,也就是以"!"開始的命令,如:!dumpheap -stat,!dumpstatcobjects。

6.調試擴展的幾個比較經常使用的命令(SOS.DLL、SOSEX.DLL)

固然這個純粹是個人我的感受,排名不分前後。

!dumpheap -stat (查看託管堆統計信息)

0:000> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
65366e78        1           12
System.Collections.Generic.EnumEqualityComparer`1[[System.Web.Compilation.FolderLevelBuildProviderAppliesTo,
System.Web]]
653667cc        1           12
System.Collections.Generic.ObjectEqualityComparer`1[[System.Web.WebSockets.IAsyncAbortableWebSocket,
System.Web]]
65365f08        1           12
System.Lazy`1+Boxed[[System.Web.Security.Cryptography.AspNetCryptoServiceProvider,
System.Web]]
65365a34        1           12
System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper
65361e20       
1           12 System.Web.Configuration.CustomErrorsMode

!dumpheap -type  (查看某個類型在堆中的信息)

0:000> !dumpheap -type System.String

Address       MT     Size
10731228 624aacc0       14    
107312c4 624aacc0 22
107312dc 624aacc0       78    
10731370
624aacc0       28 

能夠一眼看出哪些對象過大,這裏我是爲了演示而用,通常在項目開發中,咱們都大概知道哪些對象可能會有內存問題,好比:同步數據時的緩存對象。

!dumpobj 10731228 (查看對象詳情)

0:000> !dumpobj 10731228
Name:       
System.String
MethodTable: 624aacc0
EEClass:     620b486c
Size:       
14(0xe) bytes
File:       
C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:     

Fields:
      MT    Field   Offset                 Type VT     Attr   
Value Name
624ac480  40000aa        4         System.Int32  1 instance       
0 m_stringLength
624ab6b8  40000ab        8          System.Char  1 instance        0 m_firstChar
624aacc0  40000ac        c       
System.String  0   shared   static Empty
    >> Domain:Value 
00dbe558:NotInit  00e11c90:NotInit  00e5f040:NotInit  <<

!threads(查看託管線程)

0:000> !threads
ThreadCount:      17
UnstartedThread:  0
BackgroundThread: 12
PendingThread:    0
DeadThread:       5
Hosted Runtime: no
                                                   
Lock 
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   7    1 43a8 00dc2620     28220 Preemptive  1484CA40:00000000 00dbe558 0     Ukn
  15    2 4414 00dd38d0     2b220 Preemptive  00000000:00000000 00dbe558 0     MTA (Finalizer)
  17    3 441c 00e09e88   102a220 Preemptive  00000000:00000000 00dbe558 0     MTA (Threadpool
Worker)
  18    4 4420 00e0ce80     21220 Preemptive  00000000:00000000 00dbe558 0     Ukn

固然還有不少其餘很不錯的命令,這裏我我的以爲這幾個比較經常使用,要想了解全部的命令但是在調試器中使用擴展命令!help來查看全部的命令幫助。

0:000> !help

-------------------------------------------------------------------------------

SOS is a debugger extension DLL designed to aid in the debugging of managed programs. Functions are listed by category, then roughly in order of importance. Shortcut names for popular functions are listed in parenthesis. Type "!help <functionname>" for detailed info on that function.

Object Inspection                  Examining code and stacks

-----------------------------      -----------------------------

DumpObj (do)                       Threads

DumpArray (da)                     ThreadState

DumpStackObjects (dso)             IP2MD

DumpHeap                           U

DumpVC                       DumpStack

GCRoot                             EEStack

ObjSize                            CLRStack

FinalizeQueue                      GCInfo

PrintException (pe)                EHInfo

TraverseHeap                       BPMD
                                   COMState

 

Examining CLR data structures      Diagnostic Utilities

-----------------------------      -----------------------------

DumpDomain                         VerifyHeap EEHeap                             VerifyObj Name2EE                            FindRoots SyncBlk                            HeapStat DumpMT                             GCWhere DumpClass                          ListNearObj (lno) DumpMD                             GCHandles Token2EE                           GCHandleLeaks EEVersion                          FinalizeQueue (fq) DumpModule                         FindAppDomain ThreadPool                         SaveModule DumpAssembly                       ProcInfo
DumpSigElem                        StopOnException (soe) DumpRuntimeTypes                   DumpLog DumpSig                            VMMap RCWCleanupList                     VMStat DumpIL                             MinidumpMode
DumpRCW                            AnalyzeOOM (ao) DumpCCW

 

Examining the GC history           Other

-----------------------------      -----------------------------

HistInit                           FAQ HistRoot HistObj HistObjFind HistClear

7.簡單示例,常見的線上兩類問題

這裏咱們使用兩個小示例直觀的感覺一下接觸.NET運行時狀態的感覺,儘管真實的問題可能比這個複雜不少,可是解決問題的思路是同樣的。

7.1.內存問題(內存偏高,內存溢出)

服務程序最怕的性能問題之一就是內存,當內存很高的狀況下咱們可以經過對dump文件進行查看,看哪些對象致使內存一直高。當內存一直高的狀況下就會容易致使內存溢出異常,甚至是GC頻繁的執行,當GC一執行就會致使服務併發降低,由於它要掛起全部的線程(這裏指的是服務器模式的.NETCLR,相對應的還有工做站模式的.NETCLR)。

namespace OrderManager
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("app begin...");
            Console.ReadLine();

            List<byte[]> l = new List<byte[]>(); 

            for (int i = 0; i < 9999999; i++)
            {
                byte[] b = new byte[1000]; 

                l.Add(b); 

                Console.WriteLine(i);
            } 

            Console.WriteLine("end begin...");
            Console.ReadLine();
        }
    }
}

這一段代碼會一直分配內存直到最後內存溢出異常終止程序,咱們在內存比較的狀況下來獲取一個dump文件,而後經過適當的命令來定位哪一個對象佔用內存太高。

在不知道對象類型的狀況下比較簡單的方式就是使用:0:000> !dumpheap -stat,命令,該命令的意思是統計當前堆的信息,在這裏就能夠一眼找到哪一個對象佔用多少內存。

0:000> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class
Name
624ad6a8        1           12 System.Collections.Generic.GenericEqualityComparer`1[[System.String,
mscorlib]]
624ac480        1           12 System.Int32

624aa58c        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type,
mscorlib]]
624adec0        1           16 System.Security.Policy.AssemblyEvidenceFactory
624ace34        1           16 System.Text.DecoderReplacementFallback
624acde4        1           16 System.Text.EncoderReplacementFallback
6247a840        1           16 System.IO.TextReader+SyncTextReader
624ade0c        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
6245fe58        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
6245fe08        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
6245fd74        1           20 System.Text.InternalEncoderBestFitFallback
6245f714        1           20 System.IO.Stream+NullStream
624ad3d4        1           24 System.Version
6245fdc4        1           24 System.Text.InternalDecoderBestFitFallback
6245fa8c        1           24 System.IO.TextWriter+SyncTextWriter
00163170        1           24 System.Collections.Generic.List`1[[System.Byte[], mscorlib]]
624ad4b4        1           28 System.Text.StringBuilder
624ab0b4        1           28 System.SharedStatics
6247c1b8        1           28 System.Text.DBCSCodePageEncoding+DBCSDecoder
6245f94c        1           28 Microsoft.Win32.Win32Native+InputRecord
6245f664        1           28 System.Text.EncoderNLS
624ade68        1           32 System.Security.Policy.PEFileEvidenceFactory
624acc10        1           32 System.Text.UnicodeEncoding
624ab938        1           36 System.Security.PermissionSet
624aced8        2           40 Microsoft.Win32.SafeHandles.SafeFileHandle
624ab7b0        1           40 System.Security.Policy.Evidence
624aaa64        1           44 System.Threading.ReaderWriterLock
6247cd1c        1           44 System.Text.InternalEncoderBestFitFallbackBuffer
624aab90        1           48 System.Collections.Hashtable+bucket[]
620c2348        1           48 System.Collections.Generic.Dictionary`2[[System.String,
mscorlib],[System.Globalization.CultureData, mscorlib]]
620c2268       
1           48 System.Collections.Generic.Dictionary`2[[System.Type,
mscorlib],[System.Security.Policy.EvidenceTypeDescriptor,
mscorlib]]
624acf98        1           52 System.Collections.Hashtable
624ab8d8        1           52 System.Threading.Thread
624acb20        2           56 System.Reflection.RuntimeAssembly
6245f994        2           56 System.IO.__ConsoleStream
624adaa8        1           60 System.IO.StreamWriter
624ad7b4        1           60 System.Collections.Generic.Dictionary`2+Entry[[System.String,
mscorlib],[System.Globalization.CultureData, mscorlib]][]
6249fbec       
1           64 System.IO.StreamReader
624ab4e4        1           68 System.AppDomainSetup
6247c624        1           76 System.Text.DBCSCodePageEncoding
624ad474        1           84 System.Globalization.CalendarData
624ab060        7           84 System.Object
624aafe4        1           84 System.ExecutionEngineException
624aafa0        1           84 System.StackOverflowException
624aaf5c        1           84 System.OutOfMemoryException
624aae08        1           84 System.Exception
624ab130        1          112 System.AppDomain
624ad164        2          144 System.Globalization.CultureInfo
624ab028        2          168 System.Threading.ThreadAbortException
624ad82c        2          264 System.Globalization.NumberFormatInfo
624aa9f8        1          284 System.Collections.Generic.Dictionary`2+Entry[[System.Type,
mscorlib],[System.Security.Policy.EvidenceTypeDescriptor,
mscorlib]][]
624ac448        8          484 System.Int32[]
624ad3a0        2          616 System.Globalization.CultureData
624abe78       26          728 System.RuntimeType
624ab680        7         2910 System.Char[]
6245ab98       25        18064 System.Object[]
624aacc0     3283        85972 System.String
00363a78        7      2031754 Free
624696f8        2      2097184 System.Byte[][]
624acf54   301232    304844554 System.Byte[]

最後一個顯然內存佔用比較高,佔了304844554 byte,若是你想在此狀況下知道對象的內存地址你就直接使用!dumpheap ,不帶任何參數。因爲此命令會致使不少輸出,我這裏就寫出輸出內容了。經過!dumpheap 會獲得內存很高的對象地址,02d55368,這個地址就是System.Byte[]對象,爲了找到對象在哪裏分配的,咱們須要使用!gcroot 02d55368,命令,查看對象的根在哪裏。

 0:000> !gcroot 02d55368
Thread 143310:    0028f364
004f0100 OrderManager.Program.Main(System.String[])
[e:\NETDebug\DebugDemoProject\OrderManager\Program.cs @ 22]
        ebp+18: 0028f380
            ->  01b746c0 System.Collections.Generic.List`1[[System.Byte[], mscorlib]]
            ->  02d55368 System.Byte[][]

 知道了根就好辦多了,直接看源代碼就能發現問題。若是你還不死心的話可使用!dumpobj 查看List對象。

 0:000> !dumpobj 01b746c0
Name:       
System.Collections.Generic.List`1[[System.Byte[], mscorlib]]
MethodTable:
00163170
EEClass:     6211c8b0
Size:        24(0x18) bytes
File:       
C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name

6245ab98  4000c75        4      System.Object[]  0 instance 02d55368 _items

624ac480  4000c76        c         System.Int32  1 instance   301229 _size
624ac480  4000c77       10         System.Int32  1 instance   301229 _version
624ab060 4000c78 8 System.Object 0 instance 00000000 _syncRoot

6245ab98  4000c79        0      System.Object[]  0   shared   static _emptyArray

     >> Domain:Value dynamic statics NYI 00359520:NotInit  <<

這裏須要注意的是,若是你是想執行!Clrstack -a 命令的話,當你使用調試器啓動或者是附加進程的方式的化,要記住切換到適當的線程上才能看行。

7.2.線程問題(CPU太高,線程死鎖)

CPU太高也是線上比較棘手的問題之一,查看CPU太高的步驟通常分爲兩步,查看線程的執行時間,而後切換到線程上下文,執行!ClrStack -a,看當前線程在哪裏工做,到底作什麼操做呢。

0:004> !runaway
User Mode Time
  Thread       Time
0:143310      0 days 0:00:01.934
4:142ac0      0 days 0:00:00.046
7:143874      0 days 0:00:00.000
6:143870      0 days 0:00:00.000
5:14386c      0 days 0:00:00.000
3:1432ec      0 days 0:00:00.000
2:143384      0 days 0:00:00.000
1:143254      0 days 0:00:00.000

測試線程ID爲0的執行時間比較大,咱們須要切換到線程0上去執行查看調用堆棧信息,~0s。

0:000> !ClrStack -a

0028f348 62b897f9 System.IO.TextWriter+SyncTextWriter.WriteLine(Int32)    

PARAMETERS:        

this (<CLR reg>) = 0x01b74258         value = <no data>

0028f358 62a66313 System.Console.WriteLine(Int32)    

PARAMETERS:        

value = <no data>

0028f364 004f0100 OrderManager.Program.Main(System.String[]) [e:\NETDebug\DebugDemoProject\OrderManager\Program.cs @ 22]     PARAMETERS:        

args (0x0028f38c) = 0x01b71fe4    

LOCALS:        

0x0028f380 = 0x01b746c0        

0x0028f388 = 0x000498ac        

0x0028f37c = 0x16a2e338        

0x0028f384 = 0x00000001

0028f51c 63162952 [GCFrame: 0028f51c]

咱們會發如今Main方法中有一個本地變量0x0028f380 ,保存的值是0x01b746c0,它就是指向剛纔分配不少內存的List<byte[]>對象。

線程死鎖比較複雜,這裏只給我認爲比較簡單的命令,經過此命令能夠一眼看出哪一個線程持有了哪一個鎖,目前在等待哪一個鎖。

0:000> !syncblk
Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
    4 0021fb20            3         1  00221f98 14974c   3   01ae2394 OrderManager.ImportOrder
  5 0021fb54           3          1 002234a8 149754    4   01ae23a0 OrderManager.ImportOrder
-----------------------------
Total          
5
CCW             0
RCW             0
ComClassFactory
0
Free            0

這是兩個鎖,也就是兩個對象同步塊。進一步使用SOSEX.dll中的!dlk查看死鎖的自動化檢查信息。

0:000> !dlk

Examining SyncBlocks... Scanning for ReaderWriterLock instances... Scanning for holders of ReaderWriterLock locks... Scanning for ReaderWriterLockSlim instances... Scanning for holders of ReaderWriterLockSlim locks... Examining CriticalSections... Could not find symbol ntdll!RtlCriticalSectionList. Scanning for threads waiting on SyncBlocks... Scanning for threads waiting on ReaderWriterLock locks... Scanning for threads waiting on ReaderWriterLocksSlim locks... Scanning for threads waiting on CriticalSections... *DEADLOCK DETECTED* CLR thread 0x3 holds the lock on SyncBlock 0021fb20 OBJ:01ae2394[OrderManager.ImportOrder] ...and is waiting for the lock on SyncBlock 0021fb54 OBJ:01ae23a0[OrderManager.ImportOrder] CLR thread 0x4 holds the lock on SyncBlock 0021fb54 OBJ:01ae23a0[OrderManager.ImportOrder] ...and is waiting for the lock on SyncBlock 0021fb20 OBJ:01ae2394[OrderManager.ImportOrder] CLR Thread 0x3 is waiting at System.Threading.Monitor.Enter(System.Object, Boolean ByRef)(+0x17 Native) CLR Thread 0x4 is waiting at System.Threading.Monitor.Enter(System.Object, Boolean ByRef)(+0x17 Native)

1 deadlock detected.

注意我加粗的那段話,檢測到死鎖。

8.獲取Dump文件時的重要注意事項

在獲取dump文件方面我也要分享一下重要的注意事項。若是獲取dump文件不正確的話是沒法進行分析的,會出現任何奇怪的問題。

第一個就是使用64位機器上的任務管理獲取32位進程dump文件,這一般是發生在服務器上,因爲服務器IIS默認的啓動進程方式是64位的,可是也有些狀況下會變成32位的。

圖19:

若是進程是以32位方式運行的,那麼這個時候獲取出來的dump文件是很差分析的,此時應該使用調試器工具進行dump的獲取。獲取出來的dump文件和分析機器上的調試器環境不一致的狀況下會出現以下幾個錯誤。

圖20:

這個問題是未能加載正確版本的mscordacwks.dll .NETDAC調式組件。

圖21:

這個問題是當前SOS.dll和.NET程序所使用的.NET版本不一致,這個問題的出現通常都是咱們經過.load xx\xx\SOS.dll,手動方式加載的。

圖22:

這個問題出現有好幾種可能性,對常見的問題就是未能使用正確的方法或者工具獲取dump文件,致使dum文件獲取的機器和本地調試的機器整個環境不一致。

9.總結

本篇文章分享我對.NET應用程序調試方面學習和實踐的一些經驗,供廣大博友參考。若是想系統的學習一下這方面的知識能夠參考《.NET高級調試》一書,此書很是底層,對.NET運行時原理講的很透徹,能夠做爲深刻學習.NET的一門參考書。

 

相關文章
相關標籤/搜索