Most people associate a virtual machine (VM) with something slow and complex. Here I will show you two things - 1. VMs are not slow and 2. VMs can be super simple to use. We will use KVM ( ) to run our code. This means that in order to execute this code you will need a Linux machine with KVM installed. Alternatively you can use Google Cloud Compute Engine with nested virtualization enabled ( ). https://www.linux-kvm.org/page/Main_Page https://cloud.google.com/compute/docs/instances/enable-nested-virtualization-vm-instances Following is the code that we will run in our VM: start , , , , , , global start: mov dx 0x3f8 add al bl add al `0` out dx al mov al `\n` out dx al hlt The above code takes two values in registers and and outputs the value via a serial port. We add "0" (ASCII value of character zero) to register so we can display correct character. instructions are normally used by a CPU to output a value to an IO port. al bl al out Since our VM does not have any IO ports this instruction will cause what is known as VMEXIT and signal our host OS to help with handling this instruction. The above code when compiled produces the following sequence of bytes: code[] = { , , , , , , , , , , , }; const uint8_t 0xba 0xf8 0x03 0x00 0xd8 0x04 '0' 0xee 0xb0 '\n' 0xee 0xf4 KVM API[1] is a set of that operate on file descriptors. The API is relatively simple, easy to work with and well documented - . ioctls https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt Let's now try run this code in a VM! 1. obtain a handle to the KVM subsystem. This is done by opening file. /dev/kvm kvm = open( , O_RDWR | O_CLOEXEC); int "/dev/kvm" 2. Use to check if API version is exactly 12. KVM_GET_API_VERSION int ver = ioctl(kvm, KVM_GET_API_VERSION, NULL); (ver != ) { printf( , ver); } if 12 "KVM_GET_API_VERSION expected 12 but got %d. Exiting.\n" 3. Using API call we can check if some capability is supported. In our example we will use . This capability will enable our host to specify memory contents (our example code) for our VM. KVM_CHECK_EXTENSION KVM_CAP_USER_MEMORY (ioctl(kvm, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY) == ) { ( ; } if -1 printf "KVM_CAP_USER_MEMORY not available. Exiting." return -1 4. Now it's time to create our VM. For that we will use KVM_CREATE_VM ioctl with machine type 0. vmfd = ioctl(kvm, KVM_CREATE_VM, ( ) ); (vmfd == ) { ( , vmfd); ; } int unsigned long 0 if -1 printf "There was a problem creating VM. KVM_CREATE_VM exit code: %d\n" return -1 5. Next we'll allocate a chunk of memory and place our code into it *mem = mmap( , , PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, , ); (mem, code, (code)); void NULL 0x1000 -1 0 memcpy sizeof 6. Use to create a guest physical memory slot KVM_SET_USER_MEMORY_REGION .slot = , .guest_phys_addr = , .memory_size = , .userspace_addr = ( )mem, }; ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region); = { struct kvm_userspace_memory_region region 0 0x1000 0x1000 uint64_t Here we tell KVM that the memory we allocated ( ) is the memory that will be associated with slot 0. It will be accessible starting at location 0x1000 from within the VM and it will be 0x1000 bytes (4Kb) in size. mem 7. We now need to add VCPU to our VM. To do that we can use ioctl. KVM_CREATE_VCPU vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, ( ) ); (vcpufd == ) { ( , vmfd, vmcpufd); ; } int unsigned long 0 if -1 printf "Could not create VCPU for VM %d. Error code: %d" return -1 8. VCPU communicates with host OS via a shared memory region. To get the size of that region we can use . KVM_GET_VCPU_MMAP_SIZE mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, ); size_t NULL * = ( *) ( , , | , , , 0); struct kvm_run run struct kvm_run mmap NULL mmap_size PROT_READ PROT_WRITE MAP_SHARED vcpufd 9. In order for our VCPU to read the code form the right location we need to configure special register CS (code segment). We need to set base and selector value to 0. To get special registers we use and to write back changes we use ioctl. KVM_GET_SREGS KVM_SET_SREGS ioctl(vcpufd, KVM_GET_SREGS, &sregs); sregs.cs.base = ; sregs.cs.selector = ; ioctl(vcpufd, KVM_SET_SREGS, &sregs); ; struct kvm_sregs sregs 0 0 The above configuration will ensure that if the code is found at address 0x1000 it is actually read from that physical address. In case CS is not set to 0 code would be read from a different physical location. If you're interested why you may want to learn more about segmentation in x86 CPUs[*]. 10. Before we can execute our program in the new VM we must configure the following registers RIP (instruction pointer) needs to be set to 0x1000. This is where we placed our code to execute. RAX, RBX need to be set to some value. Values in those two registers will be added and the result will be written to stdout. RFLAGS needs to be set to 0x2. This is specified by x86 architecture. Not setting this register to 0x2 will cause VM to fail. .rax = , .rbx = , .rip = , .rflags = , }; ioctl(vcpufd, KVM_SET_REGS, &regs); = { struct kvm_regs regs 2 2 0x1000 0x2 11. Finally we are ready to run our VM. To do that we use KVM_RUN ioctl. ioctl(vcpufd, KVM_RUN, ); NULL 12. While running a VM some operations will not be able to complete and will require help from our host OS. In the case of our example program those are , and instructions. and instructions will exit with reason while will exit with . At minimum those are exits that we want to handle. Once the VM exits you can run it again using ioctl. IN OUT HLT IN OUT KVM_EXIT_IO HLT KVM_EXIT_HLT KVM_RUN ( ) { ioctl(vcpufd, KVM_RUN, ); (run->exit_reason) { KVM_EXIT_HLT: KVM_EXIT_IO: (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == && run->io.port == && run->io.count == ) { (*((( *)run) + run->io.data_offset)); ( ); } { ( ); } ; } } while 1 NULL switch case // handle HLT case if 1 0x3f8 1 putchar char putchar '\n' else printf "unhandled KVM_EXIT_IO\n" break Full source code is available at: . https://gitlab.com/mvuksano/kvm-playground Hopefully this shows you how easy it is to run a piece of code in a VM. You can use this boilerplate code to run a wide range of programs. You can also use it to learn more about how CPUs work or using nested virtualization. In the next article I will show how to load arbitrary code and run it in a VM. Until then keep exploring KVM and keep learning about virtualization.