Lesson 62
Debugging with GDB
Covers compiling with debug information, breakpoints, stepping through code, inspecting variables, watchpoints, call stacks, and debugging best practices using GDB.
Lesson content
What is Debugging with GDB in C++?
Debugging with GDB in C++ is the process of finding, analyzing, and fixing errors in C++ programs using the GNU Debugger (GDB). Instead of guessing why a program crashes or produces incorrect results, GDB allows you to pause execution, inspect variables, step through code line by line, and analyze the program's state.
Imagine reading a mystery novel where you can stop at any page, inspect every character, and replay events before moving forward. That's exactly what GDB does for your program, letting you investigate what's happening while the program is running.
Debugging exists because even well-written programs can contain bugs. GDB helps developers identify problems quickly, reducing development time and improving software quality.
Real-world use cases
- Finding segmentation faults
- Debugging runtime crashes
- Inspecting variable values
- Tracing function calls
- Diagnosing memory-related issues
- Debugging multithreaded applications
- Analyzing core dump files
- Verifying program logic
Prerequisites
- Variables and data types
- Functions
- Loops and conditionals
- Pointers and references
- Basic command-line usage
- How to compile a C++ program
What is GDB?
GDB (GNU Debugger) is a command-line debugging tool that allows developers to control program execution and inspect the program while it is running.
It can pause execution, execute code one line at a time, display variable values, inspect memory, trace function calls, modify variables during execution, and analyze crashes.
Compiling with debug information
Before using GDB, compile your program with the -g flag. This option includes debugging information in the executable without changing the program's behavior.
g++ -g main.cpp -o programStarting GDB
# Launch GDB
gdb program
# Run the program inside GDB
runBreakpoints
A breakpoint tells GDB to pause execution at a specific line or function.
break main
break 25
break calculateSumUseful commands include break to set a breakpoint, delete to remove one, disable and enable to toggle a breakpoint, and info breakpoints to list all breakpoints.
Stepping through code
GDB allows you to execute your program one step at a time. next (n) executes the current line without entering functions, step (s) executes the current line and enters function calls, finish runs until the current function returns, and continue (c) continues execution until the next breakpoint.
Inspecting variables
# Display a variable
print variableName
# Example
print total
# Display all local variables
info locals
# Display function arguments
info argsWatchpoints
A watchpoint pauses execution when a variable's value changes. This is useful when you know a variable is being modified incorrectly but don't know where.
watch counterViewing the call stack
If a program crashes, you can inspect the sequence of function calls using backtrace, or its shortcut bt. The call stack helps locate the source of runtime errors.
backtrace
// Example output:
// main()
// calculateArea()
// divide()Examining memory
# Print memory contents
x variable
# Example
x &number
# View hexadecimal memory
x/8x addressUseful GDB commands
- run — start the program
- quit — exit GDB
- break — set a breakpoint
- continue — continue execution
- next — execute next line
- step — step into a function
- finish — finish current function
- print — display variable value
- watch — set a watchpoint
- backtrace — show call stack
- list — display source code
- help — show command help
Example 1: Using a breakpoint
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int y = 20;
int sum = x + y;
cout << sum << endl;
return 0;
}
// Compile:
// g++ -g main.cpp -o program
// Debug:
// gdb program
// break main
// run
// next
// print xExample 2: Stepping into a function
#include <iostream>
using namespace std;
int multiply(int a, int b)
{
return a * b;
}
int main()
{
int result = multiply(4, 5);
cout << result << endl;
return 0;
}
// Debug:
// break multiply
// run
// step
// print a
// print b
// finishExample 3: Finding a logic error
#include <iostream>
using namespace std;
int main()
{
int total = 0;
for (int i = 1; i <= 5; i++)
{
total += i;
}
cout << total << endl;
return 0;
}
// Debug:
// break 10
// run
// print i
// print total
// next
// continueExample 4: Debugging a segmentation fault
#include <iostream>
using namespace std;
int main()
{
int* ptr = nullptr;
cout << *ptr << endl;
return 0;
}
// Compile and debug:
// g++ -g crash.cpp -o crash
// gdb crash
// run
// backtrace
// print ptrThe debugger shows that ptr is nullptr, helping identify the cause of the crash.
Example 5: Using a watchpoint
#include <iostream>
using namespace std;
int main()
{
int counter = 0;
counter++;
counter += 5;
cout << counter << endl;
return 0;
}
// Debug:
// break main
// run
// watch counter
// continueThe program pauses each time counter changes, allowing you to inspect its new value.
Common mistakes and pitfalls
- Compiling without -g. Fix: compile with -g to include debug symbols
- Guessing the cause of a bug. Fix: use breakpoints and inspect variables
- Setting too many breakpoints. Fix: use only relevant breakpoints
- Ignoring the call stack. Fix: use backtrace after crashes
- Debugging optimized code (-O2, -O3). Fix: disable optimization (-O0) while debugging
// Wrong
g++ main.cpp -o program
// Correct
g++ -g -O0 main.cpp -o programUsing -O0 disables compiler optimizations, making debugging more predictable.
Best practices
- Always compile with -g while debugging
- Use -O0 to avoid optimization-related confusion
- Set breakpoints close to where the issue occurs
- Inspect variables before changing code
- Use watchpoints to track unexpected variable modifications
- Read the call stack after a crash
- Remove unnecessary breakpoints to keep debugging sessions manageable
- Learn keyboard shortcuts such as n, s, c, and bt for faster navigation
- Combine GDB with tools like sanitizers and static analyzers for more effective debugging
When not to use this
Avoid relying solely on GDB when you only need simple logging or diagnostic output, when static analysis tools can detect the issue before runtime, when performance profiling is the main goal, when memory leak detection requires specialized tools like Valgrind or AddressSanitizer, or when the application is running in a production environment where interactive debugging is impractical.
Summary
- Debugging with GDB in C++ helps identify and fix runtime errors efficiently
- Compile programs with -g to include debugging symbols
- Use breakpoints to pause execution at specific locations
- Step through code using next, step, finish, and continue
- Inspect variables with print, info locals, and info args
- Use watchpoints to monitor changes to variables
- Analyze crashes using the call stack with backtrace
- Compile with -O0 during debugging for more predictable behavior
Frequently asked questions
What is GDB in C++?
GDB (GNU Debugger) is a command-line debugging tool that lets you pause a program, inspect variables, step through code, and diagnose runtime errors.
Why should I compile with the -g option?
The -g option includes debugging information in the executable, allowing GDB to map machine instructions back to your source code.
What is the difference between next and step?
next executes the current line without entering function calls, while step enters called functions so you can debug them line by line.
What is a breakpoint?
A breakpoint is a marker that tells GDB to pause program execution at a specific line or function so you can inspect the program's state.
Can GDB debug segmentation faults?
Yes. GDB can stop when a segmentation fault occurs, allowing you to inspect the call stack, variables, and memory to determine the cause of the crash.