Debugging C with GDB and Valgrind

July 2017 ยท 12 minute read

Debugging C can be a chore, but being able to pinpoint your memory leaks with Valgrind and monitoring the flow of your program with GDB (or LLDB) can speed up the development of your code significantly. It’s a significant improvement over sticking a bunch of printf statements in your code and taking them out before production (which should by no means be your sole tool for debugging).

Setup

Installing gdb is pretty easy on both OS X and Linux. Use your favorite package manager to install gcc and gdb. You can also install the LLVM compiler collection which uses lldb as its debugging tool rather than gdb.

Installing Valgrind is similar - fire up your favorite package manager, build from source, or download a release directly from the webpage. On MacOS, there are some caveats if you’re trying to install Valgrind on MacOS > 10.12.

Usage

GDB/LLDB

GDB and LLDB allow a user to scrutizine the execution of their programs line by line. You can look at the state of your program, inspecting variables, seeing what is executing, and even evaluating arbitrary statements based on the state of your program. We will examine how to use these programs.

The basics

Both GDB/LLDB provide some basic debugging functions like:

GDB allows you to inspect your code by viewing its state as it executes on a per line basis. You can step through your program line by line. More commonly, if you suspect that your problem is in a particular segment of code, you can create a breakpoint at a function or a line number. You can do some powerful things while you step through your program like inspect what variables are in your program, and execute arbitrary statements based on the state of your program.

GDB/LLDB gives you access to certain key commands that will help you debug your program. next will execute the current line, and the pause at the next line. This will not go into any functions, it will block gdb until the instructions on the current line have been executed. step (aka step into) will actually step into the function on the current line. If you have a function call on the current line, then GDB will “step into” that function, allowing you to continue debugging line by line inside the function. finish is like stepping out - it will continue execution until the function of the current line finishes executing/until it returns.


You can find a reference for these commands (in GDB) here. Here is a cheat sheet from LLVM with LLDB commands and their gdb equivalents.

Usage

In order to run gdb, you need to make sure that your binary has debug symbols so that GDB knows the relationship between what is being executed and your code. You compiling with optimization flags (-OX) can cause issues because an optimization may not necessarily stay true to your code. For example, a compiler will not keep unused variables because they waste stack space. To stay safe, compile debug builds without any optimizations. You can use an optimization flag with the debug symbols. The flag is -g, so when compiling with gcc, add the -g flag to your list of flags. We will explore an example with an example program I created.

Example

Setup

You can download my example source code here

You can compile the code with this command

gcc -g gdb_valgrind_example.c -o gve -Wall -Wextra -std=c11 -pedantic
-Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition

Try running the code. It seems like it should work. It’s simple, it just allocates a string and initializes it to "hello".

You can run the code with the following command:

./gve

The output I got was:

This next string should be blank...
zsh: segmentation fault (core dumped)  ./gve

So what happened? Let’s step through this program with GDB. Using LLDB should be extremely similar.

Debug

In order to start running gdb, we have to give it a program to run. GDB takes a binary followed by its arguments to initialize the debug runtime.

gdb --args gve

If we had arguments, we’d use something like gdb --args gve arg1 arg2 ...

Now GDB is initialized with our program, gve (GDB Valgrind Example)

Here’s what you’ll see at first (or something like this)

GNU gdb (GDB) Fedora 7.12.1-48.fc25
Copyright (C) 2017 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-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://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 gve...(no debugging symbols found)...done.
(gdb)

We want to set a breakpoint at the main function. Otherwise, GDB will just run without giving us anything useful to look at.

(gdb) break main
Breakpoint 1 at 0x400577

Let’s run the program, (just type in run)

Starting program: /net/ifs-users/afnan/gve

Breakpoint 1, 0x0000000000400577 in main ()

Now GDB is frozen at the beginning of the main function. Let’s step through the main function and see what’s happening to the str variable before and after we call init_str.

Breakpoint 1, main () at gdb_valgrind_example.c:32
32	    char *str = NULL;
(gdb) n
33	    printf("This next string should be blank...\n");
(gdb) n
This next string should be blank...
34	    init_str(str);

Right before we call init_str, let’s check the value of str by calling p str

(gdb) p str
$1 = 0x0

We can see that str is NULL, as it was assigned. Let’s step and see what happens after we call init_str. Theoretically, str should be changed to “hello” after init_str is called.

(gdb) n
35	    printf("%s\n", str);
(gdb) p str
$3 = 0x0

We can see that str is still NULL. Now we know the function is not working as intended. We could do two things: we could rerun the program and step until we reach the init_str function call, or we could just set a breakpoint directly at the function. Let’s set a breakpoint, since it’ll be faster.

We’ve already gone past the function call, so we will have to restart the program. Let’s set a breakpoint at init_str then restart the program.

(gdb) break init_str
Breakpoint 2 at 0x400552: file gdb_valgrind_example.c, line 25.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /net/ifs-users/afnan/gve

Breakpoint 1, main () at gdb_valgrind_example.c:32
32	    char *str = NULL;
(gdb)

Our breakpoint set earlier is still there (at main). We don’t want to step all the way to the function, so let’s continue until we hit the next breakpoint.

(gdb) c
Continuing.
This next string should be blank...

Breakpoint 2, init_str (str=0x0) at gdb_valgrind_example.c:25
25	    str = malloc(5);

Now we’ve reached the start of the init_str function. GDB tells us which file and line the breakpoint is one. It also tells us the value of the argument: str = 0x0. So we know str=NULL when init_str is starting. Let’s step through the function, inspecting the variables as we go through, so we can see what’s going on.

25	    str = malloc(5);
(gdb) n
26	    strcpy(str, "hello");
(gdb) p str
$4 = 0x602420 ""
(gdb) n
27	}
(gdb) p str
$5 = 0x602420 "hello"
(gdb) n
main () at gdb_valgrind_example.c:35
35	    printf("%s\n", str);
(gdb) p str
$6 = 0x0

It looks like str gets allocated correctly as evidence by $4. It is assigned correctly with strcpy, but when we return, str becomes NULL again.

This means that we’re not actually changing str outside of the scope of init_str. This is because we need some pointer indirection - to affect a variable outside of its enclosing scope, we need a pointer to that variable. It’s essentially the same here: we need a pointer to our char*.

We can easily rectify the problem by passing a reference to our string rather than passing in the string directly. This is called indirection. Let’s fix the program:

/* gdb_valgrind_example.c    Afnan Enayet
 *
 * An example designed to help a user understand how to
 * use GDB/LLDB and Valgrind
 *
 * Functions:
 *   - init_str: a functions that initializes a string
 *     to "hello"
 *
 */

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

/****** Function prototypes ******/

static void init_str(char **str);

/****** Function definitions *****/

/* Initializes and creates a string that says "hello"
*/
static void init_str(char **str)
{
    *str = malloc(5);
    strcpy(*str, "hello");
}

int main(void)
{
    // Allocating a string with the literal "hello"
    char *str = NULL;
    printf("This next string should say hello...\n");
    init_str(&str);
    printf("%s\n", str);
    return 0;
}

Let’s go ahead and check that the program is working as intended. Recompile the file and load it with GDB. Let’s set a breakpoint right before and right after the function call so we can see if str was allocated/set properly.

(gdb) break 33
Breakpoint 1 at 0x40058c: file gdb_valgrind_example.c, line 33.
(gdb) break 35
Breakpoint 2 at 0x4005a2: file gdb_valgrind_example.c, line 35.
(gdb) r
Starting program: /net/ifs-users/afnan/gve

Breakpoint 1, main () at gdb_valgrind_example.c:33
33	    printf("This next string should say hello...\n");
(gdb) p str
$1 = 0x0
(gdb) n
This next string should say hello...
34	    init_str(&str);
(gdb) n

Breakpoint 2, main () at gdb_valgrind_example.c:35
35	    printf("%s\n", str);
(gdb) p str
$2 = 0x602420 "hello"

We can see that the string now reads “hello”. The program has been corrected.

Valgrind

Even if a program is working correctly or appears to be working correctly, memory errors could be present that you’ll never see, or will pop up seemingly randomly in a manner that makes it difficult to diagnose with GDB.

Valgrind is a tool that inspects your binary to see if there are any memory errors or leaks. It’s a very powerful tool that you should use to inspect your programs to ensure program correctness.

The basics

Valgrind contains a number of tools that you can use to help you diagnose/inspect your program. You can initialize a certain tool by passing an argument flag to Valgrind. We’re going to use memcheck, which is described here. There are other tools available that are very helpful for increasing the performance of your C/C++ programs, but this particular post will not cover them. The memcheck tool will run your program, display output, and display errors as they arise while the program is executing.

The example usage for our program would be valgrind --tool=memcheck --leak-check=full

Let’s try it

==27065== Memcheck, a memory error detector
==27065== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==27065== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==27065== Command: ./gve
==27065==
This next string should say hello...
==27065== Invalid write of size 2
==27065==    at 0x400573: init_str (gdb_valgrind_example.c:26)
==27065==    by 0x4005A1: main (gdb_valgrind_example.c:34)
==27065==  Address 0x5200484 is 4 bytes inside a block of size 5 alloc'd
==27065==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==27065==    by 0x40055B: init_str (gdb_valgrind_example.c:25)
==27065==    by 0x4005A1: main (gdb_valgrind_example.c:34)
==27065==
==27065== Invalid read of size 1
==27065==    at 0x4C30BC4: strlen (vg_replace_strmem.c:454)
==27065==    by 0x4EAAA61: puts (ioputs.c:35)
==27065==    by 0x4005AD: main (gdb_valgrind_example.c:35)
==27065==  Address 0x5200485 is 0 bytes after a block of size 5 alloc'd
==27065==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==27065==    by 0x40055B: init_str (gdb_valgrind_example.c:25)
==27065==    by 0x4005A1: main (gdb_valgrind_example.c:34)
==27065==
hello
==27065==
==27065== HEAP SUMMARY:
==27065==     in use at exit: 5 bytes in 1 blocks
==27065==   total heap usage: 2 allocs, 1 frees, 1,029 bytes allocated
==27065==
==27065== 5 bytes in 1 blocks are definitely lost in loss record 1 of 1
==27065==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==27065==    by 0x40055B: init_str (gdb_valgrind_example.c:25)
==27065==    by 0x4005A1: main (gdb_valgrind_example.c:34)
==27065==
==27065== LEAK SUMMARY:
==27065==    definitely lost: 5 bytes in 1 blocks
==27065==    indirectly lost: 0 bytes in 0 blocks
==27065==      possibly lost: 0 bytes in 0 blocks
==27065==    still reachable: 0 bytes in 0 blocks
==27065==         suppressed: 0 bytes in 0 blocks
==27065==
==27065== For counts of detected and suppressed errors, rerun with: -v
==27065== ERROR SUMMARY: 4 errors from 3 contexts (suppressed: 0 from 0)

We have a memory leak! We’ve lost 5 bytes from where we called malloc in the init_str method. This means that we forgot to free the string that we allocated. Let’s fix this and then run Valgrind again.

Here is the corrected program:

/* gdb_valgrind_example.c    Afnan Enayet
 *
 * An example designed to help a user understand how to
 * use GDB/LLDB and Valgrind
 *
 * Functions:
 *   - change_str: a functions that converts all of the
 *     characters in a string to spaces
 *
 */

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

/****** Function prototypes ******/
static void init_str(char **str);

/****** Function definitions *****/

/* Initializes and creates a string that says "hello"
*/
static void init_str(char **str)
{
    *str = malloc(5 * sizeof(char));
    strcpy(*str, "hello");
}

int main(void)
{
    // Allocating a string with the literal "hello"
    char *str = NULL;
    printf("This next string should say hello...\n");
    init_str(&str);
    printf("%s\n", str);
    free(str);
    return 0;
}

Here is the Valgrind output:

[afnan@moose]~% valgrind --tool=memcheck --leak-check=full ./gve
==27500== Memcheck, a memory error detector
==27500== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==27500== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==27500== Command: ./gve
==27500==
This next string should say hello...
==27500== Invalid write of size 2
==27500==    at 0x4005B3: init_str (gdb_valgrind_example.c:26)
==27500==    by 0x4005E1: main (gdb_valgrind_example.c:34)
==27500==  Address 0x5200484 is 4 bytes inside a block of size 5 alloc'd
==27500==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==27500==    by 0x40059B: init_str (gdb_valgrind_example.c:25)
==27500==    by 0x4005E1: main (gdb_valgrind_example.c:34)
==27500==
==27500== Invalid read of size 1
==27500==    at 0x4C30BC4: strlen (vg_replace_strmem.c:454)
==27500==    by 0x4EAAA61: puts (ioputs.c:35)
==27500==    by 0x4005ED: main (gdb_valgrind_example.c:35)
==27500==  Address 0x5200485 is 0 bytes after a block of size 5 alloc'd
==27500==    at 0x4C2DB9D: malloc (vg_replace_malloc.c:299)
==27500==    by 0x40059B: init_str (gdb_valgrind_example.c:25)
==27500==    by 0x4005E1: main (gdb_valgrind_example.c:34)
==27500==
hello
==27500==
==27500== HEAP SUMMARY:
==27500==     in use at exit: 0 bytes in 0 blocks
==27500==   total heap usage: 2 allocs, 2 frees, 1,029 bytes allocated
==27500==
==27500== All heap blocks were freed -- no leaks are possible
==27500==
==27500== For counts of detected and suppressed errors, rerun with: -v
==27500== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)

There are some errors, but we fixed the memory leak! I will address these errors later or in another blog post.

Happy debugging!