As an alternative to an emulator, this tool translates CHIP-8 binaries into equivalent C code, which can be compiled on POSIX-compliant OSes with ncurses.
This is just a proof of concept, and the way it works could be still improved (see Details).
Compiling the tool:
make
Translating a CHIP-8 binary:
./chip8c breakout.ch8
Running the above will produce 3 files:
- mem.h - memory map of the binary
- breakout.ch8.c - translated C
- breakout.ch8.bin - runnable binary
CHIP-8 has its own keyboard layout. To interact with the binary, use mapped keys:
1 2 3 C --mapped as--> 1 2 3 4
4 5 6 D Q W E R
7 8 9 E A S D F
A 0 B F Z X C V
Some CHIP-8 executables can be found on repos like
this one. The ones that
were tested and work well include pong
, breakout
and trip8
demo.
What the tool was created for, is to check whether it's feasible to decompile machine code into a long switch-case, where the switched value is the program counter register, and the cases are instruction addresses coupled with code performing register and IO manipulation equivalent to that of a given instruction set.
Insides of the resulting switch-case look like the following example:
switch (pc) {
/* ... */
case 0x0200: reg[0] = reg[6];
case 0x0202: reg[1] = 0xFC;
case 0x0204: reg[0] &= reg[1];
case 0x0206: regi = 0x30C;
/* ... */
case 0x0242: reg[0] = 0xFE;
case 0x0244: reg[9] ^= reg[0];
case 0x0246: stack[sp++] = 0x0246 + 2; pc = 0x2A4; break;
case 0x0248: reg[5] += 0x01;
case 0x024A: stack[sp++] = 0x024A + 2; pc = 0x2A4; break;
case 0x024C: if (reg[5] != 0x60) { pc = 0x024C + 4; break; }
/* ... */
}
Worth noting, that breaking out from the switch-case is required only when jumps are performed, in all other cases it suffices to allow for the default linear flow.
There are a few disadvantages of the technique:
- Code gets separated from data, so whatever takes advantage of von Neumann architecture won't work.
- A hexdump of a translated binary must be included in the resulting C file, so it could access its static data.
- Jumps to unexpected addresses aren't handled (odd addresses, for instance).
This implementation uses ncurses for IO, which was chosen for its popularity
and simplicity, but it's not a good fit for mimicking IO of CHIP-8. For instance,
the instructions EX9E
and EXA1
work best if the program maintains a map of
keys that are currently pressed. It seems that ncurses doesn't detect key
releases, so its hard to maintain such a map.
Also, it would be nice if Unicode block characters could be used for the
output, then neighbouring pairs of pixels could be packed into one of:
, ▀
, ▄
and █
to simulate nice looking square pixels.