Net 高階偵錯之三:型別後設資料介紹(同步塊表、型別控制程式碼、方法描述符等)

2023-10-30 12:00:50
一、簡介
    今天是《Net 高階偵錯》的第三篇文章,壓力還是不小的。上一篇文章,我們淺淺的談了談 CLR 和 Windows 載入器是如何載入 Net 程式集的,如何找到程式的入口點的,有了前面的基礎,我們今天看一點更詳細的東西。既然 Windows 作業系統已經載入了 CLR,初始化了應用程式域,載入了我們的 Net 程式,那我們就看看Net 型別在記憶體中的具體樣子。這一篇文章還是有一點難度的,我看第一遍視訊的時候,也不知道說了個啥,後來又看了《Net 高階偵錯》,似懂非懂。一遍不行,那就再來一遍,還不行,那就再來一遍,俗話說的好,書讀千遍,其意自現。

    如果在沒有說明的情況下,所有程式碼的測試環境都是 Net Framewok 4.8,但是,有時候為了檢視原始碼,可能需要使用 Net Core 的專案,我會在專案章節裡進行說明。好了,廢話不多說,開始我們今天的偵錯工作。
    偵錯環境我需要進行說明,以防大家不清楚,具體情況我已經羅列出來。
          作業系統:Windows Professional 10
          偵錯工具:Windbg Preview(可以去Microsoft Store 去下載)
          開發工具:Visual Studio 2022
          Net 版本:Net Framework 4.8
          CoreCLR原始碼:原始碼下載

二、相關知識
    我們知道了 CLR,瞭解了 JIT,曉得了 Net 的編譯過程,也真正做到了眼見為實,所有的知識點都有根了,這次好好的研究一下型別的東西,當然,這寫東西平時時很難遇到的,就是不懂,也可以寫出東西。但是,如果要想做到,知其一也要知其二的話,這些只是還是有必要了解的,對我們寫出高效的程式碼還是很有幫助的,一以下就是相關的知識點,我一一羅列出來。

    棧stack(先進後出)是編譯期間就分配好的記憶體空間,因此你的程式碼中必須就棧的大小有明確的定義;

    堆heap(佇列優先,先進先出)是程式執行期間動態分配的記憶體空間,你可以根據程式的執行情況確定要分配的堆記憶體的大小

    1、簡介
        型別是 Net 程式中基本程式設計單元,型別又可以細分為:值型別,參照型別。
        a)、值型別
          列舉【enum】,結構【Struct】和其他簡單型別,比如:int,float,double,char,bool等。這些型別佔據的空間小,一般存放線上程棧上,當然也可以儲存在暫存器中、託管堆中或者是私有堆中。
        b)、參照型別
          介面、陣列、類和我們自定義的 Class,都是參照型別,這樣的型別,一般佔據的空間比較大,它們存在託管堆中,由 GC 負責分配記憶體和回收記憶體來管理這些參照型別的範例。

    2、值型別佈局
        一般而言,方法的引數、在方法內部宣告的區域性變數都是存放在當前的執行緒棧上,也就是說線上程棧上直接儲存值型別的值。

        

    3、參照型別佈局
        class 型別是一種參照型別,範例物件在託管堆中分配空間,並將物件的首地址存在棧地址上。
        

 


    4、同步塊表
        這個名稱叫的不太準確,叫 ObjectHeader 更好點,因為原始碼中就是叫這個名稱。託管堆上的每個物件的前面都有一個同步塊索引,它指向 CLR 中私有堆上的同步塊表,同步塊表中可以包含很多資訊,比如:物件雜湊碼、鎖資訊、應用程式域索引。
        
        
    5、型別控制程式碼(方法表)
        型別控制程式碼是針對型別的描述資訊,比如:這個類中有多少個方法,方法的結構,方法的欄位資訊等。
        
    6、方法描述符
        用來描述C# 方法在 CLR 層面的特徵,使用 MethodDesc 類結構來承載,記錄了方法的位元組碼,所屬類,Token 等資訊。

    7、模組
        模組是包含在程式集中,程式集是一個 Net 程式的部署單元,可以用 !dumpAssembly 和 !dumpmodule 顯示各自的資訊。

    8、後設資料標記
        因為程式集是自描述的,型別資訊都有響應的 Metadata 來表示,可以使用 ILSpy 來檢視。可以使用 !token2ee 來檢索對應的方法。
        
    9、EEClass
        EEClass 和 MethodTable 是同級別的,用來描述 C# 的一個類,可以使用 !dumpclass 來顯示型別的 EECLass 資訊。

三、偵錯過程
    廢話不多說,這一節是具體的偵錯操作的過程,有可以說是眼見為實的過程,在開始之前,我還是要囉嗦兩句,這一節分為兩個部分,第一部分是測試的原始碼部分,沒有程式碼,當然就談不上測試了,偵錯必須有載體。第二部分就是根據具體的程式碼來證實我們學到的知識,是具體的眼見為實。
    1、測試原始碼
        1.1、Example_3_1_1
 1 namespace Example_3_1_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             int a = 10;
 8             long b = 11;
 9             short c = 12;
10             Console.ReadLine();
11         }
12     }
13 }
View Code

        1.2、Example_3_1_2
 1 namespace Example_3_1_2
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var person = new Person()
 8             {
 9                 Name = "jack",
10                 Age = 20
11             };
12             Console.ReadLine();
13         }
14     }
15 
16     public class Person
17     {
18         public string Name { get; set; }
19 
20         public int Age { get; set; }
21     }
22 }
View Code

        1.3、Example_3_1_3            
 1 namespace Example_3_1_3
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var person = new Person() { Name = "jack", Age = 20 };
 8             var hashcode = person.GetHashCode().ToString("x");
 9             Console.WriteLine($"hashcode={hashcode}");
10             Debugger.Break();
11             Console.ReadLine();
12         }
13     }
14 
15     public class Person
16     {
17         public string Name { get; set; }
18 
19         public int Age { get; set; }
20     }
21 }
View Code

        1.4、Example_3_1_4            
 1 namespace Example_3_1_4
 2 {
 3     internal class Program
 4     {
 5         public static Person person=new Person();
 6 
 7         static void Main(string[] args)
 8         {
 9             Task.Run(() =>
10             {
11                 lock (person)
12                 {
13                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId}進入鎖了");
14                     Console.ReadLine();
15                 }
16             });
17             Task.Run(() => {
18                 lock (person)
19                 {
20                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId}進入鎖了");
21                     Console.ReadLine();
22                 }
23             });
24 
25             Console.ReadLine();
26         }
27     }
28 
29     public class Person
30     {
31         public string Name { get; set; }
32 
33         public int Age { get; set; }
34     }
35 }
View Code


        1.5、Example_3_1_5     

 1 namespace Example_3_1_5
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var person = new Person()
 8             {
 9                 Name = "jack",
10                 Age = 20
11             };
12             Console.WriteLine("Hello World!");
13             Console.ReadLine();
14         }
15     }
16     public class Person
17     {
18         public string Name { get; set; }
19 
20         public int Age { get; set; }
21     }
22 }
View Code

 

        1.6、Example_3_1_5_1(這個專案是 Net 7.0版本的)
 1 namespace Example_3_1_5_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var person = new Person()
 8             {
 9                 Name = "jack",
10                 Age = 20
11             };
12             Console.WriteLine("Hello World!");
13             Console.ReadLine();
14         }
15     }
16     public class Person
17     {
18         public string Name { get; set; }
19 
20         public int Age { get; set; }
21     }
22 }
View Code

  
    2、眼見為實
        2.1、值型別的佈局
            程式碼樣例:Example_3_1_1
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_1.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。我們還需要通過【~0s】命令,切換到主執行緒,當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
             !clrstack -l 這個命令是顯示當前的執行緒呼叫棧區域性變數,l 表示 local,區域性變數,程式碼關鍵部分
 1 0:000> !clrstack -l
 2 OS Thread Id: 0x317c (0)
 3 Child SP       IP Call Site
 4 00aff1c4 778e10fc [InlinedCallFrame: 00aff1c4] 
 5 00aff1c0 6fee9b71 ...(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
 6 
 7 ......
 8 
 9 00aff2c0 00d3089e Example_3_1_1.Program.Main(System.String[]) [E:\...\Example_3_1_1\Program.cs @ 12]
10     LOCALS:【表示區域性變數】
11         0x00aff2d0 = 0x0000000a【0x00aff2d0是棧地址,0x0000000a 是棧上的值,這是十六進位制的】
12         0x00aff2c8 = 0x0000000b0x00aff2c8是棧地址,0x0000000b 是棧上的值,這是十六進位制的】
13         0x00aff2c4 = 0x0000000c0x00aff2d0是棧地址,0x0000000c 是棧上的值,這是十六進位制的】
14 
15 00aff448 70f1f036 [GCFrame: 00aff448] 

            以上顯示的紅色部分是最重要的,LOCALS 表示區域性變數,11,12,13 三行是具體的區域性變數,等號前面是 執行緒棧上的變數地址,後面是具體的值,我們可以使用【?】命令檢視具體的值。

1 0:000> ? 0x0000000a
2 Evaluate expression: 10 = 0000000a
3 0:000> ? 0x0000000b
4 Evaluate expression: 11 = 0000000b
5 0:000> ? 0x0000000c
6 Evaluate expression: 12 = 0000000c
View Code
            對應 C# 程式碼中的賦值操作。
            
            由於棧的特點,先進後出,後進先出,所以說【a】是最先入棧的,在棧底,依次是【b】,最上面的是【c】,所以我們從【c】的地址列印,可以顯示【c、b、a】的值。由此,我們執行【dp】命令,效果如下。
1 0:000> dp 0x00aff2c4 l4
2 00aff2c4  0000000c 0000000b 00000000 0000000a
            我們可以繼續驗證,由於棧的地址是由高到低的分配,所以,【c】的地址加上 0x4,為什麼加4呢,雖然【c】佔用2個位元組,但是會按4個位元組算的,就是【b】變數的值,如下:
1 0:000> dp 00aff2c4+0x4 l1
2 00aff2c8  0000000b

            繼續驗證,【b】的地址加上 0x8,就是【a】變數的值,為什麼是加8呢,因為【b】佔用8個位元組,如下:

1 0:000> dp 00aff2c8+0x8 l1
2 00aff2d0  0000000a

            當然,我們可以以【c】變數的地址為基準,算出【b】和【a】的值,如下:

1 0:000> dp 0x00aff2c4+0x4 l1(以c 的地址為基準,找到b的地址,加4)
2 00aff2c8  0000000b
3 0:000> dp 0x00aff2c4+0xc l1(以c 的地址為基準,找到a的地址,加12,十六進位制就是0xc)
4 00aff2d0  0000000a

        2.2、參照型別的佈局
            程式碼樣例:Example_3_1_2
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_2.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。我們還需要通過【~0s】命令,切換到主執行緒,當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
            我們先使用【!clrstack -a】命令,檢視執行緒棧的區域性變數。
 1 0:000> !clrstack -a
 2 OS Thread Id: 0x3930 (0)
 3 Child SP       IP Call Site
 4 0133ee8c 778e10fc [InlinedCallFrame: 0133ee8c] 
 5 0133ee88 6fee9b71
 6 ......
 7 0133ef88 018c08b1 Example_3_1_2.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\......\Example_3_1_2\Program.cs @ 14]
 8     PARAMETERS:
 9         args (0x0133ef94) = 0x033b24bc
10     LOCALS:
11         0x0133ef90 = 0x033b24e0(0x0133ef90 是棧地址,0x033b24e0 person變數的參照地址)
12 
13 0133f108 70f1f036 [GCFrame: 0133f108] 

            我們可以通過【dp】命令檢視棧地址,值是 033b24e0,這個值就是 person變數參照的地址。

1 0:000> dp 0x0133ef90 l1
2 0133ef90  033b24e0(這個地址就是 person變數的地址)

            我們可以使用【!do|!DumpObj】命令,檢視物件的詳情。

 1 0:000> !DumpObj /d 033b24e0
 2 Name:        Example_3_1_2.Person
 3 MethodTable: 01874e1c
 4 EEClass:     01871314
 5 Size:        16(0x10) bytes
 6 File:        E:\Visual Studio 2022\Source\Projects\......\Example_3_1_2\bin\Debug\Example_3_1_2.exe
 7 Fields:
 8       MT    Field   Offset                 Type VT     Attr    Value Name
 9 6fa424e4  4000001        4        System.String  0 instance 033b24c8 <Name>k__BackingField
10 6fa442a8  4000002        8         System.Int32  1 instance       20 <Age>k__BackingField

            033b24c8 <Name>k__BackingField,這個是 string 型別的欄位,033b24c8又是一個參照地址,我們繼續【!do】,檢視詳情。

 1 0:000> !DumpObj /d 033b24c8
 2 Name:        System.String
 3 MethodTable: 6fa424e4
 4 EEClass:     6fb47690
 5 Size:        22(0x16) bytes
 6 File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
 7 String:      jack(這個就是我們賦值的)
 8 Fields:
 9       MT    Field   Offset                 Type VT     Attr    Value Name
10 6fa442a8  4000283        4         System.Int32  1 instance        4 m_stringLength
11 6fa42c9c  4000284        8          System.Char  1 instance       6a m_firstChar
12 6fa424e4  4000288       70        System.String  0   shared   static Empty
13     >> Domain:Value  0151ca70:NotInit  <<

            每一個參照型別物件都包含兩個附加欄位,一個是同步塊索引,另外一個就是型別控制程式碼。我們通過 !clrstack -l 獲取的 Program.Main 方法的控制程式碼變數,我們可以通過【dp】命令檢視一下細節,執行如下命令:dp 0x033b24e0-0x4 l4LOCALS:0x0133ef90 = 0x033b24e0)  

1 0:000> dp 0x033b24e0-0x4 l4
2 033b24dc  00000000 01874e1c 033b24c8 00000014

            033b24dc 00000000 01874e1c 033b24c8 00000014,033b24dc 這個地址就是同步塊的地址,0x033b24e0 person參照地址只想型別控制程式碼01874e1c,型別控制程式碼再用4個位元組,所以 0x033b24e0-0x4,向前移動4個位元組,就是同步塊的指標地址。033b24c8這個部分就是person變數的範例欄位了。

 1 0:000> !do 033b24c8
 2 Name:        System.String
 3 MethodTable: 6fa424e4
 4 EEClass:     6fb47690
 5 Size:        22(0x16) bytes
 6 File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
 7 String:      jack
 8 Fields:
 9       MT    Field   Offset                 Type VT     Attr    Value Name
10 6fa442a8  4000283        4         System.Int32  1 instance        4 m_stringLength
11 6fa42c9c  4000284        8          System.Char  1 instance       6a m_firstChar
12 6fa424e4  4000288       70        System.String  0   shared   static Empty
13     >> Domain:Value  0151ca70:NotInit  <<

            00000014是十六進位制的,表示的就是20。

1 0:000> ? 00000014
2 Evaluate expression: 20 = 00000014

            如果我們想檢視型別控制程式碼的詳情,我們可以使用【!dumpmt】命令。

 1 0:000> !dumpmt 01874e1c
 2 EEClass:         01871314
 3 Module:          01874044
 4 Name:            Example_3_1_2.Person
 5 mdToken:         02000003
 6 File:            E:\Visual Studio 2022\Source\Projects\......\Example_3_1_2\bin\Debug\Example_3_1_2.exe
 7 BaseSize:        0x10
 8 ComponentSize:   0x0
 9 Slots in VTable: 9
10 Number of IFaces in IFaceMap: 0

        2.3、同步塊包含物件雜湊碼
            程式碼樣例:Example_3_1_3
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_3.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Debugger.Break()】次會暫停執行,我們程式的輸出結果是:hashcode=2bf8098。
            接下來,我們看看物件頭中是否雜湊碼,就可以檢驗了。我們先使用【!clrstack -l】命令,看看執行緒棧。
 1 0:000> !clrstack -l
 2 OS Thread Id: 0x2600 (0)
 3 Child SP       IP Call Site
 4 00dcef18 7696f262 [HelperMethodFrame: 00dcef18] System.Diagnostics.Debugger.BreakInternal()
 5 00dcef94 705bf195 System.Diagnostics.Debugger.Break() [f:\dd\ndp\clr\src\BCL\system\diagnostics\debugger.cs @ 91]
 6 
 7 00dcefbc 02f40905 Example_3_1_3.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\......\Example_3_1_3\Program.cs @ 13]
 8     LOCALS:
 9         0x00dcefd0 = 0x030b2510
10         0x00dcefcc = 0x030b39ac
11         0x00dcefd8 = 0x02bf8098
12 
13 00dcf154 70f1f036 [GCFrame: 00dcf154] 

            0x00dcefd0 = 0x030b2510,這個地址就是我們宣告的 person 變數。既然由了物件的地址,只要用物件的地址,減去 0x4,就是同步塊的地址,然後使用【dp】命令就可以檢視了。   

1 0:000> dp 0x030b2510-0x4 l4
2 030b250c  0ebf8098 01414e1c 030b24c8 00000014

            第二行的第二列以前是0,表示沒有任何資料,現在有值了。現在我們用這個值,減去我們得到的雜湊碼,看看是什麼。

1 0:000> ? 0ebf8098-2bf8098
2 Evaluate expression: 201326592 = 0c000000

            0c000000它就是一個掩碼,告訴CLR 這個欄位中包含的是雜湊碼的值,起到標識的作用,因為還可以存放其他東西。


        2.4、同步塊包含物件鎖資訊
            程式碼樣例:Example_3_1_4
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_4.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態,此時,我們程式的輸出是:tid=3進入鎖了,說明 Person 被鎖住了。
            接下來,我們就要檢視物件的物件頭包含什麼東西,意圖很明顯。
            我們首先找到 Person 物件,可以使用【!dumpheap -type Person】命令獲取物件。
1 0:001> !dumpheap -type Person
2  Address       MT     Size
3 033824c8 014d4e60       16     
4 
5 Statistics:
6       MT    Count    TotalSize Class Name
7 014d4e60        1           16 Example_3_1_4.Person
8 Total 1 objects

            紅色標記的就是Person 物件的地址,然後我們使用這個地址減去 0x4,就可以獲取同步塊索引了。    

1 0:001> dp 033824c8-0x4 l4
2 033824c4  08000007 014d4e60 00000000 00000000

            08000007 就是同步塊索引的值,08是一個掩碼,表示是同步塊索引,07就是執行緒 Id。我們可以使用【!syncblk】命令來驗證。

 1 0:001> !syncblk
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     6 015670f0            3         1 01512ba8 3d4c   0   03388210 System.IO.TextReader+SyncTextReader
 4     7 01567124            3         1 0157c340 f8     9   033824c8 Example_3_1_4.Person(被鎖的物件是 person)

              3:(一個執行緒持有鎖,一個等待鎖)

 5 -----------------------------
 6 Total           7
 7 CCW             1
 8 RCW             2
 9 ComClassFactory 0
10 Free            0

            這裡是9,為什麼我們的程式輸出是3,3是託管執行緒的編號。9是windbg 標識的號碼。

 1 0:001> !t
 2 ThreadCount:      4
 3 UnstartedThread:  0
 4 BackgroundThread: 3
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8     (託管執行緒ID)                                                         Lock  
 9        ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
10    0    1 3d4c 01512ba8     2a020 Preemptive  03388254:00000000 0150ca30 1     MTA 
11    5    2 324c 0154f738     2b220 Preemptive  00000000:00000000 0150ca30 0     MTA (Finalizer) 
12    9    3   f8 0157c340   3029220 Preemptive  03387214:00000000 0150ca30 1     MTA (Threadpool Worker) 
13   11    4  264 0157cd28   3029220 Preemptive  0338A21C:00000000 0150ca30 0     MTA (Threadpool Worker) 

             我們可以切換到9好執行緒,看看他的執行緒棧。

 1 0:001> ~~[f8]s
 2 eax=00000000 ebx=00000001 ecx=00000000 edx=00000000 esi=00000001 edi=00000001
 3 eip=778e166c esp=05e2f0e8 ebp=05e2f278 iopl=0         nv up ei pl nz na pe nc
 4 cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
 5 ntdll!NtWaitForMultipleObjects+0xc:
 6 778e166c c21400          ret     14h
 7 
 8 
 9 0:009> !clrstack
10 OS Thread Id: 0xf8 (9)
11 Child SP       IP Call Site
12 05e2f444 778e166c [GCFrame: 05e2f444] 
13 05e2f524 778e166c [HelperMethodFrame_1OBJ: 05e2f524] System.Threading.Monitor.Enter(System.Object)
14 05e2f59c 7076377b System.IO.TextReader+SyncTextReader.ReadLine() [f:\dd\ndp\clr\src\BCL\system\io\textreader.cs @ 363]
15 05e2f5ac 705c1845 System.Console.ReadLine() [f:\dd\ndp\clr\src\BCL\system\console.cs @ 1984]
16 05e2f5b4 016f0ae8 Example_3_1_4.Program+c.b__1_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_4\Program.cs @ 17]
17 05e2f600 6fe8d4bb System.Threading.Tasks.Task.InnerInvoke() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2884]
18 05e2f60c 6fe8b731 System.Threading.Tasks.Task.Execute() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2498]
19 05e2f630 6fe8b6fc System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2861]
20 05e2f634 6fe28604 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 980]
21 05e2f6a0 6fe28537 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\executioncontext.cs @ 928]
22 05e2f6b4 6fe8b4b2 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2827]
23 05e2f718 6fe8b357 System.Threading.Tasks.Task.ExecuteEntry(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2767]
24 05e2f728 6fe8b29d System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 2704]
25 05e2f72c 6fdfeb7d System.Threading.ThreadPoolWorkQueue.Dispatch() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 820]
26 05e2f77c 6fdfe9db System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() [f:\dd\ndp\clr\src\BCL\system\threading\threadpool.cs @ 1161]
27 05e2f99c 70f1f036 [DebuggerU2MCatchHandlerFrame: 05e2f99c] 

        2.5、檢視型別控制程式碼
            程式碼樣例:Example_3_1_5
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_5.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。我們還需要通過【~0s】命令,切換到主執行緒,當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
            我們先去託管堆中查詢一下 Person 物件,使用【!dumpheap -type Person】。紅色標註的就是 Person 的參照地址。
1 0:000> !dumpheap -type Person
2  Address       MT     Size
3 02d72508 01004e1c       16     
4 
5 Statistics:
6       MT    Count    TotalSize Class Name
7 01004e1c        1           16 Example_3_1_5.Person
8 Total 1 objects

            我們由了 Person 物件的指標地址,就可以通過這個地址檢視它的方法表的資訊了。

 1 0:000> !DumpObj /d 02d72508
 2 Name:        Example_3_1_5.Person
 3 MethodTable: 01004e1c
 4 EEClass:     01001318
 5 Size:        16(0x10) bytes
 6 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe
 7 Fields:
 8       MT    Field   Offset                 Type VT     Attr    Value Name
 9 6fa424e4  4000001        4        System.String  0 instance 02d724c8 <Name>k__BackingField
10 6fa442a8  4000002        8         System.Int32  1 instance       20 <Age>k__BackingField

            當然,我們通過【dp】命令也能證明型別控制程式碼的資訊。標紅的 01004e1c 就是方法表的地址。

1 0:000> dp 02d72508 l4
2 02d72508  01004e1c 02d724c8 00000014 00000000

            我們可以【!dumpmt -md】列出所有的方法描述資訊。

 1 0:000> !dumpmt -md 01004e1c
 2 EEClass:         01001318
 3 Module:          01004044
 4 Name:            Example_3_1_5.Person
 5 mdToken:         02000003
 6 File:            E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe
 7 BaseSize:        0x10
 8 ComponentSize:   0x0
 9 Slots in VTable: 9
10 Number of IFaces in IFaceMap: 0
11 --------------------------------------
12 MethodDesc Table
13    Entry MethodDe    JIT Name
14 6fe397b8 6fa3c838 PreJIT System.Object.ToString()
15 6fe396a0 6fb78978 PreJIT System.Object.Equals(System.Object)
16 6fe421f0 6fb78998 PreJIT System.Object.GetHashCode()
17 6fdf4f2c 6fb789a0 PreJIT System.Object.Finalize()
18 02c008d8 01004e08    JIT Example_3_1_5.Person..ctor()
19 02c0044d 01004dd8   NONE Example_3_1_5.Person.get_Name()
20 02c00910 01004de4    JIT Example_3_1_5.Person.set_Name(System.String)
21 02c00455 01004df0   NONE Example_3_1_5.Person.get_Age()
22 02c00950 01004dfc    JIT Example_3_1_5.Person.set_Age(Int32)
View Code
            PreJIT表示已經預編譯了,JIT表示已經被 JIT 編譯過了,NONE表示還沒有被 JIT 編譯過。

        2.6、檢視 MethodTable 詳情(Net Framework是閉源的,看不到,NetCore是可以的)
            程式碼樣例:Example_3_1_5_1
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_5.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
            進入偵錯狀態後,我們先找到我們需要的 Person 物件,命令就是【!dumpheap -type Person】
1 0:006> !dumpheap -type Person
2          Address               MT           Size
3     026828409f60     7ffb8a239c50             32 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffb8a239c50     1        32 Example_3_1_5_1.Person
8 Total 1 objects, 32 bytes

            我們知道了物件的地址,可以執行【!do】命令,檢視 Person 物件的詳情。紅色標註的就是方法表,我們可以使用【dt】命令檢視結構。

 1 0:006> !do 026828409f60
 2 Name:        Example_3_1_5_1.Person
 3 MethodTable: 00007ffb8a239c50
 4 EEClass:     00007ffb8a222578
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\.....\Example_3_1_5_1\bin\Debug\net7.0\Example_3_1_5_1.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffb8a12fd10  4000004        8        System.String  0 instance 0000026828409f10 <Name>k__BackingField
11 00007ffb8a0ae8d0  4000005       10         System.Int32  1 instance               20 <Age>k__BackingField

            執行命令 【dt coreclr!MethodTable 00007ffb8a239c50】,00007ffb8a239c50就是方法表的地址。

 1 0:006> dt coreclr!MethodTable 00007ffb8a239c50
 2    =00007ffb`e9f688a8 s_pMethodDataCache : 0x00000268`2424c440 MethodDataCache
 3    =00007ffb`e9f688b0 s_fUseParentMethodData : 0n1
 4    =00007ffb`e9f688a0 s_fUseMethodDataCache : 0n1
 5    +0x000 m_dwFlags        : 0x1000200
 6    +0x004 m_BaseSize       : 0x20
 7    +0x008 m_wFlags2        : 0x4088
 8    +0x00a m_wToken         : 7
 9    +0x00c m_wNumVirtuals   : 4
10    +0x00e m_wNumInterfaces : 0
11    +0x010 m_pParentMethodTable : 0x00007ffb`89f893b8 MethodTable
12    +0x018 m_pLoaderModule  : 0x00007ffb`8a20cf48 Module
13    +0x020 m_pWriteableData : 0x00007ffb`8a239cb8 MethodTableWriteableData
14    +0x028 m_pEEClass       : 0x00007ffb`8a222578 EEClass
15    +0x028 m_pCanonMT       : 0x00007ffb`8a222578
16    +0x030 m_pPerInstInfo   : 0x00007ffb`8a24a2d0  -> 0x8b4c0000`0ffa25ff Dictionary
17    +0x030 m_ElementTypeHnd : 0x00007ffb`8a24a2d0
18    +0x030 m_pMultipurposeSlot1 : 0x00007ffb`8a24a2d0
19    +0x038 m_pInterfaceMap  : (null) 
20    +0x038 m_pMultipurposeSlot2 : 0
21    =00007ffb`e9ea9fb8 c_DispatchMapSlotOffsets : [0]  "080@"
22    =00007ffb`e9ea9fb0 c_NonVirtualSlotsOffsets : [0]  "080@8@@H080@"
23    =00007ffb`e9ea9fa0 c_ModuleOverrideOffsets : [0]  "080@8@@H8@@H@HHP080@8@@H080@"
24    =00007ffb`e9ebb648 c_OptionalMembersStartOffsets : [0]  "@@@@@@@H@@@H@HHP@@@H@HHP@HHPHPPX"
View Code

            以上就是 MethodTable 在 CLR 級別的結構。

        2.7、檢視方法描述符 MethodDesc。
            程式碼樣例:Example_3_1_5_1
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_5.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
            進入偵錯狀態後,我們先找到我們需要的 Person 物件,命令就是【!dumpheap -type Person】
            
1 0:006> !dumpheap -type Person
2          Address               MT           Size
3     026828409f60     7ffb8a239c50             32 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffb8a239c50     1        32 Example_3_1_5_1.Person
8 Total 1 objects, 32 bytes

             我們得到了紅色標記的 Person 物件的地址,然後執行【!do】命令檢視 Person 物件的詳情。

 1 0:006> !do 026828409f60
 2 Name:        Example_3_1_5_1.Person
 3 MethodTable: 00007ffb8a239c50
 4 EEClass:     00007ffb8a222578
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\......\Example_3_1_5_1\bin\Debug\net7.0\Example_3_1_5_1.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffb8a12fd10  4000004        8        System.String  0 instance 0000026828409f10 <Name>k__BackingField
11 00007ffb8a0ae8d0  4000005       10         System.Int32  1 instance               20 <Age>k__BackingField

              執行以上命令,我們得到了 Person 物件的方法表,然後我們使用【!dumpmt】檢視方法表詳情。

 1 0:006> !dumpmt -md 00007ffb8a239c50
 2 EEClass:             00007ffb8a222578
 3 Module:              00007ffb8a20cf48
 4 Name:                Example_3_1_5_1.Person
 5 mdToken:             0000000002000007
 6 File:                E:\Visual Studio 2022\Source\Projects\.....\Example_3_1_5_1\bin\Debug\net7.0\Example_3_1_5_1.dll
 7 AssemblyLoadContext: Default ALC - The managed instance of this context doesn't exist yet.
 8 BaseSize:            0x20
 9 ComponentSize:       0x0
10 DynamicStatics:      false
11 ContainsPointers:    true
12 Slots in VTable:     9
13 Number of IFaces in IFaceMap: 0
14 --------------------------------------
15 MethodDesc Table
16            Entry       MethodDesc    JIT Name
17 00007FFB8A0B0048 00007ffb89f89348   NONE System.Object.Finalize()
18 00007FFB8A0B0060 00007ffb89f89358   NONE System.Object.ToString()
19 00007FFB8A0B0078 00007ffb89f89368   NONE System.Object.Equals(System.Object)
20 00007FFB8A0B00C0 00007ffb89f893a8   NONE System.Object.GetHashCode()
21 00007FFB8A24A2D0 00007ffb8a239c28    JIT Example_3_1_5_1.Person..ctor()
22 00007FFB8A24A270 00007ffb8a239bc8   NONE Example_3_1_5_1.Person.get_Name()
23 00007FFB8A24A288 00007ffb8a239be0    JIT Example_3_1_5_1.Person.set_Name(System.String)
24 00007FFB8A24A2A0 00007ffb8a239bf8   NONE Example_3_1_5_1.Person.get_Age()
25 00007FFB8A24A2B8 00007ffb8a239c10    JIT Example_3_1_5_1.Person.set_Age(Int32)

            執行命令後,紅色標記的就是方法描述符,我們可以點選去檢視 MethodDesc 詳情。我們執行【!dumpmd】命令,檢視 MethodDesc。

 1 0:006> !DumpMD /d 00007ffb8a239be0
 2 Method Name:          Example_3_1_5_1.Person.set_Name(System.String)
 3 Class:                00007ffb8a222578
 4 MethodTable:          00007ffb8a239c50
 5 mdToken:              0000000006000009
 6 Module:               00007ffb8a20cf48
 7 IsJitted:             yes
 8 Current CodeAddr:     00007ffb8a1407c0
 9 Version History:
10   ILCodeVersion:      0000000000000000
11   ReJIT ID:           0
12   IL Addr:            00000268240a20ef
13      CodeAddr:           00007ffb8a1407c0  (MinOptJitted)
14      NativeCodeVersion:  0000000000000000

              由於程式碼已經編譯了,所以是有地址的,我們可以執行【!u】命令檢視set_Name()方法的組合程式碼。

 1 E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5_1\Program.cs @ 18:
 2 >>> 00007ffb`8a1407c0 55              push    rbp
 3 00007ffb`8a1407c1 57              push    rdi
 4 00007ffb`8a1407c2 4883ec28        sub     rsp,28h
 5 00007ffb`8a1407c6 488d6c2430      lea     rbp,[rsp+30h]
 6 00007ffb`8a1407cb 48894d10        mov     qword ptr [rbp+10h],rcx
 7 00007ffb`8a1407cf 48895518        mov     qword ptr [rbp+18h],rdx
 8 00007ffb`8a1407d3 833d16ca0c0000  cmp     dword ptr [00007ffb`8a20d1f0],0
 9 00007ffb`8a1407da 7405            je      00007ffb`8a1407e1
10 00007ffb`8a1407dc e8ef6bc15f      call    coreclr!JIT_DbgIsJustMyCode (00007ffb`e9d573d0)
11 00007ffb`8a1407e1 488b5510        mov     rdx,qword ptr [rbp+10h]
12 00007ffb`8a1407e5 488d4a08        lea     rcx,[rdx+8]
13 00007ffb`8a1407e9 488b5518        mov     rdx,qword ptr [rbp+18h]
14 00007ffb`8a1407ed e81ef8e2ff      call    00007ffb`89f70010 (JitHelp: CORINFO_HELP_ASSIGN_REF)
15 00007ffb`8a1407f2 90              nop
16 00007ffb`8a1407f3 4883c428        add     rsp,28h
17 00007ffb`8a1407f7 5f              pop     rdi
18 00007ffb`8a1407f8 5d              pop     rbp
19 00007ffb`8a1407f9 c3              ret

        2.8、我們可以通過【!token2ee】命令根據指定的 token 查詢 MethodDesc。
            程式碼樣例:Example_3_1_5_1
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_5.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
            
1 0:006> !token2ee Example_3_1_5 06000001
2 Module:      00e64044
3 Assembly:    Example_3_1_5.exe
4 Token:       06000001
5 MethodDesc:  00e64d58
6 Name:        Example_3_1_5.Program.Main(System.String[])
7 JITTED Code Address: 02990848

        2.9、檢視 EECLass 的結構。
            程式碼樣例:Example_3_1_5_1
            我們使用 Windbg Preview 偵錯程式,通過【launch executable】選單載入【Example_3_1_5.exe】專案,通過【g】命令,執行程式,偵錯程式執行代【Console.ReadLine()】次會暫停執行,然後我們點選【break】按鈕,進入偵錯狀態。當然,我們可以使用【cls】命令清理一下偵錯程式顯示的過多資訊,自己來決定,我是會清理的。
 1 0:006> !dumpheap -type Person
 2  Address       MT     Size
 3 029d2508 00e64e1c       16     
 4 
 5 Statistics:
 6       MT    Count    TotalSize Class Name
 7 00e64e1c        1           16 Example_3_1_5.Person
 8 Total 1 objects
 9 
10 
11 0:006> !do 029d2508
12 Name:        Example_3_1_5.Person
13 MethodTable: 00e64e1c
14 EEClass:     00e61318
15 Size:        16(0x10) bytes
16 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe
17 Fields:
18       MT    Field   Offset                 Type VT     Attr    Value Name
19 6fa424e4  4000001        4        System.String  0 instance 029d24c8 <Name>k__BackingField
20 6fa442a8  4000002        8         System.Int32  1 instance       20 <Age>k__BackingField
21 
22 
23 0:006> !DumpClass /d 00e61318
24 Class Name:      Example_3_1_5.Person
25 mdToken:         02000003
26 File:            E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\Example_3_1_5\bin\Debug\Example_3_1_5.exe
27 Parent Class:    6fa315c8
28 Module:          00e64044
29 Method Table:    00e64e1c
30 Vtable Slots:    4
31 Total Method Slots:  5
32 Class Attributes:    100001  
33 Transparency:        Critical
34 NumInstanceFields:   2
35 NumStaticFields:     0
36       MT    Field   Offset                 Type VT     Attr    Value Name
37 6fa424e4  4000001        4        System.String  0 instance           <Name>k__BackingField
38 6fa442a8  4000002        8         System.Int32  1 instance           <Age>k__BackingField
View Code
            
四、總結
    終於完成了,這篇文章寫了好幾天,看底層的東西,需要耐性和堅持。寫完了,感覺還是收穫不小的,對 Net 底層的細節瞭解更多了。學習是艱苦的過程,還挺費時費力的。就寫到這裡了,不忘初心,繼續努力。