Repository navigation
The theme of this release: the debugger reads the program the way its source does.
0.7.0 put a source-level debugger beside the code. Three places showed where it still thought like an emulator instead: a call stack that was always two frames deep, a watch that could not take a line copied out of the C it was stopped in, and a cartridge save that could not come in from, or go out to, anything else that runs GBA games.
GBA Debugger for VS Code is updated with this release.
All nine packages and the extension move to 0.8.0 together.
⚠️ Save states of EEPROM games
The emulator's EEPROM was byte-reversed within every 8-byte word against every real .sav (see below). A save state written with 0.7.0, of a game that saves to EEPROM, still loads, but the game's own save inside it comes back byte-swapped and will not be read. Re-import the .sav to get a state the game accepts. States of SRAM games, and states of games that never save, are unaffected.
The call stack goes as deep as the stack does
Unwinding was .debug_frame plus one guess, and on a GBA that covers very little: agbcc emits no call-frame information at all, and a devkitARM build's table stops at the edge of libgba, newlib and crt0. So the stack was always two frames, the second inferred from the link register with nothing confirming it.
Each frame is now recovered by an ordered set of layers, and one layer declining hands the frame to the next instead of ending the walk:
- the teardown a function has left to run, at a pc that has already begun popping its frame — where gcc's synchronous CFI would place the CFA a whole frame too high;
- call-frame information, everywhere else;
- an exception boundary, reading the interrupt stub's pushed block and the interrupted mode's banked stack pointer, so an interrupt handler has the interrupted code above it;
- the callee's own prologue, decoded to measure its frame, including the real agbcc and devkitARM shapes that copy a high register low before pushing it or park r11 in lr;
- the link register, only where the decode proves it was not spilled;
- a stack word, last, and only one that sits in executable code, matches the instruction set there, and has a real call ending exactly at it.
Every frame says which layer found it (StackFrame.method) and what about it is not established (StackFrame.doubt), and the walk says why it ended (Session.stack().end), shown in the editor as a final row. A caller's registers are the caller's: r4–r11 come from the slots the callee saved them in, r13 is the frame's own address, and r0–r3 and r12 say they were not recovered instead of repeating the callee's values. Selecting any frame shows that frame's variables. Step-out runs to the caller the walk found, so stepping out of a function called from an interrupt handler stops in the handler.
Expressions the way C spells them
gEntityInfo[arg0].xPosBg2, copied straight out of the source being decompiled, used to be refused with a hint to work the address out by hand. A variable index, ->, * and casts are now part of the grammar everywhere an expression is accepted: watches, hovers, the debug console, breakpoint conditions, logpoints, data breakpoints, and writes.
gEntityInfo[arg0].xPosBg2
- !! only constant subscripts and .member paths are supported
+ => 240 (0x00f0) [u16] @0x03002b6c
gUnk_03004654->unk1B
- !! unknown symbol 'gUnk_03004654->unk1B'
+ => 51 '3' [u8] @0x080521c3
- Pointer arithmetic is scaled, as in C and GDB, and only where the debug info says pointer or array:
e + 1on astruct Entity *steps one entity, and an array decays, soa[i],*aanda + 1treat arrays and pointers alike. Registers, literals andu32()reads have no type, sor3 + 1means what it always meant. - A cast means what it means in C.
(T *)xis a pointer, and*(T *)x,((T *)x)->mand((T *)x)[i]read through it. This changes(T *)x, which used to ignore the star and show the T at x;(T)xis unchanged. - Everything these paths name is writable:
gEntityInfo[i].xPosBg2 = 10,p->hp = 0, a bitfield through a pointer. - A literal index outside a sized array is still refused when the expression compiles. A runtime index is not bounds-checked, as in GDB.
- Types are resolved once when an expression compiles, so a condition like
g_player.pos.x == 5evaluates about 18× faster.
.sav import and export
A ⋯ button beside Save state in the Screen panel opens Import from a .sav file and Export to a .sav file.
An import is a power-on machine of the ROM with the file already in its cartridge, at frame 0, the way VBA-M's Import battery file and mGBA's Load alternate save game work: load the state, press continue, and the game finds its save. The state is named after the file, a second import of the same name gets (2), and the machine being debugged is not touched. An export is the size the cartridge really has — 32768 bytes for SRAM, 512 or 8192 for EEPROM — so it opens in mGBA, VBA-M or on a flash cart.
Which chip a file belongs in follows from the save type the ROM declares and the file's size together. A file that does not fit is refused with a message naming both, never padded or truncated into the wrong chip. Flash cartridges are refused in both directions: gba-kit backs the cartridge with plain memory and emulates no flash chip, so a game could not read a flash save back.
Over DAP and in-process, the requests are gba-kit/importSave and gba-kit/exportSave.
In the emulator core
- EEPROM images are byte-compatible with mGBA, VBA-M and flash carts. A 64-bit EEPROM word goes out most significant byte first and the GBA is little-endian, so the byte a game sends first is the last of the eight in memory; the array had them the other way round.
SRAM_F_Vcartridges have working saves. Detection looked for the literalSRAM_V, whichSRAM_F_V102(Fire Emblem) does not contain. It now reads the SDK string the build embeds and keeps it onGbaSystemBus.save.- A 64 Kbit EEPROM is read at the right addresses. The address width was latched at 6 bits by the first six bits of any address and never revised; it now comes from the length of the read the game makes.
GbaSystemBus.readBackup()/writeBackup()read and write the cartridge's backup memory as the bytes a.savholds.BOOT_STACK_POINTERS,BIOS_IRQ_STUBandBIOS_IRQ_STUB_PUSHare the one place the post-boot machine's shape is written down, so the stub the emulator installs and the block an unwinder reads back cannot drift apart.ArmCpu.getBankedSP/getBankedLR/getBankedSPSRread another mode's state without serializing the CPU.
In debug-info and debug-core
- New exports in
@gba-kit/debug-info:frameConfidence,FRAME_METHODS, theMachineFactsport,ElfSection.flags,SymbolIndex.isExecutable/symbolRangeAt, andbitfieldPlacement,isSignedType,formatBitfield,le32,scalarSize. - New in
@gba-kit/debug-core:Session.stack(),compilewithCompiled,ExprPlace,ExprLvalueand therootType/typeByNamehints,Session.importSaveStateandexportSaveFile. - Breaking for custom
ExprHints/ExprEnvimplementations:ExprHints.symbolSignedis removed, andExprEnv.symbolis asked for a bare name only, never a dotted path. An implementation written against 0.7.0 still satisfies both interfaces unless it spellssymbolSignedout in an object literal.