原文链接: WinDbg. From A to Z!
文章目录
- 为什么使用WinDbg
- 为什么通过本书学习
- 底层原理简述
- Windows的调试工具一览
- dbghelp.dll -- Windows 调试助手
- dbgeng.dll -- 调试引擎接口
- 调试符号 (Debug Symbols)
- 有哪些调试信息
- 生成调试信息
- 匹配调试信息
- 调用堆栈
- 侵入式与非侵入式
- 异常机制
- 异常分发
- AeDebug? Postmortem Debugging!
- 使用WinDbg
- WinDbg 命令
- 主要的扩展插件
- WinDbg 使用符号
- WinDbg 使用源代码
- Windows 上的进程与线程 (Processes and Threads on Windows NT)
- PEB 与 TEB
- 示例 -- 输出完整的PEB
- 用于显示进程信息和模块信息的WinDbg命令
- 示例 -- 模块信息
- 用于显示线程信息的WinDbg命令
- 示例 -- 线程相关
- WinDbg的窗口与菜单
- 调试器标记语言 (Debugger Markup Language, DML)
- 内存 -- 栈详情
- 示例 -- 线程的栈大小
- 内存 -- 栈增长
- 查看调用栈信息的WinDbg命令
- 处理内存的WinDbg命令
- 示例 -- 进程内存信息
- 查看堆信息的WinDbg 命令
- Heap Structs
- 得到HeapAlloc的调用者
- 示例 -- 得到HeapAlloc的调用者
- HeapCreate的调用者
- 示例 -- 得到HeapCreate的调用者
- 排查堆内存泄漏
- 示例 -- 排查堆内存泄漏
为什么使用WinDbg
- 微软官方开发的调试工具
- 比visual studio 有更强大的程序调试能力
- 可以用dll来进行扩展
- WinDbg的调试引擎是Windows操作系统的一部分
为什么通过本书学习
- WinDbg的官方文档对新手不友好
- 没有良好的文档和示例,以致于WinDbg的学习曲线很陡峭,从而很多同鞋安装完WinDbg后就放到硬盘里吃灰。
- “WinDbg. From A to Z”可以帮助同鞋们快速上手,读完本书后,你就会觉得自己又行了。
底层原理简述
Windows的调试工具一览
dbghelp.dll – Windows 调试助手
- 文档在MSDN
- 从Windows2000开始就被包含在系统中
- 被以下程序依赖
- Process Dumping (MiniDumpWriteDump , DbgHelpCreateUSerDump, …)
- Obtaining Stack Traces (StackWalk64, …)
- Symbol Handling (SymFromAddr, Sym* …)
- Obtaining info about executable images (ImageNtHeader, FindDebugInfoFile, …)
dbgeng.dll – 调试引擎接口
- WinDbg的文档中有说明
- 从Windows XP开始被包含在系统中
- 实现了接口如: IDebugAdvanced, IDebugControl, IDebugSystemObjects, …
- 调试器的所有调试功能都来自于dbgeng.dll
Fact 1: WinDbg is really just a shell on top of a debugging engine.
Fact 2: You can write new standalone tools on top of this engine.
调试符号 (Debug Symbols)
- 可执行程序就是一堆机器码
- 高度符号有助于:
- 机器码映射到源代码
- 分析应用程序的内部布局与数据
- Program DataBase -> PDB 文件
- 与linux系统不一样,调试信息与可执行文件分开存储
- 需要特殊的API来使用它:DbgHelp.dll和MsDiaXY.dll
有哪些调试信息
- 公开的函数与变量
- FPO 即用来检索堆栈的信息
- 非公开的函数,局部变量,函数参数
- 源码文件与行数信息
- 类型信息
生成调试信息
Compiler options: /Z7, /Zi, /ZI
Linker options: /debug, /pdb, /pdbstripped
匹配调试信息
- 在可执行文件和PDB文件中的有签名信息
- 调试器会匹配签名信息
- 搜索pdb文件的算法步骤大概如下:
* 先会尝试指定的文件夹
* 再尝试PE文件中记录的路径
* 最后尝试从环境变量 _NT_SYMBOL_PATH 和_NT_ALT_SYMBOL_PATH中搜索
调用堆栈
侵入式与非侵入式
-
侵入式附加
- 会调用DebugActiveProcess 接口
- 会创造一个break-in thread ,就是可以中断进程
- 在WindowsXP之前,调试器分离进程或结束调试时,进程会结束
- 同一时间只能有一个侵入式附加来调试同一个进程
-
非侵入式调试
- 会调用 OpenProcess 接口
- 不会创建一个break-in thread
- 无法作为一个调试器附加到进程
- 目标进程的所有线程都冻结
- 可以改变与测试内存
- 不能设置断点
- 无法单步调试程序
- 可以在一个进程上附加几个非侵入性调试器
异常机制
- 是操作系统的机制而不是编程语言的特性
- 可以通过语言扩展来使用异常机制,如VC++的__try __except 语句
- 不要在需要高效执行的部分使用try catch 语句,因为比较低效。
异常分发
- 系统首先尝试通知进程的调试器(如果有的话)
- 如果进程未被调试,或者关联的调试器未处理异常
(Winge->gN==Go with Exception Not Handled),系统尝试定位基于帧的异常处理程序 - 如果找不到基于帧的处理程序,或者没有基于帧的处理器处理异常,UnhandledException Filter会再次尝试通知进程的调试器。这被称为第二次机会或最后一次机会通知。
- 如果进程未被调试,或者关联的调试器未处理异常,则将启动AeDebug中指定的Postmortem调试器。
AeDebug? Postmortem Debugging!
注册表项 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
使用WinDbg
WinDbg 命令
-
常规命令
- 用于调试进程
- 如: k, lm, g
-
元命令(点命令)
- 通常用于控制调试器的行为
- 如: .cls, .sympath, .lastevent, .detach
-
扩展命令
- dll扩展中的函数
- 通常用于丰富调试器
- 可以自行开发扩展插件
- 如: !address, !analyze
主要的扩展插件
WinDbg 使用符号
-
必须设置 _NT_SYMBOL_PATH 环境变量
如MS symbols:
_NT_SYMBOL_PATH=srv*C:\Symbols\MsSymbols*http://msdl.microsoft.com/download/symbols;
如此设置,WinDbg会自动下载需要的符号
可以设置缓存目录
cache*E:\MyTemp\SymbolCache;SRV*https://msdl.microsoft.com/download/symbols
-
在WinDbg 界面可以如此设置
-
常用的命令
WinDbg 使用源代码
- 可以通过设置环境变量 _NT_SOURCE_PATH
- 在WinDbg界面
- 常用的命令
Windows 上的进程与线程 (Processes and Threads on Windows NT)
先解释一下NT是什么意思
NT 就是New Technology, 是1993年首次发布的windows内核架构,之前使用的是9x内核架构,从windows xp开始Windows统一到NT内核上,9x被弃用。因为历史原因,许多Windows的库会有nt前缀。
- 每个Windows进程都由内核模式下的执行进程块(EPROCESS)表示
- EPROCESS指向了许多相关的数据结构;例如,每个进程都有一个或多个由执行线程块(ETHREAD)表示的线程
- EPROCESS指向进程地址空间中的进程环境块(PEB)
- ETHREAD指向进程地址空间中的线程环境块(TEB)
PEB 与 TEB
-
PEB (Process Environment Block)
- 包含基础信息如:基址,版本号,模块列表
- 进程堆信息
- 环境变量
- 命令行参数
- DLL搜索路径
- 在WinDbg 使用命令显示:
!peb
,dt nt!_PEB
-
TEB (Thread Environment Block)
- 线程栈信息如: 栈基址和栈长度上限
- TLS(Thread Local Storage)数组
- 在WinDbg上的输出命令:
!teb
,dt nt!_TEB
事实上WinDbg的诸多命令如: lm, !dlls, !imgreloc, !tls, !gle 等,都是从数据结构PEB与TEB中得到的。
示例 – 输出完整的PEB
命令组解析
- dt : Data Type , 用于显示数据结构(如结构体、联合体)的定义及其在内存中的值。
- nt!_PEB
- nt! 表示符号属于 NT 内核模块(通常是 ntoskrnl.exe 或 ntdll.dll)。
- _PEB 是进程环境块的结构体名称。PEB 包含进程的全局信息(如加载的模块、命令行参数、堆信息等)。
- -r: 递归选项,表示递归展开嵌套的结构体。默认递归一层(相当于 -r1),若需更多层级可指定(如 -r2)。此选项会展开指针指向的子结构。
- @$peb : 伪寄存器 $peb,存储当前进程的 PEB 地址。@ 符号用于访问伪寄存器。
功能:
-
显示 PEB 的完整结构,解析 @$peb 地址处的内存,按 _PEB 结构体的定义格式化输出,包括所有成员的值(如 Ldr, ProcessParameters 等)。
-
递归展开嵌套成员, 如果成员是指向其他结构体的指针(如 PEB_LDR_DATA* Ldr),-r 会进一步展开这些子结构体的内容。
用于显示进程信息和模块信息的WinDbg命令
示例 – 模块信息
用于显示线程信息的WinDbg命令
示例 – 线程相关
WinDbg的窗口与菜单
- WinDbg的窗口是可以停靠与悬浮的
- 每个WinDbg的子窗口有其自己的功能菜单
调试器标记语言 (Debugger Markup Language, DML)
- DML允许调试器输出以标签的形式包含指令和额外的非显示信息
- 调试器用户界面解析出额外信息以提供新行为
- DML主要旨在解决以下问题:
- 相关信息的链接
- 调试器和扩展功能的可发现性
- 增强调试器和扩展的输出
- DML是在调试工具6.6.0.7版本中引入的
内存 – 栈详情
- 每一个新的线程都会有一个栈空间,是由已经提交的和已经预约的内存组成
- 默认情况下每个线程有1MB的预约空间和1个分页的已经提交空间
不明白己预约的内存和己提交的内存是什么意思的同学自行搜索Windows中 VirtualAlloc的资料。
示例 – 线程的栈大小
内存 – 栈增长
- ESP寄存器指向线程的当前堆栈位置。
- 如果程序试图访问保护页中的地址,系统会引发 _UNGUARD-PAGE_VIOLATION(0x80000001)异常。保护页面为内存页面访问提供one-shot警报。
- 如果堆栈一直增长到保留内存的末尾,则会引发STATUS_STACK_OVERFLOW异常。
查看调用栈信息的WinDbg命令
处理内存的WinDbg命令
示例 – 进程内存信息
查看堆信息的WinDbg 命令
Heap Structs
如果您的应用程序禁用了页面堆,则应用以下结构。
请注意,默认情况下页面堆是禁用的。
-
_HEAP
- 在ntdll.dll 中定义 :
dt ntdll!_HEAP
- 每成功调用一次
HeapCreate
就会产生一个_HEAP - 可以使用
!heap -p -all
来得到进程中所有_HEAP 结构体的地址
- 在ntdll.dll 中定义 :
-
_HEAP_ENTRY
- 在ntdll.dll 中定义:
dt ntdll!_HEAP_ENTRY
- 每成功调用一次
HeapAlloc
就会产生一个_HEAP_ENTRY - 使用
!heap -p -all
来得到地址
- 在ntdll.dll 中定义:
如果为应用程序启用了页堆,则应用以下结构。
您可以使用全局标志(gflags.exe)启用页面堆。
-
_DPH_HEAP_ROOT
- 在ntdll.dll 中定义:
dt ntdll!_DPH_HEAP_ROOT
- HeapCreate时产生
!heap -p -all
查看
- 在ntdll.dll 中定义:
-
_DPH_HEAP_BLOCK
- ntdll.dll 中定义
- HeapAlloc产生
!heap -p a-all
查看
得到HeapAlloc的调用者
-
为您的应用程序启用堆栈跟踪和页面堆
使用Global Flags, 在Windows SDK里面有
-
重启应用,WinDbg重新附加进程
-
WinDbg的命令序列:
!heap -p -a <UserAddr>
dt ntdll!_DPH_HEAP_BLOCK StackTrace<MyHeapBlockAddr>
dds <StackTrace>
示例 – 得到HeapAlloc的调用者
先通过!heap -p -all
来找到一个DPH_HEAP_BLOCK,这里得到些结构体的地址是 144e1f3c, UserAddr 是 144e7c00
然后直接查询栈跟踪StackTrace的地址。
打印出栈!
HeapCreate的调用者
与HeapAlloc同理
示例 – 得到HeapCreate的调用者
找到一个DPH_HEAP_ROOT
直接找到跟踪栈
打印跟踪栈,找到源码位置。
排查堆内存泄漏
!address –summary
进程内存使用情况摘要。如果RegionUsageHeap或RegionUsagePageHeap不断增长,那么堆上可能会出现内存泄漏。继续执行以下步骤- 使用Global Flags 开启 stack traces 与 page heap
- 重启应用,重连WinDbg
命令序列:
!heap –stat –h 0
: 将列出每个AllocSize的特定分配统计信息。对于每个AllocSize,都会列出以下内容:AllocSize、#blocks和TotalMem。
!heap –flt –s <size>
!heap -p -a <UserAddr>
示例 – 排查堆内存泄漏
先运行起来查看内存统计
再运行段时间后查看内存统计
对比前后内存在增长, 其中 heap @ 147d0000
从 0x2b 个 块长到 0x40 个块
打印出该堆的信息
抽取其中一个Busy allocations 的UserAddr
通过一系统命令找到源码进而分析。