· tutorials · 4 min read
6502 Emulation - The Jump Indirect Bug
Recently, I’ve been developing an emulator for the MOS 6502, an 8-bit microprocessor used in many famous systems such as the Apple II, Commodore PET, and Nintendo NES.
While implementing the jump indirect instruction, some (but not all) of the instruction’s single step tests would fail. My debug logs revealed that the culprit was the high-order byte of the 16-bit program counter (PC), which was always being set to $00 as opposed to the expected value ($98 in this case):
Test $6C $FF $70
Expected registers: A:23 X:66 Y:B2 S:47 P:24 PC:989D
Actual registers: A:23 X:66 Y:B2 S:47 P:24 PC:009D
Expected memory: 2887:6C 2888:FF 2889:70 7000:98 70FF:9D 989D:23
Actual memory: 2887:6C 2888:FF 2889:70 7000:98 70FF:9D 989D:23
PC mismatch!
After some investigation, I discovered that there’s a bug in the original 6502 hardware that occurs when a page boundary is crossed during the jump indirect instruction. According to 6502.org, there’s no carry associated with jump indirect, and so they caution that it must never use a vector beginning on the last byte of a page.
To better understand what’s happening here, consider the above test case:
JMP ($70FF) ; Jump indirect to $70FF
which is $6C $FF $70 in machine code.
The first byte fetched by the emulator is the opcode for jump indirect, $6C. Next, we need to construct the pointer ($70FF), and we do so by first obtaining the low-order byte by incrementing the PC by one to get $FF. Next, we increment the PC a second time to fetch the high-order byte $70. By combining these two bytes, we get our pointer.
The pointer is then used to get the value of PCL (program counter low), which in our case is $9D. To get the value of PCH (program counter high), we need to access the address found at (pointer + 1). The page boundary is crossed ($70FF + 1), but the CPU fails to carry the low-order byte, resulting in an address of $7000 instead of $7100. Because we’re emulating the system (and programming languages do increment the page), we end up trying to access the wrong address, resulting in a PCH value of $00 in the tests (since no value is stored here).
The solution
To fix this, we need to simulate the behavior of a real 6502 by not incrementing the pointer while on a page boundary, thus ensuring that we get the PCH from address $XX00 and not ($XXFF + 1).
I do this by first comparing the pointer with $00FF to determine if the low-order byte is on a page boundary, and if so, read the high-order byte from address $XX00. If the byte isn’t on a boundary, then we read from address (pointer + 1) like normal. My code ended up looking like this:
if (addressingMode == AddressingMode.Indirect)
{
var ptrLow = FetchByte();
var ptrHigh = FetchByte();
var ptr = (ushort)(ptrHigh << 8 | ptrLow);
var pcLow = Memory.Read(ptr);
byte pcHigh;
// JMP indirect bug: If ptr is at 0xXXFF, the high byte
// comes from 0xXX00 and not (0xXXFF + 1) as there's no carry.
if ((ptr & 0xFF) == 0xFF)
{
pcHigh = Memory.Read((ushort)(ptr & 0xFF00));
}
else
{
pcHigh = Memory.Read((ushort)(ptr + 1));
}
Pc = (ushort)(pcHigh << 8 | pcLow);
Clock += 5;
}
As to how this bug was handled in the contemporary period, compilers typically implemented adjustments to avoid addresses ending in $XXFF, like the following code from the fig-Forth compiler:
; The following offset adjusts all code fields to avoid an
; address ending $XXFF. This must be checked and altered on
; any alteration , for the indirect jump at W-1 to operate !
.ORIGIN *+2 ; Line 0094
.WORD DP ;) Lines 2396 - 2401
.WORD CAT ;| 6502 only. The code field
.WORD CLIT ;| must not straddle page
.BYTE $FD ;| boundaries
.WORD EQUAL ;|
.WORD ALLOT ;)
Hopefully this post helps explain exactly what’s going on with the 6502’s jump indirect bug, and one of the ways that you can handle it in your own emulator!