Function Logging
Volume Number: | | 8
|
Issue Number: | | 7
|
Column Tag: | | C Workshop
|
Function Logging in Think C
For debugging, optimizing, and better understanding a programs execution path
By Eric Shapiro, Ann Arbor, Michigan
About the author
Eric Shapiros works include Business Simulator®, EzTape®, and two new QuickTime programs: Spectator, a screen recorder from Baseline Publishing and VideoBeep, a silly Control Panel from Sound Source Unlimited.
Eric has taught Mac programming seminars for Apple and developed the course materials for Apples Macintosh Device Driver seminar. He is best known, however, as the author of everyones favorite singing trash can, The Grouch.
Overview
This article presents a simple way to log all function calls into a text file using Think C 5.0. The technique presented here is useful for debugging, optimizing, and obtaining a better understanding of a programs execution path.
History
In the old days before source level debugging, function logging was an important way to debug Mac programs. On one of my first Mac projects, I remember sending function names out the serial port to a machine running a terminal program in order to figure out where the program was crashing. At MacHack this year, Marshall Clow from Hewlett Packard showed how to log function calls to MacsBug in MPW. Preferring Think C to MPW, I decided that Id do a similar project for Think C. This article presents the results.
To Use the Logger
If all you want to do is use the function logger, heres what you need to do:
Add the file LogToFile.c to your Think C project.
Add the file ANSI or ANSI-Small to your project (for some string utilities).
Turn both the profiling and stack frame options on for the project.
Call LogInit during program initialization (with appropriate parameters).
Call LogDInit during program de-initialization.
Recompile your code.
Thats all there is to it! You can control logging more precisely as follows:
The following line of code turns profiling on for an entire file (if it appears outside of all functions) or for a single routine (if it appears within a function):
/* 1 */
#pragma options( profile, force_frame )
Likewise, the following line of code turns profiling off:
/* 2 */
#pragma options( !profile, !force_frame )
You can determine if logging is active at compile-time using:
/* 3 */
#if _option( profile )
Call SetLogState( true ) or SetLogState( false ) to turn logging on or off during program execution. You can do this via a menu selection, for example.
Call SetLogFlush( true ) or SetLogFlush( false ) to turn flushing on or off during program execution. Performance suffers considerably when flushing is active, but it can help find the location where a crash occurs.
Note: Writing to disk when debugging a program can be a dangerous activity. Be sure you have adequate backups before trying any new debugging technique.
Note: By default, the function logger handles calling depths up to 100 levels. This should be sufficient for all but the most recursive programs. You can change the constant kMaxLogDepth to control the maximum depth.
How The Logger Works
When profiling is active, Think C generates a call to the function _profile_ at the beginning of every function call. Actually, only functions that generate stack frames call _profile_, which is why we turn on the stack frame option as well (stack frames are described below). The _profile_ function has the following form:
/* 4 */
void _profile_( void *functionNamePString )
The first version of _profile_ I wrote simply logged the function name to a file. While this is adequate for some uses, I really wanted to create an indented list of function calls so a programmer can tell exactly who called each function. To do this, our code needs to be notified when a function exits as well as when it is entered. Heres where life gets a bit complicated.
Unfortunately, Think Cs profiler doesnt generate a function call whenever a function exits. I looked at Think Cs profiler source code and used a similar technique for my logger. The technique involves replacing return addresses on the stack with the address of our own routine. When a function exits via an rts instruction, it will unknowingly call our exit handler. Our exit handler will decrement a depth counter, write some text to the log file, and jump to the original return address. To understand how we trace back the stack to find return addresses, we need to know exactly how Think C handles stack frames.
Stack Frames
Before and during function execution, space must be allocated for the following items:
The functions parameters
The callers return address
The functions local variables
Additional information, such as register contents, that the function may want to store
The first two items, the parameters and return address, are placed on the program stack by the calling code. Space for the the local variables and register storage is also allocated on the stack, but by the function itself. For convenience, the compiler uses register A6 to reference local variables while the stack pointer itself can be lowered and raised during function execution. The previous value of A6 is also placed on the stack so the compiler can easily clean up the stack when a function exits.
If FuncA calls FuncB, and FuncB calls FuncC, the stack looks like that shown in Figure 1.
As you can see, register A6 always points to the start of a linked list of previous A6 values. This is how debuggers such as MacsBug can back-trace function calls (using the SC command). In the _profile_ routine, we need to find the return address of the function that called the function that called us. The assembly language code finds this value on the stack at 4 + the previous A6 value. We save the old return address in a global array before modifying the stack so we know where to return to when our exit handler is called.
Note: If Think Cs force stack frame option is not active, the compiler doesnt generate a stack frame for functions that have no local variables or parameters. Our _profile_ code is not called for these functions.
Sample Output
The code shown below produces the very simple log file also shown below. Note that the recursion in Func1 works properly. The braces in the output allow you to select an entire function using Think Cs Balance command.
/* 5 */
void DoSomething( void )
{
Func1();
Func2( 10 );
}
void Func1( void )
{
static short x = 0;
if ( ++x < 2 )
Func1();
}
short Func2( short x )
{
return( 2 * x );
}
Here is the sample output from the code:
/* 6 */
DoSomething
{
Func1
{
Func1 {}
}
Func2 {}
}
A simple object-oriented example produces the following output. Note how easy it is to follow the calling sequence of the constructors and destructors for both a base class and a child class.
/* 7 */
DoSomeOopStuff
{
BaseClass::BaseClass {}
ChildClass::ChildClass {}
ChildClass::MiscFunc
{
BaseClass::MiscFunc {}
}
ChildClass::~ChildClass {}
BaseClass::~BaseClass {}
}
Summary
A fairly simple technique was given to log function names to a text file for any Think C project, including object-oriented ones. The technique can be used to debug and optimize programs as well as to investigate a programs runtime activity.
Possible future additions include:
Put more information into the log file, such as the stack and register contents.
Make the output look better, perhaps by drawing a graph of the programs runtime activity.
Do some validity checking on the heap and file id to make sure that the system is in a reasonably safe state for writing.
Optionally log information to MacsBug using DebugStr() instead of to the log file.
Support for interrupt-driven code and trap patches
This would involve setting some semaphores, restoring the global variable context, and changing the I/O calls to be asynchronous. I dont think it would be very difficult, but as most Mac programmers know, nothing is difficult until you have to implement it.
Good luck, and let me know if you find any bugs.
Listing LogToFile.c
/*
LogToFile.c
© 1992 Rock Ridge Enterprises. All Rights Reserved.
Requires Think C 5.0x
To use this file:
1a) Turn on the profiling and stack frame options for your
entire project from the Edit/Options/Debugging dialog.
1b) Or, if you prefer, add the following line to your files:
#pragma options( profile, force_frame )
Place it within a function to profile just that function,
within a file for just that file, or in a header file.
2)Call LogInit at program initialization. For example:
LogInit( NULL, true, false, true );
3)Call LogDInit at program de-initialization. For example:
LogDInit();
4)You can call SetLogState( true ) or SetLogState( false )
to turn logging on and off dynamically.
*/
#include <string.h>
#include <files.h>
#include <errors.h>
#include <memory.h>
#include <pascal.h>
#include LogToFile.h
#define kMaxLogDepth 100 // maximum 100 calls deep
// create Think C text files as output
#define kThinkCreatorID KAHL
#define kTextFileTypeTEXT
// strings written to output file
#define kTabString ((void*) \t )
#define kReturnString((void*) \r )
#define kOpenBraceString ((void*) {)
#define kCloseBraceString ((void*) })
// exiting a function that called nobody
#define kSimpleExitString {}
// write this string the top of every log file
#define kStringForNewFile ((void*) )
// write this string between program runs when appending to
// the log file
#define kStringForReusedFile((void*)
\r\r*****************************\r)
// the largest buffer well need (+ a little extra)
#define kMaxStringLength ( kMaxLogDepth + 100 )
// the default output file is on the root of the boot disk
#define kBootVol -1
#define kRootDir 2L
#define kDefaultPName\PLog File.txt
// dont profile our profile handler
#pragma options( !profile )
/*
Permanent storage for this file
*/
static short gLogFileID = 0;
// vRefNum of log files disk
static shortgLogFileVol = -1;
// true = call FlushVol after every write
static Boolean gAlwaysFlush = false;
// true = logging is active
static Boolean gLogActive = false;
static FSSpec gDefaultFileLoc = { kBootVol, kRootDir,
kDefaultPName };
/*
Info on the calling chain
*/
// how many calls deep are we?
static long gDepth = 0;
// the return addresses
static void *gStack[ kMaxLogDepth ];
// are braces needed for this function?
static Boolean gNeedBraces[ kMaxLogDepth ];
/*
Internal routine prototypes
*/
void _profile_( void *pFuncName );
void StringPtoC( void *source, void *target );
OSErr WriteOpenBrace( short howDeep );
OSErr WriteFunctionEntry( void *pString );
OSErr WriteFunctionExit( void );
OSErr WriteOpenBrace( short howDeep );
/**********************************
LogInit - Call at program start
fileLoc- set to NULL or a valid FSSpec for the
output file
deleteOld- true => truncate old log file
alwaysFlush- true => call FlushVol a lot (better log
file if you crash)
startLogging - true => turn logging on, false => dont
log yet
**********************************/
OSErr LogInit( FSSpec *fileLoc, Boolean deleteOld,
Boolean alwaysFlush, Boolean startLogging )
{
OSErr err = noErr;
BooleancreatedFile = false;
if ( !fileLoc )
// use default if user doesnt specify one
fileLoc = &gDefaultFileLoc;
// in case user calls init twice
if ( !gLogFileID )
{
/*
Create the file & open the data fork for writing.
*/
err = FSpCreate( fileLoc, kThinkCreatorID,
kTextFileType, 0 );
if ( !err )
createdFile = true;
err = FSpOpenDF( fileLoc, fsRdWrPerm, &gLogFileID );
if ( err ) goto DONE;
}
/*
Clear out the file if the user requests it.
*/
if ( deleteOld )
{
err = SetEOF( gLogFileID, 0L );
if ( err ) goto DONE;
}
/*
Append to the file
*/
err = SetFPos( gLogFileID, fsFromLEOF, 0 );
if ( err ) goto DONE;
/*
Setup the globals and write **** to the file.
*/
gAlwaysFlush = alwaysFlush;
gLogActive = startLogging;
gLogFileVol = fileLoc->vRefNum;
// write a header to the file
if ( deleteOld || createdFile )
err = WriteLogString( kStringForNewFile );
else
err = WriteLogString( kStringForReusedFile );
DONE:
if ( err )
LogDInit();
return( err );
}
/**********************************
LogDInit - Call at program exit
**********************************/
OSErr LogDInit( void )
{
OSErr err;
if ( !gLogFileID )
return( openErr );
/*
Close the file and flush the data to the disk.
*/
err = FSClose( gLogFileID );
if ( !err )
err = FlushVol( NULL, gLogFileVol );
/*
Clear out the globals so we know were not active.
*/
gLogFileID = 0;
gLogActive = false;
return( err );
}
/**********************************
SetLogState - Call to start or restart logging.
Returns previous state.
**********************************/
Boolean SetLogState( Boolean startLogging )
{
Booleanresult;
result = gLogActive;
gLogActive = startLogging;
return( result );
}
/**********************************
SetLogFlush - Call to start or restart flushing.
Returns previous state.
**********************************/
Boolean SetLogFlush( Boolean startFlushing )
{
Booleanresult;
result = gAlwaysFlush;
gAlwaysFlush = startFlushing;
return( result );
}
/**********************************
ClearLogFile - Call to zero out the log file
**********************************/
OSErr ClearLogFile( void )
{
OSErr err = noErr;
OSErr err2;
if ( !gLogFileID )
return( openErr );
err = SetEOF( gLogFileID, 0L );
if ( gAlwaysFlush )
{
err2 = FlushVol( NULL, gLogFileVol );
if ( !err )
err = err2;// return the first error to occur
}
return( err );
}
/**********************************
WriteLogString - Write a C string to the log file
**********************************/
OSErr WriteLogString( void *cString )
{
OSErr err = noErr;
OSErr err2;
long numBytes;
if ( !gLogFileID )
return( openErr );
/*
Write the data to the file
*/
numBytes = strlen( cString );
err = FSWrite( gLogFileID, &numBytes, cString );
/*
Flush the volume if we always flush
*/
if ( gAlwaysFlush )
{
err2 = FlushVol( NULL, gLogFileVol );
if ( !err )
err = err2;
}
return( err );
}
/**********************************
StringPtoC - Convert a pascal string to a c string
**********************************/
static void StringPtoC( void *source, void *target )
{
BlockMove( source, target, 1 + *( (unsigned char*)source ));
PtoCstr( target );
}
/**********************************
WriteFunctionEntry - called by _profile_ whenever a function is entered
The following string is written to the output:
<CR> + <TABS> + FunctionName
Where <CR> is a carriage return and <TABS> indicates 1
tab per depth.
**********************************/
static OSErr WriteFunctionEntry( void *pString )
{
Str255 cString;
unsigned char theString[ kMaxStringLength ];
long count;
OSErr err;
// convert func name to c string
StringPtoC( pString, cString );
// start with a carriage return
strcpy( (void*)theString, kReturnString );
// 1 tab for each level
for ( count=0; count<gDepth; count++ )
strcat( (void*)theString, kTabString );
// the function name
strcat( (void*)theString, (void*)cString );
// write the string
err = WriteLogString( theString );
return( err );
}
/**********************************
WriteFunctionExit - called whenever a function is exited
by our exit handler
If this function called another function, write the
following string:
<CR> + <TABS> + }
Otherwise, write:
{}
Where <CR> is a carriage return and <TABS> indicates 1
tab per depth.
**********************************/
static OSErr WriteFunctionExit( void )
{
OSErr err = noErr;
long count;
unsigned char theString[ kMaxStringLength ];
if ( gNeedBraces[ gDepth ] )
{
// start with a carriage return
strcpy( (void*)theString, kReturnString );
// indent 1 tab for each level
for ( count=0; count<gDepth; count++ )
strcat( (void*)theString, kTabString );
// then a close-brace
strcat( (void*)theString, kCloseBraceString );
}
else
{
// just write exit
strcpy( (void*)theString, kSimpleExitString );
}
// write the string
err = WriteLogString( theString );
return( err );
}
/**********************************
WriteOpenBrace - adds some tabs and an open brace to the
output
The following string is written to the output:
<CR> + <TABS> + {
Where <CR> is a carriage return and <TABS> indicates 1
tab per depth.
**********************************/
static OSErr WriteOpenBrace( short howDeep )
{
OSErr err;
unsigned char theString[ kMaxStringLength ];
// start with a return
strcpy( (void*)theString, kReturnString );
// 1 tab per level deep
while( howDeep- > 0 )
strcat( (void*)theString, kTabString );
// add an open-brace
strcat( (void*)theString, kOpenBraceString );
err = WriteLogString( theString );
return( err );
}
/**********************************
_profile_ - Called every time a function is entered
This more complicated version does the following:
1) Prints the function name to the file in
an indented list
2) Saves the return address of the callers
caller into gStack[]
3) Modifies the stack so that the caller returns
to our code and not to its caller.
4) Prints exit info when the function is exited and
then jumps to the correct return address.
**********************************/
void _profile_( void *pFuncName )
{
OSErr err;
if ( !gLogFileID ) return; // output file not opened
if ( !gLogActive ) return; // logging is off
if ( gDepth >= kMaxLogDepth ) return; // were too deep
/*
We have to put an open brace in the output if the parent
function hasnt called any other functions until now.
*/
if ( gDepth > 0 )
if ( !gNeedBraces[gDepth-1] )
{
gNeedBraces[gDepth-1] = true;
err = WriteOpenBrace( gDepth-1 );
if ( err ) return;
}
// write the function name
err = WriteFunctionEntry( pFuncName );
if ( err ) return;
gNeedBraces[ gDepth ] = false;
/*
Save the return address that the caller will return to.
Modify the stack so that the caller will return to us
instead.
*/
asm
{
; gStack[ gDepth ] = return address where caller
; will return to
; A1 = &gStack[ gDepth ]
lea.l gStack, A1
move.l gDepth, D0
lsl.l #2, D0
adda.l D0, A1
move.l (A6), A0 ; A0 = A6 from previous call
move.l 4(A0), (A1); gStack[ gDepth ] = ret Addr
; Change the return address on the stack to
; point to our code
lea @FuncExit, A1
move.l A1, 4(A0)
addq.l #1, gDepth ; were one level deeper now
; return to caller
unlk A6
rts
}
/*
This code is executed when a profiled function exits
*/
FuncExit:
asm
{
move.l D0, -(SP); save return value onto stack
subq.l #1, gDepth ; were one level more shallow
; write exit info to the file
jsr WriteFunctionExit
; get the real return address from our array
; and jump to it
; A0 = &gStack[ gDepth ]
lea.l gStack, A0
move.l gDepth, D0
lsl.l #2, D0
adda.l D0, A0
move.l (SP)+, D0; restore return value
move.l (A0), A0 ; A0=real return address
jmp (A0); jump to real return address
}
}