The first time GDB feels genuinely useful is not when it prints a prompt. It is when a crash that looked random becomes one source line, one call path, and one value that could never have worked. We will deliberately break a tiny C program, follow the evidence, repair it, and then move beyond run and quit.
Install the tools and record their versions
sudo apt update
sudo apt install build-essential gdb
gdb --version
gcc --versionGNU gdb (Ubuntu ...)
gcc (Ubuntu ...) ...Why versions belong in a bug report
build-essentialsupplies GCC and the standard native build tools.gdb --versionidentifies debugger features and behavior available on that Ubuntu release.Installation changes system packages but does not modify the project source.
Exact version output varies across supported Ubuntu releases.
Create a crash worth investigating
#include <stdio.h>
static void update_score(int *score)
{
*score += 10;
}
int main(void)
{
int *score = NULL;
puts("updating score");
update_score(score);
printf("score: %d\n", *score);
return 0;
}A small, reproducible null-pointer failure across two stack frames.
The defect is small but realistic
scoreis a null pointer, not storage for an integer.update_score()dereferences its parameter for both a read and a write.Dereferencing NULL is undefined behavior and normally raises
SIGSEGVon Ubuntu.The helper creates a second frame, so GDB can show where the bad value came from.
Compile for a useful debugging session
gcc -g3 -Og -Wall -Wextra -Wpedantic crash.c -o crash
file crash
./crashcrash: ELF 64-bit ... with debug_info, not stripped
updating score
Segmentation fault (core dumped)Those flags are deliberate
-g3emits source-level debug information plus macro definitions.GCC recommends
-Ogfor the edit-compile-debug cycle because it preserves useful debug tracking.Warning flags can expose suspicious C before the program runs.
A successful compile proves syntax and linking, not memory safety.
Catch the signal and read the stack
gdb --quiet ./crash
(gdb) run
(gdb) backtrace
(gdb) frame 0
(gdb) info args
(gdb) info locals
(gdb) print scoreProgram received signal SIGSEGV, Segmentation fault.
update_score (score=0x0) at crash.c:5
#0 update_score (score=0x0) at crash.c:5
#1 main () at crash.c:12
$1 = (int *) 0x0Turn the transcript into a causal story
runstarts the inferior—the program controlled by GDB.backtrace, orbt, prints frame zero at the stop point followed by callers.frame 0selects the crashing function for argument and local-variable inspection.score=0x0connects the faulting dereference to the value passed bymain.
Stop before the invalid access
(gdb) break update_score
(gdb) run
(gdb) list
(gdb) print score
(gdb) up
(gdb) list
(gdb) print score
(gdb) nextBreakpoint 1, update_score (score=0x0) at crash.c:5
$1 = (int *) 0x0
#1 main () at crash.c:12Navigate without losing context
break update_scorestops at function entry before the dereference.listshows nearby source andprintevaluates a C expression in the selected frame.upselects the caller and reveals where NULL originated.nextsteps over calls; usestepwhen entering a called function is useful.
Fix the ownership error
int main(void)
{
int score = 0;
puts("updating score");
update_score(&score);
printf("score: %d\n", score);
return 0;
}What changed in memory
scoreis now an actual integer object in themainstack frame.&scorepasses a valid address to the helper.That pointer remains valid until
mainreturns.The caller prints the integer directly instead of dereferencing a nullable pointer.
Verify the fix reproducibly
gcc -g3 -Og -Wall -Wextra -Wpedantic crash.c -o crash
gdb --quiet --batch -ex 'break update_score' -ex run -ex 'print *score' -ex continue ./crashBreakpoint 1, update_score (...) at crash.c:5
$1 = 0
score: 10
Inferior 1 exited normallyA debugger session can be a regression check
--batchexits after scripted commands instead of waiting at a prompt.Each
-exexecutes one GDB command in order.print *scoreproves the pointer references readable integer storage before the update.Normal exit verifies this path and input, not every possible use of the function.
Watch data change
(gdb) break main
(gdb) run
(gdb) next
(gdb) watch score
(gdb) continueHardware watchpoint 2: score
Old value = 0
New value = 10Watchpoints ask a different question
A breakpoint asks where execution arrives; a watchpoint asks when an expression changes.
Hardware watchpoint capacity is limited by the CPU and target.
The variable must be in scope when the watchpoint is created.
This technique is valuable when state becomes corrupt long before a later crash.
When GDB output looks incomplete
No debugging symbols found: rebuild with
-gand use the executable that exactly matches the failure.Value optimized out: reproduce with
-Og, or inspect assembly and registers in the optimized build.Corrupt backtrace: stack memory may be overwritten, symbols may mismatch, or unwind information may be unavailable.
Cannot attach: ptrace policy, process ownership, or container capabilities may deny access.
Crash disappears under GDB: timing, uninitialized state, or a race may be involved; preserve core dumps and inputs.
Primary references
Current GDB manual documents execution, breakpoints, frames, values, and watchpoints.
GDB backtrace reference explains stack ordering and full-frame output.
GCC debugging options explains debug information and the
-Ogrecommendation.
Comments and corrections