【问题标题】:Exceptions on unmanaged threads in .NET.NET 中非托管线程的异常
【发布时间】:2015-09-03 03:17:47
【问题描述】:

当我的应用程序终止时,如何使用终止前的回调来处理情况?

.NET 处理程序在以下情况下不起作用,SetUnhandledExceptionHandler 是正确的选择吗?它似乎有下面讨论的缺点。

场景

我想通过 .net 应用中的服务向我们的服务发送消息和错误报告来响应所有应用终止情况。

但是,我有一个 WPF 应用程序,其中我们的两个测试人员得到绕过的未处理异常:

  • AppDomain.UnhandledException(最重要的)
  • Application.ThreadException
  • Dispatcher.UnhandledException

它们被标记为 SecuirtyCriticalHandleProcessCorruptedStateExceptionslegacyCorruptedStateExceptionsPolicy 在 app.config 中设置为 true

我在野外的两个例子

  • 在某处初始化 WPF 时,运行 widows10 的 VirtualBox 会抛出一些 vboxd3d.dll(关闭 vbox 3d 加速“修复它”)
  • Win8 机器在系统上下文菜单中带有可疑选项“在显卡 A/B 上运行”,在 WPF 启动期间在某处 (:/) 崩溃,但仅在应用反破解工具时才会崩溃。

无论哪种方式,当应用上线时,应用必须在终止之前响应这些类型的故障

我可以使用非托管异常重现此情况,该异常发生在 .net 中 PInvoked 方法的非托管线程中:

test.dll

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

DWORD WINAPI myThread(LPVOID lpParameter)
{
    long testfail = *(long*)(-9022);
    return 1;
}

extern "C" __declspec(dllexport) void test()
{
    DWORD tid;
    HANDLE myHandle = CreateThread(0, 0, myThread, NULL, 0, &tid);
    WaitForSingleObject(myHandle, INFINITE);
}

app.exe

class TestApp
{
    [DllImport("kernel32.dll")]
    static extern FilterDelegate SetUnhandledExceptionFilter(FilterDelegate lpTopLevelExceptionFilter);

    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    delegate int FilterDelegate(IntPtr exception_pointers);

    static int Win32Handler(IntPtr nope)
    {
        MessageBox.Show("Native uncaught SEH exception"); // show + report or whatever
        Environment.Exit(-1); // exit and avoid WER etc
        return 1; // thats EXCEPTION_EXECUTE_HANDLER, although this wont be called due to the previous line
    }

    [DllImport("test.dll")]
    static extern void test();

    [STAThread]
    public static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        SetUnhandledExceptionFilter(Win32Handler);
        test(); // This is caught by Win32Handler, not CurrentDomain_UnhandledException
    }
    [SecurityCritical, HandleProcessCorruptedStateExceptions ]
    static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Exception ex = e.ExceptionObject as Exception;
        MessageBox.Show(ex.ToString()); // show + report or whatever
        Environment.Exit(-1); // exit and avoid WER etc
    }
}

这会处理裸 WPF 测试应用程序中 vboxd3d.dll 中的故障,该应用程序当然也注册了 WCF 调度程序和 WinForms 应用程序(为什么不)异常处理程序。

更新

  • 在我尝试使用它的生产代码中,处理程序似乎被其他调用者覆盖,我可以通过每 100 毫秒调用一次方法来解决这个问题,这当然是愚蠢的。
    • 在存在 vbox3d.dll 问题的机器上,执行上述操作会将异常替换为 clr.dll 中的异常。
    • 出现崩溃时,传递给kernel32的托管函数指针不再有效。使用本机帮助程序 dll 设置处理程序,该程序调用内部的本机函数似乎正在工作。托管函数是一种静态方法 - 我不确定此处是否适用固定,也许 clr 正在终止...
    • 确实正在收集托管委托。没有发生处理程序的“覆盖”。我已添加作为答案..不确定要接受什么或这里的 SO 约定是什么...

【问题讨论】:

  • 这没什么意义。您不会使用 CreateProcess()“启动方法”。这将创建一个进程,该进程中的任何异常都无法在您的进程中进行代码观察。
  • 你对非托管异常的堆栈感兴趣,如果是,那为什么不使用Windbg,它会给你Win32线程的堆栈跟踪。 Infact Windows 调试工具做得很好
  • 另外,如果您有相关的非托管代码以及 pdb 文件,请在 VS - Debug - Exceptions 中启用相关的异常类别,它包含 Win32 异常和其他几个类别
  • @MrinalKamboj 这是用于客户端处理崩溃的问题,我已经编辑了这个问题,现在更有意义了吗?
  • @HansPassant 抱歉,我的意思当然是 createthread!显然没有足够的咖啡。这个问题现在已经改进了(我希望)?

标签: c# .net winapi unmanaged unhandled-exception


【解决方案1】:

问题中代码的问题是这样的:

SetUnhandledExceptionFilter(Win32Handler);

由于委托是自动创建的,因此等同于:

FilterDelegate del = new FilterDelegate(Win32Handler);
SetUnhandledExceptionFilter(del);

问题在于,GC 可以在最终引用之后的任何时候收集它,以及创建的 native->managed thunk。所以:

SetUnhandledExceptionFilter(Win32Handler);
GC.Collect();
native_crash_on_unmanaged_thread();

总是会导致严重的崩溃,其中传递给 kernel32.dll 的处理程序不再是有效的函数指针。这可以通过不允许 GC 收集来解决:

public class Program
{
    static FilterDelegate mdel;
    public static void Main(string[] args)
    {
        FilterDelegate del = new FilterDelegate(Win32Handler);
        SetUnhandledExceptionFilter(del);
        GC.KeepAlive(del);  // do not collect "del" in this scope (main)
        // You could also use mdel, which I dont believe is collected either
        GC.Collect();
        native_crash_on_unmanaged_thread(); 
    }
}

其他答案也是一个很好的资源;不知道现在要标记什么作为答案。

【讨论】:

  • 当然,整个方法的缺点是您调用的一些伪劣代码随后可能会替换并忽略您的处理程序。没有办法防止这种情况发生。在这方面,这里的其他答案更好。
【解决方案2】:

容我们说,我不得不处理不可预测的非托管库。

如果您要 P/Invoking 到非托管代码中,您可能会遇到问题。我发现在非托管代码周围使用 C++/CLI 包装器更容易,并且在某些情况下,我在使用 C++/CLI 之前围绕库编写了另一组非托管 C++ 包装器。

你可能会想,“你到底为什么要写两组包装器?”

首先,如果你隔离了非托管代码,就更容易捕获异常,让它们更可口。

第二个纯粹是实用的 - 如果您有一个使用 stl 的库(不是 dll),您会发现该链接将神奇地提供所有代码(托管和非托管)的 stl 函数的 CLI 实现。防止这种情况发生的最简单方法是完全隔离使用 stl 的代码,这意味着每次在非托管代码中通过 stl 访问数据结构时,最终都会在托管代码和非托管代码之间进行多次转换,并且性能会下降。你可能会想,“我是一个严谨的程序员 - 我会非常小心地将 #pragma managed 和/或 #pragma unmanaged 包装器放在正确的位置,我已经准备好了。”不,不,不,不。这不仅困难且不可靠,当(不是如果)你没有正确地做到这一点时,你将没有很好的方法来检测它。

和往常一样,您应该确保您编写的任何包装器都是厚实的而不是冗长的。

这是处理不稳定库的典型非托管代码块:

try {
    // a bunch of set up code that you don't need to
    // see reduced to this:
    SomeImageType *outImage = GetImage();
    // I was having problems with the heap getting mangled
    // so heapcheck() is conditional macro that calls [_heapchk()][1]
    heapcheck();
    return outImage;
}
catch (std::bad_alloc &) {
    throw MyLib::MyLibNoMemory();
}
catch (MyLib::MyLibFailure &err)
{
    throw err;
}
catch (const char* msg)
{
    // seriously, some code throws a string.
    throw msg;
}
catch (...) {
    throw MyLib::MyLibFailure(MyKib::MyFailureReason::kUnknown2);
}

【讨论】:

  • 这是一个很好的 PInvoking 非托管代码的解决方案。你会说我疯了,我们得到的未处理异常来自框架内;我会带着一些调用堆栈回来 - 我提到问题的顶部,会澄清。
  • 另外,我认为您也可以使用 catch(int code) {} + 不要忘记链接器上的 /EHa 用于 try/catch 中的 SEH 异常。
【解决方案3】:

无法正确处理的异常总是会发生,并且无论您如何努力从内部保护它,进程都可能意外死亡。但是,您可以从外部对其进行监控。

另一个 进程来监控您的主进程。如果主进程突然消失而没有记录错误或优雅地报告,那么第二个进程可以做到这一点。第二个过程可以简单得多,根本没有非托管调用,因此它突然消失的可能性要小得多。

作为最后的手段,当您的进程启动时,请检查它们是否已正确关闭。如果没有,那么您可以报告关闭错误。如果整个机器都死了,这将很有用。

【讨论】:

  • 一个很好的安全网,这个。当然,这无助于调试崩溃——但你可以回滚版本,其中一定有一段令人抓狂的代码。
  • 如果要调试,让进程崩溃并产生转储。然后你就可以看到发生了什么。
  • 如果您有权访问转储。可以通过 WER 和 microsoft 元数据交换从客户端获取它。不久前,我什至写了一个小技巧来自动将产品映射提交给 microsoft (github.com/flavourous/Mexer),以便与 CI 一起使用。尽管 WER 不是您希望经常或主要使用的资源 - 但希望这些情况很少见。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-01
  • 1970-01-01
  • 2011-12-15
  • 1970-01-01
  • 2014-07-09
  • 1970-01-01
  • 2011-10-14
相关资源
最近更新 更多