UNIT 06 · LESSON 3 OF 6

Inspecting Registers, Memory, and the Stack

How do you read memory so that it means something, and how much can you trust what the debugger reconstructs?

INTERACTIVEThe same 16 bytes, four ways
Sixteen bytes of memory and how a debugger displays them for a chosen unit and formatbytes in memory, lowest address first78563412FFFFFFFF486921000000803F+0+15(gdb) x/4xw 0x200001000x2000_0100:0x123456780xffffffff0x002169480x3f800000The first word reads 0x12345678 although its first byte is 0x78: little-endian.
Sixteen bytes of memory and how a debugger displays them for a chosen unit and formatbytes in memory, lowest address first78563412FFFFFFFF486921000000803F+0+15(gdb) x/4xw 0x200001000x2000_0100:0x123456780xffffffff0x2000_0108:0x002169480x3f800000The first word reads 0x12345678 although its firstbyte is 0x78: little-endian.

Try this

Unit
Format
x/4xw 0x20000100: 0x12345678, 0xffffffff, 0x00216948, 0x3f800000.

A debugger shows memory as raw bytes; the unit size and format you choose decide what you see. Almost all Cortex-M parts are little-endian, so the byte at the lowest address is the least significant byte of a halfword or word. The command shown is GDB’s x/nfu (count, format, unit). The bytes are an illustrative example at 0x2000_0100.

What you will be able to do
  • Read the same bytes of memory as bytes, halfwords, words and floats, and explain the little-endian result.
  • Write GDB x commands with the right count, format and unit.
  • Explain how a debugger reads core registers of a halted Cortex-M, and why registers look different in each stack frame.
  • Describe how a backtrace is rebuilt from saved return addresses and unwind information, and recognise one built from a corrupted stack.
  • Identify peripheral registers that a debugger must not read casually.
Before you start
  • Breakpoints and halting the core (lesson 2).
  • Endianness and the stack (unit 2, lesson 5; unit 3, lesson 5).
Steps in this lesson
  1. Registers of a halted core
  2. Memory has no types
  3. Rebuilding the call chain
  4. Worked example: decoding a word by hand
  5. Common misconceptions

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.

INTERACTIVEHow a backtrace is rebuilt from the stack
A backtrace built from saved return addresses, intact or after a stack overflow(gdb) bt#0 copy_fieldframe at 0x2000_1F70, returns to 0x0800_04A4#1 parse_cmdframe at 0x2000_1F88, returns to 0x0800_031C#2 handle_rxframe at 0x2000_1FA8, returns to 0x0800_0260#3 mainframe at 0x2000_1FC8Every frame’s saved LR points into the caller, so the unwinder reaches main. Theinnermost frame (#0) is where the core stopped.
A backtrace built from saved return addresses, intact or after a stack overflow(gdb) bt#0 copy_fieldframe at 0x2000_1F70, returns to 0x0800_04A4#1 parse_cmdframe at 0x2000_1F88, returns to 0x0800_031C#2 handle_rxframe at 0x2000_1FA8, returns to 0x0800_0260#3 mainframe at 0x2000_1FC8Every frame’s saved LR points into the caller, sothe unwinder reaches main. The innermost frame (#0)is where the core stopped.
#0 copy_field, #1 parse_cmd, #2 handle_rx, #3 main.

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:

0x48+0x69×28+0x21×216+0x00×224=72+26 880+2 162 688=2 189 640=0x002169480x48 + 0x69 \times 2^{8} + 0x21 \times 2^{16} + 0x00 \times 2^{24} = 72 + 26\,880 + 2\,162\,688 = 2\,189\,640 = 0x00216948

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)
  1. 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)
  2. 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
  3. 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
  4. 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)