发布于2026-07-23 阅读(0)
扫一扫,手机访问
先说一个典型的崩溃场景:收到一个.NET程序崩溃的dump,分析后发现,祸根其实是一个托管线程(DBG=XXXX)的异常退出。参考如下:
0:011> !tThreadCount: 17UnstartedThread: 0BackgroundThread: 16PendingThread: 0DeadThread: 0Hosted Runtime: no Lock DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 84d8 000001C0801EAC20 26020 Preemptive 0000000000000000:0000000000000000 000001c080266300 -00001 STA 3 2 9d78 000001C0801F8210 2b220 Preemptive 0000000000000000:0000000000000000 000001c080266300 -00001 MTA (Finalizer) 4 4 8760 000001C08466C800 102b220 Preemptive 0000000000000000:0000000000000000 000001c080266300 -00001 MTA (Threadpool Worker) ... 44 16 b2fc 000001C08F949450 102b220 Preemptive 0000000000000000:0000000000000000 000001c080266300 -00001 MTA (GC) (Threadpool Worker) 46 15 9904 000001C08F9487B0 102b220 Preemptive 0000000000000000:0000000000000000 000001c080266300 -00001 MTA (Threadpool Worker) XXXX 3 a23c 000001C08F948E00 102b220 Preemptive 0000000000000000:0000000000000000 000001c080266300 -00001 Ukn (Threadpool Worker)
线程异常退出后,CLR完全不知情。当GC触发时,会在这个XXXX线程上寻找引用根——可它已经不存在了,访问它的空间自然就是访问违例。从ScanStackRoots函数调用栈可以看得一清二楚:
0:011> .ecxrrax=00007ffdbefcc8a0 rbx=000000a42007f5f0 rcx=000000a42187f688rdx=0000000000000000 rsi=000000a42007ee60 rdi=000000a42007f100rip=00007ffdbec36cbb rsp=000000a42007f828 rbp=000001c08f948e00 r8=000000a42007f910 r9=000001c08f948e00 r10=00000fffb7da5860r11=0555501544555545 r12=ffffffffffffffff r13=0000000000000000r14=0000000000000000 r15=00007ffdbec14fb0iopl=0 nv up ei pl nz ac pe cycs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010211coreclr!InlinedCallFrame::FrameHasActiveCall+0x13:00007ffd`bec36cbb 483b01 cmp rax,qword ptr [rcx] ds:000000a4`2187f688=????????????????0:011> k *** Stack trace for last set context - .thread/.cxr resets it # Child-SP RetAddr Call Site00 000000a4`2007f828 00007ffd`bec36c2e coreclr!InlinedCallFrame::FrameHasActiveCall+0x13 [D:\a\_work\1\s\src\coreclr\vm\frames.h @ 2927] 01 000000a4`2007f830 00007ffd`bec36aef coreclr!ScanStackRoots+0x3a [D:\a\_work\1\s\src\coreclr\vm\gcenv.ee.cpp @ 121] 02 000000a4`2007f8a0 00007ffd`bec29627 coreclr!GCToEEInterface::GcScanRoots+0x8f [D:\a\_work\1\s\src\coreclr\vm\gcenv.ee.cpp @ 282] 03 (Inline Function) --------`-------- coreclr!GCScan::GcScanRoots+0x73 [D:\a\_work\1\s\src\coreclr\gc\gcscan.cpp @ 152] 04 000000a4`2007f8e0 00007ffd`bec14865 coreclr!WKS::gc_heap::background_mark_phase+0xdf [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 37866] 05 000000a4`2007f990 00007ffd`bed286a0 coreclr!WKS::gc_heap::gc1+0x511 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 22315] 06 000000a4`2007f9f0 00007ffd`bed391c1 coreclr!WKS::gc_heap::bgc_thread_function+0x68 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 39244] 07 000000a4`2007fa20 00007ffe`3533e8d7 coreclr!::operator()+0xa1 [D:\a\_work\1\s\src\coreclr\vm\gcenv.ee.cpp @ 1441] 08 000000a4`2007fa50 00007ffe`363f14fc kernel32!BaseThreadInitThunk+0x1709 000000a4`2007fa80 00000000`00000000 ntdll!RtlUserThreadStart+0x2c
这种崩溃其实并不少见,之前遇到的多半是new Thread创建出来的,用harmony对Thread.StartCore拦截就能轻松定位。但这次的情况比较特殊——出问题的不是手工创建的线程,而是线程池里散养的线程(ThreadPool),排查难度一下子就上去了。既然是复盘,就来好好梳理一下这类问题的解决思路。
为了演示,我们用C#调用C,然后在C中通过TerminateThread让程序异常退出。先看C代码:
extern "C"{_declspec(dllexport) void dowork();}#include "iostream"#include using namespace std;void dowork(){DWORD threadId = GetCurrentThreadId();printf("C++:当前线程ID(十进制):%lu,十六进制:0x%X\n", threadId, threadId);printf("C++:我准备退出了哦。。。\n");TerminateThread(GetCurrentThread(), 1);}
然后在C#中调用导出的dowork方法:
namespace Example_1_1{ internal class Program { static void Main(string[] args) { DoRequest(); Console.ReadLine(); } static void DoRequest() { Task.Run(() => { Console.WriteLine("1. 调用 C++ 代码..."); try { dowork(); Console.WriteLine("2. C++ 代码执行完毕..."); } catch (Exception ex) { Console.WriteLine($"2. C++ 代码执行异常: {ex.Message}"); } }); } [DllImport("Example_1_2", CallingConvention = CallingConvention.Cdecl)] public extern static void dowork(); }}
运行程序,用windbg附加,果然看到一个XXXX线程:

故障复现了,接下来就是找到底是谁让这个ThreadPool线程异常退出的。
要找到祸根,得知道调用TerminateThread的调用栈。一个简单粗暴的方法是用process monitor——Windows的ETW机制在线程退出时会发出一个Event,process monitor可以捕获,还能记录调用栈。配置界面如下:

运行程序,用windbg附加进程,找到问题线程ID:
0:005> !tThreadCount: 5UnstartedThread: 0BackgroundThread: 3PendingThread: 0DeadThread: 1Hosted Runtime: no Lock DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 153c 00000202C603C240 2a020 Preemptive 00000202CA819060:00000202CA81B020 00000202c6088980 -00001 MTA 3 2 afc 00000202C60F0DB0 2b220 Preemptive 0000000000000000:0000000000000000 00000202c6088980 -00001 MTA (Finalizer) XXXX 4 4718 00000202C6057D10 102b220 Preemptive 00000202CA80CF70:00000202CA80E740 00000202c6088980 -00001 Ukn (Threadpool Worker) 4 5 4420 00000202C605D510 302b220 Preemptive 00000202CA80EB40:00000202CA810760 00000202c6088980 -00001 MTA (Threadpool Worker) 0:005> ? 4718Evaluate expression: 18200 = 00000000`00004718
卦中显示一个osid=18200的线程异常退出。在process monitor界面果然看到了Thread ID:18200的Thread Exit事件:

双击打开Stack选项卡,可以清晰看到是Example_1_2!dowork导致的退出:

真实项目中,看到dowork这个函数,排查范围一下子就缩小了很多。问题的根源,基本就摆在眼前了。
process monitor虽好,但有个遗憾——看不到托管栈。有没有办法既看到托管栈,又抓到现场?方法很简单:对kernel32!TerminateThread进行注入。一旦有人调用这个方法,就记录Terminate线程的线程ID和调用栈。完整代码如下:
namespace Example_1_1{ internal class Program { static void Main(string[] args) { // Install the hook before any TerminateThread calls can occur TerminateThreadHook.InstallHook(); Console.WriteLine("Hook installed. Starting test..."); DoRequest(); // Uninstall hook when done TerminateThreadHook.UninstallHook(); Console.ReadLine(); } static void DoRequest() { Task.Run(() => { Console.WriteLine("1. 调用 C++ 代码..."); try { dowork(); Console.WriteLine("2. C++ 代码执行完毕..."); } catch (Exception ex) { Console.WriteLine($"2. C++ 代码执行异常: {ex.Message}"); } }); } [DllImport("Example_1_2", CallingConvention = CallingConvention.Cdecl)] public extern static void dowork(); } public static class TerminateThreadHook { // TerminateThread function signature [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate bool TerminateThreadDelegate(IntPtr hThread, uint dwExitCode); private static TerminateThreadDelegate _originalTerminateThread; private static IntPtr _terminateThreadPtr = IntPtr.Zero; public static void InstallHook() { // 1. Get TerminateThread address from kernel32.dll _terminateThreadPtr = MinHook.GetProcAddress( MinHook.GetModuleHandle("kernel32.dll"), "TerminateThread"); if (_terminateThreadPtr == IntPtr.Zero) { Console.WriteLine("Failed to find TerminateThread address."); return; } // 2. Initialize MinHook var status = MinHook.MH_Initialize(); if (status != MinHook.MH_STATUS.MH_OK) { Console.WriteLine($"MH_Initialize failed: {status}"); return; } // 3. Create Hook var detourPtr = Marshal.GetFunctionPointerForDelegate( new TerminateThreadDelegate(HookedTerminateThread)); status = MinHook.MH_CreateHook(_terminateThreadPtr, detourPtr, out var originalPtr); if (status != MinHook.MH_STATUS.MH_OK) { Console.WriteLine($"MH_CreateHook failed: {status}"); return; } _originalTerminateThread = Marshal.GetDelegateForFunctionPointer(originalPtr); // 4. Enable Hook status = MinHook.MH_EnableHook(_terminateThreadPtr); if (status != MinHook.MH_STATUS.MH_OK) { Console.WriteLine($"MH_EnableHook failed: {status}"); return; } Console.WriteLine("TerminateThread hook installed successfully!"); } public static void UninstallHook() { if (_terminateThreadPtr == IntPtr.Zero) return; // 1. Disable Hook var status = MinHook.MH_DisableHook(_terminateThreadPtr); if (status != MinHook.MH_STATUS.MH_OK) Console.WriteLine($"MH_DisableHook failed: {status}"); // 2. Uninitialize MinHook status = MinHook.MH_Uninitialize(); if (status != MinHook.MH_STATUS.MH_OK) Console.WriteLine($"MH_Uninitialize failed: {status}"); _terminateThreadPtr = IntPtr.Zero; Console.WriteLine("Hook uninstalled."); } private static bool HookedTerminateThread(IntPtr hThread, uint dwExitCode) { // Get current thread ID uint currentThreadId = GetCurrentThreadId(); uint targetThreadId = GetThreadId(hThread); Console.WriteLine($"[HOOK] TerminateThread intercepted!"); Console.WriteLine($" Attempting to terminate thread: 0x{targetThreadId.ToString("X")} (ID: {targetThreadId})"); Console.WriteLine($" Called from thread ID: {currentThreadId}"); // Print managed call stack Console.WriteLine("\n [Managed Call Stack]:"); Console.WriteLine(Environment.StackTrace); return _originalTerminateThread(hThread, dwExitCode); } [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); [DllImport("kernel32.dll")] private static extern uint GetThreadId(IntPtr hThread); } public static class MinHook { public enum MH_STATUS { MH_OK = 0, MH_ERROR_ALREADY_INITIALIZED, MH_ERROR_NOT_INITIALIZED, // ... other status codes } [DllImport("MinHook.x64.dll", CallingConvention = CallingConvention.Cdecl)] public static extern MH_STATUS MH_Initialize(); [DllImport("MinHook.x64.dll", CallingConvention = CallingConvention.Cdecl)] public static extern MH_STATUS MH_Uninitialize(); [DllImport("MinHook.x64.dll", CallingConvention = CallingConvention.Cdecl)] public static extern MH_STATUS MH_CreateHook(IntPtr pTarget, IntPtr pDetour, out IntPtr ppOriginal); [DllImport("MinHook.x64.dll", CallingConvention = CallingConvention.Cdecl)] public static extern MH_STATUS MH_EnableHook(IntPtr pTarget); [DllImport("MinHook.x64.dll", CallingConvention = CallingConvention.Cdecl)] public static extern MH_STATUS MH_DisableHook(IntPtr pTarget); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll", CharSet = CharSet.Ansi)] public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); }}

从卦中信息看,果然拦截到了。Environment.StackTrace属性把托管栈完整展现了出来。当然,这里还差一个非托管部分的调用栈,如果真需要,可以借助dbghelp.dll来实现,这里就不展开了。总之,有了这些调用栈日志,再对比dump中的异常退出线程,真相就会浮出水面。
如今.NET的主战场在工控领域,C#与C++交互非常频繁,C++处理稍有不当,就可能给C#带来灾难性后果。这篇文章梳理的经验,希望能帮后来者少踩一些坑,少走一些弯路。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8