A successful write can look like a failed write.
Daniel Mangum provisioned two key slots on an nRF54LM20 and pushed each value to the same memory address while the CPU was halted. GDB showed the first value after the second push. A direct read through the J-Link access port showed the second value.[1]
The peripheral write worked. The normal debugger read returned a cached value. Both displays looked precise. Only one described current memory.
The pretty pane is the end of a chain.
VS Code documents that debugger extensions decide which launch settings, breakpoint types, expression syntax, and remote features they support.[2] A variable view therefore depends on more than the editor. It depends on the adapter, debug server, transport, target, and compiler information beneath it.
Do not ask only, "What value does the debugger show?" Ask, "Which path produced this value?"
Mangum's case crossed GDB, the Remote Serial Protocol, JLinkGDBServer, a memory cache, an Arm access port, and a peripheral that could update memory while the CPU stayed halted. Advancing the CPU invalidated the cache. The next ordinary read became fresh.[1]
Stale memory is one failure class.
Optimized code creates another. GDB's manual says optimized assembly can diverge from the source. Variables may move, disappear, or never exist in the generated program. The debugger maps the running program back to source with compiler debug information, but that map is less direct under optimization.[3]
Do not collapse these cases. A stale backend cache, an optimized-out variable, a hollow breakpoint, and a value from the wrong stack frame need different tests.
Make the transport visible.
GDB can log remote serial communication to a file and change the log radix. Its remote configuration also exposes packet controls for the active target.[4] Mangum enabled remote debugging and confirmed that the normal memory command and the direct access-port command took different packet routes.[1]
That trace turned a vague suspicion into a small experiment. The target address stayed fixed. The target state stayed fixed. The read path changed. The returned value changed with it.
Run a two-witness drill.
- Record the exact target state. Include halted or running, core, thread, stack frame, address, build, and optimization level.
- Repeat the read without changing target state. Save the value and route.
- Read the same storage through an independent path. Use a direct access port, target-side print, second probe, or post-stop memory dump when the platform supports it.
- Capture the adapter or transport trace. Confirm whether the two reads use the same backend command.
- Advance, invalidate, or reconnect through a documented mechanism. Read again and record which value changed.
- Keep the mismatch as a regression fixture. A debugger update should not erase the lesson.