Debugging Techniques

Using debugging tools and techniques to identify and fix errors in your C programs.


Debugging C Programs with GDB

Debugging Tools (GDB)

The GNU Debugger (GDB) is a powerful, command-line debugging tool that allows you to control the execution of your programs, examine their internal states, and pinpoint the source of errors. It's invaluable for understanding why your C programs are behaving unexpectedly. GDB works by allowing you to step through your code line by line, inspect variable values, and analyze the call stack. This gives you deep insight into the program's runtime behavior.

Introduction to Using GDB for C Programs

This section will introduce you to the fundamental GDB commands and techniques for debugging C programs.

Preparing Your Code for Debugging

Before you can effectively debug your C program with GDB, you need to compile it with debugging information. This is typically achieved by adding the -g flag to your compilation command:

gcc -g myprogram.c -o myprogram

The -g flag instructs the compiler to include extra information in the executable that GDB can use to map machine code back to your source code, including line numbers, variable names, and function names. Without this, debugging will be much harder.

Starting GDB

To start GDB, use the following command, replacing myprogram with the name of your executable:

gdb myprogram

This will launch GDB and load your program. You'll then see the GDB prompt: (gdb).

Basic GDB Commands

Here's a breakdown of essential GDB commands:

1. Breakpoints

Breakpoints allow you to pause the execution of your program at specific locations. This is crucial for inspecting the program's state at critical points.

  • break line_number: Sets a breakpoint at a specific line number in the current file.
    (gdb) break 15
  • break function_name: Sets a breakpoint at the beginning of a function.
    (gdb) break my_function
  • break filename:line_number: Sets a breakpoint at a specific line in a specific file.
    (gdb) break myprogram.c:22
  • info breakpoints: Lists all currently set breakpoints.
    (gdb) info breakpoints
  • delete breakpoint_number: Deletes a specific breakpoint. Get the breakpoint number from the info breakpoints command.
    (gdb) delete 1
  • disable breakpoint_number: Disables a specific breakpoint without deleting it.
    (gdb) disable 1
  • enable breakpoint_number: Enables a previously disabled breakpoint.
    (gdb) enable 1

2. Running and Stepping

These commands control the execution of your program.

  • run: Starts or restarts the program from the beginning. You can pass arguments to the program after the run command.
    (gdb) run
    (gdb) run arg1 arg2
  • continue: Continues execution from the current breakpoint.
    (gdb) continue
  • next: Executes the next line of code, stepping *over* function calls. If the next line is a function call, the entire function will execute and GDB will stop at the line *after* the function call.
    (gdb) next
  • step: Executes the next line of code, stepping *into* function calls. If the next line is a function call, GDB will jump into the first line of that function.
    (gdb) step
  • finish: Executes until the current function returns.
    (gdb) finish

3. Examining Variables

GDB allows you to inspect the values of variables during program execution.

  • print variable_name: Prints the value of a variable.
    (gdb) print my_variable
  • display variable_name: Prints the value of a variable after each step (next or step). Useful for tracking changes.
    (gdb) display my_variable
  • undisplay display_number: Removes a variable from the display list. Use info display to get the display number.
    (gdb) undisplay 1
  • ptype variable_name: Prints the data type of a variable.
    (gdb) ptype my_variable

4. Call Stack Inspection

The call stack shows the sequence of function calls that led to the current point in the program. This is extremely helpful for understanding how you arrived at a particular state, especially when dealing with nested function calls.

  • backtrace or bt: Prints the current call stack. Each line represents a frame in the stack, with the most recent call at the top.
    (gdb) backtrace
  • frame frame_number: Switches to a specific frame in the call stack. Use backtrace to determine the frame numbers. Once you switch frames, commands like print will operate in the context of that frame (e.g., accessing local variables of the function in that frame).
    (gdb) frame 2
  • up: Moves up one level in the call stack (to the caller function).
    (gdb) up
  • down: Moves down one level in the call stack (to the called function).
    (gdb) down

5. Exiting GDB

  • quit or q: Exits GDB.
    (gdb) quit

Example Scenario

Let's say you have the following C code:

 #include <stdio.h>

int add(int a, int b) {
    int sum = a + b;
    return sum;
}

int main() {
    int x = 5;
    int y = 10;
    int result = add(x, y);
    printf("The sum is: %d\n", result);
    return 0;
} 

And you suspect there might be an issue with the add function.

Here's how you might use GDB:

  1. Compile with debugging information: gcc -g myprogram.c -o myprogram
  2. Start GDB: gdb myprogram
  3. Set a breakpoint at the beginning of the add function: break add
  4. Run the program: run
  5. GDB will stop at the beginning of add. Now you can:
    • Print the values of a and b: print a, print b
    • Step through the function line by line: next
    • Print the value of sum: print sum
    • Continue execution: continue
  6. If you suspected the issue was in main, you could break at line 11 with break 11 and examine the value of `result` after the function call with print result

Tips for Effective Debugging

  • Read the Error Messages: Compiler and runtime error messages often provide valuable clues about the location and nature of the error.
  • Write Small, Testable Code: Break down your code into smaller, manageable functions or modules. This makes it easier to isolate and debug problems.
  • Use Assertions: Assertions (using the assert.h header) are a powerful way to check for conditions that should always be true at certain points in your code. If an assertion fails, the program will terminate and provide information about the failed condition.
  • Learn to Read Assembly: While not always necessary, understanding assembly language can be invaluable for debugging complex issues, especially when the source code is unavailable or the compiler is optimizing the code in unexpected ways. GDB can disassemble code with the `disassemble` command.
  • Practice, Practice, Practice: The more you use GDB, the more comfortable and efficient you'll become at finding and fixing bugs.