Program patches
Method
The image has no free space, and code banks are full of unoptimised stock code. A patch therefore rewrites a stretch of existing code more compactly, keeps its behaviour, and uses the room gained for the change.
Write the new stretch as Thumb assembly in
design/patches/NAME.S, with.orgat the bank offsets. A patch that calls into other banks is laid out from one base address so that both banks’ routines can be named (asmusic_headbar.Sdoes from the timer bank 0x18a21a00). Calls areblto labels placed at the callees’ offsets.Assemble with
arm-none-eabi-as(base.assemble), take the bytes of the stretch.Check the stock bytes: the build refuses unless the stretch in the stock image is exactly the expected one (a hash or the bytes themselves).
Run the checker:
check_NAME.py OLD.dis NEW.disruns the old and the new stretch side by side inthumb_sim.pyon every relevant combination of inputs and compares calls (target and arguments), memory written, registers and stack, apart from the intended difference (base.check). The build refuses to produce an image if it fails.Put it in (
base.replace_stretchis the general form).
For shared code the stretch is found in each program by its bytes with the bl instructions masked
(base.masked, base.find_all), its calls are re-aimed at that program’s routines, and the checker runs once per
program with that program’s addresses.
The patches
Patch |
Part |
Stage |
Where |
What |
Checker |
|---|---|---|---|---|---|
|
|
5 |
|
root-menu routine: start code 0x40 skips the menu and acts as “Playlists” |
|
|
|
7 |
shared screensaver bank 0x18ce1a00 + 0x034 to 0x0c6 (146 bytes), seven programs |
draw one animation frame per timer tick |
|
|
|
11 |
|
the whole time-and-bar routine: bar with the played part, knob moved column by column |
|
|
|
14 |
|
the tag reader fetches the file name only when the title is empty |
|
|
|
16 |
|
landscape page, bottom strip with progress bar |
|
|
|
24 |
shared status-bar on/off routine (0x74 bytes), 15 programs |
draw the bar at once when switched on, and move the bar timer’s next firing to now |
in two steps per program: |
|
|
14, 22 |
the same routine |
the first step alone: the bar at once (on the player from stage 14 to 23); the build only assembles it for the check |
|
|
|
24 |
new, 52 bytes at the end of every bank with a list box call other than the list control’s (15 programs) |
on a full redraw of style 0x6000 or 0x6002 to 0x6005 draw picture 127 at (17, 0) first, then the list box; both through the bank’s own veneer, so no literal |
|
Stage 9’s music_progress.S (the knob without flicker) was replaced by music_playing.S in stage 11 and is in the
git history only.
Smaller code changes are made directly by the build with base.patch_bytes (checked old bytes) or by re-aiming a
checked bl:
Part |
Stage |
Where |
Change |
|---|---|---|---|
|
5 |
|
menu entry 1 starts program 0 with code 0x40; grid steps |
|
7 |
screensaver timer, two places |
500 → 200 ms |
|
14 |
|
the playing screen names its own status-bar background (45) |
|
15 |
|
clock x = 22 |
|
15 |
screensaver clock key, bank offset 0x8a or 0xa6, 15 programs |
0xb5b6 → 0 |
|
16, 17 |
|
lines, width, measuring mode; page-index signature |
|
18 |
|
|
|
19 |
|
|
|
20 |
|
the Previous key calls |
|
21 |
|
result handlers call the new code; Playlists options are lists 26 to 30 |
|
21 |
|
the volume table |
|
23 |
|
the screen-off Previous calls |
|
24 |
0x18661380 in five programs |
the list control’s list box call goes to |
|
24 |
every other list box call, 15 programs |
goes to |
thumb_sim.py
design/patches/thumb_sim.py interprets straight-line Thumb code from objdump output. It knows only the
instruction forms that occur in the patched stretches and stops with an error on anything else, so a check never
rests on a guess.
import thumb_sim
code = thumb_sim.load("new.dis", START, END) # {address: (size, op, args, literal address)}
m = thumb_sim.Machine(dict(r0=1, r1=0, sp=0x100000, lr=0), {})
m.wr(0x7000, 4, 0x200138) # memory is a byte map; unwritten bytes read as 0xEE
exit = m.run(code, START, (), lambda target, m: 1) # calls out of the code are recorded and answered
m.calls # [(target, r0, r1, r2, r3), ...]
m.mem, m.r # memory and registers afterwards
It follows calls inside the code under test, restores registers saved by push on the matching pop, and treats a
b out of the code as a tail call. An earlier version mis-added a shifted operand; when a checker disagrees with
the listing, suspect the interpreter first.