<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged kvm</title>
<link>https://coderlegion.com/tag/kvm</link>
<description>Powered by Question2Answer</description>
<item>
<title>Notes on x86 PVH boot</title>
<link>https://coderlegion.com/28620/notes-on-x86-pvh-boot</link>
<description>&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;My Rust KVM project now being able to boot Linux on ARM64, I decided to try and&lt;br&gt;
boot Linux on x86. I have been trying in the past to set up the boot procedure&lt;br&gt;
via &lt;code&gt;boot_params&lt;/code&gt; struct. At the time it was a lot more complex and now I wanted&lt;br&gt;
to have some result as soon as possible. So I turned to the PVH&lt;br&gt;
(Para-Virtualized Hardware) Linux boot which simplifies this process a lot. As&lt;br&gt;
a Xen Project protocol, PVH provides a relatively simple interface between a&lt;br&gt;
VMM and the Linux kernel, without requiring the VMM to reproduce the&lt;br&gt;
traditional firmware/bootloader path.&lt;/p&gt;
&lt;h3 id=&quot;entering-protected-mode-via-bios&quot;&gt;Entering protected mode via BIOS&lt;/h3&gt;
&lt;p&gt;The traditional way of booting an operating system on an x86 machine is to start&lt;br&gt;
in real mode and then proceed to protected mode. From there, if CPU is 64-bit,&lt;br&gt;
we can go to long mode. In the PVH Linux entry path, Linux performs the&lt;br&gt;
transition from the required 32-bit protected-mode entry state into 64-bit mode&lt;br&gt;
itself.&lt;/p&gt;
&lt;h4 id=&quot;setting-up-real-mode&quot;&gt;Setting up real mode&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;x86 starts at reset vector (physical address &lt;code&gt;0xFFFFFFF0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CPU is in 16-bit real mode and firmware like SeaBIOS takes over&lt;/li&gt;
&lt;li&gt;SeaBIOS looks at first sector (512 bytes) of a bootable device&lt;/li&gt;
&lt;li&gt;if the sector contains &lt;code&gt;0xAA55&lt;/code&gt; at 510-byte offset, it's loaded at &lt;code&gt;0x7C00&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;load the rest of data manually from our boot device (e.g. on &lt;code&gt;0x8000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;jump to the code section we just loaded (e.g. to address &lt;code&gt;0x8000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;if necessary, set up Stack Pointer and Stack Segment&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;getting-to-protected-mode&quot;&gt;Getting to protected mode&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;make sure interrupts are disabled&lt;/li&gt;
&lt;li&gt;set up and load a minimal GDT (Global Descriptor Table)&lt;/li&gt;
&lt;li&gt;set PE (Protected Mode Enable) bit in CR0 (Control Register)&lt;/li&gt;
&lt;li&gt;perform far jump selecting a proper GDT entry&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;gdt-example&quot;&gt;GDT example&lt;/h4&gt;
&lt;p&gt;A minimal GDT code set up looks like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;gdt_start:
dq 0
gdt_code:; index 1
dw 0xFFFF; segment limit (low)
dw 0x0000; segment base (low)
db 0x00; base address (middle)
db 10011010b; access bits (read/write, executable, code or data, present)
db 11001111b; flags (32-bit protected, 4k blocks) - segment limit (high)
db 0x00; base address (high)

gdt_data:; index 2
dw 0xFFFF; segment limit (low)
dw 0x0000; segment base (low)
db 0x00; base address (middle)
db 10010010b; access bits (read/write, code or data, present)
db 11001111b; flags (32-bit protected, 4k blocks) - segment limit (high)
db 0x00; base address (high)
gdt_end:

gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we enter protected mode by executing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;cli
lgdt [gdt_descriptor]

mov eax, cr0
or eax, 1
mov cr0, eax

jmp 0x08:protected_mode_start ; 0x08 is 1 &amp;lt;&amp;lt; 3 (select index 1 in GDT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The far jump reloads &lt;code&gt;CS&lt;/code&gt; using the descriptor selected by &lt;code&gt;0x08&lt;/code&gt;. The other&lt;br&gt;
segment registers must be loaded with appropriate protected-mode selectors as&lt;br&gt;
well. Each segment register has a visible selector and hidden descriptor-cache&lt;br&gt;
state containing information such as the base, limit and access rights.&lt;/p&gt;
&lt;h3 id=&quot;how-pvh-helps&quot;&gt;How PVH helps&lt;/h3&gt;
&lt;p&gt;If we are in a virtual machine, we don't really need to go through all of these&lt;br&gt;
steps. We don't need to have firmware or BIOS to set up CPU state properly, we&lt;br&gt;
can simply do this from the VM.&lt;/p&gt;
&lt;p&gt;Here, we can manipulate the vCPU to our will, and therefore we can set segment&lt;br&gt;
registers ourselves to valid values, skipping GDT setup. We do need to do some&lt;br&gt;
administration to help Linux understand what's going on and this is exactly&lt;br&gt;
where PVH comes in.&lt;/p&gt;
&lt;h4 id=&quot;elf-pvh-note&quot;&gt;ELF PVH note&lt;/h4&gt;
&lt;p&gt;To determine whether an x86 Linux ELF image supports the PVH boot ABI, we can&lt;br&gt;
inspect its Xen ELF notes and look for &lt;code&gt;XEN_ELFNOTE_PHYS32_ENTRY&lt;/code&gt; (note type&lt;br&gt;
&lt;code&gt;18&lt;/code&gt;). Its descriptor gives the physical 32-bit entry point at which the guest&lt;br&gt;
should be started.&lt;/p&gt;
&lt;p&gt;Newer kernels may also contain &lt;code&gt;XEN_ELFNOTE_PHYS32_RELOC&lt;/code&gt; (type &lt;code&gt;19&lt;/code&gt;), which&lt;br&gt;
describes alignment and physical-address constraints for relocatable PVH&lt;br&gt;
kernels.&lt;/p&gt;
&lt;h3 id=&quot;pvh-boot-memory-setup&quot;&gt;PVH boot memory setup&lt;/h3&gt;
&lt;p&gt;Boot procedure from PVH is more straightforward, but requires more memory&lt;br&gt;
management from VM:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get RIP from ELF PVH note entry&lt;/li&gt;
&lt;li&gt;load Linux kernel ELF segments into memory&lt;/li&gt;
&lt;li&gt;copy kernel command line to memory&lt;/li&gt;
&lt;li&gt;construct an E820-like memory map using &lt;code&gt;HvmMemEntry&lt;/code&gt; entries&lt;/li&gt;
&lt;li&gt;construct and fill out &lt;code&gt;HvmStartInfo&lt;/code&gt; struct&lt;/li&gt;
&lt;li&gt;load memory entries and &lt;code&gt;HvmStartInfo&lt;/code&gt; to memory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If using &lt;code&gt;initramfs&lt;/code&gt; you also need to load it into memory, along with its&lt;br&gt;
separate &lt;code&gt;initramfs&lt;/code&gt; command line. We also need to fill out &lt;code&gt;HvmModuleEntry&lt;/code&gt;&lt;br&gt;
struct to point to &lt;code&gt;initramfs&lt;/code&gt; and its command line address. Note that&lt;br&gt;
here we are not talking about kernel modules, but rather Xen modules, and&lt;br&gt;
for Linux that only means &lt;code&gt;initramfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9777136250809845640&quot; alt=&quot;ELF with PVH&quot;&gt;&lt;/p&gt;
&lt;p&gt;A good reference for the structures above is actually in the Linux kernel&lt;br&gt;
headers and can be found on:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v7.1.3/source/include/xen/interface/hvm/start_info.h&quot;&gt;https://elixir.bootlin.com/linux/v7.1.3/source/include/xen/interface/hvm/start_info.h&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;setting-up-registers&quot;&gt;Setting up registers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;RIP points to entrypoint from ELF note &lt;code&gt;18&lt;/code&gt; entry&lt;/li&gt;
&lt;li&gt;RBX points to &lt;code&gt;HvmStartInfo&lt;/code&gt; physical address (this is PVH specific)&lt;/li&gt;
&lt;li&gt;set up segment registers to valid Protected Mode state&lt;/li&gt;
&lt;li&gt;enable PE bit in CR0 (as in ordinary boot)&lt;/li&gt;
&lt;li&gt;initialize CR4 (Control Register 4) to zero&lt;/li&gt;
&lt;li&gt;clear VM (bit 17), IF (bit 9) and TF (bit 8) in RFLAGS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linux kernel-side PVH register setup is described here:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v7.1.3/source/arch/x86/platform/pvh/head.S#L30&quot;&gt;https://elixir.bootlin.com/linux/v7.1.3/source/arch/x86/platform/pvh/head.S#L30&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;segment-registers&quot;&gt;Segment registers&lt;/h4&gt;
&lt;p&gt;I think the best source for how to set up segment registers is actually my own&lt;br&gt;
code from my Rust KVM project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let code = kvm_segment {
base: 0,
limit: 0xffff_ffff,
selector: 1 &amp;lt;&amp;lt; 3,
type_: 0b1010, // 3rd bit: code/data = 1
   // 2nd bit: conforming = 0
   // 1st bit: readable = 1
   // 0th bit: accessed = 0
present: 1,
dpl: 0, // descriptor privilege level (0: kernel .. 3: userspace)
db: 1,  // default operand / address size (0: 16bit, 1: 32bit)
s: 1,   // system (1: code/data segment, 0: system segment)
l: 0,   // long mode (0: not 64bit segment, 1: 64bit segment)
g: 1,   // granularity (0: limit in bytes, 1: limit in 4Kb blocks)
avl: 0, // available for software (reserved for custom OS purposes)
unusable: 0,
padding: 0,
};

let data = kvm_segment {
selector: 2 &amp;lt;&amp;lt; 3,
type_: 0b0010, // 3rd bit: code/data = 0
   // 2nd bit: conforming = 0
   // 1st bit: writable = 1
   // 0th bit: accessed = 0
..code };

sregs2.tr = kvm_segment {
base: 0,
limit: 0x67, // as per Xen project PVH protocol
g: 0,
..code };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With BIOS boot, these segment descriptors would live in a GDT in guest memory&lt;br&gt;
and the CPU would obtain their hidden descriptor state by loading the&lt;br&gt;
corresponding selectors. With KVM, we can bypass the GDT entirely and directly&lt;br&gt;
initialize the vCPU's segment state through &lt;code&gt;KVM_SET_SREGS&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;PVH turned out to be a convenient way of getting my x86 KVM implementation to&lt;br&gt;
the point where it could boot a real Linux kernel without first implementing&lt;br&gt;
the traditional x86 boot protocol.&lt;/p&gt;
&lt;p&gt;There is still plenty left to implement around the x86 side of the VMM, but&lt;br&gt;
being able to reach the Linux kernel through PVH gives me a much more useful&lt;br&gt;
starting point than reproducing the complete BIOS boot sequence first.&lt;/p&gt;
&lt;p&gt;The implementation is available in the x86 initialization code in my &lt;code&gt;kvm-rust&lt;/code&gt;&lt;br&gt;
repository:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/blob/main/src/arch/x86_64/init.rs&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/blob/main/src/arch/x86_64/init.rs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I plan to continue extending functionality of the x86 side of my VMM.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28620/notes-on-x86-pvh-boot</guid>
<pubDate>Fri, 25 Sep 2026 14:47:35 +0000</pubDate>
</item>
<item>
<title>Booting ARM64 Linux in my Rust KVM Hypervisor: Getting to the console with help of eBPF</title>
<link>https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</link>
<description>&lt;h3 id=&quot;recap&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous article&lt;/a&gt; I got ARM64 Linux as far&lt;br&gt;
as executing inside my Rust KVM hypervisor. I still couldn't actually interact&lt;br&gt;
with the guest, though: I had no working console.&lt;/p&gt;
&lt;p&gt;This article is about getting from &quot;the PC is moving&quot; to typing &quot;hello&quot; into&lt;br&gt;
Linux and getting &quot;HELLO&quot; back.&lt;/p&gt;
&lt;h3 id=&quot;uart&quot;&gt;UART&lt;/h3&gt;
&lt;p&gt;Last time we learned how to obtain the device tree blob from QEMU's &lt;code&gt;virt&lt;/code&gt;&lt;br&gt;
machine. Now we want to take a look at what's inside. We can decompile it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;dtc -I dtb -O dts -o virt.dts virt.dtb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, a simple search through the file already gives us the clue we need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i uart virt.dts -A5 -B1
        pl011@9000000 {
                clock-names = &quot;uartclk\0apb_pclk&quot;;
                clocks = &amp;lt;0x8000 0x8000&amp;gt;;
                interrupts = &amp;lt;0x00 0x01 0x04&amp;gt;;
                reg = &amp;lt;0x00 0x9000000 0x00 0x1000&amp;gt;;
                compatible = &quot;arm,pl011\0arm,primecell&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, now I had a lead: I needed to emulate the &lt;code&gt;pl011&lt;/code&gt; UART at &lt;code&gt;0x09000000&lt;/code&gt; so&lt;br&gt;
that the existing Linux &lt;code&gt;pl011&lt;/code&gt; driver could communicate with it. This was not a&lt;br&gt;
really interesting part, as most of it was simply reading Linux kernel source&lt;br&gt;
code along with &lt;code&gt;pl011&lt;/code&gt; documentation from ARM. Soon I was able to get output&lt;br&gt;
from Linux and it felt really great:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.000000] Linux version 7.1.3 (docker@stjepan-Aspire-F5-573G) (aarch64-linux-gnu-gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP PREEMPT Thu Jul 16 08:52:24 UTC 2026
[    0.000000] KASLR disabled on command line
[    0.000000] Machine model: linux,dummy-virt
[    0.000000] efi: UEFI not found.
[    0.000000] OF: reserved mem: Reserved memory: No reserved-memory node in the DT
[    0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x0000000047ffffff]
[    0.000000] NODE_DATA(0) allocated [mem 0x47fc10c0-0x47fc37bf]
                  ### omitted further kernel log ###
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-custom-initramfs&quot;&gt;Creating custom initramfs&lt;/h3&gt;
&lt;p&gt;Now I needed bidirectional UART support so that I could send input to the guest&lt;br&gt;
as well. To make further investigation easier, I created a very simple binary&lt;br&gt;
written in &lt;code&gt;C&lt;/code&gt; to be run as the Linux init process: it just prints out current&lt;br&gt;
interrupt statistics and asks user for input, echoing it back in all caps. All&lt;br&gt;
we then need is &lt;code&gt;initramfs.txt&lt;/code&gt; file with some simple folders and devices:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dir / 0755 0 0
dir /dev 0755 0 0
dir /proc 0555 0 0
dir /sys 0555 0 0
nod /dev/kmsg 0600 0 0 c 1 11
nod /dev/console 0600 0 0 c 5 1
file /init /home/stjepanp/Develop/initramfs/init 0755 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I only needed enough of a filesystem to provide &lt;code&gt;/init&lt;/code&gt;, &lt;code&gt;/dev/console&lt;/code&gt;, and&lt;br&gt;
access to &lt;code&gt;/proc&lt;/code&gt; for interrupt statistics. Then, in the Linux source code&lt;br&gt;
folder, we generate &lt;code&gt;initramfs&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;./usr/gen_initramfs.sh ../initramfs/initramfs.txt -o initramfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see at which memory address to add &lt;code&gt;initramfs&lt;/code&gt;, we can run ordinary QEMU with&lt;br&gt;
our &lt;code&gt;initramfs&lt;/code&gt; and see what happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;KERNEL=/home/pi/Image
qemu-system-aarch64                                             \
        -M virt,dumpdtb=virt.dtb                                \
        -enable-kvm                                             \
        -cpu host                                               \
        -kernel ${KERNEL}                                       \
        -append &quot;console=ttyAMA0 nokaslr rdinit=/init&quot;          \
        -serial stdio                                           \
        -nographic                                              \
        -nodefaults                                             \
        -initrd &quot;./initramfs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After decompiling &lt;code&gt;virt.dtb&lt;/code&gt; (explained in &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous&lt;/a&gt; article) we can see the location:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~/dtb $ grep initrd virt-initramfs.dts -B2 -A3
        chosen {
                bootargs = &quot;console=ttyAMA0 initramfs_async=0 rdinit=/init nokaslr&quot;;
                linux,initrd-start = &amp;lt;0x0 0x40200000&amp;gt;;
                linux,initrd-end = &amp;lt;0x0 0x40294200&amp;gt;;
                stdout-path = &quot;/pl011@9000000&quot;;
                kaslr-seed = &amp;lt;0x8114b902 0xe815aa93&amp;gt;;
        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do have to recalculate &lt;code&gt;initrd-end&lt;/code&gt; if size of our &lt;code&gt;initramfs&lt;/code&gt; file changes.&lt;br&gt;
If that happens, we can easily recompile the device tree blob after changing&lt;br&gt;
the value. Also, let's not forget to load &lt;code&gt;initramfs&lt;/code&gt; in Rust code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const LOAD_ADDR: u64 = 0x40_000_000;
const INITRAMFS_OFFS: u64 = 0x200_000;
const KERN_OFFS: u64 = 0x200_000 + INITRAMFS_OFFS;

impl VM {
    pub fn arch_load_linux(&amp;amp;mut self, vcpu: &amp;amp;mut VCPU, args: &amp;amp;Args) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        let linux_mem_idx = self.add_mem_region(1024 * 1024 * 1024, LOAD_ADDR)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;dtb_path, 0x0)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;initramfs_path, INITRAMFS_OFFS)?;
// ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setting-up-vgic&quot;&gt;Setting up vGIC&lt;/h3&gt;
&lt;p&gt;Even without having the ability to send keystrokes there was one big problem&lt;br&gt;
with the Linux console &lt;code&gt;stdout&lt;/code&gt;: The kernel messages were reaching the console,&lt;br&gt;
but once execution reached &lt;code&gt;/init&lt;/code&gt;, output stopped behaving as expected. It&lt;br&gt;
took me some time to realize that the ARM architectural timer was also involved.&lt;br&gt;
Its interrupts need to be delivered through the virtual GIC, so I first needed&lt;br&gt;
to create a vGIC. It turned out creating the vGIC was easy. I just followed the&lt;br&gt;
logic from QEMU source code:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&quot;&gt;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In Rust this translates to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut create_device: kvm_create_device = unsafe { std::mem::zeroed() };
create_device.type_ = KVM_ARM_DEVICE_VGIC_V2;
create_device.fd = 0xffffffff;
create_device.flags = 0;

unsafe { libc::ioctl(self.fd, KVM_CREATE_DEVICE, &amp;amp;mut create_device); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked. Also note that I have omitted proper error handling to keep the&lt;br&gt;
code short and concise.&lt;/p&gt;
&lt;h4 id=&quot;tracing-irq-injection&quot;&gt;Tracing IRQ injection&lt;/h4&gt;
&lt;p&gt;Now if we run our &lt;code&gt;initramfs&lt;/code&gt; with &lt;code&gt;strace&lt;/code&gt; as before, and we try to type in&lt;br&gt;
some keys, we will see that in our log we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i irq kvm.log | tail -n5
458398 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7fd932dbd0) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db30) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db20) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So our &lt;code&gt;ioctl&lt;/code&gt; for IRQ injection actually takes an address as argument. If we&lt;br&gt;
investigate this, we can find out how &lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; is handled in the Linux&lt;br&gt;
kernel:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Reading this code we see that the relevant function is &lt;code&gt;kvm_vm_ioctl_irq_line&lt;/code&gt;&lt;br&gt;
that takes &lt;code&gt;struct kvm_irq_level&lt;/code&gt; as second argument and this is what we need&lt;br&gt;
to interpret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* for KVM_IRQ_LINE */
struct kvm_irq_level {
/*
 * ACPI gsi notion of irq.
 * For IA-64 (APIC model) IOAPIC0: irq 0-23; IOAPIC1: irq 24-47..
 * For X86 (standard AT mode) PIC0/1: irq 0-15. IOAPIC0: 0-23..
 * For ARM: See Documentation/virt/kvm/api.rst
 */
union {
__u32 irq;
__s32 status;
};
__u32 level;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this purpose, we can treat it as an 8-byte structure containing the IRQ number and level. Therefore, we can easily inspect it again via eBPF when we&lt;br&gt;
run QEMU with our &lt;code&gt;initramfs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kprobe:kvm_vm_ioctl_irq_line { printf(&quot;irq_level=%x\n&quot;, *((uint64 *)arg1)); }'
Attaching 1 probe...
irq_level=1000021
irq_level=1000021
irq_level=1000021
irq_level=1000021
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without wasting much time we can easily reconstruct this behavior in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;pub fn arch_update_irq(level: u32, vm_fd: libc::c_int) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut irq: kvm_irq_level = unsafe { std::mem::zeroed() };
    irq.level = level;
    irq.__bindgen_anon_1.irq = 0x1000021;

    let ret = unsafe {
        libc::ioctl(vm_fd, KVM_IRQ_LINE, &amp;amp;mut irq)
    };

    if ret &amp;lt; 0 {
        return Err(io::Error::last_os_error());
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that, in essence, &lt;code&gt;0x1000021&lt;/code&gt; is not an arbitrary magic number. On ARM64,&lt;br&gt;
&lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; encodes the interrupt type and interrupt ID in the &lt;code&gt;irq&lt;/code&gt; field. Here &lt;code&gt;0x01&lt;/code&gt; selects an SPI and &lt;code&gt;0x21&lt;/code&gt; is interrupt 33 the UART interrupt from&lt;br&gt;
the device tree.&lt;/p&gt;
&lt;h4 id=&quot;no-such-device-or-address&quot;&gt;No such device or address&lt;/h4&gt;
&lt;p&gt;However, when we run our program, it fails with &quot;No such device or address&quot;. To&lt;br&gt;
get some clue on what is going on, we can try &lt;code&gt;strace&lt;/code&gt; and get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(5, KVM_RUN, 0)                    = -1 ENXIO (No such device or address)
Error: Os { code: 6, kind: Uncategorized, message: &quot;No such device or address&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first impulse was to search for &lt;code&gt;ENXIO&lt;/code&gt; in Elixir bootlin, but,&lt;br&gt;
unfortunately, there were simply too many functions and branches that could&lt;br&gt;
return &lt;code&gt;ENXIO&lt;/code&gt;. Rather than trying to follow every possible &lt;code&gt;ENXIO&lt;/code&gt; path&lt;br&gt;
manually, I decided to use a more brute-force approach. I attached &lt;code&gt;kretprobe&lt;/code&gt;s&lt;br&gt;
to the KVM functions and looked for one returning &lt;code&gt;-ENXIO&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:kvm_* /retval == (uint32)(-6)/ { printf(&quot;%s\n&quot;, probe); }'
Attaching 487 probes...
kretprobe:kvm_vgic_map_resources
kretprobe:kvm_arch_vcpu_run_pid_change
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second function here does not seem really relevant, so I tried looking for&lt;br&gt;
&lt;code&gt;kvm_vgic_map_resources&lt;/code&gt; on Elixir:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now taking a look at the function we can clearly see where &lt;code&gt;ENXIO&lt;/code&gt; is returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int vgic_v2_map_resources(struct kvm *kvm)
{
    struct vgic_dist *dist = &amp;amp;kvm-&amp;gt;arch.vgic;
    int ret = 0;

    if (IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_dist_base) ||
        IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_cpu_base)) {
        kvm_debug(&quot;Need to set vgic cpu and dist addresses first\n&quot;);
        return -ENXIO;
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have a hint. We need to set vGIC CPU and distributor addresses first. To&lt;br&gt;
confirm our hypothesis we can try tracing this function specifically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:vgic_v2_map_resources { printf(&quot;ret=%u\n&quot;, retval); }'
Attaching 1 probe...
ret=4294967290
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;finding-out-distributor-address&quot;&gt;Finding out distributor address&lt;/h4&gt;
&lt;p&gt;My first attempt to set up distributor address didn't go well. I wrote the&lt;br&gt;
following code (I extracted distributor address from the device tree and&lt;br&gt;
tried to reconstruct the logic from QEMU source code):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let dist_addr: u64 = 0x08_000_000;
let mut dev_attr: kvm_device_attr = unsafe { std::mem::zeroed() };
dev_attr.group = KVM_DEV_ARM_VGIC_GRP_ADDR;
dev_attr.attr = KVM_VGIC_V2_ADDR_TYPE_DIST;
dev_attr.flags = 0;
dev_attr.addr = dist_addr;

unsafe { libc::ioctl(create_device.fd as i32, KVM_SET_DEVICE_ATTR, &amp;amp;mut dev_attr) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when I ran the program, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(6, KVM_SET_DEVICE_ATTR, 0x7ffe2696d8) = -1 EFAULT (Bad address)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To debug this, I needed to inspect the values defined in&lt;br&gt;
&lt;code&gt;struct kvm_device_attr&lt;/code&gt;, but it was a lot more complicated: I had to check if&lt;br&gt;
the &lt;code&gt;struct&lt;/code&gt; I was tracing was for &lt;code&gt;KVM_DEV_ARM_VGIC_GRP_ADDR&lt;/code&gt; group and for&lt;br&gt;
&lt;code&gt;KVM_VGIC_V2_ADDR_TYPE_DIST&lt;/code&gt;. So, I decided to write my eBPF program in C with&lt;br&gt;
and called it &lt;code&gt;probe-kvm.bpf.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;linux/bpf.h&amp;gt;
#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;bpf/bpf_helpers.h&amp;gt;
#include &amp;lt;bpf/bpf_tracing.h&amp;gt;
#include &amp;lt;linux/kvm.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;

SEC(&quot;kprobe/vgic_v2_set_attr&quot;)
int BPF_KPROBE(kprobe_vgic_v2_set_attr, void *dev, struct kvm_device_attr *dev_attr)
{
        struct kvm_device_attr attr;
        __u64 value;

        bpf_probe_read_kernel(&amp;amp;attr, sizeof(attr), dev_attr);
        if (attr.group != KVM_DEV_ARM_VGIC_GRP_ADDR) {
                return 0;
        }

        if ((attr.attr != KVM_VGIC_V2_ADDR_TYPE_DIST) &amp;amp;&amp;amp; (attr.attr != KVM_VGIC_V2_ADDR_TYPE_CPU)) {
                return 0;
        }

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr);

        return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To compile and run it I wrote the following script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

clang -O2 -g -mcpu=v1 -target bpf -I /usr/include/aarch64-linux-gnu -c probe-kvm.bpf.c -D__TARGET_ARCH_arm64 -o probe-kvm.bpf.o
sudo rm -rf /sys/fs/bpf/vgic
sudo bpftool prog loadall probe-kvm.bpf.o /sys/fs/bpf/vgic autoattach
sudo cat /sys/kernel/debug/tracing/trace_pipe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, I started ordinary QEMU along my eBPF trace and got this output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./start-probe.sh
           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139387: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fd4d3c450

           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139418: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fd4d3c450
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This really looked like a userspace address while I was supplying a raw address.&lt;br&gt;
To find out what was at that address I modified the last line of the BPF program&lt;br&gt;
above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;        if (bpf_probe_read_user(&amp;amp;value, sizeof(value), (void *)attr.addr) != 0)
                return -1;

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx, *addr=0x%llx\n&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr, value);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this I got what I didn't really expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183145: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fdc806ca0, *addr=0x8010000

           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183177: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fdc806ca0, *addr=0x8000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the &lt;code&gt;addr&lt;/code&gt; field is actually a pointer to the address, and not the address&lt;br&gt;
itself. Note that &lt;code&gt;0x8010000&lt;/code&gt; and &lt;code&gt;0x8000000&lt;/code&gt; are the guest physical&lt;br&gt;
addresses of the GIC CPU interface and distributor taken from the virt machine's&lt;br&gt;
device tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pi@raspberrypi:~/dtb $ grep -i intc virt.dts -A3
        intc@8000000 {
                phandle = &amp;lt;0x8001&amp;gt;;
                reg = &amp;lt;0x00 0x8000000 0x00 0x10000 0x00 0x8010000 0x00 0x10000&amp;gt;;
                compatible = &quot;arm,cortex-a15-gic&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also explained the &lt;code&gt;EFAULT&lt;/code&gt;: KVM was trying to dereference&lt;br&gt;
&lt;code&gt;attr.addr&lt;/code&gt; as a userspace pointer. With this small fix, I got a fully working&lt;br&gt;
Linux console:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;dev_attr.addr = &amp;amp;dist_addr as *const u64 as u64;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following snippet shows that my &lt;code&gt;init&lt;/code&gt; process (as part of&lt;br&gt;
&lt;code&gt;initramfs&lt;/code&gt;) was run and it echoed back my input in capital letters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.832623] clk: Disabling unused clocks
[    0.834046] PM: genpd: Disabling unused power domains
[    0.845565] Freeing unused kernel memory: 12672K
[    0.847475] Run /init as init process
           CPU0       
 11:        199 GIC-0  27 Level     arch_timer
 13:          0 GIC-0  33 Level     uart-pl011
IPI0:         0       Rescheduling interrupts
IPI1:         0       Function call interrupts
IPI2:         0       CPU stop interrupts
IPI3:         0       CPU stop NMIs
IPI4:         0       Timer broadcast interrupts
IPI5:       168       IRQ work interrupts
IPI6:         0       CPU backtrace interrupts
IPI7:         0       KGDB roundup interrupts
Err:          0

type a string: hello
allcaps: HELLO

[    2.224776] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    2.227382] CPU: 0 UID: 0 PID: 1 Comm: init Not tainted 7.1.3 #1 PREEMPT 
[    2.229749] Hardware name: linux,dummy-virt (DT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interrupt statistics are also printed from my &lt;code&gt;init&lt;/code&gt;. Also, when &lt;code&gt;init&lt;/code&gt;&lt;br&gt;
exits, we get the (correct) Kernel panic that init process was terminated.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;At this point I finally had what I was looking for: a real Linux userspace&lt;br&gt;
running inside my own KVM-based hypervisor, with a working UART, interrupt&lt;br&gt;
delivery and a minimal &lt;code&gt;initramfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;More importantly, I now had a way to interact with the guest and inspect what&lt;br&gt;
was happening inside KVM. That gives me a much better foundation for the next&lt;br&gt;
step: implementing more of the virtual hardware needed to turn this into a&lt;br&gt;
useful ARM64 virtual machine.&lt;/p&gt;
&lt;p&gt;You can also check out the repository on my GitHub page and, if you have a&lt;br&gt;
Raspberry Pi 4B, try the VMM yourself:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust&quot;&gt;https://github.com/StjepanPoljak/kvm-rust&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</guid>
<pubDate>Fri, 25 Sep 2026 14:40:06 +0000</pubDate>
</item>
<item>
<title>What is GPU Passthrough?</title>
<link>https://coderlegion.com/27968/what-is-gpu-passthrough</link>
<description>&lt;p&gt;GPU passthrough is a virtualisation method used to directly assign a physical graphics processing unit to a single VM, ultimately bypassing the virtualisation software layers like the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.nvidia.com/vgpu/index.html&quot;&gt;NVIDIA Virtual GPU (vGPU)&lt;/a&gt; manager. In this configuration, the physical hardware is accessed exclusively by the driver running inside that specific VM and is not shared with other environments. While a single VM can be assigned multiple physical GPUs depending on hypervisor limits, individual physical GPUs cannot simultaneously host virtual GPUs and perform passthrough.&lt;/p&gt;
&lt;p&gt;This architectural limitation creates challenges for local development setups. For instance, I was trying to run a simple Kubernetes cluster through Docker Desktop on my Apple Silicon Macbook and I had encountered a now obvious hardware limitation because macOS container runtimes operates inside a Linux VM via the Hypervisor framework. This setup cannot natively pass through the Apple Silicon GPU using Metal to containers. As a result, running even a simple, small &lt;code&gt;qwen2.5:7b&lt;/code&gt; model relied entirely on CPU software inference, which easily maxed-out the processor cores and caused noticeable performance bottlenecks. Like, I was unable to perform even the most basic tasks with this local model.&lt;/p&gt;
&lt;p&gt;To address hardware isolation and GPU utilisation constraints in production environments, tools like Edera allow direct &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.edera.dev/guides/gpu/nvidia-passthrough/&quot;&gt;NVIDIA&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.edera.dev/guides/gpu/amd-passthrough/&quot;&gt;AMD&lt;/a&gt;&lt;/strong&gt; GPU passthrough into isolated Edera zones. By configuring hardware identifiers in the protect daemon, binding devices to the appropriate drivers, and launching zones with dedicated kernel variants, users can run GPU-accelerated workloads and verify hardware access directly through utilities like &lt;code&gt;nvidia-smi&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Bryan Steiner has a really useful tutorial for GPU passthrough, if this is interesting to readers:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bryansteiner/gpu-passthrough-tutorial&quot;&gt;https://github.com/bryansteiner/gpu-passthrough-tutorial&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27968/what-is-gpu-passthrough</guid>
<pubDate>Mon, 21 Sep 2026 14:40:10 +0000</pubDate>
</item>
<item>
<title>Booting ARM64 Linux in my Rust KVM Hypervisor: Debugging a Memory Issue</title>
<link>https://coderlegion.com/26346/booting-arm64-linux-in-my-rust-kvm-hypervisor-debugging-a-memory-issue</link>
<description>&lt;h3 id=&quot;motivation-1&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;The time has come: the big test. I got some simple binaries running on my Rust&lt;br&gt;
KVM hypervisor (at this point I am thinking about giving it a proper name). The&lt;br&gt;
next thing has to be Linux. For this series I will show how to actually boot&lt;br&gt;
Linux on my Rust KVM hypervisor on ARM64 CPU (I am using Raspberry Pi 4 which&lt;br&gt;
supports hardware accelerated virtualization).&lt;/p&gt;
&lt;h3 id=&quot;cross-compiling-linux-kernel&quot;&gt;Cross-compiling Linux kernel&lt;/h3&gt;
&lt;p&gt;In one of my &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-part1&quot;&gt;previous articles&lt;/a&gt; I have described how to&lt;br&gt;
set up the cross-compilation environment with a simple &lt;code&gt;Dockerfile&lt;/code&gt;. We can&lt;br&gt;
reuse it to cross-compile the Linux kernel for ARM64 (after adding some Linux&lt;br&gt;
build dependencies like &lt;code&gt;bc&lt;/code&gt;, &lt;code&gt;flex&lt;/code&gt;, &lt;code&gt;python3&lt;/code&gt;, &lt;code&gt;libssl-dev&lt;/code&gt; and &lt;code&gt;bison&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;docker run --network host -it --rm \
       -e CROSS_COMPILE=aarch64-linux-gnu- -e ARCH=arm64 \
       -v &quot;$(pwd)&quot;:&quot;/home/docker/kvm-rust&quot; -w &quot;/home/docker/kvm-rust/linux-7.1.3&quot; \
       arm-rust-build /bin/bash -c 'make defconfig &amp;amp;&amp;amp; make'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting ARM64 Linux kernel image (&lt;code&gt;Image&lt;/code&gt;) should be located in&lt;br&gt;
&lt;code&gt;arch/arm64/boot/&lt;/code&gt; directory.&lt;/p&gt;
&lt;h3 id=&quot;arm64-boot-protocol&quot;&gt;ARM64 boot protocol&lt;/h3&gt;
&lt;p&gt;The full ARM64 Linux boot protocol is quite straightforward and can be found in&lt;br&gt;
the Linux kernel documentation:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.kernel.org/doc/html/v7.1/arch/arm64/booting.html&quot;&gt;https://www.kernel.org/doc/html/v7.1/arch/arm64/booting.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We simply need to parse the 64-byte header and verify the magic field (it should&lt;br&gt;
equal &lt;code&gt;0x644d5241&lt;/code&gt;). I will just highlight a few important points. We should&lt;br&gt;
read &lt;code&gt;image_size&lt;/code&gt; and &lt;code&gt;text_offset&lt;/code&gt; fields and note that the image should be&lt;br&gt;
loaded at an offset of text_offset bytes  at a 2 MiB boundary. For our kernel,&lt;br&gt;
as I checked, &lt;code&gt;text_offset&lt;/code&gt; is zero and &lt;code&gt;image_size&lt;/code&gt; is the amount of memory the&lt;br&gt;
decompressed kernel image occupies in RAM, which is not necessarily the same as&lt;br&gt;
the file size as reported by &lt;code&gt;stat&lt;/code&gt;. Finally, the registers &lt;code&gt;x1&lt;/code&gt;, &lt;code&gt;x2&lt;/code&gt; and &lt;code&gt;x3&lt;/code&gt;&lt;br&gt;
must be set to zero, while &lt;code&gt;x0&lt;/code&gt; must point to the device tree blob (&lt;code&gt;dtb&lt;/code&gt;) in&lt;br&gt;
the RAM. So, for this I chose to load the &lt;code&gt;dtb&lt;/code&gt; at address &lt;code&gt;0x40000000&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;Image&lt;/code&gt; itself on the first 2 MiB boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const LOAD_ADDR: u64 = 0x40_000_000;
const KERN_OFFS: u64 = 0x200_000;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;obtaining-the-device-tree-blob&quot;&gt;Obtaining the device tree blob&lt;/h4&gt;
&lt;p&gt;So, for ARM64 we do need a device tree blob that will tell the Linux kernel on&lt;br&gt;
what kind of hardware it is running on. And this step is quite easy, we will&lt;br&gt;
can simply reuse our command for running QEMU, with one small addition (i.e.&lt;br&gt;
adding &lt;code&gt;dumpdtb&lt;/code&gt; option to &lt;code&gt;-M virt&lt;/code&gt; argument):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

KERNEL=/home/pi/Image
qemu-system-aarch64                                             \
        -M virt,dumpdtb=virt.dtb                                \
        -smp 1                                                  \
        -enable-kvm                                             \
        -cpu host                                               \
        -kernel ${KERNEL}                                       \
        -append &quot;console=ttyAMA0&quot;                               \
        -serial stdio                                           \
        -nographic                                              \
        -nodefaults
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us a consistent snapshot of the hardware we were running on (in this &lt;br&gt;
case it's emulated hardware for QEMU &lt;code&gt;virt&lt;/code&gt; machine). We could (and we should)&lt;br&gt;
edit and prune it for our own purposes, but let's leave that for another time.&lt;/p&gt;
&lt;h4 id=&quot;implementing-boot-in-rust&quot;&gt;Implementing boot in Rust&lt;/h4&gt;
&lt;p&gt;So, loading the kernel in Rust is quite straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;impl VM {
    pub fn arch_load_linux(&amp;amp;mut self, vcpu: &amp;amp;mut VCPU, args: &amp;amp;Args) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        let header = get_linux_header(&amp;amp;args.binary)?;
        let linux_mem_idx = self.add_mem_region((KERN_OFFS + header.image_size) as usize, LOAD_ADDR)?;

        match &amp;amp;args.dtb {
            Some(dtb_path) =&amp;gt; { self.load_file_to_memory(linux_mem_idx, &amp;amp;dtb_path, 0x0)?; },
            None =&amp;gt; { return Err(io::Error::other(&quot;Device tree blob not specified.&quot;)); }
        }

        self.load_file_to_memory(linux_mem_idx, &amp;amp;args.binary, KERN_OFFS)?;

        vcpu.set_one_reg(&quot;x0&quot;, LOAD_ADDR)?;
        vcpu.set_one_reg(&quot;x1&quot;, 0x0)?;
        vcpu.set_one_reg(&quot;x2&quot;, 0x0)?;
        vcpu.set_one_reg(&quot;x3&quot;, 0x0)?;

        vcpu.set_one_reg(&quot;pc&quot;, LOAD_ADDR + KERN_OFFS)?;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;get_linux_header()&lt;/code&gt; function was omitted here as it only parses&lt;br&gt;
the aforementioned ARM64 Linux image headers in a boilerplate manner.&lt;/p&gt;
&lt;h3 id=&quot;function-not-implemented&quot;&gt;Function not implemented&lt;/h3&gt;
&lt;p&gt;As soon as I tried to run my hypervisor with Linux &lt;code&gt;Image&lt;/code&gt;, I ran into an issue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ ./rust -d virt Image 
x0 = 0x0
pc = 0x1000
magic 0x644d5241, image_size: 38731776, file_size: 38040064, text_offset: 0x0
Loading &quot;virt&quot; to 0x0...
Loading &quot;Image&quot; to 0x200000...
x0 = 0x40000000
pc = 0x40200000
x0 = 0xfffffbfffda35000
pc = 0xffffd902f0f8a42c
Error: Os { code: 38, kind: Unsupported, message: &quot;Function not implemented&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;trying-strace&quot;&gt;Trying strace&lt;/h4&gt;
&lt;p&gt;First thing I turned to was &lt;code&gt;strace&lt;/code&gt;; it's easy to use and gets some nice&lt;br&gt;
information really fast:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(5, KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG, 0x7fcee98768) = 0
x0 = 0x40000000
ioctl(5, KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG, 0x7fcee98768) = 0
pc = 0x40200000
ioctl(5, KVM_RUN, 0)                    = -1 ENOSYS (Function not implemented)
ioctl(5, KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG, 0x7fcee988a8) = 0
x0 = 0xfffffbfffda35000
ioctl(5, KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG, 0x7fcee988a8) = 0
pc = 0xffffd902f0f8a42c
Error: Os { code: 38, kind: Unsupported, message: &quot;Function not implemented&quot; }
+++ exited with 1 +++
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I didn't learn much other than the fact that &lt;code&gt;KVM_RUN&lt;/code&gt; failed completely with&lt;br&gt;
&lt;code&gt;ENOSYS&lt;/code&gt; error code. My first attempt to find out what was going on was&lt;br&gt;
searching the ARM64 KVM source code for &lt;code&gt;ENOSYS&lt;/code&gt; in Linux repository. It didn't&lt;br&gt;
take long, as it appeared only in one place which was a promising lead:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v7.1.3/source/arch/arm64/kvm/mmio.c#L188&quot;&gt;https://elixir.bootlin.com/linux/v7.1.3/source/arch/arm64/kvm/mmio.c#L188&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As we can see, the function in question is &lt;code&gt;io_mem_abort()&lt;/code&gt; in the Linux kernel.&lt;br&gt;
Now, &lt;code&gt;strace&lt;/code&gt; got us that far, but this is a Linux kernel function and not much&lt;br&gt;
we can find out more about it with &lt;code&gt;strace&lt;/code&gt; (which traces only system calls like&lt;br&gt;
&lt;code&gt;ioctl&lt;/code&gt;). I haven't even considered &lt;code&gt;GDB&lt;/code&gt; because this was the host kernel,&lt;br&gt;
and this would require a really complex time-consuming setup. Luckily, there was&lt;br&gt;
a much easier way.&lt;/p&gt;
&lt;h4 id=&quot;analyzing-io-memory-abort-with-ebpf&quot;&gt;Analyzing IO memory abort with eBPF&lt;/h4&gt;
&lt;p&gt;The first thing I did was to list &lt;code&gt;kprobe&lt;/code&gt;s in &lt;code&gt;bpftrace&lt;/code&gt; and see if I can hook&lt;br&gt;
into the &lt;code&gt;io_mem_abort&lt;/code&gt; at all via eBPF:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -l 'kprobe:io_mem_abort'
kprobe:io_mem_abort
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was in luck, so I could at least confirm that this function was the one&lt;br&gt;
causing the issue, so I attached a &lt;code&gt;kretprobe&lt;/code&gt; to it, ran &lt;code&gt;bpftrace&lt;/code&gt; alongside&lt;br&gt;
my hypervisor and got the confirmation I needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e '
&amp;gt;    kretprobe:io_mem_abort {
&amp;gt;        printf(&quot;ret=%d\n&quot;, retval);
&amp;gt;    }'
Attaching 1 probe...
ret=-38
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;finding-out-what-really-happened&quot;&gt;Finding out what really happened&lt;/h4&gt;
&lt;p&gt;Now that I have confirmed &lt;code&gt;io_mem_abort&lt;/code&gt; was the culprit, it was time to&lt;br&gt;
analyze it more deeply. I attached an additional &lt;code&gt;kprobe&lt;/code&gt; so I could print both&lt;br&gt;
the return value and the &lt;code&gt;fault_ipa&lt;/code&gt; argument:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int io_mem_abort(struct kvm_vcpu *vcpu, phys_addr_t fault_ipa)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The updated &lt;code&gt;bpftrace&lt;/code&gt; script printed the fault address.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e '
&amp;gt;    kretprobe:io_mem_abort {
&amp;gt;        printf(&quot;ret=%d\n&quot;, retval);
&amp;gt;    }
&amp;gt;    kprobe:io_mem_abort {
&amp;gt;        printf(&quot;0x%llx\n&quot;, arg1);
&amp;gt;    }'
Attaching 2 probes...
0x47fff000
ret=-38
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the result surprised me as I initially suspected it was trying to access some&lt;br&gt;
device that I haven't created; however, I realized that this was actually just&lt;br&gt;
beyond the memory I had allocated for the Linux kernel to use. So it clicked: I probably hadn't allocated enough guest memory. Instead of allocating just&lt;br&gt;
&lt;code&gt;header.image_size + KERN_OFFS&lt;/code&gt; I increased it to 1 GiB:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let linux_mem_idx = self.add_mem_region(1024 * 1024 * 1024, LOAD_ADDR)?;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;With that adjustment, the &lt;code&gt;ENOSYS&lt;/code&gt; error disappeared. The guest was clearly&lt;br&gt;
executing code, although I still wasn't seeing any output on the serial console.&lt;br&gt;
That meant the memory issue was solved, but there was at least one more thing&lt;br&gt;
left before we would actually see Linux boot.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26346/booting-arm64-linux-in-my-rust-kvm-hypervisor-debugging-a-memory-issue</guid>
<pubDate>Sun, 06 Sep 2026 18:39:35 +0000</pubDate>
</item>
<item>
<title>KVM vs KVM - what is the question?</title>
<link>https://coderlegion.com/23702/kvm-vs-kvm-what-is-the-question</link>
<description>&lt;p&gt;Last weekend I was looking for a new project for my Raspberry Pi. I was looking for ways to set up a KVM on my Pi5 since it has 8GB of RAM, which is beefy enough for additional virtualisation tasks. Once I started Googling how I should start this task, I would look up “&lt;em&gt;KVM on Raspberry Pi&lt;/em&gt;”. Most of the search results were for another entirely different &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pikvm.org&quot;&gt;KVM&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A Keyboard, Video, and Mouse (&lt;strong&gt;KVM&lt;/strong&gt;) switch is a hardware device that allows a user to control multiple computers or servers using a single set of keyboard, video display, and mouse devices. While the KVM switch is a great technology that allows you to control multiple computers using your existing setup, I currently use Tailscale for accessing my remote Pis via my home laptop. So that wasn’t what I was looking for. Instead, I was interested in seeing whether I could run a Kernel Virtual Machine (&lt;strong&gt;KVM&lt;/strong&gt;) on my Pi5.&lt;/p&gt;
&lt;p&gt;Despite sharing the same acronym, a KVM switch and the KVM software operate in entirely different realms of technology: one is a physical hardware device for managing physical computers, while the other is open-source software built into Linux to create and manage virtual machines. The KVM Switch routes peripheral and display signals between different systems with the press of a button or a keyboard shortcut.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9303357946188213631&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-is-a-kvm-vm-&quot;&gt;What is a KVM VM?&lt;/h3&gt;
&lt;p&gt;Kernel-based VM is an open source virtualisation technology for Linux. With KVM, Linux can function as a hypervisor that runs multiple, isolated VMs. KVM was announced way back in 2006 and actually merged into the Linux kernel in 2007. &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Virtualization&quot;&gt;Virtualisation&lt;/a&gt;&lt;/strong&gt;, which lets you run an operating OS inside another OS, is possible thanks to hypervisors. &lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.edera.dev/technical-overview/concepts/hypervisor/&quot;&gt;hypervisor&lt;/a&gt;&lt;/strong&gt; pools computing resources (like processing, memory, and storage) and reallocates them among VMs. The hypervisor can run multiple VMs at once, manage them, and support the creation of new ones. The physical hardware, when used as a hypervisor, is called the host, while the many VMs that use its resources are guests. Hypervisors need OS-level components such as memory management, process scheduling, I/O, device drivers, security management, a network stack, and more to run VMs. &lt;/p&gt;
&lt;p&gt;KVM has all these components because it’s part of the Linux kernel. Every VM is implemented as a regular Linux process, scheduled by the standard Linux scheduler, with dedicated virtual hardware like a network card, graphics adapter, CPU, memory, and disks.&lt;/p&gt;
&lt;h3 id=&quot;similarities-between-kvm-and-kvm&quot;&gt;Similarities between KVM and KVM&lt;/h3&gt;
&lt;p&gt;Both technologies fundamentally exist to solve an efficiency problem: enabling a user to manage multiple OSs from a single workstation without buying duplicate input devices or displays. Whether toggling between two physical tower PCs or switching focus between multiple virtual instances, both implementations allow one operator to interact with distinct, isolated computing environments. Furthermore, both solutions preserve system isolation, ensuring that actions taken within one environment do not interfere with or crash the other operating systems running alongside it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=169879252453172863&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;differences&quot;&gt;Differences&lt;/h3&gt;
&lt;p&gt;The key distinction lies in hardware physical routing versus software virtualisation. A KVM switch is a tangible piece of hardware that physically redirects signal cables (HDMI/DisplayPort and USB) so that a single set of peripherals can control multiple physically distinct servers or PCs. It does not alter how the computers operate; it merely changes which machine receives your keypresses and sends video output to your monitor.&lt;/p&gt;
&lt;p&gt;In contrast, a Kernel-based VM is a software module integrated directly into the Linux kernel that turns the host OS into a &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://aws.amazon.com/compare/the-difference-between-type-1-and-type-2-hypervisors/&quot;&gt;Type-1 hypervisor.&lt;/a&gt;&lt;/strong&gt; Instead of connecting separate physical machines, the kernel-based KVM partitions a single computer's underlying processor, memory, and storage to simulate entirely separate virtual computers (&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.reddit.com/r/sysadmin/comments/a7mofe/who_came_up_with_guest_vm_and_why_do_we_call_it/&quot;&gt;Guest VMs&lt;/a&gt;&lt;/strong&gt;) on the same bare metal.&lt;/p&gt;
&lt;h3 id=&quot;the-question-should-ve-been-kvm-or-xen-&quot;&gt;The question should’ve been KVM or Xen?&lt;/h3&gt;
&lt;p&gt;And that brings us to a totally different question. KVM and Xen - what are the differences and which one can I run on my Pi5 for virtualisation? While both KVM and Xen are open-source Type-1 hypervisors that deliver high-performance virtualisation, their core designs and operational structures differ significantly.&lt;/p&gt;
&lt;p&gt;KVM is built directly into the Linux kernel as a standard kernel module (&lt;code&gt;kvm.ko&lt;/code&gt;). Installing KVM turns an existing Linux OS into a hypervisor, granting you immediate access to standard Linux drivers, process schedulers, and existing system management tools without having to modify the host kernel. Conversely, Xen sits below the OS as a &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Microkernel&quot;&gt;microkernel&lt;/a&gt;&lt;/strong&gt;-based layer that boots first, before any OS. It relies on a special, privileged Guest VM known as &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.xenproject.org/wiki/Dom0&quot;&gt;Dom0&lt;/a&gt;&lt;/strong&gt; (Domain 0) to handle hardware access, boot management, and admin control over other Guest VMs (&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.oracle.com/cd/E50245_01/E50249/html/vmcon-ovm-server-domains-domu.html&quot;&gt;DomU&lt;/a&gt;&lt;/strong&gt;).&lt;/p&gt;
&lt;p&gt;KVM relies purely on hardware-assisted virtualisation (using processor extensions like Intel VT-x or ARM &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://support.arm.com/documentation/ddi0438/d/Programmers-Model/Virtualization-Extensions-architecture&quot;&gt;Virtualisation Extensions&lt;/a&gt;&lt;/strong&gt;) to run unmodified Guest OSs. Xen historically pioneered &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.edera.dev/technical-overview/concepts/paravirtualization/&quot;&gt;paravirtualisation&lt;/a&gt;&lt;/strong&gt; (where guest kernels are modified to communicate directly with the hypervisor API for higher efficiency), though it now supports hardware-assisted virtualisation as well. Furthermore, Xen’s strict separation of the hypervisor microkernel from the control domain (Dom0) isolates hardware drivers away from guest VMs, which makes it a popular architecture for high-security environments like &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.qubes-os.org/&quot;&gt;Qubes OS&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For almost all Raspberry Pi 5 use cases, &lt;strong&gt;KVM is the far more practical and seamless option.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In the case of the Raspberry Pi, you’ll need to verify &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.archlinux.org/title/KVM#Hardware_support&quot;&gt;hardware virtualisation support&lt;/a&gt;&lt;/strong&gt; before doing anything. Check that ARM CPU virtualisation extensions are available. Check that the KVM hypervisor kernel module is present and accessible with the below command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l /dev/kvm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see an output showing &lt;code&gt;crw-rw---- 1 root kvm ... /dev/kvm&lt;/code&gt;. &lt;br&gt;
If &lt;code&gt;/dev/kvm&lt;/code&gt; exists, hardware acceleration is supported and active.&lt;/p&gt;
&lt;p&gt;The Raspberry Pi’s boot process relies on a proprietary &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/EEPROM&quot;&gt;EEPROM&lt;/a&gt;&lt;/strong&gt;-based bootloader that loads GPU firmware and kernel binaries directly from an SD card or NVMe storage (often chainloaded through U-Boot for standard Linux environments). Because KVM is integrated into standard ARM64 Linux kernels (like Raspberry Pi OS 64-bit or Ubuntu), it requires no special bootloader tweaks or &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/UEFI&quot;&gt;UEFI&lt;/a&gt;&lt;/strong&gt; emulation layer. You simply install &lt;code&gt;`&lt;/code&gt;qemu-kvm&lt;code&gt;`&lt;/code&gt; (&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.qemu.org/&quot;&gt;QEMU&lt;/a&gt;&lt;/strong&gt;) and &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://libvirt.org/&quot;&gt;libvirt&lt;/a&gt;&lt;/strong&gt;, and the Pi instantly acts as a hypervisor using built-in ARM hardware extensions, allowing me to easily spin up VMs using standard Linux tools like &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://virt-manager.org/&quot;&gt;virt-manager&lt;/a&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cockpit-project.org/&quot;&gt;cockpit&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can run the following command to install the hypervisor, daemon services, and CLI management tools like QEMU core binaries. Since I’m running Raspberry Pi OS Desktop with the graphical interface, I also appended &lt;code&gt;virt-manager&lt;/code&gt; to the below command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y qemu-system-arm qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst virt-manager
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the packages are installed, I just need to startup/enable the &lt;code&gt;libvirtd&lt;/code&gt; service to ensure the hypervisor daemon starts automatically on boot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now libvirtd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Libvirt includes a default NAT network interface (&lt;code&gt;virbr0&lt;/code&gt;) for guest VMs. As soon as the default virtual network was configured, I can simply create my first Guest VM on the Pi5 and use an Ubuntu ARM64 ISO image, sourced from the Canonical website. The &lt;code&gt;virt-install&lt;/code&gt; looks a little like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;virt-install \
  --name debian-vm \
  --memory 2048 \
  --vcpus 2 \
  --disk size=10,bus=virtio \
  --os-variant debian12 \
  --cdrom /path/to/debian-arm64.iso \
  --network network=default \
  --graphics vnc,listen=0.0.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In contrast, setting up Xen on a Pi 5 requires a complex boot architecture. Because Xen acts as a microkernel that must load before any OS, booting it on an &lt;strong&gt;SBC&lt;/strong&gt; (Single Board Computer) generally requires using &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.u-boot.org/&quot;&gt;U-Boot&lt;/a&gt;&lt;/strong&gt; to load the Xen binary alongside device-tree overlays, a customised Dom0 control domain, and a secondary driver domain. While projects like &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/xen-troops&quot;&gt;Xen-Troops&lt;/a&gt;&lt;/strong&gt; exist specifically to compile custom &lt;strong&gt;Yocto-based&lt;/strong&gt; Xen images for embedded systems and automotive apps, setting this up for home or homelab experiments involves significant compilation overhead, non-standard driver configs, and manual partition setups. While maybe not suited to Pi5, it’s not impossible on previous versions like the Pi4, as stated on the &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linux.com/news/xen-on-raspberry-pi-4-adventures/&quot;&gt;Linux blog&lt;/a&gt;&lt;/strong&gt;. For edge use-cases you can certainly stick with KVM for a smooth setup unless you specifically need Xen's compartmentalised driver architecture for complex embedded development. If you’d like to read why Edera specifically opted for Xen when building a secure microservice foundation, check out their &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://edera.dev/stories/why-edera-built-on-xen-a-secure-container-foundation&quot;&gt;blog post&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23702/kvm-vs-kvm-what-is-the-question</guid>
<pubDate>Tue, 28 Jul 2026 22:04:05 +0000</pubDate>
</item>
<item>
<title>Porting my Rust KVM hypervisor to ARM64: Running the binary</title>
<link>https://coderlegion.com/23276/porting-my-rust-kvm-hypervisor-to-arm64-running-the-binary</link>
<description>&lt;h3 id=&quot;recap-1&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-part1&quot;&gt;previous article&lt;/a&gt; we ported the register handling&lt;br&gt;
of my Rust KVM hypervisor from x86 to ARM64. By tracing QEMU with strace and&lt;br&gt;
inspecting the Linux headers, we discovered that ARM64 accesses CPU state&lt;br&gt;
through the &lt;code&gt;KVM_GET_ONE_REG&lt;/code&gt; and &lt;code&gt;KVM_SET_ONE_REG&lt;/code&gt; interface and reconstructed&lt;br&gt;
the register &lt;code&gt;id&lt;/code&gt; encoding in Rust.&lt;/p&gt;
&lt;p&gt;At that point the hypervisor compiled successfully, but trying to access even&lt;br&gt;
the program counter resulted in an &lt;code&gt;ENOEXEC&lt;/code&gt; error. In this article we'll&lt;br&gt;
continue the reverse-engineering process, initialize the vCPU correctly, and&lt;br&gt;
finally execute our first ARM64 guest.&lt;/p&gt;
&lt;h3 id=&quot;second-roadblock-enoexec-when-getting-setting-pc&quot;&gt;Second roadblock: ENOEXEC when getting / setting PC&lt;/h3&gt;
&lt;p&gt;I was rather optimistic this would work out of the box, but even after trying&lt;br&gt;
to do something as simple as reading the &lt;code&gt;PC&lt;/code&gt; register I hit a snag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(5&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG, 0x7fdf2c81e8) = -1 ENOEXEC (Exec format error)
Error: Os { code: 8, kind: Uncategorized, message: &quot;Exec format error&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luckily, I had a concrete lead now: error code and KVM command. So I searched&lt;br&gt;
for the &lt;code&gt;KVM_GET_ONE_REG&lt;/code&gt; in the Linux kernel code on Bootlin Elixir and found&lt;br&gt;
the following code in &lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/arm.c#L1298&quot;&gt;arch/arm64/kvm/arm.c&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;case KVM_SET_ONE_REG:
case KVM_GET_ONE_REG: {
struct kvm_one_reg reg;

r = -ENOEXEC;
if (unlikely(!kvm_vcpu_initialized(vcpu)))
break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is exactly the error we were getting and we immediately found what was&lt;br&gt;
missing: vCPU initialization. Actually, if we take a closer look at the code we&lt;br&gt;
will see the comment kernel developers have left us:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int kvm_arch_vcpu_create(struct kvm_vcpu *vcpu)
{
int err;

/* Force users to call KVM_ARM_VCPU_INIT */
vcpu-&amp;gt;arch.target = -1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;initializing-the-vcpu&quot;&gt;Initializing the vCPU&lt;/h4&gt;
&lt;p&gt;Following the lead I looked for &lt;code&gt;KVM_ARM_VCPU_INIT&lt;/code&gt; in the original &lt;code&gt;strace&lt;/code&gt; log&lt;br&gt;
and found:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4020aeae /* KVM_ARM_VCPU_INIT */, 0x7fff57e028) = 0
137156 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4020aeae /* KVM_ARM_VCPU_INIT */, 0x7f82f70e68) = 0
137152 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4020aeae /* KVM_ARM_VCPU_INIT */, 0x7fff57dd78) = 0
137152 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4020aeae /* KVM_ARM_VCPU_INIT */, 0x7fff57e048) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This wasn't really helpful: we again only see addresses, but we see no hints of&lt;br&gt;
underlying structure. So let's first take a look at how &lt;code&gt;KVM_ARM_VCPU_INIT&lt;/code&gt; is&lt;br&gt;
defined in the Linux headers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ grep -Rn KVM_ARM_VCPU_INIT /usr/include/
/usr/include/linux/kvm.h:1622:#define KVM_ARM_VCPU_INIT   _IOW(KVMIO,  0xae, struct kvm_vcpu_init)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A quick search for &lt;code&gt;struct kvm_vcpu_init&lt;/code&gt; revealed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ grep -Rn 'struct kvm_vcpu_init' /usr/include/ -A3
/usr/include/aarch64-linux-gnu/asm/kvm.h:112:struct kvm_vcpu_init {
/usr/include/aarch64-linux-gnu/asm/kvm.h-113-   __u32 target;
/usr/include/aarch64-linux-gnu/asm/kvm.h-114-   __u32 features[7];
/usr/include/aarch64-linux-gnu/asm/kvm.h-115-};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;getting-struct-data-with-python-gdb&quot;&gt;Getting struct data with Python GDB&lt;/h4&gt;
&lt;p&gt;I didn't want to spend much time understanding every target and feature flag. My&lt;br&gt;
goal was simply to reproduce QEMU's behavior as quickly as possible (and I&lt;br&gt;
didn't even have to understand it completely). Luckily, this structure was&lt;br&gt;
really simple (basically an array of eight &lt;code&gt;32&lt;/code&gt;-bit integers), so I decided to&lt;br&gt;
tackle this with Python GDB automation. I created a breakpoint to stop exactly&lt;br&gt;
on &lt;code&gt;ioctl&lt;/code&gt; call when we are in &lt;code&gt;KVM_ARM_VCPU_INIT&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;import gdb
import struct

def log(line):
    print(line)
    with open(&quot;gdb-debug.out&quot;, &quot;a&quot;) as f:
        f.write(line + &quot;\n&quot;)

class IoctlBreakpoint(gdb.Breakpoint):

    def stop(self):
        if x1 == 0x4020aeae: # KVM_ARM_VCPU_INIT
            ptr = int(gdb.parse_and_eval(&quot;$x2&quot;))
            vcpu_init_mem = gdb.selected_inferior().read_memory(ptr, 32) # u32 + 7 * u32
            target_features = struct.unpack(&quot;&amp;lt;8I&quot;, bytes(vcpu_init_mem))
            log(f&quot;KVM_ARM_VCPU_INIT: target={target_features[0]} features={target_features[1:]}&quot;)        
            return False

if __name__ == &quot;__main__&quot;:
    tracer = IoctlBreakpoint(&quot;ioctl&quot;)
    gdb.execute(&quot;handle SIGUSR1 SIGUSR2 nostop noprint pass&quot;)
    gdb.execute(&quot;run&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ran my QEMU command with GDB:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gdb -ex 'source ./trace-arm.py' ./start-qemu.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This has automatically yielded great results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;KVM_ARM_VCPU_INIT: target=5 features=(0, 0, 0, 0, 0, 0, 0)
KVM_ARM_VCPU_INIT: target=5 features=(12, 0, 0, 0, 0, 0, 0)
KVM_ARM_VCPU_INIT: target=5 features=(12, 0, 0, 0, 0, 0, 0)
KVM_ARM_VCPU_INIT: target=5 features=(12, 0, 0, 0, 0, 0, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For now I want to try just setting &lt;code&gt;target&lt;/code&gt;, but leave &lt;code&gt;features&lt;/code&gt; zeroed out. I&lt;br&gt;
could immediately reconstruct this in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;impl VCPU {
    pub fn arm_vcpu_init(&amp;amp;self) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        let mut vcpu_init : kvm_vcpu_init = unsafe { std::mem::zeroed() };
        // KVM_ARM_VCPU_INIT: target=5 features=(0, 0, 0, 0, 0, 0, 0)
        vcpu_init.target = 5;
        let ret = unsafe { libc::ioctl(self.fd, KVM_ARM_VCPU_INIT, &amp;amp;mut vcpu_init) };
        if ret &amp;lt; 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;third-roadblock-no-kvm-exit&quot;&gt;Third roadblock: No KVM exit&lt;/h3&gt;
&lt;p&gt;Soon after this I was able to get and set registers properly. The first ARM test&lt;br&gt;
binary I compiled was based on working UART code from my hobby operating system&lt;br&gt;
I started on Raspberry Pi. I expected at least a fault exit, if not an MMIO exit&lt;br&gt;
when UART code was called. However, &lt;code&gt;KVM_RUN&lt;/code&gt; never returned.&lt;/p&gt;
&lt;p&gt;This is where I was stuck for a while. I considered creating a separate thread&lt;br&gt;
that repeatedly queried &lt;code&gt;KVM_GET_ONE_REG&lt;/code&gt; for &lt;code&gt;PC&lt;/code&gt;, but from my own working&lt;br&gt;
experience I knew that calling KVM &lt;code&gt;ioctl&lt;/code&gt; calls asynchronously was a really bad&lt;br&gt;
idea. I also considered adding hardware breakpoint capabilities to at least&lt;br&gt;
trigger &lt;code&gt;KVM_EXIT_DEBUG&lt;/code&gt;, but this would require more infrastructure for just&lt;br&gt;
checking simple binary.&lt;/p&gt;
&lt;h4 id=&quot;writing-a-simple-binary&quot;&gt;Writing a simple binary&lt;/h4&gt;
&lt;p&gt;I decided to try adding a signal handler instead. If I interrupted &lt;code&gt;KVM_RUN&lt;/code&gt;&lt;br&gt;
call with &lt;code&gt;SIGINT&lt;/code&gt; maybe I could get registers then synchronously. I lowered&lt;br&gt;
my expectations for initial binary and just created something really simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-asm&quot;&gt;.section .text

.macro curr_el_to reg
mrs\reg, CurrentEL
lsr\reg, \reg, #2
and\reg, \reg, #0xf
.endm

.globl _start
_start:
mrsx0, mpidr_el1
andx0, x0, #0xFF
cbzx0, control
b .

control:
curr_el_to x0
b .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It looks like much, but this code will just load current CPU privilege level to&lt;br&gt;
&lt;code&gt;x0&lt;/code&gt; register (being that level &lt;code&gt;EL=1&lt;/code&gt; is operating-system level, &lt;code&gt;x0&lt;/code&gt; should be&lt;br&gt;
set to &lt;code&gt;0x1&lt;/code&gt;). The &lt;code&gt;mpidr_el1&lt;/code&gt; code is for checking if we are on control CPU&lt;br&gt;
(this should be unnecessary here, but I added this out of paranoia caused by&lt;br&gt;
real-life issues I had on ARM).&lt;/p&gt;
&lt;h4 id=&quot;installing-signal-handler&quot;&gt;Installing signal handler&lt;/h4&gt;
&lt;p&gt;Finally, I added a signal handler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;extern &quot;C&quot; fn handler(_sig: libc::c_int) { }

unsafe fn install_interrupt_signal() {
    let mut sa: libc::sigaction = std::mem::zeroed();
    sa.sa_sigaction = handler as *const() as usize;
    libc::sigemptyset(&amp;amp;mut sa.sa_mask);
    sa.sa_flags = 0;
    libc::sigaction(libc::SIGINT, &amp;amp;sa, std::ptr::null_mut());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler itself does nothing. I only needed the signal to interrupt the blocking &lt;code&gt;KVM_RUN&lt;/code&gt; call. So my final KVM loop looked like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;    unsafe { install_interrupt_signal() };

    let run = vcpu.kvm_run_mem as *mut kvm_run;

    loop {
        let ret = unsafe { libc::ioctl(vcpu.fd, KVM_RUN, 0usize) };
        if ret &amp;lt; 0 {
            if io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
                vcpu.print_regs()?;
            }
            return Err(io::Error::last_os_error());
        }

        let exit_reason = unsafe { (*run).exit_reason };
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;running-the-binary&quot;&gt;Running the binary&lt;/h3&gt;
&lt;p&gt;Finally, it was time to run the gues:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pi@raspberrypi:~ $ ./rust hello-world-arm.img
x0 = 0x0
pc = 0x1000
Loading &quot;hello-world-arm.img&quot; to 0x1000...
^Cx0 = 0x1
pc = 0x101c
Error: Os { code: 4, kind: Interrupted, message: &quot;Interrupted system call&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see that &lt;code&gt;PC&lt;/code&gt; did change and that &lt;code&gt;x0&lt;/code&gt; really is set to current CPU&lt;br&gt;
privilege level: &lt;code&gt;0x1&lt;/code&gt;. Why it didn't work for my UART code is probably that the&lt;br&gt;
guest itself requires more initialization than I though (x86 went more smoothly&lt;br&gt;
and felt more plug-and-play).&lt;/p&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Although x86 and ARM64 share the same KVM interface at a high level, their&lt;br&gt;
userspace APIs differ significantly. Porting my hypervisor turned out to involve&lt;br&gt;
much more than renaming registers: ARM64 uses per-register &lt;code&gt;id&lt;/code&gt;s instead of a&lt;br&gt;
single register structure, requires explicit vCPU initialization through&lt;br&gt;
&lt;code&gt;KVM_ARM_VCPU_INIT&lt;/code&gt; and demanded a fair amount of reverse engineering before I&lt;br&gt;
could successfully execute guest code. Fortunately, the same workflow that&lt;br&gt;
worked on x86 like combining strace, GDB and the Linux kernel source proved just&lt;br&gt;
as effective on ARM64 (although required a bit more effort).&lt;/p&gt;
&lt;p&gt;The code from this article can be found on my GitHub page:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-arm-part2-code&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-arm-part2-code&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23276/porting-my-rust-kvm-hypervisor-to-arm64-running-the-binary</guid>
<pubDate>Wed, 22 Jul 2026 18:02:25 +0000</pubDate>
</item>
<item>
<title>Porting my Rust KVM hypervisor to ARM64: Working with registers</title>
<link>https://coderlegion.com/22243/porting-my-rust-kvm-hypervisor-to-arm64-working-with-registers</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;This is a follow-up to my KVM in Rust series where I showed how to use &lt;code&gt;strace&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;GDB&lt;/code&gt; to reverse-engineer QEMU/KVM and reimplement pieces of it in my own&lt;br&gt;
hypervisor in Rust. So far this was done on a modern x86 CPU. This time we'll&lt;br&gt;
see how to get KVM running on 64-bit ARM architecture. For this I have used my&lt;br&gt;
Raspberry Pi 4B which has a CPU with hardware virtualization support.&lt;/p&gt;
&lt;h3 id=&quot;cross-compilation&quot;&gt;Cross-compilation&lt;/h3&gt;
&lt;p&gt;The first thing to do was to set up an environment for cross-compilation and for&lt;br&gt;
this kind of thing I almost always use docker. So I have written a simple&lt;br&gt;
&lt;code&gt;Dockerfile&lt;/code&gt; installing the cross-compilation toolchain for 64-bit ARM along&lt;br&gt;
with Rust essentials:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/blob/kvm-arm-code/Dockerfile&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/blob/kvm-arm-code/Dockerfile&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Building this image is straightforward, but it does require passing user and&lt;br&gt;
group id to have the resulting file permissions correct:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;docker build --network host \
       --build-arg GID=$(id -g) \
       --build-arg=UID=$(id -u) \
       -t arm-rust-build .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, to cross-compile just run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;docker run --network host -it --rm \
       -v &quot;$(pwd)&quot;:&quot;/home/docker/kvm-rust&quot; \
       arm-rust-build \
       cargo build --release --target=aarch64-unknown-linux-gnu
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;first-roadblock-api-difference&quot;&gt;First roadblock: API difference&lt;/h3&gt;
&lt;p&gt;At first, I thought the difference would be mostly in CPU registers, so I&lt;br&gt;
removed &lt;code&gt;KVM_GET_SREGS2&lt;/code&gt; and &lt;code&gt;KVM_SET_SREGS2&lt;/code&gt; and some x86-specific code just to&lt;br&gt;
make it compile. However, when I ran it on Raspberry Pi, the program returned an&lt;br&gt;
error on &lt;code&gt;KVM_GET_REGS&lt;/code&gt;. I once again turned to use &lt;code&gt;strace&lt;/code&gt; on QEMU to see what&lt;br&gt;
was to be done. I downloaded the Linux kernel source code (from www.kernel.org),&lt;br&gt;
cross-compiled it for ARM and ran it in QEMU:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

KERNEL=/home/pi/Image
qemu-system-aarch64                                             \
        -M virt                                                 \
        -smp 1                                                  \
        -enable-kvm                                             \
        -cpu host                                               \
        -kernel ${KERNEL}                                       \
        -append &quot;console=ttyAMA0&quot;                               \
        -serial stdio                                           \
        -nographic                                              \
        -nodefaults
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that here I used the &lt;code&gt;virt&lt;/code&gt; machine specifically: that's the option to&lt;br&gt;
use for KVM-based virtualization (machines like &lt;code&gt;raspi4b&lt;/code&gt; would be emulated as&lt;br&gt;
they require a very specific CPU and device tree, so they cannot simply run&lt;br&gt;
directly on the host CPU). I put this into &lt;code&gt;start-qemu.sh&lt;/code&gt; file and then ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;strace -yy -f -X verbose -e trace=ioctl,openat,read,write,mmap -o kvm.log ./start-qemu.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;discovering-kvm-register-api-for-arm64&quot;&gt;Discovering KVM register API for ARM64&lt;/h4&gt;
&lt;p&gt;First thing I did was to try and find &lt;code&gt;KVM_GET_REGS&lt;/code&gt; string, but there were no&lt;br&gt;
results. I tried something more generic like &lt;code&gt;grep -i regs&lt;/code&gt; and got this as&lt;br&gt;
first results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4010aeab /* KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG */, 0x7fff57e008) = 0
137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4010aeab /* KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG */, 0x7fff57e008) = 0
137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4010aeab /* KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG */, 0x7fff57e008) = 0
137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4010aeab /* KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG */, 0x7fff57e008) = 0
137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4010aeab /* KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG */, 0x7fff57e008) = 0
137152 ioctl(12&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4010aeab /* KVM_ARM_SET_DEVICE_ADDR or KVM_GET_ONE_REG */, 0x7fff57e008) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So this is when I realized it was going to be a bit harder. I wasn't getting&lt;br&gt;
names of fields and corresponding values, just addresses. I installed Linux&lt;br&gt;
headers on Raspberry Pi and searched for &lt;code&gt;KVM_[GS]ET_ONE_REG&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -Rn KVM_[GS]ET_ONE_REG /usr/include/linux/
/usr/include/linux/kvm.h:1618:#define KVM_GET_ONE_REG             _IOW(KVMIO,  0xab, struct kvm_one_reg)
/usr/include/linux/kvm.h:1619:#define KVM_SET_ONE_REG             _IOW(KVMIO,  0xac, struct kvm_one_reg)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Searching in the same file we can see that the &lt;code&gt;struct kvm_one_reg&lt;/code&gt; is defined&lt;br&gt;
as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct kvm_one_reg {
       __u64 id;
       __u64 addr;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;constructing-cpu-id&quot;&gt;Constructing CPU id&lt;/h4&gt;
&lt;p&gt;As it usually is with great discoveries, I learned how to construct the register&lt;br&gt;
id in &lt;code&gt;kvm_one_reg&lt;/code&gt; by accident. Before even knowing how KVM on ARM works with&lt;br&gt;
registers, I naively tried searching for &lt;code&gt;struct kvm_regs&lt;/code&gt; and found two&lt;br&gt;
interesting matches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -Rn kvm_regs /usr/include/
/usr/include/aarch64-linux-gnu/asm/kvm.h:50:struct kvm_regs {
/usr/include/aarch64-linux-gnu/asm/kvm.h:208:#define KVM_REG_ARM_CORE_REG(name)(offsetof(struct kvm_regs, name) / sizeof(__u32))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;struct kvm_regs&lt;/code&gt; here was just a list of registers and&lt;br&gt;
&lt;code&gt;KVM_REG_ARM_CORE_REG&lt;/code&gt; macro seemed to be taking a register name as input and&lt;br&gt;
looking for offset of the register in the &lt;code&gt;struct kvm_regs&lt;/code&gt;, but I didn't find&lt;br&gt;
it anywhere in the Linux headers.&lt;/p&gt;
&lt;p&gt;I knew, however, that QEMU was one place where this might be used and I searched&lt;br&gt;
for &lt;code&gt;KVM_REG_ARM_CORE_REG&lt;/code&gt; and found this in &lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.2/source/target/arm/kvm.c#L2080&quot;&gt;target/arm/kvm.c&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define AARCH64_CORE_REG(x)   (KVM_REG_ARM64 | KVM_REG_SIZE_U64 | \
                 KVM_REG_ARM_CORE | KVM_REG_ARM_CORE_REG(x))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What was left to do was just to find these macro definitions and reconstruct&lt;br&gt;
them in my Rust KVM code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const KVM_REG_ARM64 : u64 = 0x6000000000000000;
const KVM_REG_SIZE_U64 : u64 = 0x0030000000000000;
const KVM_REG_ARM_COPROC_SHIFT : u64 = 16;
const KVM_REG_ARM_CORE : u64 = 0x0010 &amp;lt;&amp;lt; KVM_REG_ARM_COPROC_SHIFT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, the &lt;code&gt;KVM_REG_ARM_CORE_REG&lt;/code&gt; needed to be more thought-through as it&lt;br&gt;
worked with offsets in the &lt;code&gt;struct kvm_regs&lt;/code&gt;. So I have written a function&lt;br&gt;
taking the register name as string and mapping it to the register &lt;code&gt;id&lt;/code&gt; that KVM&lt;br&gt;
understands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;fn AARCH64_CORE_REG(name: &amp;amp;str) -&amp;gt; io::Result&amp;lt;u64&amp;gt; {
    let base = KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE;
    if name.starts_with(&quot;x&quot;) {
        let reg : u64 = name[1..]
            .parse()
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
        return Ok(base | reg * 2); }

    match name {
        &quot;sp&quot; =&amp;gt; { return Ok(base | (31 * 2)); },
        &quot;pc&quot; =&amp;gt; { return Ok(base | (32 * 2)); },
        &quot;pstate&quot; =&amp;gt; { return Ok(base | (33 * 2)); },
        _ =&amp;gt; ()
    };

    Err(io::Error::other(&quot;Invalid register.&quot;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my implementation I have improved this further by utilizing &lt;code&gt;HashMap&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;LazyLock&lt;/code&gt; so that the parsing and calculation isn't done on-the fly but rather&lt;br&gt;
computed once and cached for subsequent calls.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;At this point I understood how ARM64 exposes CPU registers through&lt;br&gt;
&lt;code&gt;KVM_GET_ONE_REG&lt;/code&gt; and &lt;code&gt;KVM_SET_ONE_REG&lt;/code&gt; calls and had reproduced the register&lt;br&gt;
&lt;code&gt;id&lt;/code&gt; encoding in Rust. Unfortunately, that wasn't enough to make the hypervisor&lt;br&gt;
work. My first attempt at accessing the program counter (&lt;code&gt;PC&lt;/code&gt; register)&lt;br&gt;
immediately failed with an &lt;code&gt;ENOEXEC&lt;/code&gt; error: a clue that I was missing another&lt;br&gt;
crucial step in KVM ARM setup.&lt;/p&gt;
&lt;p&gt;The code showcasing creation of register &lt;code&gt;id&lt;/code&gt;s in a &lt;code&gt;HashMap&lt;/code&gt; and subsequent&lt;br&gt;
error when calling &lt;code&gt;KVM_SET_ONE_REG&lt;/code&gt; can be found on my GitHub page:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-arm-part1-code&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-arm-part1-code&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22243/porting-my-rust-kvm-hypervisor-to-arm64-working-with-registers</guid>
<pubDate>Fri, 10 Jul 2026 17:44:14 +0000</pubDate>
</item>
<item>
<title>Building a KVM Virtual Machine in Rust: Running a binary</title>
<link>https://coderlegion.com/21481/building-a-kvm-virtual-machine-in-rust-running-a-binary</link>
<description>&lt;h3 id=&quot;recap-2&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;This is a continuation of &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-part2&quot;&gt;Part 2&lt;/a&gt; of KVM in Rust series,&lt;br&gt;
where we successfully set up a memory region for our guest VM. Now we will see&lt;br&gt;
how to proceed further and run a very simple binary in our hypervisor.&lt;/p&gt;
&lt;h3 id=&quot;loading-the-binary&quot;&gt;Loading the binary&lt;/h3&gt;
&lt;p&gt;Now, we have only set up a memory region, but we still haven't loaded any binary&lt;br&gt;
or code to actually run. For this, I have written a small &quot;Hello world&quot; x86&lt;br&gt;
assembly file: &lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/blob/kvm-part3-code/samples/hello-world.asm&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/blob/kvm-part3-code/samples/hello-world.asm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Notice that the &lt;code&gt;org 0x1000&lt;/code&gt; header is quite arbitrary and it's actually a good&lt;br&gt;
lead of what we also need to do (set the instruction pointer). Let's compile the&lt;br&gt;
assembly file with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;nasm ./samples/hello-world.asm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, we want to load our binary into the memory region at address &lt;code&gt;0x1000&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut file = File::open(&quot;./samples/hello-world&quot;)?;
let mut code = Vec::new();
file.read_to_end(&amp;amp;mut code)?;

unsafe {
std::ptr::copy_nonoverlapping(code.as_ptr(),
  (mem_ptr as *mut u8).add(0x1000),
  code.len()) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are using the more optimal &lt;code&gt;copy_nonoverlapping&lt;/code&gt; because we can be quite&lt;br&gt;
certain that the memory where &lt;code&gt;code&lt;/code&gt; is stored is not overlapping with our&lt;br&gt;
memory region.&lt;/p&gt;
&lt;h3 id=&quot;creating-a-vcpu&quot;&gt;Creating a vCPU&lt;/h3&gt;
&lt;p&gt;As we saw in &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-part1&quot;&gt;Part 1&lt;/a&gt;, we have also discovered that QEMU also&lt;br&gt;
calls &lt;code&gt;KVM_SET_VCPU&lt;/code&gt; to obtain a vCPU file descriptor on which it will call&lt;br&gt;
&lt;code&gt;KVM_RUN&lt;/code&gt;. So, all that we have to do to set up a vCPU is to take a look at the&lt;br&gt;
&lt;code&gt;ioctl&lt;/code&gt; and recreate it in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// 140904 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0xae41 /* KVM_CREATE_VCPU */, 0) = 10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;
let vcpu_fd = unsafe { libc::ioctl(self.fd, KVM_CREATE_VCPU, 0usize) };
if vcpu_fd &amp;lt; 0 {
return Err(io::Error::last_os_error());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;investigating-vcpu-operations&quot;&gt;Investigating vCPU operations&lt;/h4&gt;
&lt;p&gt;It would be tempting to simply call &lt;code&gt;KVM_RUN&lt;/code&gt; on this file descriptor, however,&lt;br&gt;
we don't really know how vCPU is set up, where the instruction pointer is and&lt;br&gt;
even if the registers are in the valid state. If we look at the &lt;code&gt;ioctl&lt;/code&gt; calls&lt;br&gt;
referring to &lt;code&gt;kvm-vcpu:0&lt;/code&gt; in our log (and filtering out the spamming &lt;code&gt;KVM_RUN&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;KVM_SMI&lt;/code&gt;), we can see a lot of lines like these being repeated:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ grep 'kvm-vcpu:0' kvm.log | grep -v 'RUN\|SMI'

                ### omitted a lot of repetitive strace output ###

140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xc080aebe /* KVM_GET_NESTED_STATE */, 0x7768f8002010) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xc028ae92 /* KVM_TPR_ACCESS_REPORTING */, 0x77690198f090) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4140aecd /* KVM_SET_SREGS2 */, 0x77690198f2f0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4090ae82 /* KVM_SET_REGS */, {rax=0x20, ..., rsp=0x6d88, rbp=0, ..., rip=0x18, rflags=0x246}) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x5000aea5 /* KVM_SET_XSAVE */, 0x7768f8001000) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4188aea7 /* KVM_SET_XCRS */, 0x77690198f2c0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4008ae89 /* KVM_SET_MSRS */, 0x7768f80040a0) = 59
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4040aea0 /* KVM_SET_VCPU_EVENTS */, 0x77690198f500) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4008ae89 /* KVM_SET_MSRS */, 0x7768f80040a0) = 1
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4080aea2 /* KVM_SET_DEBUGREGS */, 0x77690198f500) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xc008ae88 /* KVM_GET_MSRS */, 0x77690198f5a0) = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we see that QEMU is setting vCPU registers via &lt;code&gt;KVM_SET_REGS&lt;/code&gt; and in this log&lt;br&gt;
we can see their exact values. It's also calling &lt;code&gt;KVM_SET_SREGS2&lt;/code&gt; which is&lt;br&gt;
setting system registers; unfortunately, we cannot see the values used here.&lt;br&gt;
Also, we do not need to follow the whole logic here, we just want to see what&lt;br&gt;
QEMU is doing with registers before calling &lt;code&gt;KVM_RUN&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ awk '/KVM_RUN/ {print; exit} {print}' kvm.log | grep '[GS]ET_[S]\?REGS\|KVM_RUN'
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4140aecd /* KVM_SET_SREGS2 */, 0x77690198f310) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4090ae82 /* KVM_SET_REGS */, {rax=0, ..., rsp=0, rbp=0, ..., rip=0xfff0, rflags=0x2}) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x8090ae81 /* KVM_GET_REGS */, {rax=0, ..., rsp=0, rbp=0, ..., rip=0xfff0, rflags=0x2}) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x8140aecc /* KVM_GET_SREGS2 */, 0x77690198f310) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4140aecd /* KVM_SET_SREGS2 */, 0x77690198f310) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4090ae82 /* KVM_SET_REGS */, {rax=0, ..., rsp=0, rbp=0, ..., rip=0xfff0, rflags=0x2}) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xae80 /* KVM_RUN */, 0) = -1 EINTR (Interrupted system call)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since QEMU first calls &lt;code&gt;SET_SREGS2&lt;/code&gt; and &lt;code&gt;SET_REGS&lt;/code&gt; we should actually&lt;br&gt;
investigate what the default (if any) values are in our own implementation.&lt;/p&gt;
&lt;h4 id=&quot;implementing-register-get-set-operations-in-rust&quot;&gt;Implementing register get/set operations in Rust&lt;/h4&gt;
&lt;p&gt;The procedure for this is as with all other KVM &lt;code&gt;ioctl&lt;/code&gt; calls we implemented.&lt;br&gt;
We find out how to construct their numbers (consult the previous article here)&lt;br&gt;
and reimplement these constants in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const KVM_GET_SREGS2 : u64 = _IOR::&amp;lt;kvm_sregs2&amp;gt;(KVMIO, 0xcc);
const KVM_SET_SREGS2 : u64 = _IOW::&amp;lt;kvm_sregs2&amp;gt;(KVMIO, 0xcd);
const KVM_GET_REGS : u64 = _IOR::&amp;lt;kvm_regs&amp;gt;(KVMIO, 0x81);
const KVM_SET_REGS : u64 = _IOW::&amp;lt;kvm_regs&amp;gt;(KVMIO, 0x82);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We also need to add &lt;code&gt;kvm_sregs2&lt;/code&gt; and &lt;code&gt;kvm_regs&lt;/code&gt; in our &lt;code&gt;build.rs&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;    bindgen::Builder::default()
        .header(&quot;/usr/include/linux/kvm.h&quot;)
        .allowlist_type(&quot;kvm_sregs2&quot;)
        .allowlist_type(&quot;kvm_userspace_memory_region&quot;)
        .allowlist_type(&quot;kvm_regs&quot;)
        .generate_comments(false)
        .generate()?
        .write_to_file(out_path.join(&quot;kvm-bindings.rs&quot;))?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, &lt;code&gt;ioctl&lt;/code&gt; calls are as follows (I will only give example for &lt;code&gt;get_sregs2&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;get_regs&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut sregs2 = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::ioctl(self.fd, KVM_GET_SREGS2, &amp;amp;mut sregs2) };

let mut regs = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::ioctl(self.fd, KVM_GET_REGS, &amp;amp;mut regs) };
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;peeking-at-registers&quot;&gt;Peeking at registers&lt;/h4&gt;
&lt;p&gt;After obtaining &lt;code&gt;sregs2&lt;/code&gt; and &lt;code&gt;regs&lt;/code&gt; we can print them out and see what they are&lt;br&gt;
set to by default (to accomplish this we consult the &lt;code&gt;struct&lt;/code&gt; definition in our&lt;br&gt;
&lt;code&gt;kvm-bindings.rs&lt;/code&gt; (or in the Linux headers) and print out all the values of the&lt;br&gt;
fields we find). For &lt;code&gt;regs&lt;/code&gt; we see they are pre-set to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RAX=0x0 RBX=0x0 RCX=0x0 RDX=0x600
RSI=0x0 RDI=0x0 RSP=0x0 RBP=0x0
R8=0x0  R9=0x0  R10=0x0 R11=0x0
R12=0x0 R13=0x0 R14=0x0 R15=0x0
RIP=0xfff0      RFLAGS=0x2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Taking a look at our trace from &lt;code&gt;kvm.log&lt;/code&gt; we can see that the ones QEMU sets are&lt;br&gt;
exactly the same (except for &lt;code&gt;RDX&lt;/code&gt; but we can get away with this one).&lt;/p&gt;
&lt;h4 id=&quot;peeking-at-system-registers&quot;&gt;Peeking at system registers&lt;/h4&gt;
&lt;p&gt;Now, for &lt;code&gt;sregs2&lt;/code&gt; it gets a bit more complicated. Our defaults are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CS      base=0xffff0000 selector=0xf000 limit=0xffff    type=0xb        present=0x1
        dpl=0x0         db=0x0          s=0x1   l=0x0   g=0x0           avl=0x0

DS      base=0x0        selector=0x0    limit=0xffff    type=0x3        present=0x1
        dpl=0x0         db=0x0          s=0x1   l=0x0   g=0x0           avl=0x0

ES      base=0x0        selector=0x0    limit=0xffff    type=0x3        present=0x1
        dpl=0x0         db=0x0          s=0x1   l=0x0   g=0x0           avl=0x0

FS      base=0x0        selector=0x0    limit=0xffff    type=0x3        present=0x1
        dpl=0x0         db=0x0          s=0x1   l=0x0   g=0x0           avl=0x0

GS      base=0x0        selector=0x0    limit=0xffff    type=0x3        present=0x1
        dpl=0x0         db=0x0          s=0x1   l=0x0   g=0x0           avl=0x0

SS      base=0x0        selector=0x0    limit=0xffff    type=0x3        present=0x1
        dpl=0x0         db=0x0          s=0x1   l=0x0   g=0x0           avl=0x0

TR      base=0x0        selector=0x0    limit=0xffff    type=0xb        present=0x1
        dpl=0x0         db=0x0          s=0x0   l=0x0   g=0x0           avl=0x0

LDT     base=0x0        selector=0x0    limit=0xffff    type=0x2        present=0x1
        dpl=0x0         db=0x0          s=0x0   l=0x0   g=0x0           avl=0x0

GDT     base=0x0        limit=0xffff

IDT     base=0x0        limit=0xffff

CR0=0x60000010          CR2=0x0         CR3=0x0         CR4=0x0         CR8=0x0

EFER=0x0                APIC_BASE=0xfee00900            FLAGS=0x0

PDPTRS[0]=0x0           PDPTRS[1]=0x0           PDPTRS[2]=0x0           PDPTRS[3]=0x0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Problem is that we don't see any values in our &lt;code&gt;strace&lt;/code&gt; log, only the address of&lt;br&gt;
the variable. So we need to be a bit creative here if we want to find out what &lt;br&gt;
QEMU sets these values to. We could use &lt;code&gt;ptrace&lt;/code&gt; (in fact &lt;code&gt;strace&lt;/code&gt; is built upon&lt;br&gt;
&lt;code&gt;ptrace&lt;/code&gt; API), but it may be a bit too much. Same for &lt;code&gt;uprobes&lt;/code&gt; and &lt;code&gt;eBPF&lt;/code&gt;. We&lt;br&gt;
do have GDB, though, and it's just perfect as a one-off thing here. All we have&lt;br&gt;
to do is run QEMU under GDB and then execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(gdb) break ioctl if $rsi == 0x4140aecd
(gdb) run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;0x4140aecd&lt;/code&gt; is the exact value that we extracted from &lt;code&gt;SET_SREGS2&lt;/code&gt;&lt;br&gt;
&lt;code&gt;ioctl&lt;/code&gt; call (in x86 ABI, &lt;code&gt;RSI&lt;/code&gt; is holding the value of second argument):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0x4140aecd /* KVM_SET_SREGS2 */, 0x77690198f310) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once GDB breaks, we can dump memory. However, we don't know the exact size of&lt;br&gt;
&lt;code&gt;struct kvm_sregs2&lt;/code&gt;; we could guess or manually inspect, but quickest way to get&lt;br&gt;
it is to actually have a small C program:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;linux/kvm.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

int main() {
  printf(&quot;%lu&quot;, sizeof(struct kvm_sregs2));
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The program will return value of &lt;code&gt;320&lt;/code&gt; (at least on my system) and we can then&lt;br&gt;
use this in GDB:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(gdb) dump memory dump.bin $rdx $rdx+320
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;RDX&lt;/code&gt; is holding the third argument, i.e. the address we actually saw&lt;br&gt;
in our &lt;code&gt;strace&lt;/code&gt; log. Now, we can reuse our function for printing &lt;code&gt;sregs2&lt;/code&gt; in&lt;br&gt;
Rust. We simply hack our main function to load the file and reinterpret its data&lt;br&gt;
as &lt;code&gt;sregs2&lt;/code&gt; and then print it. We got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CSbase=0xffff0000selector=0xf000limit=0xfffftype=0xbpresent=0x1
dpl=0x0db=0x0s=0x1l=0x0g=0x0avl=0x0

DSbase=0x0selector=0x0limit=0xfffftype=0x3present=0x1
dpl=0x0db=0x0s=0x1l=0x0g=0x0avl=0x0

ESbase=0x0selector=0x0limit=0xfffftype=0x3present=0x1
dpl=0x0db=0x0s=0x1l=0x0g=0x0avl=0x0

FSbase=0x0selector=0x0limit=0xfffftype=0x3present=0x1
dpl=0x0db=0x0s=0x1l=0x0g=0x0avl=0x0

GSbase=0x0selector=0x0limit=0xfffftype=0x3present=0x1
dpl=0x0db=0x0s=0x1l=0x0g=0x0avl=0x0

SSbase=0x0selector=0x0limit=0xfffftype=0x3present=0x1
dpl=0x0db=0x0s=0x1l=0x0g=0x0avl=0x0

TRbase=0x0selector=0x0limit=0xfffftype=0xbpresent=0x1
dpl=0x0db=0x0s=0x0l=0x0g=0x0avl=0x0

LDTbase=0x0selector=0x0limit=0xfffftype=0x2present=0x1
dpl=0x0db=0x0s=0x0l=0x0g=0x0avl=0x0

GDTbase=0x0limit=0xffff

IDTbase=0x0limit=0xffff

CR0=0x60000010CR2=0x0CR3=0x0CR4=0x0CR8=0x0

EFER=0x0APIC_BASE=0xfee00900FLAGS=0x0

PDPTRS[0]=0x0PDPTRS[1]=0x4b275f5fce32f200PDPTRS[2]=0x0PDPTRS[3]=0x3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see that this more or less corresponds to the defaults we observed in our&lt;br&gt;
KVM implementation. If we want to change our registers (which we will), then we&lt;br&gt;
will always first get the ones that are current in the vCPU, change the ones we&lt;br&gt;
need and then set them using &lt;code&gt;KVM_SET_REGS&lt;/code&gt; (or &lt;code&gt;KVM_SET_SREGS2&lt;/code&gt;). So one of&lt;br&gt;
first things to do is to set &lt;code&gt;CS&lt;/code&gt; to flat mapping:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut sregs2 = vcpu.get_sregs2()?;
sregs2.cs.base = 0;
sregs2.cs.selector = 0;
vcpu.set_sregs2(sregs2)?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recall that our assembly file was assembled with &lt;code&gt;org 0x1000&lt;/code&gt;, so setting RIP to&lt;br&gt;
&lt;code&gt;0x1000&lt;/code&gt; causes execution to begin at the start of the loaded binary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut regs = vcpu.get_regs()?;
regs.rip = 0x1000;
vcpu.set_regs(regs)?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Furthermore, we now have a very good process of finding out what QEMU does with&lt;br&gt;
registers.&lt;/p&gt;
&lt;h3 id=&quot;running-the-binary-1&quot;&gt;Running the binary&lt;/h3&gt;
&lt;p&gt;Now this is where &lt;code&gt;strace&lt;/code&gt; stopped being useful and my understanding of the KVM&lt;br&gt;
run loop became more useful. Another positive thing is that this part of QEMU&lt;br&gt;
source code is quite readable and can be found in function &lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-accel-ops.c&quot;&gt;kvm_vcpu_thread_fn&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The more x86-specific code with exit reasons can be found in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.1/source/target/i386/kvm/kvm.c&quot;&gt;kvm_arch_handle_exit&lt;/a&gt; function.&lt;/p&gt;
&lt;h4 id=&quot;setting-kvm-run-shared-memory-region&quot;&gt;Setting kvm_run shared memory region&lt;/h4&gt;
&lt;p&gt;To wrap things up, first we need to mmap the shared kvm_run structure that KVM&lt;br&gt;
uses to communicate VM-exit information and other runtime state between the&lt;br&gt;
kernel and userspace:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;// 140904 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae04 /* KVM_GET_VCPU_MMAP_SIZE */, 0) = 12288
let kvm_run_size = unsafe {
libc::ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0usize) };
let kvm_run_mem = unsafe {
libc::mmap(ptr::null_mut(), kvm_run_size as usize,
   libc::PROT_READ|libc::PROT_WRITE, libc::MAP_SHARED,
   vcpu_fd, 0) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, when we call &lt;code&gt;KVM_RUN&lt;/code&gt; we simply match the exit reasons and act&lt;br&gt;
accordingly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let run = vcpu.kvm_run_mem as *mut kvm_run;

loop {
    let ret = unsafe { libc::ioctl(vcpu.fd, KVM_RUN, 0usize) };
    if ret &amp;lt; 0 {
        return Err(io::Error::last_os_error());
    }

    let exit_reason = unsafe { (*run).exit_reason };

    match exit_reason {
        KVM_EXIT_MMIO =&amp;gt; { /* omitted */ }
        KVM_EXIT_HLT =&amp;gt; {
            println!(&quot;Guest halted.&quot;);
            break; }
        KVM_EXIT_SHUTDOWN =&amp;gt; {
            println!(&quot;Guest shutdown.&quot;);
            break; }
        KVM_EXIT_INTERNAL_ERROR =&amp;gt; {
            return Err(io::Error::other(&quot;KVM internal error.&quot;)); }
        _ =&amp;gt; {
            println!(&quot;EXIT REASON = {}&quot;, exit_reason);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;our-first-output&quot;&gt;Our first output&lt;/h4&gt;
&lt;p&gt;Being that our assembly file uses VGA MMIO to output the &quot;Hello world!&quot; string,&lt;br&gt;
I have only implemented &lt;code&gt;KVM_EXIT_MMIO&lt;/code&gt;. Then we can finally see the output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ cargo run ./samples/hello-world
     ## ommitted sregs2 output and warnings ##
RAX=0x0 RBX=0x0 RCX=0x0 RDX=0x600
RSI=0x0 RDI=0x0 RSP=0x0 RBP=0x0
R8=0x0  R9=0x0  R10=0x0 R11=0x0
R12=0x0 R13=0x0 R14=0x0 R15=0x0
RIP=0x1000      RFLAGS=0x2
Hello from KVM!
Guest halted.
RAX=0xb800      RBX=0x0 RCX=0x0 RDX=0x600
RSI=0x102d      RDI=0x22        RSP=0x0 RBP=0x0
R8=0x0  R9=0x0  R10=0x0 R11=0x0
R12=0x0 R13=0x0 R14=0x0 R15=0x0
RIP=0x101b      RFLAGS=0x46
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This concludes the three-part series on figuring out KVM via &lt;code&gt;strace&lt;/code&gt; and&lt;br&gt;
reimplementing it in Rust. A lot of next steps actually come down to x86&lt;br&gt;
architecture and bootloader specifics, so we may leave it here for now. The full&lt;br&gt;
code capable of running simple binaries can be found on GitHub:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-part3-code&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-part3-code&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21481/building-a-kvm-virtual-machine-in-rust-running-a-binary</guid>
<pubDate>Sun, 28 Jun 2026 05:24:29 +0000</pubDate>
</item>
<item>
<title>Building a KVM Virtual Machine in Rust: Memory Setup</title>
<link>https://coderlegion.com/20611/building-a-kvm-virtual-machine-in-rust-memory-setup</link>
<description>&lt;h3 id=&quot;recap-3&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;This is a continuation of my &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-part1&quot;&gt;previous article&lt;/a&gt; which dealt&lt;br&gt;
with reverse-engineering QEMU with &lt;code&gt;strace&lt;/code&gt; to learn how KVM works. Now it's&lt;br&gt;
time to try and follow the steps we got from the &lt;code&gt;strace&lt;/code&gt; logs to build our own&lt;br&gt;
KVM-based virtual machine in Rust.&lt;/p&gt;
&lt;h3 id=&quot;kvm-headers&quot;&gt;KVM Headers&lt;/h3&gt;
&lt;p&gt;I haven't actually used existing KVM libraries written specifically for Rust but&lt;br&gt;
opted to use the &lt;code&gt;libc&lt;/code&gt; crate which provides the required &lt;code&gt;ioctl&lt;/code&gt; bindings and&lt;br&gt;
helper macros. The main reason is that I want a complete understanding of what&lt;br&gt;
is happening under the hood. Now for each of these KVM &lt;code&gt;ioctl&lt;/code&gt; calls we can use&lt;br&gt;
the Linux headers for reference. For example, to find out how to construct&lt;br&gt;
&lt;code&gt;ioctl&lt;/code&gt; number for &lt;code&gt;KVM_CREATE_VM&lt;/code&gt; we simply can do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ grep -Rn 'KVM_CREATE_VM' /usr/include/linux/
/usr/include/linux/kvm.h:855:/* machine type bits, to be used as argument to KVM_CREATE_VM */
/usr/include/linux/kvm.h:882:#define KVM_CREATE_VM             _IO(KVMIO,   0x01) /* returns a VM fd */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Luckily in &lt;code&gt;libc&lt;/code&gt; Rust crate we have macros for &lt;code&gt;_IO&lt;/code&gt; (and the like), but we&lt;br&gt;
still need &lt;code&gt;KVMIO&lt;/code&gt; macro:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ grep -Rn 'define KVMIO' /usr/include/linux/
/usr/include/linux/kvm.h:853:#define KVMIO 0xAE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can now construct the &lt;code&gt;ioctl&lt;/code&gt; number for &lt;code&gt;KVM_CREATE_VM&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use libc::{_IOW, _IO, _IOR};

const KVMIO : u32 = 0xae;
const KVM_CREATE_VM : u64 = _IO(KVMIO, 0x01);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that exact integer type depends on the platform and &lt;code&gt;libc&lt;/code&gt; definitions.&lt;br&gt;
Then, in the main we can do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(&quot;/dev/kvm&quot;)
        .expect(&quot;failed to open /dev/kvm&quot;);

    let fd = file.as_raw_fd();
    let vm_fd = unsafe { libc::ioctl(fd, KVM_CREATE_VM, 0usize) };
Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the procedure we will follow for each relevant &lt;code&gt;ioctl&lt;/code&gt; call. In fact,&lt;br&gt;
we can &quot;reverse-engineer&quot; our own program and then compare it with the original,&lt;br&gt;
to make sure we are doing the right thing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ strace cargo run

     ### omitted irrelevant strace output ###

openat(AT_FDCWD, &quot;/dev/kvm&quot;, O_RDWR|O_CLOEXEC) = 3
ioctl(3, KVM_CREATE_VM, 0)              = 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;important-note&quot;&gt;Important note&lt;/h4&gt;
&lt;p&gt;In production code we would immediately check for a negative return value and&lt;br&gt;
convert errno into a Rust error. To keep the example focused, I am omitting&lt;br&gt;
proper error handling in this article.&lt;/p&gt;
&lt;h3 id=&quot;setting-memory-region&quot;&gt;Setting memory region&lt;/h3&gt;
&lt;p&gt;Now, to recall, the next step is setting up the memory region used by both the&lt;br&gt;
KVM guest and the host. This region will be the memory of our virtual machine, a&lt;br&gt;
place where we will load our binary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140900 mmap(NULL, 1075838976, 0 /* PROT_NONE */, 0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, -1, 0) = 0x7768b3e00000
140900 mmap(0x7768b3e00000, 1073741824, 0x3 /* PROT_READ|PROT_WRITE */, 0x32 /* MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS */, -1, 0) = 0x7768b3e00000
140900 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4020ae46 /* KVM_SET_USER_MEMORY_REGION */, {slot=0, flags=0, guest_phys_addr=0, memory_size=1073741824, userspace_addr=0x7768b3e00000}) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;recreating-mmap-call&quot;&gt;Recreating mmap call&lt;/h4&gt;
&lt;p&gt;So, first thing we need to do is follow the same logic for &lt;code&gt;mmap&lt;/code&gt; which is also&lt;br&gt;
available in the &lt;code&gt;libc&lt;/code&gt; Rust crate. After creating the virtual machine, we could&lt;br&gt;
simply recreate our own &lt;code&gt;mmap&lt;/code&gt; calls based on the &lt;code&gt;strace&lt;/code&gt; output. However,&lt;br&gt;
notice that QEMU first reserves a larger address range with &lt;code&gt;PROT_NONE&lt;/code&gt; and then&lt;br&gt;
maps only the portion it actually intends to use. For our prototype we do not&lt;br&gt;
actually need to mimic this exact reservation pattern.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mem_size: u64 = 256 * 1024;
let mem = unsafe {
libc::mmap(ptr::null_mut(),
   mem_size as usize,
   libc::PROT_READ|libc::PROT_WRITE,
   libc::MAP_PRIVATE|libc::MAP_ANONYMOUS,
   -1,
   0)
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this experiment we are only allocating 256 kilobytes because we are not yet&lt;br&gt;
booting a full operating system and therefore need very little guest memory.&lt;br&gt;
Also note that, as with &lt;code&gt;ioctl&lt;/code&gt;, production code should check whether mmap()&lt;br&gt;
returned &lt;code&gt;MAP_FAILED&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;recreating-ioctl-call&quot;&gt;Recreating ioctl call&lt;/h4&gt;
&lt;p&gt;First we need to see the definition of the &lt;code&gt;KVM_SET_USER_MEMORY_REGION&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ grep -Rn 'define KVM_SET_USER' /usr/include/linux/ -A1
/usr/include/linux/kvm.h:1433:#define KVM_SET_USER_MEMORY_REGION _IOW(KVMIO, 0x46, \
/usr/include/linux/kvm.h-1434-                                  struct kvm_userspace_memory_region)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We see that for this one, we need &lt;code&gt;struct kvm_userspace_memory_region&lt;/code&gt;. The&lt;br&gt;
values we need are visible in the &lt;code&gt;strace&lt;/code&gt; output, but copying struct definition&lt;br&gt;
to our Rust program is really not advisable. Luckily, Rust has &lt;code&gt;bindgen&lt;/code&gt; which&lt;br&gt;
we can use to get this KVM struct (and others) from Linux headers. For this&lt;br&gt;
purpose we have a separate &lt;code&gt;build.rs&lt;/code&gt; file which will contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;use bindgen;
use std::path::PathBuf;
use std::env;

fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {
    let out_path = PathBuf::from(env::var(&quot;OUT_DIR&quot;)?);

    bindgen::Builder::default()
.header(&quot;/usr/include/linux/kvm.h&quot;)
.allowlist_type(&quot;kvm_userspace_memory_region&quot;)
.generate_comments(false)
.generate()?
.write_to_file(out_path.join(&quot;kvm-bindings.rs&quot;))?;

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now in the &lt;code&gt;main.rs&lt;/code&gt; we can import these bindings with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;include!(concat!(env!(&quot;OUT_DIR&quot;), &quot;/kvm-bindings.rs&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, after &lt;code&gt;mmap()&lt;/code&gt; calls, we set the memory region, also imitating what&lt;br&gt;
QEMU is doing in the &lt;code&gt;strace&lt;/code&gt; output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let region = kvm_userspace_memory_region {
slot : 0,
flags : 0,
guest_phys_addr : 0x0,
memory_size : mem_size,
userspace_addr : mem as u64
};

let _ret = unsafe { libc::ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &amp;amp;region) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We register this region starting at guest physical address &lt;code&gt;0x0&lt;/code&gt;, meaning the&lt;br&gt;
first byte of our allocated host memory will appear as physical address 0 inside&lt;br&gt;
the guest. Also note that the &lt;code&gt;KVM_SET_USER_MEMORY_REGION&lt;/code&gt; call does not copy&lt;br&gt;
memory, but rather tells KVM that guest physical address will be backed by a&lt;br&gt;
specific userspace memory region.&lt;/p&gt;
&lt;h3 id=&quot;running-the-code&quot;&gt;Running the code&lt;/h3&gt;
&lt;p&gt;Next thing to do is to run it. We still haven't added any checks after &lt;code&gt;mmap&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;ioctl&lt;/code&gt; calls, but for this prototype we can again simply use &lt;code&gt;strace&lt;/code&gt; our&lt;br&gt;
own code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ strace -yy -X verbose -e trace=ioctl,mmap,openat,read,write cargo run

                ### omitted irrelevant strace output ###

openat(-100 /* AT_FDCWD */&amp;lt;/home/stjepan/Develop/KVM/rust&amp;gt;, &quot;/dev/kvm&quot;, 0x80002 /* O_RDWR|O_CLOEXEC */) = 3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;
ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae01 /* KVM_CREATE_VM */, 0) = 4&amp;lt;anon_inode:kvm-vm&amp;gt;
mmap(NULL, 262144, 0x3 /* PROT_READ|PROT_WRITE */, 0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, -1, 0) = 0x7d63fdfa2000
ioctl(4&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4020ae46 /* KVM_SET_USER_MEMORY_REGION */, {slot=0, flags=0, guest_phys_addr=0, memory_size=262144, userspace_addr=0x7d63fdfa2000}) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see our output is fine and no errors were reported. Note that a full&lt;br&gt;
working example with proper checking and Rust idiomatic approaches can be found&lt;br&gt;
on my GitHub page:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-part2-code&quot;&gt;https://github.com/StjepanPoljak/kvm-rust/tree/kvm-part2-code&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;next-steps&quot;&gt;Next steps&lt;/h4&gt;
&lt;p&gt;At this point we have a VM object and guest memory, but nothing is actually executing yet. In the next part we will create a vCPU, initialize its state, load a small binary into guest memory and enter the first &lt;code&gt;KVM_RUN&lt;/code&gt; loop.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20611/building-a-kvm-virtual-machine-in-rust-memory-setup</guid>
<pubDate>Mon, 15 Jun 2026 05:20:13 +0000</pubDate>
</item>
<item>
<title>Learning KVM by Reverse-Engineering QEMU with strace</title>
<link>https://coderlegion.com/20315/learning-kvm-by-reverse-engineering-qemu-with-strace</link>
<description>&lt;h3 id=&quot;motivation-2&quot;&gt;Motivation&lt;/h3&gt;
&lt;p&gt;I work with virtual machines in QEMU/KVM environment (a lot). In order to debug,&lt;br&gt;
optimize and customize the VMs requires an in-depth knowledge of both QEMU and&lt;br&gt;
KVM, the Linux kernel virtualization subsystem that exposes hardware&lt;br&gt;
virtualization features such as Intel VT-x and AMD-V to userspace applications&lt;br&gt;
like QEMU. Not only that, but I work on a lot of hobby projects requiring quick&lt;br&gt;
bare-metal boot-ups and debugging workflows, and, to be honest, a lot of times&lt;br&gt;
QEMU is an overkill for these sorts of tasks.&lt;/p&gt;
&lt;h3 id=&quot;kvm-vs-tcg&quot;&gt;KVM vs TCG&lt;/h3&gt;
&lt;p&gt;Also when it comes to QEMU, it's worth noting that we always have an option of&lt;br&gt;
using TCG, which has a completely different purpose than KVM. TCG is short for&lt;br&gt;
Tiny Code Generator, which works by translating guest instructions into host&lt;br&gt;
instructions at runtime. This is quite slow compared to running code without&lt;br&gt;
translation overhead. So, if we want to test our bare-metal code, we may want to&lt;br&gt;
test it on our own real CPU at native speed. This is where KVM comes in. Unlike&lt;br&gt;
TCG, KVM does not emulate the CPU itself. Instead, it allows guest code to&lt;br&gt;
execute directly on the host processor while the Linux kernel manages&lt;br&gt;
transitions between guest and host execution.&lt;/p&gt;
&lt;h3 id=&quot;how-kvm-works-in-linux&quot;&gt;How KVM works in Linux&lt;/h3&gt;
&lt;p&gt;So, I do already know some basics. KVM driver exposes a driver interface in&lt;br&gt;
Linux root filesystem, &lt;code&gt;/dev/kvm&lt;/code&gt;. Communicating with the driver is done via&lt;br&gt;
&lt;code&gt;ioctl()&lt;/code&gt; system call on a file descriptor. What we need to find out is how QEMU&lt;br&gt;
communicates with Linux kernel and try and follow the QEMU logic without&lt;br&gt;
reading QEMU source code and KVM API (both can be a bit more intimidating than&lt;br&gt;
just seeing how it works under the hood).&lt;/p&gt;
&lt;h3 id=&quot;reverse-engineering-kvm&quot;&gt;Reverse-engineering KVM&lt;/h3&gt;
&lt;p&gt;Now we can take some lightweight Debian Linux image and load it into the QEMU,&lt;br&gt;
with KVM enabled:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;QEMU_IMAGE=./debian-12-nocloud-amd64.qcow2

qemu-system-x86_64\
-m 1024\
-drive file=&quot;${QEMU_IMAGE}&quot;,if=virtio,cache=none\
-serial stdio\
-enable-kvm\
-cpu host\
-nodefaults\
-nographic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a pretty straightforward way to run QEMU with minimal setup. The most&lt;br&gt;
relevant options for us are &lt;code&gt;-enable-kvm&lt;/code&gt; and &lt;code&gt;-cpu host&lt;/code&gt;, which will enable&lt;br&gt;
KVM and use host CPU instead of emulating some specific CPU.&lt;/p&gt;
&lt;h4 id=&quot;tracing-qemu-kvm-with-strace&quot;&gt;Tracing QEMU/KVM with strace&lt;/h4&gt;
&lt;p&gt;Now, we want to see what QEMU is really doing by utilizing &lt;code&gt;strace&lt;/code&gt;. We can put this command in a &lt;code&gt;start-qemu.sh&lt;/code&gt; script and call it with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;strace -yy -f -X verbose\
   -e trace=ioctl,openat,read,write,mmap\
   -o kvm.log\
   ./start-qemu.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will trace all &lt;code&gt;ioctl&lt;/code&gt;, &lt;code&gt;openat&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; and &lt;code&gt;mmap&lt;/code&gt; system&lt;br&gt;
calls. Although I mentioned only &lt;code&gt;ioctl&lt;/code&gt; calls so far, I always like to include&lt;br&gt;
some other common system calls that could be used. As far as we know &lt;code&gt;/dev/kvm&lt;/code&gt;&lt;br&gt;
is the interface to KVM driver and QEMU will probably use &lt;code&gt;openat&lt;/code&gt; on it.&lt;br&gt;
Similarly, we also want to see what QEMU is doing with memory and what it's&lt;br&gt;
reading and writing in general.&lt;/p&gt;
&lt;p&gt;Note: Information on &lt;code&gt;strace&lt;/code&gt; arguments as above can be found in &lt;code&gt;strace --help&lt;/code&gt;&lt;br&gt;
or &lt;code&gt;man strace&lt;/code&gt;, but essentially, the &lt;code&gt;-yy&lt;/code&gt; tells strace to print all available&lt;br&gt;
information when decoding file descriptors, &lt;code&gt;-f&lt;/code&gt; follows forks (we need this one&lt;br&gt;
as we're wrapping it in scripts and QEMU might also do similar stuff). The&lt;br&gt;
&lt;code&gt;-X verbose&lt;/code&gt; will print names of constants and flags (very important when&lt;br&gt;
analyzing &lt;code&gt;ioctl&lt;/code&gt; calls);&lt;/p&gt;
&lt;h4 id=&quot;interpreting-the-logs&quot;&gt;Interpreting the logs&lt;/h4&gt;
&lt;p&gt;Now, we start the above command and, as soon as system boots, we can kill it&lt;br&gt;
with &lt;code&gt;CTRL+C&lt;/code&gt;. This will be quite sufficient to see how QEMU/KVM works without&lt;br&gt;
spamming our logs with redundant information. When we read the &lt;code&gt;kvm.log&lt;/code&gt; file,&lt;br&gt;
we will see a lot of traces that are not really interesting. However, we already&lt;br&gt;
have some knowledge: we know QEMU should be opening &lt;code&gt;/dev/kvm&lt;/code&gt; so a quick&lt;br&gt;
search for &lt;code&gt;kvm&lt;/code&gt; reveals exactly what we need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140900 openat(-100 /* AT_FDCWD */&amp;lt;/home/stjepan/Develop/KVM&amp;gt;, &quot;/dev/kvm&quot;, 0x80002 /* O_RDWR|O_CLOEXEC */) = 3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;
140900 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae00 /* KVM_GET_API_VERSION */, 0) = 12
140900 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae03 /* KVM_CHECK_EXTENSION */, 0x88 /* KVM_CAP_IMMEDIATE_EXIT */) = 1
140900 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae03 /* KVM_CHECK_EXTENSION */, 0xa /* KVM_CAP_NR_MEMSLOTS */) = 32764
140900 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae03 /* KVM_CHECK_EXTENSION */, 0x76 /* KVM_CAP_MULTI_ADDRESS_SPACE */) = 2
140900 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae01 /* KVM_CREATE_VM */, 0) = 9&amp;lt;anon_inode:kvm-vm&amp;gt;
140900 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0xae03 /* KVM_CHECK_EXTENSION */, 0x9 /* KVM_CAP_NR_VCPUS */) = 4
140900 ioctl(3&amp;lt;/dev/kvm&amp;lt;char 10:232&amp;gt;&amp;gt;, 0xae03 /* KVM_CHECK_EXTENSION */, 0x42 /* KVM_CAP_MAX_VCPUS */) = 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see that QEMU is opening &lt;code&gt;/dev/kvm&lt;/code&gt; and that it's checking API version&lt;br&gt;
and various extensions. We may skip these checks and focus on the calls that&lt;br&gt;
look most important; one of these here is &lt;code&gt;KVM_CREATE_VM&lt;/code&gt; which also returns a&lt;br&gt;
file descriptor &lt;code&gt;9&amp;lt;anon_inode:kvm-vm&amp;gt;&lt;/code&gt; which we can use as a further reference.&lt;/p&gt;
&lt;h5 id=&quot;setting-up-memory-regions&quot;&gt;Setting up memory regions&lt;/h5&gt;
&lt;p&gt;We know QEMU must eventually load firmware and guest memory into the VM. Looking&lt;br&gt;
for file operations after &lt;code&gt;KVM_CREATE_VM&lt;/code&gt;, we quickly encounter SeaBIOS being&lt;br&gt;
loaded:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140900 openat(-100 /* AT_FDCWD */&amp;lt;/home/stjepan/Develop/KVM&amp;gt;, &quot;/usr/share/seabios/bios-256k.bin&quot;, 0 /* O_RDONLY */) = 12&amp;lt;/usr/share/seabios/bios-256k.bin&amp;gt;
140900 mmap(NULL, 2359296, 0 /* PROT_NONE */, 0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, -1, 0) = 0x776900dc1000
140900 mmap(0x776900e00000, 262144, 0x3 /* PROT_READ|PROT_WRITE */, 0x32 /* MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS */, -1, 0) = 0x776900e00000
140900 openat(-100 /* AT_FDCWD */&amp;lt;/home/stjepan/Develop/KVM&amp;gt;, &quot;/usr/share/seabios/bios-256k.bin&quot;, 0 /* O_RDONLY */) = 12&amp;lt;/usr/share/seabios/bios-256k.bin&amp;gt;
140900 mmap(NULL, 266240, 0x3 /* PROT_READ|PROT_WRITE */, 0x22 /* MAP_PRIVATE|MAP_ANONYMOUS */, -1, 0) = 0x776900fc0000
140900 read(12&amp;lt;/usr/share/seabios/bios-256k.bin&amp;gt;, &quot;\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&quot;..., 262144) = 262144
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see QEMU opening and reading SeaBIOS binary and reserving memory along&lt;br&gt;
with it. We can use the return of the &lt;code&gt;read&lt;/code&gt; system call (the size of the&lt;br&gt;
SeaBIOS binary) and see what KVM is doing with it. Searching through the log&lt;br&gt;
for this size gives us further information on what KVM is doing with SeaBIOS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140900 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4020ae46 /* KVM_SET_USER_MEMORY_REGION */, {slot=3, flags=0x2 /* KVM_MEM_READONLY */, guest_phys_addr=0xfffc0000, memory_size=262144, userspace_addr=0x776900e00000}) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we see that it's now setting this as the memory region for KVM at guest&lt;br&gt;
physical address &lt;code&gt;0xfffc0000&lt;/code&gt; from userspace address that was actually&lt;br&gt;
obtained by &lt;code&gt;mmap&lt;/code&gt; in one of the traces above. In other words, KVM does not&lt;br&gt;
allocate guest RAM itself; userspace applications such as QEMU remain&lt;br&gt;
responsible for managing the backing memory.&lt;/p&gt;
&lt;h5 id=&quot;creating-vcpu-and-running&quot;&gt;Creating vCPU and running&lt;/h5&gt;
&lt;p&gt;Now, it gets very busy in the logs, but most of the stuff we see is still just&lt;br&gt;
checking for extensions and capabilities. However, if we take a look at the&lt;br&gt;
tail of the log, we will see a lot of these &lt;code&gt;ioctl&lt;/code&gt; calls:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xae80 /* KVM_RUN */, 0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xaeb7 /* KVM_SMI */, 0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xae80 /* KVM_RUN */, 0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xae80 /* KVM_RUN */, 0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xae80 /* KVM_RUN */, 0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xae80 /* KVM_RUN */, 0) = 0
140904 ioctl(10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;, 0xaeb7 /* KVM_SMI */, 0) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now both &lt;code&gt;KVM_RUN&lt;/code&gt; and &lt;code&gt;KVM_SMI&lt;/code&gt; are operating on a &lt;code&gt;kvm-vcpu&lt;/code&gt; file descriptor,&lt;br&gt;
something we haven't yet seen. So if we search the logs for it, we can actually&lt;br&gt;
see where it's created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;140904 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0xae41 /* KVM_CREATE_VCPU */, 0) = 10&amp;lt;anon_inode:kvm-vcpu:0&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Now we have a more complete picture of how QEMU is setting up KVM. First,&lt;br&gt;
&lt;code&gt;/dev/kvm&lt;/code&gt; is opened to obtain a file descriptor representing the KVM subsystem.&lt;br&gt;
From it we create a new virtual machine and get &lt;code&gt;kvm-vm&lt;/code&gt; file descriptor. On&lt;br&gt;
this file descriptor we are setting up memory regions and later use it to create&lt;br&gt;
a vCPU, on which we can call &lt;code&gt;KVM_RUN&lt;/code&gt;. The following diagram explains it&lt;br&gt;
better:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/dev/kvm   kvm fd
    |
    +--&amp;gt; KVM_CREATE_VM   kvm-vm fd
            |
            +--&amp;gt; KVM_SET_USER_MEMORY_REGION
            |
            +--&amp;gt; KVM_CREATE_VCPU   kvm-vcpu fd
                            |
                            +--&amp;gt; KVM_RUN (loop)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see from the logs, &lt;code&gt;KVM_RUN&lt;/code&gt; appears repeatedly during guest&lt;br&gt;
execution, while occasional &lt;code&gt;KVM_SMI&lt;/code&gt; calls inject System Management Interrupts&lt;br&gt;
into the guest. This repeated interaction between userspace and KVM is what&lt;br&gt;
ultimately drives virtual CPU execution.&lt;/p&gt;
&lt;p&gt;Next time we will recreate this exact behavior in Rust and also see about just&lt;br&gt;
a few missing pieces to get our first virtual machine running in KVM.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20315/learning-kvm-by-reverse-engineering-qemu-with-strace</guid>
<pubDate>Thu, 11 Jun 2026 15:56:48 +0000</pubDate>
</item>
</channel>
</rss>