使用Windbg分析开启LocalDumps

笔记 Aug 26, 2025

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失效。

详情可以参考SetErrorMode 函数 (errhandlingapi.h) - Win32 应用 | Microsoft Learn --- SetErrorMode function (errhandlingapi.h) - Win32 apps | Microsoft Learn

我们一开始就是踩了这个坑,导致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的一些经验和方法,希望对你有所帮助。

标签