Bugslayer: Assertions and Tracing in .NET

We were unable to locate this content in de-de.

Here is the same content in en-us.

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MSDN Magazine
Assertions and Tracing in .NET
John Robbins
Download the code for this article: Bugslayer0102.exe (42KB)
Browse the code for this article at Code Center: TraceListener

N
ow that Microsoft has released Visual Studio .NET Beta 1, many of you have started to take a closer look at .NET. Make no mistakeâ€".NET is a completely new platform. There's been quite a bit of noise about key features such as ASP .NET and ADO .NET. But as far as I'm concerned, one of the best things about .NET is that it solves the nastiest programming problem of allâ€"memory corruption and leaks.
      With the common language runtime (CLR) taking care of pointers and memory management, you can concentrate on solving the user's problems instead of fiddling around looking for that memory corruption. Additionally, Microsoft finally has a clean systemwide programming model for accessing the system. The consistent object-oriented Base Class Library (BCL) will eliminate huge numbers of bugs as well.
      Does this mean that there won't be a need for the Bugslayer column? Well, until Visual Studio® can read your mind, there will still be plenty of room for problems such as logic errors, misconceptions about the system, performance, and scalability.
      This month, I want to help you start your .NET development on the right foot. You may recall that good old assertions are near and dear to my heart. When I first started learning .NET, I was lost because my beloved ASSERT and TRACE macros were gone. Consequently, I want to show you how to do assertions and tracing in .NET. To do that, I'll first need to talk about the debuggers available in the .NET SDK. Then I'll talk about assertions and conditional compilation, and give you a better assertion tool than the one supplied by the BCL.
      All the code in this month's column was developed with Beta 1 of the .NET SDK and does not require the full Visual Studio .NET to compile and run. Unless Microsoft changes the interfaces in Beta 2, the code should continue to work just fine. One thing you should know about the .NET SDK is that it is extremely stable; I have been using it since the PDC release without problems.

The Debuggers

      In general, debugging is debugging. In other words, no matter what operating system you might be using, you will always set breakpoints, dump memory, and perform a number of other common actions. For those developers with a background in Win32®, there is one major feature of .NET debugging that is a little bit different. You can attach and detach (yes, detach) the debugger from running .NET processes all you want! Now debugging your live server applications should be much easier than ever before.
      The .NET SDK contains two debuggers, CORDBG.EXE and DBGURT.EXE. Where DBGURT.EXE is the nice GUI, CORDBG.EXE is the console-based version that has a few more capabilities, but is harder to use. The magic ? command in CORDBG.EXE is very important because it's what gets you help on all those commands. You can get more help on individual commands by passing the command name after the ?. In general, you should be able to figure out most of the commands if you have ever used a debugger like WinDBG. However, I'd like to point out a couple of commands in CORDBG.EXE that I find interesting.
      The first command I want to call to your attention is wt, which steps through an application, printing a call tree of each managed method called. I've found it very helpful to use wt to get an idea of who calls what between various system classes. Sometimes the wt command will show so much information that it's not useful, but it's invaluable to see what in the BCL does what to whom. The wt command traces the program flow, starting from the current line in the debugger to the end of the method.
      The simple program in Figure 1 demonstrates the wt command. Once you start CORDBG.EXE on the program, you will start in Main on the line that calls Foo. If you type "wt" the output is as follows,
(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
and you can see the call graph for everything Foo called. After the wt command, the instruction pointer sits on the line after the call to Foo.
      Another useful command is f (funceval), which allows you to call methods off your classes. For nonstatic methods, remember to pass "this" as the first argument. There is also a set of commands that allow you to create objects, but I haven't had much luck figuring out how they work.
      While CORDBG.EXE is a little painful to use, DBGURT is like a dream. From what I gathered at the PDC last July, DBGURT uses the same code base as Visual Studio.NET, so you can get a taste of what the final Visual Studio .NET debugger will look like. Based on just the small preview in the .NET SDK, I really like what I see. First, the dialogs in the Visual C++® 6.0 debugger that should have been dockableâ€"such as the modules, threads, and breakpointsâ€"finally are dockable, making the debugger much easier to use. Additionally, to keep all those dockable windows from requiring a 35-inch monitor because they take up too much screen real estate, the debugger has a fantastically intuitive mode where multiple docked windows can share the same window area with a tab metaphor. Figure 2 shows various dockable windows such as the Modules, Threads, and Breakpoints displays, all sharing the real estate on the bottom of the screen.
      Not everything promised by the debugger's UI exists in the Beta 1 release of the SDK. Specifically, DBGURT is missing syntax coloring for C# and some types of breakpoints. However, there is more than enough in DBGURT to achieve .NET debugging bliss. DBGURT has a couple of new skip count location breakpoints that are quite helpful. Now you break when the skip count is exactly equal to a specific number, when the skip count is a multiple of a specific number, or when the skip count is greater than or equal to a specific number.
      Now that I have covered the high points of the debuggers, I want to turn to conditional compilation, because without it, tracing and assertions don't exist!

Conditional Compilation

      All three language compilers in the .NET SDK (C#, Visual Basic®, and C++) support the standard #ifdef...#endif style of conditional compilation. However, C# and Visual Basic now have a very nifty custom attribute that supports conditional compilation another way. When declaring a C# or Visual Basic method, you can use the conditional attribute to determine when the method is callable. The beauty of the conditional attribute is that the callers never have to do any extra work when calling the method. If the compilation directive in the conditional attribute specified either by a #define for C# or the /D: compiler option for both C# and Visual Basic is set, the method call will be made. Otherwise, the compiler does not generate the Microsoft Intermediate Language (MSIL) necessary for the call.
      The following program shows the usage of the conditional attribute in a C# example:
using System ;

class HappyAppy
{

    [conditional ( "DEBUG" )]
    public static void DebugOnlyMethod ( )
    {
        Console.WriteLine ( "DEBUG is active!!" ) ;
    }

    public static void Main ( )
    {
        DebugOnlyMethod ( ) ;
    }
}
Here's the same example in Visual Basic:
imports System
imports System.Diagnostics
Public Module HappyAppy
    Sub <Conditional ( "DEBUG" )> DebugOnlyMethod ( )
        Console.WriteLine ( "DEBUG is active!!")
    End Sub
    Sub Main
        DebugOnlyMethod ( )
    End Sub
End Module
The DebugOnlyMethod exists in both samples only if you define DEBUG at compile time. The beauty of this is that the call to DebugOnlyMethod can be made without requiring lots of #ifdef...#endif stuff. If you haven't seen the conditional attribute in action, I encourage you to run the preceding programs. As you'll start to see, the conditional compilation capabilities in C# and Visual Basic make the excellent programming practice of massive assertions a piece of cake.

Tracing and TraceSwitches

      The BCL offers two identical classes that handle tracing and assertions: Trace and Debug, both from the System.Diagnostics namespace. What's interesting is that both classes have the exact same properties and methods, but do not derive from each other or from any base class except Object. The idea between the two classes is that the Debug class is active when you define DEBUG, and the Trace class is active when you define TRACE. According to the documentation, Microsoft expects you to use DEBUG for your debug builds and TRACE for all builds. One of the features of .NET aimed at network administrators is that you can turn on lightweight diagnostic tracing out in the field, so you should always have TRACE defined. However, like tracing in operating systems of yore, the output can get overwhelming if you don't keep to a strict formatting and only output the minimum necessary to help you figure out the program flow.
      The tracing methods on the Trace and Debug classes are Write, WriteIf, WriteLine, and WriteLineIf. The only difference between Write and WriteLine is that WriteLine puts a carriage return and a line feed at the end of the output. WriteIf and WriteLineIf perform the tracing only if the first parameter evaluates to true. This gives you conditional tracing capabilities.
      While that sounds nice, it's probably not a good idea to use WriteIf and WriteLineIf. Do you see what's wrong with the following code snippet?
Debug.WriteLineIf ( ShowTrace                                
                    "Num = " + Num + " value out of range!"  ) ;
The problem is that the string that will be shown is fully evaluated and generated before the call to Debug.WriteLineIf. That means even if ShowTrace is false, you still have all the overhead of the parameter generation each time the line is executed. What you should do instead is use a normal conditional check before making the call, like in the following code snippet.
if ( true == bShowTrace )
{
     Debug.WriteLine ("Num = " + Num + " value out of range!" ) ; 
}
Here you avoid the overhead of the string parameter generation until the condition evaluates to true and you are going to do the trace. The downside is that you have to do more typing.
      Since tracing is such a great method for finding problems in the field, Microsoft added another class in System.Diagnostics to help you determine the tracing level: TraceSwitch. TraceSwitch is a simple conditional class that will make it easier to allow tracing for each assembly, module, and class. The purpose of TraceSwitch is to allow you to easily determine the tracing level so your code generates the appropriate output on the fly. The properties on the TraceSwitch class allow the trace level determination because the properties all return true if the appropriate level is set. Figure 3 shows the trace levels and their values.
      Creating and using a TraceSwitch is trivial. The following code snippet shows the TraceSwitch creation and use. I used WriteLineIf to condense the code snippet.
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!" ) ;
}
      At this point, you are probably wondering how to get the trace level set. The TraceSwitch constructor takes two parameters: the switch name and the switch description. The important value is the switch name because you have to use the exact string to get the trace level set. The first way to get a switch set is to use the global registry key for all switches in HKLM\SOFTWARE\Microsoft\COMPlus\Switches. Simply create a DWORD value that matches the switch name and set the number to the appropriate one specified in Figure 3.
      Another way to set the specific trace level is to use an environment variable, which is _Switch_ followed by your switch name. Using the previous code snippet as an example, the environment variable would be _Switch_SwitchyTheSwitch. Set the environment variable to the trace level you want to see. Keep in mind that any environment variables override the registry settings.
      The idea of all application's trace switches sitting in a single environment variable seems like an accident waiting to happen. I can easily see the very real possibility of naming conflicts between companies. I felt it would be much better if there were a way to specify the trace switches from an input file. In this month's Bugslayer code, I created a derived class from TraceSwitch called BugslayerTraceSwitch (all pertinent code can be found at the link at the top of this article). BugslayerTraceSwitch's constructor takes a third parameter, which is the file to read the trace level settings. I assume the file name you pass into the constructor has enough information for it to be found. BugslayerTraceSwitch is a part of my Bugslayer assembly, so you just need to include Bugslayer as an import. The file format is very simple, as shown in the following snippet.
; The format is <SwitchName>=<value>
HappyAppyClassSwitch=4
Switcheroo=0
Note that semicolons are treated as comment lines.
      Now that you have an idea of how to use the tracing capabilities in .NET, let's discuss where the output goes. By default, tracing output goes to the attached debugger as well as through the traditional Win32 OutputDebugString call. Keep in mind that .NET is not your traditional Win32-based application, so the debugger output will be different than what you're used to seeing. I will discuss output in greater depth later in the column.
      At the beginning of this section, I mentioned that both the Trace and Debug classes have tracing methods. But which one should you use? I decided to only use Trace for tracing. That way I have one consistent way of doing the tracing and don't have to get into situations where I have to think before typing trace statements. Now that you know how tracing works, let's turn to how assertions work in .NET.

Assertions

      As I mentioned earlier, both the Trace and Debug classes have Assert methods. I only use the Assert from the Debug class. The methods are identical, but I don't want an unexpected message box popping up from the middle of my applications, so I stick with the Debug version. The default assertion message box is shown in Figure 4. Notice that the .NET assertion comes with stack walking (with fill source and line lookup) right out of the box!

Figure 4 Debug Assertion
Figure 4 Debug Assertion

      While C++ ASSERT macros are a little easier to use, the Debug.Assert method isn't too bad in C#; the overloaded Assert methods just take different parameters. The first takes a single Boolean condition; the second takes a Boolean condition and a message string; and the final takes a Boolean condition, a message string, and a detailed message string. Using Assert in .NET means that you have to do a little more typing than with the traditional C++ ASSERT. Since there are no .NET macros, in order to get the assertion string displayed in the assertion message box, you need to pass in the string yourself. The following code snippet shows all three types of C# Asserts in action.
Debug.Assert ( i > 3 ) ;
Debug.Assert ( i > 3 , "i > 3" ) ;
Debug.Assert ( i > 3 , "i > 3" , "This means I got a bad parameter") ;
      Of course, since C# supports the conditional attribute, you simply need to define DEBUG to enable the assertion code. With Visual Basic, you will need to surround each assertion with true conditional compilation, like the following:
#If DEBUG Then
Debug.Assert ( i > 3 )
#End If
      As I mentioned, assertions in .NET appear in a message box if the code is running interactively. I did a little mucking around in .NET and found that you could redirect assertions to a file globally for all applications. You should only use these techniques at your own risk. In addition, never use them on any machine other than the one you're using for development. That said, here are the steps to follow. In HKLM\Software\Microsoft\ComPlus you need to add two DWORD values, NoGuiOnAssert and LogToFile, and one string value, LogFile. Set NoGuiOnAssert to 1 to disable message boxes. Set LogToFile to 1 to turn on logging to a file. Set LogFile to the complete name and path where all assertion output should go. However, before you change global settings, you should know how to control assertion output better with TraceListeners.

TraceListenersâ€"The Listeners are Listening

      Tracing and assertions are unique in .NET because it is fairly easy to control the output. Both the Trace and Debug classes have a member, Listeners, which is an array of TraceListener objects. A TraceListener sends the output for both traces and assertions. As you can imagine, you can have one trace listener for sending output to OutputDebugString and one for sending output to a file. The function of the Trace and Debug classes is to enumerate through the TraceListener classes in the Listeners array and let each handle the output. This allows you to add or subtract the output. The default TraceListener (DefaultTraceListener), through its Write and WriteLine methods, sends tracing output to OutputDebugString and to the Log method of any attached debugger. DefaultTraceListener sends any assertion to message boxes if the user is logged in interactively through its Fail method.
      The BCL comes with some predefined TraceListeners that you can add to the Listeners array for additional output means. The first is the EventLogTraceListener class, which will send output to the specified event log. The second is TextWriterTraceListener, which directs output to a TextWriter or Stream, such as the Console.Out function of FileStream. The following code shows how to add a TextWriterTraceListener to the chain.
Debug.Listeners.Add(new TextWriterTraceListener ( "Trace.Log" ) );

BugslayerTraceListener Usage and Implementation

The ability to replace the trace output at will is an interesting idea. However, from a practical standpoint, I would much rather have one TraceListener. First, I can control it from anywhere in the application. With multiple individual TraceListeners, it's harder to control each one. Second, one TraceListener can handle all the output needs for the whole application. Consequently, I wrote BugslayerTraceListener to make my life easier. It's a complete drop-in replacement for all TraceListeners. Tracing output can go to any combination of placesâ€"the attached debugger, a file, and regular OutputDebugString calls. The assertions can go to all of those places as well as to message boxes and the event log. Adding BugslayerTraceListener to a Debug or Trace object is very simple.
Debug.Listeners.Remove ( "Default" ) ;
BugslayerTraceListener btl = new BugslayerTraceListener ( ) ;
Debug.Listeners.Add ( btl ) ;
The one thing you need to do is remove the DefaultTraceListener so BugslayerTraceListener can control the output.
      If you look at the code for BugslayerTraceListener itself, there isn't much exciting stuff going on there. The interesting part is in BUGSLAYERWIN32.CS (see Figure 5). I needed to make sure that BugslayerTraceListener also checked to see if there was an interactive user before popping up the message box. That required me to call into the Win32 API with special structures. In general, calling the Win32 API from managed code is just confusing. If you need to bring some code forward into .NET, I hope BUGSLAYERWIN32.CS can give you some idea of how to do it.

More New Features

      The compiler and linker for unmanaged Visual C++ look like they will have some interesting new features. With all the discussion of .NET, new features in traditional Visual C++ are getting less attention. With the much improved debugger and the new compiler flags, Visual C++ .NET will be a mandatory upgrade for everyone with an installed C/C++ code base. I learned about these flags by reading the Visual C++ Compiler Reference in the .NET SDK documentation.
      My favorite new flag is CL.EXE's /RTC for runtime error checks. Some of the errors it checks for include local memory overruns and underruns, uninitialized memory access, and data truncation. It's important to remember that these checks happen at runtime. The new /GL (whole program optimization) flag to CL.EXE and /LTCG (link-time code generation) offer a level of program optimization never seen before. The most interesting optimization is cross-module inlining, which is inlining a function in a module even when the function is defined in another module. Another optimization on x86 CPUs is custom calling conventions, which will allow the compiler and linker to pass parameters in registers between function calls. The CL.EXE /GS (generate security check) option will insert code to check for buffer overruns that whip out the return address on the stack. With /GS enabled, any virus or hijack code that attempts to take over your program will pop up a message box and immediately terminate the process.
      The final new flag of interest, /PDBSTRIPPED, is a LINK.EXE flag which will generate a second PDB file with just public symbols and frame pointer optimization (FPO) data. That way you can ship the second PDB file to your customers so you will get complete call stacks and information from Dr. Watson logs out in the field. Overall, there are some excellent new features in Visual C++.NET, so I can't wait to move my existing code over.

Wrap-up

      I hope that this introduction to debugging on .NET will make your development life a little easier. Specifically, BugslayerTraceListener should make your tracing and assertions much easier. When looking at .NET, don't forget to look for new features in old places. And as always, make your job easier by writing great diagnostic code right from the beginning.
      Because I think everyone should own up to their bugs, I have to admit that I had a small bug in the Smooth Working Set utility from my December 2000 column. Embarrassingly enough, I had a reallocation problem in CFileBase::AppendToDataBuffer from SWSFILE.CPP. The updated code is shown in Figure 6. Thanks to Eric Patey and Richard Cooper for reporting the problem.
Tip 41 from Ted Yu: Back in your April 2000 column, you complained that STL's bsearch function didn't return a value. Here's a bsearch function that returns the iterator corresponding to the value found. If the value isn't found, it returns end().
template<class _FI, class _Ty> 
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 ) ; 
}
Tip 42 from Patrick Gautschi: Microsoft has released an interesting set of tools to help track memory leaks called UMDH (user-mode dump heap). Download the tools at http://support.microsoft.com/ support/kb/articles/q268/3/43.asp. Make sure to read the whole Knowledge Base article on how to use them.
John Robbins is a co-founder of Wintellect, a software consulting, education, and development firm that specializes in programming in Windows and COM. He is the author of Debugging Applications (Microsoft Press, 2000). You can contact John at http://www.wintellect.com.

From the February 2001 issue of MSDN Magazine

Page view tracker