Implementing the Virtual Device in Qemu
We now have the VM set up and the sources of Qemu and Linux ready to be modified.
We'll start by modifying Qemu to implement the virtual random number generator.
The goal is to emulate that device, e.g. adhere to the same interface the guest OS would use to communicate with a real hardware component: the random number generator will be connected to the VM's virtual CPU on the PCI bus, and communication between the device and the CPU will be achieved with memory mapped I/O registers.
The implementation of the RNG itself (e.g. how random numbers are generated) will be done completely in software, for example by using the rand()
and srand()
functions provided by the C standard library in Qemu on the host.
You can refresh your mind about the functionalities of the virtual device and its registers here.
At that point the base folder for the exercise should look like that:
virt-101-exercise/ # exercise base directory
|-- alpine.qcow2 # virtual hard disk with Alpine installed
|-- launch-vm.sh # VM launch script
|-- linux-6.6/ # kernel sources
|-- qemu-8.2.0/ # Qemu sources
Adding a New Source File in Qemu
We'll start by creating a new C file in which we will implement the device:
cd ~/virt-101-exercise/qemu-8.2.0
touch hw/misc/my-rng.c
Next we need to add that file to the build system so that it gets compiled and linked against the rest of Qemu sources.
Add the following at the top of the file hw/misc/Kconfig
:
config MY_RNG
bool
default y
And add that line at the top of the file hw/misc/meson.build
:
system_ss.add(when: 'CONFIG_MY_RNG', if_true: files('my-rng.c'))
A modification of the build system requires reconfiguring and recompiling all of Qemu sources. To do so simply type the following command in Qemu's sources root directory:
make -j4 install
To make sure your file is included in the build you can force its recompilation as follows:
touch hw/misc/my-rng.c
make
You should see in the output:
[3/4] Compiling C object libcommon.fa.p/hw_misc_my-rng.c.o
Implementing the Device
Now we will implement the virtual random number generator in hw/misc/my-rng.c
.
We'll first need to include the following headers as they define data structures and functions we need:
#include "qemu/osdep.h"
#include "hw/pci/msi.h"
#include "hw/pci/pci.h"
Next we define the device's name with a macro, and create a data structure representing the device:
#define TYPE_MY_RNG "my_rng"
#define MY_RNG(obj) OBJECT_CHECK(my_rng, (obj), TYPE_MY_RNG)
typedef struct {
PCIDevice parent_obj;
uint32_t seed_register;
MemoryRegion mmio;
} my_rng;
The important bits here are the seed_register
member, that we will use to hold the seed, and mmio
, a data structure that will hold functions to read and write from the device's memory mapped registers.
Next we define the functions that will run when the device's memory mapped registers are read/written:
static uint64_t mmio_read(void *opaque, hwaddr addr, unsigned size) {
/* TODO implement that function later */
return 0x0;
}
static void mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
/* TODO implement that function later */
return;
}
static const MemoryRegionOps my_rng_ops = {
.read = mmio_read,
.write = mmio_write,
};
It will be your task to implement these functions later.
For now, it is fine to leave them empty.
Notice the my_rng_ops
data structure that contain members pointing to both functions.
The rest of the source file contains a series of initialisation functions:
static void my_rng_realize(PCIDevice *pdev, Error **errp) {
my_rng *s = MY_RNG(pdev);
memory_region_init_io(&s->mmio, OBJECT(s), &my_rng_ops, s,
"my_rng", 4096);
pci_register_bar(&s->parent_obj, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->mmio);
}
static void my_rng_class_init(ObjectClass *class, void *data) {
DeviceClass *dc = DEVICE_CLASS(class);
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);
k->realize = my_rng_realize;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0xcafe;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}
static void my_rng_register_types(void) {
static InterfaceInfo interfaces[] = {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
};
static const TypeInfo my_rng_info = {
.name = TYPE_MY_RNG,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(my_rng),
.class_init = my_rng_class_init,
.interfaces = interfaces,
};
type_register_static(&my_rng_info);
}
type_init(my_rng_register_types)
You don't need to fully understand this code. Notable things here are:
- The
my_rng_realize
function that initialises an instance of the virtual random number generator by:- Creating a region of I/O memory for the memory mapped registers with
memory_region_init
. That region has a size of 4 KB which is much larger than what we need (we have 2 registers of 4 bytes each) but corresponds to the size of a memory page. - Registering the device on the PCI bus with
pci_register_bar
.
- Creating a region of I/O memory for the memory mapped registers with
- The
my_rng_class_init
that will run once when Qemu starts and define a few characteristics common to all instances of our virtual device, such as an easily identifiable device ID (0xcafe
). A memberrealize
of the correspondingPCIDeviceClass
data structure also points to the per-instance initialisation functionmy_rng_realize
.
At that point you can try to recompile Qemu by typing, at the root of its source folder:
make install
You should fix any error or warning at that stage. Once everything compiles fine we can check if the device appears in the VM.
Checking the Presence of the Device in the VM
To enable the device in the VM, edit the launch script ~/virt-101-exercise/launch-vm.sh
and add the following command line option to Qemu's invocation:
-device my_rng
You can check the presence of the virtual device by enumerating PCI devices in the VM.
Boot the VM and install lspci
using Alpine's packet manager APK:
apk add pciutils
Next, still in the VM, enumerate PCI devices:
lspci -v
You should see the following device:
00:04.0 Unclassified device [00ff]: Device 1234:cafe (rev 10)
Subsystem: Red Hat, Inc. Device 1100
Flags: fast devsel
Memory at febf1000 (32-bit, non-prefetchable) [size=4K]
You can recognise the device ID 0xcafe
we defined earlier.
Notice also the address where the device's registers are mapped in (physical) memory.
Here it is 0xfebf1000
but it may be different on your computer.
Implementing the Read/Write MMIO Functions
To finalise the implementation of our virtual random number generator, one must implement the two functions we defined earlier:
static uint64_t mmio_read(void *opaque, hwaddr addr, unsigned size) {
/* TODO */
return 0x0;
}
static void mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
/* TODO */
return;
}
You are responsible to implement these functions. A bit of information to help you achieve that:
mmio_read
is called when the guest OS tries to read in one of the device's memory mapped registers, andmmio_write
is called when a register is written.mmio_read
returns the value that the guest OS will read.- The
addr
parameter will contain the offset from the base address in the area of memory mapped I/O at which the read/write takes place, which should allow you to identify the target register. - The
size
parameter denotes the size of the read/write operation. - The
opaque
pointer points to the device's data structure of typemy_rng
, so you can get a pointer to the device's data structure with a cast:my_rng *dev = (my_rng *)opaque;
. - The actual RNG should be implemented in software and the easiest way to achieve that is probably to use the standard C library's functions
rand()
(to get a random number) andsrand()
(to seed the random number generator).