高效调试 Dump
的通用步骤与方法
准备工作
- 确保符号文件(
.pdb
)可用- 符号文件必须与崩溃时的程序版本完全一致(编译时间、代码、优化选项一致)
- 收集必要文件:
MiniDump
文件(.dmp
)- 崩溃时的可执行文件(
.exe
)及依赖的库(.dll
) - 源代码(与编译版本一致)
初步分析
- 使用
!analyze -v
:- 自动分析崩溃类型(如访问违规、除零错误)、触发位置、调用栈等核心信息
- 查看异常代码(
!exchain
或!analyze
输出):- 例如
c0000005 (ACCESS_VIOLATION)
表示内存访问违规
- 例如
详细分析
- 查看调用栈
k
,knv
,kb
- 定位崩溃发生的函数层级,关注用户代码部分(如
MyApp!main
)
- 定位崩溃发生的函数层级,关注用户代码部分(如
- 检查寄存器和局部变量(
dv /i
或??
)- 查看崩溃时的变量值(如空指针
p
的值)
- 查看崩溃时的变量值(如空指针
- 反汇编
u
- 确认崩溃位置的汇编指令是否与预期代码一致
验证与复现
- 复现崩溃场景
- 根据
Dump
分析结果,尝试在本地复现问题(如输入相同数据或操作)
- 根据
- 代码审查
- 结合调用栈和变量值,检查代码逻辑(如指针是否未初始化、数组越界等)
修复与回归测试
- 修复问题后重新生成
Dump
- 确认修复是否有效
- 自动化测试
- 添加针对此崩溃场景的单元测试或集成测试
异常代码总结
常见的
异常代码(十六进制) | 名称 | 触发场景 | 调试建议 |
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 |
- | 临时寄存器(调用者保存) |
R12 –R15 |
- | 通用寄存器(被调用者保存,常用于长期存储数据) |
- 调试场景示例
- 查看函数参数:在函数入口断点处,
RCX
、RDX
、R8
、R9
对应前四个参数 - 检查返回值:函数执行后,
RAX
存储返回值 - 分析栈溢出:观察
RSP
是否指向非法地址
- 查看函数参数:在函数入口断点处,
指令寄存器
寄存器 | 名称 | 用途 |
RIP |
Instruction Pointer | 指向下一条要执行的指令地址 |
- 调试场景示例
- 当程序崩溃时,
RIP
指向触发异常的指令地址(如访问空指针的MOV [RAX], 42
)
- 当程序崩溃时,
段寄存器
CS |
Code Segment | 代码段(指向当前代码段描述符) |
DS |
Data Segment | 数据段(默认数据访问的段) |
SS |
Stack Segment | 栈段(指向当前栈段描述符) |
ES –GS |
- | 扩展段(Windows 中 GS 用于 TLS 或内核数据结构) |
- 调试场景示例
- 线程本地存储 (
TLS
):GS:[0x30]
在用户态指向TEB
(Thread Environment Block
),用于获取线程信息
- 线程本地存储 (
标志寄存器
位 | 名称 | 用途 |
第 0 位 | CF (Carry) |
进位标志(无符号运算溢出时置 1) |
第 6 位 | ZF (Zero) |
零标志(结果为 0 时置 1) |
第 7 位 | SF (Sign) |
符号标志(结果为负时置 1) |
第 11 位 | OF (Overflow) |
溢出标志(有符号运算溢出时置 1) |
- 调试场景示例
- 条件分支分析:通过
ZF
或OF
判断条件跳转是否触发(如JZ
、JO
指令)
- 条件分支分析:通过
浮点和向量寄存器
寄存器 | 用途 |
XMM0 –XMM15 |
浮点参数和返回值(单精度/双精度浮点数或 SIMD 数据) |
YMM0 –YMM15 |
扩展的 256 位向量寄存器(AVX 指令集) |
- 调试场景示例
- 浮点运算错误:检查
XMM0
中的浮点数值是否符合预期 SIMD
指令调试:观察YMM0
中的向量数据是否被正确加载
- 浮点运算错误:检查
调用约定
x64
程序遵循Microsoft x64
调用约定,规则如下:- 参数传递:
- 前
4
个整数参数通过RCX
,RDX
,R8
,R9
传递 - 前
4
个浮点参数通过XMM0
–XMM3
传递 - 剩余参数通过栈传递(从右向左压栈)
- 前
- 返回值
- 整数返回值通过
RAX
- 浮点返回值通过
XMM0
- 整数返回值通过
- 栈对齐
- 调用函数前,栈指针
RSP
必须按16
字节对齐
- 调用函数前,栈指针
- 影子空间
- 调用函数时,调用者需预留
32 字节
空间(即使参数少于4
个)
- 调用函数时,调用者需预留
调用约定-理解
- 如果字符串以 指针形式(如
const char*
、const wchar_t*
)传递,则视为 整数参数,使用RCX
,RDX
,R8
,R9
传递,与其他整数参数规则一致 - 如果传递的是 对象(如
std::string
按值传递),则根据对象大小处理:- 对象大小
≤ 8
字节:通过寄存器传递(如RCX
) - 对象大小
> 8
字节:通过 栈传递,但实际传递的是对象的 拷贝地址(调用者负责分配内存并拷贝)
- 对象大小
- 如果以 引用或指针(如
const std::string&
)传递,则视为 指针,使用RCX
传递
RSP
理解
Windows
中每个线程创建时,系统会为其分配 独立的栈空间(默认大小1MB
,可通过编译选项调整)
所以,每个线程都有自己的栈
所以RSP
指向的是当前线程的栈顶,而不是整个进程的栈空间的栈顶x86-64
架构中,栈是向下增长的,即向低地址方向扩展- 当数据压栈时,
RSP
的值会减小
- 当数据压栈时,
RSP
与函数调用- 调用函数时,返回地址被压入栈 →
RSP
减少8
字节
参数可能通过寄存器或栈传递(超过 4 个参数时使用栈) - 函数内部,
RSP
继续下移,为局部变量和临时数据腾出空间 - 函数返回时,
RSP
恢复到调用前的状态(通过leave
或手动调整)
- 调用函数时,返回地址被压入栈 →
RBP理解
- 栈帧的概念
- 每个函数调用都会在栈上分配一块连续内存,称为 栈帧,用于存储:
栈帧的生命周期与函数调用一致:函数开始时创建,函数返回时销毁 - 局部变量
- 函数参数(部分场景)
- 返回地址
- 调用者寄存器状态(如
RBP
)
- 每个函数调用都会在栈上分配一块连续内存,称为 栈帧,用于存储:
- 基址指针(
RBP
)的核心作用- 固定栈帧的参考点:
RSP
是动态的:随着压栈(push
)和弹栈(pop
)操作,栈指针RSP
会不断变化RBP
是稳定的:在函数内部,RBP
被设置为当前栈帧的固定基址,所有局部变量和参数通过相对于RBP
的偏移量访问
- 访问局部变量和参数
1 2 |
mov rax, [rbp-8] ; 访问局部变量(偏移量为负) mov rdx, [rbp+16] ; 访问传入的参数(偏移量为正) |
- 函数开始时栈帧的建立
- 一般是要大于
20h
也就是32
个字节的,因为有影子空间的要求
- 一般是要大于
1 2 3 |
push rbp ; 保存调用者的 RBP(旧的基址指针) mov rbp, rsp ; 设置当前 RBP = RSP(新的基址指针) sub rsp, 20h ; 分配栈空间(局部变量) |
RBX理解
- 在函数调用中,
RBX
的值必须由被调用函数保存和恢复,确保调用者的RBX
值不被破坏 - 被调用函数若使用
RBX
,需在入口处压栈保存(push rbx
),退出前弹出恢复(pop rbx
)
RSI
理解
RSI
和RDI
是 字符串指令 的核心寄存器,常用于以下场景:- 内存块复制
1 2 3 4 |
mov rsi, [src_addr] ; 源地址存入 RSI mov rdi, [dest_addr] ; 目标地址存入 RDI mov rcx, 100 ; 复制 100 字节 rep movsb ; 逐字节复制 |
-
字符串加载(
LODSB
)和存储(STOSB
) -
优化内存操作
RDI
理解
示例dump分析:空指针的访问
代码
1 2 3 4 5 6 7 8 |
int main() { init_breakpad(); int* p = nullptr; *p = 42; std::cout << "Hello World!\n"; } |
分析步骤
- 用
windbg
打开Dump
文件并加载符号ctrl + s
ctrl + p
ctrl + i
- 自动分析崩溃原因
1 |
!analyze -v |
- 看到了相关异常信息如下
1 2 3 4 5 6 7 8 |
EXCEPTION_RECORD: (.exr -1) ExceptionAddress: 00007ff63cc045ae (aet_breakpad_test!main+0x000000000000002e) ExceptionCode: c0000005 (Access violation) ExceptionFlags: 00000000 NumberParameters: 2 Parameter[0]: 0000000000000001 Parameter[1]: 0000000000000000 Attempt to write to address 0000000000000000 |
- 查看调用栈
1 |
kn |
- 输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0014eb58 00007ffa`6dd6920e ntdll!ZwWaitForSingleObject+0x14 01 00000000`0014eb60 00007ff6`3cc0a6bb KERNELBASE!WaitForSingleObjectEx+0x8e 02 00000000`0014ec00 00007ff6`3cc087a1 aet_breakpad_test!google_breakpad::ExceptionHandler::WriteMinidumpOnHandlerThread+0x10b [Q:\google_code\breakpad\src\src\client\windows\handler\exception_handler.cc @ 726] 03 00000000`0014ec40 00007ffa`6de74677 aet_breakpad_test!google_breakpad::ExceptionHandler::HandleException+0x121 [Q:\google_code\breakpad\src\src\client\windows\handler\exception_handler.cc @ 509] 04 00000000`0014ecb0 00007ffa`703f5818 KERNELBASE!UnhandledExceptionFilter+0x1e7 05 00000000`0014edd0 00007ffa`703dce46 ntdll!memset+0x1218 06 00000000`0014ee10 00007ffa`703f28bf ntdll!_C_specific_handler+0x96 07 00000000`0014ee80 00007ffa`703a2554 ntdll!_chkstk+0x11f 08 00000000`0014eeb0 00007ffa`703f13ce ntdll!RtlRaiseException+0x484 09 00000000`0014f5c0 00007ff6`3cc045ae ntdll!KiUserExceptionDispatcher+0x2e 0a 00000000`0014fcf0 00007ff6`3cc8f8d9 aet_breakpad_test!main+0x2e [Q:\google_code\breakpad\src\src\client\windows\aet_test\aet_breakpad_test\aet_breakpad_test.cpp @ 25] 0b 00000000`0014fe10 00007ff6`3cc8f782 aet_breakpad_test!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79] 0c 00000000`0014fe60 00007ff6`3cc8f63e aet_breakpad_test!__scrt_common_main_seh+0x132 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288] 0d 00000000`0014fed0 00007ff6`3cc8f96e aet_breakpad_test!__scrt_common_main+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331] 0e 00000000`0014ff00 00007ffa`6ffe7374 aet_breakpad_test!mainCRTStartup+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17] 0f 00000000`0014ff30 00007ffa`7039cc91 kernel32!BaseThreadInitThunk+0x14 10 00000000`0014ff60 00000000`00000000 ntdll!RtlUserThreadStart+0x21 |
- 可以看到和我们自己的模块相关的代码是这一行
1 |
0a 00000000`0014fcf0 00007ff6`3cc8f8d9 aet_breakpad_test!main+0x2e [Q:\google_code\breakpad\src\src\client\windows\aet_test\aet_breakpad_test\aet_breakpad_test.cpp @ 25] |
- 检查变量和寄存器(储存制作
p
的值)
1 2 3 4 5 6 |
0:000> dv /i p prv local p = 0x00000000`00000000 0:000> dv p = 0x00000000`00000000 0:000> dv p p = 0x00000000`00000000 |
- 反汇编
1 2 3 4 5 6 7 8 9 10 11 |
0:000> u aet_breakpad_test!main aet_breakpad_test!main [Q:\google_code\breakpad\src\src\client\windows\aet_test\aet_breakpad_test\aet_breakpad_test.cpp @ 21]: 00007ff6`3cc04580 4055 push rbp 00007ff6`3cc04582 57 push rdi 00007ff6`3cc04583 4881ec08010000 sub rsp,108h 00007ff6`3cc0458a 488d6c2420 lea rbp,[rsp+20h] 00007ff6`3cc0458f 488d0d7b5b2000 lea rcx,[aet_breakpad_test!_NULL_IMPORT_DESCRIPTOR <PERF> (aet_breakpad_test+0x2ea111) (00007ff6`3ce0a111)] 00007ff6`3cc04596 e82fd3feff call aet_breakpad_test!ILT+22725(__CheckForDebuggerJustMyCode) (00007ff6`3cbf18ca) 00007ff6`3cc0459b 90 nop 00007ff6`3cc0459c e866cdfeff call aet_breakpad_test!ILT+21250(?init_breakpadYAXXZ) (00007ff6`3cbf1307) |
总结
- 结合前面的异常代码和这里的空指针,可以认为是访问了未映射的地址
示例dump分析:观察相关寄存器
代码
- 还是上面生成
dump
的代码
动态调试过程
ctrl + e
去加载这个会奔溃的exe
- 然后在打断点
1 |
bp aet_breakpad_test!main |
- 可以用
g
直接运行到断点处查看相关寄存器信息
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++标准库_chrono03/28
- ♥ C++20_第一篇06/30
- ♥ C++编程规范101规则、准则与最佳实践 二01/07
- ♥ Soui五05/30
- ♥ C++标准模板库编程实战_算法和随机数12/08
- ♥ Windows API11/11