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