#chip8 #interpreter #emulation #dissassembler

app c8

🎮 CHIP-8 / S-CHIP / XO-CHIP tui toolkit featuring a virtual machine, debugger, and disassembler

2 stable releases

1.0.1 Mar 26, 2024
1.0.0 Mar 24, 2024

#119 in Debugging

Download history 220/week @ 2024-03-23 43/week @ 2024-03-30 6/week @ 2024-04-06 1/week @ 2024-04-13

55 downloads per month

MIT license

365KB
7.5K SLoC

C8

CHIP-8 / S-CHIP / XO-CHIP tui toolkit featuring a virtual machine, debugger, and disassembler

AboutFeaturesInstallationUsageMotivation


C8 Screenshot


Table of Contents

About

C8 is a terminal user interface tooklit made to run, debug, and disassemble CHIP-8, S-CHIP, and XO-CHIP games. At its core you can:

  • run a rom with c8 run [ROM_PATH]
    • add --debug to enable debug mode
    • add --kind followed by classic, chip8, schip, or xochip to force other CHIP-8 variants if auto-select fails
    • add --hz followed by your target instructions per second if needed
  • disassemble a rom into a file with c8 dasm [ROM_PATH] > [OUTPUT_FILE_PATH]
  • check a rom for potential issues* with c8 check [ROM_PATH]

Features At A Glance

Feature C8
Full Chip-8 + Classic, S-CHIP, and XO-Chip Support
Full Sound Support
Full 4-bit Color Support
debug Undo, Redo, and Step through Execution
debug Virtual Machine State Inspection
debug Register and Address Watchpoints
debug Instruction Breakpoints
debug Program Execution History
debug Keyboard State Modification
debug Dump Current Program Memory State
Static Tracing Disassembler
Configurable Execution Speed
Compatibility Profiles
Pre-defined and Custom Color Palettes 🚧
Individual Configurable Quirks 🚧
Features labeled debug are only available in debug mode

Installation

C8 is tested mostly on Windows but should work on Mac and Linux. If on Linux, see below for required system packages before you continue.

At this moment C8 can only be installed from cargo or built from source.

Install With Cargo

C8 is published on crates.io and can be installed with cargo. Rust 1.70.0 or greater is required.

cargo install c8

Build From Source

C8 can be built from source with cargo. Rust 1.70.0 or greater is required.

git clone https://github.com/tochiu/c8.git
cd c8
cargo install --path ./

To be sure C8 is installed, run the classic IBM Logo ROM from the repository directory

c8 run roms/c8/ibm_logo.ch8

Installation Caveats

On Linux, the X11 development libraries are required to query keyboard state from the OS since terminals generally do not support key up events. In addition, the Advanced Linux Sound Architecture (ALSA) development libraries are required on the system.

On Ubuntu/Debian:

sudo apt install libx11-dev
sudo apt install librust-alsa-sys-dev

On Fedora/RHEL/CentOS:

sudo dnf install xorg-x11-server-devel
sudo dnf install alsa-lib-devel

On newer versions of MacOS, you may run into issues where you only see meta keys such as shift, backspace, et cetera. This is due to a permission issue. To work around this:

  • open the MacOS system preferences
  • go to Security -> Privacy
  • scroll down to Accessibility and unlock it
  • add your terminal to the list

Usage

Running

To run a CHIP-8 program, use the c8 run command followed by the path to the program.

  • If you require the program runs at a specified frequency add the --hz flag followed by a target instructions per second (IPS) value
  • To specify a CHIP-8 variant, add a --kind flag followed by either chip8, classic, schip, or xochip
    • If --kind is not specified, c8 will make a best guess of the CHIP-8 variant
  • To load the program into the debugger, add the --debug flag

[!IMPORTANT] The classic variant is not a full COSMAC VIP emulator but instead just the quirk settings from CHIP-8 on a VIP.

For example:

c8 run roms/xo/super_neatboy.ch8 --hz 50000 --kind xochip

will run the Super Neatboy rom at 50000 IPS on the XO-CHIP variant. In the above example, the --kind flag is not necessary since C8 will auto-select the XO-CHIP variant.

Disassembling

The C8 disassembler is a static tracing disassembler. It will not execute the program to disassemble it but will instead trace the program from the starting address through all possible branches to determine what regions of memory are code and what regions are data. From there, it will output a view of program memory with the disassembled instructions alongside the raw memory data. Because this is a static analysis of the program, self-modifying code will not dissassemble quite well. The dissassembler will not always be certain whether a given address is an instruction or not (see: The Halting Problem). Each address is annotated with a label indicating the confidence level of that address being an instruction. The labels are as follows:

Symbol Label Description
' ' NOT The address is not an instruction
'?' PARSABLE The address can be parsed as a valid instruction
'*' REACHABLE The address can be parsed as a valid instruction and can theoretically be executed by the program
'O' VALID The address can be parsed as a valid instruction and can theoretically be executed by the program, and can be part of at least one static execution path
'X' PROVEN The address is an instruction that is part of at least one static execution path

A static execution path is a sequence of instructions beginning at the starting instruction address (0x200) that can be executed by the program without a dependence on the state of the virtual machine.

For example, an instruction that jumps to a specific location in memory from the starting address will create a new static (X label) execution path starting from that location. The disassembler will follow all possible static execution paths to determine the confidence level of each address.

But if an instruction jumps to a location in memory that is determined by something like a value in a register, the disassembler will create a new reachable (* label) execution path starting from all possible jump locations. This is because the disassembler cannot determine the value of the register at the time of disassembly. From there, the disassembler will follow all possible execution paths to determine the confidence level of each address. Some addresses will be promoted to valid (O label) if they are lead back to at least one static execution path.

To disassemble a CHIP-8 program, use the c8 dasm command followed by the path to the program. This will print the disassembled program to the standard output. Add a --kind flag to specify the CHIP-8 variant. For example:

c8 dasm roms/ch8/ibm_logo.ch8

will dissasemble the ibm_logo rom and output the following:

0x200 cls                    # X 00E0     clear
0x202 ld   i 0x22A           # X A22A     i = 0x22A
0x204 ld   v0 12             # X 600C     v0 = 12
0x206 ld   v1 8              # X 6108     v1 = 8
0x208 drw  v0 v1 15          # X D01F     draw 8x15 @ v0,v1
0x20A add  v0 9              # X 7009     v0 += 9
0x20C ld   i 0x239           # X A239     i = 0x239
0x20E drw  v0 v1 15          # X D01F     draw 8x15 @ v0,v1
0x210 ld   i 0x248           # X A248     i = 0x248
0x212 add  v0 8              # X 7008     v0 += 8
0x214 drw  v0 v1 15          # X D01F     draw 8x15 @ v0,v1
0x216 add  v0 4              # X 7004     v0 += 4
0x218 ld   i 0x257           # X A257     i = 0x257
0x21A drw  v0 v1 15          # X D01F     draw 8x15 @ v0,v1
0x21C add  v0 8              # X 7008     v0 += 8
0x21E ld   i 0x266           # X A266     i = 0x266
0x220 drw  v0 v1 15          # X D01F     draw 8x15 @ v0,v1
0x222 add  v0 8              # X 7008     v0 += 8
0x224 ld   i 0x275           # X A275     i = 0x275
0x226 drw  v0 v1 15          # X D01F     draw 8x15 @ v0,v1
0x228 jp   0x228             # X 1228
0x22A                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x22B                        #   00       2X GRAPHIC ................
0x22C                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x22D                        #   00       2X GRAPHIC ................
0x22E                        # ? 3C00     2X GRAPHIC ....@@@@@@@@....
0x22F                        #   00       2X GRAPHIC ................
0x230                        # ? 3C00     2X GRAPHIC ....@@@@@@@@....
0x231                        #   00       2X GRAPHIC ................
0x232                        # ? 3C00     2X GRAPHIC ....@@@@@@@@....
0x233                        #   00       2X GRAPHIC ................
0x234                        # ? 3C00     2X GRAPHIC ....@@@@@@@@....
0x235                        #   00       2X GRAPHIC ................
0x236                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x237                        #   00       2X GRAPHIC ................
0x238                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x239                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x23A                        #   00       2X GRAPHIC ................
0x23B                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x23C                        #   00       2X GRAPHIC ................
0x23D                        # ? 3800     2X GRAPHIC ....@@@@@@......
0x23E                        #   00       2X GRAPHIC ................
0x23F                        # ? 3F00     2X GRAPHIC ....@@@@@@@@@@@@
0x240                        #   00       2X GRAPHIC ................
0x241                        # ? 3F00     2X GRAPHIC ....@@@@@@@@@@@@
0x242                        #   00       2X GRAPHIC ................
0x243                        # ? 3800     2X GRAPHIC ....@@@@@@......
0x244                        #   00       2X GRAPHIC ................
0x245                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x246                        #   00       2X GRAPHIC ................
0x247                        #   FF       2X GRAPHIC @@@@@@@@@@@@@@@@
0x248                        # ? 8000     2X GRAPHIC @@..............
0x249                        # ? 00E0     2X GRAPHIC ................
0x24A                        #   E0       2X GRAPHIC @@@@@@..........
0x24B                        # ? 00E0     2X GRAPHIC ................
0x24C                        #   E0       2X GRAPHIC @@@@@@..........
0x24D                        #   00       2X GRAPHIC ................
0x24E                        # ? 8000     2X GRAPHIC @@..............
0x24F                        #   00       2X GRAPHIC ................
0x250                        # ? 8000     2X GRAPHIC @@..............
0x251                        # ? 00E0     2X GRAPHIC ................
0x252                        #   E0       2X GRAPHIC @@@@@@..........
0x253                        # ? 00E0     2X GRAPHIC ................
0x254                        #   E0       2X GRAPHIC @@@@@@..........
0x255                        #   00       2X GRAPHIC ................
0x256                        #   80       2X GRAPHIC @@..............
0x257                        #   F8       2X GRAPHIC @@@@@@@@@@......
0x258                        #   00       2X GRAPHIC ................
0x259                        #   FC       2X GRAPHIC @@@@@@@@@@@@....
0x25A                        #   00       2X GRAPHIC ................
0x25B                        # ? 3E00     2X GRAPHIC ....@@@@@@@@@@..
0x25C                        #   00       2X GRAPHIC ................
0x25D                        # ? 3F00     2X GRAPHIC ....@@@@@@@@@@@@
0x25E                        #   00       2X GRAPHIC ................
0x25F                        # ? 3B00     2X GRAPHIC ....@@@@@@..@@@@
0x260                        #   00       2X GRAPHIC ................
0x261                        # ? 3900     2X GRAPHIC ....@@@@@@....@@
0x262                        #   00       2X GRAPHIC ................
0x263                        #   F8       2X GRAPHIC @@@@@@@@@@......
0x264                        #   00       2X GRAPHIC ................
0x265                        #   F8       2X GRAPHIC @@@@@@@@@@......
0x266                        #   03       2X GRAPHIC ............@@@@
0x267                        #   00       2X GRAPHIC ................
0x268                        #   07       2X GRAPHIC ..........@@@@@@
0x269                        #   00       2X GRAPHIC ................
0x26A                        #   0F       2X GRAPHIC ........@@@@@@@@
0x26B                        #   00       2X GRAPHIC ................
0x26C                        # ? BF00     2X GRAPHIC @@..@@@@@@@@@@@@
0x26D                        #   00       2X GRAPHIC ................
0x26E                        #   FB       2X GRAPHIC @@@@@@@@@@..@@@@
0x26F                        #   00       2X GRAPHIC ................
0x270                        #   F3       2X GRAPHIC @@@@@@@@....@@@@
0x271                        #   00       2X GRAPHIC ................
0x272                        #   E3       2X GRAPHIC @@@@@@......@@@@
0x273                        #   00       2X GRAPHIC ................
0x274                        # ? 43E0     2X GRAPHIC ..@@........@@@@
0x275                        #   E0       2X GRAPHIC @@@@@@..........
0x276                        # ? 00E0     2X GRAPHIC ................
0x277                        #   E0       2X GRAPHIC @@@@@@..........
0x278                        #   00       2X GRAPHIC ................
0x279                        # ? 8000     2X GRAPHIC @@..............
0x27A                        #   00       2X GRAPHIC ................
0x27B                        # ? 8000     2X GRAPHIC @@..............
0x27C                        #   00       2X GRAPHIC ................
0x27D                        # ? 8000     2X GRAPHIC @@..............
0x27E                        #   00       2X GRAPHIC ................
0x27F                        # ? 8000     2X GRAPHIC @@..............
0x280                        # ? 00E0     2X GRAPHIC ................
0x281                        #   E0       2X GRAPHIC @@@@@@..........
0x282                        # ? 00E0     2X GRAPHIC ................
0x283                        #   E0       2X GRAPHIC @@@@@@..........

Each relevant address is printed with the instruction if needed.

Past the # symbol is the label of that address. Because ibm_logo is a simple program, every address is labelled either a proven instruction (X label) or data with the occasional ? label if it happens to be parsable as an instruction.

Following the label is the value at that address in hexadecimal:

  • If the address is an instruction, this is as long as the instruction is with overlapping addresses being collapsed
  • If the address is data, this one byte

Last is a description of the instruction or data:

  • If the address is an instruction then a description of the instruction is printed
  • If the address is data then a visual representation of the byte data is printed

In this case, the visual representation of the data region showcases the IBM graphic that is saved in the rom.

[!NOTE] This dissassembler doubles as a memory viewer. The memory panel in the debugger is simply an up-to-date dissasembly of program memory.

c8 check is a tool built on top of the disassembler that checks a rom for bad execution branches. It accomplishes this by running the disassembler on the program and logging areas where proven (X label) or valid (O label) instructions can lead to executing an invalid instruction.

Debugging

Start the Debugger

In order to debug a CHIP-8 program, run

c8 run [PATH_TO_PROGRAM] --debug

This will start the debugger with the program loaded in a paused state right before the first instruction is executed. Use the help command to see the full list of commands. At any point, press Ctrl+C to exit.

Navigate the Debugger

Below is what the debugger looks like when it first starts:

C8 Debugger Screenshot

[!TIP] Make sure the terminal window is as big as possible to ensure every panel is visible.

At a glance there will be:

  • A command line interface with an output
    • To expand the output panel, use the output command
  • The program display
    • To toggle the program display, use the show display or hide display command
  • The program keyboard state, register state, timer state, and stack
  • The program memory layout
    • To expand the memory panel, use the memory command
    • To toggle a verbose view of the memory layout, use the show memory -v or hide memory -v command
    • To navigate to a specific memory address, use the goto command followed by pc, i, or a specific address
    • To follow a pointer in memory, use the follow command followed by a pointer (e.g. pc or i)
    • To unfollow the currently followed pointer, use the unfollow command
    • To dump the entire memory view to a file, use the dump memory command followed by a file path
  • The program history
    • To focus onto history panel, use the history command

[!TIP] When focused on a panel (e.g. memory), You can seek to the start or the end using the Home and End keys. Use the Esc key to return to the command line interface.

On SCHIP and XOCHIP programs, the high resolution graphics mode alters the UI layout to look like the following:

C8 Debugger Screenshot (Hi-Res Display)

Control Program Execution

Run the program:

Start the execution of your program with the continue command:

(c8db) continue

This will minimize the debugger and run the program until a debug event is triggered or execution is paused. Press Esc to pause execution and return to the debugger.

[!IMPORTANT] A debug event is a trigger that interrupts program execution and drops into the debugger window. The features in C8DB that trigger debug events are watchpoints and breakpoints.

  • A breakpoint is set to trigger right before an instruction at a specified address is executed
  • A watchpoint is set to trigger right after a specified register or address is modified

Step through the program:

Use step to execute the next instruction. Follow it with an integer n to execute the next n instructions. This will be interrupted if a debug event is triggered. For example:

(c8db) step 50

will execute the next 50 instructions.

[!NOTE] If you use step or continue with a past program state (reachable using undo), all future program states are cleared and execution will advance. If instead you would like to replay those future states, use redo instead.

[!TIP] If you are stuck on an instruction because it is polling for a key event, use the key command to simulate key events. Type key --help for more information.

Seek through execution history:

Use undo and redo to seek through the program execution history. Follow it with an integer n to rewind or fast-forward through the last n program states.

Alternatively, use the history command to focus onto the program history panel. Use the W/S or Up/Down keys to seek through program execution. This is just a graphical layer over the undo and redo commands.

redo is a particularly special command. Technically, it doesn't simply execute the next instruction, since the execution of some instructions are non-deterministic with respect to the program state, e.g. user input or RNG. If necessary, certain properties are stored between executing instructions in order to properly replay it. That is what redo utilizes.

If you are in a specific program state and instead of replaying, you want to execute the program from that point, use step or continue instead.

Set execution speed:

Use hertz followed by n, where n is the target speed in instructions per second, to set the program execution speed. For example:

(c8db) hertz 60

will execute the program at a rate of 60 instructions per second.

[!IMPORTANT] C8 runs at a fixed frame-rate of 60hz. If your target execution speed is expressed in cycles per frame, multiply it by 60 to get the equivalent instructions per second.

Breakpoints and Watchpoints

Sometimes it is useful to pause execution when a certain condition is met. This is where breakpoints and watchpoints come in. A breakpoint is set to trigger right before an instruction at a specified address is executed. A watchpoint is set to trigger right after a specified register or address is modified.

Set a breakpoint:

Use break followed by an address to set a breakpoint. For example:

(c8db) break 0x200

will set a breakpoint at address 0x200. Once the program reaches this address, execution will pause and drop into the debugger. To list all breakpoints, type info break. To remove a breakpoint, use the clear command. In this example:

(c8db) clear break 0x200

will remove the breakpoint at address 0x200. To clear all breakpoints, type clear all break.

Set a watchpoint:

Use watch followed by a register or address to set a watchpoint. For example:

(c8db) watch i

will set a watchpoint on register i. Once this register is modified, execution will pause and drop into the debugger. If we set a watchpoint on an adresss instead, execution will pause when that address is written to. To list all watchpoints, type info watch. To remove a watchpoint, use the clear command. In this example:

(c8db) clear watch i

will remove the watchpoint on register i. To clear all watchpoints, type clear all watch.

Motivation

This is my first completed rust project (haha). A friend of mine sent me an article on how to get started with writing emulators with CHIP-8. It was a super interesting read and a good excuse to learn Rust! After I finished the emulator, I thought I could go further. So here we are. If you're thinking about writing your own CHIP-8 emulator, you should! It's a great start to emulation development and building on top of it with other CHIP-8 variants is an excellent exercise in writing extensible software.

Dependencies

~15–50MB
~600K SLoC