Debugging C code With GDB
I have finally taken the time to explore GDB. And it’s amazing! Here’s what I’ve learned.
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:
- https://dev.to/jasonelwood/setup-gdb-on-macos-in-2020-489k
- https://timnash.co.uk/getting-gdb-to-semi-reliably-work-on-mojave-macos/
And even then, for me, they weren’t enough. Whenever I get a
[New Thread 0x1c03 of process 34571]
and the terminal just freezes, I have to ctrl+z, kill the process with kill -9
and then run
sudo DevToolsSecurity -enable && sudo DevToolsSecurity -disable
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:
gcc -g source.c -o executable
-g
is to compile the code generating debug information. GDB will use this debug information to be able to inspect the code- Additionally, you may use
-ggdb
: https://stackoverflow.com/questions/668962/what-is-the-difference-between-gcc-ggdb-and-gcc-g -o
is just to name the output
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
:
(gdb) run
Starting program: .../debuggable-executable
Hello World!
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:
(gdb) break 4
Breakpoint 1 at 0x100003f5f: file source.c, line 4.
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
(gdb) run
Thread 2 hit Breakpoint 1, main () at source.c:4
4 int i = 0;
It does! Nice.
Now let’s print the variables:
(gdb) print i
$1 = 69669
(gdb) print j
$2 = 32766
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:
(gdb) next
5 int j = 1;
“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:
(gdb) print i
$3 = 0
Now going over the next few lines, the variable j
is declared and i
changes:
(gdb) next
6 i = 13;
(gdb) print j
$4 = 1
(gdb) next
8 printf("Hello World!\n");
(gdb) print i
$5 = 13
The command info
can be used to print all variables' values
(gdb) info locals
i = 13
j = 1
As we no longer want to inspect any further line of code, we can just let it finish:
(gdb) continue
Continuing.
Hello World!
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:
(gdb) list
1 #include "stdio.h"
2
3 int add_and_increment(int a, int b) {
4 int sum = a + b;
5 int incremented = sum + 1;
6 return incremented;
7 }
8
9 int main() {
10 int n = add_and_increment(2, 3);
To list the entire code, if the code has >10 lines, one needs to use the list command multiple times:
(gdb) list
11 n = add_and_increment(n, n);
12
13 printf("result = %i!\n", n);
14 return 0;
15 }
Set a breakpoint for the first function call
(gdb) break 10
Breakpoint 1 at 0x100003f4f: file source.c, line 10.(gdb) run
Thread 2 hit Breakpoint 1, main () at source.c:10
10 int n = add_and_increment(2, 3);
Now instead of running the next
command, use the step
command to step into the function execution:
(gdb) step
add_and_increment (a=2, b=3) at source.c:4
4 int sum = a + b;
Inside a function, another variation of the info
command prints the arguments passed to that function:
(gdb) info args
a = 2
b = 3
Yet another variation of info
is stack
, to see the current call stack:
(gdb) info stack
#0 add_and_increment (a=2, b=3) at source.c:4
#1 ... in main () at source.c:11
...
was the address of the function, removed for legibility's sake. It looks like this:0x0000000100003f6c
Going over the function:
(gdb) next
5 int incremented = sum + 1;
(gdb) next
6 return incremented;
(gdb) info locals
sum = 5
incremented = 6
To step out of the function, use the finish
command:
(gdb) finish
Run till exit from #0 add_and_increment (a=2, b=3) at source.c:6
... in main () at source.c:10
10 int n = add_and_increment(2, 3);
Value returned is $1 = 6
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:
(gdb) frame
#0 ... in main () at source.c:10
10 int n = add_and_increment(2, 3);
And then, you can use list
to show code around that line number, passing the line number as a parameter
(gdb) list 10
5 int incremented = sum + 1;
6 return incremented;
7 }
8
9 int main() {
10 int n = add_and_increment(2, 3);
11 n = add_and_increment(n, n);
12
13 printf("result = %i!\n", n);
14 return 0;
If you want more information about any command, you can use the help
command:
(gdb) help list
list, l
List specified function or line.
...
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:
(gdb) break max_number_in_array
Breakpoint 1 at 0x100003e6c: file source.c, line 4.(gdb) run
Thread 2 hit Breakpoint 1, max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:4
4 int max = array[0];
Now that we’re inside the function, we can set a watchpoint for the variable max
:
(gdb) watch max
Hardware watchpoint 2: max
As we have set the watchpoint before the variable initialization, the first stop will be after the variable’s initialization
(gdb) continue
Continuing.Thread 2 hit Hardware watchpoint 2: maxOld value = 0
New value = 5
max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:5
5 for (size_t i = 1; i < array_size; i++) {
Nice. Next, it’ll stop when the value changes from 5→12 and from 12→235:
(gdb) continue
Continuing.Thread 2 hit Hardware watchpoint 2: maxOld value = 5
New value = 12
max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:10
10 }(gdb) continue
Continuing.Thread 2 hit Hardware watchpoint 2: maxOld value = 12
New value = 235
max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:10
10 }
When we hit continue again, it’ll exit the function deleting the watchpoint.
(gdb) continue
Continuing.Watchpoint 2 deleted because the program has left the block in
which its expression is valid.
... in main () at source.c:16
16 int max = max_number_in_array(array, 6);
Continuing again, the program finishes.
(gdb) continue
Continuing.
result = 235!
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:
(gdb) break 6
Breakpoint 6 at 0x100003e8b: file source.c, line 6.(gdb) run
Thread 2 hit Breakpoint 6, max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:6
6 int current = array[i];
Now that we’re inside the loop, and have access to the i
variable, we can set up a conditional breakpoint based on it.
(gdb) break 6 if i == 5
Note: breakpoint 6 also set at pc 0x100003e8b.
Breakpoint 7 at 0x100003e8b: file source.c, line 6.
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.
(gdb) info break
Num Type Disp Enb Address What
6 breakpoint keep y 0x0000000100003e8b
in max_number_in_array at source.c:6
breakpoint already hit 1 time7 breakpoint keep y 0x0000000100003e8b
in max_number_in_array at source.c:6
stop only if i == 5
To delete, we just need to use the del
command, passing the breakpoint ID:
(gdb) del 6
(gdb)
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:
(gdb) info break
Num Type Disp Enb Address What
7 breakpoint keep y 0x0000000100003e8b
in max_number_in_array at source.c:6
stop only if i == 5
With all that out of the way, we can hit continue, and it’ll stop when the condition is true (when i == 5
):
(gdb) continue
Continuing.Thread 2 hit Breakpoint 7, max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:6
6 int current = array[i];(gdb) print i
$1 = 5
Perfect. If we hit continue again, the program ends:
(gdb) continue
Continuing.
result = 235!
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:
gcc cases/external-sort-urls/main.c lib/*.c -o exec -g
# ./exec # let's also comment this line for now
Now running make external-sort-urls
will compile the project with debugging instructions. Doing git status
yields this result:
Makefile
is the Makefile that was editedexec
is the executable compiled binaryexec.dSYM
is a folder containing the “debug symbols” GDB will use. When the code is compiled without the-g
flag, this folder is not created.
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
:
(gdb) info sources
.../lib/merge_sort.c,
.../lib/heaps.c,
.../lib/generic_arrays.c,
.../lib/printers.h,
.../lib/external_sorting.c,
.../cases/external-sort-urls/main.c
...
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:
(gdb) break external_sorting.c:153
Breakpoint 1 at 0x100002acc: file lib/external_sorting.c, line 154.(gdb) run
Starting program: .../exec
Thread 2 hit Breakpoint 1, external_sorting
...
By using the already explored info args
, it's possible to see the passed parameters to the function:
(gdb) info args
input_filename = 0x100003ee4 "./cases/external-sort-urls/input.txt"
output_filename = 0x100003f09 "./cases/external-sort-urls/output.txt"
max_lines_per_tape = 10
comparator = 0x100002150 <compare_entities>
tape_filename_format = 0x100003f2f "./cases/external-sort-urls/tapes/tape-%zu.txt"
Nice.
Setting up a breakpoint at a function:
(gdb) break compare_entities
Breakpoint 2 at 0x100002160: file cases/external-sort-urls/main.c, line 28.(gdb) continue
Continuing.Thread 2 hit Breakpoint 2, compare_entities (a=0x100404080, b=0x100404180) at cases/external-sort-urls/main.c:28
28 entity entity_a = make_entity(a);
It’s also possible to call functions / evaluate expressions with the print
command:
(gdb) print 1 + 2
$4 = 3(gdb) print make_entity(a)
$3 = {url = 0x100604080 "http://bbmqb.darz.com/yhiddqsc/rjmow/xsjy/dbef.html", amount = 108}(gdb) print (char*) a
$6 = 0x100404080 "http://bbmqb.darz.com/yhiddqsc/rjmow/xsjy/dbef.html 108\n"
If calling void functions, the call
command can be used to not clutter the output:
(gdb) print set_global_comparator(compare_entities)
$8 = void
(gdb) call set_global_comparator(compare_entities)
(gdb)
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:
(gdb) break 30 if entity_a.amount < entity_b.amount
Breakpoint 3 at 0x100002180: file cases/external-sort-urls/main.c, line 31.
For sanity, let’s delete the other created breakpoints:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000100002acc
in external_sorting at lib/external_sorting.c:154
breakpoint already hit 1 time2 breakpoint keep y 0x0000000100002160
in compare_entities at cases/external-sort-urls/main.c:28
breakpoint already hit 1 time3 breakpoint keep y 0x0000000100002180
in compare_entities at cases/external-sort-urls/main.c:31
stop only if entity_a.amount < entity_b.amount(gdb) del 1-2
Now let’s continue to stop at the breakpoint:
(gdb) continue
Continuing.Thread 2 hit Breakpoint 3, compare_entities (a=0x100404080, b=0x100404180) at cases/external-sort-urls/main.c:31
31 if (entity_a.amount == entity_b.amount) {
Great! It stopped exactly in the first case where it happened. Just to use the commands explored in the article:
(gdb) print entity_a
$9 = {url = 0x100304140 "http://bbmqb.darz.com/yhiddqsc/rjmow/xsjy/dbef.html", amount = 108}(gdb) print entity_b
$10 = {url = 0x100304180 "http://nec.ggx.com/orelln/apqfwkhop/coqhnwn.html", amount = 121}(gdb) finish
Run till exit from #0 compare_entities (a=0x100404080, b=0x100404180) at cases/external-sort-urls/main.c:31
0x0000000100003be6 in merge (array1=..., array2=..., comparator=0x100002150 <compare_entities>) at lib/merge_sort.c:40
40 if (comparator(array1_head, array2_head)) {Value returned is $12 = false
Where the debugger is now:
(gdb) frame
#0 ... in merge (array1=..., array2=..., comparator=... <compare_entities>)
at lib/merge_sort.c:40
40 if (comparator(array1_head, array2_head)) {
Current call stack:
(gdb) info stack
#0 ... in merge (...)
at lib/merge_sort.c:40
#1 ... in merge_sort (...)
at lib/merge_sort.c:73
#2 ... in merge_sort (...)
at lib/merge_sort.c:70
#3 ... in merge_sort (...)
at lib/merge_sort.c:70
#4 ... in flush_lines_to_tape (...)
at lib/external_sorting.c:29
#5 ... in split_input_file_into_sorted_tapes (...)
at lib/external_sorting.c:53
#6 ... in external_sorting (...)
at lib/external_sorting.c:154#7 0x0000000100002205 in main () at cases/external-sort-urls/main.c:38
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:
(gdb) delete 3
(gdb) continue
Continuing.
[Inferior 1 (process 53562) exited normally]
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 :).