PAYATHON 2026

.NET 8 如何捕获 AccessViolationException 并退出

排障赵师傅

结论

.NET 8 可以通过运行时配置启用兼容策略:

"System.Runtime.LegacyCorruptedStateExceptionsPolicy": true

建议在项目文件中配置 RuntimeHostConfigurationOption,让 SDK 将其写入生成的 .runtimeconfig.json:

<ItemGroup>
  <RuntimeHostConfigurationOption
    Include="System.Runtime.LegacyCorruptedStateExceptionsPolicy"
    Value="true" />
</ItemGroup>

启用后,可以通过 catch (AccessViolationException) 记录日志,再用 throw; 重新抛出异常。随后,进程会按未处理异常的方式终止。

这个开关用于兼容旧行为。发生访问冲突后,进程不一定还能安全运行,日志也只能尽力写入,无法保证成功。需要可靠记录故障时,应将不受控制的代码放到独立子进程中。

在项目文件中配置

编辑 .csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <RuntimeHostConfigurationOption
      Include="System.Runtime.LegacyCorruptedStateExceptionsPolicy"
      Value="true" />
  </ItemGroup>

</Project>

重新生成项目后,输出目录中的 应用程序名.runtimeconfig.json 应包含类似配置:

{
  "runtimeOptions": {
    "tfm": "net8.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "8.0.0"
    },
    "configProperties": {
      "System.Runtime.LegacyCorruptedStateExceptionsPolicy": true
    }
  }
}

不要直接修改输出目录中的 .runtimeconfig.json,因为下次构建或发布时会覆盖该文件。

使用 runtimeconfig.template.json

如果不想修改 .csproj,可以在项目根目录创建 runtimeconfig.template.json:

{
  "configProperties": {
    "System.Runtime.LegacyCorruptedStateExceptionsPolicy": true
  }
}

.NET SDK 会将其中的内容合并到最终生成的 .runtimeconfig.json。

项目中只需放置 runtimeconfig.template.json,完整的 应用程序名.runtimeconfig.json 由 SDK 生成,其中包括目标框架和共享框架版本等信息。

捕获、记录并重新抛出

启用开关后,可以在调用不受控制代码的边界处捕获异常:

try
{
    CallUntrustedCode();
}
catch (AccessViolationException ex)
{
    TryWriteCrashLog(ex);
    throw;
}

日志函数应尽量简短,避免执行复杂的业务逻辑:

private static void TryWriteCrashLog(Exception exception)
{
    try
    {
        string logPath = Path.Combine(
            AppContext.BaseDirectory,
            "fatal-error.log");

        string message =
            $"[{DateTimeOffset.Now:O}]{Environment.NewLine}" +
            $"{exception}{Environment.NewLine}{Environment.NewLine}";

        File.AppendAllText(logPath, message);
    }
    catch
    {
        // 进程状态可能已经损坏,日志写入失败时不要继续处理。
    }
}

throw; 会保留原始异常及其堆栈。不要改成:

throw ex;

throw ex; 会重置托管堆栈信息。

Console application 示例

internal static class Program
{
    private static int Main(string[] args)
    {
        try
        {
            CallUntrustedCode();
            return 0;
        }
        catch (AccessViolationException ex)
        {
            TryWriteCrashLog(ex);
            throw;
        }
    }

    private static void CallUntrustedCode()
    {
        // 调用可能触发访问冲突的代码。
    }

    private static void TryWriteCrashLog(Exception exception)
    {
        try
        {
            File.AppendAllText(
                Path.Combine(AppContext.BaseDirectory, "fatal-error.log"),
                $"[{DateTimeOffset.Now:O}]{Environment.NewLine}" +
                $"{exception}{Environment.NewLine}{Environment.NewLine}");
        }
        catch
        {
        }
    }
}

WinForms application 示例

如果异常会沿 UI 线程的托管调用栈传播,可以将 Application.Run 放在 try 中:

internal static class Program
{
    [STAThread]
    private static void Main()
    {
        ApplicationConfiguration.Initialize();

        try
        {
            Application.Run(new MainForm());
        }
        catch (AccessViolationException ex)
        {
            TryWriteCrashLog(ex);
            throw;
        }
    }

    private static void TryWriteCrashLog(Exception exception)
    {
        try
        {
            File.AppendAllText(
                Path.Combine(AppContext.BaseDirectory, "fatal-error.log"),
                $"[{DateTimeOffset.Now:O}]{Environment.NewLine}" +
                $"{exception}{Environment.NewLine}{Environment.NewLine}");
        }
        catch
        {
        }
    }
}

如果访问冲突发生在原生线程、非托管回调中,或者发生在运行时无法将其转换为可传播托管异常的位置,这个 catch 仍可能不会执行。

Application.ThreadException 主要处理普通的 WinForms UI 线程异常,不能作为捕获损坏状态异常的可靠方案。发生严重内存损坏后,AppDomain.CurrentDomain.UnhandledException 也无法保证正常运行并完成日志写入。

为什么在 Main 中设置环境变量无效

下面的写法设置得太晚:

Environment.SetEnvironmentVariable(
    "COMPlus_legacyCorruptedStateExceptionsPolicy",
    "1");

执行 Main 时,CLR 已经启动,并且可能已经读取相关配置。如果要使用环境变量,必须在进程启动前通过操作系统、启动脚本或父进程设置。

例如,在 Windows 命令提示符中:

set COMPlus_legacyCorruptedStateExceptionsPolicy=1
MyApplication.exe

在 PowerShell 中:

$env:COMPlus_legacyCorruptedStateExceptionsPolicy = "1"
.\MyApplication.exe

对于 .NET 8 应用,将 System.Runtime.LegacyCorruptedStateExceptionsPolicy 写入项目生成的运行时配置通常更清晰,也方便随应用一起部署。

环境变量名称中不能混入不可见的 Unicode 字符。从网页复制名称时,软连字符等字符可能让变量名看起来没有问题,实际名称却完全不同。

HandleProcessCorruptedStateExceptions 和 SecurityCritical

HandleProcessCorruptedStateExceptionsAttribute 属于旧版 .NET Framework 的异常处理模型,现代 .NET 不应再依赖它。仅在调用方法上添加该属性,不会自动改变 .NET 8 的损坏状态异常策略。

SecurityCriticalAttribute 和 Full Trust 相关做法也来自旧版 Code Access Security 模型,不适合用来处理 .NET 8 中的这个问题。

启用运行时兼容开关后,捕获点通常不需要这两个属性。

并非所有访问冲突都能被捕获

首先要区分两种情况:

throw new AccessViolationException();

这是显式抛出的托管异常,普通的 try/catch 就能捕获。

真正的访问冲突通常来自无效指针、损坏的原生内存、错误的 P/Invoke 声明或有缺陷的非托管组件。只有当运行时能够将底层故障转换并传递为托管 AccessViolationException 时,兼容开关才可能让 catch 执行。

以下情况仍可能直接终止进程:

  • 原生组件调用 abort、TerminateProcess 或类似终止函数。
  • 运行时进入 fail-fast 路径。
  • 内存损坏严重到无法继续展开异常栈。
  • 故障发生在没有进入托管异常处理边界的原生线程上。
  • 实际异常不是可转换的 AccessViolationException。

因此,这项配置无法保证在崩溃前成功写入日志。

更可靠的方案:隔离到子进程

如果第三方代码经常引发访问冲突,可以将它放进单独的工作进程:

  1. 主程序启动工作进程。
  2. 工作进程调用不受控制的代码。
  3. 主程序等待工作进程退出,并记录退出码、标准错误和输入参数。
  4. 工作进程崩溃后,主程序仍处于完整且未损坏的运行环境中,可以可靠地写入日志。

在 WinForms 中,可以让 UI 程序作为父进程,将危险操作交给 Console worker。两个进程可通过命令行、标准输入输出、命名管道或其他 IPC 方式交换数据。

同一进程中的 catch 可以补充诊断信息。面对不可信原生代码导致的崩溃,进程隔离更稳妥。

备注:内容仅供参考。