debug-optimized-builds · git:20260905.fd53e99 · 2026-09-05 · sha256 a9174b88c1f1b314

debug-optimized-builds git:20260905.fd53e99A

Immutable. This exact content is served forever at /api/v1/blob/a9174b88c1f1b314.

---
name: debug-optimized-builds
description: 'Use when debugging RelWithDebInfo or -O2 release builds, using -Og for debuggable optimization, split-DWARF, GDB scheduler-locking, reading inlined frames, or understanding "value optimized out".'
---

# Debugging optimized builds

## Contract

| Field | Bound contract |
|---|---|
| Trigger | GDB reports `<optimized out>`, breakpoints land on wrong lines, a release or RelWithDebInfo build needs debugging, inlined frames confuse the backtrace, or a debuggable optimized build needs configuring. |
| Authority | Read-only. Emits analysis and commands for the operator to run on the target; no file writes, no rollback needed. No remote mutation. |
| Side effect | Diagnostic commands and a verdict in chat. Nothing is written. |
| Done | The optimized-build obstacle is named, the workaround is applied or stated, and program state is observable at the needed point. |

## Inputs

1. Build configuration (required): the optimization and debug flags in use, or the CMake build type.
2. Symptom (required): `<optimized out>` values, wrong-line breakpoints, inlined frames, or a crash in a release binary.
3. Rebuild access (optional): needed when the fix is a different optimization level.

## Procedure

1. Pick the build configuration for the goal.

   | Goal | Flags |
   |---|---|
   | Full debuggability, no optimization | `-O0 -g` |
   | Debuggable with some optimization | `-Og -g` |
   | Release with debug info for crash analysis | `-O2 -g -gsplit-dwarf` |
   | Shipped binary, no symbols | `-O2 -DNDEBUG` |

   `-Og` enables the optimizations that do not interfere with debugging: variables stay where GDB can see them and line numbers stay accurate. GCC and Clang both accept it.

   ```bash
   gcc -Og -g -Wall main.c -o prog
   cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug          # -O0 -g
   cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo # -O2 -g -DNDEBUG
   cmake -S . -B build -DCMAKE_BUILD_TYPE=Release        # -O2 -DNDEBUG
   ```

   Done when: the flags match the goal.
2. Handle `<optimized out>`. The compiler decided the value needs no storage at this point: it lives only in a register, was folded to a constant, or is dead past this line. Workarounds:

   ```c
   volatile int counter = 0;                    // forces storage; changes semantics, use sparingly
   int counter2 __attribute__((used)) = 0;      // keeps the symbol
   ```

   ```cmake
   set_source_files_properties(tricky.c PROPERTIES COMPILE_FLAGS "-O0")
   ```

   Or read the value where it lives: `info registers`, then `p/x $rax`. Rebuilding the one translation unit at `-O0` or the whole build at `-Og` is cleaner than `volatile`. Done when: the value is recovered or the variable is made observable.
3. Read inlined frames. With optimization, GDB lists inlined calls as their own frames in `bt`; they show the call chain that was folded into the real frame.

   ```gdb
   (gdb) bt
   (gdb) frame 2                 # select the inlined frame
   (gdb) up / down               # move through real and inlined frames
   (gdb) break process_packet    # hits every inline expansion of the function
   (gdb) break network.c:45      # may resolve to several inlined call sites
   ```

   Done when: the real call chain is reconstructed.
4. Cope with line drift. Optimizers reorder instructions, so the reported line jumps.

   ```gdb
   (gdb) disassemble /s function_name   # source interleaved with asm
   (gdb) si / ni                        # step one instruction
   (gdb) layout split                   # TUI: source and asm side by side
   (gdb) set disassemble-next-line on
   (gdb) jump *0x400a2c                 # resume at an address when line stepping lies
   ```

   Done when: execution position is tracked at instruction level.
5. Lock the scheduler for multithreaded optimized code. Other threads racing ahead during a step hide the bug.

   ```gdb
   (gdb) set scheduler-locking step   # only the current thread steps; all run on continue
   (gdb) set scheduler-locking on     # only the current thread runs at all
   (gdb) set scheduler-locking off    # default: all threads run freely
   ```

   `replay` locks only during reverse execution. Done when: stepping is deterministic.
6. Use split DWARF for faster debug builds. `-gsplit-dwarf` moves debug info into `.dwo` sidecar files, so the linker never sees it.

   ```bash
   gcc -g -gsplit-dwarf -O2 -c file.c -o file.o   # makes file.o plus file.dwo
   gcc -g -gsplit-dwarf file.o -o prog            # binary references, not embeds, DWARF
   gdb prog                                       # finds .dwo next to the binary
   dwp -o prog.dwp prog                           # package .dwo files into one .dwp
   ```

   CMake: `add_compile_options(-gsplit-dwarf)`. Done when: link input shrinks and GDB still resolves symbols.
7. Inspect state when variable info is gone.

   ```gdb
   (gdb) info locals / info args        # may print <optimized out>
   (gdb) call (int)my_func(42)          # evaluate by calling the real function
   (gdb) watch *0x7fffffffe430          # watch an address, not a name
   (gdb) x/10xw $rsp                    # raw memory
   (gdb) bt                             # addresses still resolve without symbols
   (gdb) info sharedlibrary             # loaded libraries for symbol resolution
   ```

   Done when: program state is read despite missing variable info.

## Failure and recovery

- `<optimized out>` on the exact variable needed: rebuild that translation unit at `-O0`, or rebuild at `-Og`. Do not sprinkle `volatile` through the codebase for the debugger's sake.
- Breakpoint never hits: the line was optimized away or inlined. Break on the function name or on an address from `disassemble /s`.
- Stepping changes the bug: enable `set scheduler-locking step` and retry.
- `.dwo` files missing after a move: GDB cannot resolve split debug info. Keep `.dwo` files beside the objects, or package them with `dwp`.
- LTO builds lose still more info: see the LTO section of `dwarf-debug-format` for what survives.

## Output

A working debug configuration for the optimized build, the recovered program state, and the named cause of each observability loss.