3.2 Controlling Execution

   

For the debugger to do its job well, it must make as few changes as possible to the operation of the program, so simply attaching Visual Studio .NET's debugger does not have much immediate effect. In order to examine a program's state and behavior, you must suspend its execution, so you will need to give VS.NET the criteria under which it should freeze the application and show you what is going on.

You can control program execution in three ways with the debugger. Breakpoints enable you to bring the program to a halt on selected lines of code. You can configure the debugger to suspend execution when particular error conditions occur. And once the program has been brought to a halt, you can exercise fine control by single-stepping through the code.

3.2.1 Breakpoints

As you would expect, Visual Studio .NET allows you to set breakpointsrequests to suspend the program when it reaches certain lines of code. You can set a breakpoint by placing the cursor on the line at which you want execution to stop and pressing F9. F9 will toggle the breakpointif the line already has a breakpoint set, F9 will remove it. (You can also toggle breakpoints by clicking in the gray column at the left of the editor.) Visual Studio .NET indicates that a breakpoint has been set by placing a red circle to the left of the line, as Figure 3-6 shows. It can also optionally color the line's backgroundyou can configure this with the Options dialog. (Use Tools Options, and select the Fonts and Colors properties in the Environment category.)

Figure 3-6. A breakpoint
figs/mvs_0306.gif

Breakpoints have an effect only when the debugger is attachedif you run a program outside of the debugger, it will not stop at a breakpoint.

Old-style compiled-in breakpoints that work under any circumstances are still available if you need them. With .NET applications, you simply call the Break method of the System.Diagnostics.Debugger class. In classic C++ applications, you can either compile in an _ _asm int 3 or call the DebugBreak API. When a debugger is attached, all of these techniques have the same effect as hitting a breakpoint. If a debugger is not attached, the just-in-time debugging process described earlier will begin, allowing you to attach a debugger.

Sometimes, specifying the line at which to stop is not enoughit is not unusual to need to stop at a line that is executed many thousands of times but that you want to debug only under certain circumstances. In this case, you will need to be a little more selective. Instead of using F9 to set a breakpoint, you can use Ctrl-B, which will display the window shown in Figure 3-7.

Figure 3-7. Setting a selective breakpoint
figs/mvs_0307.gif

As you would expect, the dialog indicates the location of the breakpoint. The File tab shown here allows the location to be specified as a particular line in a file. (Breakpoints set using F9 work this way.) The Function tab allows you to set a breakpoint on a function by name . Figure 3-8 shows how to use this to trap all calls to a particular .NET system API. (This technique relies on having symbolic information for the function being trapped. This means that it doesn't work on system APIs in unmanaged applications unless you have installed the debug symbolsto trap such calls without system debug symbols installed, you will need to use the Address tab.)

If you use the Function tab to set a breakpoint on a .NET API, Visual Studio .NET will give you two warnings. When you set the breakpoint, it will indicate that it has not recognized the function name. This is because the function is not defined in your project. The second warning will be at runtime, when you hit the breakpoint: it will tell you that it has no source code for the relevant location.

Both of these warnings are unavoidable, because Microsoft does not supply the source for the .NET Framework Class Libraries. So you cannot use this technique to step through the system libraries, but it can still be useful to halt when a call to particular a system function occurs.

Figure 3-8. Setting a breakpoint by function name
figs/mvs_0308.gif

The third tab, Address, allows you to set a breakpoint based on the address of a specific instruction. This is available only with Native Win32 debuggingwith managed code (CLR programs), JIT compilation means that methods can be relocated dynamically, which makes address-based breakpoints useless. (The fields on this tab will be grayed out when working with .NET applications.) The fourth tab, Data, lets you specify location-independent breakpoints that fire only when certain data items are accessed. Data breakpoints are also available only with native debugging.

Regardless of which tab you use to specify a breakpoint's location, the bottom half of the dialog will always show the same two buttons : Condition... and Hit Count... These allow you to narrow down the conditions under which the breakpoint will suspend the program.

The Hit Count... button displays the dialog shown in Figure 3-9. The drop-down listbox provides four options. Break Always, the default, disables hit counting. "Break when hit count is equal to" causes the breakpoint to be ignored except when it is hit for the N th time, with N the number specified in the text box. This can be particularly useful when tracking down memory leaks in C++ applicationssee the sidebar. You can also specify "Break when the hit count is greater than or equal to," which is useful in situations in which code operates correctly at first but malfunctions after several executions. Finally, you can specify that the breakpoint should "Break when the hit count is a multiple of" the specified figure, which can be useful if you only want to examine occasional calls to suspect code. The Reset Hit Count button lets you reset Visual Studio .NET's record of the number of times that this breakpoint has been hit so far.

Figure 3-9. Specifying a hit count for a breakpoint
figs/mvs_0309.gif

Finding Memory Leaks in C++

The C++ runtime library is able to report leaked heap blocks. Simply add the following lines to your project's stdafx.h file:

 #define CRTDBG_MAP_ALLOC #include <stdlib.h> #include <crtdbg.h> 

With this in place, call the _CrtDumpMemoryLeaks function at program exit. (Applications created with the MFC Wizard will do this automatically.) This will scan the heap looking for unfreed blocks, reporting everything it finds to the debugger's Output window. The report includes the allocation number (i.e., the number of times that the heap allocation method had been called when that block was allocated). For example, the following output shows that the fiftieth block of memory to be allocated was 5 bytes long and was never freed:

 Detected memory leaks! Dumping objects -> {50} normal block at 0x00323AE8, 5 bytes long.  Data: <     > CD CD CD CD CD Object dump complete. 

If you can reproduce a memory leak in such a way that the allocation number is the same every time you run the program, it is easy to locate the source of the leak. Just set a breakpoint on the library's memory allocation method ( _heap_alloc_dbg , in the dbgheap.c file) and set its hit count to be whatever the offending allocation number is (50 in this case). If you choose the "Break when hit count is equal to" option in the Breakpoint Hit Count dialog (as shown in Figure 3-9), the debugger will ignore the first 49 heap allocations but then stop when the offending allocation occurs. You can then simply look at the call stack to find the line of code that allocated the leaked block.

The Condition... button of the Breakpoint Properties dialog in Figure 3-7 provides another way of being selective about when the breakpoint will halt the program. If you click this button, the dialog shown in Figure 3-10 will appear.

Figure 3-10. Setting a conditional breakpoint
figs/mvs_0310.gif

This dialog allows you to specify an expression that will be evaluated when the breakpoint is hit. (It will be evaluated at the scope of the breakpoint, so you may use local variables and method parameters in the expression. You can even call methods in the expression.) You can use the expression in two ways. You can choose to halt execution only if the expression is true. Alternatively, you can halt only if the expression is different from what it was last time the breakpoint was hit.

Choosing to halt when an expression is true can be very useful when particular function may be called extremely frequently but you want to debug only a small subset of the calls. Consider some code in a Windows application that is responsible for repainting the window. Redraw code is often particularly awkward to debug with normal breakpoints because the act of hitting a breakpoint will bring the debugger to the front. This obscures the window of the application being debugged , so when you let the program continue, its redraw code will run again, at which point it will, of course, hit the breakpoint again. While this issue can often be solved by using a hit count to stop in the debugger only every other redraw, the fact that repaint code is often called tens of times a second makes them a frequent candidate for a more selective breakpoint.

For example, suppose you notice that your window's appearance is wrong whenever the window is square, but correct otherwise . (Certain drawing algorithms have an edge case for perfectly square drawing areas that is easy to get wrong, so this is a fairly common scenario.) Conditional breakpoints can make it easy to catch the one case you are interested in and single-step through that. You can just put a breakpoint on the first line of the redraw handler and set an appropriate condition. For example, in a Windows Forms application, you could use this expression: DisplayRectangle.Width==DisplayRectangle.Height .

In order to use a conditional breakpoint, the inputs you require for the expression must be in scope. So for an MFC application you would be able to use this trick only if the window width and height had already been retrievedunlike Windows Forms, MFC does not make these values available directly through class properties. Figure 3-11 shows an example program in which the width and height have been read into local variables, and a suitable conditional breakpoint has been set.

Figure 3-11. Conditional redraw breakpoint in an MFC application
figs/mvs_0311.gif

Conditional breakpoints don't enable you to do anything that couldn't be done by modifying the code being debugged and setting normal breakpoints. Obviously, it is best not to change the target if at all possible, since such modifications may change the behavior. Conditional breakpoints are therefore very useful because they allow you to be selective without touching the code. However, if you find that you cannot set a breakpoint for the exact set of conditions you need (because the relevant information is not in scope), remember that you always have the fallback position of compiling the test you require into the target instead.

3.2.1.1 Data breakpoints

The New Breakpoint window shown in Figure 3-7 has a fourth tab, Data, which allows you to set a kind of breakpoint that is different from all the others. Data breakpoints are not associated with any particular line of code. With a data breakpoint, you simply specify the name of a variable, and the debugger will halt if that variable changes, regardless of which line of code made the change. This can be very useful for tracking down bugs when a value has changed but you do not know when or why the change occurred.

Data breakpoints are not supported in .NET programs. They are available only in native code.

Figure 3-12 shows the tab for setting a data breakpoint. The variable name must be a global variable. If it is a pointer variable and points to an array, you can use the Items field to specify the number of array elements that the debugger will monitor. The Context field allows you to specify the lexical scope in which the variable name should be evaluatedthis is useful when the expression is otherwise ambiguous. This field takes strings of the form { [function] , [source] , [module] } location . The function is the name of a method. Since function names are not necessarily globally unique, source specifies the source file in which the function was defined. When debugging across multiple modules (e.g., in a program that uses several DLLs), even source file names may not be unique, so you can specify which particular module you mean with module . Finally, location specifies the exact positionthis is specified as a line number.

Figure 3-12. A data breakpoint
figs/mvs_0312.gif

The various parts of the context string are all optionalyou need supply only as many as are required to be unambiguous. For example, to specify that the expression should be evaluated with respect to line 123 of the Hello.cpp source file, use the string {,Hello.cpp,} @123 . Because no function was provided, location was relative to the top of the file. However, if you supply a function , location is not required.

Using data breakpoints can make your program run very slowly in the debugger, because Visual Studio .NET has to go to great lengths to provide this functionality. If the code you are debugging is very processor intensive , data breakpoints will probably not be the most appropriate tool.

3.2.1.2 The Breakpoints window

You can review, modify, and remove all of the breakpoints currently in place for your project with the Breakpoints window. You can open the window using Debug Windows Breakpoints (Ctrl-Alt-B).

As Figure 3-13 shows, the Breakpoints window lists all of the breakpoints. You can choose which information will be displayed about each breakpointthe Columns button on the toolbar lets you select any aspect of a breakpoint. By default, the window will show each breakpoint's location and whether it has condition or hit count requirements specified, and the Hit Count column also indicates how many times the breakpoint has been hit so far in the current debugging session. You can modify the breakpoint by selecting it and choosing Properties from the context menuthis will open the Breakpoint Properties window, which is essentially identical to the New Breakpoint window (except that it doesn't let you change a location-based breakpoint to a data breakpoint or vice versa).

Figure 3-13. The Breakpoints window
figs/mvs_0313.gif

The tick box next to the breakpoint indicates that the breakpoint is enabled. If you uncheck this, the breakpoint will be disabled, but not forgotten. (You can also toggle this setting in the editor window by moving the cursor to the relevant line and pressing Ctrl-F9.) This is useful if you want to prevent a breakpoint from operating temporarily but don't want to have to recreate the breakpoint again later. (This is particularly helpful for complex breakpoints such as those with conditions or data breakpoints.) You can also enable and disable breakpoints using the context menu in the source window.

Visual Studio .NET saves your breakpoint settings when you save the solution, including whether they are enabled or not. These settings are not stored in the .sln file itself, but rather in the associated .suo file. Note that if you move the .suo file to another machine, you may find that some of your breakpoints stop workingthe location of source files for components outside of the project may not be the same from one machine to the next. (For example, they could be on a network share that might be mapped to different drives .) If you find that some breakpoints have disappeared after changing machines, open the Breakpoints window and check that none of the breakpoints have filenames that are no longer valid.

The toolbar at the top of the window provides the ability to create and delete breakpoints, to enable and disable them, to examine the code on which they are set, and to display their properties window. (All of these facilities are also available from the context menu.)

3.2.2 Halting on Errors

Breakpoints are very useful when you know exactly which part of your program you wish to examine, but in practice, debugging sessions often start when an unexpected error occurs. Just-in-time debugging always works this waywhen you attach the debugger just-in-time, it will halt the program and attempt to show you where the error occurred. But you do not need to rely on just-in-time attachment for this behaviorprograms started from within the debugger can be halted automatically when an unhandled error occurs.

Visual Studio .NET can identify many different sources of errors. There are four general categories: C++ exceptions, CLR exceptions, CLR runtime checks, and Win32 exceptions. These categories are subdivided into specific exceptions. You can configure how VS.NET handles these error types with the Exceptions dialog, which is displayed using Debug Exceptions... (Ctrl-Alt-E). This dialog is shown in Figure 3-14.

Figure 3-14. Configuring exception handling
figs/mvs_0314.gif

For each error type, Visual Studio .NET allows two error-handling behaviors to be specified: unanticipated errors can be treated differently from those the application is able to handle itself. Unhandled exceptions will use the setting in the "If the exception is not handled" group box. Exceptions that the application handles itself will use the setting in the "When the exception is thrown" group box.

The gray circles in Figure 3-14 indicate that the debugger will suspend the code only when an unhandled error occurs. This is the default for all categories. If you change the category's setting, the members of that category will inherit that setting unless they have been explicitly configured to override it. (The default for most category members is Use Parent Setting.) Figure 3-15 shows the effect of changing the C++ Exceptions category settings. The X in a red circle indicates that the error will always cause the debugger to break, regardless of whether the program handles the error. Notice how all of the entries inside the C++ Exceptions category have changed to a red crossthey have all inherited their parents' settings.

Figure 3-15. Exception setting inheritance
figs/mvs_0315.gif

The Exceptions dialog indicates that an entry will inherit its parent's settings by drawing a smaller iconall of the items in the C++ Exceptions category have small circles by them. If you set an item's behavior explicitly, making it ignore the parent setting, you will see a full- sized icon. Figure 3-16 shows how this looksVisual Studio .NET's default configuration has two Win32 exceptions that override their category's default, breaking into the debugger regardless of whether the exceptions are handled by the application. These are the Ctrl-C and Ctrl-Break exceptions.

Figure 3-16. Overriding parent behavior
figs/mvs_0316.gif

The Ctrl-C and Ctrl-Break error settings mean that if a program is running with the debugger attached, you can always halt the program and examine it by pressing one of these key combinations. (You must do so when the target program itself has the focus.)

Note that using Ctrl-C to enter the debugger works only for console applications. In Windows applications, Ctrl-C does not have the same meaning and just copies data to the clipboard, so normally only the Ctrl-Break key combination will work.

If Visual Studio .NET has the focus, you can always suspend the program with Debug Break All (Ctrl-Alt-Break).

The Exceptions window does not show every possible exception, it simply lists some of the more common ones. If an unlisted exception occurs, it will simply use the category defaults. If this is not what you require, you can use the Add... button to add an entry for the particular exception you wish to configure. Make sure that you select the appropriate category in the tree view before clicking Add.... (For example, don't try to add settings for a .NET exception when the Win32 Exceptions item is selected.)

Unless you are debugging your error-handling code, you will not normally need to change the default settingsthey will cause Visual Studio .NET to suspend your code only when there is an unhandled error. This is usually the most helpful behavior. When an unhandled error does occur, you will see the dialog shown in Figure 3-17. This tells you about the error and gives you the option of halting the code in the debugger or continuing with execution (the Break and Continue buttons, respectively).

Figure 3-17. An unhandled exception
figs/mvs_0317.gif

If you select Continue, the application's normal unhandled error management code will run. This will allow execution to continue instead of halting in the debugger. This can be useful if you have written your own application-level unhandled exception handler and wish to debug it.

The presence of an application-level default exception handler is not considered by VS.NET to mean that all exceptions are "handled." It will run your default handler only after you have allowed the debugger to continue in the face of an unhandled error.

Be aware that when configuring Visual Studio .NET to halt when an error occurs, you have no guarantee that there will be source code available for the location at which execution halts. If VS.NET cannot find the source code, you will be presented with disassembly. However, you will normally be able to find some of your code in the Stack Trace window, which is described later.

3.2.3 Single-Stepping

Regardless of which of the many different ways of halting code in the debugger you choose, you will end up with Visual Studio .NET showing you where the program has been stopped. It indicates the exact line with a yellow arrow in the gray margin at the left of the source code window, and it also highlights the source code in yellow, as Figure 3-18 shows. (The arrow will be drawn over the red circle if the line at which the code stopped has a breakpoint set.)

Figure 3-18. The current line in the debugger
figs/mvs_0318.gif

When execution is suspended like this, there are various things you can do. You can examine the value of any program data that is in scope, as described later. You can terminate the program with Debug Stop Debugging (Shift-F5). You can resume execution with Debug Continue (F5). Or you may decide that you want to follow the program's execution through in detail, one line at a time, by single-stepping.

The single-stepping shortcut keys are probably the ones that you will use the most, so although you can use Debug Step Over or Debug Step Into or their toolbar equivalents, in practice you will normally use their keyboard shortcuts, F10 and F11. Both Step Over (F10) and Step Into (F11) execute a single line of code; the only difference is that, if the line contains a function call, F11 will let you step into the code of the called function, whereas F10 will simply call the function and stop on the following line. (In .NET applications, properties are implemented as functions, so F11 will also step into property accessors.)

If you are currently viewing source code, Step Into (F11) will work only if source code is available for the method you are stepping into. (If no source code is available, it simply steps over the current line.) However, if you change to assembly language debugging, you can step into almost any CALL instruction. You can switch to a disassembly view with Debug Windows Disassembly, or Ctrl-Alt-D. (Certain calls into the .NET runtime cannot be stepped into in a .NET debugging session. A native debugging session can step into any CALL instruction.)

You can see assembly language when debugging by selecting Go to Disassembly from the context menu. Alternatively, you can use Debug Windows Disassembly (Ctrl-Alt-D). There is currently no way of seeing the Intermediate Language (IL) for a method in the debugger.

In versions of Visual Studio prior to .NET, Step Into suffered from ambiguity in the face of multiple method calls. Consider the following code:

 printf("Name: %s %s", GetTitle(  ), GetName(  )); 

This one line involves three functions: printf , GetTitle , and GetName . Pressing F11 will step into whichever executes first. (The C++ spec doesn't actually dictate the precise order in which the calls will occur in this particular example, beyond requiring printf to be called last. With Microsoft's C++ compiler, it turns out to call GetName first.) When that returns, you can press F11 again to call the second and so on. If you care about only one of the methods, it can be tedious to step through the rest. And although you can always drop down into disassembly mode and locate the call you want, that is hardly an elegant solution.

Fortunately, Visual Studio .NET provides a better solution for unmanaged (non-.NET) Win32 C++ applications. (Other languages don't get this feature, sadly.) If execution is halted at a line with multiple method calls, the context menu will have a Step Into Specific menu item. As Figure 3-19 shows, this item has a submenu with each of the functions shown. If you select an item from this list, the debugger will step into that one.

If the method you select happens not to be the one that will execute first, the others will not be skipped . They will simply be executed silently, just as function calls stepped over with F10 are.

Figure 3-19. Stepping into a specific function
figs/mvs_0319.gif

Single-Stepping and IL

Although VS.NET provides no support for examining IL at debug time, it is possible to work around this limitation if you are sufficiently determined. The IL Assembler ( ILASM.EXE ) is able to generate debug information. So if you write all your software in IL, then source-level debugging will consist of single-stepping through IL.

Of course, switching to IL is a high price to pay. However, if you want to carry on writing your code in C# or VB.NET but still see IL in the debugger, there is a way: compile your component as usual and then run it through ILDASM, the IL Disassembler, passing the /out=<filename> switch. This will generate an IL source file. You can then compile this using ILASM, passing in the /debug+ switch in order to generate IL debugging information. You will now be able to single-step through the IL.

There are two problems with this technique. The first is that you have to do this all by handVS.NET does not automate this for you. The second problem is that you will no longer be able to single-step through the original source codeVS.NET will consider the IL generated by ILDASM to be the source code! You can mitigate this second problem by passing the /source switch to ILDASM, which will cause it to annotate the IL with the original source code, providing you with a mixed IL/source view, which is a lot better than raw IL. (This works only if the original component was built with debugging information of course.)

Unfortunately, C# and Visual Basic .NET are not blessed with this feature. However, the debugger does provide a feature that can mitigate this shortcoming. Any method that has been marked with the System.Diagnostics.DebuggerStepThrough attribute will not be stepped into when F11 is pressedit will be executed without single-stepping. This attribute is particularly appropriate for simple property accessors. The accessor in Example 3-2 is so straightforward that it is unlikely to be informative to step into it, so the attribute will make it effectively invisible to Step Into (F11). (The code can still be stepped through if it turns out to be necessary by setting a breakpoint inside the accessor, so there is no harm in using this attribute on such methods.)

Example 3-2. Disabling Step Into for trivial methods
 private int _index; private int CurrentIndex {  [System.Diagnostics.DebuggerStepThrough]  get { return _index; } } 
3.2.3.1 Stepping through multiple lines

Sometimes, you will need to single-step through some code that has regions that are tedious to work through one line at a time. A common example is code with a long, uninteresting loop. It is relatively straightforward to avoid having to single-step through such a section by placing a breakpoint at the end and letting the code run. But there is a slightly quicker way. You can simply move the cursor past the dull section, to the first line at which you would like to resume single-stepping, and press Ctrl-F10. (Alternatively, you can select Run to Cursor from the context menu, which has the same effect; for some reason this option is not available from the main menu.)

There is another common situation in which you will wish to step through several lines in one go. Sometimes when you step into (F11) a method, it will become apparent that the method is not interesting enough to warrant stepping through all of it. You could use Run to Cursor (Ctrl-F10) to move back to the parent method, but it is easier to use Debug Step Out (Shift-F11). This will allow the code to run until it returns from the current subroutine, and it will then resume single-stepping.

3.2.3.2 Changing the current point of execution

Occasionally you will want to disrupt the natural flow of execution. You can manually adjust the current execution location of the code by using the context menu's Set Next Statement item. You can only move within the currently executing method, but you can move both forward and backward. (So you can either skip code or rerun code.)

Adjusting the execution location can be powerful technique. It can allow you to go back and watch a piece of code's execution a second time in case you missed some aspect of its behavior. Used in conjunction with the ability to modify the program's variables (see Section 3.3.1, later in this chapter) it can also provide a way of experimenting with the code's behavior in situ. However, you should avoid using this feature if possible, because it may have unintended consequences. Compilers do not generate code that is guaranteed to work when you leap from one location to another, so anomalous behavior may occur. Variables may not be initialized correctly, and you may even see more insidious problems like stack corruption. So you should always prefer to restart a program and recompile it if necessary. However, if you are tracking down a problem that is very hard to reproduce, this feature can be extremely useful, because it allows you a degree of latitude for experimentation on the occasions when the behavior you are looking for does manifest itself.

3.2.3.3 Edit and continue

Edit and continue is a feature that allows code to be edited during a debugging session. The only language that supports this feature in the first release of Visual Studio .NET is C++. This is a little surprising because Visual Basic was the first language to get edit and continue. Unfortunately, certain features of the .NET runtime make it extremely hard to implement edit and continue, so now that Visual Basic is a .NET language, only classic unmanaged Win32 C++ applications get this feature. However, we hope for its return in a future version of Visual Basic .NET.

Edit and continue can be a great time-saver, because it enables you to fix errors without having to stop your debug session, rebuild, and restart. This can be particularly helpful in scenarios in which a bug is tricky to reproduce. If you have spent half a day getting to the point to see the program fail, it can be very useful to try out a fix in situ without having to rebuild and then start again from scratch.

Edit and continue can also sometimes be useful for experimenting with a program's behavior. In combination with the ability to change the next line to be executed and to modify program variables, the ability to change the code makes it very easy to try out several snippets of code in quick succession to see how they behave.

   


Mastering Visual Studio. NET
Mastering Visual Studio .Net
ISBN: 0596003609
EAN: 2147483647
Year: 2005
Pages: 147

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net