一、介绍
这是我的《Advanced .Net Debugging 》这个系列的第八篇文章。这篇文章的内容是原书的第二部分的【调试实战】的第六章【同步】。我们经常写一些多线程的应用程序,写的多了,有关多线程的问题出现的也就多了,因此,最迫切的任务就是提高解决多线程同步问题的能力。这一节我们将从本质上、从底层上来介绍线程的同步组件和同步原理,也会给出在多线程环境下如何解决问题的最佳实践。高级调试会涉及很多方面的内容,你对 .NET 基础知识掌握越全面、细节越底层,调试成功的几率越大,当我们遇到各种奇葩问题的时候才不会手足无措。
如果在没有说明的情况下,所有代码的测试环境都是 Net 8.0,如果有变动,我会在项目章节里进行说明。好了,废话不多说,开始我们今天的调试工作。
调试环境我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10
** 调试工具:Windbg Preview(Debugger Client:1.2306.1401.0,Debugger engine:10.0.25877.1004)和 NTSD(10.0.22621.2428 AMD64)** 下载地址:可以去Microsoft Store 去下载 ** 开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3**
** Net 版本:.Net 8.0**
** CoreCLR源码:源码下载**
**在此说明:**我使用了两种调试工具,第一种:Windbg Preivew,图形界面,使用方便,操作顺手,不用担心干扰;第二种是:NTSD,是命令行形式的调试器,在命令使用上和 Windbg 没有任何区别,之所以增加了它的调试过程,不过是我的个人爱好,想多了解一些,看看他们有什么区别,为了学习而使用的。如果在工作中,我推荐使用 Windbg Preview,更好用,更方便,也不会出现奇怪问题(我在使用 NTSD 调试断点的时候,不能断住,提示内存不可读,Windbg preview 就没有任何问题)。
如果大家想了解调试过程,二选一即可,当然,我推荐查看【Windbg Preview 调试】。
二、目录结构
为了让大家看的更清楚,也为了自己方便查找,我做了一个目录结构,可以直观的查看文章的布局、内容,可以有针对性查看。
1、同步的基础知识
A、基础知识
B、眼见为实
[1)、KD 和 NTSD 调试](#1)、KD 和 NTSD 调试)
[2)、Windbg Preview 调试](#2)、Windbg Preview 调试)
2、线程同步原语
2.1、事件同步原语(内核锁)
A、基础知识
B、眼见为实
1)、[KD 和](#KD 和) [NTSD 调试](#NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
2.2、互斥体(内核锁)
A、基础知识
B、眼见为实
1)、[KD 和 NTSD 调试](#KD 和 NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
2.3、信号量(内核锁)
A、基础知识
B、眼见为实
1)、[KD 和 NTSD 调试](#KD 和 NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
2.4、监视器
A、基础知识
B、眼见为实
1)、[NTSD 调试](#NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
2.5、读写锁
A、基础知识
B、眼见为实
1)、[NTSD 调试](#NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
2.6、线程池
3、同步的内部细节
3.1、对象头
3.2、同步块
A、基础知识
B、眼见为实
1)、[NTSD 调试](#NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
3.3、瘦锁
A、基础知识
B、眼见为实
1)、[NTSD 调试](#NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
4、同步任务
4.1、死锁
A、基础知识
B、眼见为实
1)、[NTSD 调试](#NTSD 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
4.2、孤立锁:异常
A、基础知识
B、眼见为实
1)、[NTDS 调试](#NTDS 调试)
2)、[Windbg Preview 调试](#Windbg Preview 调试)
4.3、线程中止
4.4、终结器挂起
三、调试源码
废话不多说,本节是调试的源码部分,没有代码,当然就谈不上测试了,调试必须有载体。
3.1、ExampleCore_6_1
1 namespace ExampleCore_6_1
2 {
3 internal class Program
4 {
5 static void Main(string[] args)
6 {
7 var thread = new Thread(() =>
8 {
9 Console.WriteLine($"tid={Environment.CurrentManagedThreadId}");
10 Console.ReadLine();
11 });
12
13 thread.Start();
14
15 Console.ReadLine();
16 }
17 }
18 }
View Code
3.2、ExampleCore_6_2
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_2
4 {
5 internal class Program
6 {
7 static void Main(string[] args)
8 {
9 while (true)
10 {
11 Console.WriteLine("选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)");
12 var myword= Console.ReadLine();
13 if (string.Compare(myword, "Manual", true) == 0)
14 {
15 RunManualResetEvent();
16 }
17 else if (string.Compare(myword, "Auto", true) == 0)
18 {
19 RunAutoResetEvent();
20 }
21 else if (string.Compare(myword, "Exit", true) == 0)
22 {
23 break;
24 }
25 }
26 }
27
28 static void RunManualResetEvent()
29 {
30 ManualResetEvent? mre = new ManualResetEvent(false);
31
32 Console.WriteLine($"mre 默认为 false,即等待状态,请查看!");
33 Debugger.Break();
34
35 mre.Set();
36 Console.WriteLine($"mre 默认为 true,即放行状态,请查看!");
37 Debugger.Break();
38
39 mre.Reset();
40 Console.WriteLine($"mre Reset 后为 false,即等待状态,请查看!");
41 Debugger.Break();
42
43 mre = null;
44 }
45
46 static void RunAutoResetEvent()
47 {
48 AutoResetEvent? mre = new AutoResetEvent(false);
49
50 Console.WriteLine($"are 默认为 false,即等待状态,请查看!");
51 Debugger.Break();
52
53 mre.Set();
54 Console.WriteLine($"are 默认为 true,即放行状态,请查看!");
55 Debugger.Break();
56
57 mre.Reset();
58 Console.WriteLine($"are Reset 后为 false,即等待状态,请查看!");
59 Debugger.Break();
60
61 mre = null;
62 }
63 }
64 }
View Code
3.3、ExampleCore_6_3
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_3
4 {
5 internal class Program
6 {
7 private static Mutex mut = new Mutex();
8
9 static void Main()
10 {
11 UseResource();
12 }
13
14 private static void UseResource()
15 {
16 // 等到安全进入。
17 mut.WaitOne();
18
19 Console.WriteLine("已进入保护区");
20
21 Debugger.Break();
22
23 Console.WriteLine("正在离开保护区");
24
25 // 释放互斥锁。
26 mut.ReleaseMutex();
27
28 Debugger.Break();
29 }
30 }
31 }
View Code
3.4、ExampleCore_6_4
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_4
4 {
5 internal class Program
6 {
7 public static Semaphore sem = new Semaphore(1, 10);
8 static void Main(string[] args)
9 {
10 for (int i = 0; i < int.MaxValue; i++)
11 {
12 sem.Release();
13 Console.WriteLine("查看当前的 sem 值。");
14 Debugger.Break();
15 }
16 }
17 }
18 }
View Code
3.5、ExampleCore_6_5
1 using System.Diagnostics;
2
3 namespace ExampleCore_6_5
4 {
5 internal class Program
6 {
7 public static Person person = new Person();
8
9 static void Main(string[] args)
10 {
11 Task.Run(() =>
12 {
13 lock (person)
14 {
15 Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中 111111");
16 Debugger.Break();
17 }
18 });
19 Task.Run(() =>
20 {
21 lock (person)
22 {
23 Console.WriteLine($"{Environment.CurrentManagedThreadId} 已进入 Person 锁中 222222");
24 Debugger.Break();
25 }
26 });
27 Console.ReadLine();
28 }
29 }
30
31 public class Person
32 {
33 }
34 }
View Code
3.6、ExampleCore_6_6
1 namespace ExampleCore_6_6
2 {
3 internal class Program
4 {
5 private static ReaderWriterLock rwl = new ReaderWriterLock();
6 // Define the shared resource protected by the ReaderWriterLock.
7 static int resource = 0;
8
9 const int numThreads = 1;
10 static bool running = true;
11
12 // Statistics.
13 static int readerTimeouts = 0;
14 static int writerTimeouts = 0;
15 static int reads = 0;
16 static int writes = 0;
17
18 static void Main(string[] args)
19 {
20 Thread[] t = new Thread[numThreads];
21 for (int i = 0; i < numThreads; i++)
22 {
23 t[i] = new Thread(new ThreadStart(ThreadProc));
24 t[i].Name = new String((char)(i + 65), 1);
25 t[i].Start();
26 if (i > 10)
27 Thread.Sleep(300);
28 }
29
30 // Tell the threads to shut down and wait until they all finish.
31 running = false;
32 for (int i = 0; i < numThreads; i++)
33 t[i].Join();
34
35 // Display statistics.
36 Console.WriteLine("\n{0} reads, {1} writes, {2} reader time-outs, {3} writer time-outs.",
37 reads, writes, readerTimeouts, writerTimeouts);
38 Console.Write("Press ENTER to exit... ");
39 Console.ReadLine();
40 }
41
42 static void ThreadProc()
43 {
44 Random rnd = new Random();
45
46 // Randomly select a way for the thread to read and write from the shared
47 // resource.
48 while (running)
49 {
50 double action = rnd.NextDouble();
51 if (action < .8)
52 ReadFromResource(10);
53 else if (action < .81)
54 ReleaseRestore(rnd, 50);
55 else if (action < .90)
56 UpgradeDowngrade(rnd, 100);
57 else
58 WriteToResource(rnd, 100);
59 }
60 }
61
62 // Request and release a reader lock, and handle time-outs.
63 static void ReadFromResource(int timeOut)
64 {
65 try
66 {
67 rwl.AcquireReaderLock(timeOut);
68 try
69 {
70 // It is safe for this thread to read from the shared resource.
71 Display("reads resource value " + resource);
72 Interlocked.Increment(ref reads);
73 }
74 finally
75 {
76 // Ensure that the lock is released.
77 rwl.ReleaseReaderLock();
78 }
79 }
80 catch (ApplicationException)
81 {
82 // The reader lock request timed out.
83 Interlocked.Increment(ref readerTimeouts);
84 }
85 }
86
87 // Request and release the writer lock, and handle time-outs.
88 static void WriteToResource(Random rnd, int timeOut)
89 {
90 try
91 {
92 rwl.AcquireWriterLock(timeOut);
93 try
94 {
95 // It's safe for this thread to access from the shared resource.
96 resource = rnd.Next(500);
97 Display("writes resource value " + resource);
98 Interlocked.Increment(ref writes);
99 }
100 finally
101 {
102 // Ensure that the lock is released.
103 rwl.ReleaseWriterLock();
104 }
105 }
106 catch (ApplicationException)
107 {
108 // The writer lock request timed out.
109 Interlocked.Increment(ref writerTimeouts);
110 }
111 }
112
113 // Requests a reader lock, upgrades the reader lock to the writer
114 // lock, and downgrades it to a reader lock again.
115 static void UpgradeDowngrade(Random rnd, int timeOut)
116 {
117 try
118 {
119 rwl.AcquireReaderLock(timeOut);
120 try
121 {
122 // It's safe for this thread to read from the shared resource.
123 Display("reads resource value " + resource);
124 Interlocked.Increment(ref reads);
125
126 // To write to the resource, either release the reader lock and
127 // request the writer lock, or upgrade the reader lock. Upgrading
128 // the reader lock puts the thread in the write queue, behind any
129 // other threads that might be waiting for the writer lock.
130 try
131 {
132 LockCookie lc = rwl.UpgradeToWriterLock(timeOut);
133 try
134 {
135 // It's safe for this thread to read or write from the shared resource.
136 resource = rnd.Next(500);
137 Display("writes resource value " + resource);
138 Interlocked.Increment(ref writes);
139 }
140 finally
141 {
142 // Ensure that the lock is released.
143 rwl.DowngradeFromWriterLock(ref lc);
144 }
145 }
146 catch (ApplicationException)
147 {
148 // The upgrade request timed out.
149 Interlocked.Increment(ref writerTimeouts);
150 }
151
152 // If the lock was downgraded, it's still safe to read from the resource.
153 Display("reads resource value " + resource);
154 Interlocked.Increment(ref reads);
155 }
156 finally
157 {
158 // Ensure that the lock is released.
159 rwl.ReleaseReaderLock();
160 }
161 }
162 catch (ApplicationException)
163 {
164 // The reader lock request timed out.
165 Interlocked.Increment(ref readerTimeouts);
166 }
167 }
168
169 // Release all locks and later restores the lock state.
170 // Uses sequence numbers to determine whether another thread has
171 // obtained a writer lock since this thread last accessed the resource.
172 static void ReleaseRestore(Random rnd, int timeOut)
173 {
174 int lastWriter;
175
176 try
177 {
178 rwl.AcquireReaderLock(timeOut);
179 try
180 {
181 // It's safe for this thread to read from the shared resource,
182 // so read and cache the resource value.
183 int resourceValue = resource; // Cache the resource value.
184 Display("reads resource value " + resourceValue);
185 Interlocked.Increment(ref reads);
186
187 // Save the current writer sequence number.
188 lastWriter = rwl.WriterSeqNum;
189
190 // Release the lock and save a cookie so the lock can be restored later.
191 LockCookie lc = rwl.ReleaseLock();
192
193 // Wait for a random interval and then restore the previous state of the lock.
194 Thread.Sleep(rnd.Next(250));
195 rwl.RestoreLock(ref lc);
196
197 // Check whether other threads obtained the writer lock in the interval.
198 // If not, then the cached value of the resource is still valid.
199 if (rwl.AnyWritersSince(lastWriter))
200 {
201 resourceValue = resource;
202 Interlocked.Increment(ref reads);
203 Display("resource has changed " + resourceValue);
204 }
205 else
206 {
207 Display("resource has not changed " + resourceValue);
208 }
209 }
210 finally
211 {
212 // Ensure that the lock is released.
213 rwl.ReleaseReaderLock();
214 }
215 }
216 catch (ApplicationException)
217 {
218 // The reader lock request timed out.
219 Interlocked.Increment(ref readerTimeouts);
220 }
221 }
222
223 // Helper method briefly displays the most recent thread action.
224 static void Display(string msg)
225 {
226 Console.Write("Thread {0} {1}. \r", Thread.CurrentThread.Name, msg);
227 }
228 }
229 }
View Code
3.7、ExampleCore_6_7
1 namespace ExampleCore_6_7
2 {
3 internal class Program
4 {
5 static void Main(string[] args)
6 {
7 Program program = new Program();
8 program.Run();
9 }
10
11 public void Run()
12 {
13 var mycode = GetHashCode();
14 Console.WriteLine("HashCode:" + mycode);
15
16 Console.WriteLine("Press any key to acquire lock");
17 Console.ReadLine();
18
19 Monitor.Enter(this);
20
21 Console.WriteLine("Press any key to release lock");
22 Console.ReadLine();
23
24 Monitor.Exit(this);
25
26 Console.WriteLine("Press any key to Exit");
27 Console.ReadLine();
28 }
29 }
30 }
View Code
3.8、ExampleCore_6_8
1 namespace ExampleCore_6_8
2 {
3 internal class Program
4 {
5 static void Main(string[] args)
6 {
7 Program program = new Program();
8 program.Run();
9 }
10
11 public void Run()
12 {
13 Console.WriteLine("Press any key to acquire lock");
14 Console.ReadLine();
15
16 Monitor.Enter(this);
17
18 Console.WriteLine("Press any key to get hashcode");
19 Console.ReadLine();
20
21 var mycode = GetHashCode();
22 Console.WriteLine("HashCode:" + mycode);
23
24 Console.WriteLine("Press any key to release lock");
25 Console.ReadLine();
26
27 Monitor.Exit(this);
28
29 Console.WriteLine("Press any key to Exit");
30 Console.ReadLine();
31 }
32 }
33 }
View Code
3.9、ExampleCore_6_9
1 namespace ExampleCore_6_9
2 {
3 internal class Program
4 {
5 public static Person person = new Person();
6 public static Student student = new Student();
7 static void Main(string[] args)
8 {
9 Task.Run(() =>
10 {
11 lock (person)
12 {
13 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Person(1111) 锁");
14 Thread.Sleep(1000);
15 lock (student)
16 {
17 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Student(1111) 锁");
18 Console.ReadLine();
19 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经退出 Student(1111) 锁");
20 }
21 }
22 });
23
24 Task.Run(() =>
25 {
26 lock (student)
27 {
28 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Student(22222) 锁");
29 Thread.Sleep(1000);
30 lock (person)
31 {
32 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经进入 Person(22222) 锁");
33 Console.ReadLine();
34 Console.WriteLine($"tid={Environment.CurrentManagedThreadId},已经退出 Person(22222) 锁");
35 }
36 }
37 });
38
39 Console.ReadLine();
40 }
41 }
42
43 public class Student { }
44
45 public class Person { }
46 }
View Code
3.10、ExampleCore_6_10
1 namespace ExampleCore_6_10
2 {
3 internal class DBWrapper
4 {
5 private string _connectionString;
6
7 public DBWrapper(string connectionString)
8 {
9 _connectionString = connectionString;
10 }
11 }
12
13 internal class Program
14 {
15 private static DBWrapper? dBWrapper;
16
17 static void Main(string[] args)
18 {
19 dBWrapper = new DBWrapper("DB1");
20
21 Thread thread = new Thread(ThreadProc);
22 thread.Start();
23
24 Thread.Sleep(500);
25
26 Console.WriteLine("Acquiring Lock!");
27 Monitor.Enter(dBWrapper);
28
29 Thread.Sleep(2000);
30
31 Console.WriteLine("Releasing Lock!");
32 Monitor.Exit(dBWrapper);
33 }
34
35 private static void ThreadProc()
36 {
37 try
38 {
39 Monitor.Enter(dBWrapper!);
40 Call3rdPartyCode(null);
41 Monitor.Exit(dBWrapper!);
42 }
43 catch (Exception)
44 {
45 Console.WriteLine("3rd party code throw an exception");
46 }
47 }
48
49 private static void Call3rdPartyCode(object? obj)
50 {
51 if (obj == null)
52 {
53 throw new NullReferenceException();
54 }
55 }
56 }
57 }
View Code
四、基础知识
在这一段内容中,有的小节可能会包含两个部分,分别是 A 和 B,也有可能只包含 A,如果只包含 A 部分,A 字母会省略。A 是【基础知识】,讲解必要的知识点,B 是【眼见为实】,通过调试证明讲解的知识点。
4.1、同步的基础知识
A、基础知识
进程: 它描述了当一个程序在运行起来所需要的资源总和的统称,包括:CPU、内存、磁盘、网络、GPU 等,最明显我们可以通过【任务管理器】查看我们电脑上运行的进程。
线程: 它是应用程序针对用户操作做出反应的最小执行单元,也就是说,应用软件响应用户的任何操作都是通过一个线程完成的。切记,线程是操作系统的资源,不是 CLR 的,鉴于此,线程具有启动、运行和停止不确定性,也就是启动 N 个线程,每次的启动顺序都可能不一样,同一份代码,同一线程执行的时间也是不同的,启动不同,运行不同,当然,结束的时机也是不同的。
句柄: 是用来标识对象或者项目的标识符,可以用来描述窗体、控件、文件等。
多线程: 能够并发的运行任意数量的线程。
在这节开始之前,我们必须先弄懂以上 4 个概念,我用自己的语言解释了一下,如果大家不懂,可以自行去网上恶补了。多线程的应用程序如何设计的好的话,会有三个特征:1、应用程序的用户体验更好,不卡界面;2、应用程序的性能好,处理速度更快;3、多线程具有不确定性,需要我们做更多的工作来协调。
C# 的 Thread 类表示一个线程类,其实,在背后会有一些底层的数据结构做支撑,比如在 CLR 层会有一个对应的线程类生成,同时操作系统层也会有一个数据结构与之对应,所以说,我们简简单单声明一个 Thread 类,会有三个数据结构来承载。
a)、C# 层的 Thread。
C# 中的 Thread 类,其实是对 CLR 层 Thread 线程类的封装,在 C# Thread 类的定义中,会有一个 private IntPtr DONT_USE_InternalThread 实例字段,该字段就是引用的 CLR 层的线程指针引用。
b)、CLR 层的 Thread
Net Core 是开源的,所以是可以看到 CLR 线程 Thread 的定义。类名是:Thread.cpp,Net 5、6、7、8都可以看。
c)、OS 层的 KThread。
操作系统层的线程对象是通过 _KThread 来表示的。
多线程编程有一个无法避免的问题就是同步的问题,在.NET 中实现同步的方式还是挺多的,比如:事件同步、信号量、互斥体、监视器、瘦锁等。
B、眼见为实
调试源码:ExampleCore_6_1
调试任务:我们查看 C# Thread 线程所对应的 OS 层的数据结构表示
我们直接运行的 EXE 应用程序,程序启动成功,在控制台中输出:tid=4,这个值大家可能不一样。程序运行成功,就产生了一个线程对象。我们想要查看内核态线程的id,需要在借助一个【ProcessExplorer】工具,这个工具有32位和64位两个版本,根据自己系统特特性选择合适的版本,我选择的是64位版本的。
效果如图:
接着,我们在过【通过名称过滤(Filter by name)】中输入我们项目的名称:ExampleCore_6_1,来进程查找。效果如图:
接着,我们在进程名上双击,打开进程属性对话框,如图:
我们找到了我们项目进程的主键线程编号,然后就可以使用 Windbg 查看内核态的线程表示了。我们主线程的编号是:15560,这个是十进制的,要注意。
1)、KD 和 NTSD 调试
说明一下:主线程 ID 不是 15560,我重启了,现在是 2316,效果如图:
我们以管理员身份打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,并输入以下命令:【kd -kl】打开调试器。这个是内核调试器,和【NTSD】是有区别的,【NTSD】是用户态的调试器。
如图:
打开的调试器窗口如图:
太多了无用内容了,使用【.cls】清理一下。
执行命令【!process 0 2 ExampleCore_6_1.exe】
1 lkd> !process 0 2 ExampleCore_6_1.exe
2 PROCESS ffffa2067324d080
3 SessionId: 1 Cid: 3f2c Peb: 4e16f21000 ParentCid: 0da8
4 DirBase: 6bc43002 ObjectTable: 00000000 HandleCount: 0.
5 Image: ExampleCore_6_1.exe
6
7 No active threads
8 THREAD ffffa20677bb90c0 Cid 3f2c.3cc8 Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
9 THREAD ffffa20677e50240 Cid 3f2c.3960 Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
10 THREAD ffffa20677995080 Cid 3f2c.1f54 Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
11 THREAD ffffa2066e255080 Cid 3f2c.3b98 Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
12 THREAD ffffa206712dd080 Cid 3f2c.3850 Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
13 THREAD ffffa2066ead5080 Cid 3f2c.2144 Teb: 0000000000000000 Win32Thread: 0000000000000000 TERMINATED
14
15 PROCESS ffffa206780c8080
16 SessionId: 1 Cid: 4078 Peb: b9e31b9000 ParentCid: 0da8
17 DirBase: 3183bb002 ObjectTable: ffff8a8e17548a00 HandleCount: 171.
18 Image: ExampleCore_6_1.exe
19
20 THREAD ffffa2066e728080 Cid 4078.090c Teb: 000000b9e31ba000 Win32Thread: ffffa20677656660 WAIT: (Executive) KernelMode Alertable
21 ffffa20678e5b568 NotificationEvent
22
23 THREAD ffffa2066e4e1080 Cid 4078.2e48 Teb: 000000b9e31c0000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
24 ffffa20677fe3d60 NotificationEvent
25
26 THREAD ffffa206757e8080 Cid 4078.336c Teb: 000000b9e31c2000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
27 ffffa20677fe3c60 SynchronizationEvent
28 ffffa2066f679260 SynchronizationEvent
29 ffffa20677fe39e0 SynchronizationEvent
30
31 THREAD ffffa206739d4080 Cid 4078.2ef0 Teb: 000000b9e31c4000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
32 ffffa206678be6a0 NotificationEvent
33 ffffa206775ab560 SynchronizationEvent
34
35 THREAD ffffa20672ea6080 Cid 4078.3750 Teb: 000000b9e31ca000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
36 ffffa20678c15160 SynchronizationEvent
红色标注就是需要注意的内容,他会把这个进程中的所有线程找出来。我们通过【ProcessExploler】看到我们项目的主线程是:2316,这个值是十进制的,我们看看十六进制是多少。
1 lkd> ?0n2316
2 Evaluate expression: 2316 = 00000000`0000090c
我再来一个截图显示一下他们的关系,就更清楚了。
ffffa2066e728080 这个值就是线程的内核态的数据结构,我们可以继续使用【dt nt!_KThread ffffa2066e728080】命令查看一下详情。
1 lkd> dt nt!_KThread ffffa2066e728080
2 +0x000 Header : _DISPATCHER_HEADER
3 +0x018 SListFaultAddress : (null)
4 +0x020 QuantumTarget : 0xac9a2b7
5 +0x028 InitialStack : 0xffffdf00`c6b27c50 Void
6 +0x030 StackLimit : 0xffffdf00`c6b21000 Void
7 +0x038 StackBase : 0xffffdf00`c6b28000 Void
8 +0x040 ThreadLock : 0
9 +0x048 CycleTime : 0x94ce518
10 +0x050 CurrentRunTime : 0
11 +0x054 ExpectedRunTime : 0x787687
12 +0x058 KernelStack : 0xffffdf00`c6b273b0 Void
13 +0x060 StateSaveArea : 0xffffdf00`c6b27c80 _XSAVE_FORMAT
14 +0x068 SchedulingGroup : (null)
15 +0x070 WaitRegister : _KWAIT_STATUS_REGISTER
16 +0x071 Running : 0 ''
17 +0x072 Alerted : [2] ""
18 +0x074 AutoBoostActive : 0y1
19 +0x074 ReadyTransition : 0y0
20 +0x074 WaitNext : 0y0
21 +0x074 SystemAffinityActive : 0y0
22 +0x074 Alertable : 0y1
23 +0x074 UserStackWalkActive : 0y0
24 +0x074 ApcInterruptRequest : 0y0
25 +0x074 QuantumEndMigrate : 0y0
26 +0x074 UmsDirectedSwitchEnable : 0y0
27 +0x074 TimerActive : 0y0
28 +0x074 SystemThread : 0y0
29 +0x074 ProcessDetachActive : 0y0
30 +0x074 CalloutActive : 0y0
31 +0x074 ScbReadyQueue : 0y0
32 +0x074 ApcQueueable : 0y1
33 +0x074 ReservedStackInUse : 0y0
34 +0x074 UmsPerformingSyscall : 0y0
35 +0x074 TimerSuspended : 0y0
36 +0x074 SuspendedWaitMode : 0y0
37 +0x074 SuspendSchedulerApcWait : 0y0
38 +0x074 CetUserShadowStack : 0y0
39 +0x074 BypassProcessFreeze : 0y0
40 +0x074 Reserved : 0y0000000000 (0)
41 +0x074 MiscFlags : 0n16401
42 +0x078 ThreadFlagsSpare : 0y00
43 +0x078 AutoAlignment : 0y0
44 +0x078 DisableBoost : 0y0
45 +0x078 AlertedByThreadId : 0y0
46 +0x078 QuantumDonation : 0y0
47 +0x078 EnableStackSwap : 0y1
48 +0x078 GuiThread : 0y1
49 +0x078 DisableQuantum : 0y0
50 +0x078 ChargeOnlySchedulingGroup : 0y0
51 +0x078 DeferPreemption : 0y0
52 +0x078 QueueDeferPreemption : 0y0
53 +0x078 ForceDeferSchedule : 0y0
54 +0x078 SharedReadyQueueAffinity : 0y1
55 +0x078 FreezeCount : 0y0
56 +0x078 TerminationApcRequest : 0y0
57 +0x078 AutoBoostEntriesExhausted : 0y1
58 +0x078 KernelStackResident : 0y1
59 +0x078 TerminateRequestReason : 0y00
60 +0x078 ProcessStackCountDecremented : 0y0
61 +0x078 RestrictedGuiThread : 0y0
62 +0x078 VpBackingThread : 0y0
63 +0x078 ThreadFlagsSpare2 : 0y0
64 +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
65 +0x078 ThreadFlags : 0n204992
66 +0x07c Tag : 0 ''
67 +0x07d SystemHeteroCpuPolicy : 0 ''
68 +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
69 +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
70 +0x07f RunningNonRetpolineCode : 0y0
71 +0x07f SpecCtrlSpare : 0y0000000 (0)
72 +0x07f SpecCtrl : 0 ''
73 +0x080 SystemCallNumber : 6
74 +0x084 ReadyTime : 1
75 +0x088 FirstArgument : 0x00000000`00000054 Void
76 +0x090 TrapFrame : 0xffffdf00`c6b27ac0 _KTRAP_FRAME
77 +0x098 ApcState : _KAPC_STATE
78 +0x098 ApcStateFill : [43] "???"
79 +0x0c3 Priority : 9 ''
80 +0x0c4 UserIdealProcessor : 2
81 +0x0c8 WaitStatus : 0n0
82 +0x0d0 WaitBlockList : 0xffffa206`6e7281c0 _KWAIT_BLOCK
83 +0x0d8 WaitListEntry : _LIST_ENTRY [ 0xfffff806`5b7e7aa0 - 0xfffff806`5b7e7aa0 ]
84 +0x0d8 SwapListEntry : _SINGLE_LIST_ENTRY
85 +0x0e8 Queue : (null)
86 +0x0f0 Teb : 0x000000b9`e31ba000 Void
87 +0x0f8 RelativeTimerBias : 0
88 +0x100 Timer : _KTIMER
89 +0x140 WaitBlock : [4] _KWAIT_BLOCK
90 +0x140 WaitBlockFill4 : [20] "p???"
91 +0x154 ContextSwitches : 0xef
92 +0x140 WaitBlockFill5 : [68] "p???"
93 +0x184 State : 0x5 ''
94 +0x185 Spare13 : 0 ''
95 +0x186 WaitIrql : 0 ''
96 +0x187 WaitMode : 0 ''
97 +0x140 WaitBlockFill6 : [116] "p???"
98 +0x1b4 WaitTime : 0x152d42
99 +0x140 WaitBlockFill7 : [164] "p???"
100 +0x1e4 KernelApcDisable : 0n-1
101 +0x1e6 SpecialApcDisable : 0n0
102 +0x1e4 CombinedApcDisable : 0xffff
103 +0x140 WaitBlockFill8 : [40] "p???"
104 +0x168 ThreadCounters : (null)
105 +0x140 WaitBlockFill9 : [88] "p???"
106 +0x198 XStateSave : (null)
107 +0x140 WaitBlockFill10 : [136] "p???"
108 +0x1c8 Win32Thread : 0xffffa206`77656660 Void
109 +0x140 WaitBlockFill11 : [176] "p???"
110 +0x1f0 Ucb : (null)
111 +0x1f8 Uch : (null)
112 +0x200 ThreadFlags2 : 0n0
113 +0x200 BamQosLevel : 0y00000000 (0)
114 +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
115 +0x204 Spare21 : 0
116 +0x208 QueueListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
117 +0x218 NextProcessor : 1
118 +0x218 NextProcessorNumber : 0y0000000000000000000000000000001 (0x1)
119 +0x218 SharedReadyQueue : 0y0
120 +0x21c QueuePriority : 0n0
121 +0x220 Process : 0xffffa206`780c8080 _KPROCESS
122 +0x228 UserAffinity : _GROUP_AFFINITY
123 +0x228 UserAffinityFill : [10] "???"
124 +0x232 PreviousMode : 1 ''
125 +0x233 BasePriority : 8 ''
126 +0x234 PriorityDecrement : 0 ''
127 +0x234 ForegroundBoost : 0y0000
128 +0x234 UnusualBoost : 0y0000
129 +0x235 Preempted : 0 ''
130 +0x236 AdjustReason : 0 ''
131 +0x237 AdjustIncrement : 1 ''
132 +0x238 AffinityVersion : 0x50
133 +0x240 Affinity : _GROUP_AFFINITY
134 +0x240 AffinityFill : [10] "???"
135 +0x24a ApcStateIndex : 0 ''
136 +0x24b WaitBlockCount : 0x1 ''
137 +0x24c IdealProcessor : 2
138 +0x250 NpxState : 5
139 +0x258 SavedApcState : _KAPC_STATE
140 +0x258 SavedApcStateFill : [43] "???"
141 +0x283 WaitReason : 0 ''
142 +0x284 SuspendCount : 0 ''
143 +0x285 Saturation : 0 ''
144 +0x286 SListFaultCount : 0
145 +0x288 SchedulerApc : _KAPC
146 +0x288 SchedulerApcFill1 : [3] "???"
147 +0x28b QuantumReset : 0x6 ''
148 +0x288 SchedulerApcFill2 : [4] "???"
149 +0x28c KernelTime : 2
150 +0x288 SchedulerApcFill3 : [64] "???"
151 +0x2c8 WaitPrcb : (null)
152 +0x288 SchedulerApcFill4 : [72] "???"
153 +0x2d0 LegoData : (null)
154 +0x288 SchedulerApcFill5 : [83] "???"
155 +0x2db CallbackNestingLevel : 0 ''
156 +0x2dc UserTime : 3
157 +0x2e0 SuspendEvent : _KEVENT
158 +0x2f8 ThreadListEntry : _LIST_ENTRY [ 0xffffa206`6e4e1378 - 0xffffa206`780c80b0 ]
159 +0x308 MutantListHead : _LIST_ENTRY [ 0xffffa206`6e728388 - 0xffffa206`6e728388 ]
160 +0x318 AbEntrySummary : 0x3e '>'
161 +0x319 AbWaitEntryCount : 0 ''
162 +0x31a AbAllocationRegionCount : 0 ''
163 +0x31b SystemPriority : 0 ''
164 +0x31c SecureThreadCookie : 0
165 +0x320 LockEntries : 0xffffa206`6e7286d0 _KLOCK_ENTRY
166 +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
167 +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
168 +0x338 PriorityFloorCounts : [16] ""
169 +0x348 PriorityFloorCountsReserved : [16] ""
170 +0x358 PriorityFloorSummary : 0
171 +0x35c AbCompletedIoBoostCount : 0n0
172 +0x360 AbCompletedIoQoSBoostCount : 0n0
173 +0x364 KeReferenceCount : 0n0
174 +0x366 AbOrphanedEntrySummary : 0 ''
175 +0x367 AbOwnedEntryCount : 0x1 ''
176 +0x368 ForegroundLossTime : 0
177 +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
178 +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
179 +0x378 InGlobalForegroundList : 0
180 +0x380 ReadOperationCount : 0n32
181 +0x388 WriteOperationCount : 0n0
182 +0x390 OtherOperationCount : 0n158
183 +0x398 ReadTransferCount : 0n66740
184 +0x3a0 WriteTransferCount : 0n0
185 +0x3a8 OtherTransferCount : 0n3494
186 +0x3b0 QueuedScb : (null)
187 +0x3b8 ThreadTimerDelay : 0
188 +0x3bc ThreadFlags3 : 0n0
189 +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
190 +0x3bc PpmPolicy : 0y00
191 +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
192 +0x3c0 TracingPrivate : [1] 0
193 +0x3c8 SchedulerAssist : (null)
194 +0x3d0 AbWaitObject : (null)
195 +0x3d8 ReservedPreviousReadyTimeValue : 0
196 +0x3e0 KernelWaitTime : 0xe
197 +0x3e8 UserWaitTime : 0
198 +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
199 +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
200 +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
201 +0x400 SchedulerAssistPriorityFloor : 0n0
202 +0x404 Spare28 : 0
203 +0x408 ResourceIndex : 0xe7 ''
204 +0x409 Spare31 : [3] ""
205 +0x410 EndPadding : [4] 0
206 lkd>
View Code
当然,我们也可以通过【NTSD -pn ExampleCore_6_1.exe】直接查看正在执行中项目,通过【!t】或者【!threads】命令,查看线程三者的对应关系。
1 0:005> !t
2 ThreadCount: 3
3 UnstartedThread: 0
4 BackgroundThread: 1
5 PendingThread: 0
6 DeadThread: 0
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 90c 000001CFD8DCEB20 2a020 Preemptive 000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
11 3 2 2ef0 000002106F45DDF0 2b220 Preemptive 0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
12 4 4 3750 000002106F46D070 202b020 Preemptive 000001CFDD40B4D0:000001CFDD40C630 000001CFD8E1B860 -00001 MTA
13
14 0:005> !threads
15 ThreadCount: 3
16 UnstartedThread: 0
17 BackgroundThread: 1
18 PendingThread: 0
19 DeadThread: 0
20 Hosted Runtime: no
21 Lock
22 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
23 0 1 90c 000001CFD8DCEB20 2a020 Preemptive 000001CFDD4156D8:000001CFDD416680 000001CFD8E1B860 -00001 MTA
24 3 2 2ef0 000002106F45DDF0 2b220 Preemptive 0000000000000000:0000000000000000 000001CFD8E1B860 -00001 MTA (Finalizer)
25 4 4 3750 000002106F46D070 202b020 Preemptive 000001CFDD40B4D0:000001CFDD40C630 000001CFD8E1B860 -00001 MTA
26 0:005>
ID是 1 就是 C# 的托管线程编号, OSID 的值是 90c 就是操作系统层面的线程的数据结构,ThreadOBJ 就是 CLR 层面的线程。
2)、Windbg Preview 调试
然后,我们打开 Windbg,点击【File】-->【Attach to kernel(附加内核态)】,在右侧选择【local】,就是本机的内核态,点击【ok】按钮,进入调试界面。然后,我们使用【!process】命令查找一下我们的项目。
1 lkd> !process 0 2 ExampleCore_6_1.exe
2 PROCESS ffffa2067324d080
3 SessionId: 1 Cid: 3f2c Peb: 4e16f21000 ParentCid: 0da8
4 DirBase: 6bc43002 ObjectTable: ffff8a8e1a97c180 HandleCount: 171.
5 Image: ExampleCore_6_1.exe
6
7 THREAD ffffa20677bb90c0 Cid 3f2c.3cc8 Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
8 ffffa20678223bb8 NotificationEvent
9
10 THREAD ffffa20677e50240 Cid 3f2c.3960 Teb: 0000004e16f2a000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
11 ffffa20677fe5660 NotificationEvent
12
13 THREAD ffffa20677995080 Cid 3f2c.1f54 Teb: 0000004e16f2c000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
14 ffffa20677fe5560 SynchronizationEvent
15 ffffa20677fe56e0 SynchronizationEvent
16 ffffa20677fe5860 SynchronizationEvent
17
18 THREAD ffffa2066e255080 Cid 3f2c.3b98 Teb: 0000004e16f2e000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable
19 ffffa206678be6a0 NotificationEvent
20 ffffa20677cfa260 SynchronizationEvent
21
22 THREAD ffffa206712dd080 Cid 3f2c.3850 Teb: 0000004e16f34000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable
23 ffffa20677964c60 SynchronizationEvent
我们通过【ProcessExploler】看到我们项目的主线程是:1204,这个值是十进制的,我们看看十六进制是多少。
1 lkd> ? 0n15560
2 Evaluate expression: 15560 = 00000000`00003cc8
我们如果使用的调试器是【Windbg Preview】,它有一个特性,选择一个文本,和文本内容相同的也会被凸显出来,我们选择 3cc8,发现我们使用【!process】命令的结果中也有被选择了,如图:
ffffa20677bb90c0 这个值就是线程的内核态的数据结构,我们可以继续使用【dt】命令查看一下详情。
1 lkd> dt nt!_KThread ffffa20677bb90c0
2 +0x000 Header : _DISPATCHER_HEADER
3 +0x018 SListFaultAddress : (null)
4 +0x020 QuantumTarget : 0xd630923
5 +0x028 InitialStack : 0xffffdf00`c2f32c50 Void
6 +0x030 StackLimit : 0xffffdf00`c2f2c000 Void
7 +0x038 StackBase : 0xffffdf00`c2f33000 Void
8 +0x040 ThreadLock : 0
9 +0x048 CycleTime : 0x94e88f3
10 +0x050 CurrentRunTime : 0
11 +0x054 ExpectedRunTime : 0xa80710
12 +0x058 KernelStack : 0xffffdf00`c2f323b0 Void
13 +0x060 StateSaveArea : 0xffffdf00`c2f32c80 _XSAVE_FORMAT
14 +0x068 SchedulingGroup : (null)
15 +0x070 WaitRegister : _KWAIT_STATUS_REGISTER
16 +0x071 Running : 0 ''
17 +0x072 Alerted : [2] ""
18 +0x074 AutoBoostActive : 0y1
19 +0x074 ReadyTransition : 0y0
20 +0x074 WaitNext : 0y0
21 +0x074 SystemAffinityActive : 0y0
22 +0x074 Alertable : 0y1
23 +0x074 UserStackWalkActive : 0y0
24 +0x074 ApcInterruptRequest : 0y0
25 +0x074 QuantumEndMigrate : 0y0
26 +0x074 UmsDirectedSwitchEnable : 0y0
27 +0x074 TimerActive : 0y0
28 +0x074 SystemThread : 0y0
29 +0x074 ProcessDetachActive : 0y0
30 +0x074 CalloutActive : 0y0
31 +0x074 ScbReadyQueue : 0y0
32 +0x074 ApcQueueable : 0y1
33 +0x074 ReservedStackInUse : 0y0
34 +0x074 UmsPerformingSyscall : 0y0
35 +0x074 TimerSuspended : 0y0
36 +0x074 SuspendedWaitMode : 0y0
37 +0x074 SuspendSchedulerApcWait : 0y0
38 +0x074 CetUserShadowStack : 0y0
39 +0x074 BypassProcessFreeze : 0y0
40 +0x074 Reserved : 0y0000000000 (0)
41 +0x074 MiscFlags : 0n16401
42 +0x078 ThreadFlagsSpare : 0y00
43 +0x078 AutoAlignment : 0y0
44 +0x078 DisableBoost : 0y0
45 +0x078 AlertedByThreadId : 0y0
46 +0x078 QuantumDonation : 0y0
47 +0x078 EnableStackSwap : 0y1
48 +0x078 GuiThread : 0y1
49 +0x078 DisableQuantum : 0y0
50 +0x078 ChargeOnlySchedulingGroup : 0y0
51 +0x078 DeferPreemption : 0y0
52 +0x078 QueueDeferPreemption : 0y0
53 +0x078 ForceDeferSchedule : 0y0
54 +0x078 SharedReadyQueueAffinity : 0y1
55 +0x078 FreezeCount : 0y0
56 +0x078 TerminationApcRequest : 0y0
57 +0x078 AutoBoostEntriesExhausted : 0y1
58 +0x078 KernelStackResident : 0y1
59 +0x078 TerminateRequestReason : 0y00
60 +0x078 ProcessStackCountDecremented : 0y0
61 +0x078 RestrictedGuiThread : 0y0
62 +0x078 VpBackingThread : 0y0
63 +0x078 ThreadFlagsSpare2 : 0y0
64 +0x078 EtwStackTraceApcInserted : 0y00000000 (0)
65 +0x078 ThreadFlags : 0n204992
66 +0x07c Tag : 0 ''
67 +0x07d SystemHeteroCpuPolicy : 0 ''
68 +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8)
69 +0x07e ExplicitSystemHeteroCpuPolicy : 0y0
70 +0x07f RunningNonRetpolineCode : 0y0
71 +0x07f SpecCtrlSpare : 0y0000000 (0)
72 +0x07f SpecCtrl : 0 ''
73 +0x080 SystemCallNumber : 6
74 +0x084 ReadyTime : 3
75 +0x088 FirstArgument : 0x00000000`00000050 Void
76 +0x090 TrapFrame : 0xffffdf00`c2f32ac0 _KTRAP_FRAME
77 +0x098 ApcState : _KAPC_STATE
78 +0x098 ApcStateFill : [43] "X???"
79 +0x0c3 Priority : 8 ''
80 +0x0c4 UserIdealProcessor : 2
81 +0x0c8 WaitStatus : 0n256
82 +0x0d0 WaitBlockList : 0xffffa206`77bb9200 _KWAIT_BLOCK
83 +0x0d8 WaitListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0xffffa206`67903158 ]
84 +0x0d8 SwapListEntry : _SINGLE_LIST_ENTRY
85 +0x0e8 Queue : (null)
86 +0x0f0 Teb : 0x0000004e`16f22000 Void
87 +0x0f8 RelativeTimerBias : 0
88 +0x100 Timer : _KTIMER
89 +0x140 WaitBlock : [4] _KWAIT_BLOCK
90 +0x140 WaitBlockFill4 : [20] "???"
91 +0x154 ContextSwitches : 0xde
92 +0x140 WaitBlockFill5 : [68] "???"
93 +0x184 State : 0x5 ''
94 +0x185 Spare13 : 0 ''
95 +0x186 WaitIrql : 0 ''
96 +0x187 WaitMode : 0 ''
97 +0x140 WaitBlockFill6 : [116] "???"
98 +0x1b4 WaitTime : 0x11f7e8
99 +0x140 WaitBlockFill7 : [164] "???"
100 +0x1e4 KernelApcDisable : 0n-1
101 +0x1e6 SpecialApcDisable : 0n0
102 +0x1e4 CombinedApcDisable : 0xffff
103 +0x140 WaitBlockFill8 : [40] "???"
104 +0x168 ThreadCounters : (null)
105 +0x140 WaitBlockFill9 : [88] "???"
106 +0x198 XStateSave : (null)
107 +0x140 WaitBlockFill10 : [136] "???"
108 +0x1c8 Win32Thread : 0xffffa206`7765a990 Void
109 +0x140 WaitBlockFill11 : [176] "???"
110 +0x1f0 Ucb : (null)
111 +0x1f8 Uch : (null)
112 +0x200 ThreadFlags2 : 0n0
113 +0x200 BamQosLevel : 0y00000000 (0)
114 +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0)
115 +0x204 Spare21 : 0
116 +0x208 QueueListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
117 +0x218 NextProcessor : 2
118 +0x218 NextProcessorNumber : 0y0000000000000000000000000000010 (0x2)
119 +0x218 SharedReadyQueue : 0y0
120 +0x21c QueuePriority : 0n0
121 +0x220 Process : 0xffffa206`7324d080 _KPROCESS
122 +0x228 UserAffinity : _GROUP_AFFINITY
123 +0x228 UserAffinityFill : [10] "???"
124 +0x232 PreviousMode : 1 ''
125 +0x233 BasePriority : 8 ''
126 +0x234 PriorityDecrement : 0 ''
127 +0x234 ForegroundBoost : 0y0000
128 +0x234 UnusualBoost : 0y0000
129 +0x235 Preempted : 0 ''
130 +0x236 AdjustReason : 0 ''
131 +0x237 AdjustIncrement : 0 ''
132 +0x238 AffinityVersion : 0x50
133 +0x240 Affinity : _GROUP_AFFINITY
134 +0x240 AffinityFill : [10] "???"
135 +0x24a ApcStateIndex : 0 ''
136 +0x24b WaitBlockCount : 0x1 ''
137 +0x24c IdealProcessor : 2
138 +0x250 NpxState : 5
139 +0x258 SavedApcState : _KAPC_STATE
140 +0x258 SavedApcStateFill : [43] "???"
141 +0x283 WaitReason : 0 ''
142 +0x284 SuspendCount : 0 ''
143 +0x285 Saturation : 0 ''
144 +0x286 SListFaultCount : 0
145 +0x288 SchedulerApc : _KAPC
146 +0x288 SchedulerApcFill1 : [3] "???"
147 +0x28b QuantumReset : 0x6 ''
148 +0x288 SchedulerApcFill2 : [4] "???"
149 +0x28c KernelTime : 1
150 +0x288 SchedulerApcFill3 : [64] "???"
151 +0x2c8 WaitPrcb : (null)
152 +0x288 SchedulerApcFill4 : [72] "???"
153 +0x2d0 LegoData : (null)
154 +0x288 SchedulerApcFill5 : [83] "???"
155 +0x2db CallbackNestingLevel : 0 ''
156 +0x2dc UserTime : 2
157 +0x2e0 SuspendEvent : _KEVENT
158 +0x2f8 ThreadListEntry : _LIST_ENTRY [ 0xffffa206`77e50538 - 0xffffa206`7324d0b0 ]
159 +0x308 MutantListHead : _LIST_ENTRY [ 0xffffa206`77bb93c8 - 0xffffa206`77bb93c8 ]
160 +0x318 AbEntrySummary : 0x3e '>'
161 +0x319 AbWaitEntryCount : 0 ''
162 +0x31a AbAllocationRegionCount : 0 ''
163 +0x31b SystemPriority : 0 ''
164 +0x31c SecureThreadCookie : 0
165 +0x320 LockEntries : 0xffffa206`77bb9710 _KLOCK_ENTRY
166 +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY
167 +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY
168 +0x338 PriorityFloorCounts : [16] ""
169 +0x348 PriorityFloorCountsReserved : [16] ""
170 +0x358 PriorityFloorSummary : 0
171 +0x35c AbCompletedIoBoostCount : 0n0
172 +0x360 AbCompletedIoQoSBoostCount : 0n0
173 +0x364 KeReferenceCount : 0n0
174 +0x366 AbOrphanedEntrySummary : 0 ''
175 +0x367 AbOwnedEntryCount : 0x1 ''
176 +0x368 ForegroundLossTime : 0
177 +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
178 +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY
179 +0x378 InGlobalForegroundList : 0
180 +0x380 ReadOperationCount : 0n32
181 +0x388 WriteOperationCount : 0n0
182 +0x390 OtherOperationCount : 0n158
183 +0x398 ReadTransferCount : 0n66740
184 +0x3a0 WriteTransferCount : 0n0
185 +0x3a8 OtherTransferCount : 0n3494
186 +0x3b0 QueuedScb : (null)
187 +0x3b8 ThreadTimerDelay : 0
188 +0x3bc ThreadFlags3 : 0n0
189 +0x3bc ThreadFlags3Reserved : 0y00000000 (0)
190 +0x3bc PpmPolicy : 0y00
191 +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0)
192 +0x3c0 TracingPrivate : [1] 0
193 +0x3c8 SchedulerAssist : (null)
194 +0x3d0 AbWaitObject : (null)
195 +0x3d8 ReservedPreviousReadyTimeValue : 0
196 +0x3e0 KernelWaitTime : 0xe
197 +0x3e8 UserWaitTime : 0
198 +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ]
199 +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY
200 +0x3f8 InGlobalUpdateVpThreadPriorityList : 0
201 +0x400 SchedulerAssistPriorityFloor : 0n0
202 +0x404 Spare28 : 0
203 +0x408 ResourceIndex : 0x1 ''
204 +0x409 Spare31 : [3] ""
205 +0x410 EndPadding : [4] 0
View Code
这个线程的数据结构内容还是不少的。
我们可以使用【!thread ffffa20677bb90c0】命令查看更易阅读的结果。
1 lkd> !thread ffffa20677bb90c0
2 THREAD ffffa20677bb90c0 Cid 3f2c.3cc8 Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable
3 ffffa20678223bb8 NotificationEvent
4 IRP List:
5 ffffa2067802cdc0: (0006,0160) Flags: 00060900 Mdl: ffffa20670216220
6 ffffa2067802bc80: (0006,0160) Flags: 00060800 Mdl: 00000000
7 Not impersonating
8 DeviceMap ffff8a8e0d39f7e0
9 Owning Process ffffa2067324d080 Image: ExampleCore_6_1.exe
10 Attached Process N/A Image: N/A
11 Wait Start TickCount 1177576 Ticks: 163639 (0:00:42:36.859)
12 Context Switch Count 222 IdealProcessor: 2
13 UserTime 00:00:00.031
14 KernelTime 00:00:00.015
15 Win32 Start Address 0x00007ff7359f1360
16 Stack Init ffffdf00c2f32c50 Current ffffdf00c2f323b0
17 Base ffffdf00c2f33000 Limit ffffdf00c2f2c000 Call 0000000000000000
18 Priority 8 BasePriority 8 IoPriority 2 PagePriority 5
19 Child-SP RetAddr : Args to Child : Call Site
20 ffffdf00`c2f323f0 fffff806`5d841330 : ffffbb80`50317180 00000000`ffffffff ffffa206`00000000 00000000`50317180 : nt!KiSwapContext+0x76
21 ffffdf00`c2f32530 fffff806`5d84085f : 00000000`00000002 ffff8a8e`00000000 ffffdf00`c2f326f0 fffff806`00000000 : nt!KiSwapThread+0x500
22 ffffdf00`c2f325e0 fffff806`5d840103 : 000002af`00000000 00000000`00000000 00000000`00000000 ffffa206`77bb9200 : nt!KiCommitThreadWait+0x14f
23 ffffdf00`c2f32680 fffff806`5d9f18bc : ffffa206`78223bb8 ffffa206`00000000 00000000`00000000 ffffa206`77bb9001 : nt!KeWaitForSingleObject+0x233
24 ffffdf00`c2f32770 fffff806`5dc45b5b : 00000000`00000000 00000000`00000001 ffffa206`78223b20 ffffa206`7802cdc0 : nt!IopWaitForSynchronousIoEvent+0x50
25 ffffdf00`c2f327b0 fffff806`5dbcf918 : ffffdf00`c2f32b40 ffffa206`78223b20 00000000`00000000 00000000`00000000 : nt!IopSynchronousServiceTail+0x50b
26 ffffdf00`c2f32850 fffff806`5dc0c4b8 : ffffa206`78223b20 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopReadFile+0x7cc
27 ffffdf00`c2f32940 fffff806`5da11578 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!NtReadFile+0x8a8
28 ffffdf00`c2f32a50 00007ffa`7f08d0a4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x28 (TrapFrame @ ffffdf00`c2f32ac0)
29 0000004e`1717e558 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : 0x00007ffa`7f08d0a4
当然,我们也可以通过 Windbg Preview 直接查看了,我们的项目正在执行中,所以我们可以通过【Attach to process】进入调试界面,然后,通过【!t】或者【!threads】命令,查看线程三者的对应关系。
1 0:005> !t
2 ThreadCount: 3
3 UnstartedThread: 0
4 BackgroundThread: 1
5 PendingThread: 0
6 DeadThread: 0
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 3cc8 00000246EFD07630 2a020 Preemptive 00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA
11 3 2 3b98 00000246EFD70060 2b220 Preemptive 0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer)
12 4 4 3850 00000246EFCCD3F0 202b020 Preemptive 00000246F440B4D0:00000246F440C630 00000246efca59d0 -00001 MTA
13
14 0:005> !threads
15 ThreadCount: 3
16 UnstartedThread: 0
17 BackgroundThread: 1
18 PendingThread: 0
19 DeadThread: 0
20 Hosted Runtime: no
21 Lock
22 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
23 0 1 3cc8 00000246EFD07630 2a020 Preemptive 00000246F44156D8:00000246F4416680 00000246efca59d0 -00001 MTA
24 3 2 3b98 00000246EFD70060 2b220 Preemptive 0000000000000000:0000000000000000 00000246efca59d0 -00001 MTA (Finalizer)
25 4 4 3850 00000246EFCCD3F0 202b020 Preemptive 00000246F440B4D0:00000246F440C630 00000246efca59d0 -00001 MTA
我们在【!t/threads】命令的结果中,查看【OSID】列,也能看到 3cc8 的标识。ID是1就是C#的托管线程编号, OSID的值是 3cc8 就是操作系统层面的线程的数据结构,ThreadOBJ 就是 CLR 层面的线程。
4.2、线程同步原语
在开始之前,先解释一下以下概念:用户态和内核态,这两个概念不清楚,就会搞得云里雾里的。
用户态:
用户态也被称为用户模式,是指应用程序的运行状态。在这种模式下,应用程序拥有有限的系统资源访问权限,只能在操作系统划定的特定空间内运行。用户态下运行的程序不能直接访问硬件设备或执行特权指令,所有对硬件的访问都必须通过操作系统进行。
在用户态下,应用程序通过系统调用来请求操作系统提供的服务。例如,文件操作、网络通信等都需要通过系统调用来实现。当应用程序发出系统调用时,会触发上下文切换,将CPU的控制权交给操作系统内核,进入内核态。
内核态:
内核态也被称为内核模式或特权模式,是操作系统内核的运行状态。处于内核态的CPU可以执行所有的指令,访问所有的内存地址,拥有最高的权限。内核态下运行的程序可以访问系统的所有资源,包括CPU、内存、I/O等。
在内核态下,操作系统可以响应所有的中断请求,处理硬件事件和系统调用。当应用程序发出系统调用时,CPU会切换到内核态,执行相应的操作,然后返回用户态。此外,当发生严重错误或异常时,也会触发内核态的切换。
4.2.1、事件同步原语(AutoResetEvent 和 ManulResetEvent(内核锁))
A、基础知识
事件同步的本质实在内核态维护了一个 bool 值,通过 bool 值来实现线程间的同步,具体的使用方法网上很多,我这里就不过多的赘述了,这里我们看看是如何通过 bool 值的变化实现线程间的同步的。
事件是一种内核态的原语,可以在用户态中通过句柄来访问。事件也是一个同步对象,它有两种状态:已触发(signaled)和未触发(nonsignaled)。当事件是未触发的状态,在这个事件上的线程就会处于等待的状态,如果事件的状态变为已触发时,这个线程也会恢复执行。
事件对象经常用于对多个线程之间的代码执行流程进行同步。 AutoResetEvent 和 ManulResetEvent 区别:ManulResetEvent 在手动重置事件中,事件对象保持为已触发的状态,直到被手动重置,因此,所有在这个事件对象上等待的线程都会被释放。AutoResetEvent 自动重置事件只允许其中一个等待线程被释放,然后,又立即自动的回到未触发状态。如果没有任何等待的线程,那么这个事件对象将保持为未触发的状态,直到第一个线程在这个事件上开始等待。
我们都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底层的功能,说白了就是 C# 只是使用了 Windows 内核提供的事件,C# 不过是对其进行了包装,如果你想要查看内存地址,必须到内核态去看。
AutoResetEvent 或者 ManulResetEvent 类型内部包含了 SafeWaitHandle 引用类型的一个字段 _waitHandle,_waitHandle** 类型内部包含了一个值类型的(System.IntPtr)的 handle 实现的同步操作。**
B、眼见为实
调试源码:ExampleCore_6_2
调试任务:我们看看 AutoResetEvent 是如何通过 bool 值变化实现线程间的同步的。
注意:这里的调试都需要用到两种调试器,分别是用户态的和内核态的,还有一个获取对象内核地址的工具【Process Explorer】。在用户态调试器执行调用,在内核态调试器里看具体地址内容的变化。
1)、KD 和 NTSD 调试
在这里,我只测试 ManualResetEvent 类型的变化,AutoResetEvent 暂时我忽略,因为它们没区别。调试器使用用户态的 NTSD 和内核态的 KD。
编译我们的项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_2\bin\Debug\net8.0\ExampleCore_6_2.exe】打开调试器。
进入调试器后,【g】直接运行,直到调试器输出"选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出) "字样,我们输入 manual,不区分大小写,就进入到了 RunManualResetEvent 方法内,调试器会输出"mre 默认为 false,即等待状态,请查看! "字样。调试器中断执行,开始我们的调试了。
首先,我们在托管堆上查找 ManualResetEvent 类型的对象,执行命令【!DumpHeap -type ManualResetEvent】。
1 0:000> !DumpHeap -type ManualResetEvent
2 Address MT Size
3 0000020f29414180 00007ff8db192a88 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 00007ff8db192a88 1 24 System.Threading.ManualResetEvent
8 Total 1 objects
ManualResetEvent 对象的地址是 0000020f29414180,我们继续使用【!do】或者【!DumpObj】命令查看它的详情。
1 0:000> !do 0000020f29414180
2 Name: System.Threading.ManualResetEvent
3 MethodTable: 00007ff8db192a88
4 EEClass: 00007ff8db182508
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff8db193318 4000b7a 8 ...es.SafeWaitHandle 0 instance 0000020f294142d8 _waitHandle11 00007ff8db0370a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
红色标注的就是一个引用类型实例,地址是 0000020f294142d8,针对该地址,继续执行【!do】命令。
1 0:000> !do 0000020f294142d8
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle
3 MethodTable: 00007ff8db193318
4 EEClass: 00007ff8db182970
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff8db0370a0 400126e 8 System.IntPtr 1 instance 00000000000002B8 handle11 00007ff8dafc1188 400126f 10 System.Int32 1 instance 4 _state
12 00007ff8daf8d070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ff8daf8d070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
红色标注的是一个 handle 对象,我们可以使用【!handle 00000000000002B8 f】命令继续查看,必须具有 f 参数。
1 0:000> !handle 00000000000002B8 f
2 Handle 2b8
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 32769
10 Name <none>
11 Object Specific Information
12 Event Type Manual Reset(事件类型是 ManualResetEvent)13 Event is Waiting(初始状态是等待)
到此,说明 ManualResetEvent(false) 默认是等待的状态。
此刻,我们在借助【Process Explorer】工具,找到事件同步对象的内核地址,看看内核地址上的数据的变化。打开这个工具,然后在【Filter by name】输入项目名称 ExampleCore_6_2,结果如图:
我们在【Handles】选项里,找到我们的事件对象,然后双击,打开属性框,找到内核的地址。如图:
我们找到了事件对象在内核上的地址,我们需要再打开一个【kd】调试器,开始内核调试。
我们就找到了内核地址【0xFFFF940C4DC558E0】了。然后,我们到 kd 的内核态中去查看一下这个地址,使用【dp 0xFFFF940C4DC558E0 l1】命令。当前值:0(00000000)
1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0 00000000`00060000
说明 ManualResetEvent 的 fase 表示的是等待,通过用户态命令【!handle 00000000000002B8 f】和内核态命令【dp 0xFFFF940C4DC558E0 l1】都能证明。
然后我们【g】一下用户态的 NTSD 调试器,控制台输出"mre 默认为 true,即放行状态,请查看!"字样,再次执行命令【!handle 00000000000002B8 f】。
1 0:000> !handle 00000000000002B8 f
2 Handle 2b8
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65535
10 Name <none>
11 Object Specific Information
12 Event Type Manual Reset
13 Event is Set(放行状态)
然后切换到【内核态】的 KD 调试器,继续使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。
1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0 00000001`00060000(红色变成 1 ,表示 true)
【!handle】命令的结果是 Set,【dp】命令变成了 00000001,后面的不用管。
最后,我们再【g】一下【用户态】的 KD,控制台输出"mre Reset后为 false,即等待状态,请查看! "字样,再次执行【!handle 00000000000002B8 f】命令。
1 0:000> !handle 00000000000002B8 f
2 Handle 2b8
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65534
10 Name <none>
11 Object Specific Information
12 Event Type Manual Reset
13 Event is Waiting(处于等待)
Reset 后是等待的状态,然后切换到【内核态】的 KD,继续使用【dp 0xFFFF940C4DC558E0 l1】命令,查看一下。
1 lkd> dp 0xFFFF940C4DC558E0 l1
2 ffff940c`4dc558e0 00000000`00060000(红色是 0,0 代表就是 false)
我们就看到了,状态是0和1相互切换的。
2)、Windbg Preview 调试
我们编译项目,打开【Windbg Preview】调试器,点击【文件】----》【Launch executable】加载我们的程序,打开调试器的界面,程序已经处于中断状态。我们使用【g】命令,继续运行程序,在【Debugger.Break()】语句处停止,我们的控制台应用程序输出:mre 默认为 false,即等待状态,请查看! ,Windbg 处于暂停状态,我们就可以调试了。
首先,我们去托管堆中查找一下 ManualResetEvent 这个对象,执行【!dumpheap -type ManualResetEvent】命令。
1 0:000> !DumpHeap -type ManualResetEvent
2 Address MT Size
3 012b87014180 7ff8da3e2a88 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 7ff8da3e2a88 1 24 System.Threading.ManualResetEvent
8 Total 1 objects, 24 bytes
ManualResetEvent 对象的地址是 012b87014180,针对这个地址,我们使用【!do】或者【!DumpObj】命令,查看它的详情。
1 0:000> !DumpObj 012b87014180
2 Name: System.Threading.ManualResetEvent(手动重置事件) 3 MethodTable: 00007ff8da3e2a88
4 EEClass: 00007ff8da3d2508
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff8da3e3318 4000b7a 8 ...es.SafeWaitHandle 0 instance 0000012b870142d8 _waitHandle11 00007ff8da2870a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
红色标注的是一个 instance 引用类型(VT=0)实例对象,我们可以使用【!DumpObj 0000012b870142d8】命令继续查看。
1 0:000> !DumpObj 0000012b870142d8
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle
3 MethodTable: 00007ff8da3e3318
4 EEClass: 00007ff8da3d2968
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff8da2870a0 400126e 8 System.IntPtr 1 instance 0000000000000248 handle11 00007ff8da211188 400126f 10 System.Int32 1 instance 4 _state
12 00007ff8da1dd070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ff8da1dd070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
红色标注的是一个 System.IntPtr 值类型(VT=1)实例对象,我们可以使用【!DumpVC 00007ff8da2870a0 0000000000000248】命令继续查看。
1 0:000> !DumpVC 00007ff8da2870a0 0000000000000248
2 Name: System.IntPtr
3 MethodTable: 00007ff8da2870a0
4 EEClass: 00007ff8da266100
5 Size: 24(0x18) bytes
6 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
7 Fields:
8 MT Field Offset Type VT Attr Value Name
9 00007ff8da2870a0 4000525 0 System.IntPtr 1 instance _value
10 00007ff8da2870a0 4000526 a78 System.IntPtr 1 static 0000000000000000 Zero
我们可以不使用【!DumpVC】命令,直接使用【!handle】命令。
红色标注的是一个 handle 对象,我们可以使用【!handle 0000000000000248 f】命令继续查看,必须具有 f 参数。
1 0:000> !handle 0000000000000248 f
2 Handle 248
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 32769
10 Name <none>
11 Object Specific Information
12 Event Type Manual Reset(事件类型是 ManualResetEvent)
13 Event is Waiting(当前是等待状态)
说明 false 是等待的状态,然后,我们继续【g】运行一下,等我们的控制台项目输出:mre 默认为 true,即放行状态,请查看!, 我们继续执行【!handle 0000000000000248 f】命令查看。
1 0:000> !handle 0000000000000248 f
2 Handle 248
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65536
10 Name <none>
11 Object Specific Information
12 Event Type Manual Reset
13 Event is Set
然后,我们继续【g】运行一下,等我们的控制台项目输出:mre Reset后为 false,即等待状态,请查看!我们继续执行【!handle 0000000000000248 f】命令查看。
1 0:000> !handle 0000000000000248 f
2 Handle 248
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65535
10 Name <none>
11 Object Specific Information
12 Event Type Manual Reset
13 Event is Waiting(等待了)
我们再次输入 auto 测试一下 AutoResetEvent。
【g】继续运行,提示【选择事件模型:1、Manual(手动模式) 2、Auto(自动模式) 3、Exit(退出)】,此次,我们输入 auto,控制台程序输出"are 默认为 false,即等待状态,请查看! "字样。
我们在托管堆上查找一下 AutoResetEvent 对象,执行命令【!DumpHeap -type AutoResetEvent】。
1 0:000> !DumpHeap -type AutoResetEvent
2 Address MT Size
3 012b87014318 7ff8da3e5f58 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 7ff8da3e5f58 1 24 System.Threading.AutoResetEvent
8 Total 1 objects, 24 bytes
AutoResetEvent 对象的地址是 012b87014318,我们直接使用【!do】或者【!DumpObj】命令查看对象详情。
1 0:000> !do 012b87014318
2 Name: System.Threading.AutoResetEvent
3 MethodTable: 00007ff8da3e5f58
4 EEClass: 00007ff8da3d3638
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff8da3e3318 4000b7a 8 ...es.SafeWaitHandle 0 instance 0000012b87014330 _waitHandle11 00007ff8da2870a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
_waitHandle 是应用类型的实例变量,我们继续使用【!do 0000012b87014330】命令查看该类型的详情。
1 0:000> !do 0000012b87014330
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle
3 MethodTable: 00007ff8da3e3318
4 EEClass: 00007ff8da3d2968
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff8da2870a0 400126e 8 System.IntPtr 1 instance 00000000000002A4 handle11 00007ff8da211188 400126f 10 System.Int32 1 instance 4 _state
12 00007ff8da1dd070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ff8da1dd070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
SafeWaitHandle 类型内部又包含了一个 handle 类型对象,值是 00000000000002A4,针对这个值我们可以使用【!dumpvc】查看,也可以使用【!handle】命令查看。
1 0:000> !handle 00000000000002A4 f
2 Handle 2a4
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 32769
10 Name <none>
11 Object Specific Information
12 Event Type Auto Reset(AutoResetEvent)13 Event is Waiting(False 就是等待)
【g】继续运行,控制台程序输出"are 默认为 true,即放行状态,请查看! "字样,再次执行【!handle 00000000000002A4 f】命令。
1 0:000> !handle 00000000000002A4 f
2 Handle 2a4
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65536
10 Name <none>
11 Object Specific Information
12 Event Type Auto Reset13 Event is Set
【g】继续运行,控制台程序输出"are Reset 后为 false,即等待状态,请查看! "字样,再次执行【!handle 00000000000002A4 f】命令。
1 0:000> !handle 00000000000002A4 f
2 Handle 2a4
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65535
10 Name <none>
11 Object Specific Information
12 Event Type Auto Reset
13 Event is Waiting
我们都知道 AutoResetEvent 和 ManulResetEvent 的功能就是 Windows 底层的功能,说白了就是 C# 只是使用了 Windows 内核提供的事件,C# 不过是对其进行了包装,如果你想要查看内存地址,必须到内核态去看。
我们有了句柄的值了 00000000000002A4,我们需要借助【Process Explorer】工具找到句柄的内核态地址。打开这个工具,然后在【Filter by name】输入项目名称 ExampleCore_6_2,结果如图:
我们在【ProcessExplorer】工具下面【Handles】选项中找到我的事件对象,然后双击打开属性对话框,如图:
我们就找到了内核地址了。打开一个 Windbg,点击【File】-->【Attach to Kernel】,右侧选择【local】,点击【ok】进入调试器界面。使用【dp 0xFFFF940C4DC47A60】命令。当前值:0(00000000),控制台程序输出"are 默认为 false,即等待状态,请查看!"
1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60 00000000`00060001
切换到用户态 Windbg 继续【g】运行,控制台程序输出"are 默认为 true,即放行状态,请查看!"字样。回到内核态 Windbg 继续运行【dp 0xFFFF940C4DC47A60】命令。
1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60 00000001`00060001
然后,我们再【g】一下【用户态】的 Windbg,控制台输出"are Reset后为 false,即等待状态,请查看!"字样,当前值:0(00000000),然后切换到【内核态】的Windbg,继续使用【dp】命令,查看一下。
1 lkd> dp 0xFFFF940C4DC47A60 l1
2 ffff940c`4dc47a60 00000000`00060001
我们就看到了,状态是0和1相互切换的。
4.2.2、互斥体(内核锁)
A、基础知识
互斥体(Mutex)是一个内核态的同步结构,即可以用于对某个进程内的线程进行同步,也可以在多个进程之间进行同步(通过在创建互斥体时指定名称)。通常来说,如果所有同步操作都位于同一个进程内,那么应该使用监视器对象(Monitor/Lock)或者其他的用户态同步原语。而另一方面,如果需要在多个进程之间进行同步,最合适的就是使用命名互斥体了。
由于互斥体是一种内核态结构,因此,用户态代码需要 System.Threading.Mutex 来访问互斥体。
当在用户态中进行调试时,可以使用【!do】或者【!DumpObj】命令来获取关于互斥体更多详细的信息。
在内核态的数据的 0 表示拥有锁,1 表示释放锁。
Mutex 类型内部包含了 SafeWaitHandle 引用类型的一个字段 _waitHandle,_waitHandle** 类型内部包含了一个值类型的(System.IntPtr)的 handle 实现的同步操作。**
B、眼见为实
调试源码:ExampleCore_6_3
调试任务:分别在用户态和内核态两中情况下 Mutex 值的变化。
由于我们需要在用户态和内核态查看同步对象具体值的变化,需要开启两种调试器,一种是内核态的调试器,一种是用户态的调试器。
1)、KD 和 NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_3\bin\Debug\net8.0\ExampleCore_6_3.exe】,打开调试器。
【g】开始运行我们的调试器,直到调试器输出如图,并进入中断模式,就可以开始我们的调试了。效果如图:
我们现在托管堆上查找一下 Mutex 对象,执行【!DumpHeap -type Mutex】命令。
1 0:000> !DumpHeap -type Mutex
2 Address MT Size
3 0000013097009628 00007ffef219a190 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 00007ffef219a190 1 24 System.Threading.Mutex
8 Total 1 objects
红色标注的就是 Mutex 对象的地址 0000013097009628,针对该地址执行【!do 0000013097009628】命令查看详情。
1 0:000> !do 0000013097009628
2 Name: System.Threading.Mutex 3 MethodTable: 00007ffef219a190
4 EEClass: 00007ffef21a2ef8
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffef219ee70 4000b7a 8 ...es.SafeWaitHandle 0 instance 0000013097009780 _waitHandle11 00007ffef20c70a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
我们看到了Mutex 类型的内部包含了 SafeWaitHandle 类型的对象 _waitHandle,地址是 0000013097009780,针对该地址继续执行【!do 0000013097009780】命令查看其详情。
1 0:000> !do 0000013097009780
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle 3 MethodTable: 00007ffef219ee70
4 EEClass: 00007ffef21a59e8
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffef20c70a0 400126e 8 System.IntPtr 1 instance 0000000000000290 handle11 00007ffef2051188 400126f 10 System.Int32 1 instance 4 _state
12 00007ffef201d070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ffef201d070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
SafeWaitHandle 类型的内部包含了句柄对象 handle,它的值是 0000000000000290,针对该值执行【!handle 0000000000000290 f】命令查看句柄的详情。
1 0:000> !handle 0000000000000290 f
2 Handle 290
3 Type Mutant
4 Attributes 0
5 GrantedAccess 0x1f0001:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState
8 HandleCount 2
9 PointerCount 65536
10 Name <none>
11 Object Specific Information
12 Mutex is Owned(说明已经获取了锁)13 Mutant Owner b24.de4(这是拥有锁的线程 OSID de4)
我们可以使用【!t】命令验证这一点。
1 0:000> !t
2 ThreadCount: 3
3 UnstartedThread: 0
4 BackgroundThread: 2
5 PendingThread: 0
6 DeadThread: 0
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 de4 0000013092951570 2a020 Preemptive 0000013097009EF0:000001309700A610 0000013092992E10 -00001 MTA
11 6 2 23f0 00000130943ADDA0 21220 Preemptive 0000000000000000:0000000000000000 0000013092992E10 -00001 Ukn (Finalizer)
12 7 3 36dc 000001309295D370 2b220 Preemptive 0000000000000000:0000000000000000 0000013092992E10 -00001 MTA
13 0:000>
关系如图:
我们看到了用户态下 Mutex 值的变化,也需要看看内核态上数据的变化,因此,我们需要借助【Process Explorer】工具。
具体操作如图:
我们需要双击【ProcessExplorer】下方的【Handles】标红的数据项,打开 Mutex 属性对话框,就能找到内核地址了。
在内核态的地址是 0xFFFFD2824D881CD0,有了地址,我们需要打开【KD】内核调试器,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,数据命令【kd -kl】打开调试器,直接执行命令【dp 0xFFFFD2824D881CD0 l1】。
1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0 00000000`00000002
Mutex 有了锁,内核数据的值是 00000000。我们需要切换到【NTSD】用户态调试器,继续【g】执行,直到调试器自动进入中断模式。输出如图:
说明此时已经释放了锁,再次执行【!handle 0000000000000290 f】查看句柄的变化。
1 0:000> !handle 0000000000000290 f
2 Handle 290
3 Type Mutant
4 Attributes 0
5 GrantedAccess 0x1f0001:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState
8 HandleCount 2
9 PointerCount 65534
10 Name <none>
11 Object Specific Information
12 Mutex is Free(现在已经释放锁了)
同样,我们切换到内核【kd】调试器,执行命令【dp 0xFFFFD2824D881CD0 l1】,查看结果。
1 lkd> dp 0xFFFFD2824D881CD0 l1
2 ffffd282`4d881cd0 00000001`00000002
内核态的数据的值现在是 1 了,说明 Mutex 已经释放了锁。
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】调试器,依次点击【文件】---【Launch executable】,加载我们的调试项目:ExampleCore_6_3.exe,进入到调试器。
直接使用【g】命令运行调试器,直到我们的控制台程序输出"已进入保护区 "字样,调试器也进入了中断模式。
我们先在堆上查找一下 Mutex 对象,执行【!DumpHeap -type Mutex】命令。
1 0:000> !DumpHeap -type Mutex
2 Address MT Size
3 020ea5409628 7ffecdada190 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 7ffecdada190 1 24 System.Threading.Mutex
8 Total 1 objects, 24 bytes
红色标注的 020ea5409628 数据就是 Mutex 对象的地址,然后,执行命令【!do 020ea5409628】,查看 Mutex 详情。
1 0:000> !do 020ea5409628
2 Name: System.Threading.Mutex
3 MethodTable: 00007ffecdada190
4 EEClass: 00007ffecdae2ef8
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffecdadee70 4000b7a 8 ...es.SafeWaitHandle 0 instance 0000020ea5409780 _waitHandle11 00007ffecda070a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
我们知道了 Mutex 内部还包含了一个 SafeWaitHandle 类型的 _waitHandle,这个类型是引用类型,我们继续【!do 0000020ea5409780】命令,查看这句柄类型的信息。
1 0:000> !do 0000020ea5409780
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle
3 MethodTable: 00007ffecdadee70
4 EEClass: 00007ffecdae59e8
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffecda070a0 400126e 8 System.IntPtr 1 instance 00000000000002A0 handle11 00007ffecd991188 400126f 10 System.Int32 1 instance 4 _state
12 00007ffecd95d070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ffecd95d070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
在 _waitHandle 类型的里面包含了一个值类型的 handle 句柄类型,它的值是 00000000000002A0。有了句柄的值,我们可以使用【!DumpVC 00007ffecda070a0 00000000000002A0】命令查看明细,也可以直接使用【!handle 00000000000002A0 f】命令查看。
1 0:000> !DumpVC 00007ffecda070a0 00000000000002A0
2 Name: System.IntPtr
3 MethodTable: 00007ffecda070a0
4 EEClass: 00007ffecd9e6100
5 Size: 24(0x18) bytes
6 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
7 Fields:
8 MT Field Offset Type VT Attr Value Name
9 00007ffecda070a0 4000525 0 System.IntPtr 1 instance _value
10 00007ffecda070a0 4000526 a78 System.IntPtr 1 static 0000000000000000 Zero
11
12 0:000> !handle 00000000000002A0 f
13 Handle 2a0
14 Type Mutant
15 Attributes 0
16 GrantedAccess 0x1f0001:
17 Delete,ReadControl,WriteDac,WriteOwner,Synch
18 QueryState
19 HandleCount 2
20 PointerCount 65536
21 Name <none>
22 Object Specific Information
23 Mutex is Owned(进入锁状态)24 Mutant Owner 3438.3b78(持有 Mutex 线程的 ID 3b78)
我们可以使用【!t】命令,证明一下。
1 0:000> !t
2 ThreadCount: 3
3 UnstartedThread: 0
4 BackgroundThread: 2
5 PendingThread: 0
6 DeadThread: 0
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 3b78 0000020EA0D37F10 2a020 Preemptive 0000020EA5409EF0:0000020EA540A610 0000020ea0d79770 -00001 MTA
11 5 2 4210 0000020EA0DE7C40 21220 Preemptive 0000000000000000:0000000000000000 0000020ea0d79770 -00001 Ukn (Finalizer)
12 6 3 1ef0 0000020EA0D43DC0 2b220 Preemptive 0000000000000000:0000000000000000 0000020ea0d79770 -00001 MTA
效果如图:
此时,我们可以使用【Process Explorer】工具查找一下 Mutex 对象在内核态上的地址,看看内核态地址上的内容的变化。我们打开【Process Explorer】,如图操作:
我们点击【ProcessExplorer】工具【Handles】选项,双击 Mutant 打开属性对话框。效果如图:
我们找到了内核中的数据的地址 0xFFFFD2824D1A5BB0,此时,我们需要再重新打开另外一个【Windbg Preview】,依次点击【文件】---【Attach to kernel】,在右侧选择【local】,进入到调试器。
继续执行命令【dp 0xFFFFD2824D1A5BB0 l1】命令,看看内核数据是怎么表示的。
1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0 00000000`00000002
此时,我们再次切换到用户态的【Windbg Preview】,【g】继续运行调试器,控制台程序会输出"正在离开保护区 "的字样。我们继续执行【!handle 00000000000002A0 f】命令,看看是什么结果。
1 0:000> !handle 00000000000002A0 f
2 Handle 2a0
3 Type Mutant
4 Attributes 0
5 GrantedAccess 0x1f0001:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState
8 HandleCount 2
9 PointerCount 65534
10 Name <none>
11 Object Specific Information
12 Mutex is Free(已经释放了锁)
已经执行了 ReleaseMutex 方法了,所以就是释放了锁了。
此时,我们再次切换到内核态的【Windbg Preview】,继续执行【dp 0xFFFFD2824D1A5BB0 l1】命令,结果如下:
1 lkd> dp 0xFFFFD2824D1A5BB0 l1
2 ffffd282`4d1a5bb0 00000001`00000002
此时,内核态的数据已经变成 1 了。也就是说在内核态的数据的 0 表示拥有锁,1 表示释放锁。
4.2.3、信号量(内核锁)
A、基础知识
Semaphore(信号量)是一种内核态的同步对象,可以在用户态访问。它类似 Mutex(互斥体),可以实现对资源的互斥访问。它们的区别在于,信号量采用了资源计数,因此可以同时允许 X 个线程访问这个资源。
AutoResetEvent、ManulResetEvent 维护的是 bool 类型的值,信号量本质上就是维护了一个 int 值,这就是两者的区别,我们可以使用 Windbg 来查看一下 waitHandle 的值,可以发现 Semaphore 的 Count 的值在不断的变化。
Semaphore(信号量)可以使用【!do】或者【!DumpObj】命令查看对象信息,也可以使用【!handle】命令查看句柄的信息。
Semaphore 类型内部包含了 SafeWaitHandle 引用类型的一个字段 _waitHandle,_waitHandle** 类型内部包含了一个值类型的(System.IntPtr)的 handle 实现的同步操作。**
B、眼见为实
调试源码:ExampleCore_6_4
调试任务:分别在用户态和内核态看 Semaphore 值的变化。
1)、KD 和 NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_4\bin\Debug\net8.0\ExampleCore_6_4.exe】,打开调试器。
进入调试器后,就可以执行【g】命令运行调试器,直到调试器输出如图就可以开始调试了。
我们现在托管堆上查找一下Semaphore 对象,直接执行【!DumpHeap -type Semaphore】命令。
1 0:000> !DumpHeap -type Semaphore
2 Address MT Size
3 000002754fc09628 00007ffa1ed0a198 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 00007ffa1ed0a198 1 24 System.Threading.Semaphore
8 Total 1 objects
我们知道了 Semaphore 对象的地址是 000002754fc09628,然后执行【!do 000002754fc09628】命令。
1 0:000> !do 000002754fc09628
2 Name: System.Threading.Semaphore 3 MethodTable: 00007ffa1ed0a198
4 EEClass: 00007ffa1ed12ea8
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffa1ed31148 4000b7a 8 ...es.SafeWaitHandle 0 instance 000002754fc09780 _waitHandle11 00007ffa1ec370a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
System.Threading.Semaphore 类型内部包含了一个 SafeWaitHandle 类型的域 _waitHandle,该 _waitHandle 类型的地址是 000002754fc09780,我们有了地址,继续执行【!do 000002754fc09780】命令查看它的详情。
1 0:000> !do 000002754fc09780
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle 3 MethodTable: 00007ffa1ed31148
4 EEClass: 00007ffa1ed16bb8
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffa1ec370a0 400126e 8 System.IntPtr 1 instance 0000000000000290 handle11 00007ffa1ebc1188 400126f 10 System.Int32 1 instance 4 _state
12 00007ffa1eb8d070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ffa1eb8d070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
Microsoft.Win32.SafeHandles.SafeWaitHandle 类型内部包含了 System.IntPtr 类型一个域 handle,它的值是 0000000000000290,有了这个值,我们就可以使用【!handle 0000000000000290 f】命令查看句柄的详情了。
1 0:000> !handle 0000000000000290 f
2 Handle 290
3 Type Semaphore
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65536
10 Name <none>
11 Object Specific Information
12 Semaphore Count 2(当前计数是2,每次执行都会累加)
13 Semaphore Limit 10(这是最大值,超过就会抛出异常)
内容很简单,就不做过多解释了。这个句柄的值 0000000000000290 要记住,后面找内核地址要使用这个。
我们想要找到句柄的内核地址,必须 借助【ProcessExplorer】工具,操作如图:
双击【ProcessExloprer】下方【Handles】的 Semaphore 记录,打开详情,内核地址就在里面。
handle 句柄的内核地址是 0xFFFFA68F9E3CE2E0,有了地址,我们就可以使用【kd】内核调试器显示数据内容了。
打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【kd -kl】打开调试器,执行命令【!dp 0xFFFFA68F9E3CE2E0 l4】。效果如图:
我们再次切换到用户态的【NTSD】调试器中,执行【g】命令和【!handle 0000000000000290 f】,查看变化。
1 0:000> g
2 查看当前的 sem 值。
3 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
4 KERNELBASE!wil::details::DebugBreak+0x2:
5 00007ffb`4129b502 cc int 3
6
7 0:000> !handle 0000000000000290 f
8 Handle 290
9 Type Semaphore
10 Attributes 0
11 GrantedAccess 0x1f0003:
12 Delete,ReadControl,WriteDac,WriteOwner,Synch
13 QueryState,ModifyState
14 HandleCount 2
15 PointerCount 65534
16 Name <none>
17 Object Specific Information
18 Semaphore Count 3(第一次执行是2,现在是 3,每次执行都会递增)
19 Semaphore Limit 10(最大值)
我们再切换到内核态【kd】调试器上,执行【dp 0xFFFFA68F9E3CE2E0 l4】命令。
1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0 00000003`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0 ffffa68f`9e3ce2e8 00000000`0000000a
数值已经变为为 3 了,和用户态调试器输出是一致的。我们可以重复多次,每次查看变化,很简单,我就省略了。
我在用户态下执行执行到计数数字 10,然后在执行,看看会不会发生异常。
1 0:000> g
2 查看当前的 sem 值。
3 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
4 KERNELBASE!wil::details::DebugBreak+0x2:
5 00007ffb`4129b502 cc int 3
6
7 0:000> g
8 查看当前的 sem 值。
9 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
10 KERNELBASE!wil::details::DebugBreak+0x2:
11 00007ffb`4129b502 cc int 3
12
13 0:000> g
14 查看当前的 sem 值。
15 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
16 KERNELBASE!wil::details::DebugBreak+0x2:
17 00007ffb`4129b502 cc int 3
18
19 0:000> g
20 查看当前的 sem 值。
21 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
22 KERNELBASE!wil::details::DebugBreak+0x2:
23 00007ffb`4129b502 cc int 3
24
25 0:000> g
26 查看当前的 sem 值。
27 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
28 KERNELBASE!wil::details::DebugBreak+0x2:
29 00007ffb`4129b502 cc int 3
30
31 0:000> g
32 查看当前的 sem 值。
33 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
34 KERNELBASE!wil::details::DebugBreak+0x2:
35 00007ffb`4129b502 cc int 3
36
37 0:000> g
38 查看当前的 sem 值。
39 (23a8.1c70): Break instruction exception - code 80000003 (first chance)
40 KERNELBASE!wil::details::DebugBreak+0x2:
41 00007ffb`4129b502 cc int 3
42
43 0:000> !handle 0000000000000290 f
44 Handle 290
45 Type Semaphore
46 Attributes 0
47 GrantedAccess 0x1f0003:
48 Delete,ReadControl,WriteDac,WriteOwner,Synch
49 QueryState,ModifyState
50 HandleCount 2
51 PointerCount 65527
52 Name <none>
53 Object Specific Information
54 Semaphore Count 10
55 Semaphore Limit 10
我们在看看内核态数据的变化,切换到【kd】调试器上,执行命令【dp 0xFFFFA68F9E3CE2E0 l4】。
1 lkd> dp 0xFFFFA68F9E3CE2E0 l4
2 ffffa68f`9e3ce2e0 0000000a`00080005 ffffa68f`9e3ce2e8
3 ffffa68f`9e3ce2f0 ffffa68f`9e3ce2e8 00000000`0000000a
我们看到内核态的值已经变成 0000000a了。
我们回到用户态的【NTSD】调试器,继续【g】,看看会发生什么。
1 0:000> g
2 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000 C:\Windows\SYSTEM32\icu.dll
3 (23a8.1c70): CLR exception - code e0434352 (first chance)
4 (23a8.1c70): CLR exception - code e0434352 (!!! second chance !!!)
5 KERNELBASE!RaiseException+0x69:
6 00007ffb`411dcf19 0f1f440000 nop dword ptr [rax+rax]
我们看到发生了 CLR exception 异常了,和我们期望的一样。
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_6_4.exe,直接进入调试器。
进入到调试器后,【g】直接运行调试器,我们的控制台程序会输出"查看当前的 sem 值。 "字样,调试器会自动进入中断模式,此时,就可以开始我们的调试了。
我们先在托管堆上查找一下 Semaphore 对象是否存在,执行命令【!DumpHeap -type Semaphore】。
1 0:000> !DumpHeap -type Semaphore
2 Address MT Size
3 027685409628 7ffa06f8a198 24
4
5 Statistics:
6 MT Count TotalSize Class Name
7 7ffa06f8a198 1 24 System.Threading.Semaphore
8 Total 1 objects, 24 bytes
我们找到了 Semaphore 对象的地址,有了地址就好办了,我们直接执行【!do 027685409628】命令,查看它的详情。
1 0:000> !do 027685409628
2 Name: System.Threading.Semaphore 3 MethodTable: 00007ffa06f8a198
4 EEClass: 00007ffa06f92ea8
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffa06fb1148 4000b7a 8 ...es.SafeWaitHandle 0 instance 0000027685409780 _waitHandle11 00007ffa06eb70a0 4000b79 b28 System.IntPtr 1 static 0000000000000000 InvalidHandle
12 0000000000000000 4000b7b 20 SZARRAY 0 TLstatic t_safeWaitHandlesForRent
13 >> Thread:Value <<
System.Threading.Semaphore 内部包含了一个 SafeWaitHandle 类型的 _waitHandle 域,针对该域我们使用【!do 0000027685409780】命令,查看 _waitHandle 的详情。
1 0:000> !do 0000027685409780
2 Name: Microsoft.Win32.SafeHandles.SafeWaitHandle 3 MethodTable: 00007ffa06fb1148
4 EEClass: 00007ffa06f96bb8
5 Tracked Type: false
6 Size: 32(0x20) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Private.CoreLib.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffa06eb70a0 400126e 8 System.IntPtr 1 instance 0000000000000290 handle11 00007ffa06e41188 400126f 10 System.Int32 1 instance 4 _state
12 00007ffa06e0d070 4001270 14 System.Boolean 1 instance 1 _ownsHandle
13 00007ffa06e0d070 4001271 15 System.Boolean 1 instance 1 _fullyInitialized
Microsoft.Win32.SafeHandles.SafeWaitHandle 内部包含了一个 System.IntPtr 类型的域 handle。我们有了 handle 的值 0000000000000290,就可以使用命令【!handle 0000000000000290 f】查看这个句柄的详情了。
1 0:000> !handle 0000000000000290 f
2 Handle 290
3 Type Semaphore
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65536
10 Name <none>
11 Object Specific Information
12 Semaphore Count 2(当前的计数,初始值我们设置的是 1)
13 Semaphore Limit 10(这个是极限值,超过会抛出异常)
这些都是在用户态调试器下的显示,我们也要看看在内核态下是怎么显示的,记住 handle 的值,后面会用到。
我们想要在内核态想查看数据的变化,必须找到句柄的内核态地址,所以我们要借助【ProcessExplorer】工具,操作如图:
我们在【ProcessExplorer】下方的【Handles】找到 Semaphore 信号量对象,继续双击就可以看到它的内核态的地址。
很简单,就不多说了,我们知道了它的内核地址 0xFFFFA68F9E3E1CE0。此时,我们需要在打开一个【Windbg Preview】,依次点击【文件】----【Attach to kernel】,在窗口的右侧选择【local】,点击【ok】进去调试器,就可以使用【dp 0xFFFFA68F9E3E1CE0 l4】命令查看数据了。
1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0 00000002`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0 ffffa68f`9e3e1ce8 00000000\`0000000a
00000002 就是当前值,00000000`0000000a 就是极限值。
接下来就简单了,我们多次执行用户态的调试器,然后再在内核态调试器里查看变化,一目了然。
我先执行一次用户态下【g】命令,在执行【!handle 0000000000000290 f】命令,查看变化。
1 0:000> !handle 0000000000000290 f
2 Handle 290
3 Type Semaphore
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65534
10 Name <none>
11 Object Specific Information
12 Semaphore Count 3(上一次是2,此次是3)
13 Semaphore Limit 10
我们在切换到内核态调试器中执行【dp 0xFFFFA68F9E3E1CE0 l4】命令。
1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0 00000003`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0 ffffa68f`9e3e1ce8 00000000`0000000a
00000003 变为 3了。
我们可以继续连续执行同样的命令,查看结果。
当我在用户态执行的时候,当当前计数大于10的时候,会发生异常。
1 0:000> !handle 0000000000000290 f
2 Handle 290
3 Type Semaphore
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65527
10 Name <none>
11 Object Specific Information
12 Semaphore Count 10
13 Semaphore Limit 10
14
15 0:000> g
16 ModLoad: 00007ffb`0b440000 00007ffb`0b66e000 C:\Windows\SYSTEM32\icu.dll
17 (3a8c.940): CLR exception - code e0434352 (first chance)
18 (3a8c.940): CLR exception - code e0434352 (!!! second chance !!!)
19 KERNELBASE!RaiseException+0x69:
20 00007ffb`411dcf19 0f1f440000 nop dword ptr [rax+rax]
我们在看看内核态的数据,继续执行命令。
1 lkd> dp 0xFFFFA68F9E3E1CE0 l4
2 ffffa68f`9e3e1ce0 0000000a`8d083005 ffffa68f`9e3e1ce8
3 ffffa68f`9e3e1cf0 ffffa68f`9e3e1ce8 00000000`0000000a
当前的计数值就是 10(十六进制 0xa) 了。
4.2.4、监视器(混合锁)
A、基础知识
监视器是一种对某个对象的访问操作进行监视的结构,它能在对象上创建一个锁,因而只有当持有该监视器对象的线程离开监视器对象后,其他线程才能访问。
监视器和其他同步原语不同,它不是对内核 Windows 同步原语进行是简单的封装,而是在 .NET 中定义的类,即:System.Threading.Monitor,Monitor 类不能实例化,而是包含了一组静态方法,用于获取一个锁。Enter 和 Exit 是很常用的两个方法,Enter 用于获取指定对象上的互斥锁,Exit 用于指定对象上的互斥锁。
lock 关键字就是对 Monitor 对象的封装,lock 语句会自动进入一个监视器,并将保护区域内的代码封装在一个 try/finally 块中,以确保监视器在作用域结束后释放锁。
由于 Monitor 类是一个不能被实例化的对象,因此无法看到它的任何状态,锁的信息保存在被锁定的对象中。
监视器是由 C# 中的 AwareLock 实现的,底层是基于 AutoResetEvent 机制,可以参见 coreclr 源码。因为 Monitor 是基于对象头的同步块索引来实现的,我们可以查看对象头的数据结构就可以明白了。
B、眼见为实
调试源码:ExampleCore_6_5
调试任务:我们使用 Windbg 查看 Monitor 的实现
1)、NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_5\bin\Debug\net8.0\ExampleCore_6_5.exe】打开调试器。【g】直接运行调试器,调试器会输出"4 已进入 Person 锁中 111111 "字样,自动进入中断模式,现在,就可以开始我们的调试了。如图:
因为我们知道是锁的问题,所以可以直接执行【!syncblk】命令。
1 0:008> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 2 0000015A8405C070 3 1 0000015A8404A830 38f0 8 00000119f200c9f8 ExampleCore_6_5.Person
4 -----------------------------
5 Total 3
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
我们说过 Monitor 的底层实现就是 AwareLock,这个标红 0000015A8405C070 地址就是指向 AwareLock。我们使用【dt coreclr!AwareLock 0000015A8405C070】命令查看一番。
1 0:008> dt coreclr!AwareLock 0000015A8405C070
2 +0x000m_lockState : AwareLock::LockState(这里就说明了 Monitor 底层是 AwareLock) 3 +0x004 m_Recursion : 1
4 +0x008 m_HoldingThread : 0x0000015a\`8404a830 Thread(持有锁的托管线程标识,和 !synck 输出 Owning Thread Info 列的前部分一致) 5 +0x010 m_HoldingOSThreadId : 0x38f0(持有锁的操作系统线程标识,**和 !synck 输出 Owning Thread Info 列的后部分一致**)
6 +0x018 m_TransientPrecious : 0n1
7 +0x01c m_dwSyncIndex : 0x80000002(同步块的索引值,和 !synck 输出的 Index 值一样)
8 +0x020m_SemEvent : CLREvent(这里说明,底层还是使用了 Event 同步原语,如果在 Windbg 里是可以点击的,这里没办法了) 9 +0x030 m_waiterStarvationStartTimeMs : 0x10c6663
10 +0x034 m_emittedLockCreatedEvent : 0n0
我们继续使用【dx -r1 (*((coreclr!CLREvent *) XXXXXXXXX))】命令查看 m_SemEvent 是什么。XXXXXXXXX 是 m_SemEvent 的地址,我没有算出来,下面的步骤就没办法进行了。在【Windbg Preview】里是直接可以点击查看的,这就是【Windbg】和 命令行工具的区别。
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch executable】,加载我们的项目文件 ExampleCore_6_5.exe,进入到调试器。
我们使用【g】命令,继续运行调试器,我们的控制台程序输出:6 已进入 Person 锁中 222222(这里不一定是这个,我的输出是这个),Windbg 有一个 int 3 中断,就可以调试程序了。
然后,我们使用【!syncblk】命令,查看一下同步块。
1 0:009> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 2 00000217A549CE10 3 1 00000217A54963A0 26c 9 000001d713010a28 ExampleCore_6_5.Person
4 -----------------------------
5 Total 2
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
我们说过 Monitor 的底层实现就是 AwareLock,这个标红 00000217A549CE10 地址就是指向 AwareLock。我们使用【dt coreclr!AwareLock 00000217A549CE10】命令查看一番。
1 0:009> dt coreclr!AwareLock 00000217A549CE10
2 +0x000m_lockState : AwareLock::LockState(底层的 awarelock) 3 +0x004 m_Recursion : 1
4 +0x008 m_HoldingThread : 0x00000217\`a54963a0 Thread(持有锁的线程的标识,**也就是!syncblk 命令输出的 Owning Thread Info 列的值前部分(00000217A54963A0)**) 5 +0x010 m_HoldingOSThreadId : 0x26c(持有锁的操作系统线程标识,**也就是!syncblk 命令输出的 Owning Thread Info 列的值后部分(26c)**)
6 +0x018 m_TransientPrecious : 0n1
7 +0x01c m_dwSyncIndex : 0x80000002(这个就是同步块索引,也就是!syncblk 命令输出的 Index 列的值)
8 +0x020m_SemEvent : CLREvent**(底层还是使用的 Event 实现同步)** 9 +0x030 m_waiterStarvationStartTimeMs : 0xf4b013
10 +0x034 m_emittedLockCreatedEvent : 0n0
我们继续使用【dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))】命令查看 m_SemEvent 是什么,不用执行命令,直接点击就可以了。
1 0:009> dx -r1 (*((coreclr!CLREvent *)0x217a549ce30))
2 (*((coreclr!CLREvent *)0x217a549ce30)) [Type: CLREvent]
3 [+0x000] m_handle : 0x314 \[Type: void \*\](这里是一个句柄)4 [+0x008] m_dwFlags : 0xd [Type: Volatile<unsigned long>]
既然是一个 handle,我们就使用【!handle 0x314 f】命令查看一下就知道了。
1 0:009> !handle 0x314 f
2 Handle 314
3 Type Event
4 Attributes 0
5 GrantedAccess 0x1f0003:
6 Delete,ReadControl,WriteDac,WriteOwner,Synch
7 QueryState,ModifyState
8 HandleCount 2
9 PointerCount 65537
10 Name <none>
11 Object Specific Information
12 Event Type Auto Reset13 Event is Waiting
我们看到了吧,Monitor 底层也是使用 AutoResetEvent 实现的。
4.2.5、读写锁(ReaderWriterLock)
A、基础知识
Monitor 类每次只允许一个线程独占式的访问一个对象。虽然,在写入操作非常频繁的情况下,Monitor 能工作的很好,但当读取操作多于写操作或者在锁上存在高度竞争的情况下,Monitor 的性能就很受影响了。
为了解决这个问题,系统为我们提供了读写锁,即 ReaderWriterLock 。ReaderWriterLock 能够使多个线程并发的执行读操作,而每次只允许一个线程执行写操作。ReaderWriterLock 类本身就包含了状态来控制对锁的访问。
注意:
.NET Framework 有两个读取器-写入器锁和 ReaderWriterLockSlim、ReaderWriterLock。 建议对所有新开发的项目使用 ReaderWriterLockSlim。 虽然 ReaderWriterLockSlim 类似于 ReaderWriterLock,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。 ReaderWriterLockSlim 避免了许多潜在的死锁情况。 另外,ReaderWriterLockSlim 的性能显著优于 ReaderWriterLock。
B、眼见为实
调试源码:ExampleCore_6_6
调试任务:使用调试器从底层了解 ReaderWriterLock 到底是什么。
1)、NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_6\bin\Debug\net8.0\ExampleCore_6_6.exe】直接进入调试器。
直接【g】运行调试器,直到调试器输出"Press ENTER to exit... "字样时,按组合键【ctrl+c】进入中断模式,开始调试了。
我们现在托管堆上查找一下 ReaderWriterLock 对象,执行【!DumpHeap -type ReaderWriterLock】命令。
1 0:003> !DumpHeap -type ReaderWriterLock
2 Address MT Size
3 000001354f409848 00007ff9e50c75e8 56
4
5 Statistics:
6 MT Count TotalSize Class Name
7 00007ff9e50c75e8 1 56 System.Threading.ReaderWriterLock
8 Total 1 objects
标红的 000001354f409848 就是 ReaderWriterLock 对象的地址,继续执行【!do 000001354f409848】命令,查看它的详情。
1 0:003> !do 000001354f409848
2 Name: System.Threading.ReaderWriterLock
3 MethodTable: 00007ff9e50c75e8
4 EEClass: 00007ff9e50aa388
5 Tracked Type: false
6 Size: 56(0x38) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ff9e4f593f0 400001d 8 System.Void 0 instance 0000000000000000_readerEvent11 00007ff9e4f593f0 400001e 10 System.Void 0 instance 0000000000000000_writerEvent12 00007ff9e4f7a5f0 400001f 18 System.Int64 1 instance 1_lockID13 00007ff9e4f51188 4000020 20 System.Int32 1 instance 0_state14 00007ff9e4f51188 4000021 24 System.Int32 1 instance -1_writerID15 00007ff9e4f51188 4000022 28 System.Int32 1 instance 1 _writerSeqNum
16 00007ff9e4f767b8 4000023 2c System.UInt16 1 instance 0_writerLevel17 00007ff9e4f51188 400001b 58 System.Int32 1 static 500 DefaultSpinCount
18 00007ff9e4f7a5f0 400001c 50 System.Int64 1 static 1 s_mostRecentLockID
_readerEvent 和 _writerEvent 是指针类型,分别用来控制对读取队列和写入队列的访问。_state 表示锁的各种不同的内部状态。_lockID 持有锁线程的内部标识。_writerID 持有锁线程的 ID,_writerLevel 持有写入线程的递归锁计数(Recursive lock count)。
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】调试器,依次点击【文件】---【Launch executable】,加载我们的项目文件 ExampleCore_6_6.exe,直接进入调试器。执行【g】命令,运行调试器,直到我们的控制台程序输出"Press ENTER to exit... "字样,然后点击调试器的【break】按钮,进入中断状态,现在开始我们的调试吧。
我们现在托管堆上查找一下 ReaderWriterLock 对象,执行【!DumpHeap -type ReaderWriterLock】命令。
1 0:006> !DumpHeap -type ReaderWriterLock
2 Address MT Size
3 022afb409848 7ffa021b7788 56
4
5 Statistics:
6 MT Count TotalSize Class Name
7 7ffa021b7788 1 56 System.Threading.ReaderWriterLock
8 Total 1 objects, 56 bytes
红色标注的 022afb409848 就是 ReaderWriterLock 对象的地址,有了地址,我们执行【!do 022afb409848】命令。
1 0:006> !do 022afb409848
2 Name: System.Threading.ReaderWriterLock
3 MethodTable: 00007ffa021b7788
4 EEClass: 00007ffa0219a4f0
5 Tracked Type: false
6 Size: 56(0x38) bytes
7 File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.4\System.Threading.dll
8 Fields:
9 MT Field Offset Type VT Attr Value Name
10 00007ffa020493f0 400001d 8 System.Void 0 instance 0000000000000000_readerEvent11 00007ffa020493f0 400001e 10 System.Void 0 instance 0000000000000000_writerEvent12 00007ffa0206a5f0 400001f 18 System.Int64 1 instance 1_lockID13 00007ffa02041188 4000020 20 System.Int32 1 instance 0_state14 00007ffa02041188 4000021 24 System.Int32 1 instance -1_writerID15 00007ffa02041188 4000022 28 System.Int32 1 instance 1 _writerSeqNum
16 00007ffa020667b8 4000023 2c System.UInt16 1 instance 0_writerLevel17 00007ffa02041188 400001b 58 System.Int32 1 static 500 DefaultSpinCount
18 00007ffa0206a5f0 400001c 50 System.Int64 1 static 1 s_mostRecentLockID
_readerEvent 和 _writerEvent 是指针类型,分别用来控制对读取队列和写入队列的访问。_state 表示锁的各种不同的内部状态。_lockID 持有锁线程的内部标识。_writerID 持有锁线程的 ID,_writerLevel 持有写入线程的递归锁计数(Recursive lock count)。
4.2.6、线程池
创建新线程的方式很多,比如:Thread、ThreadPool、Task、Parallel 等,除了 Thread 类,其他都是使用了线程池技术,让 CLR 来高效的管理这个线程池,所以,.NET 开发建议使用具有线程池的类型。每个进程有且只有一个线程池。需要注意一点,当线程被还回线程池时,在线程上设置的任何状态都会保留下来。如果同一个线程被用于服务另一个任务请求,并且该任务请求与线程状态不兼容,那么程序可能会失败。
4.3、同步的内部细节
4.3.1、对象头
在托管堆上保存的每个对象都包含一个对象头,在对象头中包含了与对象相关的一组信息。在对象头中可以包含包括散列码、锁信息、同步块索引等。如图所示:
在对象中需要保存的所有信息总量大于对象头本身的大小。这句话的意思,任何一个对象都可能需要(也可能不需要)所有的信息,这取决于具体的执行流程。只要在执行操作中需要的信息(例如:对象的散列码)不超过对象头的大小,这些信息就会直接保存在对象头中。如果对象头中无法保存所需的信息,CLR 会创建一个独立的同步块数据结构,并将当前保存在对象头中的所有信息都复制到这个同步块中,并且,将对象头中保存的信息替换成同步块在同步块表中的索引。同步块位于非 GC 的内存中,通过同步块表中的索引来访问。
CLR 通过对象头中的位元的组织方式区分对象头中包含的信息的种类。如果在对象头中设置了掩码 0x08000000,就表示对象头中包含要么是对象的散列码,要么是同步块索引。如果同时设置了掩码 0x04000000,就表示对象头中保存的是散列码。
4.3.2、同步块
A、基础知识
这一节主要是验证对象头保存数据的方式,例如:如何保存锁信息,如何保存散列码等信息。和同步块相关的有一个命令很重要,就是【!syncblk】,如果该命令不携带任何参数,表示它将输出某个线程中所有对象的同步块。当然,我们也可以将同步块的索引值作为参数,输出指定同步块的信息。
请记住,对象指针指向的是类型句柄域,紧接着才是实际的对象数据。在类型句柄前的 4 或者 8 个字节也是对象布局的一部分,其中就包含了对象头,所以,如果我们想找到对象头,就要使用对象的地址减去 4 或者 8 个字节(32位减去4字节,4 字节就是 0x4,64位减去8字节,8字节就是 0x8)就是对象头的数据。
如果我们想得到同步块索引,可以执行如下操作:
1)、通过使用【!ClrStack -a】命令输出这个线程的所有的调用栈及其所有参数和局部变量。最底层的栈帧对应于 Main 方法。
2)、继续使用【!do】命令,确认是否是我们需要的对象。
3)、最后使用【dp】命令输出对象头,它位于对象指针减去 4 或者 8 个字节(32位减去4字节,4 字节就是 0x4,64位减去8字节,8字节就是 0x8)的位置上。
接下来,我们在说说【!syncblk】命令各列的意思。
Index :同步块索引
SyncBlock :同步块数据结构的地址(未公开)
MonitorHeld :持有的监视器的数量
Recursion :同一个线程获取这个锁的次数
Owning thread info :第一个数据项是指向内部线程数据结构的指针,第二个数据项是操作系统线程ID,第三个数据项是调试器线程ID
SyncBlock Owner :第一个数据项是指向持有锁的对象的指针,第二个数据项是锁所在的对象的类型
B、眼见为实
调试源码:ExampleCore_6_7
调试任务:通过调试器了解对象头保存数据的方式。
1)、NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.exe】打开调试器。
进入调试器后,直接【g】运行调试器,直到调试器输出如图:
此时,我们按组合键【ctrl+c】进入中断模式,由于我们是手动中断的,需要执行【~0s】命令将调试器上下文切换到托管线程上下文中。
1 0:009> ~0s
2 ntdll!NtWriteFile+0x14:
3 00007ffd`ece6d0e4 c3 ret
继续执行【!clrstack -a】命令,查看托管线程调用栈和所有参数和变量。
1 0:000> !clrstack -a
2 OS Thread Id: 0x1c20 (0)
3 Child SP IP Call Site
4 0000003E0397E0E0 00007ffdece6d0e4 [InlinedCallFrame: 0000003e0397e0e0]
5 0000003E0397E0E0 00007ffdc9b87d6b [InlinedCallFrame: 0000003e0397e0e0]
6 。。。。。。(省略了)
7 0000003E0397E800 00007FFCC6E51ABF ExampleCore_6_7.Program.Run()
8 PARAMETERS:
9 this (0x0000003E0397E870) = 0x0000020b49409628
10 LOCALS:
11 0x0000003E0397E858 = 0x000000000378734a
12
13 0000003E0397E870 00007FFCC6E51988 ExampleCore_6_7.Program.Main(System.String[])
14 PARAMETERS:
15 args (0x0000003E0397E8B0) = 0x0000020b49408e90
16 LOCALS:
17 0x0000003E0397E898 = 0x0000020b49409628
18
19 0:000>
0x0000020b49409628 这个就是 Program 对象地址,我们可以使用【!do 0x0000020b49409628】命令,确认一下。
1 0:000> !do 0x0000020b49409628
2 Name: ExampleCore_6_7.Program3 MethodTable: 00007ffcc6f00100
4 EEClass: 00007ffcc6eefb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None
证明了我们的猜想。我们知道对象的地址指向的是类型句柄,如果想要查看对象头的数据,还要减去 4 或者 8 个字节才是对象头的地址,4 或者 8 是根据系统的位数 32 位就减去 4,64 位就减去 8,从对象的地址也可以看出是该减去 8 还是 4,我的对象地址是 0x0000020b49409628,就要减去 8 了。
执行【dp 0x0000020b49409628-0x8 l1】命令,查看对象头的数据。
1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620 0f78734a`00000000
我们看到了对象头的值是 0f78734a,这个值是可以推出来的。我们知道对象的 HashCode 的值是 58225482,这个数字是十进制的结果值,我们转换成十六进制,看看是多少。
1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a
0378734a 这个值和【dp】命令的结果 0f78734a 类似,我们再使用 58225482 十六进制表示 0378734a,分别加上 0x08000000 和 0x04000000,执行命令【? 0378734a++0x08000000+0x04000000】,这个值就是对象头的值。
1 0:000> ? 0378734a++0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000\`0f78734a
00000000`0f78734a 这个值和【dp】命令的输出是一样的,说明对象头保存是散列码了。
我们恢复调试器的执行,直到调试器输出"Press any key to release lock "字样,点击【ctrl+c】组合键,进入中断模式。
如图:
由于 GC 会执行垃圾回收,内存压缩和对象地址转移,我们避免产生误操作。还是先执行线程切换【~0s】。
1 0:002> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3 ret
我们执行【!clrstack -a】命令查看托管线程调用栈,查找我们的Program 对象。
1 0:000> !clrstack -a
2 OS Thread Id: 0x1e14 (0)
3 Child SP IP Call Site
4 000000EB377AE170 00007ff942b0d0a4 [InlinedCallFrame: 000000eb377ae170]
5 000000EB377AE170 00007ff91b2076eb [InlinedCallFrame: 000000eb377ae170]
6 。。。。。。(省略了)
7
8 000000EB377AE4C0 00007FF85B971AEC ExampleCore_6_7.Program.Run()
9 PARAMETERS:
10 this (0x000000EB377AE530) = 0x0000020c1a409628
11 LOCALS:
12 0x000000EB377AE518 = 0x000000000378734a
13
14 000000EB377AE530 00007FF85B971988 ExampleCore_6_7.Program.Main(System.String[])
15 PARAMETERS:
16 args (0x000000EB377AE570) = 0x0000020c1a408e90
17 LOCALS:
18 0x000000EB377AE558 = 0x0000020c1a409628
0x0000020c1a409628 这个地址就是我们的 Program对象的地址,我们可以使用【!DumpObj 0x0000020c1a409628】命令确认一下。
1 0:000> !DumpObj 0x0000020c1a409628
2 Name: ExampleCore_6_7.Program
3 MethodTable: 00007ff85ba20100
4 EEClass: 00007ff85ba0fb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None
我们现在就可以查看对象头中的内容了。执行命令【dp 0x0000020c1a409628-0x8 l1】,由于我的程序是64位的,所以需要减去 8,32位减去4就可以了。
1 0:000> dp 0x0000020c1a409628-0x8 l1
2 0000020c`1a409620 08000001`00000000
由于内容太多了,需要创建同步块存储内容,所以在对象头中就存储同步块的索引了。08000000 表示是同步块,1 表示同步块在同步块表中的索引位置。
此时,我们可以使用【!syncblk 0x1】命令查看同步块的信息了。
1 0:000> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 1 0000024F99D534E8 1 1 0000020F030FE480 3e34 0 0000020f07809628 ExampleCore_6_7.Program
4 -----------------------------
5 Total 1
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】,依次点击【文件】----【Launch Excutable】,加载我们的项目文件 ExampleCore_6_7.exe,进入到调试器后,我们使用【g】命令直接运行调试器,直到控制台程序输出"Press any key to acquire lock "字样。我们回到调试器界面,点击【Break】按钮,进入中断模式,开始我们的调试旅程。
由于我们手动中断,所以必须切换到托管线程上下文中,因为当前在调试器的上下文环境中,执行命令【~0s】切换线程上下文。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ffd`ece6d0a4 c3 ret
继续执行【!clrstack -a】命令,查看托管线程调用栈和所有参数。
1 0:000> !clrstack -a
2 OS Thread Id: 0x1138 (0)
3 Child SP IP Call Site
4 00000026DAD7E8A0 00007ffdece6d0a4 [InlinedCallFrame: 00000026dad7e8a0]
5 00000026DAD7E8A0 00007ffd667676eb [InlinedCallFrame: 00000026dad7e8a0]
6 。。。。。。(省略无用的)
7
8 00000026DAD7EBF0 00007ffcc0fa1aa0 ExampleCore_6_7.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\Program.cs @ 17]
9 PARAMETERS:
10 this (0x00000026DAD7EC60) = 0x000001c4c6409628
11 LOCALS:
12 0x00000026DAD7EC48 = 0x000000000378734a
13
14 00000026DAD7EC60 00007ffcc0fa1988 ExampleCore_6_7.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\Program.cs @ 8]
15 PARAMETERS:
16 args (0x00000026DAD7ECA0) = 0x000001c4c6408e90
17 LOCALS:
18 0x00000026DAD7EC88 = 0x000001c4c6409628
红色标注的地址就是 0x000001c4c6409628 就是 Program 类型对象的地址,我们可以使用【!do 0x000001c4c6409628】命令验证。
1 0:000> !do 0x000001c4c6409628
2 Name: ExampleCore_6_7.Program3 MethodTable: 00007ffcc1050100
4 EEClass: 00007ffcc103fb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_7\bin\Debug\net8.0\ExampleCore_6_7.dll
8 Fields:
9 None
继续使用【dp 0x000001c4c6409628-0x8 l1】命令,查看对象头的数据。
1 0:000> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620 0f78734a`00000000
对象头的当前值 0f78734a,表示在对象头中保存的是散列码,我们控制台程序散列码的输出值是 58225482,这个数字是十进制的,我们转换为十六进制,看看结果。
1 0:000> ? 0n58225482
2 Evaluate expression: 58225482 = 00000000`0378734a
我们看到了十进制的 58225482 转换为十六进就是 0378734a,0x08000000 这个掩码只能确定是不是散列码,也有可能是同步块索引,只有在加上一个 0x04000000 掩码才能确定是散列码,所以,我们使用执行【? 00000000`0378734a+0x08000000+0x04000000】命令,这个结果就是对象头的值。
1 0:000> ? 00000000`0378734a+0x08000000+0x04000000
2 Evaluate expression: 259552074 = 00000000`0f78734a
0f78734a 这个值和【dp】命令的输出是一样的,说明对象头保存是散列码了。
我们恢复调试器的执行,直到控制台程序输出"Press any key to release lock "字样,回到调试器,点击【Break】按钮,继续进入中断模式。如图:
我们继续执行【dp 0x000001c4c6409628-0x8 l1】命令,看看对象头的输出。说明一下,在执行此命令之前,最好执行一次【!clrstack -a】命令获取对象地址,然后执行【!do】命令确认对象,最后在执行这个【dp】命令,因为垃圾收集器会在任意时刻移动对象,对象的地址也可能变化。
1 0:001> dp 0x000001c4c6409628-0x8 l1
2 000001c4`c6409620 08000001`00000000
08000001 这个结果值就很合理了,就是同步块索引了。此时,我们可以使用【!syncblk 0x1】命令查看同步块的信息了。
1 0:001> !syncblk 0x1
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 1 000001C4C1FC6268 1 1 000001C4C1F29FE0 1138 0 000001c4c6409628 ExampleCore_6_7.Program
4 -----------------------------
5 Total 1(同步块表中同步块的总数量)
6 CCW 0(COM 可调用包装的数量)
7 RCW 0(运行时可调用包装的数量)
8 ComClassFactory 0
9 Free 0(在同步块表中多少个同步块)
4.3.3、瘦锁
A、基础知识
在 CLR 2.0 中引入了瘦锁,它实现了一种更高效的机制管理锁。在使用瘦锁时,保存在对象头中唯一的信息就是获取锁的线程 ID(既没有同步块),它是一个自旋锁(spinning lock)。因为要实现一个更为高效的等待锁,需要保存更多的信息。然后,这个瘦锁并不会无限的循环,而是当自旋到某个阈值就会停止。如果超过了这个阈值还不能获取这个锁,那么接下来就会创建一个实际的同步块,并将相应的信息保存下来来实现一个高效的等待(例如一个事件)。
CLR 通常采用以下算法来判断是使用同步块和瘦锁。
I、如果同步块存在,则使用同步块存储锁信息。
II、如果同步块不存在,判断在当前对象的对象头中是否可以包含一个瘦锁。
如果可以容纳,就将线程 ID 保存在对象头中。如果后面需要保存更多的信息,那么将自动创建一个同步块,并把当前对象头中的内容转移到新的同步块中。
如果不可以容纳,就会创建一个新的同步块,并将对象头的内容转移到新的同步块中,并保存锁。
我们可以通过调试器来验证这个算法,通过以下三步就可以了。
1】、在获取锁之前,将同步块转储出来,验证其为空。
2】、获取这个锁,中断程序执行,并验证已经创建了一个瘦锁。
3】、获取散列码,中断程序执行,并验证这个瘦锁已经被一个同步块替代了。
我们可以使用【!DumpHeap -thinlock】命令找出托管堆上所有带有瘦锁的对象。
B、眼见为实
调试源码:ExampleCore_6_8
调试任务:验证瘦锁存储的算法。
1)、NTSD 调试
编译项目,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.exe】打开调试器。
进入调试器,【g】直接运行,直到调试器输出,并暂停,如图:
按【ctrl+c】组合键进入中断模式,还需要切换到托管线程上下文中,执行【~0s】命令,继续执行【!clrstack -a】命令查找 Program 对象。
1 0:000> !clrstack -a
2 OS Thread Id: 0x16d4 (0)
3 Child SP IP Call Site
4 000000B881DDE628 00007ff942b0e814 [PrestubMethodFrame: 000000b881dde628] System.Text.DecoderDBCS.GetChars(Byte[], Int32, Int32, Char[], Int32, Boolean)
5 。。。。。。(省略了)
6 000000B881DDE980 00007FF83A191A52 ExampleCore_6_8.Program.Run()
7 PARAMETERS:
8 this (0x000000B881DDEA00) = 0x000001c613c09628
9 LOCALS:
10 0x000000B881DDE9E8 = 0x0000000000000000
11
12 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
13 PARAMETERS:
14 args (0x000000B881DDEA40) = 0x000001c613c08e90
15 LOCALS:
16 0x000000B881DDEA28 = 0x000001c613c09628
0x000001c613c09628 就是 Program 类型对象地址,执行【!do 0x000001c613c09628】命令验证一下。
1 0:000> !do 0x000001c613c09628
2 Name: ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass: 00007ff83a22fb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None
执行【dp 0x000001c613c09628-8 l1】命令查看对象头的内容。
1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620 00000000\`00000000
0 就是表示没有任何值。继续【g】恢复调试器的执行,直到调试器输出,如图:
继续执行切换线程和查看线程的命令,分别是【~0s】、【!clrstack -a】查找我们的 Program 对象。
1 0:001> ~0s
2 ntdll!NtWriteFile+0x14:
3 00007ff9`42b0d0e4 c3 ret
4
5 0:000> !clrstack -a
6 OS Thread Id: 0x16d4 (0)
7 Child SP IP Call Site
8 000000B881DDE260 00007ff942b0d0e4 [InlinedCallFrame: 000000b881dde260]
9 000000B881DDE260 00007ff91e0b7d6b [InlinedCallFrame: 000000b881dde260]
10 。。。。。。(省略了)
11 000000B881DDE980 00007FF83A191A71 ExampleCore_6_8.Program.Run()
12 PARAMETERS:
13 this (0x000000B881DDEA00) = 0x000001c613c09628
14 LOCALS:
15 0x000000B881DDE9E8 = 0x0000000000000000
16
17 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
18 PARAMETERS:
19 args (0x000000B881DDEA40) = 0x000001c613c08e90
20 LOCALS:
21 0x000000B881DDEA28 = 0x000001c613c09628
继续执行【!do 0 x000001c613c09628】命令,查看内容。
1 0:000> !do 0x000001c613c09628
2 Name: ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass: 00007ff83a22fb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None
10 ThinLock owner 1 (000001C60F9C8A80), Recursive 0
ThinLock owner 1 (000001C60F9C8A80), Recursive 0 说明对象上有了一个瘦锁,线程对象的 ID 是 000001C60F9C8A80 ,递归技术是 0。
继续执行【dp 0x000001c613c09628-8 l1】命令,查看对象头。
1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620 00000001`00000000
这里的 1 就是持有锁的线程 ID,是托管线程的 ID 值。可以使用【!t】或者【!threads】命令验证。
1 0:000> !t
2 ThreadCount: 2
3 UnstartedThread: 0
4 BackgroundThread: 1
5 PendingThread: 0
6 DeadThread: 0
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 16d4 000001C60F9C8A80 2a020 Preemptive 000001C613C13D60:000001C613C14660 000001C60F9C0540 -00001 MTA
11 6 2 2724 000001C60FA11810 21220 Preemptive 0000000000000000:0000000000000000 000001C60F9C0540 -00001 Ukn (Finalizer)
【dp】命令和【!t】命令都能找到 000001C60F9C8A80 这个指针的值。
我们继续【g】恢复调试器的执行,直到调试器输出如图:
此时,说明对象的锁和散列值都保存了,然后我们【ctrl+c】进入中断模式,切换线程【~0s】,并且执行【!clrstack -a】命令查找 Program 对象,查一下它的状态。
1 0:000> !clrstack -a
2 OS Thread Id: 0x16d4 (0)
3 Child SP IP Call Site
4 000000B881DDE630 00007ff942b0d0a4 [InlinedCallFrame: 000000b881dde630]
5 000000B881DDE630 00007ff91e0b76eb [InlinedCallFrame: 000000b881dde630]
6 。。。。。。(省略了)
7 000000B881DDE980 00007FF83A191B0E ExampleCore_6_8.Program.Run()
8 PARAMETERS:
9 this (0x000000B881DDEA00) = 0x000001c613c09628
10 LOCALS:
11 0x000000B881DDE9E8 = 0x000000000378734a
12
13 000000B881DDEA00 00007FF83A191988 ExampleCore_6_8.Program.Main(System.String[])
14 PARAMETERS:
15 args (0x000000B881DDEA40) = 0x000001c613c08e90
16 LOCALS:
17 0x000000B881DDEA28 = 0x000001c613c09628
18
19 0:000>
执行【!do 0x000001c613c09628】命令,查看一下该对象有什么变化吗?
1 0:000> !do 0x000001c613c09628
2 Name: ExampleCore_6_8.Program
3 MethodTable: 00007ff83a240100
4 EEClass: 00007ff83a22fb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None(这里没有东西了,锁信息已经转到同步块中保存了。)
继续执行【dp 0x000001c613c09628-8 l1】命令,查看一下对象头保存的数据。
1 0:000> dp 0x000001c613c09628-8 l1
2 000001c6`13c09620 08000001`00000000
08000001 看到这个值就知道是同步块索引了。我们使用【!syncblk】命令查看同步块的数据。
1 0:000> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 -----------------------------(这里是需要有值的,我这里没有输出,原因不知道,重来一次就可以)
4 Total 1
5 CCW 0
6 RCW 0
7 ComClassFactory 0
8 Free 0
我们也可以使用【!DumpHeap -thinlock】命令查找托管堆上所有具有瘦锁的对象。
1 0:000> !DumpHeap -thinlock
2 Address MT Size
3 000001c613c12ec0 00007ff83a295820 24 ThinLock owner 1 (000001C60F9C8A80) Recursive 0
4 Found 1 objects.
内容很简单,就不解释了。
2)、Windbg Preview 调试
编译项目,打开【Windbg Preview】,依次点击【文件】---【Launch executable】,加载我们的控制台项目 ExampleCore_6_8.exe,点击【打开】进入调试器。
进入调试器后,直接执行【g】命令,运行调试器,直到我们的控制台程序输出"Press any key to acquire lock ",此时,回到调试器,点击【Break】按钮,进入到中断模式,开始我们的调试。
由于我们是手动中断的,当前是调试器的上下文,需要切换到托管上下文中,需要执行【~0s】命令。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3 ret
我们使用【!clrstack -a】命令,查看托管线程调用栈,找出我们的 Program 类型的局部变量 program。
1 0:000> !clrstack -a
2 OS Thread Id: 0x34b8 (0)
3 Child SP IP Call Site
4 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100]
5 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100]
6 。。。。。。(省略了)
7
8 0000006CE77EE450 00007ff80e731a52 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 14]
9 PARAMETERS:
10 this (0x0000006CE77EE4D0) = 0x000001ace3409628
11 LOCALS:
12 0x0000006CE77EE4B8 = 0x0000000000000000
13
14 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
15 PARAMETERS:
16 args (0x0000006CE77EE510) = 0x000001ace3408e90
17 LOCALS:
18 0x0000006CE77EE4F8 = 0x000001ace3409628
0x000001ace3409628 就是Program 类型的实例对象的地址,我们可以使用【!do 0x000001ace3409628】来验证。
1 0:000> !do 0x000001ace3409628
2 Name: ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass: 00007ff80e7cfb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None
我们执行命令【dp 0x000001ace3409628-8 l1】查看它的对象头。
1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620 00000000\`00000000
00000000`00000000 表示没有任何数据。
我们【g】恢复调试器的执行,直到控制台程序输出"Press any key to get hashcode ",此时,对象已经获取了锁,但是还没有获取散列值。回调调试器中,点击【Break】按钮,再次进入中断模式,继续我们的调试。
由于手动进入中断模式,所以需要有调试器上下文切换到托管线程上下文中,执行命令【~0s】。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3 ret
继续执行【!clrstack -a】命令查找 Program 对象。
1 0:000> !clrstack -a
2 OS Thread Id: 0x34b8 (0)
3 Child SP IP Call Site
4 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100]
5 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100]
6 。。。。。。(省略了)
7 0000006CE77EE450 00007ff80e731a78 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 19]
8 PARAMETERS:
9 this (0x0000006CE77EE4D0) = 0x000001ace3409628
10 LOCALS:
11 0x0000006CE77EE4B8 = 0x0000000000000000
12
13 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
14 PARAMETERS:
15 args (0x0000006CE77EE510) = 0x000001ace3408e90
16 LOCALS:
17 0x0000006CE77EE4F8 = 0x000001ace3409628
0x000001ace3409628 这就是我们的 Program 类型实例的地址,可以执行【!do 0x000001ace3409628】命令来验证,我就省略了。
此时,该对象已经获取锁了,我们查看对象头的数据,执行【dp 0x000001ace3409628-8 l1】命令。
1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620 00000001`00000000
00000001 这个就是所有者线程的 ID,此时我们可以执行【!do 0x000001ace3409628】或者【!DumpObj 0x000001ace3409628】命令,查看Program 对象,也有体现。
1 0:000> !do 0x000001ace3409628
2 Name: ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass: 00007ff80e7cfb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None
10 ThinLock owner 1 (000001ACDEFF2770), Recursive 0
红色标注的告诉我们 Program 对象上获取了一个瘦锁,线程对象指针是 000001ACDEFF2770 ,且递归计数位0,我们可以使用【!t】或者【!threads】命令来验证。
1 0:000> !t
2 ThreadCount: 2
3 UnstartedThread: 0
4 BackgroundThread: 1
5 PendingThread: 0
6 DeadThread: 0
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 34b8 000001ACDEFF2770 2a020 Preemptive 000001ACE3412F38:000001ACE3414660 000001acdf03e270 -00001 MTA
11 5 2 419c 000001ACDF01B210 21220 Preemptive 0000000000000000:0000000000000000 000001acdf03e270 -00001 Ukn (Finalizer)
我们看到了【!do】命令和【!t】命令的输出线程ID都是 000001ACDEFF2770,在对象头中包含了持有锁的线程 ID。
接下来,我们执行代码,获取散列码,再次中断执行,查看同步块和瘦锁的状态。
【g】继续运行,直到我们的控制台程序输出"HashCode:58225482 Press any key to release lock "。此时已经有了锁,并且也获取了散列码。回到调试器,点击【Break】按钮,进入中断模式,继续调试。
继续切换线程上下文【~0s】,并执行【!clrstack -a】命令查找我们的 Program 对象。
1 0:001> ~0s
2 ntdll!NtReadFile+0x14:
3 00007ff9`42b0d0a4 c3 ret
4
5 0:000> !clrstack -a
6 OS Thread Id: 0x34b8 (0)
7 Child SP IP Call Site
8 0000006CE77EE100 00007ff942b0d0a4 [InlinedCallFrame: 0000006ce77ee100]
9 0000006CE77EE100 00007ff8bcf376eb [InlinedCallFrame: 0000006ce77ee100]
10 。。。。。。(省略了)
11 0000006CE77EE450 00007ff80e731ae8 ExampleCore_6_8.Program.Run() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 25]
12 PARAMETERS:
13 this (0x0000006CE77EE4D0) = 0x000001ace3409628
14 LOCALS:
15 0x0000006CE77EE4B8 = 0x000000000378734a
16
17 0000006CE77EE4D0 00007ff80e731988 ExampleCore_6_8.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\Program.cs @ 8]
18 PARAMETERS:
19 args (0x0000006CE77EE510) = 0x000001ace3408e90
20 LOCALS:
21 0x0000006CE77EE4F8 = 0x000001ace3409628
执行【!do 0x000001ace3409628】命令,查看 Program 对象。
1 0:000> !do 0x000001ace3409628
2 Name: ExampleCore_6_8.Program
3 MethodTable: 00007ff80e7e0100
4 EEClass: 00007ff80e7cfb48
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_8\bin\Debug\net8.0\ExampleCore_6_8.dll
8 Fields:
9 None(这里没有任何信息了,已经移到同步块中了)
继续执行【dp 0x000001ace3409628 -8 l1】命令,查看对象头。
1 0:000> dp 0x000001ace3409628-8 l1
2 000001ac`e3409620 08000001`00000000
08000001 说明现在已经在使用同步块保存数据了,索引值是 1。
我们使用【!syncblk】命令来验证一下。
1 0:000> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 1 000001ED75B257D8 1 1 000001ACDEFF2770 34b8 0 000001ace3409628 ExampleCore_6_8.Program
4 -----------------------------
5 Total 1
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
当然,我们可以使用【!DumpHeap -thinlock】命令找出托管堆上所有带有瘦锁的对象。
1 0:000> !DumpHeap -thinlock
2 Object Thread OSId Recursion
3 01ace3412ec0 01acdeff2770 0x34b8 0
很简单,就不多说了。
4.4、同步任务
4.4.1、死锁
A、基础知识
死锁:当两个或者多个线程分别持有一些被保护的资源,并且都拒绝释放各自的资源而等待另一方释放资源时,死锁就产生了。
这里会用到一些【k】命令,我就稍作介绍,【k】命令显示给定线程的堆栈帧以及相关信息,【kp】显示堆栈跟踪中调用的每个函数的所有参数。【kb】显示传递给堆栈跟踪中每个函数的前三个参数。
如果想学更多的命令,可以去微软官网:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/k--kb--kc--kd--kp--kp--kv--display-stack-backtrace-
B、眼见为实
调试源码:ExampleCore_6_9
调试任务:手动调试线程死锁的问题。
1)、NTSD 调试
编译项目,然后直接运行我们的 EXE 可执行程序,直到我们的程序输出如图:
此时,打开【Visual Studio 2022 Developer Command Prompt v17.9.6】命令行工具,输入命令【NTSD -pn ExampleCore_6_9.exe】通过进程名称附加我们的程序,当然,也可以通过进程 id 来附加我们的程序。
回车,直接进入调试器,调试器会有一个 int 3 的中断,就可以开始我们的调试了。
已经成功附加进程,截图效果,不是全部:
此时,调试已经处于中断模式了,效果如图:
我们可以使用【~*e!clrstack】命令,将托管线程和非托管线程的栈回溯都转储出来。
1 0:007> ~*e!clrstack
2 OS Thread Id: 0x2860 (0)
3 Child SP IP Call Site
4 000000FBA677E2B0 00007ff8a9c8d0a4 [InlinedCallFrame: 000000fba677e2b0]
5 000000FBA677E2B0 00007ff8961676eb [InlinedCallFrame: 000000fba677e2b0]
6 000000FBA677E280 00007FF8961676EB Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr)
7 000000FBA677E370 00007FF89616C9C0 System.ConsolePal+WindowsConsoleStream.ReadFileNative(IntPtr, System.Span`1<Byte>, Boolean, Int32 ByRef, Boolean)
8 000000FBA677E3D0 00007FF89616C8BB System.ConsolePal+WindowsConsoleStream.Read(System.Span`1<Byte>)
9 000000FBA677E410 00007FF89616FB84 System.IO.ConsoleStream.Read(Byte[], Int32, Int32)
10 000000FBA677E480 00007FFFE0CE89F1 System.IO.StreamReader.ReadBuffer()
11 000000FBA677E4D0 00007FFFE0CE90D4 System.IO.StreamReader.ReadLine()
12 000000FBA677E580 00007FF89617005D System.IO.SyncTextReader.ReadLine()
13 000000FBA677E5D0 00007FF896169319 System.Console.ReadLine()
14 000000FBA677E600 00007FFF81B71B08 ExampleCore_6_9.Program.Main(System.String[])
15 OS Thread Id: 0x2e20 (1)
16 Unable to walk the managed stack. The current thread is likely not a
17 managed thread. You can run !threads to get a list of managed threads in
18 the process
19 Failed to start stack walk: 80070057
20 OS Thread Id: 0x2a8c (2)
21 Unable to walk the managed stack. The current thread is likely not a
22 managed thread. You can run !threads to get a list of managed threads in
23 the process
24 Failed to start stack walk: 80070057
25 OS Thread Id: 0x3260 (3)
26 Child SP IP Call Site
27 000000FBA707F9F0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000fba707f9f0]
28 OS Thread Id: 0x1d7c (4)4号托管线程的调用栈---》执行---》**System.Threading.Monitor.ReliableEnter**(说明在这里等待了)29 Child SP IP Call Site
30 000000FBA737F098 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba737f098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
31 000000FBA737F1F0 00007FFF81B724DE ExampleCore_6_9.Program+\<\>c.\<Main\>b__2_0()(NTSD 没有显示源码行号,Windbg Preview是有的,更容易调试)32 000000FBA737F340 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
33 000000FBA737F390 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
34 000000FBA737F430 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
35 000000FBA737F4C0 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
36 000000FBA737F810 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba737f810]
37 OS Thread Id: 0x2444 (5)
38 Child SP IP Call Site
39 000000FBA638F418 00007ff8a9c8db34 [HelperMethodFrame: 000000fba638f418] System.Threading.WaitHandle.WaitOneCore(IntPtr, Int32)
40 000000FBA638F520 00007FFFE0C00C04 System.Threading.WaitHandle.WaitOneNoCheck(Int32)
41 000000FBA638F580 00007FFFE0C18F66 System.Threading.PortableThreadPool+GateThread.GateThreadStart()
42 000000FBA638F910 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba638f910]
43 OS Thread Id: 0x1130 (6)(6号托管线程的调用栈)---》执行--》**System.Threading.Monitor.ReliableEnter(说明在这里等待了,没有进入)**44 Child SP IP Call Site
45 000000FBA74FF258 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba74ff258] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
46 000000FBA74FF3B0 00007FFF81B7215E ExampleCore_6_9.Program+\<\>c.\<Main\>b__2_1()(源码的调用位置,NTSD 没显示行号,Windbg Preview 是有行号的,更易调试)47 000000FBA74FF500 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
48 000000FBA74FF550 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
49 000000FBA74FF5F0 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
50 000000FBA74FF680 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
51 000000FBA74FF9D0 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba74ff9d0]
52 OS Thread Id: 0x12d0 (7)
53 Unable to walk the managed stack. The current thread is likely not a
54 managed thread. You can run !threads to get a list of managed threads in
55 the process
56 Failed to start stack walk: 80070057
其实,我们从红色标注的可以看出一些端倪,OS Thread Id: 0x1d7c (4) 4号托管线程执行源码 ExampleCore_6_9.Program+<>c.<Main>b__2_0() 这个代码时,调用同步原语 Monitor 的 System.Threading.Monitor.ReliableEnter 方法想进入,却没进入,处于等待,因为后面没有调用栈了。说明一下,Windbg Preview 是可以显示源码行号的,可以直到在哪里处于等待,但是在 NTSD 是没有的。
OS Thread Id: 0x1130 (6) 的 6 号托管线程执行源码 ExampleCore_6_9.Program+<>c.<Main>b__2_1() 时调用了 System.Threading.Monitor.ReliableEnter 方法,想获取锁,由于后面没有执行,所以也是出于等待状态。
此时,我们知道他们都是处于等待状态,虽然输出的信息很简单,但是它却展示了一种常见的死锁识别技术。
这个输出的信息有点多,其实我们还可以使用另外一个命令,【!syncblk】查看同步快表的数据,也能看出一些信息。
1 0:007> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 4 000002AAA66FC1D0 3 1 0000026A0FB1CF60 1130 6 0000026a14010a40 ExampleCore_6_9.Student 4 5 000002AAA66FC228 3 1 0000026A114DE970 1d7c 4 0000026a14010a28 ExampleCore_6_9.Person 5 -----------------------------
6 Total 6
7 CCW 0
8 RCW 0
9 ComClassFactory 0
10 Free 0
4 号托管线程持有 0000026a14010a28 ExampleCore_6_9.Person 对象,也就是锁定了该对象,我们的控制台程序输出也能说明这一点,输出是"tid=4,已经进入 Person(1111) 锁 ",结合【~*e!clrstack】命令的输出,我们知道,4 号线程在执行 Monitor 的 Enter 方法的时候处于等待状态,我们就可以退出等待的位置在源码的 17 行,如图:
再用同样的道理分析,6 号托管线程已经持有 0000026a14010a40 ExampleCore_6_9.Student 对象,说明该对象已经被锁定了,在结合【~*e!clrstack】命令的输出,我们知道 6 号线程在执行 Monitor 的 Enter 方法时是处于等待的状态,我们在结合我们控制台程序的输出"tid=6,已经进入 Student(22222) 锁 ",我们可以知道源码在 32 行处于等待的。如图:
代码很简单,所以我们分析也不难。我们可以根据【~*e!clrstack】命令的输出,分别切换到 4 和 6 号线程上查看一下具体调用栈,也能找出问题。
我们先切换到 4 号线程,执行命令【~4s】。
1 0:007> ~4s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3 ret
我们继续执行【!clrstack -a】命令,查看一下调用栈的局部变量,主要观察 Person 和 Student 。
1 0:004> !clrstack -a
2 OS Thread Id: 0x1d7c (4)
3 Child SP IP Call Site
4 000000FBA737F098 00007ff8a9c8db34 [HelperMethodFrame_1OBJ: 000000fba737f098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
5 000000FBA737F1F0 00007FFF81B724DE ExampleCore_6_9.Program+<>c.<Main>b__2_0()
6 PARAMETERS:
7 this (0x000000FBA737F340) = 0x0000026a14009628
8 LOCALS:
9 0x000000FBA737F328 = 0x0000026a14010a28(这个就是我们的 ExampleCore_6_9.Person 对象)
10 0x000000FBA737F320 = 0x0000000000000001
11 0x000000FBA737F2F8 = 0x0000000000000000
12 0x000000FBA737F2F0 = 0x0000026a14010a40(这个就是我们的 ExampleCore_6_9.Student 对象)
13 0x000000FBA737F2E8 = 0x0000000000000000
14
15 000000FBA737F340 00007FFFE0C06532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
16 PARAMETERS:
17 threadPoolThread (0x000000FBA737F390) = 0x0000026a1400aaa0
18 executionContext = <no data>
19 callback = <no data>
20 state = <no data>
21 LOCALS:
22 0x000000FBA737F368 = 0x0000000000000000
23 <no data>
24 <no data>
25 <no data>
26
27 000000FBA737F390 00007FFFE0C20698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
28 PARAMETERS:
29 this (0x000000FBA737F430) = 0x0000026a14009698
30 currentTaskSlot (0x000000FBA737F438) = 0x0000026a1400c6c0
31 threadPoolThread = <no data>
32 LOCALS:
33 0x000000FBA737F3C8 = 0x0000000000000000
34 0x000000FBA737F3C0 = 0x0000026a140098d8
35 <no data>
36 0x000000FBA737F3F4 = 0x0000000000000000
37 <no data>
38 <no data>
39
40 000000FBA737F430 00007FFFE0C0F430 System.Threading.ThreadPoolWorkQueue.Dispatch()
41 LOCALS:
42 <CLR reg> = 0x0000026a14009bb0
43 <CLR reg> = 0x0000026a1400c6f8
44 <no data>
45 <CLR reg> = 0x0000026a1400c8e8
46 <CLR reg> = 0x0000026a1400aaa0
47 <CLR reg> = 0x00000000001b0116
48 <no data>
49 <no data>
50 <no data>
51 <no data>
52 <no data>
53
54 000000FBA737F4C0 00007FFFE0C1C203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
55 LOCALS:
56 <CLR reg> = 0x0000026a1400a688
57 <CLR reg> = 0x0000026a1400a908
58 <CLR reg> = 0x0000026a1400a9b0
59 <CLR reg> = 0x0000000000004e20
60 <no data>
61 <no data>
62 <no data>
63 <no data>
64
65 000000FBA737F810 00007fffe16cb8d3 [DebuggerU2MCatchHandlerFrame: 000000fba737f810]
0x0000026a14010a28 和 0x0000026a14010a40 就是我们的 ExampleCore_6_9.Person 对象和 ExampleCore_6_9.Student 对象,我们可以执行【!do 0x0000026a14010a28】和【!do 0x0000026a14010a40】命令来确认它们。
1 0:004> !do 0x0000026a14010a28
2 Name: ExampleCore_6_9.Person 3 MethodTable: 00007fff81c73300
4 EEClass: 00007fff81c3c3e0
5 Tracked Type: false
6 Size: 24(0x18) bytes
7 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
8 Fields:
9 None
10
11 0:004> !do 0x0000026a14010a40
12 Name: ExampleCore_6_9.Student13 MethodTable: 00007fff81c73930
14 EEClass: 00007fff81c3c5f8
15 Tracked Type: false
16 Size: 24(0x18) bytes
17 File: E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\bin\Debug\net8.0\ExampleCore_6_9.dll
18 Fields:
19 None
我们在分别查看一下这两个对象的对象头包含了什么数据,执行命令【dp 0x0000026a14010a28-8 l1】和【dp 0x0000026a14010a40-8 l1】。
1 0:004> dp 0x0000026a14010a28-8 l1
2 0000026a`14010a20 08000005`00000000
3
4 0:004> dp 0x0000026a14010a40-8 l1
5 0000026a`14010a38 08000004`00000000
说明它们都使用了同步块保存数据和锁信息了。此时,可以再使用【!syncblk】命令查看同步块表的数据,上面已经执行,此处省略。
以下就简单了,根据我们的代码查找问题吧。
2)、Windbg Preview 调试
编译项目,然后直接运行我们的 EXE 可执行程序,我们的程序输出如图:
然后,打开【Windbg Preview】,依次点击【文件】----【Attach to Process】,附加我们的进程,进入调试器,我们先把进程中所有线程转储出来看看,执行【~*e!clrstack】命令。
1 0:007> ~*e!clrstack
2 OS Thread Id: 0x35e8 (0)
3 Child SP IP Call Site
4 00000035F5D7E7E0 00007ffeddc8d0a4 [InlinedCallFrame: 00000035f5d7e7e0]
5 00000035F5D7E7E0 00007ffe22d376eb [InlinedCallFrame: 00000035f5d7e7e0]
6 00000035F5D7E7B0 00007ffe22d376eb Interop+Kernel32.ReadFile(IntPtr, Byte*, Int32, Int32 ByRef, IntPtr) [/_/src/libraries/System.Console/src/Microsoft.Interop.LibraryImportGenerator/Microsoft.Interop.LibraryImportGenerator/LibraryImports.g.cs @ 412]
7 00000035F5D7E8A0 00007ffe22d3c9c0 System.ConsolePal+WindowsConsoleStream.ReadFileNative(IntPtr, System.Span`1, Boolean, Int32 ByRef, Boolean) [/_/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @ 1150]
8 00000035F5D7E900 00007ffe22d3c8bb System.ConsolePal+WindowsConsoleStream.Read(System.Span`1) [/_/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @ 1108]
9 00000035F5D7E940 00007ffe22d3fb84 System.IO.ConsoleStream.Read(Byte[], Int32, Int32) [/_/src/libraries/System.Console/src/System/IO/ConsoleStream.cs @ 34]
10 00000035F5D7E9B0 00007ffdff8c89f1 System.IO.StreamReader.ReadBuffer() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 613]
11 00000035F5D7EA00 00007ffdff8c90d4 System.IO.StreamReader.ReadLine() [/_/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs @ 802]
12 00000035F5D7EAB0 00007ffe22d4005d System.IO.SyncTextReader.ReadLine() [/_/src/libraries/System.Console/src/System/IO/SyncTextReader.cs @ 77]
13 00000035F5D7EB00 00007ffe22d39319 System.Console.ReadLine() [/_/src/libraries/System.Console/src/System/Console.cs @ 752]
14 00000035F5D7EB30 00007ffda0751b08 ExampleCore_6_9.Program.Main(System.String[]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 41]
15 OS Thread Id: 0x3188 (1)
16 Unable to walk the managed stack. The current thread is likely not a
17 managed thread. You can run !clrthreads to get a list of managed threads in
18 the process
19 Failed to start stack walk: 80070057
20 OS Thread Id: 0x40d4 (2)
21 Unable to walk the managed stack. The current thread is likely not a
22 managed thread. You can run !clrthreads to get a list of managed threads in
23 the process
24 Failed to start stack walk: 80070057
25 OS Thread Id: 0x3bc4 (3)
26 Child SP IP Call Site
27 00000035F64FFC50 00007ffeddc8db34 [DebuggerU2MCatchHandlerFrame: 00000035f64ffc50]
28 OS Thread Id: 0x6c (4)29 Child SP IP Call Site
30 00000035F67FF098 00007ffeddc8db34 [HelperMethodFrame_1OBJ: 00000035f67ff098] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
31 00000035F67FF1F0 00007ffda07528ee ExampleCore_6_9.Program+c.b__2_0() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 17]
32 00000035F67FF340 00007ffdff7e6532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 264]
33 00000035F67FF390 00007ffdff800698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2349]
34 00000035F67FF430 00007ffdff7ef430 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @ 913]
35 00000035F67FF4C0 00007ffdff7fc203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @ 102]
36 00000035F67FF810 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f67ff810]
37 OS Thread Id: 0x2a40 (5)
38 Child SP IP Call Site
39 00000035F598F1B8 00007ffeddc8db34 [HelperMethodFrame: 00000035f598f1b8] System.Threading.WaitHandle.WaitOneCore(IntPtr, Int32)
40 00000035F598F2C0 00007ffdff7e0c04 System.Threading.WaitHandle.WaitOneNoCheck(Int32) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs @ 128]
41 00000035F598F320 00007ffdff7f8f66 System.Threading.PortableThreadPool+GateThread.GateThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.GateThread.cs @ 48]
42 00000035F598F6B0 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f598f6b0]
43 OS Thread Id: 0x3dd8 (6)44 Child SP IP Call Site
45 00000035F697EF68 00007ffeddc8db34 [HelperMethodFrame_1OBJ: 00000035f697ef68] System.Threading.Monitor.ReliableEnter(System.Object, Boolean ByRef)
46 00000035F697F0C0 00007ffda075256e ExampleCore_6_9.Program+c.b__2_1() [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_9\Program.cs @ 32]
47 00000035F697F210 00007ffdff7e6532 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ExecutionContext.cs @ 264]
48 00000035F697F260 00007ffdff800698 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 2349]
49 00000035F697F300 00007ffdff7ef430 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @ 913]
50 00000035F697F390 00007ffdff7fc203 System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart() [/_/src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.NonBrowser.cs @ 102]
51 00000035F697F6E0 00007ffe002ab8d3 [DebuggerU2MCatchHandlerFrame: 00000035f697f6e0]
52 OS Thread Id: 0x4344 (7)
53 Unable to walk the managed stack. The current thread is likely not a
54 managed thread. You can run !clrthreads to get a list of managed threads in
55 the process
56 Failed to start stack walk: 80070057
【~*e!clrstack】命令将托管线程和非托管线程所有的栈回溯都输出出来了。OS Thread Id: 0x3dd8 (6) 号的线程执行 System.Threading.Monitor.ReliableEnter 方法就不执行了,说明卡住了,卡在什么地方呢,就是 ExampleCore_6_9.Program+c.b__2_1() 这样代码最后的行号,32,也就是源码的第32行,换句话说,就是 6 号线程持有 Student 锁,等待 Person 释放锁。效果如图:
OS Thread Id: 0x6c (4) 号线程执行了 System.Threading.Monitor.ReliableEnter 方法也没有后续了,说明卡住了,同样,卡住的位置在哪里,就是 ExampleCore_6_9.Program+c.b__2_0() 这行表示的意思,最后有一个数字,就是源码的行号,它是17,换句话说,就是 4 号线程持有 Person 锁,在登台 student 上的锁释放。效果如图:
其实,我们从以上也能看出一些端倪来。输出信息虽然简单,但是却展示一种常见死锁的识别技术。
我们也可以使用【!syncblk】命令查看一下同步块数据,这个也能说明一些问题。
1 0:007> !syncblk
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 5 0000019606A7CF78 3 1 0000015570129FB0 6c 4 0000015574410a28 ExampleCore_6_9.Person 4 6 0000019606A7CFD0 3 1 0000019606A77790 3dd8 6 0000015574410a40 ExampleCore_6_9.Student 5 -----------------------------
6 Total 6
7 CCW 0
8 RCW 0
9 ComClassFactory 0
10 Free 0
我们看到了 ID 是 4 的线程持有 ExampleCore_6_9.Person 对象,ID 是 6 的线程持有 ExampleCore_6_9.Student 对象,我们可以切换到 4 和 6 号线程上查看一下。
通过以上的分析,剩下就去代码里找问题吧。
4.4.2、孤立锁:异常
A、基础知识
孤儿锁是因为开发者使用 Monitor.Enter 获取一个对象后,因为某种原因没有正确调用 Monitor.Exit,导致这个对象一直处于占用状态,其他线程也就无法进入了,强烈建议使用 lock 语法。
B、眼见为实
调试源码:ExampleCore_6_10
调试任务:重现孤立锁。
1)、NTSD 调试
编译项目,直接双击我们项目的 EXE 可执行程序,直到我们的控制台程序有如图输出:
我们打开【Visual Studio 2022 Developer Command Prompt v17.9.6】,输出命令【NTSD -pn ExampleCore_6_10.exe】,进入调试器,开始我们的调试了。
我们先执行【~*e!clrstack】命令,查看一下所有线程的调用栈是什么情况。
1 0:004> ~*e!clrstack
2 OS Thread Id: 0x29c (0)
3 Child SP IP Call Site
4 000000F24A77E548 00007ff8a9c8db34 \[HelperMethodFrame_1OBJ: 000000f24a77e548\] System.Threading.Monitor.Enter(System.Object) 5 000000F24A77E6A0 00007FFF844A1A6D ExampleCore_6_10.Program.Main(System.String\[\]) 6 OS Thread Id: 0x2128 (1)
7 Unable to walk the managed stack. The current thread is likely not a
8 managed thread. You can run !threads to get a list of managed threads in
9 the process
10 Failed to start stack walk: 80070057
11 OS Thread Id: 0x3378 (2)
12 Unable to walk the managed stack. The current thread is likely not a
13 managed thread. You can run !threads to get a list of managed threads in
14 the process
15 Failed to start stack walk: 80070057
16 OS Thread Id: 0x16d0 (3)
17 Child SP IP Call Site
18 000000F24AEFFBC0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000f24aeffbc0]
19 OS Thread Id: 0x3eb4 (4)
20 Unable to walk the managed stack. The current thread is likely not a
21 managed thread. You can run !threads to get a list of managed threads in
22 the process
23 Failed to start stack walk: 80070057
24 0:004>
OS Thread Id: 0x29c (0) 这个就是 0 号主线程,它执行了 Main 方法,又执行 System.Threading.Monitor.Enter 方法,处于挂起的状态,其他线程没有任何有用信息。
我们的被锁的对象是 ExampleCore_6_10.DBWrapper,又是在主线程出的问题,我们就去主线程上找一下 DBWrapper 对象。
执行命令【~0s】切换到主线程。
1 0:004> ~0s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3 ret
继续执行【!dumpstackobjects】命令。
1 0:000> !dumpstackobjects
2 OS Thread Id: 0x29c (0)
3 RSP/REG Object Name
4 000000F24A77E068 000002cf3c00e050 System.IO.StreamWriter
5 000000F24A77E080 000002cf3c00e050 System.IO.StreamWriter
6 000000F24A77E0C0 000002cf3c00e050 System.IO.StreamWriter
7 000000F24A77E3B0 000002cf3c009630 ExampleCore_6_10.DBWrapper 8 000000F24A77E450 000002cf3c009600 System.WeakReference`1[[System.Diagnostics.Tracing.EventSource, System.Private.CoreLib]]
9 000000F24A77E4F8 000002cf3c009630 ExampleCore_6_10.DBWrapper10 000000F24A77E558 000002cf3c00e050 System.IO.StreamWriter
11 000000F24A77E5D8 0000030fce2b04c0 System.String Acquiring Lock!
12 000000F24A77E650 000002cf3c009630 ExampleCore_6_10.DBWrapper13 000000F24A77E660 000002cf3c009630 ExampleCore_6_10.DBWrapper14 000000F24A77E6B0 000002cf3c009688 System.Threading.Thread
15 000000F24A77E6C0 000002cf3c009648 System.Threading.ThreadStart
16 000000F24A77E6C8 000002cf3c009688 System.Threading.Thread
17 000000F24A77E6D0 000002cf3c009648 System.Threading.ThreadStart
18 000000F24A77E6E0 000002cf3c009630 ExampleCore_6_10.DBWrapper19 000000F24A77E6E8 000002cf3c009688 System.Threading.Thread
20 000000F24A77E700 000002cf3c008e98 System.String[]
21 000000F24A77E7A8 000002cf3c008e98 System.String[]
22 000000F24A77E9A0 000002cf3c008e98 System.String[]
23 000000F24A77E9A8 000002cf3c008e98 System.String[]
24 000000F24A77EAC0 000002cf3c008e98 System.String[]
25 000000F24A77EB40 000002cf3c008eb0 System.String E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\bin\Debug\net8.0\ExampleCore_6_10.dll
26 000000F24A77EB50 000002cf3c008e98 System.String[]
27 000000F24A77EB60 000002cf3c008e78 System.String[]
28 000000F24A77EB98 000002cf3c008eb0 System.String E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\bin\Debug\net8.0\ExampleCore_6_10.dll
29 000000F24A77ED48 000002cf3c008e98 System.String[]
30 0:000>
ExampleCore_6_10.DBWrapper 类型的地址是 000002cf3c009630,执行【dp 000002cf3c009630-8 l1】命令查看一下它的对象头。
1 0:000> dp 000002cf3c009630-8 l1
2 000002cf`3c009628 08000002`00000000
说明对象头已经创建同步块了,索引值是 2,所以我们执行【!syncblk 2】命令查看一下同步块的数据。
1 0:000> !syncblk 2
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 2 0000030FCE6B3F80 3 1 000002CF39578280 0 XXX 000002cf3c009630 ExampleCore_6_10.DBWrapper 4 -----------------------------
5 Total 2
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
10 0:000>
说明 XXX 号线程持有 ExampleCore_6_10.DBWrapper 类型,也可以说 XXX 线程拥有 ExampleCore_6_10.DBWrapper 的锁。XXX 表示的是调试器线程的ID,0 表示操作系统线程的 ID。
XXX 的含义就是,CLR 无法将操作系统线程的 ID 映射到调试器线程,出现这样情况的一个原因是,某个线程在某个时刻获取一个对象的锁,然后,这个线程消失了,却没有释放锁。
我们可以执行【!t】或者【!threads】命令验证 XXX 的说法。
1 0:000> !t
2 ThreadCount: 3
3 UnstartedThread: 0
4 BackgroundThread: 1
5 PendingThread: 0
6 DeadThread: 1(有一个死亡的线程)
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 29c 000002CF37BB7940 202a020 Preemptive 000002CF3C009830:000002CF3C00A618 000002CF37BC5B20 -00001 MTA
11 3 2 16d0 000002CF37C82510 2b220 Preemptive 0000000000000000:0000000000000000 000002CF37BC5B20 -00001 MTA (Finalizer)
12 XXXX 4 0 000002CF39578280 39820 Preemptive 0000000000000000:0000000000000000 000002CF37BC5B20 -00001 Ukn(这个就是死亡的线程)
只要没有执行终结操作,即使处于死亡状态的线程也会被输出。
到这里就差不多了,我们还需要结合代码和调试器一起来找问题,很简单,我直接贴图了。
图上说的很情况,就不多解释了。
2)、Windbg Preview 调试
编译项目,直接双击我们项目的 EXE 可执行程序,直到我们的控制台程序有如图输出:
我们打开【Windbg Preview】,依次点击【文件】---【Attach to process】,在右侧选择我们运行的程序,点击【附加】,附加我们的进程,进入调试器,开始我们的调试了。
我们先执行【~*e!clrstack】命令,查看一下所有线程的调用栈是什么情况。
1 0:004> ~*e!clrstack
2 OS Thread Id: 0x29c (0) 3 Child SP IP Call Site
4 000000F24A77E548 00007ff8a9c8db34 \[HelperMethodFrame_1OBJ: 000000f24a77e548\] System.Threading.Monitor.Enter(System.Object) 5 000000F24A77E6A0 00007fff844a1a6d ExampleCore_6_10.Program.Main(System.String\[\]) [E:\Visual Studio 2022\Source\Projects\AdvancedDebug.NetFramework.Test\ExampleCore_6_10\Program.cs @ 27]
6 OS Thread Id: 0x2128 (1)
7 Unable to walk the managed stack. The current thread is likely not a
8 managed thread. You can run !clrthreads to get a list of managed threads in
9 the process
10 Failed to start stack walk: 80070057
11 OS Thread Id: 0x3378 (2)
12 Unable to walk the managed stack. The current thread is likely not a
13 managed thread. You can run !clrthreads to get a list of managed threads in
14 the process
15 Failed to start stack walk: 80070057
16 OS Thread Id: 0x16d0 (3)
17 Child SP IP Call Site
18 000000F24AEFFBC0 00007ff8a9c8db34 [DebuggerU2MCatchHandlerFrame: 000000f24aeffbc0]
19 OS Thread Id: 0x12cc (4)
20 Unable to walk the managed stack. The current thread is likely not a
21 managed thread. You can run !clrthreads to get a list of managed threads in
22 the process
23 Failed to start stack walk: 80070057
我们从命令的输出中可以看到,有用的信息不多,红色标注的就是主线程的运行情况。我们发现 0 号线程,也就是主线程在执行 System.Threading.Monitor.Enter 方法时挂起了,不执行了,问题大概也就是在这里。
既然主线程有了问题,我们就切换到主线程看看情况,执行命令【~0s】。
1 0:004> ~0s
2 ntdll!NtWaitForMultipleObjects+0x14:
3 00007ff8`a9c8db34 c3 ret
我们执行【!dumpstackobjects】命令,找到我们要分析的对象 DBWrapper。
1 0:000> !dumpstackobjects
2 OS Thread Id: 0x29c (0)
3 SP/REG Object Name
4 00f24a77e068 02cf3c00e050 System.IO.StreamWriter
5 00f24a77e080 02cf3c00e050 System.IO.StreamWriter
6 00f24a77e0c0 02cf3c00e050 System.IO.StreamWriter
7 00f24a77e3b0 02cf3c009630 ExampleCore_6_10.DBWrapper 8 00f24a77e450 02cf3c009600 System.WeakReference<System.Diagnostics.Tracing.EventSource>
9 00f24a77e4f8 02cf3c009630 ExampleCore_6_10.DBWrapper10 00f24a77e558 02cf3c00e050 System.IO.StreamWriter
11 00f24a77e5d8 030fce2b04c0 System.String
12 00f24a77e650 02cf3c009630 ExampleCore_6_10.DBWrapper13 00f24a77e660 02cf3c009630 ExampleCore_6_10.DBWrapper14 00f24a77e6b0 02cf3c009688 System.Threading.Thread
15 00f24a77e6c0 02cf3c009648 System.Threading.ThreadStart
16 00f24a77e6c8 02cf3c009688 System.Threading.Thread
17 00f24a77e6d0 02cf3c009648 System.Threading.ThreadStart
18 00f24a77e6e0 02cf3c009630 ExampleCore_6_10.DBWrapper19 00f24a77e6e8 02cf3c009688 System.Threading.Thread
20 00f24a77e700 02cf3c008e98 System.String[]
21 00f24a77e7a8 02cf3c008e98 System.String[]
22 00f24a77e9a0 02cf3c008e98 System.String[]
23 00f24a77e9a8 02cf3c008e98 System.String[]
24 00f24a77eac0 02cf3c008e98 System.String[]
25 00f24a77eb40 02cf3c008eb0 System.String
26 00f24a77eb50 02cf3c008e98 System.String[]
27 00f24a77eb60 02cf3c008e78 System.String[]
28 00f24a77eb98 02cf3c008eb0 System.String
29 00f24a77ed48 02cf3c008e98 System.String[]
ExampleCore_6_10.DBWrapper 就是我们要找的对象,它的地址是 02cf3c009630,我们执行【dp 02cf3c009630-8 l1】命令查看该对象的对象头包含的是什么东西。
1 0:000> dp 02cf3c009630-8 l1
2 000002cf`3c009628 08000002`00000000
08000002 说明对象头已经创建了一个同步块了,索引值是 2,我们查看同步块,执行命令【!syncblk 2】。
1 0:000> !syncblk 2
2 Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
3 2 0000030FCE6B3F80 3 1 000002CF39578280 0 XXX 000002cf3c009630 ExampleCore_6_10.DBWrapper4 -----------------------------
5 Total 2
6 CCW 0
7 RCW 0
8 ComClassFactory 0
9 Free 0
输出信息告诉我们 ExampleCore_6_10.DBWrapper 对象已经被锁定了,被 XXX 线程锁定的。XXX 表示的是调试器的线程 ID,0 表示的是操作系统线程的 ID。
XXX 表示 CLR 无法将操作系统线程的 ID 无法映射到调试器线程。出现这种情况的原因是,这个线程在某个时刻获取了该对象上的锁,然后这个线程消失了但是却没有释放锁。
我们执行【!t】或者【!threads】命令验证这一点。
1 0:000> !threads
2 ThreadCount: 3
3 UnstartedThread: 0
4 BackgroundThread: 1
5 PendingThread: 0
6 DeadThread: 1(有一个死亡的线程)
7 Hosted Runtime: no
8 Lock
9 DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
10 0 1 29c 000002CF37BB7940 202a020 Preemptive 000002CF3C009830:000002CF3C00A618 000002cf37bc5b20 -00001 MTA
11 3 2 16d0 000002CF37C82510 2b220 Preemptive 0000000000000000:0000000000000000 000002cf37bc5b20 -00001 MTA (Finalizer)
12 XXXX 4 0 000002CF39578280 39820 Preemptive 0000000000000000:0000000000000000 000002cf37bc5b20 -00001 Ukn (死亡的线程)
只要没有执行终结操作,即使处于死亡状态的线程也会被出输出。
要分析具体是哪里的错误,肯定要结合代码来分析。我们的代码是这里出问题了,如图:
代码很简单,就不多说了。
4.4.3、线程中止
这节的内容就略过了,探索的意义不是很大,首先,我使用的平台是 8.0 跨平台版本,不是 .NET Framework 版本了,如果在 .NET 8.0 版本里调用 Thread.Abort() 方法是不支持的。会有绿色波浪线提示,如图:
如果大家使用的 .NET Framework 平台,可以自己试试。
4.4.4、终结器挂起
系统内存暴涨有很多原因,不良线程可以是原因之一,访问非托管资源也可以是原因之一。如果查看内容暴涨,其实还是有很多方法的,比如:我们可以使用【任务管理器】,也可以使用【ProcessExplorer】工具。具体的使用方法就不介绍了,大家可以网上自行恶补。
原书上的内容我省略了,由于没有原书的源码,所以我也无法调试了。这里是我用的以前的代码(我之前写过一个系列的代码),和终结器挂起也没关系,但是和内存暴涨有关系,原书的调试方法还是可以使用的,特此说明。
有些查找问题的方法和步骤还是很有用的,如果我们发现系统内存暴涨,可以尝试执行一下步骤排查。
1)、我们可以先执行【!eeheap -loader】命令,查看一下加载器堆是否存在异常。
2)、如果加载器堆没问题,我们可以尝试执行【!eeheap -gc】命令查看托管堆是否有什么情况。
3)、我们也可以执行【!heap -s】命令,查看所有堆的统计情况,来查找问题,如果数据有问题,可以继续使用【!heap -h】命令是否存在句柄数据。
4)、当然,我们也可以使用【!DumpHeap -stat】命令,统计一下托管堆上的对象,看看对象数据是否存在问题。
5)、直到了对象,我们就可以使用【!DumpHeap -type】查找指定对象的地址。
6)、有了对象的地址,我们就可以使用【!gcroot】命令,观察对象的根引用。
7)、我们也可以使用【FinalizeQueue】命令查看一下中介对象的情况来查找问题。
8)、通过【!t】或者【!thread】命令,了解线程的情况,直到了线程标识 ID,我们就可以使用【!clrstack】命令查看 指定线程的调用栈。
五、总结
这篇文章的终于写完了,这篇文章的内容相对来说,不是很多。写完一篇,就说明进步了一点点。Net 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。