Bootstrapping RISC-V Systems: Introducing rv-runtime-generator
Background
System software development presents a fundamental challenge that every bare metal project must solve: bridging the gap between the processor’s minimal boot state and a fully functional execution environment capable of running high-level code. When a processor first transfers control to system software — whether firmware, kernel, hypervisor, or any bare metal application — it operates in a primitive state with no stack, no memory management, and minimal initialization of the processor. This creates an immediate dependency: before any C, Rust, or other high-level language code can execute, developers must craft assembly routines to establish the required runtime environment for the software component.
The RISC-V architecture’s elegant design philosophy amplifies both the opportunity and complexity of this challenge. RISC-V defines two primary base instruction set architectures (ISA) — RV32I (32-bit registers and addresses) and RV64I (64-bit registers and addresses). As articulated in the RISC-V unprivileged specification[1], the ISA represents “the software visible interface to a wide variety of implementations rather than the design of a particular hardware artifact.” It also defines RISC-V Execution Environment Interface (EEI), which standardizes critical aspects of program execution: initial processor state, privilege mode behavior (Machine, Supervisor, and User modes), instruction semantics, and interrupt/exception handling. While this standardization creates opportunities for code reuse and shared expertise across RISC-V projects, the reality is that most projects end up implementing similar runtime initialization routines from scratch.
Recognizing this common need, we(Rivos) developed rv-runtime-generator[2], an open-source Rust-based tool that automates the generation of RISC-V runtime initialization code. Rather than forcing each project to solve the same fundamental problem repeatedly, rv-runtime-generator provides a configurable solution that adapts to diverse system software requirements.
The tool’s comprehensive approach addresses multiple aspects of this challenge:
Assembly Foundation: Generates startup routines that handle processor initialization, stack setup, and the critical transition from assembly to high-level language execution. Support spans both RV32I and RV64I ISAs, accommodating projects targeting everything from embedded microcontrollers to high-performance server systems.
Privilege Mode Flexibility: Accommodates both Machine mode (M-mode) and Supervisor mode (S-mode) execution contexts, enabling use cases ranging from firmware and hypervisors to operating system kernels.
High-Level Integration: Beyond assembly generation, the tool produces Rust bindings and data structure definitions that provide interfaces to the integrating component to work with trap frames, thread pointer blocks, and other runtime constructs.
Customizable Linking: Generates tailored linker scripts that reflect the specific memory layout, ensuring proper placement of code, data, stack, heap as per the requirements of the integrating component.
This automation doesn’t just save development time — it reduces the likelihood of subtle bugs that can plague handwritten assembly code, particularly in the complex interactions between privilege modes, interactions between assembly and high level language, memory management, and interrupt handling that characterize RISC-V system software.
This series of articles walks through the details of rv-runtime-generator implementation and how it can be used by any RISC-V software component to get started with the generated runtime.
Understanding System Software Runtime Requirements
Before diving into the architecture of rv-runtime-generator, it is essential to understand the specific requirements that different system software projects have with respect to their runtime setup.
RISC-V specification refers to a thread of execution as hardware thread (hart). The different scenarios where proper runtime setup becomes essential are:
Primary Boot Initialization
When a hart first gains control at system reset, it begins execution at the reset vector with minimal processor state. This represents the most fundamental bootstrap scenario: transforming the environment of the processor from a nearly blank slate into one capable of executing high-level code.
Trap Handling Transitions
RISC-V unprivileged specification[1] defines an exception as an unusual condition occurring at run time associated with an instruction in the current RISC-V hart. On the other hand, interrupt refers to an external asynchronous event that may cause a RISC-V hart to experience an unexpected transfer of control. Both exceptions and interrupts cause a transfer of execution control which is referred to as a trap. The execution control is transferred to designated trap handlers. Unlike voluntary function calls, these transfers occur at arbitrary points during program execution, potentially interrupting critical operations or occurring in nested sequences.
Context Switching Operations
Multi-context systems — such as operating systems managing multiple processes or hypervisors juggling virtual machines — require context switches between execution environments. While these transitions are software-initiated rather than hardware-imposed, they must interact seamlessly with the same runtime structures used for boot initialization and trap handling, creating a unified framework for state management.
All these scenarios share a common thread: each represents a transition point where the processor’s execution context must be carefully managed to ensure reliable operation of high-level code.
Core Runtime Requirements
Multi-HART Coordination and Boot Orchestration
A RISC-V system can instantiate one or more harts. In the case of a multi-hart system, if all harts come out of reset at the same time, the runtime is responsible for implementing a protocol for selecting a primary boot hart and coordinating the flow between the primary and secondary harts.
Stack Allocation
The runtime is responsible for allocating memory for per-hart stack coming out of reset. Additionally, the runtime needs to be able to perform proper stack management to support nested traps without state corruption.
Hardware State Normalization
The runtime must systematically initialize hardware registers to a valid state coming out of reset. Hardware registers might not have a well known reset default, so the runtime assembly code is responsible for initializing these registers. This register initialization involves configuring essential system registers, including trap handler base registers (mtvec/stvec), status registers, and the global pointer register, among others, to establish proper processor state before program execution.
BSS Initialization
BSS (.bss section) refers to a section in the object file(executable) that is used for statically allocated objects that are not explicitly initialized by the programmer and hence are zero initialized (See reference [3]). However, this section does not occupy any file space in the executable (See reference [4]). Instead, the loader or runtime initialization routine is expected to allocate this section in memory and zero out its contents before control is transferred to the high-level language. This initialization is critical for ensuring correct operation of the program.
Interrupt State Preservation
Trap handlers must preserve sufficient processor state to enable transparent resumption of interrupted code.
Beyond the core assembly-level functionality, practical system software development demands additional capabilities:
- The runtime must provide a comprehensive interface for querying and manipulating runtime structures, including the ability to inspect and update interrupted trap context frames and associated processor context.
- It should support querying boot identification for harts and provide functions to determine base addresses, limits, and sizes of defined memory regions for the executing program.
- The tool must generate linker scripts that define memory layout for integrating components, ensuring proper memory region alignment and allocation while supporting configurable memory mapping schemes based on target architecture requirements.
- The runtime must support extensible initialization sequences that execute immediately following system reset, providing hooks for specialized hardware or software configuration procedures while maintaining proper initialization order dependencies and error handling.
These capabilities collectively ensure that system software development extends beyond basic assembly operations to provide a complete, production-ready development framework suitable for complex system integration scenarios.
rv-runtime-generator addresses the above requirements through a configuration-driven approach that separates policy decisions (what needs to be done) from mechanism implementation (how it’s accomplished). This architecture enables the tool to generate specialized runtime code while maintaining the flexibility essential for diverse RISC-V system software projects. Subsequent articles in this series will examine the architectural design of the rv-runtime-generator tool and provide comprehensive examples demonstrating its application in the rapid development of runtime environments for RISC-V software components.
References:
[1] https://lf-riscv.atlassian.net/wiki/spaces/HOME/pages/16154769/RISC-V+Technical+Specifications
[2] https://github.com/rivosinc/rv-runtime/tree/main/rv-runtime-generator
[3] ISO/IEC 9899:2011, section 6.7.9 Initialization
[4] ELF specification, Special Sections (https://refspecs.linuxfoundation.org/elf/elf.pdf)