2 stable releases
1.0.1 | Mar 26, 2024 |
---|---|
1.0.0 | Mar 24, 2024 |
#259 in Command-line interface
55 downloads per month
365KB
7.5K
SLoC
C8
CHIP-8 / S-CHIP / XO-CHIP tui toolkit featuring a virtual machine, debugger, and disassembler
About • Features • Installation • Usage • Motivation
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 byclassic
,chip8
,schip
, orxochip
to force other CHIP-8 variants if auto-select fails - add
--hz
followed by your target instructions per second if needed
- add
- 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 eitherchip8
,classic
,schip
, orxochip
- If
--kind
is not specified, c8 will make a best guess of the CHIP-8 variant
- If
- 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:
[!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
- To expand the output panel, use the
- The program display
- To toggle the program display, use the
show display
orhide display
command
- To toggle the program display, use the
- 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
orhide memory -v
command - To navigate to a specific memory address, use the
goto
command followed bypc
,i
, or a specific address - To follow a pointer in memory, use the
follow
command followed by a pointer (e.g.pc
ori
) - 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
- To expand the memory panel, use the
- The program history
- To focus onto history panel, use the
history
command
- To focus onto history panel, use the
[!TIP] When focused on a panel (e.g. memory), You can seek to the start or the end using the
Home
andEnd
keys. Use theEsc
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:
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
orcontinue
with a past program state (reachable usingundo
), all future program states are cleared and execution will advance. If instead you would like to replay those future states, useredo
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. Typekey --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
~16–45MB
~575K SLoC