// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include // absolute byte offsets in the bzImage setup header // values are 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, read is not guaranteed to read the entire file // in a single function call, thus we keep track of the number of read bytes // and keep reading until we have read the entire file 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, ®ion) < 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, ®s) < 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, ®s) == 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, ®s) < 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] \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] \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; }