GNU Project Debugger Introduction
“Debugging is like being the detective in a crime movie where you’re also the murderer.” - Filipe Fortes https://www.pinterest.com/pin/825636544166469145/, https://fortes.com/filipe/, https://www.linkedin.com/in/fortes/
I have used the GNU (https://www.gnu.org/home.en.html) Project Debugger (https://sourceware.org/gdb/) to examine my code for decades. It gives a lot of insight into what your code is doing and is a great debugger. I usually use it to debug C/C++ code, but it supports other languages such as Assembly (https://simple.wikipedia.org/wiki/Assembler), Go (https://go.dev/), Objective-C (https://en.wikipedia.org/wiki/Objective-C), and Rust (https://www.rust-lang.org/). This article is meant to be an introduction to GDB.
I will be doing this on an Ubuntu 20.04 machine with Visual Studio Code (https://code.visualstudio.com/) installed. See my articles at https://www.keypuncher.net/blog/visual-studio-code or https://www.keypuncher.net/blog/vscode-debugging for details on how to get VS Code installed onto Ubuntu.
Debug Symbols
I think any introductory debugging tutorial would be incomplete without a section on Debug Symbols. These are explained here: https://www.gdbtutorial.com/gdb-debug-symbols and here: https://en.wikipedia.org/wiki/Debug_symbol.
Essentially, they map memory addresses to a symbol table along with other information like the source files and line numbers where the symbols have been defined.
All of this additional information can take up quite a bit of space, especially the filenames and line numbers. Binaries with debug symbols can become quite large, often several times the stripped file size. To avoid this extra size, most operating system distributions ship binaries that are stripped, i.e., from which all of the debugging symbols have been removed.
We’ll get into Debug Symbols a bit more when we discuss compilation.
Obtain GDB
GDB is available as a package on most Linux variants. The Ubuntu package is called gdb and it and its dependencies can be installed by entering the following into a terminal:
sudo apt install gdb
The install process is similar for most Linux flavors, you just have to know how your package manager or source repositories function. That is outside the scope of this article.
If you want to download from source you can also do so at https://sourceware.org/pub/gdb/releases/ but GDB is pretty mature and I have never had to download from source.
GDB + VS Code
The example code for this tutorial lives here: https://github.com/mday299/keypuncher/tree/main/C%2B%2B/VSCode/GDB/intro
Note that if you need to install any additional VS Code extensions you probably will need to restart VS Code. See C++ programming with Visual Studio Code.
Inside the referenced repository you should find this C++ code:
#include <iostream>
#include <string>
int loopInt (int iterations) {
int i;
for (i = 0; i < iterations; i++) {
std::cout << i << std::endl;
}
return i;
}
int main (int argc, char *argv[]) {
std::cout << "Howdy" << std::endl;
float theFloat = 8;
std::string theString = "Boo!";
int it;
it = loopInt(5);
return 0;
}
Along with this launch.json file:
{
"name": "C++ Launch",
"type": "cppdbg",
"request": "launch",
"stopAtEntry": false,
"customLaunchSetupCommands": [
{ "text": "target-run", "description": "run target", "ignoreFailures": false }
],
"launchCompleteCommand": "exec-run",
"linux": {
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
}
}
and this tasks.json file:
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ build active file",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Task generated by Debugger."
}
],
"version": "2.0.0"
}
The args section of tasks.json is about the most important part. The -g in the section tells the compiler to include the debugging symbols.
Next, we will insert what’s known as a breakpoint. According to Wikipedia:
… a breakpoint is an intentional stopping or pausing place in a program, put in place for debugging purposes. It is also sometimes simply referred to as a pause.
To insert a breakpoint in VS Code, proceed between the left and right panes until you see a faint red dot. Clicking on the dot will set your breakpoint. Set it on this line:
std::cout << i << std::endl;
inside the for loop. See the figure below:
Next, on the left-hand side there should be an icon with a little bug and a play button. Click there to bring up the “Run and Debug” button. Clicking on this button should start your GDB session:
When you have started your debugging session the breakpoint will have an arrow icon put inside it indicating that the execution has paused at the breakpoint. Click “Step Over” a few times to get a bit of a feel about how stepping works.
Next try clicking on the “Step Over” button a few more times (boxed in green below). Your output should be similar to the following:
This indicates we have proceeded through our for loop up to where it has stopped on line 7. The arrow icon with the highlighted line indicates we are just about to iterate through the for loop.
If you click “Step Over” or “Continue” enough times you should get to the end of your program. This for loop only will iterate 5 times. Hot keys for Step Over and Continue are F10 and F5, respectively. If you wish to stop the debugger click the red box or <SHIFT> + <F5>.
Please refer to my article at https://www.keypuncher.net/blog/vscode-debugging to get an idea of how watch variables and enhanced breakpoints work in GDB and VS Code.
“Pure” GDB
Sometimes there may be a need to use GDB without VS Code. Examples include:
Remote Debugging: GDB supports remote debugging, allowing you to debug a program running on a different machine. While VS Code also supports remote debugging, setting it up can be more complex compared to GDB.
Command-Line Interface: GDB operates in a command-line interface, which can be faster and more efficient for those who are comfortable with command-line tools. It’s also easier to automate tasks in GDB through scripting.
Low-Level Debugging: GDB provides a set of powerful commands for low-level debugging, such as inspecting memory, registers, and assembly code. This can be crucial when debugging complex low-level issues.
Limited Resources: If you’re working on a system with limited resources, GDB would be a better choice as it’s less resource-intensive compared to VS Code.
Compatibility and Portability: GDB supports a wide range of programming languages and platforms. If you’re working with a language or platform that VS Code doesn’t support, GDB might be your best option.
Advanced Features: Some advanced features of GDB, such as reverse debugging (ability to step back in time), are not available or not as mature in VS Code.
To invoke GDB from the command line without using VS Code you enter:
gdb <program_name>
For the above example (once we have built with VS Code), at terminal prompt enter:
cd <path to repo>
for me this is:
cd /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro
gdb debugging
Then, to run through the entire program type:
run
or
r
at the gdb prompt. Doing so gets me the following output on my machine:
(gdb) run
Starting program: /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging
Howdy
0
1
2
3
4
[Inferior 1 (process 182919) exited normally]
To exit gdb enter
q
or
quit
Breakpoints
Breakpoints by Function Name
In my opinion breakpoints based on function name are by far the most resilient to code changes. To insert such a breakpoint simply enter the following at a GDB prompt:
break <functionName>
so to do it for our little example enter:
break loopInt
You can then run the program with
r
and it will stop at the beginning of the function loopInt. When you want to continue enter
continue
or
c
Note also the existence of the step command.
step
or
s
For details see: https://docs.adacore.com/gdb-docs/html/gdb.html#Continuing-and-Stepping
Breakpoints by Line Number
You can set a breakpoint by a line number, for example:
break 16
inserts a breakpoint at line 16
Breakpoints by Offset
You can set a breakpoint by an offset from the current line:
break +2
sets a breakpoint 2 lines below the current line.
Conditional Breakpoints
To set a conditional breakpoint enter:
break loopInt if i > 2
sets a breakpoint if the variable i is > 2.
Get a List of All Breakpoints
Enter
info breakpoints
at the gdb prompt and you will get all of the current gdb session’s breakpoints.
Watch Variables
I find that watch variables are somewhat less useful in GDB because they must be in scope, unlike in VS Code. You may even need to know the exact memory address of the variable, which has its own limitations -- e.g., memory addresses tend to be volatile (see: https://www.techtarget.com/whatis/definition/volatile-memory) between program runs.
With that caveat, if the variable is in scope you can set a watch on it with:
watch <variableName>
To see the watches you have set enter
info watchpoints
which gives you a list of the watches you have set along with a somewhat arbitrary ID number.
Example GDB Use
Watch variables aside, there is something to be said for the elegance that raw GDB can provide. I will demonstrate some of the more common GDB commands using a practical example.
Open GDB
cd <path_to_repo>
in my case this is:
cd $HOME/dev/keypuncher/C++/VSCode/GDB/intro/
gdb debugging (after compiling debugging .cpp)
Set a few breakpoints then start running:
break main
break loopInt
break 7 if i > 2
break 16
r
My screen looks like this after running through those commands:
(gdb) break main
Breakpoint 1 at 0x12d8: file /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp, line 12.
(gdb) break loopInt
Breakpoint 2 at 0x1289: file /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp, line 4.
(gdb) break 7 if i > 2
Breakpoint 3 at 0x12a7: file /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp, line 7.
(gdb) break 16
Breakpoint 4 at 0x1330: file /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp, line 16.
(gdb) r
Starting program: /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging
Breakpoint 1, main (argc=1, argv=0x280)
at /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp:12
12 int main (int argc, char *argv[]) { Next, set a watch and display it
Next enter the following commands:
watch theFloat
display theFloat
We come to something interesting here: because theFloat variable is in the symbol table but it has not yet been initialized its value is undefined. For me it is:
(gdb) display theFloat
1: theFloat = 1.465986e+13
run through the program a bit more (by pressing ‘c’) and it will be set to the value 8. My screen looks like this after running a ‘c’ command:
(gdb) c
Continuing.
Howdy
Breakpoint 3, main (argc=1, argv=0x7fffffffe078)
at /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp:16
16 std::string theString = "Boo!";
1: theFloat = 8
pushing ‘c’ again should bring us to our next breakpoint. My screen looks like this:
(gdb) c
Continuing.
Breakpoint 2, loopInt (iterations=-810532992)
at /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp:4
4 int loopInt (int iterations) {
We have now arrived at the beginning of the loopInt function. Pushing ‘c’ again should bring us all the way to the conditional break point. My screen looks like this after pushing ‘c’:
(gdb) c
Continuing.
0
1
2
Breakpoint 3, loopInt (iterations=5)
at /home/mday39/dev/keypuncher/C++/VSCode/GDB/intro/debugging.cpp:7
7 std::cout << i << std::endl;
Printing out the value of the variable ‘i’ should make it clear why the function stopped executing here. Enter this at the GDB prompt:
print i
on my screen I get this:
(gdb) print i
$1 = 3
In GDB, when you use the print
command to display the value of a variable, the output is prefixed with a dollar sign ($
) followed by a number. This number is an automatic convenience variable that GDB creates for you. In my case, $1
is the first convenience variable that GDB created in my session. The number after the dollar sign increments each time you use the print
command. So, if you print another variable, it will be stored in $2
, and so on. These convenience variables are useful because you can refer to them later in your GDB session.
After poking around as much as you like: if you press ‘c’ several more times it should bring you to the end of the program.
You will likely get one or more messages about C library files not being found. These messages are indicating that it’s stopped whatever library function can’t be found.
You can safely ignore these messages.
If you wish to read more about my particular case then read the next few paragraphs.
The __libc_start_main
function is the entry point to the C library (libc), and it’s responsible for setting up the environment for the main
function, which is the entry point of your program. It takes several arguments, including a pointer to the main
function, the argc
and argv
parameters that will be passed to main (see https://en.wikipedia.org/wiki/Command-line_argument_parsing)
.
The error message ../csu/libc-start.c: No such file or directory
is indicating that GDB is unable to find the source code for the __libc_start_main
function. This is expected because the source code for the C library is not typically available on most systems.
Scripting GDB
https://github.com/mday299/keypuncher/tree/main/C%2B%2B/VSCode/GDB/intro includes a script with the following example included:
#my_script.gdb
break main
break loopInt
break 7 if i > 2
break 16
r
watch theFloat
display theFloat
c
c
c
print i
c
print i
c
c
To access from the GDB prompt enter:
cd <path_to_script>
gdb debugging
source my_script.gdb
Note the paging system in GDB might interfere a bit. To deal with the paging issue I simply pressed <RETURN> when I was finished reviewing the current page.
More GDB
GDB has over 1500 commands! Use them for tasks such as:
User-Defined Functions: GDB supports user-defined functions via the “define” command.
Commands at Breakpoints: GDB allows you to automatically execute commands when stopping at a specific breakpoint. This is useful when you have to repeatedly print or execute the same commands over and over again.
Python API: GDB has integration with Python API, allowing users to extend GDB even further to their needs. You can perform more complex tasks such as storing return values into variables, creating loops, and so on.
Logging: GDB has built-in commands for logging the session. You can use the “set logging file” command to specify the log file and the “set logging on” command to start logging.
Navigating Help Menus: The “apropos” command is useful to search for commands matching a regular expression.
Saving Breakpoints: You can save breakpoints to a file using the “save breakpoints” command, allowing you to load these breakpoints later.
Temporary Breakpoints: The “tbreak” command sets a temporary breakpoint. Temporary breakpoints are automatically deleted when hit.
Advanced Breakpoints: The “rbreak” command sets a breakpoint on all functions matching a regular expression. This is useful when you want to set breakpoints on multiple functions at once.
Feedback
As always, do make a comment or write me an email if you have something to say about this post!
Credits:
Visual Studio 2019 Tricks and Techniques: A Developer's Guide to Writing Better Code and Maximizing Productivity, Authors: Schroeder, Paul, Cure, Aaron, Price, Ed
https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
https://opensource.com/downloads/gnu-debugger-cheat-sheet
https://sourceware.org/gdb/documentation/
https://docs.adacore.com/gdb-docs/html/gdb.html
https://opensource.com/article/22/12/gdb-step-command
https://code.visualstudio.com/docs/cpp/launch-json-reference
https://www.cse.unsw.edu.au/~learn/debugging/modules/gdb_watch_display/
https://www.keypuncher.net/blog/vscode-debugging