Skip to content

0.6.0

Choose a tag to compare

@macabeus macabeus released this 18 Aug 14:55
· 9 commits to main since this release

The theme of this release: the analysis surface refuses or flags what it cannot answer, instead of returning a plausible value.

A GBA bus answers every address. It rounds an unaligned load down, reads undecoded space as open bus, and discards a store to ROM — all correct for the CPU, and all indistinguishable from the value you actually asked for. That is fine for emulation and wrong for a debugger. The bus is unchanged; the scripting and debug-info surfaces on top of it now refuse.

All six packages move to 0.6.0 together.

Breaking changes

  • read16 / read32 throw on a misaligned address, naming the one the hardware would have read, and on space the bus decodes to nothing. They previously answered. Use readBytes(address, size) for 1–4 bytes at any alignment.
  • read32 is now unsigned. It previously returned a signed 32-bit value, so a high bit came back negative.
  • wait({ pc }) is now wait({ execution }). wait() also throws on an unrecognised condition, where it previously returned immediately — so a stale { pc } in an untyped script was a silent no-op.

Reads that refuse

  • readBytes(address, size) — 1–4 bytes at any alignment, assembled byte by byte.
  • readMember / writeMember — read or write a DWARF MemberLocation at a base address, for an instance no symbol names: one reached through a pointer, an array element, anything placed at run time.
  • addressToSymbol reports exact — whether st_size covered the address, or whether containment was merely inferred from the next symbol's start. In a decomp ELF most symbols come from hand-written asm and carry no size, so a lookup landing kilobytes past a function still resolved to it with nothing in the answer to say so.

Writes, and bounds the DWARF states

The scripting surface had no write API at all, so scripts reached for the raw bus — which rounds a misaligned store down and silently discards a store to ROM.

  • write8 / write16 / write32 / writeBytes(address, size, value) — new, carrying the same guards as the reads.
  • writeVariable(path, value) — the write counterpart to readVariable, merging a bitfield into its container without disturbing its neighbours.
  • Variable paths take subscripts, bounds-checked against the DWARF extent: readVariable('gLayers[2].width'), writeVariable('gGrid[1][3]', 0). An index past the end throws instead of resolving into whatever the linker placed next. A dimension the DWARF leaves unstated (extern T x[][4]) is not checked.
  • symbolExtent(name) — an object's byte size, and whether it came from st_size or the DWARF type. A write starting inside a known extent and running past its end is refused, naming what it would have hit.
  • addressToSymbol resolves linker-placed globals (SHN_ABS / NOTYPE), which it previously skipped entirely. In a decomp that is most of them.

Execution, observed rather than sampled

wait({ pc }) compared the PC once per frame, so code that runs constantly read as never reached.

  • ArmCpu.addExecWatchpoint(address, cb) — fires from the CPU's instruction step and returns a disposer. Composable, and independent of setDebugHooks, which is a single slot.
  • watchExecution(target, options?) — the execution counterpart to watchMemory, reporting count (every execution, always exact — 0 means it did not run), hits, dropped and stop(). Takes an address or a symbol name.
  • wait({ execution }) — wait for an instruction to execute, by address or symbol.
  • watchMemory reports dropped, so a capped hits array is not read as the whole story.

Each hit carries lr and its source location. Note that lr names a caller only for an address a bl reached — one fallen into from the instruction above, branched to, or entered by a tail call carries whatever the last unrelated call left behind.

Symbols wherever an address goes

  • A numeric code address has bit 0 cleared. A Thumb function pointer carries it set, so watchExecution(ptr) counted 0 where the same function by name counted 420.
  • watchMemory takes a symbol name, and throws on an unknown one instead of coercing it to address 0 and watching nothing.
  • A symbol watch defaults to the object's whole extent rather than st_size — which is null for a linker-placed global, so watchSymbol('gLayers') watched 1 byte of 112.

ARM7TDMI

  • THUMB empty-Rlist quirk for LDMIA / STMIA. THUMB.15 encodes the register list in 8 bits, so Rlist == 0 is representable, and the ARM7TDMI does not treat it as a no-op: it transfers R15 and advances the base by 0x40 (GBATEK, THUMB.15). The per-register loop did not run and the base was left alone, so stmia r1!, {} stored nothing and moved nothing.

Install

npm install @gba-kit/gba-node@0.6.0     # Node
npm install @gba-kit/gba-browser@0.6.0  # browser
npm install @gba-kit/gba-react@0.6.0    # React

Also published at 0.6.0: @gba-kit/gba-emulator, @gba-kit/arm-emulator, @gba-kit/debug-info.

Per-package detail is in each package's CHANGELOG.md. Scripting API reference: docs/scripting.md.

Full diff: https://github.com/macabeus/gba-kit/compare/@gba-kit/gba-emulator@0.5.0...v0.6.0