Advanced .Net Debugging 8:線程同步

一、介紹
    這是我的《Advanced .Net Debugging》這個系列的第八篇文章。這篇文章的內容是原書的第二部分的【調試實戰】的第六章【同步】。我們經常寫一些多線程的應用程序,寫的多了,有關多線程的問題出現的也就多了,因此,最迫切的任務就是提高解決多線程同步問題的能力。這一節我們將從本質上、從底層上來介紹線程的同步組件和同步原理,也會給出在多線程環境下如何解決問題的最佳實踐。高級調試會涉及很多方面的內容,你對 .NET 基礎知識掌握越全面、細節越底層,調試成功的機率越大,當我們遇到各種奇葩問題的時候纔不會手足無措。
    如果在沒有說明的情況下,所有代碼的測試環境都是 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源碼:源碼下載

    
在此說明:我使用了兩種調試工具,第一種:Windbg Preivew,圖形界面,使用方便,操作順手,不用擔心干擾;第二種是:NTSD,是命令行形式的調試器,在命令使用上和 Windbg 沒有任何區別,之所以增加了它的調試過程,不過是我的個人愛好,想多瞭解一些,看看他們有什麼區別,爲了學習而使用的。如果在工作中,我推薦使用 Windbg Preview,更好用,更方便,也不會出現奇怪問題(我在使用 NTSD 調試斷點的時候,不能斷住,提示內存不可讀,Windbg preview 就沒有任何問題)。
    
如果大家想了解調試過程,二選一即可,當然,我推薦查看【Windbg Preview 調試】。

二、目錄結構
    爲了讓大家看的更清楚,也爲了自己方便查找,我做了一個目錄結構,可以直觀的查看文章的佈局、內容,可以有針對性查看。
    1、同步的基礎知識
        A、基礎知識
        B、眼見爲實
            1)、KD 和 NTSD 調試
            2)、Windbg Preview 調試
    2、線程同步原語
        2.1、事件同步原語(內核鎖)
            A、基礎知識
            B、眼見爲實
                1)、KD 和 NTSD 調試
                2)、Windbg Preview 調試
        2.2、互斥體(內核鎖)
            A、基礎知識
            B、眼見爲實
                1)、KD 和 NTSD 調試
                2)、Windbg Preview 調試
        2.3、信號量(內核鎖)
            A、基礎知識
            B、眼見爲實
                1)、KD 和 NTSD 調試
                2)、Windbg Preview 調試
        2.4、監視器
            A、基礎知識
            B、眼見爲實
                1)、NTSD 調試
                2)、Windbg Preview 調試
        2.5、讀寫鎖
            A、基礎知識
            B、眼見爲實
                1)、NTSD  調試
                2)、Windbg Preview 調試
        2.6、線程池

    3、同步的內部細節
        3.1、對象頭
        3.2、同步塊
            A、基礎知識
            B、眼見爲實
                1)、NTSD 調試
                2)、Windbg Preview 調試
        3.3、瘦鎖
            A、基礎知識
            B、眼見爲實
                1)、NTSD 調試
                2)、Windbg Preview 調試
    4、同步任務
        4.1、死鎖
            A、基礎知識
            B、眼見爲實
                1)、NTSD 調試
                2)、Windbg Preview 調試
        4.2、孤立鎖:異常
            A、基礎知識
            B、眼見爲實
                1)、NTDS 調試
                2)、Windbg Preview 調試
        4.3、線程中止
        4.4、終結器掛起

三、調試源碼
    廢話不多說,本節是調試的源碼部分,沒有代碼,當然就談不上測試了,調試必須有載體。
    3.1、ExampleCore_6_1
 1 namespace ExampleCore_6_1
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             var thread = new Thread(() =>
 8             {
 9                 Console.WriteLine($"tid={Environment.CurrentManagedThreadId}");
10                 Console.ReadLine();
11             });
12 
13             thread.Start();
14 
15             Console.ReadLine();
16         }
17     }
18 }
View Code

    3.2、ExampleCore_6_2

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_2
 4 {
 5     internal class Program
 6     {        
 7         static void Main(string[] args)
 8         {            
 9             while (true)
10             {
11                 Console.WriteLine("選擇事件模型:1、Manual(手動模式) 2、Auto(自動模式) 3、Exit(退出)");
12                 var myword= Console.ReadLine();
13                 if (string.Compare(myword, "Manual", true) == 0)
14                 {
15                     RunManualResetEvent();
16                 }
17                 else if (string.Compare(myword, "Auto", true) == 0)
18                 {
19                     RunAutoResetEvent();
20                 }
21                 else if (string.Compare(myword, "Exit", true) == 0)
22                 {
23                     break;
24                 }
25             }
26         }
27 
28         static void RunManualResetEvent()
29         {
30             ManualResetEvent? mre = new ManualResetEvent(false);
31 
32             Console.WriteLine($"mre 默認爲 false,即等待狀態,請查看!");
33             Debugger.Break();
34 
35             mre.Set();
36             Console.WriteLine($"mre 默認爲 true,即放行狀態,請查看!");
37             Debugger.Break();
38 
39             mre.Reset();
40             Console.WriteLine($"mre Reset 後爲 false,即等待狀態,請查看!");
41             Debugger.Break();
42 
43             mre = null;
44         }
45 
46         static void RunAutoResetEvent()
47         {
48             AutoResetEvent? mre = new AutoResetEvent(false);
49 
50             Console.WriteLine($"are 默認爲 false,即等待狀態,請查看!");
51             Debugger.Break();
52 
53             mre.Set();
54             Console.WriteLine($"are 默認爲 true,即放行狀態,請查看!");
55             Debugger.Break();
56 
57             mre.Reset();
58             Console.WriteLine($"are Reset 後爲 false,即等待狀態,請查看!");
59             Debugger.Break();
60 
61             mre = null;
62         }
63     }
64 }
View Code

    3.3、ExampleCore_6_3

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_3
 4 {
 5     internal class Program
 6     {
 7         private static Mutex mut = new Mutex();
 8 
 9         static void Main()
10         {
11             UseResource();
12         }
13 
14         private static void UseResource()
15         {
16             // 等到安全進入。
17             mut.WaitOne();
18 
19             Console.WriteLine("已進入保護區");
20 
21             Debugger.Break();
22 
23             Console.WriteLine("正在離開保護區");
24 
25             // 釋放互斥鎖。
26             mut.ReleaseMutex();
27 
28             Debugger.Break();
29         }
30     }
31 }
View Code

    3.4、ExampleCore_6_4

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_4
 4 {
 5     internal class Program
 6     {
 7         public static Semaphore sem = new Semaphore(1, 10);
 8         static void Main(string[] args)
 9         {
10             for (int i = 0; i < int.MaxValue; i++)
11             {
12                 sem.Release();
13                 Console.WriteLine("查看當前的 sem 值。");
14                 Debugger.Break();
15             }
16         }
17     }
18 }
View Code

    3.5、ExampleCore_6_5

 1 using System.Diagnostics;
 2 
 3 namespace ExampleCore_6_5
 4 {
 5     internal class Program
 6     {
 7         public static Person person = new Person();
 8 
 9         static void Main(string[] args)
10         {
11             Task.Run(() =>
12             {
13                 lock (person)
14                 {
15                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已進入 Person 鎖中 111111");
16                     Debugger.Break();
17                 }
18             });
19             Task.Run(() =>
20             {
21                 lock (person)
22                 {
23                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已進入 Person 鎖中 222222");
24                     Debugger.Break();
25                 }
26             });
27             Console.ReadLine();
28         }
29     }
30 
31     public class Person
32     {
33     }
34 }
View Code

    3.6、ExampleCore_6_6

  1 namespace ExampleCore_6_6
  2 {
  3     internal class Program
  4     {
  5         private static ReaderWriterLock rwl = new ReaderWriterLock();
  6         // Define the shared resource protected by the ReaderWriterLock.
  7         static int resource = 0;
  8 
  9         const int numThreads = 1;
 10         static bool running = true;
 11 
 12         // Statistics.
 13         static int readerTimeouts = 0;
 14         static int writerTimeouts = 0;
 15         static int reads = 0;
 16         static int writes = 0;
 17 
 18         static void Main(string[] args)
 19         {
 20             Thread[] t = new Thread[numThreads];
 21             for (int i = 0; i < numThreads; i++)
 22             {
 23                 t[i] = new Thread(new ThreadStart(ThreadProc));
 24                 t[i].Name = new String((char)(i + 65), 1);
 25                 t[i].Start();
 26                 if (i > 10)
 27                     Thread.Sleep(300);
 28             }
 29 
 30             // Tell the threads to shut down and wait until they all finish.
 31             running = false;
 32             for (int i = 0; i < numThreads; i++)
 33                 t[i].Join();
 34 
 35             // Display statistics.
 36             Console.WriteLine("\n{0} reads, {1} writes, {2} reader time-outs, {3} writer time-outs.",
 37                   reads, writes, readerTimeouts, writerTimeouts);
 38             Console.Write("Press ENTER to exit... ");
 39             Console.ReadLine();
 40         }
 41 
 42         static void ThreadProc()
 43         {
 44             Random rnd = new Random();
 45 
 46             // Randomly select a way for the thread to read and write from the shared
 47             // resource.
 48             while (running)
 49             {
 50                 double action = rnd.NextDouble();
 51                 if (action < .8)
 52                     ReadFromResource(10);
 53                 else if (action < .81)
 54                     ReleaseRestore(rnd, 50);
 55                 else if (action < .90)
 56                     UpgradeDowngrade(rnd, 100);
 57                 else
 58                     WriteToResource(rnd, 100);
 59             }
 60         }
 61 
 62         // Request and release a reader lock, and handle time-outs.
 63         static void ReadFromResource(int timeOut)
 64         {
 65             try
 66             {
 67                 rwl.AcquireReaderLock(timeOut);
 68                 try
 69                 {
 70                     // It is safe for this thread to read from the shared resource.
 71                     Display("reads resource value " + resource);
 72                     Interlocked.Increment(ref reads);
 73                 }
 74                 finally
 75                 {
 76                     // Ensure that the lock is released.
 77                     rwl.ReleaseReaderLock();
 78                 }
 79             }
 80             catch (ApplicationException)
 81             {
 82                 // The reader lock request timed out.
 83                 Interlocked.Increment(ref readerTimeouts);
 84             }
 85         }
 86 
 87         // Request and release the writer lock, and handle time-outs.
 88         static void WriteToResource(Random rnd, int timeOut)
 89         {
 90             try
 91             {
 92                 rwl.AcquireWriterLock(timeOut);
 93                 try
 94                 {
 95                     // It's safe for this thread to access from the shared resource.
 96                     resource = rnd.Next(500);
 97                     Display("writes resource value " + resource);
 98                     Interlocked.Increment(ref writes);
 99                 }
100                 finally
101                 {
102                     // Ensure that the lock is released.
103                     rwl.ReleaseWriterLock();
104                 }
105             }
106             catch (ApplicationException)
107             {
108                 // The writer lock request timed out.
109                 Interlocked.Increment(ref writerTimeouts);
110             }
111         }
112 
113         // Requests a reader lock, upgrades the reader lock to the writer
114         // lock, and downgrades it to a reader lock again.
115         static void UpgradeDowngrade(Random rnd, int timeOut)
116         {
117             try
118             {
119                 rwl.AcquireReaderLock(timeOut);
120                 try
121                 {
122                     // It's safe for this thread to read from the shared resource.
123                     Display("reads resource value " + resource);
124                     Interlocked.Increment(ref reads);
125 
126                     // To write to the resource, either release the reader lock and
127                     // request the writer lock, or upgrade the reader lock. Upgrading
128                     // the reader lock puts the thread in the write queue, behind any
129                     // other threads that might be waiting for the writer lock.
130                     try
131                     {
132                         LockCookie lc = rwl.UpgradeToWriterLock(timeOut);
133                         try
134                         {
135                             // It's safe for this thread to read or write from the shared resource.
136                             resource = rnd.Next(500);
137                             Display("writes resource value " + resource);
138                             Interlocked.Increment(ref writes);
139                         }
140                         finally
141                         {
142                             // Ensure that the lock is released.
143                             rwl.DowngradeFromWriterLock(ref lc);
144                         }
145                     }
146                     catch (ApplicationException)
147                     {
148                         // The upgrade request timed out.
149                         Interlocked.Increment(ref writerTimeouts);
150                     }
151 
152                     // If the lock was downgraded, it's still safe to read from the resource.
153                     Display("reads resource value " + resource);
154                     Interlocked.Increment(ref reads);
155                 }
156                 finally
157                 {
158                     // Ensure that the lock is released.
159                     rwl.ReleaseReaderLock();
160                 }
161             }
162             catch (ApplicationException)
163             {
164                 // The reader lock request timed out.
165                 Interlocked.Increment(ref readerTimeouts);
166             }
167         }
168 
169         // Release all locks and later restores the lock state.
170         // Uses sequence numbers to determine whether another thread has
171         // obtained a writer lock since this thread last accessed the resource.
172         static void ReleaseRestore(Random rnd, int timeOut)
173         {
174             int lastWriter;
175 
176             try
177             {
178                 rwl.AcquireReaderLock(timeOut);
179                 try
180                 {
181                     // It's safe for this thread to read from the shared resource,
182                     // so read and cache the resource value.
183                     int resourceValue = resource;     // Cache the resource value.
184                     Display("reads resource value " + resourceValue);
185                     Interlocked.Increment(ref reads);
186 
187                     // Save the current writer sequence number.
188                     lastWriter = rwl.WriterSeqNum;
189 
190                     // Release the lock and save a cookie so the lock can be restored later.
191                     LockCookie lc = rwl.ReleaseLock();
192 
193                     // Wait for a random interval and then restore the previous state of the lock.
194                     Thread.Sleep(rnd.Next(250));
195                     rwl.RestoreLock(ref lc);
196 
197                     // Check whether other threads obtained the writer lock in the interval.
198                     // If not, then the cached value of the resource is still valid.
199                     if (rwl.AnyWritersSince(lastWriter))
200                     {
201                         resourceValue = resource;
202                         Interlocked.Increment(ref reads);
203                         Display("resource has changed " + resourceValue);
204                     }
205                     else
206                     {
207                         Display("resource has not changed " + resourceValue);
208                     }
209                 }
210                 finally
211                 {
212                     // Ensure that the lock is released.
213                     rwl.ReleaseReaderLock();
214                 }
215             }
216             catch (ApplicationException)
217             {
218                 // The reader lock request timed out.
219                 Interlocked.Increment(ref readerTimeouts);
220             }            
221         }
222 
223         // Helper method briefly displays the most recent thread action.
224             static void Display(string msg)
225             {
226                 Console.Write("Thread {0} {1}.       \r", Thread.CurrentThread.Name, msg);
227             }
228     }
229 }
View Code

    3.7、ExampleCore_6_7

 1 namespace ExampleCore_6_7
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Program program = new Program();
 8             program.Run();
 9         }
10 
11         public void Run()
12         {
13             var mycode = GetHashCode();
14             Console.WriteLine("HashCode:" + mycode);
15 
16             Console.WriteLine("Press any key to acquire lock");
17             Console.ReadLine();
18 
19             Monitor.Enter(this);
20 
21             Console.WriteLine("Press any key to release lock");
22             Console.ReadLine();
23 
24             Monitor.Exit(this);
25 
26             Console.WriteLine("Press any key to Exit");
27             Console.ReadLine();
28         }
29     }
30 }
View Code

    3.8、ExampleCore_6_8

 1 namespace ExampleCore_6_8
 2 {
 3     internal class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Program program = new Program();
 8             program.Run();
 9         }
10 
11         public void Run()
12         {            
13             Console.WriteLine("Press any key to acquire lock");
14             Console.ReadLine();
15 
16             Monitor.Enter(this);
17 
18             Console.WriteLine("Press any key to get hashcode");
19             Console.ReadLine();
20 
21             var mycode = GetHashCode();
22             Console.WriteLine("HashCode:" + mycode);
23 
24             Console.WriteLine("Press any key to release lock");
25             Console.ReadLine();
26 
27             Monitor.Exit(this);
28 
29             Console.WriteLine("Press any key to Exit");
30             Console.ReadLine();
31         }
32     }
33 }
View Code

    3.9、ExampleCore_6_9

 1 namespace ExampleCore_6_9
 2 {
 3     internal class Program
 4     {
 5         public static Person person = new Person();
 6         public static Student student = new Student();
 7         static void Main(string[] args)
 8         {
 9             Task.Run(() =>
10             {
11                 lock (person)
12                 {
13                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Person(1111) 鎖");
14                     Thread.Sleep(1000);
15                     lock (student)
16                     {
17                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Student(1111) 鎖");
18                         Console.ReadLine();
19                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Student(1111) 鎖");
20                     }
21                 }
22             });
23 
24             Task.Run(() =>
25             {
26                 lock (student)
27                 {
28                     Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Student(22222) 鎖");
29                     Thread.Sleep(1000);
30                     lock (person)
31                     {
32                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經進入 Person(22222) 鎖");
33                         Console.ReadLine();
34                         Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已經退出 Person(22222) 鎖");
35                     }
36                 }
37             });
38 
39             Console.ReadLine();
40         }
41     }
42 
43     public class Student { }
44 
45     public class Person { }
46 }
View Code
    3.10、ExampleCore_6_10
 1 namespace ExampleCore_6_10
 2 {
 3     internal class DBWrapper
 4     {
 5         private string _connectionString;
 6 
 7         public DBWrapper(string connectionString)
 8         {
 9             _connectionString = connectionString;
10         }
11     }
12 
13     internal class Program
14     {
15         private static DBWrapper? dBWrapper;
16 
17         static void Main(string[] args)
18         {
19             dBWrapper = new DBWrapper("DB1");
20 
21             Thread thread = new Thread(ThreadProc);
22             thread.Start();
23 
24             Thread.Sleep(500);
25 
26             Console.WriteLine("Acquiring Lock!");
27             Monitor.Enter(dBWrapper);
28 
29             Thread.Sleep(2000);
30 
31             Console.WriteLine("Releasing Lock!");
32             Monitor.Exit(dBWrapper);
33         }
34 
35         private static void ThreadProc()
36         {
37             try
38             {
39                 Monitor.Enter(dBWrapper!);
40                 Call3rdPartyCode(null);
41                 Monitor.Exit(dBWrapper!);
42             }
43             catch (Exception)
44             {
45                 Console.WriteLine("3rd party code throw an exception");
46             }
47         }
48 
49         private static void Call3rdPartyCode(object? obj)
50         {
51             if (obj == null)
52             {
53                 throw new NullReferenceException();
54             }
55         }
56     }
57 }
View Code


四、基礎知識
    在這一段內容中,有的小節可能會包含兩個部分,分別是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母會省略。A 是【基礎知識】,講解必要的知識點,B 是【眼見爲實】,通過調試證明講解的知識點。

    4.1、同步的基礎知識
        A、基礎知識
            進程:它描述了當一個程序在運行起來所需要的資源總和的統稱,包括:CPU、內存、磁盤、網絡、GPU 等,最明顯我們可以通過【任務管理器】查看我們電腦上運行的進程。
            線程:它是應用程序針對用戶操作做出反應的最小執行單元,也就是說,應用軟件響應用戶的任何操作都是通過一個線程完成的。切記,線程是操作系統的資源,不是 CLR 的,鑑於此,線程具有啓動、運行和停止不確定性,也就是啓動 N 個線程,每次的啓動順序都可能不一樣,同一份代碼,同一線程執行的時間也是不同的,啓動不同,運行不同,當然,結束的時機也是不同的。
            句柄:是用來標識對象或者項目的標識符,可以用來描述窗體、控件、文件等。
            多線程:能夠併發的運行任意數量的線程。

            在這節開始之前,我們必須先弄懂以上 4 個概念,我用自己的語言解釋了一下,如果大家不懂,可以自行去網上惡補了。多線程的應用程序如何設計的好的話,會有三個特徵:1、應用程序的用戶體驗更好,不卡界面;2、應用程序的性能好,處理速度更快;3、多線程具有不確定性,需要我們做更多的工作來協調。

            C# 的 Thread 類表示一個線程類,其實,在背後會有一些底層的數據結構做支撐,比如在 CLR 層會有一個對應的線程類生成,同時操作系統層也會有一個數據結構與之對應,所以說,我們簡簡單單聲明一個 Thread 類,會有三個數據結構來承載。            
            a)、C# 層的 Thread。
                C# 中的 Thread 類,其實是對 CLR 層 Thread 線程類的封裝,在 C# Thread 類的定義中,會有一個 private IntPtr DONT_USE_InternalThread 實例字段,該字段就是引用的 CLR 層的線程指針引用。

            b)、CLR 層的 Thread
                Net Core 是開源的,所以是可以看到 CLR 線程 Thread 的定義。類名是:Thread.cpp,Net 5、6、7、8都可以看。

            c)、OS 層的 KThread。
                操作系統層的線程對象是通過 _KThread 來表示的。

            多線程編程有一個無法避免的問題就是同步的問題,在.NET 中實現同步的方式還是挺多的,比如:事件同步、信號量、互斥體、監視器、瘦鎖等。

        B、眼見爲實
            調試源碼:ExampleCore_6_1
            調試任務:我們查看 C# Thread 線程所對應的 OS 層的數據結構表示
            我們直接運行的 EXE 應用程序,程序啓動成功,在控制檯中輸出:tid=4,這個值大家可能不一樣。程序運行成功,就產生了一個線程對象。我們想要查看內核態線程的id,需要在藉助一個【ProcessExplorer】工具,這個工具有32位和64位兩個版本,根據自己系統特特性選擇合適的版本,我選擇的是64位版本的。
            效果如圖:
              

            接着,我們在過【通過名稱過濾(Filter by name)】中輸入我們項目的名稱:ExampleCore_6_1,來進程查找。效果如圖:

            

            接着,我們在進程名上雙擊,打開進程屬性對話框,如圖:
            

            我們找到了我們項目進程的主鍵線程編號,然後就可以使用 Windbg 查看內核態的線程表示了。我們主線程的編號是:15560,這個是十進制的,要注意。            

            1)、KD 和 NTSD 調試
                說明一下:主線程 ID 不是 15560,我重啓了,現在是 2316,效果如圖:
                
                我們以管理員身份打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,並輸入以下命令:【kd -kl】打開調試器。這個是內核調試器,和【NTSD】是有區別的,【NTSD】是用戶態的調試器。
                如圖:
                

                打開的調試器窗口如圖:
                

                太多了無用內容了,使用【.cls】清理一下。
                執行命令【!process 0 2 ExampleCore_6_1.exe

 1 lkd> !process 0 2 ExampleCore_6_1.exe
 2 PROCESS ffffa2067324d080
 3     SessionId: 1  Cid: 3f2c    Peb: 4e16f21000  ParentCid: 0da8
 4     DirBase: 6bc43002  ObjectTable: 00000000  HandleCount:   0.
 5     Image: ExampleCore_6_1.exe
 6 
 7 No active threads
 8         THREAD ffffa20677bb90c0  Cid 3f2c.3cc8  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
 9         THREAD ffffa20677e50240  Cid 3f2c.3960  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
10         THREAD ffffa20677995080  Cid 3f2c.1f54  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
11         THREAD ffffa2066e255080  Cid 3f2c.3b98  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
12         THREAD ffffa206712dd080  Cid 3f2c.3850  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
13         THREAD ffffa2066ead5080  Cid 3f2c.2144  Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
14 
15 PROCESS ffffa206780c8080
16     SessionId: 1  Cid: 4078    Peb: b9e31b9000  ParentCid: 0da8
17     DirBase: 3183bb002  ObjectTable: ffff8a8e17548a00  HandleCount: 171.
18     Image: ExampleCore_6_1.exe
19 
20         THREAD ffffa2066e728080  Cid 4078.090c  Teb: 000000b9e31ba000 Win32Thread: ffffa20677656660 WAIT: (Executive) KernelMode Alertable
21             ffffa20678e5b568  NotificationEvent
22 
23         THREAD ffffa2066e4e1080  Cid 4078.2e48  Teb: 000000b9e31c0000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
24             ffffa20677fe3d60  NotificationEvent
25 
26         THREAD ffffa206757e8080  Cid 4078.336c  Teb: 000000b9e31c2000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
27             ffffa20677fe3c60  SynchronizationEvent
28             ffffa2066f679260  SynchronizationEvent
29             ffffa20677fe39e0  SynchronizationEvent
30 
31         THREAD ffffa206739d4080  Cid 4078.2ef0  Teb: 000000b9e31c4000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
32             ffffa206678be6a0  NotificationEvent
33             ffffa206775ab560  SynchronizationEvent
34 
35         THREAD ffffa20672ea6080  Cid 4078.3750  Teb: 000000b9e31ca000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
36             ffffa20678c15160  SynchronizationEvent

                紅色標註就是需要注意的內容,他會把這個進程中的所有線程找出來。我們通過【ProcessExploler】看到我們項目的主線程是:2316,這個值是十進制的,我們看看十六進制是多少。

1 lkd> ?0n2316
2 Evaluate expression: 2316 = 00000000`0000090c

                我再來一個截圖顯示一下他們的關係,就更清楚了。
                

                ffffa2066e728080 這個值就是線程的內核態的數據結構,我們可以繼續使用【dt nt!_KThread ffffa2066e728080】命令查看一下詳情。

  1 lkd> dt nt!_KThread ffffa2066e728080
  2    +0x000 Header           : _DISPATCHER_HEADER
  3    +0x018 SListFaultAddress : (null)
  4    +0x020 QuantumTarget    : 0xac9a2b7
  5    +0x028 InitialStack     : 0xffffdf00`c6b27c50 Void
  6    +0x030 StackLimit       : 0xffffdf00`c6b21000 Void
  7    +0x038 StackBase        : 0xffffdf00`c6b28000 Void
  8    +0x040 ThreadLock       : 0
  9    +0x048 CycleTime        : 0x94ce518
 10    +0x050 CurrentRunTime   : 0
 11    +0x054 ExpectedRunTime  : 0x787687
 12    +0x058 KernelStack      : 0xffffdf00`c6b273b0 Void
 13    +0x060 StateSaveArea    : 0xffffdf00`c6b27c80 _XSAVE_FORMAT
 14    +0x068 SchedulingGroup  : (null)
 15    +0x070 WaitRegister     : _KWAIT_STATUS_REGISTER
 16    +0x071 Running          : 0 ''
 17    +0x072 Alerted          : [2]  ""
 18    +0x074 AutoBoostActive  : 0y1
 19    +0x074 ReadyTransition  : 0y0
 20    +0x074 WaitNext         : 0y0
 21    +0x074 SystemAffinityActive : 0y0
 22    +0x074 Alertable        : 0y1
 23    +0x074 UserStackWalkActive : 0y0
 24    +0x074 ApcInterruptRequest : 0y0
 25    +0x074 QuantumEndMigrate : 0y0
 26    +0x074 UmsDirectedSwitchEnable : 0y0
 27    +0x074 TimerActive      : 0y0
 28    +0x074 SystemThread     : 0y0
 29    +0x074 ProcessDetachActive : 0y0
 30    +0x074 CalloutActive    : 0y0
 31    +0x074 ScbReadyQueue    : 0y0
 32    +0x074 ApcQueueable     : 0y1
 33    +0x074 ReservedStackInUse : 0y0
 34    +0x074 UmsPerformingSyscall : 0y0
 35    +0x074 TimerSuspended   : 0y0
 36    +0x074 SuspendedWaitMode : 0y0
 37    +0x074 SuspendSchedulerApcWait : 0y0
 38    +0x074 CetUserShadowStack : 0y0
 39    +0x074 BypassProcessFreeze : 0y0
 40    +0x074 Reserved         : 0y0000000000 (0)
 41    +0x074 MiscFlags        : 0n16401
 42    +0x078 ThreadFlagsSpare : 0y00
 43    +0x078 AutoAlignment    : 0y0
 44    +0x078 DisableBoost     : 0y0
 45    +0x078 AlertedByThreadId : 0y0
 46    +0x078 QuantumDonation  : 0y0
 47    +0x078 EnableStackSwap  : 0y1
 48    +0x078 GuiThread        : 0y1
 49    +0x078 DisableQuantum   : 0y0
 50    +0x078 ChargeOnlySchedulingGroup : 0y0
 51    +0x078 DeferPreemption  : 0y0
 52    +0x078 QueueDeferPreemption : 0y0
 53    +0x078 ForceDeferSchedule : 0y0
 54    +0x078 SharedReadyQueueAffinity : 0y1
 55    +0x078 FreezeCount      : 0y0
 56    +0x078 TerminationApcRequest : 0y0
 57    +0x078 AutoBoostEntriesExhausted : 0y1
 58    +0x078 KernelStackResident : 0y1
 59    +0x078 TerminateRequestReason : 0y00
 60    +0x078 ProcessStackCountDecremented : 0y0
 61    +0x078 RestrictedGuiThread : 0y0
 62    +0x078 VpBackingThread  : 0y0
 63    +0x078 ThreadFlagsSpare2 : 0y0
 64    +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
 65    +0x078 ThreadFlags      : 0n204992
 66    +0x07c Tag              : 0 ''
 67    +0x07d SystemHeteroCpuPolicy : 0 ''
 68    +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
 69    +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
 70    +0x07f RunningNonRetpolineCode : 0y0
 71    +0x07f SpecCtrlSpare    : 0y0000000 (0)
 72    +0x07f SpecCtrl         : 0 ''
 73    +0x080 SystemCallNumber : 6
 74    +0x084 ReadyTime        : 1
 75    +0x088 FirstArgument    : 0x00000000`00000054 Void
 76    +0x090 TrapFrame        : 0xffffdf00`c6b27ac0 _KTRAP_FRAME
 77    +0x098 ApcState         : _KAPC_STATE
 78    +0x098 ApcStateFill     : [43]  "???"
 79    +0x0c3 Priority         : 9 ''
 80    +0x0c4 UserIdealProcessor : 2
 81    +0x0c8 WaitStatus       : 0n0
 82    +0x0d0 WaitBlockList    : 0xffffa206`6e7281c0 _KWAIT_BLOCK
 83    +0x0d8 WaitListEntry    : _LIST_ENTRY [ 0xfffff806`5b7e7aa0 - 0xfffff806`5b7e7aa0 ]
 84    +0x0d8 SwapListEntry    : _SINGLE_LIST_ENTRY
 85    +0x0e8 Queue            : (null)
 86    +0x0f0 Teb              : 0x000000b9`e31ba000 Void
 87    +0x0f8 RelativeTimerBias : 0
 88    +0x100 Timer            : _KTIMER
 89    +0x140 WaitBlock        : [4] _KWAIT_BLOCK
 90    +0x140 WaitBlockFill4   : [20]  "p???"
 91    +0x154 ContextSwitches  : 0xef
 92    +0x140 WaitBlockFill5   : [68]  "p???"
 93    +0x184 State            : 0x5 ''
 94    +0x185 Spare13          : 0 ''
 95    +0x186 WaitIrql         : 0 ''
 96    +0x187 WaitMode         : 0 ''
 97    +0x140 WaitBlockFill6   : [116]  "p???"
 98    +0x1b4 WaitTime         : 0x152d42
 99    +0x140 WaitBlockFill7   : [164]  "p???"
100    +0x1e4 KernelApcDisable : 0n-1
101    +0x1e6 SpecialApcDisable : 0n0
102    +0x1e4 CombinedApcDisable : 0xffff
103    +0x140 WaitBlockFill8   : [40]  "p???"
104    +0x168 ThreadCounters   : (null)
105    +0x140 WaitBlockFill9   : [88]  "p???"
106    +0x198 XStateSave       : (null)
107    +0x140 WaitBlockFill10  : [136]  "p???"
108    +0x1c8 Win32Thread      : 0xffffa206`77656660 Void
109    +0x140 WaitBlockFill11  : [176]  "p???"
110    +0x1f0 Ucb              : (null)
111    +0x1f8 Uch              : (null)
112    +0x200 ThreadFlags2     : 0n0
113    +0x200 BamQosLevel      : 0y00000000 (0)
114    +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
115    +0x204 Spare21          : 0
116    +0x208 QueueListEntry   : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
117    +0x218 NextProcessor    : 1
118    +0x218 NextProcessorNumber : 0y0000000000000000000000000000001 (0x1)
119    +0x218 SharedReadyQueue : 0y0
120    +0x21c QueuePriority    : 0n0
121    +0x220 Process          : 0xffffa206`780c8080 _KPROCESS
122    +0x228 UserAffinity     : _GROUP_AFFINITY
123    +0x228 UserAffinityFill : [10]  "???"
124    +0x232 PreviousMode     : 1 ''
125    +0x233 BasePriority     : 8 ''
126    +0x234 PriorityDecrement : 0 ''
127    +0x234 ForegroundBoost  : 0y0000
128    +0x234 UnusualBoost     : 0y0000
129    +0x235 Preempted        : 0 ''
130    +0x236 AdjustReason     : 0 ''
131    +0x237 AdjustIncrement  : 1 ''
132    +0x238 AffinityVersion  : 0x50
133    +0x240 Affinity         : _GROUP_AFFINITY
134    +0x240 AffinityFill     : [10]  "???"
135    +0x24a ApcStateIndex    : 0 ''
136    +0x24b WaitBlockCount   : 0x1 ''
137    +0x24c IdealProcessor   : 2
138    +0x250 NpxState         : 5
139    +0x258 SavedApcState    : _KAPC_STATE
140    +0x258 SavedApcStateFill : [43]  "???"
141    +0x283 WaitReason       : 0 ''
142    +0x284 SuspendCount     : 0 ''
143    +0x285 Saturation       : 0 ''
144    +0x286 SListFaultCount  : 0
145    +0x288 SchedulerApc     : _KAPC
146    +0x288 SchedulerApcFill1 : [3]  "???"
147    +0x28b QuantumReset     : 0x6 ''
148    +0x288 SchedulerApcFill2 : [4]  "???"
149    +0x28c KernelTime       : 2
150    +0x288 SchedulerApcFill3 : [64]  "???"
151    +0x2c8 WaitPrcb         : (null)
152    +0x288 SchedulerApcFill4 : [72]  "???"
153    +0x2d0 LegoData         : (null)
154    +0x288 SchedulerApcFill5 : [83]  "???"
155    +0x2db CallbackNestingLevel : 0 ''
156    +0x2dc UserTime         : 3
157    +0x2e0 SuspendEvent     : _KEVENT
158    +0x2f8 ThreadListEntry  : _LIST_ENTRY [ 0xffffa206`6e4e1378 - 0xffffa206`780c80b0 ]
159    +0x308 MutantListHead   : _LIST_ENTRY [ 0xffffa206`6e728388 - 0xffffa206`6e728388 ]
160    +0x318 AbEntrySummary   : 0x3e '>'
161    +0x319 AbWaitEntryCount : 0 ''
162    +0x31a AbAllocationRegionCount : 0 ''
163    +0x31b SystemPriority   : 0 ''
164    +0x31c SecureThreadCookie : 0
165    +0x320 LockEntries      : 0xffffa206`6e7286d0 _KLOCK_ENTRY
166    +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
167    +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
168    +0x338 PriorityFloorCounts : [16]  ""
169    +0x348 PriorityFloorCountsReserved : [16]  ""
170    +0x358 PriorityFloorSummary : 0
171    +0x35c AbCompletedIoBoostCount : 0n0
172    +0x360 AbCompletedIoQoSBoostCount : 0n0
173    +0x364 KeReferenceCount : 0n0
174    +0x366 AbOrphanedEntrySummary : 0 ''
175    +0x367 AbOwnedEntryCount : 0x1 ''
176    +0x368 ForegroundLossTime : 0
177    +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
178    +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
179    +0x378 InGlobalForegroundList : 0
180    +0x380 ReadOperationCount : 0n32
181    +0x388 WriteOperationCount : 0n0
182    +0x390 OtherOperationCount : 0n158
183    +0x398 ReadTransferCount : 0n66740
184    +0x3a0 WriteTransferCount : 0n0
185    +0x3a8 OtherTransferCount : 0n3494
186    +0x3b0 QueuedScb        : (null)
187    +0x3b8 ThreadTimerDelay : 0
188    +0x3bc ThreadFlags3     : 0n0
189    +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
190    +0x3bc PpmPolicy        : 0y00
191    +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
192    +0x3c0 TracingPrivate   : [1] 0
193    +0x3c8 SchedulerAssist  : (null)
194    +0x3d0 AbWaitObject     : (null)
195    +0x3d8 ReservedPreviousReadyTimeValue : 0
196    +0x3e0 KernelWaitTime   : 0xe
197    +0x3e8 UserWaitTime     : 0
198    +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
199    +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
200    +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
201    +0x400 SchedulerAssistPriorityFloor : 0n0
202    +0x404 Spare28          : 0
203    +0x408 ResourceIndex    : 0xe7 ''
204    +0x409 Spare31          : [3]  ""
205    +0x410 EndPadding       : [4] 0
206 lkd>
View Code

                當然,我們也可以通過【NTSD -pn ExampleCore_6_1.exe】直接查看正在執行中項目,通過【!t】或者【!threads】命令,查看線程三者的對應關係。

 1 0:005> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      90c 000001CFD8DCEB20    2a020 Preemptive  000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
11    3    2     2ef0 000002106F45DDF0    2b220 Preemptive  0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
12    4    4     3750 000002106F46D070  202b020 Preemptive  000001CFDD40B4D0:000001CFDD40C630 000001CFD8E1B860 -00001 MTA
13 
14 0:005> !threads
15 ThreadCount:      3
16 UnstartedThread:  0
17 BackgroundThread: 1
18 PendingThread:    0
19 DeadThread:       0
20 Hosted Runtime:   no
21                                                                                                             Lock
22  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
23    0    1      90c 000001CFD8DCEB20    2a020 Preemptive  000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
24    3    2     2ef0 000002106F45DDF0    2b220 Preemptive  0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
25    4    4     3750 000002106F46D070  202b020 Preemptive  000001CFDD40B4D0:000001CFDD40C630 000001CFD8E1B860 -00001 MTA
26 0:005>

                ID是 1 就是 C# 的託管線程編號, OSID 的值是 90c 就是操作系統層面的線程的數據結構,ThreadOBJ 就是 CLR 層面的線程。

                
            2)、Windbg Preview 調試
                然後,我們打開 Windbg,點擊【File】-->【Attach to kernel(附加內核態)】,在右側選擇【local】,就是本機的內核態,點擊【ok】按鈕,進入調試界面。然後,我們使用【!process】命令查找一下我們的項目。
 1 lkd> !process 0 2 ExampleCore_6_1.exe
 2 PROCESS ffffa2067324d080
 3     SessionId: 1  Cid: 3f2c    Peb: 4e16f21000  ParentCid: 0da8
 4     DirBase: 6bc43002  ObjectTable: ffff8a8e1a97c180  HandleCount: 171.
 5     Image: ExampleCore_6_1.exe
 6 
 7         THREAD ffffa20677bb90c0  Cid 3f2c.3cc8  Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
 8             ffffa20678223bb8  NotificationEvent
 9 
10         THREAD ffffa20677e50240  Cid 3f2c.3960  Teb: 0000004e16f2a000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
11             ffffa20677fe5660  NotificationEvent
12 
13         THREAD ffffa20677995080  Cid 3f2c.1f54  Teb: 0000004e16f2c000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
14             ffffa20677fe5560  SynchronizationEvent
15             ffffa20677fe56e0  SynchronizationEvent
16             ffffa20677fe5860  SynchronizationEvent
17 
18         THREAD ffffa2066e255080  Cid 3f2c.3b98  Teb: 0000004e16f2e000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
19             ffffa206678be6a0  NotificationEvent
20             ffffa20677cfa260  SynchronizationEvent
21 
22         THREAD ffffa206712dd080  Cid 3f2c.3850  Teb: 0000004e16f34000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
23             ffffa20677964c60  SynchronizationEvent

                我們通過【ProcessExploler】看到我們項目的主線程是:1204,這個值是十進制的,我們看看十六進制是多少。

1 lkd> ? 0n15560
2 Evaluate expression: 15560 = 00000000`00003cc8

                我們如果使用的調試器是【Windbg Preview】,它有一個特性,選擇一個文本,和文本內容相同的也會被凸顯出來,我們選擇 3cc8,發現我們使用【!process】命令的結果中也有被選擇了,如圖:
                

                ffffa20677bb90c0 這個值就是線程的內核態的數據結構,我們可以繼續使用【dt】命令查看一下詳情。

  1 lkd> dt nt!_KThread ffffa20677bb90c0
  2    +0x000 Header           : _DISPATCHER_HEADER
  3    +0x018 SListFaultAddress : (null) 
  4    +0x020 QuantumTarget    : 0xd630923
  5    +0x028 InitialStack     : 0xffffdf00`c2f32c50 Void
  6    +0x030 StackLimit       : 0xffffdf00`c2f2c000 Void
  7    +0x038 StackBase        : 0xffffdf00`c2f33000 Void
  8    +0x040 ThreadLock       : 0
  9    +0x048 CycleTime        : 0x94e88f3
 10    +0x050 CurrentRunTime   : 0
 11    +0x054 ExpectedRunTime  : 0xa80710
 12    +0x058 KernelStack      : 0xffffdf00`c2f323b0 Void
 13    +0x060 StateSaveArea    : 0xffffdf00`c2f32c80 _XSAVE_FORMAT
 14    +0x068 SchedulingGroup  : (null) 
 15    +0x070 WaitRegister     : _KWAIT_STATUS_REGISTER
 16    +0x071 Running          : 0 ''
 17    +0x072 Alerted          : [2]  ""
 18    +0x074 AutoBoostActive  : 0y1
 19    +0x074 ReadyTransition  : 0y0
 20    +0x074 WaitNext         : 0y0
 21    +0x074 SystemAffinityActive : 0y0
 22    +0x074 Alertable        : 0y1
 23    +0x074 UserStackWalkActive : 0y0
 24    +0x074 ApcInterruptRequest : 0y0
 25    +0x074 QuantumEndMigrate : 0y0
 26    +0x074 UmsDirectedSwitchEnable : 0y0
 27    +0x074 TimerActive      : 0y0
 28    +0x074 SystemThread     : 0y0
 29    +0x074 ProcessDetachActive : 0y0
 30    +0x074 CalloutActive    : 0y0
 31    +0x074 ScbReadyQueue    : 0y0
 32    +0x074 ApcQueueable     : 0y1
 33    +0x074 ReservedStackInUse : 0y0
 34    +0x074 UmsPerformingSyscall : 0y0
 35    +0x074 TimerSuspended   : 0y0
 36    +0x074 SuspendedWaitMode : 0y0
 37    +0x074 SuspendSchedulerApcWait : 0y0
 38    +0x074 CetUserShadowStack : 0y0
 39    +0x074 BypassProcessFreeze : 0y0
 40    +0x074 Reserved         : 0y0000000000 (0)
 41    +0x074 MiscFlags        : 0n16401
 42    +0x078 ThreadFlagsSpare : 0y00
 43    +0x078 AutoAlignment    : 0y0
 44    +0x078 DisableBoost     : 0y0
 45    +0x078 AlertedByThreadId : 0y0
 46    +0x078 QuantumDonation  : 0y0
 47    +0x078 EnableStackSwap  : 0y1
 48    +0x078 GuiThread        : 0y1
 49    +0x078 DisableQuantum   : 0y0
 50    +0x078 ChargeOnlySchedulingGroup : 0y0
 51    +0x078 DeferPreemption  : 0y0
 52    +0x078 QueueDeferPreemption : 0y0
 53    +0x078 ForceDeferSchedule : 0y0
 54    +0x078 SharedReadyQueueAffinity : 0y1
 55    +0x078 FreezeCount      : 0y0
 56    +0x078 TerminationApcRequest : 0y0
 57    +0x078 AutoBoostEntriesExhausted : 0y1
 58    +0x078 KernelStackResident : 0y1
 59    +0x078 TerminateRequestReason : 0y00
 60    +0x078 ProcessStackCountDecremented : 0y0
 61    +0x078 RestrictedGuiThread : 0y0
 62    +0x078 VpBackingThread  : 0y0
 63    +0x078 ThreadFlagsSpare2 : 0y0
 64    +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
 65    +0x078 ThreadFlags      : 0n204992
 66    +0x07c Tag              : 0 ''
 67    +0x07d SystemHeteroCpuPolicy : 0 ''
 68    +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
 69    +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
 70    +0x07f RunningNonRetpolineCode : 0y0
 71    +0x07f SpecCtrlSpare    : 0y0000000 (0)
 72    +0x07f SpecCtrl         : 0 ''
 73    +0x080 SystemCallNumber : 6
 74    +0x084 ReadyTime        : 3
 75    +0x088 FirstArgument    : 0x00000000`00000050 Void
 76    +0x090 TrapFrame        : 0xffffdf00`c2f32ac0 _KTRAP_FRAME
 77    +0x098 ApcState         : _KAPC_STATE
 78    +0x098 ApcStateFill     : [43]  "X???"
 79    +0x0c3 Priority         : 8 ''
 80    +0x0c4 UserIdealProcessor : 2
 81    +0x0c8 WaitStatus       : 0n256
 82    +0x0d0 WaitBlockList    : 0xffffa206`77bb9200 _KWAIT_BLOCK
 83    +0x0d8 WaitListEntry    : _LIST_ENTRY [ 0x00000000`00000000 - 0xffffa206`67903158 ]
 84    +0x0d8 SwapListEntry    : _SINGLE_LIST_ENTRY
 85    +0x0e8 Queue            : (null) 
 86    +0x0f0 Teb              : 0x0000004e`16f22000 Void
 87    +0x0f8 RelativeTimerBias : 0
 88    +0x100 Timer            : _KTIMER
 89    +0x140 WaitBlock        : [4] _KWAIT_BLOCK
 90    +0x140 WaitBlockFill4   : [20]  "???"
 91    +0x154 ContextSwitches  : 0xde
 92    +0x140 WaitBlockFill5   : [68]  "???"
 93    +0x184 State            : 0x5 ''
 94    +0x185 Spare13          : 0 ''
 95    +0x186 WaitIrql         : 0 ''
 96    +0x187 WaitMode         : 0 ''
 97    +0x140 WaitBlockFill6   : [116]  "???"
 98    +0x1b4 WaitTime         : 0x11f7e8
 99    +0x140 WaitBlockFill7   : [164]  "???"
100    +0x1e4 KernelApcDisable : 0n-1
101    +0x1e6 SpecialApcDisable : 0n0
102    +0x1e4 CombinedApcDisable : 0xffff
103    +0x140 WaitBlockFill8   : [40]  "???"
104    +0x168 ThreadCounters   : (null) 
105    +0x140 WaitBlockFill9   : [88]  "???"
106    +0x198 XStateSave       : (null) 
107    +0x140 WaitBlockFill10  : [136]  "???"
108    +0x1c8 Win32Thread      : 0xffffa206`7765a990 Void
109    +0x140 WaitBlockFill11  : [176]  "???"
110    +0x1f0 Ucb              : (null) 
111    +0x1f8 Uch              : (null) 
112    +0x200 ThreadFlags2     : 0n0
113    +0x200 BamQosLevel      : 0y00000000 (0)
114    +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
115    +0x204 Spare21          : 0
116    +0x208 QueueListEntry   : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
117    +0x218 NextProcessor    : 2
118    +0x218 NextProcessorNumber : 0y0000000000000000000000000000010 (0x2)
119    +0x218 SharedReadyQueue : 0y0
120    +0x21c QueuePriority    : 0n0
121    +0x220 Process          : 0xffffa206`7324d080 _KPROCESS
122    +0x228 UserAffinity     : _GROUP_AFFINITY
123    +0x228 UserAffinityFill : [10]  "???"
124    +0x232 PreviousMode     : 1 ''
125    +0x233 BasePriority     : 8 ''
126    +0x234 PriorityDecrement : 0 ''
127    +0x234 ForegroundBoost  : 0y0000
128    +0x234 UnusualBoost     : 0y0000
129    +0x235 Preempted        : 0 ''
130    +0x236 AdjustReason     : 0 ''
131    +0x237 AdjustIncrement  : 0 ''
132    +0x238 AffinityVersion  : 0x50
133    +0x240 Affinity         : _GROUP_AFFINITY
134    +0x240 AffinityFill     : [10]  "???"
135    +0x24a ApcStateIndex    : 0 ''
136    +0x24b WaitBlockCount   : 0x1 ''
137    +0x24c IdealProcessor   : 2
138    +0x250 NpxState         : 5
139    +0x258 SavedApcState    : _KAPC_STATE
140    +0x258 SavedApcStateFill : [43]  "???"
141    +0x283 WaitReason       : 0 ''
142    +0x284 SuspendCount     : 0 ''
143    +0x285 Saturation       : 0 ''
144    +0x286 SListFaultCount  : 0
145    +0x288 SchedulerApc     : _KAPC
146    +0x288 SchedulerApcFill1 : [3]  "???"
147    +0x28b QuantumReset     : 0x6 ''
148    +0x288 SchedulerApcFill2 : [4]  "???"
149    +0x28c KernelTime       : 1
150    +0x288 SchedulerApcFill3 : [64]  "???"
151    +0x2c8 WaitPrcb         : (null) 
152    +0x288 SchedulerApcFill4 : [72]  "???"
153    +0x2d0 LegoData         : (null) 
154    +0x288 SchedulerApcFill5 : [83]  "???"
155    +0x2db CallbackNestingLevel : 0 ''
156    +0x2dc UserTime         : 2
157    +0x2e0 SuspendEvent     : _KEVENT
158    +0x2f8 ThreadListEntry  : _LIST_ENTRY [ 0xffffa206`77e50538 - 0xffffa206`7324d0b0 ]
159    +0x308 MutantListHead   : _LIST_ENTRY [ 0xffffa206`77bb93c8 - 0xffffa206`77bb93c8 ]
160    +0x318 AbEntrySummary   : 0x3e '>'
161    +0x319 AbWaitEntryCount : 0 ''
162    +0x31a AbAllocationRegionCount : 0 ''
163    +0x31b SystemPriority   : 0 ''
164    +0x31c SecureThreadCookie : 0
165    +0x320 LockEntries      : 0xffffa206`77bb9710 _KLOCK_ENTRY
166    +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
167    +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
168    +0x338 PriorityFloorCounts : [16]  ""
169    +0x348 PriorityFloorCountsReserved : [16]  ""
170    +0x358 PriorityFloorSummary : 0
171    +0x35c AbCompletedIoBoostCount : 0n0
172    +0x360 AbCompletedIoQoSBoostCount : 0n0
173    +0x364 KeReferenceCount : 0n0
174    +0x366 AbOrphanedEntrySummary : 0 ''
175    +0x367 AbOwnedEntryCount : 0x1 ''
176    +0x368 ForegroundLossTime : 0
177    +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
178    +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
179    +0x378 InGlobalForegroundList : 0
180    +0x380 ReadOperationCount : 0n32
181    +0x388 WriteOperationCount : 0n0
182    +0x390 OtherOperationCount : 0n158
183    +0x398 ReadTransferCount : 0n66740
184    +0x3a0 WriteTransferCount : 0n0
185    +0x3a8 OtherTransferCount : 0n3494
186    +0x3b0 QueuedScb        : (null) 
187    +0x3b8 ThreadTimerDelay : 0
188    +0x3bc ThreadFlags3     : 0n0
189    +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
190    +0x3bc PpmPolicy        : 0y00
191    +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
192    +0x3c0 TracingPrivate   : [1] 0
193    +0x3c8 SchedulerAssist  : (null) 
194    +0x3d0 AbWaitObject     : (null) 
195    +0x3d8 ReservedPreviousReadyTimeValue : 0
196    +0x3e0 KernelWaitTime   : 0xe
197    +0x3e8 UserWaitTime     : 0
198    +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
199    +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
200    +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
201    +0x400 SchedulerAssistPriorityFloor : 0n0
202    +0x404 Spare28          : 0
203    +0x408 ResourceIndex    : 0x1 ''
204    +0x409 Spare31          : [3]  ""
205    +0x410 EndPadding       : [4] 0
View Code

                這個線程的數據結構內容還是不少的。
                我們可以使用【!thread ffffa20677bb90c0】命令查看更易閱讀的結果。

 1 lkd> !thread ffffa20677bb90c0
 2 THREAD ffffa20677bb90c0  Cid 3f2c.3cc8  Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
 3     ffffa20678223bb8  NotificationEvent
 4 IRP List:
 5     ffffa2067802cdc0: (0006,0160) Flags: 00060900  Mdl: ffffa20670216220
 6     ffffa2067802bc80: (0006,0160) Flags: 00060800  Mdl: 00000000
 7 Not impersonating
 8 DeviceMap                 ffff8a8e0d39f7e0
 9 Owning Process            ffffa2067324d080       Image:         ExampleCore_6_1.exe
10 Attached Process          N/A            Image:         N/A
11 Wait Start TickCount      1177576        Ticks: 163639 (0:00:42:36.859)
12 Context Switch Count      222            IdealProcessor: 2             
13 UserTime                  00:00:00.031
14 KernelTime                00:00:00.015
15 Win32 Start Address 0x00007ff7359f1360
16 Stack Init ffffdf00c2f32c50 Current ffffdf00c2f323b0
17 Base ffffdf00c2f33000 Limit ffffdf00c2f2c000 Call 0000000000000000
18 Priority 8  BasePriority 8  IoPriority 2  PagePriority 5
19 Child-SP          RetAddr               : Args to Child                                                           : Call Site
20 ffffdf00`c2f323f0 fffff806`5d841330     : ffffbb80`50317180 00000000`ffffffff ffffa206`00000000 00000000`50317180 : nt!KiSwapContext+0x76
21 ffffdf00`c2f32530 fffff806`5d84085f     : 00000000`00000002 ffff8a8e`00000000 ffffdf00`c2f326f0 fffff806`00000000 : nt!KiSwapThread+0x500
22 ffffdf00`c2f325e0 fffff806`5d840103     : 000002af`00000000 00000000`00000000 00000000`00000000 ffffa206`77bb9200 : nt!KiCommitThreadWait+0x14f
23 ffffdf00`c2f32680 fffff806`5d9f18bc     : ffffa206`78223bb8 ffffa206`00000000 00000000`00000000 ffffa206`77bb9001 : nt!KeWaitForSingleObject+0x233
24 ffffdf00`c2f32770 fffff806`5dc45b5b     : 00000000`00000000 00000000`00000001 ffffa206`78223b20 ffffa206`7802cdc0 : nt!IopWaitForSynchronousIoEvent+0x50
25 ffffdf00`c2f327b0 fffff806`5dbcf918     : ffffdf00`c2f32b40 ffffa206`78223b20 00000000`00000000 00000000`00000000 : nt!IopSynchronousServiceTail+0x50b
26 ffffdf00`c2f32850 fffff806`5dc0c4b8     : ffffa206`78223b20 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopReadFile+0x7cc
27 ffffdf00`c2f32940 fffff806`5da11578     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtReadFile+0x8a8
28 ffffdf00`c2f32a50 00007ffa`7f08d0a4     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x28 (TrapFrame @ ffffdf00`c2f32ac0)
29 0000004e`1717e558 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0x00007ffa`7f08d0a4

                當然,我們也可以通過 Windbg Preview 直接查看了,我們的項目正在執行中,所以我們可以通過【Attach to process】進入調試界面,然後,通過【!t】或者【!threads】命令,查看線程三者的對應關係。

 1 0:005> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     3cc8 00000246EFD07630    2a020 Preemptive  00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA 
11    3    2     3b98 00000246EFD70060    2b220 Preemptive  0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer) 
12    4    4     3850 00000246EFCCD3F0  202b020 Preemptive  00000246F440B4D0:00000246F440C630 00000246efca59d0 -00001 MTA 
13 
14 0:005> !threads
15 ThreadCount:      3
16 UnstartedThread:  0
17 BackgroundThread: 1
18 PendingThread:    0
19 DeadThread:       0
20 Hosted Runtime:   no
21                                                                                                             Lock  
22  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
23    0    1     3cc8 00000246EFD07630    2a020 Preemptive  00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA 
24    3    2     3b98 00000246EFD70060    2b220 Preemptive  0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer) 
25    4    4     3850 00000246EFCCD3F0  202b020 Preemptive  00000246F440B4D0:00000246F440C630 00000246efca59d0 -00001 MTA 

                我們在【!t/threads】命令的結果中,查看【OSID】列,也能看到 3cc8 的標識。ID是1就是C#的託管線程編號, OSID的值是 3cc8 就是操作系統層面的線程的數據結構,ThreadOBJ 就是 CLR 層面的線程。


    4.2、線程同步原語
        在開始之前,先解釋一下以下概念:用戶態和內核態,這兩個概念不清楚,就會搞得雲裏霧裏的。
        用戶態:
          用戶態也被稱爲用戶模式,是指應用程序的運行狀態。在這種模式下,應用程序擁有有限的系統資源訪問權限,只能在操作系統劃定的特定空間內運行。用戶態下運行的程序不能直接訪問硬件設備或執行特權指令,所有對硬件的訪問都必須通過操作系統進行。
          在用戶態下,應用程序通過系統調用來請求操作系統提供的服務。例如,文件操作、網絡通信等都需要通過系統調用來實現。當應用程序發出系統調用時,會觸發上下文切換,將CPU的控制權交給操作系統內核,進入內核態。

        內核態:
          內核態也被稱爲內核模式或特權模式,是操作系統內核的運行狀態。處於內核態的CPU可以執行所有的指令,訪問所有的內存地址,擁有最高的權限。內核態下運行的程序可以訪問系統的所有資源,包括CPU、內存、I/O等。
          在內核態下,操作系統可以響應所有的中斷請求,處理硬件事件和系統調用。當應用程序發出系統調用時,CPU會切換到內核態,執行相應的操作,然後返回用戶態。此外,當發生嚴重錯誤或異常時,也會觸發內核態的切換。

        4.2.1、事件同步原語(AutoResetEvent 和 ManulResetEvent(內核鎖))
            A、基礎知識
                事件同步的本質實在內核態維護了一個 bool 值,通過 bool 值來實現線程間的同步,具體的使用方法網上很多,我這裏就不過多的贅述了,這裏我們看看是如何通過 bool 值的變化實現線程間的同步的。
                事件是一種內核態的原語,可以在用戶態中通過句柄來訪問。事件也是一個同步對象,它有兩種狀態:已觸發(signaled)和未觸發(nonsignaled)。當事件是未觸發的狀態,在這個事件上的線程就會處於等待的狀態,如果事件的狀態變爲已觸發時,這個線程也會恢復執行。
                事件對象經常用於對多個線程之間的代碼執行流程進行同步。
                
AutoResetEvent 和 ManulResetEvent 區別:ManulResetEvent 在手動重置事件中,事件對象保持爲已觸發的狀態,直到被手動重置,因此,所有在這個事件對象上等待的線程都會被釋放。AutoResetEvent 自動重置事件只允許其中一個等待線程被釋放,然後,又立即自動的回到未觸發狀態。如果沒有任何等待的線程,那麼這個事件對象將保持爲未觸發的狀態,直到第一個線程在這個事件上開始等待。

               我們都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底層的功能,說白了就是 C# 只是使用了 Windows 內核提供的事件,C# 不過是對其進行了包裝,如果你想要查看內存地址,必須到內核態去看。

               AutoResetEvent 或者 ManulResetEvent 類型內部包含了 SafeWaitHandle 引用類型的一個字段 _waitHandle,_waitHandle 類型內部包含了一個值類型的(System.IntPtr)的 handle 實現的同步操作。

            B、眼見爲實
                調試源碼:ExampleCore_6_2
                調試任務:我們看看 AutoResetEvent 是如何通過 bool 值變化實現線程間的同步的。
                注意:這裏的調試都需要用到兩種調試器,分別是用戶態的和內核態的,還有一個獲取對象內核地址的工具【Process Explorer】。在用戶態調試器執行調用,在內核態調試器裏看具體地址內容的變化。
                1)、KD 和 NTSD 調試
                    在這裏,我只測試 ManualResetEvent 類型的變化,AutoResetEvent 暫時我忽略,因爲它們沒區別。調試器使用用戶態的 NTSD 和內核態的 KD。
                    編譯我們的項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_2\bin\Debug\net8.0\ExampleCore_6_2.exe】打開調試器。
                    進入調試器後,【g】直接運行,直到調試器輸出“選擇事件模型:1、Manual(手動模式) 2、Auto(自動模式) 3、Exit(退出)”字樣,我們輸入 manual,不區分大小寫,就進入到了 RunManualResetEvent 方法內,調試器會輸出“mre 默認爲 false,即等待狀態,請查看!”字樣。調試器中斷執行,開始我們的調試了。
                    首先,我們在託管堆上查找 ManualResetEvent 類型的對象,執行命令【!DumpHeap -type ManualResetEvent】。
1 0:000> !DumpHeap -type ManualResetEvent
2          Address               MT     Size
3 0000020f29414180 00007ff8db192a88       24
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ff8db192a88        1           24 System.Threading.ManualResetEvent
8 Total 1 objects

                    ManualResetEvent 對象的地址是 0000020f29414180,我們繼續使用【!do】或者【!DumpObj】命令查看它的詳情。

 1 0:000> !do 0000020f29414180
 2 Name:        System.Threading.ManualResetEvent
 3 MethodTable: 00007ff8db192a88
 4 EEClass:     00007ff8db182508
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8db193318  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000020f294142d8 _waitHandle
11 00007ff8db0370a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    紅色標註的就是一個引用類型實例,地址是 0000020f294142d8,針對該地址,繼續執行【!do】命令。

 1 0:000> !do 0000020f294142d8
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ff8db193318
 4 EEClass:     00007ff8db182970
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8db0370a0  400126e        8        System.IntPtr  1 instance 00000000000002B8 handle
11 00007ff8dafc1188  400126f       10         System.Int32  1 instance                4 _state
12 00007ff8daf8d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ff8daf8d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    紅色標註的是一個 handle 對象,我們可以使用【!handle 00000000000002B8 f】命令繼續查看,必須具有 f 參數。

 1 0:000> !handle 00000000000002B8 f
 2 Handle 2b8
 3   Type          Event
 4   Attributes    0
 5   GrantedAccess 0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount   2
 9   PointerCount  32769
10   Name          <none>
11   Object Specific Information
12     Event Type Manual Reset(事件類型是 ManualResetEvent)
13     Event is Waiting(初始狀態是等待)

                    到此,說明 ManualResetEvent(false) 默認是等待的狀態。
                    此刻,我們在藉助【Process Explorer】工具,找到事件同步對象的內核地址,看看內核地址上的數據的變化。打開這個工具,然後在【Filter by name】輸入項目名稱 ExampleCore_6_2,結果如圖:

                    

                    我們在【Handles】選項裏,找到我們的事件對象,然後雙擊,打開屬性框,找到內核的地址。如圖:
                    

                    我們找到了事件對象在內核上的地址,我們需要再打開一個【kd】調試器,開始內核調試。
                    我們就找到了內核地址【0xFFFF940C4DC558E0】了。然後,我們到 kd 的內核態中去查看一下這個地址,使用【dp 0xFFFF940C4DC558E0 l1】命令。當前值:0(00000000)

 

1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0  00000000`00060000

                    說明 ManualResetEvent 的 fase 表示的是等待,通過用戶態命令【!handle 00000000000002B8 f】和內核態命令【dp 0xFFFF940C4DC558E0 l1】都能證明。
                    然後我們【g】一下用戶態的 NTSD 調試器,控制檯輸出“mre 默認爲 true,即放行狀態,請查看!”字樣,再次執行命令【!handle 00000000000002B8 f】。

 1 0:000> !handle 00000000000002B8 f
 2 Handle 2b8
 3   Type          Event
 4   Attributes    0
 5   GrantedAccess 0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount   2
 9   PointerCount  65535
10   Name          <none>
11   Object Specific Information
12     Event Type Manual Reset
13     Event is Set(放行狀態)

                    然後切換到【內核態】的 KD 調試器,繼續使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。

1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0  00000001`00060000(紅色變成 1 ,表示 true)

                    【!handle】命令的結果是 Set,【dp】命令變成了 00000001,後面的不用管。
                    最後,我們再【g】一下【用戶態】的 KD,控制檯輸出“mre Reset後爲 false,即等待狀態,請查看!”字樣,再次執行【!handle 00000000000002B8 f】命令。

 1 0:000> !handle 00000000000002B8 f
 2 Handle 2b8
 3   Type          Event
 4   Attributes    0
 5   GrantedAccess 0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount   2
 9   PointerCount  65534
10   Name          <none>
11   Object Specific Information
12     Event Type Manual Reset
13     Event is Waiting(處於等待)

                    Reset 後是等待的狀態,然後切換到【內核態】的 KD,繼續使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。

1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0  00000000`00060000(紅色是 0,0 代表就是 false)

                    我們就看到了,狀態是0和1相互切換的。


                2)、Windbg Preview 調試
                    我們編譯項目,打開【Windbg Preview】調試器,點擊【文件】----》【Launch executable】加載我們的程序,打開調試器的界面,程序已經處於中斷狀態。我們使用【g】命令,繼續運行程序,在【Debugger.Break()】語句處停止,我們的控制檯應用程序輸出:mre 默認爲 false,即等待狀態,請查看!,Windbg 處於暫停狀態,我們就可以調試了。
                    首先,我們去託管堆中查找一下 ManualResetEvent 這個對象,執行【!dumpheap -type ManualResetEvent】命令。
1 0:000> !DumpHeap -type ManualResetEvent
2          Address               MT           Size
3     012b87014180     7ff8da3e2a88             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ff8da3e2a88     1        24 System.Threading.ManualResetEvent
8 Total 1 objects, 24 bytes
                    ManualResetEvent 對象的地址是 012b87014180,針對這個地址,我們使用【!do】或者【!DumpObj】命令,查看它的詳情。
 1 0:000> !DumpObj 012b87014180
 2 Name:        System.Threading.ManualResetEvent(手動重置事件)
 3 MethodTable: 00007ff8da3e2a88
 4 EEClass:     00007ff8da3d2508
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da3e3318  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000012b870142d8 _waitHandle
11 00007ff8da2870a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<
                    紅色標註的是一個 instance 引用類型(VT=0)實例對象,我們可以使用【!DumpObj 0000012b870142d8】命令繼續查看。
 1 0:000> !DumpObj 0000012b870142d8
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ff8da3e3318
 4 EEClass:     00007ff8da3d2968
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da2870a0  400126e        8        System.IntPtr  1 instance 0000000000000248 handle
11 00007ff8da211188  400126f       10         System.Int32  1 instance                4 _state
12 00007ff8da1dd070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ff8da1dd070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    紅色標註的是一個 System.IntPtr 值類型(VT=1)實例對象,我們可以使用【!DumpVC 00007ff8da2870a0  0000000000000248】命令繼續查看。

 1 0:000> !DumpVC 00007ff8da2870a0  0000000000000248
 2 Name:        System.IntPtr
 3 MethodTable: 00007ff8da2870a0
 4 EEClass:     00007ff8da266100
 5 Size:        24(0x18) bytes
 6 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 7 Fields:
 8               MT    Field   Offset                 Type VT     Attr            Value Name
 9 00007ff8da2870a0  4000525        0        System.IntPtr  1 instance  _value
10 00007ff8da2870a0  4000526      a78        System.IntPtr  1   static 0000000000000000 Zero
                    我們可以不使用【!DumpVC】命令,直接使用【!handle】命令。
                    紅色標註的是一個 handle 對象,我們可以使用【!handle 0000000000000248 f】命令繼續查看,必須具有 f 參數。
 1 0:000> !handle 0000000000000248 f
 2 Handle 248
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     32769
10   Name             <none>
11   Object Specific Information
12     Event Type Manual Reset(事件類型是 ManualResetEvent13     Event is Waiting(當前是等待狀態

                    說明 false 是等待的狀態,然後,我們繼續【g】運行一下,等我們的控制檯項目輸出:mre 默認爲 true,即放行狀態,請查看!,我們繼續執行【!handle 0000000000000248 f】命令查看。

 1 0:000> !handle 0000000000000248 f
 2 Handle 248
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65536
10   Name             <none>
11   Object Specific Information
12     Event Type Manual Reset
13     Event is Set

                    然後,我們繼續【g】運行一下,等我們的控制檯項目輸出:mre Reset後爲 false,即等待狀態,請查看!我們繼續執行【!handle 0000000000000248 f】命令查看。

 1 0:000> !handle 0000000000000248 f
 2 Handle 248
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65535
10   Name             <none>
11   Object Specific Information
12     Event Type Manual Reset
13     Event is Waiting(等待了)

                    我們再次輸入 auto 測試一下 AutoResetEvent。
                    【g】繼續運行,提示【選擇事件模型:1、Manual(手動模式) 2、Auto(自動模式) 3、Exit(退出)】,此次,我們輸入 auto,控制檯程序輸出“are 默認爲 false,即等待狀態,請查看!”字樣。
                    我們在託管堆上查找一下 AutoResetEvent 對象,執行命令【!DumpHeap -type AutoResetEvent】。

1 0:000> !DumpHeap -type AutoResetEvent
2          Address               MT           Size
3     012b87014318     7ff8da3e5f58             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ff8da3e5f58     1        24 System.Threading.AutoResetEvent
8 Total 1 objects, 24 bytes

                    AutoResetEvent 對象的地址是 012b87014318,我們直接使用【!do】或者【!DumpObj】命令查看對象詳情。

 1 0:000> !do 012b87014318 
 2 Name:        System.Threading.AutoResetEvent
 3 MethodTable: 00007ff8da3e5f58
 4 EEClass:     00007ff8da3d3638
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da3e3318  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000012b87014330 _waitHandle
11 00007ff8da2870a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    _waitHandle 是應用類型的實例變量,我們繼續使用【!do 0000012b87014330】命令查看該類型的詳情。

 1 0:000> !do 0000012b87014330
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ff8da3e3318
 4 EEClass:     00007ff8da3d2968
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff8da2870a0  400126e        8        System.IntPtr  1 instance 00000000000002A4 handle
11 00007ff8da211188  400126f       10         System.Int32  1 instance                4 _state
12 00007ff8da1dd070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ff8da1dd070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    SafeWaitHandle 類型內部又包含了一個 handle 類型對象,值是 00000000000002A4,針對這個值我們可以使用【!dumpvc】查看,也可以使用【!handle】命令查看。

 1 0:000> !handle 00000000000002A4 f
 2 Handle 2a4
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     32769
10   Name             <none>
11   Object Specific Information
12     Event Type Auto Reset(AutoResetEvent)
13     Event is Waiting(False 就是等待)

                    【g】繼續運行,控制檯程序輸出“are 默認爲 true,即放行狀態,請查看!”字樣,再次執行【!handle 00000000000002A4 f】命令。

 1 0:000> !handle 00000000000002A4 f
 2 Handle 2a4
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65536
10   Name             <none>
11   Object Specific Information
12     Event Type Auto Reset
13     Event is Set

                    【g】繼續運行,控制檯程序輸出“are Reset 後爲 false,即等待狀態,請查看!”字樣,再次執行【!handle 00000000000002A4 f】命令。

 1 0:000> !handle 00000000000002A4 f
 2 Handle 2a4
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65535
10   Name             <none>
11   Object Specific Information
12     Event Type Auto Reset
13     Event is Waiting
                    我們都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底層的功能,說白了就是 C# 只是使用了 Windows 內核提供的事件,C# 不過是對其進行了包裝,如果你想要查看內存地址,必須到內核態去看。
                    我們有了句柄的值了 00000000000002A4,我們需要藉助【Process Explorer】工具找到句柄的內核態地址。打開這個工具,然後在【Filter by name】輸入項目名稱 ExampleCore_6_2,結果如圖:

                    

                    我們在【ProcessExplorer】工具下面【Handles】選項中找到我的事件對象,然後雙擊打開屬性對話框,如圖:
                    

                    我們就找到了內核地址了。打開一個 Windbg,點擊【File】-->【Attach to Kernel】,右側選擇【local】,點擊【ok】進入調試器界面。使用【dp 0xFFFF940C4DC47A60】命令。當前值:0(00000000),控制檯程序輸出“are 默認爲 false,即等待狀態,請查看!

1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60  00000000`00060001

                    切換到用戶態 Windbg 繼續【g】運行,控制檯程序輸出“are 默認爲 true,即放行狀態,請查看!”字樣。回到內核態 Windbg 繼續運行【dp 0xFFFF940C4DC47A60】命令。

1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60  00000001`00060001

                    然後,我們再【g】一下【用戶態】的 Windbg,控制檯輸出“are Reset後爲 false,即等待狀態,請查看!”字樣,當前值:0(00000000),然後切換到【內核態】的Windbg,繼續使用【dp】命令,查看一下。

1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60  00000000`00060001

                    我們就看到了,狀態是0和1相互切換的。

        4.2.2、互斥體(內核鎖)
            A、基礎知識
                互斥體(Mutex)是一個內核態的同步結構,即可以用於對某個進程內的線程進行同步,也可以在多個進程之間進行同步(通過在創建互斥體時指定名稱)。通常來說,如果所有同步操作都位於同一個進程內,那麼應該使用監視器對象(Monitor/Lock)或者其他的用戶態同步原語。而另一方面,如果需要在多個進程之間進行同步,最合適的就是使用命名互斥體了。
                由於互斥體是一種內核態結構,因此,用戶態代碼需要 System.Threading.Mutex 來訪問互斥體。
                當在用戶態中進行調試時,可以使用【!do】或者【!DumpObj】命令來獲取關於互斥體更多詳細的信息。

                在內核態的數據的 0 表示擁有鎖,1 表示釋放鎖。

                Mutex 類型內部包含了 SafeWaitHandle 引用類型的一個字段 _waitHandle,_waitHandle 類型內部包含了一個值類型的(System.IntPtr)的 handle 實現的同步操作。

            B、眼見爲實
                調試源碼:ExampleCore_6_3
                調試任務:分別在用戶態和內核態兩中情況下 Mutex 值的變化。
                由於我們需要在用戶態和內核態查看同步對象具體值的變化,需要開啓兩種調試器,一種是內核態的調試器,一種是用戶態的調試器。
                1)、KD 和 NTSD 調試
                    編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_3\bin\Debug\net8.0\ExampleCore_6_3.exe】,打開調試器。
                    【g】開始運行我們的調試器,直到調試器輸出如圖,並進入中斷模式,就可以開始我們的調試了。效果如圖:
                    

                    我們現在託管堆上查找一下 Mutex 對象,執行【!DumpHeap -type Mutex】命令。

1 0:000> !DumpHeap -type Mutex
2          Address               MT     Size
3 0000013097009628 00007ffef219a190       24
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ffef219a190        1           24 System.Threading.Mutex
8 Total 1 objects

                    紅色標註的就是 Mutex 對象的地址 0000013097009628,針對該地址執行【!do 0000013097009628】命令查看詳情。

 1 0:000> !do 0000013097009628
 2 Name:        System.Threading.Mutex
 3 MethodTable: 00007ffef219a190
 4 EEClass:     00007ffef21a2ef8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffef219ee70  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000013097009780 _waitHandle
11 00007ffef20c70a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    我們看到了Mutex 類型的內部包含了 SafeWaitHandle 類型的對象 _waitHandle,地址是 0000013097009780,針對該地址繼續執行【!do 0000013097009780】命令查看其詳情。

 1 0:000> !do 0000013097009780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffef219ee70
 4 EEClass:     00007ffef21a59e8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffef20c70a0  400126e        8        System.IntPtr  1 instance 0000000000000290 handle
11 00007ffef2051188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffef201d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffef201d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    SafeWaitHandle 類型的內部包含了句柄對象 handle,它的值是 0000000000000290,針對該值執行【!handle 0000000000000290 f】命令查看句柄的詳情。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type          Mutant
 4   Attributes    0
 5   GrantedAccess 0x1f0001:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState
 8   HandleCount   2
 9   PointerCount  65536
10   Name          <none>
11   Object Specific Information
12     Mutex is Owned(說明已經獲取了鎖)
13     Mutant Owner b24.de4(這是擁有鎖的線程 OSID de4)

                    我們可以使用【!t】命令驗證這一點。

 1 0:000> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 2
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      de4 0000013092951570    2a020 Preemptive  0000013097009EF0:000001309700A610 0000013092992E10 -00001 MTA
11    6    2     23f0 00000130943ADDA0    21220 Preemptive  0000000000000000:0000000000000000 0000013092992E10 -00001 Ukn (Finalizer)
12    7    3     36dc 000001309295D370    2b220 Preemptive  0000000000000000:0000000000000000 0000013092992E10 -00001 MTA
13 0:000>

                    關係如圖:
                    

                    我們看到了用戶態下 Mutex 值的變化,也需要看看內核態上數據的變化,因此,我們需要藉助【Process Explorer】工具。
                    具體操作如圖:
                    

                    我們需要雙擊【ProcessExplorer】下方的【Handles】標紅的數據項,打開 Mutex 屬性對話框,就能找到內核地址了。
                    

                    在內核態的地址是 0xFFFFD2824D881CD0,有了地址,我們需要打開【KD】內核調試器,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,數據命令【kd -kl】打開調試器,直接執行命令【dp 0xFFFFD2824D881CD0 l1】。

1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0  00000000`00000002

                    Mutex 有了鎖,內核數據的值是 00000000。我們需要切換到【NTSD】用戶態調試器,繼續【g】執行,直到調試器自動進入中斷模式。輸出如圖:
                    

                    說明此時已經釋放了鎖,再次執行【!handle 0000000000000290 f】查看句柄的變化。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type          Mutant
 4   Attributes    0
 5   GrantedAccess 0x1f0001:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState
 8   HandleCount   2
 9   PointerCount  65534
10   Name          <none>
11   Object Specific Information
12     Mutex is Free(現在已經釋放鎖了)

                    同樣,我們切換到內核【kd】調試器,執行命令【dp 0xFFFFD2824D881CD0 l1】,查看結果。

1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0  00000001`00000002

                    內核態的數據的值現在是 1 了,說明 Mutex 已經釋放了鎖。


                2)、Windbg Preview 調試
                    編譯項目,打開【Windbg Preview】調試器,依次點擊【文件】---【Launch executable】,加載我們的調試項目:ExampleCore_6_3.exe,進入到調試器。
                    直接使用【g】命令運行調試器,直到我們的控制檯程序輸出“已進入保護區”字樣,調試器也進入了中斷模式。
                    我們先在堆上查找一下 Mutex 對象,執行【!DumpHeap -type Mutex】命令。
1 0:000> !DumpHeap -type Mutex
2          Address               MT           Size
3     020ea5409628     7ffecdada190             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffecdada190     1        24 System.Threading.Mutex
8 Total 1 objects, 24 bytes

                    紅色標註的 020ea5409628 數據就是 Mutex 對象的地址,然後,執行命令【!do 020ea5409628】,查看 Mutex 詳情。

 1 0:000> !do 020ea5409628
 2 Name:        System.Threading.Mutex
 3 MethodTable: 00007ffecdada190
 4 EEClass:     00007ffecdae2ef8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffecdadee70  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000020ea5409780 _waitHandle
11 00007ffecda070a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    我們知道了 Mutex 內部還包含了一個 SafeWaitHandle 類型的 _waitHandle,這個類型是引用類型,我們繼續【!do 0000020ea5409780】命令,查看這句柄類型的信息。

 1 0:000> !do 0000020ea5409780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffecdadee70
 4 EEClass:     00007ffecdae59e8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffecda070a0  400126e        8        System.IntPtr  1 instance 00000000000002A0 handle
11 00007ffecd991188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffecd95d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffecd95d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    在 _waitHandle 類型的裏面包含了一個值類型的 handle 句柄類型,它的值是 00000000000002A0。有了句柄的值,我們可以使用【!DumpVC 00007ffecda070a0 00000000000002A0】命令查看明細,也可以直接使用【!handle 00000000000002A0 f】命令查看。

 1 0:000> !DumpVC 00007ffecda070a0 00000000000002A0
 2 Name:        System.IntPtr
 3 MethodTable: 00007ffecda070a0
 4 EEClass:     00007ffecd9e6100
 5 Size:        24(0x18) bytes
 6 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 7 Fields:
 8               MT    Field   Offset                 Type VT     Attr            Value Name
 9 00007ffecda070a0  4000525        0        System.IntPtr  1 instance  _value
10 00007ffecda070a0  4000526      a78        System.IntPtr  1   static 0000000000000000 Zero
11 
12 0:000> !handle 00000000000002A0 f
13 Handle 2a0
14   Type             Mutant
15   Attributes       0
16   GrantedAccess    0x1f0001:
17          Delete,ReadControl,WriteDac,WriteOwner,Synch
18          QueryState
19   HandleCount      2
20   PointerCount     65536
21   Name             <none>
22   Object Specific Information
23     Mutex is Owned(進入鎖狀態)
24     Mutant Owner 3438.3b78(持有 Mutex 線程的 ID 3b78)

                    我們可以使用【!t】命令,證明一下。

 1 0:000> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 2
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     3b78 0000020EA0D37F10    2a020 Preemptive  0000020EA5409EF0:0000020EA540A610 0000020ea0d79770 -00001 MTA 
11    5    2     4210 0000020EA0DE7C40    21220 Preemptive  0000000000000000:0000000000000000 0000020ea0d79770 -00001 Ukn (Finalizer) 
12    6    3     1ef0 0000020EA0D43DC0    2b220 Preemptive  0000000000000000:0000000000000000 0000020ea0d79770 -00001 MTA 

                    效果如圖:
                    

                    此時,我們可以使用【Process Explorer】工具查找一下 Mutex 對象在內核態上的地址,看看內核態地址上的內容的變化。我們打開【Process Explorer】,如圖操作:
                    

                    我們點擊【ProcessExplorer】工具【Handles】選項,雙擊 Mutant 打開屬性對話框。效果如圖:
                    

                    我們找到了內核中的數據的地址 0xFFFFD2824D1A5BB0,此時,我們需要再重新打開另外一個【Windbg Preview】,依次點擊【文件】---【Attach to kernel】,在右側選擇【local】,進入到調試器。
                    繼續執行命令【dp 0xFFFFD2824D1A5BB0 l1】命令,看看內核數據是怎麼表示的。

1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0  00000000`00000002

                    此時,我們再次切換到用戶態的【Windbg Preview】,【g】繼續運行調試器,控制檯程序會輸出“正在離開保護區”的字樣。我們繼續執行【!handle 00000000000002A0 f】命令,看看是什麼結果。

 1 0:000> !handle 00000000000002A0 f
 2 Handle 2a0
 3   Type             Mutant
 4   Attributes       0
 5   GrantedAccess    0x1f0001:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState
 8   HandleCount      2
 9   PointerCount     65534
10   Name             <none>
11   Object Specific Information
12     Mutex is Free(已經釋放了鎖)

                    已經執行了 ReleaseMutex 方法了,所以就是釋放了鎖了。
                    此時,我們再次切換到內核態的【Windbg Preview】,繼續執行【dp 0xFFFFD2824D1A5BB0 l1】命令,結果如下:

1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0  00000001`00000002

                    此時,內核態的數據已經變成 1 了。也就是說在內核態的數據的 0 表示擁有鎖,1 表示釋放鎖。


        4.2.3、信號量(內核鎖)
            A、基礎知識
                Semaphore(信號量)是一種內核態的同步對象,可以在用戶態訪問。它類似 Mutex(互斥體),可以實現對資源的互斥訪問。它們的區別在於,信號量採用了資源計數,因此可以同時允許 X 個線程訪問這個資源。
                AutoResetEvent、ManulResetEvent 維護的是 bool 類型的值,信號量本質上就是維護了一個 int 值,這就是兩者的區別,我們可以使用 Windbg 來查看一下 waitHandle 的值,可以發現 Semaphore 的 Count 的值在不斷的變化。
                Semaphore(信號量)可以使用【!do】或者【!DumpObj】命令查看對象信息,也可以使用【!handle】命令查看句柄的信息。
  
                Semaphore 類型內部包含了 SafeWaitHandle 引用類型的一個字段 _waitHandle,_waitHandle 類型內部包含了一個值類型的(System.IntPtr)的 handle 實現的同步操作。

            B、眼見爲實
                調試源碼:ExampleCore_6_4
                調試任務:分別在用戶態和內核態看 Semaphore 值的變化。
                1)、KD 和 NTSD 調試
                    編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_4\bin\Debug\net8.0\ExampleCore_6_4.exe】,打開調試器。
                    進入調試器後,就可以執行【g】命令運行調試器,直到調試器輸出如圖就可以開始調試了。
                    

                    我們現在託管堆上查找一下Semaphore 對象,直接執行【!DumpHeap -type Semaphore】命令。

1 0:000> !DumpHeap -type Semaphore
2          Address               MT     Size
3 000002754fc09628 00007ffa1ed0a198       24
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ffa1ed0a198        1           24 System.Threading.Semaphore
8 Total 1 objects

                    我們知道了 Semaphore 對象的地址是 000002754fc09628,然後執行【!do 000002754fc09628】命令。

 1 0:000> !do 000002754fc09628
 2 Name:        System.Threading.Semaphore
 3 MethodTable: 00007ffa1ed0a198
 4 EEClass:     00007ffa1ed12ea8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa1ed31148  4000b7a        8 ...es.SafeWaitHandle  0 instance 000002754fc09780 _waitHandle
11 00007ffa1ec370a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    System.Threading.Semaphore 類型內部包含了一個 SafeWaitHandle 類型的域 _waitHandle,該 _waitHandle 類型的地址是 000002754fc09780,我們有了地址,繼續執行【!do 000002754fc09780】命令查看它的詳情。

 1 0:000> !do 000002754fc09780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffa1ed31148
 4 EEClass:     00007ffa1ed16bb8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa1ec370a0  400126e        8        System.IntPtr  1 instance 0000000000000290 handle
11 00007ffa1ebc1188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffa1eb8d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffa1eb8d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    Microsoft.Win32.SafeHandles.SafeWaitHandle 類型內部包含了 System.IntPtr 類型一個域 handle,它的值是 0000000000000290,有了這個值,我們就可以使用【!handle 0000000000000290 f】命令查看句柄的詳情了。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type          Semaphore
 4   Attributes    0
 5   GrantedAccess 0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount   2
 9   PointerCount  65536
10   Name          <none>
11   Object Specific Information
12     Semaphore Count 2(當前計數是2,每次執行都會累加)
13     Semaphore Limit 10(這是最大值,超過就會拋出異常)

                    內容很簡單,就不做過多解釋了。這個句柄的值  0000000000000290 要記住,後面找內核地址要使用這個。
                    我們想要找到句柄的內核地址,必須 藉助【ProcessExplorer】工具,操作如圖:
                    

                    雙擊【ProcessExloprer】下方【Handles】的 Semaphore 記錄,打開詳情,內核地址就在裏面。
                    

                    handle 句柄的內核地址是 0xFFFFA68F9E3CE2E0,有了地址,我們就可以使用【kd】內核調試器顯示數據內容了。
                    打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【kd -kl】打開調試器,執行命令【!dp 0xFFFFA68F9E3CE2E0 l4】。效果如圖:
                    

                    我們再次切換到用戶態的【NTSD】調試器中,執行【g】命令和【!handle 0000000000000290 f】,查看變化。

 1 0:000> g
 2 查看當前的 sem 值。
 3 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
 4 KERNELBASE!wil::details::DebugBreak+0x2:
 5 00007ffb`4129b502 cc              int     3
 6 
 7 0:000> !handle 0000000000000290 f
 8 Handle 290
 9   Type          Semaphore
10   Attributes    0
11   GrantedAccess 0x1f0003:
12          Delete,ReadControl,WriteDac,WriteOwner,Synch
13          QueryState,ModifyState
14   HandleCount   2
15   PointerCount  65534
16   Name          <none>
17   Object Specific Information
18     Semaphore Count 3(第一次執行是2,現在是 3,每次執行都會遞增)
19     Semaphore Limit 10(最大值)

                    我們再切換到內核態【kd】調試器上,執行【dp 0xFFFFA68F9E3CE2E0 l4】命令。

1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0  00000003`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0  ffffa68f`9e3ce2e8 00000000`0000000a

                    數值已經變爲爲 3 了,和用戶態調試器輸出是一致的。我們可以重複多次,每次查看變化,很簡單,我就省略了。
                    我在用戶態下執行執行到計數數字 10,然後在執行,看看會不會發生異常。

 1 0:000> g
 2 查看當前的 sem 值。
 3 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
 4 KERNELBASE!wil::details::DebugBreak+0x2:
 5 00007ffb`4129b502 cc              int     3
 6 
 7 0:000> g
 8 查看當前的 sem 值。
 9 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
10 KERNELBASE!wil::details::DebugBreak+0x2:
11 00007ffb`4129b502 cc              int     3
12 
13 0:000> g
14 查看當前的 sem 值。
15 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
16 KERNELBASE!wil::details::DebugBreak+0x2:
17 00007ffb`4129b502 cc              int     3
18 
19 0:000> g
20 查看當前的 sem 值。
21 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
22 KERNELBASE!wil::details::DebugBreak+0x2:
23 00007ffb`4129b502 cc              int     3
24 
25 0:000> g
26 查看當前的 sem 值。
27 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
28 KERNELBASE!wil::details::DebugBreak+0x2:
29 00007ffb`4129b502 cc              int     3
30 
31 0:000> g
32 查看當前的 sem 值。
33 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
34 KERNELBASE!wil::details::DebugBreak+0x2:
35 00007ffb`4129b502 cc              int     3
36 
37 0:000> g
38 查看當前的 sem 值。
39 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
40 KERNELBASE!wil::details::DebugBreak+0x2:
41 00007ffb`4129b502 cc              int     3
42 
43 0:000> !handle 0000000000000290 f
44 Handle 290
45   Type          Semaphore
46   Attributes    0
47   GrantedAccess 0x1f0003:
48          Delete,ReadControl,WriteDac,WriteOwner,Synch
49          QueryState,ModifyState
50   HandleCount   2
51   PointerCount  65527
52   Name          <none>
53   Object Specific Information
54     Semaphore Count 10
55     Semaphore Limit 10

                    我們在看看內核態數據的變化,切換到【kd】調試器上,執行命令【dp 0xFFFFA68F9E3CE2E0 l4】。

1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0  0000000a`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0  ffffa68f`9e3ce2e8 00000000`0000000a

                    我們看到內核態的值已經變成 0000000a 了。
                    我們回到用戶態的【NTSD】調試器,繼續【g】,看看會發生什麼。

1 0:000> g
2 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000   C:\Windows\SYSTEM32\icu.dll
3 (23a8.1c70): CLR exception - code e0434352 (first chance)
4 (23a8.1c70): CLR exception - code e0434352 (!!! second chance !!!)
5 KERNELBASE!RaiseException+0x69:
6 00007ffb`411dcf19 0f1f440000      nop     dword ptr [rax+rax]

                    我們看到發生了 CLR exception 異常了,和我們期望的一樣。


                2)、Windbg Preview 調試
                    編譯項目,打開【Windbg Preview】,依次點擊【文件】----【Launch executable】,加載我們的項目文件 ExampleCore_6_4.exe,直接進入調試器。
                    進入到調試器後,【g】直接運行調試器,我們的控制檯程序會輸出“查看當前的 sem 值。”字樣,調試器會自動進入中斷模式,此時,就可以開始我們的調試了。
                    我們先在託管堆上查找一下 Semaphore 對象是否存在,執行命令【!DumpHeap -type Semaphore】。
1 0:000> !DumpHeap -type Semaphore
2          Address               MT           Size
3     027685409628     7ffa06f8a198             24 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffa06f8a198     1        24 System.Threading.Semaphore
8 Total 1 objects, 24 bytes

                    我們找到了 Semaphore 對象的地址,有了地址就好辦了,我們直接執行【!do 027685409628】命令,查看它的詳情。

 1 0:000> !do 027685409628
 2 Name:        System.Threading.Semaphore
 3 MethodTable: 00007ffa06f8a198
 4 EEClass:     00007ffa06f92ea8
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa06fb1148  4000b7a        8 ...es.SafeWaitHandle  0 instance 0000027685409780 _waitHandle
11 00007ffa06eb70a0  4000b79      b28        System.IntPtr  1   static 0000000000000000 InvalidHandle
12 0000000000000000  4000b7b       20              SZARRAY  0 TLstatic  t_safeWaitHandlesForRent
13     >> Thread:Value <<

                    System.Threading.Semaphore 內部包含了一個 SafeWaitHandle 類型的 _waitHandle 域,針對該域我們使用【!do 0000027685409780】命令,查看 _waitHandle 的詳情。

 1 0:000> !do 0000027685409780
 2 Name:        Microsoft.Win32.SafeHandles.SafeWaitHandle
 3 MethodTable: 00007ffa06fb1148
 4 EEClass:     00007ffa06f96bb8
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa06eb70a0  400126e        8        System.IntPtr  1 instance 0000000000000290 handle
11 00007ffa06e41188  400126f       10         System.Int32  1 instance                4 _state
12 00007ffa06e0d070  4001270       14       System.Boolean  1 instance                1 _ownsHandle
13 00007ffa06e0d070  4001271       15       System.Boolean  1 instance                1 _fullyInitialized

                    Microsoft.Win32.SafeHandles.SafeWaitHandle 內部包含了一個 System.IntPtr 類型的域 handle。我們有了 handle 的值 0000000000000290,就可以使用命令【!handle 0000000000000290 f】查看這個句柄的詳情了。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type             Semaphore
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65536
10   Name             <none>
11   Object Specific Information
12     Semaphore Count 2(當前的計數,初始值我們設置的是 1)
13     Semaphore Limit 10(這個是極限值,超過會拋出異常)

                    這些都是在用戶態調試器下的顯示,我們也要看看在內核態下是怎麼顯示的,記住 handle 的值,後面會用到。
                    我們想要在內核態想查看數據的變化,必須找到句柄的內核態地址,所以我們要藉助【ProcessExplorer】工具,操作如圖:
                    

                    我們在【ProcessExplorer】下方的【Handles】找到 Semaphore 信號量對象,繼續雙擊就可以看到它的內核態的地址。
                    

                    很簡單,就不多說了,我們知道了它的內核地址 0xFFFFA68F9E3E1CE0。此時,我們需要在打開一個【Windbg Preview】,依次點擊【文件】----【Attach to kernel】,在窗口的右側選擇【local】,點擊【ok】進去調試器,就可以使用【dp 0xFFFFA68F9E3E1CE0 l4】命令查看數據了。

1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0  00000002`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0  ffffa68f`9e3e1ce8 00000000`0000000a

                    00000002 就是當前值,00000000`0000000a 就是極限值。
                    接下來就簡單了,我們多次執行用戶態的調試器,然後再在內核態調試器裏查看變化,一目瞭然。
                    我先執行一次用戶態下【g】命令,在執行【!handle 0000000000000290 f】命令,查看變化。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type             Semaphore
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65534
10   Name             <none>
11   Object Specific Information
12     Semaphore Count 3(上一次是2,此次是3)
13     Semaphore Limit 10

                    我們在切換到內核態調試器中執行【dp 0xFFFFA68F9E3E1CE0 l4】命令。

1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0  00000003`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0  ffffa68f`9e3e1ce8 00000000`0000000a

                    00000003 變爲 3了。
                    我們可以繼續連續執行同樣的命令,查看結果。
                    當我在用戶態執行的時候,噹噹前計數大於10的時候,會發生異常。

 1 0:000> !handle 0000000000000290 f
 2 Handle 290
 3   Type             Semaphore
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65527
10   Name             <none>
11   Object Specific Information
12     Semaphore Count 10
13     Semaphore Limit 10
14 
15 0:000> g
16 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000   C:\Windows\SYSTEM32\icu.dll
17 (3a8c.940): CLR exception - code e0434352 (first chance)
18 (3a8c.940): CLR exception - code e0434352 (!!! second chance !!!)
19 KERNELBASE!RaiseException+0x69:
20 00007ffb`411dcf19 0f1f440000      nop     dword ptr [rax+rax]

                    我們在看看內核態的數據,繼續執行命令。

1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0  0000000a`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0  ffffa68f`9e3e1ce8 00000000`0000000a

                    當前的計數值就是 10(十六進制 0xa) 了。


        4.2.4、監視器(混合鎖)
            A、基礎知識
                監視器是一種對某個對象的訪問操作進行監視的結構,它能在對象上創建一個鎖,因而只有當持有該監視器對象的線程離開監視器對象後,其他線程才能訪問。
                監視器和其他同步原語不同,它不是對內核 Windows 同步原語進行是簡單的封裝,而是在 .NET 中定義的類,即:System.Threading.Monitor,Monitor 類不能實例化,而是包含了一組靜態方法,用於獲取一個鎖。Enter 和 Exit 是很常用的兩個方法,Enter 用於獲取指定對象上的互斥鎖,Exit 用於指定對象上的互斥鎖。
                lock 關鍵字就是對 Monitor 對象的封裝,lock 語句會自動進入一個監視器,並將保護區域內的代碼封裝在一個 try/finally 塊中,以確保監視器在作用域結束後釋放鎖。
                由於 Monitor 類是一個不能被實例化的對象,因此無法看到它的任何狀態,鎖的信息保存在被鎖定的對象中。
                監視器是由 C# 中的 AwareLock 實現的,底層是基於 AutoResetEvent 機制,可以參見 coreclr 源碼。因爲 Monitor 是基於對象頭的同步塊索引來實現的,我們可以查看對象頭的數據結構就可以明白了。
                
            B、眼見爲實
                調試源碼:ExampleCore_6_5
                調試任務:我們使用 Windbg 查看 Monitor 的實現
                1)、NTSD 調試
                    編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_5\bin\Debug\net8.0\ExampleCore_6_5.exe】打開調試器。【g】直接運行調試器,調試器會輸出“4 已進入 Person 鎖中 111111”字樣,自動進入中斷模式,現在,就可以開始我們的調試了。如圖:
                    
                    因爲我們知道是鎖的問題,所以可以直接執行【!syncblk】命令。
1 0:008> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     2 0000015A8405C070            3         1 0000015A8404A830 38f0   8   00000119f200c9f8 ExampleCore_6_5.Person
4 -----------------------------
5 Total           3
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    我們說過 Monitor 的底層實現就是 AwareLock,這個標紅 0000015A8405C070 地址就是指向  AwareLock。我們使用【dt coreclr!AwareLock 0000015A8405C070】命令查看一番。

 1 0:008> dt coreclr!AwareLock 0000015A8405C070
 2    +0x000 m_lockState      : AwareLock::LockState(這裏就說明了 Monitor 底層是 AwareLock)
 3    +0x004 m_Recursion      : 1
 4    +0x008 m_HoldingThread  : 0x0000015a`8404a830 Thread(持有鎖的託管線程標識,和 !synck 輸出  Owning Thread Info 列的前部分一致)
 5    +0x010 m_HoldingOSThreadId : 0x38f0(持有鎖的操作系統線程標識,和 !synck 輸出  Owning Thread Info 列的後部分一致
 6    +0x018 m_TransientPrecious : 0n1
 7    +0x01c m_dwSyncIndex    : 0x80000002(同步塊的索引值,和 !synck 輸出的 Index 值一樣)
 8    +0x020 m_SemEvent       : CLREvent(這裏說明,底層還是使用了 Event 同步原語,如果在 Windbg 裏是可以點擊的,這裏沒辦法了)
 9    +0x030 m_waiterStarvationStartTimeMs : 0x10c6663
10    +0x034 m_emittedLockCreatedEvent : 0n0

                    我們繼續使用【dx -r1 (*((coreclr!CLREvent *) XXXXXXXXX))】命令查看 m_SemEvent 是什麼。XXXXXXXXX 是 m_SemEvent 的地址,我沒有算出來,下面的步驟就沒辦法進行了。在【Windbg Preview】裏是直接可以點擊查看的,這就是【Windbg】和 命令行工具的區別。


                2)、Windbg Preview 調試
                    編譯項目,打開【Windbg Preview】,依次點擊【文件】----【Launch executable】,加載我們的項目文件 ExampleCore_6_5.exe,進入到調試器。
                    我們使用【g】命令,繼續運行調試器,我們的控制檯程序輸出:6 已進入 Person 鎖中 222222(這裏不一定是這個,我的輸出是這個),Windbg 有一個 int 3 中斷,就可以調試程序了。
                    然後,我們使用【!syncblk】命令,查看一下同步塊。
1 0:009> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     2 00000217A549CE10            3         1 00000217A54963A0 26c   9   000001d713010a28 ExampleCore_6_5.Person
4 -----------------------------
5 Total           2
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    我們說過 Monitor 的底層實現就是 AwareLock,這個標紅 00000217A549CE10 地址就是指向  AwareLock。我們使用【dt coreclr!AwareLock 00000217A549CE10】命令查看一番。

 1 0:009> dt coreclr!AwareLock 00000217A549CE10
 2    +0x000 m_lockState      : AwareLock::LockState(底層的 awarelock)
 3    +0x004 m_Recursion      : 1
 4    +0x008 m_HoldingThread  : 0x00000217`a54963a0 Thread(持有鎖的線程的標識,也就是!syncblk 命令輸出的 Owning Thread Info 列的值前部分(00000217A54963A0) 5    +0x010 m_HoldingOSThreadId : 0x26c(持有鎖的操作系統線程標識也就是!syncblk 命令輸出的 Owning Thread Info 列的值後部分(26c)
 6    +0x018 m_TransientPrecious : 0n1
 7    +0x01c m_dwSyncIndex    : 0x80000002(這個就是同步塊索引,也就是!syncblk 命令輸出的 Index 列的值)
 8    +0x020 m_SemEvent       : CLREvent(底層還是使用的 Event 實現同步)
 9    +0x030 m_waiterStarvationStartTimeMs : 0xf4b013
10    +0x034 m_emittedLockCreatedEvent : 0n0

                    我們繼續使用【dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))】命令查看 m_SemEvent 是什麼,不用執行命令,直接點擊就可以了。

1 0:009> dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))
2 (*((coreclr!CLREvent *)0x217a549ce30))                 [Type: CLREvent]
3     [+0x000] m_handle         : 0x314 [Type: void *](這裏是一個句柄)
4     [+0x008] m_dwFlags        : 0xd [Type: Volatile<unsigned long>]

                    既然是一個 handle,我們就使用【!handle 0x314 f】命令查看一下就知道了。

 1 0:009> !handle 0x314 f
 2 Handle 314
 3   Type             Event
 4   Attributes       0
 5   GrantedAccess    0x1f0003:
 6          Delete,ReadControl,WriteDac,WriteOwner,Synch
 7          QueryState,ModifyState
 8   HandleCount      2
 9   PointerCount     65537
10   Name             <none>
11   Object Specific Information
12     Event Type Auto Reset
13     Event is Waiting

                  我們看到了吧,Monitor 底層也是使用 AutoResetEvent 實現的。


        4.2.5、讀寫鎖(ReaderWriterLock)
            A、基礎知識
                Monitor 類每次只允許一個線程獨佔式的訪問一個對象。雖然,在寫入操作非常頻繁的情況下,Monitor 能工作的很好,但當讀取操作多於寫操作或者在鎖上存在高度競爭的情況下,Monitor 的性能就很受影響了。
                爲了解決這個問題,系統爲我們提供了讀寫鎖,即 ReaderWriterLock 。ReaderWriterLock 能夠使多個線程併發的執行讀操作,而每次只允許一個線程執行寫操作。ReaderWriterLock 類本身就包含了狀態來控制對鎖的訪問。

                注意:
                  .NET Framework 有兩個讀取器-寫入器鎖和 ReaderWriterLockSlim、ReaderWriterLock。 建議對所有新開發的項目使用 ReaderWriterLockSlim。 雖然 ReaderWriterLockSlim 類似於 ReaderWriterLock,但不同之處在於,前者簡化了遞歸規則以及鎖狀態的升級和降級規則。 ReaderWriterLockSlim 避免了許多潛在的死鎖情況。 另外,ReaderWriterLockSlim 的性能顯著優於 ReaderWriterLock。

            B、眼見爲實
                調試源碼:ExampleCore_6_6
                調試任務:使用調試器從底層瞭解 ReaderWriterLock 到底是什麼。
                1)、NTSD 調試
                    編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_6\bin\Debug\net8.0\ExampleCore_6_6.exe】直接進入調試器。
                    直接【g】運行調試器,直到調試器輸出“Press ENTER to exit...”字樣時,按組合鍵【ctrl+c】進入中斷模式,開始調試了。
                    我們現在託管堆上查找一下 ReaderWriterLock 對象,執行【!DumpHeap -type ReaderWriterLock】命令。
1 0:003> !DumpHeap -type ReaderWriterLock
2          Address               MT     Size
3 000001354f409848 00007ff9e50c75e8       56
4 
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ff9e50c75e8        1           56 System.Threading.ReaderWriterLock
8 Total 1 objects

                    標紅的 000001354f409848 就是 ReaderWriterLock 對象的地址,繼續執行【!do 000001354f409848】命令,查看它的詳情。

 1 0:003> !do 000001354f409848
 2 Name:        System.Threading.ReaderWriterLock
 3 MethodTable: 00007ff9e50c75e8
 4 EEClass:     00007ff9e50aa388
 5 Tracked Type: false
 6 Size:        56(0x38) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff9e4f593f0  400001d        8          System.Void  0 instance 0000000000000000 _readerEvent
11 00007ff9e4f593f0  400001e       10          System.Void  0 instance 0000000000000000 _writerEvent
12 00007ff9e4f7a5f0  400001f       18         System.Int64  1 instance                1 _lockID
13 00007ff9e4f51188  4000020       20         System.Int32  1 instance                0 _state
14 00007ff9e4f51188  4000021       24         System.Int32  1 instance               -1 _writerID
15 00007ff9e4f51188  4000022       28         System.Int32  1 instance                1 _writerSeqNum
16 00007ff9e4f767b8  4000023       2c        System.UInt16  1 instance                0 _writerLevel
17 00007ff9e4f51188  400001b       58         System.Int32  1   static              500 DefaultSpinCount
18 00007ff9e4f7a5f0  400001c       50         System.Int64  1   static                1 s_mostRecentLockID

                    _readerEvent_writerEvent 是指針類型,分別用來控制對讀取隊列和寫入隊列的訪問。_state 表示鎖的各種不同的內部狀態。_lockID 持有鎖線程的內部標識。_writerID 持有鎖線程的 ID,_writerLevel 持有寫入線程的遞歸鎖計數(Recursive lock count)。


                2)、Windbg Preview 調試
                    編譯項目,打開【Windbg Preview】調試器,依次點擊【文件】---【Launch executable】,加載我們的項目文件 ExampleCore_6_6.exe,直接進入調試器。執行【g】命令,運行調試器,直到我們的控制檯程序輸出“Press ENTER to exit...”字樣,然後點擊調試器的【break】按鈕,進入中斷狀態,現在開始我們的調試吧。
                    我們現在託管堆上查找一下 ReaderWriterLock 對象,執行【!DumpHeap -type ReaderWriterLock】命令。
1 0:006> !DumpHeap -type ReaderWriterLock
2          Address               MT           Size
3     022afb409848     7ffa021b7788             56 
4 
5 Statistics:
6           MT Count TotalSize Class Name
7 7ffa021b7788     1        56 System.Threading.ReaderWriterLock
8 Total 1 objects, 56 bytes

                    紅色標註的 022afb409848 就是 ReaderWriterLock 對象的地址,有了地址,我們執行【!do 022afb409848】命令。

 1 0:006> !do 022afb409848
 2 Name:        System.Threading.ReaderWriterLock
 3 MethodTable: 00007ffa021b7788
 4 EEClass:     00007ffa0219a4f0
 5 Tracked Type: false
 6 Size:        56(0x38) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ffa020493f0  400001d        8          System.Void  0 instance 0000000000000000 _readerEvent
11 00007ffa020493f0  400001e       10          System.Void  0 instance 0000000000000000 _writerEvent
12 00007ffa0206a5f0  400001f       18         System.Int64  1 instance                1 _lockID
13 00007ffa02041188  4000020       20         System.Int32  1 instance                0 _state
14 00007ffa02041188  4000021       24         System.Int32  1 instance               -1 _writerID
15 00007ffa02041188  4000022       28         System.Int32  1 instance                1 _writerSeqNum
16 00007ffa020667b8  4000023       2c        System.UInt16  1 instance                0 _writerLevel
17 00007ffa02041188  400001b       58         System.Int32  1   static              500 DefaultSpinCount
18 00007ffa0206a5f0  400001c       50         System.Int64  1   static                1 s_mostRecentLockID

                    _readerEvent 和 _writerEvent 是指針類型,分別用來控制對讀取隊列和寫入隊列的訪問。_state 表示鎖的各種不同的內部狀態。_lockID 持有鎖線程的內部標識。_writerID 持有鎖線程的 ID,_writerLevel 持有寫入線程的遞歸鎖計數(Recursive lock count)。


        4.2.6、線程池
            創建新線程的方式很多,比如:Thread、ThreadPool、Task、Parallel 等,除了 Thread 類,其他都是使用了線程池技術,讓 CLR 來高效的管理這個線程池,所以,.NET 開發建議使用具有線程池的類型。每個進程有且只有一個線程池。需要注意一點,當線程被還回線程池時,在線程上設置的任何狀態都會保留下來。如果同一個線程被用於服務另一個任務請求,並且該任務請求與線程狀態不兼容,那麼程序可能會失敗。

    4.3、同步的內部細節
        4.3.1、對象頭
            在託管堆上保存的每個對象都包含一個對象頭,在對象頭中包含了與對象相關的一組信息。在對象頭中可以包含包括散列碼、鎖信息、同步塊索引等。如圖所示:
            
            在對象中需要保存的所有信息總量大於對象頭本身的大小。這句話的意思,任何一個對象都可能需要(也可能不需要)所有的信息,這取決於具體的執行流程。只要在執行操作中需要的信息(例如:對象的散列碼)不超過對象頭的大小,這些信息就會直接保存在對象頭中。如果對象頭中無法保存所需的信息,CLR 會創建一個獨立的同步塊數據結構,並將當前保存在對象頭中的所有信息都複製到這個同步塊中,並且,將對象頭中保存的信息替換成同步塊在同步塊表中的索引。同步塊位於非 GC 的內存中,通過同步塊表中的索引來訪問。

            CLR 通過對象頭中的位元的組織方式區分對象頭中包含的信息的種類。如果在對象頭中設置了掩碼 0x08000000,就表示對象頭中包含要麼是對象的散列碼,要麼是同步塊索引。如果同時設置了掩碼 0x04000000,就表示對象頭中保存的是散列碼。


        4.3.2、同步塊
            A、基礎知識
                這一節主要是驗證對象頭保存數據的方式,例如:如何保存鎖信息,如何保存散列碼等信息。和同步塊相關的有一個命令很重要,就是【!syncblk】,如果該命令不攜帶任何參數,表示它將輸出某個線程中所有對象的同步塊。當然,我們也可以將同步塊的索引值作爲參數,輸出指定同步塊的信息。
                請記住,對象指針指向的是類型句柄域,緊接着纔是實際的對象數據。在類型句柄前的 4 或者 8 個字節也是對象佈局的一部分,其中就包含了對象頭,所以,如果我們想找到對象頭,就要使用對象的地址減去 4 或者 8 個字節(32位減去4字節,4 字節就是 0x4,64位減去8字節,8字節就是 0x8)就是對象頭的數據。

                如果我們想得到同步塊索引,可以執行如下操作:
                1)、通過使用【!ClrStack -a】命令輸出這個線程的所有的調用棧及其所有參數和局部變量。最底層的棧幀對應於 Main 方法。
                2)、繼續使用【!do】命令,確認是否是我們需要的對象。
                3)、最後使用【dp】命令輸出對象頭,它位於對象指針減去 4 或者 8 個字節(32位減去4字節,4 字節就是 0x4,64位減去8字節,8字節就是 0x8)的位置上。

                接下來,我們在說說【!syncblk】命令各列的意思。
                Index:同步塊索引
                SyncBlock:同步塊數據結構的地址(未公開)
                MonitorHeld:持有的監視器的數量
                Recursion:同一個線程獲取這個鎖的次數
                Owning thread info:第一個數據項是指向內部線程數據結構的指針,第二個數據項是操作系統線程ID,第三個數據項是調試器線程ID
                SyncBlock Owner:第一個數據項是指向持有鎖的對象的指針,第二個數據項是鎖所在的對象的類型

            B、眼見爲實
                調試源碼:ExampleCore_6_7
                調試任務:通過調試器瞭解對象頭保存數據的方式。
                1)、NTSD 調試
                    編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.exe】打開調試器。
                    進入調試器後,直接【g】運行調試器,直到調試器輸出如圖:
                    

                    此時,我們按組合鍵【ctrl+c】進入中斷模式,由於我們是手動中斷的,需要執行【~0s】命令將調試器上下文切換到託管線程上下文中。

1 0:009> ~0s
2 ntdll!NtWriteFile+0x14:
3 00007ffd`ece6d0e4 c3              ret

                    繼續執行【!clrstack -a】命令,查看託管線程調用棧和所有參數和變量。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x1c20 (0)
 3         Child SP               IP Call Site
 4 0000003E0397E0E0 00007ffdece6d0e4 [InlinedCallFrame: 0000003e0397e0e0]
 5 0000003E0397E0E0 00007ffdc9b87d6b [InlinedCallFrame: 0000003e0397e0e0]
 6 。。。。。。(省略了)
 7 0000003E0397E800 00007FFCC6E51ABF ExampleCore_6_7.Program.Run()
 8     PARAMETERS:
 9         this (0x0000003E0397E870) = 0x0000020b49409628
10     LOCALS:
11         0x0000003E0397E858 = 0x000000000378734a
12 
13 0000003E0397E870 00007FFCC6E51988 ExampleCore_6_7.Program.Main(System.String[])
14     PARAMETERS:
15         args (0x0000003E0397E8B0) = 0x0000020b49408e90
16     LOCALS:
17         0x0000003E0397E898 = 0x0000020b49409628
18 
19 0:000>

                    0x0000020b49409628 這個就是 Program 對象地址,我們可以使用【!do 0x0000020b49409628】命令,確認一下。

1 0:000> !do 0x0000020b49409628
2 Name:        ExampleCore_6_7.Program
3 MethodTable: 00007ffcc6f00100
4 EEClass:     00007ffcc6eefb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None

                    證明了我們的猜想。我們知道對象的地址指向的是類型句柄,如果想要查看對象頭的數據,還要減去 4 或者 8 個字節纔是對象頭的地址,4 或者 8 是根據系統的位數 32 位就減去 4,64 位就減去 8,從對象的地址也可以看出是該減去 8 還是 4,我的對象地址是 0x0000020b49409628,就要減去 8 了。
                    執行【dp 0x0000020b49409628-0x8 l1】命令,查看對象頭的數據。

1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620  0f78734a`00000000

                    我們看到了對象頭的值是 0f78734a,這個值是可以推出來的。我們知道對象的 HashCode 的值是 58225482,這個數字是十進制的結果值,我們轉換成十六進制,看看是多少。

1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a

                    0378734a 這個值和【dp】命令的結果 0f78734a 類似,我們再使用 58225482 十六進制表示 0378734a,分別加上 0x080000000x04000000,執行命令【? 0378734a++0x08000000+0x04000000】,這個值就是對象頭的值。

1 0:000> ? 0378734a++0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000`0f78734a

                    00000000`0f78734a 這個值和【dp】命令的輸出是一樣的,說明對象頭保存是散列碼了。
                    我們恢復調試器的執行,直到調試器輸出“Press any key to release lock”字樣,點擊【ctrl+c】組合鍵,進入中斷模式。
                    如圖:
                    

                    由於 GC 會執行垃圾回收,內存壓縮和對象地址轉移,我們避免產生誤操作。還是先執行線程切換【~0s】。

1 0:002> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3              ret

                    我們執行【!clrstack -a】命令查看託管線程調用棧,查找我們的Program 對象。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x1e14 (0)
 3         Child SP               IP Call Site
 4 000000EB377AE170 00007ff942b0d0a4 [InlinedCallFrame: 000000eb377ae170]
 5 000000EB377AE170 00007ff91b2076eb [InlinedCallFrame: 000000eb377ae170]
 6 。。。。。。(省略了)
 7 
 8 000000EB377AE4C0 00007FF85B971AEC ExampleCore_6_7.Program.Run()
 9     PARAMETERS:
10         this (0x000000EB377AE530) = 0x0000020c1a409628
11     LOCALS:
12         0x000000EB377AE518 = 0x000000000378734a
13 
14 000000EB377AE530 00007FF85B971988 ExampleCore_6_7.Program.Main(System.String[])
15     PARAMETERS:
16         args (0x000000EB377AE570) = 0x0000020c1a408e90
17     LOCALS:
18         0x000000EB377AE558 = 0x0000020c1a409628

                    0x0000020c1a409628 這個地址就是我們的 Program對象的地址,我們可以使用【!DumpObj 0x0000020c1a409628】命令確認一下。

1 0:000> !DumpObj 0x0000020c1a409628
2 Name:        ExampleCore_6_7.Program
3 MethodTable: 00007ff85ba20100
4 EEClass:     00007ff85ba0fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None

                    我們現在就可以查看對象頭中的內容了。執行命令【dp 0x0000020c1a409628-0x8 l1】,由於我的程序是64位的,所以需要減去 8,32位減去4就可以了。

1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620  08000001`00000000

                    由於內容太多了,需要創建同步塊存儲內容,所以在對象頭中就存儲同步塊的索引了。08000000 表示是同步塊,1 表示同步塊在同步塊表中的索引位置。
                    此時,我們可以使用【!syncblk 0x1】命令查看同步塊的信息了。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     1 0000024F99D534E8            1         1 0000020F030FE480 3e34   0   0000020f07809628 ExampleCore_6_7.Program
4 -----------------------------
5 Total           1
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0


                2)、Windbg Preview 調試
                    編譯項目,打開【Windbg Preview】,依次點擊【文件】----【Launch Excutable】,加載我們的項目文件 ExampleCore_6_7.exe,進入到調試器後,我們使用【g】命令直接運行調試器,直到控制檯程序輸出“Press any key to acquire lock”字樣。我們回到調試器界面,點擊【Break】按鈕,進入中斷模式,開始我們的調試旅程。
                    由於我們手動中斷,所以必須切換到託管線程上下文中,因爲當前在調試器的上下文環境中,執行命令【~0s】切換線程上下文。

1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ffd`ece6d0a4 c3              ret

                    繼續執行【!clrstack -a】命令,查看託管線程調用棧和所有參數。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x1138 (0)
 3         Child SP               IP Call Site
 4 00000026DAD7E8A0 00007ffdece6d0a4 [InlinedCallFrame: 00000026dad7e8a0] 
 5 00000026DAD7E8A0 00007ffd667676eb [InlinedCallFrame: 00000026dad7e8a0] 
 6 。。。。。。(省略無用的)
 7 
 8 00000026DAD7EBF0 00007ffcc0fa1aa0 ExampleCore_6_7.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\Program.cs @ 17]
 9     PARAMETERS:
10         this (0x00000026DAD7EC60) = 0x000001c4c6409628
11     LOCALS:
12         0x00000026DAD7EC48 = 0x000000000378734a
13 
14 00000026DAD7EC60 00007ffcc0fa1988 ExampleCore_6_7.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\Program.cs @ 8]
15     PARAMETERS:
16         args (0x00000026DAD7ECA0) = 0x000001c4c6408e90
17     LOCALS:
18         0x00000026DAD7EC88 = 0x000001c4c6409628

                    紅色標註的地址就是 0x000001c4c6409628 就是 Program 類型對象的地址,我們可以使用【!do 0x000001c4c6409628】命令驗證。

1 0:000> !do 0x000001c4c6409628
2 Name:        ExampleCore_6_7.Program
3 MethodTable: 00007ffcc1050100
4 EEClass:     00007ffcc103fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None

                    繼續使用【dp 0x000001c4c6409628-0x8 l1】命令,查看對象頭的數據。

1 0:000> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620  0f78734a`00000000

                    對象頭的當前值 0f78734a,表示在對象頭中保存的是散列碼,我們控制檯程序散列碼的輸出值是 58225482,這個數字是十進制的,我們轉換爲十六進制,看看結果。

1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a

                    我們看到了十進制的 58225482 轉換爲十六進就是  0378734a,0x08000000 這個掩碼只能確定是不是散列碼,也有可能是同步塊索引,只有在加上一個 0x04000000 掩碼才能確定是散列碼,所以,我們使用執行【? 00000000`0378734a+0x08000000+0x04000000】命令,這個結果就是對象頭的值。

1 0:000> ? 00000000`0378734a+0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000`0f78734a

                    0f78734a 這個值和【dp】命令的輸出是一樣的,說明對象頭保存是散列碼了。
                    我們恢復調試器的執行,直到控制檯程序輸出“Press any key to release lock”字樣,回到調試器,點擊【Break】按鈕,繼續進入中斷模式。如圖:
                    

                    我們繼續執行【dp 0x000001c4c6409628-0x8 l1】命令,看看對象頭的輸出。說明一下,在執行此命令之前,最好執行一次【!clrstack -a】命令獲取對象地址,然後執行【!do】命令確認對象,最後在執行這個【dp】命令,因爲垃圾收集器會在任意時刻移動對象,對象的地址也可能變化。

1 0:001> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620  08000001`00000000

                    08000001 這個結果值就很合理了,就是同步塊索引了。此時,我們可以使用【!syncblk 0x1】命令查看同步塊的信息了。

1 0:001> !syncblk 0x1
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     1 000001C4C1FC6268            1         1 000001C4C1F29FE0 1138   0   000001c4c6409628 ExampleCore_6_7.Program
4 -----------------------------
5 Total           1(同步塊表中同步塊的總數量)
6 CCW             0(COM 可調用包裝的數量)
7 RCW             0(運行時可調用包裝的數量)
8 ComClassFactory 0
9 Free            0(在同步塊表中多少個同步塊)


        4.3.3、瘦鎖
            A、基礎知識
                在 CLR 2.0 中引入了瘦鎖,它實現了一種更高效的機制管理鎖。在使用瘦鎖時,保存在對象頭中唯一的信息就是獲取鎖的線程 ID(既沒有同步塊),它是一個自旋鎖(spinning lock)。因爲要實現一個更爲高效的等待鎖,需要保存更多的信息。然後,這個瘦鎖並不會無限的循環,而是當自旋到某個閾值就會停止。如果超過了這個閾值還不能獲取這個鎖,那麼接下來就會創建一個實際的同步塊,並將相應的信息保存下來來實現一個高效的等待(例如一個事件)。
                CLR 通常採用以下算法來判斷是使用同步塊和瘦鎖。
                I、如果同步塊存在,則使用同步塊存儲鎖信息。
                II、如果同步塊不存在,判斷在當前對象的對象頭中是否可以包含一個瘦鎖。
                如果可以容納,就將線程 ID 保存在對象頭中。如果後面需要保存更多的信息,那麼將自動創建一個同步塊,並把當前對象頭中的內容轉移到新的同步塊中。
                如果不可以容納,就會創建一個新的同步塊,並將對象頭的內容轉移到新的同步塊中,並保存鎖。
                我們可以通過調試器來驗證這個算法,通過以下三步就可以了。
                1】、在獲取鎖之前,將同步塊轉儲出來,驗證其爲空。
                2】、獲取這個鎖,中斷程序執行,並驗證已經創建了一個瘦鎖。
                3】、獲取散列碼,中斷程序執行,並驗證這個瘦鎖已經被一個同步塊替代了。
            
                我們可以使用【!DumpHeap -thinlock】命令找出託管堆上所有帶有瘦鎖的對象。

            B、眼見爲實
                調試源碼:ExampleCore_6_8
                調試任務:驗證瘦鎖存儲的算法。
                1)、NTSD 調試
                    編譯項目,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.exe】打開調試器。
                    進入調試器,【g】直接運行,直到調試器輸出,並暫停,如圖:
                    

                    按【ctrl+c】組合鍵進入中斷模式,還需要切換到託管線程上下文中,執行【~0s】命令,繼續執行【!clrstack -a】命令查找 Program 對象。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x16d4 (0)
 3         Child SP               IP Call Site
 4 000000B881DDE628 00007ff942b0e814 [PrestubMethodFrame: 000000b881dde628] System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean)
 5 。。。。。。(省略了)
 6 000000B881DDE980 00007FF83A191A52 ExampleCore_6_8.Program.Run()
 7     PARAMETERS:
 8         this (0x000000B881DDEA00) = 0x000001c613c09628
 9     LOCALS:
10         0x000000B881DDE9E8 = 0x0000000000000000
11 
12 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
13     PARAMETERS:
14         args (0x000000B881DDEA40) = 0x000001c613c08e90
15     LOCALS:
16         0x000000B881DDEA28 = 0x000001c613c09628

                    0x000001c613c09628 就是 Program 類型對象地址,執行【!do 0x000001c613c09628】命令驗證一下。

1 0:000> !do 0x000001c613c09628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass:     00007ff83a22fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None

                    執行【dp 0x000001c613c09628-8 l1】命令查看對象頭的內容。

1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620  00000000`00000000

                    0 就是表示沒有任何值。繼續【g】恢復調試器的執行,直到調試器輸出,如圖:
                    

                    繼續執行切換線程和查看線程的命令,分別是【~0s】、【!clrstack -a】查找我們的 Program 對象。

 1 0:001> ~0s
 2 ntdll!NtWriteFile+0x14:
 3 00007ff9`42b0d0e4 c3              ret
 4 
 5 0:000> !clrstack -a
 6 OS Thread Id: 0x16d4 (0)
 7         Child SP               IP Call Site
 8 000000B881DDE260 00007ff942b0d0e4 [InlinedCallFrame: 000000b881dde260]
 9 000000B881DDE260 00007ff91e0b7d6b [InlinedCallFrame: 000000b881dde260]
10 。。。。。。(省略了)
11 000000B881DDE980 00007FF83A191A71 ExampleCore_6_8.Program.Run()
12     PARAMETERS:
13         this (0x000000B881DDEA00) = 0x000001c613c09628
14     LOCALS:
15         0x000000B881DDE9E8 = 0x0000000000000000
16 
17 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
18     PARAMETERS:
19         args (0x000000B881DDEA40) = 0x000001c613c08e90
20     LOCALS:
21         0x000000B881DDEA28 = 0x000001c613c09628

                    繼續執行【!do 0x000001c613c09628】命令,查看內容。

 1 0:000> !do 0x000001c613c09628
 2 Name:        ExampleCore_6_8.Program
 3 MethodTable: 00007ff83a240100
 4 EEClass:     00007ff83a22fb48
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
 8 Fields:
 9 None
10 ThinLock owner 1 (000001C60F9C8A80), Recursive 0

                    ThinLock owner 1 (000001C60F9C8A80), Recursive 0 說明對象上有了一個瘦鎖,線程對象的 ID 是 000001C60F9C8A80,遞歸技術是 0。
                    繼續執行【dp 0x000001c613c09628-8 l1】命令,查看對象頭。

1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620  00000001`00000000

 

                    這裏的 1 就是持有鎖的線程 ID,是託管線程的 ID 值。可以使用【!t】或者【!threads】命令驗證。

 1 0:000> !t
 2 ThreadCount:      2
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     16d4 000001C60F9C8A80    2a020 Preemptive  000001C613C13D60:000001C613C14660 000001C60F9C0540 -00001 MTA
11    6    2     2724 000001C60FA11810    21220 Preemptive  0000000000000000:0000000000000000 000001C60F9C0540 -00001 Ukn (Finalizer)

                    【dp】命令和【!t】命令都能找到 000001C60F9C8A80 這個指針的值。
                    我們繼續【g】恢復調試器的執行,直到調試器輸出如圖:
                    

                    此時,說明對象的鎖和散列值都保存了,然後我們【ctrl+c】進入中斷模式,切換線程【~0s】,並且執行【!clrstack -a】命令查找 Program 對象,查一下它的狀態。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x16d4 (0)
 3         Child SP               IP Call Site
 4 000000B881DDE630 00007ff942b0d0a4 [InlinedCallFrame: 000000b881dde630]
 5 000000B881DDE630 00007ff91e0b76eb [InlinedCallFrame: 000000b881dde630]
 6 。。。。。。(省略了)
 7 000000B881DDE980 00007FF83A191B0E ExampleCore_6_8.Program.Run()
 8     PARAMETERS:
 9         this (0x000000B881DDEA00) = 0x000001c613c09628
10     LOCALS:
11         0x000000B881DDE9E8 = 0x000000000378734a
12 
13 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
14     PARAMETERS:
15         args (0x000000B881DDEA40) = 0x000001c613c08e90
16     LOCALS:
17         0x000000B881DDEA28 = 0x000001c613c09628
18 
19 0:000>

                    執行【!do 0x000001c613c09628】命令,查看一下該對象有什麼變化嗎?

1 0:000> !do 0x000001c613c09628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass:     00007ff83a22fb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None(這裏沒有東西了,鎖信息已經轉到同步塊中保存了。)

                    繼續執行【dp 0x000001c613c09628-8 l1】命令,查看一下對象頭保存的數據。

1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620  08000001`00000000

                    08000001 看到這個值就知道是同步塊索引了。我們使用【!syncblk】命令查看同步塊的數據。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3 -----------------------------(這裏是需要有值的,我這裏沒有輸出,原因不知道,重來一次就可以)
4 Total           1
5 CCW             0
6 RCW             0
7 ComClassFactory 0
8 Free            0

                    我們也可以使用【!DumpHeap -thinlock】命令查找託管堆上所有具有瘦鎖的對象。

1 0:000> !DumpHeap -thinlock
2          Address               MT     Size
3 000001c613c12ec0 00007ff83a295820       24 ThinLock owner 1 (000001C60F9C8A80) Recursive 0
4 Found 1 objects.

                    內容很簡單,就不解釋了。


                2)、Windbg Preview 調試
                    編譯項目,打開【Windbg Preview】,依次點擊【文件】---【Launch executable】,加載我們的控制檯項目 ExampleCore_6_8.exe,點擊【打開】進入調試器。
                    進入調試器後,直接執行【g】命令,運行調試器,直到我們的控制檯程序輸出“Press any key to acquire lock”,此時,回到調試器,點擊【Break】按鈕,進入到中斷模式,開始我們的調試。
                    由於我們是手動中斷的,當前是調試器的上下文,需要切換到託管上下文中,需要執行【~0s】命令。

1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3              ret

                    我們使用【!clrstack -a】命令,查看託管線程調用棧,找出我們的 Program 類型的局部變量 program。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x34b8 (0)
 3         Child SP               IP Call Site
 4 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100] 
 5 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100] 
 6 。。。。。。(省略了)
 7 
 8 0000006CE77EE450 00007ff80e731a52 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 14]
 9     PARAMETERS:
10         this (0x0000006CE77EE4D0) = 0x000001ace3409628
11     LOCALS:
12         0x0000006CE77EE4B8 = 0x0000000000000000
13 
14 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
15     PARAMETERS:
16         args (0x0000006CE77EE510) = 0x000001ace3408e90
17     LOCALS:
18         0x0000006CE77EE4F8 = 0x000001ace3409628

                    0x000001ace3409628 就是Program 類型的實例對象的地址,我們可以使用【!do 0x000001ace3409628】來驗證。

1 0:000> !do 0x000001ace3409628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass:     00007ff80e7cfb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None

                    我們執行命令【dp 0x000001ace3409628-8 l1】查看它的對象頭。

1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620  00000000`00000000

                    00000000`00000000 表示沒有任何數據。
                    我們【g】恢復調試器的執行,直到控制檯程序輸出“Press any key to get hashcode”,此時,對象已經獲取了鎖,但是還沒有獲取散列值。回調調試器中,點擊【Break】按鈕,再次進入中斷模式,繼續我們的調試。
                    由於手動進入中斷模式,所以需要有調試器上下文切換到託管線程上下文中,執行命令【~0s】。

1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3              ret

                    繼續執行【!clrstack -a】命令查找 Program 對象。

 1 0:000> !clrstack -a
 2 OS Thread Id: 0x34b8 (0)
 3         Child SP               IP Call Site
 4 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100] 
 5 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100] 
 6 。。。。。。(省略了)
 7 0000006CE77EE450 00007ff80e731a78 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 19]
 8     PARAMETERS:
 9         this (0x0000006CE77EE4D0) = 0x000001ace3409628
10     LOCALS:
11         0x0000006CE77EE4B8 = 0x0000000000000000
12 
13 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
14     PARAMETERS:
15         args (0x0000006CE77EE510) = 0x000001ace3408e90
16     LOCALS:
17         0x0000006CE77EE4F8 = 0x000001ace3409628

                    0x000001ace3409628 這就是我們的 Program 類型實例的地址,可以執行【!do 0x000001ace3409628】命令來驗證,我就省略了。
                    此時,該對象已經獲取鎖了,我們查看對象頭的數據,執行【dp 0x000001ace3409628-8 l1】命令。

1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620  00000001`00000000

                    00000001 這個就是所有者線程的 ID,此時我們可以執行【!do 0x000001ace3409628】或者【!DumpObj 0x000001ace3409628】命令,查看Program 對象,也有體現。

 1 0:000> !do 0x000001ace3409628
 2 Name:        ExampleCore_6_8.Program
 3 MethodTable: 00007ff80e7e0100
 4 EEClass:     00007ff80e7cfb48
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
 8 Fields:
 9 None
10 ThinLock owner 1 (000001ACDEFF2770), Recursive 0

                    紅色標註的告訴我們 Program 對象上獲取了一個瘦鎖,線程對象指針是 000001ACDEFF2770 ,且遞歸計數位0,我們可以使用【!t】或者【!threads】命令來驗證。

 1 0:000> !t
 2 ThreadCount:      2
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       0
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1     34b8 000001ACDEFF2770    2a020 Preemptive  000001ACE3412F38:000001ACE3414660 000001acdf03e270 -00001 MTA 
11    5    2     419c 000001ACDF01B210    21220 Preemptive  0000000000000000:0000000000000000 000001acdf03e270 -00001 Ukn (Finalizer) 

                    我們看到了【!do】命令和【!t】命令的輸出線程ID都是 000001ACDEFF2770,在對象頭中包含了持有鎖的線程 ID。
                    接下來,我們執行代碼,獲取散列碼,再次中斷執行,查看同步塊和瘦鎖的狀態。
                    【g】繼續運行,直到我們的控制檯程序輸出“HashCode:58225482 Press any key to release lock”。此時已經有了鎖,並且也獲取了散列碼。回到調試器,點擊【Break】按鈕,進入中斷模式,繼續調試。
                    繼續切換線程上下文【~0s】,並執行【!clrstack -a】命令查找我們的 Program 對象。

 1 0:001> ~0s
 2 ntdll!NtReadFile+0x14:
 3 00007ff9`42b0d0a4 c3              ret
 4 
 5 0:000> !clrstack -a
 6 OS Thread Id: 0x34b8 (0)
 7         Child SP               IP Call Site
 8 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100] 
 9 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100] 
10 。。。。。。(省略了)
11 0000006CE77EE450 00007ff80e731ae8 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 25]
12     PARAMETERS:
13         this (0x0000006CE77EE4D0) = 0x000001ace3409628
14     LOCALS:
15         0x0000006CE77EE4B8 = 0x000000000378734a
16 
17 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
18     PARAMETERS:
19         args (0x0000006CE77EE510) = 0x000001ace3408e90
20     LOCALS:
21         0x0000006CE77EE4F8 = 0x000001ace3409628

                    執行【!do 0x000001ace3409628】命令,查看 Program 對象。

1 0:000> !do 0x000001ace3409628
2 Name:        ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass:     00007ff80e7cfb48
5 Tracked Type: false
6 Size:        24(0x18) bytes
7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None(這裏沒有任何信息了,已經移到同步塊中了)

                    繼續執行【dp 0x000001ace3409628-8 l1】命令,查看對象頭。

1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620  08000001`00000000

                    08000001 說明現在已經在使用同步塊保存數據了,索引值是 1。
                    我們使用【!syncblk】命令來驗證一下。

1 0:000> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     1 000001ED75B257D8            1         1 000001ACDEFF2770 34b8   0   000001ace3409628 ExampleCore_6_8.Program
4 -----------------------------
5 Total           1
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    當然,我們可以使用【!DumpHeap -thinlock】命令找出託管堆上所有帶有瘦鎖的對象。

1 0:000> !DumpHeap -thinlock
2           Object           Thread               OSId      Recursion
3     01ace3412ec0     01acdeff2770             0x34b8              0

                    很簡單,就不多說了。

    4.4、同步任務
        4.4.1、死鎖
            A、基礎知識
                死鎖:當兩個或者多個線程分別持有一些被保護的資源,並且都拒絕釋放各自的資源而等待另一方釋放資源時,死鎖就產生了。
                這裏會用到一些【k】命令,我就稍作介紹,【k】命令顯示給定線程的堆棧幀以及相關信息,【kp】顯示堆棧跟蹤中調用的每個函數的所有參數。【kb】顯示傳遞給堆棧跟蹤中每個函數的前三個參數。
                如果想學更多的命令,可以去微軟官網:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/k--kb--kc--kd--kp--kp--kv--display-stack-backtrace-

            B、眼見爲實
                調試源碼:ExampleCore_6_9
                調試任務:手動調試線程死鎖的問題。
                1)、NTSD 調試
                    編譯項目,然後直接運行我們的 EXE 可執行程序,直到我們的程序輸出如圖:
                    

                    此時,打開【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,輸入命令【NTSD -pn ExampleCore_6_9.exe】通過進程名稱附加我們的程序,當然,也可以通過進程 id 來附加我們的程序。
                    回車,直接進入調試器,調試器會有一個 int 3 的中斷,就可以開始我們的調試了。
                    已經成功附加進程,截圖效果,不是全部:
                    

                    此時,調試已經處於中斷模式了,效果如圖:
                    

                    我們可以使用【~*e!clrstack】命令,將託管線程和非託管線程的棧回溯都轉儲出來。

 1 0:007> ~*e!clrstack
 2 OS Thread Id: 0x2860 (0)
 3         Child SP               IP Call Site
 4 000000FBA677E2B0 00007ff8a9c8d0a4 [InlinedCallFrame: 000000fba677e2b0]
 5 000000FBA677E2B0 00007ff8961676eb [InlinedCallFrame: 000000fba677e2b0]
 6 000000FBA677E280 00007FF8961676EB Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
 7 000000FBA677E370 00007FF89616C9C0 System.ConsolePal+WindowsConsoleStream.ReadFileNative(IntPtr, System.Span`1<Byte>, Boolean, Int32 ByRef, Boolean)
 8 000000FBA677E3D0 00007FF89616C8BB System.ConsolePal+WindowsConsoleStream.Read(System.Span`1<Byte>)
 9 000000FBA677E410 00007FF89616FB84 System.IO.ConsoleStream.Read(Byte[], Int32, Int32)
10 000000FBA677E480 00007FFFE0CE89F1 System.IO.StreamReader.ReadBuffer()
11 000000FBA677E4D0 00007FFFE0CE90D4 System.IO.StreamReader.ReadLine()
12 000000FBA677E580 00007FF89617005D System.IO.SyncTextReader.ReadLine()
13 000000FBA677E5D0 00007FF896169319 System.Console.ReadLine()
14 000000FBA677E600 00007FFF81B71B08 ExampleCore_6_9.Program.Main(System.String[])
15 OS Thread Id: 0x2e20 (1)
16 Unable to walk the managed stack. The current thread is likely not a
17 managed thread. You can run !threads to get a list of managed threads in
18 the process
19 Failed to start stack walk: 80070057
20 OS Thread Id: 0x2a8c (2)
21 Unable to walk the managed stack. The current thread is likely not a
22 managed thread. You can run !threads to get a list of managed threads in
23 the process
24 Failed to start stack walk: 80070057
25 OS Thread Id: 0x3260 (3)
26         Child SP               IP Call Site
27 000000FBA707F9F0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000fba707f9f0]
28 OS Thread Id: 0x1d7c (4)4號託管線程的調用棧---》執行---》System.Threading.Monitor.ReliableEnter(說明在這裏等待了)
29         Child SP               IP Call Site
30 000000FBA737F098 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba737f098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
31 000000FBA737F1F0 00007FFF81B724DE ExampleCore_6_9.Program+<>c.<Main>b__2_0()(NTSD 沒有顯示源碼行號,Windbg Preview是有的,更容易調試)
32 000000FBA737F340 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
33 000000FBA737F390 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
34 000000FBA737F430 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
35 000000FBA737F4C0 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
36 000000FBA737F810 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba737f810]
37 OS Thread Id: 0x2444 (5)
38         Child SP               IP Call Site
39 000000FBA638F418 00007ff8a9c8db34 [HelperMethodFrame: 000000fba638f418] System.Threading.WaitHandle.WaitOneCore(IntPtr, Int32)
40 000000FBA638F520 00007FFFE0C00C04 System.Threading.WaitHandle.WaitOneNoCheck(Int32)
41 000000FBA638F580 00007FFFE0C18F66 System.Threading.PortableThreadPool+GateThread.GateThreadStart()
42 000000FBA638F910 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba638f910]
43 OS Thread Id: 0x1130 (6)(6號託管線程的調用棧)---》執行--》System.Threading.Monitor.ReliableEnter(說明在這裏等待了,沒有進入)
44         Child SP               IP Call Site
45 000000FBA74FF258 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba74ff258] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
46 000000FBA74FF3B0 00007FFF81B7215E ExampleCore_6_9.Program+<>c.<Main>b__2_1()(源碼的調用位置,NTSD 沒顯示行號,Windbg Preview 是有行號的,更易調試)
47 000000FBA74FF500 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
48 000000FBA74FF550 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
49 000000FBA74FF5F0 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
50 000000FBA74FF680 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
51 000000FBA74FF9D0 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba74ff9d0]
52 OS Thread Id: 0x12d0 (7)
53 Unable to walk the managed stack. The current thread is likely not a
54 managed thread. You can run !threads to get a list of managed threads in
55 the process
56 Failed to start stack walk: 80070057

                    其實,我們從紅色標註的可以看出一些端倪,OS Thread Id: 0x1d7c (4) 4號託管線程執行源碼 ExampleCore_6_9.Program+<>c.<Main>b__2_0() 這個代碼時,調用同步原語 Monitor 的 System.Threading.Monitor.ReliableEnter 方法想進入,卻沒進入,處於等待,因爲後面沒有調用棧了。說明一下,Windbg Preview 是可以顯示源碼行號的,可以直到在哪裏處於等待,但是在 NTSD 是沒有的。

                    OS Thread Id: 0x1130 (6) 的 6 號託管線程執行源碼 ExampleCore_6_9.Program+<>c.<Main>b__2_1() 時調用了 System.Threading.Monitor.ReliableEnter 方法,想獲取鎖,由於後面沒有執行,所以也是出於等待狀態。
                    此時,我們知道他們都是處於等待狀態,雖然輸出的信息很簡單,但是它卻展示了一種常見的死鎖識別技術。
                    這個輸出的信息有點多,其實我們還可以使用另外一個命令,【!syncblk】查看同步快表的數據,也能看出一些信息。

 1 0:007> !syncblk
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     4 000002AAA66FC1D0            3         1 0000026A0FB1CF60 1130   6   0000026a14010a40 ExampleCore_6_9.Student
 4     5 000002AAA66FC228            3         1 0000026A114DE970 1d7c   4   0000026a14010a28 ExampleCore_6_9.Person
 5 -----------------------------
 6 Total           6
 7 CCW             0
 8 RCW             0
 9 ComClassFactory 0
10 Free            0

                    4 號託管線程持有 0000026a14010a28 ExampleCore_6_9.Person 對象,也就是鎖定了該對象,我們的控制檯程序輸出也能說明這一點,輸出是“tid=4,已經進入 Person(1111) 鎖”,結合【~*e!clrstack】命令的輸出,我們知道,4 號線程在執行 Monitor 的 Enter 方法的時候處於等待狀態,我們就可以退出等待的位置在源碼的 17 行,如圖:
                    

                    再用同樣的道理分析,6 號託管線程已經持有 0000026a14010a40 ExampleCore_6_9.Student 對象,說明該對象已經被鎖定了,在結合【~*e!clrstack】命令的輸出,我們知道 6 號線程在執行 Monitor 的 Enter 方法時是處於等待的狀態,我們在結合我們控制檯程序的輸出“tid=6,已經進入 Student(22222) 鎖”,我們可以知道源碼在 32 行處於等待的。如圖:

                    代碼很簡單,所以我們分析也不難。我們可以根據【~*e!clrstack】命令的輸出,分別切換到 4 和 6 號線程上查看一下具體調用棧,也能找出問題。
                    我們先切換到 4 號線程,執行命令【~4s】。

1 0:007> ~4s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3              ret

                    我們繼續執行【!clrstack -a】命令,查看一下調用棧的局部變量,主要觀察 Person 和 Student 。

 1 0:004> !clrstack -a
 2 OS Thread Id: 0x1d7c (4)
 3         Child SP               IP Call Site
 4 000000FBA737F098 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba737f098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
 5 000000FBA737F1F0 00007FFF81B724DE ExampleCore_6_9.Program+<>c.<Main>b__2_0()
 6     PARAMETERS:
 7         this (0x000000FBA737F340) = 0x0000026a14009628
 8     LOCALS:
 9         0x000000FBA737F328 = 0x0000026a14010a28(這個就是我們的 ExampleCore_6_9.Person 對象)
10         0x000000FBA737F320 = 0x0000000000000001
11         0x000000FBA737F2F8 = 0x0000000000000000
12         0x000000FBA737F2F0 = 0x0000026a14010a40(這個就是我們的 ExampleCore_6_9.Student 對象)
13         0x000000FBA737F2E8 = 0x0000000000000000
14 
15 000000FBA737F340 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
16     PARAMETERS:
17         threadPoolThread (0x000000FBA737F390) = 0x0000026a1400aaa0
18         executionContext = <no data>
19         callback = <no data>
20         state = <no data>
21     LOCALS:
22         0x000000FBA737F368 = 0x0000000000000000
23         <no data>
24         <no data>
25         <no data>
26 
27 000000FBA737F390 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
28     PARAMETERS:
29         this (0x000000FBA737F430) = 0x0000026a14009698
30         currentTaskSlot (0x000000FBA737F438) = 0x0000026a1400c6c0
31         threadPoolThread = <no data>
32     LOCALS:
33         0x000000FBA737F3C8 = 0x0000000000000000
34         0x000000FBA737F3C0 = 0x0000026a140098d8
35         <no data>
36         0x000000FBA737F3F4 = 0x0000000000000000
37         <no data>
38         <no data>
39 
40 000000FBA737F430 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
41     LOCALS:
42         <CLR reg> = 0x0000026a14009bb0
43         <CLR reg> = 0x0000026a1400c6f8
44         <no data>
45         <CLR reg> = 0x0000026a1400c8e8
46         <CLR reg> = 0x0000026a1400aaa0
47         <CLR reg> = 0x00000000001b0116
48         <no data>
49         <no data>
50         <no data>
51         <no data>
52         <no data>
53 
54 000000FBA737F4C0 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
55     LOCALS:
56         <CLR reg> = 0x0000026a1400a688
57         <CLR reg> = 0x0000026a1400a908
58         <CLR reg> = 0x0000026a1400a9b0
59         <CLR reg> = 0x0000000000004e20
60         <no data>
61         <no data>
62         <no data>
63         <no data>
64 
65 000000FBA737F810 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba737f810]

                    0x0000026a14010a28 和 0x0000026a14010a40 就是我們的 ExampleCore_6_9.Person 對象和 ExampleCore_6_9.Student 對象,我們可以執行【!do 0x0000026a14010a28】和【!do 0x0000026a14010a40】命令來確認它們。

 1 0:004> !do 0x0000026a14010a28
 2 Name:        ExampleCore_6_9.Person
 3 MethodTable: 00007fff81c73300
 4 EEClass:     00007fff81c3c3e0
 5 Tracked Type: false
 6 Size:        24(0x18) bytes
 7 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
 8 Fields:
 9 None
10 
11 0:004> !do 0x0000026a14010a40
12 Name:        ExampleCore_6_9.Student
13 MethodTable: 00007fff81c73930
14 EEClass:     00007fff81c3c5f8
15 Tracked Type: false
16 Size:        24(0x18) bytes
17 File:        E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
18 Fields:
19 None

                    我們在分別查看一下這兩個對象的對象頭包含了什麼數據,執行命令【dp 0x0000026a14010a28-8 l1】和【dp 0x0000026a14010a40-8 l1】。

1 0:004> dp 0x0000026a14010a28-8 l1
2 0000026a`14010a20  08000005`00000000
3 
4 0:004> dp 0x0000026a14010a40-8 l1
5 0000026a`14010a38  08000004`00000000

                    說明它們都使用了同步塊保存數據和鎖信息了。此時,可以再使用【!syncblk】命令查看同步塊表的數據,上面已經執行,此處省略。
                    以下就簡單了,根據我們的代碼查找問題吧。
                    
                2)、Windbg Preview 調試
                    編譯項目,然後直接運行我們的 EXE 可執行程序,我們的程序輸出如圖:
                    

                    然後,打開【Windbg Preview】,依次點擊【文件】----【Attach to Process】,附加我們的進程,進入調試器,我們先把進程中所有線程轉儲出來看看,執行【~*e!clrstack】命令。

 1 0:007> ~*e!clrstack
 2 OS Thread Id: 0x35e8 (0)
 3         Child SP               IP Call Site
 4 00000035F5D7E7E0 00007ffeddc8d0a4 [InlinedCallFrame: 00000035f5d7e7e0] 
 5 00000035F5D7E7E0 00007ffe22d376eb [InlinedCallFrame: 00000035f5d7e7e0] 
 6 00000035F5D7E7B0 00007ffe22d376eb Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr) [/_/src/libraries/System.Console/src/Microsoft.Interop.LibraryImportGenerator/Microsoft.Interop.LibraryImportGenerator/LibraryImports.g.cs @ 412]
 7 00000035F5D7E8A0 00007ffe22d3c9c0 System.ConsolePal+WindowsConsoleStream.ReadFileNative(IntPtr, System.Span`1, Boolean, Int32 ByRef, Boolean) [/_/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @ 1150]
 8 00000035F5D7E900 00007ffe22d3c8bb System.ConsolePal+WindowsConsoleStream.Read(System.Span`1) [/_/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @ 1108]
 9 00000035F5D7E940 00007ffe22d3fb84 System.IO.ConsoleStream.Read(Byte[], Int32, Int32) [/_/src/libraries/System.Console/src/System/IO/ConsoleStream.cs @ 34]
10 00000035F5D7E9B0 00007ffdff8c89f1 System.IO.StreamReader.ReadBuffer() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 613]
11 00000035F5D7EA00 00007ffdff8c90d4 System.IO.StreamReader.ReadLine() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 802]
12 00000035F5D7EAB0 00007ffe22d4005d System.IO.SyncTextReader.ReadLine() [/_/src/libraries/System.Console/src/System/IO/SyncTextReader.cs @ 77]
13 00000035F5D7EB00 00007ffe22d39319 System.Console.ReadLine() [/_/src/libraries/System.Console/src/System/Console.cs @ 752]
14 00000035F5D7EB30 00007ffda0751b08 ExampleCore_6_9.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 41]
15 OS Thread Id: 0x3188 (1)
16 Unable to walk the managed stack. The current thread is likely not a 
17 managed thread. You can run !clrthreads to get a list of managed threads in
18 the process
19 Failed to start stack walk: 80070057
20 OS Thread Id: 0x40d4 (2)
21 Unable to walk the managed stack. The current thread is likely not a 
22 managed thread. You can run !clrthreads to get a list of managed threads in
23 the process
24 Failed to start stack walk: 80070057
25 OS Thread Id: 0x3bc4 (3)
26         Child SP               IP Call Site
27 00000035F64FFC50 00007ffeddc8db34 [DebuggerU2MCatchHandlerFrame: 00000035f64ffc50] 
28 OS Thread Id: 0x6c (4)
29         Child SP               IP Call Site
30 00000035F67FF098 00007ffeddc8db34 [HelperMethodFrame_1OBJ: 00000035f67ff098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
31 00000035F67FF1F0 00007ffda07528ee ExampleCore_6_9.Program+c.b__2_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 17]
32 00000035F67FF340 00007ffdff7e6532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 264]
33 00000035F67FF390 00007ffdff800698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2349]
34 00000035F67FF430 00007ffdff7ef430 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @ 913]
35 00000035F67FF4C0 00007ffdff7fc203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @ 102]
36 00000035F67FF810 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f67ff810] 
37 OS Thread Id: 0x2a40 (5)
38         Child SP               IP Call Site
39 00000035F598F1B8 00007ffeddc8db34 [HelperMethodFrame: 00000035f598f1b8] System.Threading.WaitHandle.WaitOneCore(IntPtr, Int32)
40 00000035F598F2C0 00007ffdff7e0c04 System.Threading.WaitHandle.WaitOneNoCheck(Int32) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs @ 128]
41 00000035F598F320 00007ffdff7f8f66 System.Threading.PortableThreadPool+GateThread.GateThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.GateThread.cs @ 48]
42 00000035F598F6B0 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f598f6b0] 
43 OS Thread Id: 0x3dd8 (6)
44         Child SP               IP Call Site
45 00000035F697EF68 00007ffeddc8db34 [HelperMethodFrame_1OBJ: 00000035f697ef68] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
46 00000035F697F0C0 00007ffda075256e ExampleCore_6_9.Program+c.b__2_1() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 32]
47 00000035F697F210 00007ffdff7e6532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 264]
48 00000035F697F260 00007ffdff800698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2349]
49 00000035F697F300 00007ffdff7ef430 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @ 913]
50 00000035F697F390 00007ffdff7fc203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @ 102]
51 00000035F697F6E0 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f697f6e0] 
52 OS Thread Id: 0x4344 (7)
53 Unable to walk the managed stack. The current thread is likely not a 
54 managed thread. You can run !clrthreads to get a list of managed threads in
55 the process
56 Failed to start stack walk: 80070057

                    【~*e!clrstack】命令將託管線程和非託管線程所有的棧回溯都輸出出來了。OS Thread Id: 0x3dd8 (6) 號的線程執行 System.Threading.Monitor.ReliableEnter 方法就不執行了,說明卡住了,卡在什麼地方呢,就是 ExampleCore_6_9.Program+c.b__2_1() 這樣代碼最後的行號,32,也就是源碼的第32行,換句話說,就是 6 號線程持有 Student 鎖,等待 Person 釋放鎖。效果如圖:
                    

                    OS Thread Id: 0x6c (4) 號線程執行了 System.Threading.Monitor.ReliableEnter 方法也沒有後續了,說明卡住了,同樣,卡住的位置在哪裏,就是 ExampleCore_6_9.Program+c.b__2_0() 這行表示的意思,最後有一個數字,就是源碼的行號,它是17,換句話說,就是 4 號線程持有 Person 鎖,在登臺 student 上的鎖釋放。效果如圖:
                    

                    其實,我們從以上也能看出一些端倪來。輸出信息雖然簡單,但是卻展示一種常見死鎖的識別技術。

                    我們也可以使用【!syncblk】命令查看一下同步塊數據,這個也能說明一些問題。

 1 0:007> !syncblk
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     5 0000019606A7CF78            3         1 0000015570129FB0 6c   4   0000015574410a28 ExampleCore_6_9.Person
 4     6 0000019606A7CFD0            3         1 0000019606A77790 3dd8   6   0000015574410a40 ExampleCore_6_9.Student
 5 -----------------------------
 6 Total           6
 7 CCW             0
 8 RCW             0
 9 ComClassFactory 0
10 Free            0

                    我們看到了 ID 是 4 的線程持有 ExampleCore_6_9.Person 對象,ID 是 6 的線程持有 ExampleCore_6_9.Student 對象,我們可以切換到 4 和 6 號線程上查看一下。
                    通過以上的分析,剩下就去代碼裏找問題吧。


        4.4.2、孤立鎖:異常
            A、基礎知識
                孤兒鎖是因爲開發者使用 Monitor.Enter 獲取一個對象後,因爲某種原因沒有正確調用 Monitor.Exit,導致這個對象一直處於佔用狀態,其他線程也就無法進入了,強烈建議使用 lock 語法。

            B、眼見爲實
                調試源碼:ExampleCore_6_10
                調試任務:重現孤立鎖。
                1)、NTSD 調試
                    編譯項目,直接雙擊我們項目的 EXE 可執行程序,直到我們的控制檯程序有如圖輸出:
                    
                    我們打開【Visual Studio 2022 Developer Command Prompt v17.9.6】,輸出命令【NTSD -pn ExampleCore_6_10.exe】,進入調試器,開始我們的調試了。
                    我們先執行【~*e!clrstack】命令,查看一下所有線程的調用棧是什麼情況。

 1 0:004> ~*e!clrstack
 2 OS Thread Id: 0x29c (0)
 3         Child SP               IP Call Site
 4 000000F24A77E548 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000f24a77e548] System.Threading.Monitor.Enter(System.Object)
 5 000000F24A77E6A0 00007FFF844A1A6D ExampleCore_6_10.Program.Main(System.String[])
 6 OS Thread Id: 0x2128 (1)
 7 Unable to walk the managed stack. The current thread is likely not a
 8 managed thread. You can run !threads to get a list of managed threads in
 9 the process
10 Failed to start stack walk: 80070057
11 OS Thread Id: 0x3378 (2)
12 Unable to walk the managed stack. The current thread is likely not a
13 managed thread. You can run !threads to get a list of managed threads in
14 the process
15 Failed to start stack walk: 80070057
16 OS Thread Id: 0x16d0 (3)
17         Child SP               IP Call Site
18 000000F24AEFFBC0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000f24aeffbc0]
19 OS Thread Id: 0x3eb4 (4)
20 Unable to walk the managed stack. The current thread is likely not a
21 managed thread. You can run !threads to get a list of managed threads in
22 the process
23 Failed to start stack walk: 80070057
24 0:004>

                    OS Thread Id: 0x29c (0) 這個就是 0 號主線程,它執行了 Main 方法,又執行 System.Threading.Monitor.Enter 方法,處於掛起的狀態,其他線程沒有任何有用信息。
                    我們的被鎖的對象是 ExampleCore_6_10.DBWrapper,又是在主線程出的問題,我們就去主線程上找一下 DBWrapper 對象。
                    執行命令【~0s】切換到主線程。

1 0:004> ~0s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3              ret

                    繼續執行【!dumpstackobjects】命令。

 1 0:000> !dumpstackobjects
 2 OS Thread Id: 0x29c (0)
 3 RSP/REG          Object           Name
 4 000000F24A77E068 000002cf3c00e050 System.IO.StreamWriter
 5 000000F24A77E080 000002cf3c00e050 System.IO.StreamWriter
 6 000000F24A77E0C0 000002cf3c00e050 System.IO.StreamWriter
 7 000000F24A77E3B0 000002cf3c009630 ExampleCore_6_10.DBWrapper
 8 000000F24A77E450 000002cf3c009600 System.WeakReference`1[[System.Diagnostics.Tracing.EventSource, System.Private.CoreLib]]
 9 000000F24A77E4F8 000002cf3c009630 ExampleCore_6_10.DBWrapper
10 000000F24A77E558 000002cf3c00e050 System.IO.StreamWriter
11 000000F24A77E5D8 0000030fce2b04c0 System.String    Acquiring Lock!
12 000000F24A77E650 000002cf3c009630 ExampleCore_6_10.DBWrapper
13 000000F24A77E660 000002cf3c009630 ExampleCore_6_10.DBWrapper
14 000000F24A77E6B0 000002cf3c009688 System.Threading.Thread
15 000000F24A77E6C0 000002cf3c009648 System.Threading.ThreadStart
16 000000F24A77E6C8 000002cf3c009688 System.Threading.Thread
17 000000F24A77E6D0 000002cf3c009648 System.Threading.ThreadStart
18 000000F24A77E6E0 000002cf3c009630 ExampleCore_6_10.DBWrapper
19 000000F24A77E6E8 000002cf3c009688 System.Threading.Thread
20 000000F24A77E700 000002cf3c008e98 System.String[]
21 000000F24A77E7A8 000002cf3c008e98 System.String[]
22 000000F24A77E9A0 000002cf3c008e98 System.String[]
23 000000F24A77E9A8 000002cf3c008e98 System.String[]
24 000000F24A77EAC0 000002cf3c008e98 System.String[]
25 000000F24A77EB40 000002cf3c008eb0 System.String    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\bin\Debug\net8.0\ExampleCore_6_10.dll
26 000000F24A77EB50 000002cf3c008e98 System.String[]
27 000000F24A77EB60 000002cf3c008e78 System.String[]
28 000000F24A77EB98 000002cf3c008eb0 System.String    E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\bin\Debug\net8.0\ExampleCore_6_10.dll
29 000000F24A77ED48 000002cf3c008e98 System.String[]
30 0:000>

                    ExampleCore_6_10.DBWrapper 類型的地址是 000002cf3c009630,執行【dp 000002cf3c009630-8 l1】命令查看一下它的對象頭。

1 0:000> dp 000002cf3c009630-8 l1
2 000002cf`3c009628  08000002`00000000

                    說明對象頭已經創建同步塊了,索引值是 2,所以我們執行【!syncblk 2】命令查看一下同步塊的數據。

 1 0:000> !syncblk 2
 2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
 3     2 0000030FCE6B3F80            3         1 000002CF39578280 0 XXX   000002cf3c009630 ExampleCore_6_10.DBWrapper
 4 -----------------------------
 5 Total           2
 6 CCW             0
 7 RCW             0
 8 ComClassFactory 0
 9 Free            0
10 0:000>

                    說明 XXX 號線程持有 ExampleCore_6_10.DBWrapper 類型,也可以說 XXX 線程擁有  ExampleCore_6_10.DBWrapper 的鎖。XXX 表示的是調試器線程的ID,0 表示操作系統線程的 ID。
                    XXX 的含義就是,CLR 無法將操作系統線程的 ID 映射到調試器線程,出現這樣情況的一個原因是,某個線程在某個時刻獲取一個對象的鎖,然後,這個線程消失了,卻沒有釋放鎖。

                    我們可以執行【!t】或者【!threads】命令驗證 XXX 的說法。

 1 0:000> !t
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       1(有一個死亡的線程)
 7 Hosted Runtime:   no
 8                                                                                                             Lock
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      29c 000002CF37BB7940  202a020 Preemptive  000002CF3C009830:000002CF3C00A618 000002CF37BC5B20 -00001 MTA
11    3    2     16d0 000002CF37C82510    2b220 Preemptive  0000000000000000:0000000000000000 000002CF37BC5B20 -00001 MTA (Finalizer)
12 XXXX    4        0 000002CF39578280    39820 Preemptive  0000000000000000:0000000000000000 000002CF37BC5B20 -00001 Ukn(這個就是死亡的線程)

                    只要沒有執行終結操作,即使處於死亡狀態的線程也會被輸出。
                    到這裏就差不多了,我們還需要結合代碼和調試器一起來找問題,很簡單,我直接貼圖了。
                    
                    圖上說的很情況,就不多解釋了。

                2)、Windbg Preview 調試
                    編譯項目,直接雙擊我們項目的 EXE 可執行程序,直到我們的控制檯程序有如圖輸出:
                    

                    我們打開【Windbg Preview】,依次點擊【文件】---【Attach to process】,在右側選擇我們運行的程序,點擊【附加】,附加我們的進程,進入調試器,開始我們的調試了。
                    我們先執行【~*e!clrstack】命令,查看一下所有線程的調用棧是什麼情況。

 1 0:004> ~*e!clrstack
 2 OS Thread Id: 0x29c (0)
 3         Child SP               IP Call Site
 4 000000F24A77E548 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000f24a77e548] System.Threading.Monitor.Enter(System.Object)
 5 000000F24A77E6A0 00007fff844a1a6d ExampleCore_6_10.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\Program.cs @ 27]
 6 OS Thread Id: 0x2128 (1)
 7 Unable to walk the managed stack. The current thread is likely not a 
 8 managed thread. You can run !clrthreads to get a list of managed threads in
 9 the process
10 Failed to start stack walk: 80070057
11 OS Thread Id: 0x3378 (2)
12 Unable to walk the managed stack. The current thread is likely not a 
13 managed thread. You can run !clrthreads to get a list of managed threads in
14 the process
15 Failed to start stack walk: 80070057
16 OS Thread Id: 0x16d0 (3)
17         Child SP               IP Call Site
18 000000F24AEFFBC0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000f24aeffbc0] 
19 OS Thread Id: 0x12cc (4)
20 Unable to walk the managed stack. The current thread is likely not a 
21 managed thread. You can run !clrthreads to get a list of managed threads in
22 the process
23 Failed to start stack walk: 80070057

                    我們從命令的輸出中可以看到,有用的信息不多,紅色標註的就是主線程的運行情況。我們發現 0 號線程,也就是主線程在執行 System.Threading.Monitor.Enter 方法時掛起了,不執行了,問題大概也就是在這裏。
                    既然主線程有了問題,我們就切換到主線程看看情況,執行命令【~0s】。

1 0:004> ~0s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3              ret

                    我們執行【!dumpstackobjects】命令,找到我們要分析的對象 DBWrapper。

 1 0:000> !dumpstackobjects
 2 OS Thread Id: 0x29c (0)
 3           SP/REG           Object Name
 4     00f24a77e068     02cf3c00e050 System.IO.StreamWriter
 5     00f24a77e080     02cf3c00e050 System.IO.StreamWriter
 6     00f24a77e0c0     02cf3c00e050 System.IO.StreamWriter
 7     00f24a77e3b0     02cf3c009630 ExampleCore_6_10.DBWrapper
 8     00f24a77e450     02cf3c009600 System.WeakReference<System.Diagnostics.Tracing.EventSource>
 9     00f24a77e4f8     02cf3c009630 ExampleCore_6_10.DBWrapper
10     00f24a77e558     02cf3c00e050 System.IO.StreamWriter
11     00f24a77e5d8     030fce2b04c0 System.String
12     00f24a77e650     02cf3c009630 ExampleCore_6_10.DBWrapper
13     00f24a77e660     02cf3c009630 ExampleCore_6_10.DBWrapper
14     00f24a77e6b0     02cf3c009688 System.Threading.Thread
15     00f24a77e6c0     02cf3c009648 System.Threading.ThreadStart
16     00f24a77e6c8     02cf3c009688 System.Threading.Thread
17     00f24a77e6d0     02cf3c009648 System.Threading.ThreadStart
18     00f24a77e6e0     02cf3c009630 ExampleCore_6_10.DBWrapper
19     00f24a77e6e8     02cf3c009688 System.Threading.Thread
20     00f24a77e700     02cf3c008e98 System.String[]
21     00f24a77e7a8     02cf3c008e98 System.String[]
22     00f24a77e9a0     02cf3c008e98 System.String[]
23     00f24a77e9a8     02cf3c008e98 System.String[]
24     00f24a77eac0     02cf3c008e98 System.String[]
25     00f24a77eb40     02cf3c008eb0 System.String
26     00f24a77eb50     02cf3c008e98 System.String[]
27     00f24a77eb60     02cf3c008e78 System.String[]
28     00f24a77eb98     02cf3c008eb0 System.String
29     00f24a77ed48     02cf3c008e98 System.String[]

                    ExampleCore_6_10.DBWrapper 就是我們要找的對象,它的地址是 02cf3c009630,我們執行【dp 02cf3c009630-8 l1】命令查看該對象的對象頭包含的是什麼東西。

1 0:000> dp 02cf3c009630-8 l1
2 000002cf`3c009628  08000002`00000000

                    08000002 說明對象頭已經創建了一個同步塊了,索引值是 2,我們查看同步塊,執行命令【!syncblk 2】。

1 0:000> !syncblk 2
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3     2 0000030FCE6B3F80            3         1 000002CF39578280 0 XXX   000002cf3c009630 ExampleCore_6_10.DBWrapper
4 -----------------------------
5 Total           2
6 CCW             0
7 RCW             0
8 ComClassFactory 0
9 Free            0

                    輸出信息告訴我們 ExampleCore_6_10.DBWrapper 對象已經被鎖定了,被 XXX 線程鎖定的。XXX 表示的是調試器的線程 ID,0 表示的是操作系統線程的 ID。
                    XXX 表示 CLR 無法將操作系統線程的 ID 無法映射到調試器線程。出現這種情況的原因是,這個線程在某個時刻獲取了該對象上的鎖,然後這個線程消失了但是卻沒有釋放鎖。

                    我們執行【!t】或者【!threads】命令驗證這一點。

 1 0:000> !threads
 2 ThreadCount:      3
 3 UnstartedThread:  0
 4 BackgroundThread: 1
 5 PendingThread:    0
 6 DeadThread:       1(有一個死亡的線程)
 7 Hosted Runtime:   no
 8                                                                                                             Lock  
 9  DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
10    0    1      29c 000002CF37BB7940  202a020 Preemptive  000002CF3C009830:000002CF3C00A618 000002cf37bc5b20 -00001 MTA 
11    3    2     16d0 000002CF37C82510    2b220 Preemptive  0000000000000000:0000000000000000 000002cf37bc5b20 -00001 MTA (Finalizer) 
12 XXXX    4        0 000002CF39578280    39820 Preemptive  0000000000000000:0000000000000000 000002cf37bc5b20 -00001 Ukn (死亡的線程)

                    只要沒有執行終結操作,即使處於死亡狀態的線程也會被出輸出。

                    要分析具體是哪裏的錯誤,肯定要結合代碼來分析。我們的代碼是這裏出問題了,如圖:
                    

                    代碼很簡單,就不多說了。
                    
        4.4.3、線程中止
            這節的內容就略過了,探索的意義不是很大,首先,我使用的平臺是 8.0 跨平臺版本,不是 .NET Framework 版本了,如果在 .NET 8.0 版本里調用  Thread.Abort() 方法是不支持的。會有綠色波浪線提示,如圖:
            

            如果大家使用的 .NET Framework 平臺,可以自己試試。


        4.4.4、終結器掛起
            系統內存暴漲有很多原因,不良線程可以是原因之一,訪問非託管資源也可以是原因之一。如果查看內容暴漲,其實還是有很多方法的,比如:我們可以使用【任務管理器】,也可以使用【ProcessExplorer】工具。具體的使用方法就不介紹了,大家可以網上自行惡補。
            原書上的內容我省略了,由於沒有原書的源碼,所以我也無法調試了。這裏是我用的以前的代碼(我之前寫過一個系列的代碼),和終結器掛起也沒關係,但是和內存暴漲有關係,原書的調試方法還是可以使用的,特此說明。

            有些查找問題的方法和步驟還是很有用的,如果我們發現系統內存暴漲,可以嘗試執行一下步驟排查。
            1)、我們可以先執行【!eeheap -loader】命令,查看一下加載器堆是否存在異常。
            2)、如果加載器堆沒問題,我們可以嘗試執行【!eeheap -gc】命令查看託管堆是否有什麼情況。
            3)、我們也可以執行【!heap -s】命令,查看所有堆的統計情況,來查找問題,如果數據有問題,可以繼續使用【!heap -h】命令是否存在句柄數據。
            4)、當然,我們也可以使用【!DumpHeap -stat】命令,統計一下託管堆上的對象,看看對象數據是否存在問題。
            5)、直到了對象,我們就可以使用【!DumpHeap -type】查找指定對象的地址。
            6)、有了對象的地址,我們就可以使用【!gcroot】命令,觀察對象的根引用。
            7)、我們也可以使用【FinalizeQueue】命令查看一下中介對象的情況來查找問題。
            8)、通過【!t】或者【!thread】命令,瞭解線程的情況,直到了線程標識 ID,我們就可以使用【!clrstack】命令查看 指定線程的調用棧。

              
五、總結
    這篇文章的終於寫完了,這篇文章的內容相對來說,不是很多。寫完一篇,就說明進步了一點點。Net 高級調試這條路,也剛剛起步,還有很多要學的地方。皇天不負有心人,努力,不辜負自己,我相信付出就有回報,再者說,學習的過程,有時候,雖然很痛苦,但是,學有所成,學有所懂,這個開心的感覺還是不可言喻的。不忘初心,繼續努力。做自己喜歡做的,開心就好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章