• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-03-15 16:58 Aet 隐藏边栏 |   抢沙发  7 
文章评分 1 次,平均分 5.0

高效调试 Dump 的通用步骤与方法

准备工作

  1. 确保符号文件(.pdb)可用
    1. 符号文件必须与崩溃时的程序版本完全一致(编译时间、代码、优化选项一致)
  2. 收集必要文件:
    1. MiniDump 文件(.dmp
    2. 崩溃时的可执行文件(.exe)及依赖的库(.dll
    3. 源代码(与编译版本一致)

初步分析

  1. 使用 !analyze -v
    1. 自动分析崩溃类型(如访问违规、除零错误)、触发位置、调用栈等核心信息
  2. 查看异常代码(!exchain!analyze 输出):
    1. 例如 c0000005 (ACCESS_VIOLATION) 表示内存访问违规

详细分析

  1. 查看调用栈 k,knv,kb
    1. 定位崩溃发生的函数层级,关注用户代码部分(如 MyApp!main
  2. 检查寄存器和局部变量(dv /i??
    1. 查看崩溃时的变量值(如空指针 p 的值)
  3. 反汇编u
    1. 确认崩溃位置的汇编指令是否与预期代码一致

验证与复现

  1. 复现崩溃场景
    1. 根据 Dump 分析结果,尝试在本地复现问题(如输入相同数据或操作)
  2. 代码审查
    1. 结合调用栈和变量值,检查代码逻辑(如指针是否未初始化、数组越界等)

修复与回归测试

  1. 修复问题后重新生成 Dump
    1. 确认修复是否有效
  2. 自动化测试
    1. 添加针对此崩溃场景的单元测试或集成测试

异常代码总结

常见的

异常代码(十六进制) 名称 触发场景 调试建议
0xC0000005 ACCESS_VIOLATION 非法内存访问: - 写入只读内存 - 访问未映射地址(如空指针) 1. 检查指针是否初始化 2. 使用 !address 验证地址有效性 3. 反汇编崩溃指令(u
0xC0000094 DIVIDE_BY_ZERO 整数除法或模运算中除数为零 检查除数是否为0,使用 r 查看寄存器状态
0xC00000FD STACK_OVERFLOW 栈溢出(递归过深或大局部变量) 1. 增大栈大小(/STACK 链接选项) 2. 优化递归或使用堆内存
0xC0000194 POSSIBLE_DEADLOCK 线程死锁 使用 !locks~*k 分析线程等待链
0xC0000409 STACK_BUFFER_OVERRUN 栈缓冲区溢出(如 strcpy 未检查长度) 启用 /GS 编译选项(栈保护),检查缓冲区操作
0x80000003 BREAKPOINT 调试断点(int 3 指令) 检查代码中的 __debugbreak() 或调试器断点
0xC000001D ILLEGAL_INSTRUCTION 执行非法指令(如 CPU 不支持的 AVX 指令) 检查编译器目标平台(如是否启用 AVX)
0xC0000025 NONCONTINUABLE_EXCEPTION 不可继续执行的异常(如异常处理失败后再次异常) 分析首次异常原因,检查异常过滤器(__except
0xC0000056 IN_PAGE_ERROR 页面文件错误(如硬盘故障或内存损坏) 使用 !analyze -v 检查 I/O 状态,验证内存/磁盘健康

.NET 托管异常

异常代码 名称 触发场景
0xE0434352 CLR_EXCEPTION 托管代码抛出未捕获异常(如 C# 的 System.NullReferenceException
0xE06D7363 CPP_EH_EXCEPTION C++ 托管扩展(/clr)中的异常

寄存器相关总结

通用寄存器

寄存器 名称 主要用途
RAX Accumulator 返回值(函数返回时存储结果)
RCX Counter 第一个参数(函数调用时传递参数)
RDX Data 第二个参数
R8 - 第三个参数
R9 - 第四个参数
RBX Base 通用寄存器(通常由被调用者保存,调试时可能保存关键数据)
RSP Stack Pointer 栈顶指针(指向当前栈顶位置)
RBP Base Pointer 栈帧基址(可选,用于定位局部变量和参数)
RSI Source Index 源操作数指针(字符串/内存操作中常用)
RDI Destination Index 目标操作数指针
R10 - 临时寄存器(调用者保存)
R11 - 临时寄存器(调用者保存)
R12R15 - 通用寄存器(被调用者保存,常用于长期存储数据)
  1. 调试场景示例
    1. 查看函数参数:在函数入口断点处,RCXRDXR8R9 对应前四个参数
    2. 检查返回值:函数执行后,RAX 存储返回值
    3. 分析栈溢出:观察 RSP 是否指向非法地址

指令寄存器

寄存器 名称 用途
RIP Instruction Pointer 指向下一条要执行的指令地址
  1. 调试场景示例
    1. 当程序崩溃时,RIP 指向触发异常的指令地址(如访问空指针的 MOV [RAX], 42

段寄存器

CS Code Segment 代码段(指向当前代码段描述符)
DS Data Segment 数据段(默认数据访问的段)
SS Stack Segment 栈段(指向当前栈段描述符)
ESGS - 扩展段(Windows 中 GS 用于 TLS 或内核数据结构)
  1. 调试场景示例
    1. 线程本地存储 (TLS):GS:[0x30] 在用户态指向 TEBThread Environment Block),用于获取线程信息

标志寄存器

名称 用途
第 0 位 CF (Carry) 进位标志(无符号运算溢出时置 1)
第 6 位 ZF (Zero) 零标志(结果为 0 时置 1)
第 7 位 SF (Sign) 符号标志(结果为负时置 1)
第 11 位 OF (Overflow) 溢出标志(有符号运算溢出时置 1)
  1. 调试场景示例
    1. 条件分支分析:通过 ZFOF 判断条件跳转是否触发(如 JZJO 指令)

浮点和向量寄存器

寄存器 用途
XMM0XMM15 浮点参数和返回值(单精度/双精度浮点数或 SIMD 数据)
YMM0YMM15 扩展的 256 位向量寄存器(AVX 指令集)
  1. 调试场景示例
    1. 浮点运算错误:检查 XMM0 中的浮点数值是否符合预期
    2. SIMD 指令调试:观察 YMM0 中的向量数据是否被正确加载

调用约定

  1. x64 程序遵循 Microsoft x64调用约定,规则如下:
  2. 参数传递:
    1. 4 个整数参数通过 RCX, RDX, R8, R9 传递
    2. 4 个浮点参数通过 XMM0XMM3 传递
    3. 剩余参数通过栈传递(从右向左压栈)
  3. 返回值
    1. 整数返回值通过 RAX
    2. 浮点返回值通过 XMM0
  4. 栈对齐
    1. 调用函数前,栈指针 RSP 必须按 16 字节对齐
  5. 影子空间
    1. 调用函数时,调用者需预留 32 字节 空间(即使参数少于 4 个)

调用约定-理解

  1. 如果字符串以 指针形式(如 const char*const wchar_t*)传递,则视为 整数参数,使用 RCX, RDX, R8, R9 传递,与其他整数参数规则一致
  2. 如果传递的是 对象(如 std::string 按值传递),则根据对象大小处理:
    1. 对象大小 ≤ 8字节:通过寄存器传递(如 RCX
    2. 对象大小 > 8字节:通过 栈传递,但实际传递的是对象的 拷贝地址(调用者负责分配内存并拷贝)
  3. 如果以 引用或指针(如 const std::string&)传递,则视为 指针,使用 RCX 传递

RSP理解

  1. Windows 中每个线程创建时,系统会为其分配 独立的栈空间(默认大小 1MB,可通过编译选项调整)
    所以,每个线程都有自己的栈
    所以RSP指向的是当前线程的栈顶,而不是整个进程的栈空间的栈顶
  2. x86-64架构中,栈是向下增长的,即向低地址方向扩展
    1. 当数据压栈时,RSP的值会减小
  3. RSP与函数调用
    1. 调用函数时,返回地址被压入栈 → RSP 减少 8 字节
      参数可能通过寄存器或栈传递(超过 4 个参数时使用栈)
    2. 函数内部,RSP 继续下移,为局部变量和临时数据腾出空间
    3. 函数返回时,RSP 恢复到调用前的状态(通过 leave 或手动调整)

RBP理解

  1. 栈帧的概念
    1. 每个函数调用都会在栈上分配一块连续内存,称为 栈帧,用于存储:
      栈帧的生命周期与函数调用一致:函数开始时创建,函数返回时销毁
    2. 局部变量
    3. 函数参数(部分场景)
    4. 返回地址
    5. 调用者寄存器状态(如RBP
  2. 基址指针(RBP)的核心作用
    1. 固定栈帧的参考点:
    2. RSP 是动态的:随着压栈(push)和弹栈(pop)操作,栈指针 RSP 会不断变化
    3. RBP 是稳定的:在函数内部,RBP 被设置为当前栈帧的固定基址,所有局部变量和参数通过相对于 RBP 的偏移量访问
  3. 访问局部变量和参数

  1. 函数开始时栈帧的建立
    1. 一般是要大于20h也就是32个字节的,因为有影子空间的要求

RBX理解

  1. 在函数调用中,RBX的值必须由被调用函数保存和恢复,确保调用者的RBX值不被破坏
  2. 被调用函数若使用RBX,需在入口处压栈保存(push rbx),退出前弹出恢复(pop rbx

RSI理解

  1. RSIRDI 是 字符串指令 的核心寄存器,常用于以下场景:
  2. 内存块复制

  1. 字符串加载(LODSB)和存储(STOSB

  2. 优化内存操作

RDI理解

示例dump分析:空指针的访问

代码

分析步骤

  1. windbg打开Dump 文件并加载符号
    1. ctrl + s
    2. ctrl + p
    3. ctrl + i
  2. 自动分析崩溃原因

  1. 看到了相关异常信息如下

  1. 查看调用栈

  1. 输出

  1. 可以看到和我们自己的模块相关的代码是这一行

  1. 检查变量和寄存器(储存制作p的值)

  1. 反汇编

总结

  1. 结合前面的异常代码和这里的空指针,可以认为是访问了未映射的地址

示例dump分析:观察相关寄存器

代码

  1. 还是上面生成dump的代码

动态调试过程

  1. ctrl + e去加载这个会奔溃的exe
  2. 然后在打断点

  1. 可以用g直接运行到断点处查看相关寄存器信息

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享