- A+
一:背景
1. 讲故事
在这么多的案例分析中,往往会发现一些案例是卡死在线程的内核态栈上,但拿过来的dump都是用户态模式下,所以无法看到内核态栈,这就比较麻烦,需要让朋友通过其他方式生成一个蓝屏的dump,这里我们简单汇总下。
二:如何生成内核态dump
1. 案例代码
为了方便演示,来一段简单的测试代码,目的就是观察 Console.ReadLine
方法的内核态栈。
internal class Program { static void Main(string[] args) { Console.WriteLine("hello world!"); Console.ReadLine(); } }
通过 任务管理器
或者 Process Explorer
默认抓取的dump都是 ntdll 之上的空间,可以用 k
来看一下。
0:000> k 3 # Child-SP RetAddr Call Site 00 000000d6`7c9fe328 00007ffe`61405593 ntdll!NtReadFile+0x14 01 000000d6`7c9fe330 00007ffd`50724782 KERNELBASE!ReadFile+0x73 02 000000d6`7c9fe3b0 00007ffe`215bc742 0x00007ffd`50724782
问题来了,如果我要看下 ntdll!NtReadFile
函数对应在内核态中的 nt!NtReadFile
方法怎么办呢?只能抓内核态dump,抓内核态dump的方式有很多,这里聊一下其中的两种方式。
2. 使用 notmyfault 抓取
说到 蓝屏
我相信有很多朋友都知道,简而言之就是内核态代码出bug导致系统崩溃,也有朋友知道通过增加一些配置可以在蓝屏
的时候自动生成 dump 文件,这种 dump 文件就属于内核态,配置如下:
但这里有一个问题,操作系统不可能无缘无故
的蓝屏,那怎么办呢?微软想了一个办法,人为的造蓝屏,所以提供了一个叫 notmyfault.exe
的工具, MSDN网址:https://learn.microsoft.com/en-us/sysinternals/downloads/notmyfault
有了这些前置基础,接下来就可以操练一下,双击 notmyfault.exe 工具,崩溃原因选择默认的 High IRQL fault
,最后点击 Crash
按钮,稍等片刻电脑就会蓝屏。截图如下:
我这里用的是一台物理的 迷你主机
测试,再次远程连接后,在 C:Windows
下会生成一个 MEMORY.dmp
文件,截图如下:
拿到 dump 之后就可以用 windbg 中的 !process
之类的命令分析了,非常爽。
1: kd> !process 0 2 ConsoleApp1.exe PROCESS ffffdb05c1641080 SessionId: 1 Cid: 1bc8 Peb: fd877dd000 ParentCid: 15ec DirBase: 1b9ef3000 ObjectTable: ffffa105fc3d5280 HandleCount: 161. Image: ConsoleApp1.exe THREAD ffffdb05bf3c7080 Cid 1bc8.0924 Teb: 000000fd877de000 Win32Thread: ffffdb05c00d0ad0 WAIT: (Executive) KernelMode Alertable ffffdb05c1902ef8 NotificationEvent THREAD ffffdb05c0fc6080 Cid 1bc8.07c8 Teb: 000000fd877e4000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable ffffdb05be642ae0 NotificationEvent THREAD ffffdb05be694080 Cid 1bc8.17dc Teb: 000000fd877e6000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable ffffdb05be645860 SynchronizationEvent ffffdb05be646e60 SynchronizationEvent ffffdb05be645d60 SynchronizationEvent THREAD ffffdb05be7e2080 Cid 1bc8.1020 Teb: 000000fd877e8000 Win32Thread: 0000000000000000 WAIT: (UserRequest) UserMode Non-Alertable ffffdb05b68b53a0 NotificationEvent ffffdb05be651de0 SynchronizationEvent 1: kd> .thread ffffdb05bf3c7080 Implicit thread is now ffffdb05`bf3c7080 1: kd> k *** Stack trace for last set context - .thread/.cxr resets it # Child-SP RetAddr Call Site 00 fffff50f`606ed570 fffff800`52c1c9c0 nt!KiSwapContext+0x76 01 fffff50f`606ed6b0 fffff800`52c1beef nt!KiSwapThread+0x500 02 fffff50f`606ed760 fffff800`52c1b793 nt!KiCommitThreadWait+0x14f 03 fffff50f`606ed800 fffff800`52df04c4 nt!KeWaitForSingleObject+0x233 04 fffff50f`606ed8f0 fffff800`53010cdb nt!IopWaitForSynchronousIoEvent+0x50 05 fffff50f`606ed930 fffff800`52fcc9e8 nt!IopSynchronousServiceTail+0x50b 06 fffff50f`606ed9d0 fffff800`52ff9ae8 nt!IopReadFile+0x7cc 07 fffff50f`606edac0 fffff800`52e0f3f5 nt!NtReadFile+0x8a8 08 fffff50f`606edbd0 00007ffa`2fb4d124 nt!KiSystemServiceCopyEnd+0x25 09 000000fd`8797e108 00000000`00000000 0x00007ffa`2fb4d124
从卦中看,主线程的内核态栈中的 nt!NtReadFile
函数果然给找到了。
2. 使用 procdump
如果仅仅是看线程的内核态栈,我发现有一个非常简单的方式,就是在 procudump 中多加一个 mk 参数即可,截图如下:
接下来使用 Terminal 执行 procdump,输出如下:
PS C:UsersAdministratorDesktop> procdump -ma -mk ConsoleApp -o D:testdump ProcDump v11.0 - Sysinternals process dump utility Copyright (C) 2009-2022 Mark Russinovich and Andrew Richards Sysinternals - www.sysinternals.com [16:24:49] Dump 1 initiated: D:testdumpConsoleApp1.exe_230605_162449.dmp [16:24:50] Dump 1 writing: Estimated dump file size is 57 MB. [16:24:50] Dump 1 complete: 57 MB written in 0.1 seconds [16:24:50] Dump 1 kernel: D:testdumpConsoleApp1.exe_230605_162449.Kernel.dmp [16:24:50] Dump count reached.
从卦中看,当前生成了两个 dmp
文件,一个是用户态dump,一个是内核态dump,也能看到后者还不到 1M,和刚才用 notmyfault
生成的 500M dump 所存储的信息量相差甚远,但对我目前的场景来说已经够用了。
接下来打开 ConsoleApp1.exe_230605_162449.Kernel.dmp
文件,使用 !process
找到 ConsoleApp1.exe 的进程。
.................................................. For analysis of this file, run !analyze -v nt!DbgkpLkmdSnapThreadInContext+0x95: fffff804`5e688b51 488364242800 and qword ptr [rsp+28h],0 ss:0018:ffffe10d`62386fd8=ffffe10d5b8fa810 0: kd> !process 0 2 ConsoleApp1.exe Unable to read _LIST_ENTRY @ fffff8045ea1e080 0: kd> .reload /user Loading User Symbols 0: kd> !process 0 2 ConsoleApp1.exe Unable to read _LIST_ENTRY @ fffff8045ea1e080
从卦中看居然报错了,那怎么办呢?办法肯定是有办法的,可以到用户态dump中寻找进程ID即可。
0:000> ~ . 0 Id: 3adc.5920 Suspend: 0 Teb: 000000d6`7cb98000 Unfrozen 1 Id: 3adc.2240 Suspend: 0 Teb: 000000d6`7cba0000 Unfrozen 2 Id: 3adc.514 Suspend: 0 Teb: 000000d6`7cba2000 Unfrozen 3 Id: 3adc.3c68 Suspend: 0 Teb: 000000d6`7cba4000 Unfrozen ".NET Finalizer"
拿到 3adc
进程号后再找下面的主线程,观察它的线程栈信息,输出如下:
0: kd> .process 3adc Implicit process is now 00000000`00003adc 0: kd> !process PROCESS ffffcf8d5d5b0080 SessionId: none Cid: 3adc Peb: d67cb97000 ParentCid: 4c80 DirBase: 367d95000 ObjectTable: ffff8e81710bbb40 HandleCount: <Data Not Accessible> Image: ConsoleApp1.ex VadRoot ffffcf8d5b20fcb0 Vads 90 Clone 0 Private 1529. Modified 941. Locked 2. DeviceMap ffff8e8172645110 Token ffff8e815e216060 ReadMemory error: Cannot get nt!KeMaximumIncrement value. fffff78000000000: Unable to get shared data ElapsedTime 00:00:00.000 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 153768 QuotaPoolUsage[NonPagedPool] 12648 Working Set Sizes (now,min,max) (14126, 50, 345) (56504KB, 200KB, 1380KB) PeakWorkingSetSize 14033 VirtualSize 2101882 Mb PeakVirtualSize 2101888 Mb PageFaultCount 15757 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 1628 Job ffffcf8d53a102c0 THREAD ffffcf8d5ae14080 Cid 3adc.5920 Teb: 000000d67cb98000 Win32Thread: ffffcf8d54c3a3b0 RUNNING on processor 0 THREAD ffffcf8d4f63e080 Cid 3adc.2240 Teb: 000000d67cba0000 Win32Thread: 0000000000000000 INVALID THREAD ffffcf8d69a32080 Cid 3adc.0514 Teb: 000000d67cba2000 Win32Thread: 0000000000000000 INVALID THREAD ffffcf8d55003580 Cid 3adc.3c68 Teb: 000000d67cba4000 Win32Thread: 0000000000000000 INVALID 0: kd> .thread ffffcf8d5ae14080 Implicit thread is now ffffcf8d`5ae14080 0: kd> k *** Stack trace for last set context - .thread/.cxr resets it # Child-SP RetAddr Call Site 00 ffffe10d`62386fb0 fffff804`5e688a7b nt!DbgkpLkmdSnapThreadInContext+0x95 01 ffffe10d`623874f0 fffff804`5e01dcd0 nt!DbgkpLkmdSnapThreadApc+0x3b 02 ffffe10d`62387520 fffff804`5e01bb67 nt!KiDeliverApc+0x1b0 03 ffffe10d`623875d0 fffff804`5e01ad6f nt!KiSwapThread+0x827 04 ffffe10d`62387680 fffff804`5e01a613 nt!KiCommitThreadWait+0x14f 05 ffffe10d`62387720 fffff804`5e439c68 nt!KeWaitForSingleObject+0x233 06 ffffe10d`62387810 fffff804`5e411fe9 nt!IopSynchronousServiceTail+0x238 07 ffffe10d`623878b0 fffff804`5e20d9f5 nt!NtReadFile+0x599 08 ffffe10d`62387990 00007ffe`6390d184 nt!KiSystemServiceCopyEnd+0x25 09 000000d6`7c9fe328 00000000`00000000 0x00007ffe`6390d184
怎么样,上面的 nt!NtReadFile+0x599
函数就是。
三:总结
有时候真的需要去抓内核态dump,总有一些千奇百怪的问题,太难了,这里总结一下给后来人少踩坑吧。