Pretty-Printing Ada Containers with GDB Scripts
by Pierre-Marie de Rodat –
When things don’t work as expected, developers usually do one of two things: either add debug prints to their programs, or run their programs under a debugger. Today we’ll focus on the latter activity.
Debuggers are fantastic tools. They turn our compiled programs from black boxes into glass ones: you can interrupt your program at any point during its execution, see where it stopped in the source code, inspect the value of your variables (even some specific array item), follow chains of accesses, and even modify all these values live. How powerful! However, sometimes there’s so much information available that navigating through it to reach the bit of state you want to inspect is just too complex.
Take a complex container, such as an ordered map from Ada.Containers.Ordered_Maps, for example. These are implemented in GNAT as binary trees: collections of nodes, each node having a link to its parent and its left and children a key and a value. Unfortunately, finding a particular node in a debugger that corresponds to the key you are looking for is a painful task. See for yourself:
with Ada.Containers.Ordered_Maps;
procedure PP is
package Int_To_Nat is
new Ada.Containers.Ordered_Maps (Integer, Natural);
Map : Int_To_Nat.Map;
begin
for I in 0 .. 9 loop
Map.Insert (I, 10 * I);
end loop;
Map.Clear; -- BREAK HERE
end PP;
Build this program with debug information and execute it until line 13:
$ gnatmake -q -g pp.adb
$ gdb -q ./pp
Reading symbols from ./pp...done.
(gdb) break pp.adb:13
Breakpoint 1 at 0x406a81: file pp.adb, line 13.
(gdb) r
Breakpoint 1, pp () at pp.adb:13
13 Map.Clear; -- BREAK HERE
(gdb) print map
$1 = (tree => (first => 0x64e010, last => 0x64e1c0, root => 0x64e0a0, length => 10, tc => (busy => 0, lock => 0)))
# “map” is a record that contains a bunch of accesses…
# not very helpful. We need to go deeper.
(gdb) print map.tree.first.all
$2 = (parent => 0x64e040, left => 0x0, right => 0x0, color => black, key => 0, element => 0)
# Ok, so what we just saw above is the representation of the node
# that holds the key/value association for key 0 and value 0. This
# first node has no child (see left and right above), so we need to
# inspect its parent:
(gdb) print map.tree.first.parent.all
$3 = (parent => 0x64e0a0, left => 0x64e010, right => 0x64e070, color => black, key => 1, element => 10)
# Great, we havethe second element! It has a left child,
# which is our first node ($2). Now let’s go to its right
# child:
(gdb) print map.tree.first.parent.right.all
$4 = (parent => 0x64e040, left => 0x0, right => 0x0, color => black, key => 2, element => 20)
# That was the third element: this one has no left or right
# child, so we have to get to the parent of $3:
(gdb) print map.tree.first.parent.parent.all
$5 = (parent => 0x0, left => 0x64e040, right => 0x64e100, color => black, key => 3, element => 30)
# So far, so good: we already visited the left child ($4), so
# now we need to visit $5’s right child:
(gdb) print map.tree.first.parent.parent.right.all
$6 = (parent => 0x64e0a0, left => 0x64e0d0, right => 0x64e160, color => black, key => 5, element => 50)
# Key 5? Where’s the node for the key 4? Oh wait, we should
# also visit the left child of $6:
(gdb) print map.tree.first.parent.parent.right.left.all
$7 = (parent => 0x64e100, left => 0x0, right => 0x0, color => black, key => 4, element => 40)
# Ad nauseam…
Manually visiting a binary tree is much easier for computers than it is for humans, as everyone knows. So in this case, it seems easier to write a debug procedure in Ada that iterates on the container and prints each key/value association and call this debug procedure from GDB.
But this has its own drawbacks: first, it forces you to remember to write this procedure for each container instantiation you do, eventually forcing you to rebuild your program each time you debug it. But there’s worse: if you debug your program from a core dump, it’s not possible to call a debug procedure from GDB. Besides, if the state of your program is somehow corrupted, due to stack overflow or a subtle memory handling bug (dangling pointers, etc.), calling this debug procedure will probably corrupt your process even more, making the debugging session a nightmare!
This is where GDB comes to the rescue: there’s a feature called pretty-printers, which makes it possible to hook into GDB to customize how it displays values. For example, you can write a hook that intercepts values whose type matches the instantiation of ordered maps and that displays only the “useful” content of the maps.
We developed several GDB scripts to implement such pretty-printers for the most common standard containers in Ada: not only vectors, hashed/ordered maps/sets, linked lists, but also unbounded strings. You can find them in the dedicated repository hosted on GitHub: https://github.com/AdaCore/gnat-gdb-scripts.
With these scripts properly installed, inspecting the content of containers becomes much easier:
(gdb) print map
$1 = pp.int_to_nat.map of length 10 = {[0] = 0, [1] = 10, [2] = 20,
[3] = 30, [4] = 40, [5] = 50, [6] = 60, [7] = 70, [8] = 80,
[9] = 90}
Note that beginning with GNAT Pro 18, GDB ships with these pretty-printers, so there’s no setup other than adding the following commands to your .gdbinit file:
python import gnatdbg; gnatdbg.setup()
Happy debugging!