Debugging C with GDB

Prerequisites

In this tutorial, we’re going to walk through how to debug C programs. There’s tons of different ways to do this, but I’m going to focus on the way I use (for the most part). I’m also assuming you’re using a development environment like the one I outline in my dev environment tutorial, so if you haven’t read that, you should before continuing.

C debugging basics

C is a notoriously unforgiving language. The phrase “segmentation fault” can strike fear in even the saltiest C developers. Manually-managed memory, strong typing, pointers, buffered streaming of stdout, and the limited amount of built-in libraries all serve to make C scary for beginners, and difficult to debug without the right tools.

The core of the C debugging world is the GNU Project Debugger (GDB). GDB is a program that runs your compiled programs, and allows you to inspect its state and behavior interactively. It’s a powerful tool for C debugging, but with that power comes complexity, and the manual is one of those old-school (c. 1988) GNU/Linux manuals that can be dense and difficult to crack for beginners.

Get some code to fail

I made an intentionally-broken base repo for this tutorial, you can download it from my GitHub profile. Clone that repo, because we’re going to debug it:

$ git clone https://github.com/OptimalPandemic/gdb-tutorial.git

$ cd gdb-tutorial

You’ll see that repo has a Makefile, and that the .PHONY is set up to run the tests. Let’s run them:

$ make


**************** Linked Lists Tests ******************

Create List Test PASS
Insert Into List Test PASS
Search List (Item Present) Test PASS
make: *** [Makefile:21: test] Segmentation fault

Oh no! Our first 3 tests passed, and then we hit the dreaded segmentation fault. In case you don’t know, that means our code somehow tried to access memory that it isn’t allowed to access. It’s one of the most common errors in C, and most difficult to debug. The error message doesn’t give you any kind of stacktrace or indiciation where the failure is, which can be very confusing to C beginners that have used higher-level languages.

Finding our first bug

You installed GDB, right? If not:

$ sudo apt install gdb

Let’s start GDB with our test executable. You’ll get a bunch of boilerplate output, plus a (gdb) prompt where you can enter commands.

$ gdb test/testLinkedList


Copyright (C) 2022 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 test/testLinkedList...

(gdb)

At the (gdb) prompt, type run (or just r) to run the test program. It may prompt you to enable debuginfod, type ‘y’ for yes.

(gdb) r


Starting program: /home/austin/project/gdb-tutorial/test/testLinkedList

This GDB supports auto-downloading debuginfo from the following URLs:
https://debuginfod.ubuntu.com
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

**************** Linked Lists Tests ******************

Create List Test PASS
Insert Into List Test PASS
Search List (Item Present) Test PASS

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7fbb456 in search (list=0x55555555a950, data=0x7fffffffded0, cmp=0x555555555460 <int_cmp>) at src/linkedList.c:93
93 while(curr->data != NULL) {

(gdb)

OK, now we have something useful. We see the output of the program, the segmentation fault, and now we see the file and line where it failed: src/linkedList.c on line 93, inside the search() function. It also shows us the current values of the function’s parameters, which are memory addresses because those parameters are all pointers.

Since we have a segmentation fault, it’s likely that at some point we’re trying to read curr->data when it’s NULL, but where? It’s in a while loop, so it’s not immediately obvious. We can do some further inspection by setting a breakpoint at that line, which will stop execution each time it gets to that line and wait for us to tell it to continue.

To set the breakpoint at line 93 in src/linkedList.c, type b src/linkedList.c:93 into the GDB prompt (b is short for breakpoint):

(gdb) b src/linkedList.c:93


Breakpoint 1 at 0x7ffff7fbb423: file src/linkedList.c, line 93.

(gdb)

Now that our breakpoint is set, we need to restart our program. Type r again, and y to allow it to restart:

(gdb) r


The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/austin/project/gdb-tutorial/test/testLinkedList
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

**************** Linked Lists Tests ******************

Create List Test PASS
Insert Into List Test PASS

Breakpoint 1, search (list=0x55555555a870, data=0x7fffffffded8, cmp=0x555555555460 <int_cmp>) at src/linkedList.c:93
93 while(curr->data != NULL) {

(gdb)

Now we’re getting somewhere. We’ve stopped at the first time line 93 is hit, and we haven’t segfaulted yet. We can use the print (or p) command to inspect the value of curr at the current breakpoint:

Starting program: /home/austin/project/gdb-tutorial/test/testLinkedList

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

**************** Linked Lists Tests ******************

Create List Test PASS
Insert Into List Test PASS

Breakpoint 1, search (list=0x55555555a870, data=0x7fffffffded8, cmp=0x555555555460 <int_cmp>) at src/linkedList.c:93
93 while(curr->data != NULL) {
(gdb) p curr
$1 = (Node *) 0x55555555a870

(gbd)

We can see that curr is certainly not NULL at the first hit on line 93. Since this is a while loop, it should probably be getting hit more than once, but our execution is currently paused. Let’s let it continue with the continue command (or c), and keep printing the value of curr until we crash:

Starting program: /home/austin/project/gdb-tutorial/test/testLinkedList

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

**************** Linked Lists Tests ******************

Create List Test PASS
Insert Into List Test PASS

Breakpoint 1, search (list=0x55555555a870, data=0x7fffffffded8, cmp=0x555555555460 <int_cmp>) at src/linkedList.c:93
93 while(curr->data != NULL) {

(gdb) p curr
$1 = (Node *) 0x55555555a870

(gdb) c
Continuing.
Search List (Item Present) Test PASS

Breakpoint 1, search (list=0x55555555a950, data=0x7fffffffded0, cmp=0x555555555460 <int_cmp>) at src/linkedList.c:93
93 while(curr->data != NULL) {

(gdb) p curr
$2 = (Node *) 0x55555555a950

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7fbb456 in search (list=0x55555555a950, data=0x7fffffffded0, cmp=0x555555555460 <int_cmp>) at src/linkedList.c:93
93 while(curr->data != NULL) {

(gdb) p curr
$3 = (Node *) 0x0

(gdb) c
Continuing.

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.

(gdb)

We continued the loop 2 more times, and we found our bug! In the 3rd iteration of the loop, curr is set to 0x0, which is equivalent to NULL. Since we aren’t allowed to read a NULL pointer, we get a segmentation fault and crash! Go ahead and exit GDB with the quit command.

So how do we fix it? That’s going to involve less of the debugger and more of an understanding of what our code is trying to do. In this case, the fix is to modify

while(curr->data != NULL)

to read

while(curr != NULL)

Once you make that change in src/linkedList.c at line 93, run make again. You should see:

$ make


**************** Linked Lists Tests ******************

Create List Test PASS
Insert Into List Test PASS
Search List (Item Present) Test PASS
Search List (Item Not Present) Test PASS
Destroy List Test PASS
make: *** [Makefile:21: test] Segmentation fault

Oh no! Our next two tests passed (progress!!) but we still have a segmentation fault, so we have at least one more bug somewhere. I’ll leave finding and fixing it as an exercise for the reader. Happy coding!

If you have thoughts, or you think I’m wrong and just have to tell me, email me!