<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged strace</title>
<link>https://coderlegion.com/tag/strace</link>
<description>Powered by Question2Answer</description>
<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&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&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>