一、前言
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)是以測(cè)試作為開(kāi)發(fā)過(guò)程的中心,它堅(jiān)持,在編寫(xiě)實(shí)際代碼之前,先寫(xiě)好基于產(chǎn)品代碼的測(cè)試代碼。開(kāi)發(fā)過(guò)程的目標(biāo)就是首先使測(cè)試能夠通過(guò),然后再優(yōu)化設(shè)計(jì)結(jié)構(gòu)。測(cè)試驅(qū)動(dòng)開(kāi)發(fā)式是極限編程的重要組成部分。XUnit,一個(gè)基于測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的測(cè)試框架,它為我們?cè)陂_(kāi)發(fā)過(guò)程中使用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)提供了一個(gè)方便的工具,使我們得以快速的進(jìn)行單元測(cè)試。XUnit的成員有很多,如JUnit,PythonUnit等。今天給大家介紹的CppUnit即是XUnit家族中的一員,它是一個(gè)專(zhuān)門(mén)面向C++的測(cè)試框架。
本文不對(duì)CppUnit源碼做詳細(xì)的介紹,而只是對(duì)CppUnit的應(yīng)用作一些介紹。你將看到:
- CppUnit源代碼的各個(gè)組成部分;
- 怎樣設(shè)置你的開(kāi)發(fā)環(huán)境以能夠使用CppUnit;
- 怎樣為你的產(chǎn)品代碼添加測(cè)試代碼(實(shí)際上應(yīng)該反過(guò)來(lái),為測(cè)試代碼添加產(chǎn)品代碼。在TDD中,先有測(cè)試代碼后有產(chǎn)品代碼),并通過(guò)CppUnit來(lái)進(jìn)行測(cè)試;
本文敘述背景為:CppUnit1.12.0, Visual C++ 6.0, WindowsXP。文中敘述有誤之處,敬請(qǐng)批評(píng)指正。
一. CppUnit的安裝
從http://sourceforge.net/projects/cppunit CppUnit的源碼包. CppUnit是開(kāi)源產(chǎn)品 , 當(dāng)前最高版本為1.12.0. (在上面的鏈接所指向的頁(yè)面上選擇 Development Snapshot ).
下載后,將源碼包解壓縮到本地硬盤(pán),例如解壓到E:\ cppunit-1.12.0。筆者把文件夾名稱(chēng)中的版本號(hào)去掉,即源碼包解壓縮到E:\cppunit。下載解壓后,你將看到如下文件夾:

主要的文件夾有:
- doc: CppUnit的說(shuō)明文檔。另外,代碼的根目錄,還有三個(gè)說(shuō)明文檔,分別是INSTALL,INSTALL-unix,INSTALL-WIN32.txt;
- examples: CpppUnit提供的例子,也是對(duì)CppUnit自身的測(cè)試,通過(guò)它可以學(xué)習(xí)如何使用CppUnit測(cè)試框架進(jìn)行開(kāi)發(fā);
- include: CppUnit頭文件;
- src: CppUnit源代碼目錄;
- config:配置文件;
- contrib:contribution,其他人貢獻(xiàn)的外圍代碼;
- lib:存放編譯好的庫(kù);
- src:源文件,以及編譯庫(kù)的project等;
接下來(lái)進(jìn)行編譯工作。 在src/目錄下, 將CppUnitLibraries.dsw工程文件用vc 打開(kāi)。執(zhí)行build/batch build,編譯成功的話(huà),生成的庫(kù)文件將被拷貝到lib目錄下。中途或者會(huì)有些project編譯失敗,一般不用管它,我們重點(diǎn)看的是cppunit和TestRunner 這兩個(gè)project的debug和release版本。
編譯通過(guò)以后, 在lib/目錄下,會(huì)生成若干lib,和dll文件, 都以cppunit開(kāi)頭. cppunitd表示debug版, cppunit表示release版。
CppUnit為我們提供了兩套框架庫(kù),一個(gè)為靜態(tài)的lib,一個(gè)為動(dòng)態(tài)的dll。cppunit project:靜態(tài)lib;cppunit_dll project:動(dòng)態(tài)dll和lib。在開(kāi)發(fā)中我們可以根據(jù)實(shí)際情況作出選擇。
你也可以根據(jù)需要選擇所需的項(xiàng)目進(jìn)行編譯,其中項(xiàng)目cppunit為靜態(tài)庫(kù),cppunit_dll為動(dòng)態(tài)庫(kù),生成的庫(kù)文件為:
- cppunit.lib:靜態(tài)庫(kù)release版;
- cppunitd.lib:靜態(tài)庫(kù)debug版;
- cppunit_dll.lib:動(dòng)態(tài)庫(kù)release版;
- cppunitd_dll.lib:動(dòng)態(tài)庫(kù)debug版;
另外一個(gè)需要關(guān)注的project是TestRunner,它輸出一個(gè)dll,提供了一個(gè)基于GUI 方式的測(cè)試環(huán)境,在CppUnit下, 可以選擇控制臺(tái)方式和GUI方式兩種表現(xiàn)方案。兩種方案分別如下圖所示:


我們選擇GUI方式,所以我們也需要編譯這個(gè)project,輸出位置亦為lib文件夾。
要使用CppUnit,還得設(shè)置好頭文件和庫(kù)文件路徑,以VC6為例,選擇Tools/Options/Directories,在Include files和Library files中分別添加%CppUnitPath%\include和%CppUnitPath%\lib,其中%CppUnitPath%表示CppUnit所在路徑。本文這里分別填的是E:\CPPUNIT\INCLUDE和E:\CPPUNIT\LIB。

二、概念
在使用之前,我們有必要認(rèn)識(shí)一下CppUnit中的主要類(lèi),當(dāng)然你也可以先看后面的例子,遇到問(wèn)題再回過(guò)頭來(lái)看這一節(jié)。
CppUnit核心內(nèi)容主要包括一些關(guān)鍵類(lèi):
Test:所有測(cè)試對(duì)象的基類(lèi)。
CppUnit采用樹(shù)形結(jié)構(gòu)來(lái)組織管理測(cè)試對(duì)象(類(lèi)似于目錄樹(shù),如下圖所示),因此這里采用了組合設(shè)計(jì)模式(Composite Pattern),Test的兩個(gè)直接子類(lèi)TestLeaf和TestComposite分別表示“測(cè)試樹(shù)”中的葉節(jié)點(diǎn)和非葉節(jié)點(diǎn),其中TestComposite主要起組織管理的作用,就像目錄樹(shù)中的文件夾,而TestLeaf才是最終具有執(zhí)行能力的測(cè)試對(duì)象,就像目錄樹(shù)中的文件。

Test最重要的一個(gè)公共接口為:
virtual void run(TestResult *result) = 0;
其作用為執(zhí)行測(cè)試對(duì)象,將結(jié)果提交給result。
在實(shí)際應(yīng)用中,我們一般不會(huì)直接使用Test、TestComposite以及TestLeaf,除非我們要重新定制某些機(jī)制。
TestFixture:用于維護(hù)一組測(cè)試用例的上下文環(huán)境。
在實(shí)際應(yīng)用中,我們經(jīng)常會(huì)開(kāi)發(fā)一組測(cè)試用例來(lái)對(duì)某個(gè)類(lèi)的接口加以測(cè)試,而這些測(cè)試用例很可能具有相同的初始化和清理代碼。為此,CppUnit引入TestFixture來(lái)實(shí)現(xiàn)這一機(jī)制。
TestFixture具有以下兩個(gè)接口,分別用于處理測(cè)試環(huán)境的初始化與清理工作:
virtual void setUp();
virtual void tearDown();
TestCase:測(cè)試用例,從名字上就可以看出來(lái),它便是單元測(cè)試的執(zhí)行對(duì)象。
TestCase從Test和TestFixture多繼承而來(lái),通過(guò)把Test::run制定成模板函數(shù)(Template Method)而將兩個(gè)父類(lèi)的操作融合在一起,run函數(shù)的偽定義如下:
// 偽代碼
void TestCase::run(TestResult* result)
{
result->startTest(this); // 通知result測(cè)試開(kāi)始
if( result->protect(this, &TestCase::setUp) ) // 調(diào)用setUp,初始化環(huán)境
result->protect(this, &TestCase::runTest); // 執(zhí)行runTest,即真正的測(cè)試代碼
result->protect(this, &TestCase::tearDown); // 調(diào)用tearDown,清理環(huán)境
result->endTest(this); // 通知result測(cè)試結(jié)束
}
這里要提到的是函數(shù)runTest,它是TestCase定義的一個(gè)接口,原型如下:
virtual void runTest();
用戶(hù)需從TestCase派生出子類(lèi)并實(shí)現(xiàn)runTest以開(kāi)發(fā)自己所需的測(cè)試用例。
另外還要提到的就是TestResult的protect方法,其作用是對(duì)執(zhí)行函數(shù)(實(shí)際上是函數(shù)對(duì)象)的錯(cuò)誤信息(包括斷言和異常等)進(jìn)行捕獲,從而實(shí)現(xiàn)對(duì)測(cè)試結(jié)果的統(tǒng)計(jì)。
TestSuit:測(cè)試包,按照樹(shù)形結(jié)構(gòu)管理測(cè)試用例
TestSuit是TestComposite的一個(gè)實(shí)現(xiàn),它采用vector來(lái)管理子測(cè)試對(duì)象(Test),從而形成遞歸的樹(shù)形結(jié)構(gòu)。
TestFactory:測(cè)試工廠(chǎng)
這是一個(gè)輔助類(lèi),通過(guò)借助一系列宏定義讓測(cè)試用例的組織管理變得自動(dòng)化。參見(jiàn)后面的例子。
TestRunner:用于執(zhí)行測(cè)試用例
TestRunner將待執(zhí)行的測(cè)試對(duì)象管理起來(lái),然后供用戶(hù)調(diào)用。其接口為:
virtual void addTest( Test *test );
virtual void run( TestResult &controller, const std::string &testPath = "" );
這也是一個(gè)輔助類(lèi),需注意的是,通過(guò)addTest添加到TestRunner中的測(cè)試對(duì)象必須是通過(guò)new動(dòng)態(tài)創(chuàng)建的,用戶(hù)不能刪除這個(gè)對(duì)象,因?yàn)門(mén)estRunner將自行管理測(cè)試對(duì)象的生命期。
三、CppUnit 的使用
以上工作完成以后,就可以正式使用CppUnit了,由于單元測(cè)試是TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))的利器,一般人會(huì)先寫(xiě)測(cè)試代碼,然后再寫(xiě)產(chǎn)品代碼,不過(guò)筆者認(rèn)為先寫(xiě)產(chǎn)品代碼框架后再寫(xiě)測(cè)試代碼,然后通過(guò)慢慢補(bǔ)充產(chǎn)品代碼以使得能通過(guò)測(cè)試的方法會(huì)好些。不管先寫(xiě)誰(shuí)只要寫(xiě)得舒服安全就可以。本文決定先寫(xiě)測(cè)試代碼。
前面我們提到過(guò),CppUnit最小的測(cè)試單位是TestCase,多個(gè)相關(guān)TestCase組成一個(gè)TestSuite。要添加測(cè)試代碼最簡(jiǎn)單的方法就是利用CppUnit為我們提供的幾個(gè)宏來(lái)進(jìn)行(當(dāng)然還有其他的手工加入方法,但均是殊途同歸,大家可以查閱CppUnit頭文件中的演示代碼)。這幾個(gè)宏是:
CPPUNIT_TEST_SUITE() 開(kāi)始創(chuàng)建一個(gè)TestSuite;
CPPUNIT_TEST() 添加TestCase;
CPPUNIT_TEST_SUITE_END() 結(jié)束創(chuàng)建TestSuite;
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION() 添加一個(gè)TestSuite到一個(gè)指定的TestFactoryRegistry工廠(chǎng)。
感興趣的朋友可以在HelperMacros.h看看這幾個(gè)宏的聲明,本文在此不做詳述。
假定我們要實(shí)現(xiàn)一個(gè)類(lèi),類(lèi)名暫且取做CPlus,它的功能主要是實(shí)現(xiàn)兩個(gè)數(shù)相加(多簡(jiǎn)單的一個(gè)類(lèi)啊,這也要測(cè)試嗎?不要緊,我們只是了解怎樣加入測(cè)試代碼來(lái)測(cè)試它就行了,所以越簡(jiǎn)單越好)。 假定這個(gè)類(lèi)要實(shí)現(xiàn)的相加的方法是:
int Add(int nNum1, int nNum2);
OK,那我們先來(lái)寫(xiě)測(cè)試這個(gè)方法的代碼吧。TDD 可是先寫(xiě)測(cè)試代碼,后寫(xiě)產(chǎn)品代碼(CPlus)的哦!先寫(xiě)的測(cè)試代碼往往是不能運(yùn)行或編譯的,我們的目標(biāo)是在寫(xiě)好測(cè)試代碼后寫(xiě)產(chǎn)品代碼,使之編譯通過(guò),然后再進(jìn)行重構(gòu)。這就是Kent Beck說(shuō)的“red/green/refactor”。所以,上面的類(lèi)名和方法應(yīng)該還只是在你的心里,還只是你的idea而已。
根據(jù)測(cè)試驅(qū)動(dòng)的原理,我們需要先建立一個(gè)單元測(cè)試框架。我們?cè)赩C中為測(cè)試代碼建立一個(gè)project。通常,測(cè)試代碼和被測(cè)試對(duì)象(產(chǎn)品代碼)是處于不同的project中的。這樣就不會(huì)讓你的產(chǎn)品代碼被測(cè)試代碼所“污染 ”。
由于在CppUnit下, 可以選擇控制臺(tái)方式和UI方式兩種表現(xiàn)方案,我們選擇UI方式。在本例中,我們將建立一個(gè)基于GUI 方式的測(cè)試環(huán)境。因此我們建立一個(gè)基于對(duì)話(huà)框的Project。假設(shè)名為UnitTest。
建立了UnitTest project之后,我們首先配置這個(gè)工程。
首先在project中打開(kāi)RTTI開(kāi)關(guān),具體位置在菜單Project/Settings/C++/C++ Language。如下圖所示設(shè)置:

由于CppUnit所用的動(dòng)態(tài)運(yùn)行期庫(kù)均為多線(xiàn)程動(dòng)態(tài)庫(kù),因此你的單元測(cè)試程序也得使用相應(yīng)設(shè)置,否則會(huì)發(fā)生沖突。于是我們?cè)赑roject/Settings/C++/Code Generation中進(jìn)行如下設(shè)置:
在Use run-time library一欄中,針對(duì)debug和release分別設(shè)置為‘Debug Multithreaded DLL’和‘Multithreaded DLL’。如下圖所示:


最后別忘了在project中l(wèi)ink正確的lib。包括本例采用的cppunit.lib和cppunitd.lib靜態(tài)庫(kù)以及用于GUI方式的TestRunner.dll對(duì)應(yīng)的lib。具體位置在Project/Settings/Link/General
在‘Object/library modules’中,針對(duì)debug和release分別加入cppunitd.lib testrunnerd.lib和cppunit.lib TestRunner.lib。如下圖所示:


最后,由于TestRunner.dll為我們提供了基于GUI的測(cè)試環(huán)境。為了讓我們的測(cè)試程序能正確的調(diào)用它,TestRunner.dll必須位于你的測(cè)試程序的路徑下。所以把/lib目錄下的testrunnerd.dll和TestRunner.dll文件分別拷貝到UnitTest priject的程序debug和release版本輸出目錄中。如下圖所示:

(這是release版本)只要放在一起就可以了。
配置工作終于完成,下面開(kāi)始寫(xiě)測(cè)試框架。
在CppUnit中, 是以TestCase為最小的測(cè)試單位, 若干TestCase組成一個(gè)TestSuite。所以我們要先建立一個(gè)TestCase。
在UnitTest project中新建一個(gè)類(lèi), 命名為CPlusTestCase, 讓其從CppUnit::TestCase派生。為其新增一個(gè)方法,假設(shè)為 void testAdd(); 我們將在這個(gè)函數(shù)中寫(xiě)入我們的一些測(cè)試代碼(還記得我們要測(cè)試的構(gòu)想中的CPlus::Add(…)嗎)。代碼如下:切記要包含頭文件
#include <cppunit/TestCase.h>
class CPlusTestCase : public CppUnit::TestCase
{
public:
CPlusTestCase ();
virtual ~ CPlusTestCase ();
void testAdd();
};
接下來(lái), 我們要對(duì)我們的CPlusTestCase進(jìn)行聲明。聲明用到了三個(gè)宏.
CPPUNIT_TEST_SUITE();
CPPUNIT_TEST();
CPPUNIT_TEST_SUITE_END();
第一個(gè)宏聲明一個(gè)測(cè)試包(suite),第二個(gè)宏聲明(添加)一個(gè)測(cè)試用例. 現(xiàn)在我們的CPlusTestCase類(lèi)看上去象這樣:切記要包含頭文件,否則無(wú)法識(shí)別這些宏。
#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();
};
通過(guò)這幾個(gè)宏,我們就把CPlusTestCase和testAdd注冊(cè)到了測(cè)試列表當(dāng)中。
接下來(lái),我們要注冊(cè)我們的測(cè)試suite. 使用CPPUNIT_TEST_SUITE_NAMED_REGISTRATION()來(lái)注冊(cè)一個(gè)測(cè)試suite. 這個(gè)宏的第二個(gè)參數(shù)是我們注冊(cè)的suite的名字. 在這里我們可以用字符串來(lái)代替, 但我們用一個(gè)靜態(tài)函數(shù)來(lái)返回這個(gè)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注冊(cè)我們的suite.
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CPlusTestCase, CPlusTestCase::GetSuiteName());
它將CPlusTestCase這個(gè)TestSuite注冊(cè)到一個(gè)指定的TestFactory工廠(chǎng)中。
接下來(lái)我們寫(xiě)一個(gè)注冊(cè)函數(shù)static CppUnit::Test* GetSuite(), 使其在運(yùn)行期生成一個(gè)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>
最后, 我們?yōu)閱卧獪y(cè)試建立一個(gè)UI測(cè)試界面.
由于我們希望這個(gè)Project運(yùn)行后顯示的是GUI界面,所以我們需要在App的 InitInstance ()中屏蔽掉原有的對(duì)話(huà)框,代之以CppUnit的GUI。
我們?cè)贑UnitTestApp::InitInstance()函數(shù)中,將原先顯示主對(duì)話(huà)框的代碼以下面的代碼取代:
CppUnit::MfcUi::TestRunner runner;
runner.addTest(CPlusTestCase::GetSuite());//添加測(cè)試
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 "
到此為止, 我們已經(jīng)建立好一個(gè)簡(jiǎn)單的單元測(cè)試框架。測(cè)試框架雖然寫(xiě)好了,但是測(cè)試代碼仍然為空,產(chǎn)品代碼也還沒(méi)有寫(xiě)。下面我們來(lái)寫(xiě)測(cè)試代碼:
如前所述,在測(cè)試類(lèi)中,我們添加了一個(gè)測(cè)試方法:
void testAdd();
它測(cè)試的對(duì)象是前面提到的CPlus類(lèi)的方法:int Add(int nNum1, int nNum2);(產(chǎn)品代碼)我們來(lái)看看testAdd()的實(shí)現(xiàn):記得在PlusTestCase.h中包含頭文件
#include <cppunit/TestAssert.h>
// PlusTestCase.cpp
void CPlusTestCase::testAdd()
{
CPlus plus;
int nResult = plus.Add(10, 20); //執(zhí)行Add操作
CPPUNIT_ASSERT_EQUAL(30, nResult); //檢查結(jié)果是否等于30
}
CPPUNIT_ASSERT_EQUAL是一個(gè)判斷結(jié)果的宏。CppUnit中類(lèi)似的其它宏請(qǐng)查閱TestAssert.h,本文在此不做詳述 。
另外,我們還可以覆寫(xiě)基類(lèi)的 setUp()、tearDown()兩個(gè)函數(shù)。這兩個(gè)函數(shù)實(shí)際上是一個(gè)模板方法,在測(cè)試運(yùn)行之前會(huì)調(diào)用setUp()以進(jìn)行一些初始化的工作,測(cè)試結(jié)束之后又會(huì)調(diào)用tearDown()來(lái)做一些“善后工作” ,比如資源的回收等等。當(dāng)然,你也可以不覆寫(xiě)這兩個(gè)函數(shù),因?yàn)樗鼈冊(cè)诨?lèi)里定義成了空方法,而不是純虛函數(shù)。
編寫(xiě)完上面的測(cè)試代碼后,進(jìn)行編譯。編譯肯定通不過(guò),編譯器會(huì)告訴我們CPlus類(lèi)沒(méi)有聲明,因?yàn)槲覀冞€沒(méi)有實(shí)現(xiàn)CPlus類(lèi)呢!現(xiàn)在的工作就是馬上實(shí)現(xiàn)CPlus類(lèi),讓編譯通過(guò)。現(xiàn)在你應(yīng)該嗅到一點(diǎn)“測(cè)試驅(qū)動(dòng)”(Test Driven Develop)的味道了吧?
在VC中建立一個(gè)MFC Extension Dll的Project,在這個(gè)Project 中加入類(lèi)CPlus,它的聲明如下:
// Plus.h
class AFX_EXT_CLASS CPlus
{
public:
CPlus();
virtual ~CPlus();
public:
int Add(int nNum1, int nNum2);
};
Add在Plus.cpp中實(shí)現(xiàn)如下
int CPlus::Add(int nNum1, int nNum2)
{
return nNum1 + nNum2;//這里可以寫(xiě)一些錯(cuò)誤的語(yǔ)句,用來(lái)看看錯(cuò)誤的結(jié)果
}
非常簡(jiǎn)單,不是嗎?現(xiàn)在讓前面那個(gè)包含測(cè)試代碼的Project dependent這個(gè)Project,并且include 相關(guān)頭文件 ,Rebuild All,你會(huì)發(fā)現(xiàn)編譯已通過(guò)。你體會(huì)到了測(cè)試代碼驅(qū)動(dòng)產(chǎn)品代碼了嗎?當(dāng)然我們的這個(gè)例子還很簡(jiǎn)單 ,沒(méi)有重構(gòu)這一步驟。
運(yùn)行我們的測(cè)試程序,單擊Browse,你就會(huì)看到如下圖所示的界面:

選擇CPlusTestCase::testAdd后,單擊Run,你就會(huì)看到如下圖所示的界面:

這下你應(yīng)該對(duì)前面我們說(shuō)的TestSuite的名字理解更深了吧。CPlus是一個(gè)測(cè)試包TestSuite,它的下面包含一個(gè)測(cè)試用例,這個(gè)測(cè)試用例下面又包含一個(gè)測(cè)試方法。
如果我修改CPlus::Add的代碼如下:
int CPlus::Add(int nNum1, int nNum2)
{
// return nNum1 + nNum2;
return 2;
}
重新編譯通過(guò),運(yùn)行程序就會(huì)發(fā)現(xiàn):

GUI顯示有一個(gè)單元測(cè)試不通過(guò),并顯示出錯(cuò)的地方和原因,這樣就很好的控制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
{
//通過(guò)這幾個(gè)宏,我們就把CPlusTestCase和testAdd注冊(cè)到了測(cè)試列表當(dāng)中.
CPPUNIT_TEST_SUITE(CPlusTestCase); //聲明一個(gè)測(cè)試包
CPPUNIT_TEST(testAdd); //聲明一個(gè)測(cè)試用例
CPPUNIT_TEST_SUITE_END();
public:
CPlusTestCase();
virtual ~CPlusTestCase();
void testAdd(); //測(cè)試方法
static std::string GetSuiteName();
//寫(xiě)一個(gè)注冊(cè)函數(shù), 使其在運(yùn)行期生成一個(gè)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
//注冊(cè)一個(gè)測(cè)試suite到一個(gè)指定的TestFactory工廠(chǎng)中
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); //執(zhí)行Add操作
CPPUNIT_ASSERT_EQUAL(30, nResult); //檢查結(jié)果是否等于30
}
std::string CPlusTestCase::GetSuiteName()
{
return "CPlus";
}
/*
* 注意:CPlusTestCase::GetSuite()返回一個(gè)指向CppUnit::Test的指針.
* 這個(gè)指針就是整個(gè)測(cè)試的起點(diǎn)。
* CppUnit::TestFactoryRegistry::getRegistry()根據(jù)TestSuite的名字返回TestFactoryRegistry工
* 然后調(diào)用工廠(chǎng)里的makeTest()對(duì)TestSuite進(jìn)行組裝,將建立起一個(gè)樹(shù)狀的測(cè)試結(jié)構(gòu)。
*/
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()); //添加測(cè)試 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測(cè)試框架入門(mén)》
- Freefalcon《CppUnit快速入門(mén)》
- 《使用CppUnit進(jìn)行單元測(cè)試》
[轉(zhuǎn)載]通往WinDbg的捷徑(二) 原文:http://www.debuginfo.com/articles/easywindbg2.html
譯者:arhat
時(shí)間:2006年4月14日
關(guān)鍵詞:CDB WinDbg
保存 dumps 在我們調(diào)試不容易重現(xiàn)的問(wèn)題時(shí),可能想把應(yīng)用程序狀態(tài)的快照(內(nèi)存內(nèi)容,打開(kāi)名柄的列表,等等)保存起來(lái),以便日后分析。例如,當(dāng)我懷疑當(dāng)前的狀態(tài)可能包含我試圖解決的問(wèn)題的關(guān)鍵點(diǎn),而想繼續(xù)運(yùn)行應(yīng)用程序來(lái)查看情形怎樣發(fā)展時(shí),它就很有用了。有時(shí)候,我會(huì)做一系列的快照,一個(gè)接一個(gè),以便稍后我能比較它們,查看在應(yīng)用程序運(yùn)行時(shí)有些數(shù)據(jù)結(jié)構(gòu)怎樣變化。當(dāng)我最終能重現(xiàn)這個(gè)問(wèn)題時(shí),我總是創(chuàng)建一個(gè)快照來(lái)確保我沒(méi)有因?yàn)槟承╁e(cuò)誤(錯(cuò)誤關(guān)閉了調(diào)試會(huì)話(huà))而丟失有價(jià)值的信息。或許,大家不難猜到當(dāng)我說(shuō)“快照”時(shí),我真正的意思是“minidump”,因?yàn)閙inidump為隨時(shí)保存應(yīng)用程序的狀態(tài)提供了便利。
下面是創(chuàng)建minidump的命令行示例:
cdb -pv -pn myapp.exe -c ".dump /m c:\myapp.dmp;q"
讓我們仔細(xì)看一下.dump命令。在上面的例子里,我們只用到這條命令的一個(gè)選項(xiàng)(/m),后面跟著minidump的文件名。用/m來(lái)指定minidump里應(yīng)當(dāng)包括哪種信息。最重要的(依我之見(jiàn))/m選項(xiàng)的變量列在下表中:
---------------------------------------------------------------------------------------------------------------------------
選項(xiàng) 描述 例子
---------------------------------------------------------------------------------------------------------------------------
/m 默認(rèn)就是這個(gè)選項(xiàng)。它創(chuàng)建標(biāo)準(zhǔn)的minidump,等同于MiniDumpNormal minidump類(lèi)型。由此生成的minidump
一般很小,因此,如果你想通過(guò)慢速的網(wǎng)絡(luò)傳輸minidump,那么這個(gè)選項(xiàng)非常有用。但不幸地是,小體積的
minidump也意味著在大多數(shù)情況下,它包含的信息不足以進(jìn)行完整的分析(你可以在
這篇文章里找到更多有
關(guān)minidump內(nèi)容的信息)。 dump /m c:\myapp.dmp
---------------------------------------------------------------------------------------------------------------------------
/ma 帶所有可選項(xiàng)的Minidump(完整的內(nèi)存內(nèi)容,名柄,已卸載的模塊,等等),由此生成的minidump將非常
大。如果可以隨意使用磁盤(pán)空間,這個(gè)選項(xiàng)將非常適合本地調(diào)試。 .dump /ma c:\myapp.dmp
---------------------------------------------------------------------------------------------------------------------------
/mFhutwd 這個(gè)選項(xiàng)將生成帶數(shù)據(jù)段,非共享讀/寫(xiě)內(nèi)存頁(yè)和其它有用信息的minidump。如果你想盡可能的收集信息,
但仍想使minidump保持小體積(并壓縮),就可以用這個(gè)選項(xiàng)。 .dump /mFhutwd c:\myapp.dmp
---------------------------------------------------------------------------------------------------------------------------
下面的命令生成包含所有信息的minidump:
cdb -pv -pn myapp.exe -c ".dump /ma c:\myapp.dmp;q"
如果我們想生成一個(gè)新minidump,并覆蓋已有的,該怎么辦呢?在默認(rèn)情況下,.dump命令不允許這樣做??它會(huì)抱怨文件已經(jīng)存在。為了改變默認(rèn)行為,覆蓋已存在的.dump文件,我們可以用/o選項(xiàng):
cdb -pv -pn myapp.exe -c ".dump /ma /o c:\myapp.dmp;q"
如果我們想生成一系列的minidump,一個(gè)接一個(gè),那么它能很方便的為minidump命名,并使文件名反映生成minidump時(shí)的時(shí)間嗎。嗯,如果我們指定了/u選項(xiàng),.dump命令就可以自動(dòng)為我們這樣做,這真是一個(gè)好消息,不是嗎?例如,下面的命令可以生成名為myapp_02CC_
2006-01-28_04-11-18-171_0158.dmp的minidump(0158是進(jìn)程ID):
cdb -pv -pn myapp.exe -c ".dump /m /u c:\myapp.dmp;q"
.dump命令也支持其它有趣的選項(xiàng)(你可以在文檔里發(fā)現(xiàn)它們)。
如果你想生成運(yùn)行在Visual Studio調(diào)試器下的進(jìn)程的minidump,我建議在生成dump前,先在Visual Studio里臨時(shí)禁用所有的斷點(diǎn)。如果沒(méi)有禁用斷點(diǎn),生成的minidump將包含Visual Studio調(diào)試器插入目標(biāo)進(jìn)程代碼里的斷點(diǎn)指令(int 3)。
分析故障轉(zhuǎn)儲(chǔ)CDB也可以用于自動(dòng)分析故障轉(zhuǎn)儲(chǔ)。當(dāng)我們分析故障轉(zhuǎn)儲(chǔ)時(shí),通常會(huì)執(zhí)行同樣的操作,所以可以把這些操作自動(dòng)化。什么樣的操作呢?這要看故障轉(zhuǎn)儲(chǔ)的類(lèi)型。我把所有的故障轉(zhuǎn)儲(chǔ)分成兩大類(lèi):
• 帶異常信息的故障轉(zhuǎn)儲(chǔ)
• 不帶異常信息的故障轉(zhuǎn)儲(chǔ)
當(dāng)應(yīng)用程序引發(fā)未經(jīng)處理的異常并調(diào)用just-in-time調(diào)試器(Dr. Watson,
NTSD , 或其它的調(diào)試器),或者用為
未經(jīng)處理的異常定制的過(guò)濾器 生成minidump時(shí),通常會(huì)生成帶異常信息的故障轉(zhuǎn)儲(chǔ)。通過(guò)寫(xiě)入故障轉(zhuǎn)儲(chǔ)里的異常信息,我們可以確定異常的類(lèi)型和發(fā)生時(shí)它在代碼里的位置。當(dāng)我們想為以后的分析生成進(jìn)程的快照時(shí)(例如,這方面的描述參見(jiàn)本文的前一部分“保存dumps”),通常手動(dòng)生成不帶異常信息的故障轉(zhuǎn)儲(chǔ)。
當(dāng)我們調(diào)試帶異常信息的故障轉(zhuǎn)儲(chǔ)時(shí),通常想知道下面這些信息:
• 異常在代碼中出現(xiàn)的位置(地址,源文件和行號(hào))
• 異常發(fā)生時(shí)的調(diào)用棧
• 調(diào)用棧上一些或所有函數(shù)的參數(shù)值和局部變量
WinDbg和CDB為調(diào)試故障轉(zhuǎn)儲(chǔ)提供了非常有用的命令??!analyze。這條命令分析故障轉(zhuǎn)儲(chǔ)里的異常信息,確定異常發(fā)生的位置,調(diào)用棧,并顯示詳細(xì)的報(bào)告。下面是這條命令的示例:
cdb -z c:\myapp.dmp -logo out.txt -lines -c "!analyze -v;q"
(-v選項(xiàng)要求!analyze輸出詳細(xì)的內(nèi)容)
CrashDemo.cpp 例子演示了怎樣用定制的過(guò)濾器捕獲未經(jīng)處理的異常并生成minidumps。如果你編譯并運(yùn)行它,然后用上述的CDB命令分析生成的minidump,你將可以得到和下面類(lèi)似的輸出內(nèi)容:
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
---------
注意用粗體表示的內(nèi)容。第一處報(bào)告了異常的地址和類(lèi)型。第二外報(bào)告調(diào)用棧。第三處為我們提供了怎樣訪(fǎng)問(wèn)保存在故障轉(zhuǎn)儲(chǔ)里的異常信息的額外信息。
現(xiàn)在,我們知道異常發(fā)生的位置,甚至可以查看調(diào)用棧。那么,是得到函數(shù)的參數(shù)值及局部變量的時(shí)候了。在開(kāi)始之前,讓我們注意!analyze報(bào)告中的第三處信息。這里再重復(fù)一下第三處所包含的內(nèi)容:
STACK_COMMAND: .ecxr ; kb
對(duì)'kb'命令我們已經(jīng)不陌生了(它顯示調(diào)用棧)。但.ecxr是什么?這條命令要求調(diào)試器把當(dāng)前的內(nèi)容切換到保存在故障轉(zhuǎn)儲(chǔ)里的異常信息。我們執(zhí)行這條命令后,將能訪(fǎng)問(wèn)異常拋出時(shí)調(diào)用棧和局部變量的值。
在我們要求調(diào)試使用異常的上下文后,我們可以用'dv'命令顯示函數(shù)的參數(shù)值以及局部變量。因?yàn)槲覀兺ǔO氩榭凑{(diào)用棧上每一個(gè)函數(shù)的信息,因此,我們可以用'!for_each_frame dv /t'命令(/t選項(xiàng)要求'dv'顯示有用的類(lèi)型信息)。(當(dāng)然,我們必須記住,使用優(yōu)化編譯時(shí),在函數(shù)的整個(gè)生存期中,局部變量有可能會(huì)被取消,重注冊(cè)或被重用來(lái)保存其它的數(shù)據(jù),因此,可能會(huì)導(dǎo)致'dv'命令輸出錯(cuò)誤的值)。
下面是分析帶異常信息的故障轉(zhuǎn)儲(chǔ)的命令行示例:
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沒(méi)有包括目標(biāo)進(jìn)程內(nèi)存的完整內(nèi)容,那么只有當(dāng)調(diào)試器能正確發(fā)現(xiàn)被目標(biāo)進(jìn)程加載的、相同版本的可執(zhí)行模塊時(shí),才能分析dump。在某些情形下,你必須幫助調(diào)試器定位這些模塊??通過(guò)指定模塊搜索路徑。關(guān)于模塊搜索路徑的詳細(xì)信息和相關(guān)內(nèi)容可以在
這篇文章 中找到。
現(xiàn)在,我們來(lái)處理不帶異常信息的故障轉(zhuǎn)儲(chǔ)。當(dāng)我們分析這樣的dump時(shí),通常想知道所有線(xiàn)程的調(diào)用棧。下面是怎樣得到這些信息:
cdb -z c:\myapp.dmp -logo out.txt -lines -c "~*kb;q"
如果我們不知道故障轉(zhuǎn)儲(chǔ)是否包含異常信息,該怎么做呢?對(duì)于minidumps來(lái)說(shuō),我們可以用
MiniDumpView 打印dump的內(nèi)容,查看它里面是否包含異常信息。對(duì)于過(guò)時(shí)的'full user dumps',或許唯一的選擇是,照現(xiàn)在的樣子啟動(dòng)包含異常信息的dump,并查看!analyze是否報(bào)告了有意義的內(nèi)容。
有一個(gè)有趣的特例??因?yàn)槲唇?jīng)處理的異常生成故障轉(zhuǎn)儲(chǔ),但因?yàn)槟承┰驔](méi)有包含異常信息是有可能的。在這種情形下,在下面過(guò)程的幫助下,仍可能找出異常發(fā)生的位置:
1. 打印所有線(xiàn)程的調(diào)用棧(用前面提過(guò)的CDB命令)。
2. 找出包含kernel32!UnhandledExceptionFilter函數(shù)調(diào)用棧的線(xiàn)程。
3. 使用
UnhandledExceptionFilter 函數(shù)的第一個(gè)參數(shù)(包含一個(gè)指向EXCEPTION_POINTERS 結(jié)構(gòu)的指針)的實(shí)際值。
下面是EXCEPTION_POINTERS 結(jié)構(gòu)的聲明:
typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
如果我們知道這個(gè)結(jié)構(gòu)的地址,就能得到指向異常上下文的指針(保存在ContextRecord字段里),把它傳遞給.cxr命令,從而把調(diào)試器上下文切換到異常發(fā)生的位置。在.cxr命令執(zhí)行后,我們可以用'kb'命令得到異常發(fā)生時(shí)的調(diào)用棧。下面是一個(gè)例子:
1. 打印所有線(xiàn)程的調(diào)用棧。
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. 改變調(diào)試器的上下文,得到異常的調(diào)用棧。
cdb -z c:\myapp.dmp -logo out.txt -lines -c ".cxr dwo(0x006af9e0+4);kb;q"
('dwo'操作符返回保存在指定地址里的double word,并把它傳遞給.cxr命令)
本篇文章后面提供的批處理文件(實(shí)際上是DumpStackCtx.bat)將簡(jiǎn)化這個(gè)任務(wù)。
還有另外的方法可以解決這個(gè)問(wèn)題??你可以在
這里 找到更多的信息。
分析虛擬內(nèi)存當(dāng)我們想審查被調(diào)試進(jìn)程的虛擬內(nèi)存布局時(shí),CDB可以協(xié)助Visual Studio調(diào)試器。下面的命令顯示進(jìn)程完整的虛擬內(nèi)存映射:
cdb -pv -pn myapp.exe -logo out.txt -c "!vadump -v;q"
(!vadump命令負(fù)責(zé)打印虛擬內(nèi)存映射,照常,用-v選項(xiàng)要求它顯示詳細(xì)的內(nèi)容)
下面是!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為審查虛擬內(nèi)存布局提供了一條更好的命令??!address。這條命令可以完成下面的任務(wù):
• 顯示進(jìn)程的虛擬內(nèi)存映射(依我之見(jiàn),比!vadump的輸出內(nèi)容更易閱讀)
• 顯示有用的、虛擬內(nèi)存使用的統(tǒng)計(jì)數(shù)據(jù)
• 確定指定的地址屬于哪種虛擬內(nèi)存區(qū)域(例如,它是屬于棧,堆,還是可執(zhí)行映象?)
下面是怎樣用!address報(bào)告虛擬內(nèi)存映射的示例:
cdb -pv -pn myapp.exe -logo out.txt -c "!address;q"
下面顯示被線(xiàn)程的棧占用的內(nèi)存區(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非常智能,可以報(bào)告屬于棧的線(xiàn)程的線(xiàn)程ID。
在!address報(bào)告虛擬內(nèi)存區(qū)域之后,它也能報(bào)告有趣的、虛擬內(nèi)存使用的統(tǒng)計(jì)數(shù)據(jù):
-------------------- 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
當(dāng)我們正在調(diào)試內(nèi)存泄露,以及想確定內(nèi)存泄露的類(lèi)型(堆,棧,原始的虛擬內(nèi)存,等等)時(shí),這些統(tǒng)計(jì)數(shù)據(jù)非常有用。通過(guò)最后一行,我們可以確定虛擬內(nèi)存中最大的空閑區(qū)域的大小,這在我們必須設(shè)計(jì)請(qǐng)求大量?jī)?nèi)存的應(yīng)用程序時(shí)非常有幫助。
如果你只想查看統(tǒng)計(jì)數(shù)據(jù),而不想看虛擬內(nèi)存映射,可以用-summary參數(shù):
cdb -pv -pn myapp.exe -logo out.txt -c "!address -summary;q"
如果我們想確定給定地址的虛擬內(nèi)存屬于哪種類(lèi)型,可以把這個(gè)地址作為參數(shù)傳遞給!address命令。下面是一個(gè)例子:
0:000> !address 0x000a2480;q
000a0000 : 000a0000 - 000d7000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage
RegionUsageHeap Handle 000a0000
搜索符號(hào)有時(shí)候,我們可能需要確定符號(hào)(函數(shù)或變量)的地址。如果我們知道準(zhǔn)確的符號(hào)名,可以把它輸入Visual Studio調(diào)試器的反匯編窗口,從中找出它對(duì)應(yīng)的地址。但是,假設(shè)我們忘了準(zhǔn)確的符號(hào)名呢?或者想找出名字上有相同規(guī)律的、一組符號(hào)的地址(例如,類(lèi)的所有成員函數(shù))?CDB很容易解決這個(gè)問(wèn)題??它提供'x'命令,可以列出匹配指定掩碼的所有符號(hào)名:
x Module!Symbol
下面的命令設(shè)法定位位于kernel32.dll 中的UnhandledExceptionFilter函數(shù)的地址:
cdb -pv -pn notepad.exe -logo out.txt -c "x kernel32!UnhandledExceptionFilter;q"
下面是輸出內(nèi)容:
0:000> x kernel32!UnhandledExceptionFilter;q
7c862b8a kernel32!UnhandledExceptionFilter = <no type information>
'x'命令可以接受多個(gè)通配符,提供一些有用的選項(xiàng),可用于排序輸出內(nèi)容以及輸出符號(hào)的額外信息??你可以在WinDbg文檔里找到更多的信息。例如,下面的命令可以把我們?cè)趹?yīng)用程序的主可執(zhí)行模塊中定義的CmainFrame類(lèi)所有的成員函數(shù)和統(tǒng)計(jì)數(shù)據(jù)列出來(lái):
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也可以反著做??通過(guò)地址找符號(hào),使用'ln'命令:
ln Address
下面是它的用法:
cdb -pv -pn notepad.exe -logo out.txt -c "ln 0x77d491c8;q"
下面是它的輸出內(nèi)容:
0:000> ln 0x77d491c8;q
(77d491c6) USER32!GetMessageW+0x2 | (77d49216) USER32!CharUpperBuffW
注意,我們不必指定符號(hào)的起始地址(在這個(gè)例子里,是函數(shù)),而可以用符號(hào)占用的地址范圍內(nèi)的任何地址。'ln'將找出符號(hào),報(bào)告它的地址,另外還報(bào)告跟在指定內(nèi)容后面的地址和符號(hào)名。
顯示數(shù)據(jù)結(jié)構(gòu)如果我們想研究數(shù)據(jù)結(jié)構(gòu)的內(nèi)容,通常會(huì)用Visual Studio的Watch,QuickWatch或其它類(lèi)似的窗口。這些窗口允許我們查看結(jié)構(gòu)成員變量的類(lèi)型和值。但是假設(shè)我們也需要知道結(jié)構(gòu)的精確布局,包括它的成員的偏移量?Visual Studio不提供易用的解決方法,但幸運(yùn)的是,CDB可以。在'dt'命令的幫助下,我們可以顯示數(shù)據(jù)結(jié)構(gòu)或類(lèi)的精確布局。
如果我們只想了解數(shù)據(jù)類(lèi)型的布局,可以用下面這條命令:
dt -b TypeName
(-b選項(xiàng)啟用遞歸顯示,顯示結(jié)構(gòu)或類(lèi)的成員類(lèi)型的嵌入式數(shù)據(jù)結(jié)構(gòu))。
下面是命令的示例:
cdb -pv -pn myapp.exe -logo out.txt -c "dt -b CSymbolInfoPackage;q"
下面是輸出內(nèi)容(在運(yùn)行
SymFromAddr 應(yīng)用程序時(shí)得到的):
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 ''
... 省略部分輸出內(nèi)容
[1990] 0 ''
[1991] 0 ''
[1992] 0 ''
[1993] 0 ''
[1994] 0 ''
[1995] 0 ''
[1996] 0 ''
[1997] -52 ''
[1998] -52 ''
[1999] -52 ''
[2000] -52 ''
注意,'dt'也顯示結(jié)構(gòu)成員變量的值。
批處理文件我們已經(jīng)知道怎樣用CDB解決一些有趣的調(diào)試問(wèn)題了。現(xiàn)在可以用它解決更多的問(wèn)題了??用易用的批處理文件代替難記的CDB命令行。考慮我們?cè)诒疚拈_(kāi)始部分使用的命令行示例:
cdb -pv -pn myapp.exe -logo out.txt -c "lm;q"
這條命令中的大部分是固定的,用不著改變。唯一可變的部分是目標(biāo)信息(-pn myapp.exe),我們可能會(huì)用另外的可執(zhí)行文件名,或另外的附著方式(例如,通過(guò)進(jìn)程ID)替換它。
下面介紹了怎樣用批處理文件代替這條命令:
; lm.bat
cdb -pv %1 %2 -logo out.txt -c "lm;q"
如果我們運(yùn)行這個(gè)批處理文件,通過(guò)進(jìn)程來(lái)得到已加載模塊的列表,可以用下面的方法:
通過(guò)可執(zhí)行文件名附上進(jìn)程:
lm -pn myapp.exe
通過(guò)進(jìn)程ID附上進(jìn)程:
lm -p 1234
通過(guò)服務(wù)名附上進(jìn)程:
lm -psn MyService
打開(kāi)故障轉(zhuǎn)儲(chǔ)文件:
lm -z c:\myapp.dmp
這條命令與具體的目標(biāo)無(wú)關(guān),只做同樣的事情??打印已加載模塊的列表。
如果我們想為CDB命令指定另外的參數(shù),我們可以用同樣的方法。考慮下面用于顯示數(shù)據(jù)結(jié)構(gòu)布局的命令:
cdb -pv -pn myapp.exe -logo out.txt -c "dt /b MyStruct;q"
當(dāng)然,我們可能想使用任何數(shù)據(jù)類(lèi)型運(yùn)行這條命令,而不僅僅是MyStruct。下面是怎樣做的示例:
; dt.bat
cdb -pv %1 %2 -logo out.txt -c "dt /b %3;q"
現(xiàn)在,我們可以象下面這樣運(yùn)行這條命令:
dt -pn myapp.exe CTestClass
或者象這樣:
dt -p 1234 SYMBOL_INFO
或者,也可以象這樣:
dt -z c:\myapp.dmp EXCEPTION_POINTERS
我們可以用同樣的方法處理其它的命令。你可以從
這里 找到本文曾討論過(guò)的批處理文件。在以后,我準(zhǔn)備用另外有用的命令擴(kuò)充它。