GDB has evolved in the last several years to provide a Python API. This series of articles will look at how a user can program GDB with the API and will also take an in-depth look at several features of that API. But, before we begin, a small history lesson is needed and a look at just why an API was needed.
Why an API?
The humble debugger. We've all used one at some point in our careers, sometimes with a little trepidation, sometimes in frustration, but always to try to help solve a nasty bug. The software world moves ever more swiftly and debuggers have to evolve in-step with this modern programming environment. This is true of all software, but of debuggers, it is especially true. To be useful and offer a productive tool, debuggers have to shape themselves to what engineers want at that time and, if possible, be ahead of that need. This is a complex and difficult aspiration. Debuggers are highly complex pieces of software themselves, are subject to their own software bugs as well as new features and must adapt to the changing needs of languages and hardware. If the debugger is open-source, as GDB is, there is also the ebb and flow of the community. GDB developers come and go and sometimes it is a full-time task for the community to keep up with maintenance. So how does the GDB community predict what engineers of today (and tomorrow) want?
In my view, it can't. If not all goes well a user might never contact a GDB developer, complete a bug report or send an email to the GDB mailing list. We’ve all got our own bugs to fix, deadlines to meet and tasks to carry out. If all doesn’t go well, however, it might result in a slightly frustrated bug report to the GDB developers. After all, the last thing a user wants is a debugger to crash when that user is trying to solve bugs in their own code. Therefore, communication can be limited. How can GDB developers know what users want? A debugger has its own vocabulary that refers to complex concepts. DWARF? ELF? Inferior function calls? There are much more such examples. So, not only is limited contact an issue but lacking a common vocabulary can also hamper such efforts.
A few years ago, the GDB community decided that the introduction of a scripting API was a way to combat this. Not only could users now script GDB by calling specific GDB functions via a defined API, but they could also alter GDB’s output by registering scripts to be called when GDB has data to present. These two innovations changed how users interact with GDB. You could still use the CLI but it also changed GDB to become programmable and granted users leeway to tailor GDB for their own experience. This changed several paradigms in some fundamental ways. The first, and most important, is it evolved the view of GDB from a monolithic-style program wrapped in a command-line interface to a more modular and programmable “engine”. Thinking about the question posed in the first paragraph this provides, if not a solution, then a way to evolve GDB into a new, more nascent environment. If GDB does not have an internal command to perform the function the user wants, then it may be possible for that user to program that functionality into GDB without knowing about the mechanics of GDB’s internals. They could program that function in Python, use the Python API to receive functional data from GDB, and massage that data in Python to provide whatever functionality they required. With an API the user can customize GDB in a meaningful and complex way and export that functionality back to GDB either in the form of a GDB command or as a supply to a programming hook that GDB subsequently calls.
These series of articles will look at some of the APIs available in GDB. These articles are not definitive but rather hope to encourage exploration of these features and enhance a debugging experience to be a more productive one. The first of the APIs this article will look at are GDB pretty printers.
Python pretty printers
What is a pretty printer?
Data can be indecipherable. It can be cryptic, unreadable, misleading, confusing and all the adjectives in between. Data representation isn’t designed to be this way. However, the reality of software maintenance and how computers store data can make it seem that way even though that was not the intent of the designer of that data. This is especially so when GDB is used to decipher complex data objects. When GDB is asked to print a value it tries to generically print out the members of a data structure. It does not try to interpret the meaning of those members. It can’t. The meaning of the data is not implied in the structure of the object but rather in the contents and the structure, something that is only known to the designer. To GDB, pointers to other data structures, for example, remain pointers. A linked-list design in a data structure might be evident to the designer (or, as often is this case, the person doing the debugging), but to GDB the meaning of the data structure is opaque. This generic, non-interpretative approach has some utility. It works for multiple languages, for example, and if the data object is straightforward or simple enough it works well enough to be useful. Sometimes it can prove to be less than useful. When the members of that data object are complex or refer to other members of a remote data structure, or the meaning of the object is implied in the data it contains, GDB struggles. The example below shows a std::vector, declared in the usual way in a C++ program:
std::vector<int> vec = {7, 5, 16, 8};
Taking a standard GDB, that does not have a std::vector Python pretty printer installed, results in the following GDB output:
(gdb) print vec \$1 = { <std::_Vector_base<int, std::allocator<int> >> = { _M_impl = { <std::allocator<int>> = { <__gnu_cxx::new_allocator<int>> = {<No data fields>}, <No data fields>}, members of std::_Vector_base<int, std::allocator<int> >::_Vector_impl: _M_start = 0x615c20, _M_finish = 0x615c30, _M_end_of_storage = 0x615c30 } }, <No data fields>
That's not very useful. It presents little real useful data to the user wanting to inspect the contents of the vector "v". The data is there but you have to look at the internal implementation of the std::vector. For objects such as these (which are commonly used in the programming community), having every user of std::vector be required to know the internals of a vector makes little sense. In the above example, GDB generically prints out members of the vector class. This is because GDB also does not know the internal implementation of a std::vector.
Let’s see what happens when a GDB Python Pretty Printer is installed and GDB calls this printer to assemble the output:
(gdb) print vec \$1 = std::vector of length 4, capacity 4 = {7, 5, 16, 8}
That's a far more useful view of the data and contains the actual content of the vector. That pretty printer, used in this example, exists today. It's been written for GDB, using the Python API, and is maintained by the developers of the libstdc++ library. The API it uses and implements is the GDB Python pretty printer interface. This was among the first of the Python APIs to be introduced to GDB and is one of the most popular.
The std::vector is a good example of a useful printer but it is too complex to deconstruct in a blog article. It's included here to show the tremendous utility of pretty printers in GDB and the power of the Python API.
So let’s write our own pretty printer.
Writing a Python Pretty Printer
For the pretty printer we'll write in this article we’ll use a simple data structure. Take the following two C structs:
struct inner_example {
int bar
};
struct example_struct {
int foo;
struct inner_example *ie;
};
For now, assume that example_struct and inner_example are allocated on the heap in the usual way. The allocated structure example_struct is stored in a pointer "example". In GDB, printing out "example" would yield:
(gdb) print *example \$1 = { foo = 1, ie = 0x602030 }
Notice that the pointer "ie" of the inner struct, "inner_example" shows the address of the pointer. Printing out that inner structure can be achieved like this:
(gdb) print *example->ie \$2 = { bar = 0 }
But that gets tedious especially with data structures that have a lot of these kinds of pointers. Because this is code we have written, we have insider knowledge of those structs, and we can teach and program GDB via the Python API how to print this value, and all values that have the same type, to present better output. In the following pretty printer, we will be telling GDB how to interpret that type and print the value in a more useful fashion.
Here is our pretty printer with example:
import gdb.printing
class examplePrinter:
"""Print an example_struct type struct"""
def __init__(self, val):
self.val = val
def to_string(self):
return ("example_struct = {foo = " + str(self.val["foo"]) +
" {inner_example = {bar = "
+ str(self.val["ie"]["bar"]) + "}}")
def build_pretty_printer():
pp = gdb.printing.RegexpCollectionPrettyPrinter(
"Example library")
pp.add_printer('Example Printer', '^example_struct$', examplePrinter)
return pp
gdb.printing.register_pretty_printer(
gdb.current_objfile(),
build_pretty_printer())
And here’s the output when "example" is printed with the pretty printer installed.
(gdb) print *example \$1 = example_struct = {foo = 1 {inner_example = {bar = 2}}
Because these are data structures the user is familiar with, and that user understands the meaning of that data, as well as the structure of that data, they can program GDB to be more introspective when printing data of that type. This supersedes GDB's more generic approach of just printing what is there without interpreting it.
Breaking down the pretty printer, we can see it is built in several steps.
The init function.
This is the constructor of the pretty printer and it is passed the value to be printed. In our example printer, it assigns it to an internal variable for later reference.
The to_string function.
When GDB wants to print a value and it has a pretty printer registered for that type, it will first call the init function with the value to be printed. Subsequently, it will call the to_string function of the pretty printer and this is where the printer can assemble its output. The return value of this function is what GDB will print. So in the example above, the sequence is:
(gdb) print *example
- GDB finds example's type.
- GDB searches for pretty printers that are registered for this type.
- GDB, if it finds a printer, calls the pretty printer's init function and passes the printer the value to be printed (in this case, "example").
- GDB calls the to_string function call of the printer.
- GDB prints the return value of the to_string printer.
The printer accesses the data through the value that was first passed to it in the init function. In the example above, the printer assigned the value val to self.val for later reference. Because val represents a value of structure type, and GDB knows this type, the Python API allows access to the elements of that structure via the names defined in that structure. In that example, using the GDB Python Value API.
self.val["foo"]
Is the equivalent of
example->foo
And, later in the example,
self.val[“ie”][“bar”]
Is the equivalent of
example->ie->bar
Note that the pretty printer function to_string has to return a string value. It is up to the implementer of the pretty printer to convert all values.
A more complex printer
Sometimes data cannot be summed up in a single one-line string. The example above condenses the information into a more readable format but not all such structures are compressible in such a neat and packaged way. The pretty printing API has another set of functions that can help you expand the presentation of your data while keeping the output as simple and easy to understand as before.
The Children function
Taking the example from above, what if it is a collection of objects assembled as a linked-list? It would be difficult to represent a whole list in a single string and would be steering the data presentation toward a more confusing and chaotic format. The children function allows the printer to split up the output into a more hierarchical concept. Taking the examples above, let’s modify it to be a linked list:
struct inside_example {
int bar;
};
struct example {
int foo;
struct inside_example *ie;
struct example *next;
};
As before, the elements of the linked list are allocated on the heap in the usual way. The next field, as with all linked lists, points to the next element in the list. What happens if we want to look at the third element in the linked list? Assuming the object in GDB is the first element, printing it out, we’ll see:
(gdb) print *example \$1 = { foo = 1, ie = 0x602070, next = 0x602030 }
To get to the third element, we have to:
(gdb) print *example->next->next \$2 = { foo = 3, ie = 0x6020b0, next = 0x0 }
And to look at the inside example struct of the third element, we’d have to:
(gdb) print *example->next->next->ie \$3 = { bar = 44 }
This becomes confusing and disorienting with a linked list of any length or complexity.
The children function allows you to hide these details from the user. The function must return any Python iterable object that contains a Python tuple of two elements. The first element is the name of the child or the label, and the second is the value of that element. The value can be any value type, Python or sourced from GDB directly. So, for our children function, we’d need to iterate the linked list and output those elements found in that list. An example output from the children function would look something like this:
Python List “Output” = [(label,value), (label,value), (label,value), (label,value), ...]
But there’s a problem here. If the linked list were very long we would have to duplicate the entire linked list in Python. That’s a bit unwieldy and, depending on the size of the linked list, can be memory intensive. We want to avoid that and write a conservative printer. The solution is to define a Python iterator that only computes each linked list element when called upon for each iteration. Let’s look at our new pretty printer.
class examplePrinter:
"""Print an example type foo struct"""
class _iterator:
def __init__(self, base):
self.base = base
self.count = 0
self.end = False
def __iter__(self):
return self
def next(self):
if self.end == True:
raise StopIteration
value = "example_struct = {foo = %d {inner_example = {bar = %d}}" \
% (self.base["foo"], self.base["ie"]["bar"])
item = ('[%d]' % self.count, value)
self.base = self.base['next']
if (self.base == 0):
self.end = True
self.count = self.count + 1
return item
def __init__(self, val):
self.val = val
def to_string(self):
return ("A linked list of example structs containing")
def children(self):
return self._iterator(self.val)
Note for brevity, I’ve only included the examplePrinter class here. The rest of the code in the previous printer is exactly the same.
That printer may look complicated but only three things have changed.
- The to_string function has been changed to print out a summary label.
- The inclusion of the inner-class.
- The inclusion of the children function, which returns the inner-class.
The most interesting thing here is the iterator. When GDB calls the children function, it requires an iterable Python object. Whether that iterable object is a standard Python list, or as in our case, an iterator, does not matter. An iterator is a better choice for this printer, as, with most linked lists, we do not have any knowledge of the length of the list. In this case, we do not need to know the length as the next function of the iterator is called until it raises a StopIteration exception. Looking at the next function, we can see it does the following things:
- Checks if the printer has exhausted the traversal of the linked list.
- If not, compute the value part of the tuple and store it in value.
- Take the value part of the tuple, construct the tuple with a label indicating count and store it in the tuple, item.
- Calculate the next item in the linked list for the next iteration.
- Check if the next item is NULL, signaling the end of the linked list.
- Update the label count.
- Return the tuple.
With the pretty printer installed in GDB, it produces the following output:
(gdb) print *example $1 = A linked list of example structs containing = { [0] = example_struct = {foo = 1 {inner_example = {bar = 42}}, [1] = example_struct = {foo = 2 {inner_example = {bar = 43}}, [2] = example_struct = {foo = 3 {inner_example = {bar = 44}} }
The display_hint function
A function that we have not covered here (the defaults worked fine for the printers we produced) is the display_hint function. This optional function hints to GDB how the output should be formatted. The three pre-defined values that this function can return are:
'array'
Display the result in an array-like format.
'map'
This is a special option to map two values together and indicates that the output is map-like. The children of this printer should be output as alternate keys and values per iteration.
'string'
This indicates the output is string-like and that GDB should treat the output as a string.
And that's the end! I hope you enjoyed this quick look at pretty-printers in GDB and I hope you will be joining me again in future forthcoming articles.
Last updated: December 27, 2023