Debugging is an indispensable skill for programmers and security researchers. Having a strong grasp of debugging allows you to understand an executable on a lower level and catch any lurking errors.
The GNU debugger or, GDB, is a timeless debugging tool that has been relied upon by programmers for years now. Here’s how to use GDB on Linux.
Preparing Sample Programs
To explore the features of GDB you’ll need an executable to experiment with. For demonstration, you will be running GDB on a key-checking program once with source code and debug symbols available, once without source code, and on a simple multithreaded program that prints messages to the screen, both written in C and compiled with GCC (GNU C Compiler).
You’ll most likely be running GDB on your own programs. So make sure to compile them with the -g flag with gcc to enable debug symbols.
Without the debug symbols present and with a heavily stripped binary, you’ll have to debug the disassembly of the program. This will require you to have a strong grasp of assembly language and how memory allocation works on Linux to understand the data in the stack and registers.
Running a Program in GDB
You run a program in GDB in a couple of ways. Either type in gdb <program>, and once it loads up, type run. Or start gdb and then using the file <program> command, load the binary into gdb, and then execute it with the run command.
If your program requires command-line arguments to function properly, make sure to add the arguments after the program name. Here’s the syntax for loading the program onto GDB and executing it with arguments:
Setting Breakpoints With GDB
Breakpoints in debugging are manually set hard stops in code that stop the flow of execution when the program reaches a breakpoint. Setting breakpoints allows you to step through code and inspect how each stage of execution affects data and variables.
In GDB, when you’re debugging a program with debug symbols, you can either set a breakpoint by the name of the function or set a breakpoint based on the line number. Here’s the syntax:
To view all breakpoints in the current debugging session, type:
To delete a particular breakpoint or multiple breakpoints, type:
GDB also allows you to set conditional breakpoints, meaning the program will only come to a halt if a particular condition is satisfied during execution. It could be the change in the value of a variable or unsuccessful function call or, anything you want. Here’s the syntax to set conditional breakpoints:
break <location> if n == 2
If you wish to continue execution of the program after hitting a breakpoint, type in the continue command:
Stepping Through Code
Stepping through the code is crucial to understanding how the program is handling the data. By stepping through various functions in your program and examining the state of data, you can gain a better understanding of how the program is implementing the logic you’ve written in code.
It also helps you detect the root of crashes and study program behavior with surgical precision as you’re able to step through each line of code as you wish. You can step through code in three primary ways in GDB:
- step: This command tells GDB to step into the next line of the source file. This allows you to essentially traverse the length of the source code line by line.
- next: This command executes the next line of source code inside the current function and then stops. next treats a function as a single line so if you use next before a function call, it will treat it as a single line and step over it, unlike the step command.
- finish: The finish command executes all the remaining lines inside the current function and then stops.
As you step through the code, you would want to examine the value of variables to see how the program logic is changing them. Here’s the syntax to view the value of variables in GDB:
In case you wish to print the changes in the value of a variable every time it is updated, you should use the display command. This is especially useful when you want to track and print the value of a variable in a loop:
Watchpoints and conditional breakpoints are closely related as they both respond to changes in a program. Watchpoints are used to track changes in data in the code. For instance, you might want the program to break whenever the value of a variable changes. Here’s how to do so with GDB:
Thread-Specific Debugging With GDB
GDB allows you to perform thread-specific debugging when working with multithreaded programs. For demonstration, we’ll be working with a simple C Program that uses four threads to print messages with each thread.
To view the currently spawned threads in your program, use the info command:
To work with a specific thread, you can select it from the list using its index number. For example:
After selecting the thread you can step through its execution flow using the step, next, and finish commands as demonstrated above.
Remote Debugging With GDB
You can also remotely debug programs located on a different system. To do so, you need to set up gdbserver on the target machine. You can easily install it using the default package manager of your distribution or other package managers you’ve installed on your system.
For instance, to install gdbserver on your Ubuntu or Debian-based systems, use APT:
sudo apt install gdbserver
Once installed, move into the folder of the binary and run this command to start gdbserver:
gdbserver <ip>:<port> <binary>
gdbserver should return the output that it’s up and listening on the port you defined. Now on the client machine, start GDB and then connect to the remote server using the target command:
target remote <server_ip>:<port>
Writing GDB Scripts to Automate Debugging
GDB allows programmers to write GDB scripts that will execute GDB commands automatically. This helps immensely when you’re trying to debug the same part of a code multiple times. Instead of having to set the breakpoint, step through code, and print variable values each time you load the binary, you can use a GDB script to automate the entire process.
Here’s an example:
set logging enabled on
set logging file sample.out
In the script above, you’re telling GDB to enable logging and save the log to a file called sample.out, then set a breakpoint at the main function.
For breakpoint number 1, in this case, the breakpoint at function main, run the following commands: backtrace, print, continue. Basically, GDB will first run a backtrace, then print the value of the variable “N”, continue execution, and finally quit.
To execute this script, use:
gdb -x <script> <binary>
Now You Know How to Debug Your Programs With GDB!
Debugging is an essential skill and debugging with GDB is a great skill to have in your repertoire. The different features of GDB like stepping through code, setting breakpoints, selective thread debugging, and more make it a powerful tool when debugging binaries on Linux.
If you’re looking to debug applications on Windows, you might consider learning more about a Windows native debugger, WinDBG.