Simulating SoCs with isolated address spaces in Renode

Published: July 13th, 2022

Renode is Antmicro's open source, multi-architecture hardware simulator that has been helping our customers to develop countless complex hardware models over the years. Renode allows you to run the same software you would use on real hardware. To achieve this, it provides users with a multitude of built-in models of boards and SoCs, which mimic their physical counterparts. To give users even more customization options, we have developed support for multi-core CPUs having access to different devices under the same bus addresses. There are many use cases for this feature, with the most prevalent being security. In this note, we will describe how we implemented the possibility to isolate address spaces within a single SoC for security and other use cases, as required by our customers' usage of Renode.

Platform architecture in Renode

Typically, platforms in Renode are modeled with the system bus (identified as sysbus) of the machine functioning as the central building block to which the peripherals are connected. What is also typical is that all peripherals are accessible from anywhere within the created platform.

The real hardware might use a variety of intricately connected buses, but the same platform in Renode will just use a single bus. This simplification does not usually impact the behavior of the simulation in any way, as the structure of the interconnect in the SoC is hidden from the software perspective.

The below fragment of a .repl file is an example of a platform in which all the peripherals are connected to a single sysbus:

nvic: IRQControllers.NVIC @ sysbus 0xE000E000
    systickFrequency: 72000000
    priorityMask: 0xF0
    IRQ -> cpu@0

cpu: CPU.CortexM @ sysbus
    cpuType: "cortex-m7"
    nvic: nvic

itcm: Memory.MappedMemory @ sysbus 0x0
    size: 0x80000

dtcm: Memory.MappedMemory @ sysbus 0x20000000
    size: 0x80000

ocram2: Memory.MappedMemory @ sysbus 0x20200000 

Isolating address spaces

For some of your projects, a single-bus solution may not reflect the behavior of the real hardware. As an example, for performance reasons, in some multicore systems, memory may be tightly coupled with the CPU. Some platforms utilize this solution to make a certain memory region or a peripheral accessible only to a specific CPU for security reasons. In other cases, like in NVIC in the Cortex-M, every core has its own interrupt controller that is memory mapped but not accessible to other cores. Previously, there was no easy way to model such configurations in Renode because, by default, all peripherals were available to all registered CPUs.

Developing secure, multi-core solutions

The development of support for separate address spaces in Renode was driven by a use case of one of our customers. The goal was to create an OpenTitan-based secure solution with a trusted controller core and an untrusted, separated worker core. An alternative solution to a similar problem was developed in the past using two SoCs in Precursor, but trust zones in that project were enforced at a higher level. This time we needed the same kind of isolation, except within a single SoC.

Diagram depicting the isolated SoC

Modeling isolated address spaces in Renode

With the new developments in Renode, creating the kind of bus isolation described above within the single SoC is now possible thanks to support for separate address spaces. A .repl file with two CPU cores each having access to a special peripheral that functions as its memory can look the following way:

ram: Memory.MappedMemory @ sysbus 0x0
    size: 0x2000000

cpu1: CPU.RiscV64 @ sysbus
    cpuType: "rv64imacfd"
    privilegeArchitecture: PrivilegeArchitecture.Priv1_10
    timeProvider: empty
    hartId: 0

cpu2: CPU.RiscV64 @ sysbus
    cpuType: "rv64imafdc"
    privilegeArchitecture: PrivilegeArchitecture.Priv1_10
    timeProvider: empty
    hartId: 1

uart: UART.SiFive_UART @ sysbus 0x50230000

core1_mem: Memory.MappedMemory @ sysbus new Bus.BusPointRegistration {
        address: 0x3000000; 
        cpu: cpu1 
    }     
    size: 0x1000 

core2_mem: Memory.MappedMemory @ sysbus new Bus.BusPointRegistration {
        address: 0x3000000; 
        cpu: cpu2 
    } 
    size: 0x1000

On this platform, each core is assigned its own memory. This memory is located at 0x3000000 and will return different values depending on the core that tries to access it. cpu1 will find core1_mem under this address and cpu2 will find core2_mem. Those memory peripherals can be accessed only by the cpu listed as its parameter. This ensures that the memory can only be accessed by the selected CPU and is not globally accessible. The sysbus of the machine first checks the global mapping of the peripherals and only if the mapping is not found, the CPU-specific mapping is checked. It is important to remember this, as the global registration may shadow a per-core peripheral.

Testing the isolation of your SoC

The Monitor, Renode's CLI, provides you with many useful commands that can be used in the context of a specific CPU. The most basic and versatile one is loading an .elf file:

(machine-0) sysbus LoadELF @file.elf cpu0

Note that the last parameter, which designates the CPU name, is optional. Other files, like .hex files, can also be loaded in a similar fashion:

(machine-0) sysbus LoadHEX @file.hex cpu0

You can read from the common memory (in our example, the ram peripheral, starting at 0x0) of two cores when you provide an appropriate address. The values read by each core will be the same, as shown in the Renode's Monitor:

(machine-0) sysbus WriteDoubleWord 0x1400000 0xDEADF00D
(machine-0) sysbus ReadDoubleWord 0x1400000 sysbus.cpu1
0xDEADF00D
(machine-0) sysbus ReadDoubleWord 0x1400000 sysbus.cpu2
0xDEADF00D

You can write values directly to the memory of a specified core by accessing the target peripheral instead of sysbus:

(machine-0) sysbus.core1_mem WriteDoubleWord 0x100 0xFEEDFACE

The same effect can be achieved using sysbus with a context:

(machine-0) sysbus WriteDoubleWord 0x3000100 0xFEEDFACE sysbus.cpu1

Keep in mind that for direct access to the memory object you have to provide relative offsets (in this case: 0x100), while you use absolute offsets when accessing sysbus (in this case: 0x3000100).

Reading from the address in the memory of your core uses a similar syntax:

(machine-0) sysbus ReadDoubleWord 0x3000000 sysbus.cpu1

You can also disassemble code from the memory of your core:

(machine-0) sysbus.cpu1 DisassembleBlock 0x3000000

You can find more examples of the interaction with the core-specific registration in this Robot Framework test suite. The Robot Framework also enables you to generate a detailed report from the test and inspect its execution line-by-line.

Future development

In the future, we plan to extend our current separate address space implementation with proper multiple bus support to improve multi-core support in Renode. Our aim is to make this solution more flexible and enable other peripherals to use separate address spaces.

Thanks to features like isolated address spaces, Renode addresses complex use cases, from the level of SoC design and development to network traffic analysis in connected multi-node systems. With Antmicro, you can develop truly secure systems and test them on real, production binaries. If you want to develop complex, multi-core solutions, don't hesitate to contact us at contact@antmicro.com to find out how we can help you. We offer complex engineering services to help you with every step of your project and give you the expertise you need.

Go back