diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b2570e105ae3efc2a2616570deb567a155b69425
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+#[build]
+#target = "x86_64-unknown-none"
+
+[target.x86_64-unknown-none]
+runner = "./.cargo/runner.sh"
diff --git a/.cargo/runner.sh b/.cargo/runner.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5914a40ca832eeb293638ee09e78f146e92fb401
--- /dev/null
+++ b/.cargo/runner.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+#
+# Hyperion x86_64 is runnable
+
+set -xe
+
+LIMINE_GIT_URL="https://github.com/limine-bootloader/limine.git"
+ISO_DIR=target/hyperion/x86_64/iso
+KERNEL=$1
+
+# Clone the `limine` repository if we don't have it yet.
+if [ ! -d target/limine ]; then
+    git clone $LIMINE_GIT_URL --depth=1 --branch v3.0-branch-binary target/limine
+fi
+
+# Make sure we have an up-to-date version of the bootloader.
+cd target/limine
+git fetch
+make
+cd -
+
+# Copy the needed files into an ISO image.
+mkdir -p $ISO_DIR
+cp $KERNEL cfg/limine.cfg target/limine/limine{.sys,-cd.bin,-cd-efi.bin} $ISO_DIR
+
+xorriso -as mkisofs \
+    -b limine-cd.bin \
+    -no-emul-boot -boot-load-size 4 -boot-info-table \
+    --efi-boot limine-cd-efi.bin \
+    -efi-boot-part --efi-boot-image --protective-msdos-label \
+    $ISO_DIR -o $KERNEL.iso
+
+# For the image to be bootable on BIOS systems, we must run `limine-deploy` on it.
+target/limine/limine-deploy $KERNEL.iso
+
+# Run the created image with QEMU.
+qemu-system-x86_64 \
+    -machine q35 -cpu qemu64 -M smm=off \
+    -D target/log.txt -d int,guest_errors -no-reboot -no-shutdown \
+    -serial stdio \
+    $KERNEL.iso
diff --git a/Cargo.lock b/Cargo.lock
index 5124ebb6e1567de5e586afb5258d91c95f10ea46..1131368621289df3d2ff0b22ed4c8eebeb90faca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,14 +8,35 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "bit_field"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "hyperion"
 version = "0.1.0"
 dependencies = [
+ "limine",
  "spin",
+ "uart_16550",
  "volatile",
+ "x86_64",
 ]
 
+[[package]]
+name = "limine"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c847ac148a0c53ba3755dfa9830722b1043179584009869e6afc2b413e13f105"
+
 [[package]]
 name = "lock_api"
 version = "0.4.9"
@@ -26,6 +47,12 @@ dependencies = [
  "scopeguard",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -41,8 +68,31 @@ dependencies = [
  "lock_api",
 ]
 
+[[package]]
+name = "uart_16550"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b074eb9300ad949edd74c529c0e8d451625af71bb948e6b65fe69f72dc1363d9"
+dependencies = [
+ "bitflags",
+ "rustversion",
+ "x86_64",
+]
+
 [[package]]
 name = "volatile"
 version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3ca98349dda8a60ae74e04fd90c7fb4d6a4fbe01e6d3be095478aa0b76f6c0c"
+
+[[package]]
+name = "x86_64"
+version = "0.14.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "100555a863c0092238c2e0e814c1096c1e5cf066a309c696a87e907b5f8c5d69"
+dependencies = [
+ "bit_field",
+ "bitflags",
+ "rustversion",
+ "volatile",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 793abe36bf13afd0a95af116816056a459e3d702..ecdebf34a2c9dd3706f5d3d73f63471946ad64b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,10 +5,21 @@ edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
-[lib]
-crate-type = ["staticlib"]
+[features]
+default = ["limine"]
+limine = ["dep:limine"]
+bootboot = []
+multiboot1 = []
+multiboot2 = []
+# Pick limine OR bootboot OR multiboot1 OR multiboot2, they conflict with eachother
+
+# [lib]
+# crate-type = ["staticlib"]
 
 [dependencies]
 spin = "0.9.4"
 volatile = "0.4.5"
+x86_64 = "0.14.10"
+uart_16550 = "0.2.18"
+limine = { version = "0.1.9", optional = true }
 #tracing = { version = "0.1.37", default-features = false }
diff --git a/Makefile b/Makefile
index aaef11cce026a1233240c3befe9a61c621a33e81..74ff7dca26ef91ddf3a66c04186498783a2bd05b 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ ARCH          ?= x86_64
 #ARCH          ?= x86
 PROFILE       ?= debug
 #PROFILE       ?= release
+GDB           ?= false
 
 # binary config
 NASM          ?= nasm
@@ -36,23 +37,23 @@ ${KERNEL_LIB} : ${KERNEL_SRC} Makefile Cargo.toml Cargo.lock
 	${CARGO} build ${CARGO_FLAGS}
 
 # hyperion boot code
-BOOT_SRC      := ${ARCH_DIR}/start.asm
-BOOT_OBJ      := ${HYPER_DIR}/start.o
-NASM_F_x86_64 := elf64
-NASM_F_x86    := elf32
-NASM_FLAGS    ?=
-NASM_FLAGS    += ${BOOT_SRC}
-NASM_FLAGS    += -o ${BOOT_OBJ}
-NASM_FLAGS    += -f ${NASM_F_${ARCH}}
-${BOOT_OBJ} : ${BOOT_SRC} Makefile
-	@echo "\n\033[32m--[[ building Hyperion boot ]]--\033[0m"
-	mkdir -p ${HYPER_DIR}
-	${NASM} ${NASM_FLAGS}
+# BOOT_SRC      := ${ARCH_DIR}/start.asm
+# BOOT_OBJ      := ${HYPER_DIR}/start.o
+# NASM_F_x86_64 := elf64
+# NASM_F_x86    := elf32
+# NASM_FLAGS    ?=
+# NASM_FLAGS    += ${BOOT_SRC}
+# NASM_FLAGS    += -o ${BOOT_OBJ}
+# NASM_FLAGS    += -f ${NASM_F_${ARCH}}
+# ${BOOT_OBJ} : ${BOOT_SRC} Makefile
+# 	@echo "\n\033[32m--[[ building Hyperion boot ]]--\033[0m"
+# 	mkdir -p ${HYPER_DIR}
+# 	${NASM} ${NASM_FLAGS}
 
 # hyperion kernel elf
 LD_SCRIPT     := ${ARCH_DIR}/link.ld
 KERNEL_ELF    := ${HYPER_DIR}/hyperion
-KERNEL_DEPS   := ${BOOT_OBJ} ${KERNEL_LIB}
+KERNEL_DEPS   := ${KERNEL_LIB} #${BOOT_OBJ}
 LD_M_x86_64   := elf_x86_64
 LD_M_x86      := elf_i386
 LD_FLAGS      ?=
@@ -70,18 +71,52 @@ ${KERNEL_ELF} : ${KERNEL_DEPS} ${LD_SCRIPT} Makefile
 #	the entry format has to be x86 not x86_64
 	${OBJCOPY} -O elf32-i386 ${KERNEL_ELF}
 
+# hyperion iso
+HYPERION      := ${HYPER_DIR}/hyperion.iso
+ISO_DIR       := ${HYPER_DIR}/iso
+BOOT_DIR      := ${ISO_DIR}/boot
+GRUB_DIR      := ${BOOT_DIR}/grub
+${HYPERION} : ${KERNEL_ELF} cfg/grub.cfg Makefile
+	@echo "\n\033[32m--[[ building Hyperion iso ]]--\033[0m"
+	mkdir -p ${GRUB_DIR}
+	cp cfg/grub.cfg ${GRUB_DIR}
+	cp ${KERNEL_ELF} ${BOOT_DIR}/
+	grub-mkrescue /usr/lib/grub/i386-pc -o $@ ${ISO_DIR}
+
 # build alias
 build : ${KERNEL_ELF}
 
-# qemu alias
+# qemu direct kernel boot alias
 QEMU_x86_64   ?= qemu-system-x86_64
-QEMU_x86      ?= qemu-system-x86
+QEMU_x86      ?= qemu-system-i386
 QEMU_FLAGS    ?=
+QEMU_FLAGS    += -serial stdio
+QEMU_FLAGS    += -s
+ifeq (${GDB},true)
+QEMU_FLAGS    += -S
+endif
 QEMU_FLAGS    += -enable-kvm
 QEMU_FLAGS    += -d cpu_reset,guest_errors
-QEMU_FLAGS    += -kernel ${KERNEL_ELF}
+#QEMU_FLAGS    += -M pc-i440fx-7.2
+#QEMU_FLAGS    += -device VGA,vgamem_mb=64
+QEMU_FLAGS    += -vga std
+QEMU_KERNEL   := -kernel ${KERNEL_ELF} -append qemu
 qemu : ${KERNEL_ELF}
-	${QEMU_${ARCH}} ${QEMU_FLAGS}
+	${QEMU_${ARCH}} ${QEMU_FLAGS} ${QEMU_KERNEL}
+
+# qemu iso boot alias
+#QEMU_FLAGS    += -bios ${QEMU_OVMF}
+QEMU_OVMF     ?= /usr/share/ovmf/x64/OVMF.fd
+QEMU_ISO      := -drive format=raw,file=${HYPERION}
+qemu_iso : ${HYPERION}
+	${QEMU_${ARCH}} ${QEMU_FLAGS} ${QEMU_ISO}
+
+# connect gdb to qemu
+GDB_FLAGS     ?=
+GDB_FLAGS     += --eval-command="target remote localhost:1234"
+GDB_FLAGS     += --eval-command="symbol-file ${KERNEL_ELF}"
+gdb:
+	gdb ${GDB_FLAGS}
 
 # objdump
 objdump : ${KERNEL_ELF}
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b53fae90545914afdc65e1e5228ea312a921c36b
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,39 @@
+use std::{env::var, error::Error};
+
+//
+
+fn main() -> Result<(), Box<dyn Error>> {
+    let kernel = var("CARGO_PKG_NAME")?;
+    println!("cargo:rerun-if-env-changed=CARGO_PKG_NAME");
+    let arch = var("CARGO_CFG_TARGET_ARCH")?;
+    println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_ARCH");
+
+    let mut bootloader: Option<&'static str> = None;
+    let mut set = |s| {
+        if let Some(already) = bootloader {
+            println!("cargo:warning=Bootloaders {s} and {already} are mutually exclusive");
+            panic!();
+        } else {
+            bootloader = Some(s);
+        }
+    };
+    #[cfg(feature = "limine")]
+    set("limine");
+    #[cfg(feature = "bootboot")]
+    set("bootboot");
+    #[cfg(feature = "multiboot1")]
+    set("multiboot1");
+    #[cfg(feature = "multiboot2")]
+    set("multiboot2");
+
+    if let Some(bootloader) = bootloader {
+        let script = format!("src/arch/{arch}/{bootloader}/link.ld");
+        println!("cargo:rustc-link-arg-bin={kernel}=--script={script}");
+        println!("cargo:rerun-if-changed={script}");
+    } else {
+        println!("cargo:warning=No bootloaders given");
+        panic!();
+    };
+
+    Ok(())
+}
diff --git a/cfg/grub.cfg b/cfg/grub.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..eda56d4e2f277501d7ac4eb6cb34a9c518e36391
--- /dev/null
+++ b/cfg/grub.cfg
@@ -0,0 +1,14 @@
+# boot instantly
+set timeout=0
+# default to Hyperion multiboot1
+set default=0
+
+menuentry "Hyperion multiboot1" {
+	multiboot /boot/hyperion
+	boot
+}
+
+menuentry "Hyperion multiboot2" {
+	multiboot2 /boot/hyperion
+	boot
+}
diff --git a/cfg/limine.cfg b/cfg/limine.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..1777d21e9ef695df6a3a46ba4f16d936389bd62f
--- /dev/null
+++ b/cfg/limine.cfg
@@ -0,0 +1,6 @@
+# boot instantly
+TIMEOUT=0
+
+:Hyperion
+    PROTOCOL=limine
+    KERNEL_PATH=boot:///hyperion
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000000000000000000000000000000000000..5d56faf9ae08cb604e06df9aa6281e2b51ce5809
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly"
diff --git a/src/arch/x86_64/bootboot/mod.rs b/src/arch/x86_64/bootboot/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..95bb6858abe5ffdef58ac9cf88b0a3d17c72d85a
--- /dev/null
+++ b/src/arch/x86_64/bootboot/mod.rs
@@ -0,0 +1,126 @@
+#[allow(unused)]
+const BOOTBOOT_MAGIC: &'static [u8; 5usize] = b"BOOT\0";
+
+#[allow(unused)]
+const BOOTBOOT_MMIO: u64 = 0xfffffffff8000000; /* memory mapped IO virtual address */
+#[allow(unused)]
+const BOOTBOOT_FB: u64 = 0xfffffffffc000000; /* frame buffer virtual address */
+#[allow(unused)]
+const BOOTBOOT_INFO: u64 = 0xffffffffffe00000; /* bootboot struct virtual address */
+#[allow(unused)]
+const BOOTBOOT_ENV: u64 = 0xffffffffffe01000; /* environment string virtual address */
+#[allow(unused)]
+const BOOTBOOT_CORE: u64 = 0xffffffffffe02000; /* core loadable segment start */
+
+#[allow(unused)]
+const PROTOCOL_MINIMAL: u32 = 0;
+#[allow(unused)]
+const PROTOCOL_STATIC: u32 = 1;
+#[allow(unused)]
+const PROTOCOL_DYNAMIC: u32 = 2;
+#[allow(unused)]
+const PROTOCOL_BIGENDIAN: u32 = 0x80;
+
+#[allow(unused)]
+const LOADER_BIOS: u32 = 0 << 2;
+#[allow(unused)]
+const LOADER_UEFI: u32 = 1 << 2;
+#[allow(unused)]
+const LOADER_RPI: u32 = 2 << 2;
+#[allow(unused)]
+const LOADER_COREBOOT: u32 = 3 << 2;
+
+#[allow(unused)]
+const FB_ARGB: u32 = 0;
+#[allow(unused)]
+const FB_RGBA: u32 = 1;
+#[allow(unused)]
+const FB_ABGR: u32 = 2;
+#[allow(unused)]
+const FB_BGRA: u32 = 3;
+
+#[allow(unused)]
+const MMAP_USED: u32 = 0; /* don't use. Reserved or unknown regions */
+#[allow(unused)]
+const MMAP_FREE: u32 = 1; /* usable memory */
+#[allow(unused)]
+const MMAP_ACPI: u32 = 2; /* acpi memory, volatile and non-volatile as well */
+#[allow(unused)]
+const MMAP_MMIO: u32 = 3; /* memory mapped IO region */
+
+#[allow(unused)]
+const INITRD_MAXSIZE: u32 = 16; /* Mb */
+
+#[derive(Debug, Clone, Copy)]
+#[repr(C, packed)]
+struct MMapEnt {
+    ptr: u64,
+    size: u64,
+}
+
+#[derive(Clone, Copy)]
+#[repr(C, packed)]
+struct BootBoot {
+    /* first 64 bytes is platform independent */
+    magic: [u8; 4usize],    /* 'BOOT' magic */
+    size: u32,              /* length of bootboot structure, minimum 128 */
+    protocol: u8,           /* 1, static addresses, see PROTOCOL_* and LOADER_* above */
+    fb_type: u8,            /* framebuffer type, see FB_* above */
+    numcores: u16,          /* number of processor cores */
+    bspid: u16,             /* Bootsrap processor ID (Local APIC Id on x86_64) */
+    timezone: i16,          /* in minutes -1440..1440 */
+    datetime: [u8; 8usize], /* in BCD yyyymmddhhiiss UTC (independent to timezone) */
+
+    initrd_ptr: u64, /* ramdisk image position and size */
+    initrd_size: u64,
+
+    fb_ptr: *mut u8, /* framebuffer pointer and dimensions */
+    fb_size: u32,
+    fb_width: u32,
+    fb_height: u32,
+    fb_scanline: u32,
+
+    arch: Arch,
+
+    mmap: MMapEnt,
+}
+
+#[derive(Clone, Copy)]
+#[repr(C)]
+union Arch {
+    x86_64: ArchX86,
+    aarch64: ArchAarch64,
+    _bindgen_union_align: [u64; 8usize],
+}
+
+#[derive(Debug, Clone, Copy)]
+#[repr(C)]
+struct ArchX86 {
+    acpi_ptr: u64,
+    smbi_ptr: u64,
+    efi_ptr: u64,
+    mp_ptr: u64,
+    unused0: u64,
+    unused1: u64,
+    unused2: u64,
+    unused3: u64,
+}
+
+#[derive(Debug, Clone, Copy)]
+#[repr(C)]
+struct ArchAarch64 {
+    acpi_ptr: u64,
+    mmio_ptr: u64,
+    efi_ptr: u64,
+    unused0: u64,
+    unused1: u64,
+    unused2: u64,
+    unused3: u64,
+    unused4: u64,
+}
+
+#[no_mangle]
+extern "C" fn _start() -> ! {
+    *crate::BOOTLOADER.lock() = "BOOTBOOT";
+    crate::kernel_main()
+}
diff --git a/src/arch/x86_64/limine/link.ld b/src/arch/x86_64/limine/link.ld
new file mode 100644
index 0000000000000000000000000000000000000000..1f53501c84b51393f55bce31c21e67568a6f4eef
--- /dev/null
+++ b/src/arch/x86_64/limine/link.ld
@@ -0,0 +1,74 @@
+ENTRY(_start)
+OUTPUT_ARCH(i386:x86-64)
+OUTPUT_FORMAT(elf64-x86-64)
+
+KERNEL_BASE = 0xffffffff80000000;
+
+SECTIONS {
+    . = KERNEL_BASE + SIZEOF_HEADERS;
+
+    .hash                   : { *(.hash) }
+    .gnu.hash               : { *(.gnu.hash) }
+    .dynsym                 : { *(.dynsym) }
+    .dynstr                 : { *(.dynstr) }
+    .rela                   : { *(.rela*) }
+    .rodata                 : { *(.rodata .rodata.*) }
+    .note.gnu.build-id      : { *(.note.gnu.build-id) }
+    .eh_frame_hdr           : {
+        PROVIDE(__eh_frame_hdr = .);
+        KEEP(*(.eh_frame_hdr))
+        PROVIDE(__eh_frame_hdr_end = .);
+    }
+    .eh_frame               : {
+        PROVIDE(__eh_frame = .);
+        KEEP(*(.eh_frame))
+        PROVIDE(__eh_frame_end = .);
+    }
+    .gcc_except_table       : { KEEP(*(.gcc_except_table .gcc_except_table.*)) }
+
+    . += CONSTANT(MAXPAGESIZE);
+
+    .plt                    : { *(.plt .plt.*) }
+    .text                   : { *(.text .text.*) }
+
+    . += CONSTANT(MAXPAGESIZE);
+
+    .tdata                  : { *(.tdata .tdata.*) }
+    .tbss                   : { *(.tbss .tbss.*) }
+
+    .data.rel.ro            : { *(.data.rel.ro .data.rel.ro.*) }
+    .dynamic                : { *(.dynamic) }
+
+    . = DATA_SEGMENT_RELRO_END(0, .);
+
+    .got                    : { *(.got .got.*) }
+    .got.plt                : { *(.got.plt .got.plt.*) }
+    .data                   : { *(.data .data.*) }
+    .bss                    : { *(.bss .bss.*) *(COMMON) }
+
+    . = DATA_SEGMENT_END(.);
+
+    .comment              0 : { *(.comment) }
+    .debug                0 : { *(.debug) }
+    .debug_abbrev         0 : { *(.debug_abbrev) }
+    .debug_aranges        0 : { *(.debug_aranges) }
+    .debug_frame          0 : { *(.debug_frame) }
+    .debug_funcnames      0 : { *(.debug_funcnames) }
+    .debug_info           0 : { *(.debug_info .gnu.linkonce.wi.*) }
+    .debug_line           0 : { *(.debug_line) }
+    .debug_loc            0 : { *(.debug_loc) }
+    .debug_macinfo        0 : { *(.debug_macinfo) }
+    .debug_pubnames       0 : { *(.debug_pubnames) }
+    .debug_pubtypes       0 : { *(.debug_pubtypes) }
+    .debug_ranges         0 : { *(.debug_ranges) }
+    .debug_sfnames        0 : { *(.debug_sfnames) }
+    .debug_srcinfo        0 : { *(.debug_srcinfo) }
+    .debug_str            0 : { *(.debug_str) }
+    .debug_typenames      0 : { *(.debug_typenames) }
+    .debug_varnames       0 : { *(.debug_varnames) }
+    .debug_weaknames      0 : { *(.debug_weaknames) }
+    .line                 0 : { *(.line) }
+    .shstrtab             0 : { *(.shstrtab) }
+    .strtab               0 : { *(.strtab) }
+    .symtab               0 : { *(.symtab) }
+}
diff --git a/src/arch/x86_64/limine/mod.rs b/src/arch/x86_64/limine/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ef9274aceb65802760e45470b8bafa801a6a9c00
--- /dev/null
+++ b/src/arch/x86_64/limine/mod.rs
@@ -0,0 +1,52 @@
+use core::fmt::{self, Arguments, Write};
+use limine::{LimineTerminalRequest, LimineTerminalResponse};
+use spin::{Lazy, Mutex, MutexGuard, Once};
+
+//
+
+#[no_mangle]
+pub extern "C" fn _start() -> ! {
+    *crate::BOOTLOADER.lock() = "Limine";
+    crate::kernel_main()
+}
+
+//
+
+struct Writer(pub &'static LimineTerminalResponse);
+
+unsafe impl Send for Writer {}
+
+impl Write for Writer {
+    fn write_str(&mut self, s: &str) -> fmt::Result {
+        let mut write = self.0.write().ok_or(fmt::Error)?;
+
+        for term in self.0.terminals() {
+            write(term, s);
+        }
+
+        Ok(())
+    }
+}
+
+static TERMINALS: LimineTerminalRequest = LimineTerminalRequest::new(0);
+static WRITER: Once<Mutex<Writer>> = Once::new();
+
+fn get() -> Result<MutexGuard<'static, Writer>, fmt::Error> {
+    WRITER.try_call_once(|| {
+        Ok(Mutex::new(Writer(
+            TERMINALS.get_response().get().ok_or(fmt::Error)?,
+        )))
+    })?;
+    WRITER.get().ok_or(fmt::Error).map(|mutex| mutex.lock())
+}
+
+fn print(args: Arguments) -> Option<()> {
+    Some(())
+}
+
+#[doc(hidden)]
+pub fn _print(args: Arguments) {
+    if let Ok(mut writer) = get() {
+        _ = writer.write_fmt(args)
+    }
+}
diff --git a/src/arch/x86_64/mod.rs b/src/arch/x86_64/mod.rs
index f3dcc9932f43b006432368a4ca24923103ec4405..42f8285e42045437e0c818ec14c0073133b4e7b8 100644
--- a/src/arch/x86_64/mod.rs
+++ b/src/arch/x86_64/mod.rs
@@ -1,12 +1,15 @@
-// both cannot coexist (AFAIK.) and QEMU
-// cannot boot multiboot2 kernels directly
-//
-// so multiboot1 it is .. temporarily
+#[cfg(feature = "multiboot1")]
+#[path = "multiboot1/mod.rs"]
+pub mod boot;
 
-// multiboot1 header and glue code
-#[cfg(all())]
-mod multiboot1;
+#[cfg(feature = "multiboot2")]
+#[path = "multiboot2/mod.rs"]
+pub mod boot;
 
-// multiboot2 header and glue code
-#[cfg(any())]
-mod multiboot2;
+#[cfg(feature = "bootboot")]
+#[path = "bootboot/mod.rs"]
+pub mod boot;
+
+#[cfg(feature = "limine")]
+#[path = "limine/mod.rs"]
+pub mod boot;
diff --git a/src/arch/x86_64/multiboot1.rs b/src/arch/x86_64/multiboot1.rs
deleted file mode 100644
index 3a61044346ae6ccf6ea4a6270223d9bfbeb605c3..0000000000000000000000000000000000000000
--- a/src/arch/x86_64/multiboot1.rs
+++ /dev/null
@@ -1,82 +0,0 @@
-use core::ffi::CStr;
-
-#[allow(unused)]
-#[repr(C)]
-struct Multiboot1Header {
-    magic: u32,
-    flags: u32,
-    checksum: u32,
-
-    _unused: [u32; 5], // header_addr, load_addr, load_end_addr, bss_end_addr, entry_addr
-
-    mode_type: u32,
-    width: u32,
-    height: u32,
-    depth: u32,
-}
-
-const MAGIC: u32 = 0x1BADB002;
-const ALIGN: u32 = 1 << 0;
-const MEMINFO: u32 = 1 << 1;
-const VIDEO: u32 = 1 << 2;
-const FLAGS: u32 = ALIGN | MEMINFO | VIDEO;
-
-#[used]
-#[no_mangle]
-#[link_section = ".multiboot"]
-static MULTIBOOT1_HEADER: Multiboot1Header = Multiboot1Header {
-    magic: MAGIC,
-    flags: FLAGS,
-    checksum: (0x100000000 - (MAGIC + FLAGS) as u64) as u32,
-
-    _unused: [0; 5],
-
-    mode_type: 0, // 0 = linear graphics
-    width: 0,     // 0 = no preference
-    height: 0,    // 0 = no preference
-    depth: 0,     // 0 = no preference
-};
-
-#[derive(Debug, Clone, Copy)]
-#[repr(C)]
-struct Multiboot1Information {
-    flags: u32,
-    optional: [u8; 112],
-}
-
-impl Multiboot1Information {
-    fn bootloader_name(&self) -> Option<&str> {
-        if (self.flags & 1 << 9) != 0 {
-            let ptr = u32::from_le_bytes((self.optional[60..=64]).try_into().ok()?) as _;
-            let s = unsafe { CStr::from_ptr(ptr) };
-            let s = s.to_str().ok()?;
-
-            Some(s)
-        } else {
-            None
-        }
-    }
-
-    fn framebuffer(&self) -> Option<&[u8]> {
-        if (self.flags & 1 << 12) != 0 {
-            Some(&self.optional[84..])
-        } else {
-            None
-        }
-    }
-}
-
-#[no_mangle]
-extern "C" fn kernel_main(magic_num: u64) {
-    let mb1_info_pointer = magic_num & u32::MAX as u64;
-    let mb1_info = unsafe { *(mb1_info_pointer as *const Multiboot1Information) };
-
-    crate::println!(
-        "\0{:?}\n{:#b}\n{:?}",
-        mb1_info.bootloader_name(),
-        mb1_info.flags,
-        mb1_info.framebuffer(),
-    );
-
-    crate::kernel_main();
-}
diff --git a/src/arch/x86_64/link.ld b/src/arch/x86_64/multiboot1/link.ld
similarity index 67%
rename from src/arch/x86_64/link.ld
rename to src/arch/x86_64/multiboot1/link.ld
index 874eb9bed9f802b5e4d5e33e116831be639ed98d..345eb98bf182cedddc926f0b434cf2bb8ea49189 100644
--- a/src/arch/x86_64/link.ld
+++ b/src/arch/x86_64/multiboot1/link.ld
@@ -1,15 +1,10 @@
-ENTRY(start)
+ENTRY(_start)
 
 SECTIONS {
     . = 1M;
 
-    .multiboot : ALIGN(4k) {
-        KEEP(*(.multiboot))
-        KEEP(*(.multiboot_rust))
-    }
-
     .boot : ALIGN(4k) {
-        *(.boot)
+        KEEP(*(.boot))
     }
 
     .text : ALIGN(4k) {
diff --git a/src/arch/x86_64/multiboot1/mod.rs b/src/arch/x86_64/multiboot1/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8f3db54e1751d19013847927b69167fadc426212
--- /dev/null
+++ b/src/arch/x86_64/multiboot1/mod.rs
@@ -0,0 +1,232 @@
+//! https://www.gnu.org/software/grub/manual/multiboot/multiboot.html
+
+use crate::println;
+use core::{ffi::CStr, mem::transmute, slice};
+use spin::Lazy;
+use uart_16550::SerialPort;
+use volatile::Volatile;
+use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
+
+//
+
+#[allow(unused)]
+#[repr(packed)]
+struct Multiboot1Header {
+    magic: u32,
+    flags: u32,
+    checksum: u32,
+
+    _unused: [u32; 5], // header_addr, load_addr, load_end_addr, bss_end_addr, entry_addr
+
+    mode_type: u32,
+    width: u32,
+    height: u32,
+    depth: u32,
+}
+
+const MAGIC: u32 = 0x1BADB002;
+const ALIGN: u32 = 1 << 0;
+const MEMINFO: u32 = 1 << 1;
+const VIDEO: u32 = 1 << 2;
+const FLAGS: u32 = ALIGN | MEMINFO | VIDEO;
+
+#[used]
+#[no_mangle]
+#[link_section = ".boot"]
+static MULTIBOOT1_HEADER: Multiboot1Header = Multiboot1Header {
+    magic: MAGIC,
+    flags: FLAGS,
+    checksum: (0x100000000 - (MAGIC + FLAGS) as u64) as u32,
+
+    _unused: [0; 5],
+
+    mode_type: 0, // 0 = linear graphics
+    width: 1280,  // 0 = no preference
+    height: 720,  // 0 = no preference
+    depth: 32,    // 0 = no preference
+};
+
+#[derive(Debug, Clone, Copy)]
+#[repr(packed)]
+struct Multiboot1Information {
+    flags: u32,
+
+    mem_lower: u32,
+    mem_upper: u32,
+
+    boot_device: u32,
+
+    cmdline: u32,
+
+    mods_count: u32,
+    mods_addr: u32,
+
+    syms: [u32; 4],
+    mmap_len: u32,
+    mmap_addr: u32,
+
+    drives_len: u32,
+    drives_addr: u32,
+
+    config_table: u32,
+
+    boot_loader_name: u32,
+
+    apm_table: u32,
+
+    // VESA Bios Extensions table
+    vbe_control_info: u32,
+    vbe_mode_info: u32,
+    vbe_mode: u16,
+    vbe_interface_seg: u16,
+    vbe_interface_off: u16,
+    vbe_interface_len: u16,
+
+    // Framebuffer table
+    framebuffer_addr: u64,
+    framebuffer_pitch: u32,
+    framebuffer_width: u32,
+    framebuffer_height: u32,
+    framebuffer_bpp: u8,
+    framebuffer_type: u8,
+    color_info: [u8; 6],
+}
+
+#[allow(unused)]
+const fn test() {
+    unsafe {
+        // at compile time: make sure that Multiboot1Information is exactly 116 bytes
+        transmute::<[u8; 116], Multiboot1Information>([0; 116]);
+    }
+}
+
+#[derive(Debug)]
+pub struct BootInfo {
+    pub cmdline: Option<&'static str>,
+    pub bootloader: Option<&'static str>,
+    pub framebuffer: Option<Framebuffer>,
+}
+
+pub struct Framebuffer {
+    pub buffer: &'static mut [u8],
+    pub stride: u32, // bytes per row
+    pub width: u32,
+    pub height: u32,
+}
+
+impl core::fmt::Debug for Framebuffer {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        f.debug_struct("Framebuffer")
+            .field("buffer", &self.buffer.as_ptr_range())
+            .field("stride", &self.stride)
+            .field("width", &self.width)
+            .field("height", &self.height)
+            .finish()
+    }
+}
+
+impl Multiboot1Information {
+    fn build(&self) -> BootInfo {
+        BootInfo {
+            cmdline: self.cmdline(),
+            bootloader: self.bootloader(),
+            framebuffer: self.framebuffer(),
+        }
+    }
+
+    fn cmdline(&self) -> Option<&'static str> {
+        if self.get_bit(2) {
+            // SAFETY: if flags[2] is set, this pointer is valid
+            let s = unsafe { CStr::from_ptr(self.cmdline as _) };
+            let s = s.to_str().ok()?;
+
+            Some(s)
+        } else {
+            None
+        }
+    }
+
+    fn bootloader(&self) -> Option<&'static str> {
+        if self.get_bit(9) {
+            // SAFETY: if flags[9] is set, this pointer is valid
+            let s = unsafe { CStr::from_ptr(self.boot_loader_name as _) };
+            let s = s.to_str().ok()?;
+
+            Some(s)
+        } else {
+            None
+        }
+    }
+
+    fn vbe(&self) -> Option<impl core::fmt::Debug> {
+        if self.get_bit(11) {
+            let vbe = (
+                self.vbe_control_info,
+                self.vbe_mode_info,
+                self.vbe_mode,
+                self.vbe_interface_seg,
+                self.vbe_interface_off,
+                self.vbe_interface_len,
+            );
+            crate::println!("{vbe:?}");
+            Some(vbe)
+        } else {
+            None
+        }
+    }
+
+    fn framebuffer(&self) -> Option<Framebuffer> {
+        if self.get_bit(12) {
+            let size = self.framebuffer_pitch as usize * self.framebuffer_height as usize;
+            let buffer =
+                unsafe { slice::from_raw_parts_mut(self.framebuffer_addr as *mut _, size) };
+
+            Some(Framebuffer {
+                buffer,
+                stride: self.framebuffer_pitch,
+                width: self.framebuffer_width,
+                height: self.framebuffer_height,
+            })
+        } else {
+            None
+        }
+    }
+
+    fn get_bit(&self, n: u8) -> bool {
+        (self.flags & 1 << n) != 0
+    }
+}
+
+static IDT: Lazy<InterruptDescriptorTable> = Lazy::new(|| {
+    let mut idt = InterruptDescriptorTable::new();
+    idt.breakpoint.set_handler_fn(breakpoint);
+    idt
+});
+
+#[no_mangle]
+extern "C" fn _start_rust(magic_num: u64) -> ! {
+    *crate::BOOTLOADER.lock() = "Multiboot1";
+    crate::print!("\0");
+    // let mb1_info_pointer = magic_num & u32::MAX as u64;
+    // let mb1_info = unsafe { *(mb1_info_pointer as *const Multiboot1Information) };
+    // let mut boot_info = mb1_info.build();
+
+    // crate::println!("{boot_info:#?} {mb1_info:#?}");
+
+    crate::println!("test");
+
+    // IDT.load();
+    // x86_64::instructions::interrupts::int3();
+    crate::println!("comp");
+
+    // if let Some(fb) = &mut boot_info.framebuffer {
+    //     // fb.buffer[1000] = 255;
+    //     // fb.buffer.fill(255);
+    // }
+
+    crate::kernel_main();
+}
+
+extern "x86-interrupt" fn breakpoint(stack: InterruptStackFrame) {
+    // println!("{stack:?}");
+}
diff --git a/src/arch/x86_64/start.asm b/src/arch/x86_64/multiboot1/start.asm
similarity index 93%
rename from src/arch/x86_64/start.asm
rename to src/arch/x86_64/multiboot1/start.asm
index 4bcb16a1cf4400c5567236c7066abe21b6555037..cefcd854f62503524e5c940e55f350041785803a 100644
--- a/src/arch/x86_64/start.asm
+++ b/src/arch/x86_64/multiboot1/start.asm
@@ -1,5 +1,5 @@
     global start
-    extern kernel_main
+    extern _start_rust
 
     ;; ----------
     ;; Boot entry
@@ -9,7 +9,7 @@
     global start
     bits 32
 
-start:
+_start:
     cli
     cld
 
@@ -139,22 +139,22 @@ setup_page_tables:
 	ret
 
 enable_paging:
-	; pass page table location to the cpu
+	;; pass page table location to the cpu
 	mov eax, page_table_l4
 	mov cr3, eax
 
-	; enable PAE
+	;; enable Physical Address Extension
 	mov eax, cr4
 	or  eax, 1 << 5
 	mov cr4, eax
 
-	; enable long mode
+	;; enable long mode
 	mov ecx, 0xC0000080
 	rdmsr
 	or  eax, 1 << 8
 	wrmsr
 
-	; enable paging
+	;; enable paging
 	mov eax, cr0
 	or  eax, 1 << 31
 	mov cr0, eax
@@ -175,12 +175,9 @@ long_mode_start:
 	mov fs, ax
 	mov gs, ax
 
-	; print 'OK'
-	mov dword [0xb8000], 0x2F4B2F4F
-
     ;; take the multiboot info struct pointer
     pop rdi
-	call kernel_main
+	call _start_rust
 .halt:
 	hlt
     jmp halt
diff --git a/src/arch/x86_64/multiboot2/link.ld b/src/arch/x86_64/multiboot2/link.ld
new file mode 100644
index 0000000000000000000000000000000000000000..345eb98bf182cedddc926f0b434cf2bb8ea49189
--- /dev/null
+++ b/src/arch/x86_64/multiboot2/link.ld
@@ -0,0 +1,26 @@
+ENTRY(_start)
+
+SECTIONS {
+    . = 1M;
+
+    .boot : ALIGN(4k) {
+        KEEP(*(.boot))
+    }
+
+    .text : ALIGN(4k) {
+        *(.text)
+    }
+
+    .rodata : ALIGN(4k) {
+        *(.rodata)
+    }
+
+    .data : ALIGN(4k) {
+        *(.data)
+    }
+
+    .bss : ALIGN(4k) {
+        *(COMMON)
+        *(.bss)
+    }
+}
diff --git a/src/arch/x86_64/multiboot2.rs b/src/arch/x86_64/multiboot2/mod.rs
similarity index 80%
rename from src/arch/x86_64/multiboot2.rs
rename to src/arch/x86_64/multiboot2/mod.rs
index 892aac2015781eb8740eab28c1353312f3cfa986..bb2a49a58961fe3a20581623c86eb5e84f9b4c58 100644
--- a/src/arch/x86_64/multiboot2.rs
+++ b/src/arch/x86_64/multiboot2/mod.rs
@@ -16,7 +16,7 @@ const CHECKSUM: u32 = (0x100000000 - (MAGIC + ARCH + LEN) as u64) as u32;
 
 #[used]
 #[no_mangle]
-#[link_section = ".multiboot"]
+#[link_section = ".boot"]
 pub static MULTIBOOT2_HEADER: Multiboot2Header = Multiboot2Header {
     magic: MAGIC,
     architecture: ARCH,
@@ -27,6 +27,9 @@ pub static MULTIBOOT2_HEADER: Multiboot2Header = Multiboot2Header {
 };
 
 #[no_mangle]
-pub extern "C" fn kernel_main(_magic_num: u64) {
+extern "C" fn _start_rust(_magic_num: u64) -> ! {
+    *crate::BOOTLOADER.lock() = "Multiboot2";
     crate::kernel_main();
 }
+
+compile_error!("TODO: Multiboot2");
diff --git a/src/arch/x86_64/multiboot2/start.asm b/src/arch/x86_64/multiboot2/start.asm
new file mode 100644
index 0000000000000000000000000000000000000000..cefcd854f62503524e5c940e55f350041785803a
--- /dev/null
+++ b/src/arch/x86_64/multiboot2/start.asm
@@ -0,0 +1,210 @@
+    global start
+    extern _start_rust
+
+    ;; ----------
+    ;; Boot entry
+    ;; ----------
+
+    section .boot
+    global start
+    bits 32
+
+_start:
+    cli
+    cld
+
+    ;; init stack
+    mov esp, stack_top
+
+    ;; support checks
+    call check_multiboot1
+    push ebx
+    push ebx
+    call check_cpuid
+    call check_long_mode
+
+    ;; setup
+    call setup_page_tables
+    call enable_paging
+
+    ;; enter long mode
+    lgdt [gdt64.pointer]
+    jmp gdt64.code_segment: long_mode_start
+    jmp halt
+
+error:
+    ;; print 'ERR: <err>'
+    mov dword [0xb8000], 0x4F524F45
+    mov dword [0xb8004], 0x4F3A4F52
+    mov dword [0xb8008], 0x4F204F20
+    mov byte  [0xb800a], al
+    jmp halt
+
+halt:
+    ;; print ZZZ
+	mov word [0xb8f00], 0x0F5A
+	mov word [0xb8f02], 0x0F5A
+	mov word [0xb8f04], 0x0F5A
+    hlt
+    jmp halt
+
+    ;; ------
+    ;; Checks
+    ;; ------
+
+    section .boot
+    bits 32
+
+check_multiboot2:
+	cmp eax, 0x36D76289
+	jne .no_multiboot2
+	ret
+
+.no_multiboot2:
+	mov al, 'M'
+	jmp error
+
+check_multiboot1:
+	cmp eax, 0x2BADB002
+	jne .no_multiboot1
+	ret
+
+.no_multiboot1:
+	mov al, 'M'
+	jmp error
+
+check_cpuid:
+	pushfd
+	pop eax
+	mov ecx, eax
+	xor eax, 1 << 21
+	push eax
+	popfd
+
+	pushfd
+	pop eax
+	push ecx
+	popfd
+
+	cmp eax, ecx
+	je .no_cpuid
+	ret
+
+.no_cpuid:
+	mov al, 'C'
+	jmp error
+
+check_long_mode:
+	mov eax, 0x80000000
+	cpuid
+	cmp eax, 0x80000001
+	jb .no_long_mode
+
+	mov eax, 0x80000001
+	cpuid
+	test edx, 1 << 29
+	jz .no_long_mode
+
+	ret
+
+.no_long_mode:
+	mov al, 'L'
+	jmp error
+
+    ;; ----------
+    ;; Page setup
+    ;; ----------
+
+setup_page_tables:
+	mov eax, page_table_l3
+	or  eax, 0b11 ; present, writeable
+	mov [page_table_l4], eax
+
+	mov eax, page_table_l2
+	or  eax, 0b11 ; present, writeable
+	mov [page_table_l3], eax
+
+	mov ecx, 0 ; counter
+
+.loop:
+	mov eax, 0x200000 ; 2MiB
+	mul ecx,
+	or  eax, 0b10000011 ; present, writeable, huge page
+	mov [page_table_l2 + ecx * 8], eax
+
+	inc ecx ; inc counter
+	cmp ecx, 512 ; check if the whole table is mapped
+	jne .loop ; if not: continue
+
+	ret
+
+enable_paging:
+	;; pass page table location to the cpu
+	mov eax, page_table_l4
+	mov cr3, eax
+
+	;; enable Physical Address Extension
+	mov eax, cr4
+	or  eax, 1 << 5
+	mov cr4, eax
+
+	;; enable long mode
+	mov ecx, 0xC0000080
+	rdmsr
+	or  eax, 1 << 8
+	wrmsr
+
+	;; enable paging
+	mov eax, cr0
+	or  eax, 1 << 31
+	mov cr0, eax
+
+	ret
+
+    ;; ---------
+    ;; Long mode
+    ;; ---------
+
+    section .text
+    bits 64
+long_mode_start:
+    mov ax, 0
+	mov ss, ax
+	mov ds, ax
+	mov es, ax
+	mov fs, ax
+	mov gs, ax
+
+    ;; take the multiboot info struct pointer
+    pop rdi
+	call _start_rust
+.halt:
+	hlt
+    jmp halt
+
+    ;; ------
+    ;; Memory
+    ;; ------
+
+    section .bss
+
+page_table_l4:
+    resb 4096
+page_table_l3:
+    resb 4096
+page_table_l2:
+    resb 4096
+
+stack_bottom:
+    ;; 16 KiB
+    resb 4096 * 4
+stack_top:
+
+    section .rodata
+gdt64:
+    dq 0                        ; zero entry
+.code_segment: equ $ - gdt64
+    dq (1 << 43) | (1 << 44) | (1 << 47) | (1 << 53)
+.pointer:
+    dw $ - gdt64 - 1
+    dq gdt64
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index 82848fbdcbeb0a92a347c671f894b03a3db9f0e1..0000000000000000000000000000000000000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-#![no_std]
-#![no_main]
-
-#[path = "arch/x86_64/mod.rs"]
-pub mod arch;
-
-pub mod vga;
-
-#[panic_handler]
-fn panic_handler(_: &core::panic::PanicInfo) -> ! {
-    loop {}
-}
-
-fn kernel_main() -> ! {
-    // null byte clears the VGA buffer
-    // print!("\0");
-
-    // println!("Hello from Hyperion, pointer = {pointer:#x}, fb = {fb:#x}");
-
-    loop {
-        unsafe {
-            core::arch::asm!("hlt");
-        }
-    }
-}
diff --git a/src/log.rs b/src/log.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b8e024dcbb05b35200b265a395b8583574400978
--- /dev/null
+++ b/src/log.rs
@@ -0,0 +1,47 @@
+use core::fmt::Arguments;
+use spin::Lazy;
+
+//
+
+#[macro_export]
+macro_rules! print {
+    ($($t:tt)*) => { $crate::log::_print(format_args!($($t)*)) };
+}
+
+#[macro_export]
+macro_rules! println {
+    ()          => { $crate::log::_print(format_args!("\n")); };
+    ($($t:tt)*) => { $crate::log::_print(format_args_nl!($($t)*)); };
+}
+
+//
+
+static LOGGER: Lazy<Logger> = Lazy::new(Logger::init);
+
+struct Logger {
+    term: bool,
+    qemu: bool,
+}
+
+impl Logger {
+    fn init() -> Self {
+        Logger {
+            term: true,
+            qemu: true,
+        }
+    }
+
+    fn print(&self, args: Arguments) {
+        if self.term {
+            crate::arch::boot::_print(args);
+        }
+        if self.qemu {
+            crate::qemu::_print(args);
+        }
+    }
+}
+
+#[doc(hidden)]
+pub fn _print(args: Arguments) {
+    LOGGER.print(args)
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..77827b89551ee834ce728191804b522f9f3f33f8
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,30 @@
+#![no_std]
+#![no_main]
+#![feature(format_args_nl)]
+#![feature(abi_x86_interrupt)]
+
+use spin::Mutex;
+
+#[path = "arch/x86_64/mod.rs"]
+pub mod arch;
+pub mod log;
+pub mod qemu;
+// pub mod vga;
+
+static BOOTLOADER: Mutex<&'static str> = Mutex::new("Hyperion");
+
+#[panic_handler]
+fn panic_handler(_: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
+
+fn kernel_main() -> ! {
+    println!("Hello from Hyperion");
+    println!(" - Hyperion was booted with {}", BOOTLOADER.lock());
+
+    loop {
+        unsafe {
+            core::arch::asm!("hlt");
+        }
+    }
+}
diff --git a/src/qemu.rs b/src/qemu.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d2c85f9e6540c4431149f7690b99aeaa30505fb6
--- /dev/null
+++ b/src/qemu.rs
@@ -0,0 +1,19 @@
+use core::fmt::{Arguments, Write};
+use spin::{Lazy, Mutex};
+use uart_16550::SerialPort;
+
+//
+
+static COM1: Lazy<Mutex<SerialPort>> = Lazy::new(|| {
+    let mut port = unsafe { SerialPort::new(0x3f8) };
+    port.init();
+    Mutex::new(port)
+});
+
+//
+
+#[doc(hidden)]
+pub fn _print(args: Arguments) {
+    let mut writer = COM1.lock();
+    writer.write_fmt(args).unwrap();
+}
diff --git a/src/vga.rs b/src/vga.rs
index 690d6383e6c927b8f37521bb43717e9ca125feaa..1c404904a5e17c622c1f5fd2ff643f6e3c476080 100644
--- a/src/vga.rs
+++ b/src/vga.rs
@@ -7,30 +7,6 @@ use volatile::Volatile;
 
 //
 
-#[macro_export]
-macro_rules! println {
-    () => {
-        println!("");
-    };
-
-    ($($arg:tt)*) => {
-        $crate::vga::_println(format_args!($($arg)*))
-    }
-}
-
-#[macro_export]
-macro_rules! print {
-    () => {
-        print!("");
-    };
-
-    ($($arg:tt)*) => {
-        $crate::vga::_print(format_args!($($arg)*))
-    };
-}
-
-//
-
 pub struct Writer {
     cursor: [usize; 2],
     color: ColorCode,