diff --git a/Makefile b/Makefile
index 6652838..5ec4fa9 100644
--- a/Makefile
+++ b/Makefile
@@ -34,12 +34,22 @@ endif
 
 HEADERS = $(shell find . -name *.h)
 
+ASM_SOURCES = $(shell find asm/ -name *.c)
+ASM_OBJECTS = $(patsubst %.c,build/%.o,$(ASM_SOURCES))
+
 VM_SOURCES = $(shell find vm/ -name *.c)
 VM_OBJECTS = $(patsubst %.c,build/%.o,$(VM_SOURCES))
 
-all: bin/vm
+KERN_SOURCES = $(shell find kern/ -name *.c)
+KERN_OBJECTS = $(patsubst %.c,build/%.o,$(KERN_SOURCES))
 
-bin/vm: $(VM_OBJECTS)
+all: bin/vm bin/build_disk_image
+
+bin/vm: $(VM_OBJECTS) $(ASM_OBJECTS)
+	@mkdir -p $(dir $@)
+	$(CC) $^ -o $@ $(F_FLAGS) $(OPTIMIZATION) $(L_FLAGS)
+
+bin/build_disk_image: $(KERN_OBJECTS) $(ASM_OBJECTS)
 	@mkdir -p $(dir $@)
 	$(CC) $^ -o $@ $(F_FLAGS) $(OPTIMIZATION) $(L_FLAGS)
 
diff --git a/vm/asm.c b/asm/asm.c
similarity index 99%
rename from vm/asm.c
rename to asm/asm.c
index 68c8642..d19c45b 100644
--- a/vm/asm.c
+++ b/asm/asm.c
@@ -1,5 +1,5 @@
 #include "asm.h"
-#include "vm.h"
+#include "common/op_str.h"
 #include <stdbool.h>
 #include <stddef.h>
 #include <stdint.h>
@@ -633,6 +633,9 @@ uint16_t assemble_to_binary(uint16_t* out, const Line* lines, size_t lines_size)
         printf("done!\n");
     }
 
+    free(unres_labels);
+    free(res_labels);
+
     return ip * 2;
 }
 
diff --git a/vm/asm.h b/asm/asm.h
similarity index 99%
rename from vm/asm.h
rename to asm/asm.h
index c706b5b..64a999f 100644
--- a/vm/asm.h
+++ b/asm/asm.h
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "vm.h"
+#include "common/arch.h"
 #include <stddef.h>
 #include <stdint.h>
 
diff --git a/common/arch.h b/common/arch.h
new file mode 100644
index 0000000..32c9f91
--- /dev/null
+++ b/common/arch.h
@@ -0,0 +1,69 @@
+#pragma once
+
+typedef enum {
+    R0 = 0,
+    R1 = 1,
+    R2 = 2,
+    R3 = 3,
+    R4 = 4,
+    Rbp = 5,
+    Rsp = 6,
+    Rfl = 7,
+    Rcs = 8,
+    Rip = 9,
+} Reg;
+
+typedef enum {
+    Fl_Zero,
+    Fl_Eq,
+    Fl_Be,
+    Fl_Lt,
+    Fl_Err,
+    Fl_Int,
+    Fl_Vcd,
+} Flag;
+
+typedef enum {
+    Op_Nop,
+    Op_Hlt,
+    Op_Jmp,
+    Op_Jnz,
+    Op_Test,
+    Op_Cmp,
+    Op_Mov8,
+    Op_Mov16,
+    Op_In,
+    Op_Out,
+    Op_Call,
+    Op_Ret,
+    Op_Lit,
+    Op_Int,
+    Op_Or,
+    Op_Xor,
+    Op_And,
+    Op_Shl,
+    Op_RShl,
+    Op_Shr,
+    Op_RShr,
+    Op_Add,
+    Op_Sub,
+    Op_RSub,
+    Op_Mul,
+    Op_IMul,
+    Op_Div,
+    Op_IDiv,
+    Op_RDiv,
+    Op_RIDiv,
+    Op_Mod,
+    Op_RMod,
+} Op;
+
+typedef enum {
+    Int_DiskRead = 0,
+    Int_DiskWrite = 1,
+    Int_Key = 32,
+} VM_Int;
+
+typedef enum {
+    Device_Keyboard,
+} VM_Device;
diff --git a/common/op_str.h b/common/op_str.h
new file mode 100644
index 0000000..a22dde3
--- /dev/null
+++ b/common/op_str.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "arch.h"
+
+static inline const char* op_str(Op op)
+{
+    switch (op) {
+        case Op_Nop:
+            return "nop";
+        case Op_Hlt:
+            return "hlt";
+        case Op_Jmp:
+            return "jmp";
+        case Op_Jnz:
+            return "jnz";
+        case Op_Test:
+            return "test";
+        case Op_Cmp:
+            return "cmp";
+        case Op_Mov8:
+            return "mov8";
+        case Op_Mov16:
+            return "mov16";
+        case Op_In:
+            return "in";
+        case Op_Out:
+            return "out";
+        case Op_Call:
+            return "call";
+        case Op_Ret:
+            return "ret";
+        case Op_Lit:
+            return "lit";
+        case Op_Int:
+            return "int";
+        case Op_Or:
+            return "or";
+        case Op_Xor:
+            return "xor";
+        case Op_And:
+            return "and";
+        case Op_Shl:
+            return "shl";
+        case Op_RShl:
+            return "rshl";
+        case Op_Shr:
+            return "shr";
+        case Op_RShr:
+            return "rshr";
+        case Op_Add:
+            return "add";
+        case Op_Sub:
+            return "sub";
+        case Op_RSub:
+            return "rsub";
+        case Op_Mul:
+            return "mul";
+        case Op_IMul:
+            return "imul";
+        case Op_Div:
+            return "div";
+        case Op_IDiv:
+            return "idiv";
+        case Op_RDiv:
+            return "rdiv";
+        case Op_RIDiv:
+            return "ridiv";
+        case Op_Mod:
+            return "mod";
+        case Op_RMod:
+            return "rmod";
+    }
+    return "---";
+}
diff --git a/common/video_character_display.h b/common/video_character_display.h
new file mode 100644
index 0000000..bf9059b
--- /dev/null
+++ b/common/video_character_display.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#define vcd_ch_width 8
+#define vcd_ch_height 8
+static const int vcd_width_in_ch = 40;
+static const int vcd_height_in_ch = 12;
+
+static const int vcd_px_width = 4;
+static const int vcd_px_height = 8;
+static const int vcd_width_in_px
+    = vcd_width_in_ch * vcd_ch_width * vcd_px_width;
+static const int vcd_height_in_px
+    = vcd_height_in_ch * vcd_ch_height * vcd_px_height;
diff --git a/kern/main.c b/kern/main.c
new file mode 100644
index 0000000..e0eb8bc
--- /dev/null
+++ b/kern/main.c
@@ -0,0 +1,215 @@
+#include "asm/asm.h"
+#include "common/video_character_display.h"
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+const size_t block_size = 512;
+const size_t block_amount = 32;
+
+void write_program(FILE* fp);
+
+int main(void)
+{
+    FILE* fp = fopen("build/image", "wb");
+    if (!fp) {
+        fprintf(
+            stderr, "error: could not open build/image: %s\n", strerror(errno));
+        return -1;
+    }
+
+    printf("clearing disk...\n");
+    uint8_t* data = calloc(block_size, 1);
+    for (size_t i = 0; i < block_amount; ++i) {
+        fwrite(data, 1, block_size, fp);
+    }
+    free(data);
+    fseek(fp, 0, SEEK_SET);
+    printf("done!\n");
+
+    write_program(fp);
+
+    fclose(fp);
+}
+
+void write_program(FILE* fp)
+{
+    int label_ids = 0;
+
+    int main_loop = label_ids++;
+    int interrupt_table = label_ids++;
+    int keyboard_interrupt = label_ids++;
+    int keyboard_interrupt_0 = label_ids++;
+    int keyboard_interrupt_1 = label_ids++;
+    int keyboard_interrupt_2 = label_ids++;
+    int keyboard_interrupt_3 = label_ids++;
+    int keyboard_interrupt_4 = label_ids++;
+    int put_char = label_ids++;
+    int put_char_0 = label_ids++;
+    int put_char_1 = label_ids++;
+    int screen_x = label_ids++;
+    int screen_y = label_ids++;
+
+#define L(LABEL) s_label(LABEL)
+
+    Line program_asm[] = {
+        // clang-format off
+
+            // rsp points *at* the top element
+            s_mov16_r_i(Rbp, 2048),
+            s_mov16_r_i(Rsp, 2048 - 2),
+
+            s_lit_l(interrupt_table),
+            s_or_i(Rfl, Rfl, 1 << Fl_Int),
+
+            s_or_i(Rfl, Rfl, 1 << Fl_Vcd),
+
+            s_mov16_r_i(R0, 512),
+            s_mov16_r_i(R1, 1),
+            s_int(Int_DiskRead),
+
+        L(main_loop),
+            s_hlt(),
+            s_jmp_l(main_loop),
+
+        L(interrupt_table),
+            // size
+            s_data_i(1),
+            s_data_l(keyboard_interrupt),
+            s_nop(),
+
+        L(keyboard_interrupt),
+            s_and_i(Rfl, Rfl, (uint16_t)~(1 << Fl_Int)),
+            s_push_r(Rbp),
+            s_mov16_r_r(Rbp, Rsp),
+            s_push_r(R0),
+            s_push_r(R1),
+            s_push_r(R2),
+            s_push_r(R3),
+
+            s_in_i(R0, Device_Keyboard),
+
+            s_cmp_i(R0, 44),
+            s_mov16_r_r(R1, Rfl),
+            s_and_i(R1, R1, 1 << Fl_Eq),
+            s_jnz_l(R1, keyboard_interrupt_0),
+
+            s_cmp_i(R0, 42),
+            s_mov16_r_r(R1, Rfl),
+            s_and_i(R1, R1, 1 << Fl_Eq),
+            s_jnz_l(R1, keyboard_interrupt_1),
+
+            s_cmp_i(R0, 40),
+            s_mov16_r_r(R1, Rfl),
+            s_and_i(R1, R1, 1 << Fl_Eq),
+            s_jnz_l(R1, keyboard_interrupt_2),
+
+            s_jmp_l(keyboard_interrupt_3),
+
+        L(keyboard_interrupt_0),
+            s_mov16_r_i(R0, ' '),
+            s_call_l(put_char),
+            s_jmp_l(keyboard_interrupt_4),
+
+        L(keyboard_interrupt_1),
+            s_mov16_r_ml(R1, screen_x),
+            s_cmp_i(R1, 0),
+            s_mov16_r_r(R2, Rfl),
+            s_and_i(R2, R2, 1 << Fl_Eq),
+            s_jnz_l(R2, keyboard_interrupt_4),
+            s_sub_i(R1, R1, 1),
+            s_mov16_ml_r(screen_x, R1),
+            s_mov16_r_i(R0, ' '),
+            s_call_l(put_char),
+            s_mov16_r_ml(R1, screen_x),
+            s_sub_i(R1, R1, 1),
+            s_mov16_ml_r(screen_x, R1),
+            s_jmp_l(keyboard_interrupt_4),
+
+        L(keyboard_interrupt_2),
+            s_mov16_r_ml(R1, screen_y),
+            s_add_i(R1, R1, 1),
+            s_mov16_ml_r(screen_y, R1),
+            s_mov16_r_i(R1, 0),
+            s_mov16_ml_r(screen_x, R1),
+            s_jmp_l(keyboard_interrupt_4),
+
+        L(keyboard_interrupt_3),
+            s_add_i(R0, R0, 'A' - 4),
+            s_call_l(put_char),
+            s_jmp_l(keyboard_interrupt_4),
+
+        L(keyboard_interrupt_4),
+
+            s_pop_r(R3),
+            s_pop_r(R2),
+            s_pop_r(R1),
+            s_pop_r(R0),
+            s_mov16_r_r(Rsp, Rbp),
+            s_pop_r(Rbp),
+            s_or_i(Rfl, Rfl, 1 << Fl_Int),
+            s_iret(),
+
+        L(put_char),
+            s_push_r(Rbp),
+            s_mov16_r_r(Rbp, Rsp),
+            s_push_r(R1),
+            s_push_r(R2),
+
+            s_mov16_r_ml(R2, screen_y),
+            s_mul_i(R2, R2, vcd_width_in_ch),
+            s_mov16_r_ml(R1, screen_x),
+            s_add_i(R1, R1, 0x0c00),
+            s_add_r(R1, R1, R2),
+            s_mov8_mr_r(R1, R0),
+
+            s_mov16_r_ml(R1, screen_x),
+            s_add_i(R1, R1, 1),
+            s_mov16_ml_r(screen_x, R1),
+
+            s_cmp_i(R1, vcd_width_in_ch),
+            s_mov16_r_r(R2, Rfl),
+            s_and_i(R2, R2, 1 << Fl_Eq),
+            s_jnz_l(R2, put_char_0),
+            s_jmp_l(put_char_1),
+
+        L(put_char_0),
+            s_mov16_r_ml(R1, screen_y),
+            s_add_i(R1, R1, 1),
+            s_mov16_ml_r(screen_y, R1),
+            s_mov16_r_i(R1, 0),
+            s_mov16_ml_r(screen_x, R1),
+
+        L(put_char_1),
+
+            s_pop_r(R1),
+            s_pop_r(R2),
+            s_mov16_r_r(Rsp, Rbp),
+            s_pop_r(Rbp),
+            s_ret(),
+
+        L(screen_x),
+            s_data_i(0),
+        L(screen_y),
+            s_data_i(0),
+
+        // clang-format on
+    };
+
+    size_t program_asm_size = sizeof(program_asm) / sizeof(program_asm[0]);
+
+    uint16_t* program = calloc(512 * 2, sizeof(uint16_t));
+
+    printf("assembling program...\n");
+    uint16_t program_size
+        = assemble_to_binary(program, program_asm, program_asm_size);
+    printf("done!\n");
+    printf("program size = %d\n", program_size);
+
+    printf("writing program to disk...\n");
+    fwrite(program, sizeof(uint16_t), program_size, fp);
+    printf("done!\n");
+
+    free(program);
+}
diff --git a/vm/main.c b/vm/main.c
index 9fb8136..822d784 100644
--- a/vm/main.c
+++ b/vm/main.c
@@ -1,5 +1,7 @@
+#include "asm/asm.h"
+#include "common/op_str.h"
+#include "common/video_character_display.h"
 #include "vm.h"
-#include "vm/asm.h"
 #include <SDL2/SDL.h>
 #include <SDL2/SDL_error.h>
 #include <SDL2/SDL_events.h>
@@ -75,16 +77,6 @@ Interrupt int_queue_pop(InterruptQueue* queue)
     return val;
 }
 
-#define ch_width 8
-#define ch_height 8
-static const int width_in_ch = 40;
-static const int height_in_ch = 12;
-
-static const int px_width = 4;
-static const int px_height = 8;
-static const int width_in_px = width_in_ch * ch_width * px_width;
-static const int height_in_px = height_in_ch * ch_height * px_height;
-
 typedef struct {
     IODevice io_device;
 
@@ -121,7 +113,7 @@ int sdldevice_construct(SdlDevice* device)
     SDL_Window* window;
     SDL_Renderer* renderer;
     res = SDL_CreateWindowAndRenderer(
-        width_in_px, height_in_px, 0, &window, &renderer);
+        vcd_width_in_px, vcd_height_in_px, 0, &window, &renderer);
     if (res != 0) {
         fprintf(stderr,
             "error: could not create window/renderer: %s\n",
@@ -132,8 +124,8 @@ int sdldevice_construct(SdlDevice* device)
     SDL_Texture* buffer_texture = SDL_CreateTexture(renderer,
         SDL_PIXELFORMAT_RGBA32,
         SDL_TEXTUREACCESS_STREAMING,
-        width_in_px,
-        height_in_px);
+        vcd_width_in_px,
+        vcd_height_in_px);
     if (buffer_texture == NULL) {
         fprintf(stderr,
             "error: could not create buffer texture: %s\n",
@@ -205,22 +197,23 @@ void sdldevice_set_char(IODevice* io_device, uint16_t offset, uint8_t value)
         return;
     }
 
-    for (int ch_y = 0; ch_y < ch_height; ++ch_y) {
-        for (int ch_x = 0; ch_x < ch_width; ++ch_x) {
-            bool ch
-                = charset[value] >> (ch_y * ch_width + (ch_width - ch_x - 1))
+    for (int ch_y = 0; ch_y < vcd_ch_height; ++ch_y) {
+        for (int ch_x = 0; ch_x < vcd_ch_width; ++ch_x) {
+            bool ch = charset[value]
+                    >> (ch_y * vcd_ch_width + (vcd_ch_width - ch_x - 1))
                 & 1;
 
-            for (int px_y = 0; px_y < px_height; ++px_y) {
-                for (int px_x = 0; px_x < px_width; ++px_x) {
+            for (int px_y = 0; px_y < vcd_px_height; ++px_y) {
+                for (int px_x = 0; px_x < vcd_px_width; ++px_x) {
 
-                    int x = (offset % width_in_ch * ch_width + ch_x) * px_width
+                    int x = (offset % vcd_width_in_ch * vcd_ch_width + ch_x)
+                            * vcd_px_width
                         + px_x;
-                    int y
-                        = (offset / width_in_ch * ch_height + ch_y) * px_height
+                    int y = (offset / vcd_width_in_ch * vcd_ch_height + ch_y)
+                            * vcd_px_height
                         + px_y;
 
-                    buffer[y * width_in_px + x] = ch
+                    buffer[y * vcd_width_in_px + x] = ch
                         ? (SDL_Color) { 0xff, 0xff, 0xff, 0xff }
                         : (SDL_Color) { 0x00, 0x00, 0x00, 0xff };
                 }
@@ -507,7 +500,7 @@ int main(void)
             s_push_r(R2),
 
             s_mov16_r_ml(R2, screen_y),
-            s_mul_i(R2, R2, width_in_ch),
+            s_mul_i(R2, R2, vcd_width_in_ch),
             s_mov16_r_ml(R1, screen_x),
             s_add_i(R1, R1, 0x0c00),
             s_add_r(R1, R1, R2),
@@ -517,7 +510,7 @@ int main(void)
             s_add_i(R1, R1, 1),
             s_mov16_ml_r(screen_x, R1),
 
-            s_cmp_i(R1, width_in_ch),
+            s_cmp_i(R1, vcd_width_in_ch),
             s_mov16_r_r(R2, Rfl),
             s_and_i(R2, R2, 1 << Fl_Eq),
             s_jnz_l(R2, put_char_0),
diff --git a/vm/vm.c b/vm/vm.c
index 67822ec..7812399 100644
--- a/vm/vm.c
+++ b/vm/vm.c
@@ -1,4 +1,5 @@
 #include "vm.h"
+#include "common/arch.h"
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -552,74 +553,3 @@ static inline uint16_t ins_op2_or_imm(VM* vm, uint16_t ins)
 {
     return ins_reg_val_or_imm(vm, ins, 6, 7, 0x7);
 }
-
-const char* op_str(Op op)
-{
-    switch (op) {
-        case Op_Nop:
-            return "nop";
-        case Op_Hlt:
-            return "hlt";
-        case Op_Jmp:
-            return "jmp";
-        case Op_Jnz:
-            return "jnz";
-        case Op_Test:
-            return "test";
-        case Op_Cmp:
-            return "cmp";
-        case Op_Mov8:
-            return "mov8";
-        case Op_Mov16:
-            return "mov16";
-        case Op_In:
-            return "in";
-        case Op_Out:
-            return "out";
-        case Op_Call:
-            return "call";
-        case Op_Ret:
-            return "ret";
-        case Op_Lit:
-            return "lit";
-        case Op_Int:
-            return "int";
-        case Op_Or:
-            return "or";
-        case Op_Xor:
-            return "xor";
-        case Op_And:
-            return "and";
-        case Op_Shl:
-            return "shl";
-        case Op_RShl:
-            return "rshl";
-        case Op_Shr:
-            return "shr";
-        case Op_RShr:
-            return "rshr";
-        case Op_Add:
-            return "add";
-        case Op_Sub:
-            return "sub";
-        case Op_RSub:
-            return "rsub";
-        case Op_Mul:
-            return "mul";
-        case Op_IMul:
-            return "imul";
-        case Op_Div:
-            return "div";
-        case Op_IDiv:
-            return "idiv";
-        case Op_RDiv:
-            return "rdiv";
-        case Op_RIDiv:
-            return "ridiv";
-        case Op_Mod:
-            return "mod";
-        case Op_RMod:
-            return "rmod";
-    }
-    return "---";
-}
diff --git a/vm/vm.h b/vm/vm.h
index 79a4904..fd89e1e 100644
--- a/vm/vm.h
+++ b/vm/vm.h
@@ -2,74 +2,6 @@
 
 #include <stdint.h>
 
-typedef enum {
-    R0 = 0,
-    R1 = 1,
-    R2 = 2,
-    R3 = 3,
-    R4 = 4,
-    Rbp = 5,
-    Rsp = 6,
-    Rfl = 7,
-    Rcs = 8,
-    Rip = 9,
-} Reg;
-
-typedef enum {
-    Fl_Zero,
-    Fl_Eq,
-    Fl_Be,
-    Fl_Lt,
-    Fl_Err,
-    Fl_Int,
-    Fl_Vcd,
-} Flag;
-
-typedef enum {
-    Op_Nop,
-    Op_Hlt,
-    Op_Jmp,
-    Op_Jnz,
-    Op_Test,
-    Op_Cmp,
-    Op_Mov8,
-    Op_Mov16,
-    Op_In,
-    Op_Out,
-    Op_Call,
-    Op_Ret,
-    Op_Lit,
-    Op_Int,
-    Op_Or,
-    Op_Xor,
-    Op_And,
-    Op_Shl,
-    Op_RShl,
-    Op_Shr,
-    Op_RShr,
-    Op_Add,
-    Op_Sub,
-    Op_RSub,
-    Op_Mul,
-    Op_IMul,
-    Op_Div,
-    Op_IDiv,
-    Op_RDiv,
-    Op_RIDiv,
-    Op_Mod,
-    Op_RMod,
-} Op;
-
-typedef enum {
-    Int_DiskRead = 0,
-    Int_DiskWrite = 1,
-    Int_Key = 32,
-} VM_Int;
-
-typedef enum {
-    Device_Keyboard,
-} VM_Device;
-
 typedef struct Drive Drive;
 typedef void (*DriveReadFn)(Drive* drive, uint8_t* block, uint16_t i);
 typedef void (*DriveWriteFn)(Drive* drive, const uint8_t* block, uint16_t i);
@@ -114,5 +46,3 @@ struct IODevice {
 };
 
 void vm_start(Drive* boot_drive, IODevice* io_device);
-
-const char* op_str(Op op);