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 调试](#1)、KD 和 NTSD 调试)
            [2)、Windbg Preview 调试](#2)、Windbg Preview 调试)
    2、线程同步原语
        2.1、事件同步原语(内核锁)
            A、基础知识
            B、眼见为实
                1)、[KD 和](#KD 和) [NTSD 调试](#NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        2.2、互斥体(内核锁)
            A、基础知识
            B、眼见为实
                1)、[KD 和 NTSD 调试](#KD 和 NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        2.3、信号量(内核锁)
            A、基础知识
            B、眼见为实
                1)、[KD 和 NTSD 调试](#KD 和 NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        2.4、监视器
            A、基础知识
            B、眼见为实
                1)、[NTSD 调试](#NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        2.5、读写锁
            A、基础知识
            B、眼见为实
                1)、[NTSD 调试](#NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        2.6、线程池

3、同步的内部细节
        3.1、对象头
        3.2、同步块
            A、基础知识
            B、眼见为实
                1)、[NTSD 调试](#NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        3.3、瘦锁
            A、基础知识
            B、眼见为实
                1)、[NTSD 调试](#NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
    4、同步任务
        4.1、死锁
            A、基础知识
            B、眼见为实
                1)、[NTSD 调试](#NTSD 调试)
                2)、[Windbg Preview 调试](#Windbg Preview 调试)
        4.2、孤立锁:异常
            A、基础知识
            B、眼见为实
                1)、[NTDS 调试](#NTDS 调试)
                2)、[Windbg Preview 调试](#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 _waitHandle11 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 handle11 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 _waitHandle11 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 handle11 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(事件类型是 ManualResetEvent)
13     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 _waitHandle11 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 handle11 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 Reset13     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 _waitHandle11 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 handle11 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 _waitHandle11 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 handle11 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 _waitHandle11 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 handle11 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 _waitHandle11 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 handle11 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    +0x000m_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    +0x020m_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    +0x000m_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    +0x020m_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 Reset13     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_readerEvent11 00007ff9e4f593f0  400001e       10          System.Void  0 instance 0000000000000000_writerEvent12 00007ff9e4f7a5f0  400001f       18         System.Int64  1 instance                1_lockID13 00007ff9e4f51188  4000020       20         System.Int32  1 instance                0_state14 00007ff9e4f51188  4000021       24         System.Int32  1 instance               -1_writerID15 00007ff9e4f51188  4000022       28         System.Int32  1 instance                1 _writerSeqNum
16 00007ff9e4f767b8  4000023       2c        System.UInt16  1 instance                0_writerLevel17 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_readerEvent11 00007ffa020493f0  400001e       10          System.Void  0 instance 0000000000000000_writerEvent12 00007ffa0206a5f0  400001f       18         System.Int64  1 instance                1_lockID13 00007ffa02041188  4000020       20         System.Int32  1 instance                0_state14 00007ffa02041188  4000021       24         System.Int32  1 instance               -1_writerID15 00007ffa02041188  4000022       28         System.Int32  1 instance                1 _writerSeqNum
16 00007ffa020667b8  4000023       2c        System.UInt16  1 instance                0_writerLevel17 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.Program3 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,分别加上 0x08000000 和 0x04000000,执行命令【? 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.Program3 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 0 x000001c613c09628】命令,查看内容。

复制代码
 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]

0x0000026a14010a280x0000026a14010a40 就是我们的 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.Student13 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.DBWrapper10 000000F24A77E558 000002cf3c00e050 System.IO.StreamWriter
11 000000F24A77E5D8 0000030fce2b04c0 System.String    Acquiring Lock!
12 000000F24A77E650 000002cf3c009630 ExampleCore_6_10.DBWrapper13 000000F24A77E660 000002cf3c009630 ExampleCore_6_10.DBWrapper14 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.DBWrapper19 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.DBWrapper10     00f24a77e558     02cf3c00e050 System.IO.StreamWriter
11     00f24a77e5d8     030fce2b04c0 System.String
12     00f24a77e650     02cf3c009630 ExampleCore_6_10.DBWrapper13     00f24a77e660     02cf3c009630 ExampleCore_6_10.DBWrapper14     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.DBWrapper19     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.DBWrapper4 -----------------------------
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 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

相关推荐
fuxing.18 小时前
一文搞懂到底什么是 AQS
多线程
救苦救难韩天尊18 小时前
Java基础:线程的三种创建方式
java·多线程
IT界的奇葩2 天前
关于Disruptor监听策略
java·高并发·多线程·disruptor
苟且.2 天前
ThreadLocal、InheritableThreadLocal 和 TransmittableThreadLocal
java·多线程
martian6653 天前
学懂C#编程:常用高级技术——学会C#多线程开发(一)
开发语言·c#·多线程
三村阿明3 天前
Java多线程
java·开发语言·多线程·reentrantlock
橘色的喵3 天前
多进程下使用文件锁互斥压缩业务
并发控制·信号量·gzip压缩·文件锁·日志压缩·多进程同步·filelock
王小义笔记4 天前
异步线程的@Async开发总结
java·开发语言·spring boot·线程池·异步
laufing4 天前
C++ 教程 - 09 多线程处理
c++·thread·pthread
martian6656 天前
学懂C#编程:常用高级技术——学会C#多线程开发(三):学会线程池的使用
开发语言·c#·线程池·多线程