一次 Windows 内核卡住的调试经历
今年整了个电脑,本想提升生活游戏体验,却被一个及其影响体验的问题折磨了三个月,后来终于算是得到了解决(规避)。过程是曲折的,结果是比较抽象的。于是乎记录一下这个问题的前因后果,毕竟也算是不影响体验了,不算白费力气。
语音聊天,神秘卡死
某知名聊天软件,经常是大家水群的好去处,有时,大家也会通过群聊语音开黑。但是我却发现一个神秘现象——系统卡死。毫无征兆,随时触发,短则 2 秒,长则 2 分钟,通常 20 秒多一点。
这里说一下背景:上网是 Intel 的 AX210 无线网卡,同时显卡是 AMD 的 9070GRE,二者都是走的 PCIe,其他就不说了,因为问题围绕二者展开。
这个卡死呢也是很奇怪,如果 WiFi 连的是 5 GHz 的网,卡死的概率大一些,卡死的时间长一些 (基本都是很长的那种),如果是 2.4 GHz 的网,卡死概率就会低,而且通常都是卡 2 秒左右。所以怀疑到网卡上去了,毕竟 AX 系列网卡也是臭名昭著了,或者说,整个 Intel 的网卡都有口皆碑。尤其是还在系统日志看到大量报错,很难不让人怀疑,是吧。

虽然还有一个诡异的现象:只有使用某软件进入群聊语音,且群聊语音窗口界面在前台,即没有最小化等情况。其他语音软件倒是没遇到这个问题。但首先主要怀疑到网卡上了,还跑去 Intel 的官方论坛发帖,得到的答复主要也是更新系统、更新驱动、重置网卡等等。我还找了客服反馈,然后说改电源模式到高性能模式(这个记住,后面要考),不允许系统自动关闭网卡等,有一定缓解,但没有本质的结果。当然这个事情我也求助了 D 老师(大名鼎鼎的 Deep Seek),不过也没什么有效的结果(虽然 D 老师后面立大功)。
眼看这个问题似乎无解,就这么一直卡着(我是到了 8 月中旬才开始积极排查想要解决的,毕竟也不是不能规避,就是把窗口最小化,但这样不是个事,尤其是屏幕共享还不能给最小化了,然后就非常容易卡死)。后面甚至怀疑到某软件的 bug 去了,毕竟该软件所属公司的情况也是人尽皆知。
暂时搁置,记录现象
但是由于短时间没什么进展,我就想能不能自动的记录这个,避免人眼盯着看的麻烦。
直觉告诉我,这样的卡死,一定是某个驱动导致了内核阻塞,那么既然是内核阻塞,大概率会导致线程无法调度。所以我就想到写一个小工具,在线程里面检测 Sleep 前后两次 Tick 的差值,就可以知道是不是卡住了。判断阈值是 100ms,Sleep 时间是 40ms,排除偶尔的高负载导致卡顿,基本不太可能会出现 Tick 差值大于 100ms 的情况。
事实也是如此。

这是某一次记录到的情况,是不是非常夸张,短短 15 分钟出现了那么多次。(这里 125 的那次看似误判,实则不然,因为后来问题解决后我长时间运行从来没有遇到过类似误判,说明就是极其短暂的卡顿 )
陷入僵局,无从下手
事实上,这个检测工具是从我开始着手解决问题的时候写的,而半个多月了几乎没有进展。难道这个问题就无法解决了吗,难道我就要被这个问题一直困扰了吗?也许哪一天,系统的一次更新、驱动的一次更新、软件的一次更新,这个问题就自己消失了。
但我不知道那一天会是什么时候。某一天,一次偶然的 BSOD(大家常说的蓝屏,但是现在的 Windows 11 没有“蓝”屏了)打破了一切。我熟练的使用 WinDbg 打开了 Minidump 文件,找到了调用栈,根据 D 老师的指引,确定是一个死锁问题。
# Child-SP RetAddr Call Site
00 ffffb100`d8033c28 fffff807`7abe4866 nt!KeBugCheckEx
01 ffffb100`d8033c30 fffff807`7abe3d9a nt!KeAccumulateTicks+0x596
02 ffffb100`d8033ca0 fffff807`7ab55361 nt!KiUpdateRunTime+0xca
03 ffffb100`d8033e60 fffff807`7ab55cbb nt!KeClockInterruptNotify+0x431
04 ffffb100`d8033f50 fffff807`7aea67be nt!KiCallInterruptServiceRoutine+0x31b
05 ffffb100`d8033fb0 fffff807`7aea6fcc nt!KiInterruptSubDispatchNoLockNoEtw+0x4e
06 ffff8385`1770f190 fffff807`7aa585ff nt!KiInterruptDispatchNoLockNoEtw+0x3c
07 ffff8385`1770f320 fffff807`7abe290f nt!KxWaitForLockOwnerShip+0x5f
08 ffff8385`1770f3a0 fffff807`7aa4940d nt!KiAcquireThreadStateLockForWrite+0x14f
09 ffff8385`1770f410 fffff807`7aa4913a nt!KiSetPriorityThread+0x5d
0a ffff8385`1770f4c0 fffff807`7aa4a62d nt!KiAbCpuBoostOwners+0x2da
0b ffff8385`1770f570 fffff807`7ab31de2 nt!KiAbProcessThreadLocks+0x67d
0c ffff8385`1770f660 fffff807`7ab322e5 nt!KiAbPropagateBoosts+0x72
0d ffff8385`1770f6a0 fffff807`7aa0bee6 nt!KiExecuteAllDpcs+0x4e5
0e ffff8385`1770f8f0 fffff807`7aea564e nt!KiRetireDpcList+0x326
0f ffff8385`1770fb80 00000000`00000000 nt!KiIdleLoop+0x9e
可惜当我想进一步追究的时候,我才发觉 Minidump 没有那么多的内存信息(后来我改成把活动内存 dump 下来了)。众所周知,系统卡死,除了系统本身的原因,大概率就是驱动导致的了。加上这个死锁现象,我怀疑卡死和蓝屏有关。当然只是怀疑,毕竟如果真是死锁,那应该经常蓝屏了,但是也可以基本确定是某个驱动由于 bug 导致执行某个任务耗时过长了。
此时我想到,既然蓝屏求之不得(从没想过会这么盼着蓝屏到来),不如主动出击,用 WinDbg 直接上机调试。说干就干。(顺便夸一下,微软商店那个新版的 WinDbg 比起祖传的那个是好看又好用,强烈推荐)
初现端倪,主动出击
我查了一下 WinDbg 调试物理机内核的方法,种种路径最终指向这一篇官方文档:设置内核模式调试 - Windows drivers | Microsoft Learn,官方推荐的是自动设置 KDNET 网络内核调试 - Windows drivers | Microsoft Learn,但是由于我用工具检测不支持,所以只好抱着试一试的心态,参考手动设置 KDNET 网络内核调试 - Windows drivers | Microsoft Learn,手动试了一番。虽然过程有点曲折,但还是搞定了。(如果用 kdnet 检测没问题,建议使用 kdnet 一键完成配置,比手动配置方便的多;kdnet 需要单独去 Windows SDK - Windows 应用开发 | Microsoft Developer 下载,然后安装时勾选 Debugging Tool for Windows 才可以,默认路径在 C:\Program Files (x86)\Windows Kits\10\Debuggers\x64)
首先准备一台主机和目标机,主机是运行 WinDbg 客户端的,目标机就是被调试的机器。使用以太网线将二者网口相连(可以通过交换机、路由器,也可以直连,简单点就直连)。
使用 ipconfig 命令确定主机的 IP 地址,以我的为例,是 169.254.131.234,使用目标机去 ping 这个 IP,确保能通。保险起见,也可以查询目标机的 IP 地址,用主机去 ping,确保双方通信正常。如果不通可以检查防火墙的入站规则 文件和打印机共享 (回显请求 - ICMPv4-In),确保已启用且允许连接,还不行再重启。全程开启 Wireshark 抓包也可以快速排查网络问题导致的调试器连接失败,大幅度提升体验。
之后使用如下命令去设置调试模式和调试用的网卡。
bcdedit /debug on
bcdedit /dbgsettings net hostip:169.254.131.234 port:50000
bcdedit /set "{dbgsettings}" busparams 94.0.0
- 第一行启用调试,需要确保 BIOS 的安全启动(Security Boot)是关掉的,否则是不能调试的
- 第二行设置调试模式,hostip 就是前面确定的主机 IP,port 通常是 50000
- 第三行设置 busparams,需要去设备管理器看看网卡对应的位置,比如我的是“PCI 总线 94、设备 0、功能 0”,那么就是
94.0.0
设置完后可以用 bcdedit /dbgsettings 查看设置结果,类似下面这样就说明 OK 了。
busparams 94.0.0
key 1.2.3.4
debugtype NET
hostip 169.254.131.234
port 50000
dhcp Yes
isolatedcontext Yes
之后为了避免一些问题,先拔掉网线,然后重启。重启之后再看网卡属性,可以看到“此设备已为 Windows 内核调试程序预留,以便在此启动会话持续期间使用。 (代码 53)”,说明已经启用了调试。为什么要拔掉网线呢,因为一旦网线接好了,重启的时候内核就会加载调试模式了,如果哪里出了问题,可能导致无法调试。
确认无误后,接上网线,再重启目标机,注意此时主机千万别启用调试,因为这个阶段可能会 BSOD 导致没法进系统(除非要调试有些驱动的初始化过程,但是我们肯定用不到)。这个时间会很久,开机个十分钟很正常,看似卡死实际上是正常的,耐心等待就行了。
在开机后,任务管理器还能看到多了一个内核调试专用的网卡。这个时候可以检查一下双机之间的网络通信情况,一般来说 IP 地址是不会变的,但是说不准,一定要确保主机的 IP 固定不变。
之后在主机使用 WinDbg 连接调试即可。

选择 Attach to kernel,只需要指定端口号和 Key 即可,剩下的会自动连接。
连接成功后,应该会打印日志,此时就可以随时按下快捷键中断内核了。有一个小细节,此时如果在目标机按下截图键,也会被中断(当然对我来说没什么用,毕竟我需要中断的时候系统已经卡死了)。
问题现场,中断分析
基于前面的经验,通常卡死时间会有 20 秒左右,所以当出现卡死时,心中默念几秒,发现还是卡住立刻按下快捷键中断内核。此时现场就在眼前,秘密即将被揭开。
然而不想的时候总是来,想要的时候等不到。许多事情总是这样,所以运气也是很重要的一部分。功夫不负有心人,在等了好久之后,终于遇到了一次标准的卡死。果断按下中断快捷键,内核很快就停了下来。
00 ffff9105`2784e238 fffff800`0a5414a9 nt!DbgBreakPointWithStatus
01 ffff9105`2784e240 fffff800`08700f52 kdnic!MPSendNetBufferLists+0x3e9
02 ffff9105`2784e330 fffff800`08700e1e ndis+0x40f52
03 ffff9105`2784e470 fffff800`086fc122 ndis+0x40e1e
04 ffff9105`2784e4b0 fffff800`087b1bb2 ndis+0x3c122
05 ffff9105`2784e5c0 fffff800`08ac474d ndis+0xf1bb2
06 ffff9105`2784e840 fffff800`08ac3a3f tcpip!FlpNdisSendNbls+0xbd
07 ffff9105`2784ea70 fffff800`08ac2d18 tcpip!FlpSendPacketsHelper+0xdf
08 ffff9105`2784eb30 fffff800`08ac2147 tcpip!IppFragmentPackets+0x368
09 ffff9105`2784ec80 fffff800`08ac0ce7 tcpip!IppDispatchSendPacketHelper+0x77
0a ffff9105`2784edc0 fffff800`08ae43bb tcpip!IppPacketizeDatagrams+0x2e7
0b ffff9105`2784efd0 fffff800`08b27b0d tcpip!IppSendDatagramsCommon+0x70b
0c ffff9105`2784f180 fffff800`08ab468a tcpip!IppSendDatagrams+0x25
0d ffff9105`2784f1c0 fffff800`08ab5611 tcpip!IppSendDirect+0xee
0e ffff9105`2784f300 fffff800`08b2a951 tcpip!Ipv6pSendNeighborSolicitation+0x141
0f ffff9105`2784f3c0 fffff800`08b26bbb tcpip!IppSendNeighborSolicitation+0xf9
10 ffff9105`2784f420 fffff800`08b25bee tcpip!IppNeighborSetTimeout+0x1eb
11 ffff9105`2784f580 fffff800`08b25369 tcpip!Ipv6pInterfaceSetTimeout+0x8e
12 ffff9105`2784f5d0 fffff800`08b247e0 tcpip!IppCompartmentSetTimeout+0x129
13 ffff9105`2784f630 fffff800`752eed92 tcpip!IppTimeout+0xb0
14 ffff9105`2784f680 fffff800`752ef998 nt!KiProcessExpiredTimerList+0x502
15 ffff9105`2784f7b0 fffff800`75319f36 nt!KiTimerExpiration+0x5d8
16 ffff9105`2784f8f0 fffff800`756a09fe nt!KiRetireDpcList+0xc46
17 ffff9105`2784fb80 00000000`00000000 nt!KiIdleLoop+0x9e
这是中断现场的栈,很明显,这是因为我们使用调试器下的中断,所以应该使用 !running -it 来查看其他 CPU 核执行的情况。(执行命令的时候可能会很慢,此时左下角可以看到在从微软的服务器下载符号表,耐心等待吧)
果然,发现一个可疑的栈:
5 ffffb6815e4d7180 ffff808deba73180 (16) ffff808da65bb280 ................
Unable to load image \SystemRoot\System32\DriverStore\FileRepository\u0417878.inf_amd64_cf56f0cbce08e931\B417693\amdkmdag.sys, Win32 error 0n2
# Child-SP RetAddr Call Site
00 ffff9105`28d05e78 fffff800`1f1b5247 amdkmdag+0x1987b14
01 ffff9105`28d05e80 fffff800`1f63974f amdkmdag+0x14f5247
02 ffff9105`28d05ef0 fffff800`1f656d74 amdkmdag+0x197974f
03 ffff9105`28d06310 fffff800`1e22cb2a amdkmdag+0x1996d74
04 ffff9105`28d069a0 fffff800`1e25ef79 amdkmdag+0x56cb2a
05 ffff9105`28d069f0 fffff800`1e24310b amdkmdag+0x59ef79
06 ffff9105`28d06a40 fffff800`1e25ddea amdkmdag+0x58310b
07 ffff9105`28d06ab0 fffff800`1e1a8282 amdkmdag+0x59ddea
08 ffff9105`28d06ae0 fffff800`1de88cd5 amdkmdag+0x4e8282
09 ffff9105`28d06b30 fffff800`1dd00b01 amdkmdag+0x1c8cd5
0a ffff9105`28d06be0 fffff800`1deb1edc amdkmdag+0x40b01
0b ffff9105`28d06da0 fffff800`070c28b6 amdkmdag+0x1f1edc
0c ffff9105`28d06f30 fffff800`0ad7a02d dxgkrnl!ADAPTER_DISPLAY_DdiSetVidPnSourceAddressWithMultiPlaneOverlay3+0x116
0d ffff9105`28d07020 fffff800`0adee705 dxgmms2!VidSchiExecuteMmIoFlipAtPassiveLevel+0x13d
0e ffff9105`28d07aa0 fffff800`0ade5361 dxgmms2!VidSchiRun_PriorityTable+0x205
0f ffff9105`28d07af0 fffff800`75487c2a dxgmms2!VidSchiWorkerThread+0xe1
10 ffff9105`28d07b30 fffff800`756a0b24 nt!PspSystemThreadStartup+0x5a
11 ffff9105`28d07b80 00000000`00000000 nt!KiStartSystemThread+0x34
看起来就是从 Windows 的 DX 层一路调用到了 AMD 的显卡驱动。尽管没有符号,但基本可以看出来是在驱动里面卡住了。这里其实有一个很细节的点,也是后来 D 老师提醒的时候我才发现的,就是 0c 这一层的栈,函数名叫 ADAPTER_DISPLAY_DdiSetVidPnSourceAddressWithMultiPlaneOverlay3,后面的 MultiPlaneOverlay 就是所谓的 MPO,而网上一搜就可以看到多个显卡驱动和 MPO 之间的兼容性问题导致的卡死问题,不过现象不太一样,但本质还是这个。只是没想到按照网上的说法这个 bug 应该已经修了,但实际上看起来并没有完全解决。后面 AMD 显卡驱动层的具体原因就没法看了,但是应该不是死锁,因为总是能恢复的,只是从几秒到几十秒不等,不知道是什么逻辑耗时很长。
注册表里加一个
Windows Registry Editor Version 5.00[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Dwm]
"OverlayTestMode"=dword:00000005
禁用 MPO 来规避这个问题。不过这里我还怀疑是不是背后还有其他原因,毕竟只有使用某软件才会出现,不使用就不会有问题,是不是某软件触发了什么奇怪的东西。但是分析这个软件比较棘手,而且可能会偏离方向(毕竟都是调用系统接口,不会直接操作网卡、显卡的),就没有继续。
本来以为到这一步,问题总算是解决了。但是不料现实狠狠打脸,刚改完重启,想着验证一下结果,马上就给我卡了几次,不过都是很轻微的,只有几百毫秒,而且也没有出现长时间的卡顿。问题现象的不同,也说明是有效果的,但是还有地方没有完全解决。
双管齐下,烟消云散
这里还有一个细节,无线网卡和显卡都是接在 PCIe 上面的,而二者通常是同时工作的,会不会互相影响了呢?这可能是一个可以排查的点。
首先,按照 D 老师指引,分析 IRQ 是否冲突,这个一看就并没有。而第二点,PCIe 电源管理看着就很有可能了。打开控制面板,依次 系统和安全——电源选项——更改计划设置——更改高级电源设置——PCI Express——链接状态电源管理,可以看到默认是最大电源节省量,把它改成关闭(高性能模式是会关掉的),保险起见在网卡驱动设置里面也把允许计算机关闭此设备以节约电源给关掉。
之后,重启电脑,接着验证。这次奇迹发生了,经过长时间的验证,确实没有出现明显卡顿,工具也检测不到了。好几天过后,依然没有问题,说明基本已经解决了。然后鸽了俩月,还是确认没有问题。
现象是这样,原因就不得而知了,首先 A 卡驱动和 MPO 肯定是有兼容性问题的,然后是 Windows 的 PCIe 电源策略可能有问题,至于是和 A 卡还是和 Intel 的 AX 网卡哪个有问题,就不知道了。
好了好了,也不细究,规避掉没问题了就行,牛马打工够累了,想玩个游戏电脑还来这一出,😫