第一章:非托管句柄泄露检测

📅 2026/7/22 2:18:15
第一章:非托管句柄泄露检测
非托管互操作的隐藏威胁当 C# 程序通过 P/Invoke 与 C 交互时就进入了非托管泥潭。即使是一个简单的 C 调用也可能引入从托管代码角度几乎不可见的句柄泄露问题。场景C# 应用调用一个创建 Event 句柄但从未关闭它的 C 原生方法extern “C” {_declspec(dllexport) void CSharpCreateEvent();}#include Windows.hvoid CSharpCreateEvent() {HANDLE hEvent CreateEvent(NULL, TRUE, FALSE, NULL);printf(“\nEvent句柄值: %#08x\t”, hEvent);// 缺失: CloseHandle(hEvent);}C# 调用方internal class Program {[DllImport(“Example.dll”, CallingConvention CallingConvention.Cdecl)]extern static void CSharpCreateEvent();static void Main(string[] args) { while (true) { Task.Run(() CSharpCreateEvent()); Thread.Sleep(10); } }}1.2 使用 WinDbg 识别泄露的句柄使用 !handle 命令检查句柄类型和数量0:004 !handleHandle 16fcType Event1411 HandlesType CountNone 6Event 1337 - 异常高File 16Directory 4Mutant 3…1.3 使用 !htrace 追踪句柄分配!htrace 命令启用句柄追踪并比较快照以找到泄露的句柄0:011 !htrace -enableHandle tracing enabled.Handle tracing information snapshot successfully taken.0:011 g(一段时间后中断)0:007 !htrace -diffHandle tracing information snapshot successfully taken.0xad new stack traces since the previous snapshot.Outstanding handles opened since the previous snapshot:Handle 0x0000199c - OPENThread ID 0x000017c8, Process ID 0x00000e14…0x770e2b04: KERNELBASE!CreateEventW0x000000240x6ac91755: Example_20_1_5!CSharpCreateEvent0x000000351.4 结合断点与托管栈检查在原生方法上设置断点并捕获托管调用栈0:007 bp Example_20_1_5!CSharpCreateEvent “k; gc”0:007 gChildEBP RetAddr00 0848f9e4 080674f3 Example_20_1_5!CSharpCreateEvent02 0848f9f0 0806e3dd Example_20_1_4!Program.c.b__1_00x1b03 0848f9fc 0806e38d System_Private_CoreLib!Task.InnerInvoke0x3d…第二章终结器队列瓶颈2.1 理解终结器内存泄露终结器队列是内存泄露的常见来源。当带析构函数的对象创建速度超过单个终结器线程的处理速度时内存就会累积。场景一个具有慢速析构函数的类public class Person {public string Name { get; set; }public int Age { get; set; }~Person() { Thread.Sleep(new Random().Next(0, 3000)); // 慢速终结 Console.WriteLine($name{Name} finalized...); }}// 创建对象的速度超过终结速度for (int i 0; i 1000000; i) {var person new Person { Name $“jack{i}”, Age i };}2.2 诊断终结器队列积压在 WinDbg 中使用 !fq终结器队列命令0:015 !fqSyncBlocks to be cleaned up: 0Free-Threaded Interfaces to be released: 0generation 0 has 28423 finalizable objectsgeneration 1 has 4 finalizable objectsgeneration 2 has 21 finalizable objectsReady for finalization 971560 objects - 严重积压Statistics for all finalizable objects:MT Count TotalSize Class Name00007ffdbaa4fb58 999987 31999584 ConsoleApp2.Person - 罪魁祸首2.3 调查终结器线程活动检查终结器线程正在做什么0:001 !t5 2 3f4c 000000001AA94090 202b220 Preemptive … MTA (Finalizer)0:001 ~~[3f4c]sntdll!NtDelayExecution0x14:00007ffe8908c634 c3 ret0:005 !clrstackOS Thread Id: 0x3f4c (5)000000001ACEF868 00007ffe8908c634 [HelperMethodFrame] System.Threading.Thread.SleepInternal(Int32)000000001ACEF960 00007ffe19f0c46b System.Threading.Thread.Sleep(Int32)000000001ACEF990 00007ffdba986e15 ConsoleApp2.Person.Finalize() - 阻塞在 Sleep2.4 使用 PerfView 测量终结时间使用 ETW 事件测量终结持续时间打开 PerfView 并开始收集在 Events 视图中搜索 Finalize 事件分析 FinalizersStart、FinalizerObject 和 FinalizersStop 事件计算连续 FinalizerObject 事件之间的时间差第三章线程同步内部机制3.1 理解 Monitor.Wait/Pulse 机制Monitor.Wait 和 Monitor.Pulse 机制对于线程协调至关重要但常常被误解。场景Worker1 等待 Worker2 完成static Person lockObject new Person();static void Worker1() {lock (lockObject) {Console.WriteLine(“1. Execute worker1…”);Monitor.Wait(lockObject); // 释放锁并等待Console.WriteLine(“4. Continue worker1…”);}}static void Worker2() {lock (lockObject) {Console.WriteLine(“3. worker2 completed…”);Monitor.Pulse(lockObject); // 唤醒一个等待线程}}3.2 WaitEventLink 数据结构CoreCLR 内部使用 WaitEventLink 来追踪等待线程struct WaitEventLink {SyncBlock* m_WaitSB; // 当前对象的 syncblockCLREvent* m_EventWait; // 当前线程的等待事件PTR_Thread m_Thread; // 所属线程PTR_WaitEventLink m_Next; // 链接到下一个 SyncBlockSLink m_LinkSB; // 链接到下一个等待线程DWORD m_RefCount; // 同一 SyncBlock 上的等待计数};3.3 Monitor.Wait 执行流程Monitor.Wait 方法执行以下步骤创建 WaitEventLink用当前 SyncBlock 和线程信息初始化入队到线程队列添加到 m_LinkSB 队列以便 Pulse 通知释放 Monitor调用 LeaveMonitorCompletely() 释放锁阻塞线程等待 m_EventWait 直到 Pulse 被调用BOOL SyncBlock::Wait(INT32 timeOut) {WaitEventLink* walk pCurThread-WaitEventLinkForSyncBlock(this);CLREvent* hEvent (pCurThread-m_EventWait);waitEventLink.m_WaitSB this; waitEventLink.m_EventWait hEvent; waitEventLink.m_Thread pCurThread; ThreadQueue::EnqueueThread(pWaitEventLink, this); syncState.m_EnterCount LeaveMonitorCompletely(); isTimedOut pCurThread-Block(timeOut, syncState); return !isTimedOut;}3.4 Monitor.Pulse vs PulseAllPulse只唤醒队列中的第一个线程void SyncBlock::Pulse() {WaitEventLink* pWaitEventLink;if ((pWaitEventLink ThreadQueue::DequeueThread(this)) ! NULL)pWaitEventLink-m_EventWait-Set();}PulseAll唤醒所有等待线程void SyncBlock::PulseAll() {WaitEventLink* pWaitEventLink;while ((pWaitEventLink ThreadQueue::DequeueThread(this)) ! NULL)pWaitEventLink-m_EventWait-Set();}第四章使用 Harmony 进行运行时补丁4.1 Harmony 是什么Harmony 是一个强大的库用于运行时修补、替换和装饰 .NET 方法。它跨所有主要平台工作并提供类似 AOP 的功能无需修改源代码。4.2 关键注入点补丁类型 描述 使用场景Prefix 在原始方法之前运行 验证、日志Postfix 在原始方法之后运行 结果转换Transpiler 直接修改 IL 代码 高级代码操作Finalizer 用 try/finally 包装整个方法 异常隔离Reverse Patch 创建指向原始方法的代理 跨模块调用4.3 实际用例追踪线程创建问题线程数突然飙升到 1000但不知道原因。解决方案Hook Thread.Start() 来捕获调用栈var harmony new Harmony(“com.example.threadhook”);harmony.PatchAll();[HarmonyPatch(typeof(Thread), “Start”, new Type[] { })]public class ThreadStartHook {public static void Prefix(Thread __instance) {Console.WriteLine($“Thread {__instance.ManagedThreadId} starting:”);Console.WriteLine(Environment.StackTrace);}}4.4 底层原理JMP 指令 HookHarmony 通过重写 JIT 编译方法的开头使其跳转到动态生成的代理0:013 !U 00007ff85bd0e440preJIT generated codeSystem.Threading.Thread.Start()00007ff85bd0e440 e9cb22fba2 jmp 00007ff7fecc0710 - 跳转到代理0:013 !U 00007ff7fecc0710Normal JIT generated codeDynamicClass.System.Threading.Thread.Start_Patch1(System.Threading.Thread)