1. WinDbg 查看内存的情况:
!address -summary : 内存概况,显示当前内存的使用情况
!address : 查看内存的情况,详细情况
!dh [module addres] : 查看模块的内存详细信息
!heap 查看堆的使用情况
!address -f:stack : 查看栈的使用情况
~* kp 可以查看每一个线程的栈的情况
2. 公司符号服务器地址
srv*F:\symbols* http://symbol1.corp.qihoo.net/QihooSymbols/index.php;C:\Windows\System32
3. 内存错误调试一 ---- 栈
1. 通过 dc 将指针的内容打印出来,如果看到了字符串,则可以通过da 或 du 打印出来
2. 通过!address 收集关于内存的信息。其中包括了内存的类型(比如私有内存),保护级别(读取,写入与执行),状态(提交或保留)以及用途
(堆或是栈)
3. dds 命令将内存转储位双字或者字符。有助于将内存与特定的类型联系起来。
4. dpp命令对指针解引用,以双字形式转储内存的内容,如果有一个双字匹配了某个符号,这个符号将被显示。如果指针指向的内存包含了
一个虚函数表,那么它将非常有用。
5. dpa 和 dpu命令将指向的内存分别显示为ASCII码和Unicode格式
6. 如果内存的内容是一个很小的数值(4的倍数),那么它可能是一个句柄,通过扩展命令 !handle 来转储这个句柄的信息。
7. 如果没有任何结果,可以在整个地址空间中搜索这块内存的地址。
内存破坏的检测工具:
针对栈内存的破坏,编译器是比较好的工具,编译器可以在程序中添加对栈进行验证的代码
对于堆内存破坏,最好的工具就是应用程序验证器,Application Verifier。
栈结构:
| 函数参数s |
| 函数调用返回地址 |
---------------------
| 保存ebp |
| 函数内的局部变量 |
前导指令:
8bff mov edi, edi
55 push ebp
8bec mov ebp, esp
后继指令:
8be5 mov esp, ebp
5d pop ebp
c3 ret // 函数调用方式不同,ret 指令也不同
这条指令很奇怪,似乎没有用。
8bff mov edi, edi
一般情况下,这条指令就是一条NOP操作,在特定情况下实现动态修补 Hot Patching。
即对运行中的代码打补丁,这样就无需停机,额可以降低系统的停机时间。
动态修补的基本原理是用一个jmp指令来替代mov edi,edi指令,从而使得程序跳转到执行新的代码。
这个指令就有两个字节,因此是一个短跳转,可以向前或向后跳转127个字节。而在该指令之前,有5个Nop
指令,因此可以将该指令替换为一个短跳转,而将5个Nop指令替换为一个长跳转,突破了跳转的限制。
栈溢出:
当线程覆盖了为其他用途所保留的栈空间时,就是发生了栈溢出(Overflow,也称为上溢)。
栈溢出的种类:
1. 数据复制,造成返回地址等栈信息被覆盖,造成错误。 strcpy函数,在给栈上一个有限大小的字符数组复制字符串时就会发生
2. 局部变量无效:一个函数栈上申请的局部变量被传递给一个线程或函数,当该函数返回后,局部变量无效,其他函数或线程写
该局部变量造成其他的函数崩溃。
3. 调用约定不匹配:调用函数和被调用函数定义的一组规则,双方都需遵守。
__cdecl C/C++的一种调用约定,可以支持变长数量的参数。由调用方清理栈。在函数名字前面加"_"
__stdcall 被调用方清理参数,函数名字前加"_",在后面加"@"以及栈空间所需字节数
__fastcall 快速调用。被调用函数清理栈。前两个参数由ECX,EDX传递,在函数名前面加"@",函数名后面也加"@"以及所需栈空间字节数
__thiscall this指针通过ecx传递,其余参数通过栈传递。被调用函数清理栈。
当栈遭到毁灭性破坏时,通过如下的方式手动分析栈:栈回溯
1. dd esp esp+100 显示当前的栈帧(所有的,如果栈太深,可以用更大数字。)
2. 查看其中的有效地址(在我们程序模块的地址中,lm可以查看我们模块地址范围,然后将在这个范围的地址使用 ln 查看附近的符号)
3. ln addr 来查看这些有效地址附近的一些符号,用于寻找有用的函数调用过程。
这样可以分析出一些有意义的函数调用过程。
同时要查找是什么原因造成了栈的破坏。
u eip 查看当前执行代码是什么
r 查看当前所执行的代码的位置。eip,esp,ebp
4. 内存错误调试二 ---- 堆
堆的管理是由堆管理器完成的,堆管理器分为前端和后端两部分,前端负责分配小的内存块,当前端没有时,再调用后端分配。
如果分配内存较大,则直接放入虚拟分配块列表中,直接分配。
堆的一些信息被放入了 _PEB 进程信息结构体中,其中的ProcessHeaps 项是一个指针数组,其中包含了堆的地址
堆破坏:
扩展命令 !heap 可以有效调试堆破坏情况。
堆破坏的种类:
1. 使用未初始化的状态,例如一个指针未分配内存便使用,造成访问违规。
2. 堆的上溢或下溢:错误的代码覆盖了元数据,堆的完整性被破坏,程序出错。最好的例子就是字符串复制,将一个字符串复制到有限堆空间上。
!heap -s 给出了进程的堆的分配情况
!heap -a addr 显示指定堆的信息,堆列表的信息中可以找到被破坏堆的一些信息。
比较简单的方法是使用 应用程序验证器,它会跟中堆破坏时将使用Heaps测试设置。也称为页堆。
页堆的工作机制是在堆块的周围加上保护层,用来将各个堆块分开,如果堆块被覆盖了,那么它可以在尽量接近
破坏的地方中断程序。
普通堆块和页堆块的区别在于页堆元数据,页堆元数据包含了一定信息,堆块的大小,时机大小,以及栈回溯。
通过这个栈回溯,可以得到该栈分配的操作的完整栈回溯,可以缩小代码肥西范围。
比如HeapAlloc 分配了指针,0019e260,转储页堆元数据内容,首先要减去32(0x20)字节,才能得到元数据。
元数据在内存中设置: 32字节
常规元数据 前置填充 页堆元数据 填充模式
常规元数据 ABCDAAAA 页堆元数据 DCBAAAA // 已分配堆块
常规元数据 ABCDAAA9 页堆元数据 DCBAAA9 // 空闲堆块
页堆元数据 _DPH_BLOCK_INFORMATION
堆 请求大小 实际大小 空闲队列 跟踪索引 栈回溯
使用 dds 命令,可以将"栈回溯" 指向的地址中的栈信息显示出来。
3. 堆句柄不匹配
从两块不同的堆块上分配的内存,释放时,将不同的内存释放到了非分配堆块上,造成错误。
4. 重用已删除的堆块
两次释放同一个堆块,会造成堆管理器的前端的 旁视列表 出现链表循环,造成访问错误。
这种错误只有在再次访问堆管理器前端时,才会出现。
!heap -p -a <heap blockaddr> 通过这个命令查看页堆块的详细信息,找出释放两次的堆块地址。
5. 安全
6. 进程间通信
本地通信分析:
本地通信是通过 LPC通信协议实现的。 LPC通信的调试时通过内核调试实现的。
内核态调试器的扩展命令 !lpc 可以使用这个标识来跟踪调用路径。
通过 !thread扩展命令获取正在处理的LPC请求,与当前请求相对应的消息标识。
!lpc 获取与消息相关的信息。 !lpc message addr
!lpc port <port_id> 获取端口信息
!lpc thread 获取系统上的所有LPC行为
LPC协议的调试功能很强大,当服务器线程处理消息时,客户端线程将被阻塞,可以通过扩展命令 !lpc 分析内核结构发现
7. 资源泄漏
跟踪资源泄漏的工具:任务管理器,性能监视器
任务管理器打开后,选择多列:内存使用,内存使用增量,内存使用高峰值,虚拟内存大小,句柄计数,线程计数,GDI对象
Process Explorer 可以查看进程中打开的资源类型,以及名称,对应的句柄值。
!htrace 命令通过操作系统来跟踪所有打开句柄或者关闭句柄的调用,以及相应的栈回溯。
!htrace -?
!htrace [handle [max_traces]]
!htrace [-enable [max_traces]]
!htrace -disable
!htrace -snapshot
!htrace -diff
内存泄漏:
LeakDiag工具可以监控内存泄漏
!address 命令显示当前进程的内存使用情况。
!heap -s 显示进程的堆使用统计信息
!heap -s addr 显示指定的堆的情况,分配,使用等。
!heap -l 检测泄漏,-l 选项可以让命令以内存回收的形式列举出进程中处于活跃状态,但是没有被引用的内存。
!heap -p -a addr 可以看到堆分配操作的栈回溯。
8. 同步
同步的基础知识:
事件(Event):
使用!handle 命令可以查看当前进程的Handle,以及其对应的类型。
0:001> !handle
Handle 4
Type Directory
Handle 8
Type File
!handle id f 命令来查看句柄的详细信息, id为 !handle 所列举的句柄的值 比如 4 ,8等
临界区(Critical_section):
临界区的内存布局为RTL_CRITICAL_SECTION结构体。
dt RTL_CRITICAL_SECTION 可以查看结构体的成员。
0:001> dt RTL_CRITICAL_SECTION
uxtheme!RTL_CRITICAL_SECTION
+0x000 DebugInfo : Ptr32 _RTL_CRITICAL_SECTION_DEBUG
+0x004 LockCount : Int4B
+0x008 RecursionCount : Int4B
+0x00c OwningThread : Ptr32 Void
+0x010 LockSemaphore : Ptr32 Void
+0x014 SpinCount : Uint4B
DebugInfo是系统分配的结构,包含临界区的信息。
LockCount 表示有多少个线程正在等待进入临界区
RecursionCount 表示统一线程进入临界区多少次
OwningThread 标识拥有该 临界区的线程
LockSemaphore 表示临界区是否空闲,是否可以进入
SpinCount 多CPU系统中,等待进入临界区时会进行spin操作,这个是进入之前进行操作的次数
!cs 命令可以列举出当前进程的所有临界区的信息
互斥体(Mutex):
信号量(Semaphore):
死锁调试:
!runaway 列举出当前所有线程的执行时间
!runaway 7 列举出执行时间的详细信息
!locks 列举出死锁的信息
~*kb 查看当前在进程中运行的所有的线程堆栈
0:002> ~*kb
0 Id: c9c.131c Suspend: 1 Teb: 7ffde000Unfrozen
ChildEBP RetAddr Args toChild
002dfe04 77486a64 77472278 00000474 00000000 ntdll!KiFastSystemCallRet
002dfe08 77472278 00000474 00000000 00000000ntdll!ZwWaitForSingleObject+0xc //等待锁
002dfe6c 7747215c 00000000 00000000 00a7336c ntdll!RtlpWaitOnCriticalSection+0x13e
002dfe94 6ffbc20c 00a7336c 7720c326 0000046cntdll!RtlEnterCriticalSection+0x150
002dfeac 00a710fd 00a7336c 00a733a4 002dff0cvfbasics!AVrfpRtlEnterCriticalSection2+0x4d
002dfec8 00a712b0 00000001 0307bfd0 0302df68 memory1!main+0x9d [c:\users\guotao\desktop\memory1\main.cpp@ 58]
002dff0c 7720ee1c 7ffdf000 002dff58 774a37ebmemory1!__tmainCRTStartup+0x10f[f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 582]
002dff18 774a37eb 7ffdf000 760d4651 00000000kernel32!BaseThreadInitThunk+0xe
002dff58 774a37be 00a713f8 7ffdf000 00000000ntdll!__RtlUserThreadStart+0x70
002dff70 00000000 00a713f8 7ffdf000 00000000ntdll!_RtlUserThreadStart+0x1b
1 Id: c9c.63c Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr Args toChild
032efb34 77486a64 77472278 00000470 00000000 ntdll!KiFastSystemCallRet
032efb38 77472278 00000470 00000000 00000000ntdll!ZwWaitForSingleObject+0xc //等待锁
032efb9c 7747215c 00000000 00000000 00a73384ntdll!RtlpWaitOnCriticalSection+0x13e
032efbc4 6ffbc20c 00a73384 6e5561a2 6ffbc290ntdll!RtlEnterCriticalSection+0x150
032efbdc 00a71031 00a73384 00000000 02e68fe0vfbasics!AVrfpRtlEnterCriticalSection2+0x4d
032efbec 6ffc42f7 00000000 6da35f20 00000000 memory1!ThreadProc+0x31[c:\users\guotao\desktop\memory1\main.cpp @ 19]
032efc24 7720ee1c 02e68fe0 032efc70 774a37ebvfbasics!AVrfpStandardThreadFunction+0x2f
032efc30 774a37eb 02e68fe0 750e4579 00000000kernel32!BaseThreadInitThunk+0xe
032efc70 774a37be 6ffc42c8 02e68fe0 00000000 ntdll!__RtlUserThreadStart+0x70
032efc88 00000000 6ffc42c8 02e68fe0 00000000ntdll!_RtlUserThreadStart+0x1b
很明显两个线程都在等待事件对象,而停止运行了。
!cs addr 可以显示内存处临界区的内容
0:002> !cs 0x00a73384
-----------------------------------------
Critical section = 0x00a73384 (memory1!cs_DB2+0x0)
DebugInfo = 0x02b3afe0
LOCKED
LockCount = 0x1
WaiterWoken = No
OwningThread = 0x0000131c
RecursionCount = 0x1
LockSemaphore = 0x470
SpinCount = 0x00000fa0
该临界区被锁住了,所属的线程id是 131c
!cs -s 显示每个临界区的初始堆栈回溯
!cs -l 仅显示锁定的临界区
!cs -? 显示 !cs 的帮助信息
~~[tid] tid为线程id,查看当前线程等待的锁是什么
会产生死锁的集中情况:
1. 孤立临界区的情况 —— 异常
临界区被放入 try 中,在临界区中的代码发生了异常,这直接导致执行catch代码,而不会执行LeaveCriticalSection的代码
因此调用其他接口时,需要注意是否会出现这种异常而直接跳出的情况。
比较好的解决办法是将进入和离开临界区的代码封装成类对象,声明局部变量,从而实现跳出作用域即解锁。
因此发生异常,出现栈回退时,该局部变量即被销毁,从而解锁。
2. 孤立临界区的情况 —— 线程结束
在线程过程中,进入临界区后,直接调用TerminateThread,退出了线程,而没有机会执行LeaveCriticalSection
3. 动态库
动态库在加载时,Windows为了确保DLL的加载过程与卸载过程的完整性,使用了一个加载器锁来将所有对DLLMain函数的
访问串行化。这么做的目的是防止并发执行DllMain函数可能出现的问题。
这样如果在DllMain函数中调用了线程创建,并等待线程结束,就会出现问题。
当前线程在调用DllMain时,会对加载器锁进行加锁,而当线程创建时,会再一次调用到本DllMain中,
这时就会等待加载器锁的释放,而前一个线程正在等待这个线程结束的事件。造成了死锁。
因此在DllMain中应该做尽量少的工作,尽快返回。
4.竞争锁
出现"锁护送"的问题。
解决的方法就是使用自旋计数。
管理临界区:
使用临界区之前没有初始化
在临界区删除之后,仍然使用它
过度释放临界区
没有充分释放临界区
9. 64位调试:
64位函数调用方式:
一种是类似x86的fastcall调用,rcx,rdx,r8,r9四个寄存器分别保存前四个参数,剩余参数通过压栈传递
新的调试命令:
.effmach x86 转到兼容模式调试,即32位模式调试
!straddr 列举出当前线程的PEB,在兼容模式进程中,每个线程都有两个TEB,64位和32位的。
当前执行指令 依旧 在 $ip中。
x64的栈,依旧是向低地址方向增长,但是在调用函数之前要按照16字节对齐的。
调用者依旧会给这些通过寄存器传递的参数分配空间,但是不会去初始化它,也不会用它。
而被调用函数,可以使用这些参数空间。比如被调用函数中要使用传参寄存器,可以将寄存器值放入这些分配的参数空间中
10. 事后调试
Windbg 生成dump文件:
.dump /mf /m 表示生成mini dump /f 表示包括所有可访问的和已提交的内存页。
.dump /ma filename 生成一个完整的minidump
分析转储文件
windbg -z C:\Awdbin\dumfile.dmp来启动一个dump调试分析