在使用 .NET Native AOT 发布应用后,很多开发者会遇到一个典型问题:开发环境运行正常,一旦发布成 AOT 原生程序就出现异常甚至崩溃。这类问题本质上与 AOT 的运行机制、裁剪(Trimming)以及调试能力受限密切相关。本文将从原理、排查步骤到实战技巧,系统讲解如何定位和解决问题。
为什么 Native AOT 更容易发布后出问题?
Native AOT 与传统 .NET 最大区别在于:
- 代码在发布阶段就被编译为原生二进制
- 运行时不再依赖 JIT
- 默认启用裁剪(Trim),移除未使用代码
这带来性能优势,但也引入了几个隐患:
- 反射和动态代码受限:很多依赖反射的逻辑,在 AOT 下可能被裁剪掉,导致运行时报错。
- 运行时行为变化:AOT 是静态世界,动态加载、Emit 等能力基本不可用。
- 调试能力下降:发布后的程序是纯原生二进制,不能再用传统托管调试器。
排查核心思路:从可控环境到AOT环境
官方建议的第一原则其实很简单:先在完整 .NET Runtime 下调试,再迁移到 AOT。因为dotnet run 使用完整运行时,dotnet publish -p:PublishAot=true 才是 AOT。
排查顺序建议:
- 确认非 AOT 模式是否正常
- 逐步开启 AOT 相关特性
- 定位差异点
发布后异常的常见原因与解决方案
1. 反射/序列化导致崩溃
典型现象:
- JSON 序列化失败
- 类型找不到
- MissingMethodException
原因:
- AOT + Trimming 删除了未显式引用的类型
解决方法:
- 使用 Source Generator(如 System.Text.Json)
- 添加 DynamicallyAccessedMembers
- 或关闭裁剪测试问题来源
2. 依赖库不兼容 AOT
并不是所有库都支持 Native AOT:
- EF Core(部分场景仍有限制)
- 动态代理类库
- 运行时生成代码的框架
排查方式:
- 查看编译 warning(AOT 会给出提示)
- 替换为 AOT 友好库(如 Dapper AOT)
3. 发布后直接崩溃(无日志)
这种最棘手,但也最常见。
原因可能包括:
- 本地调用(P/Invoke)问题
- 内存访问错误
- 裁剪导致关键代码缺失
解决关键:使用原生调试器
Native AOT 调试的正确打开方式
1. 使用原生调试工具
发布后程序已经是 native binary,需要使用:
- Windows:WinDbg / Visual Studio Native Debugger
- Linux:gdb / lldb
可以直接加载 exe 进行调试。
2. 捕获异常的关键技巧
设置函数断点:
RhThrowEx
作用:
- 所有托管异常都会经过这里
然后查看寄存器:
- x64:rcx
- Arm64:x0
即可拿到真实异常对象
3. 确保符号文件存在
AOT 发布会生成:
- 可执行文件
- 符号文件(.pdb / .dbg)
没有符号文件:几乎无法调试
日志与诊断能力(AOT可用手段)
虽然调试受限,但仍有一些可用手段:
1. EventPipe + 诊断工具
Native AOT 支持:
- dotnet-trace
- dotnet-counters
- dotnet-monitor
前提:启用配置
<EventSourceSupport>true</EventSourceSupport>
可用于性能、事件追踪。
2. CPU Profiling
可使用:
- PerfView
- Linux perf
3. 注意限制
目前 AOT 不支持:
- Heap 分析
- GC dump
这点在排查内存问题时要特别注意。
实战排查流程(推荐)
当你遇到 AOT 发布异常时,可以按这个流程走:
- 关闭 AOT 验证问题是否消失
- 查看 publish 日志 warning(非常关键)
- 检查反射/序列化代码
- 确认第三方库兼容性
- 开启日志(EventPipe / 自定义日志)
- 使用 native debugger + RhThrowEx 捕获异常
- 检查是否被 Trimming 误删
总结
Native AOT 带来了更快启动和更低资源占用,但同时也改变了 .NET 的运行模型。发布后异常通常不是偶发 Bug,而是以下几类问题:
- 裁剪导致代码缺失
- 反射/动态能力失效
- 库不兼容
- 调试方式变化
掌握正确的排查思路,比盲目调试更重要。