第二章排错的工具:调试器Windbg(上)

感谢博主 http://book.51cto.com/art/200711/59731.htm

《Windows用户态程序高效排错》第二章主要介绍用户态调试相关的知识和工具。本文主要讲了排错的工具调试器Windbg。

 

 

第二章 汇编、异常、内存、同步和调试器
——重要的知识点和神兵利器

这一部分主要介绍用户态调试相关的知识和工具。包括汇编、异常exception、内存布局、堆heap、栈stack、CRTC Runtime、handle/Criticalsection/thread context/windbg/ dump/live debug和Dr Watson等。
书中不会对知识点作全面的介绍而是针对知识点在调试中过程中应该如何使用进行说明。知识点本身在下面两本书中有非常详细的介绍

Programming Applications for Microsoft Windows
Debugging Applications for Windows     

2.1  排错的工具调试器Windbg

本节介绍调试器Windbg的相关知识。Windbg的使用贯穿本书很多章节它是分析问题的高效工具。
Windbg的下载地址是

Install Debugging Tools for Windows 32-bit Version
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx

建议安装到C:\Debuggers目录后面的例子默认用这个目录。

开发人员写完代码通常会在Visual Studio中按F5直接进行调试。使用Visual Studio自带调试器能够非常方便地在源代码上设定断点检查程序的中间变量单步骤执行。在完成代码阶段Visual Studio自带的调试器能够非常方便地做源代码级别的排错。

Visual Studio调试器的典型用例是源代码级别的排错。与其相比Windbg并不是一款针对特殊用例的调试器。Windbg提供了一个GUI界面也可以在源代码上直接用F5设定断点但更多的情况下调试人员会直接用文本的方式输入调试命令Windbg执行对应的操作用文本的方式返回对应的结果。Windbg的调试命令覆盖了Windows平台提供的所有调试功能。

本节首先对调试器和符号文件作大致的介绍然后针对常用的Windbg调试命令作演示。接下来介绍Windbg中强大而灵活的条件断点最后介绍调试器目录下的相关工具。

对调试器深入的了解后相信读者就能体会到Windbg和Visual Studio调试器设计上的区别选用最合适的调试器来解决问题。

书中不会从Windbg的基本使用方法说起而是着重介绍调试器原理常用的命令Windbg的高级用法和相关的工具。如果读者从来没有使用过Windbg下面的文章可以提供帮助

DebugInfo:
http://www.debuginfo.com/
Windows Debuggers: Part 1: A WinDbg Tutorial
http://www.codeproject.com/debug/windbg_part1.asp

2.1.1  调试器的功能检查代码和资料保存dump文件控制程序的执行

调试器无论是Visual Studio调试器还是Windbg都是用来观察和控制目标进程的工具。对于用户态的进程调试器可以查看用户态内存空间和寄存器上的资料。对于不同类型的数据和代码调试器能方便地把这些信息用特定的格式区分和显示出来。调试器还可以把一个目标进程某一时刻的所有信息写入一个文件dump直接打开这个文件分析。调试器还可以通过设置断点的机制来控制目标程序什么时候停下来接受检查什么时候继续运行。

关于调试器的工作原理请参考Debugging Applications for Windows这本书。

Windbg及其相关工具的下载地址
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx

在安装好Windbg后可以在windbg.exe的主窗口按F1弹出帮助。这是了解和使用Windbg的最好文档。每个命令的详细说明都可以在里面找到。

调试器可以直观地看到下面一些信息

进程运行的状态和系统状态比如进程运行了多少时间环境变量是什么。
当前进程加载的所有EXE/DLL的详细信息。
某一个地址上的汇编指令。
查看内存地址的内容和属性比如是否可写。
每个的call stack需要symbol。
Call stack上每个函数的局部变量。
格式化地显示程序中的数据结构需要symbol。
查看和修改内存地址上的资料或者寄存器上的资料。
部分操作系统管理的数据结构比如Heap、Handle、CriticalSection等。

在Visual Studio调试器中要查看上面的信息需要在很多调试窗口中切换。而在Windbg中只需要简单的命令就可以完成。

调试器的另外一个作用是设定条件断点。可以设定在某一个指令地址上停下来也可以设定当某一个内存地址等于多少的时候停下来或者当某一个exception/notification发生的时候停下来。还可以进入一个函数调用的时候停下来或跳出当前函数调用的时候停下来。停下来后可以让调试器自动运行某些命令记录某些信息然后让调试器自动判断某些条件来决定是否要继续运行。通过简单的条件断点功能可以很方便地实现下面一些任务

当某一个函数被调用的时候在调试器输出窗口中打印出函数参数。
计算某一个变量被修改了多少次。
监视一个函数调用了哪些子函数分别被调用了多少次。
每次抛C++异常的时候自动产生dump文件。

在Visual Studio调试器中也能够设定条件断点但灵活性和功能远不能跟Windbg相比。

2.1.2  符号文件Symbol file把二进制和源代码对应起来

当用VC/VB编译生成EXE/DLL后往往会同时生成PDB文件。PDB里面包含的是EXE/DLL的符号信息。
符号是指代码中使用到的类型和名字。比如下面这些都是符号包含的内容

代码所定义的Class的名字Class的所有成员的名字和所有成员的类型。
变量的名字和变量的类型。
函数的名字函数所有参数的名字和类型以及函数的返回值。

PDB文件除了包含符号外还负责把符号和该符号所处的二进制地址联系起来。比如有一个全局变量叫做gBufferPDB文件不仅仅记录了gBuffer的类型还能让调试器找到保存gBuffer的内存地址。

有了符号文件当在调试器中试图读取某一个内存地址的时候调试器会尝试在对应的PDB文件中配对看这个内存地址是否有符号对应。如果能够找到调试器就可以把对应的符号显示出来。这样极大程度上方便了开发人员的观察。 对于操作系统EXE/DLL微软也提供了对应的符号文件下载地址。

默认情况下符号文件中包含了所有的结构、函数以及对应的源代码信息。微软提供的Windows符号文件去掉了源代码信息、函数参数定义和一些内部数据结构的定义。

2.1.3  一个简单的上手程序

接下来用一个简单的例子演示一下Windbg的基本使用。下面这段代码的目的是把字符串"6969,3p3p"中的所有3都修改为4。

 

 

 #include "stdafx.h"
#include "stdlib.h"

char* getcharBuffer()
{
return "6969,3p3p";
}

void changeto4p(char * buffer)
{
while(*buffer)
{
if(*buffer == '3')
*buffer='4';
buffer++;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
printf("%s\n","Any key continue...");
getchar();
char *str=getcharBuffer();
changeto4p(str);
printf("%s",str);
return 0;
}   

 

 

这段牖岬贾卤览览罂吹降慕涌谌缤?.1所示。

 

图2.1

接下来一起用Windbg来看看上述对话框的具体含义是什么。

在启动Windbg调试以前首先把程序对应的PDB文件放到一个指定的文件夹。上面程序的EXE叫做crashscreen-shot.exe把编译时候生成的crashscreen-shot.pdb文件拷贝到C:\PDB文件夹。同时把程序的主CPP文件拷贝到C:\SRC文件夹。

接下来启动Windbg。像用Visual Studio调试程序一样我们需要在调试器中运行对应的EXE。所以在Windbg的主窗口中使用File→Open Executable菜单找到crashscreen-shot.exe然后打开。

Windbg不会让目标进程立刻开始运行。相反Windbg这时会停下来让用户有机会对进程启动过程进行排错或者进行一些准备工作比如设定断点如图2.2所示。

 

图2.2

 

2.1.4  用Internet Explorer来操练调试器的基本命令

下面用Internet Explorer作为目标进程演示Windbg中更多的调试命令。

用Windbg来调试目标进程有两种方法,分别是通过调试器启动和用调试器直接监视attach正在运行的进程。
通过File→Open Executable菜单可以选择对应的EXE在调试器中启动。通过File→Attach to a process可以选择一个正在运行的进程进行调试。

打开IE访问www.msdn.com, 然后启动Windbg按F6选择刚刚启动的最下面iexplorer.exe进程。
IE的PDB文件也需要从微软的网站上下载。具体做法请参考上一节的链接。在我本地我的symbol路径设定如下

 

 

SRV*D:\websymbols*http://msdl.microsoft.com/download/symbols;D:\MyAppSymbol

 

 

这里的D:\websymbols目录是用来保存从msdl.microsoft.com上自动下载的操作系统符号文件。而我自己编译生成的符号文件我都手动拷贝到D:\MyAppSymbol路径下。
接下来在Windbg的命令窗口中如果看不到可以用Alt+1打开运行下面命令。

 

 

vertarget检查进程概况
vertarget命令显示当前进程的大致信息
0:026> vertarget
Windows Server 2003 Version 3790 (Service Pack 1) MP (2 procs) Free x86 compatible
Product: Server, suite: Enterprise TerminalServer SingleUserTS
kernel32.dll version: 5.2.3790.1830 (srv03_sp1_rtm.050324-1447)
Debug session time: Thu Apr 27 13:53:50.414 2006 (GMT+8)
System Uptime: 15 days 1:59:13.255
Process Uptime: 0 days 0:07:34.508
Kernel time: 0 days 0:00:01.109
User time: 0 days 0:00:00.609    

 

 

上面的0:026>是命令提示符026表示当前的线程ID。后面会介绍切换线程的命令到时候就可以看到提示符的变化。

跟大多数的命令输出一样vertarget的输出非常明白直观显示当前系统的版本和运行时间。

 

 

!peb 显示Process Environment Block  

 

 

接着可以用!peb命令来显示Process Environment Block。由于输出太长这里就省略了。

lmvm 检查模块的加载信息

用lmvm命令可以看任意一个DLL/EXE的详细信息以及symbol的情况

 

 

0:026> lmvm msvcrt
start    end        module name
77ba0000 77bfa000   msvcrt     (deferred)
Image path: C:\WINDOWS\system32\msvcrt.dll
Image name: msvcrt.dll
Timestamp:        Fri Mar 25 10:33:02 2005 (4243785E)
CheckSum:         0006288A
ImageSize:        0005A000
File version:     7.0.3790.1830
Product version:  6.1.8638.1830
File flags:       0 (Mask 3F)
File OS:          40004 NT Win32
File type:        1.0 App
File date:        00000000.00000000
Translations:     0409.04b0
CompanyName:      Microsoft Corporation
ProductName:      Microsoft Windows Operating System
InternalName:     msvcrt.dll
OriginalFilename: msvcrt.dll
ProductVersion:   7.0.3790.1830
FileVersion:      7.0.3790.1830 (srv03_sp1_rtm.050324-1447)
FileDescription:  Windows NT CRT DLL
LegalCopyright:    Microsoft Corporation. All rights reserved.   

 

 

命令的第二行显示deferred表示目前并没有加载msvcrt的symbol可以用.reload命令来加载。在加载前可以用!sym命令来打开symbol加载过程的详细输出

 

 

.reload / !sym 加载符号文件

默认情况下调试器不会加载所有的symbol文件。只有某个调试器命令需要使用symbol的时候调试器才在设定的符号文件路径中检查和加载。!sym命令可以让调试器在自动寻找symbol的时候给出详细的信息比如搜索和下载的路径。.reload命令可以让调试器加载指定模块的symbol。

 

 

 

 

0:026> !sym noisy
noisy mode - symbol prompts on
0:026> .reload /f msvcrt.dll
SYMSRV:  msvcrt.pd_ from http://msdl.microsoft.com/download/symbols:
80847 bytes copied
DBGHELP: msvcrt - public symbols
c:\websymbols\msvcrt.pdb\62B8BDC3CC194D2992DCFAED78B621FC1\msvcrt.pdb
0:026> lmvm msvcrt
start    end        module name
77ba0000 77bfa000   msvcrt     (pdb symbols)
c:\websymbols\msvcrt.pdb\62B8BDC3CC194D2992DCFAED78B621FC1\msvcrt.pdb
Loaded symbol image file: C:\WINDOWS\system32\msvcrt.dll
Image path: C:\WINDOWS\system32\msvcrt.dll
Image name: msvcrt.dll
Timestamp:        Fri Mar 25 10:33:02 2005 (4243785E)
CheckSum:         0006288A
ImageSize:        0005A000
File version:     7.0.3790.1830
Product version:  6.1.8638.1830
File flags:       0 (Mask 3F)
File OS:          40004 NT Win32
File type:        1.0 App
File date:        00000000.00000000
Translations:     0409.04b0
CompanyName:      Microsoft Corporation
ProductName:      Microsoft Windows Operating System
InternalName:     msvcrt.dll
OriginalFilename: msvcrt.dll
ProductVersion:   7.0.3790.1830
FileVersion:      7.0.3790.1830 (srv03_sp1_rtm.050324-1447)
FileDescription:  Windows NT CRT DLL
LegalCopyright:    Microsoft Corporation. All rights reserved.   

 

 

可以看到symbol从msdl.microsoft.com自动下载后加载。

lmf 列出当前进程中加载的所有模块

lmf命令可以列出当前进程中加载的所有DLL文件和对应的路径

 

 

0:018> lmf
start    end        module name
00d40000 00dda000   iexplore C:\Program Files\Internet Explorer\iexplore.exe
04320000 043c9000   atiumdva C:\Windows\system32\atiumdva.dll
10000000 1033d000   googletoolbar2 c:\program files\google\googletoolbar2.dll
37f00000 37f0f000   Cjktl32  E:\Program Files\Powerword 2003\Cjktl32.dll  

r,d,e 寄存器内存的检查和修改
r命令显示和修改寄存器上的值。
d命令显示内存地址上的值。
e命令修改内存地址上的值。
显示寄存器

 

 

 

 

0:018> r
eax=7ffdc000 ebx=00000000 ecx=00000000 edx=7707f06d esi=00000000 edi=00000000
eip=77032ea8 esp=054efc14 ebp=054efc40 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!DbgBreakPoint
77032ea8 cc              int     3

如果需要修改寄存器比如把eax的值修改为0x0可以用 r eax=0。
用d命令显示esp 寄存器指向的内存默认为byte格式。

 

 

 

 

0:018> d esp
054efc14  a9 f0 07 77 e9 ef 4e 05-00 00 00 00 00 00 00 00  ...w..N.........
054efc24  00 00 00 00 18 fc 4e 05-00 00 00 00 7c fc 4e 05  ......N.....|.N.
054efc34  f2 8b ff 76 a1 f5 03 77-00 00 00 00 4c fc 4e 05  ...v...w....L.N.
054efc44  33 38 b4 75 00 00 00 00-8c fc 4e 05 bd a9 02 77  38.u......N....w
054efc54  00 00 00 00 25 ef 4e 05-00 00 00 00 00 00 00 00  ....%.N.........
054efc64  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
054efc74  58 fc 4e 05 00 00 00 00-ff ff ff ff f2 8b ff 76  X.N............v
054efc84  a1 e2 03 77 00 00 00 00-00 00 00 00 00 00 00 00  ...w............ 

 

 

用dd命令直接指定054efc14 地址第二个d表示用DWORD格式。除了DWORD外还有dbbyteduUnicodedcchar等等。详细信息请参考帮助文档中d命令的说明。

 

 

0:018> dd 054efc14
054efc14  7707f0a9 054eefe9 00000000 00000000
054efc24  00000000 054efc18 00000000 054efc7c
054efc34  76ff8bf2 7703f5a1 00000000 054efc4c
054efc44  75b43833 00000000 054efc8c 7702a9bd
054efc54  00000000 054eef25 00000000 00000000
054efc64  00000000 00000000 00000000 00000000
054efc74  054efc58 00000000 ffffffff 76ff8bf2
054efc84  7703e2a1 00000000 00000000 00000000   

 

 

e命令可以用来修改内存地址。跟d命令一样e命令后面也可以跟类型后缀。比如ed命令表示用DWORD的方式修改。下面的命令把054efc14 地址上的值修改为11112222。

 

0:018> ed 054efc14  11112222

修改完成后用dd命令检查054efc14 地址上的值。后面的 L4参数指定内存区间的长度长度为4个DWORD。这样输出就只有1行而不是默认的8行。

0:018> dd 054efc14  L4
054efc14  11112222 40a15c00 00000000 40a15c00

 

 

有了上面几个命令就可以访问和修改当前进程中的所有内存。这些命令的参数和格式非常灵活详细内容参考帮助文档。

 

!address显示内存页信息

该命令在前面的例子中就用到过可以显示某一个地址上的页信息

0:001> !address 7ffde000
7ffde000 : 7ffde000 - 00001000
Type     00020000 MEM_PRIVATE
Protect  00000004 PAGE_READWRITE
State    00001000 MEM_COMMIT
Usage    RegionUsagePeb  

 

 

如果不带参数可以显示更详细的统计信息。

S 搜索内存

S命令可以搜索内存。比如用在下面的地方

1. 寻找内存泄漏的线索。比如知道当前内存泄漏的内容是一些固定的字符串就可以在 DLL区域搜索这些字符串出现的地址然后再搜索这些地址用到什么代码中找出这些内存是从什么地方开始分配的。
2. 寻找错误代码的根源。比如知道当前程序返回了0x80074015这样的一个代码但是不知道这个代码是由哪一个内层函数返回的就可以在代码区搜索0x80074015找到可能返回这个代码的函数。
下面就是访问sina.com的时候用Windbg搜索ie里面www.sina.com.cn的结果

 

 

0:022> s -u 0012ff40 L?80000000 "www.sina.com.cn"
001342a0  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.
00134b82  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.
00134f2e  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.
0013570c  0077 0077 0077 002e 0073 0069 006e 0061  w.w.w...s.i.n.a.

 

 

结合S命令和前面介绍的修改内存命令根本不需要用什么金山游侠就可以查找/修改游戏中主角的生命了 :-
接下来看看跟线程相关的命令。

!runaway 检查线程的CPU消耗

!runaway可以显示每一个线程所耗费usermode CPU时间的统计信息

 

 

0:001> !runaway
User Mode Time
Thread       Time
0:83c       0 days 0:00:00.406
13:bd4       0 days 0:00:00.046
10:ac8       0 days 0:00:00.046
24:4f4       0 days 0:00:00.031
11:d8c       0 days 0:00:00.015
26:109c      0 days 0:00:00.000
25:1284      0 days 0:00:00.000
23:12cc      0 days 0:00:00.000
22:16c0      0 days 0:00:00.000
21:57c       0 days 0:00:00.000
20:c00       0 days 0:00:00.000
19:14e8      0 days 0:00:00.000
18:1520      0 days 0:00:00.000
16:9dc       0 days 0:00:00.000
15:1654      0 days 0:00:00.000
14:13f4      0 days 0:00:00.000
9:104c      0 days 0:00:00.000
8:1760      0 days 0:00:00.000
7:cc8       0 days 0:00:00.000
6:530       0 days 0:00:00.000
5:324       0 days 0:00:00.000
4:178c      0 days 0:00:00.000
3:1428      0 days 0:00:00.000
2:1530      0 days 0:00:00.000
1:448       0 days 0:00:00.000

 

 

上面输出的第一列是线程编号和线程id。后一列对应的是该线程在用户态模式中的总繁忙时间。在该命令加上f参数还可以看到内核态的繁忙时间。当进程内存占用率高的时候通过该命令可以方便地找到对应的繁忙线程。

~ 切换目标线程

用~命令可以显示线程信息和在不同线程之间切换

 

 

0:001> ~
0  Id: c0.83c Suspend: 1 Teb: 7ffdd000 Unfrozen
.  1  Id: c0.448 Suspend: 1 Teb: 7ffdb000 Unfrozen
2  Id: c0.1530 Suspend: 1 Teb: 7ffda000 Unfrozen
3  Id: c0.1428 Suspend: 1 Teb: 7ffd9000 Unfrozen
4  Id: c0.178c Suspend: 1 Teb: 7ffd8000 Unfrozen
5  Id: c0.324 Suspend: 1 Teb: 7ffdc000 Unfrozen
6  Id: c0.530 Suspend: 1 Teb: 7ffd7000 Unfrozen
7  Id: c0.cc8 Suspend: 1 Teb: 7ffd6000 Unfrozen
8  Id: c0.1760 Suspend: 1 Teb: 7ffd5000 Unfrozen
9  Id: c0.104c Suspend: 1 Teb: 7ffd4000 Unfrozen
10  Id: c0.ac8 Suspend: 1 Teb: 7ffd3000 Unfrozen
11  Id: c0.d8c Suspend: 1 Teb: 7ff9f000 Unfrozen
13  Id: c0.bd4 Suspend: 1 Teb: 7ff9d000 Unfrozen
14  Id: c0.13f4 Suspend: 1 Teb: 7ff9c000 Unfrozen
15  Id: c0.1654 Suspend: 1 Teb: 7ff9b000 Unfrozen
16  Id: c0.9dc Suspend: 1 Teb: 7ff9a000 Unfrozen
18  Id: c0.1520 Suspend: 1 Teb: 7ff96000 Unfrozen
19  Id: c0.14e8 Suspend: 1 Teb: 7ff99000 Unfrozen
20  Id: c0.c00 Suspend: 1 Teb: 7ff97000 Unfrozen
21  Id: c0.57c Suspend: 1 Teb: 7ff95000 Unfrozen
22  Id: c0.16c0 Suspend: 1 Teb: 7ff94000 Unfrozen
23  Id: c0.12cc Suspend: 1 Teb: 7ff93000 Unfrozen
24  Id: c0.4f4 Suspend: 1 Teb: 7ff92000 Unfrozen
25  Id: c0.1284 Suspend: 1 Teb: 7ff91000 Unfrozen
26  Id: c0.109c Suspend: 1 Teb: 7ff90000 Unfrozen
0:001> ~0s
eax=0013e7c4 ebx=00000000 ecx=0013e7c4 edx=0000000b esi=001642e8 edi=00000000
eip=7c82ed54 esp=0013eb3c ebp=0013ed98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!KiFastSystemCallRet
7c82ed54 c3               ret
0:000>

 

 

上面的~0s命令把当前线程切换到0号线程也就是主线程。切换后提示符会变为0:000。

k,kb,kp,kv,kn 检查call stack

k命令显示当前线程的call stack。跟d命令一样k后面可以跟很多后缀比如kb、kp、kn、kv、kL等。这些后缀控制了显示的格式和信息量。具体信息请参考帮助文档和动手实践。

 

 

0:000> k
ChildEBP RetAddr
0013eb38 7739d02f ntdll!KiFastSystemCallRet
0013ed98 75ecb30f USER32!NtUserWaitMessage+0xc
0013ee24 75ed7ce5 BROWSEUI!BrowserProtectedThreadProc+0x44
0013fea8 779ac61e BROWSEUI!SHOpenFolderWindow+0x22c
0013fec8 0040243d SHDOCVW!IEWinMain+0x129
0013ff1c 00402748 iexplore!WinMain+0x316
0013ffc0 77e523cd iexplore!WinMainCRTStartup+0x186
0013fff0 00000000 kernel32!BaseProcessStart+0x23

 

 

可以结合~和k命令来显示所有线程的call stack. 输入~*k试一下。

u 反汇编

u命令把指定地址上的代码翻译成汇编输出。

 

 

0:000> u 7739d023
USER32!NtUserWaitMessage
7739d023 b84a120000       mov     eax,0x124a
7739d028 ba0003fe7f       mov     edx,0x7ffe0300
7739d02d ff12             call    dword ptr [edx]
7739d02f c3               ret

 

 

如果符号文件加载正确可以用uf命令直接反汇编整个函数比如uf USER32! NtUserWaitMessage。

x 查找符号的二进制地址

有了符号文件调试器就能查找源代码符号和该符号所处的二进制地址之间的映射。如果要找一个符号保存在什么二进制地址上可以用x命令

 

 

0:000> x msvcrt!printf
77bd27c2 msvcrt!printf = <no type information>

 

 

上面的命令找到了printf函数的入口地址在77bd27c2。

 

 

0:001> x ntdll!GlobalCounter
7c99f72c ntdll!GlobalCounter = <no type information>

 

 

上面的命令表示ntdll!GlobalCounter这个变量保存的地址是7c99f72c。

注意: 符号对应的是变量和变量所在的地址不是变量的值。上面的命令不是说ntdll!GlobalCounter这个变量的值是7c99f72c。要找到变量的值需要用d命令读取内存地址来获取。

x命令还支持通配符比如 x ntdll!*命令列出ntdll模块中所有的符号以及对应的二进制地址。由于输出太长这里就省略了。

dds 对应二进制地址的符号

dds打印内存地址上的二进制值同时自动搜索二进制值对应的符号。比如要看看当前stack 中保存了哪些函数地址就可以检查ebp指向的内存

 

 

0:000> dds ebp
0013ed98  0013ee24
0013ed9c  75ecb30f BROWSEUI!BrowserProtectedThreadProc+0x44
0013eda0  00163820
0013eda4  0013ee50
0013eda8  00163820
0013edac  00000000
0013edb0  0013ee10
0013edb4  75ece83a BROWSEUI!__delayLoadHelper2+0x23a
0013edb8  00000005
0013edbc  0013edcc
0013edc0  0013ee50
0013edc4  00163820
0013edc8  00000000
0013edcc  00000024
0013edd0  75f36d2c BROWSEUI!_DELAY_IMPORT_DESCRIPTOR_SHELL32
0013edd4  75f3a184 BROWSEUI!_imp__SHGetInstanceExplorer
0013edd8  75f36e80 BROWSEUI!_sz_SHELL32
0013eddc  00000001
0013ede0  75f3726a BROWSEUI!urlmon_NULL_THUNK_DATA_DLN+0x116
0013ede4  7c8d0000 SHELL32!_imp__RegCloseKey <PERF> (SHELL32+0x0)
0013ede8  7c925b34 SHELL32!SHGetInstanceExplorer

 

 

这里dds命令从ebp指向的内存地址0013ed98 开始打印。第1列是内存地址的值第2列是地址上对应的二进制数据第3列是二进制数据对应的符号。上面的命令自动找到了75ecb30f对应的符号是BROWSEUI!BrowserProtectedThreadProc+0x44。

COM Interface和C++ Vtable里面的成员函数都是顺序排列的所以dds命令可以方便地找到虚函数表中具体的函数地址。比如用下面的命令可以找到OpaqueDataInfo类型中虚函数对应的实际函数地址。

首先用x命令找到OpaqueDataInfo虚函数表地址

 

 

0:000> x ole32!OpaqueDataInfo::`vftable'
7768265c ole32!OpaqueDataInfo::`vftable' = <no type information>
77682680 ole32!OpaqueDataInfo::`vftable' = <no type information> 

 

 

接下来dds命令可以打印出虚函数表中的函数名字

 


0:000> dds 7768265c 7768265c 77778245 ole32!ServerLocationInfo::QueryInterface 77682660 77778254 ole32!ScmRequestInfo::AddRef 77682664 77778263 ole32!ScmRequestInfo::Release 77682668 77779d26 ole32!OpaqueDataInfo::Serialize 7768266c 77779d3d ole32!OpaqueDataInfo::UnSerialize 77682670 77779d7a ole32!OpaqueDataInfo::GetSize 77682674 77779dcb ole32!OpaqueDataInfo::GetCLSID 77682678 77779deb ole32!OpaqueDataInfo::SetParent 7768267c 77779e18 ole32!OpaqueDataInfo::SerializableQueryInterface 77682680 777799b5 ole32!InstantiationInfo::QueryInterface 77682684 77689529 ole32!ServerLocationInfo::AddRef 77682688 776899cc ole32!ScmReplyInfo::Release 7768268c 77779bcd ole32!OpaqueDataInfo::AddOpaqueData 77682690 77779c43 ole32!OpaqueDataInfo::GetOpaqueData 77682694 77779c99 ole32!OpaqueDataInfo::DeleteOpaqueData 77682698 776a8cf6 ole32!ServerLocationInfo::GetRemoteServerName 7768269c 776aad96 ole32!OpaqueDataInfo::GetAllOpaqueData 776826a0 77777a3b ole32!CDdeObject::COleObjectImpl::GetClipboardData 776826a4 00000021 776826a8 77703159 ole32!CClassMoniker::QueryInterface 776826ac 77709b01 ole32!CErrorObject::AddRef 776826b0 776edaff ole32!CClassMoniker::Release 776826b4 776ec529 ole32!CClassMoniker::GetUnmarshalClass 776826b8 776ec546 ole32!CClassMoniker::GetMarshalSizeMax 776826bc 776ec589 ole32!CClassMoniker::MarshalInterface 776826c0 77702ca9 ole32!CClassMoniker::UnmarshalInterface 776826c4 776edbe1 ole32!CClassMoniker::ReleaseMarshalData 776826c8 776e5690 ole32!CDdeObject::COleItemContainerImpl::LockContainer 776826cc 7770313b ole32!CClassMoniker::QueryInterface 776826d0 7770314a ole32!CClassMoniker::AddRef 776826d4 776ec5a8 ole32!CClassMoniker::Release 776826d8 776ec4c6 ole32!CClassMoniker::GetComparisonData

 

 

2.1.5  检查程序资料的小例子

下面用一个小例子来演示如何具体地观察程序中的数据结构。

首先在debug模式下编译并且按Ctrl+F5运行下面的代码

 

 


struct innner { char arr[10]; }; class MyCls { private: char* str; innner inobj; public: void set(char* input) { str=input; strcpy(inobj.arr,str); } int output() { printf(str); return 1; } void hold() { getchar(); } };

void foo1() { MyCls *pcls=new MyCls(); void *rawptr=pcls; pcls->set("abcd"); pcls->output(); pcls->hold(); }; void foo2() { printf("in foo2\n"); foo1(); }; void foo3() { printf("in foo3\n"); foo2(); };

int _tmain(int argc, _TCHAR* argv[]) { foo3(); return 0; }

 

 

当console等待输入的时候启动Windbg然后用F6加载目标进程。

用~0s命令切换到主线程查看callstack

 

 

0:000> knL
# ChildEBP RetAddr
00 0012f7a0 7c821c94 ntdll!KiFastSystemCallRet
01 0012f7a4 7c836066 ntdll!NtRequestWaitReplyPort+0xc
02 0012f7c4 77eaaba3 ntdll!CsrClientCallServer+0x8c
03 0012f8bc 77eaacb8 kernel32!ReadConsoleInternal+0x1b8
04 0012f944 77e41990 kernel32!ReadConsoleA+0x3b
05 0012f99c 10271754 kernel32!ReadFile+0x64
06 0012fa28 10271158 MSVCR80D!_read_nolock+0x584
07 0012fa74 10297791 MSVCR80D!_read+0x1a8
08 0012fa9c 102a029b MSVCR80D!_filbuf+0x111
09 0012faf0 102971ce MSVCR80D!getc+0x24b
0a 0012fafc 102971e8 MSVCR80D!_fgetchar+0xe
0b 0012fb04 0041163b MSVCR80D!getchar+0x8
0c 0012fbe4 00413f82 exceptioninject!MyCls::hold+0x2b
0d 0012fcec 0041169a exceptioninject!foo1+0xa2
0e 0012fdc0 004114fa exceptioninject!foo2+0x3a
0f 0012fe94 004116d3 exceptioninject!foo3+0x3a
10 0012ff68 00412016 exceptioninject!wmain+0x23
11 0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6
12 0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd
13 0012fff0 00000000 kernel32!BaseProcessStart+0x23

 

 

.frame 在栈中切换以便检查局部变量

上面callstack中每一行前面的序号叫做frame number。通过.frame命令可以切换到对应的函数中检查局部变量。比如exceptioninject!foo1 这个函数前面的 frame number是d于是执行.frame d命令

 

 

0:000> .frame d
0d 0012fcec 0041169a exceptioninject!foo1+0xa2
[d:\xiongli\today\exceptioninject\exceptioninject\exceptioninject.cpp @ 72]

 

 

x命令显示当前frame的局部变量。在foo1函数中两个局部变量分别是pcls和rawptr

 

 

0:000> x
0012fce4 pcls = 0x0039ba80
0012fcd8 rawptr = 0x0039ba80

 

 

dt 格式化显示资料

在符号文件加载的情况下dt命令格式化显示变量的资料和结构

 

 

0:000> dt pcls
Local var @ 0x12fce4 Type MyCls*
0x0039ba80
+0x000 str              : 0x00416648  "abcd"
+0x004 inobj            : inner

 

 

上面的命令打印出pcls的类型是MyCls指针指向的地址是0x0039ba80其中的两个class成员的偏移分别在+0和+4对应的值在第2列显示。加上-b -r参数可以显示inner class和数组的信息

 

 


0:000> dt pcls -b -r Local var @ 0x12fce4 Type MyCls* 0x0039ba80 +0x000 str : 0x00416648 "abcd" +0x004 inobj : innner +0x000 arr : "abcd" [00] 97 'a' [01] 98 'b' [02] 99 'c' [03] 100 'd' [04] 0 '' [05] 0 '' [06] 0 '' [07] 0 '' [08] 0 '' [09] 0 ''

 

 

对于任意的地址也可以手动指定符号类型来格式化显示。比如把0x0039ba80地址上的数据用MyCls类型来显示

 

 

0:000> dt 0x0039ba80 MyCls
+0x000 str              : 0x00416648  "abcd"
+0x004 inobj            : innner

 

 

2.1.6  用Windbg控制程序进行实时调试Live Debug

除了检查静态资料外调试器还能够控制和观察代码的执行。

1. wt命令
wt命令的作用是watch and trace data。 它可以跟踪一个函数的所有执行过程并且给出统计信息。
2. 设定断点
Windbg里面可以设定灵活而强大的条件断点。比如可以通过条件断点实现这样的功能当某个全局变量被修改100次以后同时stack上的第2个参数是100那么就停下来进入调试模式如果第2个参数是200那么就生成1个dump文件否则就只打印出当前的callstack然后继续运行。

Wt Watch and Trace, 跟踪执行的强大命令

还是对于上面那个程序。

首先用bp break point 命令在foo3上面设断点

 

 


0:001> bp exceptioninject!foo3
breakpoint 0 redefined

 

 

然后用g命令让程序执行

 


0:001> g

执行到foo3上的时候调试器停下来了


Breakpoint 0 hit
eax=0000000a ebx=7ffd7000 ecx=0043780e edx=10310bd0 esi=0012fe9c edi=0012ff68
eip=004114c0 esp=0012fe98 ebp=0012ff68 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
exceptioninject!foo3
004114c0 55 push ebp

 

 

用bdbreakpoint disable命令取消设定好的断点以免打扰wt的执行

 


0:000> bd 0

 

 

用wt命令监视foo3的执行深度设定成2-l2参数

 


0:000> wt -l2
Tracing exceptioninject!foo3 to return address 0041186a
60 0 [ 0] exceptioninject!foo3
28 0 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
32 5 [ 1] MSVCR80D!printf
12 0 [ 2] MSVCR80D!_lock_file2
35 17 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
38 22 [ 1] MSVCR80D!printf
50 0 [ 2] MSVCR80D!_stbuf
46 72 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
49 77 [ 1] MSVCR80D!printf
575 0 [ 2] MSVCR80D!_output_l
52 652 [ 1] MSVCR80D!printf
5 0 [ 2] MSVCR80D!__iob_func
57 657 [ 1] MSVCR80D!printf
33 0 [ 2] MSVCR80D!_ftbuf
60 690 [ 1] MSVCR80D!printf
7 0 [ 2] MSVCR80D!printf
71 697 [ 1] MSVCR80D!printf
63 768 [ 0] exceptioninject!foo3
1 0 [ 1] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 1] exceptioninject!_RTC_CheckEsp
64 771 [ 0] exceptioninject!foo3
1 0 [ 1] exceptioninject!ILT+340(?foo2YAXXZ)
60 0 [ 1] exceptioninject!foo2
71 0 [ 2] MSVCR80D!printf
63 71 [ 1] exceptioninject!foo2
1 0 [ 2] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 2] exceptioninject!_RTC_CheckEsp
64 74 [ 1] exceptioninject!foo2
1 0 [ 2] exceptioninject!ILT+215(?foo1YAXXZ)
108 0 [ 2] exceptioninject!foo1
70 183 [ 1] exceptioninject!foo2
1 0 [ 2] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 2] exceptioninject!_RTC_CheckEsp
73 186 [ 1] exceptioninject!foo2
70 1031 [ 0] exceptioninject!foo3
1 0 [ 1] exceptioninject!ILT+380(__RTC_CheckEsp)
2 0 [ 1] exceptioninject!_RTC_CheckEsp
73 1034 [ 0] exceptioninject!foo3

1107 instructions were executed in 1106 events (0 from other threads)

Function Name Invocations MinInst MaxInst AvgInst
MSVCR80D!__iob_func 4 5 5 5
MSVCR80D!_ftbuf 1 33 33 33
MSVCR80D!_lock_file2 1 12 12 12
MSVCR80D!_output_l 1 575 575 575
MSVCR80D!_stbuf 1 50 50 50
MSVCR80D!printf 3 7 71 49
exceptioninject!ILT+215(?foo1YAXXZ) 1 1 1 1
exceptioninject!ILT+340(?foo2YAXXZ) 1 1 1 1
exceptioninject!ILT+380(__RTC_CheckEsp) 4 1 1 1
exceptioninject!_RTC_CheckEsp 4 2 2 2
exceptioninject!foo1 1 108 108 108
exceptioninject!foo2 1 73 73 73
exceptioninject!foo3 1 73 73 73

0 system calls were executed

eax=00000073 ebx=7ffd7000 ecx=00437c7e edx=10310bd0 esi=0012fe9c edi=0012ff68
eip=0041186a esp=0012fe9c ebp=0012ff68 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
exceptioninject!wmain+0x4a
0041186a ebe1 jmp exceptioninject!wmain+0x2d (0041184d)

 

 

上面wt命令一直监视到foo3函数执行完为止。随着函数的执行Windbg打印出foo3调用过的子函数。如果需要更详细的信息可以调整深度参数的值。

wt命令最后给出统计信息。无论是观察函数执行过程和分支或者是评估性能wt命令都是很有帮助的。

断点和条件断点 condition breakpoint高效地控制观测目标

Windbg中的断点分为3种命令格式和功能如下

1. bp+地址/函数名字可以在某个地址上设定断点。当程序运行到这个地址的时候断点触发。
2. ba break on access用来设定访问断点在某个地址被读/写的时候断点触发。
3. Exception断点。当发生某个Exception/Notification的时候断点触发。详情请参考Windbg帮助中的sxSet Exception小结。

第1种格式前面已经实践过。第2种格式的断点也很简单。比如程序有一个全局变量符号是testapp!g_Buffer。要想在程序修改这个变量的时候停下来可以使用下面的命令设定断点

 

 


ba w4 testapp!g_Buffer

上面的w4表示需要检查的类型和长度。W4中的W表示类型为写(Write)4表示长度为4字节。testapp!g_Buffer是符号的名字调试器会自动转换成该符号所在的内存地址。所以该断点的作用是监视一块内存地址区域起点是testapp!g_Buffer所在的地址长度为4字节。当有代码对该块地址任意位置有写操作发生的时候调试器就把程序断下来。

 

 

其实设置ba断点的原理很简单。在设置断点后调试器通过API把所监视地址的页面属性改为不可访问。这样当有代码访问这块地址的时候就会引起访问异常。这样调试器就可以监视内存的读写操作作出相应判断。

第3种命令用来监视异常。调试器能捕获程序中所有的异常但是并不是说任何异常发生的时候调试器就一定要把程序断下来。调试人员可以通过sx命令来指定异常发生时候的对应操作。下面3条命令达到的效果是当Access Violation异常发生的时候调试器就停下来。当DLL Load事件发生的时候调试器就只是在屏幕上输出。当C++ exception发生的时候调试器什么都不做。

 

 


Sxe av
Sxn ld
Sxd eh

 

 

关于异常的详细说明在后面的小结有详细介绍。

条件断点condition breakpoint的是指在上面3种基本断点停下来后执行一些自定义的判断。详细说明参考Windbg帮助中的Setting a Conditional Breakpoint小结。

在基本断点命令后加上自定义调试命令可以让调试器在断点触发停下来后执行调试器命令。每个命令之间用分号分割。

下面这个命令在exceptioninject!foo3上设断点每次断下来后先用k显示callstack然后用.echo命令输出简单的字符串‘breaks’最后g命令继续执行

 

 


0:001> bp exceptioninject!foo3 "k;.echo 'breaks';g"
breakpoint 0 redefined
0:001> g
ChildEBP RetAddr
0012fe94 0041186a exceptioninject!foo3
0012ff68 00412016 exceptioninject!wmain+0x4a
0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6
0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd
0012fff0 00000000 kernel32!BaseProcessStart+0x23
'breaks'
ChildEBP RetAddr
0012fe94 0041186a exceptioninject!foo3
0012ff68 00412016 exceptioninject!wmain+0x4a
0012ffb8 00411e5d exceptioninject!__tmainCRTStartup+0x1a6
0012ffc0 77e523cd exceptioninject!wmainCRTStartup+0xd
0012fff0 00000000 kernel32!BaseProcessStart+0x23
'breaks'

 

 

更复杂一点的例子是

 

 


int i=0;
int _tmain(int argc, _TCHAR* argv[])
{

while(1)
{
getchar();
i++;
foo3();
}
return 0;
}

 

 

条件断点的命令是

 

 

ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) '
.printf \"exceptioninject!i value is:%d\",
poi(exceptioninject!i);.echo;g';'.echo stop!'"

 

 

首先ba w4 exceptioninject!i表示在修改exceptioninject!i这个全局变量的时候停下来。jjudge命令的作用是对后面的表达式作条件判断如果为true执行第1个单引号里面的命令否则执行第2个单引号里面的命令。
条件表达式是poi“exceptioninject!i”<0n40。在Windbg中exceptioninject!i符号表示符号所在的内存地址而不是符号的数值相当于C语言中的 &操作符的作用。Windbg命令poi的作用是取这个地址上的值相当于C语言中的*操作符。所以这个条件的意思就是判断exceptioninject!i的值是否小于十进制Windbg中十进制用0n当前缀的40。
如果为真那么就执行第1个单引号

 

 

printf \"exceptioninject!i value is:%d\",poi(exceptioninject!i);.echo;g

 

 

这一个单引号里面有3个命令.printf、.echo 和g这里的printf语法跟C中printf函数语法一样。不过由于这个printf命令本身是在ba命令的双引号里面所以需要用\来转义printf中的引号。转义的结果是

 

 

printf “exceptioninject!i valus is %d”, poi(exceptioninject!i)

 

 

所以第1个单引号命令的作用是

1打印出当前exceptioninject!i的值2.echo命令换行3g命令继续执行。
如果为假那么就执行第2个单引号.echo stop! 这个命令就是显示stop由于后面没有g命令所以windbg会停下。运行输出如下

 

 

0:001> ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) '
.printf \"exceptioninject!i value is:%d\",poi(exceptioninject!i);.echo;g';'.echo stop!'"
breakpoint 0 redefined
0:001> g
exceptioninject!i value is:35
exceptioninject!i value is:36
exceptioninject!i value is:37
exceptioninject!i value is:38
exceptioninject!i value is:39
stop!
eax=00000028 ebx=7ffd5000 ecx=5e186b9c edx=10310bd0 esi=0012fe9c edi=0012ff68
eip=00411872 esp=0012fe9c ebp=0012ff68 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
exceptioninject!wmain+0x52
00411872 e856f8ffff       call exceptioninject!ILT+200(?foo3YAXXZ) (004110cd)

 

 

伪寄存器,帮助保存调试的中间信息

考虑这样的情况如果要记录某一个函数被执行了多少次应该怎么做简单的做法就是修改代码在对应的函数入口做记录。可是如果要记录的函数是系统API呢

下面的命令可以统计VirtualAllocEx被执行了多少次

 

 

bp /1 /c @$csp @$ra;g

 

 

bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf \"function executes: %d times \",@$t0;.echo;g"

这里用到的$t0就是Windbg提供的伪寄存器。可以用来存储中间信息。这里用它来存储函数执行的次数。r命令可以用来查看修改寄存器CPU寄存器和Windbg的伪寄存器都有效的值。随便挑一个繁忙的进程用这个命令设定断点后观察

 

0:009> bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf
\"function executes: %d times \",@$t0;.echo;g"
0:009> g
function executes: 1 times
function executes: 2 times
function executes: 3 times
function executes: 4 times
…

 

 

关于伪寄存器信息可以参考帮助文档中的Pseudo-Register Syntax小结。

Step Out的实现

Step Out的定义是“Target executes until the current function is complete.”。Windbg中是如何实现这个功能的呢根据这个定义可以简单地在当前函数的返回地址上设定bp 断点就可以了。当前函数的返回地址保存在函数入口时候的EBP+4上。但如果简单地在EBP+4上面设定断点有两个问题

1. 无法区分递归调用和函数返回甚至其他线程对该地址的调用。
2. 第一次触发后不会自动清除端点可能会多次触发。

如果观察windbg中step  out的实现可以看到

 

 

bp /1 /c @$csp @$ra;g

这里的/1参数使得断点在触发后自动清除避免了第2个问题/c @$csp参数通过指定callstack 的最小深度避免了第1个问题。而$ra伪寄存器直接表示当前函数的返回地址。多方便 :-)

 

 

2.1.7  远程调试Remote debug

远程调试是让调试人员远程地操作调试器的一种手段。

在调试WinForm程序的时候如果要保持目标程序一直全屏运行就没办法在同一台机器上切换到调试器输入命令。使用remote debug可以解决这样的情况避免调试器对目标程序的干扰。

如果开发团队中的开发人员在两个城市通过远程调试可以节省创建多个调试环境的时间。如果某些问题只能在固定的机器上重现远程调试让排错过程简单便利。

在Windbg中一种方法使用.server命令在本地创建一个TCP端口或者通过named pipe使得远程的Windbg可以连接到本地调试。双方都可以输入命令执行结果在双方的Windbg上都显示出来。具体介绍参考Windbg中关于.server命令的帮助。

另外一种更为强大的方法是使用DbgSrv。DbgSrv是一个调试服务。跟.server命令不同的地方在于.server只是简单地通过重定向方便远程调试人员检查而实际的调试工作都发生在目标机器上。DbgSrv则是让非常必要的调试动作发生在目标机器上而次要的调试功能比如加载PDB显示符号等发生在调试人员的机器上。在DbgSrv出现以前调试系统的核心服务比如lsass.exe进程需要同时结合用户态调试器和内核调试器而且符号文件必须位于目标机器上。DbgSrv的出现让这个过程大为简化请参考

 

 

Debugging LSASS ... oh what fun, it is to ride..
http://blogs.msdn.com/spatdsg/archive/2005/12/27/507265.aspx

 

 

2.1.8  如何通过Windbg命令行让中文魔兽争霸运行在英文系统上

买了中文版的魔兽争霸但家里的Windows却是英文版。中文的魔兽争霸必须要运行到中文的操作系统上否则就报告操作系统语言不匹配。

为了解决这个问题首先能想到的就是到Windows的地区设置里面去把国家改为中国。尝试后发现问题依旧。看来魔兽争霸判断的并非本地Local设置而是操作系统的语言版本。怎么办呢重装系统去网上找破解其实Windbg就可以解决问题。

获取系统语言版本的API是GetSystemDefaultUILanguage。所以可以在这个 API上设定条件断点然后观察魔兽争霸判断语言版本的逻辑是怎样的。

用Windbg启动war3.exe然后在GetSystemDefaultUILanguage上设定断点。API触发后发现调用完这个API后war3.exe的下一条语句是一个cmp eaxChineselanID判断当前是否是中文系统。如果不是中文就退出程序。

那好在cmp这条语句上设定断点然后用下面的命令把eax修改成中文语言符的 ID就可以欺骗程序让程序认为当前系统是中文

 

 

r eax = 0n2052

eax被修改成了中文的语言符后接下来的cmp执行结果就跟中文系统上的一样了war3就可以正确运行了。每次都要做这样的修改麻烦得很为了简化这个过程创建内容为如下的script文件在GetSystemDefaultUILanguage API返回前把ax设定为0n2052

 

 

 

 

bp kernel32!GetSystemDefaultUILanguage+0x2c "r ax=0n2052;g"

 

 

每次启动魔兽争霸都要手动设定断点是很麻烦的事情。如果要简化整个过程可以采用下面文章中介绍的方法让war3.exe启动的时候自动启动Windbg通过-cf参数自动执行我们的条件断点来达到欺骗war3的目的。

 

 

How to debug Windows services
http://support.microsoft.com/?kbid=824344

 

 

2.1.9  Dump文件

前面提到过dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。当在调试器中打开dump文件时使用前面介绍的命令检查看到的结果跟用调试器检查进程是一样的。
在Windbg中可以通过.dump命令保存进程的dump文件。比如下面的命令把当前进程的镜像保存为c:\testdump.dmp文件

 

 

.dump /ma C:\testdump.dmp

 

 

其中的/ma参数表示dump文件应该包含进程的完整信息包括整个用户态的内存这样dump文件尺寸会比较大信息非常全面。如果不使用/ma参数保存下来的dump文件只包含了部分重要资料比如寄存器和线程栈空间文件尺寸会比较小无法分析所有的数据。

在Windbg中通过File→Open Crash Dump菜单可以打开dump文件进行分析。打开dump文件后运行调试命令看到的信息和状态就是dump文件保存时进程的状态。通过dump文件能够方便地保存发生问题时进程的状态方便事后分析。

2.1.10  CDB、NTSD和重定向到Kernel Debugging

除了Windbg另外有两个调试器分别叫做CDB和NTSD。Windbg、CDB和NTSD三者使用的命令都完全一样。只是Windbg提供了窗口接口剩下两个是基于命令行的工具。NTSD位于system32目录下不需要特别安装。
这3个工具其实都使用了同样的调试引擎dbgeng.dll。关于调试引擎的详细信息请参考

 

 

Symbols and Crash Dumps
http://msdn.microsoft.com/msdnmag/issues/02/06/Bugslayer/

 

 

由于CDB和NTSD采用命令行标准输入输出所以可以很方便地通过重定向来控制这两个工具。一个典型的用例就是可以把用户态的调试重定向到Kernel Debugger。这样只需要一个Debugging Session就可以同时控制核心态和用户态的调试例程。详细信息请参考Windbg 帮助中的CDB and NTSD小结。

2.1.11  Debugger Extension扩展Windbg的功能

Debugger Extension相当于是用户自定义可编程的Windbg插件。一个最有用的extension就是.NET Framework 提供的sos.dll。它可以用来检查.NET程序中的内存、线程、callstack、appdomain、assembly等等信息。关于sos.dll后面会作详细讲解。关于如何开发自己的Debugger Extension可以参考

 

 

 

Debug Tutorial Part 4: Writing WINDBG Extensions
http://www.codeproject.com/debug/cdbntsd4.asp

 

 

 

时间: 2024-10-30 05:35:27

第二章排错的工具:调试器Windbg(上)的相关文章

简单MVVM教程(改进版) - 第二章

问题描述 第一章见.上一次,我只是开了个头而已,然而在这一章中,我们将看到一点实际的代码了.我构想了很久,怎样让新手能快速掌握我想要传达的知识,然后我得出一个结论:一定一定要简单化,并且要有看的见摸的着的代码实例.好吧,我们开始.打开你的VS2010,新建一个WPF项目,命名为MvvmTutorial即可.紧接着,在当前Solution添加4个文件夹,分别为:Infrastructure,Views,ViewModels,Models.然后,把App.xaml改成如下:<Applicationx

《Oracle DBA工作笔记》第二章 常用工具和问题分析

<Oracle DBA工作笔记>第二章 常用工具和问题分析   一.1  BLOG文档结构图     一.2  本文简介 建荣的新书<Oracle DBA工作笔记>第二章的目录如下图,主要讲解了SQL*Plus.exp/imp.expdp/impdp以及常见的问题分析,第二章的目录如下:     下边小麦苗将自己阅读完第二章后整理的一些内容分享给大家. 一.3  第一章内容修改 一.3.1  删除数据库的几种方式 这个内容是第一章(http://blog.itpub.net/267

BuGLe 0.0.20120312发布 图形调试器工具

BuGLe 是一个合并了OpenGL命令流上的过滤器选择的图形调试器.该调试器http://www.aliyun.com/zixun/aggregation/18736.html">允许用户查看状态.纹理.帧缓冲和着色器.其过滤器允许用户进行日志记录.错误检查.免费摄像机控制.视频捕捉等等. BuGLe 0.0.20120312该版本大大提高了对OpenGL 3.0及以后的支持. 软件信息:http://www.opengl.org/sdk/tools/BuGLe/ 下载地址:http:/

BuGLe 0.0.20111115发布 图形调试器合并工具

BuGLe合并了OpenGL命令流上的过滤器选择的图形调试器.该调试器http://www.aliyun.com/zixun/aggregation/18736.html">允许用户查看状态.纹理.帧缓冲和着色器.其过滤器允许用户进行日志记录.错误检查.免费摄像机控制.视频捕捉等等. BuGLe 0.0.20111115是一个维护版本,没有重大的新功能添加,但改进了一些性能,以及多个错误的修正.该版本能更好地运行在Ubuntu 11.10版本. 软件信息:http://www.opengl

Xdebug PHP 调试器的使用详解

  虽然您可以使用 PHP 为系统管理和传统数据处理之类的任务创建命令行脚本,但是编程语言对 Web 应用程序的性能有主要影响.在使用过程中,每个 PHP 应用程序都驻留在服务器上,并且将通过代理(例如 Apache)调用 PHP 应用程序处理到来的请求.对于每个请求,典型的 PHP Web 应用程序在简短运行后将得到一个 Web 页面或 XML 数据结构. 假定经过简单的运行后,一个分层构造的 Web 应用程序 -- 包括客户机.网络.HTTP 服务器.应用程序代码和底层数据库 -- 将会很难

&amp;gt; 第二章 NGWS Runtime 技术基础(rainbow 翻译) (转自重粒子空

<<展现C#>> 第二章 NGWS Runtime 技术基础(rainbow 翻译)   出处:http://www.informit.com/matter/ser0000001/chapter1/ch02.shtml 正文: 第二章  NGWS  runtime 技术基础     既然你已经具有了C#全面的印象,我也想让你了解NGWS runtime的全貌.C#依靠由NGWS提供的运行时:因此,有必要知道运行时如何工作,以及它背后所蕴含的概念.    所以,这一章分为两部分--它

《.net编程先锋C#》第二章 理论基础-公用语言 运行环境(转)

编程 第二章 理论基础-公用语言 运行环境既然你已经具有了C#全面的印象,我也想让你了解NGWS runtime的全貌.C#依靠由NGWS提供的运行时:因此,有必要知道运行时如何工作,以及它背后所蕴含的概念.所以,这一章分为两部分--它们是所有的概念和使用的基础.两部分的内容虽然有些重叠,但它有助于加深理解正在学习的概念. 2.1 NGWS RuntimeNGWS和NGWS Runtime为你提供了一种运行时环境.该运行时管理执行代码,并提供了使编程更容易的服务.只要你的编译器支持这种运行时,你

gcc/g++编译器和gdb调试器

gcc and g++分别是gnu的c & c++编译器 gcc/g++在执行编译工作的时候,总共需要4步 1.预处理,生成.i的文件[预处理器cpp]2.将预处理后的文件不转换成汇编语言,生成文件.s[编译器egcs]3.有汇编变为目标代码(机器代码)生成.o的文件[汇编器as]4.连接目标代码,生成可执行程序[链接器ld] [参数详解]-x language filename 设定文件所使用的语言,使后缀名无效,对以后的多个有效.也就是根据约定C语言的后缀名称是.c的,而C++的后缀名是.C

调试器的工作原理(一):基础篇

这一篇会讲什么 我将为大家展示 Linux 中调试器的主要构成模块 - ptrace 系统调用.这篇文章所有代码都是基于 32 位 Ubuntu 操作系统.值得注意的是,尽管这些代码是平台相关的,将它们移植到其它平台应该并不困难. 缘由 为了理解我们要做什么,让我们先考虑下调试器为了完成调试都需要什么资源.调试器可以开始一个进程并调试这个进程,又或者将自己同某个已经存在的进程关联起来.调试器能够单步执行代码,设定断点并且将程序执行到断点,检查变量的值并追踪堆栈.许多调试器有着更高级的特性,例如在