Debugging Custom Microsoft Windows CE 3.0-based Systems
Summary: This document discusses in detail the Platform Builder debugging environment for the Microsoft® Windows® CE operating system, which includes new integrated development environment (IDE) features, the use of newly integrated Windows CE debug shell (CESH) functionality, and eXdi hardware-assisted debugging. (23 printed pages)
Kernel Debugging vs. Application Debugging
Using the Kernel Debugger
Kernel Debugging Using Standard Ports
Obtaining Process Information
Obtaining Thread Information
Watching Variables and Expressions
Displaying Function Calls
View Disassembly and Source Code
Viewing Memory and Registers
Working with Modules and Symbols
Using Debug Zones
Calling Debugging Macros
Using eXdi Hardware-Assisted Debugging
Kernel Debugging Using Platform Builder 3.0
Kernel debugging in Platform Builder 3.0 is simpler, cleaner, and better performing than ever before. Once you have built your Microsoft Windows CE operating system–based platform, you can download your image and start debugging with a single click of the mouse. This paper discusses in detail the Platform Builder debugging environment, including new integrated development environment (IDE) features, the use of newly integrated Windows CE debug shell (CESH) functionality, and eXdi hardware-assisted debugging.
Platform Builder uses a kernel debugger for debugging embedded platforms. The kernel debugger differs from the application debugger in the following ways:
- The kernel debugger debugs code in the Windows CE kernel as well as Windows CE–based applications.
- The kernel debugger requires a special operating system image. This operating system image contains a module that is automatically built into the kernel when the user chooses to enable kernel debugging. Any application that you want to debug with the kernel debugger must be started manually.
- The kernel debugger remains active when you quit the application that is being debugged on the target device.
- The application remains open and running on the target device when a stop debugging command is performed in the kernel debugger.
- You can debug dynamic-link library files (DLLs) with the kernel debugger when the executable file (EXE), such as a Control Panel application, is contained in read-only memory (ROM).
- The application debugger controls the behavior of a single application, whereas the kernel debugger controls the behavior of the entire operating system. Therefore, termination of the kernel debugger causes the operating system to cease responding to outside input while it waits for feedback from the kernel debugger. In some cases, you may need to exit and restart the debugging session in order to allow the kernel debugger to resume communication with the operating system.
- Debug zones provide a way to selectively turn the debugging message output on and off by using macros. This allows you to trace execution of the code without halting the Windows CE operating system. You can access this feature through a new Debug Zones window in the IDE.
- All of the functionality necessary to configure device connections; download images to the target; and acquire process, thread, and other target debugging information has been integrated into the Platform Builder 3.0 IDE.
You can use the kernel debugger to debug application code in the Windows CE kernel. The kernel debugger starts automatically if you have enabled kernel debugging in the Platform Settings dialog box. If enabled, the debugging stub, KdStub, is included in the operating system image that is downloaded to the target device.
In addition to improved debugging performance, the following new features are included in this release:
- Multiple new IDE windows. The debugger now includes several refreshable docking windows. The following dockable windows are used to debug applications in the IDE:
- Call Stack
- Module and Symbols
Each of these windows is discussed later in this document.
- Drag-and-drop operations. The debugger user interface supports drag-and-drop operations. The result of a drag-and-drop operation depends, in part, on the location where the drop takes place.
For example, you can drag a variable from the Variables window to the Watch window. This action puts the variable information into the Watch window, where it is updated each time that the Watch window is updated. If you drag the variable to a text window instead, the variable information is converted into text. If you drag the variable to the Memory window or the Disassembly window, the variable is used as a pointer, and the window scrolls to display the memory contents or instructions at the indicated address.
If you expand an object in the Variables window, you can drag a member of that object to the Watch window.
- Spreadsheet fields. The debugger user interface uses spreadsheet fields, which have an interface that is similar to that of Microsoft® Excel. These spreadsheet fields appear in the Watch window, the Variables window, and the QuickWatch dialog box.
Spreadsheet fields contain controls for viewing of array, object, structure, and pointer variables. If the variable is a pointer, the branch immediately below the pointer contains the value that is pointed to. If the variable is an array, an object, or a structure, the branch below the variable contains the component elements or members.
- Setting breakpoints. Use the Breakpoints dialog box from the Breakpoints command on the Edit menu to set, remove, disable, enable, or view breakpoints. The breakpoints that you set are saved as a part of your project. You must have a project open before you can set a breakpoint. If no project is open, the Breakpoints command does not appear on the Edit menu. You can set breakpoints at a source code line or in the Disassembly window or Call Stack window.
- Viewing and enabling breakpoints. If you set more than one breakpoint on a line, and some breakpoints are disabled while others are enabled, a gray dot appears in the left margin in the Source window, Disassembly window, or Call Stack window. The gray dot does not appear in the Breakpoints dialog box (shown in Figure 1 below). The first time you choose the Enable/Disable Breakpoint button on the Build toolbar, all breakpoints on the line are disabled, and the gray dot changes to a hollow circle. If you choose the Enable/Disable Breakpoint button again, all breakpoints on the line are enabled, and the hollow circle changes to a red dot.
Figure 1. The Breakpoints dialog box
- Stepping into, over, and out of functions. When debugging, you have a choice of stepping into, stepping over, and stepping out of a function. Stepping works for the current process or thread only. To step through a section of code, you must first insert a breakpoint. When that breakpoint is reached, you can begin stepping through the code.
The kernel debugger requires a dedicated physical transport. Typically this transport is either a serial connection or an Ethernet connection and is supplied on an add-on debug board provided by an original equipment manufacturer (OEM). However, these debug boards are not available on all devices, and typically they are not provided to independent software vendors (ISVs) by an OEM. To address this, an OEM I/O controller has been defined that you can use to dynamically switch a standard port, such as COM1, from normal mode to debug mode. This section describes the basic mechanism for implementing this feature.
The IOCTL_SET_KERNEL_DEV_PORT is defined in Pkfuncs.h. Once you have fully implemented this I/O controller, you can create an application that dynamically switches a port between standard mode and debug mode by means of the I/O controller.
A Windows CE PC–based hardware development platform (CEPC) defines eight unique port identifiers: KERNEL_PORT_NONE, KERNEL_PORT_COM1, KERNEL_PORT_COM2, KERNEL_PORT_COM3, KERNEL_PORT_COM4, KERNEL_PORT_LPT1, KERNEL_PORT_LPT2, and KERNEL_PORT_ETH1. You must define only the available ports and PORT_NONE for a device.
Not all services need to be supported. For example, a basic implementation might support only mapping the kernel debugger to the primary serial port. An advanced implementation might also allow mapping any kernel service to a built-in Ethernet port.
Handling the I/O Controller
Because switching ports for a mode is best accomplished by rebooting, the first step in implementing the DEV_PORT is to provide an area to store the requested mode that is preserved when you reboot, such as the driver global or other reserved area of memory. Three bytes are required for storage—one byte for each kernel service. Each byte contains the port identifier of the port that is used by that service.
Enabling the Service
Once each service is configured for the appropriate port, the IOCTL_OAL_REBOOT I/O controller is used to force a warm boot. Upon rebooting, the OAL should examine the arguments area that was set by IOCTL_SET_KERNEL_DEV_PORT. The exact implementation of this will depend on the ports that are supported. For example, a CEPC determines which serial port should be used for initial debugging of messages by checking the bootargs structure in the OEMInitDebugSerial function in %_WINCEROOT%\Platform\Cepc\Kernel\HAL\X86\Debug.c.
The Processes window is a dockable window that displays information on all processes running on the target device. When the Processes window is initially opened, it displays an alphabetical list of all processes running on the target device.
The Processes window provides different information, depending on whether the debugger is running or at a debug stop or breakpoint. When both the debugger and Target Control (CESH) are disconnected, the Processes window closes.
The kernel debugger is started automatically within Platform Builder when a debugger-enabled image is downloaded to the target. The Process window is populated from two different sources, depending on the state of the target. When the target is running, the Process window obtains information through CESH.exe. When the target is in a break state, the debugger stub on the target provides information to the Process window.
Figure 2 below shows what the Processes window looks like when the kernel debugger is attached to the target device, the target device is currently running, and Target Control (CESH) is currently running.
Figure 2. The Processes window
The Threads window is a dockable window that displays the threads, or paths of execution, within a process. Like the Process window, the Threads window is populated from a different source depending on the state of the target, and the information displayed in the Threads window will vary based on the current state. When the target is running, the Target Control (CESH) service provides the information for this window; note that if the Target Control service is not running, this information will not be available. If the target is in a break state, the debugger stub in the kernel provides the information necessary to fill the contents of this window. By default, when the Threads window is first opened, it displays the threads for the first process listed in the Process combo box.
Figure 3 below shows what the Threads window looks like when the kernel debugger is attached to the target device, the target device is currently running, and Target Control (CESH) is currently running.
Figure 3. The Threads window
Table 1 below shows some of the exceptions that the kernel debugger can handle.
Table 1. Debugger exceptions-handling
|STATUS_DATATYPE_MISALIGNMENT||MIPS: 4, 5
SH3: 0x0e0, 0x100
|An address is unaligned. Verify that all data references are aligned and that the process is in the correct mode, either User or Kernel.|
|STATUS_ACCESS_VIOLATION||MIPS: 1, 2, 3
SH3: 0x40, 0x60, 0xa0, 0xc0
|An attempt was made to access data through an invalid pointer or you do not have permission to access the object to which the pointer refers.|
|A breakpoint was generated by a call to the DebugBreak function.|
SH3: 0x180, 0x1a0
|The microprocessor has tried to execute an unknown instruction or an instruction that is invalid in a delay slot. This exception will only be encountered if you are writing assembly language code.|
|STATUS_INVALID_DISPOSITION||NA||The structured exception-handler code called a handler to handle an exception, but the handler returned an erroneous or unknown disposition.|
Use the Exceptions dialog box (Figure 4 below) to control how the debugger handles exceptions. The Exceptions dialog box displays system-defined and user-defined exceptions and their actions for your project.
Figure 4. The Exceptions dialog box
Use the Watch window to specify variables and expressions that you want to watch while debugging your program. You can also modify the value of a variable in the Watch window.
The Watch window contains four tabs: Watch1, Watch2, Watch3, and Watch4. Each tab displays a user-specified list of variables and expressions in a spreadsheet field. You can group variables that you want to watch together onto the same tab. For example, you could put variables related to a specific window on one tab and variables related to a dialog box on another tab. You could watch the first tab when debugging the window and the second tab when debugging the dialog box.
The Watch window displays values in their default format. You can change the display format—to display Unicode characters, for example—by using formatting characters.
In addition to the global variables of the current process, you can also review global variables of any module that is loaded, if you qualify the name with the context operator. For example, no matter what the current process is, you can examine the PROCARRAY structure in the kernel by typing a command in a Watch window.
You can review a module by casting a thread address. If you want to look at the thread structure for the Shell.exe process from the Processes command output, you can type a command in the Watch window.
The Watch window does not display variable type information. You can view information for a variable type by using the window's property page.
The QuickWatch Dialog Box
The QuickWatch dialog box contains a text box, where you can type an expression or variable name, and a spreadsheet field that displays the current value of the variable or expression that you specified.
You can use the QuickWatch dialog box to examine the value of a variable or expression. You can also use the QuickWatch dialog box to modify the value of a variable or to add a variable or expression to the Watch window.
The Current Value spreadsheet field displays only one variable or expression at a time. If you type a new variable or expression in the text box, and then press ENTER, the previous variable or expression in the Current Value field is replaced.
The QuickWatch dialog box displays values in their default format. You can change the display format—to display Unicode characters, for example—by using formatting characters.
Watching Program Variables
The Variables window provides quick access to variables that are important in the program's current context. The window includes three tabs:
- The Auto tab displays the variables that are used in the current statement and in the previous statement.
- The Locals tab displays the variables that are local to the current function.
- The This tab displays the object that is pointed to by this.
Each tab contains a spreadsheet with fields for the variable name and value. The debugger automatically fills in these fields. If a value appears in red, it indicates that the value has recently changed. Only the last value to change appears in red.
You cannot add variables or expressions to the Variables window—you must use the Watch window—but you can change the value of a variable by double-clicking on its Value field and entering new data.
In addition to the tabs, the Variables window has a context box on the toolbar that contains a copy of the current call stack in a drop-down list box. Use this list to specify the current scope of the variables that are displayed. You can hide the Context list by right-clicking in the Variables window, and then clearing the Toolbar check box.
You can navigate to a function's source code or disassembled object code from the Context list. This procedure displays the function's source code, if it is available, in a source window. If source code for the selected function is not available, it displays the function's object code in the Disassembly window.
During a debug session, the Call Stack window (Figure 5 below) displays the stack of function calls that are currently active. When a function is called, it is pushed onto the stack. When the function returns, it is popped off the stack.
Figure 5. The Call Stack window
The Call Stack window displays the currently executing function at the top of the stack and older function calls below that function. By default, the window also displays parameter types and values for each function call. You can display or hide parameter types and values by using the Debug tab in the Options dialog box (as shown in Figure 6 below) or the shortcut menu.
Figure 6. The Debug tab in the Options dialog box
The Process list box in the Call Stack window contains a CURRENT selection. If the user selects Current, the Call Stack window displays the call stack of the current process and thread. You can also select call stacks by specifying Process and Thread names.
You can navigate to the source code or disassembled object code for a function by double-clicking the function in the Call Stack window. If source code for the selected function is not available, the object code is displayed in the Disassembly window.
Navigating to a function's code changes how the view of the program is shown in the Variables window and other debugger windows, but does not change the context of the target, which is either the next line of execution or the value that is stored in the program counter.
The functions are listed in the order that they are called with the current function at the top. The Context list in the Variables window also contains call stack functions.
The Disassembly window and the Source window provide different views of the code that is running on the target device. It is also possible to view code in mixed source and disassembly mode.
The Disassembly window is a dockable window that operates on disassembled (assembly-language or bytecode) instructions instead of source-code statements or lines. By using the Disassembly window, you can set a breakpoint on any instruction. If you use the Step Into or Step Over command while the Disassembly window has focus, the debugger steps through your program instruction-by-instruction instead of line-by-line. Viewing and stepping through your code by disassembled instructions can be especially useful when you are debugging optimized code or source code lines that contain multiple statements.
The Memory window displays a snapshot of the data in memory at a given address. You can change the display format by using the shortcut menu. Figure 7 below shows the Memory window with its display in byte format.
Figure 7. The Memory window in byte format
The Registers window displays core CPU registers. You can change the display format by using the shortcut menu.
The Modules and Symbols window (Figure 8 below) is a dockable window that displays the name, address, status, and path for each module that is loaded into memory. Only one instance of this window can be open at a given time.
Figure 8. The Modules and Symbols window
The information displayed in the Modules and Symbols window varies, depending on the state of the target device, the services running on the device, and the kernel debugger. The symbols for individual modules can be unloaded by selecting a particular module and then clicking the Unload Module tool bar button in the Modules and Symbols window.
The Windows CE operating system kernel provides debugging support for applications, including printing debugging messages and registering debug zones. Debug zones allow you to debug by enabling macros that control the output of debugging messages.
Defining Debug Zones
To use debug zones, first define each debug zone to a bitmask and initialize it in the application source code. The application programming interface (API) for debug zones is included in the Dbgapi.h header file. You can define up to 16 debug zones in one application.
After defining a debug zone to a bitmask, associate a debug zone mask name with the defined bitmask. A debug zone mask is a named bitmask that is used to turn a debug zone on or off.
Declaring a DBGPARAM Structure
Implement the debug zones and debug zone masks by declaring a DBGPARAM structure in the source code. DBGPARAM holds the debug output information by setting the global variable dpCurSettings.
Registering Debug Zones
After declaring the DBGPARAM structure, call the DEBUGREGISTER macro to register the structure with the debugging subsystem. The syntax for this macro is DEBUGREGISTER(hMod | NULL). If you are debugging a DLL file, call DEBUGREGISTER with the module name. If you are debugging an EXE file, call DEBUGREGISTER with NULL.
The DEBUGREGISTER macro calls RegisterDbgZones. On the development workstation, RegisterDbgZones reads the registry key HKEY_CURRENT_USER\Pegasus\Zones and searches for the module name, specified in the DBGPARAM macro. If the key exists, RegisterDbgZones calls SetDbgZone, which uses the stored debug zone mask to update the debug zone name in the DBGPARAM macro.
For additional information and code samples, see the Platform Builder 3.0 online documentation.
Using the Debug Zones Dialog Box
The Debug Zones dialog box (shown in Figure 9 below) enables you to view debug zones for processes and modules. You can also turn debug zones on and off from this dialog box.
Figure 9. The Debug Zones dialog box
The Debug Zones menu item and button are enabled only in the following circumstances.
- Kernel debugger is attached to the target device, the target device is currently running, and Target Control (CESH) is running.
- Kernel debugger is not attached to the target device, the target device is currently running, and Target Control (CESH) is running.
Debug zones are not enabled if the target device is empty, at a debug stop, Target Control (CESH) is not running, or if the kernel debugger is not attached to the target device.
After the debug zones are registered, use macro calls in the source code to output debugging messages. Release and debug configurations call different macros.
Release configurations use the following macros:
- RETAILMSG(cond, printf_exp). Conditionally displays the print message.
- RETAILLED(cond, parms). Conditionally outputs WORD values to the LED.
- ERRORMSG(cond, printf_exp). Prints "Error: File Line" before the print message.
To enable the debug macros, you must build a debug configuration. Debug configurations use the three retail macros listed above, as well as the following debug macros:
- DEBUGMSG(cond, printf_exp). Conditionally displays the print message.
- DEBUGLED(cond, parms). Conditionally outputs WORD values to the LED.
- DEBUGCHK(expr). Asserts the expression. If expr is FALSE, the macro calls DEBUGBREAK.
- DEBUGZONE(zone_id). Tests the mask bit in the current debug zone settings. You can use DEBUGZONE to turn debug zones on or off.
Platform Builder enables you to use eXdi hardware–assisted debugging to control the execution of a target device, and to examine and modify the state of the device. Third-party vendors provide the required hardware and software, which includes at least a driver and either a probe or an emulator. The third-party vendor may also provide a target device, or the target and probe may be one piece of hardware, and a software plug-in that adds additional hardware specific functionality to the debugger. Platform Builder provides an IDE interface to the third-party components.
To use eXdi hardware–assisted debugging, you must install the third-party eXdi driver on a development workstation running Platform Builder, and connect a probe or emulator between the development workstation and the target device. The driver multiplexes all communication between the probe or emulator and all clients, including Platform Builder and debugger plug-ins. The same driver can sometimes support multiple CPUs, even those from different CPU families.
Figure 10 is an illustration showing a typical eXdi hardware debugging configuration that uses a probe.
Figure 10. Typical exDi hardware debugging configuration
The eXdi driver works solely with the debug support available on the hardware, and does not rely on an operating system for this support. For this reason, the hardware debugger can work even if the operating system is not fully functional, or even if there is no operating system on the target device. The hardware debugger can debug initialization phases such as hardware setup, boot loading, and OEM adaptation layer (OAL) initialization, as well as OAL routines and the kernel itself. It can also be used to debug other kinds of code, although the kernel debugger may be more appropriate and is recommended.
Although the hardware debugger does not rely on an operating system, some hardware debugging features are relevant or available only when the Windows CE kernel is running.
Benefits of eXdi Hardware Debugging
The following benefits are offered by eXdi hardware debugging:
- Low intrusiveness and low-level control of the target CPU, and possibly other peripheral devices. For example, the CPU state can be completely frozen when it stops at an exception or a breakpoint, after a single step, or by asynchronous break.
- The potential to debug almost any code because of a lack of reliance on kernel support. For example, eXdi hardware debugging can be used to debug the boot loader, initialization, and critical kernel routines.
- Use of the hardware debugging register for enhanced code breakpoints, data breakpoints, and stepping in kernel debugger mode. For example, you can step or set breakpoints in ROM. They also cause a dramatic improvement in the execution time when data breakpoints are set.
- Extensibility through the use of plug-ins. Additional plug-ins are provided by third-party vendors for their probe or emulator. These plug-ins provide the following kinds of extensions to the hardware debugger:
- Execution trace
- Complex data breakpoint and code breakpoint triggering
- Flash programming utility or RAM downloader
- Peripheral registers browser
- Probe and target configuration
- Code coverage
- Post-mortem debugging
eXdi Hardware Debugging vs. Kernel Debugging
Table 2 below shows the differences between eXdi hardware debugging and kernel debugging with a kernel debug stub, KdStub.
Table 2. Comparison of eXdi hardware debugging and kernel debugging
|eXdi Hardware Debugging||KdStub Kernel Debugging|
|Does not rely on target-side debug routines. Can debug in every situation, just out of reset, provided the probe and the on-chip debug hardware, if applicable, are working and are not disrupted by the target application itself; for example, by accessing debug registers. Operates non-intrusively and independently of the kernel state.||Relies on KdStub to capture exceptions and breakpoints, access kernel information, request some kernel operations, communicate with the debugger, and restart the debugged code. Operates relatively intrusively and dependently.|
|Gets module information from the .bin file that is downloaded to the target device. Gets the first module, usually Nk.exe for the kernel module file, load address, and data relocation address.
By default, Platform Builder loads only one Nk.exe module. This means that, by default, only Nk.exe can be debugged with sources and symbols. However, there are ways to get more module loads.
The eXdi driver can return the information to Platform builder, provided the kernel tables are current and the driver matches the version of the kernel. If this capability is not supported, contact the driver vendor for an update.
You can manually edit the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Platform Builder\3.0\Hardware Debug\BinImage to force more modules to load.
|Finds the location of symbols dynamically, at run time through kernel-generated load and unload notifications.|
|May be able to read or write memory, registers, or breakpoints while running, depending on the type of target and probe being used.||Cannot read or write memory, registers, or breakpoints while running.|
|Choosing Break from the Debug menu results in an asynchronous break at the exact current execution location.||Choosing Break from the Debug menu results in calling a DebugBreak() in Shell.exe, a process on the target device. This implies that the Shell.exe threads are running. DebugBreak() contains a software breakpoint that is trapped by the Windows CE kernel and led to invoke KdStub.|
|When the target device is running, the Process and Thread windows can be populated by querying the Shell.exe process on the target device. However, the Process and Thread windows may not be available when the target is halted, depending on the eXdi driver. If this is the case, contact the driver vendor for an update. In both cases, the Windows CE operating system needs to be initialized properly.||When the target device is running, the Process and Thread windows are populated by querying the Shell.exe process on the target device. When the target device is halted, those windows are populated by querying KdStub. Therefore, these windows are always available.|
|The call stack does not go across processes. It is limited to the current process.||The call stack can be walked across processes by following interprocess communication (IPC) calls, similar to PSL.|
|The reduced intrusiveness of the eXdi hardware debugger has some side effects. For example, the target cannot, at the debugger's request, commit virtual memory pages that are not already committed by the operating system, running the target code. Therefore, the debugger cannot always read or write memory, or set software breakpoints, on any locations of a process space that has page-on-demand enabled. However, this is not the case for Nk.exe, and page-on-demand can be disabled globally for all other modules.||KdStub calls kernel routines, even when halted, to page in any virtual memory pages it needs to access.|
© 2000 Microsoft Corporation. All rights reserved.
The information contained in this document represents the current view of Microsoft Corporation on the issues discussed as of the date of publication. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication.
This white paper is for informational purposes only. MICROSOFT MAKES NO WARRANTIES, EXPRESS OR IMPLIED, IN THIS DOCUMENT.
Microsoft, Visual Basic, Visual C++, the eMbedded Visual Tools logo, Windows, and Windows NT are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.
Other product and company names mentioned herein may be the trademarks of their respective owners.
Microsoft Corporation, One Microsoft Way, Redmond, WA 98052-6399 USA