Training
Certifications
Books
Special Offers
Community




 
Debugging Applications for Microsoft® .NET and Microsoft Windows®
Author John Robbins (Wintellect Collection)
Pages 848
Disk 1 Companion CD(s)
Level All Levels
Published 03/26/2003
ISBN 9780735615366
Price $59.99
To see this book's discounted price, select a reseller below.
 

More Information

About the Book
Table of Contents
Sample Chapter
Index
Related Series
Related Books
About the Author

Support: Book & CD

Rate this book
Barnes Noble Amazon Quantum Books

 


Chapter 5: Advanced Debugger Usage with Visual Studio .NET



5  Advanced Debugger Usage with Visual Studio .NET

No matter how much great diagnostics code you use and how much planning you do, occasionally you need to run the debugger. As I've mentioned multiple times in this book, the whole key to debugging effectively is to avoid the debugger as much as possible because that's where you waste all your time. Now I know that most of you will be in the debugger to fix your coworkers' code, not your own (since the code you write is undoubtedly perfect). I want to make sure that when you must resort to the debugger, you're able to get the most out of it and fix problems as quickly as possible. This means you'll want to be able to get the most out of the debugger so that you can find and fix problems as fast as possible. In this chapter, I'll talk about how to take advantage of the wonderful Microsoft Visual Studio .NET debugger. If you've been developing for Microsoft platforms for a long time like I have, you can certainly see a marked progression of debugger improvements over the years. In my opinion, Visual Studio .NET is a huge jump in progress and is the state-of-the-art debugging tool. The team has done an outstanding job of combining an extremely easy-to-use user interface (UI) with power to spare for the really hard problems. The fact that Windows developers now have one debugger that handles script, Microsoft Active Server Pages (ASP), Microsoft ASP.NET, .NET, XML Web Services, native code, and SQL debugging in a single debugger UI is amazing.

This is the first of three chapters on the Visual Studio .NET debugger. In this chapter, I'll cover the common advanced ground of .NET and native debugging because so much is similar between the two environments. These features, which include advanced breakpoints, will assist you in solving your coding problems. I'll also provide a slew of tips to help you make the most out of the time you spend in the debugger. In Chapter 6, I'll cover specific issues related to .NET development. In Chapter 7, I'll discuss issues more specific to native code debugging. No matter what type of code you're debugging, you'll find many relevant tips in this chapter.

If you're new to the Visual Studio .NET debugger, I suggest that you read the documentation before continuing. I won't be covering the basics of the debugger in this chapter; I'll assume that you'll study the documentation if you need to. The debugger is discussed in the Visual Studio .NET documentation under Visual Studio .NET\Developing with Visual Studio .NET\Building, Debugging, and Testing or in the MSDN Online documentation under .NET Development\Visual Studio .NET\Product Documentation\Developing with Visual Studio .NET\Building, Debugging, and Testing (http://msdn.microsoft.com/library/en-us/vsintro7/html/vxoriBuildingDebuggingandTesting.asp).

Before you read any further, take note: if you haven't read about your symbol server and set it up as discussed in Chapter 2, you're missing one of the best capabilities of Visual Studio .NET. No matter whether you're developing .NET or native applications, getting perfect symbols automatically loaded means you'll always have a leg up solving your debugging problems.

Advanced Breakpoints and How to Use Them

Setting a breakpoint on a source line in the Visual Studio .NET debugger with Debug or project configuration is simple. Just load the source file, put the cursor on the line you want to stop on, and press the default breakpoint key, F9. Alternatively, you can click in the left margin next to the line. Although the folks coming from Microsoft Visual Basic 6 might not find the setting of margin breakpoints exciting (because Visual Basic has had them for years), C# and C++ developers will see them as a huge improvement. Setting a breakpoint on a source line this way is called setting a location breakpoint. When the code for such a line executes, the debugger will stop at that location. The ease of setting a location breakpoint belies its importance: the location breakpoint on a specific source code line is what separates the modern age of debugging from the debugging dark ages.

In the early days of computing, breakpoints simply didn't exist. Your only "strategy" for finding a bug was to run your program until it crashed and then look for the problem by wading through page after page of hexadecimal core-dump printouts of the state of memory. The only debuggers in the debugging dark ages were trace statements and faith. In the renaissance age of debugging, made possible by the introduction of higher-level languages, developers could set breakpoints but had to debug only at the assembly-language level. The higher-level languages still had no provisions for viewing local variables or seeing a program in source form. As the languages evolved into more sophisticated tools, the debugging modern age began, and developers were able to set a breakpoint on a line of source code and see their variables in a display that interpreted the variables' values into the exact types they specified. This simple location breakpoint is still extremely powerful, and with just it alone, my guess is that you can solve 99.46 percent of your debugging problems.

However wonderful, though, location breakpoints can get tedious very quickly. What would happen if you set the breakpoint on a line inside a for loop that executed from 1 through 10,000, and the bug turned up on the 10,000th iteration? Not only would you wear your index finger down to a nub from pressing the key assigned to the Go command, but you would also spend hours waiting to get to the iteration that produced the bug. Wouldn't it be nice if there were some way to tell the debugger that you want the breakpoint to execute 9,999 times before stopping?

Fortunately, there is a way: welcome to the realm of advanced breakpoints. In essence, advanced breakpoints allow you to program some smarts into breakpoints, letting the debugger handle the menial chores involved in tracking down bugs and minimizing the time and effort you have to spend in the debugger. A couple of conditions you can add with advanced breakpoints are having the breakpoint skip for a certain count and breaking when an expression is true. The advanced breakpoint capabilities have finally moved debuggers solidly into the modern age, allowing developers to do in minutes what used to take hours with simple location breakpoints.

Breakpoint Tips

Before we jump into advanced breakpoints, I just want to quickly mention four things you might not have been aware of when setting breakpoints. The first is that when setting advanced breakpoints, before you set them, it's always best if you start debugging with a single-step command. When you aren't debugging, the debugger uses IntelliSense to set the breakpoints. However, when you start debugging, you also get the actual Program Database (PDB) debugging symbols in addition to the IntelliSense to help set your breakpoints. For all the examples in this and the next two chapters, I started debugging before attempting to set any breakpoints.

One recommendation I have is that you should ensure your Breakpoints window—the view that shows which breakpoints are set—isn't a docked window. Sometimes when you're setting breakpoints, Visual Studio .NET will set them, and you'll wonder why they aren't triggered. By having the Breakpoints window as a full-fledged window, you'll be able to find it among all the other thousands of dockable windows in the Visual Studio .NET IDE. I don't know about you, but running Visual Studio .NET makes me long for a dual 35-inch monitor setup. To view the Breakpoints window, press Ctrl+Alt+B with the default keyboard mappings. Right-click on the Breakpoints window title bar, or tab and uncheck Dockable on the shortcut menu. You'll need to do this for both the normal editing mode as well as the debugging mode. Once you've got the Breakpoint window set as a full-fledged window, click and drag its tab over to the first position so that you can always find it.

The Breakpoints window shows various glyphs that indicate whether the breakpoint was set or not. Table 5-1 shows all the codes you'll see. The Warning glyph, the red dot with a white question mark in it, needs some extra explanation. In your normal debugging, you'll see the Warning glyph when you set a location breakpoint in a source file whose module has not been loaded yet, so the breakpoint is in essence an unresolved breakpoint. For those of you coming from the Microsoft Visual C++ 6 camp, the Additional DLLs dialog box is a thing of the past. Since I recommend that you start debugging before you set advanced breakpoints, if you see the Warning glyph in the Breakpoints window, you have a sign that the breakpoint wasn't set correctly.

Table 5-1  Breakpoint Window Codes

GlyphStateMeaning
EnabledNormal and active breakpoint
DisabledBreakpoint is ignored by the debugger until re-enabled.
ErrorBreakpoint could not be set.
WarningBreakpoint could not be set because location is not loaded. If the debugger has to guess at the breakpoint, it shows the warning glyph.
MappedA breakpoint set in ASP code on an HTML page

Although you might never have realized it, you can set breakpoints in the Call Stack window, except when doing SQL debugging. This capability is extremely helpful when trying to get stopped on recursion or deeply nested stacks. All you need to do is highlight the call you want to stop on and either press F9 or right-click on the line and select Insert Breakpoint from the shortcut menu. Even nicer, just as with margin breakpoints, you can right-click on any breakpoint in the Call Stack window to enable, disable, or set the properties of that breakpoint.

Another much underused feature for setting breakpoints is the Run To Cursor option for setting one-shot breakpoints. You can set them in source edit windows by right-clicking on the line and selecting the Run To Cursor option from the menu, which is available both when debugging and editing, and which will start debugging. For the default keyboard layout, pressing Ctrl+F10 will do the same thing. As with breakpoints, right-clicking in the magical Call Stack window pops up the shortcut menu, which also has a Run To Cursor option. If you hit a breakpoint before execution takes place on the Run To Cursor line, the debugger stops on that breakpoint and discards your Run To Cursor one-shot breakpoint.

Finally, in managed code, you can now set subexpression breakpoints. For example, if you have the following expression when debugging and you click in the margin next to the line, the red highlight extends only on the i = 0 , m = 0 or on the initializer's portion of the expression. If you wanted to stop on the iterator's subexpression (in which the increment and decrement take place), place the cursor anywhere in the i++ , m-- portion of the statement and press F9. In the following statement, you can have up to three breakpoints on the line. You can differentiate them in the Breakpoints window because each will indicate the line and character position. There will be only a single red dot in the margin indicating the breakpoints. To clear all breakpoints at once, click the red dot in the left margin.

for ( i = 0 , m = 0 ; i < 10 ; i++ , m-- )
{
}

Quickly Breaking on Any Function

The starting point for any advanced breakpoint is the Breakpoint dialog box, which is accessible by pressing Ctrl+B in the default keyboard mapping. This dialog box serves double duty as the New Breakpoint dialog box as well as the Breakpoint Properties dialog box. In many ways, the Breakpoint dialog box is simply a front end to the IntelliSense system. IntelliSense is extremely helpful for writing your code, but it's also used to help set breakpoints. There's an option for turning off IntelliSense to help verify breakpoints in the Options dialog box, Debugging folder, General property page, which can speed up mixed mode debugging, but I would highly recommend that you always leave the option Use IntelliSense To Verify Breakpoints checked on your systems. Also, you'll get IntelliSense breakpoints only when you have a project with source code open.

By using IntelliSense, you get a very powerful breakpoint-setting feature that can save you a tremendous amount of time. In the midst of my debugging battles, if I know the name of the class and method I want to break on, I can type it directly into the Breakpoint dialog box, Function tab's Function edit control. I've looked over the shoulder of countless developers who know the name of the method but spend 20 minutes wandering all over the project opening files just so they can move the cursor to the line and press F9. Setting breakpoints like this has some limitations, but they aren't too onerous. The first is that the name is case sensitive if the language is case sensitive, as you would expect. (This is where Visual Basic .NET is especially nice!) Second, in native C++, it's sometimes not possible to set the breakpoint because the actual method name is hidden by a define. Finally, the language selected in the Language drop-down list in the Breakpoint dialog box must be the correct language for the code.

Numerous things can happen when you try to set a breakpoint on a function using the Breakpoint dialog box. I want to go through the possible outcomes you'll see and explain how to work around any problems you might encounter. What you might want to do so that you can see the outcomes yourself is open up one of your projects or a project with this book's sample files and try to set a few breakpoints with the Breakpoint dialog box as I go through this discussion.

The first case of quickly setting a breakpoint that I'll discuss is when you want to set the breakpoint on a class and method that exists. For example, I have a Visual Basic .NET class named MyThreadClass with a method named ThreadFunc. When I pop up the Breakpoint dialog box with Ctrl+B, all I have to type in is mythreadclass.threadfunc (remember Visual Basic .NET is a case-insensitive language) and click OK. For Visual Basic .NET, J#, and C#, you separate the class and the method name with a period. For native C++, you separate the class and the method name with the language-specific double colons (::). Interestingly, for managed C++, you have to prefix the expression with MC:: to get the expression parse correctly. Assuming the same class and method name from the Visual Basic .NET example were in managed C++, you'd have to enter MC::MyThreadClass::ThreadFunc. Figure 5-1 shows the filled-out Breakpoint dialog box using the Visual Basic .NET example. If you aren't currently debugging when you set the breakpoint, the breakpoint dot appears in the margin, but the red highlight doesn't appear on the Public Sub ThreadFunc because the breakpoint still has to be resolved. Once you start debugging, Public Sub ThreadFunc is highlighted in red. If you're debugging native C++, the instruction isn't highlighted. If you're currently debugging, the breakpoint will be fully resolved by showing a filled-in red dot in the Breakpoints window and you'll see that the Public Sub ThreadFunc is highlighted in red. (In C# or native C++, the breakpoint appears inside the function, but it's still the first instruction after the function prolog. Also, just a breakpoint dot appears in the margin.)

Click to view graphic
Click to view graphic

Figure 5-1  Breakpoint dialog box about to set a quick breakpoint on a function

If the class and method name you specify in the Breakpoint dialog is incorrect, you'll see a message box that says this: "IntelliSense could not find the specified location. Do you still want to set the breakpoint?" If you go ahead and set the breakpoint on the nonexistent method and you're doing managed code, you can be pretty certain that when you're debugging, the breakpoint will always have a question mark glyph next to it in the Breakpoints window because the breakpoint can't be resolved. As you'll see in Chapter 7, when we talk about native code debugging, you still have a chance to set the breakpoint correctly.

Because setting a breakpoint by specifying the class and method works well, I had to experiment with attempting to set a breakpoint in the Breakpoint dialog box by just specifying the method name. For C#, J#, Visual Basic .NET, and native C++, if the method name is unique in the application, the Breakpoint dialog box simply sets the breakpoint as if you typed in the complete class and method. Unfortunately, managed C++ doesn't seem to support setting breakpoints with just the method name.

Since setting a breakpoint with just the method name works quite well and will save you time debugging, you might be wondering what would happen if you had a large project and you wanted to set a breakpoint on a common method or an overloaded method. For example, suppose you have the WDBG MFC project from Chapter 4 open and you want to set a breakpoint on the OnOK method, which is the default method for handling the OK button click. If you enter OnOK in the Breakpoint dialog box and click OK, something wonderfully interesting happens and is shown in Figure 5-2.

Click to view graphic
Click to view graphic

Figure 5-2  The Choose Breakpoints dialog box

What you see in Figure 5-2 is the IntelliSense listing from all classes in the WDBG project that have OnOK as a method. I don't know about you, but I think this is an outstanding feature—especially because you can see that clicking the All button allows you to set breakpoints on all those methods in one fell swoop, and it works for all languages supported by Visual Studio .NET except managed C++! The Choose Breakpoints dialog box also displays for overloaded methods in the same class. I don't know how many times I've gone to set a breakpoint and the Choose Breakpoints dialog box reminded me that I should consider stopping on other methods as well.

Another of my favorite things to do in C++ code is to type just the class name in the Breakpoint dialog box. The wonderful Choose Breakpoints dialog box pops up and offers every method in the class! Sadly, this killer feature works only on C++ code (and amazingly in managed C++ as well) and not on C#, J#, or Visual Basic .NET. But for C++, this incredible feature is a boon for testing. If you want to ensure your test cases are hitting every method in a class, simply type the class name in the Breakpoint dialog box and click All in the resulting Choose Breakpoints dialog box. This instantly sets breakpoints on every method so that you can start seeing whether your test cases are calling every method. This feature is also great for looking for dead code because if you set all the breakpoints and clear them as you hit them, any remaining breakpoints are code that's never executed.

At the time I wrote this book, I was using Visual Studio .NET 2003 RC2. When I was trying to figure out if C# and J# could set class breakpoints like native C++, I ran into a huge bug that will crash Visual Studio .NET. I hope this bug will be fixed in the final release; but if it's not, I want to make sure you know about it. For C# or J# projects, if you type in the class name followed by a period in the Breakpoint dialog box, for example, Form1. and click the OK button, you'll get a Visual C++ run time error message box telling you the application has attempted to terminate in an unusual way. When you click the OK button, the IDE will simply disappear. Attempting the same type of breakpoint in a Visual Basic .NET application does not close the IDE. Although this is a very nasty bug, at least only a few of you should run into it.

I'm simply amazed that the ability to select from multiple methods in your application when setting a breakpoint in the debugger isn't pointed out in big, bold type as a killer feature of Visual Studio .NET. In fact, there's only a passing reference to the Choose Breakpoints dialog box in the MSDN documentation. However, I'm very happy to toot the debugger team's horn for them.

As you can probably guess by this point, you don't have to go through the Choose Breakpoints dialog box if you know the class and method because you can type them directly in. Of course, you have to use the appropriate class and method separator based on the language. If you want to be extremely specific, you can even specify the parameter types to the overloaded method for Visual Basic .NET and C++, keeping in mind the particular syntactic requirements of the languages. Interestingly, C# and J# don't look at parameters and always pop up the Choose Breakpoints dialog box, which is probably a bug.

If you're debugging when you want to set a quick breakpoint, things get a little more interesting, especially in managed code. For native code, I'll save the discussion for Chapter 7 because when you're debugging, there's quite a bit more you have to manually check when setting breakpoints. For managed code, I noticed some very interesting power in the Breakpoint dialog box. During a brain cramp one day while debugging, I typed in the name of a class and method in the Microsoft .NET Framework class library instead of the class and method from my project, and a whole bunch of really weird-looking breakpoints popped up in the Breakpoints window. Let's say I have a console-managed application that calls Console.WriteLine, and while debugging, you type Console.WriteLine into the Breakpoint dialog box. You'll get the usual message about IntelliSense not knowing what to do. If you click Yes and go to your Breakpoints window, you'll see something that looks like Figure 5-3. (You might have to expand the top tree node.)

Click to view graphic
Click to view graphic

Figure 5-3  Child breakpoints in the Breakpoints window

What you're looking at in Figure 5-3 are child breakpoints. Basically, the Visual Studio .NET documentation says that they exist and that's it. For example, the documentation says child breakpoints occur when you set breakpoints on overloaded functions, but the Breakpoints window always shows them as top-level breakpoints. You can see child breakpoints when you're debugging multiple executables and both programs load the same control into their AppDomain/address spaces, and you set breakpoints in the same spot in that control in both programs. What's wild is that the Breakpoints window in Figure 5-3 is showing you a single program that's currently running in which I set a breakpoint on Console.WriteLine.

If you right-click on a child breakpoint while debugging and select Go To Disassembly, the Disassembly window displays the disassembly code. However, you'll get a clue as to what's happening if you right-click on a child breakpoint, select Properties from the shortcut menu, and in the resulting Breakpoint dialog box, click on the Address tab, shown in Figure 5-4. You'll see that the debugger reports that the breakpoint is set on System.Console.WriteLine at the very start of the function.

If that's not clear enough, you can always execute your program and notice that you stop deep down in the x86 assembly language soup. If you pull up the Call Stack window, you'll see that you're stopped inside a call to Console.WriteLine and you'll even see the parameter(s) passed. The beauty of this undocumented means of handling a breakpoint is that you'll always be able to get your applications stopped at a known point of execution.

Click to view graphic
Click to view graphic

Figure 5-4  Breakpoint on any call to Console.WriteLine

Although I made only a single call to Console.WriteLine in my program, the Breakpoints window shows 19 child breakpoints, as shown in Figure 5-3. Based on some trial and error, I discovered that the child breakpoints count is related to the number of overloaded methods. In other words, setting breakpoints by typing in the .NET Framework class and method name, or typing where you don't have source code, will set a breakpoint on all overloaded methods. In case you check the documentation and see that Console.WriteLine only has 18 overloaded methods, let me tell you that if you look at the System.Console class with ILDASM, you'll see that there are really 19 overloaded methods.

The ability to stop on a method that's called anywhere in my AppDomain is amazingly cool. I've learned quite a bit about how the .NET Framework class library fits together by choosing to stop at specific .NET Framework class library methods. Although I've been using a static method as an example, this technique works perfectly well on instance methods as well as properties as long as those methods or properties are called in your AppDomain. Keep in mind that to set breakpoints on a property, you'll need to prefix the property with get_ or set_, depending on what you want to break on. For example, to set a breakpoint on the Console.In property, you'd specify Console.get_In. Also, the language selected in the Breakpoint dialog box is still important. When setting these AppDomain-wide breakpoints in places where you don't have source code, I like to use Basic so that if I mess up the case, the breakpoint is still set.

There are two issues you need to be aware of when setting these AppDomain-wide breakpoints. The first is that breakpoints set this way aren't saved across executions. Consider the example in which we added a breakpoint while debugging on Console.WriteLine. The Breakpoints window will show "Console.WriteLine" when you re-execute the program, but the breakpoint changes to a question mark glyph and the breakpoint changes to unresolved and will never be set. Second, you can set these AppDomain-wide breakpoints only when the instruction pointer is sitting in code in which you have a PDB file. If you try to set the breakpoint in the Disassembly window with no source code available, the breakpoint will be set only as unresolved and never activated.

Although you might think I've beaten the topic of setting quick location breakpoints to death, there's still one completely non-obvious but extremely powerful place to set location breakpoints. Figure 5-5 shows the Find combo box on the toolbar. If you type the name of the class and method you want to break on in that combo box and press F9, a breakpoint on that method is set if that method exists. If you specify an overloaded method, the system will automatically set breakpoints on all the overloaded methods. The Find combo box is a little more discriminating in that if it can't set the breakpoint, it won't. Additionally, like the Breakpoint dialog box, if you're working on a C++ project, specifying the class name in the Find combo box and pressing F9 will set a breakpoint on each method in that class.

Click to view graphic
Click to view graphic

Figure 5-5  The Find combo box

That little Find combo box has two other hidden secrets as well. For the default keyboard layout, if you type in the name of a project file or an include file in the INCLUDE environment variable and press Ctrl+Shift+G, the Find combo box opens the file ready for editing. Finally, if you like the Command window in Visual Studio .NET, try this: in the Find combo box, enter the right arrow key symbol (>), and see the window turn into a mini Command window with its own IntelliSense. With all this undocumented magic in the Find combo box, I often wonder if I could type in "Fix my bugs!" and with a magic keystroke have it do just that.

Location Breakpoint Modifiers

Now that you know how to set location breakpoints anywhere with aplomb, I can turn to some of the scenarios discussed in the opening section on breakpoints. The whole idea is to add some real smarts to breakpoints so that you can use the debugger even more efficiently. The vehicles for these smarts are hit counts and conditional expressions.

Hit Counts

The simplest modifier applicable to location breakpoints is a hit count, also sometimes referred to as a skip count. A hit count tells the debugger that it should put the breakpoint in but not stop on it until the line of code executes a specific number of times. With this modifier, breaking inside loops at the appropriate time is trivial.

Adding a hit count to a location breakpoint is easy. First, set a regular location breakpoint either on a line or a subexpression of the line. For managed code, right-click on the red area of the line for the location breakpoint, and select Breakpoint Properties from the shortcut menu. For native code, right-click on the red dot in the left margin. Alternatively, you could also select the breakpoint in the Breakpoints window and click the Properties button, or right-click it in the window and select Properties. No matter how you do it, you'll end up in the Breakpoint dialog box, where you click the Hit Count button.

In the resulting Breakpoint Hit Count dialog box, you'll see that Microsoft improved the hit-count options from previous debuggers. In the When The Breakpoint Is Hit drop-down list, you can choose how you want the hit count calculated four different ways, as shown in Table 5-2. After choosing the evaluation you want, type the hit-count number in the edit box next to the drop-down list.

Table 5-2  Hit-Count Evaluations

Hit-Count EvaluationDescription
Break alwaysStop every time this location is executed.
Break when the hit count is equal toOnly stop when the exact number of executions of this location has occurred. Note that the count is a 1-based count.
Break when the hit count is a multiple ofBreak every x number of executions.
Break when the hit count is greater than or equal toSkip all executions of this location until the hit count is reached and break every execution thereafter. This was the only hit count in previous editions of Microsoft debuggers.

What makes hit counts so useful is that when you're stopped in the debugger, the debugger tells you how many times the breakpoint has executed. If you have a loop that's crashing or corrupting data but you don't know which iteration is causing the problem, add a location breakpoint to a line in the loop and add a hit-count modifier that is larger than the total number of loop iterations. When your program crashes or the corruption occurs, bring up the Breakpoints window and, in the Hit Count column for that breakpoint, you'll see the number times that loop executed in parentheses. The Breakpoints window shown in Figure 5-6 displays the remaining hit count after a data corruption. For native code, keep in mind that the remaining count works only when your program is running at full speed. Single-stepping over a breakpoint doesn't update the hit count.

Click to view graphic
Click to view graphic

Figure 5-6  An example of remaining hit count breakpoint expressions

A new feature of hit counts is that you can reset the count back to zero at any time. Jump back into the Breakpoint dialog box, click the Hit Count button, and click the Reset Hit Count button in the Breakpoint Hit Count dialog box. One additional nice feature is that you can change the hit-count evaluation at any time or the hit-count number without changing the current hit count.

Conditional Expressions

The second modifier for location breakpoints—and the one that if used correctly saves you more time than any other type of breakpoint—is a conditional expression. A location breakpoint that has a conditional expression triggers only if its expression evaluates to true or changes from the last time it was evaluated. A conditional expression is a powerful weapon for gaining control exactly when you need it. The debugger can handle just about any expression you throw at it. To add a conditional expression to your breakpoint, open the Breakpoint dialog box for a location breakpoint and click the Condition button, which brings up the Breakpoint Condition dialog box.

In the Condition edit box, enter the condition you want to check and click OK. Both managed and native code have sufficiently varying support for conditions, each with their own set of "gotchas" that I'll have to discuss in their respective upcoming chapters. In a nutshell, the differences are that in managed code, you can call methods and functions from the conditional expressions (be very, very careful!) and have no support for pseudo registers/values (the special codes that begin with the @ or $ symbol). For native code, you can't call functions from your conditional expressions, but you do have access to the pseudo registers/values.

However, both environments support general expressions that you can think of like this: "What's in the parentheses of an if statement that I'd enter on the breakpoint line?" You have full access to local and global variables because they are evaluated in the context of the currently executing scope. The conditional expression breakpoint modifier is extremely powerful because it allows you to directly test the specific hypothesis that you're out to prove or disprove. The important point to remember is that the expression syntax must follow the same rules as the language in which the breakpoint is set, so remember to mind your And's and ||'s as appropriate.

The default condition handling breaks into the debugger if the conditional expression you enter evaluates to true. However, if you want to break when the condition changes value, you can change the option button selection from Is True to Has Changed. One thing that can be confusing, but makes sense after some thought, is that the expression isn't evaluated when you enter the condition. The first time it's evaluated is when the associated location breakpoint is hit. Since the condition has never been evaluated, the debugger sees no value stored in the internal expression result field, so it saves off the value and continues execution. Thus it can possibly take two executions of that location before the debugger stops.

What's extra nice about the Has Changed evaluation is that the condition you enter doesn't have to be an actual condition. You can also enter a variable that is accessible from the scope of the location breakpoint, and the debugger evaluates and stores the result of that variable so that you can stop when it changes. Make sure to read the breakpoint discussions in both Chapter 6 and Chapter 7 to learn about the managed and native code twists on conditional breakpoint modifiers. Additionally, native debugging offers other types of breakpoints to make debugging easier. I do want to answer the question on the tip of your tongue (thus ruining the suspense): managed debugging does not support global breakpoints.

Multiple Breakpoints on a Single Line

One of the most common questions I'm asked when it comes to setting breakpoints is about setting multiple breakpoints on a single line. Prior to Visual Studio .NET, it wasn't possible, but now you can. However, you won't use multiple breakpoints on a line every day because they're not that easy to set. You'll also be using only conditional expression location breakpoint modifiers on each of the breakpoints. As you can imagine, setting two regular stop-always location breakpoints on a line isn't going to buy you very much!

After setting the first breakpoint using normal methods, the trick to setting the second one is to use the Breakpoint dialog box to do the magic. The easiest way to show you how to set multiple breakpoints is through an example. The following code snippet shows the line number in comments before each line.

/* Source file: CSharpBPExamples.cs. */
/*84*/    for ( i = 0 , m = 0 ; i < 10 ; i++ , m-- )
/*85*/    {
/*86*/        Console.WriteLine ( "i = {0} m = {0}" , i , m ) ;
/*87*/    }

I set the first breakpoint on line 86 by right-clicking in the margin and setting the conditional expression to i==3. To set the second breakpoint, shift to the Breakpoints window so that you can see the line number for the original breakpoint and bring up the Breakpoint dialog box by pressing Ctrl+B. Click the File tab in the Breakpoint dialog box because that's where you need to set the breakpoint. Type the file name into the File edit box, enter the line number in the Line edit box, and leave the Character edit box set to 1. Using this example, I use CSharpBPExamples.CS as the file and 86 for the line. Finally, click the Condition button and enter the condition you want for the second breakpoint. In my case, I'll enter m==-1.

The Breakpoints window shows two breakpoints set on the same line, and the icon glyph on the left-hand side shows full red circles, indicating that they are both active. When you start debugging, you'll see that the program stops on both breakpoints when the appropriate conditions are met. To check which condition is causing the breakpoint, look at the Breakpoints window to see which one is bold. Interestingly, right-clicking on the breakpoint in the source window and selecting Breakpoint Properties from the shortcut menu always displays the properties of the first breakpoint set for the line. Additionally, if you click on the margin dot, both breakpoints are cleared. While the example I showed you here was a little contrived—I would have combined both conditions with an or (||) operator—multiple breakpoints on a line come in very handy when you need to look at two different complicated expressions.

The Watch Window

If I had to give an Oscar for technical achievement and overall usefulness in Visual Studio .NET, the Watch window would win hands down. One idea that companies creating development tools for other environments and operating systems haven't figured out at all is that if developers can easily develop and debug their applications, they'll more likely flock to that environment or platform. The incredible power offered by the Watch window and its related cousins, the QuickWatch dialog box, Autos window, Locals window, and This/Me window, are what make the difference between floundering all day looking for an answer and quickly solving a bug.

I want to make sure to point out that you can use any of the related Watch windows to change a variable's value. Unfortunately, many developers coming over from other environments, and a few who have been doing Windows development for many years, aren't aware of the capabilities. Let's take the Autos window as an example. You just select the variable or the child variable you want to change, and click once on the value field for that variable. Simply type in the new value and you've changed the variable.

Many developers treat the Watch window as a read-only place in which they drop their variables and watch. What makes the Watch window exciting is that it has a complete expression evaluator built in. If you want to see something as an integer that's not an integer, simply cast or convert it in the same way you would if you were programming in the currently active programming language. Here's a simple example: Suppose CurrVal is declared as an integer and you want to see what it evaluates to as a Boolean. In the Name column of the Watch window, in C# and C++, enter (bool)CurrVal, or for Visual Basic .NET, enter CBool(CurrVal). The value is displayed as true or false as appropriate.

Changing an integer to a Boolean might not be that exciting, but the ability of the Watch window to evaluate expressions gives you the ultimate code-testing trick. As I've said several times in this book, code coverage is one of the goals you need to strive for when doing your unit testing. If, for example, you have a conditional expression inside a function and it's difficult to see at a glance what it evaluates to so that you can step into the true or false branch as appropriate, the Watch window becomes your savior. Since there's a full expression evaluator built in, you can simply drag the expression in question down to the Watch window and see what it evaluates to. Granted, there are some restrictions. If your expression calls all sorts of functions instead of using variables, you could be in trouble. If you look at my code, I follow the rule in which if I have three or more subexpressions in a conditional, I use only variables just so that I can see the result of the expression in the Watch window. Some of you might be truth table savants and be able to see how those expressions evaluate off the top of your heads, but I certainly am not one.

To make this clearer, let's use the next expression as an example. You can highlight everything inside the outer parentheses and drag it to the Watch window. However, since it's on three lines, the Watch window interprets it as three separate lines in its display. I still do that so that I can copy and paste the two lines into the first one and thus build my final expression without too much typing. Once the expression is entered on one line in the Watch window, the value column is either true or false, depending on the values in the variables.

if ( ( eRunning == m_eState    ) ||
     ( eException == m_eState  ) &&
     ( TRUE == m_bSeenLoaderBP )   )

The next step is to put each of the variables that make up a part of the expression in their own entries in the Watch window. The really cool part is that you can start changing the values of the individual variables and see that the full expression on the first line automatically changes based on the changing subexpressions. I absolutely love this feature because it not only helps with code coverage, but it also helps you see the data coverage you need to generate.

Calling Methods in the Watch Window

Something I've found relatively amusing about some developers who have moved to Windows development from those UNIX operating systems is that they insist UNIX is better. When I ask why, they indignantly respond in their suspender-wearing glory, "In GDB you can call a function in the debuggee from the debugger!" I was amazed to learn that operating system evaluations revolved around an arcane debugger feature. Of course, those suspenders snap pretty quickly when I tell them that we've been able to call functions from Microsoft debuggers for years. You might wonder what's so desirable about that. If you think like a debugging guru, however, you'll realize that being able to execute a function within the debugger allows you to fully customize your debugging environment. For example, instead of spending 10 minutes looking at 10 different data structures to ensure data coherency, you can write a function that verifies the data and then call it when you need it most—when your application is stopped in the debugger.

Let me give you two examples of places I've written methods that I called only from the Watch window. The first example is when I had a data structure that was expandable in the Watch window, but to see all of it I would have been expanding the little pluses all the way past the Canadian border and up into the North Pole area. By having the debugger-only method, I could more easily see the complete data structure. The second example was when I inherited some code that (don't laugh) had nodes that were shared between a linked list and a binary tree. The code was fragile, and I had to be doubly sure I didn't screw up anything. By having the debugger-only method, I was in essence able to have an assertion function I could use at will.

What's interesting in managed applications is that any time you view an object property in the Watch window, the getter accessor is called. You can easily verify this by putting the following property in a class, starting to debug, switching to the This/Me window, and expanding the this/Me value for the object. You'll see that the name returned is the name of the AppDomain the property is part of.

Public ReadOnly Property WhereAmICalled() As String
    Get
        Return AppDomain.CurrentDomain.FriendlyName
    End Get
End Property

Of course, if you have an object property that copies a 3-gigabyte database, the automatic property evaluation could be a problem. Fortunately, Visual Studio .NET allows you to turn off the property evaluation in the Options dialog box, Debugging folder, General property page, Allow Property Evaluation In Variables Windows check box. What's even better is that you can turn this property evaluation on and off on the fly and the debugger immediately responds to the change. You can instantly tell when property evaluation is disabled because the Watch window family says this in the Value field: "Function evaluation is disabled in debugger windows. Check your settings in Tools.Options.Debugging.General."

Native code is a bit different in that you have to tell the Watch window to call the method. Please note that I'm using the generic term "method" here. In actuality, for native code, you can reliably call only C functions or static C++ methods. As regular native C++ methods need the this pointer, which you might or might not have depending on the context. Managed methods are a little more this pointer friendly. If you're stopped in the class that contains the method to call, the Watch window automatically assumes the active this pointer is the one to use.

As you've seen, calling managed code properties is trivial. Calling methods in managed or native code is just like calling them from your code. If the method doesn't take any parameters, simply type the method name and add the open and close parameters. For example, if your debugging method is MyDataCheck ( ), you'd call it in the Watch window with MyDataCheck ( ). If your debugging method takes parameters, just pass them as if you're calling the function normally. If your debugging method returns a value, the Value column in the Watch window displays the return value.

A common problem when calling native functions from the Watch window is ensuring that they are valid. To check a function, type the function name into the Watch window and don't add any quotes or parameters. The Watch window will report the type and address of the function in the Value column if it can be found. Additionally, if you'd like to specify advanced breakpoint syntax (which I'll discuss in detail in Chapter 7) to the function, to narrow down scope, you can do that as well.

A few rules apply to both managed and native code when calling methods from the Watch window. The first rule is that the method should execute in less than 20 seconds because the debugger UI stops responding until the method finishes. After 20 seconds, managed code shows "Evaluation of expression or statement stopped," "error: function '<method name>' evaluation timed out," or "Error: cannot obtain value," and native code shows "CXX001: Error: error attempting to execute user function." The good news is that your threads will continue to execute. That's wonderful news because for calling native methods in Visual Studio 6 that timed out, the debugger just killed the currently executing thread. The other good news compared to previous editions of Visual Studio is that if you want to, you can leave called methods in the Watch window in multithreaded programs. Previous versions killed any thread that happened to become active in the place you originally executed your debug method in another thread. The final rule is common sense: only read memory to do data validations. If you think debugging a program that changes behavior because of a side effect in an assertion is tough, wait until you mess with something on the fly in the Watch window. Additionally, if you do some sort of output, make sure to stick with just the trace method of choice for the environment.

What I've covered here about the Watch window is what .NET and native debugging have in common. You can also expand your own types automatically in the Watch window, but there's quite a difference between how it's done in managed vs. native code. Additionally, native debugging offers all sorts of other options for data formatting and control. To learn more, make sure to read in Chapter 6 and Chapter 7 the individual discussions of the Watch window.

The Set Next Statement Command

One of the coolest hidden features in the Visual Studio .NET debugger is the Set Next Statement command. It is accessible in both source windows and the Disassembly window on the shortcut menu, but only when you're debugging. What the Set Next Statement command lets you do is change the instruction pointer to a different place in the program. Changing what the program executes is a fantastic debugging technique when you're trying to track down a bug or when you're unit testing and want to test your error handlers.

A perfect example of when to use Set Next Statement is manually filling a data structure. You single-step over the method that does the data insertion, change any values passed to the function, and use Set Next Statement to force the execution of that call again. Thus, you fill the data structure by changing the execution code.

I guess I should mention that changing the instruction pointer can easily crash your program if you're not extremely careful. If you're running in a debug build, you can use Set Next Statement without much trouble in the source windows. For native optimized builds in particular, your safest bet is to use Set Next Statement only in the Disassembly window. The compiler will move code around so that source lines might not execute linearly. In addition, you need to be aware if your code is creating temporary variables on the stack when you use Set Next Statement. In Chapter 7, I'll cover this last situation in more detail.

If I'm looking for a bug and my hypothesis is that said bug might be in a certain code path, I set a breakpoint in the debugger before the offending function or functions. I check the data and the parameters going into the functions, and I single-step over the functions. If the problem isn't duplicated, I use the Set Next Statement command to set the execution point back to the breakpoint and change the data going into the functions. This tactic allows me to test several hypotheses in one debugging session, thus saving time in the end. As you can imagine, you can't use this technique in all cases because once you execute some code in your program, executing it again can destroy the state. Set Next Statement works best on code that doesn't change the state too much.

As I mentioned earlier, the Set Next Statement command comes in handy during unit testing. For example, Set Next Statement is useful when you want to test error handlers. Say that you have an if statement and you want to test what happens when the condition fails. All you need to do is let the condition execute and use Set Next Statement to move the execution point down to the failure case. In addition to Set Next Statement, the Run To Cursor menu option, also available on the right-click shortcut menu in a source code window when debugging, allows you to set a one-shot breakpoint. I also use Run To Cursor quite a bit in testing.

Filling data structures, especially lists and arrays, is another excellent use of Set Next Statement when you're testing or debugging. If you have some code that fills a data structure and adds the data structure to a linked list, you can use Set Next Statement to add some additional items to the linked list so that you can see how your code handles those cases. This use of Set Next Statement is especially handy when you need to set up hard-to-duplicate data conditions when you're debugging.

Summary

Visual Studio .NET debugging is the state-of-the-art debugger on the market today. Microsoft listened to developers and produced a debugger that makes some extremely difficult debugging problems much easier to debug. This chapter introduced the common breakpoint features across managed and native code. As you've seen, the debugger can do a considerable amount of work for you if you know how to utilize it effectively. You should strive to make the most of the Visual Studio .NET debugger so that you can minimize the time you spend in it.

Advanced breakpoints help you avoid tedious debugging sessions by allowing you to specify the exact conditions under which a breakpoint triggers. While both managed and native code have special breakpoint features, the location breakpoint modifiers, hit counts, and condition expressions are your best friends, mainly because the breakpoint modifiers will save you a huge amount of time since they'll allow you to use the debugger more efficiently. I strongly encourage you to play with them a bit so that you can see what you can and can't do, thus avoiding having to learn their idiosyncrasies under extreme pressure.



Last Updated: March 11, 2003
Top of Page