main.c
// pal boots a Linux bzImage directly, without firmware or a conventional
// bootloader. "we have a virtual machine manager at home" :)
// https://youtu.be/odouCACH9-w
// https://docs.kernel.org/arch/x86/boot.html
// https://docs.kernel.org/virt/kvm/api.html

#include <errno.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

// absolute byte offsets in the bzImage setup header, from:
// https://docs.kernel.org/arch/x86/boot.html
#define OFFSET_SETUP_SECTS 0x1F1
#define OFFSET_HDR_MAGIC 0x202
#define OFFSET_VERSION 0x206
#define OFFSET_PREF_ADDRESS 0x258
#define OFFSET_INIT_SIZE 0x260

// Guest-physical addresses chosen for the kernel and boot-time structures.
// They do not overlap: the GDT is in low memory, boot_params and the command
// line are below 1 MiB, and the protected-mode kernel begins at 1 MiB.
#define KERNEL_LOAD_ADDR 0x100000
#define BOOT_PARAMS_ADDR 0x10000
#define CMDLINE_ADDR 0x20000
#define GDT_ADDR 0x500
#define GUEST_MEM_SIZE (128ULL * 1024 * 1024)
#define E820_RAM 1
#define COM1_PORT 0x3f8
#define CMOS_INDEX_PORT 0x70
#define CMOS_DATA_PORT 0x71
#define UART_LCR_DLAB 0x80

// Signal handlers may safely set sig_atomic_t flags, but cannot perform the
// terminal reads or KVM ioctls themselves.  The main loop consumes these flags
// after KVM_RUN returns with EINTR.
static volatile sig_atomic_t startup_timeout;
static volatile sig_atomic_t stdin_ready;

// Terminal state is process-global so the atexit handler can restore it even
// when an error path calls die().
static struct termios saved_termios;
static int saved_stdin_flags = -1;
static bool terminal_configured;

static void die(const char* msg);

static void handle_alarm(int signo) {
  (void)signo;
  startup_timeout = 1;
}

static void handle_stdin(int signo) {
  (void)signo;
  stdin_ready = 1;
}

static void restore_terminal(void) {
  if (!terminal_configured)
    return;
  // Best-effort cleanup: an atexit callback cannot usefully report a failure.
  tcsetattr(STDIN_FILENO, TCSANOW, &saved_termios);
  if (saved_stdin_flags >= 0)
    fcntl(STDIN_FILENO, F_SETFL, saved_stdin_flags);
}

static void set_irq(int vm, uint32_t irq, bool level) {
  // KVM_IRQ_LINE drives an input on the in-kernel interrupt controller.  COM1
  // is conventionally connected to x86 IRQ 4; callers decide when to assert
  // and deassert it according to the emulated UART's pending conditions.
  struct kvm_irq_level irq_level = {
      .irq = irq,
      .level = level,
  };
  if (ioctl(vm, KVM_IRQ_LINE, &irq_level) < 0)
    die("KVM_IRQ_LINE");
}

static void die(const char* msg) {
  perror(msg);
  exit(1);
}

// Linux's zero-page ABI is defined by byte offsets, so these packed structures
// describe an on-wire layout rather than a compiler-native data structure.
// E820 entries tell the kernel which guest-physical ranges are usable RAM.
struct e820_entry {
  uint64_t addr;
  uint64_t size;
  uint32_t type;
} __attribute__((packed));

struct boot_params {
  // Preserve the holes from the documented boot_params/zero_page layout.  Only
  // e820_entries, hdr, and e820_table are accessed by this small loader.
  uint8_t _pad0[0x1e8];
  uint8_t e820_entries;
  uint8_t _pad1[0x1f1 - 0x1e9];
  uint8_t hdr[0x290 - 0x1f1];
  uint8_t _pad2[0x2d0 - 0x290];
  struct e820_entry e820_table[128];
  uint8_t _pad3[0x1000 - 0x2d0 - (128 * sizeof(struct e820_entry))];
} __attribute__((packed));

_Static_assert(sizeof(struct boot_params) == 0x1000,
               "boot_params must be one page");

struct kernel_image {
  uint8_t* data;
  size_t file_size;
  size_t setup_size;
  size_t protected_mode_size;
  uint64_t preferred_address;
  uint32_t init_size;
};

struct virtual_machine {
  int kvm;
  int vm;
  int vcpu;
  uint8_t* mem;
  size_t mem_size;
  struct kvm_run* run;
  size_t run_size;
};

struct uart {
  uint8_t regs[8];
  uint8_t input_queue[256];
  size_t input_head;
  size_t input_count;
  bool thre_irq_pending;
  bool irq_asserted;
};

struct cmos_rtc {
  uint8_t index;
};

// load_kernel is a small helper function to read the bzImage file
static struct kernel_image load_kernel(const char* path, bool verbose) {
  struct kernel_image kernel = {0};
  int fd = open(path, O_RDONLY);
  if (fd < 0)
    die("open bzImage");

  struct stat st;
  if (fstat(fd, &st) < 0)
    die("fstat");
  kernel.file_size = (size_t)st.st_size;
  kernel.data = malloc(kernel.file_size);
  if (!kernel.data)
    die("malloc");

  // read the entire bzImage. Since read() may return fewer bytes than
  // requested, we keep track of how many bytes have been read and continue
  // until the whole file is loaded.
  size_t total = 0;
  while (total < kernel.file_size) {
    ssize_t count = read(fd, kernel.data + total, kernel.file_size - total);
    if (count <= 0)
      die("read bzImage");
    total += (size_t)count;
  }
  close(fd);

  uint8_t setup_sects = kernel.data[OFFSET_SETUP_SECTS];
  // if this field is 0, the real value is 4.
  if (setup_sects == 0)
    setup_sects = 4;
  // the real-mode code consists of the boot sector (always one 512-byte sector)
  // plus the setup code.
  kernel.setup_size = (size_t)(setup_sects + 1) * 512;

  uint32_t magic;
  // "HdrS" (0x53726448) in little-endian identifies the Linux x86 boot protocol
  // header.
  memcpy(&magic, &kernel.data[OFFSET_HDR_MAGIC], sizeof(magic));
  if (magic != 0x53726448) {
    fprintf(stderr, "bad bzImage magic\n");
    exit(1);
  }

  uint16_t version;
  memcpy(&version, &kernel.data[OFFSET_VERSION], sizeof(version));
  if (version < 0x0206)
    fprintf(stderr, "warning: old boot protocol version\n");

  // preferred_address represents the preferred physical memory address to load
  // the kernel to
  memcpy(&kernel.preferred_address, &kernel.data[OFFSET_PREF_ADDRESS],
         sizeof(kernel.preferred_address));

  // init_size represents the linear memory required during initialization
  memcpy(&kernel.init_size, &kernel.data[OFFSET_INIT_SIZE],
         sizeof(kernel.init_size));

  // protected_mode_size represents the bulk of the kernel image, cf. 1.1 memory
  // layout
  kernel.protected_mode_size = kernel.file_size - kernel.setup_size;

  if (verbose)
    fprintf(stderr,
            "[I]: setup_size=%zu, protected_mode_size=%zu, "
            "preferred_address=%llu, init_size=%u\n",
            kernel.setup_size, kernel.protected_mode_size,
            (unsigned long long)kernel.preferred_address,
            (unsigned int)kernel.init_size);

  return kernel;
}
// check_kernel_fits is a small helper function to check the kernel  has enough
// memory to work (includes decompressing itself)
static void check_kernel_fits(const struct kernel_image* kernel) {
  bool runtime_fits =
      kernel->init_size <= GUEST_MEM_SIZE &&
      kernel->preferred_address <= GUEST_MEM_SIZE - kernel->init_size;
  bool loaded_image_fits =
      kernel->protected_mode_size <= GUEST_MEM_SIZE - KERNEL_LOAD_ADDR;
  if (!runtime_fits || !loaded_image_fits) {
    fprintf(stderr, "kernel does not fit in guest memory\n");
    exit(1);
  }
}

// create_vm creates a KVM virtual machine, sets up the interrupt controller and
// the programmable interval timer and and allocates and maps guest phyisical
// memory
static struct virtual_machine create_vm(const struct kernel_image* kernel) {
  struct virtual_machine machine = {0};
  // get an handle to the `dev/kvm` file, everything is a file in linux!!!
  machine.kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
  if (machine.kvm < 0)
    die("open /dev/kvm");

  // create a virtual machine, with no cpu and no memory
  machine.vm = ioctl(machine.kvm, KVM_CREATE_VM, 0);
  if (machine.vm < 0)
    die("KVM_CREATE_VM");

  // create an in-kernel virtual interrupt controller, used whenever a device
  // wants CPU attention, used for example by keyboard when inputting something
  if (ioctl(machine.vm, KVM_CREATE_IRQCHIP, 0) < 0)
    die("KVM_CREATE_IRQCHIP");

  // create an in-kernel Programmable Interval Timer, used to schedule
  // processes, divide cpu time, implement sleep() and timeouts, it emits
  // signals to the interrupt controller created above
  struct kvm_pit_config pit = {0};
  if (ioctl(machine.vm, KVM_CREATE_PIT2, &pit) < 0)
    die("KVM_CREATE_PIT2");

  check_kernel_fits(kernel);

  // allocate memory for the virtual machine
  machine.mem_size = GUEST_MEM_SIZE;
  machine.mem = mmap(NULL, machine.mem_size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (machine.mem == MAP_FAILED)
    die("mmap guest memory");

  // set memory region for the virtual machine to the buffer we mmapped above
  struct kvm_userspace_memory_region region = {
      .slot = 0,
      .flags = 0,
      .guest_phys_addr = 0,
      .memory_size = machine.mem_size,
      .userspace_addr = (uint64_t)machine.mem,
  };
  if (ioctl(machine.vm, KVM_SET_USER_MEMORY_REGION, &region) < 0)
    die("KVM_SET_USER_MEMORY_REGION");

  return machine;
}

static void load_guest_memory(struct virtual_machine* machine,
                              const struct kernel_image* kernel) {
  // Skip the real-mode setup sectors and reproduce the bootloader-created state
  // expected by the kernel's direct 32-bit entry point.
  memcpy(machine->mem + KERNEL_LOAD_ADDR, kernel->data + kernel->setup_size,
         kernel->protected_mode_size);

  struct boot_params* bp =
      (struct boot_params*)(machine->mem + BOOT_PARAMS_ADDR);
  memset(bp, 0, sizeof(*bp));
  memcpy(bp->hdr, kernel->data + OFFSET_SETUP_SECTS, sizeof(bp->hdr));

  uint8_t* hdr = bp->hdr;
  // These indices are absolute setup-header offsets rebased to hdr[0].
  hdr[0x210 - OFFSET_SETUP_SECTS] = 0xFF;
  hdr[0x211 - OFFSET_SETUP_SECTS] |= 0x01;

  uint32_t cmdline_ptr = CMDLINE_ADDR;
  memcpy(&hdr[0x228 - OFFSET_SETUP_SECTS], &cmdline_ptr, sizeof(cmdline_ptr));
  // Show all kernel messages while booting. The initramfs lowers the live
  // console threshold immediately before handing ttyS0 to the shell.
  const char* cmdline =
      "console=ttyS0,115200 earlycon=uart,io,0x3f8,115200 loglevel=8 "
      "rdinit=/init";
  memcpy(machine->mem + CMDLINE_ADDR, cmdline, strlen(cmdline) + 1);

  bp->e820_entries = 2;
  // Leave the conventional PC hole between 0x9fc00 and 1 MiB unadvertised;
  // Linux consequently treats it as reserved rather than usable RAM.
  bp->e820_table[0] =
      (struct e820_entry){.addr = 0, .size = 0x9fc00, .type = E820_RAM};
  bp->e820_table[1] = (struct e820_entry){
      .addr = KERNEL_LOAD_ADDR,
      .size = machine->mem_size - KERNEL_LOAD_ADDR,
      .type = E820_RAM,
  };
}

static void configure_cpu(struct virtual_machine* machine) {
  // Give the vCPU KVM's supported CPUID model, map the shared exit-information
  // page, then establish the segment and register state required by boot.rst.
  machine->vcpu = ioctl(machine->vm, KVM_CREATE_VCPU, 0);
  if (machine->vcpu < 0)
    die("KVM_CREATE_VCPU");

  const uint32_t max_cpuid_entries = 256;
  size_t cpuid_size = sizeof(struct kvm_cpuid2) +
                      max_cpuid_entries * sizeof(struct kvm_cpuid_entry2);
  struct kvm_cpuid2* cpuid = calloc(1, cpuid_size);
  if (!cpuid)
    die("calloc CPUID");
  cpuid->nent = max_cpuid_entries;
  if (ioctl(machine->kvm, KVM_GET_SUPPORTED_CPUID, cpuid) < 0)
    die("KVM_GET_SUPPORTED_CPUID");
  if (ioctl(machine->vcpu, KVM_SET_CPUID2, cpuid) < 0)
    die("KVM_SET_CPUID2");
  free(cpuid);

  int run_size = ioctl(machine->kvm, KVM_GET_VCPU_MMAP_SIZE, 0);
  if (run_size < 0)
    die("KVM_GET_VCPU_MMAP_SIZE");
  machine->run_size = (size_t)run_size;
  machine->run = mmap(NULL, machine->run_size, PROT_READ | PROT_WRITE,
                      MAP_SHARED, machine->vcpu, 0);
  if (machine->run == MAP_FAILED)
    die("mmap kvm_run");

  // ---------- protected mode flat segments ----------
  // The 32-bit boot protocol requires a loaded GDT containing __BOOT_CS at
  // selector 0x10 and __BOOT_DS at selector 0x18.  KVM's cached segment state
  // alone is insufficient because the decompressor may reload these selectors.
  uint64_t* gdt = (uint64_t*)(machine->mem + GDT_ADDR);
  gdt[0] = 0;                      // selector 0x00: mandatory null descriptor
  gdt[1] = 0;                      // selector 0x08: unused here
  gdt[2] = 0x00cf9b000000ffffULL;  // 0x10: 32-bit, 4 GiB code
  gdt[3] = 0x00cf93000000ffffULL;  // 0x18: 32-bit, 4 GiB data

  struct kvm_sregs sregs;
  if (ioctl(machine->vcpu, KVM_GET_SREGS, &sregs) < 0)
    die("KVM_GET_SREGS");

  struct kvm_segment flat_code = {
      .base = 0,
      .limit = 0xFFFFFFFF,
      .selector = 0x10,
      .type = 0x0B,  // execute/read, accessed
      .present = 1,  // descriptor is valid
      .dpl = 0,      // kernel privilege level
      .db = 1,       // default operand/address size is 32 bits
      .s = 1,        // code/data descriptor, not a system descriptor
      .l = 0,        // not a 64-bit code segment
      .g = 1,        // limit is expressed in 4 KiB pages
  };
  struct kvm_segment flat_data = flat_code;
  flat_data.type = 0x03;  // read/write, accessed
  flat_data.selector = 0x18;

  sregs.cs = flat_code;
  sregs.ds = flat_data;
  sregs.es = flat_data;
  sregs.fs = flat_data;
  sregs.gs = flat_data;
  sregs.ss = flat_data;
  sregs.gdt.base = GDT_ADDR;
  sregs.gdt.limit = (4 * sizeof(*gdt)) - 1;
  // KVM maintains both the descriptor-table state and cached interpretations
  // of every segment, so set both before enabling protected mode.
  sregs.cr0 |= 1;  // CR0.PE: protected-mode enable

  if (ioctl(machine->vcpu, KVM_SET_SREGS, &sregs) < 0)
    die("KVM_SET_SREGS");

  // ---------- Set the Linux 32-bit entry register contract ----------
  struct kvm_regs regs = {0};
  regs.rip = KERNEL_LOAD_ADDR;  // 32-bit entry point per boot.rst
  regs.rsi = BOOT_PARAMS_ADDR;  // kernel expects boot_params here
  regs.rflags = 0x2;            // reserved bit only, IF=0
  if (ioctl(machine->vcpu, KVM_SET_REGS, &regs) < 0)
    die("KVM_SET_REGS");
}

static void configure_terminal(void) {
  // Raw, asynchronous terminal input lets keystrokes reach the guest serial
  // port immediately. The signal handler only wakes KVM_RUN; drain_stdin does
  // the non-signal-safe read work in normal process context.
  if (isatty(STDIN_FILENO)) {
    // Disable canonical line buffering and echo so each host keystroke can be
    // delivered immediately and is printed only if the guest echoes it.
    if (tcgetattr(STDIN_FILENO, &saved_termios) < 0)
      die("tcgetattr");
    struct termios raw = saved_termios;
    raw.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
    raw.c_cc[VMIN] = 1;
    raw.c_cc[VTIME] = 0;
    if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) < 0)
      die("tcsetattr");
    terminal_configured = true;
    if (atexit(restore_terminal) != 0)
      die("atexit");

    struct sigaction stdin_action = {.sa_handler = handle_stdin};
    sigemptyset(&stdin_action.sa_mask);
    if (sigaction(SIGIO, &stdin_action, NULL) < 0)
      die("sigaction SIGIO");

    // O_ASYNC asks the kernel to deliver SIGIO when input becomes readable;
    // O_NONBLOCK lets the main loop drain everything available without waiting.
    saved_stdin_flags = fcntl(STDIN_FILENO, F_GETFL);
    if (saved_stdin_flags < 0)
      die("fcntl F_GETFL");
    if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) < 0)
      die("fcntl F_SETOWN");
    if (fcntl(STDIN_FILENO, F_SETFL, saved_stdin_flags | O_NONBLOCK | O_ASYNC) <
        0)
      die("fcntl F_SETFL");
  }
}

static void install_alarm(void) {
  startup_timeout = 0;

  struct sigaction alarm_action = {.sa_handler = handle_alarm};
  sigemptyset(&alarm_action.sa_mask);
  if (sigaction(SIGALRM, &alarm_action, NULL) < 0)
    die("sigaction");
  // A malformed boot setup could otherwise stay inside KVM_RUN forever before
  // producing observable output. SIGALRM interrupts that first run after 3 s;
  // the alarm is cancelled as soon as the guest performs any port I/O.
  alarm(3);
}

static void drain_stdin(struct uart* uart) {
  if (!stdin_ready)
    return;
  stdin_ready = 0;

  uint8_t incoming[64];
  ssize_t received;
  while ((received = read(STDIN_FILENO, incoming, sizeof(incoming))) > 0) {
    for (ssize_t i = 0;
         i < received && uart->input_count < sizeof(uart->input_queue); i++) {
      size_t tail =
          (uart->input_head + uart->input_count) % sizeof(uart->input_queue);
      uart->input_queue[tail] = incoming[i];
      uart->input_count++;
    }
  }
}

static void update_uart_irq(struct virtual_machine* machine,
                            struct uart* uart) {
  // COM1 IRQ 4 is level-triggered here: keep it asserted as long as either an
  // enabled receive-data or transmitter-empty condition remains pending.
  bool rx_pending = uart->input_count > 0 && (uart->regs[1] & 0x01) != 0;
  bool tx_pending = uart->thre_irq_pending && (uart->regs[1] & 0x02) != 0;
  bool wanted = rx_pending || tx_pending;
  if (wanted != uart->irq_asserted) {
    set_irq(machine->vm, 4, wanted);
    uart->irq_asserted = wanted;
  }
}

static uint8_t to_bcd(int value) {
  return (uint8_t)(((value / 10) << 4) | (value % 10));
}

// Without CMOS emulation, the default unhandled-port value 0xff leaves the
// update-in-progress bit set forever. Linux then waits one second before
// printing "Unable to read current time from RTC" during every boot.
static uint8_t read_cmos_rtc(uint8_t index) {
  time_t now = time(NULL);
  struct tm utc;
  if (now == (time_t)-1 || !gmtime_r(&now, &utc))
    return 0;

  switch (index) {
    case 0x00:
      return to_bcd(utc.tm_sec);
    case 0x02:
      return to_bcd(utc.tm_min);
    case 0x04:
      return to_bcd(utc.tm_hour);
    case 0x06:
      return to_bcd(utc.tm_wday + 1);
    case 0x07:
      return to_bcd(utc.tm_mday);
    case 0x08:
      return to_bcd(utc.tm_mon + 1);
    case 0x09:
      return to_bcd((utc.tm_year + 1900) % 100);
    case 0x0a:
      return 0x26;  // 32.768 kHz divider, update not in progress.
    case 0x0b:
      return 0x02;  // 24-hour, BCD mode; interrupts disabled.
    case 0x0c:
      return 0;
    case 0x0d:
      return 0x80;  // RTC battery/time is valid.
    case 0x32:
      return to_bcd((utc.tm_year + 1900) / 100);
    default:
      return 0;
  }
}

static void emulate_io(struct kvm_run* run,
                       struct uart* uart,
                       struct cmos_rtc* rtc) {
  // Implement the read-only CMOS clock and byte-wide COM1 accesses. Reads of
  // all other ports return all ones, matching the usual no-device bus value.
  uint8_t* data = (uint8_t*)run + run->io.data_offset;

  for (uint32_t i = 0; i < run->io.count; i++) {
    uint16_t port = run->io.port;
    uint8_t* value = data + (i * run->io.size);
    if (run->io.size == 1 && port == CMOS_INDEX_PORT) {
      if (run->io.direction == KVM_EXIT_IO_OUT)
        rtc->index = *value & 0x7f;
      else
        *value = rtc->index;
      continue;
    }
    if (run->io.size == 1 && port == CMOS_DATA_PORT) {
      if (run->io.direction == KVM_EXIT_IO_IN)
        *value = read_cmos_rtc(rtc->index);
      continue;  // This minimal RTC ignores writes.
    }
    if (port < COM1_PORT || port >= COM1_PORT + 8 || run->io.size != 1) {
      if (run->io.direction == KVM_EXIT_IO_IN)
        memset(value, 0xff, run->io.size);
      continue;
    }

    uint8_t reg = (uint8_t)(port - COM1_PORT);
    bool dlab = (uart->regs[3] & UART_LCR_DLAB) != 0;
    // DLAB changes registers 0 and 1 into baud-rate divisor latches. Otherwise
    // they act as the transmit/receive buffer and interrupt-enable register.
    if (run->io.direction == KVM_EXIT_IO_OUT) {
      if (reg == 0 && !dlab) {
        putchar(*value);
        fflush(stdout);
        uart->thre_irq_pending = true;
      } else if (reg == 1 && !dlab) {
        uint8_t old_ier = uart->regs[1];
        uart->regs[1] = *value;
        if ((*value & 0x02) != 0 && (old_ier & 0x02) == 0)
          uart->thre_irq_pending = true;
      } else {
        uart->regs[reg] = *value;
      }
    } else if (reg == 0 && !dlab && uart->input_count > 0) {
      *value = uart->input_queue[uart->input_head];
      uart->input_head = (uart->input_head + 1) % sizeof(uart->input_queue);
      uart->input_count--;
    } else if (reg == 2) {
      // IIR prioritizes receive data over THRE. Reporting THRE acknowledges the
      // latched condition until another transmission or enable transition.
      if (uart->input_count > 0 && (uart->regs[1] & 0x01) != 0) {
        *value = 0x04;
      } else if (uart->thre_irq_pending && (uart->regs[1] & 0x02) != 0) {
        *value = 0x02;
        uart->thre_irq_pending = false;
      } else {
        *value = 0x01;
      }
    } else if (reg == 5) {
      *value = 0x60 | (uart->input_count > 0 ? 0x01 : 0x00);
    } else if (reg == 6) {
      *value = 0xb0;
    } else {
      *value = uart->regs[reg];
    }
  }
}

static bool handle_exit(struct virtual_machine* machine,
                        struct uart* uart,
                        struct cmos_rtc* rtc,
                        bool* saw_guest_io) {
  // Return true for exits after which the vCPU should be entered again, and
  // false for terminal exits. HLT is nonterminal because Linux uses it to idle
  // until the in-kernel PIT or an injected UART interrupt wakes the vCPU.
  struct kvm_run* run = machine->run;
  switch (run->exit_reason) {
    case KVM_EXIT_IO:
      if (!*saw_guest_io) {
        *saw_guest_io = true;
        alarm(0);
      }
      emulate_io(run, uart, rtc);
      return true;
    case KVM_EXIT_HLT:
      return true;
    case KVM_EXIT_SHUTDOWN:
      printf("\n[guest shutdown/triple fault]\n");
      return false;
    case KVM_EXIT_FAIL_ENTRY:
      fprintf(
          stderr, "KVM_EXIT_FAIL_ENTRY: reason=0x%llx\n",
          (unsigned long long)run->fail_entry.hardware_entry_failure_reason);
      return false;
    case KVM_EXIT_INTERNAL_ERROR: {
      fprintf(stderr, "KVM internal error: suberror=%u ndata=%u\n",
              run->internal.suberror, run->internal.ndata);
      for (uint32_t i = 0; i < run->internal.ndata; i++)
        fprintf(stderr, "  data[%u]=0x%016llx\n", i,
                (unsigned long long)run->internal.data[i]);

      struct kvm_regs regs;
      if (ioctl(machine->vcpu, KVM_GET_REGS, &regs) == 0)
        fprintf(stderr, "  RIP=0x%llx RSP=0x%llx RFLAGS=0x%llx\n",
                (unsigned long long)regs.rip, (unsigned long long)regs.rsp,
                (unsigned long long)regs.rflags);
      return false;
    }
    case KVM_EXIT_MMIO:
      // No MMIO devices exist, but Linux may probe platform ranges. Discard
      // writes and return all ones for reads instead of aborting the VM.
      if (!run->mmio.is_write)
        memset(run->mmio.data, 0xff, run->mmio.len);
      return true;
    default:
      fprintf(stderr, "unhandled exit_reason=%u\n", run->exit_reason);
      return false;
  }
}

static void report_startup_timeout(int vcpu) {
  struct kvm_regs regs;
  if (ioctl(vcpu, KVM_GET_REGS, &regs) < 0)
    die("KVM_GET_REGS after startup timeout");
  fprintf(stderr,
          "guest produced no I/O for 3 seconds: RIP=0x%llx RSP=0x%llx "
          "RAX=0x%llx RSI=0x%llx\n",
          (unsigned long long)regs.rip, (unsigned long long)regs.rsp,
          (unsigned long long)regs.rax, (unsigned long long)regs.rsi);
}

static void run_guest(struct virtual_machine* machine) {
  // This is the only long-running control loop: move host input into the UART,
  // synchronize its interrupt line, enter KVM, and service the resulting exit.
  struct uart uart = {0};
  struct cmos_rtc rtc = {0};
  uart.regs[5] = 0x60;
  bool saw_guest_io = false;

  configure_terminal();
  install_alarm();

  for (;;) {
    drain_stdin(&uart);
    update_uart_irq(machine, &uart);

    if (ioctl(machine->vcpu, KVM_RUN, 0) < 0) {
      if (errno == EINTR && startup_timeout) {
        report_startup_timeout(machine->vcpu);
        return;
      }
      if (errno == EINTR)
        continue;
      die("KVM_RUN");
    }

    if (!handle_exit(machine, &uart, &rtc, &saw_guest_io))
      return;
  }
}

static void destroy_vm(struct virtual_machine* machine) {
  munmap(machine->run, machine->run_size);
  close(machine->vcpu);
  munmap(machine->mem, machine->mem_size);
  close(machine->vm);
  close(machine->kvm);
}

int main(int argc, char** argv) {
  bool verbose = false;
  int option;
  while ((option = getopt(argc, argv, "v")) != -1) {
    switch (option) {
      case 'v':
        verbose = true;
        break;
      default:
        fprintf(stderr, "usage: %s [-v] <bzImage>\n", argv[0]);
        return 1;
    }
  }
  // optind is a global variable used by getopt() to keep track of which command
  // line item it should process next
  if (optind != argc - 1) {
    fprintf(stderr, "usage: %s [-v] <bzImage>\n", argv[0]);
    return 1;
  }

  struct kernel_image kernel = load_kernel(argv[optind], verbose);
  struct virtual_machine machine = create_vm(&kernel);
  load_guest_memory(&machine, &kernel);
  configure_cpu(&machine);
  run_guest(&machine);
  destroy_vm(&machine);
  free(kernel.data);
  return 0;
}