我正在尝试使用
-mk
(版本 11)的
procdump
选项来获取 64 位 Windows 进程的堆栈跟踪的内核部分。内核转储已创建,但在 WinDbg 中打开它,我得到的只是单个堆栈帧 (DbgkpLkmdSnapThreadInContext
),而不是完整的堆栈跟踪:
0: kd> k20
# Child-SP RetAddr Call Site
00 fffff50e`7b54df30 00000000`00000000 nt!DbgkpLkmdSnapThreadInContext+0x9d
这是在 Windows 11 22H2 (22621.4169) 上完成的,但我在 Windows 10 上也得到了相同的结果。 我做错了什么吗?还是 procdump 的
-mk
选项坏了?
更多详情: 例如,我从 Windows 终端调用 procdump(以管理员权限运行)来创建完全相同的 Windows 终端进程 (pid 46304) 的转储:
PS C:\dev\Tools> .\procdump.exe -mk -ma 46304 "C:\dev\test\dump.dmp"
ProcDump v11.0 - Sysinternals process dump utility
Copyright (C) 2009-2022 Mark Russinovich and Andrew Richards
Sysinternals - www.sysinternals.com
[08:23:43] Dump 1 initiated: C:\dev\test\dump.dmp
[08:23:43] Dump 1 writing: Estimated dump file size is 535 MB.
[08:23:44] Dump 1 complete: 536 MB written in 1.5 seconds
[08:23:44] Dump 1 kernel: C:\dev\test\dump.Kernel.dmp
[08:23:44] Dump count reached.
用户模式转储 (
dump.dmp
) 显示该进程位于系统调用内部:
0:000> k9
# Child-SP RetAddr Call Site
00 00000048`c24ff558 00007ffd`199b53fa win32u!NtUserGetMessage+0x14
01 00000048`c24ff560 00007ff6`f0b840b5 user32!GetMessageW+0x2a
02 (Inline Function) --------`-------- WindowsTerminal!WindowEmperor::WaitForWindows+0x61 [C:\__w\1\s\src\cascadia\WindowsTerminal\WindowEmperor.cpp @ 125]
03 00000048`c24ff5c0 00007ff6`f0b8e2a6 WindowsTerminal!WindowEmperor::HandleCommandlineArgs+0x535 [C:\__w\1\s\src\cascadia\WindowsTerminal\WindowEmperor.cpp @ 102]
04 00000048`c24ff8a0 00007ff6`f0b927f2 WindowsTerminal!wWinMain+0x166 [C:\__w\1\s\src\cascadia\WindowsTerminal\main.cpp @ 118]
05 (Inline Function) --------`-------- WindowsTerminal!invoke_main+0x21 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 118]
06 00000048`c24ff980 00007ffd`19e7257d WindowsTerminal!__scrt_common_main_seh+0x106 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
07 00000048`c24ff9c0 00007ffd`1bc2af28 kernel32!BaseThreadInitThunk+0x1d
08 00000048`c24ff9f0 00000000`00000000 ntdll!RtlUserThreadStart+0x28
RIP 指向
00007ffd`198d1534
:
win32u!NtUserGetMessage:
00007ffd`198d1520 4c8bd1 mov r10, rcx
00007ffd`198d1523 b804100000 mov eax, 1004h
00007ffd`198d1528 f604250803fe7f01 test byte ptr [7FFE0308h], 1
00007ffd`198d1530 7503 jne win32u!NtUserGetMessage+0x15 (7ffd198d1535)
00007ffd`198d1532 0f05 syscall
00007ffd`198d1534 c3 ret
所以我确实希望得到一些内核框架。 但是打开由 procdump 创建的内核转储 (
dump.Kernel.dmp
),我看到的只是 DbgkpLkmdSnapThreadInContext
堆栈帧,即使在显式切换到我的进程之后也是如此。另一方面,当收集实时内核转储时,我确实看到堆栈确实在继续:
0: kd> k20
# Child-SP RetAddr Call Site
00 fffff50e`7bb86ba0 fffff803`48e5ffa5 nt!KiSwapContext+0x76
01 fffff50e`7bb86ce0 fffff803`48e613c7 nt!KiSwapThread+0xaa5
02 fffff50e`7bb86e30 fffff803`48f40176 nt!KiCommitThreadWait+0x137
03 fffff50e`7bb86ee0 fffff803`48f51c93 nt!KeWaitForSingleObject+0x256
04 fffff50e`7bb87280 ffff8968`68f4d670 nt!KeWaitForMultipleObjects+0x5d3
05 fffff50e`7bb874e0 ffff8968`68f4d2af win32kfull!xxxRealSleepThread+0x310
06 fffff50e`7bb87600 ffff8968`68f508ab win32kfull!xxxSleepThread2+0xaf
07 fffff50e`7bb87650 ffff8968`68f4dadc win32kfull!xxxRealInternalGetMessage+0x15bb
08 fffff50e`7bb87950 ffff8968`687d6ed2 win32kfull!NtUserGetMessage+0x8c
09 fffff50e`7bb879e0 fffff803`4902b605 win32k!NtUserGetMessage+0x16
0a fffff50e`7bb87a20 00007ffd`198d1534 nt!KiSystemServiceCopyEnd+0x25
0b 00000048`c30ff8e8 00007ffd`199b53fa win32u!NtUserGetMessage+0x14
0c 00000048`c30ff8f0 00000000`00000000 user32!GetMessageW+0x2a
但是procdump似乎无法捕获它。为什么?
上下文:我们已经在测试系统中使用 procdump 来自动创建挂起/崩溃进程的转储。我尝试扩展它以创建相关进程的内核转储(当然,仅堆栈跟踪)。
TL;DR: 如果处理器中的逻辑核心过多(任何高于或接近的逻辑核心数),“内核小型转储”又名“内核分类转储”在当前 Windows 版本(至少 Windows 11 22H2)上会被破坏20 是危险的)。原因:转储限制为 1MB,并且包含每个核心的信息。如果核心太多,仅使用核心信息就达到 1MB 限制。
经过相当多的挖掘(通过 ProcMon 和反汇编程序),我想我知道发生了什么:首先,
procdump -mk
正在使用 WinAPI MiniDumpWriteDump() 的 WriteKernelMinidumpCallback 功能来创建“内核迷你转储” ”.
该 API 在内部使用未记录的系统调用
NtSystemDebugControl()
。除此之外,这个系统调用实现了“内核迷你转储”,在该上下文中称为“内核分类转储”。人们可以在线找到有关该系统调用的一些信息,例如这里、这里或这里。 SYSDBG_COMMAND
的NtSystemDebugControl()
参数(第一个参数)需要设置为SysDbgGetTriageDump = 29
。
此外,还需要传入一个充满数据的缓冲区以及缓冲区的大小。支持的最大缓冲区大小为 1MB。尝试传递更大的大小仍会产生最多 1MB 的转储。反汇编NtSystemDebugControl()
中的ntoskrnl.exe
时可以看到这一点(我使用的版本是10.0.19041.5007,Windows 10 22H2),其中包含诸如之类的代码
usedBufferSize = 0x100000;
if (givenBufferSize <= 0x100000)
usedBufferSize = givenBufferSize;
其中
givenBufferSize
是在调用 NtSystemDebugControl()
时指定的缓冲区大小。
然后代码调用内部函数
DbgkCaptureLiveDump()
(以创建内核分类转储),其中包含对 DbgkpLkmdSnapGlobals()
的调用。该函数基本上获取处理器信息(通过 KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS)
、KeGetPrcb()
和 KeEnumerateProcessorDpcs()
)。就我而言,我有 16 个具有 SMT 的真实核心,即 32 个逻辑核心。因此,KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS)
返回32。代码循环遍历每个“逻辑处理器”(意味着每个逻辑核心)并获取其“内核处理器控制块”(KPRCB)。然后,它显然将每一个写入分类转储缓冲区。事实是,KPRCB 结构非常庞大,并且从 Windows 版本到版本不断增长。在 Windows 11 23H2 上,它的大小似乎为 0xBF00。就我而言(Windows 10 22H2),我可以从反汇编中看到它的大小为 0xAF00 字节。
给定 1MB 缓冲区,我们可以将 23 KPRCB 放入该缓冲区。因此,如果逻辑核心超过 23 个,则整个缓冲区将仅填充处理器信息,而不会为实际感兴趣的数据(调用堆栈等)留下空间。
在更高版本的 Windows 上,情况更糟,因为 KPRCB 结构的大小甚至更大(Windows 11 23H2:0xBF00,意味着 21 个核心已填充转储)。
确实,我使用了虚拟机(Windows 10 22H2)并改变了虚拟机可用的核心数量。对于 =22 个核心,WinDbg 无法做到这一点。该门槛非常接近 23 个 KPRCB。
此外,在 32 个核心上使用转储时,WinDbg 命令 !prcb 0x15
<=21 cores, WinDbg is able to display proper call stacks from the kernel triage dump. But with > (0x15=21) 显示信息,而
!prcb 0x16
(0x16=22) 失败。因此,由于大小限制为 1MB,内核分类转储在具有多个内核的现代 CPU 上很容易被破坏(或至少无用)。
如果有人想直接玩
NtSystemDebugControl()
:
github.com/Sedeniono/livedump.