Advanced .Net Debugging 3:基本調試任務(上)

一、簡介
    這是我的《Advanced .Net Debugging》這個系列的第三篇文章。這個系列的每篇文章寫的週期都要很長,因爲每篇文章都是原書的一章內容(太長的就會分開寫)。再者說,原書寫的有點早,有些內容還是需要修正的,調試每個案例,這都是需要時間的。今天這篇文章的標題雖然叫做“基本調試任務”,但是這章的內容還是挺多的。我本來想用一篇文章把這個章節寫完,我發現是不可能的,於是就分“上“和”下”用兩篇來寫。既然,我們要調試我們的 .Net 應用程序,那必須掌握一些調試技巧、方法和工具。我們習慣了使用 Visual Studio IDE 的調試技巧,比如:單步調試、下斷點、過程調試等,但是,有些時候,VS 是使用不了的。那我們也必須學習如何使用 Windbg 的命令,在沒有 VS IDE 的情況下,如何調試我們的程序,如何設置斷點、恢復執行、中斷執行、退出調試回話,如何爲 JIT 編譯的方法設置斷點,如何爲沒有被 JIT 編譯的方法設置斷點,爲泛型方法設置斷點等等。如果我們想成爲一名合格程序員,這些調試技巧都是必須要掌握的。
    如果在沒有說明的情況下,所有代碼的測試環境都是 Net 8.0,如果有變動,我會在項目章節裏進行說明。好了,廢話不多說,開始我們今天的調試工作。

       調試環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
          操作系統:Windows Professional 10
          調試工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
          下載地址:可以去Microsoft Store 去下載
          開發工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
          Net 版本:.Net 8.0
          CoreCLR源碼:源碼下載

      說明一下,這個系列內容安排有些變動,我把基礎知識和眼見爲實放在了一起,講什麼內容,立刻就將講的內容做一個眼見爲實驗證,這樣做更便於大家理解,我認爲這樣會更好一些,不用在文章裏來回跑了。
      命令行調試器要想成功使用,必須先安裝 MSVC,想要了解詳情,可以去微軟的官網:https://learn.microsoft.com/zh-cn/cpp/build/building-on-the-command-line?view=msvc-170,如果我們使用的 Visual Studio 2022,本身也有命令行工具,我們就可以直接使用。安裝如圖:
      

二、調試源碼
    廢話不多說,本節是調試的源碼部分,沒有代碼,當然就談不上測試了,調試必須有載體。
    2.1、ExampleCore_3_1_1
 1 namespace ExampleCore_3_1_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Console.WriteLine("Welcome to Advanced .Net Debugging!");
 8             Console.Read();
 9         }
10     }
11 }

    2.2、ExampleCore_3_1_2
 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_3_1_2
 4 {
 5     internal class Program
 6     {
 7         static void Main(string[] args)
 8         {
 9             Console.WriteLine("第一次執行,並開始中斷執行!");
10             Debugger.Break();
11             Console.WriteLine("第二次執行,並開始中斷執行!");
12             Debugger.Break();
13             Console.WriteLine("第三次執行,並開始中斷執行!");
14             Debugger.Break();
15 
16             Console.WriteLine("恢復執行調試完畢!");
17             Console.ReadLine();
18         }
19     }
20 }

    2.3、ExampleCore_3_1_3
 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_3_1_3
 4 {
 5     internal class Program
 6     {
 7         static void Main(string[] args)
 8         {
 9             Sum1(10);
10             Debugger.Break();
11 
12             int i = 10;
13             int j = 20;
14 
15             var sum = Sum1(i);
16             Console.WriteLine($"sum={sum},i={i},j={j}");
17 
18             Console.ReadLine();
19         }
20 
21         private static int Sum1(int a)
22         {
23             var i = a;
24             var j = 11;
25             int sum = Sum2(i, j);
26 
27             return sum;
28         }
29 
30         private static int Sum2(int a, int b)
31         {
32             var i = a;
33             var j = b;
34             var k = 13;
35 
36             var sum = Sum3(i, j, k);
37             return sum;
38         }
39 
40         private static int Sum3(int i, int j, int k)
41         {
42             return i + j + k;
43         }
44     }
45 }

    2.4、ExampleCore_3_1_4
 1 namespace ExampleCore_3_1_4
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             //第一次調用函數
 8             Console.WriteLine("Press any key(1st instance function)");
 9             Console.ReadKey();
10             BreakPoint bp = new BreakPoint();
11             bp.AddAndPrint(10, 5);
12 
13             //第二次調用函數
14             Console.WriteLine("Press any key(2nd instance function)");
15             Console.ReadKey();
16             bp = new BreakPoint();
17             bp.AddAndPrint(100, 50);
18         }
19     }
20 
21     internal class BreakPoint
22     {
23         public void AddAndPrint(int a, int b)
24         {
25             int res = a + b;
26             Console.WriteLine("Adding {0}+{1}={2}", a, b, res);
27         }
28     }
29 }

    2.5、ExampleCore_3_1_5
 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_3_1_5
 4 {
 5     internal class Program
 6     {
 7         static void Main(string[] args)
 8         {
 9             Debugger.Break();
10 
11             var mylist = new MyList<int>();
12 
13             mylist.Add(10);
14 
15             Console.ReadLine();
16         }
17     }
18 
19     public class MyList<T>
20     {
21         public T[] arr = new T[10];
22 
23         public void Add(T t)
24         {
25             arr[0] = t;
26         }
27     }
28 }

三、基礎知識和眼見爲實
    3.1、調試器以及調試目標
        A、知識介紹
            在任何調試中都包含兩個組件:調試器和調試目標。
            調試器:是一個引擎,我們必須通過這個引擎和調試目標進行交互。所有與調試目標之間的交互操作(如:設置斷點、觀察狀態等),都可以通過調試器的命令完成,而調試器將在調試目標的環境中執行這些命令。
            調試目標:一般指我們編寫的程序,對於 .Net程序員來說就是,或者是要調試的程序。
            它們之間的關係,有一張圖可以更好的表現他們之間的關係。如圖:
            
        B、眼見爲實
            在【眼見爲實】這個章節裏,有些調試動作是一樣的,我就不每個節點都寫了。我就寫在這裏了。首先編譯好自己的項目,根據自己的喜好,可以切換到編譯項目目錄下,也可以直接輸入項目所在目錄,接着就可以進行項目調試了。我使用的命令行工具是【Developer Command Prompt for VS 2022】

            1)、使用NTSD調試器
                調試源碼:ExampleCore_3_1_1
                我們命令行工具中輸入命令:ntsd,打開新窗口。效果如圖:
                
                NTSD 的新窗口。

                

                如果沒有指定任何參數,只能顯示一組可用的選項。我們將我們的項目完整路徑和項目名稱作爲輸入參數。執行【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\ExampleCore_3_1_1.exe】,彈出新窗口,如圖:
                

                新的 NTSD 窗口,如圖:
                

                上面這個截圖分三個部分:第一部分是符號文件搜索路徑,如圖:
                

                第二部分:加載的所有模塊,表示應用程序所需要的模塊都已經加載完畢。如圖:
                

                第三部分:中斷指令異常。每當調試器啓動一個進程或者調試器附加到一個進程的時候,調試器都會注入一箇中斷指令,這條指令將使調試目標停止運行。斷點指令的作用:使用戶與調試器和調試目標進行交互。這裏是 int 3 中斷。如圖:
                

                調試器的命令提示符是:X:Y>,X 表示當前正在被調試的活動目標(在大多數調試器中,這個值爲 0),Y 表示導致調試器中斷的線程 ID。如圖:
                


            2)、使用 NTSD 附加進程
                調試源碼:ExampleCore_3_1_1
                當在調試器下啓動有問題的引用程序的時候,這種調試方式很有作用。如果是引用程序已經運行起來了,那我們該如何調試呢?我們可以通過給【ntsd】命令,加上 -p 命令,就可以附加進程了。比如:你寫的一個 Web 服務已經成功運行起來了。隨着時間的推移,這個 Web 服務開始表現出一些怪怪的行爲,你希望當程序具有這總奇怪行爲的時候對它進行調試,附加調試就可以大展拳腳了。
                -p 參數告訴調試器希望調試一個正在運行的進程。對於這個參數後,再跟上要調試進程的 Id 就可以了。
                執行命令【ntsd -p 14624】,如圖:
                

                打開新的調試器窗口,如圖:
                

                下面很有很多內容,就不顯示了。


            3)、使用 TList.exe 顯示進程 Id。
                調試源碼:ExampleCore_3_1_1
                如果我們想獲取一個進程的 id,可以有很多方法,我們可以使用 Windows 調試工具集中的 tlist.exe,tlist.exe 會輸出所有運行的進程名稱和 ID。啓動我們的 ExampleCore_3_1_1.exe,在控制檯輸出:Welcome to Advanced .Net Debugging!,我們打開命令工具【Developer Command Prompt for vs 2022】,輸出命令 tlist,顯示如下:
                

                我們進程的信息如下:
                


            4)、使用 Windbg 調試
                調試源碼:ExampleCore_3_1_1
                編譯好我們的項目,打開【Windbg Preview】調試器。依次點擊【文件】--->【Launch executable】加載我們的項目文件:ExampleCore_3_1_1.exe,選擇【打開】按鈕,成功加載並進入調試器界面。
                
                點擊【文件】按鈕,切換界面。

                

                點擊【Launch executable】按鈕打開新窗口。如圖:

                

                進入調試器界面,如圖:
                

                我們就可以在下方的命令框中輸入命令,調試程序了。如圖:
                
                以上是在調試器中啓動有問題的引用程序的流程,如果想使用【Windbg Preview】附加進程該怎麼辦呢?其實,也很簡單,編譯項目,打開調試器,依次點擊【文件】--->【Attach to process】,如圖:
                
                打開選擇進程的窗口,在右側。
                

                之後就是進入調試器界面,使用方法就一樣了。


    3.2、符號
        A、知識介紹
            符號文件:是一種輔助數據,它包含了對引用程序代碼的一些標註信息,這些信息在調試過程中非常有用。如果沒有這些輔助數據,那獲得的信息只有引用程序的二進制文件了。對二進制代碼進行調試是非常困難的,因爲你無法看到代碼中的函數名、數據結構名等。符號文件的擴展名通常是 .pdb。
            在符號文件中包含很多重要的信息,例如:行號和局部變量的名稱等,它能極大的提高調試的效率。
            符號文件有兩種類型:私有符號文件(Private)和公有符號文件(Public)。
            私有符號文件:是大多數開發人員在日常工作中使用的符號文件,其中包含了調試會話中所需要的所有符號信息。私有符號文件一般是和我們編譯的程序存放在一起的,調試器在開始調試的時候,會自動加載他們。如圖:
            
            

            公有符號文件:這類型的符號文件只是有選擇的包含了一些符號信息,這會使調試工作困難一點。比如:在 Microsoft 符號服務器上存儲了一些公有符號文件。每當將調試器指向 Microsoft 符號服務器時,都可以下載這些符號文件,並在調試會話中使用它們。
            之所以有【私有符號文件】和【公有符號文件】之分,主要是爲了保護知識產權。私有符號中包含大量底層技術信息,就很容易對應用程序進行逆向工程。公有符號就不存在這樣的問題,既可以調試,又不會泄露核心技術信息。

            
        B、眼見爲實
            .sympath(+) 命令的使用,加號表示不會替換,而是追加。
                調試源碼:ExampleCore_3_1_1
                編譯好我們的項目,打開【Windbg Preview】工具,依次打開【文件】--->【Launch executable】,加載我們的項目文件:ExampleCore_3_1_1.exe,進入調試器。直接執行【.sympath】命令,就可以看到當前的符號文件信息。
1 0:000> .sympath 
2 Symbol search path is: srv*
3 Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols
4 
5 ************* Path validation summary **************
6 Response                         Time (ms)     Location
7 Deferred                                       srv*

                【.sympath】命令可以設置符號文件路徑。在命令後跟上具體的符號文件所在的地址。

1 0:007> .sympath E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\
2 Symbol search path is: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\
3 Expanded Symbol search path is: e:\visual studio 2022\source\projects\advanceddebug.netframework.test\examplecore_3_1_1\bin\debug\net8.0\
4 
5 ************* Path validation summary **************
6 Response                         Time (ms)     Location
7 OK                                             E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_1\bin\Debug\net8.0\

                雖然,我們設置新的符號文件的路徑,但是並不會從這個路徑中加載任何符號。如果想要加載符號信息,必須執行【.reload】命令。我們還是設置回去吧,防止以後有錯誤。

1 0:007> .sympath srv*
2 Symbol search path is: srv*
3 Expanded Symbol search path is: cache*;SRV*https://msdl.microsoft.com/download/symbols
4 
5 ************* Path validation summary **************
6 Response                         Time (ms)     Location
7 Deferred                                       srv*

                如果我們使用【.sympath】命令設置錯了符號文件地址,使用【.reload】也無法加載符號文件,我們可以使用【.symfix】命令,修復問題就可以了。

 1 0:007> .symfix
 2 DBGHELP: Symbol Search Path: cache*;SRV*https://msdl.microsoft.com/download/symbols
 3 SYMSRV:  BYINDEX: 0x17
 4          C:\ProgramData\Dbg\sym
 5          ntdll.pdb
 6          63E12347526A46144B98F8CF61CDED791
 7 SYMSRV:  PATH: C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb
 8 SYMSRV:  RESULT: 0x00000000
 9 DBGHELP: ntdll - public symbols  
10         C:\ProgramData\Dbg\sym\ntdll.pdb\63E12347526A46144B98F8CF61CDED791\ntdll.pdb
11 SYMSRV:  BYINDEX: 0x18
12          C:\ProgramData\Dbg\sym
13          kernel32.pdb
14          85A257DB4B7B82F2E19AD96AB7BB116A1
15 SYMSRV:  PATH: C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb
16 SYMSRV:  RESULT: 0x00000000
17 DBGHELP: KERNEL32 - public symbols  
18         C:\ProgramData\Dbg\sym\kernel32.pdb\85A257DB4B7B82F2E19AD96AB7BB116A1\kernel32.pdb

                這裏有這麼多輸出,是因爲我執行了【!sym noisy】命令,開啓了顯示符號加載的詳細信息,如果不想顯示,可以使用【!sym quiet 】命令。

1 0:007> !sym quiet 
2 quiet mode - symbol prompts on
3 0:007> .symfix
                【.symfix】命令後面可以跟一個參數,就是本地路徑,用來緩存下載的符號文件,就不用調試器每次去下載相同的符號了,可以直接從本地加載。在默認情況下,如果沒有指定本地路徑緩存,那麼調試器將使用調試軟件包安裝的路徑下的 sym 文件夾。

    3.3、控制調試目標的執行
        在任何調試會話中,能夠控制調試目標的執行是非常重要的。我們可以設置斷點,然後恢復程序的執行知道斷點處,在此可以查看應用程序的狀態,單步跟蹤到函數內部,然後在恢復執行等。

        3.3.1、中斷執行
            調試器中斷程序的方式有很多種,我舉三種最常用的中斷執行的方式。
            1)、如果我們使用的命令行調試器,可以使用【ctrol+c】組合鍵手動方式中斷調試目標的執行,例如:調試死鎖問題。
            2)、給我們的應用程序設置斷點來中斷調試目標的執行。通過設置斷點,可以很方便的使調試器在執行流程的任意位置上中斷執行。
            3)、拋出異常可以使調試器中斷執行。          
                
        3.3.2、恢復執行
            A、知識介紹
                當調試器中斷執行時(可能是觸發了斷點或者其他的事件),可以使用【g】命令回覆調試器的執行。如果【g】命令不帶任何參數,只是回覆調試目標的執行,直到下一次發生某個調試事件。
            B、眼見爲實
                1)、使用【g】命令恢復執行。
                    調試源碼:ExampleCore_3_1_2
                    編譯好我們的項目,打開我們的命令工具,輸入命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】,打開調試器新窗口。
                    

                    打開新的調試器窗口,如圖:
                    

                    觸發初始斷點是調試器的默認行爲,也是調試人員開始分析應用程序的最早時機。此時,調試目標會停止執行並等待輸入命令。此刻,我們可以輸入【g】命令,恢復執行,調試器輸出如圖:
                    

                2)、禁用初始和退出斷點
                    調試源碼:ExampleCore_3_1_2
                    如果不希望調試器在初始啓動時停止程序的執行,可以在啓動調試器時使用 -g 命令開關,每當調試器退出時,調試器也將停止執行,可以使用 -G(大寫)命令開關,避免在進程結束時觸發最終的斷點。
                    編譯好我們的項目,打開我們的命令行工具,使用【ntsd -g E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】命令,啓動調試器。如圖:
                    

                    打開新的調試器窗口,這次已經輸出“第一次執行,並開始中斷執行!”,如圖:
                    

                    如果我們使用 -G 命令開關,執行命令【ntsd -G E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】,調試器在初始會中斷執行。如圖:
                    

                    如果沒有使用 -G 命令開關,我們還需要輸入命令才能退出。

        3.3.3、單步調試代碼
            我們使用過 VS IDE 的調試功能,快捷鍵有:F10,F11,F9等,調試器也爲我們提供了類似的命令。但是,需要注意:如果在調試託管代碼時使用非託管調試器,那麼通常是對 JIT 編譯器產生的機器代碼進行單步調試。
            A、知識介紹
                1)、p 命令
                    p(step):命令其實就是 VS 中的 f10 快捷鍵,單步執行,遇到函數也是當成一條指令執行,不會進入函數體。
                2)、t 命令
                    t(trace):命令其實就是 VS 的 f11 快捷鍵,它是一種進入函數的單步執行調試。
                3)、pc 命令
                    pc(Step to Next Call)    就是一直運行直到遇到 call 爲止,不會進入函數體,call 是一個函數調用,彙編指令。
                4)、tc 命令
                    tc(Trace to Next Call)    和 pc 不同的是,tc 會進入方法體,直到遇到 call 爲止。
                5)、pt 命令
                    pt(Step to Next Return)    如果有方法會進入方法內部遞歸處理,遇到下一個 ret 爲止。  
                6)、tt 命令
                    tt(Trace to Next Return)    會進入函數體直到遇到 ret 爲止。遞歸的意思。

            B、眼見爲實
                這一節的演示,使用【Windbg Preview】,我沒有使用的是【NSTD】調試器,其他他們是一樣的,只不過一個是由界面的,一個是沒界面的。有些操作是一樣的,我就寫在這裏了。
                編譯好我們的項目,打開【Windbg Preview】,依次點擊【文件】--->【Launch executable】,加載我們的項目文件:ExampleCore_3_1_3.exe,進入調試器。界面的內容太多,我們可以使用【.cls】命令,清空調試器的界面。我們再使用【g】命令,繼續運行調試器,我們現在查看一下託管代碼的調用棧,執行命令【!clrstack】。
0:000> g
ModLoad: 00007ff9`454c0000 00007ff9`454f0000   C:\Windows\System32\IMM32.DLL
ModLoad: 00007ff8`8d8e0000 00007ff8`8d938000   C:\Program Files\dotnet\host\fxr\8.0.0\hostfxr.dll
ModLoad: 00007ff8`812d0000 00007ff8`81334000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\hostpolicy.dll
ModLoad: 00007ff8`80de0000 00007ff8`812cb000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\coreclr.dll
ModLoad: 00007ff9`454f0000 00007ff9`45619000   C:\Windows\System32\ole32.dll
ModLoad: 00007ff9`44b10000 00007ff9`44e64000   C:\Windows\System32\combase.dll
ModLoad: 00007ff9`45d60000 00007ff9`45e35000   C:\Windows\System32\OLEAUT32.dll
ModLoad: 00007ff9`444f0000 00007ff9`4456f000   C:\Windows\System32\bcryptPrimitives.dll
(3994.1028): Unknown exception - code 04242420 (first chance)
ModLoad: 00007ff8`7fb10000 00007ff8`807a8000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Private.CoreLib.dll
ModLoad: 00007ff8`7f950000 00007ff8`7fb08000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\clrjit.dll
ModLoad: 00007ff9`44120000 00007ff9`44133000   C:\Windows\System32\kernel.appcore.dll
ModLoad: 0000021a`398c0000 0000021a`398c8000   E:\Visual Studio 2022\.\ExampleCore_3_1_3\bin\Debug\net8.0\ExampleCore_3_1_3.dll
ModLoad: 0000021a`398d0000 0000021a`398de000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Runtime.dll
ModLoad: 00007ff8`7f920000 00007ff8`7f948000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.0\System.Console.dll
(3994.1028): Break instruction exception - code 80000003 (first chance)
KERNELBASE!wil::details::DebugBreak+0x2:
00007ff9`44799202 cc              int     3

0:000> !clrstack
OS Thread Id: 0x1028 (0)
        Child SP               IP Call Site
00000020B9B7E6A8 00007ff944799202 [HelperMethodFrame: 00000020b9b7e6a8] System.Diagnostics.Debugger.BreakInternal()
00000020B9B7E7B0 00007ff87ff360aa System.Diagnostics.Debugger.Break() [/_/src/coreclr/./Debugger.cs @ 18]
00000020B9B7E7E0 00007ff8213f197f ExampleCore_3_1_3.Program.Main(System.String[]) [E:\Visual Studio 2022\.\ExampleCore_3_1_3\Program.cs @ 10]

                    我們找到了紅色標註的【Program.Main()】方法的地址:00007ff8213f197f有了這個地址,我們就可以對這個地址下一個斷點。

1 0:000> bp 00007ff8213f197f

                    設置好斷點後,我們就可以使用【g】命令,繼續運行調試器。


                1)、p、pc、pt 命令的使用
                    調試源碼:ExampleCore_3_1_3
                    我們設置好了斷點,就可以開始我們的調試工作了。繼續使用【g】運行調試器,調試器會在【Debugger.Break()】這行代碼暫停,效果如圖:
                    

                    我們可以使用【p】命令,單步執行,到了第15行代碼,會直接跳過而執行,不會進入方法。當然,這個過程要執行多次【p】命令。

 1 0:000> p
 2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x50:
 3 00007ff8`213f1980 c745fc0a000000  mov     dword ptr [rbp-4],0Ah ss:00000020`b9b7e84c=00000000
 4 0:000> p
 5 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x57:
 6 00007ff8`213f1987 c745f814000000  mov     dword ptr [rbp-8],14h ss:00000020`b9b7e848=00000000
 7 0:000> p
 8 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x5e:
 9 00007ff8`213f198e 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:00000020`b9b7e84c=0000000a
10 0:000> p
11 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x61:
12 00007ff8`213f1991 ff1531520a00    call    qword ptr [00007ff8`21496bc8] ds:00007ff8`21496bc8=00007ff8213f1a20
13 0:000> p
14 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x67:
15 00007ff8`213f1997 8945c0          mov     dword ptr [rbp-40h],eax ss:00000020`b9b7e810=00000000
16 0:000> p
17 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x6a:
18 00007ff8`213f199a 8b4dc0          mov     ecx,dword ptr [rbp-40h] ss:00000020`b9b7e810=00000022
19 0:000> p
20 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x6d:
21 00007ff8`213f199d 894df4          mov     dword ptr [rbp-0Ch],ecx ss:00000020`b9b7e844=00000000
22 0:000> p
23 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x70:
24 00007ff8`213f19a0 488d4dc8        lea     rcx,[rbp-38h]

                    【pc】命令調試:
                        前面的操作一樣,查看堆棧,設置斷點,開始運行,到斷點出暫停。
                        【pc】命令很簡單,我們直接輸入【pc】,代碼直接會運行到【var sum = Sum1(i)】,如圖:
                        

                        中間的代碼是直接跳過的。

                    【pt】命令調試:
                        前面的操作一樣,查看堆棧,設置斷點,開始運行,到斷點出暫停。
                        我又增加了一些斷點,斷點如圖:
                        

                        接着如圖:

                        

                        執行【pt】命令的過程如下,執行【g】命令,到【Debugger.Break()】這樣代碼處中斷執行。

1 0:000> g
2 Breakpoint 0 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:
4 00007ff8`2101197f 90              nop

                        效果如圖:
                        

                        繼續執行【pt】命令,會在【var sum = Sum1(i)】這行帶出中斷執行。

1 0:000> g
2 Breakpoint 1 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x5e:
4 00007ff8`20af198e 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:000000ef`fb17e6ac=0000000a

                        執行效果如圖:
                        

                        繼續執行【pt】命令,會進入【Sum1()】方法內部,在斷點處中斷執行。

1 0:000> pt
2 Breakpoint 2 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x24:
4 00007ff8`20af1a84 90              nop

                        執行效果如圖:
                        

                        繼續執行【pt】命令,會到【int sum = Sum2(i, j)】這行代碼中斷執行。

1 0:000> pt
2 Breakpoint 8 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x32:
4 00007ff8`20af1a92 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:000000ef`fb17e62c=0000000a

                        執行效果如圖:
                        

                        繼續執行【pt】命令,會進入【Sum2()】方法內部。

1 0:000> pt
2 Breakpoint 4 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x2c:
4 00007ff8`20af1afc 90              nop

                        執行效果如圖:
                        

                        繼續執行【pt】命令,會到【var sum=Sum3(i,j,k)】這行代碼處中斷執行。

1 0:000> pt
2 Breakpoint 5 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x40:
4 00007ff8`20af1b10 8b4dfc          mov     ecx,dword ptr [rbp-4] ss:000000ef`fb17e5dc=0000000a

                        執行效果如圖:
                        

                        繼續執行【pt】命令,會進入【Sum3()】方法內部。

1 0:000> pt
2 Breakpoint 13 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x27:
4 00007ff8`20af1b77 90              nop

                        執行效果如圖:
                        

                        繼續執行【pt】命令,執行到43行代碼處中斷執行。

1 0:000> pt
2 Breakpoint 7 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x35:
4 00007ff8`20af1b85 8b45fc          mov     eax,dword ptr [rbp-4] ss:000000ef`fb17e58c=00000022

                        執行效果如圖:
                        

                        最後我們執行一個【pt】命令,也就是【Sum3()】方法結束,遇到【ret】,調試器中斷執行。

1 0:000> pt
2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x3d:
3 00007ff8`20af1b8d c3              ret

                        這裏我增加了很多斷點,是爲了測試是否會進入方法內部。執行【pt】命令,如果有方法調用會進入方法內部,知道遇到【ret】爲止。


                2)、t、tc、tt 命令的使用
                    調試源碼:ExampleCore_3_1_3
                    【t】命令使用:
                        我們進入調試器,設置斷點,使用【g】命令運行調試器。調試器會在 Program.Main() 方法的【Debugger.Break()】這行代碼中斷執行。
1 0:000> g
2 Breakpoint 0 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:
4 00007ff8`20dd197f 90              nop

                        執行效果如圖:
                        

                        繼續運行【t】命令,單步執行,遇到【Sum1(i)】方法,就會進入方法內部進行單步調試。
                        

                        【t】命令很簡單,就像 VS 的 F11快捷鍵一樣,按一下執行一條命令。


                    【tc】命令使用:
                        當我們在調試器中使用【bp】命令設置好斷點後,就可以看是測試命令了。
1 0:000> g
2 Breakpoint 0 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:
4 00007ff8`20e0197f 90              nop

                        執行【g】命令,調試器會在 Program.Main() 方法的【Debugger.Break()】這行代碼出中斷執行。執行效果如圖:
                        

                        我們繼續執行【tc】命令,它會到【var sum = Sum1(i)】這行代碼處中斷執行,因爲調用 Sum1方法是通過【call】指令的。

1 0:000> tc
2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x61:
3 00007ff8`20e01991 ff1531520a00    call    qword ptr [00007ff8`20ea6bc8] ds:00007ff8`20ea6bc8=00007ff820e01a60

                        執行效果如圖:
                        

                        再次執行【tc】命令,調試器會在Sum1方法內的【int sum = Sum2(i, j)】這行代碼處中斷執行。

1 0:000> tc
2 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x38:
3 00007ff8`20e01a98 ff1542510a00    call    qword ptr [00007ff8`20ea6be0] ds:00007ff8`20ea6be0=00007ff820e01ad0

                        執行效果如圖:
                        

                        就不繼續了,下一個中斷執行點是Sum2方法【var sum = Sum3(i, j, k)】這行代碼,這個命令很簡單。


                    【tt】命令使用:
                        當我們在調試器中設置到斷點後,就可以開始調試了,測試我們的命令了。
                        我們使用【g】命令運行調試器,調試器會在Program.Main方法的【Debugger.Break()】這行代碼處中斷執行。
1 0:000> g
2 Breakpoint 0 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Main+0x4f:
4 00007ff8`20e0197f 90              nop

                        執行效果如圖:
                        

                        繼續執行【tt】命令,會進入Sum1方法內部。

1 0:000> tt
2 Breakpoint 2 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum1+0x24:
4 00007ff8`20e01a84 90              nop

                        執行效果如圖:
                        

                        再次繼續執行【tt】命令,會進入Sum2方法內部。

1 0:000> tt
2 Breakpoint 4 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum2+0x2c:
4 00007ff8`20e01afc 90              nop

                        執行效果如圖:
                        

                        再次繼續執行【tt】命令,會進入Sum3方法內部。

1 0:000> tt
2 Breakpoint 6 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x27:
4 00007ff8`20e01b77 90              nop

                        執行效果如圖:
                        

                        當我們再次運行【tt】命令,調試器會在【43】行中斷執行。再次執行【tt】命令,遇到Sum3方法的返回命令【ret】則爲止。

1 0:000> tt
2 Breakpoint 7 hit
3 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x35:
4 00007ff8`20e01b85 8b45fc          mov     eax,dword ptr [rbp-4] ss:00000045`d7f7e52c=00000022
5 0:000> tt
6 ExampleCore_3_1_3!ExampleCore_3_1_3.Program.Sum3+0x3d:
7 00007ff8`20e01b8d c3              ret

                        這個命令也不復雜,大家慢慢體會吧。


        3.3.4、退出調試回話
            在執行完一個調試會話後,可以有很多方式退出調試回話,這裏演示主要是以命令行調試器爲主。
            A、知識介紹
                1)、q(quit):結束調試會話+調試程序退出
                    調試會話結束,應用程序也會退出。
                2)、qd(quit and detach):結束調試會話+調試程序繼續運行
                    調試會話結束,應用程序保持運行態,不會退出。
            B、眼見爲實
                這裏調試我使用的是【ntsd】,沒有使用【Windbg Preview】,使用是一樣的。
                1)、q 命令退出
                    調試源碼:ExampleCore_3_1_2
                    編譯好我們的項目,執行命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】加載調試器。如圖:
                    

                    開啓新的調試器窗口,我們可以輸入【q】命令,查看結果。如圖:
                    
                    按回車,調試器也關閉了,程序也關了。

                
                2)、qd 命令退出
                    調試源碼:ExampleCore_3_1_2
                    編譯好我們的項目,通過命令【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_2\bin\Debug\net8.0\ExampleCore_3_1_2.exe】加載調試器。如圖:
                    

                    打開新的調試器窗口,輸入命令【qd】,按回車,如圖:
                    

                    效果很明顯,不用多說了。


    3.4、加載託管代碼調試的擴展命令
        3.4.1、加載 SOS 調試器擴展
            A、知識介紹
                SOS 調試器的擴展 DLL 與程序使用的 CLR 版本是是相關的。因此,在發佈每個 CLR 主版本的同時,都會發佈一個新版本的 SOS 調試擴展。以確保這個 DLL 可以使用該版本 CLR 的新功能。SOS 擴展作爲運行時的一部分發布的,它的路徑位於:%systemRoot%Microsoft.Net\Framework\<framework version>\sos.dll。
                在非託管調試器中可以使用兩類命令,一類是:元命令,另一類是:擴展命令
                元命令:指在調試器引擎中內置的命令,當使用該命令的時候,必須在命令前加上英文點號。如:.cls。如果想要列出所有的元命令,可以使用【.help】命令。
                擴展命令:指在調試器引擎之外的獨立的 dll 中實現的,這些 DLL 也被稱爲調試器擴展。在使用擴展命令的時候,命令前面加上前綴“!”。如:!clrstack。
                無論是【NTSD】還是【Windbg Preview】,現在會自動加載 SOS.DLL,以前的老版本需要使用【.load】加載 SOS.DLL。
                
            B、眼見爲實
                使用【.load】加載 SOS.DLL
1 0:000> .load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\sos.dll

        3.4.2、加載 SOSEX 調試器擴展            
            這個調試器擴展很好用,但是也很可惜,它只支持 Net Framework 版本。在最新的 .Net 版本是拋棄的,不能使用了。如果想查看調試過程,可以查看我的另外一個系列【Net 高級調試】中有一篇文章,地址:https://www.cnblogs.com/PatrickLiu/p/17788840.html

    3.5、控制 CLR 的調試
        如果我們想在託管代碼調試的過程總輸出各種信息(例如:SOS 命令的輸出),我們可以加載一個輔助 DLL,稱爲:mscordacwks.dll。加載【mscordacwks.dll】的路徑取決於倍加再到進程中【mscorwks.dll】的路徑。在【實時調試】中一般沒有問題,兩個 dll 版本是一致的。如果是【事後調試】,可能會出現版本不一致的情況,我們可以使用元命令:cordll 來解決。
        比如:.cordll -lp c:\x\y\z,這樣就能告訴調試器從 c:\x\y\z 目錄下加載 mscordacwks.dll。

    3.6、設置斷點
        設置斷點的目的就是爲了告訴目標程序在執行到了斷點處停止執行。斷點可以使用開發人員分析程序在執行流中的狀態,並且找到出現問題的根本原因。在非託管代碼中設置斷點很容易,因爲我們知道了代碼的位置,於是就可以使用【bp】命令在代碼位置處設置斷點了。

        3.6.1、在非託管代碼中設置斷點。
            這次我們使用 notepad.exe 做這個調試,因爲它是非託管程序,代碼已經編譯成機器碼了,代碼地址有了,我們就可以使用【bp】命令直接設置斷點了。
            眼見爲實:
                1)、使用【NTSD】調試
                    我們先打開【notepad.exe】應用程序,然後,執行【tlist】命令,獲取 notepad 應用程序的 id,效果如圖:
                           
                    執行命令【ntsd -p 9580】,附加 notepad 應用程序的進程,打開調試器。
1 D:\Program Files\Microsoft Visual Studio\2022\Community>ntsd -p 9580

                    執行效果如圖:
                    

                    打開調試器窗口,如下:
                    

                    這個時候,我們打開的 notepad 是不能操作的,因爲調試器已經中斷執行了。
                    執行【X notepad!*Save*】命令,查找 Notepad 的包含 Save 關鍵字的方法。

 1 0:001> X notepad!*Save*
 2 00007ff6`510e86a0 notepad!ShowOpenSaveDialog (void)
 3 00007ff6`510ec5cc notepad!InitLegacyOpenSaveEncodingComboBox (void)
 4 00007ff6`510ffd54 notepad!TraceFileSaveStart (void __cdecl TraceFileSaveStart(void))
 5 00007ff6`510f047c notepad!SaveGlobals (void __cdecl SaveGlobals(void))
 6 00007ff6`510f0314 notepad!RegGetIntSaveDefault (unsigned long __cdecl .....)
 7 00007ff6`510ec7a0 notepad!NpLegacySaveDialogHookProc (unsigned __int64 __cdecl ....))
 8 00007ff6`511013a8 notepad!FileSaveDialog_GetSelectedEnterpriseId (FileSaveDialog_GetSelectedEnterpriseId)
 9 00007ff6`510e8b8c notepad!InvokeLegacySaveDialog (long __cdecl InvokeLegacySaveDialog(unsigned short const *,...)
10 00007ff6`510fef58 notepad!RestartHandler::TryAutosaveOpenedDocument (public: bool __cdecl RestartHandler::TryAutosaveOpenedDocument(void))
11 00007ff6`510ee780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,.....))
12 00007ff6`510ea2e8 notepad!CheckSave (int __cdecl CheckSave(void))
13 00007ff6`511123bc notepad!fInSaveAsDlg = <no type information>
14 00007ff6`510ea124 notepad!CheckSaveTaskDlgBox (int __cdecl CheckSaveTaskDlgBox(unsigned short const *))
15 00007ff6`510ffdd4 notepad!TraceFileSaveComplete (void __cdecl TraceFileSaveComplete(struct _NP_FileInfo *,int))
16 00007ff6`510e8d90 notepad!InvokeSaveDialog (long __cdecl InvokeSaveDialog(struct HWND__ *,...))
17 00007ff6`511105c0 notepad!szSaveCaption = <no type information>
18 00007ff6`5110508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW)
19 00007ff6`51111640 notepad!g_ftSaveAs = <no type information>
20 00007ff6`51107570 notepad!CLSID_FileSaveDialog = <no type information>
21 00007ff6`510ff258 notepad!RestartHandler::TryRestoreAutosavedDocument (public: bool __cdecl...)
22 00007ff6`511150b0 notepad!_imp_GetSaveFileNameW = <no type information>

                    代碼中有些【...】這樣的省略號,表示內容太長,省略了。

                    紅色標註的就是我們找到了 notepad 保存功能的方法名稱和地址。我們直接執行【bp notepad!SaveFile】命令或者【bp 00007ff6`510ea2e8】命令,都可以在 SaveFile 方法上下斷點。
1 0:001> bp notepad!SaveFile

                    下完斷點後,我們【g】繼續執行。但是,此時調試器的光標在閃動,我們打開的 notepad 窗口也可以使用了。效果如圖:
                    

                    我們在 notepad 窗口中隨意寫一些文字,點擊【文件】-->【保存】,就會觸發斷點。效果如圖:
                    

                    此時的 notepad 應用程序的窗口是不能使用的,因爲在斷點出已經中斷執行了。
                    我們繼續使用【g】命令,繼續調試器的運行,notepad 纔可以正常使用,文件也保存成功。

                2)、使用【Windbg Preview】調試。

                   我們先打開一個 notepad.exe 應用程序。然後再打開【Windbg Preview】,依次點擊【文件】--->【Attach to process】,在窗口右側【進程列表】框中選擇 notepad 進程,點擊【附加】,進入調試器。 

                    

                    我們使用【X notepad!*Save*】命令,查找 notepad 的保存數據的方法。

 1 0:002> X notepad!*Save*
 2 00007ff6`510e86a0 notepad!ShowOpenSaveDialog (void)
 3 00007ff6`510ec5cc notepad!InitLegacyOpenSaveEncodingComboBox (void)
 4 00007ff6`510ffd54 notepad!TraceFileSaveStart (void __cdecl TraceFileSaveStart(void))
 5 00007ff6`510f047c notepad!SaveGlobals (void __cdecl SaveGlobals(void))
 6 00007ff6`510f0314 notepad!RegGetIntSaveDefault (unsigned long __cdecl ...)
 7 00007ff6`510ec7a0 notepad!NpLegacySaveDialogHookProc (unsigned __int64 __cdecl ...)
 8 00007ff6`511013a8 notepad!FileSaveDialog_GetSelectedEnterpriseId (FileSaveDialog_GetSelectedEnterpriseId)
 9 00007ff6`510e8b8c notepad!InvokeLegacySaveDialog (long __cdecl ...)
10 00007ff6`510fef58 notepad!RestartHandler::TryAutosaveOpenedDocument (public: bool __cdecl RestartHandler::TryAutosaveOpenedDocument(void))
11 00007ff6`510ee780 notepad!SaveFile (bool __cdecl SaveFile(struct HWND__ *,class ...))
12 00007ff6`510ea2e8 notepad!CheckSave (int __cdecl CheckSave(void))
13 00007ff6`511123bc notepad!fInSaveAsDlg = <no type information>
14 00007ff6`510ea124 notepad!CheckSaveTaskDlgBox (int __cdecl CheckSaveTaskDlgBox(unsigned short const *))
15 00007ff6`510ffdd4 notepad!TraceFileSaveComplete (void __cdecl TraceFileSaveComplete(struct _NP_FileInfo *,int))
16 00007ff6`510e8d90 notepad!InvokeSaveDialog (long __cdecl InvokeSaveDialog(struct HWND__ *...))
17 00007ff6`511105c0 notepad!szSaveCaption = <no type information>
18 00007ff6`5110508c notepad!_imp_load_GetSaveFileNameW (__imp_load_GetSaveFileNameW)
19 00007ff6`51111640 notepad!g_ftSaveAs = <no type information>
20 00007ff6`51107570 notepad!CLSID_FileSaveDialog = <no type information>
21 00007ff6`510ff258 notepad!RestartHandler::TryRestoreAutosavedDocument (public: bool __cdecl...)
22 00007ff6`511150b0 notepad!_imp_GetSaveFileNameW = <no type information>

                    紅色標註的就是我們要找的方法名和地址。此時,notepad的窗口是不可以使用的。使用【bp 00007ff6`510ee780】命令下斷點。

1 0:002> bp 00007ff6`510ee780

                    繼續【g】,運行調試器,我們操作 notepad窗口,隨意輸入文字,然後點擊【文件】--->【保存】,調試器運行,在 SaveFile 方法的斷點出停止執行,notepad 窗口也不能使用了。
                    

                    我們使用【g】命令,繼續運行,notepad 保存成功。


        3.6.2、在 JIT 編譯的託管函數上下斷點
            A、知識介紹
                非託管方法設置斷點很容易,因爲代碼都已經被編譯了,代碼的地址就是已知的。但是,託管代碼要進行兩次編譯才能運行。我們想要給代碼設置斷點,必須先找到代碼的位置。這一節我們討論已經編譯的函數如何設置斷點,既然已經編譯了,說明代碼的地址就是可以直接找到的,設置斷點就很容易了。
                JIT 編譯器編譯了一個函數並將其放在內存中。如果我們知道了 JIT 編譯器保存機器代碼的位置,我們就可以使用調試器命令【bp】設置斷點了。
            B、眼見爲實
                調試任務:在第二次調用 AddAndPrint 方法的時候設置斷點。爲什麼選擇第二次,第一次已經執行過了,說明已經編譯了。第二次就是使用編譯的機器碼。
                1)、使用【NTSD】調試
                    調試源碼:ExampleCore_3_1_4
                    執行【ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\bin\Debug\net8.0\ExampleCore_3_1_4.exe】命令,開啓調試器。
                    

                    打開【ntsd】調試器窗口。
                    

                    我們使用【g】命令,運行調試器,直到調試器顯示【Press any key(1st instance function)】暫停,等待輸入。
                    

                    我們按下任意鍵,程序繼續執行,直到調試器輸出【Press any key(2nd instance function)】。此時,我們按下【ctrl+c】進入調試器的中斷模式。
                    

                    現在,我們可以使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令找到方法的是否編譯的信息。

1 0:002> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint
2 Module:      00007ffccc24e0a0
3 Assembly:    ExampleCore_3_1_4.dll
4 Token:       0000000006000003
5 MethodDesc:  00007ffccc279398
6 Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
7 JITTED Code Address: 00007ffccc1c1a90

                    紅色標註的說明代碼已經編譯了,地址是:00007ffccc1c1a90,如果不信,我們可以使用【u】命令確認一下。

 1 0:002> !U 00007ffccc1c1a90
 2 Normal JIT generated code
 3 ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
 4 ilAddr is 000002B7069720AC pImport is 000001A39F836140
 5 Begin 00007FFCCC1C1A90, size b7
 6 >>> 00007ffc`cc1c1a90 55              push    rbp
 7 00007ffc`cc1c1a91 4883ec40        sub     rsp,40h
 8 00007ffc`cc1c1a95 488d6c2440      lea     rbp,[rsp+40h]
 9 00007ffc`cc1c1a9a c5d857e4        vxorps  xmm4,xmm4,xmm4
10 00007ffc`cc1c1a9e c5f97f65e0      vmovdqa xmmword ptr [rbp-20h],xmm4
11 00007ffc`cc1c1aa3 c5f97f65f0      vmovdqa xmmword ptr [rbp-10h],xmm4
12 00007ffc`cc1c1aa8 48894d10        mov     qword ptr [rbp+10h],rcx
13 00007ffc`cc1c1aac 895518          mov     dword ptr [rbp+18h],edx
14 00007ffc`cc1c1aaf 44894520        mov     dword ptr [rbp+20h],r8d
15 00007ffc`cc1c1ab3 833d6ec8080000  cmp     dword ptr [00007ffc`cc24e328],0
16 00007ffc`cc1c1aba 7405            je      00007ffc`cc1c1ac1
17 00007ffc`cc1c1abc e84fefc75f      call    coreclr!JIT_DbgIsJustMyCode (00007ffd`2be40a10)
18 00007ffc`cc1c1ac1 90              nop
19 00007ffc`cc1c1ac2 8b4d18          mov     ecx,dword ptr [rbp+18h]
20 00007ffc`cc1c1ac5 034d20          add     ecx,dword ptr [rbp+20h]
21 00007ffc`cc1c1ac8 894dfc          mov     dword ptr [rbp-4],ecx
22 00007ffc`cc1c1acb 48b9881113ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32)
23 00007ffc`cc1c1ad5 e8469ab55f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520)
24 00007ffc`cc1c1ada 488945f0        mov     qword ptr [rbp-10h],rax
25 00007ffc`cc1c1ade 488b4df0        mov     rcx,qword ptr [rbp-10h]
26 00007ffc`cc1c1ae2 8b4518          mov     eax,dword ptr [rbp+18h]
27 00007ffc`cc1c1ae5 894108          mov     dword ptr [rcx+8],eax
28 00007ffc`cc1c1ae8 48b9881113ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32)
29 00007ffc`cc1c1af2 e8299ab55f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520)
30 00007ffc`cc1c1af7 488945e8        mov     qword ptr [rbp-18h],rax
31 00007ffc`cc1c1afb 488b4de8        mov     rcx,qword ptr [rbp-18h]
32 00007ffc`cc1c1aff 8b4520          mov     eax,dword ptr [rbp+20h]
33 00007ffc`cc1c1b02 894108          mov     dword ptr [rcx+8],eax
34 00007ffc`cc1c1b05 48b9881113ccfc7f0000 mov rcx,7FFCCC131188h (MT: System.Int32)
35 00007ffc`cc1c1b0f e80c9ab55f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2bd1b520)
36 00007ffc`cc1c1b14 488945e0        mov     qword ptr [rbp-20h],rax
37 00007ffc`cc1c1b18 4c8b4de0        mov     r9,qword ptr [rbp-20h]
38 00007ffc`cc1c1b1c 8b55fc          mov     edx,dword ptr [rbp-4]
39 00007ffc`cc1c1b1f 41895108        mov     dword ptr [r9+8],edx
40 00007ffc`cc1c1b23 4c8b4de0        mov     r9,qword ptr [rbp-20h]
41 00007ffc`cc1c1b27 488b55f0        mov     rdx,qword ptr [rbp-10h]
42 00007ffc`cc1c1b2b 4c8b45e8        mov     r8,qword ptr [rbp-18h]
43 00007ffc`cc1c1b2f 48b9880a8d9bf7020000 mov rcx,2F79B8D0A88h ("Adding {0}+{1}={2}")
44 00007ffc`cc1c1b39 ff15d12c0d00    call    qword ptr [00007ffc`cc294810]
45 00007ffc`cc1c1b3f 90              nop
46 00007ffc`cc1c1b40 90              nop
47 00007ffc`cc1c1b41 4883c440        add     rsp,40h
48 00007ffc`cc1c1b45 5d              pop     rbp
49 00007ffc`cc1c1b46 c3              ret

                    在反彙編代碼的第一部分很清楚的表明方法的名稱,並且是 JIT 生成的。第四行【Begin 00007FFCCC1C1A90, size b7】表示方法的起始地址和生成代碼的大小。
                    設置斷點,執行命令【 bp 00007ffccc1c1a90】。

1 0:002> bp 00007ffccc1c1a90

                    斷點設置成功後,但是我在運行調試的時候出錯,還沒有找到原因和解決辦法,如果有知道原因的,不吝賜教。
                    


                2)、使用【Windbg Preview】調試
                    調試源碼:ExampleCore_3_1_4
                    編譯好我們的項目,打開【Windbg Preview】,依次點擊【文件】--->【Launch executable】,加載我們的項目文件:ExampleCore_3_1_4.exe,進入到調試器。
                    先執行【g】命令,運行調試器。等我們的控制檯輸出:Press any key(1st instance function),我們在按任意鍵繼續。
                    

                    控制檯程序如圖:
                    

                    我們按【回車鍵】,如下:
                    

                    這時,我們回到【Windbg Preview】調試器中,調試窗口是這樣的,如圖:
                    

                    我們點擊【Break】按鈕,讓調試器進入中斷模式。

1 (39a0.944): Break instruction exception - code 80000003 (first chance)
2 ntdll!DbgBreakPoint:
3 00007ffd`f89ee880 cc              int     3

                    我們使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令,查看方法的信息。

0:001> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint
Module:      00007ffcccd0e0a0
Assembly:    ExampleCore_3_1_4.dll
Token:       0000000006000003
MethodDesc:  00007ffcccd39398
Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
JITTED Code Address: 00007ffcccc81c40

                   JITTED 表示已經是編譯過的,編譯的地址是:00007ffcccc81c40。當然,我們可以使用【!U 00007ffcccc81c40】命令查看彙編代碼。

 1 0:001> !U 00007ffcccc81c40
 2 Normal JIT generated code (說明是 JIT 生成的)
 3 ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)(方法的名稱,說明我們獲取的地址是對的)
 4 ilAddr is 00000217571620AC pImport is 00000214BF4B0480
 5 Begin 00007FFCCCC81C40, size b7(代碼的開始地址和生成代碼的大小)
 6 
 7 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 24:
 8 >>> 00007ffc`ccc81c40 55              push    rbp
 9 00007ffc`ccc81c41 4883ec40        sub     rsp,40h
10 00007ffc`ccc81c45 488d6c2440      lea     rbp,[rsp+40h]
11 00007ffc`ccc81c4a c5d857e4        vxorps  xmm4,xmm4,xmm4
12 00007ffc`ccc81c4e c5f97f65e0      vmovdqa xmmword ptr [rbp-20h],xmm4
13 00007ffc`ccc81c53 c5f97f65f0      vmovdqa xmmword ptr [rbp-10h],xmm4
14 00007ffc`ccc81c58 48894d10        mov     qword ptr [rbp+10h],rcx
15 00007ffc`ccc81c5c 895518          mov     dword ptr [rbp+18h],edx
16 00007ffc`ccc81c5f 44894520        mov     dword ptr [rbp+20h],r8d
17 00007ffc`ccc81c63 833dbec6080000  cmp     dword ptr [00007ffc`ccd0e328],0
18 00007ffc`ccc81c6a 7405            je      00007ffc`ccc81c71
19 00007ffc`ccc81c6c e89fedc95f      call    coreclr!JIT_DbgIsJustMyCode (00007ffd`2c920a10)
20 00007ffc`ccc81c71 90              nop
21 
22 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 25:
23 00007ffc`ccc81c72 8b4d18          mov     ecx,dword ptr [rbp+18h]
24 00007ffc`ccc81c75 034d20          add     ecx,dword ptr [rbp+20h]
25 00007ffc`ccc81c78 894dfc          mov     dword ptr [rbp-4],ecx
26 
27 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 26:
28 00007ffc`ccc81c7b 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32)
29 00007ffc`ccc81c85 e89698b75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520)
30 00007ffc`ccc81c8a 488945f0        mov     qword ptr [rbp-10h],rax
31 00007ffc`ccc81c8e 488b4df0        mov     rcx,qword ptr [rbp-10h]
32 00007ffc`ccc81c92 8b4518          mov     eax,dword ptr [rbp+18h]
33 00007ffc`ccc81c95 894108          mov     dword ptr [rcx+8],eax
34 00007ffc`ccc81c98 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32)
35 00007ffc`ccc81ca2 e87998b75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520)
36 00007ffc`ccc81ca7 488945e8        mov     qword ptr [rbp-18h],rax
37 00007ffc`ccc81cab 488b4de8        mov     rcx,qword ptr [rbp-18h]
38 00007ffc`ccc81caf 8b4520          mov     eax,dword ptr [rbp+20h]
39 00007ffc`ccc81cb2 894108          mov     dword ptr [rcx+8],eax
40 00007ffc`ccc81cb5 48b98811bfccfc7f0000 mov rcx,7FFCCCBF1188h (MT: System.Int32)
41 00007ffc`ccc81cbf e85c98b75f      call    coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007ffd`2c7fb520)
42 00007ffc`ccc81cc4 488945e0        mov     qword ptr [rbp-20h],rax
43 00007ffc`ccc81cc8 4c8b4de0        mov     r9,qword ptr [rbp-20h]
44 00007ffc`ccc81ccc 8b55fc          mov     edx,dword ptr [rbp-4]
45 00007ffc`ccc81ccf 41895108        mov     dword ptr [r9+8],edx
46 00007ffc`ccc81cd3 4c8b4de0        mov     r9,qword ptr [rbp-20h]
47 00007ffc`ccc81cd7 488b55f0        mov     rdx,qword ptr [rbp-10h]
48 00007ffc`ccc81cdb 4c8b45e8        mov     r8,qword ptr [rbp-18h]
49 00007ffc`ccc81cdf 48b9880a82ed57020000 mov rcx,257ED820A88h ("Adding {0}+{1}={2}")
50 00007ffc`ccc81ce9 ff15212b0d00    call    qword ptr [00007ffc`ccd54810]
51 00007ffc`ccc81cef 90              nop
52 
53 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\Program.cs @ 27:
54 00007ffc`ccc81cf0 90              nop
55 00007ffc`ccc81cf1 4883c440        add     rsp,40h
56 00007ffc`ccc81cf5 5d              pop     rbp
57 00007ffc`ccc81cf6 c3              ret

                    這裏比【NTSD】好看的多,不多說了。
                    我們使用【bp 00007ffcccc81c40】命令,設置斷點。

1 0:001> bp 00007ffcccc81c40
2 
3 0:001> g
4 Breakpoint 0 hit
5 ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint:
6 00007ffc`ccc81c40 55              push    rbp

                    我們進入 AddAndPrint 方法的斷點出了,我們就可以使用【p】或者【t】命令就行調試了。

 

        3.6.3、在還沒有被 JIT 編譯的託管函數上下斷點
            A、知識介紹
                非託管方法設置斷點很容易,因爲代碼都已經被編譯了,代碼的地址就是已知的。但是,託管代碼要進行兩次編譯才能運行。我們想要給代碼設置斷點,必須先找到代碼的位置。這一節我們討論在未編譯的函數上如何設置斷點,我們就不能使用【bp】命令,需要使用另外一個命令【bpmd】,它能自動找出被 JIT 編譯後代碼正確地址,並且,可以僅根據完整的方法名來設置斷點。
                【bpmd】命令可以用來在還沒有被 JIT編譯的代碼上設置斷點,它設置的是一個延遲斷點,設置斷點時位置是未知的,只有在將來某個事件發生時,纔會真正的設置斷點。【bpmd】命令是通過註冊內部的 CLR JIT 編譯通知來實現延遲斷點的。當調試器收到 JIT 編譯通知時,它會檢查這個通知是否和現有的某一個延遲斷點相關,如果相關,那麼就會在函數執行之前就會使斷點生效。而且,【bpmd】命令還會接受模塊加載通知,這就意味着在設置斷點時甚至可以不需要加載程序集。當程序集加載的時候,這個命令會再次得到通知,並檢查是否有某個延遲斷點位於這個模塊中,如果有,便會激活這個斷點。
            B、眼見爲實
                1)、使用【NTSD】調試
                    調試源碼:ExampleCore_3_1_4
                    調試任務:在 AddAndPrint 方法第一次執行前設置斷點。
                    執行命令【D:\Program Files\Microsoft Visual Studio\2022\Community>ntsd E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_4\bin\Debug\net8.0\ExampleCore_3_1_4.exe】打開調試器窗口。
                    我們直接【g】運行調試器,看到調試器中輸出:Press any key(1st instance function)
                    

                    我們使用【ctrl+c】進入調試器中斷模式。我們使用【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令,查看方法是否已經編譯。

1 0:009> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint
2 Module:      00007ffccdfde0a0
3 Assembly:    ExampleCore_3_1_4.dll
4 Token:       0000000006000003
5 MethodDesc:  00007ffcce009398
6 Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
7 Not JITTED yet. Use !bpmd -md 00007FFCCE009398 to break on run.

                    我們使用【!bpmd -md 00007FFCCE009398】命令設置斷點。

1 0:009> !bpmd -md 00007FFCCE009398
2 MethodDesc = 00007FFCCE009398
3 Adding pending breakpoints...

                    我們斷點設置成功。但是我在運行調試的時候出錯,還沒有找到原因和解決辦法,如果有知道原因的,不吝賜教。

 1 0:008> g
 2 g(3308.f8): CLR notification exception - code e0444143 (first chance)
 3 JITTED ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
 4 Setting breakpoint: bp 00007FFCCDF51A90 [ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)]
 5 Unable to insert breakpoint 0 at 00007ffc`cdf51a90, Win32 error 0n998
 6     "內存位置訪問無效。"
 7 The breakpoint was set with BP.  If you want breakpoints
 8 to track module load/unload state you must use BU.
 9 bp0 at 00007ffc`cdf51a90 failed
10 WaitForEvent failed, Win32 error 0n998
11 內存位置訪問無效。
12 KERNELBASE!RaiseException+0x69:
13 00007ffd`f5fb3e49 0f1f440000      nop     dword ptr [rax+rax]                    

                2)、使用【Windbg Preview】調試
                    調試源碼:ExampleCore_3_1_4
                    調試任務:在 AddAndPrint 方法第一次執行前設置斷點。
                    編譯好我們的項目,打開【Windbg Preview】,依次點擊【文件】--->【Launch executable】,加載我們的項目文件:ExampleCore_3_1_4.exe,進入調試器。
                    我們使用【g】命令,繼續運行,我們的控制檯程序輸出:Press any key(1st instance function),這是第一次輸出,AddAndPrint 方法還沒有執行,也就還沒有編譯。
                    

                    我們點擊【Break】按鈕,中斷執行,我們先證明 AddAndPrint 這個方法還沒有編譯,執行【!name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint】命令。

1 0:001> !name2ee ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint
2 Module:      00007ffccdfce0a0
3 Assembly:    ExampleCore_3_1_4.dll
4 Token:       0000000006000003
5 MethodDesc:  00007ffccdff9398(這個就是方法描述符)
6 Name:        ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
7 Not JITTED yet. Use !bpmd -md 00007FFCCDFF9398 to break on run.

                    Not JITTED yet:表示未編譯。【Use !bpmd -md 00007FFCCDFF9398 to break on run.】這句話是說可以通過使用【bpmd】命令和方法描述符來設置一個斷點。

1 0:001> !bpmd -md 00007ffccdff9398
2 MethodDesc = 00007FFCCDFF9398
3 Adding pending breakpoints...

                    當我們使用【g】命令繼續執行,並在控制檯應用程序中按下【回車鍵】,調試器輸出如下:

1 0:001> g
2 (21e8.31c8): CLR notification exception - code e0444143 (first chance)
3 JITTED ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)
4 Setting breakpoint: bp 00007FFCCDF41C40 [ExampleCore_3_1_4.BreakPoint.AddAndPrint(Int32, Int32)]
5 Breakpoint 0 hit
6 ExampleCore_3_1_4!ExampleCore_3_1_4.BreakPoint.AddAndPrint:
7 00007ffc`cdf41c40 55              push    rbp

                    需要注意的是 notification 部分的輸出,調試器已經接受到了 CLR 通知異常(e0444143),JIT 編譯方法的時候,重新設置了斷點在地址:00007FFCCDF41C40,並且成功在斷點處中斷執行。


        3.6.4、在預編譯的程序集中設置斷點
            .NET 代碼也需要在進程的上下文中執行。JIT 編譯器將程序集的 IL 代碼編譯爲機器代碼,每當 .NET 代碼訪問同一段代碼時,CLR 首先檢查它是否已經被編譯了,如果是,則重用已編譯的代碼。當然,當進程結束了,JIT 編譯器生成的所有機器代碼也會隨之消失。當下一次需要執行程序集時,JIT 編譯器再重新對相同的代碼進行編譯。
            預編譯程序集是與某個程序集對應的非託管映像,其中全部的代碼已經全部被編譯爲機器代碼。如果 CLR 需要執行這個程序集中的代碼,並且這個程序集在機器上有一個非託管的映像,就會直接跳過 JIT 編譯步驟,並直接從這個非託管映像中加載機器代碼。
            需要說明一點,這本書寫的有點早,那個時候只有 .NET Framework 平臺,NGEN 也是針對 .NET Framework 平臺的。我這個系列是針對 .NET 8,也就是跨平臺的版本,所以是不能直接使用 NGEN 生成預編譯的程序集的。如果想生成跨平臺的預編譯程序集,需要使用  CrossGen
            NET 6 引入了 CrossGen2,它是已被刪除的 CrossGen 的後繼版本。 CrossGen 和 CrossGen2 是用於提供預先 (AOT) 編譯的工具,可改進應用的啓動時間。 CrossGen2 是用 C# (而不是 C++)編寫的,可執行之前的版本無法實現的分析和優化。 如果想了解 CrossGen2,可以去微軟官網:https://devblogs.microsoft.com/dotnet/conversation-about-crossgen2/

        3.6.5、在泛型方法上設置斷點
            A、知識介紹
                如果我們想對泛型類型的方法下斷點,最首要的任務就是找到泛型類型的名稱和方法的名稱,找到之後,我們就可以下斷點了。找泛型類型的名稱和方法的名稱有兩種辦法,第一種是通過命令,第二種是我們可以使用 ILSpy 找到。

            B、眼見爲實
                我們想要在泛型類型的方法上下斷點,首要的任務是找到泛型類型的名稱和方法的名稱,這是關鍵。
                1)、使用【NTSD】調試
                    a、我們通過 Windbg 和 SOS 的命令找到類型的名稱。
                        編譯好我們的項目,打開【Visual Studio 2022 Developer Command Prompt】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.exe】,打開調試器。
                        使用【g】命令,運行調試器。
 1 0:000> g
 2 ModLoad: 00007ffe`90a90000 00007ffe`90ac0000   C:\Windows\System32\IMM32.DLL
 3 ModLoad: 00007ffe`4f1d0000 00007ffe`4f229000   C:\Program Files\dotnet\host\fxr\8.0.2\hostfxr.dll
 4 ModLoad: 00007ffe`3b840000 00007ffe`3b8a4000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\hostpolicy.dll
 5 ModLoad: 00007ffe`28ab0000 00007ffe`28f98000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\coreclr.dll
 6 ModLoad: 00007ffe`914d0000 00007ffe`915f9000   C:\Windows\System32\ole32.dll
 7 ModLoad: 00007ffe`91f30000 00007ffe`92284000   C:\Windows\System32\combase.dll
 8 ModLoad: 00007ffe`918a0000 00007ffe`91975000   C:\Windows\System32\OLEAUT32.dll
 9 ModLoad: 00007ffe`902c0000 00007ffe`9033f000   C:\Windows\System32\bcryptPrimitives.dll
10 (3b74.3fac): Unknown exception - code 04242420 (first chance)
11 ModLoad: 00007ffe`27be0000 00007ffe`2886c000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll
12 ModLoad: 00007ffe`27a20000 00007ffe`27bd9000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll
13 ModLoad: 00007ffe`8f900000 00007ffe`8f913000   C:\Windows\System32\kernel.appcore.dll
14 ModLoad: 000001ba`47fc0000 000001ba`47fc8000   E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
15 ModLoad: 000001ba`47fd0000 000001ba`47fde000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll
16 ModLoad: 00007ffe`75c60000 00007ffe`75c88000   C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll
17 (3b74.3fac): Break instruction exception - code 80000003 (first chance)

                        按【ctrl+c】組合鍵進入中斷模式。
                        輸入【!dumpdomain】命令查看應用程序域詳情,該命令會列出每個應用程序域中加載的所有程序集和模塊。

 1 0:000> !dumpdomain
 2 --------------------------------------
 3 System Domain:      00007ffe28f460d0
 4 LowFrequencyHeap:   00007FFE28F465A8
 5 HighFrequencyHeap:  00007FFE28F46638
 6 StubHeap:           00007FFE28F466C8
 7 Stage:              OPEN
 8 Name:               None
 9 --------------------------------------
10 Domain 1:           000001ba465a1ff0
11 LowFrequencyHeap:   00007FFE28F465A8
12 HighFrequencyHeap:  00007FFE28F46638
13 StubHeap:           00007FFE28F466C8
14 Stage:              OPEN
15 Name:               clrhost
16 Assembly:           000001ba46564d20 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll]
17 ClassLoader:        000001BA46564DB0
18   Module
19   00007ffdc8f44000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll
20 
21 Assembly:           000001ba465506e0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll]
22 ClassLoader:        000001BA46550FC0
23   Module
24   00007ffdc912e0a0    E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
25 
26 Assembly:           000001ba465507e0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll]
27 ClassLoader:        000001BA46550870
28   Module
29   00007ffdc912fbc8    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll
30 
31 Assembly:           000001ba47f97460 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll]
32 ClassLoader:        000001BA47F97DE0
33   Module
34   00007ffdc91597f0    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Console.dll

                        我們找到了模塊,就可以將模塊中所有的類型輸出來,可以使用【!dumpmodule -mt 】命令。

 1 0:000> !dumpmodule -mt 00007ffdc912e0a0
 2 Name: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
 3 Attributes:              PEFile
 4 TransientFlags:          00209011
 5 Assembly:                000001ba465506e0
 6 BaseAddress:             000001BA47FC0000
 7 PEFile:                  000001BA4654FCD0
 8 ModuleId:                00007FFDC912E458
 9 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FFE28F46598
11 TypeDefToMethodTableMap: 00007FFDC9134320
12 TypeRefToMethodTableMap: 00007FFDC9134340
13 MethodDefToDescMap:      00007FFDC9134470
14 FieldDefToDescMap:       00007FFDC9134498
15 MemberRefToDescMap:      00007FFDC91343D0
16 FileReferencesMap:       0000000000000000
17 AssemblyReferencesMap:   00007FFDC91344B8
18 MetaData start address:  000001BA47FC20A8 (1612 bytes)
19 
20 Types defined in this module
21 
22               MT          TypeDef Name
23 ------------------------------------------------------------------------------
24 00007ffdc91500e8 0x02000002 ExampleCore_3_1_5.Program
25 00007ffdc91593e0 0x02000003 ExampleCore_3_1_5.MyList`1
26 
27 Types referenced in this module
28 
29               MT            TypeRef Name
30 ------------------------------------------------------------------------------
31 00007ffdc8fd5fa8 0x0200000d System.Object
32 00007ffdc9159700 0x02000010 System.Diagnostics.Debugger
33 00007ffdc915ab08 0x02000011 System.Console

                        紅色標註的就是我們要查找泛型類型真實的名稱。有了類型,我們繼續可以使用【!dumpmt -md 00007ffdc91593e0】命令,輸出它所有方法。

 1 0:000> !dumpmt -md 00007ffdc91593e0
 2 EEClass:         00007FFDC9161F48
 3 Module:          00007FFDC912E0A0
 4 Name:            ExampleCore_3_1_5.MyList`1
 5 mdToken:         0000000002000003
 6 File:            E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
 7 BaseSize:        0x18
 8 ComponentSize:   0x0
 9 DynamicStatics:  false
10 ContainsPointers true
11 Slots in VTable: 6
12 Number of IFaces in IFaceMap: 0
13 --------------------------------------
14 MethodDesc Table
15            Entry       MethodDesc    JIT Name
16 00007FFDC8FE0048 00007FFDC8FD5F38   NONE System.Object.Finalize()
17 00007FFDC8FE0060 00007FFDC8FD5F48   NONE System.Object.ToString()
18 00007FFDC8FE0078 00007FFDC8FD5F58   NONE System.Object.Equals(System.Object)
19 00007FFDC8FE00C0 00007FFDC8FD5F98   NONE System.Object.GetHashCode()
20 00007FFDC914B948 00007FFDC91593B8   NONE ExampleCore_3_1_5.MyList`1..ctor()
21 00007FFDC914B930 00007FFDC91593A8   NONE ExampleCore_3_1_5.MyList`1.Add(!0)

                        紅色標記就是我們要查找的 Add 方法,有了方法的地址,我們就可以使用【!bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add】命令爲其下斷點了。

1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add
2 MethodDesc = 00007FFDC91593A8
3 Adding pending breakpoints...

                        斷點設置成功。但是我在運行調試的時候出錯,還沒有找到原因和解決辦法,如果有知道原因的,不吝賜教。

 1 0:000> g
 2 (3b74.3fac): CLR notification exception - code e0444143 (first chance)
 3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)
 4 Setting breakpoint: bp 00007FFDC90A1A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)]
 5 Unable to insert breakpoint 0 at 00007ffd`c90a1a50, Win32 error 0n998
 6     "內存位置訪問無效。"
 7 The breakpoint was set with BP.  If you want breakpoints
 8 to track module load/unload state you must use BU.
 9 bp0 at 00007ffd`c90a1a50 failed
10 WaitForEvent failed, Win32 error 0n998
11 內存位置訪問無效。
12 KERNELBASE!RaiseException+0x69:
13 00007ffe`8fb03e49 0f1f440000      nop     dword ptr [rax+rax]

                    b、我們可以使用 ILSpy 或者 SnPay 來查找泛型類型的名稱和方法的名稱。
                        和使用【Windbg Preview】這節的內容一樣。

                2)、使用【Windbg Preview】調試
                    調試源碼:ExampleCore_3_1_5
                    a、我們通過 Windbg 和 SOS 的命令找到類型的名稱。
                        編譯程序集後,泛型類型一定在這個程序集的模塊中。然後我們再在這個模塊中打印出所有的類型,就可以找到這個類型了。    
                        編譯好我們的項目,打開【Windbg Preview】,依次點擊【文件】--->【Launch executable】,加載我們的項目文件:ExampleCore_3_1_5.exe。進入到調試器後,我們使用【g】命令運行調試器,調試器會在 Program 類型的 Main 方法的【Debugger.Break()】這個行代碼中斷執行。我們點擊【break】按鈕,進入調試模式。
                        我們現在這個程序集中查找模塊信息,我們可以使用【!dumpdomain】命令。
 1 0:000> !dumpdomain
 2 --------------------------------------
 3 System Domain:      00007ffe226360d0
 4 LowFrequencyHeap:   00007FFE226365A8
 5 HighFrequencyHeap:  00007FFE22636638
 6 StubHeap:           00007FFE226366C8
 7 Stage:              OPEN
 8 Name:               None
 9 --------------------------------------
10 Domain 1:           000001a1469ddc40
11 LowFrequencyHeap:   00007FFE226365A8
12 HighFrequencyHeap:  00007FFE22636638
13 StubHeap:           00007FFE226366C8
14 Stage:              OPEN
15 Name:               clrhost
16 Assembly:           000001a146a2e010 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll]
17 ClassLoader:        000001A146A2E0A0
18   Module
19   00007ffdc2634000    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Private.CoreLib.dll
20 
21 Assembly:           000001a1484c2bf0 [E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll]
22 ClassLoader:        000001A1484C2C80
23   Module
24   00007ffdc281e0a0    E:\Visual Studio 2022\Source\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
25 
26 Assembly:           000001a146976520 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll]
27 ClassLoader:        000001A1469765B0
28   Module
29   00007ffdc281fbc8    C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\System.Runtime.dll

                        00007ffdc281e0a0 這個地址就是我們程序集(ExampleCore_3_1_5.dll)的模塊地址。我們找到了模塊,就可以將模塊中所有的類型輸出來,可以使用【!dumpmodule -mt 】命令。

 1 0:000> !dumpmodule -mt 00007ffdc281e0a0
 2 Name: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
 3 Attributes:              PEFile 
 4 TransientFlags:          00209011 
 5 Assembly:                000001a1484c2bf0
 6 BaseAddress:             000001A1468F0000
 7 PEAssembly:              000001A14696F6A0
 8 ModuleId:                00007FFDC281E458
 9 ModuleIndex:             0000000000000001
10 LoaderHeap:              00007FFE22636598
11 TypeDefToMethodTableMap: 00007FFDC2824320
12 TypeRefToMethodTableMap: 00007FFDC2824340
13 MethodDefToDescMap:      00007FFDC2824470
14 FieldDefToDescMap:       00007FFDC2824498
15 MemberRefToDescMap:      00007FFDC28243D0
16 FileReferencesMap:       0000000000000000
17 AssemblyReferencesMap:   00007FFDC28244B8
18 MetaData start address:  000001A1468F20A8 (1612 bytes)
19 
20 Types defined in this module
21 
22               MT          TypeDef Name
23 ------------------------------------------------------------------------------
24 00007ffdc28400e8 0x02000002 ExampleCore_3_1_5.Program
25 00007ffdc28493e0 0x02000003 ExampleCore_3_1_5.MyList`1
26 
27 Types referenced in this module
28 
29               MT            TypeRef Name
30 ------------------------------------------------------------------------------
31 00007ffdc26c5fa8 0x0200000d System.Object
32 00007ffdc2849700 0x02000010 System.Diagnostics.Debugger
33 00007ffdc284ab08 0x02000011 System.Console

                        ExampleCore_3_1_5.MyList`1 就是泛型類型編譯後的名稱。紅色標註的就是我們要查找泛型類型真實的名稱。有了類型,我們繼續可以使用【!dumpmt -md 00007ffdc28493e0】命令,輸出它所有方法。

 1 0:000> !dumpmt -md 00007ffdc28493e0
 2 EEClass:             00007ffdc2851f48
 3 Module:              00007ffdc281e0a0
 4 Name:                ExampleCore_3_1_5.MyList`1
 5 mdToken:             0000000002000003
 6 File:                E:\Visual Studio 2022\...\ExampleCore_3_1_5\bin\Debug\net8.0\ExampleCore_3_1_5.dll
 7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
 8 BaseSize:            0x18
 9 ComponentSize:       0x0
10 DynamicStatics:      false
11 ContainsPointers:    true
12 Slots in VTable:     6
13 Number of IFaces in IFaceMap: 0
14 --------------------------------------
15 MethodDesc Table
16            Entry       MethodDesc    JIT Name
17 00007FFDC26D0048 00007ffdc26c5f38   NONE System.Object.Finalize()
18 00007FFDC26D0060 00007ffdc26c5f48   NONE System.Object.ToString()
19 00007FFDC26D0078 00007ffdc26c5f58   NONE System.Object.Equals(System.Object)
20 00007FFDC26D00C0 00007ffdc26c5f98   NONE System.Object.GetHashCode()
21 00007FFDC283B948 00007ffdc28493b8   NONE ExampleCore_3_1_5.MyList`1..ctor()
22 00007FFDC283B930 00007ffdc28493a8   NONE ExampleCore_3_1_5.MyList`1.Add(!0)

 

                        紅色標記就是我們要查找的 Add 方法,有了方法的地址,我們就可以使用【bpmd】命令爲其下斷點了。

1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add
2 MethodDesc = 00007FFDC28493A8
3 Adding pending breakpoints...

                        斷點設置成功後,我們使用【g】命令,程序繼續運行,就可以在斷點處暫停。

1 0:000> g
2 (2c88.2734): CLR notification exception - code e0444143 (first chance)
3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)
4 Setting breakpoint: bp 00007FFDC2791A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)]
5 Breakpoint 0 hit
6 ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add:
7 00007ffd`c2791a50 55              push    rbp

                        斷點效果如圖:
                        

                    b、我們可以使用 ILSpy 或者 SnPay 來查找泛型類型的名稱和方法的名稱。
                      我們可以使用 ILSpy 或者 snSpy 查看泛型類型和方法的名稱。我們打開【ILSpy】工具,加載我們的 ExampleCore_3_1_5.dll 文件。再左側,依次點擊【Metadata】--->【Tables】--->【TypeDef】,在右側就能看到這個程序集中定義的所有的類型名稱。如圖:
                      

                      我們知道了類型的名稱,然後就是查找方法的名稱。也很簡單。
                      

                      現在我們知道了泛型類型的名稱和方法的名稱,就可以直接設置斷點了。
                      編譯好我們的項目,打開【Windbg Preview】,依次點擊【文件】--->【Launch executable】,加載我們的項目文件:ExampleCore_3_1_5.exe。進入到調試器後,我們使用【g】命令運行調試器,調試器會在 Program 類型的 Main 方法的【Debugger.Break()】這個行代碼中斷執行。我們點擊【break】按鈕,進入調試模式。
                      我們執行命令【!bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add】,就可以直接下斷點了。

1 0:000> !bpmd ExampleCore_3_1_5 ExampleCore_3_1_5.MyList`1.Add
2 MethodDesc = 00007FFDC2DE93A8
3 Adding pending breakpoints...

                      斷點設置成功後,我們使用【g】命令,程序繼續運行,就可以在斷點處暫停。

1 0:000> g
2 (10b4.42c4): CLR notification exception - code e0444143 (first chance)
3 JITTED ExampleCore_3_1_5!ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)
4 Setting breakpoint: bp 00007FFDC2D31A50 [ExampleCore_3_1_5.MyList`1[[System.Int32, System.Private.CoreLib]].Add(Int32)]
5 Breakpoint 0 hit
6 ExampleCore_3_1_5!ExampleCore_3_1_5.MyList<int>.Add:
7 00007ffd`c2d31a50 55              push    rbp

                      斷點設置成功,我們也完成我們的任務。

 

四、總結
    這篇文章終於寫完了,是這篇文章的“上”篇寫完了,“下”篇還沒有開始呢,這篇文章寫作週期也不短,內容實在多。Net 高級調試這條路,也剛剛起步,還有很多要學的地方。皇天不負有心人,努力,不辜負自己,我相信付出就有回報,再者說,學習的過程,有時候,雖然很痛苦,但是,學有所成,學有所懂,這個開心的感覺還是不可言喻的。不忘初心,繼續努力。做自己喜歡做的,開心就好。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章