Nice — this is a natural extension of the `pal.c` you've got: instead of hand-writing 6 bytes of machine code, you load a real `bzImage` and satisfy the Linux/x86 boot protocol so the kernel itself takes over. Here's the full path, with the docs to keep open beside you.

## 1. Build a minimal kernel

Get the source and use the kernel's own KVM-guest config fragment — this is the authoritative, maintained-in-tree way to get a lean guest kernel:

```bash
git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
make tinyconfig                                              # smallest possible baseline
./scripts/kconfig/merge_config.sh -m .config kernel/configs/kvm_guest.config
```

`kernel/configs/kvm_guest.config` is a fragment shipped in the kernel tree specifically for "kernel running as a KVM guest" — it enables virtio, paravirt clock, etc. `tinyconfig` strips everything else out. On top of that merge, you need to manually turn on (via `make menuconfig` or by editing `.config` + `make olddefconfig`):

- `CONFIG_64BIT=y` (unless you want a 32-bit guest)
- `CONFIG_SERIAL_8250=y`, `CONFIG_SERIAL_8250_CONSOLE=y` — so you get console output over the emulated UART
- `CONFIG_TTY=y`, `CONFIG_PRINTK=y`
- `CONFIG_BLK_DEV_INITRD=y` if you want an initramfs
- `CONFIG_BINFMT_ELF=y` if the initramfs runs a real ELF init

Then build:

```bash
make -j$(nproc) bzImage
```

Result: `arch/x86/boot/bzImage`.

**Reference for config work:** `Documentation/admin-guide/README.rst` and the kernel's `Kconfig` help text (`make menuconfig` has search built in, `/` then keyword).

## 2. Build something for it to run (initramfs)

Simplest possible payload — a static busybox or even a trivial static binary as PID 1:

```bash
mkdir -p initramfs
# put a statically-linked init at initramfs/init
cd initramfs
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
```

## 3. The authoritative spec: `Documentation/arch/x86/boot.rst`

This is the document you actually need, at **https://docs.kernel.org/arch/x86/boot.html**. It defines:

- The on-disk layout of `bzImage`: a real-mode `setup.bin` portion (`(setup_sects+1)*512` bytes) followed by the protected-mode kernel image.
- The `setup_header` struct starting at file offset `0x1f1`.
- The **32-bit boot protocol**, which is the one you want (skip real mode entirely):
  - Load the protected-mode part of the image at `0x100000` (1MB).
  - Build a `boot_params` ("zero page") struct somewhere in low memory, copy the file's `setup_header` into it, and fill in `cmd_line_ptr`, `ramdisk_image`/`ramdisk_size`, and an E820 memory map (`e820_table[]`/`e820_entries`).
  - Jump to `load_address + 0x200` with the CPU already in flat 32-bit protected mode, paging off, interrupts off, and — critically — **`%esi` pointing at the zero page**, `%ebp`/`%edi`/`%ebx` = 0.

Everything you already do in `pal.c` for `sregs`/`regs` extends directly here: instead of a real-mode CS with base 0, you set `cs`/`ds`/`es`/`ss` to flat 4GB descriptors with `cr0`'s PE bit set (KVM lets you write these segment caches directly via `KVM_SET_SREGS` without a real in-memory GDT).

## 4. The best "worked example" to read alongside the spec: kvmtool

Don't reinvent the boot_params-filling logic from the prose alone — the kernel's own reference hypervisor does exactly this and is small enough to read in one sitting:

**https://git.kernel.org/pub/scm/linux/kernel/git/will/kvmtool.git**

Look specifically at `x86/kvm.c` (functions like `kvm__arch_setup_regs`, `kvm__load_kernel` / `bzimage.c`). It shows, in real working C:
- exact offsets used to locate `setup_header` in the file
- exact `boot_params` fields it sets
- the exact segment/register setup passed to `KVM_SET_SREGS`/`KVM_SET_REGS`

This is effectively "`pal.c`, but grown up," from the same ioctl family you're already using.

## 5. You need a UART, or you'll get no output

Once the kernel is running it'll write console bytes via `out` instructions to COM1 (I/O port `0x3f8`) if you pass `console=ttyS0` on the cmdline. Your `KVM_RUN` loop needs to catch `KVM_EXIT_IO` for that port and print the byte — that's the minimal "device emulation" required. kvmtool's `hw/serial8250.c` is the reference for a slightly-more-complete version; for a first cut you only need to handle writes to the transmit-holding register.

## 6. Put it together

Rough shape of the loader, layered onto your existing `pal.c`:

1. `open()`/`read()` the `bzImage` into a buffer.
2. Read `setup_sects` from offset `0x1f1+0x1f` (byte 0x1F1 is header start; `setup_sects` is one of the first fields — boot.rst gives the exact byte offsets in its header table).
3. `memcpy` the protected-mode kernel (file offset `(setup_sects+1)*512` to EOF) into guest memory at `0x100000`.
4. Zero a `struct boot_params` in guest memory at, say, `0x10000`; `memcpy` the file's setup_header into `boot_params.hdr`; set `type_of_loader=0xFF`, `cmd_line_ptr`, `ramdisk_image/size`, and one E820 entry marking your guest RAM as usable.
5. Copy your cmdline string (`"console=ttyS0 earlyprintk=serial,ttyS0,115200"`) somewhere and point `cmd_line_ptr` at it.
6. `KVM_SET_SREGS` with flat 32-bit segments + PE bit in `cr0`.
7. `KVM_SET_REGS`: `rip = 0x100000 + 0x200`, `rsi = 0x10000` (zero page address), everything else 0, `rflags = 0x2`.
8. Run loop: handle `KVM_EXIT_IO` on port `0x3f8` by printing the byte; handle `KVM_EXIT_HLT`/shutdown for exit.

## Docs to keep open

- `Documentation/arch/x86/boot.rst` — https://docs.kernel.org/arch/x86/boot.html (the protocol, byte-for-byte)
- kvmtool source — https://git.kernel.org/pub/scm/linux/kernel/git/will/kvmtool.git (working reference implementation)
- `Documentation/virt/kvm/api.html` — https://docs.kernel.org/virt/kvm/api.html (you're already using this one)
- `Documentation/admin-guide/kernel-parameters.txt` — for `console=`, `earlyprintk=` and other cmdline options
- `kernel/configs/kvm_guest.config` in the kernel source tree — the config fragment to merge in

Once you've got console output, the next milestone worth aiming for is a real root filesystem via `virtio-blk`, which needs `CONFIG_VIRTIO_PCI` plus a minimal PCI/virtio-mmio emulation in your VMM — happy to go through that layer if you get this far.
