Thread.Abort()如何工作?

当无效输入传递给方法或对象即将进入无效状态时,我们通常抛出exception。 让我们考虑以下示例

private void SomeMethod(string value) { if(value == null) throw new ArgumentNullException("value"); //Method logic goes here } 

在上面的例子中,我插入了一个抛出ArgumentNullException的throw语句。 我的问题是运行时如何设置抛出ThreadAbortException 。 显然,在所有方法中都不可能使用throw语句,即使运行时也设法在我们的自定义方法中抛出ThreadAbortException

我想知道他们是怎么做到的? 我很想知道幕后发生了什么,我打开一个reflection器来打开Thread.Abort并最终得到这个

 [MethodImplAttribute(MethodImplOptions.InternalCall)] private extern void AbortInternal();//Implemented in CLR 

然后我用Google搜索并发现了ThreadAbortException如何真正起作用 。 这个链接说运行时通过QueueUserAPC函数发布APC,这就是他们如何做到这一点。 我不知道QueueUserAPC方法我只是试着看看是否可以使用一些代码。 以下代码显示了我的尝试。

 [DllImport("kernel32.dll")] static extern uint QueueUserAPC(ApcDelegate pfnAPC, IntPtr hThread, UIntPtr dwData); delegate void ApcDelegate(UIntPtr dwParam); Thread t = new Thread(Threadproc); t.Start(); //wait for thread to start uint result = QueueUserAPC(APC, new IntPtr(nativeId), (UIntPtr)0);//returns zero(fails) int error = Marshal.GetLastWin32Error();// error also zero private static void APC(UIntPtr data) { Console.WriteLine("Callback invoked"); } private static void Threadproc() { //some infinite loop with a sleep } 

如果我做错了什么原谅我,我不知道该怎么做。 再回到问题,有关CLR团队的这个或部分知识的人能解释它在内部的运作方式吗? 如果APC是技巧运行时,那么在这里做错了吗?

你确定你读过你指向的页面吗? 最后它归结为:

对Thread.Abort的调用归结为.NET在要中止的线程上设置一个标志,然后在线程生命周期的某些点期间检查该标志,如果设置了标志则抛出exception。

要使APC回调起作用,您需要一个线程句柄(与线程ID不同)。 我还更新了PInvokes的属性。

还要记住,线程需要处于“警报”等待状态才能调用APC(Thread.Sleep会给我们)。 因此,如果线程正在忙于执行操作,则可能无法调用它。

 [DllImport("kernel32.dll", EntryPoint = "GetCurrentThread", CallingConvention = CallingConvention.StdCall)] public static extern IntPtr GetCurrentThread(); [DllImport("kernel32.dll", EntryPoint = "QueueUserAPC", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern uint QueueUserAPC(ApcDelegate pfnAPC, IntPtr hThread, UIntPtr dwData); [UnmanagedFunctionPointerAttribute(CallingConvention.StdCall)] public delegate void ApcDelegate(UIntPtr dwParam); [DllImport("kernel32.dll", EntryPoint = "DuplicateHandle", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern bool DuplicateHandle([In] System.IntPtr hSourceProcessHandle, [In] System.IntPtr hSourceHandle, [In] System.IntPtr hTargetProcessHandle, out System.IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAsAttribute(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions); [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern IntPtr GetCurrentProcess(); static IntPtr hThread; public static void SomeMethod(object value) { DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), out hThread, 0, false, 2); while (true) { Console.WriteLine("."); Thread.Sleep(1000); } } private static void APC(UIntPtr data) { Console.WriteLine("Callback invoked"); } static void Main(string[] args) { Console.WriteLine("in Main\n"); Thread t = new Thread(Program.SomeMethod); t.Start(); Thread.Sleep(1000); // wait until the thread fills out the hThread member -- don't do this at home, this isn't a good way to synchronize threads... uint result = QueueUserAPC(APC, hThread, (UIntPtr)0); Console.ReadLine(); } 

编辑:
CLR如何注入exception
给定线程函数的这个循环:

 while (true) { i = ((i + 7) * 3 ^ 0x73234) & 0xFFFF; } 

然后我。 .Abort线程并查看本机堆栈跟踪

 ... ntdll!KiUserExceptionDispatcher KERNELBASE!RaiseException clr!RaiseComPlusException clr!RedirectForThrowControl2 clr!RedirectForThrowControl_RspAligned clr!RedirectForThrowControl_FixRsp csTest.Program.SomeMethod(System.Object) ... 

查看RedirectForThrowControl_FixRsp调用的返回地址,它指向我的循环中间,没有跳转或调用:

 nop mov eax,dword ptr [rbp+8] add eax,7 // code flow would return to execute this line lea eax,[rax+rax*2] xor eax,73234h and eax,0FFFFh mov dword ptr [rbp+8],eax nop mov byte ptr [rbp+18h],1 jmp 000007fe`95ba02da // loop back to the top 

显然,CLR实际上正在修改有问题的线程的指令指针,以便从正常流程中物理地控制控件。 他们显然需要提供几个包装来修复和恢复所有堆栈寄存器以使其正常工作(因此适当命名为_FixRsp_RspAligned API。

在一个单独的测试中,我只是在我的线程循环中调用了Console.Write() ,看起来CLR在物理调用WriteFile之前注入了一个测试:

 KERNELBASE!RaiseException clr!RaiseTheExceptionInternalOnly clr! ?? ::FNODOBFM::`string' clr!HelperMethodFrame::PushSlowHelper clr!JIT_RareDisableHelper mscorlib_ni!DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) mscorlib_ni!System.IO.__ConsoleStream.WriteFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean) 

要使QueueUserAPC正常工作,您必须做两件事。

  1. 获取目标线程句柄。 请注意,这与本机线程ID不同。
  2. 允许目标线程进入可警告状态。

这是一个完整的程序来演示这一点。

 class Program { [DllImport("kernel32.dll", EntryPoint = "DuplicateHandle", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern bool DuplicateHandle([In] System.IntPtr hSourceProcessHandle, [In] System.IntPtr hSourceHandle, [In] System.IntPtr hTargetProcessHandle, out System.IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAsAttribute(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions); [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentThread(); [DllImport("kernel32.dll")] private static extern uint QueueUserAPC(ApcMethod pfnAPC, IntPtr hThread, UIntPtr dwData); private delegate void ApcMethod(UIntPtr dwParam); static void Main(string[] args) { Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId); IntPtr threadHandle = IntPtr.Zero; var threadHandleSet = new ManualResetEvent(false); var apcSet = new ManualResetEvent(false); var thread = new Thread( () => { Console.WriteLine("thread started"); threadHandle = GetCurrentThread(); DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), out threadHandle, 0, false, 2); threadHandleSet.Set(); apcSet.WaitOne(); for (int i = 0; i < 10; i++) { Console.WriteLine("thread waiting"); Thread.Sleep(1000); Console.WriteLine("thread running"); } Console.WriteLine("thread finished"); }); thread.Start(); threadHandleSet.WaitOne(); uint result = QueueUserAPC(DoApcCallback, threadHandle, UIntPtr.Zero); apcSet.Set(); Console.ReadLine(); } private static void DoApcCallback(UIntPtr dwParam) { Console.WriteLine("DoApcCallback: " + Thread.CurrentThread.ManagedThreadId); } } 

这实质上允许开发人员将方法的执行注入任意线程。 目标线程不必像传统方法那样具有消息泵。 但是,这种方法的一个问题是目标线程必须处于可警告状态。 所以基本上线程必须调用一个固定的.NET阻塞调用,如Thread.SleepWaitHandle.WaitOne等,以便执行APC队列。

这很容易,底层操作系统可以做到。 如果线程处于除“在另一个核心上运行”之外的任何状态,则没有问题 – 它的状态设置为“永不再运行”。 如果线程在另一个内核上运行,则OS硬件会中断另一个内核。 它是处理器间驱动程序,因此消灭了线程。

任何提及’时间片’,’量子’等只是……

我下载了SSCLI代码并开始探索。 代码很难让我遵循(主要是因为我不是C ++或ASM专家),但我确实看到很多钩子,半同步注入中止。

  • try / catch / finally / fault块流控制处理
  • GC激活(分配内存)
  • 在处于可警告状态时通过软中断(如Thread.Interrupt)代理
  • 虚拟呼叫拦截
  • JIT尾部呼叫准备
  • 不受管理的过渡管理

仅举几例。 我想知道的是如何注入异步中止。 劫持指令指针的一般想法是它发生的一部分。 但是,它比我上面描述的要复杂得多。 似乎不总是使用Suspend-Modify-Resume习语。 从SSCLI代码我可以看到它确实在某些情况下暂停和恢复线程以准备劫持,但情况并非总是如此。 在我看来,劫持也可以在螺纹运行时完成。

您链接的文章提到在目标线程上设置了中止标志。 这在技术上是正确的。 该标志称为TS_AbortRequested并且有许多逻辑控制如何设置此标志。 存在用于确定是否存在受约束的执行区以及该线程当前是否在try-catch-finally-fault块中的检查。 其中一些工作涉及堆栈爬网,这意味着必须暂停和恢复线程。 然而,如何检测到旗帜的变化是真正的魔法发生的地方。 这篇文章并没有很好地解释这一点。

我在上面的列表中已经提到了几个半同步注入点。 理解这些应该是微不足道的。 但是,异步注入是如何发生的呢? 好吧,在我看来,JIT是幕后的向导。 JIT / GC中内置了某种轮询机制,可定期确定是否应该进行收集。 这还提供了检查任何托管线程是否已更改状态的机会(例如设置了中止标志)。 如果设置了TS_AbortRequested ,那么劫持就会发生。

如果您正在查看SSCLI代码,这里有一些很好的function。

  • HandleThreadAbort
  • CommonTripThread
  • JIT_PollGC
  • JIT_TailCallHelper
  • COMPlusCheckForAbort
  • ThrowForFlowControl
  • JIT_RareDisableHelper

还有很多其他线索。 请记住,这是SSCLI,因此方法名称可能与生产中观察到的调用堆栈不完全匹配(就像Josh Poley发现的那样 ),但是会有相似之处。 此外,许multithreading劫持都是使用汇编代码完成的,因此有时难以遵循。 我强调了JIT_PollGC因为我相信这是有趣的事情发生的地方。 这是我认为JIT将动态地和战略性地放入执行线程的钩子。 这基本上是那些紧密循环如何仍然可以接收中止注射的机制。 目标线程实际上主要是轮询中止请求,但作为调用GC 1 的更大策略的一部分

很明显,JIT,GC和线程中止密切相关。 当您查看SSCLI代码时,这是显而易见的。 例如,用于确定线程中止的安全点的方法用于确定是否允许GC运行的方法相同。


1 Shared Source CLI Essentials,David Stutz,2003 ,pg。 249-250