Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Synchronous RNG: Basic Device Access from Kernel Space

Before writing a proper driver, let’s see how we can access the device with MMIOs from the guest OS kernel code. 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 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, starting line 870.

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, which implementation starts on line 1428, 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: we are still in kernel space, and the system is fully initialised.

Inserting Test Calls to the Device

Our test will perform the following steps:

  1. Seed the RNG with a fixed seed, e.g. 0x42.
  2. 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(); (line 1460):

printk("------------------------------------------------------------------\n");
printk("BEGIN EDU-RNG-SYNC TEST\n");
printk("------------------------------------------------------------------\n");

// Base address of the device's registers in physical memory (the value
// held in the device's Base Address Register, BAR). Use the PCI
// identification information present in the datasheet as well as the
// output of lspci -v in the VM to find it.
unsigned long bar = /* TODO complete here */;

// MMIO register offsets, see datasheet
unsigned long ran_reg_offset = /* TODO complete here */;
unsigned long seed_reg_offset = /* TODO complete here */;

// Map the area of physical memory corresponding to the device's registers
// (starting at the base address, size 4KB) somewhere in virtual memory at
// address devmem.
void *devmem = ioremap(bar, 4096);
if(devmem) {
    
    unsigned int data = 0x0;

    for(int i=0; i<2; i++) {
        // seed with 0x42 by writing that value in the seed register
        iowrite32(0x42, devmem + seed_reg_offset);

        // obtain and print five random numbers by reading the relevant
        // register
        for(int j=0; j<5; j++) {
            data = ioread32(devmem + ran_reg_offset);
            printk("Round %d number %d: %u", i, j, data);
        }
    }
} else {
    printk("ERROR: cannot map device registers\n");
}

printk("------------------------------------------------------------------\n");
printk("END EDU-RNG-SYNC TEST\n");
printk("------------------------------------------------------------------\n");

You will need to complete the initialisation of the base address and register offset variables. A few notable points about this code:

  • We use printk to print to the kernel log. It is the kernel version of the printf function you are familiar with in user space. With printk, we display when the test starts and ends so that the test’s output is 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 at an address pointed to by devmem. This is achieved with the ioremap function, which 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 in the datasheet). Under the hood the kernel will update the page table to realise the mapping and flush translation caches if needed.

  • Once the device’s registers are mapped into virtual memory, we can read and write to them using the accessors ioread32 and iowrite32. It is important to use these functions rather than reading from or writing to memory directly (e.g., data = *(devmem + ran_reg_offset)). Indeed, device access are specific memory access operations that require special care to be successful. ioread/write ensure important properties such as bypassing the CPU caches, disabling compiler optimisations, and inserting memory barriers that prevent the compiler or CPU from reordering the corresponding instructions.

Launching the Test

Once the test code is ready, you can recompile the guest Linux kernel:

$ cd ~/workspace/linux-6.6
$ make

When you launch the VM with this newly compiled kernel, assuming your code is correct, you should see in the log at the end of the kernel boot process something like this:

[    3.519214] ------------------------------------------------------------------
[    3.519510] BEGIN EDU-RNG-SYNC 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 EDU-RNG-SYNC TEST
[    3.522101] ------------------------------------------------------------------

As you can see, for each round the sequence of generated random numbers is the same, which confirms that we are accessing the device correctly. Once the VM has booted, you can print the kernel log with the following command:

alpine:~# dmesg

⚠️ ⚠️ ⚠️ Do not remove this small test from the kernel boot code, its presence (and successful implementation) is worth 1 mark.