Tips and Tricks

[Previous] [Next]

Now it's time to turn to some tricks you can use in the debugger so that you can collect as much information about your problem as possible. As usual, I encourage you to spend some time playing with these tips on your own projects so that you can find innovative ways of applying them.

Setting Breakpoints

You might think I've already covered everything you need to know about breakpoints—but hang on, there's still a little more. Everyone knows that you can set location breakpoints on a source line and in the Disassembly window, but did you know that you can also set them in the Call Stack window? Setting a location breakpoint in a Call Stack window is a great technique when you want to get out of a deeply nested call chain. In the Call Stack window, right-click the function on which you want to break and select Insert/Remove Breakpoint to stop as soon as you return to that function.

Instead of removing breakpoints after a heavy debugging session, I always disable them. You can disable breakpoints either by right-clicking each one in the source window and selecting Disable Breakpoint or by going into the Breakpoints dialog box and turning off the check mark next to the breakpoint. I leave the breakpoints in while I'm fixing a problem so that I can quickly get the application back to the state it was in when the problem occurred. Of course, I update any breakpoints that could have changed before I start debugging. After I've checked the fix, I can safely remove the breakpoints.

To ensure that I get all my breakpoints set easily, I always click the Step Into button to get the debuggee loaded and started before I set any breakpoints other than simple location breakpoints. The debugger can verify your breakpoints and display the Resolve Ambiguity dialog box only when the debuggee is active. The help the debugger gives you means that you get your breakpoints set quicker and that you can be certain they are correct.

The Watch Window

The Watch window is high on the list of important Visual C++ debugger features. In fact, you probably end up looking at the Watch window more than any other window in the debugger. What makes the Watch window so popular is partly its versatility. You can wield it in various ways to gather information about your application. The neatest capability is that you can easily change a variable's value in the Watch window by editing it in the right-hand side of the grid. Recall from earlier in the chapter our discussion of the syntax for the breakpoint expressions. The same expression evaluator is used in the Watch window, so the advanced breakpoint scope syntax and the expression rules and pseudoregisters can be used in the Watch window as well.

Formatting Data and Expression Evaluation

The first "trick" you'll need to master on your way to becoming proficient at manipulating the Watch window is to memorize the formatting symbols in Table 5-3 and Table 5-4, which derive from the Visual C++ documentation on MSDN. The Watch window is wonderfully flexible in how it displays data, and the way you bring out its flexibility is by using the format codes in these tables. As you can see from the tables, the formats are easy to use: follow your variable with a comma and with the format you want to use. The most useful format specifier for COM programming is ",hr." If you keep the expression "@EAX,hr" in your Watch window, as you step over a COM method call, you can see the results of the call in a form you can understand. (@EAX is the Intel CPU register at which return values are stored.) Using the format specifiers will allow you to easily control how you see your data so that you can save huge amounts of time interpreting it.

Table 5-3 Formatting Symbols for Watch Window Variables

Symbol Format Description Sample Displays
d, i Signed decimal integer (int)0xF000F065,d -268373915
u Unsigned decimal integer 0x0065,u 101
o Unsigned octal integer 0xF065,o 0170145
x, X Hexadecimal integer 61541,X 0x0000F065
l, h Long or short prefix for d, i, u, o, x, X 0x00406042,hx 0x0c22
f Signed floating-point 3./2.,f 1.500000
e Signed scientific notation 3./2,e 1.500000e+000
g Signed floating-point or signed scientific notation, whichever is shorter 3./2,g 1.5
c Single character 0x0065,c 'e'
s String szHiWorld,s "Hello world"
su Unicode string szWHiWorld,su "Hello world"
st Unicode string or ANSI string, depending on Unicode Strings setting in AUTOEXP.DAT    
hr HRESULT or Win32 error code 0x00000000,hr S_OK
wc Windows class flag 0x00000040,wc WC_DEFAULTCHAR (Note that although documented, this format doesn't work in Visual C++ 6.)
wm Windows message numbers 0x0010,wm WM_CLOSE

Table 5-4 Formatting Symbols for Watch Window Memory Dumps

Symbol Format Description Sample Displays
ma 64 ASCII characters 0x0012ffac,ma 0x0012ffac
.4...0...".0W&.......1W&.0.:W..1
...."..1.JO&.1.2.."..1...0y....1
m 16 bytes in hexadecimal followed by 16 ASCII characters 0x0012ffac,m 0x0012ffac
b3 34 cb 00 84 30 94 80 ff 22 8a 30 57 26 00 00
.4...0...".0W&..
mb 16 bytes in hexadecimal followed by 16 ASCII characters 0x0012ffac,mb 0x0012ffac
b3 34 cb 00 84 30 94 80 ff 22 8a 30 57 26 00 00
.4...0...".0W&..
mw 8 words 0x0012ffac,mw 0x0012ffac
34b3 00cb 3084 8094 22ff 308a 2657 0000
md 4 double words 0x0012ffac,md 0x0012ffac
00cb34b3 80943084 308a22ff 00002657
mq 4 quadwords 0x0012ffac,mq 0x0012ffac
8094308400cb34b3 00002657308a22ff
mu 2-byte characters (Unicode) 0x0012ffac,mu 0x0012ffac
34b3 00cb 3084 8094 22ff 308a 2657 0000
?.?????.
# (Undocumented) Expands a pointer to a memory location to the specified number of values pCharArray,10 Expanded array of 10 characters using +/- expanders

One format specifier not documented is the one that allows you to expand a pointer to a memory location to a specific number of values. If you have a pointer to an array of 10 longs, the Watch window will show only the first value. To see the entire array, follow the variable with the number of values you'd like to see. For example, "pLong,10" would show an expandable array of your 10 items. If you have a large array, you can point into the middle of it and expand just the values you want with "(pBigArray+100),20" to show the 20 elements starting at offset 99. There's a bug in the display: the index values always begin at 0, regardless of the position of the first displayed element. In the pBigArray example, the first index, shown as 0, is the 100th array element, 1 is the 101st array element, and so on.

In addition to allowing you to format the data as you'd like it, the Watch window allows you to cast and cajole your data variables so that you can see exactly what you need to see. For example, you can use the BY, WO, and DW expressions mentioned earlier in the chapter to get at pointer offsets. The address-of operator (&) and the pointer operator (*) are also allowed, and both allow you to get the values at memory addresses and to see the results of casts in your code. If you need to be specific, the context specifiers described in the section "Advanced Breakpoint Syntax and Location Breakpoints" earlier in the chapter allow you to explicitly specify the context for a variable in the Watch window. Finally, all the formatting and specifying used in the Watch window also works in the QuickWatch window.

If you haven't guessed by now, the Watch window is much more than a static variable viewer. In fact, it is a whole expression evaluator in which you can check any conditional statements you want. I use the Watch window a lot during my unit testing to verify if statements and the like. I put the individual variables for a conditional statement in the Watch window, followed by the conditional statement. With this setup, I can see the values of each variable and the results of the conditional evaluation. If I need to change the evaluation of a conditional statement, I can start changing the values of the individual variables and keep track of the outcome in the Watch window. In addition, because the Watch window handles expressions so well, you no longer need to pull up Microsoft Calculator to do your calculations—just do them in the Watch window.

Timing Code in the Watch Window

Here's another neat trick—using the Watch window to time code. An undocumented pseudoregister, @CLK, can serve as a rudimentary timer. In many cases, you just want a rough idea of the time between two points, and @CLK makes it easy to find out how long it took to execute between two breakpoints. Keep in mind that this time includes the debugger overhead. The trick is to enter two @CLK watches, the first just "@CLK" and the second "@CLK=0". The second watch zeros out the timer after you start running again. Because the time is in microseconds and I prefer the time in milliseconds, I set the first @CLK to "@CLK/1000,d". I include the ",d" to force the display into decimal when I have the Watch window set to Hexadecimal Display. Although not a perfect timer, @CLK is good enough for some ballpark guesses.

Calling Functions in the Watch Window

The last trick with the Watch window is something almost every UNIX developer asks for when converting to Windows programming: a way to execute a function from the debugger. At first, 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.

The cool part is that your program never has to call the functions you want to use only in the Watch window. In debug builds, your program has all functions linked into it, but in release builds, any function that is never called isn't linked. When you enter a function in the Watch window, you can also pass parameters, so you can write generic functions that can work on different data pointers. You can think of the Watch window as a limited Immediate window like the one in Microsoft Visual Basic.

If your debugging function has no parameters, make sure to use parentheses so that the Visual C++ debugger knows to call your debugging function. For example, if your debugging function is void MyMemCheck ( ), you'd call it in the Watch window with "MyMemCheck ( )". If your debugging function takes parameters, just pass them as if you're calling the function normally. If your debugging function returns a value, the right-hand side of the Watch window will display the return value.

You'll face a few limitations when calling debugging functions in the Watch window. These limitations shouldn't cause you any problems, though, as long as you follow a few rules. The first rule is that the function can execute only in a single thread context while it's in the Watch window. If you have a multithreaded program, you should enter the debugging function into the Watch window, check the results, and then immediately delete it from the Watch window. If the debugging function does execute in a thread other than the first one it executed in, the second thread will immediately terminate. The second rule is that the debugging function must execute in less than 20 seconds; if the debugging function has an exception, your entire program will terminate under the debugger. The final rule is common sense: only do memory reads with data verifications. If you have a problem, just call OutputDebugString or printf. There's no telling what can happen if you start changing memory or calling Windows API functions.

Keep in mind that your debugging function executes whenever the Watch window reevaluates the expressions in it. That happens in the following conditions:

  • When the program is running and a breakpoint triggers
  • When you single-step a line or an instruction
  • When you finish editing the debugging function text in the left-hand side of the Watch window and press Enter
  • When an exception occurs in your running program and drops you back to the debugger

I need to stress again that you should get into the habit of entering your special debugging function, letting it evaluate, and immediately deleting it from the Watch window. That way, you avoid any surprises and you control when the debugging function executes. In addition, you don't need to write debugging functions for every little thing in your application. I write them only for the most critical data structures, especially if the structure is one that I need to see in its entirety. I also write them to validate data that must be coordinated. For example, if structure A is supposed to have a field that corresponds to a field in structure B and both fields need to be updated to keep data coherence, a debugging function that you can call from the Watch window is a good way to coordinate the fields. Finally, don't bother with dump functions for individual structures. The Watch window already expands structures for you, so you needn't waste your time reinventing that particular wheel.

Expanding Your Own Types Automatically

Although the Visual C++ documentation barely mentions this topic, you can have your own types automatically expanded in the Watch window as well as in the QuickWatch window and in DataTips. You've probably seen a few common types, such as CObject and RECT, expand in the Watch window without realizing that you could easily arrange for your own types to benefit from the Watch window's expansiveness. The magic happens in the AUTOEXP.DAT text file in the <VS Common>\MSDev98\Bin subdirectory. Just add an entry for your own types at the bottom of the file.

As an example, I'll add an auto expand entry for the PROCESS_INFORMATION structure that is passed to the CreateProcess API function. The first step is to check what the Visual C++ debugger recognizes as the type. In a sample program, I put a PROCESS_INFORMATION variable in the Watch window, right-clicked it, and selected Properties from the menu. In the Program Variable Properties dialog box, the type was _PROCESS_INFORMATION, which if you look at the structure definition below, matches the structure tag.

 typedef struct _PROCESS_INFORMATION {     HANDLE hProcess;     HANDLE hThread;      DWORD dwProcessId;     DWORD dwThreadId; } PROCESS_INFORMATION 

The documentation in AUTOEXP.DAT says that the format for an auto expand entry is "type=[text]<member[,format]>...". Table 5-5 shows the meanings for each field. Note that more than one member can be displayed as part of the auto expand.

Table 5-5 AUTOEXP.DAT Auto Expand Entries

Field Description
type The type name. For template types, this field can be followed by "<*>" to encompass all derived types.
text Any literal text. This field is generally the member name or a shorthand version of it.
member The actual data member to display. This field can be an expression, so if you need to add some offsets to various pointers you can include the offsets in the calculation. The casting operators also work.
format Additional format specifiers for the member variables. These specifiers are the same as the formatting symbols shown in Table 5-3.

With the PROCESS_INFORMATION structure, I'm interested in looking at the hProcess and hThread values, so my auto expand rule would be "_PROCESS _INFORMATION =hProcess=<hProcess,X> hThread=<hThread,X>." I use the ",X" format specifiers because I always want to see the values as hexadecimal values.

One special formatting code you'll see in the file is "<,t>." This code tells the debugger to put in the type name of the most derived type. For example, if you have a base class A with a derived class B and only A has an auto expand rule, the auto expand for a variable of type B will be the class name B followed by the auto expand rule for class A. The "<,t>" format is very helpful for keeping your classes straight.

The Set Next Statement Command

One of the coolest hidden features in the debugger is the Set Next Statement command. It is accessible in both source windows and the Disassembly window on the right-click 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. You can also change the instruction pointer in this way by setting the Intel CPU EIP register directly. 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.

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 optimized release builds, however, 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 the next chapter, 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 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 will allow 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 if the condition fails. All you need to do is to 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 Debug menu, 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.

Debugging Visual Basic Compiled Code

The whole trick to debugging compiled Visual Basic code is to test and debug your application thoroughly as p-code before you ever compile it. Debugging compiled Visual Basic isn't as easy as debugging C code because the debug information that Visual Basic generates doesn't have sufficient type information in it; consequently, the debugger can't decipher the various objects. Before you resort to the Visual C++ debugger, you should use a third-party tool such as Compuware NuMega's SmartCheck because it will make Visual Basic debugging much easier. SmartCheck knows how to convert the obtuse Visual Basic error messages into exactly what conditions lead up to the problem so that you don't need to use the Visual C++ debugger. SmartCheck can also produce a complete flow of your Visual Basic application so that you can see how your program executed. Additionally, for compiled code, you should use conditionally compiled OutputDebugString function calls to assist you in tracking the problem. In many ways, I've found it easier to debug compiled Visual Basic code at the assembly-language level. I know that sounds crazy, but with all the gyrations that happen in Visual Basic code, it really is easier. Chapter 6 will help you brush up on your assembly-language skills.

To prepare your Visual Basic files for debugging, you must first generate the PDB files when you compile. You can set this option on the Compile tab of the Project Properties dialog box. It also helps to set the compiler to No Optimizations. Just remember to turn the optimizations back on when you start building for shipping.

In the Visual C++ debugger's Variables window, switch to the Locals tab so that you can see the standard-type local variables. Visual Basic uses many temporary variables, so the tab shows many "unnamed_var1" variables. If you scroll down to the bottom of the window, you'll see the local variables.

When I started playing with Visual Basic, I was confused when I'd get the occasional exception, "Floating-point inexact result." I knew I wasn't doing any floating-point number manipulation, so I had no idea what was causing this message to trigger. After doing a bit of detective work, I discovered that Visual Basic uses its own version of structured exception handling (SEH). Unfortunately, it uses the EXCEPTION_FLT_INEXACT_RESULT value for one of its exception values, and when the exception isn't handled, you and I see the misleading exception message.

One trick I've seen some folks use, especially when they're testing a compiled ActiveX control, is to run the entire Visual Basic environment under the Visual C++ debugger. This trick allows you to debug the ActiveX control and use a p-code program as the test harness. Being able to step between a p-code test program and a compiled component allows you to see both sides of the equation more easily.



Debugging Applications
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2000
Pages: 122
Authors: John Robbins

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