Winter 92 - TRACKS: A NEW TOOL FOR DEBUGGING DRIVERS
TRACKS: A NEW TOOL FOR DEBUGGING DRIVERS
BRAD LOWE
Here's a tool that gives you access to what you really need to know while debugging a
driver. With Tracks, you decide what kind of information you want to track-- variable
contents, who called the current function, timing information, and more--all while
your driver's running. When a problem arises, you can easily tell where your
driver's been and what it's been doing, so you can find out just what went wrong.
If you've ever written a device driver, you know how hard it is to keep track of what's
going on. Learning the value of variables and other data as the driver runs usually
requires a lot of time in a debugger.
When a driver crashes, trashing the stack in the process, it's often impossible to
determine the last routine that was executed. Finding the bug can take many hours,
especially if the crash appears only periodically. Even after you've found the bug, each
crash requires recompiling, building, restarting, and retesting. Anything to help
locate bugs more quickly and accurately could save a lot of time and frustration.
That's why Tracks was written. Whether you're writing your first or your fiftieth
driver, it can help you track down those nasty bugs that always show up. The simple
macros in Tracks make it easy to log all kinds of information from a driver written in
C or C++. You can record strings, data blocks, longs, and even formatted data types.
Tracks can write debugging information directly to disk as it comes in, or it can keep
the information in a circular buffer and dump it to disk on command--a MacsBug dcmd
(debugger command) lets you do this even after a crash.
You can completely control what information is logged, and your driver won't even
know it. If you know a routine works, you can turn off calls from it at any time--
including while your driver's running.
On theDeveloper CD Series disc, you'll find TestDrvr, a sample driver that
demonstrates how to implement Tracks functionality in a simple (and useless) driver.
Also enclosed is the complete source for the Tracks utilities, as well as all the
necessary support tools. In the following sections, you'll find out about how Tracks
works, what kind of information the Tracks macros log, and what the code does. You'll
also get some pointers on installing and using Tracks. If you're eager to start using
Tracks, take a look at "Tracks in Action.
HOW TRACKS WORKS
Tracks works somewhat like a message service that can accept telephone calls on 128
different lines from the target driver. You decide where to install the lines and what
kinds of messages each line will deliver. You can control which lines to listen to (or
not) and where to save incoming messages.
The invocations of macros--or calls--that send information to Tracks are
calledtracepoints. You assign each tracepoint a number between 0 and 127, called
adiagnostic ID (diagID), and a name. The diagID represents one bit in a 128-bit flag
that can be set or cleared from the Tracks control panel device (cdev). When a
tracepoint is encountered, data is logged only if the corresponding bit has been set.
Being able to set or clear tracepoints on the fly allows you to tailor the type of
information being traced. By assigning a meaningful name to each tracepoint, you'll
know which ones to set or clear, and the name of the tracepoint will be recorded with
any Tracks output. Tracksbreakpoints are tracepoints that will drop you into your
debugger.
GROUPING INFORMATION
Because the diagID doesn't have to be unique, a tracepoint can represent a single Tracks
call, a type of Tracks call, or a grouping of Tracks calls. A type of Tracks call, for
instance, might be all error- reporting calls. A grouping might be all tracepoints in a
particular routine.
This kind of flexibility allows you to group your information into logical and
functional units. It's up to you to create as many or as few tracepoints as you need. For
instance, if you're working on a new routine, you may set a whole bunch of Tracks
calls all to the same diagID. When you test the routine, you can set some or all of the
other switches to off and focus on the messages from that routine. Later, when you
know the function works, you can keep that switch off.
Numbering for ease of use. There aren't any limits on how you group your
diagIDs. You might assign all messages to one tracepoint or simply start at 0 and
increment by 1 from there. The key is that once you know something works, you want
to be able to turn off tracing in that area. By assigning unique diagIDs to groups of
Tracks calls, you can quickly tailor your tracing.
For convenience, there are four groups of 32 tracepoints each (0-31, 32-63,
64-95, and 96-127) that you can turn on or off with a click. (The Tracks cdev
contains buttons for levels 1 through 4, which correspond to these four groups.) Most
new users start out tracing all information. But as more and more Tracks output is
added, information overload can be a problem, and it's great to be able to limit Tracks
information easily.
PartCodes are used to identify consecutive Tracks calls that have the same diagID.
PartCodes should start at 0 and increment by 1 for each additional Tracks call with the
same diagID. For example, say you wanted to dump the contents of all three parameters
you receive on entry to a function. You'd probably want all these to have the same
diagID. The first Tracks call should have a partCode of 0, the next call a partCode of 1,
and so on. The partCode makes it more evident if some Tracks information is lost. Data
can be lost if the circular buffer fills before writing to a file, and data can be locked
out if Tracks is already in use.
THE TRACKS MACROS
To log data from your driver, you call one of five simple macros from your driver
code. Each macro logs a different kind of information. All the calls must have access to
your driver's global storage and follow the numbering conventions just described for
the diagID and partCode.
T_STACK(diagID);
T_STACK, one of the most useful calls, records the current function and who called it.
If the driver is written in C++, a special unmangler automatically prints out the
arguments that were passed to the function. If called from every major routine,
T_STACK will leave the proverbial trail of bread crumbs. T_STACK's partCode is
always 0.
T_DATA(diagID, partCode, &dataBlock, sizeof(dataBlock));
T_DATA is used to dump a block of memory, formatted in hexadecimal and ASCII.
T_TYPE(diagID, partCode, recordPtr, sizeof(Record), "\pRecord");
T_TYPE records a data structure. The address, size, and a Pascal string with the name
of the structure must be passed to the macro. The format of the data structure must be
defined in an 'mxwt' resource, stored in your driver or in your MacsBug Debugger
Prefs file. If the resource to define the structure isn't found, the data will be treated as
a T_DATA call. Since the templates are used only to format data, you don't need to use
MacsBug.
T_PSTR(diagID, partCode, "\pA string you'd like to see");
T_PSTR simply records a Pascal string.
T_PSTRLONG(diagID, partCode, "\ptheLong = ", theLong);
T_PSTRLONG records a Pascal string and a long. Usually the string is used to tell you
what follows. Feel free to cast whatever you can get away with to the long.
TRACKS IN ACTION
TestDrvr is a simple driver skeleton that checks the status of the keyboard. If the
Option key is down, it logs one type of data, and if the Command key is down, it logs
another type of data.
To see Tracks in action, follow these condensed instructions:
1. Put DumpTracks into your MPW Tools folder and TestDrvr into your
System Folder or Extensions folder.
2. Put Tracks into your System Folder or, in System 7, into your Control
Panels folder.
3. Restart your Macintosh.
4. Open the Tracks cdev, click the Driver Name button, and locate the file
TestDrvr.
5. Turn on Tracks and click the Level 1 button to turn on tracepoints 0-31
(only 0-3 are used).
6. Press the Command key or the Option key to begin to log data. The Bytes
Buffered field should change.
7. Click Write Buffer to send TestDrvr output to disk. Only data written to
disk can be examined.
8. Start up MPW and type "DumpTracks" to see what was just traced.
Look over the TestDrvr source code if you haven't already done so. Don't forget to
remove TestDrvr when you're done. For information about the output from the
example, see the section "Examining Tracks Output" under "Using Tracks.
A LOOK AT THE TRACKS CODE
This section is for folks who are really wide awake and ready for the gritty details. (If
you're not one of those folks, you may want to jump ahead to the "Installing Tracks
section.) The Tracks file contains a cdev, an INIT, and the Tracks driver. The Tracks
driver has three key responsibilities: maintaining the cdev, sending messages to the
target driver, and accepting data from the target driver via the trace procedure
(TraceProc). Figure 1 shows the flow of data between Tracks and the target driver.
MAINTAINING THE CDEV
The Tracks driver's first responsibility includes sending status information to the
cdev and responding to cdev commands like "clear buffer" and "write file." Because the
cdev displays the status of fields that can change at any time, the cdev monitors the
driver and updates fields as they change.
The Tracks driver doesn't always need periodic (accRun) messages. When the driver
gets a message to turn its periodic write-to-file flag on or off, the driver sets or
clears its dNeedTime bit in the dCtlFlags. (Recall that BitClr, BitSet, and BitTst test
bits starting at the high-order bit.)
BitClr(&dCtl->dCtlFlags, 2L);/* Clear bit 5 = dNeedTime bit. */
BitSet(&dCtl->dCtlFlags, 2L);/* Set bit 5 = dNeedTime bit. */
SENDING MESSAGES TO THE TARGET DRIVER
The Tracks driver can send one of two messages to the target driver: "enable tracing
or "disable tracing." The enable message passes the target driver a function pointer
that points to an address within the Tracks driver code as well as a pointer that points
to the Tracks driver's own globals. The target driver needs to save both of these
because they're needed by the Tracks macros. The macros use the function pointer to
call the Tracks driver directly, passing it the globals pointer along with tracing data.
When the target driver gets the disable message, the saved function pointer is set to
nil. (For the code to handle enable and disable messages, see the "Installing Tracks
section.) The Tracks macros in the target driver check to see if the function pointer is
nil, and if it isn't, the target driver calls the function pointer within Tracks with
arguments that correspond to the particular Tracks function. The macro that checks
and invokes a non-nil function pointer is defined in the following code. The macros
used in the target driver's code reference this macro. Notice that for a Tracks call to
compile, it needs to access your globals by the same name, in this case by the name
globals. Macros are used so that they can easily be compiled out of the final product.
Figure 1How Tracks Interacts With the Target Driver
#define TRACE(diagID, partCode, formatID, data1, data2, data3)
func = globals->fTraceProcPtr;
if ( func != nil )
(*((pascal void (*)(long, unsigned char, unsigned char,
unsigned char, long, long, long))func))