Debugging with gdb, sanitizers and valgrind

Breakpoints, watchpoints, core dumps, and the sanitizers that find in seconds what a debugger finds in an hour.

Driving gdb

gcc -std=c17 -g -O0 -Wall app.c -o app
gdb --args ./app input.txt

(gdb) break app.c:42          # breakpoint at a line
(gdb) break sum if n > 1000   # conditional: only when the condition holds
(gdb) run
(gdb) print arr[0]@10         # print 10 elements starting at arr[0]
(gdb) watch counter           # stop whenever the value changes
(gdb) info locals
(gdb) info args
(gdb) frame 2                 # switch to a stack frame
(gdb) backtrace full
(gdb) disassemble /m main
(gdb) x/8xw buffer            # examine 8 hex words in memory
(gdb) print sizeof(struct Node)
CommandPurpose
next / stepStep over / step into a call
finishRun until the current function returns
until 60Run until a later line in this frame
continueResume to the next breakpoint
watch / rwatchStop when a location is written / read
display xPrint an expression after every step
set var i = 0Change a variable and retry the code path

A watchpoint is the fastest way to find who corrupts a variable: set it on the field that goes wrong and gdb stops at the exact store instruction.

Core dumps and post-mortem debugging

ulimit -c unlimited                      # allow core files
cat /proc/sys/kernel/core_pattern         # where they land

gdb ./app core
(gdb) backtrace
(gdb) frame 0
(gdb) info registers
(gdb) print *(struct Conn *)arg

Production binaries should be built with -g and the symbols archived per release. Without them a core dump tells you an address and nothing else, which is exactly when you need it most.

Sanitizers and valgrind

# AddressSanitizer + UndefinedBehaviorSanitizer: compile-time, fast (~2x slower)
cc -std=c17 -g -O1 -fsanitize=address,undefined -fno-sanitize-recover=all app.c -o app

# LeakSanitizer is part of ASan on Linux
ASAN_OPTIONS=detect_leaks=1 ./app

# ThreadSanitizer: data races (not compatible with ASan)
cc -std=c17 -g -O1 -fsanitize=thread app.c -o app

# valgrind: no recompile needed, ~20x slower
valgrind --tool=memcheck --leak-check=full --track-origins=yes ./app
valgrind --tool=helgrind ./threaded_app
valgrind --tool=callgrind ./app && callgrind_annotate callgrind.out.*
  • ASan cannot coexist with TSan in one binary; build separate binaries or use separate CI jobs.
  • ASan needs to intercept allocators, so it must be linked into the final executable, not only the objects.
  • TSan reports only races it actually observes. A clean run reduces the probability of a race but does not prove its absence.
  • -fsanitize=undefined with -fno-sanitize-recover=all turns undefined behaviour into an immediate abort with a file and line.
💡
Run the sanitisers in continuous integration on every commit. A memory bug reported the day it is introduced costs minutes; the same bug found in production costs a debugging session with no reproduction.

FAQ

Do sanitizers change what the program does?
They add instrumentation and change memory layout, so a bug that only manifests under a particular heap layout can disappear. Use them alongside the debugger, not as a replacement, and always keep a clean non-instrumented build.
My program crashes only in release mode. Why?
Undefined behaviour is most likely, because the optimiser is entitled to assume it does not happen. Rebuild the release configuration with -fsanitize=undefined and -O2 together to reproduce it.

Dynamic memory patterns and allocation bugs Portability, standards and disciplined C

Last refreshed 2026-09-18.