.NET 中的斷言和跟蹤

來源:互聯網
上載者:User
發布日期: 9/9/2004 | 更新日期: 9/9/2004

John Robbins 下載本文中的代碼:Bugslayer0102.exe (42KB)

本頁內容
調試器
條件編譯
跟蹤和 TraceSwitch
斷言
TraceListener — Listeners 正在偵聽
BugslayerTraceListener 的用法和實現
更多新功能
小結

因為 Microsoft 已經發布了 Visual Studio .NET Beta 1,所以許多讀者已經開始深入研究 .NET。但不要搞錯了 — .NET 是一種全新的平台。關於諸如 ASP .NET 和 ADO .NET 之類的關鍵功能,已經進行了大量的宣傳。但對於我而言,.NET 的最大優點之一在於它解決了所有編程問題中最棘手的一個問題 — 記憶體損壞和泄漏。

藉助於公用語言運行庫 (CLR) 來照管指標和記憶體管理,您可以集中精力來解決使用者的問題,而不是浪費時間去尋找記憶體損壞。此外,Microsoft 最終具有了一個用於訪問系統的全新系統範圍的編程模型。一致的物件導向的基底類別庫 (BCL) 也可以消除大量的錯誤。

難道這就意味著沒有撰寫 Bugslayer 專欄文章的需要了嗎?在 Visual Studio 能夠實現您的想法之前,仍然會出現很多問題,如邏輯錯誤,以及對系統、效能和延展性的誤解。

本月,我打算協助您開始進行正確的 .NET 開發。您可能會回想起來,有效舊斷言非常符合我的心意。當我第一次開始學習 .NET 時,我感到若有所失,因為我所鐘愛的 ASSERT 和 TRACE 宏不見了。因此,我希望向您說明如何在 .NET 中進行斷言和跟蹤。為此,我首先需要討論一下 .NET SDK 中提供的調試器。然後,我將討論斷言和條件編譯,並為您提供一個比 BCL 所提供的更好的斷言工具。

本月專欄中的所有代碼都是用 .NET SDK Beta 1 開發的,因此不需要使用完整的 Visual Studio .NET 來編譯和運行。除非 Microsoft 在 Beta 2 中更改了介面,否則這些代碼將繼續正常工作。您應該瞭解的關於 .NET SDK 的一件事情就是它極為穩定;我從 PDC 版本開始就一直在使用它,並且沒有出現過任何問題。

調試器

通常,調試就是調試。換句話說,無論您使用何種作業系統,總是要設定斷點、轉儲記憶體並執行其他一些常見操作。對於那些具有 Win32, 背景的開發人員而言,.NET 調試有一項主要功能略有不同。您可以在正在啟動並執行 .NET 進程中隨意附加和分離(沒錯,就是分離)調試器!現在,調試正在啟動並執行伺服器應用程式應該比以前更加容易。

.NET SDK 包含兩個調試器:CORDBG.EXE 和 DBGURT.EXE。其中,DBGURT.EXE 是較好的 GUI,CORDBG.EXE 是基於控制台的版本,它具有幾項附加功能,但比較難使用。CORDBG.EXE 中神奇的 ? 命令非常重要,因為您可以使用它來擷取有關所有命令的協助。通過在 ? 後面輸入命令名稱,您可以獲得有關單個命令的更多協助。通常,如果您曾經使用過 WinDBG 之類的調試器,則應該能夠推測出大多數命令。但是,我還是打算向您說明 CORDBG.EXE 中我覺得有趣的幾個命令。

我希望引起您注意的第一個命令是 wt,它會逐句通過應用程式,並列印所調用的每個託管方法的調用樹。我發現使用 wt 來瞭解各種系統類別之間的調用關係非常有用。有時,wt 命令會顯示許多沒有用處的資訊,但查看一下 BCL 中誰對誰執行了什麼操作卻是非常有用的。wt 命令能夠從調試器的當前程式碼開始到相應方法的結尾為止,來跟蹤程式流。

1 中的簡單程式示範了 wt 命令。一旦您開始對程式執行 CORDBG.EXE,將從 Main 中調用 Foo 的程式碼開始。如果您鍵入“wt”,則輸出將如下所示:

(cordbg) wt       1        HappyAppy::Main       6        HappyAppy::Foo       6        HappyAppy::Bar      10        HappyAppy::Baz       7        HappyAppy::Bar       7        HappyAppy::Foo       2        HappyAppy::Main      39 instructions total

並且您可以看到 Foo 所調用的每個元素的調用圖。在執行 wt 命令之後,指令指標將指向對 Foo 的調用之後的程式碼。

另一個有用的命令是 f (funceval),它使您能夠調用您的類外部的方法。對於非靜態方法,請記住傳遞“this”作為第一個參數。還有一組使您能夠建立對象的命令,但是我尚未猜測出它們的工作方式。

CORDBG.EXE 使用起來有一點兒麻煩,而 DBGURT 則像夢一樣美妙。從我去年七月在 PDC 收集的資訊來看,DBGURT 與 Visual Studio .NET 使用相同的代碼基,因此您可以領略一下最終的 Visual Studio .NET 調試器的樣子。只是從 .NET SDK 中的小型預覽看來,我確實很喜歡我所看到的內容。首先,Visual C++ 6.0 調試器中應該可以停靠的對話方塊(如 Modules、Threads 和 Breakpoints)最終還是可停靠的,從而使調試器更加便於使用。此外,為了防止所有這些可停靠的視窗由於佔據太多的螢幕地區而要求使用 35 英寸的監視器,調試器具有一個非常直觀的模式,在該模式中,多個停靠視窗可以共用同一個帶有類比標籤的視窗地區。 2 顯示了各種可停靠視窗(如 Modules、Threads 和 Breakpoints 顯示),所有視窗都共用螢幕底部的地區。

並非調試器 UI 所承諾的每件事情都存在於 SDK 的 Beta 1 版本中。具體說來,DBGURT 缺少 C# 的文法著色和某些類型的斷點。但是,DBGURT 中有足夠的功能可讓您獲得 .NET 調試樂趣。DBGURT 有幾個新的跳過計數位置斷點非常有用。現在,當跳過計數恰好等於特定數字、或者特定數位倍數、或者大於或等於特定數字時,您將中斷運行。

既然我已經討論了調試器的要點,那麼現在我要轉而討論一下條件編譯,因為離開了它,跟蹤和斷言將不複存在!

返回頁首

條件編譯

.NET SDK 中的所有三個語言編譯器(C#、Visual Basic 和 C++)都支援標準的 #ifdef...#endif 風格的條件編譯。但是,C# 和 Visual Basic 現在具有一個非常好的自訂屬性,它可通過另一種方式來支援條件編譯。在聲明 C# 或 Visual Basic 方法時,您可以使用該條件屬性來確定該方法何時可以調用。該條件屬性的優點是:調用方在調用方法時不必執行任何額外的工作。如果設定了 C# 的 #define 所指定的條件屬性中的編譯指令,或者設定了 C# 與 Visual Basic 的 /D: 編譯器選項所指定的條件屬性中的編譯指令,則將進行方法調用。否則,編譯器不會產生該調用所需的 Microsoft 中繼語言 (MSIL)。

下面的程式顯示了 C# 樣本中條件屬性的用法:

using System ;class HappyAppy{    [conditional ( "DEBUG" )]    public static void DebugOnlyMethod ( )    {        Console.WriteLine ( "DEBUG is active!!" ) ;    }    public static void Main ( )    {        DebugOnlyMethod ( ) ;    }}以下是用 Visual Basic 編寫的同一樣本:imports Systemimports System.DiagnosticsPublic Module HappyAppy    Sub  DebugOnlyMethod ( )        Console.WriteLine ( "DEBUG is active!!")    End Sub    Sub Main        DebugOnlyMethod ( )    End SubEnd Module

只有當您在編譯時間定義了 DEBUG,DebugOnlyMethod 才會在這兩個樣本中出現。這種做法的優點是在進行 DebugOnlyMethod 調用時,無須使用大量的 #ifdef...#endif。如果您尚未明白條件屬性是如何工作的,我建議您運行一下前面的程式。正像您開始看到的那樣,C# 和 Visual Basic 中的條件編譯功能使得使用大量斷言的卓越編程實踐變得十分輕鬆愉快。

返回頁首

跟蹤和 TraceSwitch

BCL 提供了兩個完全相同的類來處理跟蹤和斷言:Trace 和 Debug,這兩個類都來自 System.Diagnostics 命名空間。有趣的是,這兩個類具有完全相同的屬性和方法,但它們並不是從對方派生的,也不是從 Object 以外的任何基類派生的。這兩個類之間的觀念是:當您定義 DEBUG 類時,Debug 類是活動的;而當您定義 TRACE 時,Trace 類是活動的。根據文檔資料,Microsoft 希望您對調試版本使用 DEBUG,而對所有版本使用 TRACE。.NET 的面向網路系統管理員的一個功能是您可以實地啟用輕型診斷跟蹤,因此您應該始終定義 TRACE。但是,像以前作業系統中的跟蹤一樣,如果您不遵循嚴格的格式設定並且只輸出協助您瞭解程式流所需的最少內容,則輸出可能會過多。

Trace 和 Debug 類上的跟蹤方法包括 Write、WriteIf、WriteLine 和 WriteLineIf。Write 和 WriteLine 之間的唯一區別是 WriteLine 會在輸出的結尾放置一個斷行符號符和一個分行符號。WriteIf 和 WriteLineIf 僅當第一個參數計算為真時才會執行跟蹤。這可以為您提供條件跟蹤功能。

儘管這聽起來不錯,但使用 WriteIf 和 WriteLineIf 或許並不是一個好主意。您能看出下面的程式碼片段有什麼錯誤嗎?

Debug.WriteLineIf ( ShowTrace                                                    "Num = " + Num + " value out of range!"  ) ;

問題是在調用 Debug.WriteLineIf 之前對將要顯示的字串執行完全計算和產生。這意味著在每次執行該行時,即使 ShowTrace 為假,您仍然具有參數產生的所有系統開銷。與此不同,您應該做的是在進行調用之前使用正常的條件檢查,就像下面的程式碼片段一樣。

if ( true == bShowTrace ){     Debug.WriteLine ("Num = " + Num + " value out of range!" ) ; }

此處,在條件計算為真並且您將執行跟蹤之前,將避免字串參數產生的系統開銷。缺點在於您必須執行更多的鍵入操作。

因為跟蹤是實地尋找問題的一種好方法,所以 Microsoft 在 System.Diagnostics 中添加了另一個類來協助您確定追蹤層級:TraceSwitch。TraceSwitch 是一個簡單的條件類,它使得跟蹤各個程式集、模組和類變得更加容易。TraceSwitch 的目的是使您可以輕鬆地確定追蹤層級,以便您的代碼能夠即時產生適當的輸出。可以通過 TraceSwitch 類的屬性來確定追蹤層級,因為如果設定的層級恰當,這些屬性將全部返回真。 3顯示了追蹤層級和它們的值。

建立和使用 TraceSwitch 的過程有一些瑣碎。下面的程式碼片段說明了 TraceSwitch 的建立和使用。我使用 WriteLineIf 壓縮了該程式碼片段。

public static void Main ( ){    TraceSwitch TheSwitch = new TraceSwitch (         "SwitchyTheSwitch", "Example Switch"  );    Trace.WriteLineIf ( TheSwitch.TraceError ,                        "Error tracing is on!" ) ;    Trace.WriteLineIf ( TheSwitch.TraceWarning ,                        "Warning tracing is on!" ) ;    Trace.WriteLineIf ( TheSwitch.TraceInfo ,                       "Info tracing is on!" ) ;    Trace.WriteLineIf ( TheSwitch.TraceVerbose ,                        "VerboseSwitching is on!" ) ;}

此時,您可能很想知道如何設定追蹤層級。TraceSwitch 建構函式採用了兩個參數:開關名稱和開關說明。重要的值是開關名稱,因為您必須使用確切的字串來設定追蹤層級。第一種設定開關的方法是為 HKLM/SOFTWARE/Microsoft/COMPlus/Switches 中的所有開關使用全域登錄機碼。只須建立一個與開關名稱匹配的 DWORD 值,並將數字設定為 3 中指定的相應數字。

另一種設定特定追蹤層級的方法是使用環境變數,即 _Switch_ 後面跟開關名稱。以前面的程式碼片段為例,環境變數應該是 _Switch_SwitchyTheSwitch。將環境變數設定為您希望看到的追蹤層級。請記住,任何環境變數都將重寫註冊表設定。

將所有應用程式的跟蹤開關放入單個環境變數中的思想看起來好像是一起等待發生的事故。我可以很容易地看到各個企業之間發生命名衝突的非常真實的可能性。我感覺如果有一種可以從輸入檔案指定跟蹤開關的方法,那麼將會好得多。在本月的 Bugslayer 代碼中,我建立了一個名為 BugslayerTraceSwitch 的派生自 TraceSwitch 的類(所有相關代碼都可以在本文頂部的連結處找到)。BugslayerTraceSwitch 的建構函式還採用了第三個參數,即用於讀取追蹤層級設定的檔案。假設您傳入建構函式的檔案名稱具有足夠的資訊,以便它能夠被找到。BugslayerTraceSwitch 是我的 Bugslayer 程式集的一部分,因此您只須將 Bugslayer 作為匯入檔案予以包括。檔案格式非常簡單,如下面的程式碼片段所示。

; The format is =HappyAppyClassSwitch=4Switcheroo=0

請注意,帶有分號的行被視為注釋行。

既然您已經瞭解了如何使用 .NET 中的跟蹤功能,下面讓我們討論一下輸出的去向。預設情況下,跟蹤輸出除了經曆傳統的 Win32 OutputDebugString 調用以外,還會去往附加的調試器。請記住,.NET 不是基於 Win32 的傳統應用程式,因此調試器輸出將不同於您習慣看到的內容。在本專欄的後面,我將詳細討論輸出。

在本部分的開頭,我提到過 Trace 和 Debug 類都具有跟蹤方法。但您應該使用哪個方法呢?我決定只使用 Trace 來進行跟蹤。這樣,我在進行跟蹤時就有了一致的方式,而不必在鍵入跟蹤語句之前進行一番考慮。既然您已經瞭解了跟蹤的工作方式,下面讓我們轉而討論一下 .NET 中的斷言是如何工作的。

返回頁首

斷言

正像我在前面提到的那樣,Trace 和 Debug 類都具有 Assert 方法。我只使用 Debug 類中的 Assert。這些方法是完全相同的,但我不希望在運行應用程式的過程中彈出意外的訊息框,因此我堅持使用 Debug 版本。預設的斷言訊息框如 4 所示。請注意,.NET 斷言附帶了現成的堆棧審核(帶有填充源和行尋找)。

4 Debug 斷言

儘管 C++ ASSERT 宏使用起來更加容易一些,但 C# 中的 Debug.Assert 方法也不錯;重載的 Assert 方法只是採用了不同的參數。第一個方法採用單個 Boolean 條件;第二個方法採用一個 Boolean 條件和一個訊息字串;最後一個方法採用一個 Boolean 條件、一個訊息字串和一個詳細的訊息字串。使用 .NET 中的 Assert 意味著您必須比傳統的 C++ ASSERT 完成更多的鍵入操作。因為沒有 .NET 宏,所以為了在斷言訊息框中顯示斷言字串,您需要自己傳入字串。下面的程式碼片段顯示了全部三種類型的 C# Assert 的工作方式。

Debug.Assert ( i > 3 ) ;Debug.Assert ( i > 3 , "i > 3" ) ;Debug.Assert ( i > 3 , "i > 3" , "This means I got a bad parameter") ;

當然,因為 C# 支援條件屬性,所以您只需定義 DEBUG 來啟用斷言代碼。對於 Visual Basic,您需要用真正的條件編譯來環繞各個斷言,如下所示:

#If DEBUG ThenDebug.Assert ( i > 3 )#End If

正像我在前面提到的那樣,如果代碼以互動方式運行,則 .NET 中的斷言將顯示在訊息框中。我在 .NET 中搗鼓了一下,發現可以將所有應用程式的斷言全域性地重新定向到檔案中。但使用這些技術需要您自擔風險。此外,除了您用於進行開發的電腦以外,絕對不要在其他任何電腦上使用它們。有了這個前提,下面是需要遵循的步驟。在 HKLM/Software/Microsoft/ComPlus 中,您需要添加兩個 DWORD 值(NoGuiOnAssert 和 LogToFile)以及一個字串值 (LogFile)。將 NoGuiOnAssert 設定為 1 以禁用訊息框。將 LogToFile 設定為 1 以啟用將日誌記錄到檔案的功能。將 LogFile 設定為所有斷言輸出應該前往的完整名稱和路徑。但是,在更改全域設定之前,您應該知道如何通過 TraceListener 更好地控制斷言輸出。

返回頁首

TraceListener — Listeners 正在偵聽

跟蹤和斷言在 .NET 中是唯一的,因為控制輸出相當容易。Trace 和 Debug 類都有一個成員 Listeners,它是 TraceListener 對象的數組。TraceListener 發送跟蹤和斷言的輸出。正如您能夠想象的那樣,您可以有一個用於將輸出發送到 OutputDebugString 的跟蹤接聽程式和一個用於將輸出發送到檔案的跟蹤接聽程式。Trace 和 Debug 類的功能是逐個枚舉 Listeners 數組中的 TraceListener 類,並讓每個類處理輸出。這可以讓您添加或減少輸出。預設的 TraceListener (DefaultTraceListener) 通過它的 Write 和 WriteLine 方法將跟蹤輸出發送到 OutputDebugString,以及所有附加調試器的 Log 方法。如果使用者以互動方式登入,則 DefaultTraceListener 將通過它的 Fail 方法將所有斷言發送到訊息框。

BCL 附帶了一些預定義的 TraceListener,您可以將它們添加到 Listeners 數組中以作為附加的輸出手段。第一個是 EventLogTraceListener 類,它可將輸出發送到指定的事件記錄。第二個是 TextWriterTraceListener,它可將輸出定向到 TextWriter 或 Stream,如 FileStream 的 Console.Out 函數。下面的代碼顯示了如何將一個 TextWriterTraceListener 添加到鏈中。

Debug.Listeners.Add(new TextWriterTraceListener ( "Trace.Log" ) );
返回頁首

BugslayerTraceListener 的用法和實現

能夠隨意替換跟蹤輸出是一個有趣的主意。但是,從實際的觀點出發,我寧願有一個 TraceListener。首先,我可以從應用程式中的任意位置來控制它。如果有多個獨立的 TraceListener,則對其中的每一個進行控制會變得更加困難。其次,一個 TraceListener 可以處理整個應用程式的所有輸出需要。因此,我編寫了 BugslayerTraceListener 以便簡化我的工作。它是所有 TraceListener 的完全插入替換。跟蹤輸出可以去往多個位置(附加的調試器、檔案以及標準的 OutputDebugString 調用)的任意組合。斷言除了可以去往訊息框和事件記錄以外,還可以去往上述所有位置。將 BugslayerTraceListener 添加到 Debug 或 Trace 對象的過程非常簡單。

Debug.Listeners.Remove ( "Default" ) ;BugslayerTraceListener btl = new BugslayerTraceListener ( ) ;Debug.Listeners.Add ( btl ) ;

您需要做的一件事情是刪除 DefaultTraceListener,以便 BugslayerTraceListener 可以控制輸出。

如果您查看 BugslayerTraceListener 本身的代碼,則沒有多少令人激動的內容。有趣的部分位於 BUGSLAYERWIN32.CS 中(請參見 5)。我需要確保 BugslayerTraceListener 在彈出訊息框之前還要檢查一下是否有互動式使用者。這要求我用特殊的結構調入 Win32 API。通常,從Managed 程式碼中調用 Win32 API 會令人感到困惑。如果您需要將某些代碼提到 .NET 中,我希望 BUGSLAYERWIN32.CS 可以為您提供一些有關如何做到這一點的提示。

返回頁首

更多新功能

非託管 Visual C++ 的編譯器和連結器看上去似乎有一些有趣的新功能。在進行了關於 .NET 的所有討論以後,傳統 Visual C++ 中的新功能將獲得較少的關注。藉助於顯著改進的調試器和新的編譯器標誌,Visual C++ .NET 將是對已安裝 C/C++ 代碼基的所有使用者的強制性升級。我通過閱讀 .NET SDK 文檔資料中的 Visual C++ Compiler Reference 來瞭解這些標誌。

我最喜歡的新標誌是 CL.EXE 用於進行執行階段錯誤檢查的 /RTC。它所檢查的一些錯誤包括局部記憶體上溢和下溢、未初始化的記憶體訪問和資料截斷。需要記住的是這些檢查在運行時發生。CL.EXE 和 /LTCG(連結時代碼產生)的新 /GL(全程式最佳化)標誌提供了前所未有的程式最佳化層級。最有趣的最佳化是跨模組內聯,即在模組中內嵌函式(即使該函數是在另一個模組中定義的)。對 x86 CPU 的另一個最佳化是自訂呼叫慣例,它將允許編譯器和連結器在函數調用之間使用寄存器傳遞參數。CL.EXE /GS(產生安全檢查)選項將插入代碼,以檢查是否存在使返回地址衝出堆棧的緩衝區溢位。啟用 /GS 以後,任何試圖接管您的程式的病毒或欺詐代碼都將彈出一個訊息框,並立即終止進程。

最後一個有趣的新標誌 /PDBSTRIPPED 是一個 LINK.EXE 標誌,它將只使用公用符號和架構指標最佳化 (FPO) 資料產生第二個 PDB 檔案。這樣,您就可以將第二個 PDB 檔案發送給您的客戶,以便您可以實地從 Dr. Watson 日誌中擷取完整的呼叫堆疊和資訊。總的來說,Visual C++ .NET 中的確有一些非常好的新功能,因此我已經迫不及待地要將現有代碼遷移過去。

返回頁首

小結

我希望這一有關在 .NET 上進行調試的簡介將使您的開發工作變得容易一些。具體說來,BugslayerTraceListener 應該使您的跟蹤和斷言變得更加容易。在考察 .NET 時,請不要忘記在老地方尋找新功能。並且,應該始終通過從一開始就編寫較好的診斷代碼,來使您的工作變得更加輕鬆。

因為我認為每個人都應該坦白他們的錯誤,所以我必須承認在我的 2000 年 12 月刊專欄的 Smooth Working Set 工具 + 生產力中有一個小錯誤。令人非常難堪的是,我在 SWSFILE.CPP 的 CFileBase::AppendToDataBuffer 中有一個重新分配問題。更新後的代碼如 6所示。感謝 Eric Patey 和 Richard Cooper 報告了這一問題。

技巧 41(來自 Ted Yu):在您的 2000 年 4 月刊專欄中,您抱怨 STL 的 bsearch 函數不傳回值。下面是一個能夠返回與找到的值相對應的迭代器的 bsearch 函數。如果找不到該值,則該函數將返回 end()。

template inline _FI bsearch ( _FI _F , _FI _L , const _Ty& _V ){     _FI _I = lower_bound(_F, _L, _V);     if (_I == _L || _V < *_I)      {          return _L ;     }     return ( _I ) ; }

技巧 42(來自 Patrick Gautschi):Microsoft 已經發布了一組有趣的工具,以協助使用者跟蹤名為 UMDH(使用者模式轉儲堆)的記憶體流失。您可以從 How to Use Umdh.exe to Find Memory Leaks 下載這些工具。請確保閱讀有關如何使用它們的整篇知識庫文章。

John Robbins 是 Wintellect 的創始人之一,該公司是一家專門致力於 Windows 和 COM 編程的軟體諮詢、教育和開發公司。他是 Debugging Applications (Microsoft Press, 2000) 一書的作者。要聯絡 John,請訪問 http://www.wintellect.com。

本文摘自 MSDN Magazine 的 2001 年 2 月刊。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.