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

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

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

 

 

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

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

Programming Applications for Microsoft Windows 
Debugging Applications for Windows     

2.1  排错的工具:调试器Windbgsession

本节介绍调试器Windbg的相关知识。Windbg的使用贯穿本书不少章节,它是分析问题的高效工具。
Windbg的下载地址是:数据结构

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

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

开发人员写完代码,一般会在Visual Studio中按F5直接进行调试。使用Visual Studio自带调试器可以很是方便地在源代码上设定断点,检查程序的中间变量,单步骤执行。在完成代码阶段,Visual Studio自带的调试器可以很是方便地作源代码级别的排错。ide

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文件除了包含符号外,还负责把符号和该符号所处的二进制地址联系起来。好比有一个全局变量叫作gBuffer,PDB文件不只仅记录了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览:罂吹降慕涌谌缤?.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外,还有db(byte),du(Unicode),dc(char)等等。详细信息请参考帮助文档中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

 

 

用bd(breakpoint 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帮助中的sx(Set 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这个全局变量的时候停下来。j(judge)命令的做用是对后面的表达式做条件判断,若是为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命令换行;3)g命令继续执行。
若是为假,那么就执行第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 eax,ChineselanID,判断当前是不是中文系统。若是不是中文,就退出程序。

那好,在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