.NET MAUI Blazor Hybrid and Web App 随机报 0xc0000374,如何排查?
堆损坏:需要捕获原生崩溃现场
3221226356 转为十六进制是 0xC0000374,对应 Windows 状态码 STATUS_HEAP_CORRUPTION,表示进程检测到了堆损坏。
现有日志可以确认崩溃类型,还无法确定哪个组件破坏了内存。事件日志中的 ntdll.dll 通常是堆管理代码发现损坏并终止进程的位置,仅凭这一点不能认定 ntdll.dll 本身有问题。
对于尚未修改代码的模板项目,建议先保留当前环境,捕获崩溃转储并检查原生调用栈,再用对照测试缩小范围。这份输出还不足以判断升级哪个包能解决问题。
为什么调试输出看不出原因?
日志中的以下内容都是常见的调试信息:
Skipped loading symbols:调试器没有为相应模块加载符号。Just My Code:调试器启用了“仅我的代码”设置。.NET TP Worker ... exited with code 0:线程正常退出。Anonymously Hosted DynamicMethods Assembly:运行时加载了动态生成的方法所在程序集。
这些信息本身不能解释堆损坏。System.Linq.Expressions.dll 是最后一条加载记录,也不能证明崩溃由它引起。
堆损坏可能发生在较早的一次内存写入中,直到后续分配或释放内存时才被检测到。因此,应用可能在启动时退出,也可能正常显示界面后再崩溃。即使崩溃时间不固定,仍然可以追查原因。
即使业务代码完全没有修改,Windows 版 MAUI Blazor Hybrid 也会运行 WinUI、Windows App SDK、WebView2 及相关互操作代码。要确定应检查哪个组件,需要查看调用栈和模块信息。
第一步:记录环境,做启动方式对照
先在解决方案目录执行以下命令,保存输出:
dotnet --info
dotnet --list-sdks
dotnet --list-runtimes
dotnet workload list
查看 MAUI 项目的直接依赖和传递依赖:
dotnet list .\Calendar\Calendar.csproj package --include-transitive
项目路径按实际目录调整。同时记录 Visual Studio 完整版本、Windows 版本、WebView2 Runtime 版本,以及是否存在 global.json。
日志中的 Microsoft.NETCore.App\9.0.4 只说明本次进程加载的 .NET Runtime 版本,无法据此确定 SDK、MAUI 工作负载或 Windows App SDK 的版本。
保持代码和依赖不变,分别测试以下启动方式:
| 测试方式 | 主要观察点 |
|---|---|
| F5 启动 | 当前调试场景是否复现 |
| 关闭 Hot Reload 后 F5 启动 | 热重载相关行为是否影响复现 |
| Ctrl+F5 启动 | 脱离调试器后是否仍然崩溃 |
| 单独运行 Web 项目 | 故障是否主要集中在 Windows 原生宿主路径 |
每种方式都要多次启动,并让应用运行一段时间,因为已有启动后才崩溃的记录。记录启动次数、崩溃次数和退出码,偶尔成功一次还不足以说明问题已解决。
如果关闭 Hot Reload 后暂时不再崩溃,可以先以这种方式开发,但仍需继续验证。调试器和热重载都会影响运行时序,测试结果的变化只能作为排查线索。
第二步:捕获完整崩溃转储
事件日志已经列出了 WER 转储路径,可以先检查:
C:\ProgramData\Microsoft\Windows\WER\Temp\
C:\ProgramData\Microsoft\Windows\WER\ReportArchive\
如果对应的 .dmp 还在,先复制保存。临时文件可能被清理,已有转储也未必包含足够的堆内存。
也可以用 Sysinternals ProcDump 等待应用启动,在发生未处理异常时捕获转储。先下载 ProcDump,再在 Windows 上执行:
New-Item -ItemType Directory -Path C:\Dumps -Force
.\procdump64.exe -ma -e -w Calendar.exe C:\Dumps
参数含义:
-ma:生成完整用户模式转储。-e:在未处理异常时生成转储。-w:等待指定进程启动。
保持该终端运行,再用 Ctrl+F5 启动应用。如果本次没有崩溃,就继续尝试复现,直到捕获转储。ProcDump 退出后,需要重新执行命令,才能监视下一次启动。
捕获期间不要同时用 Visual Studio 调试同一个进程。如果故障只在 F5 下出现,应改用 Visual Studio 的原生或混合调试,在异常中断时保存包含堆的转储。
保存转储时,一并保留该次构建的 EXE、DLL 和 PDB,以免后续重新构建后符号不匹配。
第三步:检查原生调用栈
使用 WinDbg 打开 .dmp,执行以下命令加载符号并检查异常:
.symfix
.reload
!analyze -v
.exr -1
.ecxr
kv
lm
!analyze -v 用于初步分析异常,.exr -1 查看异常记录,.ecxr 切换到异常上下文,kv 显示调用栈,lm 查看已加载模块。
重点检查:
- 异常码是否与本次故障一致。
- 堆检查或进程终止函数之前有哪些调用。
- 多份转储是否反复出现相同的组件和调用路径。
- 相关模块的实际版本与加载路径是否符合预期。
如果栈顶停在 ntdll!RtlReportCriticalFailure 等位置,通常看到的仍是发现损坏时的现场。某个组件出现在调用栈中,也不足以证明它写坏了内存,需要结合多份转储和后续验证来判断。
第四步:必要时用 Page Heap 提前发现错误
如果普通转储只能看到堆已经损坏,可以使用 Windows Debugging Tools 中的 GFlags,为目标进程启用完整 Page Heap。
在管理员终端执行:
gflags /p /enable Calendar.exe /full
关闭已有的应用进程后重新启动,最好同时使用支持原生调试的调试器。
完整 Page Heap 会改变堆分配方式,并在部分内存边界设置保护页,让某些越界访问在更接近实际出错的位置被发现。启用后,可能先出现访问冲突或验证中断,异常码不一定仍是 0xC0000374。
排查完成后关闭:
gflags /p /disable Calendar.exe
该设置按可执行文件名应用,并会持续生效。启用期间,内存占用和启动时间可能明显增加,测试结束后应恢复设置。
如何根据证据选择修复方案?
根据转储和对照测试结果选择修改方向:
| 观察结果 | 后续处理 |
|---|---|
| 仅调试时复现 | 检查 Hot Reload、调试设置及对应工具版本 |
| 脱离调试器仍然复现 | 继续检查应用原生依赖、运行环境和转储调用栈 |
| 多份转储稳定指向同一组件 | 使用准确版本和栈信息查找对应问题或提交报告 |
| 同一构建仅在一台机器复现 | 对照系统版本、运行时及加载模块差异 |
| Page Heap 捕获到更早的非法访问 | 优先分析该次中断的位置和调用栈 |
更新前先保存当前能够复现问题的环境和转储,再到独立分支或测试环境中,验证相互兼容的 Visual Studio、.NET SDK、MAUI 工作负载及相关依赖版本。每轮测试都记录具体的版本差异和结果,以免更新后失去判断依据。
这份日志不足以支持直接替换 ntdll.dll,在 C# 启动代码外加一层 try/catch 也不能保证处理堆损坏。此时进程内存状态已经不可信,需要捕获原生崩溃现场来定位问题。
提交问题报告时,附上最小复现项目、完整版本信息、启动对照结果、崩溃调用栈和可供分析的转储。完整转储可能包含进程内的敏感数据,分享前需要检查。
备注:内容仅供参考。
