gdbWatchPoint

Debugging with pretty printers in GDB - part 2

Last updated 5th Oct 2021

In this tutorial, Software Architect Mark Williamson follows on from Greg's tutorial on Debugging with pretty-printers in GDB by illustrating how to write pretty printers for more complex data structures.

Getting to the point_t

Our examples in this tutorial will revolve around data structures from an imaginary vector graphics program.  As we work up to more complex geometry types we will extend to our pretty printers to help us understand the structures involved.

We’ll start with a very fundamental structure, representing a single point:

typedef struct {
    int x;
    int y;
} point_t;

Lets suppose our program has defined an instance of this type, representing a point in 2D space:

point_t p = {
    .x = 1,
    .y = 2,
};

Printing this immediately in GDB gives us a fairly familiar, though not particularly compact, representation:

(gdb) print p
$9 = {
 x = 1,
 y = 2
}

In our previous examples, we already learnt how to write a basic pretty printer. A similar pretty-printer script for our point_t might look like this:

import gdb

class PointPrinter:
    def __init__(self, val):
        self.val = val
    def to_string(self):
        return '({x}, {y})'.format(
            x=self.val['x'],
            y=self.val['y']
        )

def my_pp_func(val):
    if str(val.type) == 'point_t': return PointPrinter(val)
    return None

gdb.pretty_printers.append(my_pp_func)

This gives us a more compact output format that’s more suitable for the coordinate data we’re representing:

(gdb) print p
$8 = (1, 2)

Line up for nested structures!

Our point_t structure is very likely to be used as an element of other data structures. For instance, in a drawing program, we might have a line_t that contains two points:

typedef struct {
    point_t start;
    point_t end;
} line_t;

Let’s assume we’d like to pretty print this too. We could just copy-and-paste the code from the previous pretty-printer with some edits:

...
    def to_string(self):
        return '<({x1}, {y1}), ({x2}, {y2})>'.format(
            x1=self.val['start']['x'],
            y1=self.val['start']['y'],
            x2=self.val['end']['x'],
            y2=self.val['end']['y']
        )
...

But this is already getting a bit repetitive. Fortunately, GDB already has this covered for us: when formatting strings for printing, it will recursively call existing pretty printers. So, actually, we can write:

class PointPrinter:
    ...

class LinePrinter:
    def __init__(self, val):
        self.val = val
    def to_string(self):
        return '<{p1}, {p2}>'.format(
            p1=self.val['start'], p2=self.val['end']
        )

def my_pp_func(val):
    if str(val.type) == 'point_t': return PointPrinter(val)
    elif str(val.type) == 'line_t': return LinePrinter(val)
    return None

gdb.pretty_printers.append(my_pp_func)

Which produces the output:

(gdb) print l
$7 = <(3, 4), (5, 6)>

Deferring formatting decisions to existing pretty printers also means that formatting changes can be made in one place and reflected everywhere. For instance, if we now change our original PointPrinter to say:

...
    def to_string(self):
        return 'Point({x}, {y})'.format(
            x=self.val['x'],
            y=self.val['y']
        )
...

Then we’ll see also this reflected when we print a line_t:

(gdb) print l 
$8 = <Point(3, 4), Point(5, 6)>

UDB Time Travel Debugger
Find and fix test failures in minutes  - including C/C++ race conditions, deadlocks and memory corruptions
Learn more »

More complex data structures - let GDB do the walk

We can also use pretty printers to handle more complicated data structures, for instance ones that use pointers to chain together multiple constituent structs.

Our vector drawing program will need a data structure to record all of the objects in the system. This structure allows us to track all the allocated points and lines:

struct drawing_element;
typedef struct drawing_element {
    enum {
        ELEMENT_POINT,
        ELEMENT_LINE
    } kind;
    union element {
        point_t *point;
        line_t *line;
    } el;
    struct drawing_element *next; /* NULL-terminated */
} drawing_element_t;

Note that this can be an arbitrarily large data structure. For our example here, let's just chain together a couple of elements:

drawing_element_t last = {
    .kind = ELEMENT_POINT,
    .el = {
        .point = &p,
    },
    .next = NULL,
};

drawing_element_t head = {
    .kind = ELEMENT_LINE,
    .el = {
        .line = &l,
     },
     .next = &last,
};

Printing it won’t provide very helpful output by default, even with our existing pretty printers:

(gdb) print head
$1 = {
 kind = ELEMENT_LINE,
 el = {
   point = 0x7fffffffd980,
   line = 0x7fffffffd980
 },
 next = 0x7fffffffd960
}

We can see the enum value of kind - which is helpful. Other than that, we just have a pair of pointers to the el member and a next pointer. We could walk along these pointers manually but it would be much nicer if GDB could do that work for us.

If we write a pretty printer for the drawing_element_t then we can build a more complex string renderer that will walk the data structure and give us a summary. To do this, we’ll define a children() method, which tells GDB that the type it’s pretty printing somehow contains other values of interest:

class DrawingElementPrinter:
    def __init__(self, val):
        self.val = val

    def to_string(self):
        # Describe the overall container structure.
        return 'drawing_element_t @ {}'.format(self.val.address)

    def children(self):
        curr = self.val
        enum_type = self.val['kind'].type
        while curr:
            # For each drawing_element_t in the list, check “kind” and
            # choose the point_t or line_t pointer as appropriate.
            if curr['kind'] == enum_type['ELEMENT_POINT'].enumval:
                el_ptr = curr['el']['point']
            elif curr['kind'] == enum_type['ELEMENT_LINE'].enumval:
                el_ptr = curr['el']['line']

            # Prepare for next loop.
            curr = curr['next'].dereference() if curr['next'] != 0 else None

            # Yield the child element - a string name and the value 
            # we want to show.
            yield 'el', el_ptr.dereference()

    def display_hint(self):
        # Tell GDB how to display the output of children().
        return 'array'  # Format our sequence like an array’s contents.

def my_pp_func(val):
    if str(val.type) == 'point_t': return PointPrinter(val)
    elif str(val.type) == 'line_t': return LinePrinter(val)
    elif str(val.type) == 'drawing_element_t':
        return DrawingElementPrinter(val)
    return None

gdb.pretty_printers.append(my_pp_func)

This pretty printer will:

  • Display the location of our data structure
  • Walk the linked list to retrieve all the elements
  • For each element, use the kind field to determine its type
  • Delegate display of that element to the existing pretty printer (by dereferencing the appropriate pointer)

By combining these elements, we have built a pretty printer for the whole chained data structure rather than for a single value. When we try our print command again, we’ll see something much more interesting:

(gdb) p head 
$2 = drawing_element_t @ 0x7fffffffd820 = {<Point(3, 4), Point(5, 6)>, Point(1, 2)}

Since we selected the “array” display hint, this will automatically reflect preferences for printing arrays (as set by set print array).

The printer we’ve built will automatically walk a list of arbitrary length; with the full power of Python available, combined with GDB’s access to data values and types, it is possible to decode arbitrarily complex data structures.

UDB Time Travel Debugger
Find and fix bugs in minutes  - save time on debugging
Learn more »

Let GDB help you

That’s it for this tutorial - we’ve seen how GDB can automatically invoke the right pretty printer to display your data and handle more advanced data structures. Use these techniques and you’ll get a better debug experience for less effort spent.

The full source of the examples shown above can be downloaded for reference:

Don’t miss the next debugging tutorial: sign up to the gdbWatchPoint mailing list now!

Get tutorials straight to your inbox
Become a GDB Power User. Get Greg's debugging tips directly in your inbox every 2 weeks.

A dedicated resource to learn about debugging in GDB by industry leader in debugging and founder of Undo

Meet Greg