The puzzle
The program is halted at a breakpoint. The debugger shows thirteen general registers, a stack pointer and a window of hex digits. Somewhere in there is the answer to why the program misbehaves, but hex has no types: the same four bytes can be a counter, a pointer, a float or two characters of a string. How do you read memory so that it means something, and how much can you trust what the debugger reconstructs?
STEP 1
Registers of a halted core
When the core is halted, the debugger reads its registers through two debug registers, DCRSR (select a register) and DCRDR (its value), over the same memory access port as everything else. That is how info registers shows r0–r12, SP, LR, PC and xPSR.
Those values belong to the innermost frame, where the core stopped. When you select an outer frame (up, frame 2), GDB shows the registers as they were in that function, reconstructed from what was saved on the stack. Registers that a function is allowed to overwrite (r0–r3 and r12 under the Arm procedure call standard) are generally not recoverable for outer frames, which is one reason arguments appear as <optimized out>.
STEP 2
Memory has no types
A memory view is bytes. The unit size (1, 2, 4 or 8 bytes) and format (hex, signed, unsigned, character, float) you choose decide what they mean, and on a little-endian Cortex-M (almost all of them) the byte at the lowest address is the least significant one.
↑ This step uses the figure at the top of the page.
GDB’s x/nfu addr spells this out: n units, format f, unit size u. x/4xw 0x20000100 shows four words in hex; x/16cb the same bytes as characters. print uses the variable’s declared type from the debug information instead, which is usually what you want for variables; x is for raw memory, buffers and addresses without a symbol.
STEP 3
Rebuilding the call chain
A backtrace answers “how did we get here?”. The debugger starts from the current PC and SP, uses the unwind information that the compiler put in the debug data to find where the current function saved its return address, reads it from the stack, and repeats for the caller, frame by frame, until it reaches main or a frame it cannot explain.
The debugger unwinds the stack frame by frame: from each function’s saved return address (LR) and the unwind tables in the debug information it finds the caller and that caller’s frame. If a buffer overflow overwrites a saved return address, the unwinder follows the garbage and the backtrace ends in nonsense. Addresses are illustrative.
Two things break this. Missing unwind information (hand-written assembly, stripped libraries) stops the backtrace at that function. A corrupted stack, typically a buffer overflow that overwrote a saved return address, sends the unwinder to garbage: a nonsense address, a function shown as ??, or a chain that repeats. When the backtrace looks impossible, suspect the stack before suspecting the code.
Exceptions have a special frame: on entry the core stacks eight registers and puts an EXC_RETURN value (such as 0xFFFFFFF9) in LR (lesson 4). Debuggers recognise it and unwind through the handler into the interrupted code.
STEP 4
Worked example: decoding a word by hand
The debugger shows 0x20000108: 0x00216948 as a word. As bytes, lowest address first, that is 0x48, 0x69, 0x21, 0x00: ASCII “H”, “i”, “!”, then the terminating zero. It is the string "Hi!", viewed as a number:
Similarly, 0x3F800000 as an IEEE 754 single is sign 0, exponent 127, fraction 0: exactly 1.0. The bytes never change; only your choice of view does.
MYTHS AND FACTS
Common misconceptions
The debugger shows what the variable is
It shows bytes interpreted with the type it believes; with the wrong type or a stale symbol the view is wrong while the memory is right.
Registers in frame 3 are what they were when frame 3 was running
Only the ones that were saved can be reconstructed; the others belong to the innermost frame.
Reading memory is always harmless
Reading some peripheral registers has side effects.
A strange backtrace means the compiler is wrong
More often the stack was overwritten or unwind information is missing.
Check yourself
Answer in your head, then open the card.
The bytes at 0x2000_0200 are 0x34, 0x12, 0xCD, 0xAB. What does x/2xh show, and what does x/1xw show?
Halfwords: 0x1234, 0xabcd. Word: 0xabcd1234. Little-endian puts the lowest-address byte in the least significant position.
In frame 2 of a backtrace, GDB shows r0 as <not saved> or <optimized out>. Why can it not show the value?
r0 is a caller-saved argument register: the functions called after it were free to overwrite it and did not save it, so its old value is not on the stack.
A backtrace shows #0 copy_field, #1 parse_cmd, #2 0x41414140 in ?? (). What happened?
A buffer overflow in parse_cmd's frame overwrote its saved return address with 0x41414141 (ASCII "AAAA"); the unwinder followed it to a nonsense address, and parse_cmd will crash when it returns. The frames beyond #1 cannot be trusted.
Why can opening a live peripheral view of a UART make the program lose received bytes?
Reading the receive-data register pops a byte from the FIFO; the debugger's periodic reads take bytes before the program does.
Sources (4)
- GDB manual, “Examining Memory” (x/nfu), “Registers” and “Backtraces” — x/nfu with unit sizes b (bytes), h (halfwords), w (words, “the initial default”) and g (giant words, eight bytes); info registers prints registers “in the selected stack frame”; a backtrace shows “the currently executing frame (frame zero), followed by its caller (frame one)”; arguments may show as <optimized out> (read from gdb/doc/gdb.texinfo)
- OpenOCD, src/target/cortex_m.h — core registers of a halted Cortex-M are read and written through the Debug Core Register Selector and Data registers, DCRSR (0xE000EDF4) and DCRDR (0xE000EDF8); DHCSR (0xE000EDF0) holds CHALT, SHALT, S_LOCKUP and S_REGRDY
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — EXC_RETURN values saved in LR on exception entry (0xFFFFFFF1, 0xFFFFFFF9, 0xFFFFFFFD), which a debugger uses to unwind through an exception handler
- Arm, Procedure Call Standard for the Arm Architecture (AAPCS32) — r0–r3 and r12 are not preserved across calls, r4–r11 are; LR holds the return address; this is why a caller frame’s r0–r3 cannot be recovered after a call (read in unit 3)