Testing the Virtual Device from the Guest Kernel
Before writing the actual driver it is probably a good idea to perform a quick test of the device from the guest kernel and check that it behaves correctly. To that end we can make a small modification to the Linux guest kernel sources, and insert some calls to the device. For the sake of simplicity we’ll insert these calls at the end of the boot process, when the system is well initialised but without involving the user space.
Locating the Kernel Main Function
The kernel is a computer program like any other and as such it has an entry point.
This entry point is written in assembly but after a short early initialisation, the CPU will jump to C code.
More precisely, the C entry point of the kernel is the function start_kernel, which is implemented in the Linux sources in the file init/main.c.
If you check out its implementation, you’ll see that start_kernel initialises many subsystems and then calls arch_call_rest_init, which itself calls rest_init.
rest_init spawns a kernel thread that runs the kernel_init function.
The kernel_init function finalises the initialisation of the system and then starts the first user space application.
This is a suitable point in the boot process to insert our test calls to the device, because the system is fully initialised, and we are also still in kernel space.
Inserting Test Calls to the Device
Our test will perform the following things:
- Seed the RNG with a fixed seed, e.g.
0x42 - Generate five random numbers and print them to the kernel log
Steps 1 and 2 will be repeated twice, so we can check that the five random numbers generated from the same seed are the same in both iterations.
In the kernel_init function, add the following code after the call to do_sysctl_args(); (it’s around line 1460):
printk("------------------------------------------------------------------\n");
printk("BEGIN MY-RNG TEST\n");
printk("------------------------------------------------------------------\n");
// Map the area of physical memory corresponding to the device's registers
// (starting 0xfebf1000, size 4KB) somewhere in virtual memory at address
// devmem. Notice that the physical memory where the device's registers are
// present may be different on your computer, use lspci -v in the VM to
// find it
void *devmem = ioremap(0xfebf1000, 4096);
unsigned int data = 0x0;
if(devmem) {
for(int i=0; i<2; i++) {
// seed with 0x42 by writing that value in the seed register which
// is located at base address + 4 bytes
iowrite32(0x42, devmem+4);
// obtain and print five random numbers by reading the relevant
// register located at base address + 0
for(int j=0; j<5; j++) {
data = ioread32(devmem);
printk("Round %d number %d: %u", i, j, data);
}
}
} else {
printk("ERROR: cannot map device registers\n");
}
printk("------------------------------------------------------------------\n");
printk("END MY-RNG TEST\n");
printk("------------------------------------------------------------------\n");
A few notable things in this code:
- We use
printkto print to the kernel log. It’s very similar to theprintffunction you are familiar with in user space. With printk we display when the test starts and ends so that things are clearly visible in the kernel log. - The test code starts by mapping the physical memory where the device’s registers are present into virtual memory (shortly after the very early boot process the CPU can only access virtual memory) at an address pointed by
devmem. This is achieved with theioremapfunction, that takes as parameters the physical address to map into virtual memory, as well as the size of the area to map (here one page, i.e. 4 KB, as defined when we implemented the device). Note the address in physical memory where the device’s registers are mapped, here0xfebf1000. It may be different on your computer. To find it out, you can uselspciwithin the VM, as previously explained. - Once the device’s registers are mapped into virtual memory, we can read and write to them using
ioread32andiowrite32. It’s important to use these functions rather than directly read/write to memory because these are not standard memory access operations: these functions will ensure important things like bypassing the CPU caches, disabling compiler optimisations, and will have memory barriers preventing the compiler/CPU to reorder the corresponding instructions. Through these functions we have two types of operations when talking to the device:- Generating a random number: to achieve this we read the first register with
ioread32; it is located directly at the device’s base address - Seeding the RNG: to do so we write to the second register with
iowrite32; it is located 32 bits (4 bytes) from the base address
- Generating a random number: to achieve this we read the first register with
Launching the Test
Once the test code is ready you can recompile the guest Linux kernel:
cd ~/virt-101-exercise/linux-6.6
make
When you launch the VM with this newly compiled kernel, you should see in the log at the end of the kernel boot process something like that:
[ 3.519214] ------------------------------------------------------------------
[ 3.519510] BEGIN MY-RNG TEST
[ 3.519620] ------------------------------------------------------------------
[ 3.520024] Round 0 number 0: 286129175
[ 3.520046] Round 0 number 1: 1594929109
[ 3.520199] Round 0 number 2: 971802288
[ 3.520394] Round 0 number 3: 222134722
[ 3.520559] Round 0 number 4: 1335014133
[ 3.520754] Round 1 number 0: 286129175
[ 3.520918] Round 1 number 1: 1594929109
[ 3.521073] Round 1 number 2: 971802288
[ 3.521227] Round 1 number 3: 222134722
[ 3.521406] Round 1 number 4: 1335014133
[ 3.521545] ------------------------------------------------------------------
[ 3.521965] END MY-RNG TEST
[ 3.522101] ------------------------------------------------------------------
As you can see, for each round the series of generated random numbers are the same, which confirms that the RNG virtual device works correctly.