Dump分析学习笔记

33 阅读19分钟

0. 什么是Dump文件?

0.1 基本概念

Dump文件(也称为转储文件或内存转储文件)是程序在运行过程中,某一时刻的内存快照。它包含了程序在崩溃或特定时刻的:

  • 进程内存内容
  • 寄存器状态
  • 线程栈信息
  • 模块信息
  • 句柄信息
  • 异常上下文

0.2 Dump文件类型

根据捕获的内存范围和详细程度,Dump文件主要分为以下几种类型:

类型扩展名描述特点
完全内存转储.dmp捕获进程的全部内存体积大,信息完整,适合深度分析
小型内存转储.dmp仅捕获基本信息(寄存器、栈、模块列表)体积小,分析速度快,适合快速定位问题
迷你转储.dmp介于完全和小型之间,可自定义捕获内容灵活性高,适合不同场景
自动内存转储.dmpWindows系统默认的转储类型平衡了体积和信息量

0.3 Dump文件的用途

  1. 崩溃分析:定位程序崩溃的原因(如空指针、内存访问违规、异常等)
  2. 性能问题分析:分析内存泄漏、CPU高占用、死锁等性能问题
  3. 调试复杂问题:重现难以在开发环境中复现的问题
  4. 安全分析:分析恶意软件或安全漏洞
  5. 版本对比:对比不同版本程序的运行状态

0.4 如何生成Dump文件

0.4.1 通过任务管理器

  1. 打开任务管理器(Ctrl+Shift+Esc)
  2. 找到目标进程
  3. 右键点击 → 创建转储文件
  4. 系统会提示转储文件的保存位置

0.4.2 通过代码生成

在.NET应用程序中,可以通过以下方式生成Dump文件:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
​
class Program
{
    [DllImport("DbgHelp.dll")]
    public static extern bool MiniDumpWriteDump(
        IntPtr hProcess, 
        int ProcessId, 
        IntPtr hFile, 
        int DumpType, 
        IntPtr ExceptionParam, 
        IntPtr UserStreamParam, 
        IntPtr CallbackParam);
​
    static void Main(string[] args)
    {
        // 生成Dump文件的代码
        Process process = Process.GetCurrentProcess();
        using (var file = System.IO.File.Create("dump.dmp"))
        {
            MiniDumpWriteDump(
                process.Handle, 
                process.Id, 
                file.SafeFileHandle.DangerousGetHandle(), 
                2, // MiniDumpWithFullMemory
                IntPtr.Zero, 
                IntPtr.Zero, 
                IntPtr.Zero);
        }
    }
}

0.4.3 通过调试器生成

使用WinDbg或Visual Studio调试器,可以在程序运行过程中随时生成Dump文件。

1. WinDbg常用命令介绍

1.1 .loadby sos clr

功能:加载SOS调试扩展并将其附加到CLR模块。

说明

  • SOS (Son of Strike) 是.NET运行时的调试扩展,提供了丰富的命令来分析托管代码
  • .loadby 命令用于加载调试扩展,并指定扩展应该附加到哪个模块
  • clr 是.NET 4.0+中CLR的模块名称
  • 这条命令是使用WinDbg分析.NET应用程序dump文件的第一步

使用场景:在打开.NET应用程序的dump文件后,首先需要加载SOS扩展才能使用.NET相关的调试命令。

1.2 !analyze -v

功能:自动分析异常情况并生成详细报告。

说明

  • !analyze 是WinDbg中最强大的命令之一,能够自动分析dump文件中的异常情况

  • -v 参数表示输出详细信息

  • 报告包含:

    • 异常代码和类型
    • 调用栈信息
    • 进程和线程信息
    • 模块和符号信息
    • 可能的错误原因

使用场景:当需要快速了解dump文件中发生了什么异常时,首先使用此命令获取概览。

1.3 !pe

功能:打印托管异常的详细信息。

说明

  • !pe!PrintException 的缩写

  • 输出包含:

    • 异常对象地址
    • 异常类型
    • 异常消息
    • 内部异常
    • 调用栈
    • HResult值

使用场景:在使用 !analyze -v 发现托管异常后,使用此命令获取更详细的异常信息。

1.4 !clrstack

功能:显示当前线程的托管调用栈。

说明

  • 输出托管代码的调用栈,包括方法名、参数和源代码行号(如果有符号文件)
  • 显示每个方法的入口点地址
  • 区分托管代码和非托管代码

使用场景:需要了解托管代码的执行流程,查找异常发生的具体位置时使用。

1.5 基本调试命令

1.5.1 .sympath

功能:设置或显示符号搜索路径。

说明

  • 符号文件(.pdb)包含调试信息,是分析dump文件的关键
  • 常用命令格式:.sympath SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
  • 可以添加多个符号路径,用分号分隔

使用场景:在分析dump文件前,需要设置正确的符号搜索路径,以便加载符号文件。

1.5.2 .reload

功能:重新加载符号文件。

说明

  • 常用命令格式:.reload /f(强制重新加载所有符号)
  • 可以指定模块名:.reload /f MyModule.dll

使用场景:设置符号路径后,或怀疑符号加载不正确时使用。

1.5.3 .exr

功能:显示异常记录。

说明

  • 常用命令格式:.exr -1(显示最近的异常记录)
  • 输出包含异常代码、异常标志、参数等信息

使用场景:需要查看原始异常信息时使用。

1.5.4 .ecxr

功能:设置当前上下文为异常上下文。

说明

  • 将调试器上下文切换到发生异常的位置
  • 结合 kb 命令可以查看异常发生时的调用栈

使用场景:分析异常时,需要查看异常发生时的寄存器状态和调用栈。

1.5.5 kb

功能:显示当前线程的非托管调用栈。

说明

  • 输出包含返回地址、函数名、参数等信息
  • 可以指定显示的帧数:kb 20(显示20帧)

使用场景:分析非托管代码的调用栈,或当托管调用栈不完整时使用。

1.5.6 ~*kb

功能:显示所有线程的非托管调用栈。

说明

  • 输出每个线程的线程ID和调用栈
  • 可以结合 ~[线程号]s 切换到指定线程

使用场景:分析多线程问题,如死锁、线程竞争等。

1.5.7 r

功能:显示或修改寄存器状态。

说明

  • 不带参数时显示所有寄存器
  • 可以指定寄存器:r rax rbx(显示RAX和RBX寄存器)

使用场景:需要查看或修改寄存器状态时使用。

1.6 SOS扩展高级命令

1.6.1 !threads

功能:显示所有托管线程的信息。

说明

  • 输出包含线程ID、线程状态、托管调用栈等信息
  • 可以结合 -live 参数只显示活跃线程

使用场景:分析多线程问题,如死锁、线程阻塞等。

1.6.2 !clrstack -p

功能:显示托管调用栈,包括参数。

说明

  • 在基本 !clrstack 命令的基础上,增加了参数信息
  • 对于分析函数调用的参数传递非常有用

使用场景:需要查看函数调用参数时使用。

1.6.3 !clrstack -l

功能:显示托管调用栈,包括局部变量。

说明

  • 在基本 !clrstack 命令的基础上,增加了局部变量信息
  • 需要有完整的符号文件

使用场景:需要查看函数内部局部变量的值时使用。

1.6.4 !dumpobj

功能:显示托管对象的详细信息。

说明

  • 需要提供对象的地址作为参数:!dumpobj 0x0000018b801da558
  • 输出包含对象类型、字段值、方法表等信息

使用场景:需要查看特定对象的内部状态时使用。

1.6.5 !dumparray

功能:显示数组的内容。

说明

  • 需要提供数组对象的地址作为参数:!dumparray 0x0000018b801da558
  • 可以结合 -details 参数显示更详细的信息
  • 可以指定显示的元素数量:!dumparray 0x0000018b801da558 10(显示前10个元素)

使用场景:需要查看数组内容时使用。

1.6.6 !dumpheap

功能:显示托管堆的信息。

说明

  • 不带参数时显示整个堆的摘要信息
  • 可以结合 -stat 参数显示按类型统计的堆使用情况:!dumpheap -stat
  • 可以结合 -type 参数显示特定类型的对象:!dumpheap -type System.String

使用场景:分析内存泄漏、大对象分配等内存相关问题。

1.6.7 !gcroot

功能:查找对象的根引用。

说明

  • 需要提供对象的地址作为参数:!gcroot 0x0000018b801da558
  • 显示对象被哪些根引用,防止被垃圾回收

使用场景:分析内存泄漏,查找对象无法被回收的原因。

1.6.8 !dumpstackobjects

功能:显示栈上的托管对象。

说明

  • 输出包含对象地址、类型、大小等信息
  • 可以结合 -short 参数只显示对象地址

使用场景:分析栈上的对象,查找可能的内存问题。

1.6.9 !syncblk

功能:显示同步块信息,用于分析死锁。

说明

  • 输出包含同步块地址、拥有线程、等待线程等信息
  • 可以结合 -all 参数显示所有同步块

使用场景:分析死锁问题,查找被争用的锁对象。

1.6.10 !eeheap

功能:显示CLR堆的内存使用情况。

说明

  • 输出包含GC堆、JIT代码堆、 Loader堆等内存使用情况
  • 可以结合 -gc 参数只显示GC堆信息

使用场景:分析CLR内存使用情况,查找内存泄漏。

1.6.11 !threadpool

功能:显示线程池信息。

说明

  • 输出包含工作线程、IO完成端口线程、队列长度等信息
  • 可以结合 -minio 参数显示IO线程池信息

使用场景:分析线程池相关问题,如线程池饥饿、任务队列积压等。

1.7 符号和内存相关命令

1.7.1 x

功能:显示符号列表。

说明

  • 常用命令格式:x MyModule!*(显示MyModule模块的所有符号)
  • 支持通配符:x MyModule!MyClass::*(显示MyClass类的所有成员)

使用场景:查找特定符号,或了解模块的结构。

1.7.2 ln

功能:显示地址对应的符号。

说明

  • 需要提供内存地址作为参数:ln 0x00007ffcbe14f4d4
  • 输出包含符号名、模块名、偏移量等信息

使用场景:需要了解特定地址对应的函数或变量时使用。

1.7.3 !address

功能:显示内存布局。

说明

  • 不带参数时显示整个进程的内存布局
  • 可以结合内存地址显示特定区域的信息:!address 0x00007ffcbe14f4d4
  • 可以结合 -summary 参数显示内存使用摘要

使用场景:分析内存访问违规、内存布局等问题。

1.8 进程和线程高级命令

1.8.1 ~

功能:显示线程列表。

说明

  • 输出包含线程ID、线程状态、优先级等信息
  • 可以结合 ~[线程号]s 切换到指定线程

使用场景:需要查看所有线程的状态,或切换到特定线程时使用。

1.8.2 !peb

功能:显示进程环境块。

说明

  • 输出包含进程命令行、环境变量、模块列表等信息

使用场景:需要了解进程的启动信息和环境时使用。

1.8.3 !teb

功能:显示线程环境块。

说明

  • 输出包含线程本地存储、异常处理信息等
  • 可以结合 -t 参数显示指定线程的TEB

使用场景:分析线程本地存储相关问题时使用。

2. HRESULT详解

2.1 基本概念

HRESULT(Handle to a Result)是一个32位整数,用于表示Windows API和COM(Component Object Model)中的操作结果状态。在.NET中,它也被用于表示异常的错误代码。

2.2 结构组成

HRESULT是一个32位值,包含以下部分:

位范围名称描述
31严重性位0 = 成功,1 = 失败
30-29保留位通常为00
28-16设施代码表示错误来源(如FACILITY_NULL、FACILITY_ITF、FACILITY_WIN32等)
15-0错误代码具体的错误标识符

2.3 常见HRESULT值

HRESULT含义.NET异常类型
0x00000000S_OK成功,无异常
0x00000001S_FALSE成功但有警告
0x80004005E_FAILCOMException
0x80004001E_NOTIMPLNotImplementedException
0x8007000EE_OUTOFMEMORYOutOfMemoryException
0x80070057E_INVALIDARGArgumentException
0x80131500COR_E_EXCEPTIONException
0x80131501COR_E_SYSTEMSystemException
0x80070002E_FILENOTFOUNDFileNotFoundException

2.4 HRESULT解析示例

0x80131500 为例:

  • 第31位:1 → 失败
  • 第30-29位:00 → 保留
  • 第28-16位:0x13 → FACILITY_URT(.NET运行时)
  • 第15-0位:0x1500 → 具体错误代码,表示通用.NET异常

3. DEBUG_FLR_EXCEPTION_CODE vs ExceptionCode 不匹配分析

3.1 警告信息含义

在调试Windows应用程序时,经常会看到以下警告:

DEBUG_FLR_EXCEPTION_CODE(80131500) and the ".exr -1" ExceptionCode(e0434352) don't match

这条警告表示两个不同的地方报告的异常代码不一致:

  • DEBUG_FLR_EXCEPTION_CODE(80131500):调试器从故障转储文件的分析报告中提取的异常代码
  • ExceptionCode(e0434352):通过WinDbg的 .exr -1 命令直接查看异常记录得到的代码

3.2 两个值的具体含义

3.2.1 0x80131500(从转储分析得到)

这是.NET异常(特别是托管异常)的常见HRESULT:

0x80131500 = COR_E_EXCEPTION

表示一个通用的.NET托管异常。

3.2.2 0xE0434352(从异常记录得到)

这是Windows结构化异常处理(SEH)用于标识CLR异常的代码:

0xE0434352 = 0xE0 (严重错误) + "CLR" 的ASCII编码
E0 = 严重错误
43 43 52 = "CLR"

3.3 不匹配的原因

  1. 嵌套异常:可能有多个异常发生,调试器看到的是不同的异常
  2. 异常链:一个异常导致另一个异常
  3. 转储文件不完整:转储可能没有捕获完整的异常上下文
  4. 调试符号问题:符号文件(PDB)不匹配或缺失
  5. 分析时间差异:不同时间点分析得到不同的异常状态

4. 异常时刻的CPU寄存器快照

在异常发生时,调试器会捕获CPU寄存器的状态,这些寄存器包含了程序执行的关键信息。

4.1 关键寄存器介绍

寄存器名称描述
RIP指令指针寄存器存储下一条要执行的指令地址
RSP栈指针寄存器指向当前栈顶的地址
RAX累加器寄存器用于算术运算和函数返回值
RBX基址寄存器用于存储基地址
RCX计数器寄存器用于循环计数和函数第一个参数
RDX数据寄存器用于存储数据和函数第二个参数
RSI源索引寄存器用于字符串操作中的源地址
RDI目标索引寄存器用于字符串操作中的目标地址
RBP基指针寄存器指向当前栈帧的基地址
R8-R15通用寄存器用于存储额外的函数参数和临时数据

4.2 寄存器快照的作用

  • 定位问题:通过RIP寄存器可以知道异常发生时正在执行的指令
  • 了解调用上下文:通过RSP和RBP可以分析栈结构
  • 查看参数值:通过RCX、RDX等寄存器可以查看函数调用的参数
  • 分析数据状态:通过RAX等寄存器可以了解函数的返回值或中间计算结果

5. 实战分析:结合dump.txt文件

5.1 基本信息

从dump.txt文件中,我们可以看到这是一个名为 TangdaoDemo.exe 的.NET应用程序的dump文件,发生了一个CLR异常。

5.2 异常代码分析

DEBUG_FLR_EXCEPTION_CODE(80131500) and the ".exr -1" ExceptionCode(e0434352) don't match

这正是我们在第3节中讨论的异常代码不匹配情况:

  • 0x80131500 是.NET通用异常的HRESULT
  • 0xE0434352 是CLR异常的SEH代码

5.3 CPU寄存器快照

异常发生时的寄存器状态:

rax=00007ffd1debcd68 rbx=0000008653dfdcb8 rcx=00007ffd1debc658
rdx=0000008853dfd298 rsi=0000000000000001 rdi=00000000e0434352
rip=00007ffd3707782a rsp=0000008653dfdb00 rbp=0000000000000005
r8=000000000082c014  r9=000000000082c014 r10=000000000082c014
r11=0000000000000000 r12=0000000000004000 r13=0000018b8016d9e8
r14=0000008653dfdcb8 r15=0000000000000000
  • RIP指向 KERNELBASE!RaiseException+0x8a,说明异常是通过 RaiseException 函数抛出的
  • RDI寄存器的值是 0xe0434352,正是CLR异常的SEH代码

5.4 异常信息

通过 !pe 命令,我们可以看到异常的详细信息:

Exception object: 0000018b801da558
Exception type:   System.Exception
Message:          自动开启异常抛出
InnerException:   <none>
HResult: 80131500
  • 异常类型:System.Exception
  • 异常消息:自动开启异常抛出
  • HResult:0x80131500,对应 COR_E_EXCEPTION

5.5 调用栈分析

通过 !clrstack 命令,我们可以看到完整的托管调用栈:

0000008653dfdda8 00007ffd39d22714 [HelperMethodFrame: 0000008653dfdda8] 
0000008653dfde90 00007ffcbe14f4d4 TangdaoDemo.StartupStrapper.Configure() [E:\CSharp Project\ApplyClient\TangdaoDemo\TangdaoDemo\StartupStrapper.cs @ 34]
0000008653dfdf80 00007ffcbe14f1cc TangdaoDemo.App.Configure() [E:\CSharp Project\ApplyClient\TangdaoDemo\TangdaoDemo\App.xaml.cs @ 50]
0000008653dfdfb0 00007ffcbe1429ef IT.Tangdao.Framework.TangdaoApplication.OnStartup(System.Windows.StartupEventArgs) [E:\CSharp Project\自定义Nuget\IT.Tangdao.Framework\IT.Tangdao.Framework\TangdaoApplication.cs @ 51]
...

从调用栈中,我们可以清晰地看到异常发生在 TangdaoDemo.StartupStrapper.Configure() 方法的第34行,这个方法被 TangdaoDemo.App.Configure() 调用,而后者又被框架的 OnStartup 方法调用。

5.6 总结

通过对dump.txt文件的分析,我们可以得出以下结论:

  1. 这是一个.NET应用程序(TangdaoDemo.exe)的CLR异常
  2. 异常类型是 System.Exception,消息为 "自动开启异常抛出"
  3. 异常发生在 StartupStrapper.Configure() 方法的第34行
  4. 调试器显示了异常代码不匹配的警告,这是正常现象,因为一个是.NET异常的HRESULT,另一个是CLR异常的SEH代码
  5. 通过分析寄存器快照和调用栈,我们可以精确定位异常发生的位置和上下文

6. WinDbg使用技巧和最佳实践

6.1 准备工作

  1. 安装WinDbg Preview:推荐使用WinDbg Preview,它提供了更好的用户界面和更多功能

  2. 配置符号路径:在WinDbg的设置中配置默认符号路径,避免每次都手动设置

  3. 安装扩展:除了SOS,还可以安装其他有用的扩展,如:

    • PSSCOR4/PSSCOR5:SOS的扩展,提供更多.NET调试命令
    • SOSEX:提供高级.NET调试功能
    • UMDH:用于分析内存泄漏

6.2 分析流程

  1. 初始分析:使用 !analyze -v 获取异常的初步信息
  2. 设置上下文:如果是异常,使用 .ecxr 设置异常上下文
  3. 查看调用栈:使用 !clrstack(托管)和 kb(非托管)查看调用栈
  4. 查看异常详情:使用 !pe 查看托管异常的详细信息
  5. 分析相关对象:使用 !dumpobj!dumparray 等命令查看相关对象的状态
  6. 内存分析:如果是内存问题,使用 !dumpheap!gcroot 等命令分析内存
  7. 线程分析:如果是多线程问题,使用 ~*kb!threads!syncblk 等命令分析线程

6.3 常见问题解决

  1. 符号加载失败

    • 检查符号路径是否正确
    • 确保符号文件与可执行文件版本匹配
    • 使用 .reload /f 强制重新加载
  2. 托管调用栈不完整

    • 结合非托管调用栈(kb)分析
    • 检查是否有内联函数
    • 尝试使用 !clrstack -l 查看局部变量
  3. 分析速度慢

    • 只加载必要的符号
    • 使用小型转储进行初步分析
    • 关闭不必要的WinDbg功能

6.4 快捷键

  • Ctrl+Break:中断调试
  • F5:继续执行
  • F10:单步执行
  • F11:单步进入
  • Shift+F11:单步退出
  • Ctrl+D:显示调试器命令窗口
  • Ctrl+K:显示调用栈窗口

7. 学习资源推荐

7.1 官方文档

7.2 书籍

  • 《Windows高级调试》(作者:Mario Hewardt, Daniel Pravat)
  • 《.NET高级调试》(作者:Mario Hewardt)
  • 《深入解析Windows操作系统》(作者:Mark E. Russinovich)

7.3 在线资源

7.4 视频教程

  • Pluralsight:.NET Debugging Fundamentals
  • YouTube:WinDbg tutorials
  • B站:WinDbg调试教程

8. 学习总结

通过这次学习,我掌握了以下内容:

  1. Dump文件基础:了解了Dump文件的概念、类型、用途和生成方法
  2. WinDbg常用命令:学会了使用基本调试命令、SOS扩展命令、符号和内存相关命令、进程和线程命令等约40个常用命令
  3. HRESULT详解:理解了HRESULT的组成和常见值,能够通过HRESULT识别异常类型
  4. 异常代码分析:知道了DEBUG_FLR_EXCEPTION_CODE和ExceptionCode不匹配的原因和含义
  5. 寄存器快照:了解了关键CPU寄存器的作用,能够通过寄存器快照分析异常上下文
  6. 实战分析能力:能够结合以上知识,对实际的dump文件进行完整分析,定位问题所在
  7. 最佳实践:掌握了WinDbg使用的技巧和最佳实践,提高了分析效率

这些知识对于调试.NET应用程序的崩溃问题非常有帮助,能够快速定位和解决生产环境中遇到的各种异常情况。Dump分析是一项强大的技能,需要不断学习和实践才能熟练掌握。希望这篇博客能够帮助更多人了解和使用Dump分析技术,提高调试效率,解决更多复杂问题。