Simulating SoCs with isolated address spaces in Renode
Published:
Topics: Open OS, Open source tools, Open security / safety
Renode is Antmicro’s open source, multi-architecture hardware simulator that has been helping our customers address numerous use cases involving complex hardware setups over the years. Renode allows you to run the same software you would use on real hardware. To achieve this, it provides a multitude of built-in models of boards, SoCs and SoC building blocks, and interfaces which allow you to couple them together to properly mimic entire physical systems.
To enable a new class of advanced scenarios, we have developed support for multi-core CPUs having access to different devices under the same bus addresses, which we will describe in this note. We are seeing many customer use cases for this feature, with the most prevalent being security.
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.
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 are developing 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.