$npx -y skills add mohitmishra786/low-level-dev-skills --skill debug-optimized-buildsDebugging optimized builds skill for diagnosing issues in release code. Use when debugging RelWithDebInfo builds, using -Og for debuggable optimization, working with split-DWARF, applying GDB scheduler-locking, reading inlined frames, or understanding "value optimized out" messag
| 1 | # Debugging Optimized Builds |
| 2 | |
| 3 | ## Purpose |
| 4 | |
| 5 | Guide agents through debugging code compiled with optimization: choosing the right debug-friendly optimization level, reading inlined frames, diagnosing "value optimized out", using split-DWARF for faster debug builds, and applying GDB techniques specific to optimized code. |
| 6 | |
| 7 | ## Triggers |
| 8 | |
| 9 | - "GDB says 'value optimized out' — what does that mean?" |
| 10 | - "How do I debug a release build?" |
| 11 | - "How do I see inlined function frames in GDB?" |
| 12 | - "What's the difference between -O0 and -Og for debugging?" |
| 13 | - "How do I use RelWithDebInfo with CMake?" |
| 14 | - "Breakpoints in optimized code land on wrong lines" |
| 15 | |
| 16 | ## Workflow |
| 17 | |
| 18 | ### 1. Choose the right build configuration |
| 19 | |
| 20 | ``` |
| 21 | Goal? |
| 22 | ├── Full debuggability, no optimization |
| 23 | │ → -O0 -g (slowest, all vars visible) |
| 24 | ├── Debuggable, some optimization (recommended for most dev work) |
| 25 | │ → -Og -g (-Og keeps debug experience good) |
| 26 | ├── Release build with debug info (shipped, debuggable crashes) |
| 27 | │ → -O2 -g -gsplit-dwarf (or -O2 -g1 for lighter info) |
| 28 | └── Full release (no debug symbols) |
| 29 | → -O2 -DNDEBUG |
| 30 | ``` |
| 31 | |
| 32 | **`-Og`**: GCC's "debug-friendly optimization" — enables optimizations that don't interfere with debugging. Variables stay in registers where GDB can see them. Line numbers stay accurate. **Best balance for development.** |
| 33 | |
| 34 | ```bash |
| 35 | # GCC / Clang |
| 36 | gcc -Og -g -Wall main.c -o prog |
| 37 | |
| 38 | # CMake build types |
| 39 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug # -O0 -g |
| 40 | cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo # -O2 -g -DNDEBUG |
| 41 | cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # -O2 -DNDEBUG |
| 42 | ``` |
| 43 | |
| 44 | ### 2. "Value optimized out" — causes and workarounds |
| 45 | |
| 46 | ```text |
| 47 | (gdb) print my_variable |
| 48 | $1 = <optimized out> |
| 49 | ``` |
| 50 | |
| 51 | This means the compiler decided the variable's value doesn't need to be stored at this point — it might be: |
| 52 | - Kept only in a register (not the one GDB is looking at) |
| 53 | - Folded into a constant by constant propagation |
| 54 | - Eliminated because it's not used after this point |
| 55 | - Replaced by a later optimized value |
| 56 | |
| 57 | **Workarounds:** |
| 58 | |
| 59 | ```c |
| 60 | // 1. Mark variable volatile (prevents optimization away) |
| 61 | volatile int counter = 0; |
| 62 | // Use sparingly — changes semantics |
| 63 | |
| 64 | // 2. Use GCC attribute |
| 65 | int counter __attribute__((used)) = 0; |
| 66 | |
| 67 | // 3. Compile problematic TU at lower optimization |
| 68 | // In CMake: |
| 69 | set_source_files_properties(tricky.c PROPERTIES COMPILE_FLAGS "-O0") |
| 70 | |
| 71 | // 4. Use -Og instead of -O2 for the whole build |
| 72 | |
| 73 | // 5. Look at register values directly |
| 74 | // (gdb) info registers |
| 75 | // (gdb) p/x $rax # value may be in a register |
| 76 | ``` |
| 77 | |
| 78 | ### 3. Reading inlined frames in GDB |
| 79 | |
| 80 | With optimization, frequently-called small functions get inlined. GDB shows these as extra frames: |
| 81 | |
| 82 | ```text |
| 83 | (gdb) bt |
| 84 | #0 process_packet (data=0x7ff..., len=<optimized out>) |
| 85 | at network.c:45 |
| 86 | #1 0x0000... in dispatch_handler (pkt=0x7ff...) |
| 87 | at handler.c:102 |
| 88 | #2 (inlined by) event_loop () at main.c:78 |
| 89 | #3 0x0000... in main () at main.c:200 |
| 90 | |
| 91 | # (inlined by) frames are virtual — they show the call chain |
| 92 | # that was inlined into the actual frame above |
| 93 | ``` |
| 94 | |
| 95 | ```bash |
| 96 | # Navigate inlined frames |
| 97 | (gdb) frame 2 # jump to the inlined frame |
| 98 | (gdb) up # move up through frames (including inlined) |
| 99 | (gdb) down # move down |
| 100 | |
| 101 | # Show all frames including inlined |
| 102 | (gdb) backtrace full |
| 103 | |
| 104 | # Set breakpoint inside inlined function |
| 105 | (gdb) break network.c:45 # may hit multiple inlined call sites |
| 106 | (gdb) break process_packet # hits all inline expansions |
| 107 | ``` |
| 108 | |
| 109 | ### 4. Line number discrepancies |
| 110 | |
| 111 | Optimizers reorder instructions, so the "current line" in GDB may jump around: |
| 112 | |
| 113 | ```bash |
| 114 | # See which instructions map to which source lines |
| 115 | (gdb) disassemble /s function_name # interleaved source and asm |
| 116 | |
| 117 | # Step by machine instruction (more accurate in optimized code) |
| 118 | (gdb) si # stepi — one machine instruction |
| 119 | (gdb) ni # nexti — one machine instruction (no step into) |
| 120 | |
| 121 | # Show mixed source/asm at current point |
| 122 | (gdb) layout split # TUI mode: source + asm side by side |
| 123 | (gdb) set disassemble-next-line on |
| 124 | |
| 125 | # Jump to specific address (when line stepping is unreliable) |
| 126 | (gdb) jump *0x400a2c |
| 127 | ``` |
| 128 | |
| 129 | ### 5. GDB scheduler-locking for optimized multithreaded code |
| 130 | |
| 131 | With optimization, threads may race in unexpected ways when stepping: |
| 132 | |
| 133 | ```bash |
| 134 | # Lock the scheduler — only the current thread runs while stepping |
| 135 | (gdb) set scheduler-locking on |
| 136 | |
| 137 | # Modes: |
| 138 | # off — all threads run freely (default) |
| 139 | # on — only current thread runs while stepping |
| 140 | # step — only |