diff --git a/Makefile b/Makefile
index 2064b23..2c1cd65 100644
--- a/Makefile
+++ b/Makefile
@@ -33,6 +33,7 @@ qemu: $(BUILD)/disk.img
 		-smp 1 -m 2048 \
 		-machine q35 \
 		-net none \
+		-serial stdio \
 		-fda "$<"
 # -gdb tcp::26000 \
 # -S
diff --git a/bootloader/bootloader/Cargo.lock b/bootloader/bootloader/Cargo.lock
index 5dfa25d..4fd4d08 100644
--- a/bootloader/bootloader/Cargo.lock
+++ b/bootloader/bootloader/Cargo.lock
@@ -8,6 +8,33 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
+[[package]]
+name = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
 [[package]]
 name = "linked_list_allocator"
 version = "0.10.5"
@@ -33,15 +60,32 @@ version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
+[[package]]
+name = "raw-cpuid"
+version = "10.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
 [[package]]
 name = "redox_bootloader"
 version = "1.0.0"
 dependencies = [
+ "lazy_static",
  "linked_list_allocator",
  "log",
  "spin",
+ "uart_16550",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -65,3 +109,25 @@ checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0"
 dependencies = [
  "lock_api",
 ]
+
+[[package]]
+name = "uart_16550"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e492212ac378a5e00da953718dafb1340d9fbaf4f27d6f3c5cab03d931d1c049"
+dependencies = [
+ "bitflags 2.8.0",
+ "rustversion",
+ "x86",
+]
+
+[[package]]
+name = "x86"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2781db97787217ad2a2845c396a5efe286f87467a5810836db6d74926e94a385"
+dependencies = [
+ "bit_field",
+ "bitflags 1.3.2",
+ "raw-cpuid",
+]
diff --git a/bootloader/bootloader/Cargo.toml b/bootloader/bootloader/Cargo.toml
index e693adf..6d8859e 100644
--- a/bootloader/bootloader/Cargo.toml
+++ b/bootloader/bootloader/Cargo.toml
@@ -67,3 +67,8 @@ multiple_crate_versions = "deny"
 linked_list_allocator = "0.10.5"
 log = "0.4.17"
 spin = "0.9.5"
+uart_16550 = "0.3.2"
+
+[dependencies.lazy_static]
+version = "1.0"
+features = ["spin_no_std"]
diff --git a/bootloader/bootloader/src/main.rs b/bootloader/bootloader/src/main.rs
index 3a4e925..b0458d5 100644
--- a/bootloader/bootloader/src/main.rs
+++ b/bootloader/bootloader/src/main.rs
@@ -11,8 +11,8 @@ use self::os::{OsMemoryEntry, OsMemoryKind};
 
 #[macro_use]
 mod os;
-
 mod logger;
+mod serial;
 
 //TODO: allocate this in a more reasonable manner
 static mut AREAS: [OsMemoryEntry; 1024] = [OsMemoryEntry {
diff --git a/bootloader/bootloader/src/os/bios/mod.rs b/bootloader/bootloader/src/os/bios/mod.rs
index 61c6284..8261440 100644
--- a/bootloader/bootloader/src/os/bios/mod.rs
+++ b/bootloader/bootloader/src/os/bios/mod.rs
@@ -2,7 +2,7 @@ use linked_list_allocator::LockedHeap;
 use spin::Mutex;
 
 use crate::logger::LOGGER;
-use crate::KernelArgs;
+use crate::serial_println;
 
 use self::memory_map::memory_map;
 use self::thunk::ThunkData;
@@ -35,6 +35,8 @@ pub unsafe extern "C" fn start(
 	thunk15: extern "C" fn(),
 	_thunk16: extern "C" fn(),
 ) -> ! {
+	serial_println!("Entered rust; serial initialized.");
+
 	{
 		// Make sure we are in mode 3 (80x25 text mode)
 		let mut data = ThunkData::new();
@@ -59,7 +61,6 @@ pub unsafe extern "C" fn start(
 	let (heap_start, heap_size) = memory_map(thunk15).expect("No memory for heap");
 
 	ALLOCATOR.lock().init(heap_start as *mut u8, heap_size);
-	// let (page_phys, func, args) = crate::run_main(&mut os);
 
 	panic!("kernel");
 }
diff --git a/bootloader/bootloader/src/os/bios/panic.rs b/bootloader/bootloader/src/os/bios/panic.rs
index 552521c..97b3506 100644
--- a/bootloader/bootloader/src/os/bios/panic.rs
+++ b/bootloader/bootloader/src/os/bios/panic.rs
@@ -13,7 +13,7 @@ pub extern "C" fn rust_eh_personality() {}
 #[no_mangle]
 pub fn rust_begin_unwind(info: &PanicInfo<'_>) -> ! {
 	unsafe {
-		println!("BOOTLOADER PANIC:\n{}", info);
+		println!("TETRIS PANIC:\n{}", info);
 		loop {
 			asm!("hlt");
 		}
diff --git a/bootloader/bootloader/src/serial.rs b/bootloader/bootloader/src/serial.rs
new file mode 100644
index 0000000..5c24529
--- /dev/null
+++ b/bootloader/bootloader/src/serial.rs
@@ -0,0 +1,56 @@
+use core::arch::asm;
+
+use lazy_static::lazy_static;
+use spin::Mutex;
+use uart_16550::SerialPort;
+
+lazy_static! {
+	pub static ref SERIAL1: Mutex<SerialPort> = {
+		let mut serial_port = unsafe { SerialPort::new(0x3F8) };
+		serial_port.init();
+		Mutex::new(serial_port)
+	};
+}
+
+#[doc(hidden)]
+pub fn _print(args: core::fmt::Arguments<'_>) {
+	use core::fmt::Write;
+
+	// TODO: preserve previous interrupt state
+
+	// Disable interrupts to prevent deadlocks
+	// (we might get an interrupt while printing)
+	unsafe {
+		asm!("cli", options(preserves_flags, nostack));
+	}
+
+	SERIAL1
+		.lock()
+		.write_fmt(args)
+		.expect("Printing to serial failed");
+
+	// TODO:
+	// This is broken, triple-faults right away.
+	// Uncomment after enabling interrupts in the bootloader & fixing idt
+	//
+	// unsafe {
+	//	asm!("sti", options(preserves_flags, nostack));
+	//	}
+}
+
+/// Prints to the host through the serial interface.
+#[macro_export]
+macro_rules! serial_print {
+	($($arg:tt)*) => {
+		$crate::serial::_print(format_args!($($arg)*));
+	};
+}
+
+/// Prints to the host through the serial interface, appending a newline.
+#[macro_export]
+macro_rules! serial_println {
+	() => ($crate::serial_print!("\n"));
+	($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
+	($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
+		concat!($fmt, "\n"), $($arg)*));
+}