# Unity進階開發-FSM有限狀態機 前言 我們在進行開發時,到了一定程度上,會遇到數十種狀態,繼續使用Unity的Animator控制器會出現大量的bool,float類型的變數,而這些錯綜複雜的變數與Animatator控制器如同迷宮版連線相結合會變得極其的複雜且無法良好維護擴展,出現一個B ...
一、簡介
今天是《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 = 0x0000000b【0x00aff2c8是棧地址,0x0000000b 是棧上的值,這是十六進位的】 13 0x00aff2c4 = 0x0000000c【0x00aff2d0是棧地址,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 = 0000000cView 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 l4(LOCALS: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