一、前言
测试驱动开发(TDD)是以测试作为开发过程的中心,它坚持,在编写实际代码之前,先写好基于产品代码的测试代码。开发过程的目标就是首先使测试能够通过,然后再优化设计结构。测试驱动开发式是极限编程的重要组成部分。XUnit,一个基于测试驱动开发的测试框架,它为我们在开发过程中使用测试驱动开发提供了一个方便的工具,使我们得以快速的进行单元测试。XUnit的成员有很多,如JUnit,PythonUnit等。今天给大家介绍的CppUnit即是XUnit家族中的一员,它是一个专门面向C++的测试框架。
本文不对CppUnit源码做详细的介绍,而只是对CppUnit的应用作一些介绍。你将看到:
- CppUnit源代码的各个组成部分;
- 怎样设置你的开发环境以能够使用CppUnit;
- 怎样为你的产品代码添加测试代码(实际上应该反过来,为测试代码添加产品代码。在TDD中,先有测试代码后有产品代码),并通过CppUnit来进行测试;
本文叙述背景为:CppUnit1.12.0, Visual C++ 6.0, WindowsXP。文中叙述有误之处,敬请批评指正。
一. CppUnit的安装
从http://sourceforge.net/projects/cppunit CppUnit的源码包. CppUnit是开源产品 , 当前最高版本为1.12.0. (在上面的链接所指向的页面上选择 Development Snapshot ).
下载后,将源码包解压缩到本地硬盘,例如解压到E:\ cppunit-1.12.0。笔者把文件夹名称中的版本号去掉,即源码包解压缩到E:\cppunit。下载解压后,你将看到如下文件夹:
主要的文件夹有:
- doc: CppUnit的说明文档。另外,代码的根目录,还有三个说明文档,分别是INSTALL,INSTALL-unix,INSTALL-WIN32.txt;
- examples: CpppUnit提供的例子,也是对CppUnit自身的测试,通过它可以学习如何使用CppUnit测试框架进行开发;
- include: CppUnit头文件;
- src: CppUnit源代码目录;
- config:配置文件;
- contrib:contribution,其他人贡献的外围代码;
- lib:存放编译好的库;
- src:源文件,以及编译库的project等;
接下来进行编译工作。 在src/目录下, 将CppUnitLibraries.dsw工程文件用vc 打开。执行build/batch build,编译成功的话,生成的库文件将被拷贝到lib目录下。中途或者会有些project编译失败,一般不用管它,我们重点看的是cppunit和TestRunner 这两个project的debug和release版本。
编译通过以后, 在lib/目录下,会生成若干lib,和dll文件, 都以cppunit开头. cppunitd表示debug版, cppunit表示release版。
CppUnit为我们提供了两套框架库,一个为静态的lib,一个为动态的dll。cppunit project:静态lib;cppunit_dll project:动态dll和lib。在开发中我们可以根据实际情况作出选择。
你也可以根据需要选择所需的项目进行编译,其中项目cppunit为静态库,cppunit_dll为动态库,生成的库文件为:
- cppunit.lib:静态库release版;
- cppunitd.lib:静态库debug版;
- cppunit_dll.lib:动态库release版;
- cppunitd_dll.lib:动态库debug版;
另外一个需要关注的project是TestRunner,它输出一个dll,提供了一个基于GUI 方式的测试环境,在CppUnit下, 可以选择控制台方式和GUI方式两种表现方案。两种方案分别如下图所示:
我们选择GUI方式,所以我们也需要编译这个project,输出位置亦为lib文件夹。
要使用CppUnit,还得设置好头文件和库文件路径,以VC6为例,选择Tools/Options/Directories,在Include files和Library files中分别添加%CppUnitPath%\include和%CppUnitPath%\lib,其中%CppUnitPath%表示CppUnit所在路径。本文这里分别填的是E:\CPPUNIT\INCLUDE和E:\CPPUNIT\LIB。
二、概念
在使用之前,我们有必要认识一下CppUnit中的主要类,当然你也可以先看后面的例子,遇到问题再回过头来看这一节。
CppUnit核心内容主要包括一些关键类:
Test:所有测试对象的基类。
CppUnit采用树形结构来组织管理测试对象(类似于目录树,如下图所示),因此这里采用了组合设计模式(Composite Pattern),Test的两个直接子类TestLeaf和TestComposite分别表示“测试树”中的叶节点和非叶节点,其中TestComposite主要起组织管理的作用,就像目录树中的文件夹,而TestLeaf才是最终具有执行能力的测试对象,就像目录树中的文件。
Test最重要的一个公共接口为:
virtual void run(TestResult *result) = 0;
其作用为执行测试对象,将结果提交给result。
在实际应用中,我们一般不会直接使用Test、TestComposite以及TestLeaf,除非我们要重新定制某些机制。
TestFixture:用于维护一组测试用例的上下文环境。
在实际应用中,我们经常会开发一组测试用例来对某个类的接口加以测试,而这些测试用例很可能具有相同的初始化和清理代码。为此,CppUnit引入TestFixture来实现这一机制。
TestFixture具有以下两个接口,分别用于处理测试环境的初始化与清理工作:
virtual void setUp();
virtual void tearDown();
TestCase:测试用例,从名字上就可以看出来,它便是单元测试的执行对象。
TestCase从Test和TestFixture多继承而来,通过把Test::run制定成模板函数(Template Method)而将两个父类的操作融合在一起,run函数的伪定义如下:
// 伪代码
void TestCase::run(TestResult* result)
{
result->startTest(this); // 通知result测试开始
if( result->protect(this, &TestCase::setUp) ) // 调用setUp,初始化环境
result->protect(this, &TestCase::runTest); // 执行runTest,即真正的测试代码
result->protect(this, &TestCase::tearDown); // 调用tearDown,清理环境
result->endTest(this); // 通知result测试结束
}
这里要提到的是函数runTest,它是TestCase定义的一个接口,原型如下:
virtual void runTest();
用户需从TestCase派生出子类并实现runTest以开发自己所需的测试用例。
另外还要提到的就是TestResult的protect方法,其作用是对执行函数(实际上是函数对象)的错误信息(包括断言和异常等)进行捕获,从而实现对测试结果的统计。
TestSuit:测试包,按照树形结构管理测试用例
TestSuit是TestComposite的一个实现,它采用vector来管理子测试对象(Test),从而形成递归的树形结构。
TestFactory:测试工厂
这是一个辅助类,通过借助一系列宏定义让测试用例的组织管理变得自动化。参见后面的例子。
TestRunner:用于执行测试用例
TestRunner将待执行的测试对象管理起来,然后供用户调用。其接口为:
virtual void addTest( Test *test );
virtual void run( TestResult &controller, const std::string &testPath = "" );
这也是一个辅助类,需注意的是,通过addTest添加到TestRunner中的测试对象必须是通过new动态创建的,用户不能删除这个对象,因为TestRunner将自行管理测试对象的生命期。
三、CppUnit 的使用
以上工作完成以后,就可以正式使用CppUnit了,由于单元测试是TDD(测试驱动开发)的利器,一般人会先写测试代码,然后再写产品代码,不过笔者认为先写产品代码框架后再写测试代码,然后通过慢慢补充产品代码以使得能通过测试的方法会好些。不管先写谁只要写得舒服安全就可以。本文决定先写测试代码。
前面我们提到过,CppUnit最小的测试单位是TestCase,多个相关TestCase组成一个TestSuite。要添加测试代码最简单的方法就是利用CppUnit为我们提供的几个宏来进行(当然还有其他的手工加入方法,但均是殊途同归,大家可以查阅CppUnit头文件中的演示代码)。这几个宏是:
CPPUNIT_TEST_SUITE() 开始创建一个TestSuite;
CPPUNIT_TEST() 添加TestCase;
CPPUNIT_TEST_SUITE_END() 结束创建TestSuite;
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION() 添加一个TestSuite到一个指定的TestFactoryRegistry工厂。
感兴趣的朋友可以在HelperMacros.h看看这几个宏的声明,本文在此不做详述。
假定我们要实现一个类,类名暂且取做CPlus,它的功能主要是实现两个数相加(多简单的一个类啊,这也要测试吗?不要紧,我们只是了解怎样加入测试代码来测试它就行了,所以越简单越好)。 假定这个类要实现的相加的方法是:
int Add(int nNum1, int nNum2);
OK,那我们先来写测试这个方法的代码吧。TDD 可是先写测试代码,后写产品代码(CPlus)的哦!先写的测试代码往往是不能运行或编译的,我们的目标是在写好测试代码后写产品代码,使之编译通过,然后再进行重构。这就是Kent Beck说的“red/green/refactor”。所以,上面的类名和方法应该还只是在你的心里,还只是你的idea而已。
根据测试驱动的原理,我们需要先建立一个单元测试框架。我们在VC中为测试代码建立一个project。通常,测试代码和被测试对象(产品代码)是处于不同的project中的。这样就不会让你的产品代码被测试代码所“污染 ”。
由于在CppUnit下, 可以选择控制台方式和UI方式两种表现方案,我们选择UI方式。在本例中,我们将建立一个基于GUI 方式的测试环境。因此我们建立一个基于对话框的Project。假设名为UnitTest。
建立了UnitTest project之后,我们首先配置这个工程。
首先在project中打开RTTI开关,具体位置在菜单Project/Settings/C++/C++ Language。如下图所示设置:
由于CppUnit所用的动态运行期库均为多线程动态库,因此你的单元测试程序也得使用相应设置,否则会发生冲突。于是我们在Project/Settings/C++/Code Generation中进行如下设置:
在Use run-time library一栏中,针对debug和release分别设置为‘Debug Multithreaded DLL’和‘Multithreaded DLL’。如下图所示:
最后别忘了在project中link正确的lib。包括本例采用的cppunit.lib和cppunitd.lib静态库以及用于GUI方式的TestRunner.dll对应的lib。具体位置在Project/Settings/Link/General
在‘Object/library modules’中,针对debug和release分别加入cppunitd.lib testrunnerd.lib和cppunit.lib TestRunner.lib。如下图所示:
最后,由于TestRunner.dll为我们提供了基于GUI的测试环境。为了让我们的测试程序能正确的调用它,TestRunner.dll必须位于你的测试程序的路径下。所以把/lib目录下的testrunnerd.dll和TestRunner.dll文件分别拷贝到UnitTest priject的程序debug和release版本输出目录中。如下图所示:
(这是release版本)只要放在一起就可以了。
配置工作终于完成,下面开始写测试框架。
在CppUnit中, 是以TestCase为最小的测试单位, 若干TestCase组成一个TestSuite。所以我们要先建立一个TestCase。
在UnitTest project中新建一个类, 命名为CPlusTestCase, 让其从CppUnit::TestCase派生。为其新增一个方法,假设为 void testAdd(); 我们将在这个函数中写入我们的一些测试代码(还记得我们要测试的构想中的CPlus::Add(…)吗)。代码如下:切记要包含头文件
#include <cppunit/TestCase.h>
class CPlusTestCase : public CppUnit::TestCase
{
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
};
接下来, 我们要对我们的CPlusTestCase进行声明。声明用到了三个宏.
CPPUNIT_TEST_SUITE();
CPPUNIT_TEST();
CPPUNIT_TEST_SUITE_END();
第一个宏声明一个测试包(suite),第二个宏声明(添加)一个测试用例. 现在我们的CPlusTestCase类看上去象这样:切记要包含头文件,否则无法识别这些宏。
#include <cppunit/TestCase.h>
#include <CppUnit/extensions/HelperMacros.h>
class CPlusTestCase : public CppUnit::TestCase
{
CPPUNIT_TEST_SUITE(CPlusTestCase);
CPPUNIT_TEST(testAdd);
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
};
通过这几个宏,我们就把CPlusTestCase和testAdd注册到了测试列表当中。
接下来,我们要注册我们的测试suite. 使用CPPUNIT_TEST_SUITE_NAMED_REGISTRATION()来注册一个测试suite. 这个宏的第二个参数是我们注册的suite的名字. 在这里我们可以用字符串来代替, 但我们用一个静态函数来返回这个suite的名字.
// PlusTestCase.h
class CPlusTestCase : public CppUnit::TestCase
{
CPPUNIT_TEST_SUITE(CPlusTestCase);
CPPUNIT_TEST(testAdd);
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
static std::string GetSuiteName();
};
// PlusTestCase.cpp
std::string CPlusTestCase::GetSuiteName()
{
return " CPlus ";
}
记得要在PlusTestCase.h中包含 #include <string>
然后在 PlusTestCase.cpp注册我们的suite.
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CPlusTestCase, CPlusTestCase::GetSuiteName());
它将CPlusTestCase这个TestSuite注册到一个指定的TestFactory工厂中。
接下来我们写一个注册函数static CppUnit::Test* GetSuite(), 使其在运行期生成一个Test.
// PlusTestCase.h
class CPlusTestCase : public CppUnit::TestCase
{
CPPUNIT_TEST_SUITE(CPlusTestCase);
CPPUNIT_TEST(testAdd);
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
static std::string GetSuiteName();
static CppUnit::Test* GetSuite();
};
// PlusTestCase.cpp
CppUnit::Test* CPlusTestCase::GetSuite()
{
CppUnit::TestFactoryRegistry& reg =
CppUnit::TestFactoryRegistry::getRegistry (CPlusTestCase::GetSuiteName());
return reg.makeTest();
}
记住在PlusTestCase.h中包含头文件:
#include <cppunit/extensions/TestFactoryRegistry.h>
最后, 我们为单元测试建立一个UI测试界面.
由于我们希望这个Project运行后显示的是GUI界面,所以我们需要在App的 InitInstance ()中屏蔽掉原有的对话框,代之以CppUnit的GUI。
我们在CUnitTestApp::InitInstance()函数中,将原先显示主对话框的代码以下面的代码取代:
CppUnit::MfcUi::TestRunner runner;
runner.addTest(CPlusTestCase::GetSuite());//添加测试
runner.run();//show UI
/* CUnitTestDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
*/
切记必须先在UnitTest.cpp中包含头文件:
#include <cppunit/ui/mfc/TestRunner.h>
#include " PlusTestCase.h "
到此为止, 我们已经建立好一个简单的单元测试框架。测试框架虽然写好了,但是测试代码仍然为空,产品代码也还没有写。下面我们来写测试代码:
如前所述,在测试类中,我们添加了一个测试方法:
void testAdd();
它测试的对象是前面提到的CPlus类的方法:int Add(int nNum1, int nNum2);(产品代码)我们来看看testAdd()的实现:记得在PlusTestCase.h中包含头文件
#include <cppunit/TestAssert.h>
// PlusTestCase.cpp
void CPlusTestCase::testAdd()
{
CPlus plus;
int nResult = plus.Add(10, 20); //执行Add操作
CPPUNIT_ASSERT_EQUAL(30, nResult); //检查结果是否等于30
}
CPPUNIT_ASSERT_EQUAL是一个判断结果的宏。CppUnit中类似的其它宏请查阅TestAssert.h,本文在此不做详述 。
另外,我们还可以覆写基类的 setUp()、tearDown()两个函数。这两个函数实际上是一个模板方法,在测试运行之前会调用setUp()以进行一些初始化的工作,测试结束之后又会调用tearDown()来做一些“善后工作” ,比如资源的回收等等。当然,你也可以不覆写这两个函数,因为它们在基类里定义成了空方法,而不是纯虚函数。
编写完上面的测试代码后,进行编译。编译肯定通不过,编译器会告诉我们CPlus类没有声明,因为我们还没有实现CPlus类呢!现在的工作就是马上实现CPlus类,让编译通过。现在你应该嗅到一点“测试驱动”(Test Driven Develop)的味道了吧?
在VC中建立一个MFC Extension Dll的Project,在这个Project 中加入类CPlus,它的声明如下:
// Plus.h
class AFX_EXT_CLASS CPlus
{
public:
CPlus();
virtual ~CPlus();
public:
int Add(int nNum1, int nNum2);
};
Add在Plus.cpp中实现如下
int CPlus::Add(int nNum1, int nNum2)
{
return nNum1 + nNum2;//这里可以写一些错误的语句,用来看看错误的结果
}
非常简单,不是吗?现在让前面那个包含测试代码的Project dependent这个Project,并且include 相关头文件 ,Rebuild All,你会发现编译已通过。你体会到了测试代码驱动产品代码了吗?当然我们的这个例子还很简单 ,没有重构这一步骤。
运行我们的测试程序,单击Browse,你就会看到如下图所示的界面:
选择CPlusTestCase::testAdd后,单击Run,你就会看到如下图所示的界面:
这下你应该对前面我们说的TestSuite的名字理解更深了吧。CPlus是一个测试包TestSuite,它的下面包含一个测试用例,这个测试用例下面又包含一个测试方法。
如果我修改CPlus::Add的代码如下:
int CPlus::Add(int nNum1, int nNum2)
{
// return nNum1 + nNum2;
return 2;
}
重新编译通过,运行程序就会发现:
GUI显示有一个单元测试不通过,并显示出错的地方和原因,这样就很好的控制Bug了。
四、下面是完整的程序清单
// PlusTestCase.h
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include <string>
#include <cppunit/TestCase.h>
#include <CppUnit/extensions/HelperMacros.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/TestAssert.h>
class CPlusTestCase : public CppUnit::TestCase
{
//通过这几个宏,我们就把CPlusTestCase和testAdd注册到了测试列表当中.
CPPUNIT_TEST_SUITE(CPlusTestCase); //声明一个测试包
CPPUNIT_TEST(testAdd); //声明一个测试用例
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase();
virtual ~CPlusTestCase();
void testAdd(); //测试方法
static std::string GetSuiteName();
//写一个注册函数, 使其在运行期生成一个Test
static CppUnit::Test* GetSuite();
};
// PlusTestCase.cpp
#include "stdafx.h"
#include "UnitTest.h"
#include "PlusTestCase.h"
#include "plus.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
//注册一个测试suite到一个指定的TestFactory工厂中
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CPlusTestCase, CPlusTestCase::GetSuiteName());
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CPlusTestCase::CPlusTestCase()
{
}
CPlusTestCase::~CPlusTestCase()
{
}
void CPlusTestCase::testAdd()
{
CPlus plus;
int nResult = plus.Add(10, 20); //执行Add操作
CPPUNIT_ASSERT_EQUAL(30, nResult); //检查结果是否等于30
}
std::string CPlusTestCase::GetSuiteName()
{
return "CPlus";
}
/*
* 注意:CPlusTestCase::GetSuite()返回一个指向CppUnit::Test的指针.
* 这个指针就是整个测试的起点。
* CppUnit::TestFactoryRegistry::getRegistry()根据TestSuite的名字返回TestFactoryRegistry工
* 然后调用工厂里的makeTest()对TestSuite进行组装,将建立起一个树状的测试结构。
*/
CppUnit::Test* CPlusTestCase::GetSuite()
{
CppUnit::TestFactoryRegistry& reg = CppUnit::TestFactoryRegistry::getRegistry(CPlusTestCase::GetSuiteName());
return reg.makeTest();
}
// UnitTest.cpp
#include "stdafx.h"
#include "UnitTest.h"
#include <cppunit/ui/mfc/TestRunner.h>
#include "PlusTestCase.h"
…
/////////////////////////////////////////////////////////////////////////////
// CUnitTestApp initialization
BOOL CUnitTestApp::InitInstance()
{
…
CppUnit::MfcUi::TestRunner runner;
runner.addTest(CPlusTestCase::GetSuite()); //添加测试 runner.addTest(CMinusTestCase::GetSuite());
runner.run(); //show UI
/* CUnitTestDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
*/
return FALSE;
}
五、参考资料
- Cpluser《CppUnit测试框架入门》
- Freefalcon《CppUnit快速入门》
- 《使用CppUnit进行单元测试》
[转载]通往WinDbg的捷径(二) 原文:http://www.debuginfo.com/articles/easywindbg2.html
译者:arhat
时间:2006年4月14日
关键词:CDB WinDbg
保存 dumps 在我们调试不容易重现的问题时,可能想把应用程序状态的快照(内存内容,打开名柄的列表,等等)保存起来,以便日后分析。例如,当我怀疑当前的状态可能包含我试图解决的问题的关键点,而想继续运行应用程序来查看情形怎样发展时,它就很有用了。有时候,我会做一系列的快照,一个接一个,以便稍后我能比较它们,查看在应用程序运行时有些数据结构怎样变化。当我最终能重现这个问题时,我总是创建一个快照来确保我没有因为某些错误(错误关闭了调试会话)而丢失有价值的信息。或许,大家不难猜到当我说“快照”时,我真正的意思是“minidump”,因为minidump为随时保存应用程序的状态提供了便利。
下面是创建minidump的命令行示例:
cdb -pv -pn myapp.exe -c ".dump /m c:\myapp.dmp;q"
让我们仔细看一下.dump命令。在上面的例子里,我们只用到这条命令的一个选项(/m),后面跟着minidump的文件名。用/m来指定minidump里应当包括哪种信息。最重要的(依我之见)/m选项的变量列在下表中:
---------------------------------------------------------------------------------------------------------------------------
选项 描述 例子
---------------------------------------------------------------------------------------------------------------------------
/m 默认就是这个选项。它创建标准的minidump,等同于MiniDumpNormal minidump类型。由此生成的minidump
一般很小,因此,如果你想通过慢速的网络传输minidump,那么这个选项非常有用。但不幸地是,小体积的
minidump也意味着在大多数情况下,它包含的信息不足以进行完整的分析(你可以在
这篇文章里找到更多有
关minidump内容的信息)。 dump /m c:\myapp.dmp
---------------------------------------------------------------------------------------------------------------------------
/ma 带所有可选项的Minidump(完整的内存内容,名柄,已卸载的模块,等等),由此生成的minidump将非常
大。如果可以随意使用磁盘空间,这个选项将非常适合本地调试。 .dump /ma c:\myapp.dmp
---------------------------------------------------------------------------------------------------------------------------
/mFhutwd 这个选项将生成带数据段,非共享读/写内存页和其它有用信息的minidump。如果你想尽可能的收集信息,
但仍想使minidump保持小体积(并压缩),就可以用这个选项。 .dump /mFhutwd c:\myapp.dmp
---------------------------------------------------------------------------------------------------------------------------
下面的命令生成包含所有信息的minidump:
cdb -pv -pn myapp.exe -c ".dump /ma c:\myapp.dmp;q"
如果我们想生成一个新minidump,并覆盖已有的,该怎么办呢?在默认情况下,.dump命令不允许这样做??它会抱怨文件已经存在。为了改变默认行为,覆盖已存在的.dump文件,我们可以用/o选项:
cdb -pv -pn myapp.exe -c ".dump /ma /o c:\myapp.dmp;q"
如果我们想生成一系列的minidump,一个接一个,那么它能很方便的为minidump命名,并使文件名反映生成minidump时的时间吗。嗯,如果我们指定了/u选项,.dump命令就可以自动为我们这样做,这真是一个好消息,不是吗?例如,下面的命令可以生成名为myapp_02CC_
2006-01-28_04-11-18-171_0158.dmp的minidump(0158是进程ID):
cdb -pv -pn myapp.exe -c ".dump /m /u c:\myapp.dmp;q"
.dump命令也支持其它有趣的选项(你可以在文档里发现它们)。
如果你想生成运行在Visual Studio调试器下的进程的minidump,我建议在生成dump前,先在Visual Studio里临时禁用所有的断点。如果没有禁用断点,生成的minidump将包含Visual Studio调试器插入目标进程代码里的断点指令(int 3)。
分析故障转储CDB也可以用于自动分析故障转储。当我们分析故障转储时,通常会执行同样的操作,所以可以把这些操作自动化。什么样的操作呢?这要看故障转储的类型。我把所有的故障转储分成两大类:
• 带异常信息的故障转储
• 不带异常信息的故障转储
当应用程序引发未经处理的异常并调用just-in-time调试器(Dr. Watson,
NTSD , 或其它的调试器),或者用为
未经处理的异常定制的过滤器 生成minidump时,通常会生成带异常信息的故障转储。通过写入故障转储里的异常信息,我们可以确定异常的类型和发生时它在代码里的位置。当我们想为以后的分析生成进程的快照时(例如,这方面的描述参见本文的前一部分“保存dumps”),通常手动生成不带异常信息的故障转储。
当我们调试带异常信息的故障转储时,通常想知道下面这些信息:
• 异常在代码中出现的位置(地址,源文件和行号)
• 异常发生时的调用栈
• 调用栈上一些或所有函数的参数值和局部变量
WinDbg和CDB为调试故障转储提供了非常有用的命令??!analyze。这条命令分析故障转储里的异常信息,确定异常发生的位置,调用栈,并显示详细的报告。下面是这条命令的示例:
cdb -z c:\myapp.dmp -logo out.txt -lines -c "!analyze -v;q"
(-v选项要求!analyze输出详细的内容)
CrashDemo.cpp 例子演示了怎样用定制的过滤器捕获未经处理的异常并生成minidumps。如果你编译并运行它,然后用上述的CDB命令分析生成的minidump,你将可以得到和下面类似的输出内容:
0:001> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
FAULTING_IP:
CrashDemo!TestFunc+2e [c:\tests\crashdemo\crashdemo.cpp @ 124]
004309de c70000000000 mov dword ptr [eax],0x0
EXCEPTION_RECORD: ffffffff -- (.exr ffffffffffffffff)
.exr ffffffffffffffff
ExceptionAddress: 004309de (CrashDemo!TestFunc+0x0000002e)
ExceptionCode: c0000005 (Access violation)ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000
Attempt to write to address 00000000
DEFAULT_BUCKET_ID: APPLICATION_FAULT
PROCESS_NAME: CrashDemo.exe
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at "0x%08lx" referenced memory
at "0x%08lx". The memory could not be "%s".
WRITE_ADDRESS: 00000000
BUGCHECK_STR: ACCESS_VIOLATION
LAST_CONTROL_TRANSFER: from 0043096e to 004309de
STACK_TEXT:006afe88 0043096e 00000000 00354130 00350001 CrashDemo!TestFunc+0x2e
[c:\tests\crashdemo\crashdemo.cpp @ 124]
006aff6c 00430f31 00000000 52319518 00354130 CrashDemo!WorkerThread+0x5e
[c:\tests\crashdemo\crashdemo.cpp @ 115]
006affa8 00430ea2 00000000 006affec 7c80b50b CrashDemo!_callthreadstartex+0x51
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 348]
006affb4 7c80b50b 00355188 00354130 00350001 CrashDemo!_threadstartex+0xa2
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 331]
006affec 00000000 00430e00 00355188 00000000 kernel32!BaseThreadStart+0x37
FOLLOWUP_IP:
CrashDemo!TestFunc+2e [c:\tests\crashdemo\crashdemo.cpp @ 124]
004309de c70000000000 mov dword ptr [eax],0x0
SYMBOL_STACK_INDEX: 0
FOLLOWUP_NAME: MachineOwner
SYMBOL_NAME: CrashDemo!TestFunc+2e
MODULE_NAME: CrashDemo
IMAGE_NAME: CrashDemo.exe
DEBUG_FLR_IMAGE_TIMESTAMP: 43dc6ee7
STACK_COMMAND: .ecxr ; kbFAILURE_BUCKET_ID: ACCESS_VIOLATION_CrashDemo!TestFunc+2e
BUCKET_ID: ACCESS_VIOLATION_CrashDemo!TestFunc+2e
Followup: MachineOwner
---------
注意用粗体表示的内容。第一处报告了异常的地址和类型。第二外报告调用栈。第三处为我们提供了怎样访问保存在故障转储里的异常信息的额外信息。
现在,我们知道异常发生的位置,甚至可以查看调用栈。那么,是得到函数的参数值及局部变量的时候了。在开始之前,让我们注意!analyze报告中的第三处信息。这里再重复一下第三处所包含的内容:
STACK_COMMAND: .ecxr ; kb
对'kb'命令我们已经不陌生了(它显示调用栈)。但.ecxr是什么?这条命令要求调试器把当前的内容切换到保存在故障转储里的异常信息。我们执行这条命令后,将能访问异常抛出时调用栈和局部变量的值。
在我们要求调试使用异常的上下文后,我们可以用'dv'命令显示函数的参数值以及局部变量。因为我们通常想查看调用栈上每一个函数的信息,因此,我们可以用'!for_each_frame dv /t'命令(/t选项要求'dv'显示有用的类型信息)。(当然,我们必须记住,使用优化编译时,在函数的整个生存期中,局部变量有可能会被取消,重注册或被重用来保存其它的数据,因此,可能会导致'dv'命令输出错误的值)。
下面是分析带异常信息的故障转储的命令行示例:
cdb -z c:\myapp.dmp -logo out.txt -lines -c "!analyze -v;.ecxr;!for_each_frame dv /t;q"
下面是'!for_each_frame dv /t'命令输出的例子:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
00 006afe88 0043096e CrashDemo!TestFunc+0x2e [c:\tests\crashdemo\crashdemo.cpp @ 124]
int * pParam = 0x00000000
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
01 006aff6c 00430f31 CrashDemo!WorkerThread+0x5e [c:\tests\crashdemo\crashdemo.cpp @ 115]
void * lpParam = 0x00000000
int * TempPtr = 0x00000000
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
02 006affa8 00430ea2 CrashDemo!_callthreadstartex+0x51
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 348]
struct _tiddata * ptd = 0x00355188
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
03 006affb4 7c80b50b CrashDemo!_threadstartex+0xa2
[f:\rtm\vctools\crt_bld\self_x86\crt\src\threadex.c @ 331]
void * ptd = 0x00355188
struct _tiddata * _ptd = 0x00000000
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
04 006affec 00000000 kernel32!BaseThreadStart+0x37
Unable to enumerate locals, HRESULT 0x80004005
Private symbols (symbols.pri) are required for locals.
Type ".hh dbgerr005" for details.
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
00 006afe88 0043096e CrashDemo!TestFunc+0x2e [c:\tests\crashdemo\crashdemo.cpp @ 124]
如果minidump没有包括目标进程内存的完整内容,那么只有当调试器能正确发现被目标进程加载的、相同版本的可执行模块时,才能分析dump。在某些情形下,你必须帮助调试器定位这些模块??通过指定模块搜索路径。关于模块搜索路径的详细信息和相关内容可以在
这篇文章 中找到。
现在,我们来处理不带异常信息的故障转储。当我们分析这样的dump时,通常想知道所有线程的调用栈。下面是怎样得到这些信息:
cdb -z c:\myapp.dmp -logo out.txt -lines -c "~*kb;q"
如果我们不知道故障转储是否包含异常信息,该怎么做呢?对于minidumps来说,我们可以用
MiniDumpView 打印dump的内容,查看它里面是否包含异常信息。对于过时的'full user dumps',或许唯一的选择是,照现在的样子启动包含异常信息的dump,并查看!analyze是否报告了有意义的内容。
有一个有趣的特例??因为未经处理的异常生成故障转储,但因为某些原因没有包含异常信息是有可能的。在这种情形下,在下面过程的帮助下,仍可能找出异常发生的位置:
1. 打印所有线程的调用栈(用前面提过的CDB命令)。
2. 找出包含kernel32!UnhandledExceptionFilter函数调用栈的线程。
3. 使用
UnhandledExceptionFilter 函数的第一个参数(包含一个指向EXCEPTION_POINTERS 结构的指针)的实际值。
下面是EXCEPTION_POINTERS 结构的声明:
typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
如果我们知道这个结构的地址,就能得到指向异常上下文的指针(保存在ContextRecord字段里),把它传递给.cxr命令,从而把调试器上下文切换到异常发生的位置。在.cxr命令执行后,我们可以用'kb'命令得到异常发生时的调用栈。下面是一个例子:
1. 打印所有线程的调用栈。
cdb -z c:\myapp.dmp -logo out.txt -c "~*kb;q"
0:000> ~*kb
. 0 Id: 6c4.73c Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr Args to Child
0012fdf8 7c90d85c 7c8023ed 00000000 0012fe2c ntdll!KiFastSystemCallRet
0012fdfc 7c8023ed 00000000 0012fe2c 0012ff54 ntdll!NtDelayExecution+0xc
0012fe54 7c802451 0036ee80 00000000 0012ff54 kernel32!SleepEx+0x61
0012fe64 00430856 0036ee80 00330033 00300037 kernel32!Sleep+0xf
0012ff54 00431702 00000001 00352ed0 00352fb0 CrashDemo!wmain+0x96
0012ffb8 004314bd 0012fff0 7c816d4f 00330033 CrashDemo!__tmainCRTStartup+0x232
0012ffc0 7c816d4f 00330033 00300037 7ffd9000 CrashDemo!wmainCRTStartup+0xd
0012fff0 00000000 0042e5a5 00000000 00000000 kernel32!BaseProcessStart+0x23
1 Id: 6c4.5cc Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr Args to Child
006af6e4 7c90e273 7c863130 d0000144 00000004 ntdll!KiFastSystemCallRet
006af6e8 7c863130 d0000144 00000004 00000000 ntdll!NtRaiseHardError+0xc
006af96c 00438951
006af9e0 5d343834 00000000
kernel32!UnhandledExceptionFilter+0x59c006af990 00430f2a c0000005 006af9e0 0044ad30 CrashDemo!_XcptFilter+0x61
006af99c 0044ad30 00000000 00000000 00000000 CrashDemo!_callthreadstartex+0x7a
006af9b0 00438c67 00430f13 0049a230 00000000 CrashDemo!_EH4_CallFilterFunc+0x12
006af9e8 7c9037bf 006afad4 006aff98 006afaf0 CrashDemo!_except_handler4+0xb7
006afa0c 7c90378b 006afad4 006aff98 006afaf0 ntdll!ExecuteHandler2+0x26
006afabc 7c90eafa 00000000 006afaf0 006afad4 ntdll!ExecuteHandler+0x24
006afabc 004309be 00000000 006afaf0 006afad4 ntdll!KiUserExceptionDispatcher+0xe
006afe88 0043094e 00000000 00354130 00350001 CrashDemo!TestFunc+0x2e
006aff6c 00430f01 00000000 647bff58 00354130 CrashDemo!WorkerThread+0x5e
006affa8 00430e72 00000000 006affec 7c80b50b CrashDemo!_callthreadstartex+0x51
006affb4 7c80b50b 00355188 00354130 00350001 CrashDemo!_threadstartex+0xa2
006affec 00000000 00430dd0 00355188 00000000 kernel32!BaseThreadStart+0x37
2. 改变调试器的上下文,得到异常的调用栈。
cdb -z c:\myapp.dmp -logo out.txt -lines -c ".cxr dwo(0x006af9e0+4);kb;q"
('dwo'操作符返回保存在指定地址里的double word,并把它传递给.cxr命令)
本篇文章后面提供的批处理文件(实际上是DumpStackCtx.bat)将简化这个任务。
还有另外的方法可以解决这个问题??你可以在
这里 找到更多的信息。
分析虚拟内存当我们想审查被调试进程的虚拟内存布局时,CDB可以协助Visual Studio调试器。下面的命令显示进程完整的虚拟内存映射:
cdb -pv -pn myapp.exe -logo out.txt -c "!vadump -v;q"
(!vadump命令负责打印虚拟内存映射,照常,用-v选项要求它显示详细的内容)
下面是!vadump输出的例子:
BaseAddress: 00040000
AllocationBase: 00040000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 0002e000
State: 00002000 MEM_RESERVE
Type: 00020000 MEM_PRIVATE
BaseAddress: 0006e000
AllocationBase: 00040000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00001000
State: 00001000 MEM_COMMIT
Protect: 00000104 PAGE_READWRITE + PAGE_GUARD
Type: 00020000 MEM_PRIVATE
BaseAddress: 0006f000
AllocationBase: 00040000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00011000
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
在Windows XP和Windows Server 2003上,CDB为审查虚拟内存布局提供了一条更好的命令??!address。这条命令可以完成下面的任务:
• 显示进程的虚拟内存映射(依我之见,比!vadump的输出内容更易阅读)
• 显示有用的、虚拟内存使用的统计数据
• 确定指定的地址属于哪种虚拟内存区域(例如,它是属于栈,堆,还是可执行映象?)
下面是怎样用!address报告虚拟内存映射的示例:
cdb -pv -pn myapp.exe -logo out.txt -c "!address;q"
下面显示被线程的栈占用的内存区域:
00040000 : 00040000 - 0002e000
Type 00020000 MEM_PRIVATE
Protect 00000000
State 00002000 MEM_RESERVE
Usage RegionUsageStack
Pid.Tid 658.644
0006e000 - 00001000
Type 00020000 MEM_PRIVATE
Protect 00000104 PAGE_READWRITE | PAGE_GUARD
State 00001000 MEM_COMMIT
Usage RegionUsageStack
Pid.Tid 658.644
0006f000 - 00011000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage RegionUsageStack
Pid.Tid 658.644注意,!address非常智能,可以报告属于栈的线程的线程ID。
在!address报告虚拟内存区域之后,它也能报告有趣的、虚拟内存使用的统计数据:
-------------------- Usage SUMMARY --------------------------
TotSize Pct(Tots) Pct(Busy) Usage
00838000 : 0.40% 27.96% : RegionUsageIsVAD
7e28c000 : 98.56% 0.00% : RegionUsageFree
01348000 : 0.94% 65.60% : RegionUsageImage
00040000 : 0.01% 0.85% : RegionUsageStack
00001000 : 0.00% 0.01% : RegionUsageTeb
001a0000 : 0.08% 5.53% : RegionUsageHeap
00000000 : 0.00% 0.00% : RegionUsagePageHeap
00001000 : 0.00% 0.01% : RegionUsagePeb
00001000 : 0.00% 0.01% : RegionUsageProcessParametrs
00001000 : 0.00% 0.01% : RegionUsageEnvironmentBlock
Tot: 7fff0000 Busy: 01d64000
-------------------- Type SUMMARY --------------------------
TotSize Pct(Tots) Usage
7e28c000 : 98.56% : <free>
01348000 : 0.94% : MEM_IMAGE
007b6000 : 0.38% : MEM_MAPPED
00266000 : 0.12% : MEM_PRIVATE
-------------------- State SUMMARY --------------------------
TotSize Pct(Tots) Usage
01647000 : 1.09% : MEM_COMMIT
7e28c000 : 98.56% : MEM_FREE
0071d000 : 0.35% : MEM_RESERVE
Largest free region: Base 01014000 - Size 59d5c000
当我们正在调试内存泄露,以及想确定内存泄露的类型(堆,栈,原始的虚拟内存,等等)时,这些统计数据非常有用。通过最后一行,我们可以确定虚拟内存中最大的空闲区域的大小,这在我们必须设计请求大量内存的应用程序时非常有帮助。
如果你只想查看统计数据,而不想看虚拟内存映射,可以用-summary参数:
cdb -pv -pn myapp.exe -logo out.txt -c "!address -summary;q"
如果我们想确定给定地址的虚拟内存属于哪种类型,可以把这个地址作为参数传递给!address命令。下面是一个例子:
0:000> !address 0x000a2480;q
000a0000 : 000a0000 - 000d7000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage
RegionUsageHeap Handle 000a0000
搜索符号有时候,我们可能需要确定符号(函数或变量)的地址。如果我们知道准确的符号名,可以把它输入Visual Studio调试器的反汇编窗口,从中找出它对应的地址。但是,假设我们忘了准确的符号名呢?或者想找出名字上有相同规律的、一组符号的地址(例如,类的所有成员函数)?CDB很容易解决这个问题??它提供'x'命令,可以列出匹配指定掩码的所有符号名:
x Module!Symbol
下面的命令设法定位位于kernel32.dll 中的UnhandledExceptionFilter函数的地址:
cdb -pv -pn notepad.exe -logo out.txt -c "x kernel32!UnhandledExceptionFilter;q"
下面是输出内容:
0:000> x kernel32!UnhandledExceptionFilter;q
7c862b8a kernel32!UnhandledExceptionFilter = <no type information>
'x'命令可以接受多个通配符,提供一些有用的选项,可用于排序输出内容以及输出符号的额外信息??你可以在WinDbg文档里找到更多的信息。例如,下面的命令可以把我们在应用程序的主可执行模块中定义的CmainFrame类所有的成员函数和统计数据列出来:
0:000> x myapp!*CMainFrame*
004542f8 MyApp!CMainFrame::classCMainFrame = struct CRuntimeClass
00401100 MyApp!CMainFrame::`scalar deleting destructor' (void)
004011a0 MyApp!CMainFrame::OnCreate (struct tagCREATESTRUCTW *)
00401000 MyApp!CMainFrame::CreateObject (void)
00401280 MyApp!CMainFrame::PreCreateWindow (struct tagCREATESTRUCTW *)
00401070 MyApp!CMainFrame::GetRuntimeClass (void)
00401120 MyApp!CMainFrame::~CMainFrame (void)
00401090 MyApp!CMainFrame::CMainFrame (void)
00401080 MyApp!CMainFrame::GetMessageMap (void)
004578ec MyApp!CMainFrame::`RTTI Base Class Array' = <no type information>
004578dc MyApp!CMainFrame::`RTTI Class Hierarchy Descriptor' = <no type information>
004578c8 MyApp!CMainFrame::`RTTI Complete Object Locator' = <no type information>
004579ec MyApp!CMainFrame::`RTTI Base Class Descriptor at (0,-1,0,64)' = <no type information>
00461e94 MyApp!CMainFrame `RTTI Type Descriptor' = <no type information>
00454354 MyApp!CMainFrame::`vftable' = <no type information>
CDB也可以反着做??通过地址找符号,使用'ln'命令:
ln Address
下面是它的用法:
cdb -pv -pn notepad.exe -logo out.txt -c "ln 0x77d491c8;q"
下面是它的输出内容:
0:000> ln 0x77d491c8;q
(77d491c6) USER32!GetMessageW+0x2 | (77d49216) USER32!CharUpperBuffW
注意,我们不必指定符号的起始地址(在这个例子里,是函数),而可以用符号占用的地址范围内的任何地址。'ln'将找出符号,报告它的地址,另外还报告跟在指定内容后面的地址和符号名。
显示数据结构如果我们想研究数据结构的内容,通常会用Visual Studio的Watch,QuickWatch或其它类似的窗口。这些窗口允许我们查看结构成员变量的类型和值。但是假设我们也需要知道结构的精确布局,包括它的成员的偏移量?Visual Studio不提供易用的解决方法,但幸运的是,CDB可以。在'dt'命令的帮助下,我们可以显示数据结构或类的精确布局。
如果我们只想了解数据类型的布局,可以用下面这条命令:
dt -b TypeName
(-b选项启用递归显示,显示结构或类的成员类型的嵌入式数据结构)。
下面是命令的示例:
cdb -pv -pn myapp.exe -logo out.txt -c "dt -b CSymbolInfoPackage;q"
下面是输出内容(在运行
SymFromAddr 应用程序时得到的):
0:000> dt /b CSymbolInfoPackage;q
+0x000 si : _SYMBOL_INFO
+0x000 SizeOfStruct : Uint4B
+0x004 TypeIndex : Uint4B
+0x008 Reserved : Uint8B
+0x018 Index : Uint4B
+0x01c Size : Uint4B
+0x020 ModBase : Uint8B
+0x028 Flags : Uint4B
+0x030 Value : Uint8B
+0x038 Address : Uint8B
+0x040 Register : Uint4B
+0x044 Scope : Uint4B
+0x048 Tag : Uint4B
+0x04c NameLen : Uint4B
+0x050 MaxNameLen : Uint4B
+0x054 Name : Char
+0x058 name : Char
如果你想显示特殊变量的布局,可以把它的地址传递给'dt'命令:
dt -b TypeName Address
下面是例子:
cdb -pv -pn myapp.exe -logo out.txt -c "dt -b CSymbolInfoPackage 0x0012f6d0;q"
0:000> dt /b CSymbolInfoPackage 0x0012f6d0;q
+0x000 si : _SYMBOL_INFO
+0x000 SizeOfStruct : 0x58
+0x004 TypeIndex : 2
+0x008 Reserved :
[00] 0
[01] 0
+0x018 Index : 1
+0x01c Size : 0x428
+0x020 ModBase : 0x400000
+0x028 Flags : 0
+0x030 Value : 0
+0x038 Address : 0x411d30
+0x040 Register : 0
+0x044 Scope : 0
+0x048 Tag : 5
+0x04c NameLen : 0xe
+0x050 MaxNameLen : 0x7d1
+0x054 Name : "S"
[00] 83 'S'
+0x058 name : "SymbolInfo"
[00] 83 'S'
[01] 121 'y'
[02] 109 'm'
[03] 98 'b'
[04] 111 'o'
[05] 108 'l'
[06] 73 'I'
[07] 110 'n'
[08] 102 'f'
[09] 111 'o'
[10] 0 ''
[11] 0 ''
[12] 0 ''
[13] 0 ''
[14] 0 ''
[15] 0 ''
[16] 0 ''
[17] 0 ''
... 省略部分输出内容
[1990] 0 ''
[1991] 0 ''
[1992] 0 ''
[1993] 0 ''
[1994] 0 ''
[1995] 0 ''
[1996] 0 ''
[1997] -52 ''
[1998] -52 ''
[1999] -52 ''
[2000] -52 ''
注意,'dt'也显示结构成员变量的值。
批处理文件我们已经知道怎样用CDB解决一些有趣的调试问题了。现在可以用它解决更多的问题了??用易用的批处理文件代替难记的CDB命令行。考虑我们在本文开始部分使用的命令行示例:
cdb -pv -pn myapp.exe -logo out.txt -c "lm;q"
这条命令中的大部分是固定的,用不着改变。唯一可变的部分是目标信息(-pn myapp.exe),我们可能会用另外的可执行文件名,或另外的附着方式(例如,通过进程ID)替换它。
下面介绍了怎样用批处理文件代替这条命令:
; lm.bat
cdb -pv %1 %2 -logo out.txt -c "lm;q"
如果我们运行这个批处理文件,通过进程来得到已加载模块的列表,可以用下面的方法:
通过可执行文件名附上进程:
lm -pn myapp.exe
通过进程ID附上进程:
lm -p 1234
通过服务名附上进程:
lm -psn MyService
打开故障转储文件:
lm -z c:\myapp.dmp
这条命令与具体的目标无关,只做同样的事情??打印已加载模块的列表。
如果我们想为CDB命令指定另外的参数,我们可以用同样的方法。考虑下面用于显示数据结构布局的命令:
cdb -pv -pn myapp.exe -logo out.txt -c "dt /b MyStruct;q"
当然,我们可能想使用任何数据类型运行这条命令,而不仅仅是MyStruct。下面是怎样做的示例:
; dt.bat
cdb -pv %1 %2 -logo out.txt -c "dt /b %3;q"
现在,我们可以象下面这样运行这条命令:
dt -pn myapp.exe CTestClass
或者象这样:
dt -p 1234 SYMBOL_INFO
或者,也可以象这样:
dt -z c:\myapp.dmp EXCEPTION_POINTERS
我们可以用同样的方法处理其它的命令。你可以从
这里 找到本文曾讨论过的批处理文件。在以后,我准备用另外有用的命令扩充它。