PAYATHON 2026

.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 查看已加载模块。

重点检查:

  1. 异常码是否与本次故障一致。
  2. 堆检查或进程终止函数之前有哪些调用。
  3. 多份转储是否反复出现相同的组件和调用路径。
  4. 相关模块的实际版本与加载路径是否符合预期。

如果栈顶停在 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 也不能保证处理堆损坏。此时进程内存状态已经不可信,需要捕获原生崩溃现场来定位问题。

提交问题报告时,附上最小复现项目、完整版本信息、启动对照结果、崩溃调用栈和可供分析的转储。完整转储可能包含进程内的敏感数据,分享前需要检查。

备注:内容仅供参考。