What are GDB Debugging Setup Basics-Free Embedded Linux Course In Hyderabad

PREV_LEC | NEXT_LEC

GDB Debugging Setup Basics

Compile with the right debug flags before you ever touch a breakpoint — a free embedded Linux development course lecture

Every embedded developer eventually hits a bug that print statements can’t explain — a crash three function calls deep, a variable that mysteriously changes, a race that only shows up on target hardware. This is where a real debugger earns its keep. In this free linux kernel development course lecture we start the GDB (GNU Debugger) chapter by covering the one step almost everyone skips: preparing your build so GDB can actually help you. Get this wrong and GDB will lie to you — showing the wrong line, losing local variables, or refusing to step through code at all.

GDB is a source-level debugger, mainly for C and C++, and it is the backbone of debugging in any free embedded systems course that covers real target hardware. It lets you set breakpoints, step through code, inspect variables, walk the call stack, and even open up a crashed program’s core file after the fact. Later lectures in this series cover remote debugging with gdbserver, the full GDB command set, and core file analysis — but none of that works reliably unless the binary itself was built correctly.

Keywords

GDB debug symbols -g -ggdb compiler optimization frame pointers free linux device drivers course

What You Will Learn

  • Why GDB needs debug symbols and how GCC generates them
  • The difference between -g and -ggdb, and the four debug info levels (0-3)
  • How compiler optimization silently breaks single-stepping and how to fix it
  • What frame pointers are and why GDB needs them for backtraces
  • A hands-on build showing “good” vs “broken” debug builds side by side

Prerequisites

  • A Linux host with GCC and GDB installed (sudo apt install gcc gdb on Debian/Ubuntu)
  • Basic familiarity with compiling C programs from the command line
  • No prior GDB experience needed — this lecture starts from zero

Why Debug Symbols Matter

A compiled binary is just machine instructions and data — by default it carries no memory of which line of source produced which instruction, what a local variable was called, or where a function’s stack frame begins. GDB can technically debug a binary without any of this, but all it can show you is raw addresses and registers, which is close to useless for everyday work. Debug symbols are the missing map: a side table baked into the binary (or split out separately) that lets GDB translate “address 0x401136” back into “line 42 of sensor.c, inside function read_temperature(), with local variable raw_adc equal to 512”.

You generate this map at compile time, and free embedded linux course material often glosses over exactly how much control you have over its detail level — which is exactly what trips people up on real target boards where storage and load time both matter.

The -g and -ggdb Compiler Flags

GCC gives you two switches for embedding debug information: -g and -ggdb. The distinction is about the format of the emitted metadata:

FlagWhat it doesWhen to prefer it
-gEmits debug info in a format appropriate to the target OS (DWARF on Linux)Portable across debuggers/toolchains — the default sensible choice
-ggdbEmits GDB-specific extensions on top of the standard formatYou know you’re only ever debugging with GDB and want maximum detail

On a Linux target, both ultimately produce DWARF debug info, so in practice the difference is small. What matters far more is the level you request, appended as a digit:

LevelDebug info included
-g0None at all — identical to omitting -g
-g1Function names and external variables only — enough for a backtrace, not for source-level stepping
-g2 (default when you write plain -g)Adds local variables and line numbers — full source-level stepping
-g3Adds macro definitions on top of level 2 — needed if you want to inspect #define macros while stepping

For nearly all day-to-day work, plain -g (level 2) is what you want. Reach for -g3 or -ggdb3 only when you’re chasing a bug that involves macro expansion and you need GDB to understand the macro, not just its expanded form.

How Optimization Breaks Debugging

Compiler optimization reorders instructions, inlines functions, and eliminates variables that a human would consider “in scope” but the compiler has proven are dead. All of this is great for runtime performance and terrible for a debugger trying to line up machine instructions with source lines. If you find GDB skipping lines, showing “optimized out” for a variable you clearly declared, or stepping in an order that makes no sense, optimization is almost always the cause.

The fix during development is straightforward: compile without optimization (-O0, i.e. no -O flag at all) or at most -O1. Save -O2/-O3 for your release build once the logic is verified.

Frame Pointers and Backtraces

GDB’s backtrace command — showing you the chain of function calls that got you to the current line — relies on stack frame pointers. On some architectures, GCC drops frame pointers at -O2 and above as a minor performance optimization, which can leave GDB unable to reconstruct a sensible call stack. If you must ship with higher optimization but still need reliable backtraces (common in field-deployed embedded systems where you only get a crash report after the fact), add -fno-omit-frame-pointer to override that behavior. The opposite situation exists too: hand-tuned assembly or aggressively optimized code sometimes explicitly strips frame pointers with -fomit-frame-pointer — worth checking for if backtraces mysteriously stop working on code that used to debug fine.

Hands-On: Comparing a Debug Build and a Broken Build

Let’s build the same tiny program two ways and see the practical difference. Save this as ep_counter.c:

#include <stdio.h>

static int ep_double(int value)
{
    int result = value * 2;
    return result;
}

int main(void)
{
    int count = 0;

    for (count = 1; count <= 3; count++) {
        int doubled = ep_double(count);
        printf("count=%d doubled=%d\n", count, doubled);
    }

    return 0;
}

First, build it the way you’d want during development:

$ gcc -g -O0 -o ep_counter_debug ep_counter.c
$ gdb ./ep_counter_debug
(gdb) break ep_double
Breakpoint 1 at 0x1155: file ep_counter.c, line 5.
(gdb) run
Starting program: ./ep_counter_debug

Breakpoint 1, ep_double (value=1) at ep_counter.c:5
5           int result = value * 2;
(gdb) print value
$1 = 1
(gdb) backtrace
#0  ep_double (value=1) at ep_counter.c:5
#1  0x0000555555555184 in main () at ep_counter.c:14

Notice GDB knows the exact source line, the argument value, and the full call chain back to main(). Now build the same source optimized, without debug info:

$ gcc -O2 -o ep_counter_release ep_counter.c
$ gdb ./ep_counter_release
(gdb) break ep_double
Function "ep_double" not defined.

GDB can’t even find the function by name — at -O2 without -g, ep_double() was likely inlined away entirely, and there is no symbol table to look it up in even if it existed as a separate function. This is the exact failure mode that sends people down a rabbit hole of “GDB is broken” when the real issue is the build flags.

Common Mistakes and Troubleshooting

  • Debugging a release-optimized binary — always keep a separate debug build (-g -O0) for active development.
  • Assuming -g alone disables optimization — it doesn’t. You must separately control the -O level.
  • “Optimized out” variables — a sure sign the binary was built with an optimization level higher than the debug info can faithfully represent; rebuild with -O0 or add -fno-omit-frame-pointer if you need to keep optimization on.
  • Stripped release binaries with no debug info left at all — debug symbols can also be stripped after the fact with strip; more on this trade-off in a later lecture on remote debugging.

Best Practices

  • Maintain two build configurations: a debug build (-g -O0) for development and a release build for production.
  • Default to -g (level 2); only escalate to -g3 when you specifically need macro visibility.
  • If you must debug an optimized build, add -fno-omit-frame-pointer so backtraces remain trustworthy.
  • Keep the debug-info binary around even after stripping a copy for deployment — you’ll need it to symbolicate crashes later.

Summary and Key Takeaways

  • GDB needs debug symbols, generated with -g or -ggdb, to map machine code back to source.
  • Debug info has levels 0-3; level 2 (plain -g) is the everyday default, level 3 adds macro support.
  • Compiler optimization above -O1 can break single-stepping and eliminate variables/functions from the debug view.
  • Frame pointers, sometimes dropped at -O2, are what make backtrace reliable — restore them with -fno-omit-frame-pointer if needed.

Conclusion

Getting GDB working well starts long before you type gdb at the prompt — it starts at compile time. A correctly built debug binary turns GDB into a precise, trustworthy tool; a mismatched build makes it look broken even though the debugger itself is doing exactly what it was told. With that foundation in place, the next lecture in this free linux device drivers course moves on to the scenario every embedded developer eventually faces: debugging code running on target hardware from a cross-development host, using gdbserver.

FAQ

Do I need -g on every build, even release builds?

No. Ship release builds without -g (or strip the debug info afterward) to keep binary size down; keep a matching debug-info build archived separately so you can symbolicate crashes if needed.

What’s the practical difference between -g and -ggdb on Linux?

Very little in practice — both produce DWARF debug info on Linux. -ggdb can include a few GDB-specific extensions, but for everyday work they’re interchangeable.

Why did my breakpoint on a function silently vanish?

The function was almost certainly inlined by the optimizer. Rebuild with -O0, or use -fno-inline if you must keep some optimization on.

Is -g3 always better than -g2?

Not necessarily — it increases binary size for macro debug info you may never use. Reserve it for sessions where you specifically need to inspect macro expansions.

Can I add debug symbols to an already-compiled binary?

Not meaningfully — debug info has to be generated by the compiler alongside the code. You must recompile with the right flags.

Does optimization level affect debug symbol accuracy, or just presence?

Both. Even when debug info is present, higher optimization can make line numbers, variable values, and call stacks less reliable due to instruction reordering and inlining.

What is a frame pointer in simple terms?

A register that always points to the current function’s stack frame, letting the debugger walk backward through the chain of callers. Some optimizations repurpose that register for other uses, breaking the chain.

Continue the Free Linux Kernel Development Course

Next up: setting up remote debugging with gdbserver for real target hardware.

Browse All Lectures Join EmbeddedPathashala
PREV_LEC | NEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *