Published on

Using the GNU Debugger

Authors
  • avatar
    Name
    Carter Speerschneider
    Twitter

GNU Debugger Overview

Often in programming it's a good idea to verify your understanding of code execution. Often we are so comfortable in our ability, we neglect powerful tools that help us view into the inner mechanisms of our programs.

GDB or the GNU Debugger is one such tool. It allows you to step through your code, view and alter it's state, and explore the assembly underlying the higher level code (which we may explore in another blog).

GDB is notoriously lacking in beginner-friendly guides and tutorials, so in this blog, I will go through everything that I've learned to be useful in GDB. I won't go through everything-- frankly I don't know that much myself-- but I will focus on the key mechanism, shortcuts, and features it has to offer to get you started.

Beginning is the Easiest Part!

If you don't already have the command on Linux, you can install it here

In order to start debugging your code, all you must do is add a compiler flag to your build command:

gcc -o app -g main.c

This tells the C compiler to add symbols to the binary file. This is needed in order to view your source code in GDB.

Next, just run gdb, passing in a path to the program we want to debug:

gdb ./app

You should see an output similar to this:

GNU gdb (Debian 16.3-1) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from app...
(gdb)

Notice the "Reading symbols from app..."; this signals that we have successfully told gdb which program we wish to debug.

A General Flow for Debugging

When starting gdb the easiest way to begin is to set up a routine to follow each time you debug a program; this helps you gain muscle memory with the commands and shortcuts.

Here is my routine:

First, we can press Ctrl+x then a to see your program's source code in a horizontally split view.

Next, use the focus command to change focus to the shell prompt:

(gdb) focus cmd

This sets the focus on the prompt window, allowing you to go back in your command history just like a regular shell. I prefer this, but you can always revert back with

(gdb) focus src

Next, to start debugging, line by line, use the start command

(gdb) start

You'll see a little cursor (>) at the current line of the debugger. To move execution forward we have two options: next or step. Both of these command move our program's state forward; however, next will skip over functions instead of moving the cursor to the beginning of the new stack frame, step will follow the runtime stack, beginning at the first line of the invoked function.

Viewing Local Variables

As you use next or step to analyze your program's execution, you may wonder how to view the local variables in the current scope. To do this we can use the info command.

(gdb) info local

This will print out your local variables:

(gdb) info local
i = 0
str = 0x502347129487 "hello world"
b = 2
(gdb)

As you can see, it will also print out the address and value of pointers.

Here is a simple C program that we will use to demonstrate using info to print out stack information:

#include <stdio.h>
#include <stdlib.h>

void print_nums(int n) {
  for (int i = 0; i < n; i++) {
    if (i % 2 == 0) {
      continue;
    }
    printf("%d \n", i);
  }
}

int main(void) {

  [[maybe_unused]]char * str = "hello world";

  print_nums(10);

  return EXIT_SUCCESS;
}

Here we are simply defining a local variable string in the main function and then calling print_nums passing in 10 for n. This code will print all of the numbers in the interval [0, n) that are odd.

Printing stack information is useful for seeing what parameters are passed into a function once we step into it. When we do, we will see the stack info is printed automatically. To print the entire stack we can use the info stack command.

(gdb) info stack
#0 print_nums (n=10) at main.c:5
#1 0x[some address] in main() at main.c:18
(gdb)

Changing variable state

Sometimes we want to see what happens when a piece of our program is run with a different set of values. We can do this using the set var command. However, we must make sure the variable we are changing is either on the stack or listed as local variables; we cannot declare a new variable--a symbol-- in our program.

Here is an example of changing the n parameter as a part of the stack in the print_nums function.

(gdb) step
print_nums (n=10) at main.c:5
(gdb) set var n = 32
(gdb) info stack
#0 print_nums (n=32) at main.c:5
#1 0x00005555555551e9 in main () at main.c:18
(gdb)

When we step into the print_nums function, the arguments are printed. We then change the value of the parameter with set var n = 32. Next, we call info stack to see the stack trace with our updated value.

One thing to know about GDB is that if you press enter with no command, it will execute the previous command, so once we update our state, we can use step once and then just repeatedly press enter to finish the execution of print_nums.

1
3
7
9
11
13
15
17
19
21
23
25
27
29
31
main () at main.c:21
(gdb)

You'll notice the UI gets disfigured as the program outputs odd values. We can use Ctrl+l to redraw the window.

Breakpoints

It's time we graduate from moving line by line in our program. That can become very time consuming, and it's often the case when debugging that we are curious about a particular portion of the code and just want to see how it behaves at a certain point in the program's execution.

The way we achieve this is by using breakpoints. A breakpoint is just a point at which our program stops executing and allows us to start moving line by line. The start command actually works by placing breakpoint at the first line of execution in the main function.

Breakpoints can be created using the break command, passing in a location where the breakpoint should be, and we can do that using a line number:

(gdb) break 7

There are actually multiple ways we can specify the location. For instance, if you wanted to go to the start of a function, you would just pass in the function name. You can also pass in an address, representing the address of some Assembly instruction in memory.

When you set a break point, this location information is given to you:

(gdb) break 7
Breakpoint 4 at 0x5555555551b2: file main.c, line 7.
(gdb)

To see this information for all breakpoints we can do

(gdb) info breakpoints

When we set breakpoints, it doesn't make much sense to use the start command because this won't skip the uninteresting code. What you can use instead is run. Remember, run executes all code until it hits a breakpoint, so if you don't have any breakpoints set then your program just executes normally.

Jumping

So breakpoints help us get to the code we are interested in and carefully analyze it's execution, line by line. However, what can we do to rewind a bit to try executing a piece of code again, with some new parameters maybe?

Jumping allows us to change our cursor's line number. Using it in conjunction with breakpoints gives us much control. Jumps work in much the same way that run does, except we get to control where we start and our state remains intact.

Our state persisting between jumps means we have to be very careful where we actually jump, as registers or other function-dependent information would be unrecognizable in the context of another function. It's usually a good idea to stay within the same stack frame when jumping.

To use jump, we use the jump command with a location, similar to setting breakpoints:

(gdb) jump main
Continuing at 0x5555555551d0.

Breakpoint 5, main () at main.c:16
(gdb)

This time I set the location using a function name, which jumps us to the start of main. Luckily, we had a breakpoint at line 5, but if we had no breakpoints the program would just execute normally and we wouldn't have learned much. Using jumps and breakpoints can really help us move fast in GDB.

Exiting

To exit GDB itself, we can type quit, which will prompt us to kill the child process. If we want to finish executing a function we stepped into, we can use finish, and if we are in the main function, continue executes until the cursor runs into another breakpoint.

Summary

All in all, the GNU Debugger is a powerful and efficient means of debugging a program in C (among many others languages), but sometimes we are overwhelmed by it's depth of features and commands. I hope this blog shed some light on what GDB has to offer. Getting started with GDB can actually be quite simple, and once you get adjusted to the flow of things, it can be quite a rewarding experience.