Vintage Adventures - MOS 6502 - Part 2.1 Memory Layout
In Part 1 we covered the 6502's architecture and instruction set, and in Part 2 we built a working emulator in Rust with a step-through debugger. But there's a problem with how we're loading programs, the first version just drops machine code at 0x0000 and starts executing from there. When I started looking into how real 6502 systems actually lay out memory, it became clear why that's a bad idea.
The 6502 sees a flat 64 KiB address space from 0x0000 to 0xFFFF, but certain regions have special roles that our emulator needs to respect.
Overall address space
- 16-bit addresses give 64 KiB of addressable space, from 0x0000 to 0xFFFF.
- This space is conceptually divided into 256 pages, each 256 bytes.
- Page number = high byte of the address.
- Offset within page = low byte of the address.
In a real system (C64, NES, Apple II), this 64 KiB gets carved up into RAM, ROM, and memory-mapped I/O by external hardware. But from the 6502's perspective it's just one linear address space, the CPU doesn't know or care what's behind each address.
Zero page (page 0)
- Address range: 0x0000–0x00FF (256 bytes).
- Called zero page because the high byte of every address in this range is zero.
What I found interesting when I dug into this is how much the 6502's instruction set is designed around zero page. Several addressing modes treat it specially:
- Zero-page direct:
LDA 10uses a single 8-bit operand instead of a full 16-bit address. - Indexed zero-page:
LDA 10,X,STA 20,Y. - Indirect modes that use pointers stored in zero page:
JMP (FFFC),LDA (00),Y,LDA (00,X).
Because the instruction only needs one address byte, zero-page accesses have shorter instructions (saving 1 byte of code) and one fewer memory fetch, so they execute one cycle faster than the corresponding absolute modes on a real 6502.
This is why experienced 6502 programmers treat zero page like a bank of fast "pseudo-registers", temporary variables, pointers (low/high byte pairs), and frequently used state all go here. It's one of those design choices that seems simple but has a big impact on how you write code for this chip.
Stack page (page 1)
- Address range: 0x0100–0x01FF (256 bytes).
- The hardware stack is fixed to this page, you cannot relocate it.
The stack pointer S is an 8-bit register holding the low byte of the address, so the physical stack address is always 0x0100 + S. On reset, S is typically initialized to 0xFD or 0xFF by the system.
The stack grows downward (descending):
- A push (
PHA,PHP,JSR) writes at 0x0100 + S then decrements S. - A pull (
PLA,PLP,RTS,RTI) increments S then reads from 0x0100 + S.
The stack handles subroutine calls (JSR pushes the return address, RTS pulls it), interrupts (BRK, IRQ, NMI push PC and status, RTI restores them), and explicit pushes/pulls of A and the status register.
One thing that surprised me when I started paying attention to this: since S is only 8 bits, the stack capacity is fixed at 256 bytes and wraps within 0x0100–0x01FF if you overflow or underflow it. There's no protection, the CPU will happily wrap around and start overwriting data. Something to keep in mind when we get to more complex programs.
Typical high-level layout
Apart from the fixed zero page and stack page, everything else is up to the machine designer. But reading through documentation for various 6502 systems, a common pattern emerges:
- 0x0000–0x00FF: Zero page (fast variables, pointers).
- 0x0100–0x01FF: Hardware stack.
- 0x0200–0x9FFF or so: General RAM (user variables, program code).
- Some middle region: Memory-mapped I/O (video, sound, controllers).
- Top of memory: ROM (OS, BASIC interpreter, reset/IRQ/NMI vectors at the very top addresses 0xFFFA–0xFFFF).
What this means for our emulator
Looking at this layout, the problem with our current approach is obvious, loading programs at 0x0000 dumps them right into zero page, which is supposed to be reserved for fast variable access. And if the program is longer than 256 bytes, it'll spill into the stack page at 0x0100 too.
We want to keep our program out of the zero page and page one (used by the stack). The sensible thing is to load programs at 0x0200 and start execution from that address, which is what most real 6502 systems do for user programs. We'll bring in the notion of a load address into the emulator and default it to 0x0200.
Let's continue expanding the emulator....