.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。
因此,这项配置无法保证在崩溃前成功写入日志。
更可靠的方案:隔离到子进程
如果第三方代码经常引发访问冲突,可以将它放进单独的工作进程:
- 主程序启动工作进程。
- 工作进程调用不受控制的代码。
- 主程序等待工作进程退出,并记录退出码、标准错误和输入参数。
- 工作进程崩溃后,主程序仍处于完整且未损坏的运行环境中,可以可靠地写入日志。
在 WinForms 中,可以让 UI 程序作为父进程,将危险操作交给 Console worker。两个进程可通过命令行、标准输入输出、命名管道或其他 IPC 方式交换数据。
同一进程中的 catch 可以补充诊断信息。面对不可信原生代码导致的崩溃,进程隔离更稳妥。
备注:内容仅供参考。
