Teach Yourself Visual C++ 6 in 21 Days

Previous chapterNext chapterContents


- E -
Using the Debugger and Profiler



by Jon Bates

Creating Debugging and Browse Information

A large part of application development is actually debugging your program. All software development is a tight cycle of application design, implementation, and debugging.

Visual C++ has an extensive debugging environment and a range of debugging tools that really help with program development. You can quickly identify problems, watch the contents of variables, and follow the flow of programs through your own code and the MFC code.

Tools such as the Spy++ program can show you the messages passed between Windows and your application and let you spy on applications to see which user interface controls and Window styles they use.

Using Debug and Release Modes

There are two main compiler configurations that you can set to build your application: Debug and Release mode. You can change these modes by clicking the Project menu and selecting the Settings option or by pressing Alt+F7, which will display the Project Settings dialog box (see Figure E.1). The main project settings are shown at the top level and can be changed by selecting the options listed in the combo box. When one setting is selected, changes that you make to any options on the tabs on the right will be set against that configuration. When you build the application, it will be built using your current configuration settings, or you can select All Configurations to build and make changes to all configurations simultaneously.

FIGURE E.1. The C/C++ tab of the Project Settings dialog box.

Both Release and Debug configurations are supplied whenever you create a new project; they produce very different object code. When configured for Debug mode, your build will produce a large and fairly slow executable program. This is because lots of debugging information is included in your program and all the compiler optimizations are disabled.

When you compile the same program in Release mode, you'll see a small, fast executable program, but you won't be able to step through its source code or see any debugging messages from it.

Normally, when developing an application, you leave the compiler set to Debug mode so that you can easily spot and debug problems that arise in your code. When you've finished your application and are preparing to release it, you can set the configuration to Release mode and produce a small, fast program for your users.


RELEASE MODE TESTING

You should always fully test your application after rebuilding it in Release mode before sending it to users. Bugs can arise from things such as leaving proper program code in ASSERT macros (discussed later this chapter), which are then removed, or because of the effect of some speed and memory optimizations.


Setting Debug Options and Levels

You can set a variety of debugging options and levels from the C/C++ tab of the Project Settings dialog box. This dialog page is available from the Project menu by selecting the Settings option (or by pressing Alt+F7) and then selecting the C/C++ tab.

With the General Category selected, the following items are available:


LEVEL 4 WARNINGS

At level 4, you'll find that Microsoft's own AppWizard-generated code gives warnings (although usually only about unused function parameters that can be safely ignored).


int a = b * c / d + e;
#ifdef _DEBUG
CString strMessage;
strMessage.Format("Result of sum was %d",a);
AfxMessageBox(strMessage);
#endif

TABLE E.1. COMPILER WARNING LEVELS.

Level Warnings Reported
None None
Level 1 Only the most severe
Level 2 Some less severe messages
Level 3 Default level (all reasonable warnings)
Level 4 Very sensitive (good for perfectionists)

Table E.2. Debug info settings.

Setting Debugging Information Generated
None Produces no debugging information--usually reserved for Release modes.
Line Numbers Only This generates only line numbers that refer to the source code for functions and global variables. However, compile time and executable size are reduced.
C 7.0-Compatible This generates debugging information that is compatible with Microsoft C 7.0. It places all the debugging information into the executable files and increases their size, but allows full symbolic debugging.
Program Database This setting produces a file with a .pdb extension that holds the maximum level of debugging information, but doesn't create the Edit and Continue information.
Program Database for Edit and Continue This is the default and usual debug setting. It produces a .pdb file with the highest level of debugging and creates the information required for the new Edit and Continue feature.

Creating and Using Browse Information

You can use the Source Browser tool to inspect your source code in detail. This tool can be invaluable if you are examining someone else's code or coming back to your own code after you haven't viewed it for awhile.

To use the Source Browser, you must compile the application with the Generate Browse Info setting checked, in the C/C++ tab of the Project Settings dialog box. To run the tool, press Alt+F12 or click the Tools menu and select the Source Browser option. (The first time you run the tool, it will ask you to compile the browser information.)

The first dialog box the Source Browser presents requests an Identifier to browse for (as shown in Figure E.2). This identifier can be a class name, structure name, function name, or global or local variable name in your application. After you have entered an identifier, the OK button is enabled, and you can browse for details about that identifier.

FIGURE E.2. The Browse dialog box requesting a symbol to browse.

Select Query offers various options for details pertaining to your chosen symbol. You can choose from any of the following:

FIGURE E.3. Source Browser showing definitions and references.

FIGURE E.4. The file outline display of the source browser.

FIGURE E.5. The Base Classes and Members view of the source browser.

FIGURE E.6. The Derived Classes and Members view of the Source Browser showing CWnd-derived classes.

Using Remote and Just-in-Time Debugging

The debugger includes tools that let you debug a program running on a remote machine (even over the Internet via TCP/IP). This can be useful if you want to test your application in a different environment other than your development machine. To do this, you must have exactly the same versions of the .dll and .exe files on both machines. After loading the project, you can debug it via a shared directory from the remote machine by changing the Executable for Debug Session edit box to the path and filename of your local .exe file (located in the Project Settings dialog box under the Debug tab).

You must also add a path to the .exe file in the Remote Executable Path and File Name edit box at the bottom of the Debug tab, leaving the Working Directory blank. You can then start the remote debugger monitor on the remote computer by running the MSVCMON.EXE program and connecting to it by clicking the Build menu and selecting the Debugger Remote Connection option.

From the Remote Connection dialog box you can choose Local for a shared directory debug session or Remote to debug via a TCP/IP connection. (You can set the address by clicking Settings.) This will connect to the remote monitor that will start the remote debugging session.

Installing the Remote Debugger Files

You will also need the following files to run the remote debugger monitor on the remote machine: MSVCMON.EXE, MSVCRT.DLL, TLN0T.DLL, DM.DLL, MSVCP5O.DLL, and MSDIS100.DLL. These files can be found in your installed ...\Microsoft Visual Studio\Common\MSDev98\bin subdirectory.

Just-in-time debugging lets you debug a program that was run normally (not through the debugger) and then developed a problem. If you have Visual C++ installed on a machine and this option is enabled, any program that develops a fault will be loaded into a new Developer Studio session ready for debugging and show the code that caused the crash.

This often raises a chuckle when Developer Studio itself crashes and then proceeds to load another session of itself, offering you an assembly code view of where the crash took place in the original for you to debug. It can be very useful to debug your own applications when they crash unexpectedly (usually in a demonstration to your boss). You can enable this option by clicking the Tools menu and selecting Options to display the Options dialog box. Then select the Debug tab and ensure that the Just-in-Time debugging check box is checked.

The OLE RPC debugging option on this tab is also very useful when developing COM and DCOM applications because it lets the debugger traverse a function call into another out-of-process program or .dll and lets another debugger take over for the other process. It then hands control back when returning from the remote function and works across networks and different computers.

Tracing and Single Stepping

One of the most useful features of the Visual C++ debugging environment is the interactive single stepping. This feature lets you step through the code one line at a time and examine the contents of variables as you go. You can also set breakpoints so that the program runs until it reaches a breakpoint and then stops at that point, letting you step from that point until you want to continue running.

Trace statements and assertions are also very useful tools for finding program faults. Trace statements let you display messages and variables from your program in the output window as it runs through trace statements. You can use assertions to cause the program to stop if a condition isn't TRUE when you assert that it should be.

Using the TRACE Macro

You can add TRACE macros to your program at various places to indicate that various parts of the code have been run or to display the contents of variables at those positions. The TRACE macros are compiled into your code in the debug configuration and displayed in the Output window on the Debug tab, when you run your program through the debugger.

You can safely leave in the TRACE macros when you perform a release build because these macros are automatically excluded from the destination object.

You can display simple messages or output variable contents by passing a format string as the first parameter to the TRACE macro. This format string is exactly the same as you would pass to a printf() or CString::Format() function. You can specify various special formatting codes such as %d to display a number in decimal, %x to display a number in hexadecimal, or %s to display a string. The following parameters should then correspond to the order of the formatting codes. For example, the code

int nMyNum = 60;
char* szMyString = "This is my String";
TRACE("Number = %d, or %x in hex and my string is: %s\n",
          nMyNum, szMyString);

will result in this output trace line:

Number = 60, or 3c in hex and my string is 
ÂThis is my String

Listing E.1 shows the TRACE macro used to display the contents of an array before and after sorting by a very inefficient but simple sort algorithm.

If you want to try the code shown in Listing E.1, you can use the AppWizard to build a simple SDI framework. Simply add the code above the OnNewDocument() member function of your document class and then call it by adding a DoSort() call into your OnNewDocument() function.

You can run the application through the debugger (click Build, select Start Debug, and choose Go from the pop-up menu) to see the output trace.

You must ensure that the output window is visible (click the View menu and select Output) when the tabbed output window is shown (same as the compiler output). Ensure that the Debug tab is selected.

LISTING E.1. LSTE_1.CPP--A SIMPLE SORT ROUTINE TO DEMONSTRATE DEBUGGING TECHNIQUES.

1:  void Swap(CUIntArray* pdwNumbers,int i)
2:  {
3:      UINT uVal = pdwNumbers->GetAt(i);
4:      pdwNumbers->SetAt(i, pdwNumbers->GetAt(i+1));
5:      pdwNumbers->SetAt(i+1,uVal);
6:  }
7:
8:  void DoSort()
9:  {
10:      CUIntArray arNumbers;
11:      for(int i=0;i<10;i++) arNumbers.Add(1+rand()%100);
12:
13:      TRACE("Before Sort\n");
14:      for(i=0;i<arNumbers.GetSize();i++)
15:          TRACE("[%d] = %d\n",i+1,arNumbers[i]);
16: 
17:      BOOL bSorted;
18:      do
19:      {
20:          bSorted = TRUE;
21:          for(i=0;i<arNumbers.GetSize()-1;i++)
22:          {
23:              if (arNumbers[i] > arNumbers[i+1])
24:              {
25:                  Swap(&arNumbers,i);
26:                  bSorted = FALSE;
27:              }
28:          }
29:      } while(!bSorted);
30:
31:      TRACE("After Sort\n");
32:      for(i=0;i<arNumbers.GetSize();i++)
33:          TRACE("[%d] = %d\n",i+1,arNumbers[i]);
34:  }

Listing E.1 sorts an array of random numbers (between 1 and 100), generated in line 11. Lines 13 to 15 then print out the contents of the array before sorting by TRACE statements. Lines 17 through 29 sort the array by swapping pairs of numbers that are in the wrong order (by calling the Swap() function in line 25). The Swap() function (lines 1 to 6) takes a pointer to the array and a position and then swaps the two numbers at that position.

After sorting, the contents of the array are again printed in the output window by the TRACE statements in lines 31 to 33.

The trace output of this program appears in the Output window of Developer Studio, as shown in Table E.3.

TABLE E.3. OUTPUT FROM THE SORTING PROGRAM.

BEFORE SORT

AFTER SORT

[1] = 42 [1] = 1
[2] = 68 [2] = 25
[3] = 35 [3] = 35
[4] = 1 [4] = 42
[5] = 70 [5] = 59
[6] = 25 [6] = 63
[7] = 79 [7] = 65
[8] = 59 [8] = 68
[9] = 63 [9] = 70
[10] = 65 [10] = 79

Using the ASSERT and VERIFY macros

You can use the ASSERT macro to ensure that conditions are TRUE. ASSERT is passed one parameter that is either a TRUE or FALSE expression. If the expression is TRUE, all is well. If the expression is FALSE, your program will stop and the Debug Assertion Failed dialog box will be displayed (see Figure E.7), prompting you to Abort the program, Retry the code, or Ignore the assertion. It also shows the program, source file, and line number where the assertion failed. If you choose Abort, the debugging session is terminated. Retry is probably the most useful option because the compiler will then show you the code where the ASSERT macro has failed, enabling you to figure out what went wrong. If you already know or don't care about the assertion, you can choose Ignore and continue running the program, which might then result in a more fatal error.

FIGURE E.7. The Debug Assertion Failed dialog box helps you track down bugs.

A common use of ASSERT is to ensure that input parameters to functions are correct. For example, you can make the Sort() function (shown in Listing E.1) more robust by checking its input parameters. To check the input parameters, add ASSERT macros at the top of the Sort() function like this:

ASSERT(pdwNumbers);
ASSERT(i>=0 && i<10);

This will ensure that the pointer to the numbers array isn't zero and that the position to swap is between 0 and 9. If either of these is incorrect, the Debug Assertion Failed dialog box is displayed. This technique helps you track down errors caused by passing faulty parameters to functions. It is a good practice to use the ASSERT macro to check that the values passed to each of your functions conform to your expectations.

Another macro, ASSERT_VALID, can be used with CObject-derived classes such as most MFC classes. This performs a more thorough check on the object and its contents to ensure the entire object is in a correct and valid state. You can pass a pointer to the object to be checked like this:

ASSERT_VALID(pdwNumbers);

Another ASSERT macro is ASSERT_KINDOF, which is used on CObject-derived classes to check that they are of the correct class type. For example, you can check that a pointer to your view object is of the correct view class like this:

ASSERT_KINDOF(CYourSpecialView,pYView);

The Assertion Failed dialog box will be displayed if it isn't of the correct class type or any of its derivatives.

You must be careful not to put any code that is needed for normal program operation into ASSERT macros because they are excluded in the release build. A common source of release mode errors that are hard to track down is coding like this:

int a = 0;
ASSERT(++a > 0);
if (a>0) MyFunc();

In the debug build, this code will increment the integer a in the ASSERT line and then call MyFunc() in the following line because a is greater than zero. When your sales team is eager to demonstrate your new application, you might think it works fine because there aren't any Debug mode problems. So you recompile it in Release mode and hand it over to your sales department, which demonstrates it to a customer, whereupon it crashes badly. It crashes because the ++a isn't performed--the release mode excludes ASSERT lines.

The VERIFY macro helps with this problem. VERIFY works like ASSERT, and in Debug mode it throws the same Assertion Failed dialog box if the expression is zero. However, in release mode the expression is still evaluated, but a zero result won't display the Assertion dialog box. You will tend to use VERIFY when you always want to perform an expression and ASSERT when you only want to check while debugging. Therefore, replacing ASSERT in the previous example with VERIFY, as shown in the following example, will enable the release build to work properly:

VERIFY(++a > 0);

You are more likely to use VERIFY to check return codes from functions:

VERIFY(MyFunc() != FALSE);

Using Breakpoints and Single Stepping the Program

The use of single stepping and breakpoints is probably the most effective debugging tool for tracking down the majority of problems. The support for various types of breakpoints and the single-stepping information available is very sophisticated in Visual C++; I can only hope to give you a taste of the power of this debugging tool.

The key to single stepping is breakpoints. You can set a breakpoint anywhere in your code and then run your program through the debugger. When the breakpoint is reached, the code will be displayed in the editor window at the breakpoint position, ready for you to single step or continue running.

You can add a breakpoint by selecting the specific code line (clicking the editor cursor onto the line in the editor window) and then either clicking the Breakpoint icon in the Build minibar (see Figure E.8) or by pressing F9. Alternatively, most sophisticated breakpoints can be added or removed by clicking the Edit menu and selecting the Breakpoints option to display the Breakpoints dialog box (see Figure E.9). When you add a breakpoint, it's displayed as a small red circle next to the line you have specified. Breakpoints can be set only against valid code lines, so sometimes the Developer Studio will move one of your breakpoints to the closest valid code line for you.

FIGURE E.8. Adding a breakpoint to your code via the Build minibar toolbar or the F9 key.

FIGURE E.9. Adding a breakpoint using the Breakpoints dialog box.

You can toggle the breakpoint on or off by clicking the Breakpoint (hand shaped) icon or remove it by clicking the Remove or Remove All buttons on the Breakpoints dialog box. You can leave them in position but disable them by clicking the check mark to the left of each breakpoint listed in the Breakpoints dialog box. Clicking there again will show the check and re-enable the breakpoint.

When you have set your breakpoint(s), you can run the code through the debugger by choosing Build, Start Debug, Go. Alternatively, you can use the shortcut by clicking the Go icon (to the left of the Breakpoint icon on the Build minibar toolbar--refer to Figure E.8) or by pressing the F5 key.

The program will run as normal until it reaches the breakpoint, where it will stop and display an arrow against the line with the breakpoint. At that point, you can use the Debug toolbar to control the single stepping process, as shown in Figure E.10.

FIGURE E.10. The debugger stopped at a breakpoint ready for single stepping with the Debug toolbar.

When stopped in the debugger, you can see the contents of most variables merely by moving the cursor over them in the editor window. Their contents are then displayed in a ToolTip at the cursor position. More detailed contents are shown by dragging the variables into the Watch window, as discussed in detail in the next section.

You can single step through the code using the four curly brace icons shown on the Debug toolbar or by clicking the Debug menu and choosing one of the step options. The available step options are shown in Table E.4. You can find these on the Debug menu and the Debug toolbar.

TABLE E.4. STEP OPTIONS AVAILABLE IN SINGLE STEPPING.

Icon/Step Option Shortcut Key Effect When Selected
Step Into F11 The debugger will execute the current line and if the cursor is over a function call, it will enter that function.
Step Over F10 Like Step Into except when over a function call line, it will run that function at normal speed and then stop when it returns from the function, giving the effect of stepping over it.
Step Out Shift+F11 The debugger will run the rest of the current function at normal speed and stop when it returns from the function to the calling function.
Run to Cursor Ctrl+F10 The debugger will run until it reaches your specified cursor position. You can set this position by clicking the line you want to run to.
Go F5 Continue running the program at normal speed until the next breakpoint is encountered.
Stop Debugging Shift+F5 This stops the debugger and returns to editing mode.
Restart Ctrl+Shift+F5 This option restarts the program from the beginning, stopping at the very first line of code.
Break Execution

This option stops a program running at normal speed in its tracks.
Apply Code Changes Alt+F10 This option lets you compile the code after making changes during a debugging session and then continue debugging from where you left off.

By using these options, you can watch the flow of your program and see the contents of the variables as they are manipulated by the code. The yellow arrow in the Editor window will always show the next statement to be executed.

The next sections describe some of the debugging windows you can use when you are stopped in the debugger.

Using Edit and Continue

A great new feature of Visual C++ 6 is the capability to Edit and Continue. This means that you can change or edit the code while you are stopped in the debugger. After editing, you'll notice the Debug menu's Apply Code Changes option becomes enabled (as well as the corresponding debug toolbar icon). You can then select the Apply Code Changes option (or toolbar button) to compile your new code changes and then continue debugging the new changed code. By using this new feature, you can fix bugs while debugging and continue the debug run from the same place in the code with the same variable settings, which can be very useful when debugging large and complex programs.

Watching Program Variables

The Watch and Variables windows are shown in Figure E.11. These windows display the contents of variables when stopped in the debugger. You can view these windows by clicking the View menu and selecting them from the Debug Windows pop-up menu or by clicking the icons from the toolbar.

FIGURE E.11. The Watch window displays contents of variables while debugging.

The Variables window always shows the local variables of the function displayed in the Context combo box at the top of the window. To get to your current function, you can drop this combo box list to display all the functions that were called in turn. This is the call stack and shows your current context within the program by showing the list of functions that have been called in order to get to the program's currently executing function where the debugger has stopped. When you select a different function, the relevant local variables are shown for that function level.

You can expand any object pointers shown by clicking the plus symbol next to the pointer name. The special C++ this pointer is always shown for class member functions and can be opened to show all the member variables for the current object.

The Watch window lets you enter variable names from the keyboard or drag variable names from the editor window (after selecting and inverting them with the mouse point). The values that are held in the displayed variables are shown until they go out of scope (that is, aren't relevant to the function currently being debugged).

You can also enter simple casts and array indexes in the Watch window to show related values. Right-clicking the mouse can switch the displayed values between hexadecimal and decimal display. As you step through the program, the values shown in the Watch and Variable windows are updated accordingly so that you can track how the program changes the variables.

Other Debugger Windows

Other debugging display windows are available by clicking the View menu and selecting them from the Debug Windows pop-up menu or alternatively by clicking the various icons shown to the right of the Debug toolbar. These windows are

KERNEL32! bff88f75()

Additional Debugging Tools

Along with the integrated debugging tools are several nonintegrated but very useful tools. You can start these by clicking the Tools menu and selecting the specific tool option from the menu.

These tools generally let you track operating-specific items such as Windows messaging, running processes, and registered OLE objects to enhance your available information while debugging your application.

Using Spy++

Spy++ is undoubtedly one of the most useful of these tools. With Spy++, you can see the hierarchical relationships of parent to child windows, the position and flags settings for windows, and base window classes. You can also watch messages as they are sent to a window.

When you first run Spy++, it shows all the windows on the current desktop, their siblings, and the base Windows class of each object (see Figure E.12). The view shown in Figure E.12 has been scrolled to shown the standard Microsoft Windows CD Player. Spy++ shows you all the buttons and combo boxes, which are windows in their own right as child windows of the main CD Player window.

FIGURE E.12. The Spy++ initial view of the Windows desktop showing the CD Player portion.

If you click the Spy menu, you are shown the following options:

FIGURE E.13. Using the Spy++ Message Options Finder to locate windows.

FIGURE E.14. Windows Messages for a toolbar logged by Spy++.

Spy++ is too sophisticated to cover in its entirety here, but as a tool for understanding the structure of Windows hierarchies and messaging, it is unsurpassed. You can glean a lot of valuable knowledge just by looking at commercial applications with Spy++. It is also a wonderful tool for debugging messaging problems in your own application to ensure that your windows are getting the correct messages and to see how these messages are sequenced.

Process Viewer

You can see all the processes in more detail than shown in Spy++ with the Process Viewer (PView95.exe). You can start this application from your system's main Windows Start menu from Programs under the Microsoft Visual Studio 6.0 Tools option (or similar program group). This application lists the processes running on your machine and lets you sort them by clicking any of the column headers. You can then click a process to display all its threads. Figure E.15 shows Process Viewer running with the Developer Studio application (MSDEV.EXE) selected and all its many threads displayed.

FIGURE E.15. The Process Viewer showing MSDEV.EXE and its threads.

The OLE/COM Object Viewer

The OLE/COM Object Viewer tool shows you all the registered OLE/COM objects on your system, including ActiveX controls, type libraries, embeddable objects, automation objects, and many other categories.

You can even create instances of various objects and view their interfaces in detail. The OLE/COM Object Viewer is very useful if you are developing an OLE/COM application or looking for an elusive ActiveX control.

The MFC Tracer

Using the MFC Tracer tool shown in Figure E.16, you can stop the normal tracing or add specific Windows trace output to the normal program trace output. When you select this tool, you are shown a set of check boxes that you can check or uncheck to include that tracing option.

FIGURE E.16. The MFC Tracer tool options.

You can add Windows messages, database messages, OLE messages, and many other levels of trace output to help track down elusive problems. These messages are then generated by the MFC code for the various selected flags.

You can even turn off the standard tracing generated by your application by unchecking the Enable Tracing option.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.