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.4/      # kernel sources
|-- qemu-8.2.0-rc2/   # 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-rc2
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.
  • 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 member realize of the corresponding PCIDeviceClass data structure also points to the per-instance initialisation function my_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, and mmio_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 type my_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) and srand() (to seed the random number generator).