Published on

Debugging C Programs Effectively with GDB Cheatsheet Part 6 of 6 Advanced C Topics

Authors

Writing C code can be incredibly rewarding, offering power and control. However, with great power comes great responsibility... and inevitably, bugs! Segmentation faults, subtle memory leaks, off-by-one errors, and complex pointer logic can turn debugging C programs into a challenging quest. Fear not, for developers have a powerful ally: GDB, the GNU Debugger. This indispensable command-line tool allows you to peer inside your running program, understand its state, and systematically hunt down those elusive errors. This guide will walk you through the essential GDB commands and techniques to debug your C programs effectively.

Table of Contents

Why Debugging is Crucial (Especially in C)

Unlike languages with more built-in safeguards, C gives you direct access to memory, which is powerful but also prone to errors like:

  • Segmentation Faults: Accessing memory you shouldn't (e.g., dereferencing NULL or dangling pointers, buffer overflows).
  • Memory Leaks: Forgetting to free allocated memory.
  • Pointer Errors: Incorrect pointer arithmetic or usage.
  • Logic Errors: Flaws in the program's algorithm or flow control.

Simple printf statements can only get you so far. A debugger like GDB lets you pause execution, inspect the program's state precisely when things go wrong, and understand the sequence of events leading to the error.

Compiling Your Code for Debugging

Before you can effectively use GDB, you need to compile your C program with debugging symbols included. These symbols connect the compiled machine code back to your original source code (function names, variable names, line numbers).

Use the -g flag with your compiler (like GCC):

gcc -g my_program.c -o my_program

It's also highly recommended to compile with no optimization (-O0) or low optimization (-O1) during debugging. Higher optimization levels (-O2, -O3) can significantly rearrange code, inline functions, and optimize away variables, making the execution flow harder to follow in the debugger and potentially masking the original location of bugs.

gcc -g -O0 my_program.c -o my_program # Ideal for debugging

Starting and Quitting GDB

To start debugging your compiled program, simply run:

gdb ./my_program

You'll be greeted with the GDB prompt: (gdb). All commands are entered here.

To exit GDB at any time, type quit or simply q.

(gdb) quit

Running Your Program Inside GDB

To execute your program within the debugger, use the run command (or its shorthand r):

(gdb) run

If your program requires command-line arguments, provide them after run:

(gdb) run argument1 "some argument with spaces" 123

Controlling Execution: Breakpoints

Breakpoints tell GDB where to pause program execution.

Setting Breakpoints: Use the break command (shorthand b). You can specify locations in several ways:

  • By function name: break main
  • By line number in the current file: break 42
  • By file and line number: break my_other_file.c:100
  • By file and function name: break my_other_file.c:my_function
  • By address (less common): break *0x4005a0
(gdb) b main
Breakpoint 1 at 0x1149: file my_program.c, line 5.
(gdb) b my_program.c:25
Breakpoint 2 at 0x11a0: file my_program.c, line 25.

Listing Breakpoints: See all active breakpoints, their numbers, and status:

(gdb) info breakpoints
# Shorthand: i b

Managing Breakpoints:

  • Delete a breakpoint: delete <breakpoint_number> (e.g., delete 1)
  • Delete all breakpoints: delete
  • Disable a breakpoint (keep it but ignore it): disable <breakpoint_number>
  • Enable a disabled breakpoint: enable <breakpoint_number>

Conditional Breakpoints: Pause only if a specific condition is true. This is incredibly useful for debugging loops or specific scenarios.

  • Set directly: break <location> if <condition>
    (gdb) break my_program.c:50 if i == 100
    
  • Add condition to existing breakpoint: condition <breakpoint_number> <condition>
    (gdb) b 65
    Breakpoint 3 at 0x...: file my_program.c, line 65.
    (gdb) condition 3 count > 5 && strcmp(name, "test") == 0
    
  • Remove condition: condition <breakpoint_number> (with no condition specified)

Stepping Through Code

Once the program stops at a breakpoint, you can execute it line by line:

  • next (shorthand n): Execute the current line. If the line contains a function call, execute the entire function without stepping into it (step over).
  • step (shorthand s): Execute the current line. If the line contains a function call, step into that function and stop at its first line.
  • finish: Continue execution until the currently executing function returns. Useful if you accidentally stepped into a function you didn't care about.
  • continue (shorthand c): Resume normal execution until the next breakpoint is hit, a signal occurs (like a crash), or the program finishes.
  • until <line_number>: Continue execution within the current stack frame until the specified line number is reached or the frame is exited. Useful for getting out of loops quickly.

Repeating Commands: Pressing Enter without typing a command usually repeats the last command executed (very handy for repeatedly stepping with n or s).

Inspecting Data and Memory

Understanding the state of your variables is key to debugging.

Printing Values:

  • print <expression> (shorthand p): Evaluates and prints the value of a variable or a C expression.
    (gdb) p my_variable
    $1 = 10
    (gdb) p count * 2 + offset
    $2 = 45
    (gdb) p *pointer_var
    $3 = {name = 0x.... "Example", value = 123}
    (gdb) p array[i]
    $4 = 'X'
    
  • You can specify output formats: p/x my_variable (hex), p/t my_variable (binary), p/d my_variable (decimal), p/c my_variable (char).

Automatic Display:

  • display <expression>: Automatically prints the value of the expression every time the program stops (e.g., after stepping or hitting a breakpoint).
    (gdb) display i
    1: i = 0
    (gdb) display /x flags
    2: flags = 0x1a
    
  • info display: List automatically displayed expressions.
  • undisplay <display_number>: Stop automatically displaying an expression.

Variable Information:

  • info locals: Show the names and values of all local variables in the current function's stack frame.
  • info args: Show the names and values of the arguments passed to the current function.
  • whatis <expression>: Show the data type of a variable or expression.

Examining Memory Directly: Use the x (examine) command: x/<nfu> <address>

  • n: Repeat count (how many units to display). Default 1.
  • f: Format character (like print): x (hex), d (decimal), u (unsigned decimal), o (octal), t (binary), a (address), c (char), f (float), s (string), i (instruction).
  • u: Unit size: b (byte), h (halfword, 2 bytes), w (word, 4 bytes), g (giant word, 8 bytes).
(gdb) x/4xw $rsp   # Examine 4 words (w) in hex (x) starting at stack pointer
0x7fffffffdc10:	0x00000001	0x00000000	0x7fffffffdcf8	0x00000000
(gdb) x/s buffer   # Examine memory at address 'buffer' as a string (s)
0x404040:	"Hello, GDB!"
(gdb) x/12cb &my_struct # Examine 12 bytes (b) as chars (c) at struct address
0x...:	72 'H'	101 'e'	108 'l'	108 'l'	111 'o'	0 '000' 123 '{'	0 '000'

When your program is stopped, especially after a crash or inside nested function calls, you need to know how it got there.

  • backtrace (shorthand bt): Prints the function call stack. Frame 0 is the current function, Frame 1 is the function that called it, and so on.
    (gdb) bt
    #0  my_function (arg=10) at my_program.c:55
    #1  0x00005555555552a0 in helper_function (ptr=0x7fffffffdc10) at my_program.c:78
    #2  0x00005555555551c0 in main (argc=1, argv=0x7fffffffdd08) at my_program.c:25
    
  • bt full: Shows the backtrace along with local variables for each frame.
  • frame <frame_number>: Select a specific stack frame. Commands like print, info locals, info args will then operate within the context of that frame.
  • up <n>: Move n frames up the stack (towards main).
  • down <n>: Move n frames down the stack (towards the currently executing function).

More Advanced GDB Techniques

  • Watchpoints: Stop execution when the value of an expression changes, regardless of where it happens in the code. This is extremely powerful for tracking down unexpected variable modifications.
    • watch <expression>: Break when expression is written to and its value changes.
    • rwatch <expression>: Break when expression is read.
    • awatch <expression>: Break when expression is read OR written to.
    • info watchpoints: List active watchpoints.
    • Note: Hardware watchpoints (faster) are limited in number. GDB may fall back to slower software watchpoints if hardware resources are exceeded or not supported for the expression type.
  • Catchpoints: Stop on specific events, like C++ exceptions (catch throw), loading libraries (catch load), or system calls (catch syscall write).
  • Attaching to a Running Process: If a program is already running (and perhaps stuck), you can attach GDB to it without restarting.
    1. Find the Process ID (PID) using ps aux | grep my_program or pgrep my_program.
    2. Run gdb attach <pid>.
    3. You might need permission; on some Linux systems, you may need to adjust ptrace_scope (e.g., echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope). Use with caution.
  • Analyzing Core Dumps: If your program crashes and generates a "core dump" file (containing the program's memory state at the time of the crash), you can analyze it post-mortem.
    1. Ensure core dumps are enabled: ulimit -c unlimited (in your shell before running the program).
    2. Run GDB: gdb ./my_program core (where core is the core dump file, often named just core).
    3. Use commands like bt, frame, print, info locals, x to examine the state at the time of the crash. You cannot use execution commands like run, next, step.
  • Text User Interface (TUI): Provides a split-screen view in the terminal, often showing source code, assembly, registers, or command history alongside the GDB prompt.
    • Start with: gdb -tui ./my_program
    • Toggle TUI on/off: Ctrl+X A
    • Cycle layouts: Ctrl+X 2 (show source/assembly + command), Ctrl+X 1 (show source/assembly only).
    • Use layout src, layout asm, layout regs to control windows. Scroll windows using Ctrl+P/Ctrl+N or arrow keys if focus is correct (sometimes needs Ctrl+X O to switch focus).
  • .gdbinit File: Create a file named .gdbinit in your home directory or project directory to store custom commands or settings that run automatically when GDB starts.

Conclusion

GDB is an incredibly powerful tool for C development. While its command-line interface might seem daunting initially, mastering the fundamental commands for setting breakpoints, controlling execution flow, inspecting variables and memory, and analyzing the call stack will drastically improve your ability to find and fix bugs. Don't shy away from exploring conditional breakpoints, watchpoints, core dump analysis, and the TUI mode as you become more comfortable. Effective debugging is a critical skill, and GDB is your essential companion in the C programming landscape.