Better Programming

Advice for programmers.

Follow publication

Mastering GDB

Power up your debugging skills

Andrew Xu
Better Programming
Published in
7 min readMay 12, 2022
Photo by Sigmund on Unsplash

The GNU Project debugger (GDB) is a very useful debugger under Linux.
Smart programmers usually use gdb to debug bugs, when a program core dumps, or a program occurs unexpected behaviors. In this article, I will show you how to use gdb efficiently.

Choose appropriate compile flags

Compilers often make many optimizations in order to generate optimal code, which makes debugging difficult. If you encounter errors like symbols not found, you need to open appropriate compile flags for debugging, so that the debugger can get enough information.

  • use -g to let the compiler build debug information into the executable.
  • use -ggdb3 can make gdb debug macros.
  • use -fno-omit-frame-pointer to avoid the compiler optimizing out small functions, so that you can see the full call stack.
  • use -0g to let the compiler turn on optimizations that don’t affect debugging.

Start gdb

After compiling a program with appropriate flags, we can use GDB to debug the generated executable. There are many different scenarios, and the startup method in each scenario is slightly different.

  • In the first scenario, we have an executable that accepts some parameters. We can start gdb with gdb executable, and then run the program with run arg1 arg2 .
  • In the second scenario, a core dump file is generated when an executable is running, probably a segment fault. We can start gdb with gdb executable coredump_file and gdb will stop at where the error happens.
  • In the third scenario, we have an executable that is running. We can use gdb attach pid to attach to the process.
screenshots of gdb starting
Screenshots of starting gdb

Breakpoints and watches

After starting gdb, we usually set a breakpoint. The program will pause when it hits a checkpoint. And we can observe the status of the program.

  • break func will break at a function.
  • break example.cc:10 will break at line 10 of the example.cc, if there is only one file, we can omit the filename, just use break 10 . And we can type list to show the codes around the current line.
  • info breaks can show you all breakpoints you have set.
  • delete 2 will delete the second breakpoint.
  • You can also set conditional breakpoints, just append if [condition] to break command.
  • watch [variable] can set a watch on a variable, watches are another kind of breakpoint, that will pause the program if the watched variable has changed.
Illustration of setting breakpoints

Variables and expressions

When paused on a breakpoint, we can observe the status of the program by printing variables and expressions.

  • info args can show us the arguments of a function.
  • info locals can show us the local variables of the current function.
  • print can be used to show the result of a variable or an expression. See the bellow illustrations. print a prints a variable. print a=2 will first evaluate a=2 and then print the outcome. Note that if you directly type a=2 , you will get an error. print sum(1, a) calls the function sum .

(gdb) print a
$1 = 1
(gdb) print a=2
$2 = 2
(gdb) print sum(1, a)

You can also format the outcome of print ,

  • print /x <exp> will show the result in hexadecimal.
  • print /t <exp> will show the result in binary.
  • print /d <exp> will show the result in unsigned int format.
  • print /c <exp> will show the result in signed int format.
  • Note the outcomes of print , such as $1 , $2 , are also variables that can be used further.
  • dprintf locaion, format-string, expr1, expr2 . drpintf is a convenient command that can dynamically print at the location, just as you have inserted an printf expression at the location.
  • set $foo = 4 can set a variable. This is convenient if you want to save some intermediate results.
  • command 2 is another gdb command, that you can set commands to be executed when hit the specific breakpoints. The illustration below will execute info locals automatically when the first breakpoints are hit. You can also use this method to achieve the same effects of dprintf .

(gdb) command 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just “end”.
>info locals
>end

  • print *&arr[96]@5 can print the 96–100 elements of an array.

Note currently we can not easily print std::vector or other c++ stdlib containers. We will discuss this topic later.

Execution control

There are several commands allowing us to control the execution of the program in the gdb session.

  • run will start running the program and stop at the first breakpoint, if there are no breakpoints, the program will stop at the exit.
  • start will start the program and stop the main function.
  • continue will continue the program and stop at the next breakpoint or exit.
  • finish will stop when the current function finishes.
  • next will stop at the next line.
  • step will enter a function and stop there.

Text user interface

Gdb contains a text user interface (TUI), just like most IDEs, which can display lines of code while debugging. tui enable to enter the tui mode, and tui disable to exit.

Illustration of TUI
Illustration of TUI

Backtrace and threads

Backtrace is useful for debugging, especially when we debug a core dump.

  • bt will show the current backtrace.
  • up will move you to the upper frame.
  • down will move you to the lower frame.
  • frame 2 will directly take you to the second frame.

When debugging a multithread program,

  • info threads shows all threads.
  • thread 5 will go to the fifth thread.
  • thread apply all [command] will execute a command on all threads. It’s very convenient to use thread apply all bt full to print all backtrace of all threads.

Configuring gdb with .gdbinit

Gdb has a configuration file, located at ~/.gitinit or in the current folder, just like .vimrc for vim . The configuration file in the current path has a higher priority to be loaded than ~/.gitinit . Let’s discuss some configurations.

  • set logging on will output all output to gdb.txt . This is useful when commands output so much information, like the above thread apply all bt full .
  • set history save on saves the command history.
  • set pagination off disable the interactive display of long screen output.
  • set print pretty on can display a c++ class with a more pretty format.
  • set confirm off disable confirmation.

Since gdb can not print c++ containers such as std::vector conveniently, we can write some scripts to make it easy. Follows https://gist.github.com/skyscribe/3978082.

Source file path

Sometimes we encounter errors like xxx.c: No such file or directory . This means that gdb can not find the source file. The solutions are:

  • use directory [folder] to add the specific directory to gdb’s search path.
  • use set substitute-path [src] [dst] to substitute path.

Macro expanding

You can also expand macro to see the details after preprocessing.

  • use macro expand some_macro(macro_arg) to expand macro.
  • use info macro some_macro to see its definition.

Note gdb will analyze the context of the current function, only macros that are visible in the current context can be expanded.

Record the status of the program

One powerful command of gdb is record , which records the state of the running program, and allows you to back-run the program. Gdb also provides a group of commands starting with reverse prefix, like reverse-continue , reverse-finish , reverse-next , reverse-nexti , reverse-step , reverse-stepi .

One usage of record is to find memory corruption. You can run the program in gdb, record the execution of the program, and stop at the location where memory corruption occurred. Then you can watch the memory address that was corrupted. Then reverse run the program to see where the content of the address has changed.

The drawback of record is that it will slow down your program. It still takes some work to use it well.

Save and load the debug session

Sometimes, you need to adjust the code repeatedly and run the program multiple times to gradually understand the details of the bug. But if you restart gdb every time, it will be inconvenient to set checkpoints repeatedly. We can save the debug session and reload it later.

If you use a makefile to build your program, you can simply type make within the gdb session, it will rebuild your program and reset the checkpoints.

You can also use save breakpoints bp.txt to save the breakpoints to bp.txt, and use source bp.txt to reload these breakpoints later, or use gdb -x bp.txt --args [exe] to load it when gdb starts.

More about debugging

In addition to gdb, there are many other debug methods: such as analyzing logs. Binary search git commits. They have their own advantages and disadvantages. When debugging a program, you can combine these methods to solve the problem efficiently. The advantage is that you have full access to the running program, this is extremely useful when a bug is hard to reproduce.

Andrew Xu
Andrew Xu

Written by Andrew Xu

Python, c, c++, machine learning. Loves coding and sharing.

Responses (2)

Write a response