Self-Modifying Code
Code that rewrites itself
Self-modifying code changed its own instructions at runtime, enabling impossible optimisations on 8-bit systems by treating code as data.
On the 6502, indexing memory costs extra cycles. What if the address in your instruction could change dynamically? Self-modifying code rewrote instructions at runtime—changing addresses, operation codes, or branch targets. It was fast, dangerous, and essential for achieving the impossible on 8-bit hardware.
The problem
Standard indexed addressing:
lda table,x ; 4-5 cycles, X is index
But what if you need to change which table?
The solution
Modify the instruction itself:
lda table ; 4 cycles, absolute addressing
; Elsewhere, change the address:
lda #<new_table
sta load_addr+1 ; Modify low byte
lda #>new_table
sta load_addr+2 ; Modify high byte
Now lda table loads from new_table.
Common uses
Unrolled loops
lda $0400 ; First iteration
sta $d020
lda $0401 ; Second iteration (address modified)
sta $d020
; ...repeat, modifying addresses
Dynamic branch targets
jump_target = *+1
jmp $0000 ; Address modified at runtime
Variable table selection
Switch between data sources without index overhead.
6502 advantages
| Factor | Benefit |
|---|---|
| Von Neumann | Code and data same memory |
| No cache | No stale instruction problems |
| Absolute addressing | Faster than indexed |
Examples
Sprite multiplexer
lda #sprite_y_1
sta store_y+1 ; Modify destination
store_y:
sta $d001 ; Y position (address changes)
Music player
lda pattern_ptr
sta fetch+1
fetch:
lda $0000 ; Address modified each note
Dangers
| Risk | Consequence |
|---|---|
| Timing bugs | Wrong code executed |
| Debugging | Hard to trace |
| Maintenance | Confusing to read |
| Portability | Platform-specific |
Z80 differences
The Z80 has no instruction prefetch or cache in the modern sense, so SMC works the same way as on the 6502 — write to the byte that holds an operand, the next M1 fetch sees the new value. The differences are subtler:
- The R register increments during M1 cycles. Code that uses
LD A,Rfor a “random” seed sees the seed shift if SMC changes instruction lengths nearby. - Refresh cycles put the R register on the address bus during the second half of every M1 — this is invisible to RAM-side SMC but interacts with some peripherals if the address decode is loose.
- Multi-byte instructions are not atomic. A
LD (HL),norLD A,(nn)that targets the next instruction’s operand byte after the M1 fetch but before its operand fetch produces a hybrid execution. Real Z80 does this faithfully; emulators with instruction-level abstraction can miss it.
In practice, SMC on Z80 is just as common as on 6502 — Spectrum games use it constantly.
On the Commodore 64
Commodore shipped self-modifying code in the machine’s own BASIC. The Programmer’s Reference Guide’s memory map places the CHRGET subroutine — “Get Next Byte of BASIC Text” — at $0073–$008A in zero page, and TXTPTR, the “Pointer: Current Byte of BASIC Text”, at $007A–$007B, inside the routine: the pointer is the operand of CHRGET’s own load instruction, and advancing it means rewriting that instruction. Because every token passes through CHRGET, the guide recommends patching it to add commands — “This technique is often called a wedge.”
The people who wrote about C64 code in the 1990s used the technique the same way and said so:
| Author | Routine | Use of self-modification |
|---|---|---|
| Pasi ‘Albert’ Ojala, C=Hacking 5 (written 1991) | Sprite stretcher | “The fastest routine I have ever written uses some serious selfmodification tricks” — loop counter, bit mask and table indices “stored straight in the code” |
| Stephen Judd and George Taylor, C=Hacking 8 (1994) | 3D line drawing | From 18 cycles per point down to “15 cycles per point” with self-modifying code, 13 “if you are clever” |
| Todd S. Elliott, C=Hacking 13 (1996) | 3D dungeon crawl | The engine unrolls “the graphics updating loop using self-modifying code, resulting in speed increases” |
| Todd S. Elliott, C=Hacking 21 (2002) | Full-screen FLI on the SuperCPU | Per-rasterline table values are “inserted into the code that follows the STA SCROLY command”, so the writes that follow carry the right immediate operands |
| Marko Mäkelä, Codebase64 | Stable raster routine | Stores the original interrupt address as the operand of a jmp — “oldirq = * + 1 ; Placeholder for self-modifying code” |
| Fungus, Codebase64 | Double IRQ | Saves A, X and Y into the operands of the restoring loads: “VIA self modifying code (Faster than the STACK is!)” |
| Cadaver (Lasse Öörni), Codebase64 | Sprite multiplexer | “Can be speeded up about 20% by unrolling all the plotting loops and using self modifed code instead of the second set of plotting buffers” |
| White Flame and Krill, Codebase64 | Dispatch on a byte | All but the stack dispatch “use self-modification, which will run 1 cycle faster and 1 byte leaner if the dispatch routine itself is in zeropage” |
| Bruce Clark, 6502.org via Codebase64 | Memory move | Replacing (zp),Y with self-modified abs,Y addressing takes “2 fewer cycles per byte” |
Emulator and JIT compatibility
SMC is a chronic source of bugs in emulators that compile blocks of guest code ahead of time:
- JIT-based emulators (some PSX, GBA, and N64 cores) cache compiled translations of guest code. When the guest writes to that code, the cache is stale. Robust JITs invalidate the affected block on write — costly, and easy to get wrong if the write granularity doesn’t match the block boundary.
- Cycle-accurate emulators that step instruction-by-instruction handle SMC for free; the next fetch reads from current memory.
- Static recompilers (often used for retro ports) can’t handle SMC at all without falling back to interpretation.
This is why retro emulator authors stay loyal to interpretation despite its host-CPU cost — JIT speedups don’t survive contact with a self-modifying Mayhem in Monsterland.
Modern perspective
Self-modifying code today:
- Forbidden by W^X memory protection — modern OSes mark pages either Writable or eXecutable, never both. Programs that legitimately need SMC (JIT compilers) explicitly toggle protection between phases.
- JIT compilation is the modern descendant — generate code at runtime, then mark the page executable.
- Polymorphic malware uses SMC to evade signature scanners.
- Embedded firmware sometimes still uses SMC where Flash and RAM are unified and tight loops need every cycle.
Debugging tips
| Technique | Purpose |
|---|---|
| Mark modifications | Comment clearly |
| Initialise explicitly | Don’t assume values |
| Test boundaries | Check modified ranges |