使用Windbg分析开启LocalDumps
1. 开启LocalDumps
配置方法
这项功能通过修改 Windows 注册表来进行配置。相关的注册表项位于 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
。
主要的注册表值包括:
-
DumpFolder (REG_EXPAND_SZ): 指定转储文件的保存路径。
-
DumpCount (REG_DWORD): 指定在文件夹中保留的转储文件最大数量。当达到数量上限后,新的转储文件会覆盖旧的。
-
DumpType (REG_DWORD): 指定要收集的转储类型。
-
0: 自定义转储(Custom dump)。
-
1: 迷你转储(Mini dump)。
-
2: 完整转储(Full dump),这是默认值。
-
-
CustomDumpFlags (REG_DWORD): 当 DumpType 设置为 0 (自定义) 时,用来指定具体要包含在转储文件中的信息类型。
全局与特定应用的设置
你可以进行全局设置,对系统上所有应用程序生效。同时,你也可以为某个特定的应用程序创建独立的设置,只需要在 LocalDumps 项下创建一个以该应用程序名(例如 YourApp.exe)命名的注册表项,并在其中进行单独配置。特定应用的设置会覆盖全局设置。
更详细的说明可以参考Collecting User-Mode Dumps - Win32 apps | Microsoft Learn。
代码中的注意事项
如果在代码中调用了SetErrorMode
函数,并且设置了SEM_NOGPFAULTERRORBOX
,那么将会导致LocalDumps失效。
我们一开始就是踩了这个坑,导致LocalDumps没有生成。
2. 分析LocalDump
有完整源码和pdb的情况
拿到LocalDumps文件后,如果你有完整的代码和pdb文件,那么最简单的方法,直接使用Visual Studio打开.dmp文件,然后点击debug with native only
,就能看到崩溃瞬间的调用栈和参数现场了。如果在上一步设置的时候,你选择的是full dump,那么还有完整的内存转储。
如果是👆上面这种情况,你就不用继续往下看了,Visual Studio里面你可以完整的查看所有信息。
但是如果你也像我们一样,只有部分源码和pdb,那么你就需要使用windbg了。
只有部分源码和pdb的情况
现在让我们有请功能强大的windbg登场。你需要现在本地安装windbg。
Install WinDbg - Windows drivers | Microsoft Learn
安装完成后,打开windbg, 将.dmp文件拖入进去。接着需要设置源码和pdb、exe的路径。
File->Symbol File Path
,设置pdb的路径。File->Source File Path
,设置源码的路径。File->Image File Path
,设置exe的路径。
设置完成后,在前面打开的dmp文件窗口,点击蓝色的!analyze -v
按钮,就能看到崩溃瞬间的调用栈和参数现场了。
一些常用命令
1. 初步分析与概览
这是开始分析时首先要运行的命令,能提供一个关于崩溃的自动化摘要。
-
!analyze -v功能: 这是最重要的命令,没有之一。它会进行详细的自动化分析,显示异常类型、导致崩溃的模块、错误代码、参数以及关键的调用堆栈 (Call Stack)。-v 参数表示详细模式 (verbose),会提供更丰富的信息。使用场景: 加载 dump 文件后第一个就应该运行它,通常能直接定位到问题的大致方向。
-
功能: 这是最重要的命令,没有之一。它会进行详细的自动化分析,显示异常类型、导致崩溃的模块、错误代码、参数以及关键的调用堆栈 (Call Stack)。-v 参数表示详细模式 (verbose),会提供更丰富的信息。
-
使用场景: 加载 dump 文件后第一个就应该运行它,通常能直接定位到问题的大致方向。
2. 查看异常和上下文信息
如果 !analyze -v
的信息不足,你需要手动查看异常发生时的具体状态。
-
.ecxr (Display Exception Context Record)功能: 将当前的上下文切换到异常发生时的那个瞬间。这会改变寄存器和调用堆栈的显示,让你看到程序崩溃那一刻的真实情况。使用场景: 在 !analyze -v 之后运行,是手动深入分析的第一步。后续的堆栈、变量查看都应在此命令后进行。
-
功能: 将当前的上下文切换到异常发生时的那个瞬间。这会改变寄存器和调用堆栈的显示,让你看到程序崩溃那一刻的真实情况。
-
使用场景: 在 !analyze -v 之后运行,是手动深入分析的第一步。后续的堆栈、变量查看都应在此命令后进行。
3. 检查调用堆栈
调用堆栈显示了程序在崩溃前执行了哪些函数调用,是定位问题的核心。
-
k、kb、kv、kn (Display Stack Trace)k: 显示基本的调用堆栈。kb: 显示调用堆栈,并列出每个函数的前三个参数。对于快速判断函数行为非常有用。kv: 显示更详细的堆栈信息,包括函数调用约定 (FPO) 数据。kn: 在堆栈帧号前加上数字,方便引用。
-
k: 显示基本的调用堆栈。
-
kb: 显示调用堆栈,并列出每个函数的前三个参数。对于快速判断函数行为非常有用。
-
kv: 显示更详细的堆栈信息,包括函数调用约定 (FPO) 数据。
-
kn: 在堆栈帧号前加上数字,方便引用。
-
.frame <帧号>功能: 切换到指定的堆栈帧,可以查看该函数调用时的局部变量和参数。
-
功能: 切换到指定的堆栈帧,可以查看该函数调用时的局部变量和参数。
-
!for_each_frame dv /t功能: 一个非常强大的组合命令,它会遍历当前线程的每一个堆栈帧,并显示该帧内的局部变量及其类型和值。
-
功能: 一个非常强大的组合命令,它会遍历当前线程的每一个堆栈帧,并显示该帧内的局部变量及其类型和值。
4. 检查模块和符号
确认模块(DLLs, EXEs)是否正确加载,符号是否匹配。
-
lm (List Loaded Modules)功能: 列出所有已加载的模块。lmvm <模块名> 可以查看特定模块的详细信息,如版本、时间戳和符号路径。使用场景: 检查出问题的模块版本是否正确,符号是否加载成功(如果没有加载,很多信息都无法解析)。
-
功能: 列出所有已加载的模块。lmvm <模块名> 可以查看特定模块的详细信息,如版本、时间戳和符号路径。
-
使用场景: 检查出问题的模块版本是否正确,符号是否加载成功(如果没有加载,很多信息都无法解析)。
-
.reload /f <模块名>功能: 强制重新加载指定模块的符号。如果所有符号都有问题,可以直接用 .reload /f。
-
功能: 强制重新加载指定模块的符号。如果所有符号都有问题,可以直接用 .reload /f。
-
x <模块名>!*功能: 搜索并列出某个模块中所有的符号(函数名、变量名等)。可以用来确认符号是否正确加载。
-
功能: 搜索并列出某个模块中所有的符号(函数名、变量名等)。可以用来确认符号是否正确加载。
5. 查看内存和变量
直接检查内存中的数据,查看变量的值。
-
dv /t /v (Display Local Variables)功能: 在 .ecxr 和切换到正确的 .frame 后,用此命令查看当前堆栈帧的局部变量。/t 显示类型,/v 显示存储位置等详细信息。
-
功能: 在 .ecxr 和切换到正确的 .frame 后,用此命令查看当前堆栈帧的局部变量。/t 显示类型,/v 显示存储位置等详细信息。
-
dt <类型名> <地址> (Display Type)功能: 以结构化的形式显示一个数据结构的内容。例如,dt MyClass 0x12345678。
-
功能: 以结构化的形式显示一个数据结构的内容。例如,dt MyClass 0x12345678。
-
d 系列命令 (Display Memory)
-
db: 按字节 (Byte) 显示。
-
dw: 按字 (Word, 2字节) 显示。
-
dd: 按双字 (Double Word, 4字节) 显示。
-
dq: 按四字 (Quad Word, 8字节) 显示。
-
da: 按 ASCII 字符显示。
-
du: 按 Unicode 字符显示。
-
6. 线程和进程信息
对于多线程程序,需要检查其他线程的状态。
-
~ (Thread Status)
- 功能: 列出所有线程。前面带 . 的是当前线程,带 # 的是导致异常的线程。
-
~<线程号>s
- 功能: 切换到指定的线程。例如 ~1s 切换到 1 号线程。
-
!threads
- 功能: (需要加载 SOS/Pscor for .NET)显示所有托管线程的信息。
-
!process 0 0
- 功能: 显示当前进程中所有线程的摘要信息。
这是一份Windbg命令的cheetsheet,可参考:Crash Dump Analysis Poster v3.0 (HTML version)
3. 如何查看没有pdb的部分函数的调用参数
win32程序
如果你是win32程序,那么使用kb
命令,就能看到调用栈和前3个参数的地址。然后使用dq
, dd
, db
等命令查看对应地址即可。
win64程序
对于win64程序,kb
命令展示的参数就不太可靠了,因此需要用一些反汇编的方式来获取参数的实际地址。
win64程序的调用约定
在由微软编译器编译的 64 位程序中,默认遵循 __fastcall 调用约定,其规则如下:
前四个参数:
-
第一个整数或指针类型的参数通过 RCX 寄存器传递。
-
第二个整数或指针类型的参数通过 RDX 寄存器传递。
-
第三个整数或指针类型的参数通过 R8 寄存器传递。
-
第四个整数或指针类型的参数通过 R9 寄存器传递。
第五个及以后的参数: 从第五个参数开始,会像 32 位程序一样,从右到左依次压入堆栈 (Stack) 中。
浮点数参数: 前四个浮点或双精度参数通过 XMM0, XMM1, XMM2, XMM3 寄存器传递。
返回值: 整数和指针类型的返回值通常存储在 RAX 寄存器中。
查看win64程序调用参数
首先,使用k
命令获取调用栈和返回地址(RetAddr)。
假设我们需要查看第05栈帧的函数调用的参数。
0:002> k
# Child-SP RetAddr Call Site
00 00000000`0556bc38 00007ff8`ffd984f1 ntdll!NtWaitForMultipleObjects+0x14
01 00000000`0556bc40 00007ff8`ffd97785 ntdll!WerpWaitForCrashReporting+0x6d
02 00000000`0556bca0 00007ff8`ffd96797 ntdll!RtlReportExceptionHelper+0x269
03 00000000`0556c210 00007ff8`ffe8719d ntdll!RtlReportException+0x77
04 00000000`0556c290 00007ff8`ffe26786 ntdll!RtlReportCriticalFailure$filt$0+0x33
05 00000000`0556c2c0 00007ff8`ffe357f2 ntdll!_C_specific_handler+0x96
06 00000000`0556c330 00007ff8`ffe3a71d ntdll!_GSHandlerCheck_SEH+0x76
07 00000000`0556c360 00007ff8`ffdc49d3 ntdll!RtlpExecuteHandlerForException+0xd
08 00000000`0556c390 00007ff8`ffdc66e9 ntdll!RtlDispatchException+0x373
09 00000000`0556ca90 00007ff8`ffe87143 ntdll!RtlRaiseException+0x2d9
0a 00000000`0556d9b0 00007ff8`ffe87ae2 ntdll!RtlReportCriticalFailure+0x97
0b 00000000`0556dac0 00007ff8`ffe3574a ntdll!RtlpHeapHandleError+0x12
0c 00000000`0556daf0 00007ff8`ffdd6253 ntdll!RtlpLogHeapFailure+0x96
0d 00000000`0556db20 00000001`41725ecc ntdll!RtlFreeHeap+0x143
0e 00000000`0556dbd0 00000001`400e9dde YOUR_EXE_NAME!LZ4_versionString+0x15a916c
0f 00000000`0556dc00 00000001`400ed1ed YOUR_EXE_NAME+0xe9dde
10 00000000`0556e060 00000001`41b44985 YOUR_EXE_NAME+0xed1ed
11 00000000`0556e090 00000001`40189d63 YOUR_EXE_NAME!LZ4_versionString+0x19c7c25
12 00000000`0556e100 00000001`401881ed YOUR_EXE_NAME!LZ4_versionString+0xd003
13 00000000`0556e560 00000001`400ca8d7 YOUR_EXE_NAME!LZ4_versionString+0xb48d
14 00000000`0556ea20 00000001`4010a65c YOUR_EXE_NAME+0xca8d7
15 00000000`0556fee0 00000001`4010a6b6 YOUR_EXE_NAME+0x10a65c
16 00000000`0556ff10 00007ff8`ff6084d4 YOUR_EXE_NAME+0x10a6b6
17 00000000`0556ff60 00007ff8`ffde1791 kernel32!BaseThreadInitThunk+0x14
18 00000000`0556ff90 00000000`00000000 ntdll!RtlUserThreadStart+0x21
可见05栈帧的返回地址为00007ff8 ffe357f2
。
使用命令ub 00007ff8 ffe357f2
查看反汇编代码。
0:002> ub 00007ff8`ffe357f2
ntdll!_GSHandlerCheck_SEH+0x5c:
00007ff8`ffe357d8 440f44d8 cmove r11d,eax
00007ff8`ffe357dc 4585db test r11d,r11d
00007ff8`ffe357df 7413 je ntdll!_GSHandlerCheck_SEH+0x78 (00007ff8`ffe357f4)
00007ff8`ffe357e1 4c8bcf mov r9,rdi
00007ff8`ffe357e4 4d8bc6 mov r8,r14
00007ff8`ffe357e7 488bd6 mov rdx,rsi
00007ff8`ffe357ea 488bcd mov rcx,rbp
00007ff8`ffe357ed e8fe0effff call ntdll!_C_specific_handler (00007ff8`ffe266f0)
假设我们想要查看第一个参数,根据调用约定,第一个参数为rcx
寄存器,根据反汇编结果可以看到,rcx的值来自于rbp寄存器。
那么怎么查看rbp寄存器的值呢?
05栈帧的函数是由它的前一栈帧调用的,因此我们需要查看06栈帧的rbp寄存器的值。
使用命令.frame /r 06
查看06栈帧的寄存器的值。
0:002> .frame /r 06
06 00000000`0556c330 00007ff8`ffe3a71d ntdll!_GSHandlerCheck_SEH+0x76
rax=000000000000005b rbx=00007ff8ffec79ac rcx=0000000000000002
rdx=000000000556bc70 rsi=000000000556d9b0 rdi=000000000556c440
rip=00007ff8ffe357f2 rsp=000000000556c330 rbp=000000000556da00
r8=0000000000001000 r9=0000000000000000 r10=0000000000000040
r11=0000000000000246 r12=000000000556da00 r13=00007ff8ffd90000
r14=000000000556d3a0 r15=00007ff8ffe87143
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!_GSHandlerCheck_SEH+0x76:
00007ff8`ffe357f2 8bd0 mov edx,eax
可以看到,rbp的值为000000000556da00
,这个地址即为调用05栈帧函数的第一个参数的地址。
现在你可以根据参数的类型,使用d
系列命令来查看他们的值了。
比如:
dq 000000000556da00
如果是指针,则可以在地址前加上poi()
来查看。如:
dq poi(000000000556da00)
如果是结构体,你可以根据结构体布局来推算每个成员变量的偏移地址。假设你的结构体如下:
struct a {
int a1;
char s[64];
some_struct* rec[1];
};
int a1:
大小:4 字节。
对齐:4 字节对齐。
偏移量:0x00 (从头开始)。
char s[64]:
大小:64 字节。
对齐:1 字节对齐。
偏移量:0x04 (紧跟在 a1 之后)。
some_struct* rec[1]:
这是一个包含1个元素的指针数组。
大小:在64位下,一个指针的大小是 8 字节。所以总大小是 1 * 8 = 8 字节。
对齐:指针在64位下需要 8 字节对齐。
偏移量计算:
s 成员从 0x04 开始,大小为 64 (十六进制 0x40) 字节。所以 s 成员结束于 0x04 + 0x40 - 1 = 0x43。
rec 必须在8字节对齐的地址上开始。0x43 之后的第一个8字节对齐地址是 0x48。
因此,编译器会在 s 和 rec 之间插入 4个字节的填充 (Padding)。
rec 的偏移量是:0x48。
根据上述偏移量,就可以以base_addr+offset
的方式查看对应的成员的值了。
总结
以上就是使用Windbg分析LocalDumps的一些经验和方法,希望对你有所帮助。