Using the CBuilder Interactive Debugger

   

Using the C++Builder Interactive Debugger

C++Builder's interactive debugger contains many advanced features, including expression evaluation, data setting, object inspection, complex breakpoints, a machine code disassembly view, an FPU and MMX register view, cross-process debugging, remote debugging, attaching to a running process, watching expression results, call stack viewing, the capability to single-step through code, and more. During development you will spend a lot of time using it, or at least you probably will need to!

The debugger is not just for finding bugs ; it is also a general development tool that can give you great insight into how your application works at a low level.

To use the debugger effectively, you must first disable compiler optimizations. When compiler optimizations are enabled, the optimizer will do everything in its power to speed up or reduce the size of your code, including removing, rearranging, and grouping sections of machine code generated from your source. This makes it very difficult to step through your code and to match up source code with machine code in the CPU view. If you set a breakpoint on a line, and it is not hit when you are confident that the line was executed, it is probably because you have optimizations enabled.

Screen real estate becomes a problem with the many debug views that you're likely to need during a debugging session. You can make use of the desktop settings to create a layout appropriate for programming and a separate layout for debugging.

A typical desktop layout for debugging is shown in Figure 2.16. You can see at the bottom the docked windows , now tabbed pages, for the call stack and the watch list. To the right is the Debug Inspector, which is much like the Object Inspector, except it shows runtime values (this window is not dockable ). Docked to the right of the Source Code Editor are the Breakpoint List Window and the Local Variable Window (which shows the current values of the local variables for the current breakpoint function).

Figure 2.16. An example debugging window layout.

graphics/02fig16.jpg

For the remainder of this section, it is assumed that you understand the basics of debuggers . Such basics include using source breakpoints with expression and pass-count conditions, stepping over and into code, and using ToolTip expression evaluation (holding the mouse pointer over an expression while the application is paused in the debugger).

Multithreaded Application Debugging

If you are writing multithreaded applications, you can name your thread. Simply select File, New, and pick Thread Object from the Dialog. This creates a compilation unit whose class is based on TThread . The dialog shown in Figure 2.17 enables you to specify a name for the thread. Why does this matter? Well, the debug window for threads will use that name and make it much easier for you to see what's happening in your multithreaded application.

Figure 2.17. Task Object dialog.

graphics/02fig17.gif

After you have created your thread object and set up your program to run it, you can open the thread debugging window (which is done by picking ViewDebug WindowsThread). Figure 2.18 shows this dialog:

Figure 2.18. The Thread debug window shows the name of the named thread.

graphics/02fig18.gif

Advanced Breakpoints

Apart from the standard source breakpoints that simply halt execution when the selected source or assembly line is reached, there are more advanced breakpoints that can be used in particular debugging cases.

Module load breakpoints are particularly useful when debugging DLLs and packages. You can pause execution when a specified module is loaded, providing a perfect entry point into the DLL or package for debugging. To set a module load breakpoint, you have two options.

The first option applies when the application is already running within the IDE. First, display the Modules window by selecting View, Debug Windows, Modules. Next , in the modules list in the upper-left pane of the Modules window, locate the module for which you want to set the module load breakpoint. If the module is not in the modules list, it has not yet been loaded by the application. In that case, you will need to add the module to the modules list by selecting Add Module from the context menu, then name or browse to the module, and select OK. Finally, select the module in the modules list, and then select Break On Load from the context menu. This is shown in Figure 2.19. If the module has already been loaded by the application, the breakpoint will work only when the module is loaded again, either after being dynamically unloaded or when the system is restarted.

Figure 2.19. Setting a module load breakpoint from the Modules view.

graphics/02fig19.jpg

The second option applies when the application is not yet running within the IDE. Select Run, Add Breakpoint, Module Load Breakpoint, then enter the module name or browse to it and select OK. Finally, run the application.

Address breakpoints and Data breakpoints provide a way to pause the application when a particular code address is reached or data at a particular address is modified. They can only be added when the application is running or paused.

Address breakpoints work in the same manner as source breakpoints, but instead of adding the breakpoint to a particular line of source code, you add the breakpoint to the memory address for a particular machine code instruction. When the machine code is executed, the breakpoint action is taken. If you set an address breakpoint for a machine code instruction that is related to a line of source code, the breakpoint is set as a normal source breakpoint on that line of source code. Address breakpoints are typically used when debugging external modules on a low level using the CPU view. The CPU view is explained in the section "The CPU View," later in this chapter.

You can also set an address breakpoint. In the Breakpoints folder on the CD-ROM that accompanies this book, you will find the BreakpointProj.bpr project file. Load it with C++Builder, and then compile and run the application by selecting Run, Run. When the form is displayed, pause the application by selecting Run, Program Pause from the C++Builder main menu. The CPU view, which we will use in a moment, will be displayed.

Next, select View, Units, then select BreakpointForm from the units list and select OK. The unit will be displayed in the Code Editor. Scroll down to the AddressBreakpoint ButtonClick() function. Right-click the Label2->Caption = "New Caption" statement in the function and select Debug, View CPU from the context menu. The CPU view is again displayed, this time at the memory address where the machine code for the C++ statement is located.

In the upper-left pane in the CPU view, note the hexadecimal number on the far left of the line containing the machine code statement lea eax,[ebp-0x04] . On my system at present, this number is 004016ED , but it is likely to be different on yours. This is the address at which we will set the address breakpoint.

To add an address breakpoint for this address, select Run, Add Breakpoint, Address Breakpoint. Then, in the Address field, enter the address that you previously noted. Hexadecimal numbers , such as the address displayed in the CPU view that you noted, must be entered with a leading 0x ; in my case I would specify the address as 0x004016ED .

To test the address breakpoint, continue the program by selecting Run, Run. Select the application from the Windows taskbar and click the Address Breakpoint button. The address breakpoint that was previously set will cause the application to pause. The CPU view will be displayed with the current execution point, marked by a green arrow, set on the address breakpoint line of machine code. You can continue the application by selecting Run, Run.

If, in the CPU view, you display the line of machine code at the address you want to place an address breakpoint, you can set an address breakpoint simply by clicking in the gutter of the machine code line, as you would in source code for a source breakpoint.

Data breakpoints can be invaluable in helping to track bugs, by locating where in the code a particular variable or memory location is being set. As an example, load the BreakpointProj.bpr project file from the previous demonstration. Run and then pause the application. Select Run, Add Breakpoint, Data Breakpoint. In the Address field, enter Form1->FClickCount and click OK. This private data member of the form counts the number of times that the form's DataBreakpointButton button is clicked. Setting this data breakpoint will cause the application to break whenever the count is modified.

As you can see, any valid data address expression can be entered, not just a memory address. Alternatively, to obtain the address, we can select Run, Inspect, enter Form1->FClickCount in the expression, and obtain the address from the top of the Debug Inspector window. This hexadecimal address could then be entered (with a leading 0x ) in the data breakpoint Address field.

To test the data breakpoint, continue the program by selecting Run, Run. Select the application from the Windows taskbar and click the Data Breakpoint button. The previously set data breakpoint will cause the application to pause at the location where the data was modified. If this is on a source line, the Code Editor will be displayed; otherwise the CPU view will be displayed. You can continue the application by selecting Run, Run.

TIP

Adding a data breakpoint is much trickier for a property such as the Caption of a label or the Text of an edit box. These properties are not direct memory locations that are written to when the property is modified. Instead, the properties use Set functions to change their values. To break when the property is changed, it is easiest to add an address breakpoint for the Set function of the property, rather than finding the memory address where the data is actually stored and adding a data breakpoint. I'll explain this method using the Caption property of ClickCountLabel on the form of the previous demonstration project.

With the application paused, select Run, Inspect. In the Expression field enter Form1->Click CountLabel . Select the Properties tab in the Debug Inspector window and scroll down to the Caption property. The write method for this property is specified as SetText . Click the Methods tab and scroll down to the SetText method. The address of this method will be displayed on the right. Select Run, Add Breakpoint, Address Breakpoint and enter the address of the SetText method, prefixing it with 0x and leaving out the colon ; then click OK. Continue the application by selecting Run, Run. Now, whenever the label caption is modified, the breakpoint will pause the application.

For a standard AnsiString variable without a Set method to control its modification, such as the FSomeString private data member of the form in the demonstration project, you can set a data breakpoint on the .Data variable of the AnsiString class that contains the underlying string data. For the demonstration project, the data breakpoint would be set on Form1->FSomeString.Data .


When adding a data breakpoint, the Length field in the Add Data Breakpoint window should be specified for nonsingular data types such as structures or arrays. The breakpoint will pause the application when any memory location within this length from the data address is modified. Data breakpoints can also be added by selecting Break when Changed from the context menu in the View, Debug Windows, Watches view.

NOTE

Address and data breakpoints are valid only for the current application being run. You must set them for each new run because the machine code instruction and data addresses can change each time.


Advanced Breakpoint Features

Breakpoints can be organized into groups and have actions. With breakpoint actions, you can enable and disable groups of actions, enable and disable exception handling, log a message to the event log, and log the evaluation of an expression to the event log.

Using these features, you can set up complex breakpoint interaction to break only in specific program circumstances. For example, you can cause a set of breakpoints to be enabled only when a specific section of code is executed.

By disabling and enabling exceptions, you can control error handling in known problem areas of code. Message logging helps automate variable inspection and execution tracing.

Breakpoint action and group information are available in the breakpoint ToolTip in the Code Editor and in the Breakpoint List Window.

C++Builder Debugging Views

The debugger can be used to display many types of information that are helpful with debugging an application, such as local variables, a list of all breakpoints, the call stack, a list of the loaded modules, thread status, machine code, data and register status, an application event log, and more.

The Floating-Point Unit (FPU) view shows the current state of the floating point and MMX registers. All debugging views are accessible from the View, Debug Windows menu option, or by pressing the appropriate shortcut key. In the following sections we'll look at some of the advanced views, and how you can use them in debugging your application.

The CPU View

The CPU View displays your application at the machine code level. The machine code and disassembled assembly code that make up your application are displayed along with the CPU registers and flags, the machine stack, and a memory dump. The CPU view has five panes, as depicted in Figure 2.20.

Figure 2.20. The CPU view in action.

graphics/02fig20.jpg

The large pane on the left is the disassembly pane. It displays the disassembled machine code instructions, also known as assembly code, that make up your application. The instruction address is in the left column, followed by the machine code data and the equivalent assembly code. Displayed above the disassembly pane are the effective address of the expression in the currently selected line of machine code, the value stored at that address, and the thread ID.

If you enabled the Debug Information option on the Compiler tab of the project options before compiling your application, the disassembly pane shows your C++ source code lines above the corresponding assembly code instructions. Some C++ source code lines can be seen in Figure 2.20.

In the disassembly pane, you can step through the machine code one instruction at a time, much like you step through one source code line at a time in the source code editor. The green arrow shows the current instruction that is about to be executed. You can set breakpoints and use other features similar to debugging in the source code editor. Several options, such as changing threads, searching through memory for data, and changing the current execution point are available in the context menu.

The CPU registers pane is to the right of the disassembly pane. It shows the current value of each of the CPU registers. When a register changes, it is shown in red. You can modify the value of the registers via the context menu.

On the far right is the CPU flags pane. This is an expanded view of the EFL (32-bit flags) register in the CPU register pane. You can toggle the value of a flag through the context menu. Consult the online help for a description of each of the flags.

Below the disassembly pane is the memory dump pane. It can be used to display the content of any memory location in your application's address space. On the left is the memory address, followed by a hexadecimal dump of the memory at that address and an ASCII view of the memory. You can change how this data is displayed from the context menu, and also go to a specified address or search for particular data.

The final pane is the machine stack pane, located at the bottom right of the CPU window. It displays the content of the application's current stack, pointed to by the ESP (stack pointer) CPU register. It is similar to the memory dump pane and offers similar context menu options.

The CPU view is a good tool for getting to know how your application works at a very low level. If you come to understand your application at this level, you will have a better understanding of pointers and arrays; you'll know more about execution speed (helpful when optimizing your application); and you'll find it easier to debug your application because you will know what's going on in the background.

The best reference for the x86 machine code instruction set and detailed information on the Pentium processor range is the Intel Architecture Software Developer's Manual. This three-volume set tells you just about everything you want to know about the Pentium processors. It is available for download on Intel's Web site (http://developer.intel.com/design/processor) from the appropriate processor's Manuals section .

Assembly language programming is a black art these days. It is extremely complex and is usually reserved only for writing small sections of very efficient, speed-critical code.

The Call Stack View

A call stack is the path of functions that lead directly to the current point of execution. Only functions that have been previously called and have not yet returned are in the call stack.

The Call Stack view displays the call stack with the most recently entered function at the top of the list. Used in conjunction with conditional breakpoints, the Call Stack view provides useful information as to how the function containing the breakpoint was reached. This is particularly useful if the function is called from many places throughout the application.

You can double-click a function listed in the Call Stack view to display it in the Code Editor. If there is no source code for the function ”for example, if the function is located in an external module ”the disassembled machine code of the function is displayed in the CPU view. In either case, the next statement or instruction to be executed at that level in the call stack is selected.

You can display the Local Variables view for a particular function on the call stack by selecting View Locals from the context menu.

The Threads View

Debugging multiprocess and multithreaded applications can be very difficult. Threads, in particular, usually execute asynchronously. Often the threads in the application communicate with each other using the Win32 API PostThreadMessage() function or use a mutex object to gain access to a shared resource.

When debugging a multithreaded application, you can pause an individual thread. One thread might hit a breakpoint and another might not. The problems occur when another thread is still running and is relying on interthread communication, or the stopped thread has an open mutex for which another thread is waiting.

Even the fact that the application runs more slowly under the debugger can cause timing problems if the application is multithreaded. In general, it is bad programming practice not to allow for reasonable timing fluctuations because you cannot control the environment in which the application is run.

The Threads view helps to alleviate some of these difficulties by giving you a snapshot of the current status of all processes and threads in the application. Each process has a main thread, and might have additional threads. The Threads view displays the threads in a hierarchical way, such that all threads of a process are grouped together. The first process and the main thread are listed first. The process name and process ID are shown for each process, and the thread ID, state, status, and location are shown for each thread. Figure 2.18 shows an example of the Threads view.

For secondary processes the process state is Spawned, Attached, or Cross-Process Attach. The process state is Runnable, Stopped, Blocked, or None. The thread location is the current source position for the thread. If the source is not available, the current execution address is shown.

When debugging multiprocess or multithreaded applications, there is always a single-current thread. The process that the thread belongs to is the current process. The current process and current thread are denoted in the Threads view by a green arrow, which can be seen in Figure 2.18. Most debugger views and actions relate to the current thread. The current process and current thread can be changed by selecting a process or thread in the Threads view and selecting Make Current from the context menu, from which you can also terminate a process. For information on additional settings and commands in the Threads view, see "Thread status box" in the Index of the C++Builder online help.

The Modules View

The Modules view lists all DLLs and packages that have been loaded with the currently running application or modules that have a module load breakpoint set when the application is not running. It is very useful when debugging DLLs and packages, as discussed in the "Advanced Breakpoints" section earlier in this chapter. Figure 2.19 shows a typical Modules view.

The Modules view has three panes. The upper-left pane contains the list of modules, their base addresses, and the full paths to their locations. Note that the base address is the address at which the module was actually loaded, not necessarily the base address specified on the Linker tab of the project options when developing the module. By selecting a module, you can set a module load breakpoint from the context menu.

The lower-left pane contains a tree view of the source files that were used to build the module. You can select a source file and view it in the Code Editor by selecting View Source from the context menu.

The right pane lists the entry points for the module and the addresses of the entry points. From the context menu, you can go to the entry point. If there is source available for the entry point, it will be displayed in the Code Editor. If there is no source, the entry point will be displayed in the CPU view.

The FPU View

The Floating-Point Unit (FPU) view enables you to view the state of the floating-point unit or MMX information when debugging your application.

The FPU view has three panes. The left pane displays the floating-point register stack (ST0 to ST7 registers), the control word, status word, and tag words of the FPU. For the floating-point register stack, the register status and value are shown. The status is either Empty, Zero, Valid, or Spec (special), depending on the register stack's contents.

When a stack register's status is not Empty, its value is also displayed. You can toggle the formatting of the value from long double to words by selecting the appropriate option under Display As in the context menu. You can also zero, empty, or set a value for a particular stack register, and zero or set a value for the control word, status word, and tag word from the context menu.

The middle pane contains the FPU single and multibit control flags, which change as floating-point instructions are executed. Their values can be toggled or cycled via the context menu.

The right pane contains the FPU status flags. It is an expanded view of the status word in the FPU registers pane, listing each status flag individually. Their values can be toggled or cycled via the context menu. When a value changes, it is displayed in red in all panes. You can see this effect best by performing single steps through floating-point instructions in the CPU view.

Watches, Evaluating, and Modifying

A watch is simply a means of viewing the contents of an expression throughout the debugging process. An expression can be a simple variable name or a complex expression involving pointers, arrays, functions, values, and variables. The Watches view displays the expressions and their results in the watch list. You can display the Watches view by selecting View, Debug Windows, Watches. The Watches view is automatically displayed when a new watch expression is added.

You can add expressions to the watch list using one of three methods. The first method is from the Add Watch item of the context menu in the Watches view. The second method is by selecting Run, Add Watch. The third method is by right-clicking the appropriate expression in the Code Editor and selecting Debug, Add Watch at Cursor from the context menu. This last method automatically enters the expression for you.

Watches can be edited, disabled, or enabled via the context menu. Watches can be deleted by selecting the appropriate expression and pressing the Delete key or by selecting Delete Watch from the context menu. If the expression cannot be evaluated because one or more of the parts of the expression is not in scope, an undefined symbol message appears instead of the evaluated result.

On the other hand, evaluating and modifying expressions enables you to more readily change the expression, view the subsequent result, and modify variables at runtime. With Evaluate/Modify, you can perform detailed live testing that is difficult to perform by other means.

To use Evaluate/Modify, your application must be paused. There are two ways to use it. One is to simply select Run, Evaluate/Modify, and enter the expression to evaluate. Perhaps the easiest method is to invoke Evaluate/Modify from the Code Editor.

When the application is paused in the debugger, you can evaluate expressions in the source code simply by placing the mouse pointer over them. Evaluate/Modify enables you to change the expression at will. You can invoke it by right-clicking the expression and selecting Debug, Evaluate/Modify. In the Evaluate/Modify window, you will see the expression and its result. The Modify field enables you to change the expression value if it is a simple data type. If you need to modify a structure or an array, you will have to modify each field or item individually.

Function calls can be included in the expression. Be aware, though, that evaluating an expression produces the same result as if your application executed that expression. If the expression contains side effects, they will be reflected in the running state of your application when you continue to step through or run.

Unlike the Watches view, the Evaluate/Modify dialog box doesn't update the result of the expression automatically when you step through your code. You must click the Evaluate button to see the current result. The expression result can also be formatted using a format modifier at the end of the expression. See the online help for more information.

Typical uses for Evaluate/Modify include testing error conditions and tracking down bugs. To test an error condition, simply set a breakpoint at or just before the error check or step through the code to reach it, and then force an error by setting the appropriate error value using Modify. Use Single Step or Run to verify that the error is handled correctly.

If you suspect that a certain section of code contains a bug and sets incorrect data, set a breakpoint just after the suspected code, fix the data manually, and then continue execution to verify that the bad data is producing the bug's symptoms. Trace backward through code until you locate the bug, or use a data breakpoint to find out when the data is modified.

The Debug Inspector

The Debug Inspector is like a runtime object inspector. It can be used to display the data, methods, and properties of classes, structures, arrays, functions, and simple data types at runtime, thus providing a convenient all-in-one watch/modifier.

With the application paused in the debugger, you can start the Debug Inspector by selecting Run, Inspect, and entering the element to inspect as an expression, or by right-clicking an element expression in the Code Editor and selecting Debug, Inspect from the context menu. The element expression in the second method is automatically entered into the inspector.

The title of the Debug Inspector window contains the thread ID. In the top of the window are the name, type, and memory address of the element. There are up to three tabs, depending on the element type, that display the name and contents or address of each data member, method, or property. The Property tab is shown only for classes derived from the VCL. At the bottom of the window, the type of the currently selected item is shown.

The values of simple types can be modified. If the item can be modified, an ellipsis will be shown in the value cell . Click the ellipsis and enter the new value.

The Debug Inspector can be used to walk down and back up the class and data hierarchy. To inspect one of the data members , methods, or properties in the current inspector window simply select it, and then choose Inspect from the context menu. You can also hide or show inherited items.

There are four Debug Inspector options that can be set from Tools, Debugger Options: Inspectors Stay On Top, Show Inherited, Sort By Name, and Show Fully Qualified Names. Show Inherited switches the view in the Data, Methods, and Properties tabs between two modes, one that shows all intrinsic and inherited data members or properties of a class and one that shows only those declared in the class. Sort By Name switches between sorting the items listed by name or by declaration order. Show Fully Qualified Names shows inherited members using their fully qualified names and is displayed only if Show Inherited is also enabled. All three new options can be set via the context menu in the Debug Inspector.

The Debug Inspector is a commonly used tool during a debugging session because it displays so many items at once. It also enables you to walk up and down the class and data hierarchy.


   
Top


C++ Builder Developers Guide
C++Builder 5 Developers Guide
ISBN: 0672319721
EAN: 2147483647
Year: 2002
Pages: 253

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