Windbg教程-调试非托管程序的基本命令中
前面的文章调试非托管程序的基本命令上讲到如何在windbg里面启动一个程序并且加载调试符号文件。一旦符号文件加载完毕以后,就可以进行调试了,例如设置断点,查看堆栈信息等等。
因为是刚刚启动程序(main函数还没有机会执行),可以查看源代码了解要设置断点的地方。设置断点可以使用bp、bu和bm来做,其中bp可以根据函数名、指令地址以及源代码文件地址来设置断点。
bp命令是在设置断点过程用的比较多的一个命令,下面的表格演示了它的简单用法:
命令格式 | 示例 | 说明 |
bp 函数名 | bp Usage | 在函数Usage的入口中断程序的执行。 |
bp 指令地址 | bp 010113c0 | 在执行地址在010113c0的指令前中断程序的执行。 |
bp `源文件地址` | bp `nativedebug.cpp:21` | 在源代码nativedebug.cpp的第21行设置断点,请注意符号“`”(感叹号键左边的反引号)。
|
如果你有源代码的话,通过windbg的菜单“File”—“Open Source File”打开源文件,找到相应的代码行,按下键盘的F9就可以设置断点了(当然前提条件是你已经设置好正确的符号文件,符号文件请参考文章Visual Studio调试之符号文件)。
看起来好像没有什么特别的,只不过是设置断点的方法比Visual studio复杂一些罢了,不过在windbg中,bp等命令允许在触发断点的时候执行一系列的调试命令。例如中断程序后,打印堆栈,保存内存文件然后退出,或者执行一个小的调试命令脚本程序等等,这个过程与visual studio里面的跟踪断点(Trace Point)非常相像,当然操作起来稍微复杂一些(visual studio的跟踪断点的用法请参考文章Visual Studio调试之断点技巧篇)。 在windbg中设置触发断点执行其他命令的方法会在后续的文章里面讲到。
例如在调试本文的示例程序(示例程序在文章调试非托管程序的基本命令上里面),可以执行以下的命令:
bp Usage
#
# 没有输出结果,正所谓没有消息就是好消息,如果断点成功设置,
# windbg不会显示任何信息。
#
如果在设置断点时,出现类似下面的消息:
bp UsageA
#
# 输出结果
#
Bp expression 'UsageA' could not be resolved, adding deferred bp
那么有两个检查步骤,第一是检查符号文件是否正确加载,第二步是检查设置断点的函数名是否真的存在于程序当中。
第一步,检查符号文件是否正确加载,可以使用lm命令查看已加载模块的详细信息,例如在上面的例子中,我们相信UsageA命令应该在模块nativedebug.exe中,可以执行下面的命令来查看nativedebug模块的详细信息(请注意模块名是紧跟在vm选项后面的,没有空格,没有后缀名,也没有蛀牙):
lm vmnativedebug
#
# 输出结果
#
start end module name
# 注意下面这一行里面的private pdb symbols,说明我们已经加载了正确的符号文件。
# 至于private的含义,会在以后的文章里面讲到。
01000000 0101b000 nativedebug C (private pdb symbols) D:\Debuggers\sym\nativedebug.pdb\E873A517513C4CC9BA5C805D1A709F206\nativedebug.pdb
Loaded symbol image file: nativedebug.exe
# Image path指明了模块加载的路径,在64位机器上调试程序的时候,
#这个信息是蛮有用的 。因为你需要知道一些系统模块是在system32还是
# SysWow64文件夹里加载的。
Image path: nativedebug.exe
Image name: nativedebug.exe
Timestamp: Sat Feb 20 20:05:20 2010 (4B7FD000)
CheckSum: 00000000
ImageSize: 0001B000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
顺便说一下,因为nativedebug是我们自己编译的,有一些版本方面的信息在编译的时候没有加进去。如果你查看一个Windows自带的模块的详细信息的话,你可能会看到类似下面的输出:
lm vmntdll
#
# 输出结果
#
start end module name
775f0000 7772c000 ntdll (pdb symbols) D:\Debuggers\sym\ntdll.pdb\F0164DA71FAF4765B8F3DB4F2D7650EA2\ntdll.pdb
Loaded symbol image file: ntdll.dll
Image path: ntdll.dll
Image name: ntdll.dll
Timestamp: Tue Jul 14 09:09:47 2009 (4A5BDADB)
CheckSum: 0014033F
ImageSize: 0013C000
# 模块的版本号,如果你的程序象微软的产品那样有多个版本,而且需要对多个
# 版本提供技术支持的话,下面的信息对于找到正确版本的符号文件非常非常非常
# 重要。
File version: 6.1.7600.16385
Product version: 6.1.7600.16385
File flags: 0 (Mask 3F)
# 模块要求的子系统
File OS: 40004 NT Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: ntdll.dll
OriginalFilename: ntdll.dll
ProductVersion: 6.1.7600.16385
# 下面只显示了已发布的产品的信息,版本号已经在前面的注释里介绍过了。
# win7_rtm的意思是当前的模块是从win7_rtm这个源代码分支里编译出来的。
# 版本分支的概念在团队软件产品开发过程中是一个平常的做法,大部分版本
# 控制软件都支持代码分支的做法。这个过程解释起来有点复杂,现在你需要
# 知道的是,如果你现在工作的公司没有采取版本分支的做法,那么祝贺你,
# 至少在寻找符号文件的过程里,你会比较轻松(不需要考虑分支的影响),
# 虽然会在后面发布高质量的软件产品你的团队会死的比较难看。
# 如果你工作的公司正在采取版本分支的做法的话,那么你一定要在正确的分支
# 下寻找对应版本的符号文件,否则你会死的很难看。
#
# 另外,下面一行的输出里还有一个重要的信息没有显示,那就是模块是否为调试版
# ,还是发布版。与软件分支一样,如果考虑进去,也是一样无法加载到正确的
# 符号文件的。
#
# 如果使用类似微软的方法编译软件,会在后面的文章中讲到。
FileVersion: 6.1.7600.16385 (win7_rtm.090713-1255)
FileDescription: NT Layer DLL
# 这个嘛,地球人都知道。
LegalCopyright: © Microsoft Corporation. All rights reserved.
既然知道符号文件已经被正确加载,那么下一步就是确认设置的函数名是否存在于模块中,可以使用x命令来检查符号文件保存的名字信息—就是函数名呀,全局变量名之类的信息。如果直接调用x命令,windbg会显示模块里面所有的名字。一般都是使用x加上一个匹配模式来查找指定的名字在模块中是否已定义。比如,为了检查UsageA这个名字在nativedebug.exe模块中是否已定义,可以执行下面的命令来查看(感叹号前面是告诉x命令要在哪一个模块中查找名字,感叹号后面就是要查找的名字):
x nativedebug!UsageA
#
# 输出结果—没有输出结果
#
如果x没有找到指定的名字,就不会输出任何信息,否则,会有类似下面的输出:
x nativedebug!Usage
#
# 输出结果,前面的地址是函数入口在内存中的地址,而后面则显示了函数的声明信息。
#
010113c0 nativedebug!Usage (void)
X命令允许你在查找过程中使用通配符进行匹配,例如,在我们的示例程序中,被用来执行转换的“函数”_ttol不是一个真实的函数,而是一个宏。下面是这个宏的定义:
#ifdef _UNICODE
# define _ttol _wtol
#else
# define _ttol atol
#endif
而宏是在编译期间就被编译器扩展,并不会被加到符号文件中去,因此如果你试图使用bp命令在_ttol入口设置断点的话,是会失败的。因此你可以使用类似下面的通配符来查找正确的函数名:
x MSVCR90D!*tol*
#
# 输出结果(注意黄色高亮的名字)
#
65cd1bb0 MSVCR90D!__STRINGTOLD (struct _LDOUBLE *, char **, char *, int)
65d47c80 MSVCR90D!_ld12told (struct _LDBL12 *, struct _LDOUBLE *)
65cd1900 MSVCR90D!_atoldbl (struct _LDOUBLE *, char *)
65cd6790 MSVCR90D!_wcstol_l (wchar_t *, wchar_t **, int, struct localeinfo_struct *)
65cd4400 MSVCR90D!strtol (char *, char **, int)
65d4bac0 MSVCR90D!__mtold12 (char *, unsigned int, struct _LDBL12 *)
65cd5030 MSVCR90D!_tolower_l (int, struct localeinfo_struct *)
65cd6300 MSVCR90D!wcstol (wchar_t *, wchar_t **, int)
65ca0d50 MSVCR90D!atol (char *)
65cd4940 MSVCR90D!_strtol_l (char *, char **, int, struct localeinfo_struct *)
65ca12d0 MSVCR90D!_wtol (wchar_t *)
65d4a980 MSVCR90D!__wstrgtold12_l (struct _LDBL12 *, wchar_t **, wchar_t *, int, int, int, int, struct localeinfo_struct *)
65d544e0 MSVCR90D!_ftol (void)
65cd5010 MSVCR90D!_tolower (int)
65cd5210 MSVCR90D!tolower (int)
65ca12f0 MSVCR90D!_wtol_l (wchar_t *, struct localeinfo_struct *)
65d48fd0 MSVCR90D!__dtold (struct _LDOUBLE *, double *)
65ce3c30 MSVCR90D!_mbctolower_l (unsigned int, struct localeinfo_struct *)
65d48dc0 MSVCR90D!__STRINGTOLD_L (struct _LDOUBLE *, char **, char *, int, struct localeinfo_struct *)
65cd17f0 MSVCR90D!_atoldbl_l (struct _LDOUBLE *, char *, struct localeinfo_struct *)
65ce3d80 MSVCR90D!_mbctolower (unsigned int)
65ca0d70 MSVCR90D!_atol_l (char *, struct localeinfo_struct *)
65d38670 MSVCR90D!__lc_strtolc (struct tagLC_STRINGS *, char *)
65cdd8e0 MSVCR90D!CPtoLCID (int)
65d47d40 MSVCR90D!__strgtold12_l (struct _LDBL12 *, char **, char *, int, int, int, int, struct localeinfo_struct *)
65c6109c MSVCR90D!_imp__FileTimeToLocalFileTime = <no type information>
在上面的输出,可以看到atol和_wtol在msvcr90d.dll这个模块中都定义了,而我们现在不是很确定当时程序编译的时候,_UNICODE这个宏是否被定义了。因此我们即可以采用一个笨方法,就是使用bp命令在atol和_wtol两个函数入口上都设置断点,运行看看到底程序会中断在哪一个函数上。
或者,可以使用bm命令,bm命令相当于bp命令的扩展,允许用户使用一个通配符设置断点。例如:
bm *tol*
#
# 输出结果 – Windbg会在所有匹配的函数入口上设置断点。
# 很多,的确很多,因此请慎用bm命令。
#
4: 65cd1bb0 @!"MSVCR90D!__STRINGTOLD"
5: 65d47c80 @!"MSVCR90D!_ld12told"
6: 65cd1900 @!"MSVCR90D!_atoldbl"
7: 65cd6790 @!"MSVCR90D!_wcstol_l"
8: 65cd4400 @!"MSVCR90D!strtol"
9: 65d4bac0 @!"MSVCR90D!__mtold12"
10: 65cd5030 @!"MSVCR90D!_tolower_l"
11: 65cd6300 @!"MSVCR90D!wcstol"
12: 65ca0d50 @!"MSVCR90D!atol"
13: 65cd4940 @!"MSVCR90D!_strtol_l"
14: 65ca12d0 @!"MSVCR90D!_wtol"
15: 65d4a980 @!"MSVCR90D!__wstrgtold12_l"
16: 65d544e0 @!"MSVCR90D!_ftol"
17: 65cd5010 @!"MSVCR90D!_tolower"
18: 65cd5210 @!"MSVCR90D!tolower"
19: 65ca12f0 @!"MSVCR90D!_wtol_l"
20: 65d48fd0 @!"MSVCR90D!__dtold"
21: 65ce3c30 @!"MSVCR90D!_mbctolower_l"
22: 65d48dc0 @!"MSVCR90D!__STRINGTOLD_L"
23: 65cd17f0 @!"MSVCR90D!_atoldbl_l"
24: 65ce3d80 @!"MSVCR90D!_mbctolower"
25: 65ca0d70 @!"MSVCR90D!_atol_l"
26: 65d38670 @!"MSVCR90D!__lc_strtolc"
27: 65cdd8e0 @!"MSVCR90D!CPtoLCID"
28: 65d47d40 @!"MSVCR90D!__strgtold12_l"
设置好断点后,可以使用bl命令(breakpoint list)来查看已经设置好的断点:
bl
#
# 输出结果
# 第一列是断点的编号;
# 第二列,e表示(enabled),u表示(unresolved),因此如果那一列的值为e,则说明
# 断点是启用状态,如果为d表示(disabled),则表示禁用状态。如果有u,则基本上
# 说明这个断点是没有设置成功的,虽然windbg会在后续加载每一个模块的时候,都尝试
# 根据那个名字设置断点;
# 后面几列,放在后面的文章讲。
#
0 e 010113c0 0001 (0001) 0:**** nativedebug!Usage
1 eu 0001 (0001) (UsageA)
# 这个断点没有设置正确
2 eu 0001 (0001) (`22`)
4 e 65cd1bb0 0001 (0001) 0:**** MSVCR90D!__STRINGTOLD
5 e 65d47c80 0001 (0001) 0:**** MSVCR90D!_ld12told
6 e 65cd1900 0001 (0001) 0:**** MSVCR90D!_atoldbl
7 e 65cd6790 0001 (0001) 0:**** MSVCR90D!_wcstol_l
8 e 65cd4400 0001 (0001) 0:**** MSVCR90D!strtol
9 e 65d4bac0 0001 (0001) 0:**** MSVCR90D!__mtold12
10 e 65cd5030 0001 (0001) 0:**** MSVCR90D!_tolower_l
11 e 65cd6300 0001 (0001) 0:**** MSVCR90D!wcstol
12 e 65ca0d50 0001 (0001) 0:**** MSVCR90D!atol
13 e 65cd4940 0001 (0001) 0:**** MSVCR90D!_strtol_l
14 e 65ca12d0 0001 (0001) 0:**** MSVCR90D!_wtol
15 e 65d4a980 0001 (0001) 0:**** MSVCR90D!__wstrgtold12_l
16 e 65d544e0 0001 (0001) 0:**** MSVCR90D!_ftol
17 e 65cd5010 0001 (0001) 0:**** MSVCR90D!_tolower
18 e 65cd5210 0001 (0001) 0:**** MSVCR90D!tolower
19 e 65ca12f0 0001 (0001) 0:**** MSVCR90D!_wtol_l
20 e 65d48fd0 0001 (0001) 0:**** MSVCR90D!__dtold
21 e 65ce3c30 0001 (0001) 0:**** MSVCR90D!_mbctolower_l
22 e 65d48dc0 0001 (0001) 0:**** MSVCR90D!__STRINGTOLD_L
23 e 65cd17f0 0001 (0001) 0:**** MSVCR90D!_atoldbl_l
24 e 65ce3d80 0001 (0001) 0:**** MSVCR90D!_mbctolower
25 e 65ca0d70 0001 (0001) 0:**** MSVCR90D!_atol_l
26 e 65d38670 0001 (0001) 0:**** MSVCR90D!__lc_strtolc
27 e 65cdd8e0 0001 (0001) 0:**** MSVCR90D!CPtoLCID
28 e 65d47d40 0001 (0001) 0:**** MSVCR90D!__strgtold12_l
在上面的输出中,可以看到断点1和2是无效的断点,因此可以使用bc(breakpoint clear)这个命令删除掉这两个断点:
bc 1
#
# 没有输出结果—没有消息就是好消息
#
bc 2
#
# 没有输出结果—没有消息就是好消息
#
因此在前面的bm命令中,设置了太多的断点,为了避免在不必要的函数上中断,我们既可以使用bc命令将它们删掉,也可以使用bd(breakpoint disabled)命令将其禁用。因为命令实在太多,所以我们可以使用一个小技巧—使用一个范围来禁用一批断点:
bd 4-10
#
# 没有输出结果—没有消息就是好消息,
# 这个命令将从断点4到断点10的所有断点都禁用了。
#
bd和bc命令的语法是一样的,既可以根据指定的范围禁用或删除一批断点,也可以根据指定的通配符来操作一批断点,还可以使用一种稀奇古怪的语法来操作断点(这个稀奇古怪的语法会在后面的文章中讲到)。
设置好断点以后,可以继续进程的运行了,断点触发以后,我们才能查看进程的堆栈以及一些变量的数据。这些内容放在下一篇文章调试非托管程序的基本命令下讲解。