Thread.Abort()如何工作?

时间:2013-08-09 19:22:38

标签: c# multithreading thread-abort

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

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

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

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

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

然后我用谷歌搜索了How does ThreadAbortException really work。此链接表示运行时通过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是特技运行时,那么在这里做错了吗?

5 个答案:

答案 0 :(得分:11)

您确定要阅读您指向的页面吗?最后归结为:

  

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

答案 1 :(得分:10)

要使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如何注入异常
给出线程函数的这个循环:

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)

答案 2 :(得分:1)

要让QueueUserAPC工作,你必须做两件事。

  1. 获取目标线程句柄。请注意,这与本机线程ID不同。
  2. 允许目标线程进入可警告状态。
  3. 这是一个完整的程序,用于演示这一点。

    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队列。

答案 3 :(得分:0)

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

  • try / catch / finally / fault block flow control processing
  • GC激活(分配内存)
  • 在处于警报状态时通过软中断代理(如使用Thread.Interrupt)
  • 虚拟来电拦截
  • JIT尾部呼叫准备
  • 非托管到托管转换

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

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

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

如果你正在查看SSCLI代码,这里有一些很好的功能。

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

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

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


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

答案 4 :(得分:0)

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

任何提及'时间片','量子'等只是......