KVM

KVM is surprisingly simple (until you get to drivers, of course)

#include <fcntl.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/mman.h>
#include <linux/kvm.h>
#include <termios.h>

#define MEM_AMT 1024 * 64 /* 64KiB */

#define ERR(x) do { perror(x); exit(-1); } while (0)

int main(int argc, char *argv[])
{
    if (argc < 2) return printf("usage: %s <rom>\n", argv[0]);

    struct termios termios;
    if (tcgetattr(0, &termios) < 0) ERR("tcgetattr");
    struct termios new_termios = termios;
    new_termios.c_lflag &= (~ICANON  & ~ECHO);
    if (tcsetattr(0, 0, &new_termios) < 0) ERR("tcsetattr");

    int kvm = open("/dev/kvm", O_RDWR);
    if (!kvm) ERR("open kvm");

    int vm = ioctl(kvm, KVM_CREATE_VM, 0);
    if (!vm) ERR("create vm");

    struct kvm_userspace_memory_region mem =
    {
        .slot = 0,
        .flags = 0,
        .memory_size = MEM_AMT,
        .userspace_addr = (uint64_t)mmap(NULL, MEM_AMT, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0),
        .guest_phys_addr = 0
    };

    if (!mem.userspace_addr) ERR("malloc");

    if (ioctl(vm, KVM_SET_USER_MEMORY_REGION, &mem)) ERR("set memory region");

    FILE *f = fopen(argv[1], "rb");
    if (!f) ERR("fopen rom");
    uint8_t *w = (uint8_t *)mem.userspace_addr;
    size_t read;
    while ((read = fread(w, 1, 8192, f)) > 0)
    {
        if (read < 0) ERR("fread rom");
        w += read;
    }
    fclose(f);

    int vcpu = ioctl(vm, KVM_CREATE_VCPU, 0);
    if (!vcpu) ERR("create vcpu");

    struct kvm_sregs sregs;
    if (ioctl(vcpu, KVM_GET_SREGS, &sregs)) ERR("get sregs");
    sregs.cs.selector = sregs.cs.base = sregs.ds.selector = sregs.ds.base = sregs.es.selector = sregs.es.base = sregs.fs.selector = sregs.fs.base = sregs.gs.selector = sregs.gs.base = sregs.ss.selector = sregs.ss.base = sregs.fs.selector = sregs.fs.base = 0;
    if (ioctl(vcpu, KVM_SET_SREGS, &sregs)) ERR("set sregs");

    struct kvm_regs regs;
    if (ioctl(vcpu, KVM_GET_REGS, &regs)) ERR("get regs");
    regs.rip = 0;
    regs.rflags = 2;
    if (ioctl(vcpu, KVM_SET_REGS, &regs)) ERR("set regs");

    struct kvm_run *run = mmap(NULL, ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, 0), PROT_READ | PROT_WRITE, MAP_SHARED, vcpu, 0);
    if (!run) ERR("kvm run map");

    char c;
    int ret = 0;

    for (;;)
    {
        if (ioctl(vcpu, KVM_RUN, 0)) ERR("run vcpu 0");
        switch (run->exit_reason)
        {
            case KVM_EXIT_SHUTDOWN: goto end;
            case KVM_EXIT_IO:
            {
                uint8_t *data = (uint8_t *)((uint64_t)run + run->io.data_offset);
                if (run->io.direction == KVM_EXIT_IO_OUT) putchar(*data);
                else *data = c;
            }
                break;
            case KVM_EXIT_HLT: c = getchar(); if (c == '\n') c = '\r'; break;
            default: printf("unknown exit reason %d\n", run->exit_reason); ret = -1; goto end;
        }
    }

end:
    tcsetattr(0, 0, &termios);
    return ret;
}

and a port of BootBASIC that causes this post editor to crash :(

TOODLES.