7. Registers, memory and disassembly

Three views for when the source-level picture has run out: a module without debug information, a crash inside the RTL, a value that is wrong before any of your code touched it.

Registers

A dedicated registers scope, readable and writable, named for the bitness of the target rather than the bitness of the adapter:

TargetRegisters shown
x64 RIP, RSP, RBP, RAXRDI, R8R15, EFlags
x86 EIP, ESP, EBP, EAXEDI, EFlags — no R8R15
The Registers scope expanded in the Variables view on a 64-bit target: RIP, RSP, RBP, RAX through RDI, R8 through R15 and EFlags, each shown in hex with its decimal value alongside. RAX is mid-edit, an inline text box reading 0x003.
Every x64 register named in the table above, hex value plus decimal alongside. RAX is being edited in place — the same inline text box every other editable value in the Variables panel uses.

Writing a register on a 64-bit target accepts either the 32-bit or the 64-bit spelling, and the value returned afterwards is re-read from the thread rather than echoed back — so what you see is what the write actually achieved.

The memory view

The extension ships its own memory panel, View Memory (Delphi), because the stock VS Code hex pane cannot do three things that matter when you are chasing a corrupted structure: scroll to before a value’s start address, show where a value begins and ends, and show what changed.

  • Reads at negative offsets, so you can look at what sits in front of a buffer.
  • Marks the exact byte extent of the value you opened it on.
  • Flashes the bytes that changed since the previous stop.
  • Writes individual bytes, behind an explicit toggle rather than by accident.

Opening it

Three ways in, and which one you get depends on what the row knows about itself:

  • From a Variables or Watch row — the usual route, via the inline icon described above. The row already carries an address.
  • From a Watch row with no address — the expression is evaluated first, then the view opens on the result.
  • From nothing — run the command with no row selected and it asks for an expression, suggesting the shapes it accepts: a variable, a field path, or @Rec for an address.
A value that exists only as a computation — an arithmetic result, a function return — is refused, because it lives nowhere in memory to be dumped. The message says so explicitly rather than opening an empty pane.

One pane per value per session: asking for the same variable again reveals the pane you already have rather than stacking another. All of them close when the session ends.

Moving around

ControlWhat it does
▲ back / ▼ forwardScroll by a page
value startJump back to the value’s own address
refreshRe-read now, and mark what changed since the last time you looked
edit: off / edit: ONArms byte editing — see below
/ number / +Bytes per row. Match it to a 2D array’s row width and the block reads as one.
Mouse wheelOne row; hold Shift for a page

Scrolling backwards past the start is the point of the thing, not an accident: a string’s length header sits four bytes before its characters, a dynamic array’s eight before its elements.

Reading the colours

Four marks, and they mean different things — telling them apart is most of the value of the view:

MarkMeaning
Blue underline This byte belongs to the value you opened the view on. Nothing is underlined when the extent could not be established — deliberately, so the view never claims a neighbouring variable’s bytes are yours.
Blue box, and the row’s address in blue Offset zero — the variable’s own address.
Orange flash, fading to orange text Changed since the last comparison point. The flash fades after a few seconds but the text stays orange until the next comparison, so a change you looked away from is still there when you look back.
Grey ?? Not readable at that address.
What counts as a comparison point — a stop, a write made in this pane, or the refresh button. Scrolling, resizing the pane and changing the column count are just looking: they never clear the marks and never produce a false “nothing changed”. The header line also states the number of changed bytes, so a flash you missed is still countable.

If the value moves — a string reallocated, an object freed and rebuilt — the pane notices on the next refresh, retargets to the new address and says so in its header.

Writing a byte

  1. Click edit: off so it reads edit: ON.
  2. Click the byte and type over it — one or two hex digits, 0x allowed.
  3. Press Enter, or simply click away: both commit. Press Escape to abandon the edit and restore the byte.

Anything that is not a byte is refused by name — "zz" is not a byte (expected 00..FF) — and a write the debuggee rejects reports that too. Unreadable ?? cells never become editable in the first place.

Open it from the hover icon on a row in the Variables or Watch view — an inline icon at the right-hand end of the row, not an entry in the right-click menu.

The FPannelliChat row in the Variables view with the mouse hovering the inline View Memory (Delphi) icon at the right-hand end of the row, its tooltip visible.
The icon sits at the row’s right-hand end, not in the right-click menu — it only appears on hover, which is why a screenshot rather than a description was needed here.
VS Code’s own hex pane (View Binary Data) is switched off by default, not merely supplemented: the extension tells the adapter to withhold the memory capabilities that put it there, so that icon is absent entirely. Set delphi-win64.stockMemoryView to true to get it back alongside this one — which is also the only remaining reason to keep the Hex Editor extension installed.
The View Memory (Delphi) panel titled Memory: FPannelliChat, header reading FPannelliChat : TList__1<fraChatRow.TfraChatRow> · 0x28851363710 · 88 byte(s) of the value, with back/forward/value start/refresh controls and a column count, and the row at 0x28851363710 boxed to mark offset zero.
The offset-zero marker — the boxed byte and the blue row address — on FPannelliChat’s own address. No byte has changed since the last comparison point here, so the orange flash this section describes above is not visible in this particular capture.

The Disassembly View

Real x86 and x64 decoding, through a bundled Zydis, symbolicated against the loaded modules: each instruction is labelled with the nearest function and an offset, plus the source file and line wherever the line table has one. Bytes that do not decode are shown honestly as db XX rather than as a plausible-looking instruction.

This is also where address breakpoints are set.

Disassembling backwards from an address is refused unless it can be done correctly. x86 instructions are variable-length, so scanning backwards is guesswork: starting one byte off produces a completely different, entirely plausible instruction stream. Instead, the view looks for a known boundary — a function start, an export entry, or a nearer line boundary — and decodes forward from it. If forward decoding from that boundary does not land exactly on the target address, the view says so rather than showing you fiction.

The Disassembly View interleaving source lines with addresses, raw bytes and decoded instructions. A green highlighted row with an arrow marks the current instruction. Call targets carry symbolicated comments such as Variants._VarClear and Variants.DispInvokeCore+offset, and unreachable padding bytes decode as int3.
The green row with the arrow gutter marker is the current instruction. Call targets are symbolicated in the trailing comment — Variants._VarClear, Variants.DispInvokeCore+… — and source lines interleave with the addresses that came from them, right down to the int3 padding between routines.

When the symbols run out

A stop at an address with no source — OS code, a module built without debug information, an address the debug info does not cover — does not go silent, and it does not leave you staring at an empty editor either. A document opens, containing:

  • the address, and the module it belongs to;
  • why there is no source — the module has no debug information, the symbols are still being indexed, the address is not covered by the symbols it does have, or the module is not known at all — each with what to do about it;
  • the disassembly around the stop: a few proven instructions before the current one and a good stretch after, each labelled with the nearest function and offset, and with a source file and line wherever the line table has one — which is often how you discover that the frame below you does have source.

That last point is the reason this exists rather than a blank screen. A stop in the RTL or in a package built without symbols is not a dead end; it tells you where you are and what is nearby, and from there instruction stepping gets you out.

If no frame on the whole stack has source, the debugger also announces it in the Debug Console rather than letting a stop pass unnoticed. Set "noSourcePlaceholder": false in the launch configuration to turn the whole mechanism off.
A stop in ntdll.dll with no source. The document header states the address, the module, that the debugger IS stopped there, and why: no debug information of any kind was found for this module. Below it, disassembly around the current instruction, each line marked (no symbol), with an arrow and 'current instruction' label on the stopped-at address.
All three pieces from the list above, in one document: address and module in the tab title and header, the stated reason, and the disassembly — the arrow marks the current instruction exactly as in the ordinary Disassembly View.

From there, instruction-granularity stepping (see chapter 2) is what moves you forward, since it needs no line table at all.