Skip to main content

Making CHIP-8 emulator in C

 Chip8 doc link | Components | Opcode Table

CHIP-8 programs are binary files, and your emulator must read them and operate on the bytes. You will also need a way to draw graphics to the screen and read keypresses. Many graphical libraries can do this for you or use something like SDL directly.

CHIP-8 components

Display

64 pixels wide and 32 pixels tall. Each pixel is a boolean value, or a bit; can be on or off (“off” pixel was just black, and “on” was white).

We’ll use SDL for rendering: SDL initialization
Not initialize:- returns -1 
Error message is stored in SDL_GetError

Initializing SDL
if(SDL_Init(SDL_INIT_VIDEO)!=0){
printf("SDL not initialized,%s\n", SDL_GetError);
exit(-1);
}
Initialize display
SDL_Window * window = SDL_CreateWindow( "chip8",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640,
320, 0x00000004);
SDL_Delay(5000);
SDL_Quit();

Now we get our display for 5 seconds.

Memory

All the memory is RAM. The memory should be 4 kB (4096 bytes), from location 0x000 (0) to 0xFFF (4095). The first 512 bytes, from 0x000 to 0x1FF, are where the original interpreter was located, so the ROM instructions must start at 0x200. CHIP-8’s index register and program counter can only address 12 bits, which is 4096 addresses. The PC should be set to 0x200 in the constructor because that will be the first instruction executed.

These machines had 4096 (0x1000) memory locations, CHIP-8 interpreter itself occupies the first 512 bytes. Programs begin at memory location 512 (0x200), the uppermost 256 bytes (0xF00–0xFFF) are reserved for display refresh, and the 96 bytes below that (0xEA0–0xEFF) are reserved for the call stack, internal use, and other variables.

Registers

CHIP-8 has 16 8-bit data registers named V0 to VF. The VF register doubles as a flag for some instructions; thus, it should be avoided. The address register, named I, is 12 bits wide and is used with several opcodes involving memory operations.

Stack

The stack is used to remember the current location before a jump is performed. So any time you perform a jump or call a subroutine, store the program counter in the stack before proceeding. The system has 16 levels of stack and in order to remember which level of the stack is used, you need to implement a stack pointer (sp).

stack[16]
sp

Keypad

Chip 8 has a HEX-based keypad (0x0-0xF), you can use an array to store the current state of the key. (key[16]).

The CHIP-8 keypad is a 4x4 grid with keys labeled from 0x0 to 0xF in hexadecimal:

123C -> 1234
456D -> QWER
789E -> ASDF
A0BF -> ZXCV

Timers

CHIP-8 has two timers. They both count down at 60 hertz until they reach 0.
Delay timer: Timing the events of games. Its value can be set and read.
Sound timer: Sound effects. When its value is nonzero, a beeping sound is made. Its value can only be set.

Font

  • Fonts in CHIP-8 are for displaying characters on the screen, specifically hexadecimal digits (0-9 and A-F), which are used in many CHIP-8 programs (like displaying numbers). In hexadecimal, this would be:
0xF0, // ****
0x90, // * *
0x90, // * *
0x90, // * *
0xF0 // ****
    0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
0x20, 0x60, 0x20, 0x20, 0x70, // 1
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
0xF0, 0x80, 0xF0, 0x80, 0x80 // F

Input

Hex keyboard that has 16 keys ranging from 0 to F. The ‘8’, ‘4’, ‘6’, and ‘2’ keys are typically used for directional input. Three opcodes are used to detect input. One skips an instruction if a specific key is pressed, while another does the same if a particular key is not pressed. The third waits for a key press and then stores it in one of the data registers.

Opcode table (operation codes)

Opcodes are the binary-coded instructions that the CHIP-8 interpreter understands and executes. CHIP-8 has 35 opcodes, which are all two bytes long, in hexadecimal, and with the following symbols:

  • NNN: address
  • NN: 8-bit constant
  • N: 4-bit constant
  • X and Y: 4-bit register identifier
  • PC: Program Counter
  • I: 12bit register (For memory address) (Similar to void pointer);
  • VN: One of the 16 available variables. N may be 0 to F (hexadecimal);

Example Opcodes for Space Invaders

6000       ; Set V0 to 0 (initialize register V0)
6105 ; Set V1 to 5 (initialize register V1)
A300 ; Set I to memory address 0x300 (sprite location)
D015 ; Draw sprite at (V0, V1) with height 5 pixels
7001 ; Add 1 to V0 (move object horizontally)
F00A ; Wait for a keypress and store it in V0
E09E ; Skip next instruction if key in V0 is pressed
1200 ; Jump to 0x200 (game loop start)
  • 6000: Initialize register V0 with the value 0 (x-coordinate for sprite).
  • 6105: Initialize register V1 with the value 5 (y-coordinate for sprite).
  • A300: Load the index register I with the memory address of a sprite (e.g., player or alien).
  • D015: Draw the sprite located at I to screen at coordinates (V0, V1) with a height of 5 pixels.
  • 7001: Move the sprite by adding 1 to V0 (horizontal movement).
  • F00A: Pause execution and wait for a key press.
  • E09E: Conditional check: if the key stored in V0 is pressed, skip the next instruction.
  • 1200: Jump to the address 0x200, looping back to the game logic.

Opcode table

Build

Program Counter → 2 bytes
Index Register → 2 bytes

  • Using struct, declare memory, register, program counter, index register, stack, stack pointer, etc...
  • Add font. Use memcpy to copy the font to the memory(0x000 to 0x1FF)
  • Clear the memory, registers, and screen, write an initialization function, and “clear” memory by using memset, and setting all the bytes in the memory block to 0.
  • Write a function to open the program file and check if the file is opened.
  • Copy the program into the memory. Use “fread” to read the instructions.
uint16_t readByte= fread(c8->memory+0x200, 1, sizeof(c8->memory)-0x200, file);
  • Point PC to the address 0x200.
  • Write a loop to fetch, decode and execute the opcodes.
  • Fetch:
uint16_t opcode= c8->memory[c8->PC] << 8 | c8->memory[c8->PC+1];
//working
opcode = (0xA2 << 8) | 0xF0
= 0xA200 | 0x00F0
= 0xA2F0
  • Decode & execute
Opcode:   XXXX
Bits: 4 4 4 4
Meaning: Type Nibble1 Nibble2 Nibble3
0x6A12: 0x6 0xA 0x1 0x2
    NNN:     address
NN: 8-bit constant
N: 4-bit constant
X and Y: 4-bit register identifier
PC : Program Counter
I : 12bit register (For memory address) (Similar to void pointer);
VN: One of the 16 available variables. N may be 0 to F (hexadecimal);


Think of these like parts of a recipe:

NNN: The location in your cookbook (address).
NN: A small ingredient quantity (8-bit).
N: A pinch of an ingredient (4-bit).
X and Y: The specific bowls (registers) you're working with.
PC: The page number you're on.
I: The address in memory where the recipe details are stored.
VN: The 16 small bowls (registers) where you hold temporary data.
  • Opcode Parsing Decoding: The 16-bit components of the opcode are parsed into individual components.
uint8_t type = opcode&0xF000;
uint16_t NNN = opcode&0x0FFF;
uint8_t NN = opcode&0x00FF;
uint8_t N = opcode&0x000F;
uint16_t X = opcode&0x0F00;
uint16_t Y= opcode&0x00F0;
  • Opcode Execution” or “Instruction Execution”: implement a switch statement to handle each opcode and execute the corresponding instruction logic. GitHub Code.
 //Check my GitHub code. I've commented on how each instruction works

switch(type){//DEcode first nibble
case 0x0:
{
switch(NNN){// handle special case for 0x0NNN
case 0x00E0:{
}

default:
break;
}

break;
}

case 0x1:{
c8->PC=NNN;//sets the PC to nnn.

break;
}
default:
printf("unknown opcode: 0x%04X\n", opcode);
}
  • Complete the opcodes till DXYN.
  • Increment PC by 2 bytes. Stop the emulator loop if the PC exceeds valid memory (PC > 0xFFF).
  • Inside the main function:
  • Allocate memory for chip8 struct using malloc. Since malloc is a runtime function, the memory cannot be allocated at compile time, and thus it must be done during program execution. 
  • Call the initialize function and pass the filename to the above function containing file read and opcodes.
  • (Go through my GitHub code while reading this for better understanding).
  • Initialize SDL.
  • Create window, create renderer and texture.
  • Write an event loop to ensure the window Runs until the program execution is complete.
  • Initializes an array of 2048 elements (64x32 resolution) where each element represents a pixel, and sets the initial value to 0 (off).
  • Loops through each row and column of the chip8 display array to retrieve values of each pixel.
  • Write a condition to set the pixel in the array above declared array to white (0xFFFFFFFF) if the pixel value is non-zero, and to black (0xFF000000) if the value is zero. Then render it.
uint8_t pixel = chip8->display[i * 64 + j]; 
pixels[i * 64 + j] = pixel? 0xFFFFFFFF : 0xFF000000;
  • Clean up the texture, renderer, and window, quit SDL, and free the allocated memory.
  • You can now run the IBMLogo program, and it will work.
IBM LOGO

Now Let's Map Keys

123C -> 1234
456D -> QWER
789E -> ASDF
A0BF -> ZXCV
  • When you press a key a scan code(often represented in hexadecimal) is sent that's how the computer knows which key is pressed.
  • Use SDLK_ to map the qwerty keyboard with the chip8 keyboard layout.
  • In the event loop, use SDL_KEYDOWN to check if any key is pressed.
  • If true then write a loop to check which key is pressed using keycode.
  • Check if the key code generated by keypress matches SDL-defined constant (e.g., SDLK_a) in KeyMap. If true then change the state of the key to pressed.
  • Now write the opcodes for keypress(Ex9E, ExA1, Fx0A). Check my GitHub to see how it is executed.
  • Use the [instruction Doc] & [opcode table] to complete the opcodes.
  • Test if the opcodes are working using: test_opcode.ch8
test opcode

Reference


Comments

Popular posts from this blog

Bug Boundy Methodology, Tools & Resources

Start by defining a clear objective, such as exploiting a remote code execution (RCE) vulnerability or bypassing authentication on your target. Then, consider how you can achieve this goal using various attack vectors like XSS, SSRF, or others - these are simply tools to help you reach your objective. Use the target as how a normal user would, while browsing keep these questions in mind: 1)How does the app pass data? 2)How/where does the app talk about users? 3)Does the app have multi-tenancy or user levels? 4)Does the app have a unique threat model? 5)Has there been past security research & vulnerabilities? 6)How does the app handle XSS, CSRF, and code injection?

Install & set up mitmweb or mitmproxy in Linux

Step 1: Go to the mitmproxy page and download the binaries. Step 2: Install the downloaded tar file with the command " tar -xzf <filename>.tar.gz " Step 3: In the FoxyProxy add the proxy 127.0.0.1:8080  and turn it on. Step 4 : In the terminal run command " ./mitmweb " Step 5: Go to the page  http://mitm.it/   and download the mitmproxy's Certificate. Step 6: If you downloaded the certificate for Firefox, then go to " settings -> Privacy & Security -> Click View Certificates -> Click  Import ", then import the certificate.  Step 7: Now you are ready to capture the web traffic. Step 8 : In terminal run " ./mitmweb"

pip error in Kali Linux: error: externally-managed-environment : SOLVED

 error: externally-managed-environment × This environment is externally managed ╰─> To install Python packages system-wide, try apt install     python3-xyz, where xyz is the package you are trying to     install.     If you wish to install a non-Kali-packaged Python package,     create a virtual environment using python3 -m venv path/to/venv.     Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make     sure you have pypy3-venv installed.     If you wish to install a non-Kali-packaged Python application,     it may be easiest to use pipx install xyz, which will manage a     virtual environment for you. Make sure you have pipx installed.     For more information, refer to the following:     * https://www.kali.org/docs/general-use/python3-external-packages/     * /usr/share/doc/python3.12/README.venv note: If you believe this is a mistake, please contac...