Resources
Enhance your GDB experience with TUI many windows
Author: Magne Hov, Senior Software Engineer at Undo
GDB ships with a fantastic graphical user interface called “Text User Interface”, or TUI for short. This interface is great: it’s super lightweight and works in a terminal, so it’s always available wherever you run GDB.
TUI mode has had the same features and look for a long time, but since GDB version 10 it has become possible to extend TUI’s functionality. By the end of this article, you should know:
- How to define new windows that show debug-related data.
- How to build new layouts that combine multiple windows into a debugging dashboard.
- How to download and use our prototype multi-windowed view.
What does GDB’s TUI mode look like?
First, let’s have a look at what you already get out of the box. After enabling TUI mode with the command tui enable
(or the shortcut “Ctrl-x A”) you are presented with the source code of your program in the top half of your terminal (layout src
).
Several other windows are also available. The assembly window (
layout asm
) shows you the machine instructions that you are currently stopped at. This can be helpful if you end up crashing in a library for which you do not have debugging information and cannot see the source code! The register window (layout regs
) shows you the current value of the general CPU registers. The register window helps you understand where pointers and values are stored when executing code optimized by a compiler!
By cycling through the available layouts with the “Ctrl-x 2” shortcut we can also access different “split” layouts. For example, we can show the registers and assembly code at the same time:
Time to extend!
But we can do much more! With the Python API that GDB exposes for “Implementing new TUI windows” we can make new windows that better fit our debugging workflow.
All we have to do is implement and register a new Python class with a few specific methods. For a minimal implementation you just need to define an __init__
method to save a reference to an underlying window object, and a render
method which sends your content to the write
method on the underlying window object. Our new window class can then be registered as a window factory and be associated with a name to be used in layouts.
import gdb class MyTUIWindow: def __init__(self, tui_window): self._tui_window = tui_window def render(self): self._tui_window.write("This is my very own TUI window!") gdb.register_window_type("my-window", MyTUIWindow) gdb.execute("tui new-layout my-layout my-window 1 status 0 cmd 1")
This is all we need to create a new window, but there are a few other details to take care of.
Unless our content is static we need to arrange for the render
method to be called whenever we want the content of our window to change. GDB provides a convenient event system which makes it easy to connect triggers to our render
method. For most purposes, it will be sufficient to rerender the content every time GDB prints a new prompt to the screen.
def __init__(self, tui_window): self._tui_window = tui_window gdb.events.before_prompt.connect(self.render) def close(self): gdb.events.before_prompt.disconnect(self.render)
It is also possible to react to the user scrolling the window with the arrow keys or mouse. We need to define the vscroll
and hscroll
methods to react to scrolling events, and we need to do a bit of bookkeeping to make sure we only write the visible part of the content to the viewport. In the following code block we have refactored and extended our code into a ScrollableWindow
class that handles scrolling events and truncates the content to fit the size of the window.
- The
get_lines
method takes care of generating the content for the window. We use thegdb.execute
function to run a GDB command “under the hood” and then return the output that it produces. - The
get_viewport_content
method takes care of truncating the lines of output according to the current window size and scrolling offsets, and takes care to reuse the content if we’re just scrolling.
class ScrollableWindow: """ Base class for displaying a GDB command in a scrollable TUI window. Subclasses must define the class attributes `name`, `title` and `gdb_command`. """ name = None title = None gdb_command = None def __init_subclass__(cls): if cls.name is None or cls.title is None or cls.gdb_command is None: raise TypeError("Subclass must define `name`, `tile` and `gdb_command` attributes") gdb.register_window_type(cls.name, cls) gdb.execute(f"tui new-layout {cls.name} {cls.name} 1 status 1 cmd 1") def __init__(self, tui_window): self._tui_window = tui_window self._tui_window.title = self.title # When scrolling we use cached lines to avoid regenerating the content. self.use_cached_lines = False self.cached_lines = None self.vscroll_offset = 0 self.hscroll_offset = 0 gdb.events.before_prompt.connect(self.render) def close(self): gdb.events.before_prompt.disconnect(self.render) def get_lines(self): """ Return the content of the window as lines. """ return gdb.execute(self.gdb_command, to_string=True).splitlines() def get_lines_or_error(self): """ Return the content to be displayed in this window as a list of lines, or exception message if a gdb.error occurs. """ try: return self.get_lines() except gdb.error as exc: return str(exc).splitlines() def get_viewport_content(self): """ Return the content that should be displayed in the window, taking into account the current window size and scroll offsets. """ if self.use_cached_lines and self.cached_lines is not None: lines, content_height, content_width = self.cached_lines self.use_cached_lines = False else: lines = self.get_lines_or_error() if lines: content_height = len(lines) content_width = max(map(len, lines)) self.cached_lines = (lines, content_height, content_width) if not lines: return "" # Limit scroll to the content height window_height = self._tui_window.height self.vscroll_offset = min(content_height - window_height, self.vscroll_offset) self.vscroll_offset = max(0, self.vscroll_offset) # Truncate content vertically free_height = window_height - content_height if free_height < 0: # We have to truncate the height, after adjusting for scroll. scrolled_free_height = window_height - (content_height - self.vscroll_offset) if scrolled_free_height >= 0: lines = lines[self.vscroll_offset :] else: lines = lines[self.vscroll_offset : self.vscroll_offset + window_height] # Limit scroll to the content width window_width = self._tui_window.width - 1 self.hscroll_offset = min(content_width - window_width, self.hscroll_offset) self.hscroll_offset = max(0, self.hscroll_offset) # Truncate content horizontally truncated_lines = [l[:window_width] for l in lines] return "\n".join(truncated_lines) def render(self): if not self._tui_window.is_valid(): return output = self.get_viewport_content() self._tui_window.write(output, True) def vscroll(self, num): self.vscroll_offset += num self.use_cached_lines = True self.render() def hscroll(self, num): self.hscroll_offset += num self.use_cached_lines = True self.render()
The user is expected to define a few class attributes when creating new window types:
name
, which is used to refer to the window type when creating new layouts,title
, which is shown in the border of the window,- and
gdb_command
, which is executed to produce the content of the window.
What information does it make sense to show?
Some of the GDB commands I run the most often are related to understanding “where I am” in the program. The backtrace
command gives you an overview of how the program got to the current location in the code, and since we need this information every time the program stops, let’s make a dedicated window for it! The up
and down
commands are also super handy for exploring the call stack in more detail, but these already work really well with the source window which automatically updates to show the currently selected function.
class BacktraceWindow(ScrollableWindow): name = "backtrace" title = "Backtrace" gdb_command = "backtrace"
After I have understood where I am, and I have managed to navigate to an interesting point in the program, I typically want to inspect some of the program’s internal state to understand why we’ve ended up here. If you already know what data structures to look at then the print
or x
commands are the ones to use. However, if you’re not familiar with the function you’ve been stopped inside, the info locals
command may be helpful to explore what local state is accessible in your current scope. Sometimes this is information that you don’t even think of checking, so having it always shown on the screen can be useful. Let’s make a window for that command as well!
class LocalsWindow(ScrollableWindow): name = "locals" title = "Local Variables" gdb_command = "info locals"
When debugging multithreaded programs GDB will automatically select and show you the thread that caused the program to stop. Sometimes we only need to look at the thread that hit a breakpoint or caused a crash, but when investigating concurrency issues it is also important to understand what the other threads are doing. The info threads
command lists out the names of all your threads and which functions they are currently executing. That may be useful to check, so we’ll make a window for that too.
class ThreadsWindow(ScrollableWindow): name = "threads" title = "Threads" gdb_command = "info threads"
How do I arrange these windows into a useful layout?
You can view the new windows in isolation by switching layouts with commands like layout backtrace
and layout locals
. However, it’s much more useful to combine several of these windows into a single layout.
The tui new-layout
command is used to create new layouts. A layout is defined as a sequence of pairs where each pair specifies a window type and a relative size for that window. A simple sequence is arranged as a vertical stack, but by using curly braces and the -horizontal
specifier we can nest vertical and horizontal stacks inside of each other. The gdb.execute
function can be used to call the tui new-layout
command directly in our Python file:
gdb.execute( " ".join( ( "tui new-layout many-windows", "{-horizontal {src 2 status 1 cmd 1} 3", "{locals 1 backtrace 1 threads 1} 2} 1", ) ) )
Essentially, this is like a little tiling window manager that’s built directly into GDB!
This is what our new many-windows
layout looks like for a multi-threaded program:
The “source” window and the “locals” are updated every time we run an up
or down
command, and we can scroll the “backtrace” window to see the entire call stack without having to deal with pagination prompts. Sweet!
After using this layout for a while you might find that some of the new windows are more useful than the others for your specific workflow. In this case you can simply adapt the layout to add, remove or change the size of windows. Maybe you have really long call stacks and want to see as much as possible of the backtrace?
(gdb) tui new-layout src-hsplit-backtrace {-horizontal {src 2 status 1 cmd 1} 3 backtrace 2} 1(gdb) layout src-hsplit-backtrace
In our UDB product we have started making this new TUI functionality available to our customers. It is encouraging to see that TUI mode has recently been getting quite a bit of attention amongst the GDB maintainers. Occasionally UDB will pull in upstream developments before they’re merged into GDB, such as this patch which lets us use the colored output of the commands for the TUI windows:
When writing this blogpost a similar patch was reviewed and merged upstream, so after GDB 17 has been released we can adapt our code to get these kinds of colorful layouts!
How can I try this?
You can check out the prototype code at https://github.com/undoio/tui-many-windows. The code works with GDB version 11 and newer, and is released in the public domain, so feel free to adapt it to your needs. Try it out and see if you like it.
If you think debuggers are cool and want to see what an advanced one can really do, you should download an Undo evaluation using the button below: