65.9K
CodeProject 正在变化。 阅读更多。
Home

异常注入:在另一个线程中抛出异常

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (13投票s)

2010 年 4 月 8 日

CPOL

6分钟阅读

viewsIcon

74979

如何通过异常中止不合作的线程

引言

本文介绍了一种“中止”一个不合作线程的方法。更准确地说,它可以用于中止从另一个线程调用的某个不合作函数,并将执行权返回到该线程内的某个“友好”点。本文描述的方法会导致在目标线程中引发异常(类似于 .NET 中的 Thread.Abort)。

方法

首先,我们约定一个可能要抛出的异常类型。我们称之为 ThreadAbort 并进行定义

class ThreadAbort
{
	__declspec (noreturn) static void Throw();
public:
	static bool RaiseInThread(HANDLE hThread);
	static void DontOptimize() throw (...);
};

正如你所见,ThreadAbort 没有成员变量。这意味着我们不会用异常传递任何参数。实际上也可以添加参数,但我们这里不讨论。static 成员函数的作用如下

  • RaiseInThread 导致指定线程抛出 ThreadAbort 异常。
  • DontOptimize 没有任何作用。但是,它应该在目标线程的适当 try/catch 块中调用。这与编译器在编译时看不到我们的异常在那里发生的可能性有关,因此在优化过程中可能会省略 try/catch 块,从而导致我们的异常未被处理。解决此问题的另一种方法是设置异步异常处理模型(稍后会详细介绍)。

我们可能在该线程中调用的不合作函数应该被适当的 try/catch 块包装

try {
	ThreadAbort::DontOptimize();
	
	// pass control to the func
	SomeNonResponsiveFunc();
	
} catch (ThreadAbort&) {
	// process abortion
}

现在让我们深入了解 ThreadAbort 的实现

__declspec (noreturn) void ThreadAbort::Throw()
{
	// just throw
	throw ThreadAbort();
}

void ThreadAbort::DontOptimize() throw (...)
{
	// By this awkward method we may convince the compiler that during the runtime
	// the exception *may* be thrown.
	// However it may not actually.
	volatile int i=0;
	if (i)
		Throw();
}

bool ThreadAbort::RaiseInThread(HANDLE hThread)
{
	bool ok = false;

	// Suspend the thread, so that we won't have surprises
	DWORD dwVal = SuspendThread(hThread);
	if (INFINITE != dwVal)
	{
		// Get its context (processor registers)
		CONTEXT ctx;
		ctx.ContextFlags = CONTEXT_CONTROL;
		if (GetThreadContext(hThread, &ctx))
		{
			// Jump into our Throw() function
			ctx.Eip = (DWORD) (DWORD_PTR) Throw;

			if (SetThreadContext(hThread, &ctx))
				ok = true;
		}

		// go ahead
		VERIFY(ResumeThread(hThread));
	}
	return ok;
}

从上面的代码可以看出,为了让线程抛出异常,我们挂起它,修改它的 EIP 寄存器(指令指针),使其指向 ThreadAbort::Throw,然后恢复它,使其愉快地进入深渊。这是一种暴力方法:我们不知道该线程实际上在做什么,它可能正处于某个操作的中间。所以我们可以在任何时候中断它。

与其他方法相比

还有其他方法可以中断线程正在进行的操作,但它们不如异常方法灵活

  • 可以调用 TerminateThread 来立即终止线程。但是,这不允许将控制权传递到线程内的“友好”代码。也就是说,我们可能不想终止该线程,只想将控制权传递到该线程内的其他代码。此外,当一个线程被 TerminateThread 终止时,其堆栈内存不会被操作系统释放。所以我们有内存泄漏(以及被中止代码分配的泄漏)。
  • 我们可以修改 EIP 直接指向“友好”代码,而不是涉及异常处理机制(该机制将(希望)在我们的友好代码中完成)。

    这里的问题是我们没有给被中止的代码执行清理的机会。因此,它分配的所有资源都会丢失,我们很可能会出现资源/内存泄漏。另一方面,如果代码是以支持异常的方式编写的,则可以优雅地进行清理。

也就是说,异常方法允许优雅地中止。然而,不幸的是,它不能保证我们不会有任何泄漏。这有几个原因

  • 并非所有代码都以支持异常的方式编写(意味着没有“悬空”的已分配资源,所有资源都受到自动变量的析构函数或 __try/__finally SEH 块的保护)。有些代码块的编写方式假设异常不会在其中发生。
  • 即使所有代码都以支持异常的方式编写,编译器也可以自由地优化代码。如果它没有看到异常发生的可能性,它可能会省略所需的异常处理记录。幸运的是,这可以通过选择所谓的异步异常处理模型来防止(有关更多信息,请阅读本文)。
  • 如果所有代码都以支持异常的方式编写,甚至如果我们选择了异步异常处理模型——我们仍然可能遇到问题。

    根据 C++ 规则,自动对象的生命周期在构造函数完成后正式开始。例如,如果对象在构造函数中抛出异常,则不会调用其析构函数。

    现在,由于我们盲目地在任何线程正在执行的操作的中间引起异常,我们可能会在对象的构造函数结束时抛出异常。在分配了资源之后,但之前它正式“出生”。在这种情况下,不会调用其析构函数,我们将出现泄漏。

    幸运的是,这也有一个变通方法:我们可以避免在构造函数中进行分配。也就是说,不要在构造函数中分配任何内容,只需“零初始化”您的变量。然后实际的分配可以在另一个方法中完成,该方法应该在构造函数之后立即调用。这称为两阶段对象构造。

  • 最后,如果我们以最谨慎的方式进行所有操作——我们仍然可能遇到问题。这次是析构函数。在正常的程序流程中,当对象的生命周期结束时,编译器生成的代码会移除该对象的异常处理信息,然后立即调用其析构函数。但是,如果我们恰好在移除异常处理信息之后,但在对象析构函数调用之前抛出异常呢?或者在析构函数执行过程中,当它尚未完成清理时?

不幸的是,在所有可能的情况下,都无法保证对非合作代码的正确清理。这就是为什么我们的方法是暴力方法。但是,与其他方法相比,它更优雅。实际上,您很有可能正确清理,而在最坏的情况下(如果一切都仔细编写),您最多只会遇到一个泄漏。

内核模式调用

我们的方法在目标线程处于用户模式运行时在其内部引发异常。然而,如果该线程当前正在执行系统(内核模式)调用,它不会立即中止。中止将被推迟到它从系统调用返回为止。如果我们谈论的是持续时间较短的系统调用(例如调用 SetEventCreateMutex 等),那么没有问题。但是,如果有人调用可等待函数(例如 SleepWaitForSingleObject 等),它们可能需要很长时间才能完成,甚至永远不会完成。

据我所知,从用户模式代码中无法中止系统调用。这只能通过深入操作系统内部来实现。中止此类调用的唯一方法是使用 TerminateThread

结论

最重要的结论是,您不应该使用非合作代码。任何可能耗时长的代码必须提供常规的中止方式。

接下来,无法中止未知代码并确信所有内容都已清理。然而,注入异常的方法提供了优雅清理的最佳机会。

从用户模式代码无法中止系统调用。在某些情况下,别无选择,只能调用 TerminateThread 函数。

我很感激评论。欢迎批评和新想法。

历史

  • 2010年4月8日:初始发布
© . All rights reserved.