Windbg 教程-调试非托管程序的基本命令下
前面的文章调试非托管程序的基本命令中讲到如何使用windbg在程序中设置断点,既然断点已经设置好了,下一步就是直接执行程序,程序中断以后,第一件事情就是查看堆栈。在windbg中查看堆栈使用k命令就可以,在最新的windbg中(记不得从哪个版本开始了),如果你加载了sos.dll,k命令也可以显示托管程序部分的堆栈信息。下面是程序在_ttol函数入口处中断时的堆栈输出:
0:000> k # # ChildEBP和RetAddr的意思在后面讲解到调用规范(calling convention)的 # 时候会提到 # ChildEBP RetAddr 0019fda4 012b1464 MSVCR90D!_wtol [f:\dd\vctools\crt_bld\self_x86\crt\src\atox.c @ 55] # # 如果模块的私有符号文件被加载的话,那么k命令可以显示函数所在的源文件 # 的名称和行号,这个信息在调试过程中对找源文件帮助是很大的。 # 0019fe88 012b1a88 nativedebug!wmain+0x44 [e:\临时文档\windbg教程\nativedebug\nativedebug.cpp @ 25] 0019fed8 012b18cf nativedebug!__tmainCRTStartup+0x1a8 [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 583] 0019fee0 76761194 nativedebug!wmainCRTStartup+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 403] # # 下面的函数说明我们没有加载到私有符号文件,最多只加载了公开符号文件 # 因此有可能看不到变量的详细信息。如果真的要看变量的值的话,只能通过 # 反汇编和自己的一些经验技巧来获取了。 # 0019feec 7715b3f5 kernel32!BaseThreadInitThunk+0xe 0019ff2c 7715b3c8 ntdll!__RtlUserThreadStart+0x70 0019ff44 00000000 ntdll!_RtlUserThreadStart+0x1b |
当然啦,k命令的输出太简单了,所以windbg提供了几个辅助命令,kp,kP, kn,kM以及其他一些辅助命令。Windbg为了节省程序员在调试中输入过长的命令,一般都采用类似unix命令的风格,大多数都采取简写(扩展命令除外)。例如k是callstack的简写(c的发音是k),bp是breakpoint的简写,bu是break unresolved的意思,bm是break matches,等等。
我个人经常用的命令是kP,kn以及kM,kp/P(callstack with parameters),用来在显示堆栈的同时,打印各个函数参数的值;
0:000> kp ChildEBP RetAddr # # 打印每个参数的值,只有在有私有符号文件的情况下才有用。 # 0019fda4 012b1464 MSVCR90D!_wtol(wchar_t * nptr = 0x000813c2 "123432") [f:\dd\vctools\crt_bld\self_x86\crt\src\atox.c @ 55] 0019fe88 012b1a88 nativedebug!wmain(int argc = 2, unsigned short ** argv = 0x00081350)+0x44 [e:\临时文档\windbg教程\nativedebug\nativedebug.cpp @ 25] 0019fed8 012b18cf nativedebug!__tmainCRTStartup(void)+0x1a8 [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 583] 0019fee0 76761194 nativedebug!wmainCRTStartup(void)+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 403] # # 没有私有符号文件的话,又不懂汇编的话,那就只能放弃了 # 0019feec 7715b3f5 kernel32!BaseThreadInitThunk+0xe 0019ff2c 7715b3c8 ntdll!__RtlUserThreadStart+0x70 0019ff44 00000000 ntdll!_RtlUserThreadStart+0x1b |
大写的P打印的格式好看一些。
0:000> kP ChildEBP RetAddr 0019fda4 012b1464 MSVCR90D!_wtol( wchar_t * nptr = 0x000813c2 "123432") [f:\dd\vctools\crt_bld\self_x86\crt\src\atox.c @ 55] 0019fe88 012b1a88 nativedebug!wmain( int argc = 2, unsigned short ** argv = 0x00081350)+0x44 [e:\临时文档\windbg教程\nativedebug\nativedebug.cpp @ 25] 0019fed8 012b18cf nativedebug!__tmainCRTStartup(void)+0x1a8 [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 583] 0019fee0 76761194 nativedebug!wmainCRTStartup(void)+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 403] 0019feec 7715b3f5 kernel32!BaseThreadInitThunk+0xe 0019ff2c 7715b3c8 ntdll!__RtlUserThreadStart+0x70 0019ff44 00000000 ntdll!_RtlUserThreadStart+0x1b |
分析了堆栈后,一般都要去上几层函数分析一下,以便了解当前函数被传入错误参数值的原因。在windbg中,在堆栈中切换到不同的函数,需要用到.frame命令(注意前面的点号)。你需要告诉.frame命令,希望查看哪一个函数的信息,因此你要给.frame命令提供函数的索引值,这个索引值可以通过kn(callstack with index number)命令获取。
0:000> kn # ChildEBP RetAddr # # 注意前面黄色高亮显示的索引号,是从0开始索引的,最近一次调用的函数的索引 # 是0 # 00 0019fda4 012b1464 MSVCR90D!_wtol+0x5 [f:\dd\vctools\crt_bld\self_x86\crt\src\atox.c @ 56] 01 0019fe88 012b1a88 nativedebug!wmain+0x44 [e:\临时文档\windbg教程\nativedebug\nativedebug.cpp @ 25] 02 0019fed8 012b18cf nativedebug!__tmainCRTStartup+0x1a8 [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 583] 03 0019fee0 76761194 nativedebug!wmainCRTStartup+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 403] 04 0019feec 7715b3f5 kernel32!BaseThreadInitThunk+0xe 05 0019ff2c 7715b3c8 ntdll!__RtlUserThreadStart+0x70 06 0019ff44 00000000 ntdll!_RtlUserThreadStart+0x1b |
在上例中,假设要查看wmain函数里面的各个局部变量的情况,则使用1作为索引号输入给.frame命令。
0:000> .frame 1 # # 告诉你切换已经成功,显示1所代表的函数名 # 01 0019fe88 012b1a88 nativedebug!wmain+0x44 [e:\临时文档\windbg教程\nativedebug\nativedebug.cpp @ 25] |
切换了函数以后,下一步就是查看变量的值,使用dv(display variables)命令来查看变量信息,这个命令相当于Visual Studio里面的局部变量(locals)窗口。
0:000> dv argc = 2 argv = 0x00081350 result = 0 |
因为查看函数在堆栈中的索引值,使用.frame命令在堆栈中切换函数,以及显示函数的局部变量这一个操作实在是太普遍了,因此windbg提供了一个快捷命令,kM(callstack with markup)。这个命令提供了一个类似html网页超链接的形式,供程序员在堆栈中快速切换函数并且显示变量值,操作方式让我想起了linux下面那个控制台界面的浏览器lynx。下面是kM命令的输出:
对于简单类型,例如整型、浮点型甚至是字符串,windbg可以直接显示出变量的值。但是对于一些复杂类型,例如数组,结构,类呀,那就需要借助另外一个命令dt(display type)了。下面的命令检查了argv类型信息,以及它的值。
0:000> dt argv # # unsigned short,在C和C++程序中,一般都意味着是wchar_t(宽字符)类型 # **表示是一个包含宽字符字符串(第一个星号)的数组(第二个星号,在C和C++中 # 数组和指针可以用相同的方法表示 )。 # Local var @ 0x19fe94 Type unsigned short** 0x00081350 -> 0x0008135c -> 0x45 |
既然已经知道argv是一个保存指针的数组,下一步就是继续查看argv数组里面的内容,根据前面的dv打印的结果,我们知道argc(也就是说明argv数组元素个数的参数)的值是2。在一台32位机(或者是在64位机器上调试一个32位的程序),使用dd(display by double-word)命令参看argv的内存,以四字节的形式显示,因为32位机器上,指针使用4个字节。如果你的机器是64位机,那就需要使用dq(display by quad-word)命令以8个字节的形式打印内存了。
以32位机作为例子,可以只给dd命令你要查看的内存地址(既可以是变量名,也可以直是内存地址),让dd命令自己控制显示内存的范围(默认是显示32个dword,也就是128个字节的内存内容)。
0:000> dd argv # # 将argv传给dd命令的时候, windbg是先将argv转换成保存 # 数组指针的地址(就是0019fe94)—毕竟数组的指针也是需要地方保存的嘛。 # 而高亮显示的00081350才是保存argv数组内容的真实地址 # 0019fe94 00081350 00081410 667a0491 00000000 0019fea4 00000000 7ffda000 001ac17a 00000000 0019feb4 00000000 001a0000 00000000 0019fe9c 0019fec4 0000001d 0019ff1c 012b1087 67489169 0019fed4 00000000 0019fee0 012b18cf 0019feec 0019fee4 76761194 7ffda000 0019ff2c 7715b3f5 0019fef4 7ffda000 7346f628 00000000 00000000 0019ff04 7ffda000 00000000 00000000 00000000 0:000> dd 0x00081350 # # 下面两个高亮的地址,就是数组保存的字符串指针 # 00081350 0008135c 000813c2 00000000 003a0045 00081360 4e34005c 658765f6 005c6863 00690057 00081370 0064006e 00670062 7a0b6559 006e005c 00081380 00740061 00760069 00640065 00620065 00081390 00670075 0044005c 00620065 00670075 000813a0 006e005c 00740061 00760069 00640065 000813b0 00620065 00670075 0065002e 00650078 000813c0 00310000 00330032 00330034 00000032 |
在上例中,既然我们已经知道argv数组的大小是2的话,你也可以将这个信息提供给dd命令,告诉它你只需要显示argv指针所指向的内存的两个元素就可以了,下面这个命令显示了这种用法。
# # L2 (Length)这个选项告诉了dd命令,只要显示两个内容就足够了。 #至于这个选项的其他用法,要在讲到windbg中地址范围的语法时才能解释了。 # 0:000> dd 0x00081350 L2 00081350 0008135c 000813c2 |
执行了这么多命令以后,我们终于可以看到argv[0]和argv[1]的值了,不容易呀!既然已经知道是unicode字符串,使用du(display unicode)命令就可以显示完整的字符串内容了。
0:000> du 0008135c # # 在C++程序中,main函数的第一个参数是程序自身的完整路径,这 # 一点跟C#程序不一样。 # 0008135c "E:\临时文档\Windbg教程\nativedebug\Deb" 0008139c "ug\nativedebug.exe" 0:000> du 000813c2 000813c2 "123432" |
但是也有很多情况下,你可能并不知道指定地址里面保存的内容是什么,这个时候,建议你用dc(display double-word values and ASCII characters)命令查看内存。
0:000> dc 000813c2 # # dc命令的输出是Visual studio内存窗口以及windbg内存窗口默认的显示 # 方式 # 000813c2 00320031 00340033 00320033 fdfd0000 1.2.3.4.3.2..... 000813d2 ababfdfd abababab feeeabab 0000feee ................ 000813e2 00000000 19310000 75b66872 13301800 ......1.rh.u..0. 000813f2 14b80008 22a40008 007566a0 008c0000 .......".fu..... 00081402 00020000 00310000 fdfd0000 14d8fdfd ......1......... 00081412 15580008 15e00008 16800008 16e80008 ..X............. 00081422 3a400008 3aa80008 3b000008 3b680008 ..@:...:...;..h; 00081432 3bf80008 3c680008 3cd80008 3d300008 ...;..h<...<..0= |
最后,如果你调试的是一个非unicode程序,即是一个只理解ASCII字符集的程序(也就是所有字符串的类型都是char),那么在查看字符串的时候,使用da(display ascii)而不是du命令来显示内存。
本来计划只需要一篇文章就够的非托管(native)程序的调试,竟然用了三篇文章的篇幅才讲完!后面接着讲使用windbg需要了解的基础知识、术语等等。
未完待续……