diff --git a/lib/real.c b/lib/real.c
new file mode 100644
index 00000000..b5796656
--- /dev/null
+++ b/lib/real.c
@@ -0,0 +1,106 @@
+#include <lib/real.h>
+
+__attribute__((naked))
+void rm_int(
+  uint8_t int_no,
+  struct rm_regs *out_regs,
+  struct rm_regs *in_regs) {
+    asm (
+        // Self-modifying code: int $int_no
+        "mov al, byte ptr ss:[esp+4]\n\t"
+        "mov byte ptr ds:[3f], al\n\t"
+
+        // Save out_regs
+        "mov eax, dword ptr ss:[esp+8]\n\t"
+        "mov dword ptr ds:[6f], eax\n\t"
+
+        // Save in_regs
+        "mov eax, dword ptr ss:[esp+12]\n\t"
+        "mov dword ptr ds:[7f], eax\n\t"
+
+        // Save non-scratch GPRs
+        "push ebx\n\t"
+        "push esi\n\t"
+        "push edi\n\t"
+        "push ebp\n\t"
+
+        // Jump to real mode
+        "jmp 0x08:1f\n\t"
+        "1: .code16\n\t"
+        "mov ax, 0x10\n\t"
+        "mov ds, ax\n\t"
+        "mov es, ax\n\t"
+        "mov fs, ax\n\t"
+        "mov gs, ax\n\t"
+        "mov ss, ax\n\t"
+        "mov eax, cr0\n\t"
+        "and al, 0xfe\n\t"
+        "mov cr0, eax\n\t"
+        "jmp 0:2f\n\t"
+        "2:\n\t"
+        "mov ax, 0\n\t"
+        "mov ds, ax\n\t"
+        "mov es, ax\n\t"
+        "mov fs, ax\n\t"
+        "mov gs, ax\n\t"
+        "mov ss, ax\n\t"
+
+        // Load in_regs
+        "mov dword ptr ds:[5f], esp\n\t"
+        "mov esp, dword ptr ds:[7f]\n\t"
+        "pop ebp\n\t"
+        "pop edi\n\t"
+        "pop esi\n\t"
+        "pop edx\n\t"
+        "pop ecx\n\t"
+        "pop ebx\n\t"
+        "pop eax\n\t"
+        "mov esp, dword ptr ds:[5f]\n\t"
+
+        // Indirect interrupt call
+        ".byte 0xcd\n\t"
+        "3: .byte 0\n\t"
+
+        // Load out_regs
+        "mov dword ptr ds:[5f], esp\n\t"
+        "mov esp, dword ptr ds:[6f]\n\t"
+        "add esp, 7*4\n\t"
+        "push eax\n\t"
+        "push ebx\n\t"
+        "push ecx\n\t"
+        "push edx\n\t"
+        "push esi\n\t"
+        "push edi\n\t"
+        "push ebp\n\t"
+        "mov esp, dword ptr ds:[5f]\n\t"
+
+        // Jump back to pmode
+        "mov eax, cr0\n\t"
+        "or al, 1\n\t"
+        "mov cr0, eax\n\t"
+        "jmp 0x18:4f\n\t"
+        "4: .code32\n\t"
+        "mov ax, 0x20\n\t"
+        "mov ds, ax\n\t"
+        "mov es, ax\n\t"
+        "mov fs, ax\n\t"
+        "mov gs, ax\n\t"
+        "mov ss, ax\n\t"
+
+        // Restore non-scratch GPRs
+        "pop ebp\n\t"
+        "pop edi\n\t"
+        "pop esi\n\t"
+        "pop ebx\n\t"
+
+        // Exit
+        "ret\n\t"
+
+        // ESP
+        "5: .long 0\n\t"
+        // out_regs
+        "6: .long 0\n\t"
+        // in_regs
+        "7: .long 0\n\t"
+    );
+}
diff --git a/lib/real.h b/lib/real.h
new file mode 100644
index 00000000..12730ee1
--- /dev/null
+++ b/lib/real.h
@@ -0,0 +1,18 @@
+#ifndef __REAL_H__
+#define __REAL_H__
+
+#include <stdint.h>
+
+struct rm_regs {
+    uint32_t ebp;
+    uint32_t edi;
+    uint32_t esi;
+    uint32_t edx;
+    uint32_t ecx;
+    uint32_t ebx;
+    uint32_t eax;
+};
+
+void rm_int(uint8_t, struct rm_regs *, struct rm_regs *);
+
+#endif
diff --git a/main.c b/main.c
index e28aa021..58803fc5 100644
--- a/main.c
+++ b/main.c
@@ -6,10 +6,15 @@ asm (
 );
 
 #include <drivers/vga_textmode.h>
+#include <lib/real.h>
 
 void main(int boot_drive) {
-    // TODO
     init_vga_textmode();
-    text_write("hello world", 11);
-    for (;;);
+    text_write("qLoader 2\n\n", 11);
+    for (;;) {
+        struct rm_regs r = {0};
+        rm_int(0x16, &r, &r);    // Real mode interrupt 16h
+        char c = (char)(r.eax & 0xff);
+        text_write(&c, 1);
+    }
 }