.NET應用程式除錯:原理、工具、方法

發表於2016-05-11

閱讀目錄:

  • 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相關概念。

圖1:(Windows除錯工具執行流程)

所有對.NET程式發起的除錯會話都要經過.NET除錯擴充套件元件進行翻譯才行,也就是要使用.NET除錯擴充套件的除錯命令來除錯.NET程式。上圖中,我們如果要想除錯.NET程式就需要將.NET除錯擴充套件元件載入到Windows除錯工具中去,然後才能方便在Windows除錯工具中使用。

2.1.Windows除錯工具箱

Windows除錯工具箱中包含了很多除錯工具,都是用來輔助於我們進行方便除錯用的。Windows除錯工具箱分為兩個執行版本,X86、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 FilesDebugging 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:WindowsMicrosoft.NETFrameworkv4.0.30319。如果你是64位系統的話地址就是:

C:WindowsMicrosoft.NETFramework64v4.0.30319。在這兩個地址下面都可以找到SOS.dll檔案,不同的目錄下對應於除錯不同機器型別的.NET程式。

有了這兩個擴充套件包之後就可以在WinDbg中對.NET程式進行分析了,具體使用我們後面會介紹。

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

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

其實.NETDAC也就是.NET Data Access .NET資料訪問層,這個是專門用來提供給SOS.DLLSOSEXDLL或者其他除錯擴充套件包使用的,所有的除錯擴充套件元件必須通過這個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檔案的存放位置預設在使用者資訊臨時檔案下面,比如:XXXUsersAdministratorAppDataLocalTemp,獲取完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:WindowsMicrosoft.NETFrameworkv4.0.30319SOS.dll,.load命令是要給出sos.dll絕對路徑的。第二個是.loadby sos modulename,.loadby 命令是可以根據已經載入的模組名稱來載入SOS.dll擴充套件。使用第一個命令有一個問題就是,我們需要人工的判斷當前環境到底是需要什麼版本的SOS擴充套件,而使用.loadby是可以根據已經載入的模組來自動的查詢對應的SOS擴充套件。

0:000> .load C:WindowsMicrosoft.NETFrameworkv4.0.30319SOS.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.EnumEqualityComparer1[[System.Web.Compilation.FolderLevelBuildProviderAppliesTo,
System.Web]]
653667cc        1           12
System.Collections.Generic.ObjectEqualityComparer
1[[System.Web.WebSockets.IAsyncAbortableWebSocket,
System.Web]]
65365f08        1           12
System.Lazy1+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:WindowsMicrosoft.NetassemblyGAC_32mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.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 " 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)。

這一段程式碼會一直分配記憶體直到最後記憶體溢位異常終止程式,我們在記憶體比較的情況下來獲取一個dump檔案,然後通過適當的命令來定位哪個物件佔用記憶體過高。

在不知道物件型別的情況下比較簡單的方式就是使用:0:000> !dumpheap -stat,命令,該命令的意思是統計當前堆的資訊,在這裡就可以一眼找到哪個物件佔用多少記憶體。

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

624aa58c        1           12 System.Collections.Generic.ObjectEqualityComparer1[[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.Dictionary2[[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.Dictionary2+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:NETDebugDebugDemoProjectOrderManagerProgram.cs @ 22]
ebp+18: 0028f380
->  01b746c0 System.Collections.Generic.List1[[System.Byte[], mscorlib]]
->  02d55368 System.Byte[][]

知道了根就好辦多了,直接看原始碼就能發現問題。如果你還不死心的話可以使用!dumpobj 檢視List物件。

0:000> !dumpobj 01b746c0
Name:
System.Collections.Generic.List1[[System.Byte[], mscorlib]]
MethodTable:
00163170
EEClass:     6211c8b0
Size:        24(0x18) bytes
File:
C:WindowsMicrosoft.NetassemblyGAC_32mscorlibv4.0_4.0.0.0__b77a5c561934e089mscorlib.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 () = 0x01b74258         value =

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

PARAMETERS:

value =

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

args (0x0028f38c) = 0x01b71fe4

LOCALS:

0x0028f380 = 0x01b746c0

0x0028f388 = 0x000498ac

0x0028f37c = 0x16a2e338

0x0028f384 = 0x00000001

0028f51c 63162952 [GCFrame: 0028f51c]

我們會發現在Main方法中有一個本地變數0x0028f380 ,儲存的值是0x01b746c0,它就是指向剛才分配很多記憶體的List物件。

執行緒死鎖比較複雜,這裡只給我認為比較簡單的命令,通過此命令可以一眼看出哪個執行緒持有了哪個鎖,目前在等待哪個鎖。

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 xxxxSOS.dll,手動方式載入的。

圖22:

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

9.總結

本篇文章分享我對.NET應用程式除錯方面學習和實踐的一些經驗,供廣大博友參考。如果想系統的學習一下這方面的知識可以參考《.NET高階除錯》一書,此書非常底層,對.NET執行時原理講的很透徹,可以作為深入學習.NET的一門參考書。

相關文章