Modern Linux Rootkit with Kernel Modules and Ftrace – Introduction

The Linux kernel is a messy complicated codebase that totals nearly 28 million lines of code1 as of December 2023. Of course this is incredibly daunting for anyone who wants to poke around the kernel internals (for whatever reasons )) or anyone who wants to get started with contributing to the development of the Linux kernel. While there are multiple ways to get acquainted with this monolithic codebase (one of the best being the book Linux Device Drivers, now freely available under the CC BY-SA 2.0 license), we’re going to start by writing a (somewhat mischievous) Linux kernel module.

Important note: I do not encourage you to use any potential knowledge you gleam here for nefarious purposes, nor am I responsible for what happens should you decide to do so. These blog posts are purely for educational purposes.


Table of Contents


Requirements

  • Basic knowledge of Linux
  • Programming knowledge

Kernel Modules

So what are kernel modules, exactly? Taken from the Arch Wiki, “Kernel Modules are pieces of code that can be loaded and unloaded into the kernel upon demand. They extend the functionality of the kernel without the need to reboot the system.” Being able to modify the functionality of the kernel at runtime is a tasty ability indeed. With this you could make the kernel do… pretty much anything you want. You can hide files, directories, processes, open ports, other kernel modules, give yourself a backdoor to root, and a million other useful things. Of course the simple “rootkit” we make in this series won’t be anything particularly fancy, and will primarily just be for demonstration and educational purposes, mainly because I don’t want to go to jail.

Rootkits

Ok, so what is a rootkit then? Taken from Wikipedia, “A rootkit is a collection of computer software, typically malicious, designed to enable access to a computer or an area of its software that is not otherwise allowed (for example, to an unauthorized user) and often masks its existence or the existence of other software.” Knowing this you can see how kernel modules are a perfect fit for this type of software, though there are other ways one can potentially make a rootkit. There are rootkits that take advantage of the dynamic linker, rootkits that take advantage of firmware, rootkits that take advantage of DKOM, hypervisor rootkits, eBPF rootkits, and probably many others2.

Rootkits don’t even necessarily have to run with kernel level privileges either. You could very possibly make a rootkit that operates perfectly fine within userland (though doing so does make it less “root”, doesn’t it?). After all most functions of a kernel level rootkit are possible within userland. Doing so does give one significantly less power, however. Still, often times that power is unecessary.

A particularly interesting type of rootkit is one that extends beyond even the kernel and lives right in the boot process. These so called “bootkits” are incredibly powerful pieces of software which are unimaginably pesky to deal with, but they go far beyond the scope of this small and simple series. Still, if one is interested in the topic it is incredibly fascinating to look into.

Ftrace

Alright, so we know what rootkits are and the technique that we’re going to use to make one, but how exactly will our kernel module modify the Linux kernel to make it do what we want? This is where a fun little Linux utility called ftrace comes it.

Ftrace is a tracing framework released in 2008 for the purpose of helping developers find out what’s going on inside the Linux kernel. Although it’s name literally means “function tracer”, ftrace’s capabilities extend far beyond that and cover a much broader range of the kernel’s internal operations. Using ftrace we can hook particular system calls (more on that in the next post) such as open, read, write, close, etc. and replace them with out own custom code. How we do this exactly is a little complicated so we’ll save it for the next post, but it is all rather interesting believe me.

Now it’s important to note that ftrace is a utility, and not an inherent piece of the kernel. It can be disabled in the kernel configuration since it isn’t critical for any of the system’s functioning, leaving our little rootkit dead in the water if it is. Luckily for us however in practice many popular Linux distros keep ftrace enabled, as it doesn’t significantly affect system performance and can be quite useful for debugging purposes. Still it’s important to keep in mind that this technique may not work everywhere, so be sure to check if it’s supported beforehand.

Also keep in mind that ftrace isn’t the only way to modify the kernel to our wants and needs. There are many other ways to go about it, but they tend to be more complicated so for the sake of simplicity we’re going to go with ftrace.

Project Setup

Before we can move on to actually developing our rootkit we need to setup an environment to run it in. Any old Linux environment with ftrace enabled should do, so you could use VirtualBox, VMWare, anything really, even a real machine if you’re so inclined (though this is not advised). I however will be using a nifty piece of software called Vagrant. If you already have VirtualBox installed Vagrant will automatically use it for virtualization, without any configuration needed.

Getting started with Vagrant is dead simple:

mkdir Ubuntu
cd Ubuntu
vagrant init generic-x64/ubuntu2204
vagrant up
vagrant sshCode language: Bash (bash)

And just like that we have a working virtual environment for developing our rootkit. Of course we’re gonna need a couple more things to compile our code, so (if you’re following along entirely and using Ubuntu) just run these couple commands within the virtual machine:

sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)Code language: Bash (bash)

And we’re all good to go.


Note: if you’d prefer to program the rootkit on your host system (which is fair since your host system will probably have a much better text editor than the guest system) you can add:

config.vm.synced_folder "./src", "/vagrant", create: true, SharedFoldersEnableSymlinksCreate: falseCode language: JavaScript (javascript)

to the Vagrantfile before calling vagrant up (if you already called vagrant up you can simply call vagrant halt and call vagrant up again, or simply call vagrant reload) to create a folder that’s shared between the guest and host. This way you can create and edit files on your host system but compile them inside of the guest system with ease. It’s what I’ll be doing in this series.

Compiling Kernel Modules

With our environment setup all that’s left to do is create a basic kernel module. Create a Makefile and insert this simple code:

obj-m += rootkit.o

all:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Code language: LLVM IR (llvm)

Afterwards create a file called rootkit.c and insert this code:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("HandleHHandle");
MODULE_DESCRIPTION("A completely innocent kernel module");
MODULE_VERSION("0.01");

static int __init rootkit_init(void) {
  printk(KERN_INFO "Hello world\n");
  return 0;
}

static void __exit rootkit_exit(void) {
  printk(KERN_INFO "Goodbye world\n");
}

module_init(rootkit_init);
module_exit(rootkit_exit);
Code language: C++ (cpp)

This is perhaps the simplest kernel module that one could make. It’s fairly self-explanatory, but I’ll go over it quickly anyway.

At the top of the file is simply our includes. These includes give us access the the data structures and functions needed to interact with the kernel. After that is just a few macros that give some information about our kernel module.

After all that comes the two actually interesting functions, rootkit_init and rootkit_exit. The rootkit_init function is called when the module is loaded, and inversely rootkit_exit is called when the module is unloaded. rootkit_init will be perhaps the most important function in the entire codebase, after all it’s the entry point of our kernel module and sets up all of our hooks.

The module_init and module_exit functions both tell the kernel about the entry and exit functions (rootkit_init and rootkit_exit) respectively.

With all of that done you should be able to run make and find yourself with a shiny rootkit.ko file, which is our kernel module file. You can load that file into the kernel using sudo insmod rootkit.ko and unload it with sudo rmmod rootkit. Upon doing so and checking the kernel output buffer using sudo dmesg you should see the messages “Hello world” and “Goodbye world”. If you do then congratulations, you’ve successfully made your first kernel module.

Conclusion

In this post we created a simple kernel module as well as an environment to run it in. In the next post we’ll go over system calls, ftrace, and create a basic hook for the mkdir system call.

You can find all the code for this project at:
https://github.com/HandleHHandle/LKMRootkit

Resources

https://xcellerator.github.io/posts/linux_rootkits_01/
https://wiki.archlinux.org/title/Kernel_module
https://www.kernel.org/doc/html/v4.17/trace/ftrace.html

Footnotes

  1. Okok admittedly this is a bit misleading. While the entirety of the Linux kernel codebase does in fact have nearly 28 million lines of code, most of that doesn’t belong to the actual Linux kernel and instead represents the combined total code for drivers, architecture, and miscellaneous other things. ↩︎
  2. If there’s significant demand for any of these other techniques — or I just end up getting curious of and tinkering with them myself — I may just write another series of blog posts on said technique. ↩︎

Making a CHIP-8 emulator in Zig [Emulation Guide Series]

Writing an emulator is a good way to get more acquainted both with the more technical details of programming and the language you decide to write said emulator in. Zig is a young up-and-coming programming language that already makes a mighty fine alternative to various other systems-level programming languages such as C, Go, Rust, Etc. And so what better way to get acquainted with this new contender than to write an emulator?

Emulators are at their heart fairly basic things, but for more complex systems they can get rather, well, complex. For this reason we’ll start of this emulation series by emulating the CHIP-8 system using Zig and SDL2. CHIP-8 itself is a specification for a simple virtual machine for 2D games designed in the 1970s. It’s virtual because CHIP-8 was never implemented in hardware, only software (which I suppose makes this more of an interpreter instead of an emulator, but that’s neither here nor there).

CHIP-8 was designed so that different implementations of the virtual machine made on different hardware could all still run the same games, regardless of the different environments. That means that by the end of this guide you’ll be able to run games made all the way back in the 1970s, neat eh? All in all it’s rather primitive — especially by modern standards — but still a good place to get started for the topic of emulation.

This guide will have everything you’ll need to get a CHIP-8 emulator up and running, but keep in mind that it’s light on the details and explanations. The project shouldn’t take much longer than a day if you hack away at the whole thing.


Table of Contents


Requirements

  • Some programming experience
  • Basic computer science knowledge

Keep in mind that this guide is not a beginner’s programmer tutorial or an SDL2 tutorial. It is merely an introduction to the Zig programming language via implementing the CHIP-8 specification.

CHIP-8 Specifications

CHIP-8 has the following components:

  • Memory: 4 kilobytes of RAM
  • Display: 64 x 32 pixel monochrome display
  • Program Counter (PC): points to the current instruction in memory
  • Index (I): 16-bit index register used to point to locations in memory
  • Stack: A stack for 16-bit addresses, which is used to call functions and return from them
  • Delay Timer: An 8-bit timer which is decremented at a rate of 60 Hz until it reaches 0
  • Sound Timer: An 8-bit timer which functions like the delay timer, but which also gives off a beeping sound as long as it’s not 0
  • Registers: 16 8-bit general-purpose variable registers numbered 0 through F in hexadecimal, called V0 through VF

Note that the VF register is also called the flag register, and many instructions use it to signify various things, such as overflow in an addition operation.

Setup

If you haven’t already, go ahead and install Zig and create a new project. We’ll be using version 0.11.0 in this project.

To start all we need to do is link SDL2 to the project. Many languages make linking or wrapping C libraries an absolute pain but Zig makes it an absolute breeze, a joy even. It has built-in support for importing C header files (which I’ve found don’t even need to be modified at all unless you’re using obscure C features) and the build system itself uses Zig for logic, meaning we don’t have to fiddle with the likes of Make or CMake or Ninja or any other pain in the ass build system to get SDL2 up and running in our project.

Matter of fact if you’re on Linux you only need to do two things to get SDL2 linked to our project. First go to your build.zig file and add this snippet of code somewhere.

exe.linkSystemLibrary("SDL2");
exe.linkLibC();Code language: CSS (css)

And then create a new file in the project source called c.zig and insert this code:

// usingnamespace marks that everything in the cImport
// is brought into the namespace of the file and
// pub marks that everything brought in by our
// usingnamespace in public
pub usingnamespace @cImport({
  @cInclude("SDL2/SDL.h");
});Code language: JavaScript (javascript)

And that’s literally it. Well, you’ll also have to install your system-specific SDL2 dev packages, but besides that we’re done, SDL2 is linked to our project.

If you’re on Windows, uhhhhh, good luck figure it out yourself. ¯\_(ツ)_/¯

(There’s an SDL2 zig package on GitHub that should’ve made Windows setup easy, but I couldn’t get it working. It should still be pretty easy to set up on Windows though. All you should have to do is download the SDL2 dev DLLs and headers, add them somewhere in your project folder, and point Zig towards them. Poke around the Zig std docs to figure out how.)

Basic Display

Before we get started with the actual emulator we’re gonna need a display to draw things to. Obviously SDL is what we’ll be using for the display, as well as input and audio. To get started create a new source file called display.zig and insert this boilerplate code:


const std = @import("std");  // We'll be using this later
const c = @import("c.zig");

// pub marks the struct as public
pub const Display = struct {
  const Self = @This();
};Code language: JavaScript (javascript)

Zig is much like C in that everything is rather simple. There are no classes, there are no objects, no templates, no borrow checker or garbage collector, nothing of that nature. Pretty much everything is either a primitive, a function, or a struct, with some nice syntactic sugar that makes things easier to read and write.

Along the way we’ll see some code that looks somewhat object oriented, but keep in mind that’s just syntactic sugar. There is no inheritance or polymorphism (at least of the OOP kind) in Zig. This simplicity of things is what makes Zig so C-like and much less of a nightmare than either C++ or Rust.

Anyway, on to creating the actual display. Modify the display.zig file to look like this:

const std = @import("std");
const c = @import("c.zig");

pub const Display = struct {
  const Self = @This();

  window: *c.SDL_Window,
  open: bool,

  // These functions are that nice
  // syntactic sugar I was talking about
  pub fn create(
    title: [*]const u8,        // The string type in Zig
    width: i32,height: i32
  ) !Self {

  }

  pub fn free(self: *Self) void {

  }
};
Code language: PHP (php)

You can see that we added some fields for the SDL window and for keeping track of if the display is still open. Below that are some helper functions that will create and free our display struct.

You’ll notice an exclamation next to the return type of the create function. That signifies that the function is capable of returning an error, and that the caller of the function must handle it should it do so. We’ll see how that’s done later, for now let’s actually get these functions working

The entirety of the create function for now will look like this:

// Initialize SDL2
if(c.SDL_Init(c.SDL_INIT_VIDEO | c.SDL_INIT_AUDIO) != 0) {
  return error.SDLInitializationFailed;
}

// Create SDL2 window
var window = c.SDL_CreateWindow(
  title,
  c.SDL_WINDOWPOS_UNDEFINED,c.SDL_WINDOWPOS_UNDEFINED,
  width,height,
  c.SDL_WINDOW_SHOWN,
) orelse {
  c.SDL_Quit();
  return error.SDLWindowCreationFailed;
};

// Return Self type struct
return Self {
  .window = window,
  .open = true,
};Code language: JavaScript (javascript)

Pretty basic, all we’re doing is initializing SDL and creating a window. But you can see a few interesting things here. First is how easy Zig makes it to use C libraries. When we imported the SDL header file the Zig compiler automatically wrapped all of the structs, functions, and macros for us, ready to use.

Second is how Zig handles null values. Unless specifically specified, pointers in Zig can’t have null values, and if you’re dealing with a function that can potentially return a null value but your variable isn’t marked as being nullable you’ll have to handle that somehow. We do so by adding an “orelse” statement after our function which will be automatically called in the case SDL_CreateWindow fails for some reason and returns null.

Third is the error handling in Zig. At a glance it seems similar to the traditional try/catch kind of error handling but in reality it’s more akin to Rust’s style of error handling with enums and whatnot. That’s all just hidden behind syntactic sugar and compiler trickery however.

Moving on to the free function. We initialized SDL and created a window, so all we have to do is destroy the window and cleanup SDL.

c.SDL_DestroyWindow(self.window);
c.SDL_Quit();Code language: CSS (css)

Having just a create and free function isn’t enough though, I mean we’re gonna actually want to use our window, which with SDL involves setting up a loop and checking for input. To do that we’ll add one more function.

pub fn input(self: *Self) void {
  var event: c.SDL_Event = undefined;
  while(c.SDL_PollEvent(&event) != 0) {
    switch(event.@"type") {
      c.SDL_QUIT => {
        self.open = false;
      },
      else => {},
    }
  }
}Code language: PHP (php)

The input function loops through and handles SDL’s queue of events. Currently the only event we’re handling is the SDL_QUIT event, but we’ll add a couple more later. But for now we have enough to get a display working so head on over to the main.zig file, clear it out, and add this code:

const std = @import("std");  // We'll be using this later
const Display = @import("display.zig").Display;

pub fn main() !void {
  var display = try Display.create("CHIP-8", 800,400);
  defer display.free();

  // Display loop
  while(display.open) {
    display.input();
  }
}Code language: JavaScript (javascript)

The code should be mostly self explanatory. You should notice however the “try” and “defer” keywords. The try keyword tries to call a function that’s capable of throwing an error and passes the error on to the next call stack if it happens. The defer keyword is another little fun feature of Zig that allows us to defer a function call to the end of a code block. It makes freeing resources in instances like these a bit neater. But anyway with this we should have a working display upon running the program.

Drawing to the Display

We have a display, but it’s not really useful if we can’t draw anything to it. Luckily for us SDL has a built-in rendering API, but we’re gonna have to set up some other stuff before getting to that first. What we’re gonna do is instead of drawing to the screen directly and having to fumble around with SDL’s stuff to get things working exactly right we’re gonna instead create an intermediary Bitmap struct which will be what our CHIP-8 emulator actually draws to and from and from which our SDL display just copies the pixel data from. Create a new source file called bitmap.zig and add this boilerplate:

const std = @import("std");

pub const Bitmap = struct {
  const Self = @This();

  allocator: std.mem.Allocator,
  width: u8,
  height: u8,
  pixels: []u1,

  // Create Bitmap
  pub fn create(allocator: std.mem.Allocator, width: u8,height: u8) !Self {

  }

  // Free Bitmap
  pub fn free(self: *Self) void {

  }

  // Clear Bitmap to specified value
  pub fn clear(self: *Self, value: u1) void {

  }

  // Set pixel value at x,y coordinate
  pub fn setPixel(self: *Self, x: u8,y: u8) bool {

  }

  // Get pixel value at x,y coordinate
  pub fn getPixel(self: *Self, x: u8,y: u8) u1 {

  }
};
Code language: PHP (php)

Two things stand out here, first is std.mem.Allocator. Allocating memory in Zig is interesting, in other languages such as C you can just call malloc and get some memory but in Zig you have to use intermediary allocators provided to you by the standard library. The reason for this is so that you can choose precisely how allocations are done in functions in the standard library. It can be a little annoying to have to move allocator structs around all the time but the flexibility it provides can be life-saving.

The other thing which stands out is the setPixel function. The pixel display in CHIP-8 is a bit wonky. There are no instructions to directly set a pixel value in CHIP-8, only instructions to XOR the value at a particular pixel. Why it was built like this I don’t know — probably has something to do with it being made in the 1970s — but it’s how it works so we’re rolling with it. The setPixel function will XOR the value in the pixel array at the specified coordinates, and return true if the value was set to 0 (if a pixel was erased essentially) and return false otherwise.

The entirety of the Bitmap struct’s methods will look like this:

// Create Bitmap
pub fn create(allocator: std.mem.Allocator, width: u8,height: u8) !Self {
  // Allocate pixel array
  var pixels = try allocator.alloc(u1, @as(u16, width) * @as(u16, height));
  @memset(pixels, 0);

  return Self {
    // We save the allocator so we can free our allocated data with it later
    .allocator = allocator,
    .width = width,
    .height = height,
    .pixels = pixels,
  };
}

// Free Bitmap
pub fn free(self: *Self) void {
  // Free allocated data with allocator
  self.allocator.free(self.pixels);
}

// Clear Bitmap to specified value
pub fn clear(self: *Self, value: u1) void {
  @memset(self.pixels, value);
}

// Set pixel value at x,y coordinate
pub fn setPixel(self: *Self, x: u8,y: u8) bool {
  // Return if x or y is invalid
  if(x >= self.width or y >= self.height) return false;

  var index: u16 = @as(u16, x) + @as(u16, y) * @as(u16, self.width);
  self.pixels[index] ^= 1;
  return (self.pixels[index] == 0);
}

// Get pixel value at x,y coordinate
pub fn getPixel(self: *Self, x: u8,y: u8) u1 {
  // Return if x or y is invalid
  if(x >= self.width or y >= self.height) return 0;

  var index: u16 = @as(u16, x) + @as(u16, y) * @as(u16, self.width);
  return self.pixels[index];
}Code language: PHP (php)

The only real notable thing is all of the @as things everywhere. Zig’s typecasting is a bit wonky and complicated but for now just know that all it’s doing is typecasting the variables to the correct integer types for the multiplication of large numbers.

In the main.zig file add this code to the beginning of the main function:

// General purpose memory allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer {
  const leaked = gpa.deinit();
  if(leaked == .leak) {
    @panic("MEMORY LEAK");
  }
}

var bitmap = try Bitmap.create(allocator, 64,32);
defer bitmap.free();Code language: JavaScript (javascript)

One of the neat things about Zig’s allocators is that we can use them to automatically check for memory leaks in our program.

Oh, of course don’t forget to import the Bitmap struct at the beginning of the file

const Bitmap = @import("bitmap.zig").Bitmap;Code language: JavaScript (javascript)

So we have a bitmap of pixels, but we still need to actually draw it. Go back to the display.zig file and add these fields to the struct:

renderer: *c.SDL_Renderer,
framebuffer: *c.SDL_Texture,
framebuffer_width: u8,
framebuffer_height: u8,Code language: HTTP (http)

Modify the create function declaration:

pub fn create(
  title: [*]const u8,
  width: i32,height: i32,
  framebuffer_width: u8,framebuffer_height: u8 // New
) !Self {Code language: PHP (php)

Then in the create function after we create our window add this code:

// Create SDL2 renderer
var renderer = c.SDL_CreateRenderer(window, -1, c.SDL_RENDERER_ACCELERATED) orelse {
  c.SDL_DestroyWindow(window);
  c.SDL_Quit();
  return error.SDLRendererCreationFailed;
};

// Create display framebuffer
var framebuffer = c.SDL_CreateTexture(
  renderer,
  c.SDL_PIXELFORMAT_RGBA8888,
  c.SDL_TEXTUREACCESS_STREAMING,
  framebuffer_width,framebuffer_height
) orelse {
  c.SDL_DestroyRenderer(renderer);
  c.SDL_DestroyWindow(window);
  c.SDL_Quit();
  return error.SDLTextureNull;
};Code language: JavaScript (javascript)

And at the end at our return don’t forget to add the new fields to the created struct:

// Return Self type struct
return Self {
  .window = window,
  .renderer = renderer,
  .framebuffer = framebuffer,
  .framebuffer_width = framebuffer_width,
  .framebuffer_height = framebuffer_height,
  .open = true,
};Code language: JavaScript (javascript)

All we’ve done here is add an SDL renderer and texture to our struct. We’re going to copy the pixel data from our bitmap to our SDL texture and then render that texture to the display using the SDL renderer.

In display.zig create a new function called draw:

pub fn draw(self: *Self, bitmap: *Bitmap) void {
  if(bitmap.width != self.framebuffer_width) return;
  if(bitmap.height != self.framebuffer_height) return;

  // You can use whatever colors you want
  const clear_value = c.SDL_Color {
    .r = 0,
    .g = 0,
    .b = 0,
    .a = 255,
  };
  const color_value = c.SDL_Color {
    .r = 255,
    .g = 255,
    .b = 255,
    .a = 255,
  };

  var pixels: ?*anyopaque = null;
  var pitch: i32 = 0;

  // Lock framebuffer so we can write pixel data to it
  if(c.SDL_LockTexture(self.framebuffer, null, &pixels, &pitch) != 0) {
    c.SDL_Log("Failed to lock texture: %s\n", c.SDL_GetError());
    return;
  }

  // Cast pixels pointer so that we can use offsets
  var upixels: [*]u32 = @ptrCast(@alignCast(pixels.?));

  // Copy pixel loop
  var y:u8 = 0;
  while(y < self.framebuffer_height) : (y += 1) {
    var x:u8 = 0;
    while(x < self.framebuffer_width) : (x += 1) {
      var index: usize = @as(usize, y) * @divExact(@as(usize, @intCast(pitch)), @sizeOf(u32)) + @as(usize, x);
      var color = if(bitmap.getPixel(x,y) == 1) color_value else clear_value;

      // It would probably be better to
      // use SDL_MapRGBA here but it was
      // giving me errors for some reason
      // and I'm too lazy to figure it out
      var r: u32 = @as(u32, color.r) << 24;
      var g: u32 = @as(u32, color.g) << 16;
      var b: u32 = @as(u32, color.b) << 8;
      var a: u32 = @as(u32, color.a) << 0;

      upixels[index] = r | g | b | a;
    }
  }

  _ = c.SDL_UnlockTexture(self.framebuffer);

  _ = c.SDL_RenderClear(self.renderer);
  _ = c.SDL_RenderCopy(self.renderer, self.framebuffer, null,null);
  _ = c.SDL_RenderPresent(self.renderer);
}Code language: PHP (php)

The draw function is a lot longer and slightly more complicated than the others, but all it’s really doing is locking the SDL texture, getting a pointer to its pixel data, converting the data in our bitmap to the proper pixel data and setting it in the texture data, and then rendering the texture.

You might be wondering what those _ = bits are doing there. In Zig every value that an expression creates has to be used or at the very least acknowledged. All of those SDL functions return values but we don’t want/need to use them so we simply use the _ = to acknowledge that they exist.

With that done we’re now finally able to actually draw stuff to the screen. Head on over to the main.zig file and change it to look like this:

const std = @import("std");
const Display = @import("display.zig").Display;
const Bitmap = @import("bitmap.zig").Bitmap;    // New

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  const allocator = gpa.allocator();
  defer {
    const leaked = gpa.deinit();
    if(leaked == .leak) {
      @panic("MEMORY LEAK");
    }
  }

  // New
  var bitmap = try Bitmap.create(allocator, 64,32);
  defer bitmap.free();
  _ = bitmap.setPixel(5,5);

  var display = try Display.create("CHIP-8", 800,400, bitmap.width,bitmap.height);
  defer display.free();

  // Display loop
  while(display.open) {
    display.input();
    display.draw(&bitmap); // New
  }
}Code language: JavaScript (javascript)

And with that you should have a single pixel displaying on the screen.

Loading ROMs

Alright so we’re almost ready to start work on the actual emulator itself, but we have two more things we have to do before that, the first up being setting up ROM loading. Plenty of CHIP-8 ROMs can be found free around the internet or on GitHub so don’t be afraid to look around. I’ll personally be using the blitz.rom ROM from this repository: https://github.com/badlogic/chip8/tree/master/roms.

To start create a new source file called device.zig and insert this boilerplate:

const std = @import("std");

const DEFAULT_FONT = [80]u8{
  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
};

pub const Device = struct {
  const Self = @This();

  allocator: std.mem.Allocator,
  memory: []u8, // RAM memory for emulator

  // Create the device
  pub fn create(allocator: std.mem.Allocator) !Self {

  }

  // Free the device
  pub fn free(self: *Self) void {

  }

  // Load raw ROM data into memory
  pub fn loadProgramIntoMemory(self: *Self, program: []u8) void {

  }

  // Load the ROM data from a file
  pub fn loadROM(self: *Self, path: []const u8) bool {

  }
};Code language: PHP (php)

This struct will serve as a sort of abstract “device” for our emulator, a device which contains the actual memory of the emulator and is capable of loading ROMs from the filesystem. Despite how important that may sound nothing particularly fancy will be happening here except for file loading.

You may be wondering about that big DEFAULT_FONT array at the top. CHIP-8 is expected to have a default font loaded into memory for games to use, so we’ve just added that default font at the top there and will load it into memory upon device creation.

// Create the device
pub fn create(allocator: std.mem.Allocator) !Self {
  var memory = try allocator.alloc(u8, 4096);
  @memcpy(memory[0x000..0x050], DEFAULT_FONT[0..80]);

  return Self {
    .allocator = allocator,
    .memory = memory,
  };
}

// Free the device
pub fn free(self: *Self) void {
  self.allocator.free(self.memory);
}

// Load raw ROM data into memory
pub fn loadProgramIntoMemory(self: *Self, program: []u8) void {
  @memcpy(self.memory[0x200..0x200 + program.len], program[0..program.len]);
}

// Load the ROM data from a file
pub fn loadROM(self: *Self, path: []const u8) bool {
  var file = std.fs.cwd().openFile(path, .{}) catch return false;
  defer file.close();

  var stat = file.stat() catch return false;
  if(stat.size > self.memory.len - 0x200) return false;

  var buffer = self.allocator.alloc(u8, stat.size) catch return false;
  defer self.allocator.free(buffer);

  file.reader().readNoEof(buffer) catch return false;

  self.loadProgramIntoMemory(buffer);

  return true;
}Code language: PHP (php)

Nothing particularly interesting is happening in the code, except for the use of Zig’s slices. Slices in Zig are a special kind of pointer that reference a contiguous subset of elements in a sequence. In English and (mostly) in practice that just means that they’re pointers to data in arrays with additional useful information (such as length) added to them. They’re used all around in the standard library and are rather flexible. You can have a slice pointing to data stored in an array, you can have a slice pointing to data stored in another slice, and you can create slices on-the-fly by using that [start_index..end_index] syntax as shown above. Like I said they’re used all the time in the standard library so that’s why they’re littered throughout the code.

With just that little bit of code we already have ROM loading done and ready, so go back to the main.zig and and this snippet somewhere near the beginning of the main function:

var device = try Device.create(allocator);
defer device.free();

if(!device.loadROM("./roms/blitz.rom")) {
  std.debug.print("Failed to load CHIP-8 ROM\n", .{});
  return;
}Code language: PHP (php)

Not forgetting to import the Device struct at the beginning of the file:

const Device = @import("device.zig").Device;Code language: JavaScript (javascript)

Input

Alright, the last thing we have to do before getting to the actual emulator is to setup keyboard input. CHIP-8 has a simple 16 key input, with each key being valued 0x0…0xF. Luckily SDL makes input for us a breeze. We’ll use scancodes instead of keycodes so that anyone can use the emulator regardless of keyboard layout.

In display.zig add this new field:

keys: [16]bool,Code language: CSS (css)

And add it add the end of the create function:

return Self {
  .width = width,
  .height = height,
  .fwidth = fwidth,
  .fheight = fheight,
  .window = window,
  .renderer = renderer,
  .framebuffer = framebuffer,
  .open = true,
  .keys = std.mem.zeroes([16]bool), // New
};Code language: JavaScript (javascript)

Afterwards go to the input function and add these two new cases to the switch statement:

c.SDL_KEYDOWN => {
  switch(event.@"key".@"keysym".@"scancode") {
    c.SDL_SCANCODE_1 => { self.keys[0x1] = true; },
    c.SDL_SCANCODE_2 => { self.keys[0x2] = true; },
    c.SDL_SCANCODE_3 => { self.keys[0x3] = true; },
    c.SDL_SCANCODE_4 => { self.keys[0xC] = true; },
    c.SDL_SCANCODE_Q => { self.keys[0x4] = true; },
    c.SDL_SCANCODE_W => { self.keys[0x5] = true; },
    c.SDL_SCANCODE_E => { self.keys[0x6] = true; },
    c.SDL_SCANCODE_R => { self.keys[0xD] = true; },
    c.SDL_SCANCODE_A => { self.keys[0x7] = true; },
    c.SDL_SCANCODE_S => { self.keys[0x8] = true; },
    c.SDL_SCANCODE_D => { self.keys[0x9] = true; },
    c.SDL_SCANCODE_F => { self.keys[0xE] = true; },
    c.SDL_SCANCODE_Z => { self.keys[0xA] = true; },
    c.SDL_SCANCODE_X => { self.keys[0x0] = true; },
    c.SDL_SCANCODE_C => { self.keys[0xB] = true; },
    c.SDL_SCANCODE_V => { self.keys[0xF] = true; },
    else => {},
  }
},
c.SDL_KEYUP => {
  switch(event.@"key".@"keysym".@"scancode") {
    c.SDL_SCANCODE_1 => { self.keys[0x1] = false; },
    c.SDL_SCANCODE_2 => { self.keys[0x2] = false; },
    c.SDL_SCANCODE_3 => { self.keys[0x3] = false; },
    c.SDL_SCANCODE_4 => { self.keys[0xC] = false; },
    c.SDL_SCANCODE_Q => { self.keys[0x4] = false; },
    c.SDL_SCANCODE_W => { self.keys[0x5] = false; },
    c.SDL_SCANCODE_E => { self.keys[0x6] = false; },
    c.SDL_SCANCODE_R => { self.keys[0xD] = false; },
    c.SDL_SCANCODE_A => { self.keys[0x7] = false; },
    c.SDL_SCANCODE_S => { self.keys[0x8] = false; },
    c.SDL_SCANCODE_D => { self.keys[0x9] = false; },
    c.SDL_SCANCODE_F => { self.keys[0xE] = false; },
    c.SDL_SCANCODE_Z => { self.keys[0xA] = false; },
    c.SDL_SCANCODE_X => { self.keys[0x0] = false; },
    c.SDL_SCANCODE_C => { self.keys[0xB] = false; },
    c.SDL_SCANCODE_V => { self.keys[0xF] = false; },
    else => {},
  }
},Code language: PHP (php)

We’ll be using all of the left-most keys on the keyboard to represent the 16 key input.

Creating the Emulator

Finally, after all that setup we’re ready to make the actual honest-to-God emulator. For other types of emulators this would easily be the most time-consuming part, but luckily for us the CHIP-8 instruction set only has 35 opcodes.

Since the main focus of this section is the implementation of the instruction set, I’ll just give you all of the boilerplate and helper functions that we use from the get-go. Create a new file called cpu.zig and insert this code:

const std = @import("std");
const Bitmap = @import("bitmap.zig").Bitmap;
const Display = @import("display.zig").Display;

pub const CPU = struct {
  const Self = @This();

  memory: *[]u8,
  bitmap: *Bitmap,
  display: *Display,
  pc: u16,
  i: u16,
  dtimer: u8,
  stimer: u8,
  v: [16]u8,
  stack: [16]u16,
  stack_idx: u8,
  paused: bool,
  paused_x: u8,
  speed: u8,

  pub fn create(
    memory: *[]u8,  // Pointer to device memory
    bitmap: *Bitmap,
    display: *Display
  ) Self {
    return Self {
      .memory = memory,
      .bitmap = bitmap,
      .display = display,
      .pc = 0x200, // ROMs are loaded in at 0x200 so this is where the PC will be
      .i = 0,
      .dtimer = 0,
      .stimer = 0,
      .v = std.mem.zeroes([16]u8), // Create zero-initialized array,
      .stack = std.mem.zeroes([16]u16),
      .stack_idx = 0,
      .paused = false,
      .paused_x = 0, // For storing key press after un-pausing
      .speed = 10,
    };
  }

  // Cycle through opcodes
  pub fn cycle(self: *Self) void {
    if(self.paused) {
      var i: u8 = 0;
      while(i < 16) : (i += 1) {
        if(self.display.keys[i]) {
          self.paused = false;
          self.v[self.paused_x] = i;
        }
      }
    }

    var i: u8 = 0;
    while(i < self.speed) : (i += 1) {
      // We aren't running instructions
      // if the emulator is paused
      if(!self.paused) {
        // CHIP-8 opcodes are two bytes long
        // .* is used to dereference pointers in Zig
        var opcode: u16 = (@as(u16, self.memory.*[self.pc]) << 8 | @as(u16, self.memory.*[self.pc+1]));
        self.executeInstruction(opcode);
      }
    }

    if(!self.paused) {
      self.updateTimers();
    }

    self.playSound();
  }



  // Update timers
  fn updateTimers(self: *Self) void {
    if(self.dtimer > 0) self.dtimer -= 1;
    if(self.stimer > 0) self.stimer -= 1;
  }

  fn playSound(self: *Self) void {
    if(self.stimer > 0) {
      // TODO
    } else {
      // TODO
    }
  }

  // Push address to stack
  fn stackPush(self: *Self, address: u16) void {
    if(self.stack_idx > 15) return;

    self.stack[self.stack_idx] = address;
    self.stack_idx += 1;
  }

  // Pop address from stack
  fn stackPop(self: *Self) u16 {
    if(self.stack_idx == 0) return 0;

    var value = self.stack[self.stack_idx-1];
    self.stack_idx -= 1;
    return value;
  }

  // Execute opcode
  fn executeInstruction(self: *Self, opcode: u16) void {
    self.pc += 2;

    var x = (opcode & 0x0F00) >> 8;
    var y = (opcode & 0x00F0) >> 4;

    // Big 'ol opcode switch
    switch(opcode & 0xF000) {
      0x0000 => {
        switch(opcode) {
          0x00E0 => {},
          0x00EE => {},
          else => {},
        }
      },
      0x1000 => {},
      0x2000 => {},
      0x3000 => {},
      0x4000 => {},
      0x5000 => {},
      0x6000 => {},
      0x7000 => {},
      0x8000 => {
        switch(opcode & 0xF) {
          0x0 => {},
          0x1 => {},
          0x2 => {},
          0x3 => {},
          0x4 => {},
          0x5 => {},
          0x6 => {},
          0x7 => {},
          0xE => {},
          else => {},
        }
      },
      0x9000 => {},
      0xA000 => {},
      0xB000 => {},
      0xC000 => {},
      0xD000 => {},
      0xE000 => {
        switch(opcode & 0xFF) {
          0x9E => {},
          0xA1 => {},
          else => {},
        }
      },
      0xF000 => {
        switch(opcode & 0xFF) {
          0x07 => {},
          0x0A => {},
          0x15 => {},
          0x18 => {},
          0x1E => {},
          0x29 => {},
          0x33 => {},
          0x55 => {},
          0x65 => {},
          else => {},
        }
      },
      else => {},
    }
  }
};Code language: PHP (php)

A big bit of boilerplate to be sure, but at this point everything should be self-explanatory.

0nnn – SYS Addr

This opcode can be ignored.

00E0 – CLS

Clears the screen.

0x00E0 => { self.bitmap.clear(0); },Code language: PHP (php)

00EE – RET

Pop the last address in the stack and store it in the program counter.

0x00EE => { self.pc = self.stackPop(); },Code language: PHP (php)

1nnn – JMP addr

Set the program counter to the value stored in nnn.

0x1000 => { self.pc = (opcode & 0xFFF); },Code language: PHP (php)

2nnn – CALL addr

Push the current program counter address to the stack and jump to the specified address.

0x2000 => { self.stackPush(self.pc); self.pc = (opcode & 0xFFF); },Code language: PHP (php)

3xkk – SE Vx, byte

Compare value in register x (Vx) to the value kk and skip the next instruction if equal.

0x3000 => { if(self.v[x] == (opcode & 0xFF)) self.pc += 2; },Code language: PHP (php)

4xkk – SNE Vx, byte

Compare value in register x (Vx) to the value kk and skip the next instruction if not equal.

0x4000 => { if(self.v[x] != (opcode & 0xFF)) self.pc += 2; },Code language: PHP (php)

5xy0 – SE Vx, Vy

Compare value in register x (Vx) to the value in register y (Vy) and skip the next instruction if equal.

0x5000 => { if(self.v[x] == self.v[y]) self.pc += 2; },Code language: PHP (php)

6xkk – LD Vx, byte

Set the value of Vx to kk.

0x6000 => { self.v[x] = @as(u8, @truncate(opcode & 0xFF)); },Code language: PHP (php)

“@truncate” allows us to type cast a value to a type with a smaller byte size in Zig.

7xkk – ADD Vx, byte

Add kk to Vx.

0x7000 => { self.v[x] +%= @as(u8, @truncate(opcode & 0xFF)); },Code language: PHP (php)

“+%=” allows us to overflow and wrap around a value while doing addition in Zig.

8xy0 – LD Vx, Vy

Load the value in Vy into Vx.

0x0 => { self.v[x] = self.v[y]; },Code language: PHP (php)

8xy1 – OR Vx, Vy

Set Vx to the value of Vx | Vy and set the flag register (VF) to 0.

0x1 => { self.v[x] |= self.v[y]; self.v[0xF] = 0; },Code language: PHP (php)

8xy2 – AND Vx, Vy

Set Vx to the value of Vx & Vy and set VF to 0.

0x2 => { self.v[x] &= self.v[y]; self.v[0xF] = 0; },Code language: PHP (php)

8xy3 – XOR Vx, Vy

Set Vx to the value of Vx ^ Vy and set VF to 0.

0x3 => { self.v[x] ^= self.v[y]; self.v[0xF] = 0; },Code language: PHP (php)

8xy4 – ADD Vx, Vy

Set Vx to the value of Vx + Vy and set VF to 1 if overflow, 0 otherwise.

0x4 => {
  var sum: u32 = (@as(u32, self.v[x]) + @as(u32, self.v[y]));
  self.v[x] = @as(u8, @truncate(sum));

  self.v[0xF] = 0;
  if(sum > 0xFF)
    self.v[0xF] = 1;
},Code language: PHP (php)

This one’s a little more complex. Instead of just wrapping around during an overflow the CHIP-8 spec says instead that if an addition is greater than 8 bits the VF register is set to 1 and only the lowest 8 bits of the result are to be kept. Some quirkery with CHIP-8 also requires that the VF register is set at specific times during an instruction, which we’ll see more of in some other instructions.

8xy5 – SUB Vx, Vy

Set Vx to the value of Vx – Vy and set VF to 1 if Vx > Vy, 0 otherwise.

0x5 => {
  var vX = self.v[x];
  var vY = self.v[y];

  self.v[x] = vX -% vY;

  self.v[0xF] = 0;
  if(vX > vY)
    self.v[0xF] = 1;
},Code language: PHP (php)

I know the implementation of this may seem a bit wonky but we implement it this way to apply to the CHIP-8 specs. In most instructions the VF flag is usually set after a calculation takes place. This is because the VF flag itself could be used in a calculation and setting it beforehand would ruin said calculation, so we do it after.

8xy6 – SHR Vx {, Vy}

Set Vx to the value of Vy SHR 1. If the least significant bit of Vy is 1 set VF to 1, 0 otherwise.

0x6 => {
  var vY = self.v[y];

  self.v[x] = vY >> 1;

  self.v[0xF] = 0;
  if(vY & 0x01 != 0)
    self.v[0xF] = 1;
},Code language: PHP (php)

8xy7 – SUBN Vx, Vy

Set vX to the value of Vy – Vx and set VF to 1 if Vy > Vx, 0 otherwise.

0x7 => {
  var vX = self.v[x];
  var vY = self.v[y];

  self.v[x] = vY -% vX;

  self.v[0xF] = 0;
  if(vY > vX)
    self.v[0xF] = 1;
},Code language: PHP (php)

8xyE – SHL Vx {, Vy}

Set Vx to the value of Vy SHL 1. If the most significant bit of Vy is 1 set VF to 1, 0 otherwise.

0xE => {
  var vY = self.v[y];

  self.v[x] = vY << 1;

  self.v[0xF] = 0;
  if(vY & 0x80 != 0)
    self.v[0xF] = 1;
},Code language: PHP (php)

9xy0 – SNE Vx, Vy

Skip the next instruction if Vx != Vy.

0x9000 => { if(self.v[x] != self.v[y]) self.pc += 2; },Code language: PHP (php)

Annn – LD I, addr

Set the value of register I to nnn.

0xA000 => { self.i = (opcode & 0xFFF); },Code language: PHP (php)

Bnnn – JP V0, addr

Set the value of the program counter to nnn plus the value of V0.

0xB000 => { self.pc = (opcode & 0xFFF) + self.v[0]; },Code language: PHP (php)

Cxkk – RND Vx, byte

Generate a random number from 0 to 255, AND it with the value kk, and store the result in Vx.

0xC000 => {
  // Get the operating system
  // to generate a random seed
  var seed: u64 = 11111;
  std.os.getrandom(std.mem.asBytes(&seed)) catch {};

  // Generate a random number
  var rnd = std.rand.DefaultPrng.init(seed);
  var num = rnd.random().int(u8);

  // AND and store
  self.v[x] = num & (@as(u8, @truncate(opcode)) & 0xFF);
},Code language: PHP (php)

Dxyn – DRW Vx, Vy, nibble

Display an n-byte sprite starting at memory location I at pixel location (Vx, Vy), set VF = collision.

The interpreter reads n bytes from memory, starting at the address stored in I. These bytes are then displayed as sprites on the screen at coordinates (Vx, Vy). Sprites are XORed onto the existing screen. If this causes any pixels to be erased, VF is set to 1, 0 otherwise. If a sprite’s x or y is positioned outside of the screen, it wraps around to the opposite side of the screen. This behavior however does not apply to the coordinates of the individual pixels in the sprite.

0xD000 => {
  var width: u16 = 8; // ALL sprite are 8 wide
  var height = (opcode & 0xF);

  // One of the few instructions
  // where it's fine to set this first
  self.v[0xF] = 0;

  var row: u8 = 0;
  while(row < height) : (row += 1) {
    var sprite = self.memory.*[self.i + row];

    var col: u8 = 0;
    while(col < width) : (col += 1) {
      // Wrap the x and y around
      // the screen if outside
      // the bounds
      var px = self.v[x] % self.bitmap.width;
      var py = self.v[y] % self.bitmap.height;

      // We don't wrap pixels that
      // are outside of the bounds
      if(px + col >= self.bitmap.width) continue;
      if(py + row >= self.bitmap.height) continue;

      // If the bit (sprite) is not 0
      // render/erase the pixel
      if((sprite & 0x80) > 0) {
        // If setPixel returns true
        // a pixel was erased, so set
        // VF to 1
        if(self.bitmap.setPixel(px + col,py + row)) {
          self.v[0xF] = 1;
        }
      }

      // Shift the sprite left 1 and
      // move the next col/bit of the
      // sprite into the first position
      sprite <<= 1;
    }
  }
},Code language: PHP (php)

Ex9E – SKP Vx

Skip the next instruction if the key with the value of Vx is pressed.

0x9E => { if(self.display.keys[self.v[x]]) self.pc += 2; },Code language: PHP (php)

ExA1 – SKNP Vx

Skip the next instruction if the key with the value of Vx is not pressed.


0xA1 => { if(!self.display.keys[self.v[x]]) self.pc += 2; },Code language: PHP (php)

Fx07 – LD Vx, DT

Set Vx to the value stored in the delay timer.

0x07 => { self.v[x] = self.dtimer; },Code language: PHP (php)

Fx0A – LD Vx, K

Pause the emulator until a key is pressed.

0x0A => { self.paused = true; self.paused_x = @as(u8, @truncate(x)); },Code language: PHP (php)

Astute readers may have observed some of these values being referenced earlier in the code. The functionality for this instruction was already setup in the boilerplate, so we don’t have to worry about adding anything more.

Fx15 – LD DT, Vx

Set the delay timer to the value stored in Vx.

0x15 => { self.dtimer = self.v[x]; },Code language: PHP (php)

Fx18 – LD ST, Vx

Set the sound timer to the value stored in Vx.

0x18 => { self.stimer = self.v[x]; },Code language: PHP (php)

Fx1E – ADD I, Vx

Add Vx to I.

0x1E => { self.i += self.v[x]; },Code language: PHP (php)

Fx29 – LD F, Vx – ADD I, Vx

Set I to the location of the sprite stored in Vx.

0x29 => { self.i = @as(u16, @intCast(self.v[x])) * 5; },Code language: PHP (php)

We multiply by 5 since every sprite is 5 bytes long.

Fx33 – LD B, Vx

Grab the hundreds, tens, and ones digits in the value stored in Vx and store them in I+0, I+1, and I+2, respectively.

0x33 => {
  self.memory.*[self.i+0] = (self.v[x] / 100) % 10;
  self.memory.*[self.i+1] = (self.v[x] / 10) % 10;
  self.memory.*[self.i+2] = self.v[x] % 10;
},Code language: PHP (php)

Fx55 – LD [I], Vx

Loop through the registers V0 through VF and store their values in memory starting at I and increment I.

0x55 => {
  var ri: u16 = 0;
  while(ri <= x) : (ri += 1) {
    self.memory.*[self.i + ri] = self.v[ri];
  }
  self.i += ri;
},Code language: PHP (php)

Fx65 – LD Vx, [I]

Read values from memory starting at I and store them in registers V0 through VF, incrementing I.

0x65 => {
  var ri: u16 = 0;
  while(ri <= x) : (ri += 1) {
    self.v[ri] = self.memory.*[self.i + ri];
  }
  self.i += ri;
},Code language: PHP (php)

Phew, and just like that we’ve implemented all of the CHIP-8 instructions. But we’re still not done quite yet! There’s one last little wrinkle to iron out: timing.

Timing

CHIP-8 expects to run at a rate of 60 Hz a second. Modern computers run, uhhhh, a helluva lot faster than that, so we’re gonna have to artificially limit how many times we cycle the CPU. We can easily do this by using Zig’s built-in time methods.

For the last time, head on over to main.zig. Remove this line:

_ = bitmap.setPixel(5,5);

And then rewrite the display loop to look like this:

// Display loop
const fps: f32 = 60.0;
const fpsInterval = 1000.0 / fps;
var previousTime = std.time.milliTimestamp();
var currentTime = std.time.milliTimestamp();

while(display.open) {
  display.input();

  currentTime = std.time.milliTimestamp();
  if(@as(f32, @floatFromInt(currentTime - previousTime)) > fpsInterval) {
    previousTime = currentTime;

    cpu.cycle();

    display.draw(&bitmap);
  }
}Code language: JavaScript (javascript)

And with that, our CHIP-8 emulator is done. I hope you enjoyed this little introduction to the Zig programming language. I’ll potentially make more stuff like this such as a NES or Gameboy emulator in the future. Or I might make a DOOM renderer. ¯\_(ツ)_/¯

Regardless, you can find more details about the more technical aspects of CHIP-8 in the resources down below. You can also find the full source code for the project at https://github.com/HandleHHandle/CHIP8Tutorial.

Till next time.

[Psssst, you may have noticed that we failed to implement the playSound function. Because I’m lazy I’ve decided to leave that as an exercise for the reader ;)]