Displaying Stack Frames in gdb with Python
I’d been meaning to explore the GDB Python API for some time when I saw an interesting tweet that posed a problem I thought it could solve.
The poster was looking for a tool to draw “ASCII art” of the state of the stack whenever it changed during program execution. This should be doable if gdb can supply two critical features:
- Break on stack changes
- Describe frame contents
Stack Change Breakpoint
This turns out to be a matter of setting a watchpoint (i.e. a data breakpoint) on the stack pointer rsp
:
(gdb) watch $rsp
Watchpoint 2: $rsp
(gdb) continue
Unfortunately rsp
changes constantly, so restricting these breakpoints to the code you really care about is essential. I found two techniques to be most useful:
Enable/Disable
You can use a pair of breakpoints to enable (or disable!) other breakpoints within a region by attaching commands, like this:
(gdb) watch foo
Hardware watchpoint 2: foo
(gdb) disable 2
(gdb) b main.cpp:28
Breakpoint 3 at 0x55555555468e: file main.cpp, line 28
(gdb) commands 3
Type commands for breakpoint(s) 3, one per line.
End with a line saying just "end".
>enable 2
>c
>end
That enables the watchpoint starting at main.cpp:28
. You can add the reverse (disable
) to mark the end of the range of interest, if desired.
Conditions
It also helped to make the rsp
watchpoint conditional on the function of interest, to avoid getting frames from deep inside library functions. gdb will tell you the current function when you print the value of $rip
:
(gdb) p $rip
$2 = (void (*)(void)) 0x555555554678 <main()+8>
We can use this value in convenience functions supplied for our use by gdb. These are not part of the Python API but can be used in gdb CLI expressions. I used two to get the printed representation of rip
and then check it against a regex representing my target function:
(gdb) watch $rsp if $_regex($_as_string($rip), ".* <main")
Because it uses the instruction pointer to distinguish among functions, we will still see some inlined function calls to other code, but it’s still a big improvement.
Displaying Frame Contents
Next we need to use the Python API to create a command that displays stack frames attractively. Our gateway is the gdb.Frame class and the gdb.newest_frame()
method, from which we can access a lot of other information describing the current function, code block, register values, and stack state.
One of the most important pieces of information is the current value of the frame pointer rbp
. This register - assuming the code was compiled with -fno-omit-frame-pointer
- helps us locate the saved return address and stack pointer from the prior frame. That’s because the first two instructions in the function will be:
push %rbp
mov %rsp,%rbp
and rbp
will be untouched from that point. That produces a stack layout like this (grows downward):
saved rip |
i.e., the return address from callq |
saved rbp |
<- rbp points here |
other saved registers | |
locals | |
<- top of stack (rsp points here) |
Function arguments - if present and not in registers - will appear in the previous stack frame, above the return address. So our picture of the frame extends from the argument with the highest address, through the saved return address and rbp
, to the current rsp
. If we record the locations of the arguments and locals for the current function, along with their sizes, we can mark the stack locations accordingly. My implementation of this approach is here.
Creating a gdb Command
All that remains is to make this accessible from the gdb command line and hook it into our rsp
watchpoint. gdb docs describe this in detail but the main idea is to subclass gdb.Command
and provide a custom invoke
method that does the work. Mine looks basically like this:
class PrintFrame (gdb.Command):
"""Display the stack memory layout for the current frame"""
def __init__ (self):
super (PrintFrame, self).__init__ ("pframe", gdb.COMMAND_STACK)
def invoke (self, arg, from_tty):
print(FramePrinter(gdb.newest_frame())) # call my code
PrintFrame()
That registers a new command called pframe
we can invoke when the watchpoint is hit:
(gdb) source ../gdb_util.py
(gdb) commands 2
Type commands for breakpoint(s) 2, one per line.
End with a line saying just "end".
>pframe
>end
Now every time the stack pointer changes we get a nice display of the stack frame. It’s not exactly “ASCII art” but I think it’s pretty informative:
Result
This example from my repo shows one dword argument s
and one 3-dword local variable bar
on the stack, along with the standard frame contents and some unknown storage (probably for temporaries and out of scope locals).
Summary
With the power of gdb’s Python API, plus some tricks, we can produce a dynamic display of the current stack frame for debugging purposes.