Testing the Virtual Device from the Guest Kernel
Before writing the actual driver it is probably a good idea to a quick test of the device from the guest kernel and check it behaves correctly. To that aim we can do a small modification of 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 call 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 5 random numbers and print them on the kernel log
Steps 1 and 2 will be repeated twice, so we can check that the 5 random numbers generated from the same seed are the same for both iterations.
In the kernel_init
function, add the following code after the call to do_sysctl_args();
(it's around line 1464):
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 5 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
printk
to print to the kernel log. It's very similar to theprintf
function 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 theioremap
function, 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 uselspci
within the VM, as previously explained. - Once the device's registers are mapped into virtual memory, we can read and write to them using
ioread32
andiowrite32
. 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 that we read with
ioread32
the first register which is located directly at the device's base address - Seeding the RNG: to do so we write with
iowrite32
the second register which is located 32 bits (4 bytes) from the base address
- Generating a random number: to achieve that we read 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 random number generated are the same, which confirms that the RNG virtual device works well.