- Published on
Debugging C Programs Effectively with GDB Cheatsheet Part 6 of 6 Advanced C Topics
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
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
(shorthandn
): Execute the current line. If the line contains a function call, execute the entire function without stepping into it (step over).step
(shorthands
): 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
(shorthandc
): 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>
(shorthandp
): 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 (likeprint
):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'
Navigating the Call Stack
When your program is stopped, especially after a crash or inside nested function calls, you need to know how it got there.
backtrace
(shorthandbt
): 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 likeprint
,info locals
,info args
will then operate within the context of that frame.up <n>
: Moven
frames up the stack (towardsmain
).down <n>
: Moven
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.
- Find the Process ID (PID) using
ps aux | grep my_program
orpgrep my_program
. - Run
gdb attach <pid>
. - 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.
- Find the Process ID (PID) using
- 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.
- Ensure core dumps are enabled:
ulimit -c unlimited
(in your shell before running the program). - Run GDB:
gdb ./my_program core
(wherecore
is the core dump file, often named justcore
). - Use commands like
bt
,frame
,print
,info locals
,x
to examine the state at the time of the crash. You cannot use execution commands likerun
,next
,step
.
- Ensure core dumps are enabled:
- 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 usingCtrl+P
/Ctrl+N
or arrow keys if focus is correct (sometimes needsCtrl+X O
to switch focus).
- Start with:
.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.