布拉德·安东尼维奇。
在这一系列的博客文章中,我们已经介绍了如何安装、安装WinDBG,并通过附加到进程和设置断点来开始。我们的下一步是实际的调试部分,我们将逐步通过一个程序并查看内存。
- 第1部分-安装、接口、符号、远程/本地调试、帮助、模块和寄存器
- 第2部分-断点
- 第3部分-检查内存、单步执行程序以及一般提示和技巧
实际上,使用调试器的全部原因是在特定操作或函数期间检查进程的状态。几乎每一条被执行的指令都会以某种方式改变程序的状态,这意味着有能力执行一条指令然后检查状态是非常重要的。第一部分是“步进”-执行指令,然后暂停。WinDBG提供了许多不同的步进命令,这取决于您在程序中的位置和您想去的地方。
大多数调试器使用以下术语描述如何浏览程序及其功能:
- 单步执行-当指令是调用时,跟随调用并在被调用函数的第一条指令处暂停。
call
call
- 单步执行-当指令是调用时,执行函数和所有子函数,在调用后暂停当前函数中的指令。
call
call
- 跳出-执行所有指令,并在当前函数完成后暂停(在当前函数结束时返回)
ret
这里要注意的是,Step-Into和Step-Over都将执行一条指令,而pause-behavior只在到达调用指令时才发生变化。
call
去吧
g(Go)命令更像是一个断点命令,但它的功能模糊了断点和单步执行命令之间的界限。它用于恢复程序的执行,但与大多数步进命令不同,它并不是真正意义上的逐条指令使用。g将继续程序,直到出现断点或异常。实际上,您可以使用g来执行所有的指令直到一个断点,而使用stepping命令执行指令时不设置断点。但是,要澄清的是,调试器在遇到断点时将暂停,而不管您是使用单步执行命令还是g之类的命令。
g
g
g
g
g很容易使用:
g
当程序运行时,WinDBG将在命令输入框中给您一条消息:
如果您知道要执行的地址,请将其作为参数提供给g:
g
单步走
执行一条指令,然后暂停称为单步执行。这可以通过使用“Step Into”或“Step Over”命令来实现,因为这两个命令在非调用指令上的行为相同。与其在这里同时显示它们,不如让我们单独看一下这些命令。
call
踏入
要使用WinDBG,请使用t(Trace)命令。每个步骤都会显示寄存器的状态和将要执行的指令。在这个例子中,我们将暂停在程序的入口点(记事本!WinMainCRTStartup)并查看要执行的前几个指令(u eip)。第一条指令是调用记事本!__安全初始化cookie函数。让我们看看调试器在使用t时的行为:
t
notepad!WinMainCRTStartup
u eip
call
notepad!__security_init_cookie
t
在这里我们可以看到我们在记事本内运行!WinMainCRTStartup,然后在通话时我们用t跟踪通话进入记事本!__security_init_cookie函数,在第一条指令上暂停。
notepad!WinMainCRTStartup
call
t
notepad!__security_init_cookie
跨过
WinDBG使用p命令跳过函数调用。这意味着调用和被调用函数中的所有子指令将被执行,程序将暂停当前函数中的下一条指令(例如记事本!WinMainCRTStartup)。让我们看看相同的场景,但这次我们将使用p:
p
call
notepad!WinMainCRTStartup
p
在这里我们可以看到,指令调用后的记事本!__security_init_cookie是push 58h。当我们使用p时,我们会自动执行记事本中的所有内容!__security_init_cookie函数,然后在其后面的推送处暂停。
call
notepad!__security_init_cookie
push 58h
p
notepad!__security_init_cookie
push
走出去
使用WinDBG跳出可以通过gu(Go Up)命令实现。此命令扫描当前函数以获取ret,然后在执行后暂停。这是一个重要的行为,因为无论出于什么原因,如果函数没有以ret结尾,或者代码路径没有导致ret,那么gu可能会出现意想不到的结果。让我们看看它是什么样子:
gu
ret
ret
gu
我们在记事本上停下来了!WinMainCRTStartup+0x1d,这是对记事本的调用!_小鬼获得创业信息。我们可以看到(u eip L2)调用后的指令是mov dword ptr[ebp-4],0FFFFFFFEh。所以我们将单步(t)进入函数,并在第一条指令处暂停。现在我们使用gu执行子函数中的所有指令和函数调用,然后暂停父函数中的下一条指令,即mov dword ptr[ebp-4],0FFFFFFFEh
notepad!WinMainCRTStartup+0x1d
call
notepad!_imp__GetStartupInfoA
u eip L2
call
mov dword ptr [ebp-4],0FFFFFFFEh
t
gu
mov dword ptr [ebp-4],0FFFFFFFEh
执行到返回
gu是很好的,但有时您希望在函数返回之前查看堆栈,在这个场景中,您需要使用tt(Trace to Next Return)或pt。两者都很容易调用:
gu
tt
pt
这里要记住的重要一点是,tt将在下一次返回时停止,即使它不在当前函数中。例如,考虑下面的伪代码,我们的目标是在func中暂停ret:
tt
ret
func
在这个例子中,如果pause at call somefunc,然后使用tt,我们将在someotherfunc中的ret处结束pause。
call somefunc
tt
ret
someotherfunc
对于这个场景,一个更好的方法可能是使用pt:使用相同的伪代码,如果我们暂停调用somefunc,然后使用pt,我们将执行somefunc中的所有代码(然后是somefunc),然后暂停在func中的ret。实际上,对于这个例子,我们可以使用p,但这并不能说明问题:)
pt
call somefunc
pt
somefunc
someotherfunc
ret
func
p
最终,这取决于作为使用调试器的人,您想要做什么。
现在我们终于可以进入调试的最重要部分:检查内存。WinDBG为此提供d(显示内存)命令。以最简单的形式运行它:
d
但这或多或少是没用的。第一次单独运行d将输出eip指向的内存。这是无用的,因为eip应该指向一个代码段,为了理解这一点,您需要使用u(Unassemble)命令。所以一个更好的启动命令是:
d
eip
eip
u
这将显示堆栈上的值。对于d,WinDBG将使用最后执行的d命令指定的格式显示数据。如果这是您第一次运行d,则它没有存储先前的命令,因此WinDBG将为您提供db(Display Byte)命令的输出。
d
d
d
db
显示字节
db将以字节为单位输出数据,并提供相应的ASCII值:
db
显示文字
单词或2字节值可以用dw(显示单词)显示。或者,可以使用dW显示单词和ASCII值:
dw
dW
显示单词
我最喜欢的内存查看命令是dd(Display DWORDs)。双字是一个双字,所以是4字节。dd将只显示DWORDS,而dc将显示DWORDS和ASCII值:
dd
dd
dc
显示四字
要在WinDBG中显示四字(4字/8字节),请使用dq:
dq
显示位
你甚至可以用dyb显示二进制:
dyb
显示字符串
字符串用da显示,实际上WinDBG将把所有内容都打印为ASCII,直到它达到一个空字节。所以在这里,即使esp不是字符串,它也会将所有内容都视为字符串,直到它达到空值。为了进一步说明这一点,我在esp和db esp L5中打印出了5个字节:
da
esp
esp
db esp L5
寻址
到目前为止,我们只是通过使用esp作为内存检查命令的参数来查看esp指向的内存,但是有许多不同的方法可以在启动时引用内存。
esp
esp
寄存器-如我们所见,您可以使用任何寄存器,WinDBG将使用该寄存器中的地址作为内存地址:
内存地址-您也可以通过提供内存地址来使用它本身:
偏移量-也可以使用数学表达式对寄存器或内存地址使用偏移量:
这些表达式可以在任何可以使用地址的地方使用。以下是WinDBG中的外观:
WinDBG将输出问号(?)用于无效/空闲内存。
?
指针
有时堆栈上的值只是指向另一个位置的指针。如果你想看看这个值,你需要做两个查找。例如,假设我们知道值ebp+4是指向要读取的某些程序集代码的指针。要查看该程序集,需要两个命令。第一个命令显示ebp+4的内存地址:
ebp+4
ebp+4
然后,第二个命令要求我们手动复制该地址的值,然后将其作为参数粘贴到u命令中,以便我们可以查看该程序集:
u
这很好,但是poi()函数有一个更简单的方法。使用poi()我们只提供ebp+4作为参数,它将自动获取该地址的值并使用它,而不是仅使用ebp+4的值:
poi()
poi()
ebp+4
ebp+4
限制输出
默认情况下,WinDBG将输出一组数据,但是我们可以限制如何使用L(大小范围说明符)属性输出该数据。我将处理大多数命令,只需要在结尾附加一个值:
L
L
用L指定的数字是与执行的命令相关的大小。例如,对于db,L表示要打印的字节数,而对于dd,L表示要打印的dword数。
L
db
L
dd
L
这真的是让你开始检查内存-我知道,三篇博客文章建立了这个功能,它只是这个小部分?是的-还有一些内存检查命令,但是要开始,d是核心命令。查看下面的提示了解更多信息。
d
现在您已经脱离实际,让我们看看一些方便的技巧和技巧,它们可以使您的调试体验更好。
键盘快捷键
在调试过程中,您可能会启动和停止应用程序数百次,因此从长远来看,任何一个小的捷径都可以为您解决大量的时间问题。键盘快捷键很大,下面是我使用最多的四个:
- F6-附加到进程。打开“附加”窗口后,使用“结束”键下拉到底部(新启动的应用程序所在的位置)。
- CTRL+E-打开可执行文件
- CTRL+Break-“Break”进入正在运行的应用程序-用于暂停正在运行的程序
- F5-g的快捷方式
g
转换格式
如果你还没有弄清楚,WinDBG默认打印十六进制数字。这意味着12和十进制12不一样。一个快速提示是.formats命令。使用它很简单:
12
.formats
其中值是要转换的内容。因此,格式将接受您提供的任何内容,并以多种格式输出:
.formats
现在我们知道12实际上是18:)。但是,也可以使用0n说明符提供十进制值:
12
0n
数学
有时你可能需要计算一个偏移量或只是做一些基本的数学运算。WinDBG将使用计算表达式?命令:
?
这些表达式可以是简单的,也可以是复杂的,可以包含WinDBG使用的所有标准地址:
…当你有WinDBG时谁需要calc.exe!
calc.exe
为了让生活更轻松,人们为WinDBG创建了许多扩展。这些是很好的小工具,可以在调试器中使用以提供功能。有些甚至是微软制造的。有用的是!堆!地址!dh,还有!佩布。我将在另一篇博客文章中讨论这些和更多内容-请继续关注!
!heap
!address
!dh
!peb
如果你不喜欢,这里有一些非常好的WinDBG命令参考、备忘单和教程。hh,这里有几个好的:
.hh
- http://windbg.info/doc/1-common-cmds.html
- http://www.codeproject.com/Articles/6084/Windows-Debuggers-Part-1-A-WinDbg-Tutorial