一:背景 1. 講故事 前些天有位朋友找到我,說他程式中的線程數爆高,讓我幫忙看下怎麼回事,這種線程數爆高的情況找問題相對比較容易,就讓朋友丟一個dump給我,看看便知。 二:為什麼會爆高 1. 查看托管線程 別人說的話不一定是真,得自己拿數據出來說話,可以用 !t 命令觀察一下便知。 0:000> ...
一、簡介
今天是《Net 高級調試》的第四篇文章。到今天為止,也有三篇文章了,對 Windbg 也有初步的認識了,當然,一個工具流暢、熟練的使用,對於我們調試 Net 程式是至關重要的。在前幾篇文章的基礎上,我們這篇文章主要介紹一些和使用 Windbg 有關的命令和操作。就我個人而言,第一次接觸這個東西,還是挺難的,以前從來沒有用過 Windbg,用的最多的就是 Visual Studio 的調試功能。不怕大家笑話,如何通過 Windbg 載入一個 exe,我都不知道,更不要談載入 DUMP 文件。我看第一遍視頻的時候,也不知道說了個啥,命令的執行,調試的開始,都感覺是一頭霧水,似懂非懂,自己一實操,總是得不到別人調試那樣的結果,很是鬱悶。怎麼辦呢?沒辦法,要想學會,除了努力那就是堅持。針對視頻,放慢速度,一幀一幀的按著視頻的操作,自己來一遍,速度雖然慢,但是有些操作開始有了感覺了,當整個視頻系列看了一遍,所有操作都操作一遍,終於有些頭緒了。還是那句老話,一遍不行,那就再來一遍,還不行,那就再來一遍,俗話說的好,書讀千遍,其意自現,我這是第三遍。
如果在沒有說明的情況下,所有代碼的測試環境都是 Net Framewok 4.8,但是,有時候為了查看源碼,可能需要使用 Net Core 的項目,我會在項目章節里進行說明。好了,廢話不多說,開始我們今天的調試工作。
調試環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
操作系統:Windows Professional 10
調試工具:Windbg Preview(可以去Microsoft Store 去下載)
開發工具:Visual Studio 2022
Net 版本:Net Framework 4.8
CoreCLR源碼:源碼下載
二、相關知識
1、Windbg 動態調試
1.1、調試概況
在任何一種調試中都有兩個組件:調試器本身,調試目標。
當調試器【Windbg】附加進程【Attach to Process】時,調試器會給 目標程式 註入一個遠程線程並用 int 3 中斷程式,後續和 Net 程式中的 DebuggerRCThread 線程交互執行命令。int 3 指令之所以可以讓進程中斷,主要是來自於 CPU 硬體中斷,當調試器發出了一個 int 3 的中斷請求,CPU會到內核態執行3號常式,也就是執行【中斷向量表】,內核執行中斷的操作。
Actor------------》調試器(Windbg)《------------》調試目標(Net 程式,C++程式...)
以上圖例就是一個調試器的作用和所處的地位。
2、程式的中斷和恢復執行
2.1、中斷執行(讓程式中斷有4中方式)
a、使用WinDbg 啟動程式,在進程初始化函數中,如果發現有調試器附加在上面,就會執行 break 中斷。
說明一下:Windbg 調試器剛開始的中斷就是 int 3 中斷,但是這個中斷的時機很早,我們可以做一些初始化的工作,比如:載入SOS.dll 等類似的工作。
b、附加進程,調試器會註入遠程線程執行 int 3 中斷程式。
這個挺簡單的,我們雙擊程式,直接運行。然後通過 Windbg 的【Attach to process】附加進程,就可以進入調試器界面,這個時候,其實什麼也不用做,調試器已經暫停了,這個暫停就是 int 3 中斷。我們通過 Windbg 的【break】命令也是 int 3 中斷。當然,我們通過 C# Debugger.Break() 代碼執行的也是 int 3 的中斷。
c、使用 bp 命令給程式下斷點。
我們可以通過【u】命令查看方法的彙編代碼,找到想要設斷點的代碼的地址,直接通過這個地址來下斷點,當程式再次運行的時候,就會在這個斷點處暫停。
d、異常中斷。
異常中斷的流程是:當你的程式發生異常,會從用戶態轉到內核態,內核態檢測到你的程式附加了調試器,內核態就會把這個請求轉交給調試器,調試器也就能中斷了,可以調試了。
2.2、恢復執行
可以使用 g 命令回覆程式的執行。
3、單步調試代碼。
當我們調試程式的時候,最多的時候是使用 Visual Studio 的 f10、f11、f5這樣的命令,在 Windbg 中也有類似的命令可以使用。
3.1、p 命令
p(step):命令其實就是VS 中的 f10 快捷鍵,單步執行,遇到函數也是當成一條指令執行,不會進入函數體。
3.2、t 命令
t(trace):命令其實就是 VS 的 f11 快捷鍵,它是一種進入函數的單步執行調試。
3.3、pc 命令
pc(Step to Next Call) 就是一直運行直到遇到 call 為止,不會進入函數體,call 是一個函數調用,彙編指令。
3.4、tc 命令
tc(Trace to Next Call) 和 pc 不同的是,tc 會進入方法體,直到遇到 call 為止。
3.5、pt 命令
pt(Step to Next Return) 遇到下一個 ret 為止。
3.6、tt 命令
tt(Trace to Next Return) 會進入函數體直到遇到 ret 為止。遞歸的意思。
4、退出測試會話
結束調試會話,有兩個目的,看是否保留程式的執行。
4.1、q(quit):結束調試會話+調試程式退出
調試會話結束,應用程式也會退出。
4.2、qd(quit and detach):結束調試會話+調試程式繼續運行
調試會話結束,應用程式保持運行態,不會退出。
三、調試過程
廢話不多說,這一節是具體的調試操作的過程,又可以說是眼見為實的過程,在開始之前,我還是要啰嗦兩句,這一節分為兩個部分,第一部分是測試的源碼部分,沒有代碼,當然就談不上測試了,調試必須有載體。第二部分就是根據具體的代碼來證實我們學到的知識,是具體的眼見為實。
1、測試源碼
1.1、Example_4_1_1
1 namespace Example_4_1_1 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 for (int i = 0; i < int.MaxValue; i++) 8 { 9 Console.WriteLine($"i={i}"); 10 Thread.Sleep(1000); 11 } 12 Console.ReadLine(); 13 } 14 } 15 }View Code 1.2、Example_4_1_2
1 namespace Example_4_1_2 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 Debugger.Break(); 8 9 int a = 10; 10 int b = 12; 11 12 var sum = a + b; 13 14 Console.WriteLine($"sum={sum}"); 15 16 Console.ReadLine(); 17 } 18 } 19 }View Code 1.3、Example_4_1_3
1 namespace Example_4_1_3 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 Run(); 8 9 Console.ReadLine(); 10 } 11 12 static void Run() 13 { 14 Console.WriteLine("請輸入一個除數:"); 15 var num = Console.ReadLine(); 16 var result = 10 / Convert.ToInt32(num); 17 } 18 } 19 }View Code 1.4、Example_4_1_4
1 namespace Example_4_1_4 2 { 3 internal class Program 4 { 5 static void Main(string[] args) 6 { 7 Sum1(10); 8 Debugger.Break(); 9 10 int i = 10; 11 int j = 20; 12 13 var sum = Sum1(i); 14 Console.WriteLine($"sum={sum}"); 15 16 Console.ReadLine(); 17 } 18 19 private static int Sum1(int a) 20 { 21 var i = a; 22 var j = 11; 23 int sum = Sum2(i, j); 24 25 return sum; 26 } 27 28 private static int Sum2(int a, int b) 29 { 30 var i = a; 31 var j = b; 32 var k = 13; 33 34 var sum = Sum3(i, j, k); 35 return sum; 36 } 37 38 private static int Sum3(int i, int j, int k) 39 { 40 return i + j + k; 41 } 42 } 43 }View Code
2、眼見為實
2.1、Windbg【Attach to Process】附加進程,通過 int 3 命令中斷程式。
測試代碼:Example_4_1_1
程式很簡單,直接運行 exe 程式,打開 Windbg,點擊菜單【attach to process】進入調試器界面。其實什麼操作都不用做,我們就可以看到調試器的輸出結果。特別強調,紅色標註的就是 int 3中斷。
1 ModLoad: 6fa30000 6faba000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll 2 ModLoad: 75f50000 75feb000 C:\Windows\System32\OLEAUT32.dll 3 (5384.3250): Break instruction exception - code 80000003 (first chance) 4 eax=00270000 ebx=00000000 ecx=7790cee0 edx=7790cee0 esi=7790cee0 edi=7790cee0 5 eip=778d3410 esp=04c2f92c ebp=04c2f958 iopl=0 nv up ei pl zr na pe nc 6 cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 7 ntdll!DbgBreakPoint: 8 778d3410 cc int 3
當然,我們也可以通過【g】命令,繼續運行程式,然後點擊工具欄的【break】按鈕,程式就進入中斷,這裡的結果和上面是一樣的。
1 0:006> g 2 (5384.528c): Break instruction exception - code 80000003 (first chance) 3 eax=00279000 ebx=00000000 ecx=7790cee0 edx=7790cee0 esi=7790cee0 edi=7790cee0 4 eip=778d3410 esp=04eaf960 ebp=04eaf98c iopl=0 nv up ei pl zr na pe nc 5 cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 6 ntdll!DbgBreakPoint: 7 778d3410 cc int 3
當然,也可以通過【u】命令,查看 ntdll!DbgBreakPoint的彙編代碼。
1 0:008> u ntdll!DbgBreakPoint 2 ntdll!DbgBreakPoint: 3 778d3410 cc int 3 4 778d3411 c3 ret(方法返回)
我們也可以查看【Disassembly】視圖,截圖如下:
解釋內核態:涉及到內核態的執行,我們也可以通過Windbg 查看,重新在打開一個 Windbg,點擊【文件】----》【Attach kernel】,選擇【local】項,點擊【ok】按鈕,進入調試器界面。然後,我們可以輸入【!idt】命令來查看。
1 lkd> !idt 2 3 Dumping IDT: fffff80069a91000 4 5 00: fffff80064b93100 nt!KiDivideErrorFaultShadow 6 01: fffff80064b93180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF80069A959D0 7 02: fffff80064b93240 nt!KiNmiInterruptShadow Stack = 0xFFFFF80069A957D0 8 03: fffff80064b932c0 nt!KiBreakpointTrapShadow 9 04: fffff80064b93340 nt!KiOverflowTrapShadow 10 ..................
紅色標註的就是在內核態的中斷函數,我們可以使用【u】命令,查看他的彙編代碼。
1 lkd> u nt!KiBreakpointTrapShadow 2 nt!KiBreakpointTrapShadow: 3 fffff800`64b932c0 f644240801 test byte ptr [rsp+8],1 4 fffff800`64b932c5 7467 je nt!KiBreakpointTrapShadow+0x6e (fffff800`64b9332e) 5 fffff800`64b932c7 0f01f8 swapgs 6 fffff800`64b932ca 0faee8 lfence 7 fffff800`64b932cd 650fba24251890000001 bt dword ptr gs:[9018h],1 8 fffff800`64b932d7 720c jb nt!KiBreakpointTrapShadow+0x25 (fffff800`64b932e5) 9 fffff800`64b932d9 65488b242500900000 mov rsp,qword ptr gs:[9000h] 10 fffff800`64b932e2 0f22dc mov cr3,rsp解釋:,我們可以通過【~* k】命令,列印出所有線程棧,......表示省略,內容太多,沒必要,顯示重要的就可以了。
1 0:008> ~*k 2 3 0 Id: 5384.4128 Suspend: 1 Teb: 0025e000 Unfrozen 4 # ChildEBP RetAddr 5 00 0057ee00 75942d3b ntdll!NtDelayExecution+0xc 6 ...... 7 8 1 Id: 5384.22a4 Suspend: 1 Teb: 00261000 Unfrozen 9 # ChildEBP RetAddr 10 00 0088fab8 778b0f30 ntdll!NtWaitForWorkViaWorkerFactory+0xc 11 ...... 12 13 2 Id: 5384.3034 Suspend: 1 Teb: 00264000 Unfrozen 14 # ChildEBP RetAddr 15 00 00affb9c 778b0f30 ntdll!NtWaitForWorkViaWorkerFactory+0xc 16 ....... 17 18 3 Id: 5384.35fc Suspend: 1 Teb: 00267000 Unfrozen 19 # ChildEBP RetAddr 20 00 00bff9f0 778b0f30 ntdll!NtWaitForWorkViaWorkerFactory+0xc 21 ...... 22 23 4 Id: 5384.3c50 Suspend: 1 Teb: 0026a000 Unfrozen 24 # ChildEBP RetAddr 25 00 0258f804 75939623 ntdll!NtWaitForMultipleObjects+0xc 26 01 0258f804 711567d7 KERNELBASE!WaitForMultipleObjectsEx+0x103 27 02 0258f86c 711566ff clr!DebuggerRCThread::MainLoop+0x99 28 03 0258f898 71156620 clr!DebuggerRCThread::ThreadProc+0xd0 29 04 0258f8c4 7711f989 clr!DebuggerRCThread::ThreadProcStatic+0xa3 30 05 0258f8d4 778c7084 KERNEL32!BaseThreadInitThunk+0x19 31 06 0258f930 778c7054 ntdll!__RtlUserThreadStart+0x2f 32 07 0258f940 00000000 ntdll!_RtlUserThreadStart+0x1b 33 34 5 Id: 5384.2050 Suspend: 1 Teb: 0026d000 Unfrozen 35 # ChildEBP RetAddr 36 00 0475fabc 75939623 ntdll!NtWaitForMultipleObjects+0xc 37 ...... 38 39 6 Id: 5384.2ce0 Suspend: 1 Teb: 00276000 Unfrozen 40 # ChildEBP RetAddr 41 00 04c2f84c 778b0f30 ntdll!NtWaitForWorkViaWorkerFactory+0xc 42 ...... 43 44 7 Id: 5384.489c Suspend: 1 Teb: 00273000 Unfrozen 45 # ChildEBP RetAddr 46 ...... 47 04 04d6ffb0 00000000 ntdll!_RtlUserThreadStart+0x1b 48 49 # 8 Id: 5384.528c Suspend: 1 Teb: 00279000 Unfrozen 50 # ChildEBP RetAddr 51 00 04eaf98c 7790cf19 ntdll!DbgBreakPoint(int 3 中斷) 52 01 04eaf98c 7711f989 ntdll!DbgUiRemoteBreakin+0x39 53 02 04eaf99c 778c7084 KERNEL32!BaseThreadInitThunk+0x19 54 03 04eaf9f8 778c7054 ntdll!__RtlUserThreadStart+0x2f 55 04 04eafa08 00000000 ntdll!_RtlUserThreadStart+0x1b
2.2、使用WinDbg 啟動程式,在進程初始化函數中斷進程。
測試代碼:Example_4_1_2
我們編譯項目,打開 Windbg,點擊【文件】----》【launch executable】附加程式,打開調試器的界面,程式已經處於中斷狀態。我們可以看到,如下輸出:
1 Executable search path is: 2 ModLoad: 00be0000 00be8000 Example_4_1_2.exe 3 ModLoad: 77860000 77a02000 ntdll.dll 4 ModLoad: 717e0000 71832000 C:\Windows\SysWOW64\MSCOREE.DLL 5 ModLoad: 77100000 771f0000 C:\Windows\SysWOW64\KERNEL32.dll 6 ModLoad: 75820000 75a33000 C:\Windows\SysWOW64\KERNELBASE.dll 7 ModLoad: 5efe0000 5f07f000 C:\Windows\SysWOW64\apphelp.dll 8 (300c.20b8): Break instruction exception - code 80000003 (first chance) 9 eax=00000000 ebx=00000000 ecx=534d0000 edx=00000000 esi=77871f64 edi=7787252c 10 eip=77910de2 esp=00f7f824 ebp=00f7f850 iopl=0 nv up ei pl zr na pe nc 11 cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 12 ntdll!LdrpDoDebuggerBreak+0x2b: 13 77910de2 cc int 3
紅色部分需要註意,然後我們使用【u】命令查看它的彙編代碼。
1 0:000> u ntdll!LdrpDoDebuggerBreak+0x2b 2 ntdll!LdrpDoDebuggerBreak+0x2b: 3 77910de2 cc int 3 4 77910de3 eb07 jmp ntdll!LdrpDoDebuggerBreak+0x35 (77910dec) 5 77910de5 33c0 xor eax,eax 6 77910de7 40 inc eax 7 77910de8 c3 ret 8 77910de9 8b65e8 mov esp,dword ptr [ebp-18h] 9 77910dec c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh 10 77910df3 8b4df0 mov ecx,dword ptr [ebp-10h]
我們可以使用【k】命令,繼續查看。
1 0:000> k 2 # ChildEBP RetAddr 3 00 00f7f850 7790b2f8 ntdll!LdrpDoDebuggerBreak+0x2b(int 3 中斷) 4 01 00f7fab0 778ba3d1 ntdll!LdrpInitializeProcess+0x1c98(進程初始化的時候執行的 break中斷,) 5 02 00f7fb08 778ba2c1 ntdll!_LdrpInitialize+0xba 6 03 00f7fb14 00000000 ntdll!LdrInitializeThunk+0x11
ntdll是一個網關函數dll,如果想使用內核的功能幾必須通過 ntdll 裡面的函數。ntdll!LdrpDoDebuggerBreak 這個中斷是在進程初始化之前進行的,是很早的一個時機,載入的東西也不多,只有【Example_4_1_2.exe,ntdll.dll,MSCOREE.DLL,KERNEL32.dll...】,之所以這樣,可以讓我們設置一些或者說配置一些初始化的東西,比如:載入 SOS等。
2.3、使用 bp 命令給程式下斷點,可以讓程式中斷。
測試代碼:Example_4_1_2
比如,我們在【int a = 10;】這樣代碼下斷點,行數:12.
我們編譯項目,打開 Windbg,點擊【文件】----》【launch executable】附加程式,打開調試器的界面,程式已經處於中斷狀態。我們使用【g】命令,繼續運行,然後我們使用【!clrstack】命令,查看線程棧。
1 0:000> !clrstack 2 OS Thread Id: 0x3c54 (0) 3 Child SP IP Call Site 4 00d5ed38 7597f262 [HelperMethodFrame: 00d5ed38] System.Diagnostics.Debugger.BreakInternal() 5 00d5edb4 7064f195 System.Diagnostics.Debugger.Break() [f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs @ 91] 6 00d5eddc 02ba0886 Example_4_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\...\Example_4_1_2\Program.cs @ 10] 7 00d5ef78 70faf036 [GCFrame: 00d5ef78]
紅色標註的是 Main 方法的地址,然後執行【!u 02ba0886】命令,查看他的彙編代碼。
1 0:000> !u 02ba0886 2 Normal JIT generated code 3 Example_4_1_2.Program.Main(System.String[]) 4 Begin 02ba0848, size a3 5 6 ...... 7 8 E:\Visual Studio 2022\...\Example_4_1_2\Program.cs @ 12:(這個行號就是C# 代碼的行號) 9 02ba0887 c745f00a000000 mov dword ptr [ebp-10h],0Ah
我們找到了代碼的位置,就可以下斷點了,使用【bp】命令。
0:000> bp 02ba0887
【g】繼續運行,就會到斷點出暫停。
效果如圖。
2.4、觸發異常,也可以讓程式中斷。
測試代碼:Example_4_1_3
代碼很簡單,我簡單說一些流程,我們首先將要測試的項目編譯好,然後打開 Windbg,通過【launch executable】附加應用程式,調試器會響應一個 int 3中斷,我們通過【g】命令,繼續運行程式。程式提示輸入一個數字,我輸入0,肯定就會異常了。
效果如圖:
異常中斷的代碼是:014b08ea f77df4 idiv eax, dword ptr [ebp-0Ch],效果如圖:
我們使用【dp】命令,查看【ebp-0Ch】代碼的值。
1 0:000> dp ebp-0Ch l1 2 012ff2fc 00000000
我們可以使用【dp】命令,也可以使用【?】命令,查看一下eax 是什麼,其實 eax就是十進位的10。
1 0:000> ? eax 2 Evaluate expression: 10 = 0000000a
代碼【idiv】就是表示除法觸發的異常。
我們也可以使用【k】命令,查看調用棧,也能看出在哪裡中斷。
1 0:000> k 2 # ChildEBP RetAddr 3 00 012ff308 014b086b Example_4_1_3!COM+_Entry_Point <PERF> (Example_4_1_3+0x6308ea) [E:\...\Example_4_1_3\Program.cs @ 18] 4 01 012ff318 7077f036 Example_4_1_3!COM+_Entry_Point <PERF> (Example_4_1_3+0x63086b) [E:\...\Example_4_1_3\Program.cs @ 9] 5 ...... 6 0f 012ffde4 77057054 ntdll!__RtlUserThreadStart+0x2f 7 10 012ffdf4 00000000 ntdll!_RtlUserThreadStart+0x1b
紅色部分就是中斷的 C# 代碼的行號。
2.5、單步調試命令測試。