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/

GDB sample output.

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:

Breakpoints are indicated by red dots.

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:

Debug section and “Run and Debug” buttons circled.

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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:

  1. User-Defined Functions: GDB supports user-defined functions via the “define” command. 

  2. 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.

  3. 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.

  4. 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.

  5. Navigating Help Menus: The “apropos” command is useful to search for commands matching a regular expression.

  6. Saving Breakpoints: You can save breakpoints to a file using the “save breakpoints” command, allowing you to load these breakpoints later.

  7. Temporary Breakpoints: The “tbreak” command sets a temporary breakpoint. Temporary breakpoints are automatically deleted when hit.

  8. 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. 

Next
Next

VirtualBox Shared Folders