Debugging C code With GDB

I have finally taken the time to explore GDB. And it’s amazing! Here’s what I’ve learned.

Gustavo de Paula
Having Fun
10 min readMay 15, 2022

--

Photo by Lone Jensen from Pexels

When it comes to debugging, I’m a printf-type of guy. No matter the environment, be it React code running on the browser or algorithms written in C, I just insert logs everywhere to debug my code.

Most of the time it’s great. It’s a cheap and fast way to get a grasp of what’s happening. But sometimes, a debugger is the right tool to use. It allows you to inspect deeply your code.

With C, I have always known that GDB exists, but I never learned how to use it. This article is a compilation of my learning notes as I explored this tool.

For Mac Users

It’s a complete P.I.T.A. to make GDB work in Mac. Here are the tutorials I had to follow to make it work:

And even then, for me, they weren’t enough. Whenever I get a

and the terminal just freezes, I have to ctrl+z, kill the process with kill -9 and then run

a couple of times. After that, it usually works.

Get up and running

To debug C/C++ code with GDB, compile it with debugging instructions:

Starting with the most simple program:

This is the GDB prompt. This is where commands are sent to inspect your code by interacting with GDB.

The most basic command is the run command. It’ll simply run the code. Just like when it is run with ./executable:

Here it is! Now let’s do something more useful.

Setting breakpoints

Using this other simple program as an example:

The list command prints our code, with line numbers attached:

This is useful because we can use the line numbers to set breakpoints. To set a breakpoint for a line, the break command is used:

This has set a breakpoint in line 4. Using line numbers is not necessary as there are other ways to set breakpoints. The most useful, perhaps, will be to set breakpoints at the entry of functions, e.g. break main.

Now running the code, it should stop at the breakpoint at #4

It does! Nice.

Now let’s print the variables:

As none of them are initialized yet, we’re just seeing garbage. Let’s continue the execution of the program by going to the next line:

“5” at the start of the output signals what line of the code the debugger is in. If we now print the i variable, we’ll get the expected value:

Now going over the next few lines, the variable j is declared and i changes:

The command info can be used to print all variables' values

As we no longer want to inspect any further line of code, we can just let it finish:

Debugging functions

The code above is sufficient for setting breakpoints at arbitrary lines of code and stepping over them. It’s also possible to step into and out of functions and inspect them with GDB.

Take the following code for example:

To list the entire code, if the code has >10 lines, one needs to use the list command multiple times:

Set a breakpoint for the first function call

Now instead of running the next command, use the step command to step into the function execution:

Inside a function, another variation of the info command prints the arguments passed to that function:

Yet another variation of info is stack, to see the current call stack:

... was the address of the function, removed for legibility's sake. It looks like this: 0x0000000100003f6c

Going over the function:

To step out of the function, use the finish command:

Note that the finish command also prints the returned value of the function!

Now let’s suppose you forgot where you are in the code, here’s what you can do: First, use frame to get the line number and the function name:

And then, you can use list to show code around that line number, passing the line number as a parameter

If you want more information about any command, you can use the help command:

Watchpoints

Watchpoints work like breakpoints, but instead of always stopping at a line or function call, they stop the execution when the content of the variable changes.

Take the following code for example:

First, setting a breakpoint at the function:

Now that we’re inside the function, we can set a watchpoint for the variable max:

As we have set the watchpoint before the variable initialization, the first stop will be after the variable’s initialization

Nice. Next, it’ll stop when the value changes from 5→12 and from 12→235:

When we hit continue again, it’ll exit the function deleting the watchpoint.

Continuing again, the program finishes.

Conditional breakpoint

We can also set conditional breaks: breakpoints that only get activated if a condition is true. Still using the previous code example, we'll explore conditional breakpoints.

First, let’s do a normal breakpoint at the first line of the for loop:

Now that we’re inside the loop, and have access to the i variable, we can set up a conditional breakpoint based on it.

Note that we now have two breakpoints at line 6. Let’s delete the first one (the non-conditional one). With info break, we can see information on every set breakpoint.

To delete, we just need to use the del command, passing the breakpoint ID:

Silent = success. If we wanted to delete multiple breakpoints, we could do del start-end, e.g. del 1-5 would delete breakpoints from 1 to 5.

Let’s check info break again:

With all that out of the way, we can hit continue, and it’ll stop when the condition is true (when i == 5):

Perfect. If we hit continue again, the program ends:

Debugging a real project

Instead of single-file toy examples, this section will use as an example this multi-file project: Études in C — External Sort URLs

TLDR: the project implements an external sorting algorithm. The repository is a personal playground to explore ideas in C. The main explored idea is generic data structures: data structures that use generic pointers so that they “don’t care” what data they’re carrying. Feel free to look around the repository!

First thing, let’s compile with debugging instructions. To do that, the makefile needs to be changed, adding the -g flag:

Now running make external-sort-urls will compile the project with debugging instructions. Doing git status yields this result:

To set a breakpoint in a file, we can use the break command with a slightly different syntax. First, all files can be listed with info:

... is the full path of the folder you have the project

Setting up a breakpoint at the start of the function external_sorting in the external_sorting.c file:

By using the already explored info args, it's possible to see the passed parameters to the function:

Nice.

Setting up a breakpoint at a function:

It’s also possible to call functions / evaluate expressions with the print command:

If calling void functions, the call command can be used to not clutter the output:

Note call does not print $8 = void

Now supposing a strange bug happened, in the compare_entities function, when entity_a.amount < entity_b.amount

To hunt down that bug, it’s possible to set a conditional breakpoint looking for that specific case:

For sanity, let’s delete the other created breakpoints:

Now let’s continue to stop at the breakpoint:

Great! It stopped exactly in the first case where it happened. Just to use the commands explored in the article:

Where the debugger is now:

Current call stack:

I’ve hidden some information that polluted the output and would not be legible in this article. But every stack information has the address of the function and the passed parameters (just like main in line #7).

Also, have you noticed that there are multiple merge_sort in the call stack? These are the result of the recursive calls :)

Now deleting the breakpoint and letting the program finish in peace:

Reference

  • GDB official docs.
    It’s great for exploring the syntax of the commands, but I didn’t find it friendly for beginners that don't know the basics of GDB. Hopefully, after this article, you're not a beginner in GDB anymore :).

--

--

Gustavo de Paula
Having Fun

Software Engineer @ Loggi. Escritor, leitor e fotógrafo nos tempos livres. Amante da Liberdade.