A major feature of the Delphi IDE is the integrated debugger. The debugger enables you to easily set breakpoints, watch variables, inspect objects, and do much more. Using the debugger, you can quickly find out what is happening (or not happening) with your program as it runs. A good debugger is vital to efficient program development.
Debugging is easy to overlook. Don't tell anyone, but when I first started Windows programming, I ignored the debugger for a long time because I had my hands full just learning how to do Windows programming. When I found out how valuable a good debugger is, I felt a little silly for cheating myself out of the use of that tool for so long. Oh well, live and learn. You have the luxury of learning from my mistakes. Today, you learn about what the debugger can do for you.
The IDE debugger provides several features and tools to help you in your debugging chores. The following are discussed today:
The quick answer is that the debugger helps you find bugs in your program. But the debugging process isn't just for finding and fixing bugs--it is a development tool as well. As important as debugging is, many programmers don't take the time to learn how to use all the features of the IDE debugger. As a result, they cost themselves time and money, not to mention the frustration caused by a bug that is difficult to find.
You begin a debugging session by starting up the program under the debugger. You automatically use the debugger when you click the Run button on the toolbar. You can also choose Run|Run from the main menu or press F9 on the keyboard.
Before getting into the details of the debugger, let's review the menu items that pertain to the debugger. Some of these menu items are on the main menu under Run, and others are on the Code Editor context menu. Table 10.1 lists the Code Editor context menu items specific to the debugger along with their descriptions.
Item | Shortcut | Description |
Toggle Breakpoint | F5 | Toggles a breakpoint on or off for the current line in the Code Editor. |
Run to Cursor | F4 | Starts the program (if necessary) and runs it until the line in the editor window containing the cursor is reached. |
Item | Shortcut | Description |
Inspect | Alt+F5 | Opens the Debug Inspect window for the object under the cursor. |
Goto Address | Ctrl+Alt+G | Enables you to specify an address in the program at which program execution will resume. |
Evaluate/Modify | Ctrl+F7 | Enables you to view and/or modify a variable at runtime. |
Add Watch at Cursor | Ctrl+F5 | Adds the variable under the cursor to the Watch List. |
View CPU | Ctrl+Alt+C | Displays the CPU window. |
The Run item on the main menu has several selections that pertain to running programs under the debugger. The Run menu items enable you to start a program under the debugger, to terminate a program running under the debugger, and to specify command-line parameters for your program, to name just a few functions. Some items found here are duplicated on the Code Editor context menu. Table 10.2 shows the Run menu items that control debugging operations.
Item | Shortcut | Description |
Run | F9 | Compiles the program (if needed) and then runs the program under the control of the IDE debugger. Same as the Run toolbar button. |
Parameters | None | Enables you to enter command-line parameters for your program and to assign a host application when debugging a DLL. |
Step Over | F8 | Executes the source code line at the execution point and pauses at the next source code line. |
Trace Into | F7 | Traces into the method at the execution point. |
Trace to Next Source Line | Shift+F7 | Causes the execution point to move to the next line in the program's source code. |
Run to Cursor | F4 | Runs the program and pauses when program execution reaches the current line in the source code. |
Show Execution Point | None | Displays the program execution point in the Code Editor. Scrolls the source code window if necessary. Works only when program execution is paused. |
Program Pause | None | Pauses program execution as soon as the execution point enters the program's source code. |
Program Reset | Ctrl+F2 | Unconditionally terminates the program and returns to the Delphi IDE. |
Inspect | None | Displays the Inspect dialog box so that you can enter the name of an object to inspect. |
Evaluate/Modify | Ctrl+F7 | Displays the Evaluate/Modify dialog box. |
Add Watch | Ctrl+F5 | Displays the Watch Properties dialog box. |
Add Breakpoint | None | Displays a submenu that contains items to add a source, address, data, or module load breakpoint. |
You will use these menu items a lot when you are debugging your programs. You should also become familiar with the various keyboard shortcuts for the debugging operations. Now let's take a look at breakpoints and how to use them in your program.
When you run your program from the Delphi IDE, it runs at full speed, stopping only where you have set breakpoints.
New Term: A breakpoint is a marker that tells the debugger to pause program execution when it reaches that place in the program.
To set a breakpoint, click in the editor window's gutter to the left of the line on which you want to pause program execution (the gutter is the gray margin along the Code Editor window's left edge). The breakpoint icon (a red circle) appears in the gutter and the entire line is highlighted in red. To clear the breakpoint, click on the breakpoint icon and the breakpoint is removed. You can also press F5 or choose Toggle Breakpoint from the Code Editor context menu to toggle a breakpoint on or off.
NOTE: A breakpoint can be set only on a line that generates actual code. Breakpoints are not valid if set on blank lines, comment lines, or declaration lines. You are not prevented from setting a breakpoint on these types of lines, but the debugger warns you if you do. Attempting to set a breakpoint on any of the following lines will produce an invalid breakpoint warning:{ This is a comment followed by a blank line. } X : Integer; { a declaration }
Breakpoints can be set on a function or procedure's end statement.
If you set a breakpoint on an invalid line, the Code Editor will display the breakpoint in green (assuming the default color scheme) and the breakpoint icon in the gutter will be grayed.
When the program is run under the debugger, it behaves as it normally would--until a breakpoint is hit, that is. When a breakpoint is hit, the IDE is brought to the top and the breakpoint line is highlighted in the source code. If you are using the default colors, the line where the program has stopped is highlighted in red because red indicates a line containing a breakpoint.
New Term: The execution point indicates the line that will be executed next in your source code.
As you step through the program, the execution point is highlighted in blue and the editor window gutter displays a green arrow glyph. Understand that the line highlighted in blue has not yet been executed but will be when program execution resumes.
NOTE: The current execution point is highlighted in blue unless the line containing the execution point contains a breakpoint. In that case, the line is highlighted in red. The green arrow glyph in the gutter is the most accurate indication of the execution point because it is present regardless of the line's highlighting color.
When you stop at a breakpoint, you can view variables, view the call stack, browse symbols, or step through your code. After you have inspected any variables and objects, you can resume normal program execution by clicking the Run button. Your application will again run normally until the next breakpoint is encountered.
NOTE: It's common to detect coding errors in your program after you have stopped at a breakpoint. If you change your source code in the middle of a debugging session and then choose Run to resume program execution, the IDE will prompt you with a message box asking whether you want to rebuild the source code. If you choose Yes, the current process will be terminated, the source code will be recompiled, and the program will be restarted.
The problem with this approach is that your program doesn't get a chance to close normally, and any resources currently in use might not be freed properly. This scenario will almost certainly result in memory leaks. Although Windows 95 and Windows NT handle resource leaks better than 16-bit Windows, it is still advisable to terminate the program normally and then recompile it.
The Delphi IDE keeps track of the breakpoints you set. These breakpoints can be viewed through the Breakpoint List window. To view the breakpoint list, choose View|Debug Windows|Breakpoints from the main menu. The Breakpoint List window is displayed as shown in Figure 10.1.
FIGURE 10.1. The Breakpoint List window.
The Breakpoint List window has four columns:
The columns can be sized by dragging the dividing line between two columns in the column header.
NOTE: The Pass column doesn't show the number of times the breakpoint has been hit; it only shows the pass condition that you have set for the breakpoint.
The Breakpoint List window has two context menus. Table 10.3 lists the context menu items you will see when you click the right mouse button over any breakpoint. I will refer to this as the window's primary context menu.
Item | Description |
Enable | Enables or disables the breakpoint. When a breakpoint is disabled, its glyph is grayed out in the Breakpoint List window. In the source window, the breakpoint glyph is also grayed, and the breakpoint line is highlighted in green to indicate that the breakpoint is disabled. |
Delete | Removes the breakpoint. |
View Source | Scrolls the source file in the Code Editor to display the source line containing the breakpoint. (The Breakpoint List retains focus.) |
Edit Source | Places the edit cursor on the line in the source file where the breakpoint is set and switches focus to the Code Editor. |
Properties | Displays the Source Breakpoint Properties dialog box. |
Dockable | Determines whether the Breakpoint List window is dockable. |
TIP: To quickly edit the source code line on which a breakpoint is set, double-click on the breakpoint in the Filename column of the Breakpoint List window. This is the same as choosing Edit Source from the Breakpoint List context menu.
The secondary context menu is displayed by clicking the right mouse button while the cursor is over any part of the Breakpoint List window that doesn't contain a breakpoint. This context menu has items called Add, Delete All, Disable All, Enable All, and Dockable. These items are self-explanatory, so I won't bother to comment on them.
NOTE: In my opinion, the Add context menu item isn't very useful. It is much easier to set a breakpoint in the Code Editor than to add a breakpoint via the Add command in the Breakpoint List window.
Breakpoints can be enabled or disabled any time you like. You disable a breakpoint if you want to run the program normally for a while; you can enable the breakpoint later without having to re-create it. The debugger ignores breakpoints that are disabled. To enable or disable a breakpoint, right-click on the breakpoint in the Breakpoint List window and toggle the Enable item on the context menu.
If you want to modify a breakpoint, choose Properties from the primary Breakpoint List context menu. When you do, the Source Breakpoint Properties dialog box is displayed (see Figure 10.2).
FIGURE 10.2. The Source Breakpoint Properties dialog box.
The primary reason for modifying a breakpoint is to add conditions to it. (Conditional breakpoints are discussed in the section "Conditional Breakpoints.")
To remove a breakpoint, select the breakpoint in the Breakpoint List window and then press the Delete key on the keyboard. To delete all breakpoints, right-click and then choose Delete All. Now let's take a look at the two breakpoint types: simple and conditional.
A simple breakpoint causes program execution to be suspended whenever the breakpoint is hit. When you initially set a breakpoint, it is by default a simple breakpoint. Simple breakpoints don't require much explanation. When the breakpoint is encountered, program execution pauses and the debugger awaits your bidding. Most of the time you will use simple breakpoints. Conditional breakpoints are reserved for special cases in which you need more control over the debugging process.
In the case of a conditional breakpoint, program execution is paused only when predefined conditions are met. To create a conditional breakpoint, first set the breakpoint in the Code Editor. Then choose View|Debug Windows|Breakpoints from the main menu to display the Breakpoint List window. Right-click on the breakpoint for which you want to set conditions and choose Properties. When the Source Breakpoint Properties dialog box is displayed, set the conditions for the breakpoint.
Conditional breakpoints come in two flavors. The first type is a conditional expression breakpoint. Enter the conditional expression in the Condition field of the Source Breakpoint Properties dialog box (refer to Figure 10.2). When the program runs, the conditional expression is evaluated each time the breakpoint is encountered. When the conditional expression evaluates to True, program execution is halted. If the condition doesn't evaluate to True, the breakpoint is ignored. For example, look back at the last breakpoint in the Breakpoint List window shown in Figure 10.1. This breakpoint has a conditional expression of X > 20. If at some point in the execution of the program X is greater than 20, the program will stop at the breakpoint. If X is never greater than 20, program execution will not stop at the breakpoint.
The other type of conditional breakpoint is the pass count breakpoint. With a pass count breakpoint, program execution is paused only after the breakpoint is encountered a specified number of times. To specify a pass count breakpoint, edit the breakpoint and specify a value for the Pass Count field in the Source Breakpoint Properties dialog box. If you set the pass count for a breakpoint to 3, program execution will stop at the breakpoint the third time the breakpoint is encountered.
NOTE: The pass count is 1-based, not 0-based. As indicated in the preceding example, a pass count of 3 means that the breakpoint will be valid the third time the breakpoint is encountered by the program.
Use pass count breakpoints when you need your program to execute through a breakpoint a certain number of times before you break to inspect variables, step through code, or perform other debugging tasks.
NOTE: Conditional breakpoints slow down the normal execution of the program because the conditions need to be evaluated each time a conditional breakpoint is encountered. If your program is acting sluggish during debugging, check your breakpoint list to see whether you have conditional breakpoints that you have forgotten about.
TIP: The fact that conditional breakpoints slow down program execution can work in your favor at times. If you have a process that you want to view in slow motion, set one or more conditional breakpoints in that section of code. Set the conditions so that they will never be met, and your program will slow down but not stop.
There is another debugging command that deserves mention here. The Run to Cursor command (found on the Run menu on the main menu and on the Code Editor context menu) runs the program until the source line containing the editing cursor is reached. At that point, the program stops as if a breakpoint were placed on that line.
Run to Cursor acts as a temporary breakpoint. You can use this command rather than set a breakpoint on a line that you want to immediately inspect. Just place the cursor on the line you want to break on and choose Run to Cursor (or press F4). The debugger behaves exactly as if you had placed a breakpoint on that line. The benefit is that you don't have to clear the breakpoint after you are done debugging that section of code.
So what do you do when you stop at a breakpoint? Usually you stop at a breakpoint to inspect the value of one or more variables. You might want to ensure that a particular variable has the value you think it should, or you might not have any idea what a variable's value is and simply want to find out.
The function of the Watch List is basic: It enables you to inspect the values of variables. Programmers often overlook this simple but essential feature because they don't take the time to learn how to fully use the debugger. You can add as many variables to the Watch List as you like. Figure 10.3 shows the Watch List during a debugging session.
FIGURE 10.3. The Watch List in action.
The variable name is displayed in the Watch List followed by its value. How the variable value is displayed is determined by the variable's data type and the current display settings for that watch item. I'll discuss the Watch List window in detail in just a bit, but first I want to tell you about a feature that makes inspecting variables easy.
The debugger and Code Editor have a nice feature that makes checking the value of a variable easy. This feature, the Tooltip expression evaluator, is on by default, so you don't have to do anything special to use it. If you want, you can turn off the Tooltip evaluator via the Code Insight page of the Environment Options dialog box (the Code Insight page was discussed yesterday).
So what is Tooltip expression evaluation (besides hard to say)? It works like this: After you stop at a breakpoint, you place the editing cursor over a variable and a tooltip window pops up showing the variable's current value. This makes it easy to quickly inspect variables. Just place your cursor over a variable and wait a half second or so.
The Tooltip evaluator has different displays for different variable types. For regular data members (Integer, Char, Byte, string, and so on), the actual value of the variable is displayed. For dynamically created objects (an instance of a class, for example), the Tooltip evaluator shows the memory location of the object. For records, the Tooltip evaluator shows all the record elements. Figure 10.4 shows the Tooltip expression evaluator inspecting a record's contents.
FIGURE 10.4. Tooltips are a great debugger feature.
NOTE: Sometimes the Tooltip evaluator acts as if it's not working properly. If, for example, you place the editing cursor over a variable that is out of scope, no tooltip appears. The Tooltip evaluator has nothing to show for that particular variable, so it doesn't display anything.
Be aware, also, that variables optimized by the compiler might not show correct values. Optimization was discussed yesterday and is also discussed later in this chapter.
Another case where the Tooltip evaluator doesn't work is within a with block. Take this code, for example:
with Point do begin X := 20; Y := 50; Label1.Caption := IntToStr(X); end;
If you were to place the mouse cursor over the variable X, the Tooltip evaluator would not report the value of X because X belongs to the target of the with statement (the Point variable). Instead, place the mouse cursor over the Point variable, and the debugger shows you the value of Point (including the X field).
The Tooltip expression evaluator is a great feature, so don't forget to use it.
As with every other Delphi window discussed so far, the Watch List has its own context menu. (You'd be disappointed if it didn't, right?) Table 10.4 lists the Watch List context menu items and their descriptions.
Item | Description |
Edit Watch | Enables you to edit the watch item with the Watch Properties dialog box. |
Add Watch | Adds a new item to the Watch List. |
Enable Watch | Enables the watch item. |
Disable Watch | Disables the watch item. |
Delete Watch | Removes the watch item from the Watch List. |
Enable All Watches | Enables all items in the Watch List. |
Disable All Watches | Disables all items in the Watch List. |
Delete All Watches | Deletes all items in the Watch List. |
Stay on Top | Forces the Watch List to the top of all other windows in the IDE. |
Break When Changed | When the variable in the watch window changes, the debugger will break. The watch variable is displayed in red to indicate that Break When Changed is in effect. |
Dockable | Determines whether the Watch List window is dockable. |
Both the Edit Watch and Add Watch context menu items invoke the Watch Properties dialog box, so let's look at that next.
You use the Watch Properties dialog box when you add or edit a watch. Figure 10.5 shows the Watch Properties dialog box as it looks when editing a variable called Buff.
FIGURE 10.5. The Watch Properties dialog box.
The Expression field at the top of the Watch Properties dialog box is where you enter a variable name to edit or add to the watch list. This field is a combo box that can be used to select previously used watch items.
You use the Repeat count field when you are inspecting arrays. For example, let's say you have an array of 20 integers. To inspect the first 10 integers in the array, you would enter the first element of the array in the Expression field (Array[0], for example) and then enter 10 in the Repeat Count field. The first 10 elements of the array would then be displayed in the Watch List.
NOTE: If you add just the array name to the Watch List, all elements in the array will be displayed. Use the Repeat Count field when you want to view only a specific number of array elements.
The Digits field is used only when inspecting floating-point numbers. Enter the number of significant digits you want to see when your floating-point number is displayed in the Watch List. The displayed digits are rounded, not truncated. Another field in this dialog box, the Enabled field, determines whether the watch item is currently enabled.
The remainder of the Watch Properties dialog box is composed of various display options. Each data type has a default display type, which is used if you choose the Default viewing option. The Default viewing option is the default. (Sorry, there's just no other way to say it.) Select the other viewing options to view the data in other ways. Figure 10.6 shows the Watch List window with two variables added and with various viewing options applied. The Buff variable is a character array, and the I variable is an integer.
FIGURE 10.6. The Watch List with various viewing options.
To modify a watch item, click the item in the Watch List and choose Edit Watch from the Watch List context menu. You can also double-click a watch item to edit it. The Watch Properties dialog box is displayed, and you can edit the watch item as needed.
TIP: The fastest way to edit a watch item is to double-click its name in the Watch List.
As with breakpoints, individual items in the Watch List can be enabled or disabled. When a watch item is disabled, it is grayed and its value shows <disabled>.
To disable a watch item, click the item's name in the Watch List and choose Disable Watch from the Watch List context menu. To enable the watch item again, choose Enable Watch from the context menu.
NOTE: You might want to disable watch items that you don't currently want to watch but will need later. Having a number of enabled items in the Watch List slows down program execution during the debugging process because all the Watch List variables must be updated each time a code line executes.
You can add variables to the Watch List in several ways. The quickest way is to click the variable name in the editor window and then select Add Watch at Cursor from the Code Editor context menu or press Ctrl+F5. The watch item will be immediately added to the Watch List. You can then edit the watch item to change the display properties, if needed.
To add a variable to the watch without first locating it in the source file, choose Run|Add Watch from the main menu. When the Watch Properties dialog box comes up, enter the name of the variable you want to add to the Watch List and click OK.
NOTE: Although you can add a class instance variable to the Watch List, the displayed value will not likely be useful. For viewing all the class data members, you should use the Debug Inspector, which I'll discuss in a minute.
When a breakpoint is hit, the Watch List displays the current value of any variables that have been added to the Watch List. If the Watch List isn't currently open, you can choose View|Debug Windows|Watches from the main menu to display it.
TIP: Dock the Watch List window to the bottom of the Code Editor window so that it will always be in view when stepping through code.
Under certain circumstances, a message will be displayed next to the variable instead of the variable's value. If, for example, a variable is out of scope or not found, the Watch List displays Undeclared identifier:'X' next to the variable name. If the program isn't running or isn't stopped at a breakpoint, the Watch List displays [process not accessible] for all watch items. A disabled watch item will have <disabled> next to it. Other messages can be displayed depending on the current state of the application or the current state of a particular variable.
As I said yesterday, you might on occasion see Variable `X' inaccessible here due to optimization in the Watch List. This is one of the minor disadvantages of having an optimizing compiler. If you need to inspect variables that are subject to optimization, you must turn optimization off. Turn off the Optimization option on the Compiler page of the Project Options dialog box. Be aware that variables that have not been initialized (assigned a value) will report random values until they are initialized.
The Watch List is a simple but vital tool in debugging applications. To illustrate the use of the Watch List, perform this exercise:
procedure TForm1.Button1Click(Sender: TObject); var S : string; X, Y : Integer; begin X := Width; S := IntToStr(X); Y := Height; X := X * Y;S := IntToStr(X);
X := X div Y; S := IntToStr(X); Width := X; Height := Y; end;
Click the Watch Test button as many times as you want to get a feel for how the Watch List works. Experiment with different watch settings each time through.
NOTE: The code in this example obtains the values for the form's Width and Height properties, performs some calculations, and then sets the Width and Height back to where they were when you started. In the end nothing changes, but there is a good reason for assigning values to the Width and Height properties at the end of the method.
If you don't actually do something with the variables X and Y, you can't inspect them because the compiler will optimize them and they won't be available to watch. Essentially, the compiler can look ahead, see that the variables are never used, and just discard them. Putting the variables to use at the end of the method avoids having them optimized away by the compiler.
I've brought this up several times now, but I want to make sure you have a basic understanding of how an optimizing compiler works. When you start debugging your applications, this knowledge will help avoid some frustration when you start getting those Variable `X' inaccessible here due to optimization messages in the Watch List.
The Debug Inspector is a new feature in Delphi 4. Simply stated, the Debug Inspector enables you to view data objects such as classes and records. You can also inspect simple data types such as integers, character arrays, and so on, but those are best viewed with the Watch List. The Debug Inspector is most useful in examining classes and records.
NOTE: You can use the Debug Inspector only when program execution is paused under the debugger.
To inspect an object, click the object's name in a source file and choose Inspect from the Code Editor context menu (or press Alt+F5). You can also choose Run|Inspect from the main menu.
The Debug Inspector window contains details of the object displayed. If the object is a simple data type, the Debug Inspector window shows the current value (in both decimal and hex for numeric data types) and the status line at the bottom displays the data type. For example, if you inspect an integer variable, the value will be shown and the status bar will say Integer. At the top of the Debug Inspector is a combo box that initially contains a description of the object being inspected.
If you are inspecting a class, the Debug Inspector will look like Figure 10.7.
FIGURE 10.7. The Debug Inspector inspecting a form class.
To better understand the Debug Inspector, follow these steps:
NOTE: You can inspect Self only from within a method of a class. If you happen to set a breakpoint in a regular function or procedure and then attempt to inspect Self, you will get an error message stating that Self is an invalid symbol. In the previous example, Self refers to the application's main form.
When inspecting classes, the Debug Inspector window contains three pages, as you can see. The first items listed are the data items that belong to the ancestor class. At the end of the list are the items that belong to the immediate class. You can choose whether to view the ancestor class information. To turn off the ancestor class items, right-click and select Show Inherited from the Debug Inspector context menu.
By using the arrow keys to move up and down the data members list, you can tell at a glance what each data member's type is (look at the status bar on the Debug Inspector window). To further inspect a data member, double-click the value column on the line showing the data member. A second Debug Inspector window is opened with the selected data member displayed. You can have multiple Debug Inspector windows open simultaneously.
The Methods page of the Debug Inspector displays the class's methods. In some cases the Methods tab isn't displayed (when inspecting simple data types, for example). The status bar shows the selected method's declaration.
The Properties page of the Debug Inspector shows the properties for the class being inspected. Viewing the properties of a class is of limited value (the information presented is not particularly useful). Most of the time you can accomplish what you are after by inspecting the data member associated with a particular property on the Data page.
NOTE: The Methods page and the Properties page of the Debug Inspector are available only when you're inspecting a class. When you're inspecting simple data types, the Data page alone is displayed.
TIP: If you want your Debug Inspector windows always on top of the Code Editor, go to the Debugger page of the Environment Options dialog box and check the Inspectors stay on top check box.
The Debug Inspector context menu has several items that enable you to work with the Debug Inspector and the individual variables. For example, rather than open a new Debug Inspector window for each object, you can right-click and choose Descend to replace the current object in the Debug Inspector window with the selected object. For example, if you are inspecting a form with a button called Button1, you can select Button1 in the Debug Inspector and choose Descend from the context menu. The Debug Inspector will then be inspecting Button1. This method has an added advantage: The IDE keeps a history list of the objects you inspect. To go back to an object you have already inspected, just choose the object from the combo box at the top of the Debug Inspector window. Choosing one of the objects in the history list will again show that object in the Debug Inspector window.
The Change item on the Debug Inspector context menu enables you to change a variable's value.
CAUTION: Take great care when changing variables with the Debug Inspector. Changing the wrong data member or specifying a value that is invalid for that data member might cause your program to crash.
The Inspect item on the Debug Inspector's context menu enables you to open a second Debug Inspector window with the item under the cursor displayed. The New Expression context menu item enables you to enter a new expression to inspect in the Debug Inspector.
The Show Inherited item on the Debug Inspector context menu is a toggle that determines how much information the Debug Inspector should display. When the Show Inherited option is on, the Debug Inspector shows all data members, methods, and properties of the class being inspected as well as the data members, methods, and properties of the immediate ancestor class. When the Show Inherited option is off, only the data members, methods, and properties of the class itself are shown. Turning off this option can speed up the Debug Inspector because it doesn't have as much information to display.
TIP: If you have a class data member and you don't remember its type, you can click on it when you are stopped at a breakpoint and press Alt+F5 to display the Debug Inspector. The status bar at the bottom of the Debug Inspector window will tell you the variable's data type.
Delphi has some additional debugging tools to aid you in tracking down bugs. Some of these tools are, by nature, advanced debugging tools. Although the advanced debugging tools are not as commonly used as the other tools, they are very powerful in the hands of an experienced programmer.
The Evaluate/Modify dialog box enables you to inspect a variable's current value and to modify the value if you want. Using this dialog box, you can test for different outcomes by modifying a particular variable. This enables you to play a what-if game with your program as it runs. Changing the value of a variable while debugging allows you to test the effects of different parameters of your program without recompiling each time. Figure 10.8 shows the Evaluate/Modify dialog box inspecting a variable.
FIGURE 10.8. The Evaluate/Modify dialog box.
NOTE: The Evaluate/Modify dialog box's toolbar can display either large toolbar buttons or small toolbar buttons. By default, it shows small toolbar buttons. The small buttons don't have captions, so you will have to pass your mouse cursor over the buttons and read the tooltip to see what each button does. To see the large toolbar buttons, drag the sizing bar immediately below the toolbar downward to resize the toolbar. The toolbar will then show the large toolbar buttons with captions underneath each button. Figure 10.8 shows the Evaluate/Modify dialog box with large toolbar buttons.
The Evaluate/Modify dialog box works similarly to the Watch List or the Debug Inspector. If you click a variable in the source code and choose Evaluate/Modify from the Code Editor context menu, the variable will be evaluated. If you want to enter a value not currently showing in the source code, you can choose Run|Evaluate/Modify from the main menu and then type a variable name to evaluate.
The Expression field is used to enter the variable name or expression you want to evaluate. When you click the Evaluate button (or press Enter), the expression will be evaluated and the result displayed in the Result field.
NOTE: The Evaluate/Modify dialog box can be used as a quickie calculator. You can enter hex or decimal numbers (or a combination) in a mathematical formula and have the result evaluated. For example, if you type$400 - 256
in the Evaluate field and press Enter, the result, 768, is displayed in the Result field.
You can also enter logical expressions in the Evaluate field and have the result shown in the Result field. For example, if you enter
20 * 20 = 400
the Result field would show True. The program must be stopped at a breakpoint for the Evaluate/Modify dialog box to function.
If you want to change a variable's value, enter a new value for the variable in the New Value field and click the Modify button. The variable's value will be changed to the new value entered. When you click the Run button to restart the program (or continue stepping), the new value will be used.
NOTE: The Evaluate/Modify dialog box doesn't update automatically when you step through your code, as do the Watch List and Debug Inspector. If your code modifies the variable in the Evaluate/Modify dialog box, you must click the Evaluate button again to see the results. This aspect of the Evaluate/Modify dialog box has one primary benefit; stepping through code is quicker because the debugger doesn't have to evaluate the expression each time you step (as it does for the Watch List and Debug Inspector). A typical interaction with this dialog box would be to evaluate a variable or expression and then immediately close the Evaluate/Modify dialog box.
While your program is running, you can view the call stack to inspect any functions or procedures your program called. From the main menu, choose View|Debug Windows|Call Stack to display the Call Stack window. This window displays a list of the functions and procedures called by your program and the order in which they were called. The most recently called function or procedure is at the top of the window.
Double-clicking a method name in the Call Stack window takes you to the source code line for that method if the method is in your program. In case of functions or procedures for which there is no source code (VCL methods, for example), the Call Stack window contains just an address and the name of the module where the procedure is located. Double-clicking a listed function or procedure without source code will display the CPU window (the CPU window is discussed in the next section).
Viewing the call stack is most helpful after a Windows Access Violation error. By viewing the call stack, you can see where your program was just before the error occurred. Knowing where your program was just before it crashed is often the first step in determining what went wrong.
TIP: If the call stack list contains seemingly nonsensical information, it could be that the call stack was corrupted. A corrupted call stack is usually an indicator of a stack overflow or a memory overwrite. A stack overflow isn't as likely to occur in a 32-bit program as in a 16-bit program, but it still can happen.
Officially speaking, the CPU window is new to Delphi 4. You could get the CPU window in previous versions of Delphi, but only if you knew the magical Registry entry. The CPU window is now officially part of Delphi and can be found on the main menu under View|Debug Windows|CPU (Ctrl+Alt+C on the keyboard).
The CPU window enables you to view your program at the assembly instruction level. Using this view, you can step into or over instructions one assembly instruction at a time. You can also run the program to a certain assembly instruction just as you can run the program to a certain source line with the regular debugger. The CPU window has five panes: the disassembly pane, the register pane, the flags pane, the raw stack pane, and the dump pane.
Each pane has a context menu associated with it. The context menus provide all the functions necessary to use that pane. To be used effectively, the CPU window requires a knowledge of assembly language. Obviously, the CPU window is an advanced debugging feature.
The Go to Address command is also an advanced debugging tool. When your program crashes, Windows displays an error message showing the address of the violation. You can use the Go to Address command to attempt to find out where in your program the crash occurred. When you get an Access Violation error message from Windows, you see a dialog box similar to the one shown in Figure 10.9.
FIGURE 10.9. A Windows message box reporting an access violation.
When you see this error message, write down the address at which the violation occurred and then choose Debug|Go to Address from the Code Editor context menu to display the Goto Address dialog box. Enter the address you just wrote down in the Address field of the Goto Address dialog box.
When you click OK, the debugger will attempt to find the source code line where the error occurred. If the error occurred in your code, the cursor will be placed on the line that generated the error. If the error occurred somewhere outside your code, you will get a message box saying that the address could not be found. As I said, this is an advanced debugging tool and one that you might never use.
Stepping through code is one of the most basic debugging operations, yet it still needs to be mentioned here. Sometimes you fail to see the forest for the trees. (Just as sometimes authors of programming books fail to include the obvious!) Reviewing the basics from time to time can reveal something you were not previously aware of.
Before beginning this section, I'll say a few words about the symbols that appear in the Code Editor gutter during a debugging session. In the section "Setting and Clearing Breakpoints," I told you that a red circle appears in the gutter when you set a breakpoint on a code line. I also said that a green arrow glyph indicates the execution point when you are stepping through code.
One point I didn't mention, though, is the little blue dots that appear in the gutter next to certain code lines. These dots indicate lines in your source code that actually generate assembly code. Figure 10.10 shows the Code Editor with the debugger stopped at a breakpoint. It shows the small dots that indicate generated code, the arrow glyph indicating the execution point, and the breakpoint glyph as well. The check mark on the breakpoint glyph indicates that the breakpoint was checked and was determined to be a valid breakpoint.
FIGURE 10.10. The Code Editor showing gutter symbols.
Take a closer look at Figure 10.10. Notice that the small dots only appear next to certain code lines. Lines without the dots don't generate any compiled code. Take these lines, for example:
var S : string; X : Integer;
Why don't these lines generate code? Because they are variable declarations. How about this line:
X := 20;
Why is no code generated for this line? Here's that word again: optimization. The compiler looks ahead and sees that the variable X is never used, so it completely ignores all references to that variable. Finally, notice these lines:
{$IFNDEF WIN32} S := `Something's very wrong here...'; {$ENDIF}
The compiler doesn't generate code for the line of source code between the compiler directives because the symbol WIN32 is defined in a Delphi 4 program. The compiler $IFNDEF WIN32 directives tell the compiler, "Compile this line of code only if the target platform is not 32-bit Windows." Because Delphi 4 is a 32-bit compiler, this line of code is not compiled. This line of code will be compiled if this code is compiled in Delphi 1 (a 16-bit environment).
Okay, back to stepping through code. When you stop at a breakpoint, you can do many things to determine what is going on with your code. You can set up variables to watch in the Watch List, inspect objects with the Debug Inspector, or view the call stack. You can also step through your code to watch what happens to your variables and objects as each code line is executed.
As you continue to step through your code, you will see that the line in your source code to be executed next is highlighted in blue. If you have the Watch List and Debug Inspector windows open, they will be updated as each code line is executed. Any changes to variables or objects will be immediately visible in the watch or inspector window. The IDE debugger has two primary stepping commands to aid in your debugging operations: Step Over and Trace Into.
Step Over means to execute the next line in the source code and pause on the line immediately following. Step Over is sort of a misnomer. The name indicates that you can step over a source line and the line won't be executed. That isn't the case, however. Step Over means that the current line will be executed and any functions or procedures called by that source line will be run at full speed. For example, let's say you set a breakpoint at a line that calls a method in your program. When you tell the debugger to step over the method, the debugger will execute the method and stop on the next line. (Contrast this with how Trace Into works, which you'll learn about in a minute, and it will make more sense.) To use Step Over to step through your program, you can either press F8 or choose Run|Step Over from the main menu.
NOTE: As you step through various source code units in your program, the Code Editor will automatically load and display the needed source units if they are not already open.
The Trace Into command enables you to trace into any functions or procedures that are encountered as you step through your code. Rather than execute the function or procedure and return to the next line as Step Over does, Trace Into places the execution point on the first source code line in the function or procedure being called. You can then step line-by-line through that function or procedure using Step Over or Trace Into as necessary. The keyboard shortcut for Trace Into is F7.
After you have inspected variables and done whatever debugging you need to do, you can again run the program at full speed by clicking the Run button. The program will function normally until the next breakpoint is encountered.
TIP: If you have the Professional or Client/Server version of Delphi, you can step into the VCL source code. When you encounter a VCL method, Trace Into will take you into the VCL source code for that method. You can inspect whatever variables you need to see. You must add the path to the VCL source in to the Search path field of the Project Options (Directories/ Conditionals page). To enable this option, you must do a Build after adding the VCL source directory to the Search path field. Stepping into the VCL source is of doubtful benefit to most programmers. Experienced programmers, though, will find it useful.
Another, less frequently used debugging command is Trace To Next Source Line (Shift+F7 on the keyboard). You will not likely use this command a lot, particularly not until you get more familiar with debugging and Windows programming in general. Some Windows API functions use what is termed a callback function. This means that the Windows function calls one of your own functions to perform some action.
If the execution point is on a Windows API function that uses a callback, using Trace To Next Source Line will jump the execution point to the first line in the callback function. The effect is similar to Trace Into, but the specific situation in which Trace To Next Source Line is used is altogether different. If that doesn't make sense to you, don't worry about it. It's not important for what you need to learn today.
NOTE: When you are stepping through a method, the execution point will eventually get to the end statement of the method. If the method you are stepping through returns control to Windows when it finishes, pressing F8 when you're on the end statement will exit the method and return control to the program being debugged. There is no obvious indication that the program is no longer paused because the IDE still has focus. This behavior can be confusing the first few times you encounter it unless you are aware of what has happened. To switch back to your program, just activate it as you would any other program (click its button on the Windows taskbar or use Alt+Tab).
As I said, stepping through your code is a basic debugging technique, but it is one that you will use constantly while debugging. Of all the keyboard shortcuts available to you in Delphi, F7 (Trace Into), F8 (Step Over), and F9 (Run) should definitely be in your arsenal.
For the most part, debugging a DLL (dynamic link library) is the same as debugging an executable file. You place breakpoints in the DLL's code and when a breakpoint is hit, the debugger will pause just as it does when debugging an EXE. Normally you test a DLL by creating a test application and running the test application under the debugger.
Sometimes, however, you need to test a DLL for use with executable files built with other development environments. For example, let's say you are building a DLL that will be called from a Visual Basic application. You certainly can't start a VB application running under the Delphi debugger. What you can do, though, is tell the Delphi debugger to start the VB application as a host application. (Naturally, the host application has to contain code that loads the DLL.) You tell Delphi to start an external host application through the Run Parameters dialog box.
To display the Run Parameters dialog box, choose Run|Parameters from the main menu. Type an EXE name in the Host Application field, click the Load button, and the host application will run. Figure 10.11 shows the Run Parameters dialog box as it appears just before debugging a DLL.
After the host application has started, you debug your DLL just as you do when using a Delphi test application: Simply place breakpoints in the DLL and begin debugging.
FIGURE 10.11. Specifying a host application with the Run Parameters dialog box.
NOTE: The Run Parameters dialog box has a tab called Remote. This tab enables you to set the parameters for debugging an application on a remote machine. Remote debugging is an advanced topic and won't be covered here.
The Event Log is a special Delphi file that shows diagnostic messages--messages generated by Delphi, by your own applications, and sometimes by Windows itself. For example, the Event Log contains information regarding modules that are loaded (mostly DLLs), whether they include debug info, when your application was started, when it was stopped, when a breakpoint was encountered, and more. You view the Event Log through the Event Log window. To see the Event Log window, choose View|Debug Windows|Event Log from the Delphi main menu. Figure 10.12 shows the Event Log window while debugging an application.
FIGURE 10.12. The Event Log window.
The Event Log window has a context menu that enables you to clear the Log, save it to a text file, or add comments to the Event Log. Saving the Event Log to a text file enables you to browse the messages list more thoroughly or search for specific text you want to see. The Event Log window context menu also has a Properties menu item that enables you to further customize the Event Log. When you choose this menu item, a dialog box is displayed that enables you to change Event Log options. This dialog box is the same as the Event Log page in the Debugger Options dialog box (discussed later in the section "The Event Log Page").
You can send your own messages to the Event Log using the Windows API function OutputDebugString. OutputDebugString is discussed later in the section "The OutputDebugString Function."
The Module window shows you the modules currently loaded, source files attached to those modules, and symbols (functions, procedures, and variables) exported from that module. You can invoke the Module window by choosing View|Debug Windows|Modules from the main menu. The Module window is primarily an advanced debugging tool, so I won't go into a detailed discussion of its features here. You should take some time to experiment with the Module window to see how it works. Figure 10.13 shows the Module window in action.
FIGURE 10.13. The Module window.
I have already touched on a few debugging techniques as I examined the various aspects of the IDE debugger in this chapter. I now mention a few more techniques that make your debugging tasks easier.
Sometimes it is helpful to track your program's execution as your program runs. Or maybe you want to see a variable's value without stopping program execution at a breakpoint. The OutputDebugString function enables you to do exactly that. This function is a convenient debugging tool that many programmers overlook, primarily because of a general lack of discussion on the subject. Look at the last entry in the Event Log shown in Figure 10.12 (earlier in the chapter). That entry was generated using this code:
OutputDebugString(`In the Button1Click method...');
That's all you have to do. Because Delphi is installed as the system debugger, any strings sent using OutputDebugString will show up in the Event Log. You can place calls to OutputDebugString anywhere you want in your code.
To view the value of a variable, you must format a string and send that string to OutputDebugString. For example:
procedure TForm1.FormCreate(Sender: TObject); var X : Integer; S : string; begin { Some code here...} S := Format(`X := %d', [X]); OutputDebugString(PChar(S)); end;
Using OutputDebugString you can see what is going on with your program even in time-critical sections of code.
When a program attempts to write to memory that it doesn't own, Windows issues an Access Violation error message. All Windows programmers encounter access violations while developing their applications.
NOTE: The term GPF (General Protection Fault) was used in 16-bit Windows. Its use is still prevalent in the 32-bit Windows programming world even though 32-bit Windows actually generates Access Violation errors instead of General Protection Faults.
Access violations can be difficult to track down for beginning and experienced Windows programmers alike. Often, as programmers gain experience in writing Windows programs, they develop a sixth sense for locating the cause of access violations. Here are some clues to look for when trying to track down the elusive access violation. These are not the only situations that cause a program to crash, but they are some of the most common.
An uninitialized pointer is a pointer that has been declared but not set to point to anything meaningful in your program. An uninitialized pointer will contain random data. In the best case, it points to some harmless spot in memory. In the worst case, the uninitialized pointer points to a random memory location somewhere in your program. This can lead to erratic program behavior because the pointer might point to a different memory location each time the program is run. Always set a pointer to nil both before it's used for the first time and after the object it points to is deleted. If you try to access a nil pointer, your program will stop with an access violation, but the offending line in the source code will be highlighted by the debugger, and you can immediately identify the problem pointer.
Deleting a pointer that has already been deleted results in an access violation. The advice given for working with uninitialized pointers applies here as well: Set deleted pointers to nil. It is perfectly safe to delete a nil pointer. By setting your deleted pointer to nil, you ensure that no ill effects will occur if you accidentally delete the pointer a second time.
Overwriting the end of an array can cause an access violation. In some cases the overwritten memory might not be critical, and the problem might not show up right away but later the program will crash. When that happens, you will likely look for a bug at the point the program crashed, but the actual problem occurred in a completely different part of the program. In other cases the memory that is overwritten is critical, and the program will immediately stop. In extreme cases you might even crash Windows.
Array overwrites can be minimized somewhat by range checking. When you have range checking on (the default), the compiler will examine any array references to see whether you are accessing an array element outside the valid range. For example, this code will result in a compiler error:
procedure TForm1.Button1Click(Sender: TObject); var A : array [0..20] of Char; begin A[30] := `a'; end;
Here I am accessing element 30 in an array that has only 21 elements. The compiler sees that the array is accessed outside its declared range and generates a compiler error. Range checking does not work with variables, however. This code, for example, will not result in a compiler error:
procedure TForm1.Button1Click(Sender: TObject); var X : Integer; A : array [0..20] of Char; begin X := 30; A[X] := `a'; end;
Although the array is overwritten by nine bytes, no compiler error will result because the compiler doesn't know the value of X at compile time.
When a program halts with an access violation on normal shutdown, it usually indicates that the stack size is set too small. Although this isn't likely in a 32-bit program, it could happen under extreme circumstances. An access violation on program termination can also be caused by deleting an already deleted pointer, as already discussed.
In addition to the many tips offered on the preceding pages, you might want to implement these:
CAUTION: If you are running Delphi on Windows 95, use the Program Reset option sparingly. In some cases, using Program Reset to kill an application can crash Windows 95. Not all Windows 95 systems behave the same way, so you might not experience this problem. Windows NT doesn't suffer from this problem nearly to the degree that Windows 95 does, so you can use Program Reset more liberally under Windows NT. Personally, I use Program Reset only when the application I am debugging locks up.
One of the best tips I can give you on debugging is to use a memory checking program such as TurboPower Software's Memory Sleuth. You might have received Memory Sleuth as part of Delphi 4 (it was included free with Delphi 4 for a limited time). Memory Sleuth checks your programs for memory leaks. This type of program can save you a lot of trouble when you put your applications in service. If your application is leaking memory, it will cause problems for your users. Problems for your users means problems for you. By taking care of those leaks early on, you save yourself and your users frustration.
If you didn't get Memory Sleuth with Delphi 4, you can check it out at TurboPower's Web site (www.turbopower.com). Another leak-checking program is BoundsChecker by NuMega Technologies.
Debugging options can be set on two levels: the project level and the environment level. Project debugging options were discussed yesterday in the sections "The Compiler Page" and "The Linker Page." The debugging options you set at the global level can be found in the Debugger Options dialog box. To invoke the Debugger Options dialog box, choose Tools|Debugger Options from the main menu.
At the bottom of the dialog box is a check box labeled Integrated debugging. This option controls whether the IDE debugger is used for debugging. If the Integrated debugging check box is checked, the IDE debugger is used. If this option is unchecked, the IDE debugger isn't used. This means that when you click the Run button, the program will execute but the debugger is disabled, so no breakpoints will function.
The Debugger Options dialog box has four pages: General, Event Log, Language Exceptions, and OS Exceptions. These pages are discussed in the following sections.
The General page is where you set general debugging options. This page is shown in Figure 10.14.
FIGURE 10.14. The Debugger Options dialog box General page.
The Map TD32 keystrokes on run option in this section tells the Code Editor to use the keystroke mapping used in Borland's stand-alone debugger, Turbo Debugger. This is a nice feature if you have spent a lot of time using Turbo Debugger and are familiar with that program's key mappings.
The Mark buffers read-only on run option sets the Code Editor buffers to read-only when the program is run under the debugger. After you start the program under the debugger, you cannot edit your source code until the program is terminated. I leave this option off because I frequently make changes to my source code while debugging.
The Inspectors stay on top check box controls whether the Debug Inspector windows are always on top of the Code Editor. This is a nice feature because most of the time you want the Debug Inspector windows to stay on top when stepping through your code.
The Rearrange editor local menu on run option changes the appearance of the Code Editor context menu when a program is running under the debugger. When this option is on, the Code Editor context menu items specific to debugging are moved to the top of the context menu so that they are easier to find.
The Event Log page enables you to set the options for the Event Log. You can choose a maximum number of messages that can appear in the Event Log at any one time or leave the number unlimited. You can also select the types of messages you want to see in the Event Log.
The Language Exceptions page is used to control the types of VCL exceptions that are caught by the debugger (exceptions are discussed on Day 14, "Advanced Programming"). The most important item on this page is the Stop on Delphi Exceptions option. When this option is on, the debugger pauses program execution when an exception is thrown. When this option is off, the VCL exception is handled in the usual way--with a message box informing the user of what went wrong in the program.
NOTE: When the Stop on Delphi Exceptions option is on, the debugger breaks on exceptions even if the exception is handled in your program. If you don't want the debugger to break on every exception, turn this option off. This option replaces the Break on exception option found in previous versions of Delphi.
The Exception Types to Ignore option is used to specify the types of exceptions that you want the debugger to ignore. Any exception classes in this list will be ignored by the debugger and the exception will be handled in the default manner. This is effectively the same as turning off the Stop on Delphi Exceptions option for selected exception types.
To add an exception type to the list, simply click on the Add button and enter the name of an exception class. To tell the debugger to ignore divide by zero exceptions, for example, you click the Add button and enter EDivByZero in the Exception Type field. Figure 10.15 shows this process.
FIGURE 10.15. Adding an exception type to the Exception Types to Ignore list.
Any exception type added to the list will be persistent across all projects (including any new projects).
The OS Exceptions Page controls whether operating system exceptions are handled by the debugger or by the user program. Figure 10.16 shows the OS Exceptions page of the Debugger Options dialog box.
FIGURE 10.16. The Debugger Options OS Exceptions Page.
When the Handled By option is set to User Program, the debugger pauses program execution when an exception is thrown. When this option is set to Debugger, the VCL exception is handled in the usual way--with a message box informing the user of what went wrong in the program.
NOTE: When the Handled By option is set to Debugger, the debugger breaks on exceptions even if the exception is handled in your program. If you don't want the debugger to break on every exception, set this option to User Program. This option replaces the Break on exception option found previous versions of Delphi.
The On Resume option determines how the exception will be treated when program execution is resumed following an exception.
The Exceptions list box contains a list of possible operating system exceptions. To set the options for a particular type, click on the exception in the Exceptions list box and then set the Handled By or On Resume options as desired. Glyphs in the right margin of the Exceptions list box indicate the handling and resume settings.
Debugging is a never-ending task. Debugging means more than just tracking down a bug in your program. Savvy programmers learn to use the debugger from the outset of a new project. The debugger is a development tool as well as a bug-finding tool. After today, you should have at least a basic understanding of how to use the debugger. You will still have to spend time actually using the debugger before you are proficient at it, but you now have a place to start.
The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find the answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."
© Copyright, Macmillan Computer Publishing. All rights reserved.