Debugging Assembly Code with GDB TUI

Grok time ~5 minutes

Apple II Display

This is just a mini introduction to using GDB’s text user interface, or GDBTUI. I use a Linux VM on my home laptop; tunneling in to my university’s linux server is painfully slow, so I just develop locally and then scp (ssh copy) my complete ASM file to the university server to verify that it compiles/runs there. If you have a Linux box that struggles with a VM, try out an older window manager like Openbox, though you’ll lose some fancy features.

On my Linux VM, I use Codeblocks (which I’m not a fan of for assembly), you’re probably using GDB to debug. GDB can be a pain to use with input and output, usually by redirecting input/output to another terminal or files, or integrating it into vi/emacs.

First, you just open/edit the ASM file in Geany, for this example it’s “Homework2.asm”. I have three terminals:

  • Terminal 1: Use to compile/run the app with the Homework2.sh script. The app runs, allowing me to use it as normal. Simply type ./Homework2.out. It runs the app and, in this case, just sits there waiting for you to enter the first number.

  • Terminal 2: This terminal is solely for getting the process ID of my running app. While the app is sitting in Terminal 1 waiting for input, I type ps aux | grep Homework2.out in this terminal. This lists all the processes with “Homework2.out” in the process name. It’ll show geany, GDB if it’s already running and the app running in the first terminal, like this:

steve@debian:~$ ps aux | grep Homework2.out
steve  5180  0.0  0.2  44668 11284 pts/0  S+ 08:30   0:00 /usr/bin/gdb --tui Homework2.out
steve  5346  0.0  0.0   2004   268 pts/2  S+ 08:34   0:00 ./Homework2.out
steve  5349  0.0  0.0   8024   912 pts/3  S+ 08:35   0:00 grep --color=auto Homework2.out

That second one is my program running in Terminal 1. Its process ID is 5346, the first four-digit number on that line.

  • Terminal 3: In this terminal, run GDBTUI. Just type ./gdbtui. An overview of the TUI features can be found here. Size this terminal properly before running GDBTUI; there’s sometimes a goofy bug, depending on your WM, that will crash it if you try to resize after firing up the terminal. I just make the terminal about twice its regular size. There are multiple layouts available for GDBTUI. The one that’s best for us has three “windows” in the terminal: “Registers”, “Assembly”, and “Command”.

Once GDBTUI is running, you’ll be at the GDB command prompt. The commands in GDBTUI are all the same as they are for GDB: si, c, r, etc. There’s some extra TUI commands. First, type layout prev. This should just bring you to a layout that shows the three “windows” I mentioned above. If not, continue cycling with layout next or layout prev to see the other layouts.

Now that you’ve got the proper layout, just type attach 5346 and GDB will attach to your program in Terminal 1. Set your breakpoints as needed and you can now interact with the app in Terminal 1 while viewing the assembly and register values in the GDBTUI interface.

Some extra things that may help you:

  • You can adjust the individual “windows” in GDBTUI. Type info win to get a list of the “windows” in the interface. For example:
(gdb) info win
        ASM     (13 lines)
        REGS    (12 lines)
        CMD     (12 lines)  <has focus>

To adjust the heights, described in lines of text, just type winheight ASM +5. Substitute ASM for whatever window and you can use +/- to adjust the height.

  • You can change the focus within GDBTUI. Notice CMD has focus for me. That means I can use Up and Down to scroll through previously commands. Change focus by typing focus CMD, replacing CMD with whatever window. If you focus on ASM, you can use Up and Down to scroll through the code in the disassembly window.

  • Place labels throughout your code, like after parts of code getting input so GDBTUI will pause there and you can continue stepping through with si line-by-line. GDBTUI will automatically step into the functions read_int, read_char and whatever else, so I always place a random label after all my input/output. For me, I set a breakpoint at asm_main to start. I type c in GDB so it just continues and waits for my input since that’s the first thing I do. After the input, it’ll just continue. I want it to break right after that, so I just have a label called cmps after all my input/output so I don’t have to step through all of the instructions in read_int or read_char (and there are a lot). I just set a new breakpoint at cmps, do my input and GDB breaks after that.

  • You can view memory locations and the stack like this:

(gdb) x/10x $sp
0xffcb1c40:     0xf7561940      0x00000400      0xf7733000      0xf763bf13
0xffcb1c50:     0xf7709000      0xf75d1ab3      0x00000000      0xf7733000
0xffcb1c60:     0x00000400      0x00000001
(gdb)

The first x is for “examine”. The /10x tells GDB how many words to display. You can increase or decrease “10” as needed. The $sp is the location. $sp tells GDB to display starting at the stack pointer. You can replace $sp with a memory location like 0xffcb1c40 to view the contents of a variable.

  • Most systems should have disable-randomization set by default. If not, just type set disable-randomization on. This causes GDB to reuse the same memory locations over and over while you’re debugging. It makes it easier since you’ll start to become familiar with memory locations and won’t have to look those up every time you run the app.

  • This is a great reference to have open while coding too: http://x86.renejeschke.de/.

Once the program crashes or closes, GDBTUI will still be open. You can simply make changes as needed in Geany/Vi/Emacs and run through the same process outlined above: run it in Terminal 1, get the process ID in Terminal 2 and attach GDBTUI in Terminal 3. Since GDBTUI continues running even after the app dies, it’ll maintain memory addresses, etc.

Everyone has their own method but this one works great for me. Hopefully it helps someone else out. Happy coding!