A Minimal Object-Oriented Debugger for C++

This portable symbolic debugger lets you trace execution, display object values, and more.


October 01, 1991
URL:http://www.drdobbs.com/cpp/a-minimal-object-oriented-debugger-for-c/184408632

OCT91: A MINIMAL OBJECT-ORIENTED DEBUGGER FOR C++

William M. Miller is director of North American Operations for Glockenspiel Ltd. and vice chair of X3J16, the ANSI Standard C++ Committee. He can be reached via Internet at [email protected] and on CompuServe at 72105,1744.


When C++ first appeared outside AT&T Bell Laboratories in 1985, early adopters of the language enjoyed its power and expressiveness, but suffered from a dearth of specialized tools. The lack of a C++-specific debugger in particular hampered these pioneering programmers.

Happily, those days are mostly past. In addition to other tools, most C++ compiler vendors now also provide quite serviceable debuggers. Nevertheless, there remain some platforms for which only a C or assembly-oriented debugger (or even no debugger at all, in some embedded development environments) is available.

With a little foresight, however, C++ source-level debugging is possible even in these tool-poor environments. That's where MOOD -- the Minimal Object-Oriented Debugger -- comes in. MOOD, in keeping with its name, makes no pretense of providing every imaginable debugging service. It does, however, offer the ability to trace through program execution, set breakpoints, and interactively display the values of objects, as well as provide a framework on which more elaborate debugging facilities can be built, as required.

Theory of Operation

In essence, MOOD transfers control to a central routine each time an "interesting event" occurs. Such interesting events include program start and termination, function entry and exit, construction and destruction of objects, and user breakpoints. Whenever the central routine is invoked, it examines its internal state and the nature of the event to determine whether to initiate an interactive dialogue with the user, or simply to return and allow the program to continue its normal execution.

C++ makes it easy to gain control during these "interesting events" through the semantics of constructor and destructor operation. Whenever an object is created, its constructor is invoked. This guarantee by the language holds whether the object is global static (and hence initialized at the very beginning of the program execution), local static or automatic (and therefore initialized when control flows through its declaration), or on the heap (and thus initialized when it is explicitly allocated). The same is true of destructors whenever an object goes out of existence. Furthermore, not only are the constructor and destructor of the object itself invoked, but those of all of its base classes as well.

Normally, the purpose of running an object's constructor is to ensure that the object is internally consistent and to acquire any resources -- memory, files, devices, and so on -- it needs; the destructor is responsible for releasing those resources. However, it is perfectly reasonable for MOOD to piggyback on top of these normal constructor and destructor operations and then transfer control to the MOOD kernel at that time.

The application of constructor and destructor semantics to tracing object creation and destruction is obvious. If the object being created or destroyed is derived from a class whose constructor and destructor contain calls to the MOOD kernel, MOOD will have the opportunity to display a message or converse with the user at those times.

Using constructors and destructors to transfer control to MOOD at function entry and exit is equally straightforward, but perhaps a bit less obvious. In this case, a class must be defined whose constructor and destructor call the MOOD kernel indicating function entry and exit, and an auto object of that class must be declared as the first statement in each function whose execution is to be traced. When that object is created, at the beginning of the function's execution, MOOD will be informed of the function's invocation; when it is destroyed, that is, when the function returns, the destructor will notify MOOD that the function has terminated.

Extending these concepts to the start and finish of the entire program is simply a matter of defining a global static object whose constructor and destructor perform the necessary MOOD calls.

Programming with MOOD

As can be inferred from the preceding section, MOOD is an intrusive debugger; that is, it is necessary to make certain changes to the source code of the program in order to debug it (the "foresight" mentioned at the start of this article). However, these changes are quite minor in nature and extent, as the following paragraphs demonstrate.

The first requirement placed on code to be debugged with MOOD is that it include the header file MOOD.hxx (Listing One, page 110). This file contains all the declarations required by the interface with MOOD.

Function entry and exit tracing is enabled by declaring an object of class trace as the first statement of each traced function. The trace constructor takes a character string argument specifying the name of the function; this name is displayed whenever MOOD is running in verbose mode and can be used to specify a breakpoint. For example, a function to be traced would be written as follows:

  void some_func( ) {
     trace t("some_func");
     // ...
     }

Debugging data is a bit more involved but still not terribly burdensome. Three things are required of each class whose objects are to be made known to MOOD: First, it must declare class monitored as a virtual base class. Second, it must pass a string, provided in the object declaration, to the monitored constructor. This string is intended to be the name of the object being constructed; like the function name in class trace, it will be displayed in verbose mode at construction and destruction time, and it can be used for breakpointing. The third requirement is to provide an override for the virtual function display( ); it should print out the contents of the object on stderr in whatever form is most appropriate. A sample class and object declaration for MOOD is shown in Example 1.

Example 1: A sample class and object declaration for MOOD

  class some_class: public virtual monitored {
  public:
     some_class(const char* obj_name):
     monitored(obj_name) { }
     void display() {
        fprintf(stderr, "i=%d\nj=%d\n", i, j);
        }
  private:
     int i;
     int j;
  };

  some_class an_object("an_object");

There are two important reasons that derivation from class monitored should be virtual instead of ordinary. First, in an elaborate inheritance hierarchy, the most-derived class (that is, the class used in the declaration of the object) may have several base classes, each of which is derived from class monitored. If ordinary inheritance were used, a verbose mode trace would include construction and destruction records for each of those classes when the object was created and destroyed. Making monitored a virtual base class causes the message to occur only once, because there is only a single instance of monitored in the object.

Second, deriving virtually from class monitored means that the most-derived class's constructor is both permitted and required to supply the argument to the class monitored constructor. This is important because only the object declaration itself "knows" the name of the object being declared, and virtual inheritance makes it both easy to pass that information along and impossible to forget to do so.

The final feature of MOOD.hxx that can be used by a program is insertion of conditional or unconditional breakpoints. Normally, MOOD allows user interaction (a "breakpoint") at any time a message would be printed in verbose tracing mode -- that is, at function entry and exit and object construction and destruction. If additional breakpoints are required, for instance, at some interesting point in an algorithm that does not correspond to one of the traced events, the programmer can insert explicit calls to the cond_break( ) function.

A call to cond_break( ) with no arguments is an unconditional break; MOOD will notify the user and enter an interactive dialogue, regardless of any other conditions. To make a breakpoint conditional, use a call that specifies a name (for example, cond_break(USER_BP, "my breakpoint")). Then, when the user tells MOOD that execution should proceed to a particular named event, MOOD will ignore conditional breakpoints whose names do not match the specified name.

Listing Two, page 110, shows a simple program that illustrates many of the features of interfacing with MOOD. The foo::display( ) function simply prints out the object name to demonstrate that the mapping from printed address to actual object is correct.

Getting into the MOOD

One other feature of MOOD.hxx is worth discussing. The class init_ctl, declared at the end of Listing One, along with its associated object declaration, dbg_init, is vital for enabling the operation of MOOD. Because dbg_init is global static, it is constructed before program execution begins and destroyed after the program completes. The constructor of class init_ctl contains the call to cond_break( ) for the start of the program, which allows the user to gain interactive control immediately to turn on verbose mode, go to a particular breakpoint, and so on.

There will be one copy of dbg_init in each compilation unit that includes MOOD.hxx (it's a static variable), so a way must be found to avoid having multiple invocations of cond_break() for program start. This once-only limitation is implemented by means of the static member_count; the init_ ctl constructor increments _count and only calls cond_break( ) when the very first instance of dbg_init is created.

MOOD will also never be called if the preprocessor variable DEBUGGER_ON is not defined; all the classes in MOOD.hxx are conditionally compiled to do nothing in its absence. (User code calls to cond_break( ) should be similarly protected.)

Using MOOD

MOOD's user interface is primitive but functional. The available commands are described in Table 1, so the description here will be limited to an overview of how to use MOOD.

Table 1: The MOOD Debugging Commands

  In its current form, MOOD recognizes the following commands:

  s           Step to the next "interesting event" and return to
              interactive mode

  g [<name>]  Go until the next event that has the
              specified name (which can be a function, object,
              or user breakpoint).  If no name is specified, go
              until the next user breakpoint or until the end of
              the program.

  d <addr>    Display an object, selected by the address
              printed out at the time the object was
              constructed.  This command calls the display()
              member function for the object whose
              "monitored" subobject is at the referenced
              address, which is displayed at construction time
              if in verbose mode.

  v           Turn on verbose mode.  This sends messages
              to stderr upon function entry and exit, and upon
              object construction and destruction.

  q           Turn off verbose mode.

All user commands are implemented in MOOD.cxx (Listing Three, page 110). The implementation is rudimentary, and is intended only to provide a foundation of basic functionality. There are many ways in which this functionality can be extended. Some suggestions for improvement are described at the end of this article.

When a program compiled for MOOD first begins execution, the user is presented a welcoming banner and a command prompt. At this point, one possible user action is to type a "v" (for verbose) command, followed by a "g" (go) command with no argument. The program will then run without pausing, and as it executes, MOOD will print (on stderr, to avoid conflict with ordinary program output on stdout) a descriptive message for each interesting event that occurs.

Another possibility at the opening prompt is to first type v" (or not, depending on the volume of output desired), and then type "g" followed by a name. The program will execute normally until an interesting event with the specified name occurs, at which time the user will be presented an opportunity for further interaction. MOOD makes no distinction among object names, function names, and breakpoint names, so any of these can be used with the "g" command.

A third possibility is to "single step" with the "s" command. Due to MOOD's implementation, the step increment is limited to interesting events," rather than individual C++ statements.

The commands mentioned so far are all allowed at any MOOD breakpoint, not just at the start of the program. You can step from one breakpoint to another using the "g" command, then turn off verbose mode using the "q" (quiet) command, and so on. Furthermore, at any breakpoint, any object that has been constructed but not destroyed can be displayed by passing its address (the one printed by MOOD at construction) to the "d" command. This command will invoke the object's display( ) member function. (Note: It is important not to attempt to display an object during the breakpoint resulting from that object's construction! MOOD is entered from the monitored constructor, and the virtual function table pointing to the derived class's display() member function has not been set up at that point. Similarly, the breakpoint for an object's destruction occurs after the object has been destroyed and its display( ) member function is no longer available.)

A sample debugging session, using the program in Listing Two, is shown in Example 2. Explanatory comments (text following "//" in the listing) were added after the log was made and are not part of the program's input or output.

Example 2: A sample session with MOOD

This is a transcript of a MOOD debugging session. The program being debugged is tdbg.cxx, shown in Listing Three. Any remarks preceded by a double-slash (//) are comments which were added after the session was transcribed.

  tdbg                  // Start the demo program.
  MOOD:
  Minimal Object-Oriented Debugger version 0.0
  cmd> v                // Verbose mode is on.
  cmd> g y              // Go till we get to y.
  Enter main
  Construct *p @ 27A8
  Enter x               // Entering function x().
  Construct xf @ 2746
  Enter y               // Entering function y().
  cmd> d 27a8           // Ask to display an object.
  *p                    // Yep, that's the right one.
  cmd> d 2746           // Display another object.
  xf                    // Which it does.
  cmd> s                // Single step.
  Construct yf @ 271C
  cmd> s                // Single step again.
  Destruct yf @ 271C
  cmd> g z              // Go till we get to z().
  Exit y
  Destruct xf @ 2746
  Exit x
  Enter z               // Entering function z().
  cmd> g                // Here we are.  So just finish up.
  Construct zf @ 2746
  Destruct zf @ 2746
  Exit z
  Destruct *p @ 27A8
  Exit main
  End of execution

Where to Go from Here

The debugging facilities of MOOD are useful but quite rudimentary. They can be improved in various ways. For example, you could, without much effort, allow symbolic lookup of object names by maintaining a dynamic symbol table of live objects. This would allow display of objects by name rather than by address. Another suggestion is to allow the user to modify object values interactively during a breakpoint. A third idea is to allow program execution to be interrupted by a particular keystroke combination.

The MOOD technique requires some cooperation from the program being debugged, but the other side of that coin is that the capabilities of the approach are limited only by the imagination of the programmer.

Acknowledgment

The techniques embodied in MOOD were described in "Debugging and Instrumentation of C++ Programs" by Martin O'Riordan, then of Glockenspiel, in the Proceedings of the 1988 USENIX C++ Conference.


_A MINIMAL OBJECT-ORIENTED DEBUGGER FOR C++_
by William M. Miller


[LISTING ONE]


// MOOD.hxx by William M. Miller, 8/3/91. MOOD user declarations.

/* This header file is included in every module which is to be metered
 * by MOOD.  Its actions are controlled by the preprocessor variable
 * DEBUGGER_ON. If not this is not defined, no debugger actions will occur.
 */
#ifndef _DEBUGGER_DEFS
#define _DEBUGGER_DEFS
                      // Conditions under which cond_break may be called:
enum break_condition {
   PROG_START,  PROG_END,  FCN_ENTRY,  FCN_EXIT,  OBJ_CTOR,  OBJ_DTOR,
   USER_BP
   };

/* The cond_break() function is called in all the above contexts for two
 * purposes: 1) to display trace information when verbose mode is on, and
 * 2) to break under the appropriate conditions to allow the user to
 * interact with the debugger.  The default arguments are to allow a user
 * program to contain the call "cond_break();" with no arguments to perform
 * an unconditional breakpoint into the debugger's interactive mode.
 */
void cond_break(break_condition cond = USER_BP, const char* name = 0,
      void* addr = 0);

/* The trace class is intended to allow function tracing.  Each function or
 * block which should be included in the trace should declare an object of
 * class trace at the very beginning.  The result, in verbose mode, will be
 * to display the function/block name at entry and exit; this name can also
 * be used to set a breakpoint from the debugger's interactive mode.
 */
class trace {
public:
#ifdef DEBUGGER_ON
    trace( const char* fcn_name): _name(fcn_name)
                  {  cond_break( FCN_ENTRY, _name);  }
   ~trace( )      {  cond_break( FCN_EXIT,  _name);  }
private:
   const char* _name;
#else
   trace( const char* ) { }
#endif
   };

/* The monitored class is intended for use as a virtual base class of any
 * classes whose construction/destruction is to be traced in verbose mode
 * or whose values are to be displayed interactively.  Derived classes must
 * pass the object or class name to the constructor and must supply an
 * override to the display() member function.
 */
class monitored {
public:
#ifdef DEBUGGER_ON
    monitored( const char* obj_name ): _name(obj_name)
                     {  cond_break(OBJ_CTOR, _name, this);   };
   ~monitored( )     {  cond_break(OBJ_DTOR, _name, this);   }
   virtual void display() = 0;
private:
   monitored() { }  // keep cfront 2.0 happy -- it requires a default
                    // constructor in virtual base classes for no good reason.
   const char* _name;
#else
   monitored( const char* ) { }
   monitored( ) { }
#endif
   };

/* The following class and static object call cond_break exactly once at
 * program start, before any debuggable object is created, to allow the
 * user to set up tracing and breakpoints, and once at the end of execution
 * to allow for any needed cleanup.
 */
#ifdef DEBUGGER_ON
class init_ctl {
public:
    init_ctl( )   { if (_count++ == 0)   cond_break(PROG_START);   }
   ~init_ctl( )   { if (--_count == 0)   cond_break(PROG_END);     }
private:
   static int _count;
};

static init_ctl dbg_init;
#endif
#endif






[LISTING TWO]


// tdbg.cxx by William M. Miller, 8/3/91. This is a sample
// program to be debugged with MOOD. A transcript of the debugging
// session is shown in Example 3, accompanying this article.

extern "C" {
#include <stdio.h>
}
#include "MOOD.hxx"

struct foo: virtual monitored {
   foo( const char* nm ): monitored(nm), my_name(nm) { }
   void display( )        { fprintf(stderr, "%s\n", my_name);  }
   const char* my_name;
   };
void x();
void y();
void z();

int main() {
   trace tt("main");
   foo* p = new foo("*p");
   x();
   z();
   delete p;
   return 0;
   }

void x() {
   trace tt("x");
   foo xf("xf");
   y();
   }

void y() {
   trace tt("y");
   foo yf("yf");
   }

void z() {
   trace tt("z");
   foo zf("zf");
   }






[LISTING THREE]


// MOOD.cxx, by William M. Miller, 8/3/91. MOOD kernel definitions.

/* This routine implements the user interface of MOOD, the Minimal Object
 * Oriented Debugger. Example 2 in the accompanying article describes
 * currently available commands: s, g, d, v, and q.
 */
extern "C" {
#include <stdio.h>
#include <string.h>
}
#define DEBUGGER_ON 1
#include "MOOD.hxx"

/* The objp() function does a system-dependent conversion of an ASCII pointer
 * specification into a pointer to a monitored object.
 */
monitored* objp(const char* str);

/* The cond_break() function is called under the various circumstances
 * described by the enumeration break_condition.  It prints a message, if
 * required, describing the reason for its call, and optionally enters
 * interactive mode to take commands.
 */
void cond_break(break_condition cond, const char* name, void* addr) {

   static int tracing = 0;          // => verbose mode
   static char brk_name[128] = "";  // name on which to break
   static int was_step = 1;         // last cmd was "step" => go to
                                    // interactive mode
   char buff[128];                  // command line buffer

/* We enter the display and possible interactive mode code under the following
 * conditions:
 * 1) We are tracing (verbose mode).
 * 2) We are stepping.  (Note: this is initially TRUE, which takes care of
 *    getting into interactive mode on the PROG_START call.)
 * 3) The breakpoint name was set and the current name matches it, or this
 *    is a user breakpoint call with no name
 * 4) The breakpoint name was not set and this is a user breakpoint call.
 */
   if (tracing || was_step ||
         (brk_name[0] && ((strcmp(brk_name, name) == 0) ||
         (cond == USER_BP && !name))) || (!brk_name[0] && cond == USER_BP)) {

      switch(cond) {     // Print an appropriate message:

      case PROG_START:
         fprintf(stderr,"MOOD: Minimal Object Oriented Debugger, V. 0.0\n");
                                                                       break;
      case PROG_END:    fprintf(stderr, "End of execution.\n");        break;

      case FCN_ENTRY:   fprintf(stderr, "Enter %s\n", name);           break;

      case FCN_EXIT:    fprintf(stderr, "Exit  %s\n", name);           break;

      case OBJ_CTOR:    fprintf(stderr, "Construct %s @ %p\n", name, addr);
                                     break;
      case OBJ_DTOR:    fprintf(stderr, "Destruct  %s @ %p\n", name, addr);
                                                                       break;
      case USER_BP:     fprintf(stderr, "Breakpoint %s (%p)\n", name, addr);
                                                                       break;
      }  // switch

/* We enter interactive mode if any of the above conditions other than
 * tracing is met.  (This implies that named user breakpoints are skipped
 * if the user uses a g <name> command in which the name does not match
 * the breakpoint name, but that unnamed user breakpoints are always
 * effective, as are named user breakpoints after a g command with no name.)
 */
      if (was_step || (brk_name[0] && ((strcmp(brk_name, name) == 0) ||
            (cond == USER_BP && !name))) ||
            (!brk_name[0] && cond == USER_BP)) {

// Reset breakpoint conditions
         was_step = 0;
         brk_name[0] = 0;

// Main command loop
         do {
            fprintf(stderr, "cmd> ");
            gets(buff);
            switch(buff[0]) {

            case 'd':  objp(buff + 2)->display();                  break;
            case 'g':  if (buff[1])  strcpy(brk_name, buff + 2);   break;
            case 'q':  tracing = 0;                                break;
            case 's':  was_step = 1;                               break;
            case 'v':  tracing = 1;                                break;
            }     // switch

         } while(buff[0] != 's' && buff[0] != 'g');
      }     // if (interactive)
   }     // if (message)
}     // cond_break()

monitored* objp(const char* str) {
        monitored* p;
        sscanf(str, "%p", &p);
        return p;
        }

int init_ctl::_count;


Copyright © 1991, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.