线上有些问题很像“应用突然消失”:访问不到页面,应用日志里没有明显异常,服务却不断重启进程。遇到这种情况,Windows Event Log 往往比应用日志更早给出方向。
本文记录一次 .NET 应用 Stack Overflow 的排查过程。重点不在某个具体插件,而在于如何从一条异常码逐步缩小到具体的方法。
先从异常码确认方向
问题最初表现为应用无法访问,同时能看到大量应用异常退出,以及保活机制不断重启应用的日志。
Event Log 中有一条关键记录:
Exception code: 0xc00000fd
在 Windows 中,0xc00000fd 通常对应栈溢出(STATUS_STACK_OVERFLOW)。
先拿到 dump,不要急着下结论
在 .NET 5 及更高版本中,可以参考 .NET 的 Stack Overflow 调试文档,让进程生成 dump。现场排查时,可以先让管理员权限启动相关服务,然后收集控制台输出;如果仍然需要更完整的现场信息,可以配置生成 mini dump。
拿到 dump 后使用 dotnet-dump 加载:
dotnet-dump analyze .\dump.6012.dmp
一个容易踩到的坑是:加载完成后直接执行 crashinfo、printexception 或 clrstack,可能看不到有价值的信息:
> crashinfo
ERROR: No crash info to display
> pe
There is no current managed exception on this thread
这并不表示 dump 没有问题。Stack Overflow 发生时,运行时可能来不及建立普通异常上下文;排查重点应该转向线程状态和栈帧本身。
从线程列表找到异常线程
先查看托管线程:
> clrthreads
DBG ID OSID Count Apt Exception
0 1 2604 -00001 MTA
3 2 17ec -00001 MTA (Finalizer)
4 5 1638 -00001 MTA (Threadpool Worker)
11 83 2c4c -00001 MTA (Threadpool Worker) System.StackOverflowException
这里的关键信息是线程 11:它被标记为 Threadpool Worker,并且关联了 System.StackOverflowException。切换到这个线程后,再查看它的调用栈:
> threads 11
> clrstack
OS Thread Id: 0x2c4c (11)
00000070084C5FF0 EhrUserManager.dll!Unknown
00000070084C5EE0 EhrUserManager.dll!Unknown
00000070084C5F20 EhrUserManager.dll!Unknown
00000070084C60F0 EhrUserManager.dll!Unknown
00000070084C61F0 EhrUserManager.dll!Unknown
00000070084C62F0 EhrUserManager.dll!Unknown
大量重复的同一模块栈帧,是无限递归的重要信号。不过此时方法名还是 Unknown,还不能直接判断是哪一行代码。
没有 PDB 时补齐符号
dump 里显示 Unknown,通常是因为缺少对应程序集的 PDB 符号文件。对于用户提供的插件,可以使用 ILSpy 反编译程序集并生成 PDB,然后把符号目录配置给 dotnet-dump:
> setsymbolserver -directory C:\symbols\EhrUserManager
> loadsymbols
系统符号可以从 Microsoft Symbol Server 下载;自有插件则需要把生成的 PDB 放在对应目录,并确保 PDB 与程序集版本匹配。符号加载完成后再次查看调用栈:
> clrstack
00000070084C5FF0 CIMD.ScanStep.Get(CIMD.ScanContext, System.String)
00000070084C60F0 CIMD.ScanStep.Get(CIMD.ScanContext, System.String)
00000070084C61F0 CIMD.ScanStep.Get(CIMD.ScanContext, System.String)
00000070084C62F0 CIMD.ScanStep.Get(CIMD.ScanContext, System.String)
这次可以看到 CIMD.ScanStep.Get 在栈上不断重复,基本可以确认该方法在某种输入下递归调用自己,且递归没有终止条件。
最终定位与经验
拿到插件源码后,代码中确实存在方法调用自身的路径。下一步是让插件作者在递归入口、关键参数和退出条件处增加日志,确认什么输入触发了不终止的递归。
这次问题的排查链路可以概括为:
- 用 Event Log 的
0xc00000fd判断栈溢出方向。 - 生成并收集 dump。
- 不依赖普通异常上下文,先用
clrthreads找异常线程。 - 切换线程查看
clrstack,通过重复栈帧识别递归。 - 缺少符号时补齐 PDB,再把
Unknown映射到具体方法。 - 回到源码和输入数据,验证递归为何没有终止。
Stack Overflow 的重点往往不是“异常对象里有什么”,而是“哪个线程的栈正在重复增长”。当常规日志没有答案时,线程、调用栈和符号文件就是最可靠的线索。