3

I'm currently in the process of writing a Gameboy emulator, and I've noticed something that seems strange to me.

My emulator is hitting a jump instruction 0xCD, for example CD B6 FF, but my understanding was that a jump should only be jumping to an address within cartridge ROM (0x7FFF maximum), because I'm assuming the CPU can only execute instructions from ROM, not RAM. The ROM in question is Dr. Mario, which I'd expect to only be carrying out valid operations. 0xFFB6 is in high RAM, which seems odd to me.

Am I correct in my thinking? If I am, presumably that means my program counter is somehow ending up at the wrong address and that the CB is actually part of another instruction's data, and not an instruction itself?

I'd be grateful for some clarification, thanks.

For reference, I've been using Gameboy Opcodes and CPU docs to implement the instructions. I know they contain a few errors, and I think I've accounted for them (for example, 0xE2 being listed as a two-byte instruction, when it's only one)

Triforcer
  • 127
  • 9
  • Have you tried using no$gmb to debug the game? Might want to reference http://stackoverflow.com/questions/6935637/game-boy-emulator-with-a-full-debugger for some tips. I personally have never messed with Gameboy but it sounds fun to reverse engineer! – Thomas Feb 04 '17 at 16:22
  • Thanks for the suggestion. I've heard other people have used that approach to debug theirs, I just thought it would be nice to try it myself first. Maybe I'm a glutton for punishment :) – Triforcer Feb 04 '17 at 20:11
  • Don't feel bad, so am I. – Thomas Feb 04 '17 at 20:52
  • which documentation are you using for coding your opcodes? – GabrielOshiro Feb 05 '17 at 18:15
  • I've edited the question to include these, thanks – Triforcer Feb 06 '17 at 16:28
  • "because I'm assuming the CPU can only execute instructions from ROM, not RAM" That assumption's incorrect; it can execute from anywhere (that's why it's possible to have arbitrary code execution glitches in Pokémon, among other things). – Pokechu22 Feb 15 '17 at 15:14

2 Answers2

3

Just checked Dr. Mario 1.1, it copies the VBlank int routine at hFFB6 at startup, then when VBlank happens, the routine at 0:01A6 is called, which calls the OAM DMA transfer routine.

During OAM DMA transfer, the CPU can only access HRAM, so writing a short routine in HRAM that will wait for the transfer to be completed is required. The OAM DMA transfer takes 160 µs, so you usually make a loop that will wait this amount of time after specifying the OAM transfer source.

This is the part of the initialization routine run at startup that copies the DMA transfer routine to HRAM:

...
ROM0:027E 0E B6            ld   c,B6             ;destination hFFB6
ROM0:0280 06 0A            ld   b,0A             ;length 0xA
ROM0:0282 21 86 23         ld   hl,2386          ;source 0:2386
ROM0:0285 2A               ldi  a,(hl)           ;copy OAM DMA transfer routine from source
ROM0:0286 E2               ld   (ff00+c),a       ;paste to destination
ROM0:0287 0C               inc  c                ;destination++
ROM0:0288 05               dec  b                ;length--
ROM0:0289 20 FA            jr   nz,0285          ;loop until DMA transfer routine is copied
...

When VBlank happens, it jumps to the routine at 0:01A6:

ROM0:0040 C3 A6 01         jp   01A6

Which contains a call to our OAM DMA transfer routine, waiting for DMA to be completed:

ROM0:01A6 F5               push af
ROM0:01A7 C5               push bc
ROM0:01A8 D5               push de
ROM0:01A9 E5               push hl
ROM0:01AA F0 B1            ld   a,(ff00+B1)
ROM0:01AC A7               and  a
ROM0:01AD 28 0B            jr   z,01BA
ROM0:01AF FA F1 C4         ld   a,(C4F1)
ROM0:01B2 A7               and  a
ROM0:01B3 28 05            jr   z,01BA
ROM0:01B5 F0 EF            ld   a,(ff00+EF)
ROM0:01B7 A7               and  a
ROM0:01B8 20 09            jr   nz,01C3
ROM0:01BA F0 E1            ld   a,(ff00+E1)
ROM0:01BC FE 03            cp   a,03
ROM0:01BE 28 03            jr   z,01C3
ROM0:01C0 CD B6 FF         call FFB6             ;OAM DMA transfer routine is in HRAM
...

OAM DMA transfer routine:

HRAM:FFB6 3E C0            ld   a,C0
HRAM:FFB8 E0 46            ld   (ff00+46),a      ;source is wC000
HRAM:FFBA 3E 28            ld   a,28             ;loop start
HRAM:FFBC 3D               dec  a
HRAM:FFBD 20 FD            jr   nz,FFBC          ;wait for the OAM DMA to be completed
HRAM:FFBF C9               ret                   ;ret to 0:01C3
Méga Lag
  • 386
  • 5
  • 10
0

Here is my analysis:

  1. Looking for CD B6 FF in the raw ROM I can only find it in one place of the memory which is 0x01C0 (448 in decimal).

  2. So I decided to disassemble the ROM, to see if it is a valid instruction.

I used gb-disasm to disassemble the ROM. Here are the values from 0x150 (ROM start) to address 0x201.

[0x00000100] 0x00           NOP
[0x00000101] 0xC3 0x50 0x01 JP $0150
[0x00000150] 0xC3 0xE8 0x01 JP $01E8
[0x00000153] 0x01 0x0E 0xD0 LD BC,$D00E
[0x00000156] 0x0A           LD A,[BC]
[0x00000157] 0xA7           AND A
[0x00000158] 0x20 0x0D      JR NZ,$0D ; 0x167
[0x0000015A] 0xF0 0xCF      LDH A,[$CF] ; HIMEM
[0x0000015C] 0xFE 0xFE      CP $FE
[0x0000015E] 0x20 0x04      JR NZ,$04 ; 0x164
[0x00000160] 0x3E 0x01      LD A,$01
[0x00000162] 0x18 0x01      JR $01 ; 0x165
[0x00000164] 0xAF           XOR A
[0x00000165] 0x02           LD [BC],A
[0x00000166] 0xC9           RET
[0x00000167] 0xFA 0x46 0xD0 LD A,[$D046]
[0x0000016A] 0xE0 0x01      LDH [$01],A ; SB
[0x0000016C] 0x18 0xF6      JR $F6 ; 0x164
[0x000001E8] 0xAF           XOR A
[0x000001E9] 0x21 0xFF 0xDF LD HL,$DFFF
[0x000001EC] 0x0E 0x10      LD C,$10
[0x000001EE] 0x06 0x00      LD B,$00
[0x000001F0] 0x32           LD [HLD],A
[0x000001F1] 0x05           DEC B
[0x000001F2] 0x20 0xFC      JR NZ,$FC ; 0x1F0
[0x000001F4] 0x0D           DEC C
[0x000001F5] 0x20 0xF9      JR NZ,$F9 ; 0x1F0
[0x000001F7] 0x3E 0x0D      LD A,$0D
[0x000001F9] 0xF3           DI
[0x000001FA] 0xE0 0x0F      LDH [$0F],A ; IF
[0x000001FC] 0xE0 0xFF      LDH [$FF],A ; IE
[0x000001FE] 0xAF           XOR A
[0x000001FF] 0xE0 0x42      LDH [$42],A ; SCY
[0x00000201] 0xE0 0x43      LDH [$43],A ; SCX
  1. The way we have to disassemble a ROM is by following the flow of instructions. For example, we know that the main program starts at position 0x150. So we should start disassembling there. Then we follow instruction by instruction until we hit any JUMP instruction (JP, JR, CALL, RET, etc). From that moment on the flow of the program is forked in two and we should follow both paths to disassemble.

The think to understand here is that if I show you a random memory position in a ROM, you cannot tell me if it is data or instructions. The only way to find out is by following the program flow. We need to define blocks of code that start in a jump destination and end in another jump instruction.

  1. gb-disasm skips any memory position that is not inside a code block. 0x16C marks the end of a block.

    [0x0000016C] 0x18 0xF6 JR $F6 ; 0x164

The next block starts on 0x1E8. We know that because it is the destination address of a jump located on 0x150.

[0x00000150] 0xC3 0xE8 0x01 JP $01E8
  1. Memory block from 0x16E until 0x1E8 is not consider a code block. That's why you don't see the memory position 0x01C0 listed as an instruction.

So there you are, it is very likely that you are interpreting the instructions in a wrong way. If you want to be 100% sure, you can disassemble the whole room and check if any instruction points to 0x16E-0x1E8 and reads it as raw data, such as a tile or something.

Please leave a comment if you agree with the analysis.

GabrielOshiro
  • 7,986
  • 4
  • 45
  • 57
  • I deleted my previous comment as I'd made a mistake. I noticed that the code in question appears to be part of the v-blank interrupt (starting at 0x40). I managed to use gb-disasm to start from the v-blank interrupt address, which produced, **[0x000001C0] 0xCD 0xB6 0xFF CALL $FFB6**. Can I take this to mean that it is indeed a valid instruction? – Triforcer Feb 06 '17 at 16:42
  • @Triforcer I'll try it too. I'll check if I get to the same conclusion as you. By the way, I don't think that there is any place where we test PC to see if it points outside of the ROM. I think you can even copy a subroutine to the RAM and then modify it on the fly. Not 100% sure... but back in the 80's witchery was a part of programming... So I would not discard it. Sorry for not addressing it on my original answer. – GabrielOshiro Feb 07 '17 at 01:09
  • I didn't delve too much into this game, but it might be the vblank interrupt routine ? You load it into hram, so you can wait for the DMA to be completed. And for GabrielOshiro's question, you can indeed do this. – Méga Lag Feb 07 '17 at 02:10
  • Also, while 99% of commercial ROMs actual code start at 0x150, this is not always the case. After the boot ROM, the PC is at 0x100, which most of the time is nop, jp 0x150, but not always. Several commercial ROMs jump at a different address (including a multiboot one). – Méga Lag Feb 07 '17 at 02:19