Windbg教程-调试非托管程序的基本命令中

前面的文章调试非托管程序的基本命令上讲到如何在windbg里面启动一个程序并且加载调试符号文件。一旦符号文件加载完毕以后,就可以进行调试了,例如设置断点,查看堆栈信息等等。

 

因为是刚刚启动程序(main函数还没有机会执行),可以查看源代码了解要设置断点的地方。设置断点可以使用bpbubm来做,其中bp可以根据函数名、指令地址以及源代码文件地址来设置断点。

 

bp命令是在设置断点过程用的比较多的一个命令,下面的表格演示了它的简单用法:

命令格式

示例

说明

bp 函数名

bp Usage

在函数Usage的入口中断程序的执行。

bp 指令地址

bp 010113c0

在执行地址在010113c0的指令前中断程序的执行。

bp `源文件地址`

bp `nativedebug.cpp:21`

在源代码nativedebug.cpp的第21行设置断点,请注意符号“`”(感叹号键左边的反引号)。

 

 

                                                                                                                       

如果你有源代码的话,通过windbg的菜单“FileOpen 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_wtolmsvcr90d.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

 

在上面的输出中,可以看到断点12是无效的断点,因此可以使用bcbreakpoint clear)这个命令删除掉这两个断点:

 

bc 1

#

# 没有输出结果没有消息就是好消息

#

bc 2

#

# 没有输出结果没有消息就是好消息

#

 

因此在前面的bm命令中,设置了太多的断点,为了避免在不必要的函数上中断,我们既可以使用bc命令将它们删掉,也可以使用bdbreakpoint disabled)命令将其禁用。因为命令实在太多,所以我们可以使用一个小技巧使用一个范围来禁用一批断点:

 

bd 4-10

#

# 没有输出结果没有消息就是好消息,

# 这个命令将从断点4到断点10的所有断点都禁用了。

#

 

bdbc命令的语法是一样的,既可以根据指定的范围禁用或删除一批断点,也可以根据指定的通配符来操作一批断点,还可以使用一种稀奇古怪的语法来操作断点(这个稀奇古怪的语法会在后面的文章中讲到)。

设置好断点以后,可以继续进程的运行了,断点触发以后,我们才能查看进程的堆栈以及一些变量的数据。这些内容放在下一篇文章调试非托管程序的基本命令下讲解。

posted @ 2010-02-28 15:06  donjuan  阅读(6097)  评论(10编辑  收藏  举报