From fce8a58cf603192ddd23b09afcc2a0c8002b9237 Mon Sep 17 00:00:00 2001
From: Gabriel <asleepymoon11@gmail.com>
Date: Thu, 13 Feb 2025 22:39:48 +0100
Subject: [PATCH] Ready. Set. Go!

Microkernel development in Zig, should be fun! =]
---
 .gitignore                          |   7 +
 .gitmodules                         |   3 +
 .vscode/settings.json               |   8 +
 LICENSE                             |  25 +++
 README.md                           |  66 ++++++++
 boot/easyboot/menu.cfg              |   7 +
 build.zig                           |  14 ++
 core/build.zig                      |  46 ++++++
 core/src/arch/debug.zig             |  35 +++++
 core/src/arch/interrupts.zig        |   9 ++
 core/src/arch/platform.zig          |   9 ++
 core/src/arch/vmm.zig               |   9 ++
 core/src/arch/x86_64/debug.zig      |  23 +++
 core/src/arch/x86_64/gdt.zig        | 135 ++++++++++++++++
 core/src/arch/x86_64/idt.zig        | 156 +++++++++++++++++++
 core/src/arch/x86_64/interrupts.zig | 147 ++++++++++++++++++
 core/src/arch/x86_64/ioports.zig    |  44 ++++++
 core/src/arch/x86_64/pic.zig        |  55 +++++++
 core/src/arch/x86_64/platform.zig   |  18 +++
 core/src/arch/x86_64/vmm.zig        | 203 ++++++++++++++++++++++++
 core/src/lib/bitmap.zig             | 138 +++++++++++++++++
 core/src/link.ld                    |  33 ++++
 core/src/main.zig                   |  56 +++++++
 core/src/mmap.zig                   |  61 ++++++++
 core/src/multiboot.zig              | 231 ++++++++++++++++++++++++++++
 core/src/pmm.zig                    | 103 +++++++++++++
 core/src/sys/print.zig              |   7 +
 core/src/sys/syscall.zig            |  28 ++++
 easyboot                            |   1 +
 system/build.zig                    |   9 ++
 system/init/build.zig               |  43 ++++++
 system/init/main.zig                |  13 ++
 tools/iso.sh                        |   7 +
 tools/run.sh                        |   5 +
 34 files changed, 1754 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .gitmodules
 create mode 100644 .vscode/settings.json
 create mode 100644 LICENSE
 create mode 100644 README.md
 create mode 100644 boot/easyboot/menu.cfg
 create mode 100644 build.zig
 create mode 100644 core/build.zig
 create mode 100644 core/src/arch/debug.zig
 create mode 100644 core/src/arch/interrupts.zig
 create mode 100644 core/src/arch/platform.zig
 create mode 100644 core/src/arch/vmm.zig
 create mode 100644 core/src/arch/x86_64/debug.zig
 create mode 100644 core/src/arch/x86_64/gdt.zig
 create mode 100644 core/src/arch/x86_64/idt.zig
 create mode 100644 core/src/arch/x86_64/interrupts.zig
 create mode 100644 core/src/arch/x86_64/ioports.zig
 create mode 100644 core/src/arch/x86_64/pic.zig
 create mode 100644 core/src/arch/x86_64/platform.zig
 create mode 100644 core/src/arch/x86_64/vmm.zig
 create mode 100644 core/src/lib/bitmap.zig
 create mode 100644 core/src/link.ld
 create mode 100644 core/src/main.zig
 create mode 100644 core/src/mmap.zig
 create mode 100644 core/src/multiboot.zig
 create mode 100644 core/src/pmm.zig
 create mode 100644 core/src/sys/print.zig
 create mode 100644 core/src/sys/syscall.zig
 create mode 160000 easyboot
 create mode 100644 system/build.zig
 create mode 100644 system/init/build.zig
 create mode 100644 system/init/main.zig
 create mode 100755 tools/iso.sh
 create mode 100755 tools/run.sh

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..624b320
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+**/.zig-cache
+boot/core
+boot/init
+tools/bin/
+tools/include/
+tools/share/
+astryon.iso
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0e8b6a4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "easyboot"]
+	path = easyboot
+	url = https://gitlab.com/bztsrc/easyboot
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5e0956a
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+    "editor.formatOnSave": true,
+    "editor.tabSize": 4,
+    "files.trimFinalNewlines": true,
+    "files.insertFinalNewline": true,
+    "git.inputValidationLength": 72,
+    "git.inputValidationSubjectLength": 72,
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..caf1ba8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2025, asleepymoon.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..254f1cb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# Astryon - a microkernel-based operating system project in Zig
+
+Note: not guaranteed to be the project's final name.
+
+## Goals
+
+This project is in its very early stages, so don't expect much yet.
+
+I've started this project to try something new in the world of OS development, after working on a classic monolithic system for some time. I've been wanting to make a microkernel-based system for a while. I've also been wanting to try out Zig, so this was a perfect opportunity to combine both.
+
+- [x] Fully written in Zig
+- [ ] Simple microkernel that only manages memory, scheduling, and basic IPC (in progress)
+- [ ] IPC system using shared memory ring buffers
+- [ ] Init process that can load other services and connect processes to each other (sort of like dbus)
+- [ ] Permission manager, VFS system, etc... all in userspace
+- [ ] Decently POSIX-compatible (with a compatibility layer and libc)
+- [ ] Window server and GUI system
+- [ ] Sandbox most regular userspace processes for security
+
+## Setup
+
+Install [Zig](https://ziglang.org/), version `0.13.0`.
+
+When cloning the repo, make sure to use `git clone --recursive` or run `git submodule update --init` after cloning.
+
+If done correctly, you should have the bootloader cloned as a submodule in the `easyboot` folder. Extract `easyboot/distrib/easyboot-x86_64-linux.tgz` into the `tools` folder (Linux only).
+
+The directory tree should look like this:
+```tools
+    - bin
+    - include
+    - share
+      iso.sh
+      run.sh
+      ...
+```
+
+On other operating systems, you're going to have to build the bootloader manually.
+
+## Building
+
+Simply run `zig build -p .`
+
+Built binaries will end up in `base/usr/bin`, with the exception of the kernel and core modules, which will be installed in `boot`.
+
+## Running
+
+### Creating the image
+Use `tools/iso.sh` to generate an ISO image containing the previously built binaries.
+
+This script assumes that you have the easyboot tool installed at `tools/bin/easyboot`. If this is not the case, you'll have to run easyboot manually. Here's the command:
+
+`/path/to/easyboot -e boot astryon.iso`
+
+### Running the image in QEMU
+Then, to run the image in QEMU, you can use the convenience script `tools/run.sh` or run the following command:
+
+`qemu-system-x86_64 -cdrom astryon.iso -serial stdio -enable-kvm`
+
+If you prefer another virtualization system (like Oracle VirtualBox or VMWare), simply import `astryon.iso` into it. Keep in mind you're going to have to do this every time you build a new image.
+
+## License
+
+The bootloader, `easyboot` by [bzt](https://gitlab.com/bztsrc/), is licensed under the GPLv3+ [LICENSE](easyboot/LICENSE).
+
+The Astryon operating system is licensed under the BSD-2-Clause [LICENSE](LICENSE).
diff --git a/boot/easyboot/menu.cfg b/boot/easyboot/menu.cfg
new file mode 100644
index 0000000..4883fa1
--- /dev/null
+++ b/boot/easyboot/menu.cfg
@@ -0,0 +1,7 @@
+framebuffer 800 600 32
+
+default 1 1000
+
+menuentry Astryon default
+    kernel core
+    module init
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..2f5e0f7
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,14 @@
+const std = @import("std");
+const core = @import("core/build.zig");
+const system = @import("system/build.zig");
+
+pub fn build(b: *std.Build) void {
+    const build_step = b.step("all", "Build and install everything");
+
+    const optimize = b.standardOptimizeOption(.{});
+
+    core.build(b, build_step, optimize);
+    system.build(b, build_step, optimize);
+
+    b.default_step = build_step;
+}
diff --git a/core/build.zig b/core/build.zig
new file mode 100644
index 0000000..5acde36
--- /dev/null
+++ b/core/build.zig
@@ -0,0 +1,46 @@
+const std = @import("std");
+
+const here = "core";
+
+pub fn build(b: *std.Build, build_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void {
+    var disabled_features = std.Target.Cpu.Feature.Set.empty;
+    var enabled_features = std.Target.Cpu.Feature.Set.empty;
+
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.mmx));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse2));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx2));
+    enabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.soft_float));
+
+    const target_query = std.Target.Query{
+        .cpu_arch = std.Target.Cpu.Arch.x86_64,
+        .os_tag = std.Target.Os.Tag.freestanding,
+        .abi = std.Target.Abi.none,
+        .cpu_features_sub = disabled_features,
+        .cpu_features_add = enabled_features,
+    };
+
+    const core = b.addExecutable(.{
+        .name = "core",
+        .root_source_file = b.path(here ++ "/src/main.zig"),
+        .target = b.resolveTargetQuery(target_query),
+        .optimize = optimize,
+        .code_model = .kernel,
+    });
+
+    core.addIncludePath(b.path(here ++ "/../easyboot/"));
+
+    core.setLinkerScript(b.path(here ++ "/src/link.ld"));
+    const install = b.addInstallArtifact(core, .{
+        .dest_dir = .{
+            .override = .{ .custom = "boot/" },
+        },
+    });
+
+    var kernel_step = b.step("core", "Build the core microkernel");
+    kernel_step.dependOn(&core.step);
+    kernel_step.dependOn(&install.step);
+
+    build_step.dependOn(kernel_step);
+}
diff --git a/core/src/arch/debug.zig b/core/src/arch/debug.zig
new file mode 100644
index 0000000..ae14b44
--- /dev/null
+++ b/core/src/arch/debug.zig
@@ -0,0 +1,35 @@
+const std = @import("std");
+const target = @import("builtin").target;
+
+const arch = switch (target.cpu.arch) {
+    .x86_64 => @import("x86_64/debug.zig"),
+    else => {
+        @compileError("unsupported architecture");
+    },
+};
+
+const DebugWriter = struct {
+    const Writer = std.io.Writer(
+        *DebugWriter,
+        error{},
+        write,
+    );
+
+    fn write(
+        _: *DebugWriter,
+        data: []const u8,
+    ) error{}!usize {
+        return arch.write(data);
+    }
+
+    fn writer(self: *DebugWriter) Writer {
+        return .{ .context = self };
+    }
+};
+
+/// Print a formatted string to the platform's debug output.
+pub fn print(comptime fmt: []const u8, args: anytype) void {
+    var debug_writer = DebugWriter{};
+    var writer = debug_writer.writer();
+    writer.print(fmt, args) catch return;
+}
diff --git a/core/src/arch/interrupts.zig b/core/src/arch/interrupts.zig
new file mode 100644
index 0000000..f9d1b19
--- /dev/null
+++ b/core/src/arch/interrupts.zig
@@ -0,0 +1,9 @@
+const std = @import("std");
+const target = @import("builtin").target;
+
+pub const arch = switch (target.cpu.arch) {
+    .x86_64 => @import("x86_64/interrupts.zig"),
+    else => {
+        @compileError("unsupported architecture");
+    },
+};
diff --git a/core/src/arch/platform.zig b/core/src/arch/platform.zig
new file mode 100644
index 0000000..16cbe14
--- /dev/null
+++ b/core/src/arch/platform.zig
@@ -0,0 +1,9 @@
+const std = @import("std");
+const target = @import("builtin").target;
+
+pub const arch = switch (target.cpu.arch) {
+    .x86_64 => @import("x86_64/platform.zig"),
+    else => {
+        @compileError("unsupported architecture");
+    },
+};
diff --git a/core/src/arch/vmm.zig b/core/src/arch/vmm.zig
new file mode 100644
index 0000000..8d396ce
--- /dev/null
+++ b/core/src/arch/vmm.zig
@@ -0,0 +1,9 @@
+const std = @import("std");
+const target = @import("builtin").target;
+
+pub const arch = switch (target.cpu.arch) {
+    .x86_64 => @import("x86_64/vmm.zig"),
+    else => {
+        @compileError("unsupported architecture");
+    },
+};
diff --git a/core/src/arch/x86_64/debug.zig b/core/src/arch/x86_64/debug.zig
new file mode 100644
index 0000000..47c7e0a
--- /dev/null
+++ b/core/src/arch/x86_64/debug.zig
@@ -0,0 +1,23 @@
+const io = @import("ioports.zig");
+
+const COM1: u16 = 0x3f8;
+
+fn serialWait() void {
+    while ((io.inb(COM1 + 5) & 0x20) == 0) {
+        asm volatile ("pause");
+    }
+}
+
+fn serialPutchar(c: u8) void {
+    serialWait();
+    io.outb(COM1, c);
+}
+
+/// Write data to the platform's debug output.
+pub fn write(s: []const u8) usize {
+    for (s) |character| {
+        serialPutchar(character);
+    }
+
+    return s.len;
+}
diff --git a/core/src/arch/x86_64/gdt.zig b/core/src/arch/x86_64/gdt.zig
new file mode 100644
index 0000000..db4e053
--- /dev/null
+++ b/core/src/arch/x86_64/gdt.zig
@@ -0,0 +1,135 @@
+const std = @import("std");
+const platform = @import("platform.zig");
+
+const GDTR align(4096) = packed struct {
+    size: u16,
+    offset: u64,
+};
+
+const GDTEntry = packed struct {
+    limit0: u16,
+    base0: u16,
+    base1: u8,
+    access: u8,
+    limit1_flags: u8,
+    base2: u8,
+};
+
+fn createGDTEntry(limit0: u16, base0: u16, base1: u8, access: u8, limit1_flags: u8, base2: u8) GDTEntry {
+    return GDTEntry{
+        .limit0 = limit0,
+        .base0 = base0,
+        .base1 = base1,
+        .access = access,
+        .limit1_flags = limit1_flags,
+        .base2 = base2,
+    };
+}
+
+const HighGDTEntry = packed struct {
+    base_high: u32,
+    reserved: u32,
+};
+
+const GlobalDescriptorTable = packed struct {
+    null: GDTEntry,
+    kernel_code: GDTEntry,
+    kernel_data: GDTEntry,
+    user_code: GDTEntry,
+    user_data: GDTEntry,
+    tss: GDTEntry,
+    tss2: HighGDTEntry,
+};
+
+fn setBase(entry: *GDTEntry, base: u32) void {
+    entry.base0 = @intCast(base & 0xFFFF);
+    entry.base1 = @intCast((base >> 16) & 0xFF);
+    entry.base2 = @intCast((base >> 24) & 0xFF);
+}
+
+fn setLimit(entry: *GDTEntry, limit: u20) void {
+    entry.limit0 = @intCast(limit & 0xFFFF);
+    entry.limit1_flags = (entry.limit1_flags & 0xF0) | (@as(u8, @intCast(limit >> 16)) & 0xF);
+}
+
+fn setTSSBase(tss1: *GDTEntry, tss2: *HighGDTEntry, addr: u64) void {
+    setBase(tss1, @intCast(addr & 0xffffffff));
+    tss2.base_high = @intCast(addr >> 32);
+}
+
+const TSS = packed struct {
+    reserved0: u32,
+    rsp0: u64,
+    rsp1: u64,
+    rsp2: u64,
+    reserved1: u64,
+    ist0: u64,
+    ist1: u64,
+    ist2: u64,
+    ist3: u64,
+    ist4: u64,
+    ist5: u64,
+    ist6: u64,
+    reserved2: u64,
+    reserved3: u16,
+    iomap_base: u16,
+};
+
+fn stackTop(begin: usize, size: usize) usize {
+    return (begin + size) - 16;
+}
+
+fn setupTSS(gdt: *GlobalDescriptorTable, tss: *TSS, stack: [*]u8, stack_length: usize) void {
+    tss.iomap_base = @sizeOf(TSS);
+    tss.ist0 = stackTop(@intFromPtr(stack), stack_length);
+    setTSSBase(&gdt.tss, &gdt.tss2, @intFromPtr(tss));
+    setLimit(&gdt.tss, @sizeOf(TSS) - 1);
+}
+
+fn loadGDT() callconv(.Naked) void {
+    asm volatile (
+        \\ cli
+        \\ lgdt (%rdi)
+        \\ mov   $0x10, %ax
+        \\ mov   %ax, %ds
+        \\ mov   %ax, %es
+        \\ mov   %ax, %fs
+        \\ mov   %ax, %gs
+        \\ mov   %ax, %ss
+        \\ push $8
+        \\ lea .reload_CS(%rip), %rax
+        \\ push %rax
+        \\ lretq
+        \\.reload_CS:
+        \\ ret
+    );
+}
+
+fn loadTR() callconv(.Naked) void {
+    asm volatile (
+        \\ mov %rdi, %rax
+        \\ ltr %ax
+        \\ ret
+    );
+}
+
+/// Setup the Global Descriptor Table.
+pub fn setupGDT() void {
+    // Store all these as static variables, as they won't be needed outside this function but need to stay alive.
+    const state = struct {
+        var gdt = GlobalDescriptorTable{ .null = std.mem.zeroes(GDTEntry), .kernel_code = createGDTEntry(0xffff, 0x0000, 0x00, 0x9a, 0xaf, 0x00), .kernel_data = createGDTEntry(0xffff, 0x0000, 0x00, 0x92, 0xcf, 0x00), .user_code = createGDTEntry(0xffff, 0x0000, 0x00, 0xfa, 0xaf, 0x00), .user_data = createGDTEntry(0xffff, 0x0000, 0x00, 0xf2, 0xcf, 0x00), .tss = createGDTEntry(0x0000, 0x0000, 0x00, 0xe9, 0x0f, 0x00), .tss2 = HighGDTEntry{ .base_high = 0x00000000, .reserved = 0x00000000 } };
+        var gdtr = std.mem.zeroes(GDTR);
+        var tss = std.mem.zeroes(TSS);
+        var alternate_stack: [platform.PAGE_SIZE * 4]u8 = std.mem.zeroes([platform.PAGE_SIZE * 4]u8);
+    };
+
+    state.gdtr.offset = @intFromPtr(&state.gdt);
+    state.gdtr.size = @sizeOf(GlobalDescriptorTable);
+    setupTSS(&state.gdt, &state.tss, @ptrCast(&state.alternate_stack[0]), @sizeOf(@TypeOf(state.alternate_stack)));
+
+    // Hackish way to call naked functions which we know conform to SysV ABI.
+    const lgdt: *const fn (g: *GDTR) callconv(.C) void = @ptrCast(&loadGDT);
+    lgdt(&state.gdtr);
+    const ltr: *const fn (t: u16) callconv(.C) void = @ptrCast(&loadTR);
+    ltr(0x2b);
+}
diff --git a/core/src/arch/x86_64/idt.zig b/core/src/arch/x86_64/idt.zig
new file mode 100644
index 0000000..107185e
--- /dev/null
+++ b/core/src/arch/x86_64/idt.zig
@@ -0,0 +1,156 @@
+const std = @import("std");
+
+const IDTEntry = packed struct {
+    offset0: u16,
+    selector: u16,
+    ist: u8,
+    type_attr: u8,
+    offset1: u16,
+    offset2: u32,
+    ignore: u32,
+};
+
+fn setOffset(entry: *IDTEntry, offset: u64) void {
+    entry.offset0 = @as(u16, @intCast(offset & 0x000000000000ffff));
+    entry.offset1 = @as(u16, @intCast((offset & 0x00000000ffff0000) >> 16));
+    entry.offset2 = @as(u32, @intCast((offset & 0xffffffff00000000) >> 32));
+}
+
+fn getOffset(entry: *IDTEntry) u64 {
+    var offset: u64 = 0;
+    offset |= @as(u64, entry.offset0);
+    offset |= @as(u64, entry.offset1) << 16;
+    offset |= @as(u64, entry.offset2) << 32;
+    return offset;
+}
+
+const IDT_TA_InterruptGate = 0b10001110;
+const IDT_TA_UserCallableInterruptGate = 0b11101110;
+const IDT_TA_TrapGate = 0b10001111;
+
+const IDTR = packed struct {
+    limit: u16,
+    offset: u64,
+};
+
+fn addIDTHandler(idt: *[256]IDTEntry, num: u32, handler: *const anyopaque, type_attr: u8, ist: u8) void {
+    var entry_for_handler: *IDTEntry = &idt.*[num];
+    entry_for_handler.selector = 0x08;
+    entry_for_handler.type_attr = type_attr;
+    entry_for_handler.ist = ist;
+    setOffset(entry_for_handler, @intFromPtr(handler));
+}
+
+fn createISRHandler(comptime num: u32) *const fn () callconv(.Naked) void {
+    return struct {
+        fn handler() callconv(.Naked) void {
+            asm volatile (
+                \\ push $0
+                \\ push %[num]
+                \\ jmp asmInterruptEntry
+                :
+                : [num] "n" (num),
+            );
+        }
+    }.handler;
+}
+
+fn createISRHandlerWithErrorCode(comptime num: u32) *const fn () callconv(.Naked) void {
+    return struct {
+        fn handler() callconv(.Naked) void {
+            asm volatile (
+                \\ push %[num]
+                \\ jmp asmInterruptEntry
+                :
+                : [num] "n" (num),
+            );
+        }
+    }.handler;
+}
+
+fn createIRQHandler(comptime num: u32, comptime irq: u32) *const fn () callconv(.Naked) void {
+    return struct {
+        fn handler() callconv(.Naked) void {
+            asm volatile (
+                \\ push %[irq]
+                \\ push %[num]
+                \\ jmp asmInterruptEntry
+                :
+                : [num] "n" (num),
+                  [irq] "n" (irq),
+            );
+        }
+    }.handler;
+}
+
+/// Setup the Interrupt Descriptor Table.
+pub fn setupIDT() void {
+    // Store these as static variables, as they won't be needed outside this function but need to stay alive.
+    const state = struct {
+        var idtr = std.mem.zeroes(IDTR);
+        var idt: [256]IDTEntry = std.mem.zeroes([256]IDTEntry);
+    };
+
+    comptime var i: u32 = 0;
+
+    // ISR 0-7 (no error code)
+    inline while (i < 8) : (i += 1) {
+        const handler: *const anyopaque = @ptrCast(createISRHandler(i));
+        addIDTHandler(&state.idt, i, handler, IDT_TA_TrapGate, 1);
+    }
+
+    // ISR 8 #DF (error code)
+    const handler_8: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(8));
+    addIDTHandler(&state.idt, 8, handler_8, IDT_TA_TrapGate, 1);
+
+    // ISR 9 obsolete
+
+    i = 10;
+    // ISR 10-14 (error code)
+    inline while (i < 15) : (i += 1) {
+        const handler: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(i));
+        addIDTHandler(&state.idt, i, handler, IDT_TA_TrapGate, 1);
+    }
+
+    // ISR 15 reserved
+
+    // ISR 16 #MF (no error code)
+    const handler_16: *const anyopaque = @ptrCast(createISRHandler(16));
+    addIDTHandler(&state.idt, 16, handler_16, IDT_TA_TrapGate, 1);
+
+    // ISR 17 #AC (error code)
+    const handler_17: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(17));
+    addIDTHandler(&state.idt, 17, handler_17, IDT_TA_TrapGate, 1);
+
+    i = 18;
+    // ISR 18-20 (no error code)
+    inline while (i < 21) : (i += 1) {
+        const handler: *const anyopaque = @ptrCast(createISRHandler(i));
+        addIDTHandler(&state.idt, i, handler, IDT_TA_TrapGate, 1);
+    }
+
+    // ISR 21 #CP (error code)
+    const handler_21: *const anyopaque = @ptrCast(createISRHandlerWithErrorCode(21));
+    addIDTHandler(&state.idt, 21, handler_21, IDT_TA_TrapGate, 1);
+
+    // ISR 22-31 reserved
+
+    i = 0;
+    // ISR 32-47 (IRQs 0-16 after remapping the PIC)
+    inline while (i < 16) : (i += 1) {
+        const handler: *const anyopaque = @ptrCast(createIRQHandler(32 + i, i));
+        addIDTHandler(&state.idt, 32 + i, handler, IDT_TA_InterruptGate, 0);
+    }
+
+    // ISR 66 (syscall)
+    const sys_handler: *const anyopaque = @ptrCast(createISRHandler(66));
+    addIDTHandler(&state.idt, 66, sys_handler, IDT_TA_UserCallableInterruptGate, 0);
+
+    state.idtr.limit = 0x0FFF;
+    state.idtr.offset = @intFromPtr(&state.idt[0]);
+
+    asm volatile ("lidt (%[idtr])"
+        :
+        : [idtr] "{rax}" (&state.idtr),
+    );
+}
diff --git a/core/src/arch/x86_64/interrupts.zig b/core/src/arch/x86_64/interrupts.zig
new file mode 100644
index 0000000..fd9a61e
--- /dev/null
+++ b/core/src/arch/x86_64/interrupts.zig
@@ -0,0 +1,147 @@
+const std = @import("std");
+const pic = @import("pic.zig");
+const debug = @import("../debug.zig");
+const sys = @import("../../sys/syscall.zig");
+
+pub const InterruptStackFrame = packed struct {
+    r15: u64,
+    r14: u64,
+    r13: u64,
+    r12: u64,
+    r11: u64,
+    r10: u64,
+    r9: u64,
+    r8: u64,
+    rbp: u64,
+    rdi: u64,
+    rsi: u64,
+    rdx: u64,
+    rcx: u64,
+    rbx: u64,
+    rax: u64,
+    isr: u64,
+    error_or_irq: u64,
+    rip: u64,
+    cs: u64,
+    rflags: u64,
+    rsp: u64,
+    ss: u64,
+};
+
+const IRQHandler = *const fn (u32, *InterruptStackFrame) void;
+
+var irq_handlers: [16]?IRQHandler = std.mem.zeroes([16]?IRQHandler);
+
+export fn asmInterruptEntry() callconv(.Naked) void {
+    asm volatile (
+        \\ push %rax
+        \\ push %rbx
+        \\ push %rcx
+        \\ push %rdx
+        \\ push %rsi
+        \\ push %rdi
+        \\ push %rbp
+        \\ push %r8
+        \\ push %r9
+        \\ push %r10
+        \\ push %r11
+        \\ push %r12
+        \\ push %r13
+        \\ push %r14
+        \\ push %r15
+        \\ cld
+        \\ mov %rsp, %rdi
+        \\ call interruptEntry
+        \\asmInterruptExit:
+        \\ pop %r15
+        \\ pop %r14
+        \\ pop %r13
+        \\ pop %r12
+        \\ pop %r11
+        \\ pop %r10
+        \\ pop %r9
+        \\ pop %r8
+        \\ pop %rbp
+        \\ pop %rdi
+        \\ pop %rsi
+        \\ pop %rdx
+        \\ pop %rcx
+        \\ pop %rbx
+        \\ pop %rax
+        \\ add $16, %rsp
+        \\ iretq
+    );
+}
+
+const SYSCALL_INTERRUPT = 66;
+
+export fn interruptEntry(frame: *InterruptStackFrame) callconv(.C) void {
+    debug.print("Caught interrupt {d}\n", .{frame.isr});
+    switch (frame.isr) {
+        SYSCALL_INTERRUPT => {
+            var args = sys.Arguments{ .arg0 = frame.rdi, .arg1 = frame.rsi, .arg2 = frame.rdx, .arg3 = frame.r10, .arg4 = frame.r8, .arg5 = frame.r9 };
+            sys.invokeSyscall(frame.rax, frame, &args, @ptrFromInt(@as(usize, @intFromPtr(&frame.rax))));
+        },
+        else => {},
+    }
+}
+
+/// Disable interrupts (except for NMIs).
+pub fn disableInterrupts() void {
+    asm volatile ("cli");
+}
+
+/// Enable interrupts.
+pub fn enableInterrupts() void {
+    asm volatile ("sti");
+}
+
+/// Check whether interrupts are enabled.
+pub fn saveInterrupts() bool {
+    var flags: u64 = 0;
+    asm volatile ("pushfq; pop %[flags]"
+        : [flags] "=r" (flags),
+    );
+    return (flags & 0x200) != 0;
+}
+
+/// Enable or disable interrupts depending on the boolean value passed.
+pub fn restoreInterrupts(saved: bool) void {
+    switch (saved) {
+        true => {
+            enableInterrupts();
+        },
+        false => {
+            disableInterrupts();
+        },
+    }
+}
+
+/// Update the PIC masks according to which IRQ handlers are registered.
+pub fn syncInterrupts() void {
+    var pic1_mask: u8 = 0b11111111;
+    var pic2_mask: u8 = 0b11111111;
+    var i: u8 = 0;
+    while (i < 8) : (i += 1) {
+        if (irq_handlers[i] != null) pic1_mask &= (~(@as(u8, 1) << @as(u3, @intCast(i))));
+        if (irq_handlers[i + 8] != null) pic2_mask &= (~(@as(u8, 1) << @as(u3, @intCast(i))));
+    }
+
+    if (pic2_mask != 0b11111111) pic1_mask &= 0b11111011;
+
+    const saved: bool = saveInterrupts();
+    disableInterrupts();
+    pic.changePICMasks(pic1_mask, pic2_mask);
+    restoreInterrupts(saved);
+}
+
+/// Register an IRQ handler.
+pub fn registerIRQ(num: u32, handler: IRQHandler) bool {
+    if (irq_handlers[num] != null) return false;
+
+    irq_handlers[num] = handler;
+
+    syncInterrupts();
+
+    return true;
+}
diff --git a/core/src/arch/x86_64/ioports.zig b/core/src/arch/x86_64/ioports.zig
new file mode 100644
index 0000000..d028d35
--- /dev/null
+++ b/core/src/arch/x86_64/ioports.zig
@@ -0,0 +1,44 @@
+pub fn inb(port: u16) u8 {
+    return asm volatile ("inb %[port], %[result]"
+        : [result] "={al}" (-> u8),
+        : [port] "N{dx}" (port),
+    );
+}
+
+pub fn outb(port: u16, value: u8) void {
+    return asm volatile ("outb %[data], %[port]"
+        :
+        : [port] "{dx}" (port),
+          [data] "{al}" (value),
+    );
+}
+
+pub fn inw(port: u16) u16 {
+    return asm volatile ("inw %[port], %[result]"
+        : [result] "={ax}" (-> u16),
+        : [port] "N{dx}" (port),
+    );
+}
+
+pub fn outw(port: u16, value: u16) void {
+    return asm volatile ("outw %[data], %[port]"
+        :
+        : [port] "{dx}" (port),
+          [data] "{ax}" (value),
+    );
+}
+
+pub fn inl(port: u16) u32 {
+    return asm volatile ("inl %[port], %[result]"
+        : [result] "={eax}" (-> u16),
+        : [port] "N{dx}" (port),
+    );
+}
+
+pub fn outl(port: u16, value: u32) void {
+    return asm volatile ("outw %[data], %[port]"
+        :
+        : [port] "{dx}" (port),
+          [data] "{eax}" (value),
+    );
+}
diff --git a/core/src/arch/x86_64/pic.zig b/core/src/arch/x86_64/pic.zig
new file mode 100644
index 0000000..be06e2b
--- /dev/null
+++ b/core/src/arch/x86_64/pic.zig
@@ -0,0 +1,55 @@
+const io = @import("ioports.zig");
+
+const PIC1_COMMAND = 0x20;
+const PIC1_DATA = 0x21;
+const PIC2_COMMAND = 0xA0;
+const PIC2_DATA = 0xA1;
+const PIC_EOI = 0x20;
+
+const ICW1_INIT = 0x10;
+const ICW1_ICW4 = 0x01;
+const ICW4_8086 = 0x01;
+
+inline fn ioDelay() void {
+    io.outb(0x80, 0);
+}
+
+/// Remap the PIC so that all IRQs are remapped to 0x20-0x2f.
+pub fn remapPIC() void {
+    io.outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
+    ioDelay();
+    io.outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
+    ioDelay();
+
+    io.outb(PIC1_DATA, 0x20);
+    ioDelay();
+
+    io.outb(PIC2_DATA, 0x28);
+    ioDelay();
+
+    io.outb(PIC1_DATA, 4);
+    ioDelay();
+    io.outb(PIC2_DATA, 2);
+    ioDelay();
+
+    io.outb(PIC1_DATA, ICW4_8086);
+    ioDelay();
+    io.outb(PIC2_DATA, ICW4_8086);
+    ioDelay();
+
+    changePICMasks(0b11111111, 0b11111111);
+}
+
+/// Update the PIC masks.
+pub fn changePICMasks(pic1_mask: u8, pic2_mask: u8) void {
+    io.outb(PIC1_DATA, pic1_mask);
+    ioDelay();
+    io.outb(PIC2_DATA, pic2_mask);
+    ioDelay();
+}
+
+/// Send an end-of-interrupt signal to the PIC.
+pub fn picEOI(irq: u8) void {
+    if (irq >= 8) io.outb(PIC2_COMMAND, PIC_EOI);
+    io.outb(PIC1_COMMAND, PIC_EOI);
+}
diff --git a/core/src/arch/x86_64/platform.zig b/core/src/arch/x86_64/platform.zig
new file mode 100644
index 0000000..0aee444
--- /dev/null
+++ b/core/src/arch/x86_64/platform.zig
@@ -0,0 +1,18 @@
+const gdt = @import("gdt.zig");
+const idt = @import("idt.zig");
+const pic = @import("pic.zig");
+const interrupts = @import("interrupts.zig");
+
+pub const PAGE_SIZE = 4096;
+
+// Initialize platform-specific components.
+pub fn platformInit() void {
+    gdt.setupGDT();
+    idt.setupIDT();
+}
+
+// Initialize platform-specific components just before beginning multitasking.
+pub fn platformEndInit() void {
+    pic.remapPIC();
+    interrupts.syncInterrupts();
+}
diff --git a/core/src/arch/x86_64/vmm.zig b/core/src/arch/x86_64/vmm.zig
new file mode 100644
index 0000000..37ee1da
--- /dev/null
+++ b/core/src/arch/x86_64/vmm.zig
@@ -0,0 +1,203 @@
+const std = @import("std");
+const easyboot = @cImport(@cInclude("easyboot.h"));
+const mmap = @import("../../mmap.zig");
+const pmm = @import("../../pmm.zig");
+
+const USER_ADDRESS_RANGE_END = 0x0000_7fff_ffff_ffff;
+const PHYSICAL_MAPPING_BASE = 0xffff_8000_0000_0000;
+const HUGE_PAGE_SIZE = 0x200000; // 2 MiB
+
+pub const PageTableEntry = packed struct {
+    present: u1,
+    read_write: u1,
+    user: u1,
+    write_through: u1,
+    cache_disabled: u1,
+    accessed: u1,
+    ignore0: u1,
+    larger_pages: u1,
+    global: u1,
+    available: u3,
+    address: u48,
+    available2: u3,
+    no_execute: u1,
+
+    pub fn set_address(self: *PageTableEntry, address: u64) void {
+        self.address = @intCast(address >> 12);
+    }
+
+    pub fn get_address(self: *PageTableEntry) u64 {
+        return self.address << 12;
+    }
+
+    pub fn clear(self: *PageTableEntry) void {
+        self = std.mem.zeroes(@TypeOf(self));
+    }
+};
+
+pub const PageDirectory = struct {
+    entries: [512]PageTableEntry,
+};
+
+const Flags = enum(u32) {
+    None = 0,
+    ReadWrite = 1,
+    User = 2,
+    NoExecute = 4,
+    WriteThrough = 8,
+    CacheDisable = 16,
+    Global = 32,
+};
+
+const PageTableIndexes = struct {
+    level4: u24,
+    level3: u24,
+    level2: u24,
+    level1: u24,
+};
+
+fn calculatePageTableIndexes(address: usize) PageTableIndexes {
+    return .{ .level4 = @intCast((address >> 39) & 0o777), .level3 = @intCast((address >> 30) & 0o777), .level2 = @intCast((address >> 21) & 0o777), .level1 = @intCast((address >> 12) & 0o777) };
+}
+
+fn hasFlag(flags: u32, flag: Flags) u1 {
+    return switch ((flags & @intFromEnum(flag)) > 0) {
+        true => 1,
+        false => 0,
+    };
+}
+
+fn updatePageTableEntry(entry: *PageTableEntry, phys: pmm.PhysFrame, flags: u32) void {
+    entry.present = 1;
+    entry.read_write = hasFlag(flags, Flags.ReadWrite);
+    entry.user = hasFlag(flags, Flags.User);
+    entry.write_through = hasFlag(flags, Flags.WriteThrough);
+    entry.cache_disabled = hasFlag(flags, Flags.CacheDisable);
+    entry.no_execute = hasFlag(flags, Flags.NoExecute);
+    entry.global = hasFlag(flags, Flags.Global);
+    entry.set_address(phys.address);
+}
+
+fn setUpParentPageTableEntry(allocator: *pmm.FrameAllocator, pte: *PageTableEntry, flags: u32, base: usize) !void {
+    if (pte.present == 0) {
+        const frame = try pmm.allocFrame(allocator);
+        pte.present = 1;
+        pte.set_address(frame.address);
+        getTable(pte, base).* = std.mem.zeroes(PageDirectory);
+    }
+    if (hasFlag(flags, Flags.ReadWrite) == 1) pte.read_write = 1;
+    if (hasFlag(flags, Flags.User) == 1) pte.user = 1;
+}
+
+fn getTable(pte: *PageTableEntry, base: usize) *allowzero PageDirectory {
+    const frame = pmm.PhysFrame{ .address = pte.get_address() };
+    return @ptrFromInt(frame.virtualAddress(base));
+}
+
+pub fn map(allocator: *pmm.FrameAllocator, directory: *PageDirectory, base: usize, virt_address: u64, phys: pmm.PhysFrame, flags: u32, use_huge_pages: bool) !void {
+    const indexes = calculatePageTableIndexes(virt_address);
+    const l4 = &directory.entries[indexes.level4];
+    try setUpParentPageTableEntry(allocator, l4, flags, base);
+
+    const l3 = &getTable(l4, base).entries[indexes.level3];
+    if (l3.larger_pages == 1) return error.MemoryAlreadyInUse;
+    try setUpParentPageTableEntry(allocator, l3, flags, base);
+
+    const l2 = &getTable(l3, base).entries[indexes.level2];
+    if (l2.larger_pages == 1) return error.MemoryAlreadyInUse;
+
+    if (use_huge_pages) {
+        l2.larger_pages = 1;
+        updatePageTableEntry(l2, phys, flags);
+        return;
+    }
+
+    try setUpParentPageTableEntry(allocator, l2, flags, base);
+
+    const l1 = &getTable(l2, base).entries[indexes.level1];
+    if (l1.present == 1) return error.MemoryAlreadyInUse;
+    updatePageTableEntry(l1, phys, flags);
+}
+
+fn mapPhysicalMemory(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t, directory: *PageDirectory, base: usize, flags: u32) !void {
+    const address_space_size = mmap.getAddressSpaceSize(tag) orelse return error.InvalidMemoryMap;
+    const address_space_pages = address_space_size / HUGE_PAGE_SIZE;
+
+    var index: usize = 0;
+    while (index < address_space_pages) : (index += 1) {
+        try map(allocator, directory, 0, base + index * HUGE_PAGE_SIZE, pmm.PhysFrame{ .address = index * HUGE_PAGE_SIZE }, flags, true);
+    }
+}
+
+fn lockPageDirectoryFrames(allocator: *pmm.FrameAllocator, directory: *PageDirectory, index: u8) !void {
+    if (index > 1) {
+        var i: u64 = 0;
+        while (i < 512) : (i += 1) {
+            const pte = &directory.entries[i];
+            if (pte.present == 0) continue;
+            if ((index < 4) and (pte.larger_pages == 1)) continue;
+
+            try pmm.lockFrame(allocator, pte.get_address());
+
+            const child_table: *PageDirectory = @ptrFromInt(pte.get_address());
+
+            try lockPageDirectoryFrames(allocator, child_table, index - 1);
+        }
+    }
+}
+
+fn lockPageDirectory(allocator: *pmm.FrameAllocator, directory: *PageDirectory) !void {
+    try pmm.lockFrame(allocator, @intFromPtr(directory));
+    try lockPageDirectoryFrames(allocator, directory, 4);
+}
+
+fn setUpKernelPageDirectory(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t) !*PageDirectory {
+    const directory = readPageDirectory();
+
+    try lockPageDirectory(allocator, directory);
+    try mapPhysicalMemory(allocator, tag, directory, PHYSICAL_MAPPING_BASE, @intFromEnum(Flags.ReadWrite) | @intFromEnum(Flags.NoExecute) | @intFromEnum(Flags.Global));
+
+    return directory;
+}
+
+fn setUpInitialUserPageDirectory(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t, kernel_directory: *PageDirectory, user_directory: *PageDirectory) !usize {
+    const physical_address_space_size = mmap.getAddressSpaceSize(tag) orelse return error.InvalidMemoryMap;
+
+    user_directory.* = std.mem.zeroes(PageDirectory);
+
+    const directory_upper_half: *[256]PageTableEntry = kernel_directory.entries[256..];
+    const user_directory_upper_half: *[256]PageTableEntry = user_directory.entries[256..];
+    @memcpy(user_directory_upper_half, directory_upper_half);
+
+    const user_physical_address_base = (USER_ADDRESS_RANGE_END + 1) - physical_address_space_size;
+
+    try mapPhysicalMemory(allocator, tag, user_directory, user_physical_address_base, @intFromEnum(Flags.ReadWrite) | @intFromEnum(Flags.NoExecute) | @intFromEnum(Flags.User));
+
+    return user_physical_address_base;
+}
+
+pub fn createInitialMappings(allocator: *pmm.FrameAllocator, tag: *easyboot.multiboot_tag_mmap_t, user_directory: *PageDirectory) !usize {
+    const directory = try setUpKernelPageDirectory(allocator, tag);
+    const base = try setUpInitialUserPageDirectory(allocator, tag, directory, user_directory);
+
+    setPageDirectory(directory);
+
+    allocator.bitmap.location = @ptrFromInt(@as(usize, PHYSICAL_MAPPING_BASE) + @intFromPtr(allocator.bitmap.location));
+
+    return base;
+}
+
+pub fn readPageDirectory() *PageDirectory {
+    var directory: *PageDirectory = undefined;
+    asm volatile ("mov %%cr3, %[dir]"
+        : [dir] "=r" (directory),
+    );
+    return directory;
+}
+
+pub fn setPageDirectory(directory: *PageDirectory) void {
+    asm volatile ("mov %[dir], %%cr3"
+        :
+        : [dir] "{rdi}" (directory),
+    );
+}
diff --git a/core/src/lib/bitmap.zig b/core/src/lib/bitmap.zig
new file mode 100644
index 0000000..3f065b3
--- /dev/null
+++ b/core/src/lib/bitmap.zig
@@ -0,0 +1,138 @@
+const std = @import("std");
+
+const BitmapError = error{
+    OutOfRange,
+};
+
+pub const Bitmap = struct {
+    location: [*]u8,
+    byte_size: usize,
+
+    pub fn bit_size(self: *Bitmap) usize {
+        return self.byte_size * 8;
+    }
+
+    pub fn set(self: *Bitmap, index: usize, value: u1) BitmapError!void {
+        if (index >= self.bit_size()) return error.OutOfRange;
+
+        const byte_index = index / 8;
+        const bit_mask = @as(u8, 0b10000000) >> @as(u3, @intCast(index % 8));
+        self.location[byte_index] &= ~bit_mask;
+        if (value == 1) {
+            self.location[byte_index] |= bit_mask;
+        }
+    }
+
+    pub fn get(self: *Bitmap, index: usize) BitmapError!u1 {
+        if (index >= self.bit_size()) return error.OutOfRange;
+
+        const byte_index = index / 8;
+        const bit_mask = @as(u8, 0b10000000) >> @as(u3, @intCast(index % 8));
+        if ((self.location[byte_index] & bit_mask) > 0) return 1;
+        return 0;
+    }
+
+    pub fn clear(self: *Bitmap, value: u1) void {
+        @memset(self.location[0..self.byte_size], byteThatOnlyContainsBit(value));
+    }
+};
+
+pub fn createBitmap(location: [*]u8, byte_size: usize) Bitmap {
+    return Bitmap{ .location = location, .byte_size = byte_size };
+}
+
+// Self-explanatory.
+fn byteThatDoesNotContainBit(value: u1) u8 {
+    return switch (value) {
+        1 => 0x00,
+        0 => 0xff,
+    };
+}
+
+fn byteThatOnlyContainsBit(value: u1) u8 {
+    return switch (value) {
+        1 => 0xff,
+        0 => 0x00,
+    };
+}
+
+pub fn findInBitmap(bitmap: *Bitmap, value: u1, begin: usize) BitmapError!?usize {
+    if (begin >= bitmap.bit_size()) return error.OutOfRange;
+
+    var index = begin;
+
+    while ((index % 8) != 0) {
+        if (try bitmap.get(index) == value) return index;
+        index += 1;
+    }
+
+    if (index == bitmap.bit_size()) return null;
+
+    var i: usize = index / 8;
+    const byte_that_does_not_contain_value = byteThatDoesNotContainBit(value);
+    while (i < bitmap.byte_size) {
+        if (bitmap.location[i] == byte_that_does_not_contain_value) {
+            i += 1;
+            continue;
+        }
+
+        var j: usize = i * 8;
+        const end: usize = j + 8;
+        while (j < end) {
+            if (try bitmap.get(j) == value) return j;
+            j += 1;
+        }
+
+        // Once we've located a byte that contains the value, we should succeed in finding it.
+        unreachable;
+    }
+
+    return null;
+}
+
+pub fn findInBitmapAndToggle(bitmap: *Bitmap, value: u1, begin: usize) BitmapError!?usize {
+    const index = try findInBitmap(bitmap, value, begin);
+
+    switch (value) {
+        0 => try bitmap.set(index, 1),
+        1 => try bitmap.set(index, 0),
+    }
+
+    return index;
+}
+
+pub fn updateBitmapRegion(bitmap: *Bitmap, begin: usize, count: usize, value: u1) BitmapError!void {
+    if ((begin + count) > bitmap.bit_size()) return error.OutOfRange;
+
+    if (count == 0) return;
+
+    var index = begin; // The bit index we're updating.
+    var bits_remaining = count; // The number of bits left to update.
+
+    // If the index is in the middle of a byte, update individual bits until we reach a byte.
+    while ((index % 8) > 0 and bits_remaining > 0) {
+        try bitmap.set(index, value);
+        index += 1;
+        bits_remaining -= 1;
+    }
+
+    // Clear out the rest in bytes. We calculate the number of bytes to update, and then memset them all.
+    const bytes: usize = bits_remaining / 8;
+
+    if (bytes > 0) {
+        const start = index / 8;
+        const end = start + bytes;
+        @memset(bitmap.location[start..end], byteThatOnlyContainsBit(value));
+
+        // Update the counting variables after the memset.
+        index += bytes * 8;
+        bits_remaining -= bytes * 8;
+    }
+
+    // Set the remaining individual bits.
+    while (bits_remaining > 0) {
+        try bitmap.set(index, value);
+        index += 1;
+        bits_remaining -= 1;
+    }
+}
diff --git a/core/src/link.ld b/core/src/link.ld
new file mode 100644
index 0000000..aa89d7d
--- /dev/null
+++ b/core/src/link.ld
@@ -0,0 +1,33 @@
+ENTRY(_start)
+OUTPUT_FORMAT(elf64-x86-64)
+
+PHDRS
+{
+  boot PT_LOAD;                                /* one single loadable segment */
+}
+SECTIONS
+{
+    . = 0xffffffffffe00000;
+	kernel_start = .;
+    .text : {
+        KEEP(*(.text.boot)) *(.text .text.*)   /* code */
+
+        . = ALIGN(0x1000);
+        start_of_kernel_rodata = .;
+        *(.rodata .rodata.*)                   /* read-only data */
+        end_of_kernel_rodata = .;
+
+        . = ALIGN(0x1000);
+        start_of_kernel_data = .;              /* data */
+        *(.data .data.*)
+
+    } :boot
+    .bss (NOLOAD) : {                          /* bss */
+        *(.bss .bss.*)
+        *(COMMON)
+    } :boot
+    end_of_kernel_data = .;
+	kernel_end = .;
+
+    /DISCARD/ : { *(.eh_frame) *(.comment) }
+}
diff --git a/core/src/main.zig b/core/src/main.zig
new file mode 100644
index 0000000..5fb71db
--- /dev/null
+++ b/core/src/main.zig
@@ -0,0 +1,56 @@
+const std = @import("std");
+const easyboot = @cImport(@cInclude("easyboot.h"));
+const debug = @import("arch/debug.zig");
+const platform = @import("arch/platform.zig").arch;
+const interrupts = @import("arch/interrupts.zig").arch;
+const vmm = @import("arch/vmm.zig").arch;
+const multiboot = @import("multiboot.zig");
+const pmm = @import("pmm.zig");
+
+const MultibootInfo = [*c]u8;
+
+export fn _start(magic: u32, info: MultibootInfo) callconv(.C) noreturn {
+    if (magic != easyboot.MULTIBOOT2_BOOTLOADER_MAGIC) {
+        debug.print("Invalid magic number: {x}\n", .{magic});
+        while (true) {}
+    }
+
+    debug.print("Hello world from the kernel!\n", .{});
+
+    multiboot.parseMultibootTags(@ptrCast(info));
+
+    interrupts.disableInterrupts();
+    platform.platformInit();
+
+    debug.print("GDT initialized\n", .{});
+
+    platform.platformEndInit();
+    interrupts.enableInterrupts();
+
+    if (multiboot.findMultibootTag(easyboot.multiboot_tag_mmap_t, @ptrCast(info))) |tag| {
+        var allocator = pmm.initializeFrameAllocator(tag) catch |err| {
+            debug.print("Error while initializing frame allocator: {}\n", .{err});
+            while (true) {}
+        };
+
+        var init_directory = std.mem.zeroes(vmm.PageDirectory);
+        const base: usize = vmm.createInitialMappings(&allocator, tag, &init_directory) catch |err| {
+            debug.print("Error while creating initial mappings: {}\n", .{err});
+            while (true) {}
+        };
+
+        debug.print("Physical memory base mapping for init: {x}\n", .{base});
+    } else {
+        debug.print("No memory map multiboot tag found!\n", .{});
+    }
+
+    asm volatile ("int3");
+
+    while (true) {}
+}
+
+pub fn panic(message: []const u8, _: ?*std.builtin.StackTrace, _: ?usize) noreturn {
+    debug.print("--- KERNEL PANIC! ---\n", .{});
+    debug.print("{s}\n", .{message});
+    while (true) {}
+}
diff --git a/core/src/mmap.zig b/core/src/mmap.zig
new file mode 100644
index 0000000..e9622e5
--- /dev/null
+++ b/core/src/mmap.zig
@@ -0,0 +1,61 @@
+const easyboot = @cImport(@cInclude("easyboot.h"));
+
+const MemoryMapIterator = struct {
+    tag: *easyboot.multiboot_tag_mmap_t,
+    entry: ?*easyboot.multiboot_mmap_entry_t,
+    end: usize,
+
+    pub fn next(self: *MemoryMapIterator) ?*easyboot.multiboot_mmap_entry_t {
+        if (self.entry) |e| {
+            const current_entry = self.entry;
+
+            var new_entry: [*c]u8 = @ptrCast(e);
+            new_entry += self.tag.entry_size;
+            self.entry = @alignCast(@ptrCast(new_entry));
+
+            if (@intFromPtr(self.entry) >= self.end) self.entry = null;
+
+            return current_entry;
+        }
+
+        return null;
+    }
+};
+
+pub fn createMemoryMapIterator(tag: *easyboot.multiboot_tag_mmap_t) MemoryMapIterator {
+    return MemoryMapIterator{ .tag = tag, .entry = @alignCast(@ptrCast(tag.entries())), .end = @intFromPtr(tag) + tag.size };
+}
+
+pub fn findLargestFreeEntry(tag: *easyboot.multiboot_tag_mmap_t) ?*easyboot.multiboot_mmap_entry_t {
+    var max_length: u64 = 0;
+    var biggest_entry: ?*easyboot.multiboot_mmap_entry_t = null;
+
+    var iter = createMemoryMapIterator(tag);
+
+    while (iter.next()) |entry| {
+        if (entry.type == easyboot.MULTIBOOT_MEMORY_AVAILABLE and entry.length > max_length) {
+            max_length = entry.length;
+            biggest_entry = entry;
+        }
+    }
+
+    return biggest_entry;
+}
+
+pub fn findHighestEntry(tag: *easyboot.multiboot_tag_mmap_t) ?*easyboot.multiboot_mmap_entry_t {
+    var highest_entry: ?*easyboot.multiboot_mmap_entry_t = null;
+
+    var iter = createMemoryMapIterator(tag);
+
+    while (iter.next()) |entry| {
+        highest_entry = entry;
+    }
+
+    return highest_entry;
+}
+
+pub fn getAddressSpaceSize(tag: *easyboot.multiboot_tag_mmap_t) ?usize {
+    const highest_entry = findHighestEntry(tag) orelse return null;
+
+    return highest_entry.base_addr + highest_entry.length;
+}
diff --git a/core/src/multiboot.zig b/core/src/multiboot.zig
new file mode 100644
index 0000000..dcfc0a8
--- /dev/null
+++ b/core/src/multiboot.zig
@@ -0,0 +1,231 @@
+const std = @import("std");
+const easyboot = @cImport(@cInclude("easyboot.h"));
+const debug = @import("arch/debug.zig");
+
+fn dumpUUID(uuid: [16]u8) void {
+    debug.print("{x:0^2}{x:0^2}{x:0^2}{x:0^2}-{x:0^2}{x:0^2}-{x:0^2}{x:0^2}-{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}{x:0^2}\n", .{ uuid[3], uuid[2], uuid[1], uuid[0], uuid[5], uuid[4], uuid[7], uuid[6], uuid[8], uuid[9], uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15] });
+}
+
+const MultibootInfo = [*]u8;
+
+/// Return the first multiboot tag of the given type.
+pub fn findMultibootTag(comptime Type: type, info: MultibootInfo) ?*Type {
+    const mb_tag: *easyboot.multiboot_info_t = @alignCast(@ptrCast(info));
+    const mb_size = mb_tag.total_size;
+
+    var tag: *easyboot.multiboot_tag_t = @alignCast(@ptrCast(info + 8));
+    const last = @intFromPtr(info) + mb_size;
+    while ((@intFromPtr(tag) < last) and (tag.type != easyboot.MULTIBOOT_TAG_TYPE_END)) {
+        switch (tag.type) {
+            easyboot.MULTIBOOT_TAG_TYPE_CMDLINE => {
+                if (Type == easyboot.multiboot_tag_cmdline_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_BOOT_LOADER_NAME => {
+                if (Type == easyboot.multiboot_tag_loader_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_MODULE => {
+                if (Type == easyboot.multiboot_tag_module_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_MMAP => {
+                if (Type == easyboot.multiboot_tag_mmap_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_FRAMEBUFFER => {
+                if (Type == easyboot.multiboot_tag_framebuffer_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EFI64 => {
+                if (Type == easyboot.multiboot_tag_efi64_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EFI64_IH => {
+                if (Type == easyboot.multiboot_tag_efi64_ih_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_SMBIOS => {
+                if (Type == easyboot.multiboot_tag_smbios_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_ACPI_OLD => {
+                if (Type == easyboot.multiboot_tag_old_acpi_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_ACPI_NEW => {
+                if (Type == easyboot.multiboot_tag_new_acpi_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_SMP => {
+                if (Type == easyboot.multiboot_tag_smp_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_PARTUUID => {
+                if (Type == easyboot.multiboot_tag_partuuid_t) return @alignCast(@ptrCast(tag));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EDID => {
+                if (Type == easyboot.multiboot_tag_edid_t) return @alignCast(@ptrCast(tag));
+            },
+            else => {},
+        }
+
+        var new_tag: [*]u8 = @ptrCast(tag);
+        new_tag += ((tag.size + 7) & ~@as(usize, 7));
+        tag = @alignCast(@ptrCast(new_tag));
+    }
+
+    return null;
+}
+
+/// Find every multiboot tag of the given type.
+pub fn findMultibootTags(comptime Type: type, info: MultibootInfo, callback: *const fn (tag: *Type) void) void {
+    const mb_tag: *easyboot.multiboot_info_t = @alignCast(@ptrCast(info));
+    const mb_size = mb_tag.total_size;
+
+    var tag: *easyboot.multiboot_tag_t = @alignCast(@ptrCast(info + 8));
+    const last = @intFromPtr(info) + mb_size;
+    while ((@intFromPtr(tag) < last) and (tag.type != easyboot.MULTIBOOT_TAG_TYPE_END)) {
+        switch (tag.type) {
+            easyboot.MULTIBOOT_TAG_TYPE_CMDLINE => {
+                if (Type == easyboot.multiboot_tag_cmdline_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_BOOT_LOADER_NAME => {
+                if (Type == easyboot.multiboot_tag_loader_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_MODULE => {
+                if (Type == easyboot.multiboot_tag_module_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_MMAP => {
+                if (Type == easyboot.multiboot_tag_mmap_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_FRAMEBUFFER => {
+                if (Type == easyboot.multiboot_tag_framebuffer_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EFI64 => {
+                if (Type == easyboot.multiboot_tag_efi64_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EFI64_IH => {
+                if (Type == easyboot.multiboot_tag_efi64_ih_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_SMBIOS => {
+                if (Type == easyboot.multiboot_tag_smbios_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_ACPI_OLD => {
+                if (Type == easyboot.multiboot_tag_old_acpi_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_ACPI_NEW => {
+                if (Type == easyboot.multiboot_tag_new_acpi_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_SMP => {
+                if (Type == easyboot.multiboot_tag_smp_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_PARTUUID => {
+                if (Type == easyboot.multiboot_tag_partuuid_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EDID => {
+                if (Type == easyboot.multiboot_tag_edid_t) callback(@alignCast(@ptrCast(tag)));
+            },
+            else => {},
+        }
+
+        var new_tag: [*]u8 = @ptrCast(tag);
+        new_tag += ((tag.size + 7) & ~@as(usize, 7));
+        tag = @alignCast(@ptrCast(new_tag));
+    }
+}
+
+/// Log every multiboot tag in a multiboot struct.
+pub fn parseMultibootTags(info: MultibootInfo) void {
+    const mb_tag: *easyboot.multiboot_info_t = @alignCast(@ptrCast(info));
+    const mb_size = mb_tag.total_size;
+
+    var tag: *easyboot.multiboot_tag_t = @alignCast(@ptrCast(info + 8));
+    const last = @intFromPtr(info) + mb_size;
+    while ((@intFromPtr(tag) < last) and (tag.type != easyboot.MULTIBOOT_TAG_TYPE_END)) {
+        switch (tag.type) {
+            easyboot.MULTIBOOT_TAG_TYPE_CMDLINE => {
+                var cmdline: *easyboot.multiboot_tag_cmdline_t = @alignCast(@ptrCast(tag));
+                debug.print("Command line = {s}\n", .{std.mem.sliceTo(cmdline.string(), 0)});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_BOOT_LOADER_NAME => {
+                var bootloader: *easyboot.multiboot_tag_loader_t = @alignCast(@ptrCast(tag));
+                debug.print("Boot loader name = {s}\n", .{std.mem.sliceTo(bootloader.string(), 0)});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_MODULE => {
+                var module: *easyboot.multiboot_tag_module_t = @alignCast(@ptrCast(tag));
+                debug.print("Module at {x}-{x}. Command line {s}\n", .{ module.mod_start, module.mod_end, std.mem.sliceTo(module.string(), 0) });
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_MMAP => {
+                var mmap: *easyboot.multiboot_tag_mmap_t = @alignCast(@ptrCast(tag));
+                debug.print("Memory map:\n", .{});
+                var entry: *easyboot.multiboot_mmap_entry_t = @alignCast(@ptrCast(mmap.entries()));
+                const end = @intFromPtr(tag) + tag.size;
+                while (@intFromPtr(entry) < end) {
+                    debug.print(" base_addr = {x}, length = {x}, type = {x} {s}, res = {x}\n", .{ entry.base_addr, entry.length, entry.type, switch (entry.type) {
+                        easyboot.MULTIBOOT_MEMORY_AVAILABLE => "free",
+                        easyboot.MULTIBOOT_MEMORY_ACPI_RECLAIMABLE => "ACPI",
+                        easyboot.MULTIBOOT_MEMORY_NVS => "ACPI NVS",
+                        else => "used",
+                    }, entry.reserved });
+
+                    var new_entry: [*c]u8 = @ptrCast(entry);
+                    new_entry += mmap.entry_size;
+                    entry = @alignCast(@ptrCast(new_entry));
+                }
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_FRAMEBUFFER => {
+                const fb: *easyboot.multiboot_tag_framebuffer_t = @alignCast(@ptrCast(tag));
+                debug.print("Framebuffer: \n", .{});
+                debug.print(" address {x} pitch {d}\n", .{ fb.framebuffer_addr, fb.framebuffer_pitch });
+                debug.print(" width {d} height {d} depth {d} bpp\n", .{ fb.framebuffer_width, fb.framebuffer_height, fb.framebuffer_bpp });
+                debug.print(" red channel:   at {d}, {d} bits\n", .{ fb.framebuffer_red_field_position, fb.framebuffer_red_mask_size });
+                debug.print(" green channel: at {d}, {d} bits\n", .{ fb.framebuffer_green_field_position, fb.framebuffer_green_mask_size });
+                debug.print(" blue channel:  at {d}, {d} bits\n", .{ fb.framebuffer_blue_field_position, fb.framebuffer_blue_mask_size });
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EFI64 => {
+                const efi: *easyboot.multiboot_tag_efi64_t = @alignCast(@ptrCast(tag));
+                debug.print("EFI system table {x}\n", .{efi.pointer});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EFI64_IH => {
+                const efi_ih: *easyboot.multiboot_tag_efi64_t = @alignCast(@ptrCast(tag));
+                debug.print("EFI image handle {x}\n", .{efi_ih.pointer});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_SMBIOS => {
+                const smbios: *easyboot.multiboot_tag_smbios_t = @alignCast(@ptrCast(tag));
+                debug.print("SMBIOS table major {d} minor {d}\n", .{ smbios.major, smbios.minor });
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_ACPI_OLD => {
+                const acpi: *easyboot.multiboot_tag_old_acpi_t = @alignCast(@ptrCast(tag));
+                const rsdp = @intFromPtr(acpi.rsdp());
+                debug.print("ACPI table (1.0, old RSDP) at {x}\n", .{rsdp});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_ACPI_NEW => {
+                const acpi: *easyboot.multiboot_tag_new_acpi_t = @alignCast(@ptrCast(tag));
+                const rsdp = @intFromPtr(acpi.rsdp());
+                debug.print("ACPI table (2.0, new RSDP) at {x}\n", .{rsdp});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_SMP => {
+                const smp: *easyboot.multiboot_tag_smp_t = @alignCast(@ptrCast(tag));
+                debug.print("SMP supported\n", .{});
+                debug.print(" {d} core(s)\n", .{smp.numcores});
+                debug.print(" {d} running\n", .{smp.running});
+                debug.print(" {x} bsp id\n", .{smp.bspid});
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_PARTUUID => {
+                const part: *easyboot.multiboot_tag_partuuid_t = @alignCast(@ptrCast(tag));
+                debug.print("Partition UUIDs\n", .{});
+                debug.print(" boot ", .{});
+                dumpUUID(part.bootuuid);
+                if (tag.size >= 40) {
+                    debug.print(" root ", .{});
+                    dumpUUID(part.rootuuid);
+                }
+            },
+            easyboot.MULTIBOOT_TAG_TYPE_EDID => {
+                const edid_tag: *easyboot.multiboot_tag_edid_t = @alignCast(@ptrCast(tag));
+                const edid: []u8 = edid_tag.edid()[0 .. tag.size - @sizeOf(easyboot.multiboot_tag_t)];
+                debug.print("EDID info\n", .{});
+                debug.print(" manufacturer ID {x}{x}\n", .{ edid[8], edid[9] });
+                debug.print(" EDID ID {x}{x} Version {d} Rev {d}\n", .{ edid[10], edid[11], edid[18], edid[19] });
+                debug.print(" monitor type {x} size {d} cm x {d} cm\n", .{ edid[20], edid[21], edid[22] });
+            },
+            else => {
+                debug.print("Unknown MBI tag, this shouldn't happen with Simpleboot/Easyboot!---\n", .{});
+            },
+        }
+
+        var new_tag: [*]u8 = @ptrCast(tag);
+        new_tag += ((tag.size + 7) & ~@as(usize, 7));
+        tag = @alignCast(@ptrCast(new_tag));
+    }
+}
diff --git a/core/src/pmm.zig b/core/src/pmm.zig
new file mode 100644
index 0000000..6d0fd5a
--- /dev/null
+++ b/core/src/pmm.zig
@@ -0,0 +1,103 @@
+const std = @import("std");
+const easyboot = @cImport(@cInclude("easyboot.h"));
+const platform = @import("arch/platform.zig").arch;
+const mmap = @import("mmap.zig");
+const bmap = @import("lib/bitmap.zig");
+
+const FrameAllocatorError = error{
+    InvalidMemoryMap,
+    MemoryAlreadyInUse,
+    MemoryNotInUse,
+    OutOfMemory,
+};
+
+pub const FrameAllocator = struct {
+    bitmap: bmap.Bitmap,
+    free_memory: u64,
+    used_memory: u64,
+    reserved_memory: u64,
+    start_index: usize,
+};
+
+pub const PhysFrame = struct {
+    address: usize,
+
+    pub fn virtualAddress(self: *const PhysFrame, base: usize) usize {
+        return base + self.address;
+    }
+};
+
+pub fn lockFrame(allocator: *FrameAllocator, address: usize) !void {
+    const index = address / platform.PAGE_SIZE;
+    if (try allocator.bitmap.get(index) == 1) return error.MemoryAlreadyInUse;
+    try allocator.bitmap.set(index, 1);
+    allocator.used_memory += platform.PAGE_SIZE;
+    allocator.free_memory -= platform.PAGE_SIZE;
+}
+
+pub fn lockFrames(allocator: *FrameAllocator, address: usize, pages: usize) !void {
+    var index: usize = 0;
+    while (index < pages) : (index += 1) {
+        try lockFrame(allocator, address + (index * platform.PAGE_SIZE));
+    }
+}
+
+pub fn freeFrame(allocator: *FrameAllocator, address: usize) !void {
+    const index = address / platform.PAGE_SIZE;
+    if (try allocator.bitmap.get(index) == 0) return error.MemoryNotInUse;
+    try allocator.bitmap.set(index, 0);
+    allocator.used_memory -= platform.PAGE_SIZE;
+    allocator.free_memory += platform.PAGE_SIZE;
+
+    if (allocator.start_index > index) allocator.start_index = index;
+}
+
+pub fn freeFrames(allocator: *FrameAllocator, address: usize, pages: usize) !void {
+    const index: usize = 0;
+    while (index < pages) : (index += 1) {
+        try freeFrame(allocator, address + (index * platform.PAGE_SIZE));
+    }
+}
+
+pub fn allocFrame(allocator: *FrameAllocator) !PhysFrame {
+    const index: usize = try bmap.findInBitmap(&allocator.bitmap, 0, allocator.start_index) orelse return error.OutOfMemory;
+    const address = index * platform.PAGE_SIZE;
+    try lockFrame(allocator, address);
+
+    allocator.start_index = index + 1;
+
+    return PhysFrame{ .address = address };
+}
+
+pub fn initializeFrameAllocator(tag: *easyboot.multiboot_tag_mmap_t) !FrameAllocator {
+    const largest_free = mmap.findLargestFreeEntry(tag) orelse return error.InvalidMemoryMap;
+    const physical_address_space_size = mmap.getAddressSpaceSize(tag) orelse return error.InvalidMemoryMap;
+
+    const bitmap_base_address: [*]u8 = @ptrFromInt(largest_free.base_addr);
+
+    const bitmap_bit_size = physical_address_space_size / @as(usize, platform.PAGE_SIZE);
+    const bitmap_size: usize = try std.math.divCeil(usize, bitmap_bit_size, 8);
+
+    var allocator: FrameAllocator = FrameAllocator{ .bitmap = bmap.createBitmap(bitmap_base_address, bitmap_size), .free_memory = 0, .used_memory = 0, .reserved_memory = 0, .start_index = 0 };
+
+    allocator.bitmap.clear(1); // Set all pages to used/reserved by default, then clear out the free ones
+
+    var iter = mmap.createMemoryMapIterator(tag);
+    while (iter.next()) |entry| {
+        const index = entry.base_addr / platform.PAGE_SIZE;
+        const pages = entry.length / platform.PAGE_SIZE;
+
+        if (entry.type != easyboot.MULTIBOOT_MEMORY_AVAILABLE) {
+            allocator.reserved_memory += entry.length;
+            continue;
+        }
+
+        allocator.free_memory += entry.length;
+        try bmap.updateBitmapRegion(&allocator.bitmap, index, pages, 0);
+    }
+
+    const frames_to_lock = try std.math.divCeil(usize, bitmap_size, platform.PAGE_SIZE);
+    try lockFrames(&allocator, @intFromPtr(bitmap_base_address), frames_to_lock);
+
+    return allocator;
+}
diff --git a/core/src/sys/print.zig b/core/src/sys/print.zig
new file mode 100644
index 0000000..53a1c42
--- /dev/null
+++ b/core/src/sys/print.zig
@@ -0,0 +1,7 @@
+const interrupts = @import("../arch/interrupts.zig").arch;
+const sys = @import("syscall.zig");
+const debug = @import("../arch/debug.zig");
+
+pub fn print(_: *interrupts.InterruptStackFrame, args: *sys.Arguments, _: *isize) anyerror!void {
+    debug.print("The userspace program gave us the number {x}\n", .{args.arg0});
+}
diff --git a/core/src/sys/syscall.zig b/core/src/sys/syscall.zig
new file mode 100644
index 0000000..aac85be
--- /dev/null
+++ b/core/src/sys/syscall.zig
@@ -0,0 +1,28 @@
+const std = @import("std");
+const interrupts = @import("../arch/interrupts.zig").arch;
+const print = @import("print.zig").print;
+
+pub const Arguments = struct {
+    arg0: usize,
+    arg1: usize,
+    arg2: usize,
+    arg3: usize,
+    arg4: usize,
+    arg5: usize,
+};
+
+const SystemCall = *const fn (frame: *interrupts.InterruptStackFrame, args: *Arguments, retval: *isize) anyerror!void;
+
+const syscalls = [_]SystemCall{print};
+
+pub fn invokeSyscall(number: usize, frame: *interrupts.InterruptStackFrame, args: *Arguments, retval: *isize) void {
+    if (number >= syscalls.len) {
+        retval.* = -1;
+        return;
+    }
+
+    syscalls[number](frame, args, retval) catch {
+        retval.* = -1;
+        return;
+    };
+}
diff --git a/easyboot b/easyboot
new file mode 160000
index 0000000..31d2059
--- /dev/null
+++ b/easyboot
@@ -0,0 +1 @@
+Subproject commit 31d20593cf83bb212bbe73418211d8058bb23007
diff --git a/system/build.zig b/system/build.zig
new file mode 100644
index 0000000..04851d0
--- /dev/null
+++ b/system/build.zig
@@ -0,0 +1,9 @@
+const std = @import("std");
+const init = @import("init/build.zig");
+
+pub fn build(b: *std.Build, build_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void {
+    const system_step = b.step("system", "Build core system services");
+    init.build(b, system_step, optimize);
+
+    build_step.dependOn(system_step);
+}
diff --git a/system/init/build.zig b/system/init/build.zig
new file mode 100644
index 0000000..2938f19
--- /dev/null
+++ b/system/init/build.zig
@@ -0,0 +1,43 @@
+const std = @import("std");
+
+const here = "system/init";
+
+pub fn build(b: *std.Build, build_step: *std.Build.Step, optimize: std.builtin.OptimizeMode) void {
+    var disabled_features = std.Target.Cpu.Feature.Set.empty;
+    var enabled_features = std.Target.Cpu.Feature.Set.empty;
+
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.mmx));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.sse2));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx));
+    disabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.avx2));
+    enabled_features.addFeature(@intFromEnum(std.Target.x86.Feature.soft_float));
+
+    const target_query = std.Target.Query{
+        .cpu_arch = std.Target.Cpu.Arch.x86_64,
+        .os_tag = std.Target.Os.Tag.freestanding,
+        .abi = std.Target.Abi.none,
+        .cpu_features_sub = disabled_features,
+        .cpu_features_add = enabled_features,
+    };
+
+    const init = b.addExecutable(.{
+        .name = "init",
+        .root_source_file = b.path(here ++ "/main.zig"),
+        .target = b.resolveTargetQuery(target_query),
+        .optimize = optimize,
+        .code_model = .default,
+    });
+
+    const install = b.addInstallArtifact(init, .{
+        .dest_dir = .{
+            .override = .{ .custom = "boot/" },
+        },
+    });
+
+    var init_step = b.step("init", "Build the init service");
+    init_step.dependOn(&init.step);
+    init_step.dependOn(&install.step);
+
+    build_step.dependOn(init_step);
+}
diff --git a/system/init/main.zig b/system/init/main.zig
new file mode 100644
index 0000000..c79eb4e
--- /dev/null
+++ b/system/init/main.zig
@@ -0,0 +1,13 @@
+fn syscall(num: u64, arg: u64) void {
+    asm volatile ("int $66"
+        :
+        : [num] "{rax}" (num),
+          [arg] "{rdi}" (arg),
+    );
+}
+
+export fn _start(base: u64) callconv(.C) noreturn {
+    syscall(0, base);
+
+    while (true) {}
+}
diff --git a/tools/iso.sh b/tools/iso.sh
new file mode 100755
index 0000000..3fae27c
--- /dev/null
+++ b/tools/iso.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -e
+
+cd $(realpath $(dirname $0)/..)
+
+tools/bin/easyboot -e boot astryon.iso
diff --git a/tools/run.sh b/tools/run.sh
new file mode 100755
index 0000000..d650930
--- /dev/null
+++ b/tools/run.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+cd $(realpath $(dirname $0)/..)
+
+qemu-system-x86_64 -cdrom astryon.iso -serial stdio -enable-kvm $@