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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
接着,我们在过【通过名称过滤(Filter by name)】中输入我们项目的名称:ExampleCore_6_1,来进程查找。效果如图:
接着,我们在进程名上双击,打开进程属性对话框,如图:
我们找到了我们项目进程的主键线程编号,然后就可以使用 Windbg 查看内核态的线程表示了。我们主线程的编号是:15560,这个是十进制的,要注意。
1)、KD 和 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>
当然,我们也可以通过【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 层面的线程。
1 lkd> !process 0 2 ExampleCore_6_1.exe 2 PROCESS ffffa2067324d080 3 SessionId: 1 Cid: 3f2c Peb: 4e16f21000 ParentCid: 0da8 4 DirBase: 6bc43002 ObjectTable: ffff8a8e1a97c180 HandleCount: 171. 5 Image: ExampleCore_6_1.exe 6 7 THREAD ffffa20677bb90c0 Cid 3f2c.3cc8 Teb: 0000004e16f22000 Win32Thread: ffffa2067765a990 WAIT: (Executive) KernelMode Alertable 8 ffffa20678223bb8 NotificationEvent 9 10 THREAD ffffa20677e50240 Cid 3f2c.3960 Teb: 0000004e16f2a000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable 11 ffffa20677fe5660 NotificationEvent 12 13 THREAD ffffa20677995080 Cid 3f2c.1f54 Teb: 0000004e16f2c000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable 14 ffffa20677fe5560 SynchronizationEvent 15 ffffa20677fe56e0 SynchronizationEvent 16 ffffa20677fe5860 SynchronizationEvent 17 18 THREAD ffffa2066e255080 Cid 3f2c.3b98 Teb: 0000004e16f2e000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable 19 ffffa206678be6a0 NotificationEvent 20 ffffa20677cfa260 SynchronizationEvent 21 22 THREAD ffffa206712dd080 Cid 3f2c.3850 Teb: 0000004e16f34000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Alertable 23 ffffa20677964c60 SynchronizationEvent
我们通过【ProcessExploler】看到我们项目的主线程是:1204,这个值是十进制的,我们看看十六进制是多少。
1 lkd> ? 0n15560 2 Evaluate expression: 15560 = 00000000`00003cc8
我们如果使用的调试器是【Windbg Preview】,它有一个特性,选择一个文本,和文本内容相同的也会被凸显出来,我们选择 3cc8,发现我们使用【!process】命令的结果中也有被选择了,如图:
ffffa20677bb90c0 这个值就是线程的内核态的数据结构,我们可以继续使用【dt】命令查看一下详情。
1 lkd> dt nt!_KThread ffffa20677bb90c0 2 +0x000 Header : _DISPATCHER_HEADER 3 +0x018 SListFaultAddress : (null) 4 +0x020 QuantumTarget : 0xd630923 5 +0x028 InitialStack : 0xffffdf00`c2f32c50 Void 6 +0x030 StackLimit : 0xffffdf00`c2f2c000 Void 7 +0x038 StackBase : 0xffffdf00`c2f33000 Void 8 +0x040 ThreadLock : 0 9 +0x048 CycleTime : 0x94e88f3 10 +0x050 CurrentRunTime : 0 11 +0x054 ExpectedRunTime : 0xa80710 12 +0x058 KernelStack : 0xffffdf00`c2f323b0 Void 13 +0x060 StateSaveArea : 0xffffdf00`c2f32c80 _XSAVE_FORMAT 14 +0x068 SchedulingGroup : (null) 15 +0x070 WaitRegister : _KWAIT_STATUS_REGISTER 16 +0x071 Running : 0 '' 17 +0x072 Alerted : [2] "" 18 +0x074 AutoBoostActive : 0y1 19 +0x074 ReadyTransition : 0y0 20 +0x074 WaitNext : 0y0 21 +0x074 SystemAffinityActive : 0y0 22 +0x074 Alertable : 0y1 23 +0x074 UserStackWalkActive : 0y0 24 +0x074 ApcInterruptRequest : 0y0 25 +0x074 QuantumEndMigrate : 0y0 26 +0x074 UmsDirectedSwitchEnable : 0y0 27 +0x074 TimerActive : 0y0 28 +0x074 SystemThread : 0y0 29 +0x074 ProcessDetachActive : 0y0 30 +0x074 CalloutActive : 0y0 31 +0x074 ScbReadyQueue : 0y0 32 +0x074 ApcQueueable : 0y1 33 +0x074 ReservedStackInUse : 0y0 34 +0x074 UmsPerformingSyscall : 0y0 35 +0x074 TimerSuspended : 0y0 36 +0x074 SuspendedWaitMode : 0y0 37 +0x074 SuspendSchedulerApcWait : 0y0 38 +0x074 CetUserShadowStack : 0y0 39 +0x074 BypassProcessFreeze : 0y0 40 +0x074 Reserved : 0y0000000000 (0) 41 +0x074 MiscFlags : 0n16401 42 +0x078 ThreadFlagsSpare : 0y00 43 +0x078 AutoAlignment : 0y0 44 +0x078 DisableBoost : 0y0 45 +0x078 AlertedByThreadId : 0y0 46 +0x078 QuantumDonation : 0y0 47 +0x078 EnableStackSwap : 0y1 48 +0x078 GuiThread : 0y1 49 +0x078 DisableQuantum : 0y0 50 +0x078 ChargeOnlySchedulingGroup : 0y0 51 +0x078 DeferPreemption : 0y0 52 +0x078 QueueDeferPreemption : 0y0 53 +0x078 ForceDeferSchedule : 0y0 54 +0x078 SharedReadyQueueAffinity : 0y1 55 +0x078 FreezeCount : 0y0 56 +0x078 TerminationApcRequest : 0y0 57 +0x078 AutoBoostEntriesExhausted : 0y1 58 +0x078 KernelStackResident : 0y1 59 +0x078 TerminateRequestReason : 0y00 60 +0x078 ProcessStackCountDecremented : 0y0 61 +0x078 RestrictedGuiThread : 0y0 62 +0x078 VpBackingThread : 0y0 63 +0x078 ThreadFlagsSpare2 : 0y0 64 +0x078 EtwStackTraceApcInserted : 0y00000000 (0) 65 +0x078 ThreadFlags : 0n204992 66 +0x07c Tag : 0 '' 67 +0x07d SystemHeteroCpuPolicy : 0 '' 68 +0x07e UserHeteroCpuPolicy : 0y0001000 (0x8) 69 +0x07e ExplicitSystemHeteroCpuPolicy : 0y0 70 +0x07f RunningNonRetpolineCode : 0y0 71 +0x07f SpecCtrlSpare : 0y0000000 (0) 72 +0x07f SpecCtrl : 0 '' 73 +0x080 SystemCallNumber : 6 74 +0x084 ReadyTime : 3 75 +0x088 FirstArgument : 0x00000000`00000050 Void 76 +0x090 TrapFrame : 0xffffdf00`c2f32ac0 _KTRAP_FRAME 77 +0x098 ApcState : _KAPC_STATE 78 +0x098 ApcStateFill : [43] "X???" 79 +0x0c3 Priority : 8 '' 80 +0x0c4 UserIdealProcessor : 2 81 +0x0c8 WaitStatus : 0n256 82 +0x0d0 WaitBlockList : 0xffffa206`77bb9200 _KWAIT_BLOCK 83 +0x0d8 WaitListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0xffffa206`67903158 ] 84 +0x0d8 SwapListEntry : _SINGLE_LIST_ENTRY 85 +0x0e8 Queue : (null) 86 +0x0f0 Teb : 0x0000004e`16f22000 Void 87 +0x0f8 RelativeTimerBias : 0 88 +0x100 Timer : _KTIMER 89 +0x140 WaitBlock : [4] _KWAIT_BLOCK 90 +0x140 WaitBlockFill4 : [20] "???" 91 +0x154 ContextSwitches : 0xde 92 +0x140 WaitBlockFill5 : [68] "???" 93 +0x184 State : 0x5 '' 94 +0x185 Spare13 : 0 '' 95 +0x186 WaitIrql : 0 '' 96 +0x187 WaitMode : 0 '' 97 +0x140 WaitBlockFill6 : [116] "???" 98 +0x1b4 WaitTime : 0x11f7e8 99 +0x140 WaitBlockFill7 : [164] "???" 100 +0x1e4 KernelApcDisable : 0n-1 101 +0x1e6 SpecialApcDisable : 0n0 102 +0x1e4 CombinedApcDisable : 0xffff 103 +0x140 WaitBlockFill8 : [40] "???" 104 +0x168 ThreadCounters : (null) 105 +0x140 WaitBlockFill9 : [88] "???" 106 +0x198 XStateSave : (null) 107 +0x140 WaitBlockFill10 : [136] "???" 108 +0x1c8 Win32Thread : 0xffffa206`7765a990 Void 109 +0x140 WaitBlockFill11 : [176] "???" 110 +0x1f0 Ucb : (null) 111 +0x1f8 Uch : (null) 112 +0x200 ThreadFlags2 : 0n0 113 +0x200 BamQosLevel : 0y00000000 (0) 114 +0x200 ThreadFlags2Reserved : 0y000000000000000000000000 (0) 115 +0x204 Spare21 : 0 116 +0x208 QueueListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ] 117 +0x218 NextProcessor : 2 118 +0x218 NextProcessorNumber : 0y0000000000000000000000000000010 (0x2) 119 +0x218 SharedReadyQueue : 0y0 120 +0x21c QueuePriority : 0n0 121 +0x220 Process : 0xffffa206`7324d080 _KPROCESS 122 +0x228 UserAffinity : _GROUP_AFFINITY 123 +0x228 UserAffinityFill : [10] "???" 124 +0x232 PreviousMode : 1 '' 125 +0x233 BasePriority : 8 '' 126 +0x234 PriorityDecrement : 0 '' 127 +0x234 ForegroundBoost : 0y0000 128 +0x234 UnusualBoost : 0y0000 129 +0x235 Preempted : 0 '' 130 +0x236 AdjustReason : 0 '' 131 +0x237 AdjustIncrement : 0 '' 132 +0x238 AffinityVersion : 0x50 133 +0x240 Affinity : _GROUP_AFFINITY 134 +0x240 AffinityFill : [10] "???" 135 +0x24a ApcStateIndex : 0 '' 136 +0x24b WaitBlockCount : 0x1 '' 137 +0x24c IdealProcessor : 2 138 +0x250 NpxState : 5 139 +0x258 SavedApcState : _KAPC_STATE 140 +0x258 SavedApcStateFill : [43] "???" 141 +0x283 WaitReason : 0 '' 142 +0x284 SuspendCount : 0 '' 143 +0x285 Saturation : 0 '' 144 +0x286 SListFaultCount : 0 145 +0x288 SchedulerApc : _KAPC 146 +0x288 SchedulerApcFill1 : [3] "???" 147 +0x28b QuantumReset : 0x6 '' 148 +0x288 SchedulerApcFill2 : [4] "???" 149 +0x28c KernelTime : 1 150 +0x288 SchedulerApcFill3 : [64] "???" 151 +0x2c8 WaitPrcb : (null) 152 +0x288 SchedulerApcFill4 : [72] "???" 153 +0x2d0 LegoData : (null) 154 +0x288 SchedulerApcFill5 : [83] "???" 155 +0x2db CallbackNestingLevel : 0 '' 156 +0x2dc UserTime : 2 157 +0x2e0 SuspendEvent : _KEVENT 158 +0x2f8 ThreadListEntry : _LIST_ENTRY [ 0xffffa206`77e50538 - 0xffffa206`7324d0b0 ] 159 +0x308 MutantListHead : _LIST_ENTRY [ 0xffffa206`77bb93c8 - 0xffffa206`77bb93c8 ] 160 +0x318 AbEntrySummary : 0x3e '>' 161 +0x319 AbWaitEntryCount : 0 '' 162 +0x31a AbAllocationRegionCount : 0 '' 163 +0x31b SystemPriority : 0 '' 164 +0x31c SecureThreadCookie : 0 165 +0x320 LockEntries : 0xffffa206`77bb9710 _KLOCK_ENTRY 166 +0x328 PropagateBoostsEntry : _SINGLE_LIST_ENTRY 167 +0x330 IoSelfBoostsEntry : _SINGLE_LIST_ENTRY 168 +0x338 PriorityFloorCounts : [16] "" 169 +0x348 PriorityFloorCountsReserved : [16] "" 170 +0x358 PriorityFloorSummary : 0 171 +0x35c AbCompletedIoBoostCount : 0n0 172 +0x360 AbCompletedIoQoSBoostCount : 0n0 173 +0x364 KeReferenceCount : 0n0 174 +0x366 AbOrphanedEntrySummary : 0 '' 175 +0x367 AbOwnedEntryCount : 0x1 '' 176 +0x368 ForegroundLossTime : 0 177 +0x370 GlobalForegroundListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ] 178 +0x370 ForegroundDpcStackListEntry : _SINGLE_LIST_ENTRY 179 +0x378 InGlobalForegroundList : 0 180 +0x380 ReadOperationCount : 0n32 181 +0x388 WriteOperationCount : 0n0 182 +0x390 OtherOperationCount : 0n158 183 +0x398 ReadTransferCount : 0n66740 184 +0x3a0 WriteTransferCount : 0n0 185 +0x3a8 OtherTransferCount : 0n3494 186 +0x3b0 QueuedScb : (null) 187 +0x3b8 ThreadTimerDelay : 0 188 +0x3bc ThreadFlags3 : 0n0 189 +0x3bc ThreadFlags3Reserved : 0y00000000 (0) 190 +0x3bc PpmPolicy : 0y00 191 +0x3bc ThreadFlags3Reserved2 : 0y0000000000000000000000 (0) 192 +0x3c0 TracingPrivate : [1] 0 193 +0x3c8 SchedulerAssist : (null) 194 +0x3d0 AbWaitObject : (null) 195 +0x3d8 ReservedPreviousReadyTimeValue : 0 196 +0x3e0 KernelWaitTime : 0xe 197 +0x3e8 UserWaitTime : 0 198 +0x3f0 GlobalUpdateVpThreadPriorityListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0x00000000`00000000 ] 199 +0x3f0 UpdateVpThreadPriorityDpcStackListEntry : _SINGLE_LIST_ENTRY 200 +0x3f8 InGlobalUpdateVpThreadPriorityList : 0 201 +0x400 SchedulerAssistPriorityFloor : 0n0 202 +0x404 Spare28 : 0 203 +0x408 ResourceIndex : 0x1 '' 204 +0x409 Spare31 : [3] "" 205 +0x410 EndPadding : [4] 0
这个线程的数据结构内容还是不少的。
我们可以使用【!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 层面的线程。
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相互切换的。
1 0:000> !DumpHeap -type ManualResetEvent 2 Address MT Size 3 012b87014180 7ff8da3e2a88 24 4 5 Statistics: 6 MT Count TotalSize Class Name 7 7ff8da3e2a88 1 24 System.Threading.ManualResetEvent 8 Total 1 objects, 24 bytes
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 <<
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
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
我们在【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、互斥体(内核锁)我们现在托管堆上查找一下 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 已经释放了锁。
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 表示释放锁。
我们现在托管堆上查找一下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 异常了,和我们期望的一样。
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) 了。
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】和 命令行工具的区别。
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 实现的。
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)。
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)。
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 高级调试这条路,也刚刚起步,还有很多要学的地方。皇天不负有心人,努力,不辜负自己,我相信付出就有回报,再者说,学习的过程,有时候,虽然很痛苦,但是,学有所成,学有所懂,这个开心的感觉还是不可言喻的。不忘初心,继续努力。做自己喜欢做的,开心就好。