17

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

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

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

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

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

然后我用谷歌搜索,发现这个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技巧运行时遵循这里做错了什么?

4

5 回答 5

11

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

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

于 2013-08-09T19:27:02.893 回答
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_RspAlignedAPI。


在一个单独的测试中,我只是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)
于 2013-08-29T18:13:28.903 回答
1

要让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 队列执行。

于 2013-08-29T18:41:13.457 回答
1

我下载了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 代码,这里有一些不错的函数可供查看。

  • 处理线程中止
  • CommonTripThread
  • JIT_PollGC
  • JIT_TailCallHelper
  • COMPlusCheckForAbort
  • ThrowForFlowControl
  • JIT_RareDisableHelper

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

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


1共享源 CLI 要点,David Stutz,2003 年,第 1 页。249-250

于 2013-08-09T20:49:56.467 回答
0

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

任何提及“时间片”、“量子”等都是......

于 2013-08-09T22:49:57.177 回答