使用Microsoft Visual Studio和Rational Purify進行運行時調試(一)

本文分爲兩部分,在此我們先來學習一些基本的使用Visual Studio調試Win32應用程序的基礎知識。
作者: Goran Begic, Technical Marketing Engineer, Development Solutions, IBM Rational
翻譯:wyingquan # hotmail.com                      2006-02-09
Visual Studio C++調試器界面
1: Visual Studio調試器窗口
每當提及我們爲提高軟件質量做了多少工作時,開發人員總會拍胸脯保證沒有問題。然而,你要永遠記住一個不爭的事實:程序中將始終存在bug。畢竟,程序是人設計的,那就當然不會沒有bug的。
因此,調試——修復缺陷這一最耗時且最昂貴的過程——在大型軟件開發過程中始終佔據一席之地。廣而言之,調試也包括各種各樣的編程技術,它能使開發人員能夠預見程序中潛在的問題。由於開發過程的高度複雜,調試工作就變得更加繁瑣。實際上,爲了全面掌控整個調試活動,你就必須關注整個開發過程。
正如一些“調試人員”將要告訴你的,定位真正的引起產生缺陷的原因是所有工作中最困難的;修復代碼的缺陷是調試過程中最簡單的一步而已。本文的第一部分將向您介紹Microsoft Visual Studio程序開發環境並將討論使用Microsoft Visual Studio編譯器進行最基本的程序調試方法。
形形色色的bug
        準確的來說,什麼是bug呢?如果你的應用程序安裝在任何機器上都會崩潰,你就會知道程序肯定有bug。但是以下這種情況呢?通過自己的測試發現程序運行良好,就自豪地發佈了它。但不久後,一些重要的客戶反饋了一些令人爲難的“問題”,這些問題僅在一些機器上的某種配置情況下出現。這種問題我們當然也要把它稱爲bug。
實際上,我們把許多不同類型的問題都稱爲bug。數據被破壞是其中級別最高的,但是應用程序由於設計缺陷或者甚至界面設計混亂也會導致用戶操作上的一些不方便(即不符合用戶的習慣或不符合約定俗成)。例如:你是否在Microsoft Outlook中使用快捷鍵Ctrl+F,彈出的不是預期的查找窗口,而是轉發窗口(在幾乎所有的應用程序中,Ctrl+F快捷鍵都會調出查找窗口)。
調試方法和技術
        時下流行的一個口號叫“防禦性的編程”。這是一個由許多技術和策略組成的用於編寫代碼的方法,有助於早期發現錯誤。例如,編寫巧妙的代碼,使用所有開發人員都理解和認可的符號(變量名稱)編寫代碼,這樣有助於減少bug。此外,防禦性的編程技術還包含了由程序語言提供的大量非常有用的宏定義和函數,這些都有助於你在程序執行期間檢查重要的事件。
        即使在度量軟件質量期間你也應該進行程序調試。在有了過硬的質量保證的同時,也讓項目管理人員能夠爲系統後期開發做出決策。
        爲了避免由於代碼中的bug引起發佈時間的延期,你應該在整個開發週期中儘可能早地開始調試。例如:最理想的是在項目計劃時就開始——在爲第一個原型整理需求時。在早期的原型中加入過多的特徵可能會引起缺陷的增加,並且會導致在修復這些特徵上花費大量的人力和時間。花費在調試一個有過多特徵的軟件上的時間要遠遠多於花在一個簡單而又強壯的程序上的時間。
你也應當使所有項目組之間保持一致的編程環境,一致的文檔和代碼——使用版本控制軟件——並且對修改後的程序進行“冒煙測試”。如果你正好文檔化了源碼的所有改變,那麼就會發現一般新的bug都出現在最近修改過的代碼中,關注於這些變化會大大地減少花費在問題修復上的時間。
在我前面提及到過,不管多麼仔細或者維護你的代碼,甚至你的程序看上去運行正確,但實際情況是:程序中還會包含bug。軟件開發工具一般都會把這些考慮進去,並且包含了有助於儘量消除軟件缺陷。
那麼開發人員需要的基本工具有哪些呢?至少需要四個:
·    一個編輯器
·    一個編譯器
·    一個調試器
·    一個自動化的運行時調試器
        如果沒有這些工具,你不可能開發一個成功的應用程序。實際上自動化運行時調試器是最近才加入到以上列表中的,它的主要任務是在程序運行期間定位錯誤,這些程序在運行時可能很難手工完成——即使對一些開發高手。
Visual Studio程序開發環境
        在本文中,我將會全面地透視使用Mocrosoft Visual C++編譯器構建的一段代碼的運行時調試。當然,市面上也有許多其它類型的編譯器,有些甚至時免費提供的,但是我選擇這個工具的原因時因爲它最有可能是你已經安裝的。它並不會快速生成代碼,也不是沒有bug,它也不是最遵循ANSI C++標準的編譯器,但是它被廣泛地用於C++ Win32平臺上的軟件開發。
        我將會以介紹一些可能用到的術語和工具來開始。儘管這些對Visual C++開發人員來說已經相當的熟悉,但對那些在UNIX上或者使用Java的人來說可能比較陌生。
Visual Studio集成開發環境(IDE)
        許多應用程序使用此環境進行開發,它由編輯器和一些菜單、工具欄組成,通過它們來調用編譯器和連接器、調試器等。也可能使用其它的編輯器編寫的代碼而通過命令行方式調用Visual C++編譯器和連接器。
VC++工程
        一個Visual C++工程就是一個文件夾,該文件夾中保存着用來構建應用程序的所有文件。它是在你選擇了所開發程序的類型後程序自動生成的。你也可以自己新建一個空的工程然後再單獨編寫代碼。當你構建應用程序時,默認情況下它會生成在工程文件夾的子文件夾中。同時默認情況下有種編譯類型:Debug和Release。Debug版包含了一些有助於調試程序的額外的信息,Release版只包含了最基本的信息,用來發布。Release版的程序在大小和速度上進行了優化,並且不會把一些程序源碼信息暴露給最終用戶。工程定義文件保存在擴展名.dsp的文件中,工作區信息保存在擴展名爲.dsw的文件中。
調試器
        用來控制程序運行,使用戶能夠瀏覽程序的每個執行步驟、檢查程序使用的所有變量、所有的內存分配和處理器的寄存器中的內容。因爲即使是一個相當簡單的應用程序也可能包含成千上萬條機器指令,如果要瀏覽這些指令的話你必須熟知這些機器碼,通過它們能夠一直檢查應用程序。你可以使用調試器(這裏指的是Visual Studio調試器)在源碼中設置斷點,從而檢查程序在斷點位置的運行情況。
調試器窗口
        在Visual Studio Debuger中運行一個程序時,它會打開一些默認的窗口;您也可以根據需要打開其它一些調試窗口。圖1顯示了我在使用Visual Studio Debugger時經常打開的窗口。
        Visual Studio Debugger的主窗口是標準的編輯窗口。箭頭指向了當前應用程序執行到的位置。如果源文件中設置的斷點不能夠到達,那麼調試器將以raw機器碼的形式顯示彙編窗口。
在Visual Studil Debugger界面的右方,你可以看到寄存器窗口和堆棧調用窗口:
·     “寄存器”窗口顯示寄存器內容。可以用來查看CPU寄存器的名稱及其內容,如果在執行程序過程中將“寄存器”窗口始終打開,則在代碼執行時會看到寄存器值的變化。最近更改過的值顯示爲紅色。
·    調用堆棧”窗口使您可以查看調用堆棧上的函數名、參數類型和參數值。僅當正在調試的程序處於中斷狀態時,才顯示調用堆棧信息。
        在調用堆棧窗口之下是“內存”窗口。顯示了指定地址指向的虛擬內存的內容。你可以在窗口該窗口的地址欄中輸入您想查看的地址從而查看它所指向的內容。你也可以從源碼窗口或其它調試窗口中拖動變量或地址到該窗口中查看所對應指向的內容。
        最後介紹的是變量窗口和監視窗口:
·    變量窗口包含三個頁籤。自動:當調試本地應用程序和逐過程進行函數調用 (F10) 時,“自動窗口”將顯示這個函數以及可能由這個函數調用的所有函數的返回值。局部變量:包含當前範圍內所有局部變量的名稱、值以及類型。This (Me):顯示 this 指針指向的對象的名稱、值以及類型。可以直接鍵入變量名或從源碼和調試窗口中直接託拽變量到該窗口來顯示它們的值。
·    監視窗口。可以使用“監視”窗口計算變量和表達式並保留結果。還可以使用“監視”窗口編輯變量或寄存器的值。同樣也可以直接鍵入變量名或從源碼和調試窗口中直接託拽變量到該窗口來顯示它們的值。
使用編譯器進行調試
        編譯器用來把源碼編譯成機器碼。然而編譯器的功能不僅僅是這些,它同時可以用來檢測報告在編譯過程中發現的各種錯誤和一些跟靜態內存分配的潛在的問題。例如:如果你注意到了編譯器的警告級別的話,你也可以通過選擇正確的警告級別設置來避免一些不必要的麻煩。幾乎每種編譯器都會檢測出語法錯誤,但是即使代碼中沒有語法錯誤也並不意味着代碼中不存在錯誤。讓我們來看下面的例子:
PLeftEdge = new char(strlen(pLeft) + 1);
strcpy(pLeftEdge, pLeft);
pLeftEdge = new char(strlen(pRight) + 1);
strcpy(pRightEdge, pRight);
        如果你在使用strcpy()函數前未檢查它的參數--或者大量的使用copy/paste函數――或許這種bug將來就會折磨你。編譯器不會認爲這樣的代碼是有錯誤的,因爲它的語法是正確的。運行含有這種代碼的程序問題很快就會暴露出來,因爲問題的根本在於:你使用了一個未初始化的字符串作爲函數stecpy(pRightEdge,pRight)的參數。在後面我們還會用到這個例子,屆時我們再調試一個存在相似問題的程序。
        但是,編譯器在一些情況下也會有很大用處。接下來詳細介紹一下使用Microsoft C++ 編譯器構建的代碼的實時調試方法。
Visual C++調試設置
        Visual C++提供了兩種默認的構建設置:Release(發行版)和Debug(調試版)。兩種構建配置使用同樣的編譯器,你也可以任意設置兩種配置的參數。對C++來說,修改了某些選項後他們還是屬於發行版和調試版。因此可能有人會問,那麼發行版和調試版有什麼不同?調試版配置包含了有助於調試程序的信息,而發行版的配置旨在提高程序的性能。因爲大多數程序員更傾向於測試一個接近發佈的程序,所以這裏我主要介紹一些有助於調試發行版的相關知識,並會解釋發行版和調試版的不同之處。
符號調試信息
        發行版和調試版最主要的不同之處是調試版的配置默認情況下會創建符號調試信息。如果你相調試發行版的程序,必須確保編譯器和連接器的選項中設置了創建符號調試信息的相關參數。符號信息文件被保存在一個單獨的文件或代碼段中,包含了從彙編指令到源碼的一系列信息。如果沒有這些信息,那調試只能在彙編級別上進行;儘管某些人可能會讀懂彙編代碼,但這顯然不便於進行調試的。
        符號調試信息有多種類型。Microsoft編譯器默認的類型是so-called PDB文件,使用參數/Zi或者/ZI創建(使用這個參數創建的信息具有“Edit and Continue”的特性)。
        PDB文件默認保存在Debug目錄下,名稱同執行文件名,擴展名是.pdb。需要注意的是默認情況下Visual C++ 6編譯器會創建一個名爲VC60.pdb的文件,它是在編譯期間生成的,創建這個文件的原因是,編譯器不知道將要產生的可執行文件的名字。在連接器激活之前,所需要的.obj文件並未確定,所以這些信息暫時保存在這個文件中。默認情況下連接器不會將它合併到projectname.pdb文件中,除非你設置了讓他們合併。
        Visual Studio中的連接器生成pdb的默認選項爲/PDBTYPE:SEPT。你也可以在發行版和調試版配置中改爲/PDBTYPE:CON。CON表示“統一的”。如果通過Visual Studio的DEBUG設置中改變這些設置的話連接中的設置不會跟着改變。我想如果使用設置生成單獨的PDB文件會在性能上有一些優勢,但是你完全可以忽略這點,因爲當今的機器配置都已經相當的高了。如果你想在別的機器上調試你的程序,那麼包含符號調試信息就顯得相當的重要——並且如果你使用了其它的測試工具的話也必須提供所需的符號調試信息。爲了保持完整性,PDB文件必須和執行文件相匹配;必須放到同一個目錄下,並且不要使用我上面提及的連接選項。
        另外可選的生成符號調試的類型是兼容C7(使用編譯選項/Z7,將產生一個.obj文件和一個.exe文件,並帶有調試器使用的行號和全部的符號調試信息)和“Line numbers only”(使用編譯選項/Zd,修改.obj文件或可執行文件的翻譯,以使其只包含全局和外部符號以及行號信息,但不包含符號調試信息)。選擇兼容C7類型生成的調試信息將包含在執行文件中幷包含行號和全部的符號調試信息。這樣兼容了DOS下的CodeView格式。“Line numbers only”不包含符號調試信息。
重定位信息
        重定位信息,或稱“relocs”,是保存在二進制可執行文件中的一個包含定位信息的表格式信息。這些信息服務於所有地址信息,當執行模塊加載到內存中得一個基址時其它地址由連接器設置。如果執行模塊被加載到預定的基址,那麼重定位信息就是沒用的。像Rational Purify這樣的運行時調試工具使用這些信息來對模塊進行“插裝”,由於Purify經常不得不重定位內存中插裝後的模塊(如果原本位置不能再被使用得話)。項目連接設置選項/FIXED:NO,提供了強制建立重定位模塊信息的功能。發佈版設置中未設置此選項,你必須手工在連接設置對話框中加入它。
優化編譯
        優化的目的是使創建的目標代碼變小(使用內存小)或者執行速度加快。爲了達到這個目的,編譯器對彙編代碼做了各種優化。例如,去除掉一些多餘的代碼,去除多餘的表達式,優化循環和使用內聯函數等。
如果是爲了調試的目的的話那麼應該關閉發佈版和調試版中的所有優化選項。優化編譯的代碼會使調試變得更加困難。同時也意味着當你在調試優化編譯的程序時設置斷點要比調試一個未優化版本的程序困難的多的多。
連接時使用debug版運行時庫
        默認情況下調試版本的構建設置在連接階段包含調試版的C運行時庫。這會對查找bug有所幫助,因爲這樣一來使用了調試版的動態內存分配。例如,增加位模式來標識已經被分配的內存,並且這些信息有助於檢測是否發生越界訪問。它們也佔用了一定的內存,位於每一處新分配內存塊的後面,因此被稱作爲“守護字節”,用於檢查越界訪問。
        調試版的內存分配函數爲malloc_dbg和heap_alloc_dbg。這樣所有對的malloc()和new()調用都將解釋爲調試版的函數。內存釋放函數free()和delete()都解釋爲調試版的函數free_dbg。
未被初始化的內存的內容爲0xCD,在內存分配結構邊界上的內存的內容爲0xFD,空閒內存的內容爲0xDD。
在發佈版的構建配置中使用調試版的運行時庫也將會使構建成爲調試版的構建版本。而且如果你有商業的運行時調試工具的話基本上就用不到它。因此在構建發佈版的調試程序時使用動態連接運行時庫時非常重要的(默認編譯選項是/MD)。
警告級別
        Visual Studio的默認警告級別是Level 3(編譯選項/W3)。該級別將報告例如在函數原型執行前調用的信息。如果是處於調試的目的的話,可以修改警告級別爲/W4,這樣有助於檢測所有未初始化的局部變量和雖然已經初始化但未被使用的變量。因爲缺少對Visual C++編譯器的瞭解,這些都是部分有用的。部分原因是由於/W4級別下也產生了大量的並非真正的警告信息(即它會誤報),這樣就會導致難於定位潛在的問題。然而,如果你把所有的警告信息都當作錯誤看待,那麼你寫的代碼當然會比默認級別下編寫的代碼存在的缺陷更少。/W4級別的另一個好處是可以預防斷言失敗。
Debug版本中捕獲Release版本的錯誤
        當使用/GZ選項時,Visual C++將自動初始化所有局部變量(使用值0xCCCCCCCC),檢查函數指針調用堆棧的合法性,並檢查調用堆棧的合法性。調試版的默認設置中默認啓用了該選項,你也可以在發佈版的配置中手工設置。
運行時調試:都是與內存相關的
        當你要執行你的程序時,它首先會被加載到內存。實際上,因爲Windows使用了內存映射機制,執行文件只有部分在需要的時候才被換入內存。另外Windows系統使用便攜式格式文檔,從結構上來看也跟他們在內存中的映象一致。
堆和棧
        當程序啓動時,使用內存頁來保存程序所使用的所有靜態的和動態的數據。每個進程都至少使用了兩種類型的內存區域:
·    棧,或靜態數據塊,是保存所有自動變量的內存區。在Win32平臺,每個線程都有它自己的靜態內存區域。主程序線程所使用的堆的大小是在編譯期間就已經確定了的,默認情況下它的值爲1MB。棧的大小也可以使用連接選項/STACK:reserve[,commit]選項指定。也可以在模塊定義文件中使用STACKSIZE語句覆蓋這個值,或者使用EDITBIN.EXT工具直接修改二進制可執行文件。
·    堆,或稱自由存儲區,是虛擬內存中使用不受約束的區域,使用句柄來標識,並且使用範圍僅受可用虛擬內存的限制。對上的動態結構和句柄是在運行時分配的。
        每個進程都至少擁有一個默認的堆,但有的進程可能有許多的動態堆。程序運行期間通過堆API函數從堆上分配內存塊。進程創建的默認的堆是私有的,並且不能被其它進程使用。默認堆的內存保留區域和提交區的大小是在連接期間已經確定了的。你可以改變默認堆的大小(1MB),使用連接選項/HEAP:reserve[,commit]。同樣也可以使用EDITBIN工具修改已經編譯連接好的二進制執行文件。
        堆的分配函數有三種類型:
·    GlobalAlloc/GlobalFree and LocalAlloc/LocalFree用於在默認堆上分配/回收內存
·    COM Imalloc allocatorCoTaskMemAlloc/CoTaskMenFree),用於默認堆上的內存分配
·    C運行時內存分配API——new()/delete()malloc()/free(),用於C運行時在私有堆上的內存分配/回收
        此外Win32 API中的VirtualAlloc()和VirtualFree()函數用於虛擬內存頁的分配。你可以在Win32應用程序中直接調用這兩個函數,但是一般這樣的函數是用不着的,除非你想一次性分配一大塊內存。
這僅僅是調試最艱難一步的開始——控制和調試動態分配結構。然而很顯然第一步是:必須讓程序運行。我將在本文的第2部分講解這一部分。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章