Table of Contents
GCC Debugging
Debug symbols (-g flag) embed source file names, line numbers, and variable information into the binary. GDB uses these to map machine instructions back to source code.
gcc -g -o app app.c # include debug symbols gcc -g -O0 -o app app.c # debug build (no optimization) gcc -g -O3 -o app app.c # optimized with debug symbols (can work)
Always use -g when compiling for debugging. The flag doesn't increase runtime overhead—it only adds metadata to the binary. You can remove it later with strip.
Debug symbols and optimization: At -O0, line numbers and variable locations are precise. At -O3, the compiler reorders code and optimizes variables away, so line number attribution becomes approximate. For serious debugging, use -O0 -g. For production debugging of optimized code, accept that stepping through source may jump around unexpectedly.
DWARF versions: -gdwarf-4 (default on GCC 10+) uses DWARF version 4. Older tools may need -gdwarf-3. Unless you have compatibility issues, use the default.
Separate debug symbols:
gcc -g -o app app.c objcopy --only-keep-debug app app.debug objcopy --strip-debug app app_stripped objcopy --add-gnu-debuglink=app.debug app_stripped
This separates debug info from the binary, reducing binary size while keeping debuggability. The app.debug file can be stored separately.
Testing with gdb:
gcc -g -O0 -o app app.c gdb ./app (gdb) break main (gdb) run (gdb) next (gdb) print variable_name
With debug symbols, gdb shows function names, file names, line numbers, and can print variable values. Without -g, you only get memory addresses and registers.
Core dumps: When a program crashes, the OS can save a core dump (memory snapshot). Debug with it using gdb:
ulimit -c unlimited # enable core dumps ./app # program crashes, produces core gdb ./app ./core # debug the core dump (gdb) bt # backtrace: where did it crash? (gdb) print variable_at_crash
Core dumps let you debug crashes that happened earlier, without re-running the program.
Address Sanitizer: Detect memory errors at runtime (similar to Valgrind but faster):
gcc -fsanitize=address -g -O1 -o app app.c ./app # reports memory errors: use-after-free, buffer overflow, etc.
-fsanitize=address adds instrumentation to catch memory bugs. It's slower than native execution but faster than Valgrind. -O1 is recommended (optimizations make debugging harder, but -O0 adds more overhead).
