商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > .NET线程异常退出引发程序崩溃的问题分析及解决方案

.NET线程异常退出引发程序崩溃的问题分析及解决方案

  发布于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),排查难度一下子就上去了。既然是复盘,就来好好梳理一下这类问题的解决思路。

二:故障重现

1. 问题代码

为了演示,我们用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线程:

.NET线程异常退出引发程序崩溃的问题分析及解决方案

故障复现了,接下来就是找到底是谁让这个ThreadPool线程异常退出的。

三:如何寻找第一现场

1. process monitor

要找到祸根,得知道调用TerminateThread的调用栈。一个简单粗暴的方法是用process monitor——Windows的ETW机制在线程退出时会发出一个Event,process monitor可以捕获,还能记录调用栈。配置界面如下:

.NET线程异常退出引发程序崩溃的问题分析及解决方案

运行程序,用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:18200Thread Exit事件:

.NET线程异常退出引发程序崩溃的问题分析及解决方案

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

.NET线程异常退出引发程序崩溃的问题分析及解决方案

真实项目中,看到dowork这个函数,排查范围一下子就缩小了很多。问题的根源,基本就摆在眼前了。

2. MinHook 注入

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);    }}

.NET线程异常退出引发程序崩溃的问题分析及解决方案

从卦中信息看,果然拦截到了。Environment.StackTrace属性把托管栈完整展现了出来。当然,这里还差一个非托管部分的调用栈,如果真需要,可以借助dbghelp.dll来实现,这里就不展开了。总之,有了这些调用栈日志,再对比dump中的异常退出线程,真相就会浮出水面。

四:总结

如今.NET的主战场在工控领域,C#与C++交互非常频繁,C++处理稍有不当,就可能给C#带来灾难性后果。这篇文章梳理的经验,希望能帮后来者少踩一些坑,少走一些弯路。

本文转载于:https://www.jb51.net/aspnet/346942z3u.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注