小僧渡红尘 发表于 2024-5-14 11:37:20

Advanced .Net Debugging 8:线程同步

一、介绍
    这是我的《Advanced .Net Debugging》这个系列的第八篇文章。这篇文章的内容是原书的第二部分的【调试实战】的第六章【同步】。我们经常写一些多线程的应用程序,写的多了,有关多线程的问题出现的也就多了,因此,最迫切的任务就是提高解决多线程同步问题的能力。这一节我们将从本质上、从底层上来介绍线程的同步组件和同步原理,也会给出在多线程环境下如何解决问题的最佳实践。高级调试会涉及很多方面的内容,你对 .NET 基础知识掌握越全面、细节越底层,调试成功的几率越大,当我们遇到各种奇葩问题的时候才不会手足无措。
    如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。

     调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
          操作系统:Windows Professional 10
          调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)
          下载地址:可以去Microsoft Store 去下载
          开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
          Net 版本:.Net 8.0
          CoreCLR源码:源码下载

    在此说明:我使用了两种调试工具,第一种:Windbg Preivew,图形界面,使用方便,操作顺手,不用担心干扰;第二种是:NTSD,是命令行形式的调试器,在命令使用上和 Windbg 没有任何区别,之所以增加了它的调试过程,不过是我的个人爱好,想多了解一些,看看他们有什么区别,为了学习而使用的。如果在工作中,我推荐使用 Windbg Preview,更好用,更方便,也不会出现奇怪问题(我在使用 NTSD 调试断点的时候,不能断住,提示内存不可读,Windbg preview 就没有任何问题)。
    如果大家想了解调试过程,二选一即可,当然,我推荐查看【Windbg Preview 调试】。

二、目录结构
    为了让大家看的更清楚,也为了自己方便查找,我做了一个目录结构,可以直观的查看文章的布局、内容,可以有针对性查看。
    1、同步的基础知识
        A、基础知识
        B、眼见为实
            1)、KD 和 NTSD 调试
            2)、Windbg Preview 调试
    2、线程同步原语
        2.1、事件同步原语(内核锁)
            A、基础知识
            B、眼见为实
                1)、KD 和 NTSD 调试
                2)、Windbg Preview 调试
        2.2、互斥体(内核锁)
            A、基础知识
            B、眼见为实
                1)、KD 和 NTSD 调试
                2)、Windbg Preview 调试
        2.3、信号量(内核锁)
            A、基础知识
            B、眼见为实
                1)、KD 和 NTSD 调试
                2)、Windbg Preview 调试
        2.4、监视器
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
        2.5、读写锁
            A、基础知识
            B、眼见为实
                1)、NTSD  调试
                2)、Windbg Preview 调试
        2.6、线程池

    3、同步的内部细节
        3.1、对象头
        3.2、同步块
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
        3.3、瘦锁
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
    4、同步任务
        4.1、死锁
            A、基础知识
            B、眼见为实
                1)、NTSD 调试
                2)、Windbg Preview 调试
        4.2、孤立锁:异常
            A、基础知识
            B、眼见为实
                1)、NTDS 调试
                2)、Windbg Preview 调试
        4.3、线程中止
        4.4、终结器挂起

三、调试源码
    废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
    3.1、ExampleCore_6_1
1 namespace ExampleCore_6_1
2 {
3   internal class Program
4   {
5         static void Main(string[] args)
6         {
7             var thread = new Thread(() =>
8             {
9               Console.WriteLine($"tid={Environment.CurrentManagedThreadId}");
10               Console.ReadLine();
11             });
12
13             thread.Start();
14
15             Console.ReadLine();
16         }
17   }
18 }View Code    3.2、ExampleCore_6_2
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_2
4 {
5   internal class Program
6   {      
7         static void Main(string[] args)
8         {            
9             while (true)
10             {
11               Console.WriteLine("选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)");
12               var myword= Console.ReadLine();
13               if (string.Compare(myword, "Manual", true) == 0)
14               {
15                     RunManualResetEvent();
16               }
17               else if (string.Compare(myword, "Auto", true) == 0)
18               {
19                     RunAutoResetEvent();
20               }
21               else if (string.Compare(myword, "Exit", true) == 0)
22               {
23                     break;
24               }
25             }
26         }
27
28         static void RunManualResetEvent()
29         {
30             ManualResetEvent? mre = new ManualResetEvent(false);
31
32             Console.WriteLine($"mre 默认为 false,即等待状态,请查看!");
33             Debugger.Break();
34
35             mre.Set();
36             Console.WriteLine($"mre 默认为 true,即放行状态,请查看!");
37             Debugger.Break();
38
39             mre.Reset();
40             Console.WriteLine($"mre Reset 后为 false,即等待状态,请查看!");
41             Debugger.Break();
42
43             mre = null;
44         }
45
46         static void RunAutoResetEvent()
47         {
48             AutoResetEvent? mre = new AutoResetEvent(false);
49
50             Console.WriteLine($"are 默认为 false,即等待状态,请查看!");
51             Debugger.Break();
52
53             mre.Set();
54             Console.WriteLine($"are 默认为 true,即放行状态,请查看!");
55             Debugger.Break();
56
57             mre.Reset();
58             Console.WriteLine($"are Reset 后为 false,即等待状态,请查看!");
59             Debugger.Break();
60
61             mre = null;
62         }
63   }
64 }View Code    3.3、ExampleCore_6_3
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_3
4 {
5   internal class Program
6   {
7         private static Mutex mut = new Mutex();
8
9         static void Main()
10         {
11             UseResource();
12         }
13
14         private static void UseResource()
15         {
16             // 等到安全进入。
17             mut.WaitOne();
18
19             Console.WriteLine("已进入保护区");
20
21             Debugger.Break();
22
23             Console.WriteLine("正在离开保护区");
24
25             // 释放互斥锁。
26             mut.ReleaseMutex();
27
28             Debugger.Break();
29         }
30   }
31 }View Code    3.4、ExampleCore_6_4
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_4
4 {
5   internal class Program
6   {
7         public static Semaphore sem = new Semaphore(1, 10);
8         static void Main(string[] args)
9         {
10             for (int i = 0; i < int.MaxValue; i++)
11             {
12               sem.Release();
13               Console.WriteLine("查看当前的 sem 值。");
14               Debugger.Break();
15             }
16         }
17   }
18 }View Code    3.5、ExampleCore_6_5
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_5
4 {
5   internal class Program
6   {
7         public static Person person = new Person();
8
9         static void Main(string[] args)
10         {
11             Task.Run(() =>
12             {
13               lock (person)
14               {
15                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中 111111");
16                     Debugger.Break();
17               }
18             });
19             Task.Run(() =>
20             {
21               lock (person)
22               {
23                     Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中 222222");
24                     Debugger.Break();
25               }
26             });
27             Console.ReadLine();
28         }
29   }
30
31   public class Person
32   {
33   }
34 }View Code    3.6、ExampleCore_6_6
1 namespace ExampleCore_6_6
2 {
3   internal class Program
4   {
5         private static ReaderWriterLock rwl = new ReaderWriterLock();
6         // Define the shared resource protected by the ReaderWriterLock.
7         static int resource = 0;
8
9         const int numThreads = 1;
10         static bool running = true;
11
12         // Statistics.
13         static int readerTimeouts = 0;
14         static int writerTimeouts = 0;
15         static int reads = 0;
16         static int writes = 0;
17
18         static void Main(string[] args)
19         {
20             Thread[] t = new Thread;
21             for (int i = 0; i < numThreads; i++)
22             {
23               t = new Thread(new ThreadStart(ThreadProc));
24               t.Name = new String((char)(i + 65), 1);
25               t.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.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: 1Cid: 3f2c    Peb: 4e16f21000ParentCid: 0da8
4   DirBase: 6bc43002ObjectTable: 00000000HandleCount:   0.
5   Image: ExampleCore_6_1.exe
6
7 No active threads
8         THREAD ffffa20677bb90c0Cid 3f2c.3cc8Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
9         THREAD ffffa20677e50240Cid 3f2c.3960Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
10         THREAD ffffa20677995080Cid 3f2c.1f54Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
11         THREAD ffffa2066e255080Cid 3f2c.3b98Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
12         THREAD ffffa206712dd080Cid 3f2c.3850Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
13         THREAD ffffa2066ead5080Cid 3f2c.2144Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
14
15 PROCESS ffffa206780c8080
16   SessionId: 1Cid: 4078    Peb: b9e31b9000ParentCid: 0da8
17   DirBase: 3183bb002ObjectTable: ffff8a8e17548a00HandleCount: 171.
18   Image: ExampleCore_6_1.exe
19
20         THREAD <strong>ffffa2066e728080Cid 4078</strong><strong>.090c</strong>Teb: 000000b9e31ba000 Win32Thread: ffffa20677656660 WAIT: (Executive) <strong>KernelMode</strong> Alertable
21             ffffa20678e5b568NotificationEvent
22
23         THREAD ffffa2066e4e1080Cid 4078.2e48Teb: 000000b9e31c0000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
24             ffffa20677fe3d60NotificationEvent
25
26         THREAD ffffa206757e8080Cid 4078.336cTeb: 000000b9e31c2000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
27             ffffa20677fe3c60SynchronizationEvent
28             ffffa2066f679260SynchronizationEvent
29             ffffa20677fe39e0SynchronizationEvent
30
31         THREAD ffffa206739d4080Cid 4078.2ef0Teb: 000000b9e31c4000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
32             ffffa206678be6a0NotificationEvent
33             ffffa206775ab560SynchronizationEvent
34
35         THREAD ffffa20672ea6080Cid 4078.3750Teb: 000000b9e31ca000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
36             ffffa20678c15160SynchronizationEvent                红色标注就是需要注意的内容,他会把这个进程中的所有线程找出来。我们通过【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          : ""
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   : "???"
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      : _KWAIT_BLOCK
90    +0x140 WaitBlockFill4   : "p???"
91    +0x154 ContextSwitches: 0xef
92    +0x140 WaitBlockFill5   : "p???"
93    +0x184 State            : 0x5 ''
94    +0x185 Spare13          : 0 ''
95    +0x186 WaitIrql         : 0 ''
96    +0x187 WaitMode         : 0 ''
97    +0x140 WaitBlockFill6   : "p???"
98    +0x1b4 WaitTime         : 0x152d42
99    +0x140 WaitBlockFill7   : "p???"
100    +0x1e4 KernelApcDisable : 0n-1
101    +0x1e6 SpecialApcDisable : 0n0
102    +0x1e4 CombinedApcDisable : 0xffff
103    +0x140 WaitBlockFill8   : "p???"
104    +0x168 ThreadCounters   : (null)
105    +0x140 WaitBlockFill9   : "p???"
106    +0x198 XStateSave       : (null)
107    +0x140 WaitBlockFill10: "p???"
108    +0x1c8 Win32Thread      : 0xffffa206`77656660 Void
109    +0x140 WaitBlockFill11: "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 : "???"
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   : "???"
135    +0x24a ApcStateIndex    : 0 ''
136    +0x24b WaitBlockCount   : 0x1 ''
137    +0x24c IdealProcessor   : 2
138    +0x250 NpxState         : 5
139    +0x258 SavedApcState    : _KAPC_STATE
140    +0x258 SavedApcStateFill : "???"
141    +0x283 WaitReason       : 0 ''
142    +0x284 SuspendCount   : 0 ''
143    +0x285 Saturation       : 0 ''
144    +0x286 SListFaultCount: 0
145    +0x288 SchedulerApc   : _KAPC
146    +0x288 SchedulerApcFill1 : "???"
147    +0x28b QuantumReset   : 0x6 ''
148    +0x288 SchedulerApcFill2 : "???"
149    +0x28c KernelTime       : 2
150    +0x288 SchedulerApcFill3 : "???"
151    +0x2c8 WaitPrcb         : (null)
152    +0x288 SchedulerApcFill4 : "???"
153    +0x2d0 LegoData         : (null)
154    +0x288 SchedulerApcFill5 : "???"
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 : ""
169    +0x348 PriorityFloorCountsReserved : ""
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   : 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          : ""
205    +0x410 EndPadding       : 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
9DBG   ID   <strong>OSID</strong> ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
10    0    1      <strong>90c</strong> 000001CFD8DCEB20    2a020 Preemptive000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
11    3    2   2ef0 000002106F45DDF0    2b220 Preemptive0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
12    4    4   3750 000002106F46D070202b020 Preemptive000001CFDD40B4D0: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
22DBG   ID   <strong>OSID</strong> ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
23    0    1      <strong>90c</strong> 000001CFD8DCEB20    2a020 Preemptive000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
24    3    2   2ef0 000002106F45DDF0    2b220 Preemptive0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
25    4    4   3750 000002106F46D070202b020 Preemptive000001CFDD40B4D0: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: 1Cid: 3f2c    Peb: 4e16f21000ParentCid: 0da8
4   DirBase: 6bc43002ObjectTable: ffff8a8e1a97c180HandleCount: 171.
5   Image: ExampleCore_6_1.exe
6
7         THREAD <strong>ffffa20677bb90c0</strong><strong>Cid 3f2c.3cc8</strong>Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
8             ffffa20678223bb8NotificationEvent
9
10         THREAD ffffa20677e50240Cid 3f2c.3960Teb: 0000004e16f2a000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
11             ffffa20677fe5660NotificationEvent
12
13         THREAD ffffa20677995080Cid 3f2c.1f54Teb: 0000004e16f2c000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
14             ffffa20677fe5560SynchronizationEvent
15             ffffa20677fe56e0SynchronizationEvent
16             ffffa20677fe5860SynchronizationEvent
17
18         THREAD ffffa2066e255080Cid 3f2c.3b98Teb: 0000004e16f2e000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
19             ffffa206678be6a0NotificationEvent
20             ffffa20677cfa260SynchronizationEvent
21
22         THREAD ffffa206712dd080Cid 3f2c.3850Teb: 0000004e16f34000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
23             ffffa20677964c60SynchronizationEvent                我们通过【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          : ""
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   : "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      : _KWAIT_BLOCK
90    +0x140 WaitBlockFill4   : "???"
91    +0x154 ContextSwitches: 0xde
92    +0x140 WaitBlockFill5   : "???"
93    +0x184 State            : 0x5 ''
94    +0x185 Spare13          : 0 ''
95    +0x186 WaitIrql         : 0 ''
96    +0x187 WaitMode         : 0 ''
97    +0x140 WaitBlockFill6   : "???"
98    +0x1b4 WaitTime         : 0x11f7e8
99    +0x140 WaitBlockFill7   : "???"
100    +0x1e4 KernelApcDisable : 0n-1
101    +0x1e6 SpecialApcDisable : 0n0
102    +0x1e4 CombinedApcDisable : 0xffff
103    +0x140 WaitBlockFill8   : "???"
104    +0x168 ThreadCounters   : (null)
105    +0x140 WaitBlockFill9   : "???"
106    +0x198 XStateSave       : (null)
107    +0x140 WaitBlockFill10: "???"
108    +0x1c8 Win32Thread      : 0xffffa206`7765a990 Void
109    +0x140 WaitBlockFill11: "???"
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 : "???"
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   : "???"
135    +0x24a ApcStateIndex    : 0 ''
136    +0x24b WaitBlockCount   : 0x1 ''
137    +0x24c IdealProcessor   : 2
138    +0x250 NpxState         : 5
139    +0x258 SavedApcState    : _KAPC_STATE
140    +0x258 SavedApcStateFill : "???"
141    +0x283 WaitReason       : 0 ''
142    +0x284 SuspendCount   : 0 ''
143    +0x285 Saturation       : 0 ''
144    +0x286 SListFaultCount: 0
145    +0x288 SchedulerApc   : _KAPC
146    +0x288 SchedulerApcFill1 : "???"
147    +0x28b QuantumReset   : 0x6 ''
148    +0x288 SchedulerApcFill2 : "???"
149    +0x28c KernelTime       : 1
150    +0x288 SchedulerApcFill3 : "???"
151    +0x2c8 WaitPrcb         : (null)
152    +0x288 SchedulerApcFill4 : "???"
153    +0x2d0 LegoData         : (null)
154    +0x288 SchedulerApcFill5 : "???"
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 : ""
169    +0x348 PriorityFloorCountsReserved : ""
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   : 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          : ""
205    +0x410 EndPadding       : 0View Code                这个线程的数据结构内容还是不少的。
                我们可以使用【!thread ffffa20677bb90c0】命令查看更易阅读的结果。
1 lkd> !thread ffffa20677bb90c0
2 THREAD ffffa20677bb90c0Cid 3f2c.3cc8Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
3   ffffa20678223bb8NotificationEvent
4 IRP List:
5   ffffa2067802cdc0: (0006,0160) Flags: 00060900Mdl: ffffa20670216220
6   ffffa2067802bc80: (0006,0160) Flags: 00060800Mdl: 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 8BasePriority 8IoPriority 2PagePriority 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
9DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
10    0    1   <strong>3cc8</strong> 00000246EFD07630    2a020 Preemptive00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA
11    3    2   3b98 00000246EFD70060    2b220 Preemptive0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer)
12    4    4   3850 00000246EFCCD3F0202b020 Preemptive00000246F440B4D0: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
22DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
23    0    1   <strong>3cc8</strong> 00000246EFD07630    2a020 Preemptive00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA
24    3    2   3b98 00000246EFD70060    2b220 Preemptive0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer)
25    4    4   3850 00000246EFCCD3F0202b020 Preemptive00000246F440B4D0: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 <strong>0000020f29414180</strong> 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 <strong>00007ff8db1933184000b7a      8 ...es.SafeWaitHandle0 instance 0000020f294142d8 _waitHandle
</strong>11 00007ff8db0370a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    到此,说明 ManualResetEvent(false) 默认是等待的状态。
                    此刻,我们在借助【Process Explorer】工具,找到事件同步对象的内核地址,看看内核地址上的数据的变化。打开这个工具,然后在【Filter by name】输入项目名称 ExampleCore_6_2,结果如图:
                    
                    我们在【Handles】选项里,找到我们的事件对象,然后双击,打开属性框,找到内核的地址。如图:
                    
                    我们找到了事件对象在内核上的地址,我们需要再打开一个【kd】调试器,开始内核调试。
                    我们就找到了内核地址【0xFFFF940C4DC558E0】了。然后,我们到 kd 的内核态中去查看一下这个地址,使用【dp 0xFFFF940C4DC558E0 l1】命令。当前值:0(00000000)
 
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 <strong>00007ff8db0370a0400126e      8      System.IntPtr1 instance 00000000000002B8 handle
</strong>11 00007ff8dafc1188400126f       10         System.Int321 instance                4 _state
12 00007ff8daf8d0704001270       14       System.Boolean1 instance                1 _ownsHandle
13 00007ff8daf8d0704001271       15       System.Boolean1 instance                1 _fullyInitialized                    说明 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   PointerCount32769
10   Name          <none>
11   Object Specific Information
12 <strong>    Event Type Manual Reset(事件类型是 ManualResetEvent)
</strong>13   <strong>Event is Waiting(初始状态是等待)</strong>                    然后切换到【内核态】的 KD 调试器,继续使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。
1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e000000000`00060000                    【!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   PointerCount65535
10   Name          <none>
11   Object Specific Information
12   Event Type Manual Reset
13   Event is <strong>Set(放行状态)</strong>                    Reset 后是等待的状态,然后切换到【内核态】的 KD,继续使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。
1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0<strong>00000001</strong>`00060000(红色变成 1 ,表示 true)                    我们就看到了,状态是0和1相互切换的。

                2)、Windbg Preview 调试
                    我们编译项目,打开【Windbg Preview】调试器,点击【文件】----》【Launch executable】加载我们的程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,在【Debugger.Break()】语句处停止,我们的控制台应用程序输出:mre 默认为 false,即等待状态,请查看!,Windbg 处于暂停状态,我们就可以调试了。
                    首先,我们去托管堆中查找一下 ManualResetEvent 这个对象,执行【!dumpheap -type ManualResetEvent】命令。
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   PointerCount65534
10   Name          <none>
11   Object Specific Information
12   Event Type Manual Reset
13   <strong>Event is Waiting(处于等待)</strong>                    ManualResetEvent 对象的地址是 012b87014180,针对这个地址,我们使用【!do】或者【!DumpObj】命令,查看它的详情。
1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0<strong>00000000</strong>`00060000(红色是 0,0 代表就是 false)                    我们可以不使用【!DumpVC】命令,直接使用【!handle】命令。
                    红色标注的是一个 handle 对象,我们可以使用【!handle 0000000000000248 f】命令继续查看,必须具有 f 参数。
1 0:000> !DumpHeap -type ManualResetEvent
2          Address               MT         Size
3   <strong>012b87014180</strong>   7ff8da3e2a88             24
4
5 Statistics:
6         MT Count TotalSize Class Name
7 7ff8da3e2a88   1      24 System.Threading.ManualResetEvent
8 Total 1 objects, 24 bytes                    说明 false 是等待的状态,然后,我们继续【g】运行一下,等我们的控制台项目输出:mre 默认为 true,即放行状态,请查看!,我们继续执行【!handle 0000000000000248 f】命令查看。
1 0:000> !DumpObj 012b87014180
2 <strong>Name:      System.Threading.ManualResetEvent(手动重置事件)
</strong> 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 <strong>00007ff8da3e33184000b7a      8 ...es.SafeWaitHandle0 instance 0000012b870142d8 _waitHandle
</strong>11 00007ff8da2870a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    然后,我们继续【g】运行一下,等我们的控制台项目输出:mre Reset后为 false,即等待状态,请查看!我们继续执行【!handle 0000000000000248 f】命令查看。
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 <strong>00007ff8da2870a0400126e      8      System.IntPtr1 instance 0000000000000248 handle
</strong>11 00007ff8da211188400126f       10         System.Int321 instance                4 _state
12 00007ff8da1dd0704001270       14       System.Boolean1 instance                1 _ownsHandle
13 00007ff8da1dd0704001271       15       System.Boolean1 instance                1 _fullyInitialized                    我们再次输入 auto 测试一下 AutoResetEvent。
                    【g】继续运行,提示【选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)】,此次,我们输入 auto,控制台程序输出“are 默认为 false,即等待状态,请查看!”字样。
                    我们在托管堆上查找一下 AutoResetEvent 对象,执行命令【!DumpHeap -type AutoResetEvent】。
1 0:000> !DumpVC 00007ff8da2870a00000000000000248
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 00007ff8da2870a04000525      0      System.IntPtr1 instance_value
10 00007ff8da2870a04000526      a78      System.IntPtr1   static 0000000000000000 Zero                    AutoResetEvent 对象的地址是 012b87014318,我们直接使用【!do】或者【!DumpObj】命令查看对象详情。
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(<strong>事件类型是 ManualResetEvent</strong>)
13   Event is Waiting(<strong>当前是等待状态</strong>)                    【g】继续运行,控制台程序输出“are 默认为 true,即放行状态,请查看!”字样,再次执行【!handle 00000000000002A4 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】继续运行,控制台程序输出“are Reset 后为 false,即等待状态,请查看!”字样,再次执行【!handle 00000000000002A4 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 <strong>Waiting(等待了)</strong>                    我们都知道 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 0:000> !DumpHeap -type AutoResetEvent
2          Address               MT         Size
3   <strong>012b87014318</strong>   7ff8da3e5f58             24
4
5 Statistics:
6         MT Count TotalSize Class Name
7 7ff8da3e5f58   1      24 System.Threading.AutoResetEvent
8 Total 1 objects, 24 bytes                    切换到用户态 Windbg 继续【g】运行,控制台程序输出“are 默认为 true,即放行状态,请查看!”字样。回到内核态 Windbg 继续运行【dp 0xFFFF940C4DC47A60】命令。
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 <strong>00007ff8da3e33184000b7a      8 ...es.SafeWaitHandle0 instance 0000012b87014330 _waitHandle
</strong>11 00007ff8da2870a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    然后,我们再【g】一下【用户态】的 Windbg,控制台输出“are Reset后为 false,即等待状态,请查看!”字样,当前值:0(00000000),然后切换到【内核态】的Windbg,继续使用【dp】命令,查看一下。
1 0:000> !DumpHeap -type AutoResetEvent
2          Address               MT         Size
3   <strong>012b87014318</strong>   7ff8da3e5f58             24
4
5 Statistics:
6         MT Count TotalSize Class Name
7 7ff8da3e5f58   1      24 System.Threading.AutoResetEvent
8 Total 1 objects, 24 bytes                    我们就看到了,状态是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> !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 <strong>    Event Type Auto Reset(AutoResetEvent)
</strong>13   <strong>Event is Waiting(False 就是等待)</strong>                    红色标注的就是 Mutex 对象的地址 0000013097009628,针对该地址执行【!do 0000013097009628】命令查看详情。
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 <strong>    Event Type Auto Reset
</strong>13   <strong>Event is Set</strong>                    我们可以使用【!t】命令验证这一点。
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                    关系如图:
                    
                    我们看到了用户态下 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 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a6000000000`00060001                    Mutex 有了锁,内核数据的值是 00000000。我们需要切换到【NTSD】用户态调试器,继续【g】执行,直到调试器自动进入中断模式。输出如图:
                    
                    说明此时已经释放了锁,再次执行【!handle 0000000000000290 f】查看句柄的变化。
1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a6000000001`00060001                    同样,我们切换到内核【kd】调试器,执行命令【dp 0xFFFFD2824D881CD0 l1】,查看结果。
1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a6000000000`00060001                    内核态的数据的值现在是 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 <strong>0000013097009628</strong> 00007ffef219a190       24
4
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ffef219a190      1         24 System.Threading.Mutex
8 Total 1 objects                    红色标注的 020ea5409628 数据就是 Mutex 对象的地址,然后,执行命令【!do 020ea5409628】,查看 Mutex 详情。
1 0:000> !do 0000013097009628
2 <strong>Name:      System.Threading.Mutex
</strong> 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 <strong>00007ffef219ee704000b7a      8 ...es.SafeWaitHandle0 instance 0000013097009780 _waitHandle
</strong>11 00007ffef20c70a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    我们可以使用【!t】命令,证明一下。
1 0:000> !do 0000013097009780
2 <strong>Name:      Microsoft.Win32.SafeHandles.SafeWaitHandle
</strong> 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 <strong>00007ffef20c70a0400126e      8      System.IntPtr1 instance 0000000000000290 handle
</strong>11 00007ffef2051188400126f       10         System.Int321 instance                4 _state
12 00007ffef201d0704001270       14       System.Boolean1 instance                1 _ownsHandle
13 00007ffef201d0704001271       15       System.Boolean1 instance                1 _fullyInitialized                    效果如图:
                    
                    此时,我们可以使用【Process Explorer】工具查找一下 Mutex 对象在内核态上的地址,看看内核态地址上的内容的变化。我们打开【Process Explorer】,如图操作:
                    
                    我们点击【ProcessExplorer】工具【Handles】选项,双击 Mutant 打开属性对话框。效果如图:
                    
                    我们找到了内核中的数据的地址 0xFFFFD2824D1A5BB0,此时,我们需要再重新打开另外一个【Windbg Preview】,依次点击【文件】---【Attach to kernel】,在右侧选择【local】,进入到调试器。
                    继续执行命令【dp 0xFFFFD2824D1A5BB0 l1】命令,看看内核数据是怎么表示的。
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   PointerCount65536
10   Name          <none>
11   Object Specific Information
12   <strong>Mutex is Owned(说明已经获取了锁)
</strong>13   <strong>Mutant Owner b24.de4(这是拥有锁的线程 OSID de4)</strong>                    此时,我们再次切换到用户态的【Windbg Preview】,【g】继续运行调试器,控制台程序会输出“正在离开保护区”的字样。我们继续执行【!handle 00000000000002A0 f】命令,看看是什么结果。
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
9DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
10    0    1   <strong> de4</strong> 0000013092951570    2a020 Preemptive0000013097009EF0:000001309700A610 0000013092992E10 -00001 MTA
11    6    2   23f0 00000130943ADDA0    21220 Preemptive0000000000000000:0000000000000000 0000013092992E10 -00001 Ukn (Finalizer)
12    7    3   36dc 000001309295D370    2b220 Preemptive0000000000000000:0000000000000000 0000013092992E10 -00001 MTA
13 0:000>                    已经执行了 ReleaseMutex 方法了,所以就是释放了锁了。
                    此时,我们再次切换到内核态的【Windbg Preview】,继续执行【dp 0xFFFFD2824D1A5BB0 l1】命令,结果如下:
1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0<strong>00000000</strong>`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> !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   PointerCount65534
10   Name          <none>
11   Object Specific Information
12   <strong>Mutex is Free(现在已经释放锁了)</strong>                    我们知道了 Semaphore 对象的地址是 000002754fc09628,然后执行【!do 000002754fc09628】命令。
1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0<strong>00000001</strong>`00000002                    内容很简单,就不做过多解释了。这个句柄的值  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> !DumpHeap -type Mutex
2          Address               MT         Size
3   <strong>020ea5409628</strong>   7ffecdada190             24
4
5 Statistics:
6         MT Count TotalSize Class Name
7 7ffecdada190   1      24 System.Threading.Mutex
8 Total 1 objects, 24 bytes                    我们再切换到内核态【kd】调试器上,执行【dp 0xFFFFA68F9E3CE2E0 l4】命令。
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 <strong>00007ffecdadee704000b7a      8 ...es.SafeWaitHandle0 instance 0000020ea5409780 _waitHandle
</strong>11 00007ffecda070a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    数值已经变为为 3 了,和用户态调试器输出是一致的。我们可以重复多次,每次查看变化,很简单,我就省略了。
                    我在用户态下执行执行到计数数字 10,然后在执行,看看会不会发生异常。
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 <strong>00007ffecda070a0400126e      8      System.IntPtr1 instance 00000000000002A0 handle
</strong>11 00007ffecd991188400126f       10         System.Int321 instance                4 _state
12 00007ffecd95d0704001270       14       System.Boolean1 instance                1 _ownsHandle
13 00007ffecd95d0704001271       15       System.Boolean1 instance                1 _fullyInitialized                    我们在看看内核态数据的变化,切换到【kd】调试器上,执行命令【dp 0xFFFFA68F9E3CE2E0 l4】。
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 00007ffecda070a04000525      0      System.IntPtr1 instance_value
10 00007ffecda070a04000526      a78      System.IntPtr1   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   <strong>Mutex is Owned(进入锁状态)
</strong>24   <strong>Mutant Owner 3438.3b78(持有 Mutex 线程的 ID 3b78)</strong>                    我们看到内核态的值已经变成 0000000a 了。
                    我们回到用户态的【NTSD】调试器,继续【g】,看看会发生什么。
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
9DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
10    0    1   <strong>3b78</strong> 0000020EA0D37F10    2a020 Preemptive0000020EA5409EF0:0000020EA540A610 0000020ea0d79770 -00001 MTA
11    5    2   4210 0000020EA0DE7C40    21220 Preemptive0000000000000000:0000000000000000 0000020ea0d79770 -00001 Ukn (Finalizer)
12    6    3   1ef0 0000020EA0D43DC0    2b220 Preemptive0000000000000000:0000000000000000 0000020ea0d79770 -00001 MTA                     我们看到发生了 CLR exception 异常了,和我们期望的一样。

                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_6_4.exe,直接进入调试器。
                    进入到调试器后,【g】直接运行调试器,我们的控制台程序会输出“查看当前的 sem 值。”字样,调试器会自动进入中断模式,此时,就可以开始我们的调试了。
                    我们先在托管堆上查找一下 Semaphore 对象是否存在,执行命令【!DumpHeap -type Semaphore】。
1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb000000000`00000002                    我们找到了 Semaphore 对象的地址,有了地址就好办了,我们直接执行【!do 027685409628】命令,查看它的详情。
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   <strong>Mutex is Free(已经释放了锁)</strong>                    这些都是在用户态调试器下的显示,我们也要看看在内核态下是怎么显示的,记住 handle 的值,后面会用到。
                    我们想要在内核态想查看数据的变化,必须找到句柄的内核态地址,所以我们要借助【ProcessExplorer】工具,操作如图:
                    
                    我们在【ProcessExplorer】下方的【Handles】找到 Semaphore 信号量对象,继续双击就可以看到它的内核态的地址。
                    
                    很简单,就不多说了,我们知道了它的内核地址 0xFFFFA68F9E3E1CE0。此时,我们需要在打开一个【Windbg Preview】,依次点击【文件】----【Attach to kernel】,在窗口的右侧选择【local】,点击【ok】进去调试器,就可以使用【dp 0xFFFFA68F9E3E1CE0 l4】命令查看数据了。
1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0<strong>00000001</strong>`00000002                    00000002 就是当前值,00000000`0000000a 就是极限值。
                    接下来就简单了,我们多次执行用户态的调试器,然后再在内核态调试器里查看变化,一目了然。
                    我先执行一次用户态下【g】命令,在执行【!handle 0000000000000290 f】命令,查看变化。
1 0:000> !DumpHeap -type Semaphore
2          Address               MT   Size
3 <strong>000002754fc09628</strong> 00007ffa1ed0a198       24
4
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ffa1ed0a198      1         24 System.Threading.Semaphore
8 Total 1 objects                    我们在切换到内核态调试器中执行【dp 0xFFFFA68F9E3E1CE0 l4】命令。
1 0:000> !do 000002754fc09628
2 <strong>Name:      System.Threading.Semaphore
</strong> 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 <strong>00007ffa1ed311484000b7a      8 ...es.SafeWaitHandle0 instance 000002754fc09780 _waitHandle
</strong>11 00007ffa1ec370a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    00000003 变为 3了。
                    我们可以继续连续执行同样的命令,查看结果。
                    当我在用户态执行的时候,当当前计数大于10的时候,会发生异常。
1 0:000> !do 000002754fc09780
2 <strong>Name:      Microsoft.Win32.SafeHandles.SafeWaitHandle
</strong> 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 <strong>00007ffa1ec370a0400126e      8      System.IntPtr1 instance 0000000000000290 handle
</strong>11 00007ffa1ebc1188400126f       10         System.Int321 instance                4 _state
12 00007ffa1eb8d0704001270       14       System.Boolean1 instance                1 _ownsHandle
13 00007ffa1eb8d0704001271       15       System.Boolean1 instance                1 _fullyInitialized                    我们在看看内核态的数据,继续执行命令。
1 0:000> !handle <strong>0000000000000290</strong> 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   PointerCount65536
10   Name          <none>
11   Object Specific Information
12   <strong>Semaphore Count 2(当前计数是2,每次执行都会累加)</strong>
13   <strong>Semaphore Limit 10(这是最大值,超过就会抛出异常)</strong>                    当前的计数值就是 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: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   PointerCount65534
16   Name          <none>
17   Object Specific Information
18   <strong>Semaphore Count</strong> <strong>3(第一次执行是2,现在是 3,每次执行都会递增)</strong>
19   <strong>Semaphore Limit 10(最大值)</strong>                    我们说过 Monitor 的底层实现就是 AwareLock,这个标红 0000015A8405C070 地址就是指向  AwareLock。我们使用【dt coreclr!AwareLock 0000015A8405C070】命令查看一番。
1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0<strong>00000003</strong>`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0ffffa68f`9e3ce2e8 00000000`<strong>0000000a</strong>                    我们继续使用【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: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   PointerCount65527
52   Name          <none>
53   Object Specific Information
54   Semaphore Count 10
55   Semaphore Limit 10                    我们说过 Monitor 的底层实现就是 AwareLock,这个标红 00000217A549CE10 地址就是指向  AwareLock。我们使用【dt coreclr!AwareLock 00000217A549CE10】命令查看一番。
1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0<strong>0000000a</strong>`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0ffffa68f`9e3ce2e8 00000000`<strong>0000000a</strong>                    我们继续使用【dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))】命令查看 m_SemEvent 是什么,不用执行命令,直接点击就可以了。
1 0:000> g
2 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000   C:\Windows\SYSTEM32\icu.dll
3 (23a8.1c70): <strong>CLR exception</strong> - code e0434352 (first chance)
4 (23a8.1c70): CLR exception - code e0434352 (!!! second chance !!!)
5 KERNELBASE!RaiseException+0x69:
6 00007ffb`411dcf19 0f1f440000      nop   dword ptr                     既然是一个 handle,我们就使用【!handle 0x314 f】命令查看一下就知道了。
1 0:000> !DumpHeap -type Semaphore
2          Address               MT         Size
3   <strong>027685409628</strong>   7ffa06f8a198             24
4
5 Statistics:
6         MT Count TotalSize Class Name
7 7ffa06f8a198   1      24 System.Threading.Semaphore
8 Total 1 objects, 24 bytes                  我们看到了吧,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:000> !do 027685409628
2 <strong>Name:      System.Threading.Semaphore
</strong> 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 <strong>00007ffa06fb11484000b7a      8 ...es.SafeWaitHandle0 instance 0000027685409780 _waitHandle
</strong>11 00007ffa06eb70a04000b79      b28      System.IntPtr1   static 0000000000000000 InvalidHandle
12 00000000000000004000b7b       20            SZARRAY0 TLstatict_safeWaitHandlesForRent
13   >> Thread:Value <<                    标红的 000001354f409848 就是 ReaderWriterLock 对象的地址,继续执行【!do 000001354f409848】命令,查看它的详情。
1 0:000> !do 0000027685409780
2 <strong>Name:      Microsoft.Win32.SafeHandles.SafeWaitHandle
</strong> 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 <strong>00007ffa06eb70a0400126e      8      System.IntPtr1 instance 0000000000000290 handle
</strong>11 00007ffa06e41188400126f       10         System.Int321 instance                4 _state
12 00007ffa06e0d0704001270       14       System.Boolean1 instance                1 _ownsHandle
13 00007ffa06e0d0704001271       15       System.Boolean1 instance                1 _fullyInitialized                    _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: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   <strong>Semaphore Count</strong> <strong>2(当前的计数,初始值我们设置的是 1)</strong>
13   <strong>Semaphore Limit 10(这个是极限值,超过会抛出异常)</strong>                    红色标注的 022afb409848 就是 ReaderWriterLock 对象的地址,有了地址,我们执行【!do 022afb409848】命令。
1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0<strong>00000002</strong>`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0ffffa68f`9e3e1ce8 <strong>00000000`0000000a</strong>                    _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: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   <strong>Semaphore Count</strong> <strong>3(上一次是2,此次是3)</strong>
13   Semaphore Limit 10                    继续执行【!clrstack -a】命令,查看托管线程调用栈和所有参数和变量。
1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0<strong>00000003</strong>`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0ffffa68f`9e3e1ce8 00000000`0000000a                    0x0000020b49409628 这个就是 Program 对象地址,我们可以使用【!do 0x0000020b49409628】命令,确认一下。
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                     证明了我们的猜想。我们知道对象的地址指向的是类型句柄,如果想要查看对象头的数据,还要减去 4 或者 8 个字节才是对象头的地址,4 或者 8 是根据系统的位数 32 位就减去 4,64 位就减去 8,从对象的地址也可以看出是该减去 8 还是 4,我的对象地址是 0x0000020b49409628,就要减去 8 了。
                    执行【dp 0x0000020b49409628-0x8 l1】命令,查看对象头的数据。
1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0<strong>0000000a</strong>`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0ffffa68f`9e3e1ce8 00000000`0000000a                    我们看到了对象头的值是 0f78734a,这个值是可以推出来的。我们知道对象的 HashCode 的值是 58225482,这个数字是十进制的结果值,我们转换成十六进制,看看是多少。
1 0:008> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3   2 <strong>0000015A8405C070</strong>            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                    0378734a 这个值和【dp】命令的结果 0f78734a 类似,我们再使用 58225482 十六进制表示 0378734a,分别加上 0x08000000 和 0x04000000,执行命令【? 0378734a++0x08000000+0x04000000】,这个值就是对象头的值。
1 0:008> dt coreclr!AwareLock 0000015A8405C070
2    +0x000<strong> m_lockState      : AwareLock::LockState(这里就说明了 Monitor 底层是 AwareLock)
</strong> 3    +0x004 m_Recursion      : 1
4    +0x008 <strong>m_HoldingThread: 0x0000015a`8404a830 Thread(持有锁的托管线程标识,和 !synck 输出  Owning Thread Info 列的前部分一致)
</strong> 5    +0x010 <strong>m_HoldingOSThreadId : 0x38f0(持有锁的操作系统线程标识,<strong>和 !synck 输出  Owning Thread Info 列的后部分一致</strong>)</strong>
6    +0x018 m_TransientPrecious : 0n1
7    +0x01c <strong>m_dwSyncIndex    :</strong> <strong>0x80000002(同步块的索引值,和 !synck 输出的 Index 值一样)</strong>
8    +0x020<strong> m_SemEvent       : CLREvent(这里说明,底层还是使用了 Event 同步原语,如果在 Windbg 里是可以点击的,这里没办法了)
</strong> 9    +0x030 m_waiterStarvationStartTimeMs : 0x10c6663
10    +0x034 m_emittedLockCreatedEvent : 0n0                    00000000`0f78734a 这个值和【dp】命令的输出是一样的,说明对象头保存是散列码了。
                    我们恢复调试器的执行,直到调试器输出“Press any key to release lock”字样,点击【ctrl+c】组合键,进入中断模式。
                    如图:
                    
                    由于 GC 会执行垃圾回收,内存压缩和对象地址转移,我们避免产生误操作。还是先执行线程切换【~0s】。
1 0:009> !syncblk
2 Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
3   <strong>2</strong> <strong>00000217A549CE10</strong>            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                    我们执行【!clrstack -a】命令查看托管线程调用栈,查找我们的Program 对象。
1 0:009> dt coreclr!AwareLock 00000217A549CE10
2    +0x000<strong> m_lockState      : AwareLock::LockState(底层的 awarelock)
</strong> 3    +0x004 m_Recursion      : 1
4    +0x008 <strong>m_HoldingThread: 0x00000217`a54963a0 Thread(持有锁的线程的标识,<strong>也就是!syncblk 命令输出的 Owning Thread Info 列的值前部分(00000217A54963A0)</strong>)
</strong> 5    +0x010 m_HoldingOSThreadId : 0x26c<strong>(持有锁的操作系统线程标识</strong><strong>,<strong>也就是!syncblk 命令输出的 Owning Thread Info 列的值后部分(26c)</strong></strong>)
6    +0x018 m_TransientPrecious : 0n1
7    +0x01c <strong>m_dwSyncIndex    :</strong> <strong>0x80000002(这个就是同步块索引,也就是!syncblk 命令输出的 Index 列的值)</strong>
8    +0x020<strong> m_SemEvent       : CLREvent<strong>(底层还是使用的 Event 实现同步)</strong>
</strong> 9    +0x030 m_waiterStarvationStartTimeMs : 0xf4b013
10    +0x034 m_emittedLockCreatedEvent : 0n0                    0x0000020c1a409628 这个地址就是我们的 Program对象的地址,我们可以使用【!DumpObj 0x0000020c1a409628】命令确认一下。
1 0:009> dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))
2 (*((coreclr!CLREvent *)0x217a549ce30))               
3   [+0x000] <strong>m_handle         : 0x314 (这里是一个句柄)
</strong>4   [+0x008] m_dwFlags      : 0xd                     我们现在就可以查看对象头中的内容了。执行命令【dp 0x0000020c1a409628-0x8 l1】,由于我的程序是64位的,所以需要减去 8,32位减去4就可以了。
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 <strong>    Event Type Auto Reset
</strong>13   Event is Waiting                    由于内容太多了,需要创建同步块存储内容,所以在对象头中就存储同步块的索引了。08000000 表示是同步块,1 表示同步块在同步块表中的索引位置。
                    此时,我们可以使用【!syncblk 0x1】命令查看同步块的信息了。
1 0:003> !DumpHeap -type ReaderWriterLock
2          Address               MT   Size
3 <strong>000001354f409848</strong> 00007ff9e50c75e8       56
4
5 Statistics:
6               MT    Count    TotalSize Class Name
7 00007ff9e50c75e8      1         56 System.Threading.ReaderWriterLock
8 Total 1 objects
                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch Excutable】,加载我们的项目文件 ExampleCore_6_7.exe,进入到调试器后,我们使用【g】命令直接运行调试器,直到控制台程序输出“Press any key to acquire lock”字样。我们回到调试器界面,点击【Break】按钮,进入中断模式,开始我们的调试旅程。
                    由于我们手动中断,所以必须切换到托管线程上下文中,因为当前在调试器的上下文环境中,执行命令【~0s】切换线程上下文。
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 00007ff9e4f593f0400001d      8          System.Void0 instance 0000000000000000<strong> _readerEvent
</strong>11 00007ff9e4f593f0400001e       10          System.Void0 instance 0000000000000000<strong> _writerEvent
</strong>12 00007ff9e4f7a5f0400001f       18         System.Int641 instance                1<strong> _lockID
</strong>13 00007ff9e4f511884000020       20         System.Int321 instance                0<strong> _state
</strong>14 00007ff9e4f511884000021       24         System.Int321 instance               -1<strong> _writerID
</strong>15 00007ff9e4f511884000022       28         System.Int321 instance                1 _writerSeqNum
16 00007ff9e4f767b84000023       2c      System.UInt161 instance                0<strong> _writerLevel
</strong>17 00007ff9e4f51188400001b       58         System.Int321   static            500 DefaultSpinCount
18 00007ff9e4f7a5f0400001c       50         System.Int641   static                1 s_mostRecentLockID                    继续执行【!clrstack -a】命令,查看托管线程调用栈和所有参数。
1 0:006> !DumpHeap -type ReaderWriterLock
2          Address               MT         Size
3   <strong>022afb409848</strong>   7ffa021b7788             56
4
5 Statistics:
6         MT Count TotalSize Class Name
7 7ffa021b7788   1      56 System.Threading.ReaderWriterLock
8 Total 1 objects, 56 bytes                    红色标注的地址就是 0x000001c4c6409628 就是 Program 类型对象的地址,我们可以使用【!do 0x000001c4c6409628】命令验证。
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 00007ffa020493f0400001d      8          System.Void0 instance 0000000000000000<strong> _readerEvent
</strong>11 00007ffa020493f0400001e       10          System.Void0 instance 0000000000000000<strong> _writerEvent
</strong>12 00007ffa0206a5f0400001f       18         System.Int641 instance                1<strong> _lockID
</strong>13 00007ffa020411884000020       20         System.Int321 instance                0<strong> _state
</strong>14 00007ffa020411884000021       24         System.Int321 instance               -1<strong> _writerID
</strong>15 00007ffa020411884000022       28         System.Int321 instance                1 _writerSeqNum
16 00007ffa020667b84000023       2c      System.UInt161 instance                0<strong> _writerLevel
</strong>17 00007ffa02041188400001b       58         System.Int321   static            500 DefaultSpinCount
18 00007ffa0206a5f0400001c       50         System.Int641   static                1 s_mostRecentLockID                    继续使用【dp 0x000001c4c6409628-0x8 l1】命令,查看对象头的数据。
1 0:009> ~0s
2 ntdll!NtWriteFile+0x14:
3 00007ffd`ece6d0e4 c3            ret                    对象头的当前值 0f78734a,表示在对象头中保存的是散列码,我们控制台程序散列码的输出值是 58225482,这个数字是十进制的,我们转换为十六进制,看看结果。
1 0:000> !clrstack -a
2 OS Thread Id: 0x1c20 (0)
3         Child SP               IP Call Site
4 0000003E0397E0E0 00007ffdece6d0e4
5 0000003E0397E0E0 00007ffdc9b87d6b
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 = <strong>0x0000020b49409628</strong>
18
19 0:000>                    我们看到了十进制的 58225482 转换为十六进就是  0378734a,0x08000000 这个掩码只能确定是不是散列码,也有可能是同步块索引,只有在加上一个 0x04000000 掩码才能确定是散列码,所以,我们使用执行【? 00000000`0378734a+0x08000000+0x04000000】命令,这个结果就是对象头的值。

1 0:000> !do 0x0000020b49409628
2 <strong>Name:      ExampleCore_6_7.Program
</strong>3 MethodTable: 00007ffcc6f00100
4 EEClass:   00007ffcc6eefb48
5 Tracked Type: false
6 Size:      24(0x18) bytes
7 File:      E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None                    0f78734a 这个值和【dp】命令的输出是一样的,说明对象头保存是散列码了。
                    我们恢复调试器的执行,直到控制台程序输出“Press any key to release lock”字样,回到调试器,点击【Break】按钮,继续进入中断模式。如图:
                    
                    我们继续执行【dp 0x000001c4c6409628-0x8 l1】命令,看看对象头的输出。说明一下,在执行此命令之前,最好执行一次【!clrstack -a】命令获取对象地址,然后执行【!do】命令确认对象,最后在执行这个【dp】命令,因为垃圾收集器会在任意时刻移动对象,对象的地址也可能变化。
1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620<strong>0f78734a</strong>`00000000                    08000001 这个结果值就很合理了,就是同步块索引了。此时,我们可以使用【!syncblk 0x1】命令查看同步块的信息了。
1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`<strong>0378734a</strong>
        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> ? 0378734a++0x08000000+0x04000000
2 Evaluate expression: 259552074 = <strong>00000000`0f78734a</strong>                    0x000001c613c09628 就是 Program 类型对象地址,执行【!do 0x000001c613c09628】命令验证一下。
1 0:002> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3            ret                    执行【dp 0x000001c613c09628-8 l1】命令查看对象头的内容。
1 0:000> !clrstack -a
2 OS Thread Id: 0x1e14 (0)
3         Child SP               IP Call Site
4 000000EB377AE170 00007ff942b0d0a4
5 000000EB377AE170 00007ff91b2076eb
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 = <strong>0x0000020c1a409628</strong>                    0 就是表示没有任何值。继续【g】恢复调试器的执行,直到调试器输出,如图:
                    
                    继续执行切换线程和查看线程的命令,分别是【~0s】、【!clrstack -a】查找我们的 Program 对象。
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                    继续执行【!do 0x000001c613c09628】命令,查看内容。
1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a40962008000001`00000000                    ThinLock owner 1 (000001C60F9C8A80), Recursive 0 说明对象上有了一个瘦锁,线程对象的 ID 是 000001C60F9C8A80,递归技术是 0。
                    继续执行【dp 0x000001c613c09628-8 l1】命令,查看对象头。

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 
                    这里的 1 就是持有锁的线程 ID,是托管线程的 ID 值。可以使用【!t】或者【!threads】命令验证。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ffd`ece6d0a4 c3            ret                    【dp】命令和【!t】命令都能找到 000001C60F9C8A80 这个指针的值。
                    我们继续【g】恢复调试器的执行,直到调试器输出如图:
                    
                    此时,说明对象的锁和散列值都保存了,然后我们【ctrl+c】进入中断模式,切换线程【~0s】,并且执行【!clrstack -a】命令查找 Program 对象,查一下它的状态。
1 0:000> !clrstack -a
2 OS Thread Id: 0x1138 (0)
3         Child SP               IP Call Site
4 00000026DAD7E8A0 00007ffdece6d0a4
5 00000026DAD7E8A0 00007ffd667676eb
6 。。。。。。(省略无用的)
7
8 00000026DAD7EBF0 00007ffcc0fa1aa0 ExampleCore_6_7.Program.Run()
9   PARAMETERS:
10         this (0x00000026DAD7EC60) = 0x000001c4c6409628
11   LOCALS:
12         0x00000026DAD7EC48 = 0x000000000378734a
13
14 00000026DAD7EC60 00007ffcc0fa1988 ExampleCore_6_7.Program.Main(System.String[])
15   PARAMETERS:
16         args (0x00000026DAD7ECA0) = 0x000001c4c6408e90
17   LOCALS:
18         0x00000026DAD7EC88 = <strong>0x000001c4c6409628</strong>                    执行【!do 0x000001c613c09628】命令,查看一下该对象有什么变化吗?
1 0:002> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3            ret(这里没有东西了,锁信息已经转到同步块中保存了。)                    继续执行【dp 0x000001c613c09628-8 l1】命令,查看一下对象头保存的数据。
1 0:000> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c64096200f78734a`00000000                    08000001 看到这个值就知道是同步块索引了。我们使用【!syncblk】命令查看同步块的数据。
1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a                    我们也可以使用【!DumpHeap -thinlock】命令查找托管堆上所有具有瘦锁的对象。
1 0:000> ? 00000000`0378734a+0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000`<strong>0f78734a</strong>                    内容很简单,就不解释了。

                2)、Windbg Preview 调试
                    编译项目,打开【Windbg Preview】,依次点击【文件】---【Launch executable】,加载我们的控制台项目 ExampleCore_6_8.exe,点击【打开】进入调试器。
                    进入调试器后,直接执行【g】命令,运行调试器,直到我们的控制台程序输出“Press any key to acquire lock”,此时,回到调试器,点击【Break】按钮,进入到中断模式,开始我们的调试。
                    由于我们是手动中断的,当前是调试器的上下文,需要切换到托管上下文中,需要执行【~0s】命令。
1 0:001> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620<strong>08000001</strong>`00000000                    我们使用【!clrstack -a】命令,查看托管线程调用栈,找出我们的 Program 类型的局部变量 program。
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(在同步块表中多少个同步块)                    0x000001ace3409628 就是Program 类型的实例对象的地址,我们可以使用【!do 0x000001ace3409628】来验证。
1 0:000> !clrstack -a
2 OS Thread Id: 0x16d4 (0)
3         Child SP               IP Call Site
4 000000B881DDE628 00007ff942b0e814 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 = <strong>0x000001c613c09628</strong>                    我们执行命令【dp 0x000001ace3409628-8 l1】查看它的对象头。
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                    00000000`00000000 表示没有任何数据。
                    我们【g】恢复调试器的执行,直到控制台程序输出“Press any key to get hashcode”,此时,对象已经获取了锁,但是还没有获取散列值。回调调试器中,点击【Break】按钮,再次进入中断模式,继续我们的调试。
                    由于手动进入中断模式,所以需要有调试器上下文切换到托管线程上下文中,执行命令【~0s】。
1 0:001> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620<strong>08000001</strong>`00000000                    继续执行【!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
9 000000B881DDE260 00007ff91e0b7d6b
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 = <strong>0</strong><strong>x000001c613c09628</strong>                    0x000001ace3409628 这就是我们的 Program 类型实例的地址,可以执行【!do 0x000001ace3409628】命令来验证,我就省略了。
                    此时,该对象已经获取锁了,我们查看对象头的数据,执行【dp 0x000001ace3409628-8 l1】命令。
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 <strong>ThinLock owner 1 (000001C60F9C8A80), Recursive 0</strong>                    00000001 这个就是所有者线程的 ID,此时我们可以执行【!do 0x000001ace3409628】或者【!DumpObj 0x000001ace3409628】命令,查看Program 对象,也有体现。
1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620<strong>00000001</strong>`00000000                    红色标注的告诉我们 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
9DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
10    0    1   16d4 <strong>000001C60F9C8A80</strong>    2a020 Preemptive000001C613C13D60:000001C613C14660 000001C60F9C0540 -00001 MTA
11    6    2   2724 000001C60FA11810    21220 Preemptive0000000000000000:0000000000000000 000001C60F9C0540 -00001 Ukn (Finalizer)                    我们看到了【!do】命令和【!t】命令的输出线程ID都是 000001ACDEFF2770,在对象头中包含了持有锁的线程 ID。
                    接下来,我们执行代码,获取散列码,再次中断执行,查看同步块和瘦锁的状态。
                    【g】继续运行,直到我们的控制台程序输出“HashCode:58225482 Press any key to release lock”。此时已经有了锁,并且也获取了散列码。回到调试器,点击【Break】按钮,进入中断模式,继续调试。
                    继续切换线程上下文【~0s】,并执行【!clrstack -a】命令查找我们的 Program 对象。
1 0:000> !clrstack -a
2 OS Thread Id: 0x16d4 (0)
3         Child SP               IP Call Site
4 000000B881DDE630 00007ff942b0d0a4
5 000000B881DDE630 00007ff91e0b76eb
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 = <strong>0x000001c613c09628</strong>
18
19 0:000>                    执行【!do 0x000001ace3409628】命令,查看 Program 对象。
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<strong>(这里没有东西了,锁信息已经转到同步块中保存了。)</strong>                    继续执行【dp 0x000001ace3409628-8 l1】命令,查看对象头。
1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620<strong>08000001</strong>`00000000                    08000001 说明现在已经在使用同步块保存数据了,索引值是 1。
                    我们使用【!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 (<strong>000001C60F9C8A80</strong>) Recursive 0
4 Found 1 objects.                    很简单,就不多说了。

    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:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3            ret                    其实,我们从红色标注的可以看出一些端倪,OS Thread Id: 0x1d7c (4) 4号托管线程执行源码 ExampleCore_6_9.Program+c.b__2_0() 这个代码时,调用同步原语 Monitor 的 System.Threading.Monitor.ReliableEnter 方法想进入,却没进入,处于等待,因为后面没有调用栈了。说明一下,Windbg Preview 是可以显示源码行号的,可以直到在哪里处于等待,但是在 NTSD 是没有的。
                    OS Thread Id: 0x1130 (6) 的 6 号托管线程执行源码 ExampleCore_6_9.Program+c.b__2_1() 时调用了 System.Threading.Monitor.ReliableEnter 方法,想获取锁,由于后面没有执行,所以也是出于等待状态。
                    此时,我们知道他们都是处于等待状态,虽然输出的信息很简单,但是它却展示了一种常见的死锁识别技术。
                    这个输出的信息有点多,其实我们还可以使用另外一个命令,【!syncblk】查看同步快表的数据,也能看出一些信息。
1 0:000> !clrstack -a
2 OS Thread Id: 0x34b8 (0)
3         Child SP               IP Call Site
4 0000006CE77EE100 00007ff942b0d0a4
5 0000006CE77EE100 00007ff8bcf376eb
6 。。。。。。(省略了)
7
8 0000006CE77EE450 00007ff80e731a52 ExampleCore_6_8.Program.Run()
9   PARAMETERS:
10         this (0x0000006CE77EE4D0) = 0x000001ace3409628
11   LOCALS:
12         0x0000006CE77EE4B8 = 0x0000000000000000
13
14 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[])
15   PARAMETERS:
16         args (0x0000006CE77EE510) = 0x000001ace3408e90
17   LOCALS:
18         0x0000006CE77EE4F8 = <strong>0x000001ace3409628</strong>                    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:000> !do 0x000001ace3409628
2 <strong>Name:      ExampleCore_6_8.Program
</strong>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                    我们继续执行【!clrstack -a】命令,查看一下调用栈的局部变量,主要观察 Person 和 Student 。
1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620<strong>00000000`00000000</strong>                    0x0000026a14010a28 和 0x0000026a14010a40 就是我们的 ExampleCore_6_9.Person 对象和 ExampleCore_6_9.Student 对象,我们可以执行【!do 0x0000026a14010a28】和【!do 0x0000026a14010a40】命令来确认它们。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3            ret                    我们在分别查看一下这两个对象的对象头包含了什么数据,执行命令【dp 0x0000026a14010a28-8 l1】和【dp 0x0000026a14010a40-8 l1】。
1 0:000> !clrstack -a
2 OS Thread Id: 0x34b8 (0)
3         Child SP               IP Call Site
4 0000006CE77EE100 00007ff942b0d0a4
5 0000006CE77EE100 00007ff8bcf376eb
6 。。。。。。(省略了)
7 0000006CE77EE450 00007ff80e731a78 ExampleCore_6_8.Program.Run()
8   PARAMETERS:
9         this (0x0000006CE77EE4D0) = 0x000001ace3409628
10   LOCALS:
11         0x0000006CE77EE4B8 = 0x0000000000000000
12
13 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[])
14   PARAMETERS:
15         args (0x0000006CE77EE510) = 0x000001ace3408e90
16   LOCALS:
17         0x0000006CE77EE4F8 = <strong>0x000001ace3409628</strong>                    说明它们都使用了同步块保存数据和锁信息了。此时,可以再使用【!syncblk】命令查看同步块表的数据,上面已经执行,此处省略。
                    以下就简单了,根据我们的代码查找问题吧。
                    
                2)、Windbg Preview 调试
                    编译项目,然后直接运行我们的 EXE 可执行程序,我们的程序输出如图:
                    
                    然后,打开【Windbg Preview】,依次点击【文件】----【Attach to Process】,附加我们的进程,进入调试器,我们先把进程中所有线程转储出来看看,执行【~*e!clrstack】命令。
1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620<strong>00000001</strong>`00000000                    【~*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: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 <strong>ThinLock owner 1 (000001ACDEFF2770), Recursive 0</strong>                    我们看到了 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:000> !t
2 ThreadCount:      2
3 UnstartedThread:0
4 BackgroundThread: 1
5 PendingThread:    0
6 DeadThread:       0
7 Hosted Runtime:   no
8                                                                                                             Lock
9DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
10    0    1   34b8 <strong>000001ACDEFF2770</strong>    2a020 Preemptive000001ACE3412F38:000001ACE3414660 000001acdf03e270 -00001 MTA
11    5    2   419c 000001ACDF01B210    21220 Preemptive0000000000000000:0000000000000000 000001acdf03e270 -00001 Ukn (Finalizer)                     OS Thread Id: 0x29c (0) 这个就是 0 号主线程,它执行了 Main 方法,又执行 System.Threading.Monitor.Enter 方法,处于挂起的状态,其他线程没有任何有用信息。
                    我们的被锁的对象是 ExampleCore_6_10.DBWrapper,又是在主线程出的问题,我们就去主线程上找一下 DBWrapper 对象。
                    执行命令【~0s】切换到主线程。
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
9 0000006CE77EE100 00007ff8bcf376eb
10 。。。。。。(省略了)
11 0000006CE77EE450 00007ff80e731ae8 ExampleCore_6_8.Program.Run()
12   PARAMETERS:
13         this (0x0000006CE77EE4D0) = 0x000001ace3409628
14   LOCALS:
15         0x0000006CE77EE4B8 = 0x000000000378734a
16
17 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[])
18   PARAMETERS:
19         args (0x0000006CE77EE510) = 0x000001ace3408e90
20   LOCALS:
21         0x0000006CE77EE4F8 = <strong>0x000001ace3409628</strong>                    继续执行【!dumpstackobjects】命令。
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(这里没有任何信息了,已经移到同步块中了)                    ExampleCore_6_10.DBWrapper 类型的地址是 000002cf3c009630,执行【dp 000002cf3c009630-8 l1】命令查看一下它的对象头。
1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620<strong>08000001</strong>`00000000                    说明对象头已经创建同步块了,索引值是 2,所以我们执行【!syncblk 2】命令查看一下同步块的数据。
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                    说明 XXX 号线程持有 ExampleCore_6_10.DBWrapper 类型,也可以说 XXX 线程拥有  ExampleCore_6_10.DBWrapper 的锁。XXX 表示的是调试器线程的ID,0 表示操作系统线程的 ID。
                    XXX 的含义就是,CLR 无法将操作系统线程的 ID 映射到调试器线程,出现这样情况的一个原因是,某个线程在某个时刻获取一个对象的锁,然后,这个线程消失了,却没有释放锁。
                    我们可以执行【!t】或者【!threads】命令验证 XXX 的说法。
1 0:000> !DumpHeap -thinlock
2         Object         Thread               OSId      Recursion
3   01ace3412ec0   01acdeff2770             0x34b8            0                    只要没有执行终结操作,即使处于死亡状态的线程也会被输出。
                    到这里就差不多了,我们还需要结合代码和调试器一起来找问题,很简单,我直接贴图了。
                    
                    图上说的很情况,就不多解释了。

                2)、Windbg Preview 调试
                    编译项目,直接双击我们项目的 EXE 可执行程序,直到我们的控制台程序有如图输出:
                    
                    我们打开【Windbg Preview】,依次点击【文件】---【Attach to process】,在右侧选择我们运行的程序,点击【附加】,附加我们的进程,进入调试器,开始我们的调试了。
                    我们先执行【~*e!clrstack】命令,查看一下所有线程的调用栈是什么情况。
1 0:007> ~*e!clrstack
2 OS Thread Id: 0x2860 (0)
3         Child SP               IP Call Site
4 000000FBA677E2B0 00007ff8a9c8d0a4
5 000000FBA677E2B0 00007ff8961676eb
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
28 <strong>OS Thread Id: 0x1d7c (4)4号托管线程的调用栈---》执行---》<strong>System.Threading.Monitor.ReliableEnter</strong>(说明在这里等待了)
</strong>29         Child SP               IP Call Site
30 000000FBA737F098 00007ff8a9c8db34 <strong>System.Threading.Monitor.ReliableEnter</strong>(System.Object, Boolean ByRef)
31 000000FBA737F1F0 00007FFF81B724DE <strong>ExampleCore_6_9.Program+<>c.<Main>b__2_0()(NTSD 没有显示源码行号,Windbg Preview是有的,更容易调试)
</strong>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
37 OS Thread Id: 0x2444 (5)
38         Child SP               IP Call Site
39 000000FBA638F418 00007ff8a9c8db34 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
43 <strong>OS Thread Id: 0x1130 (6)(6号托管线程的调用栈)---》执行--》<strong>System.Threading.Monitor.ReliableEnter(说明在这里等待了,没有进入)</strong>
</strong>44         Child SP               IP Call Site
45 000000FBA74FF258 00007ff8a9c8db34 <strong>System.Threading.Monitor.ReliableEnter</strong>(System.Object, Boolean ByRef)
46 000000FBA74FF3B0 00007FFF81B7215E <strong>ExampleCore_6_9.Program+<>c.<Main>b__2_1()(源码的调用位置,NTSD 没显示行号,Windbg Preview 是有行号的,更易调试)
</strong>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
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                    我们从命令的输出中可以看到,有用的信息不多,红色标注的就是主线程的运行情况。我们发现 0 号线程,也就是主线程在执行 System.Threading.Monitor.Enter 方法时挂起了,不执行了,问题大概也就是在这里。
                    既然主线程有了问题,我们就切换到主线程看看情况,执行命令【~0s】。
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
9 0000006CE77EE100 00007ff8bcf376eb
10 。。。。。。(省略了)
11 0000006CE77EE450 00007ff80e731ae8 ExampleCore_6_8.Program.Run()
12   PARAMETERS:
13         this (0x0000006CE77EE4D0) = 0x000001ace3409628
14   LOCALS:
15         0x0000006CE77EE4B8 = 0x000000000378734a
16
17 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[])
18   PARAMETERS:
19         args (0x0000006CE77EE510) = 0x000001ace3408e90
20   LOCALS:
21         0x0000006CE77EE4F8 = <strong>0x000001ace3409628</strong>                    我们执行【!dumpstackobjects】命令,找到我们要分析的对象 DBWrapper。
1 0:007> ~4s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3            ret                    ExampleCore_6_10.DBWrapper 就是我们要找的对象,它的地址是 02cf3c009630,我们执行【dp 02cf3c009630-8 l1】命令查看该对象的对象头包含的是什么东西。
1 0:004> !clrstack -a
2 OS Thread Id: 0x1d7c (4)
3         Child SP               IP Call Site
4 000000FBA737F098 00007ff8a9c8db34 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 = <strong>0x0000026a14010a28(这个就是我们的 ExampleCore_6_9.Person 对象)</strong>
10         0x000000FBA737F320 = 0x0000000000000001
11         0x000000FBA737F2F8 = 0x0000000000000000
12         0x000000FBA737F2F0 = <strong>0x0000026a14010a40(这个就是我们的 ExampleCore_6_9.Student 对象)</strong>
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                     08000002 说明对象头已经创建了一个同步块了,索引值是 2,我们查看同步块,执行命令【!syncblk 2】。
1 0:004> !do 0x0000026a14010a28
2 Name:      <strong>ExampleCore_6_9.Person
</strong> 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:      <strong>ExampleCore_6_9.Student
</strong>13 MethodTable: 00007fff81c73930
14 EEClass:   00007fff81c3c5f8
15 Tracked Type: false
16 Size:      24(0x18) bytes
17 File:      E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
18 Fields:
19 None                    输出信息告诉我们 ExampleCore_6_10.DBWrapper 对象已经被锁定了,被 XXX 线程锁定的。XXX 表示的是调试器的线程 ID,0 表示的是操作系统线程的 ID。
                    XXX 表示 CLR 无法将操作系统线程的 ID 无法映射到调试器线程。出现这种情况的原因是,这个线程在某个时刻获取了该对象上的锁,然后这个线程消失了但是却没有释放锁。
                    我们执行【!t】或者【!threads】命令验证这一点。
1 0:004> dp 0x0000026a14010a28-8 l1
2 0000026a`14010a2008000005`00000000
3
4 0:004> dp 0x0000026a14010a40-8 l1
5 0000026a`14010a3808000004`00000000                    只要没有执行终结操作,即使处于死亡状态的线程也会被出输出。
                    要分析具体是哪里的错误,肯定要结合代码来分析。我们的代码是这里出问题了,如图:
                    
                    代码很简单,就不多说了。
                    
        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 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。

来源:https://www.cnblogs.com/PatrickLiu/p/18155084
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Advanced .Net Debugging 8:线程同步