.NET 中非托管线程上的异常

本文关键字:异常 线程 NET | 更新日期: 2023-09-27 17:56:59

如何处理

我的应用终止的情况,在终止前使用回调?

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

场景

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

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

  • AppDomain.UnhandledException (最重要的是)
  • Application.ThreadException
  • 调度程序。未处理异常

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

我在野外的两个例子

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

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

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

测试.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);
}

应用.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
    }
}

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

更新

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

.NET 中非托管线程上的异常

问题中代码的问题如下:

SetUnhandledExceptionFilter(Win32Handler);

由于委托是自动创建的,因此可以:

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

问题是,GC 可以在最终引用后的任何时候收集它,以及创建的本机>管理的 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(); 
    }
}

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

我不得不处理不可预测的非托管库。

如果要对非托管代码进行 P/调用,则可能会遇到问题。我发现在非托管代码中使用 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);
}

无法正确处理的异常总是会发生,无论您多么努力地从内部保护它,该过程都可能会意外死亡。但是,您可以从外部监视它。

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

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