windbg - thawk/wiki GitHub Wiki

windbg

目录

1. 基本概念

1.1. 上下文

在内核模式调试时,同时有很多进程、线程有时也有很多用户会话在运行。所以,例如"虚拟地址 0x80002000"或" eax 寄存器"这样的短语都是不明确的。必须指定这些短语所在的上下文,才可以被理解。

调试器在调试时有5种不同的上下文可以设置:

  • 会话上下文(session context指定默认的用户会话。 (该上下文仅在Microsoft Windows XP和之后的Windows中具有。这些系统允许同时有多个登陆会话共存。)

  • 进程上下文(process context) 用于决定调试器如何解释虚拟地址。

  • 用户模式地址上下文(user-mode address context)几乎从来不会直接设置。该上下文在改变进程上下文时自动设置。

  • 寄存器上下文(register context)决定调试器如何 解释寄存器,也用于控制堆栈跟踪的结果。该上下文也称为线程上下文,尽管这个术语不是完全准确。显式上下文( explicit context)也是一种寄存器上下文。如果指定了显式上下文,则它会取代当前的寄存器上下文。

  • 局部上下文(local context)决定调试器如何解释局部变量。该上下文也称为作用域。

1.1.1. 会话上下文

在Windows XP和之后版本的Windows中,同时可以运行多个登陆会话。每个登陆会话都有它自己的进程。

!session扩展命令显示所有登陆会话或改变当前会话上下文。

当!sprocess和!spoolused 扩展命令输入的会话号为"-2"时,会用到会话上下文。

会话上下文改变时,进程上下文会自动切换到该会话的活动进程。

1.1.2. 进程上下文

每个进程都有自己的页目录,用于记录虚拟地址如何映射到物理地址的信息。当一个进程中的任何线程执行时,Windows操作系统使用页目录来解释虚拟地址。

用户模式调试时,当前进程决定了进程上下文。调试器命令、扩展命令和调试信息窗口都会使用当前进程的页目录来解释虚拟地址。

在内核模式调试时,使用.process (Set Process Context) 命令来设置进程上下文。该命令用来选择解释虚拟地址时使用哪个进程的页目录。设置了进程上下文滞后,任何用到地址的命令中都使用该上下文。也可以在该地址中设置断点。通过使用.process 和/i 选项来指定入侵式调试,在内核调试器里面也可以设置用户空间的断点。

也可以在内核空间函数上使用指定进程的断点来在内核调试器中设置用户模式断点。设置这种断点并等待切换到适合的上下文。

用户模式地址上下文是进程上下文的一部分。一般来说不用直接设置用户模式地址上下文。如果设置了进程上下文,用户模式地址上下文会自动改变为使用该进程关联的页表。但是,在基于Itanium的处理器上,一个进程可能拥有多于一个的页目录。这种情况下,可以使用 .context (Set User-Mode Address Context) 命令来改变用户模式地址上下文。

在内核模式调试时设置了进程上下文之后,它会一直保持到使用另一个.process命令改变为止。用户模式地址上下文也会保持到使用.process 或.context 命令改变为止。这些上下文在目标机运行时都不会改变,也不会因为改变寄存器上下文和局部上下文而受影响。

1.1.3. 寄存器上下文

每个线程都有它自己的寄存器值。当线程执行时这些值保存在CPU寄存器中;当其他线程运行时,则保存在内存中。

用户模式调试时,当前线程一般决定了寄存器上下文。所有调试器命令、扩展命令和调试信息窗口中对寄存器的引用都根据当前线程的寄存器来解释。

使用下面的方法之一,在用户模式调试时可以将寄存器上下文改变为当前线程之外的值:

ecxr (Display Exception Context Record)

内核模式调试时,可以使用包括下面一些命令在内的多种命令来控制寄存器上下文:

trap (Display Trap Frame)

这些命令不改变CPU寄存器的值,调试器是从内存位置中获得指定的寄存器上下文。实际上,调试器只能找到已保存的寄存器值。(其他值是动态设置的并且不会保存下来。重建堆栈跟踪使用已保存的值就足够了。)

寄存器上下文设置之后,任何使用寄存器值的命令都会使用新的寄存器上下文,例如k (Display Stack Backtrace)和r (Registers)。

但是,当调试多处理器的计算机时,一些命令可以指定处理器。 (关于这些命令的更多信息,查看多多处理器语法。)如果为一条命令指定了处理器,该命令使用指定的处理器上的激活线程的寄存器上下文,而不是当前寄存器上下文,即使指定的处理器就是活动处理器。

同样,如果寄存器上下文和当前处理器模式设置不匹配,这些命令会产生错误或无意义的输出。要避免这种输出错误,在将处理器模式设置为和寄存器上下文匹配之前,这些依赖寄存器状态的命令都会失败。使用.effmach (Effective Machine) 命令改变处理器模式。

改变寄存器上下文也会改变局部上下文。所以,寄存器上下文会影响局部变量的显示。

如果任何程序发生执行、单步或跟踪的事件,寄存器上下文会立即重设为和程序计数器的位置匹配。在用户模式下,当前进程或线程改变时,寄存器上下文也会重设。

寄存器上下文也作用于堆栈跟踪,因为堆栈跟踪是从堆栈寄存器(基于x86处理器上的esp或基于Itanium处理器上的sp)的指向的位置开始的。如果寄存器上下文设置为非法或不可访问的值,就不能进行堆栈跟踪。

可以使用.apply_dbp (Apply Data Breakpoint to Context)命令在指定的寄存器上下文中设置数据断点。

1.1.4. 局部上下文

程序运行时,局部变量的意义由程序计数器来决定,因为这些变量的作用域仅在定义它们的函数内部。

进行用户模式或内核模式调试时,调试器使用当前函数的作用域(堆栈上的当前帧)作为局部上下文。使用.frame (Set Local Context)命令或者在Calls window中双击需要的帧来改变局部上下文。

在用户模式调试时,局部上下文总是当前线程的堆栈回溯中的一帧。在内核模式调试时,局部上下文总是当前寄存器上下文的线程的堆栈回溯中的一帧。

同一时刻只能使用一个堆栈帧作为局部上下文。位于其他帧中的局部变量不能被访问。

下面这些事件发生时,局部上下文会被重置:

任何程序执行、单步或跟踪的动作
任何命令中使用线程限定符(~)
对寄存器上下文的任何改变

!for_each_frame扩展命令可以对堆栈中的每一个帧使用一条单独的命令。该命令为每一个帧改变局部上下文,执行命令,然后将局部上下文设置为原始值。

2. windbg在64位系统载入32位程序的dump时看不到堆栈信息

!wow64exts.sw

2.1. wow64exts其它功能

unload wow64exts:: [!unload wow64exts] 卸载wow64exts.dll模块
!sw

[!wow64exts.sw] 在多个mode:x86、x64上进行循环切换 注:如果win32程序在x64的mode下,会看到地址是64位的

!k

[!wow64exts.k] 打印32位、64位堆栈

!k 5

[!wow64exts.k 5] 打印32位、64位堆栈,栈帧个数为5

!info

[!wow64exts.info] 输出wow64相关的PEB、TEB和TLS基本信息

!r

[!wow64exts.r] 输出处理器当前上下文信息

!r dumpTest!main

[!wow64exts.r dumpTest!main] 输出main函数地址的上下文信息

3. 表达式

用?计算时,可以用MASM格式表达式。

dwo 地址

取得地址处的dword

poi(地址)

取得地址处的内容(间接引用)

4. 分析

4.1. !analyze

!analyze -v

4.2. .exr

命令显示某个异常记录的内容

.exr Address
.exr -1
Address
    指定异常记录的地址。如果指定–1作为地址,调试器显示最近一次异常。

4.3. .cxr

显示保存在指定地址的上下文信息。也可以设置寄存器上下文

.cxr [Options] [Address]

Options
    可以是下面这些选项的任意组合:

    /f Size
        强制上下文的大小的字节数等于Size。这在当上下文和实际的目标不匹配时有用 — 例如,当通过WOW64调试在x64目标上使用x86上下文时。如果指定了非法的或矛盾的大小,会显示 "Unable to convert context to canonical form" 错误。
    /w
        将当前上下文写入内存,并显示写入的内存的地址。

Address
    系统上下文记录的地址(指向CONTEXT结构)。省略该地址则不会显示任何上下文记录的信息,但是会重置寄存器上下文。

4.4. .exptr

显示一个EXCEPTION_POINTERS结构

.exptr Address

Address
    指定EXCEPTION_POINTERS结构的地址。

4.5. .lastevent

查看最后的事件

4.6. lm (List Loaded Modules)

列出已经载入的模块

5. 符号表相关

5.1. 常用命令

除了使用ld和.reload命令直接加载符号文件,某些使用符号的命令也可以触发调试器来加载符号,如:栈回溯命令(k*)和反汇编命令(u)等。

值得说明的是,windbg缺省使用的是懒惰式符号加载策略,当它收到模块加载事件时,它通常是不会加载符号的,符号状态显示为deferred(延迟加载)。

symopt- flags:: 删除符号选项
!sym noisy

激活详细符号加载(noisy symbol loading)显示

!sym quiet

禁止详细符号加载显示

ld *

为所有模块加载符号

ld kernel32

加载kernel32.dll的符号

.reload

为所有已加载模块载入符号信息

.reload /i

重新加载不匹配符号的模块【dmp文件没有对应的pdb时使用】

.reload /i TGame.exe

重新加载不匹配符号的TGame.exe

.reload /f /v

f:强制立即模式(不允许延迟载入) v:详细模式

.reload /f @"c:\windows\System32\verifier.dll"

为指定模块加载符号信息

.reload /f TGame.exe

为TGame.exe加载符号信息

.reload /u TGame.exe

卸载TGame.exe及其载符号信息

.chain

显示已经加载进来的模块

x *!

列出所有模块对应的符号信息

lm

列出所有模块(加载和未加载)对应的符号信息

lmv

列出所有模块(加载和未加载)对应的符号信息

lmvm ntdll

查看ntdll.dll的详细信息(注意exe、dll等都不要带后缀名)

x ConsoleTest!*

列出ConsoleTest模块中的所有符号

x ConsoleTest!add*

列出ConsoleTest模块中的所有add开头的符号

x /t /v ConsoleTest!*

带数据类型、符号类型和大小信息,列出ConsoleTest模块中的所有符号

x kernel32!LoadLib

列出kernel32模块中所有含LoadLib字样的符号

5.2. 查看一个文件的GUID(用于匹配pdb文件)

DUMPBIN /PDBPATH:VERBOSE filename.exe
!lmi ntdll
!chksym ntdll_777a0000
!itoldyouso ntdll_777a0000

5.3. 检查文件和pdb是否匹配

!itoldyouso tgw d:\tmp\minidumps\tgw.pdb

5.4. symbols目录结构

{可执行文件名}.pdb{PDB_SIG}{AGE}{可执行文件名}.pdb

如mdgw文件:

0:016> !chksym mdgw
*** ERROR: Module load completed but symbols could not be loaded for mdgw.exe

mdgw.exe
    Timestamp: 568B39BE
  SizeOfImage: 5B4000
          pdb: d:\jenkins\jobs\stsv5_cs_trunk_release\workspace\bin\marketdata\mdgw\msvc-11.0\release\debug-symbols-on\link-static\threading-multi\mdgw.pdb
      pdb sig: 6B6C2994-DBBD-4C6F-8C94-D26EB40B7523
          age: C

应存放在symbols目录下的 mdgw.pdb\6B6C2994DBBD4C6F8C94D26EB40B7523c\mdgw.pdb

5.6. x(Examine Symbols)

x命令显示所有上下文中匹配指定模板的符号。

5.6.1. 语法

x [Options] Module!Symbol
x [Options] *

5.6.2. 参数

  • Options

    指定符号搜索选项。可以使用下面这些选项的一个或者多个:

    • /t

      如果知道数据类型的话,显示每个符号的数据类型。

    • /v

      显示每个符号的符号类型(局部、全局、参数或未知) 。该选项还显示每个符号的大小。函数符号的大小是该函数在内存中的大小。其他符号的大小是该符号对应的数据类型的大小。该大小总是以字节为单位并且以16进制形式显示。

    • /s Size

      仅显示以字节为单位的大小等于Size的符号。函数符号的大小是该函数在内存中的大小。其他符号的大小是该符号对应的数据类型的大小。未知大小的符号总是被显示出来。Size不能为0。

    • /q

      以引用的形式显示符号名。

    • /p

      显示函数名和它的参数时省略左括号前的空格。这种类型的输出使得将x的显示复制到其他地方时更容易。

    • /f

      显示函数的数据大小。

    • /d

      显示数据的数据大小。

    • /a

      按照地址以升序的形式排序输出。

    • /A

      按照地址以降序的形式排序输出。

    • /n

      按照名字以升序的形式排序输出。

    • /N

      按照名字以降序的形式排序输出。

    • /z

      按照大小以升序的形式排序输出。

    • /Z

      按照大小以降序的形式排序输出。

  • Module

    指定要搜索的模块。该模块可以是.exe、 .dll、.sys 文件。 Module 可以包含各种通配符和修饰符。该语法的更多信息,查看字符串通配符语法。

  • Symbol

    指定符号必须包含的模板。Symbol可以包含各种通配符和修饰符。该语法的更多信息,查看字符串通配符语法。

    由于该模板用来匹配符号,这种匹配是不区分大小写的,并且开头的下划线(_) 相当于任意多个下划线。在Symbol中可以加入空格,所以可以不通过通配符来指定包含空格的符号名 (例如 "operator new" 或"Template<A, B>")。

5.6.3. 例子

x *!
x /D /f ConsoleApplication1!*

x命令显示指定模块(Module)的公有符号中匹配指定模板(Symbol)的项。例如,下面的命令查找MyModule 中所有包含字符串"spin"的符号。


0:000> x mymodule!spin ---

下面的命令在MyModule 中快速定位"DownloadMinor" 和"DownloadMajor"符号。


0:000> x mymodule!downloadm??or ---

也可以使用下面的命令显示MyModule 中所有符号。


0:000> x mymodule!* ---

前一个命令也会强制调试器重新加载MyModule 的符号信息。如果要重新加载该模块得符号但是不显示那么多信息,可以使用下面的命令。


0:000> x mymodule!start ---

包含"start"的符号较少。因此,上面的命令会显示一些表明命令运行的输出,但是避免了像x mymodule!*一样很长的输出。

显示中包含每个符号的起始地址和完整的符号名。如果符号是一个函数名,还会包含参数类型的列表。如果符号是全局变量,则还会显示它的当前值。

这是另一个x命令的特殊用法。用来显示当前上下文所有局部变量的地址和名字。


0:000> x * ---

Note
大多数情况下如果没有加载私有符号,则不能访问局部变量。关于这种情况的更多信息,查看dbgerr005: Private Symbols Required。要显示局部变量的值,使用dv (Display Local Variables)命令。

下面的示例说明附加的x选项。使用/v选项时,输出的第一列显示符号类型 (local、 global、 parameter、 function或unknown)。第二列是符号地址。第三列是符号大小,以字节为单位。第四列是模块名和符号名。某些情况下,输出后面跟了一个等号 (=)和符号的数据类型。符号的来源 (公有的或完整的符号信息)也会显示出来。


kd> x /v nt!CmType* global 806c9e68 0 nt!CmTypeName = struct _UNICODE_STRING [] global 806c9e68 150 nt!CmTypeName = struct _UNICODE_STRING [42] global 806c9e68 0 nt!CmTypeName = struct _UNICODE_STRING [] global 805bd7b0 0 nt!CmTypeString = unsigned short *[] global 805bd7b0 a8 nt!CmTypeString = unsigned short *[42] ---

上例中,是以16进制给出大小,而数据类型是以10进制形式。因此,上例的最后一行中,数据类型是42个unsigned short整数的数组。数组大小是42*4 = 168,而168的16进制是0xA8。

可以使用/s Size选项来显示大小的字节数为指定值的符号。例如,可以限制上面的命令只显示相应对象的大小为0xA8的符号。


kd> x /v /s a8 nt!CmType* global 805bd7b0 a8 nt!CmTypeString = unsigned short *[42] ---

/t选项使得调试器显示每个符号数据类型的信息。注意对于很多符号,该信息没有/t选项也会显示出来。使用/t时,这些符号的符号信息会被显示两次。


0:001> x prymes!n* 00427d84 myModule!nullstring = 0x00425de8 "(null)" 0042a3c0 myModule!_nstream = 512 Type information missing error for _nh_malloc 004021c1 myModule!MyStructInstance = struct MyStruct 00427d14 myModule!_NLG_Destination = <no type information> ---


0:001> x /t prymes!n* 00427d84 char * myModule!nullstring = 0x00425de8 "(null)" 0042a3c0 int myModule!_nstream = 512 Type information missing error for _nh_malloc 004021c1 struct MyStruct myModule!MyStructInstance = struct MyStruct 00427d14 <NoType> myModule!_NLG_Destination = <no type information> ---

下面的例子说命令过滤模块notepad.exe中的函数时对/f开关的使用。


0:000> x /f /v notepad!main prv func 00000001`00003340 249 notepad!WinMain (struct HINSTANCE *, struct HINSTANCE *, char *, int) prv func 00000001`0000a7b0 1c notepad!WinMainCRTStartup$filt$0 (void) prv func 00000001`0000a540 268 notepad!WinMainCRTStartup (void) ---

5.7. ln(List Nearest Symbols)

查看最靠近一个地址的符号

kd> ln 80136039
(80136039)   NT!_KiServiceExit+0x1e  |  (80136039)   NT!_KiServiceExit2-0x177

6. 注释

6.1. $$ (注释说明符)

$$ 关键字使得后面的文本被忽略掉,直到行末或者碰到分号。分号结束注释;分号后的文本被解析为标准的命令。

6.2. * (注释行说明符)

如果命令开头带星号( * )字符,则行中剩下的部分被当成注释,即使中间有分号。

7. 调用栈相关

7.1. 查看所有线程的栈 ~* kv

~* kv

7.2. k, kb, kd, kp, kP, kv (Display Stack Backtrace)

k*命令显示给定线程的调用堆栈,以及其他相关信息。

[~Thread] k[b|p|P|v] [n] [f] [L] [FrameCount]
[~Thread] k[b|p|P|v] [n] [f] [L] = BasePtr [FrameCount]
[~Thread] k[b|p|P|v] [n] [f] [L] = BasePtr StackPtr InstructionPtr
[~Thread] kd [WordCount]

Thread
    指定要显示调用堆栈的线程。如果省略该参数,则显示当前线程的堆栈。线程语法的更多信息,查看线程语法。仅有用户模式可以指定线程。
Processor
    指定要显示调用堆栈的处理器。处理器语法的更多信息,查看多处理器语法。仅在内核模式下可以指定处理器。
b
    显示传递给堆栈回溯中的每个函数的前三个参数。
p
    显示传递给堆栈回溯中的每个函数的所有参数。参数列表包含参数的数据类型、名字和值。p命令是区分大小写的。使用该参数需要完整符号信息。
P
    和p参数一样,显示传递给堆栈回溯中的每个函数的所有参数。但是,使用P ,函数参数在第二行中显示,而不是作为数据的结尾在行末显示。
v
    显示帧指针省略信息(FPO)。在x86处理器上,还显示调用约定(即__stcdall、__cdecl等等 — 译者注)的信息。
n
    显示帧号码。
f
    显示临近的帧之间的距离。该距离是在实际堆栈中分隔两个帧的字节数。
L
    隐藏源码行信息。L区分大小写。
FrameCount
    指定要显示的堆栈帧号码。如果没有使用n (Set Number Base)命令改变基数的话,需要使用16进制数。如果没有使用.kframes (Set Stack Length)命令改变的话,它的默认值是20(0x14)。
BasePtr
    指定堆栈回溯的基指针。BasePtr 参数仅在命令后有等号(=)时可用。在x86处理器上,BasePtr 之后可以添加一个参数(被当作FrameCount 参数)或两个参数(被当作StackPtr 和InstructionPtr 参数)。
StackPtr
    (仅x86处理器) 指定用于堆栈回溯的堆栈指针。如果省略掉StackPtr 和InstructionPtr ,命令会使用esp寄存器中指定的堆栈指针和eip中指定的指令指针。
InstructionPtr
    (仅x86处理器) 指定用于堆栈回溯的指令指针。如果省略掉StackPtr 和InstructionPtr ,命令会使用esp寄存器中指定的堆栈指针和eip中指定的指令指针。
WordCount
    指定堆栈中要转储的DWORD_PTR 值的个数。如果没有使用.kframes (Set Stack Length)命令修改的话,默认值为20 (0x14)。

7.3. dds, dps, dqs (Display Words and Symbols)

dds、dps和dqs命令显示给定范围内存的内容。该内存被假定为符号表中的一连串地址。相应的符号也会被显示出来。

dds [Options] [Range]
dqs [Options] [Range]
dps [Options] [Range]

Options
    指定一个或多个显示选项。除了不能包含一个以上/p*选项之外,可以包含任何下面的这些选项:

    /c Width
        指定要显示的列的数量。如果省略掉,默认的列数由显示类型决定。由于这些命令显示符号的方法,一般情况下最好使用只有一个数据列的默认值。
    /p
        (仅内核模式) 使用物理内存地址进行显示。Range 指定的范围将被当作物理地址而不是虚拟内存。
    /p[c]
        (仅内核模式) 除了读取高速缓冲存储器(cached memory)中的内存之外,和/p一样。必须包含c两边的中括号。
    /p[uc]
        (仅内核模式) 除了读取非缓存的内存(uncached memory)之外和/p一样。必须包含uc两边的中括号。
    /p[wc]
        (仅内核模式) 除了会读取写聚合内存(write-combined memory)之外,和/p一样。必须包含wc两边的中括号。

Range
    指定要显示的内存区域。语法的详细信息,查看地址和地址区域语法。如果省略掉Range ,命令将会从上一条内存查看命令结束的位置开始。如果Range 省略掉并且之前没有执行过内存显示命令,则从当前指令指针位置开始显示。如果只是简单的给定一个地址,默认的范围长度为128字节。
例子
0:050:x86> dps 07eedb00 07eedb54
07eedb00  07eedb18
07eedb04  777cdea3 ntdll_777a0000!RtlFreeHeap+0x105
07eedb08  0052d630
07eedb0c  0052d62c
07eedb10  0052d608
07eedb14  0052d600
07eedb18  07eedb2c
07eedb1c  763a14d1 kernel32!HeapFree+0x14
07eedb20  00460000
07eedb24  00000000
07eedb28  0052d608
07eedb2c  07eedb40
07eedb30  720bdcc2 MSVCR110!free+0x1a
07eedb34  00460000
07eedb38  00000000
07eedb3c  0052d608
07eedb40  07eedba8
07eedb44  01476aa5 mdgw!boost::log::v2s_mt_nt5::record_view::private_data::destroy+0x45 [d:\jenkins\jobs\stsv5_cs_trunk_release\workspace\lib\cppf\common\3rd\boost_1_59_0\libs\log\src\core.cpp @ 133]
07eedb48  0052d608
07eedb4c  0b8b1eb8
07eedb50  0b8b1eb8
07eedb54  0b8b1eb8
Note

在结果中,看到的函数名为调用者,其下方的几个参数是这个函数调用下一层时push到stack上的参数,并非调用这个函数的参数。

如上面的例子中,07eedb30处看到的free是free调用HeapFree的点,07eedb34-07eedb3c是HeapFree的三个参数。free本身的参数在07eedb48处。

7.4. .frame (Set Local Context)

指定使用哪个局部上下文(作用域)来解析局部变量,或者显示当前的局部上下文。

.frame [/r] [FrameNumber]
.frame [/r] = BasePtr [FrameIncrement]
.frame [/r] = BasePtr StackPtr InstructionPtr

参数
/r
    显示指定的局部上下文的寄存器和其他信息。
FrameNumber
    指定想使用的局部上下文的帧序号。如果该参数为0,则命令指定当前帧。如果省略该参数,则显示当前的局部上下文。
BasePtr
    如果在命令名(.frame)后添加了一个等号(=),指定要用来确认堆栈帧的堆栈回溯基指针。在基于x86的处理器上,在BasePtr后可以添加一个(作为 FrameIncrement)或两个(作为InstructionPtr 和StackPtr)附加的参数。
FrameIncrement
    (仅x86处理器) 指定要越过基指针之后的帧的数目。例如,如果基指针0x0012FF00是第三帧的地址,命令.frame 12ff00 相当于 .frame 3,而.frame 12ff00 2 相当于 .frame 5。
StackPtr
    (仅x86处理器) 指定用来确认帧的堆栈回溯的堆栈指针。如果省略StackPtr 和InstructionPtr,调试器会使用esp寄存器中的堆栈指针和eip中的指令指针。
InstructionPtr
    (仅x86处理器) 指定用来确认帧的堆栈回溯的指令指针。如果省略StackPtr 和InstructionPtr,调试器会使用esp寄存器中的堆栈指针和eip中的指令指针。

8. 处理线程

8.1. ~ (Thread Status)

显示指定线程或当前进程中的所有线程的信息。

0:001> ~

该命令也显示所有线程。

0:001> ~*

下面的命令显示当前活动线程。

0:001> ~.

下面的命令显示原始的产生异常的线程(或调试器附加到进程时活动的线程)。

0:001> ~#

下面显示号码为2的线程。

0:001> ~2

8.2. ~s (Set Current Thread)

命令设置或显示当前线程号。

~Thread s
~ s

Thread
    Specifies the thread to set or display. For more information about the syntax, see Thread Syntax.

9. 查看变量和内存的值

9.1. ? (Evaluate Expression)

(?) 命令可用于计算和显示一个表达式的值。

NOTE

单个问号(?)显示命令帮助。? expression 命令计算给定表达式的值。

? Expression

参数
Expression
    指定要计算的表达式。

9.2. ?? (Evaluate C++ Expression)

双问号(??)命令根据C\++表达式规则来计算和显示表达式的值。

?? Expression

参数
Expression
    指定要求值的pass:[C++]表达式。语法的更多信息,查看pass:[C++]数值和操作符。

9.3. 访问全局变量

9.4. dt (Display Type)

dt命令显示局部变量、全局变量或数据类型的信息。它也可以仅显示数据类型。即结构和联合(union)的信息。

用户模式语法
dt [-DisplayOpts] [-SearchOpts] [module!]Name [[-SearchOpts] Field] [Address] [-l List]
dt [-DisplayOpts] Address [-l List]
dt -h
内核模式语法
[Processor] dt [-DisplayOpts] [-SearchOpts] [module!]Name [[-SearchOpts] Field] [Address] [-l List]
dt [-DisplayOpts] Address [-l List]
dt -h
参数
Processor
    指定运行包含需要的信息的进程的处理器。更多信息,查看多处理器语法。处理器只能在内核模式时指定。

DisplayOpts
    指定下表中给定的一个或多个选项。这些选项以连字符开头。
    选项 	说明
    -a[quantity] 	在新行中显示每个数组成员和它的序号。共显示quantity 个成员。在a和quantity 间不能加入空格。如果-a后面没有跟数字,则数组中的所有成员都会显示出来。-a[quantity]开关必须紧跟在每个希望按这种方式显示的类型名或字段名后面。
    -b 	递归的显示块。如果被显示的结构包含子结构,它的所有层次也会被递归的展开并完整显示出来。指针仅在原始结构中显示出来,而不当作子结构展开。
    -c 	紧凑输出。如果可能的话将所有字段都显示到一行中。(当和-a开关一起使用时,每个数组元素都占据一行而不是被格式化为一个多行的块。)
    -d 	When used with a Name that is ended with an asterisk, display verbose output for all types that begin with Name. If Name does not end with an asterisk, display verbose output.
    -e 	强制dt枚举类型。该选项仅在dt错误的将NAME当作一个实例而不是类型时使用。
    -i 	不对子类型进行缩进。
    -o 	省略掉结构字段的偏移值。
    -p 	Address是一个物理地址,而不是虚拟地址。
    -r[depth] 	递归的转储子类型字段。如果给定了depth ,递归达到一定深度时就会停止。depth 必须是1到9之间的数字,并且在r和depth 之间不能包含空格。-r[depth] 必须紧靠在地址的前面现。
    -s size 	仅列举大小的字节数等于size 值的类型。-s 仅在枚举类型时有用。当指定了-s 时,-e 也被隐含的指定了。
    -t 	仅列举类型。
    -v 	详细输出。这会输出结构的总大小和字段数量这样的附加信息。当它和-y选项一起使用时,所有的符号都会被显示出来,即使他们没有任何关联的类型信息。

SearchOpts
    指定下表中的一个或多个选项。这些选项以连字符开头。
    选项 	说明
    -n 	表明下个参数是一个名字。当下一个参数完全由16进制字符组成时使用,否则它会被当作一个地址。
    -y 	表明下一个参数是一个名字的开头,而不一定是整个名字。包含-y时,所有匹配项都会列出来,并在列表中第一个匹配项后面显示详细信息。如果没有包含-y,只有精确匹配的项会显示。

module
    用于指定定义该结构的模块的可选参数。如果有一个局部变量或类型的名字和全局变量或类型的名字相同,则需要包含module 来表明它是全局变量。否则,dt命令会显示局部变量,即使和局部变量是不区分大小写的匹配而全局变量是区分大小写的匹配。

Name
    指定类型或全局变量的名字。如果Name以星号(*),结尾,所有匹配项都会被列举出来。因此,dt A*会显示所有以"A"开头的数据类型、局部变量和静态变量,但是不会显示这些类型实际的实例。(如果同时使用了-v 显示选项,所有符号都会被显示出来 — 不只是具有关联的类型信息的。)也可以将NAME用一个点号(.)代替来说明想使用上一次用过的 NAME 值。如果NAME包含空格,需要用圆括号括起来。

Field
    指定要显示的一个或多个字段。如果省略掉Field ,则显示所有字段。如果Field 后跟一个点号(.),则该字段的第一级子字段也会被显示出来。如果Field 后跟一系列的点号,则子字段显示的深度等于点号的个数。任何后面跟有点号的字段名都被当作一个前缀匹配,就像使用了-y搜索选项一样。如果Field后面跟一个星号(*),则它仅被当作字段的开头,而不是整个字段,并且所有匹配的字段都会被显示出来。

Address
    指定要显示的结构的地址。如果省略掉Name,则必须包含Address 并且必须指定全局变量的地址。Address如果没有进行其它指示,则被当作一个虚拟地址。使用-p选项来指定物理地址。用"at" 号( @ )来指定寄存器(例如@eax)。

List
    指定连接到一个链表的字段名。必须包含有Address 参数。
Note
  • dt 命令的输出总是用10进制显示有符号数,16进制显示无符号数。

  • 任何NAME或Field之前都可以有-y 和-n选项。-y选项使得可以指定类型或结构名的开始部分。例如,dt -y ALLEN 将显示类型ALLENTOWN 的数据。但是,不能使用dt -y A 来显示ALLENTOWN,而因该使用dt -ny A,因为A是一个合法的16进制值,如果没有-n的话会被当作地址

  • 如果Name指定了一个结构,它的所有字段都会显示出来(例如,dt myStruct)。如果只需要一个特定的字段,可以使用dt myStruct myField。这样会显示C中称为myStruct.myField的成员。但是,注意命令dt myStruct myField1 myField2显示myStruct.myField1和myStruct.myField2,而不是显示myStruct.myField1.myField2。

  • 如果结构名或字段后跟一个下标,则表示一个数组的单个实例。例如,dt myStruct myFieldArray[3] 将显示要查询的数组中第四个成员。但是如果是类型名后跟下标,则指定整个数组。例如,dt CHAR[8] myPtr 将显示一个8字符的字符串。不管当前基数是什么,下表总是10进制数,使用0x前缀会产生错误。

0:000:x86> dt 0x093fa880 sscc::WanM::SslConnectionImpl m_recvBufferPtr m_recvBufferPtr.
mdgw!sscc::WanM::SslConnectionImpl
   +0x3e4 m_recvBufferPtr  : 0x087e4ac0 sscc::WanM::DoubleBufferQueue<sscc::WanM::Unit>
      +0x000 m_mutex          : boost::mutex
      +0x008 m_capacity       : 0x400
      +0x00c m_produceBuffer  : std::vector<sscc::WanM::Unit,std::allocator<sscc::WanM::Unit> >
      +0x018 m_consumeBuffer  : std::vector<sscc::WanM::Unit,std::allocator<sscc::WanM::Unit> >
      +0x024 m_consumeIdx     : 0

10. 内存相关

10.1. s (Search Memory)

s [-[[Flags]Type]] Range Pattern
s -[[Flags]]v Range Object
s -[[Flags]]sa Range
s -[[Flags]]su Range
s -d 0x00000000 L?0xffffffff 30c5bf9c

10.2. !heap

!heap 扩展显示堆使用信息,控制堆管理器中的断点,检测泄露的堆块,搜索堆块或者显示页堆(page heap)信息。

Note
Windows的Heap结构可以参见 Windows Heap相关内容

10.2.1. !heap

!heap -?

显示帮助

!heap

列出Heap的地址

!heap -h

列出Heap的所有Segment

!heap -h [HeapAddr | Idx | 0]

列出指定Heap的所有Seqgment。0=所有Heap

!heap -v [HeapAddr | Idx | 0]

验证指定Heap。0=所有Heap

!heap -s [SummaryOptions] [HeapAddr | 0]

输出中包含指定Heap的摘要信息。0=所有Heap

  • SummaryOptions

    可以是任意下面这些选项。SummaryOptions 不区分大小写。

    -v

    验证所有数据块。

    -b BucketSize

    指定存储单元(bucket)的大小。默认值为1024 bit。

    -d DumpBlockSize

    指定存储单元(bucket)大小。

    -a
    -c

    指示每个块的内容都应该显示出来。

!heap -i [HeapAddr]

指定Heap的详细信息,可以用来decode _HEAP_ENTRY

!heap -x [-v] Address

搜索包含指定地址的Heap。如果添加了-v ,命令会在当前进程的整个虚拟内存空间中搜索

!heap -l

检查泄露的堆块

选项
-a

显示中包含指定堆的所有信息。这种情况下,大小会被四舍五入到堆的分配粒度。(运行!heap -a 选项相当于使用-h -f -m这三个选项,会需要较长时间。)

-f

输出中包含指定堆的所有空闲列表项(free list entries)。

-m

输出中包含指定堆的所有段条目(segment entries)。

-t

使得输出重包含指定堆的标签信息(tag information)。

-T

输出中包含指定堆的伪标签条目(pseudo-tag entries)。

-g

输出中包括全局标签信息(global tag information)。全局标签和每个无标签的分配(untagged allocation)关联。

-k

(仅x86目标) 输出中包含每个条目关联的堆栈回溯。

10.2.2. !heap -b, -B 管理断点

!heap -b [{alloc | realloc | free} [Tag]] [BreakAddress | HeapAddr | Idx | 0]

在堆管理器中设置条件断点。-b 后可跟alloc、realloc或者 free,用于指定断点是否在分配、重新分配或者释放内存时激活。

如果用BreakAddress来指定块的地址,可以省略断点类型。如果指定堆地址或者堆序号,则必须包含类型和Tag参数。

!heap -B [{alloc | realloc | free} [Tag]] [BreakAddress | HeapAddr | Idx | 0]

在堆管理器中移除一个条件断点。必须指定断点类型(alloc、 realloc或 free),并且必须和-b 选项使用的一样。

10.2.3. !heap -flt 过滤要显示的堆。

!heap -flt s Size

限制显示出来的堆必须是指定的大小。

!heap -flt r SizeMin SizeMax

限制显示出来的堆大小在指定范围内。

10.2.4. !heap -stat 显示指定的堆的使用统计。

!heap -stat

显示所有堆的使用统计。

!heap -stat -h [HeapHandle | 0] [-grp GroupBy [MaxDisplay]]

显示句柄为HeapHandle的堆的使用统计。0=所有堆

  • -grp GroupBy

    按照GroupBy 的指定重新排序显示。GroupBy 选项可以是下表中的值。

    A

    根据分配大小显示使用统计。

    B

    根据块数量显示使用统计。

    S

    根据每次分配的总大小显示使用统计。

  • MaxDisplay

    限制输出最多为MaxDisplay 行。

Note
The statistic includes AllocSize, #blocks, TotalMem for each AllocSize.

10.2.5. !heap -srch 显示包含指定模板的堆条目(heap entries)。

!heap -srch [Size] Pattern

显示包含指定模板的堆条目(heap entries)。

  • Pattern

    要查找的内容。

  • Size

    可以是下面选项中的任意一个。用于指定pattern 的大小。必须使用'-'。

    -b

    pattern的大小是一个BYTE。

    -w

    pattern的大小是一个WORD。

    -d

    pattern的大小是一个DWORD。

    -q

    pattern的大小是一个QWORD。

    如果不指定上面任何一个,则会假定pattern的大小和机器的指针大小一致。

10.2.6. !heap -p 显示指定页堆(page heap)信息。

Note
使用任何!heap -p 扩展命令之前,目标进程中必须已经启用了页堆。
!heap -p -?

显示页堆帮助(page heap help),包括堆块的图表。

!heap -p

Summary for NtGlobalFlag, HeapHandle + NormalHeap list **

!heap -p -h HeapHandle

显示句柄为HeapHandle的页堆的详细信息

!heap -p -a UserAddr

显示块中包含Address的页堆。会包含该地址和完整的页堆块的关系的详细信息,如是否该地址是页堆的一部分、它在块中的偏移,以及这个块已经被分配还是空闲的。在可能时还会包含stack backtrace。使用该选项时,显示的大小是堆分配粒度的倍数。

!heap -p -all

显示所有页堆的详细信息。

!heap -t[c|s] [Traces]

显示大量使用堆的用户(heavy heap users)的纪录(collected traces)。Traces指定要显示的纪录数量,默认值为4。如果有比指定的数量更多的纪录,则显示前面的部分纪录。如果使用-t 或者-tc ,则纪录以使用记数(count usage)排序。如果使用-ts ,则纪录以大小排序。 (-tc 和-ts 选项仅在Windows XP中支持,-t选项在Windows XP和之前的版本中都支持。)

!heap -fi [Traces]

显示最近的故障注入纪录(fault injection traces)。 Traces 指定要显示的熟练,默认值为4。

10.2.7. ValidationOptions

!heap [HeapOptions] [ValidationOptions] [Heap]

可以是下面这些选项中的一个。ValidationOptions 区分大小写。

-D

禁止指定堆的调用时验证(validate-on-call)。

-E

启用指定堆的调用时验证(validate-on-call)。

-d

禁用指定堆的堆检查(heap checking)。

-e

启用指定堆的堆检查(heap checking)。

10.2.8. !heap输出中各列的含义

HEAP_ENTRY

Heaps store allocated blocks in contiguous Segments of memory, each allocated block starts with a 8-bytes header followed by the actual allocated data. The HEAP_ENTRY column is the address of the beginning of the header of the allocated block.

Size

The heap manager handles blocks in multiple of 8 bytes. The column is the number of 8 bytes chunk allocated. In your sample, 0044 means that the block takes 0x220 bytes (0x44*8).

Prev

Multiply per 8 to have the negative offset in bytes to the previous heap block.

Flags

This is a bitmask that encodes the following information

0x01 - HEAP_ENTRY_BUSY
0x02 - HEAP_ENTRY_EXTRA_PRESENT
0x04 - HEAP_ENTRY_FILL_PATTERN
0x08 - HEAP_ENTRY_VIRTUAL_ALLOC
0x10 - HEAP_ENTRY_LAST_ENTRY
UserPtr

This is the pointer returned to the application by the HeapAlloc (callbed by malloc/new) function. Since the header is always 8 bytes long, it is always HEAP_ENTRY +8.

UserSize

This is the size passed the HeapAlloc function.

state

This is a decoding of the Flags column, telling if the entry is busy, freed, last of its segment, …

Be aware that in Windows 7/2008 R2, heaps are by default using a front-end named LFH (Low fragmented heap) that uses the default heap manager to allocate chunks in which it dispatched user allocated data. For these heaps, UserPtr and UserSize will not point to real user data. The output of !heap -s displays which heaps are LFH enabled.

10.2.9. 堆标志的含义

internal

对应HEAP_ENTRY_VIRTUAL_ALLOC

10.2.10. 堆分析方法

Who allocated memory - who called HeapAlloc?
  1. Select "Create user mode stack trace database" for your image in GFlags (gflags.exe /i +ust)

  2. From WinDbg’s command line do a !heap -p -a , where is the address of your allocation.

    is usually the address returned by HeapAlloc:
    int AllocSyze = 0x100000; 	// == 1 MB
    BYTE* pUserAddr = (BYTE*) HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, AllocSyze);
    Often any address in the range [UserAddr....UserAddr+AlloSize] is also a valid parameter:
    !heap -p -a [UserAddr....UserAddr+AlloSize]
  3. While !heap -p -a will dump a call-stack, no source information will be included.

  4. To get source information you must additionally enable page heap in step 1 (gflags.exe /i +ust +hpa)

  5. Do a dt ntdll!_DPH_HEAP_BLOCK StackTrace , where is the DPH_HEAP_BLOCK address retrieved in step 3.

  6. Do a dds ", where is the value retrieved in step 5.

  7. Note that dds will dump the stack with source information included.

Who created a heap - who called HeapCreate?
  1. Select "Create user mode stack trace database" and "Enable page heap" for your image in GFlags (gflags.exe /i +ust +hpa)

    1. From WinDbg’s command line do a !heap -p -h , where is the value returned by HeapCreate. You can do a !heap -stat or !heap -p to get all heap handles of your process.

    2. Alternatively you can use !heap -p -all to get addresses of all _DPH_HEAP_ROOT’s of your process directly.

  2. Do a dt ntdll!_DPH_HEAP_ROOT CreateStackTrace , where is the address of a _DPH_HEAP_ROOT retrieved in step 2

  3. Do a dds , where is the value retrieved in step 3.

Finding memory leaks

From WinDbg’s command line do a !address –summary.

If RegionUsageHeap or RegionUsagePageHeap are growing, then you might have a memory leak on the heap. Proceed with the following steps.

  1. Enable "Create user mode stack trace database" for your image in GFlags (gflags.exe /i +ust)

  2. From WinDbg’s command line do a !heap -stat, to get all active heap blocks and their handles.

  3. Do a !heap -stat -h 0. This will list down handle specific allocation statistics for every AllocSize.

  4. For every AllocSize the following is listed: AllocSize, #blocks, and TotalMem. Take the AllocSize with maximum TotalMem.

  5. Do a !heap -flt s . =AllocSize that we determined in the previous step. This command will list down all blocks with that particular size.

  6. Do a !heap -p -a to get the stack trace from where you have allocated that much bytes. Use the that you got in step 4.

  7. To get source information you must additionally enable page heap in step 1 (gflags.exe /i +ust +hpa)

  8. Do a dt ntdll!_DPH_HEAP_BLOCK StackTrace , where is the DPH_HEAP_BLOCK address retrieved in step 5.

  9. Do a dds ", where is the value retrieved in step 7.

    Note that dds will dump the stack with source information included.

10.3. 常见用法

!peb

查看进程的PEB,其中ProcessHeap

dt ntdll!_HEAP

Dump _HEAP structure.

dt _heap 005b0000 -r1

查看堆信息

Note
如果是64bit下的32bit dump,需要通过lm m ntdll*看一下32bit ntdll的模块名称,如ntdll_777a0000,再使用dt ntdll_777a0000!_heap 460000来以32bit解释460000处的HEAP结构
dt _heap_entry 005b8d00

查看堆项目信息

!heap

显示当前进程的堆信息。(仅针对用户模式进程。!pool扩展命令用于系统进程。)

0:000:x86> !heap
        Heap Address      NT/Segment Heap

              460000              NT Heap
              100000              NT Heap
              6d0000              NT Heap
             1280000              NT Heap
              2c0000              NT Heap
             1220000              NT Heap
             1300000              NT Heap
             3240000              NT Heap
!heap -b 和!heap -B

在堆管理器中创建或者删除条件断点。

!heap -l

检查泄露的堆块。它使用一种垃圾回收算法(garbage collector algorithm)来检测没有被进程地址空间中任何地方引用到的已占用块(busy blocks)。对很大的程序,可能需要花费数分钟才能完成。该命令只在Windows XP和之后版本中可以使用。

!heap -x

搜索包含给定地址的堆块。如果使用了-v 选项,该命令还会搜索当前进程的整个虚拟地址空间,以获得指向该堆块的指针。这个命令仅在Windows XP和之后版本中可以使用。

!heap -p

显示各种形式的页堆(page heap)信息。使用!heap -p之前,必须启用目标进程的页堆(page heap)。这可以通过全局标志 (gflags.exe) 实用工具实现。打开该工具,在Image File Name 文本框中填入目标进程的名字,选择Image File Options 以及Enable page heap ,然后点击Apply 。也可以从命令提示符窗口输入gflags /i xxx.exe +hpa来运行全局标志工具,xxx.exe是目标程序的名字。

!heap -p -t[c|s]

在Windows XP之后就不支持了。可以使用调试器工具包中的UMDH工具来获得类似的结果。

!heap -srch [Size] Pattern

显示包含指定模板的堆条目(heap entries)。

!heap -flt

限制只显示分配大小为指定值的堆。该命令仅在Windows XP和之后版本中可用。

!heap -stat

显示堆使用统计。该命令仅在Windows XP和之后版本可用。

!heap -h

List all of the current process heap with start and end addresses.

!heap -h HeapIndex

Display detailed heap information for heap with index HeapIndex.

!heap -s 0

Display summary for all heaps including reserved and committed memory …

!heap -flt s 0x50

Display all of the allocations of size 0x50.

!heap -stat -h address

Display heap usage statistics for HeapHandle is equal to address.

!heap -p -all

Display details of all allocations in all heaps in the process.

!heap -s -v

检测堆问题。reveal a corrupt heap

0:008> !heap -s -v

  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
                (k)     (k)    (k)     (k) length      blocks cont. heap
-----------------------------------------------------------------------------
.ERROR: Block 001842e8 previous size 0 does not match previous block size 4
HEAP 00140000 (Seg 00140640) At 001842e8 Error: invalid block Previous

11. 断点

11.1. bl 列出当前断点

11.2. 设置断点

bp、bu和bm命令设置一个或多个软断点(software breakpoints)。可以组合位置、条件和选项来设置各种不同类型的软断点。 语法

11.2.1. 语法

用户模式
[~Thread] bp[ID] [Options] [Address [Passes]] ["CommandString"]
[~Thread] bu[ID] [Options] [Address [Passes]] ["CommandString"]
[~Thread] bm [Options] SymbolPattern [Passes] ["CommandString"]
内核模式
bp[ID] [Options] [Address [Passes]] ["CommandString"]
bu[ID] [Options] [Address [Passes]] ["CommandString"]
bm [Options] SymbolPattern [Passes] ["CommandString"]

11.2.2. 参数

  • Thread

    指定要应用该断点的线程。该语法的更多信息,查看线程语法。只能在用户模式下指定线程。如果没有指定线程,则断点应用到所有线程。

  • ID

    指定用于标识该断点的十进制数字。

    调试器在创建断点时指派ID,但是之后可以通过br (Breakpoint Renumber)命令来改变它。在其他调试器命令中使用ID来引用断点。要显示断点的ID,可以使用bl (Breakpoint List)命令。

    在命令中使用ID时,不能在命令(bp 或bu)和ID号之间加入空格。

    ID 参数总是可选的。如果不指定ID,调试器使用第一个可用的断点号。内核模式下只能设置32个断点。用户模式下可以设置任意多个断点。但是哪种情况下对ID号的值都没有限制。如果使用中括号([])将ID括起来,ID可以包含任意表达式。该语法的详细信息,查看数值表达式语法。

  • Options

    指定断点选项。除非特别指出,可以设置任意数量的下面的选项:

  • /1

    创建一个一次性("one-shot")断点。该断点触发之后就会被从断点列表中永远去除。

  • /f PredNum

    (仅Itanium、仅用户模式) 指定一个断言号。该断点使用相应的断言寄存器(predicate register)进行判定(例如,bp /f 4 address设置一个使用p4断言寄存器进行判定的断点)。关于断言寄存器的更多信息,查看Itanium结构体系。

  • /p EProcess

    (仅内核模式) 指定一个和该断点关联的进程。EProcess 必须是EPROCESS结构的实际地址,而不是PID。这种断点仅在指定的进程上下文内遇到的时候才会触发。

  • /t EThread

    (仅内核模式) 指定一个和断点关联的线程。EThread必须是ETHREAD结构的实际地址而不是线程ID。这种断点仅在指定的线程上下文内遇到的时候才会触发。如果同时使用/p EProcess 和/t EThread ,它们可以按任意顺序排列。

  • /c MaxCallStackDepth

    使得断点仅当调用堆栈小于MaxCallStackDepth 深度时才激活。不能将此选项和/C 组合使用。

  • /C MinCallStackDepth

    使得断点仅当调用堆栈大于MinCallStackDepth深度时才激活。不能将此选项和/c 组合使用。

  • /a

    (仅bm 使用) 在所有指定位置设置断点,不管他们在数据空间还是代码空间。由于数据上的断点可能造成程序错误,所以只能在确认安全的位置使用该选项。

  • /d

    (仅bm 使用) 将断点位置转换为地址。因此,如果代码位置改变了,这个断点还是保持在原来的位置,而不是像使用SymbolPattern 来设置的一样。使用/d 来避免模块加载或重加载时重新求值对断点进行的改变。

  • /(

    (仅bm 使用) 在SymbolString 定义的符号字符串中包含参数列表信息。

    这个功能使得可以对具有相同名字但是不同参数列表的重载函数设置断点。例如, bm /( myFunc 同时在myFunc(int a)和myFunc(char a)上设置断点。如果没有"/(",对myFunc 设置的断点会失败,因为这样不能确定断点设置到哪一个myFunc 上。

  • Address

    指定要设置断点处的指令的第一个字节位置。如果省略Address ,则使用当前指令指针。该语法的更多信息,查看地址和地址区域语法。

  • Passes

    指定断点激活之前要忽略的次数。调试器跳过该断点指定次数。该数字可以是任何16位或32位值。

    默认情况下,断点在第一次执行断点位置的代码时被激活。这种默认情况和把Passes 设置为1是一样的。要使得断点在程序至少执行该代码一次之后才激活,可以将这个值设置为2或更大。例如,值为2时,使得断点在第二次执行到该代码时被激活。

    该参数创建一个在每次执行断点处的代码时被减少1的计数器。要查看Passes 计数器的初始值和当前值,使用bl (Breakpoint List)。

    Passes 仅当程序响应g (Go)命令并执行通过断点时才减少。单步或跟踪(tracing)通过它是不会减少的。当Passes 到达1时,可以通过清除并重设断点来重置它。

  • CommandString

    指定每次遭遇断点指定次数后需要执行的命令列表。必须将CommandString 放到引号中。使用分号来分隔多条命令。

    CommandString 中的调试器命令可以包含参数。可以使用标准C控制字符(如\n 和\")。二级引号(\")中的分号被当作引号中的字符串的一部分。

    CommandString命令仅当程序响应g (Go)命令并执行通过断点时才会执行。单步或跟踪(tracing)执行断点处的命令时是不会触发的。

    任何在中断后恢复程序运行的命令(如g 或t)都会结束命令列表的执行。

  • SymbolPattern

    指定符号模板。调试器尝试使用已存在的符号来匹配该模板,并在所有匹配项上设置断点。SymbolPattern可以包含各种通配符和修饰符。该语法的更多信息,查看字符串通配符语法。因为这些字符时用来匹配符号的,这种匹配不区分大小写,并且 头部的单个下划线(_)表示任意数量的起始下划线。

11.2.3. 注释

bp、bu和bm 命令设置新断点,但是它们有不同的特点:

  • bp (Set Breakpoint)命令在指定地址的断点位置上设置断点。如果设置断点时调试器还不能将地址表达式计算为断点位置,bp 断点被自动转换为bu 断点。使用bp 命令来设置在模块被卸载之后就不会再被激活的断点。

  • bu (Set Unresolved Breakpoint)命令设置延迟的或未定断点。bu 设置在命令中指定的符号引用的断点位置上(不是一个地址上),并且当所引用的模块能够确定时激活。

  • bm (Set Symbol Breakpoint) 命令在能够匹配指定模板的符号上设置断点。该命令可以设置多于一个的断点。默认情况下,当模板被匹配之后,bm 断点和bu 断点相同。即bm 断点是针对符号引用设置的延迟断点。但是bm /d 命令会创建一个或多个bp断点。每个断点都被设置在能够匹配的位置的地址上,并且不会跟随模块状态改变。

使用bp命令时,断点位置始终被转换成地址。如果bp 断点设置的代码被移动了,该断点仍然保持在相同位置并且可能指向不同的代码或者非法位置。

相反的,bu 断点始终和命令指定的符号化的断点位置关联(一般是符号加上一个可选的偏移)。这种关联在符号的值改变或者包含该位置的模块加载或卸载之后仍然保持。

使用bp 设置的断点会保持到使用bc (Breakpoint Clear)命令或WinDbg的Breakpoints 对话框移除为止。但是,因为这些断点指向的是一个地址,bp 断点在包含所引用的位置的模块卸载之后就不再有效了。

bu 设置的断点会保存在WinDbg工作空间中,但是bp 设置的断点不会保存。

当使用鼠标在WinDbg的反汇编窗口或源码窗口中设置断点时,调试器创建bu断点。

bm 在想使用包含通配符的符号模板来设置断点时很有用。bm SymbolPattern 语法和使用x SymbolPattern然后对搜索结果使用bu 是一样的。例如,要在Myprogram 模块中所有以字符串"mem"开头的符号上设置断点,可以使用如下命令。

0:000> bm myprogram!mem*
  4: 0040d070 MyProgram!memcpy
  5: 0040c560 MyProgram!memmove
  6: 00408960 MyProgram!memset

由于bm命令设置软断点(不是处理器断点),它会自动避开数据位置,以避免破坏数据。

但是,当使用bp 和bm /a时要小心。这些命令可以在数据段中设置软断点。调试器在代码上设置软断点时,会将处理器指令替换为中断指令。但是当调试器在数据段设置软断点时,会把数据替换为中断指令。这种中断指令会破坏数据。只有在只当作代码进行执行的数据上设置软件断点才是安全的。要在数据段设置断点,使用ba (Break on Access)命令。该命令可以设置数据断点而不是软件断点。

要在例如C++公有类这样的任意文本上设置断点,或者在operator new 函数上设置断点,需要将表达式括在园括号中。例如,使用bp (??MyPublic) 或bp (operator new)。

要在MASM表达式类型的任意文本上设置断点,使用bu @!"text"。要使用C++语法文本设置断点,对C++兼容的符号使用bu @@C++(text)。

Bp、bu和bm命令通过将处理器命令替换为中断指令来设置软断点。要调试只读代码或不能改变的代码,使用ba e 命令,e用于设置执行访问。

如果单个逻辑代码行跨越了多个物理行,断点设置在语句或调用的最后一个物理行上。如果调试器在要求的位置不能设置断点,则会将断点放到下一个可用的位置上。

如果指定了Thread,断点设置在指定线程上。例如,~*bp在所有线程上设置断点, ~#bp在产生当前异常的线程上设置,而 ~123bp 在线程123上设置。~bp 和~.bp 命令都在当前线程设置断点。

在内核模是下调试多处理器系统时,使用bp 或ba (Break on Access)设置的断点会应用到所有处理器。例如,如果当前处理器是3,并且输入bp MemoryAddress来在 MemoryAddress上设置了断点。任何执行到该地址的处理器(不止是处理器3)都会产生断点陷阱。 示例

11.2.4. 例子

下面的例子说明了如何使用不bp命令。这个命令在MyTest 函数后面12字节位置处设置断点。该断点忽略前6次对指定代码的执行,但是第7次执行该代码时会中断下来。

0:000> bp MyTest+0xb 7

下面的命令在RtlRaiseException 上设置断点,显示eax寄存器并显示符号MyVar 的值,然后继续。

kd> bp ntdll!RtlRaiseException "r eax; dt MyVar; g"

在源文件指定行号上设置断点。

bu `module_name!file.cpp:206`

11.3. bc 清除断点

11.4. bd 禁用断点

12. 调试命令

12.1. .restart

12.2. g (Go)

12.3. gc (Go from Conditional Breakpoint)

12.4. gh (Go with Exception Handled)

12.5. gn (Go with Exception Not Handled)

12.6. gu (Go Up)

12.7. p (Step)

12.8. pa (Step to Address)

12.9. pc (Step to Next Call)

12.10. pct (Step to Next Call or Return)

12.11. ph (Step to Next Branching Instruction)

12.12. pt (Step to Next Return)

12.13. t (Trace)

12.14. ta (Trace to Address)

12.15. tb (Trace to Next Branch)

12.16. tc (Trace to Next Call)

12.17. tct (Trace to Next Call or Return)

12.18. th (Trace to Next Branching Instruction)

12.19. tt (Trace to Next Return)

12.20. wt (Trace and Watch Data)

13. 常用技巧

13.1. 调用约定

13.1.1. ?

  • EBX用于存放this指针??

13.1.2. cdecl

cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其内容包括:

  1. 函数实参在线程栈上按照从右至左的顺序依次压栈。

  2. 函数结果保存在寄存器EAX/AX/AL中

  3. 浮点型结果存放在寄存器ST0中

  4. 编译后的函数名前缀以一个下划线字符

  5. 调用者负责从线程栈中弹出实参(即清栈)

  6. 8比特或者16比特长的整形实参提升为32比特长。

  7. 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS

  8. 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS

  9. RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)

Visual C++规定函数返回至如果是POD值且长度如果不超过32比特,用寄存器EAX传递;长度在33-64比特范围内,用寄存器EAX:EDX传递;长度超过64比特或者非POD值,则调用者为函数返回值预先分配一个空间,把该空间的地址作为隐式参数传递给被调函数。

GCC的函数返回至都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAX。GCC自4.5版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。

cdecl调用约定通常作为x86 C编译器的默认调用规则。

13.1.3. thiscall

在调用C++非静态成员函数时使用此约定。基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall。 对于GCC编译器,thiscall几乎与cdecl等同:调用者清理堆栈,参数从右到左传递。差别在于this指针,thiscall会在最后把this指针推入栈中,即相当于在函数原型中是隐式的左数第一个参数。

在微软Visual C编译器中,this指针通过ECX寄存器传递,其余同cdecl约定。当函数使用可变参数,此时调用者负责清理堆栈(参考cdecl)。thiscall约定只在微软Visual C 2005及其之后的版本被显式指定。其他编译器中,thiscall并不是一个关键字(反汇编器如IDA使用__thiscall)。

13.2. 判断ntdll是64bit还是32bit

32位
0:000:x86> dt ntdll!_LIST_ENTRY
+0x000 Flink : Ptr32 _LIST_ENTRY
+0x004 Blink : Ptr32 _LIST_ENTRY
64位
0:000:x86> dt ntdll!_LIST_ENTRY
+0x000 Flink            : Ptr64 _LIST_ENTRY
+0x008 Blink            : Ptr64 _LIST_ENTRY

13.3. 查看VC使用哪个堆

x msvcrt!_crtheap

14. 异常分析技巧

14.1. 找出UnhandledExceptionFilter()的异常栈

LONG WINAPI UnhandledExceptionFilter(
  _In_ struct _EXCEPTION_POINTERS *ExceptionInfo
);

typedef struct _EXCEPTION_POINTERS
{
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
  1. 执行~*kb,列出所有线程

  2. 找出调用Kernel32!UnhandledExceptionFilter的线程

    120  id: f0f0f0f0.a1c   Suspend: 1 Teb 7ff72000 Unfrozen
    ChildEBP RetAddr  Args to Child
    09a8f334 77eb9b46 0000244c 00000001 00000000 ntdll!ZwWaitForSingleObject+0xb [i386\usrstubs.asm @ 2004]
    09a8f644 77ea7e7a 09a8f66c 77e861ae 09a8f674 KERNEL32!UnhandledExceptionFilter+0x2b5
    [D:\nt\private\windows\base\client\thread.c @ 1753]
    09a8ffec 00000000 787bf0b8 0216fe94 00000000 KERNEL32!BaseThreadStart+0x65 [D:\nt\private\windows\base\client\support.c @ 453]
  3. 切换到有问题的线程~120s

  4. 对Kernel32!UnhandledExceptionFilter的第一个参数进行dd,显示EXCEPTION_POINTERS结构的内容

    0:120> dd 09a8f66c
    09a8f66c  09a8f738 09a8f754 09a8f698 77f8f45c
    09a8f67c  09a8f738 09a8ffdc 09a8f754 09a8f710
    09a8f68c  09a8ffdc 77f8f5b5 09a8ffdc 09a8f720
    09a8f69c  77f8f3fa 09a8f738 09a8ffdc 09a8f754
    09a8f6ac  09a8f710 77e8615b 09a8fad4 00000000
    09a8f6bc  09a8f738 74a25336 09a8f6e0 09a8f910
    09a8f6cc  01dc8ad8 0d788918 00000001 018d1f28
    09a8f6dc  00000001 61746164 7073612e 09a8f71c
  5. 第一个DWORD是exception record,可以用.exr命令进行查看

    0:120> .exr 09a8f738
    ExceptionAddress: 78011f32 (MSVCRT!strnicmp+0x00000092)
       ExceptionCode: c0000005
      ExceptionFlags: 00000000
    NumberParameters: 2
       Parameter[0]: 00000000
       Parameter[1]: 00000000
    Attempt to read from address 00000000
  6. 第二个DWORD是context record,可以用.cxr命令进行查看

    0:120> .cxr 09a8f754
    eax=027470ff ebx=7803cb28 ecx=00000000 edx=00000000 esi=00000000 edi=09a8fad4
    eip=78011f32 esp=09a8fa20 ebp=09a8fa2c iopl=0         nv up ei ng nz na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010286
    MSVCRT!strnicmp+92:
    78011f32 8a06             mov     al,[esi]
  7. 通过kv命令查看发生异常是堆栈

    0:120> kv
    ChildEBP RetAddr  Args to Child
    WARNING: Stack unwind information not available. Following frames may be wrong.
    09a8fa2c 780119ab 09a8fad4 00000000 09a8faa8 MSVCRT!strnicmp+0x92
    09a8fa40 7801197c 09a8fad4 00000000 6d7044fd MSVCRT!stricmp+0x3c
    09a8fa80 6e5a6ef6 09a8fad4 2193d68d 00e5e298 MSVCRT!stricmp+0xd
    09a8fa94 6d7043bf 09a8fad4 09a8faa8 0000001c IisRTL!CLKRHashTable::FindKey+0x59 (FPO: [2,0,1])
    09a8faac 749fc22d 09a8fad4 01d553b0 0000001c ISATQ!CDirMonitor::FindEntry+0x1e
    (FPO: [Non-Fpo]) [D:\nt\private\inet\iis\svcs\infocomm\atq\dirmon.cpp @ 884]
    09a8fac4 749fd1cb 09a8fad4 09a8fb10 525c3a46 asp!RegisterASPDirMonitorEntry+0x6e
    ...

14.2. 通过KiUserExceptionDispatcher找出相关信息

KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )

QUESTION: 有可能用的是PASCAL调用约定?

0:050:x86> kv
07eed780 01eed794 07eed7e4 07eed794 07eed7e4 ntdll_777a0000!KiUserExceptionDispatcher+0xf (FPO: [2,0,0])

0:050:x86> .exr 07eed794
ExceptionAddress: 00000000777ce23e (ntdll_777a0000!RtlpLowFragHeapFree+0x0000000000000031)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 0000000036e88950
Attempt to read from address 0000000036e88950
0:050:x86> .cxr 07eed7e4
eax=00000000 ebx=0052d608 ecx=00460000 edx=0052d608 esi=36e8894c edi=0052d600
eip=777ce23e esp=07eedacc ebp=07eedb00 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010202
ntdll_777a0000!RtlpLowFragHeapFree+0x31:
777ce23e 8b4604          mov     eax,dword ptr [esi+4] ds:002b:36e88950=????????

15. 配置

15.1. 自动下载调试符号

可以在线下载需要的调试符号,可以设置环境变量或在windbg中的symbols path进行设置

  1. 设置环境变量

    set _NT_SYMBOL_PATH = srv*DownstreamStore*SymbolStoreLocation
  2. windbg中可以把srv*DownstreamStore*SymbolStoreLocation加入symbols path中

其中,

set _NT_SYMBOL_PATH=srv*d:\symbols*https://msdl.microsoft.com/download/symbols

16. 脚本

16.1. 显示模块中所有结构

dt ntdll_779d0000!* -d -b
.foreach(s {dt verifier!*}) {.echo s; dt -b ${s}}
.foreach(s {s -[1]b 0000000 L?100000000 d6 52 00}) {dd ${s}-1 L1}

16.2. 遍历列表

r $t0=16a23138;
r $t1=@$t0;
r $t9=@@c++(#FIELD_OFFSET(verifier_6f210000!_DPH_HEAP_BLOCK, AdjacencyEntry));
.do {
    r $t1;
    dt verifier_6f210000!_DPH_HEAP_BLOCK @$t1;
    dd @$t1+@@c++(#FIELD_OFFSET(verifier_6f210000!_DPH_HEAP_BLOCK, Flags)) L1;
    r $t1=poi(@$t1+@$t9)-@$t9;
} (@$t1!= @$t0)

17. FAQ

Your debugger is not using the correct symbols …​ Type referenced: wow64!_TEB32
  • Define _TEB32 in a file (say teb.h)

  • cl -c -Fd<fullpath of the ACTUAL wow64.pdb, whatever it is called> -TP teb.h

18. 参考文章

⚠️ **GitHub.com Fallback** ⚠️