Simple debugging with gdb

Debugging

Finding and repairing software errors is known as debugging. When a program has bugs, it can crash, behave unpredictably, or generate inaccurate results. In most cases, the steps involved in debugging are:

  1. Identifying the Bug: This can be done through testing or user reports. The developer needs to understand the circumstances under which the bug occurs.

  2. Isolating the Problem: The developer narrows down the part of the code where the bug is located. This might involve examining error messages, using debug logs, or employing debugging tools.

  3. Understanding the Bug: It’s important to understand why the bug is occurring. This might involve studying the relevant section of the code and understanding how it interacts with the rest of the system.

  4. Fixing the Bug: Once the bug is understood, the developer modifies the code to fix it. This also involves ensuring that the fix doesn’t introduce new bugs.

  5. Testing the Fix: After the bug is fixed, the software must be tested to ensure that the bug is truly gone and that no new issues have been introduced.

  6. Documenting the Change: It's good practice to document the changes made during debugging, especially in a team environment or for future reference.

Debugging can be challenging, especially in complex software systems, as bugs can be subtle and hard to reproduce.

GDB (GNU Debugger)

GDB is the GNU Project's debugger, a powerful tool used in software development for debugging applications written in C, C++, and other programming languages. Key features and uses of GDB include:

  1. Breakpoints: Setting breakpoints allows the program to be stopped at specific points to inspect the current state.

  2. Stepping Through Code: GDB allows developers to execute code line by line (or instruction by instruction) to observe the behavior of the program at each step.

  3. Inspecting Variables: It can be used to print out the current values of variables at any point during the program execution.

  4. Watching Variables: GDB can watch variables for changes, which helps in understanding how and why their values are altered.

  5. Evaluating Expressions: Evaluating expressions at runtime can help in understanding the program flow and logic.

  6. Call Stack Inspection: It allows inspection of the call stack, helping to trace the sequence of function calls leading to a particular point in execution.

  7. Handling Signals and Exceptions: GDB can catch signals and exceptions, providing a way to debug issues related to these events.

  8. Post-Mortem Debugging: It can analyze core dumps, which are snapshots of a program's state at the time of a crash, allowing post-mortem debugging.

  9. Remote Debugging: GDB can be used to debug programs running on different machines.

Though it may be intimidating at first, the command-line tool GDB provides profound insights into a program's operation. Its versatility, power, and the wealth of information it gives developers make it a popular choice.

To demonstrate how GDB can be used for debugging, let's go through a simple example. We'll create a basic C program that has a bug, and then use GDB to find and fix the bug.Certainly! Let's revisit the debugging process of the sample C program using GDB, incorporating additional commands such as continue, list, step, and run.

Step 1: Write a Sample C Program

Here's the C program with a bug:

#include <stdio.h>

int factorial(int n) {
    if (n == 1) return 1;
    return n * factorial(n - 1); // Bug: No base case for n=0
}

int main() {
    int num = 5;
    printf("Factorial of %d is %d\n", num, factorial(num));
    return 0;
}

Save this as example.c.

Step 2: Compile with Debug Information

Compile the program to include debug information:

gcc -g example.c -o example

Step 3: Start GDB

Launch GDB:

gdb ./example

Step 4: Set a Breakpoint

Set a breakpoint at the start of main:

(gdb) break main

Step 5: Run the Program

Start the program execution:

(gdb) run

Execution will stop at the beginning of main.

Step 6: List Source Code

List the source code around the current line:

(gdb) list

This will display the source code near the current breakpoint.

Step 7: Step Into Functions

Step into the factorial function:

(gdb) step

This will take you to the first line of the factorial function.

Step 8: Inspect and Step Through

Inspect variables and step through the code:

(gdb) print n
(gdb) step

Repeat the print and step commands to observe how n changes with each recursive call.

Step 9: Continue Execution

Once you've identified the bug (no base case for n == 0), you can let the program continue running:

(gdb) continue

The program will run to completion or until it hits another breakpoint.

Step 10: Fix the Bug

Exit GDB and fix the bug in example.c:

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

Step 11: Rebuild and Rerun

Recompile the program and start GDB again:

gcc -g example.c -o example
gdb ./example

Step 12: Debug the Fixed Program

Set the breakpoint, run the program, and use list, step, print, and continue as needed to ensure the bug is fixed.

Step 13: Exit GDB

Once satisfied with the fix:

(gdb) quit

This demonstrates a more detailed debugging session using GDB, utilizing a variety of commands to navigate and inspect the program execution.

0
Subscribe to my newsletter

Read articles from Jyotiprakash Mishra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jyotiprakash Mishra
Jyotiprakash Mishra

I am Jyotiprakash, a deeply driven computer systems engineer, software developer, teacher, and philosopher. With a decade of professional experience, I have contributed to various cutting-edge software products in network security, mobile apps, and healthcare software at renowned companies like Oracle, Yahoo, and Epic. My academic journey has taken me to prestigious institutions such as the University of Wisconsin-Madison and BITS Pilani in India, where I consistently ranked among the top of my class. At my core, I am a computer enthusiast with a profound interest in understanding the intricacies of computer programming. My skills are not limited to application programming in Java; I have also delved deeply into computer hardware, learning about various architectures, low-level assembly programming, Linux kernel implementation, and writing device drivers. The contributions of Linus Torvalds, Ken Thompson, and Dennis Ritchie—who revolutionized the computer industry—inspire me. I believe that real contributions to computer science are made by mastering all levels of abstraction and understanding systems inside out. In addition to my professional pursuits, I am passionate about teaching and sharing knowledge. I have spent two years as a teaching assistant at UW Madison, where I taught complex concepts in operating systems, computer graphics, and data structures to both graduate and undergraduate students. Currently, I am an assistant professor at KIIT, Bhubaneswar, where I continue to teach computer science to undergraduate and graduate students. I am also working on writing a few free books on systems programming, as I believe in freely sharing knowledge to empower others.