Windbg 专题学习

 

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调试分析



你可能感兴趣的:(Windbg 专题学习)