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

Chapter 10: Machine Mode, SBI & Supervisor Mode

Part VI — Booting & System Software


🎯 Learning Objectives

After reading this chapter, you will be able to:

  1. Understand SBI Architecture: Grasp the layered design and value of the Supervisor Binary Interface
  2. Master SBI Calling Convention: Know the roles of a7 (EID), a6 (FID), a0-a5 (Args)
  3. Implement SBI Calls: Make ecall requests to OpenSBI services
  4. Understand Exception Delegation: Know how M-mode delegates traps to S-mode
  5. Distinguish M-mode and S-mode Responsibilities: Understand why RISC-V encourages thin M-mode

💡 Scenario: Please Get the Manager

Scene: Junior is pulling his hair at the screen—the UART driver from the last chapter doesn’t work on the new board.

Junior: “Senior, I’m going crazy. Remember the UART driver we wrote last chapter? I just switched to a different board to try it out, and after digging through the datasheet, I found this board’s UART address is 0x54000000, not 0x10000000. Do I have to modify the code every time I switch boards?”

Senior: “That’s exactly why we need SBI (Supervisor Binary Interface). What you’re doing now is like going to a restaurant and rushing into the kitchen to cook your own food. Change restaurants (hardware), the kitchen layout is different, and you don’t know how to cook anymore.”

Junior: “So what should I do?”

Senior: “You need to learn to ‘call the manager.’ In RISC-V, M-mode (OpenSBI) is that manager.

You (S-mode Kernel) just need to sit at your table and use the standard format (SBI Call) to shout: ‘Manager, please print a character for me!’

The manager receives your request, looks up this restaurant’s kitchen layout, and prints the character for you. This way, no matter which restaurant you go to, as long as you know how to call the manager, you’re fine.“

Junior: “Sounds much easier! How do I call?”

Senior: “Use the ecall instruction. But before calling, you need to write your request on specific ‘sticky notes’ (Registers):

RegisterPurposeAnalogy
a7Extension ID (EID)Which department?
a6Function ID (FID)What service?
a0-a5ArgumentsService parameters
a0, a1Return ValuesManager’s reply

Come on, let’s try it.“


Machine mode is RISC-V’s highest privilege level, with unrestricted access to all hardware resources. But with great power comes great responsibility—M-mode firmware must be minimal, robust, and provide essential services to supervisor mode software. This chapter explores how to design M-mode firmware, implement SBI services, and support advanced features like virtualization and security.

Unlike monolithic firmware architectures, RISC-V encourages a thin M-mode layer that delegates most functionality to S-mode. This design philosophy keeps M-mode simple and portable while allowing rich OS features in S-mode. We’ll examine M-mode firmware design patterns, the Supervisor Binary Interface (SBI) specification, hypervisor support through the H extension, and security features like Physical Memory Protection (PMP) and the WorldGuard extension.


10.1 Machine Mode Firmware Design

Minimal M-mode Firmware

The RISC-V philosophy is to keep M-mode firmware as small as possible. A minimal M-mode firmware might be only a few kilobytes, providing just enough functionality to boot S-mode software.

Minimal M-mode responsibilities:

  1. Early hardware initialization: DRAM, clocks, reset
  2. Platform-specific setup: Configure peripherals
  3. SBI runtime services: Timer, IPI, RFENCE, console
  4. Exception delegation: Pass most traps to S-mode
  5. Boot S-mode software: Set up and jump to OS

Example minimal M-mode firmware structure:

// Minimal M-mode firmware
void m_mode_main(unsigned long hartid, void *fdt) {
    // 1. Initialize platform
    platform_init();
    
    // 2. Set up trap handler
    write_csr(mtvec, (uintptr_t)&m_trap_entry);
    
    // 3. Configure PMP
    setup_pmp();
    
    // 4. Delegate exceptions and interrupts
    write_csr(medeleg, 0xb1ff);  // Delegate most exceptions
    write_csr(mideleg, 0x0222);  // Delegate S-mode interrupts
    
    // 5. Start other harts (if multi-core)
    if (hartid == 0) {
        for (int i = 1; i < num_harts; i++) {
            start_hart(i);
        }
    }
    
    // 6. Boot S-mode payload
    boot_next_stage(hartid, fdt);
}

Platform-Specific Initialization

Each RISC-V platform has unique hardware that must be initialized. M-mode firmware abstracts these details through a platform layer.

Platform initialization example:

// Platform-specific initialization
struct platform_ops {
    int (*early_init)(void);
    int (*final_init)(void);
    void (*console_putc)(char c);
    int (*console_getc)(void);
    void (*timer_init)(void);
    void (*ipi_send)(unsigned long hartid);
    void (*system_reset)(void);
};

// SiFive FU740 platform
static struct platform_ops fu740_ops = {
    .early_init = fu740_early_init,
    .final_init = fu740_final_init,
    .console_putc = uart_putc,
    .console_getc = uart_getc,
    .timer_init = clint_timer_init,
    .ipi_send = clint_ipi_send,
    .system_reset = fu740_system_reset,
};

int platform_init(void) {
    struct platform_ops *ops = &fu740_ops;
    
    // Early initialization
    if (ops->early_init)
        ops->early_init();
    
    // Initialize console
    if (ops->console_putc)
        sbi_console_init(ops->console_putc, ops->console_getc);
    
    // Initialize timer
    if (ops->timer_init)
        ops->timer_init();
    
    // Final initialization
    if (ops->final_init)
        ops->final_init();
    
    return 0;
}

Runtime Services

M-mode firmware provides runtime services to S-mode through the SBI interface. These services remain active after S-mode boots.

Core runtime services:

  • Timer: Set timer interrupts (sbi_set_timer)
  • IPI: Send inter-processor interrupts (sbi_send_ipi)
  • RFENCE: Remote fence operations (sbi_remote_fence_i, sbi_remote_sfence_vma)
  • Console: Debug output (sbi_console_putchar, sbi_console_getchar)
  • Hart management: Start/stop harts (sbi_hart_start, sbi_hart_stop)
  • System reset: Reboot/shutdown (sbi_system_reset)

10.2 SBI Call Interface

SBI Call Mechanism

S-mode software invokes SBI services using the ecall instruction. This traps to M-mode, which handles the request and returns to S-mode.

Figure 10.1: SBI Call Flow

sequenceDiagram
    participant S as S-mode<br/>(Linux)
    participant M as M-mode<br/>(OpenSBI)
    participant HW as Hardware<br/>(CLINT/PLIC)
    
    S->>S: Prepare SBI call<br/>(a7=EID, a6=FID, a0-a5=params)
    S->>M: ecall
    Note over M: Trap to M-mode<br/>mcause = 9 (ecall from S-mode)
    M->>M: Decode EID/FID
    M->>HW: Perform operation<br/>(e.g., set timer)
    HW-->>M: Operation complete
    M->>M: Set return value (a0)
    M->>S: mret
    Note over S: Resume S-mode<br/>Check a0 for result

SBI Calling Convention

SBI calls use a standard register convention:

Input registers:

  • a7: Extension ID (EID) — Identifies the SBI extension
  • a6: Function ID (FID) — Identifies the function within the extension
  • a0-a5: Function parameters (up to 6 parameters)

Output registers:

  • a0: Error code (0 = success, negative = error)
  • a1: Return value (optional, function-specific)

Preserved registers: All registers except a0-a1 are preserved across SBI calls.

Example: Send IPI

// S-mode code: Send IPI to hart 1
static inline long sbi_send_ipi(unsigned long hart_mask,
                                unsigned long hart_mask_base) {
    register unsigned long a0 asm("a0") = hart_mask;
    register unsigned long a1 asm("a1") = hart_mask_base;
    register unsigned long a6 asm("a6") = SBI_EXT_IPI_SEND_IPI;
    register unsigned long a7 asm("a7") = SBI_EXT_IPI;
    
    asm volatile("ecall"
                 : "+r"(a0), "+r"(a1)
                 : "r"(a6), "r"(a7)
                 : "memory");
    
    return a0;  // Return error code
}

// Usage
sbi_send_ipi(1 << 1, 0);  // Send IPI to hart 1

SBI Error Codes

SBI functions return standard error codes:

#define SBI_SUCCESS                0
#define SBI_ERR_FAILED            -1
#define SBI_ERR_NOT_SUPPORTED     -2
#define SBI_ERR_INVALID_PARAM     -3
#define SBI_ERR_DENIED            -4
#define SBI_ERR_INVALID_ADDRESS   -5
#define SBI_ERR_ALREADY_AVAILABLE -6
#define SBI_ERR_ALREADY_STARTED   -7
#define SBI_ERR_ALREADY_STOPPED   -8

Error handling:

long ret = sbi_send_ipi(hart_mask, 0);
if (ret < 0) {
    switch (ret) {
    case SBI_ERR_INVALID_PARAM:
        pr_err("Invalid hart mask\n");
        break;
    case SBI_ERR_FAILED:
        pr_err("IPI send failed\n");
        break;
    default:
        pr_err("Unknown error: %ld\n", ret);
    }
}

10.3 SBI Standard Extensions

SBI defines multiple extensions, each providing related functionality. Extensions are identified by EID (Extension ID).

Timer Extension (EID = 0x54494D45)

The Timer extension provides timer interrupt services.

Function: sbi_set_timer (FID = 0)

Sets the timer to fire at a specific time value.

// Set timer to fire in 1 second
uint64_t current_time = rdtime();
uint64_t next_time = current_time + 10000000;  // 10 MHz clock

register unsigned long a0 asm("a0") = next_time;
register unsigned long a6 asm("a6") = 0;  // FID_SET_TIMER
register unsigned long a7 asm("a7") = 0x54494D45;  // EID_TIME
asm volatile("ecall" : "+r"(a0) : "r"(a6), "r"(a7) : "memory");

M-mode implementation:

void sbi_set_timer(uint64_t stime_value) {
    unsigned long hartid = current_hartid();

    // Write to CLINT mtimecmp register
    volatile uint64_t *mtimecmp = (uint64_t *)(CLINT_BASE + 0x4000 + hartid * 8);
    *mtimecmp = stime_value;

    // Clear pending timer interrupt
    csr_clear(CSR_MIP, MIP_STIP);
}

IPI Extension (EID = 0x735049)

The IPI extension sends inter-processor interrupts.

Function: sbi_send_ipi (FID = 0)

Sends IPI to a set of harts specified by a hart mask.

// Send IPI to harts 1, 2, 3
unsigned long hart_mask = 0b1110;  // Bits 1, 2, 3 set
sbi_send_ipi(hart_mask, 0);

M-mode implementation:

int sbi_send_ipi(unsigned long hart_mask, unsigned long hart_mask_base) {
    for (int i = 0; i < 64; i++) {
        if (hart_mask & (1UL << i)) {
            unsigned long hartid = hart_mask_base + i;

            // Write to CLINT MSIP register
            volatile uint32_t *msip = (uint32_t *)(CLINT_BASE + hartid * 4);
            *msip = 1;
        }
    }
    return SBI_SUCCESS;
}

RFENCE Extension (EID = 0x52464E43)

The RFENCE extension performs remote fence operations (TLB flush, I-cache flush) on other harts.

Functions:

  • sbi_remote_fence_i (FID = 0): Flush instruction cache
  • sbi_remote_sfence_vma (FID = 1): Flush TLB entries
  • sbi_remote_sfence_vma_asid (FID = 2): Flush TLB entries for specific ASID

Example: Remote TLB flush

// Flush TLB on harts 1-3 for address range 0x80000000-0x80001000
unsigned long hart_mask = 0b1110;
unsigned long start_addr = 0x80000000;
unsigned long size = 0x1000;

register unsigned long a0 asm("a0") = hart_mask;
register unsigned long a1 asm("a1") = 0;  // hart_mask_base
register unsigned long a2 asm("a2") = start_addr;
register unsigned long a3 asm("a3") = size;
register unsigned long a6 asm("a6") = 1;  // FID_REMOTE_SFENCE_VMA
register unsigned long a7 asm("a7") = 0x52464E43;  // EID_RFENCE
asm volatile("ecall" : "+r"(a0) : "r"(a1), "r"(a2), "r"(a3), "r"(a6), "r"(a7) : "memory");

M-mode implementation:

int sbi_remote_sfence_vma(unsigned long hart_mask, unsigned long hart_mask_base,
                          unsigned long start_addr, unsigned long size) {
    // Send IPI to target harts
    for (int i = 0; i < 64; i++) {
        if (hart_mask & (1UL << i)) {
            unsigned long hartid = hart_mask_base + i;

            // Store fence parameters for target hart
            remote_fence_info[hartid].start = start_addr;
            remote_fence_info[hartid].size = size;
            remote_fence_info[hartid].type = FENCE_SFENCE_VMA;

            // Send IPI
            clint_send_ipi(hartid);
        }
    }

    // Wait for completion (optional, depends on implementation)
    return SBI_SUCCESS;
}

// IPI handler on target hart
void handle_remote_fence_ipi(void) {
    struct remote_fence_info *info = &remote_fence_info[current_hartid()];

    if (info->type == FENCE_SFENCE_VMA) {
        // Perform sfence.vma
        if (info->size == 0) {
            asm volatile("sfence.vma" ::: "memory");
        } else {
            // Flush specific range (implementation-specific)
            for (unsigned long addr = info->start;
                 addr < info->start + info->size;
                 addr += PAGE_SIZE) {
                asm volatile("sfence.vma %0" :: "r"(addr) : "memory");
            }
        }
    }
}

HSM Extension (EID = 0x48534D)

The Hart State Management (HSM) extension controls hart lifecycle.

Functions:

  • sbi_hart_start (FID = 0): Start a hart
  • sbi_hart_stop (FID = 1): Stop current hart
  • sbi_hart_get_status (FID = 2): Get hart status

Example: Start a hart

// Start hart 1 at address 0x80200000 with argument 0x12345678
unsigned long hartid = 1;
unsigned long start_addr = 0x80200000;
unsigned long opaque = 0x12345678;

register unsigned long a0 asm("a0") = hartid;
register unsigned long a1 asm("a1") = start_addr;
register unsigned long a2 asm("a2") = opaque;
register unsigned long a6 asm("a6") = 0;  // FID_HART_START
register unsigned long a7 asm("a7") = 0x48534D;  // EID_HSM
asm volatile("ecall" : "+r"(a0) : "r"(a1), "r"(a2), "r"(a6), "r"(a7) : "memory");

M-mode implementation:

int sbi_hart_start(unsigned long hartid, unsigned long start_addr, unsigned long opaque) {
    if (hartid >= num_harts)
        return SBI_ERR_INVALID_PARAM;

    if (hart_state[hartid] != HART_STOPPED)
        return SBI_ERR_ALREADY_STARTED;

    // Set up hart entry point
    hart_entry_addr[hartid] = start_addr;
    hart_entry_arg[hartid] = opaque;

    // Wake up hart (platform-specific)
    platform_hart_start(hartid);

    hart_state[hartid] = HART_STARTED;
    return SBI_SUCCESS;
}

System Reset Extension (EID = 0x53525354)

The System Reset extension provides system-wide reset and shutdown.

Function: sbi_system_reset (FID = 0)

// Reboot the system
#define SBI_RESET_TYPE_SHUTDOWN  0
#define SBI_RESET_TYPE_COLD_REBOOT  1
#define SBI_RESET_TYPE_WARM_REBOOT  2

register unsigned long a0 asm("a0") = SBI_RESET_TYPE_COLD_REBOOT;
register unsigned long a1 asm("a1") = 0;  // Reset reason
register unsigned long a6 asm("a6") = 0;  // FID_SYSTEM_RESET
register unsigned long a7 asm("a7") = 0x53525354;  // EID_SRST
asm volatile("ecall" : "+r"(a0) : "r"(a1), "r"(a6), "r"(a7) : "memory");
// This call does not return

Figure 10.2: SBI Extensions Overview

graph TB
    subgraph "SBI Extensions"
        BASE[Base Extension<br/>0x10<br/>Version, Features]
        TIME[Timer Extension<br/>0x54494D45<br/>Set Timer]
        IPI[IPI Extension<br/>0x735049<br/>Send IPI]
        RFENCE[RFENCE Extension<br/>0x52464E43<br/>Remote Fence]
        HSM[HSM Extension<br/>0x48534D<br/>Hart Management]
        SRST[System Reset<br/>0x53525354<br/>Reset/Shutdown]
        PMU[PMU Extension<br/>0x504D55<br/>Performance Counters]
    end

    SMODE[S-mode Software<br/>Linux, FreeBSD] -->|ecall| BASE
    SMODE -->|ecall| TIME
    SMODE -->|ecall| IPI
    SMODE -->|ecall| RFENCE
    SMODE -->|ecall| HSM
    SMODE -->|ecall| SRST
    SMODE -->|ecall| PMU

    style SMODE fill:#e1f5ff
    style TIME fill:#ccffcc
    style IPI fill:#ffffcc
    style RFENCE fill:#ffcccc

10.4 Console and Debug Output

Console I/O via SBI

SBI provides simple console I/O for early debugging before full UART drivers are available.

Legacy console functions (deprecated but widely used):

  • sbi_console_putchar (EID = 0x01): Output one character
  • sbi_console_getchar (EID = 0x02): Input one character

Example: Early printk

void sbi_putchar(char c) {
    register unsigned long a0 asm("a0") = c;
    register unsigned long a7 asm("a7") = 0x01;  // Legacy console putchar
    asm volatile("ecall" : "+r"(a0) : "r"(a7) : "memory");
}

void early_printk(const char *str) {
    while (*str) {
        if (*str == '\n')
            sbi_putchar('\r');
        sbi_putchar(*str++);
    }
}

// Usage
early_printk("Hello from S-mode!\n");

M-mode implementation:

void sbi_console_putchar(int ch) {
    // Platform-specific UART output
    uart_putc(ch);
}

int sbi_console_getchar(void) {
    // Platform-specific UART input
    return uart_getc();
}

Modern approach: Use the Debug Console Extension (DBCN) for more features (buffered I/O, formatted output).


10.5 Hypervisor Extension (H Extension)

Virtualization Support in RISC-V

The Hypervisor extension (H) adds virtualization support to RISC-V, enabling a hypervisor to run multiple guest operating systems. Unlike ARM’s built-in EL2, RISC-V virtualization is an optional extension.

Key features:

  • VS-mode and VU-mode: Virtualized supervisor and user modes
  • Two-stage address translation: Guest physical → Host physical
  • Virtual interrupts: Virtualized interrupt delivery
  • Hypervisor CSRs: Control virtualization features

Privilege modes with H extension:

  • M-mode: Machine mode (firmware)
  • HS-mode: Hypervisor-extended supervisor mode (hypervisor)
  • VS-mode: Virtual supervisor mode (guest OS)
  • U-mode: User mode (applications)
  • VU-mode: Virtual user mode (guest applications)

Figure 10.3: RISC-V Virtualization Architecture

graph TB
    subgraph "M-mode"
        OPENSBI[OpenSBI<br/>Firmware]
    end

    subgraph "HS-mode (Hypervisor)"
        KVM[KVM/Xen<br/>Hypervisor]
    end

    subgraph "VS-mode (Guest OS)"
        GUEST1[Guest Linux 1]
        GUEST2[Guest Linux 2]
    end

    subgraph "VU-mode (Guest Apps)"
        APP1[App 1]
        APP2[App 2]
        APP3[App 3]
    end

    OPENSBI -->|SBI calls| KVM
    KVM -->|VM Entry/Exit| GUEST1
    KVM -->|VM Entry/Exit| GUEST2
    GUEST1 --> APP1
    GUEST1 --> APP2
    GUEST2 --> APP3

    style OPENSBI fill:#ffe1e1
    style KVM fill:#fff4e1
    style GUEST1 fill:#e1ffe1
    style GUEST2 fill:#e1ffe1
    style APP1 fill:#e1f5ff
    style APP2 fill:#e1f5ff
    style APP3 fill:#e1f5ff

Two-Stage Address Translation

With the H extension, address translation happens in two stages:

  1. First stage (G-stage): Guest virtual address (GVA) → Guest physical address (GPA)

    • Controlled by guest OS (vsatp CSR)
    • Guest thinks it’s managing physical memory
  2. Second stage (H-stage): Guest physical address (GPA) → Host physical address (HPA)

    • Controlled by hypervisor (hgatp CSR)
    • Translates guest “physical” addresses to real physical addresses

Figure 10.4: Two-Stage Address Translation

graph LR
    GVA[Guest Virtual<br/>Address<br/>GVA] -->|G-stage<br/>vsatp| GPA[Guest Physical<br/>Address<br/>GPA]
    GPA -->|H-stage<br/>hgatp| HPA[Host Physical<br/>Address<br/>HPA]
    HPA --> MEM[Physical<br/>Memory]

    style GVA fill:#e1f5ff
    style GPA fill:#fff4e1
    style HPA fill:#e1ffe1
    style MEM fill:#ffcccc

Example:

  • Guest OS maps virtual address 0x1000 to guest physical address 0x80001000 (using vsatp)
  • Hypervisor maps guest physical 0x80001000 to host physical 0x90001000 (using hgatp)
  • Final access: 0x1000 (GVA) → 0x80001000 (GPA) → 0x90001000 (HPA)

Virtual Interrupt Handling

The H extension virtualizes interrupts, allowing the hypervisor to inject interrupts into guest VMs.

Hypervisor interrupt CSRs:

  • hvip: Hypervisor virtual interrupt pending
  • hie: Hypervisor interrupt enable
  • hgeip: Hypervisor guest external interrupt pending

Injecting a virtual interrupt:

// Hypervisor code: Inject timer interrupt into guest
void inject_guest_timer_interrupt(void) {
    // Set virtual supervisor timer interrupt pending
    csr_set(CSR_HVIP, HVIP_VSTIP);

    // When guest resumes, it will see a timer interrupt
}

Guest handling:

// Guest OS sees the interrupt as a normal S-mode interrupt
void guest_timer_handler(void) {
    // Handle timer interrupt
    // Guest doesn't know it's virtualized
}

VM Entry and Exit

The hypervisor switches between HS-mode and VS-mode using special instructions and CSR manipulation.

VM Entry (HS-mode → VS-mode):

void vm_enter(struct vcpu *vcpu) {
    // 1. Load guest state
    write_csr(CSR_VSSTATUS, vcpu->vsstatus);
    write_csr(CSR_VSIE, vcpu->vsie);
    write_csr(CSR_VSTVEC, vcpu->vstvec);
    write_csr(CSR_VSSCRATCH, vcpu->vsscratch);
    write_csr(CSR_VSEPC, vcpu->vsepc);
    write_csr(CSR_VSCAUSE, vcpu->vscause);
    write_csr(CSR_VSTVAL, vcpu->vstval);
    write_csr(CSR_VSATP, vcpu->vsatp);

    // 2. Set hstatus.SPV = 1 (virtualization enabled)
    csr_set(CSR_HSTATUS, HSTATUS_SPV);

    // 3. Set sepc to guest entry point
    write_csr(CSR_SEPC, vcpu->pc);

    // 4. Enter VS-mode
    asm volatile("sret");  // Return to VS-mode
}

VM Exit (VS-mode → HS-mode):

When the guest executes certain instructions (ecall, WFI, privileged CSR access) or takes a trap, control returns to the hypervisor.

void vm_exit_handler(struct vcpu *vcpu) {
    // Save guest state
    vcpu->vsstatus = read_csr(CSR_VSSTATUS);
    vcpu->vsepc = read_csr(CSR_VSEPC);
    vcpu->vscause = read_csr(CSR_VSCAUSE);
    vcpu->vstval = read_csr(CSR_VSTVAL);
    vcpu->pc = read_csr(CSR_SEPC);

    // Handle exit reason
    unsigned long cause = read_csr(CSR_SCAUSE);

    switch (cause) {
    case CAUSE_VIRTUAL_SUPERVISOR_ECALL:
        // Guest made hypercall
        handle_hypercall(vcpu);
        break;
    case CAUSE_GUEST_PAGE_FAULT:
        // Guest page fault (G-stage or H-stage)
        handle_guest_page_fault(vcpu);
        break;
    case CAUSE_VIRTUAL_INSTRUCTION:
        // Guest tried to execute privileged instruction
        emulate_instruction(vcpu);
        break;
    default:
        // Other traps
        inject_exception_to_guest(vcpu, cause);
    }
}

10.6 Security Model

Physical Memory Protection (PMP)

PMP is RISC-V’s primary memory protection mechanism, enforced in M-mode. It defines memory regions and access permissions for lower privilege modes.

PMP use cases:

  • Protect M-mode firmware from S-mode
  • Isolate security-critical regions
  • Enforce memory access policies
  • Implement basic TEE (Trusted Execution Environment)

PMP configuration registers:

  • pmpcfg0-pmpcfg15: Configuration for PMP entries (8 entries per register)
  • pmpaddr0-pmpaddr63: Address registers (up to 64 entries)

PMP entry format (pmpcfg):

Bits [7:0] for each entry:
  [7]: L (Lock) - Entry cannot be modified until reset
  [6:5]: Reserved
  [4:3]: A (Address matching mode)
         00 = OFF, 01 = TOR, 10 = NA4, 11 = NAPOT
  [2]: X (Execute permission)
  [1]: W (Write permission)
  [0]: R (Read permission)

Example: Protect M-mode firmware

void protect_m_mode_firmware(void) {
    // Protect 0x80000000 - 0x80100000 (1 MB M-mode firmware)
    // Use TOR (Top-Of-Range) mode

    // Entry 0: Start address (0x80000000)
    write_csr(pmpaddr0, 0x80000000 >> 2);

    // Entry 1: End address (0x80100000)
    write_csr(pmpaddr1, 0x80100000 >> 2);

    // Configure: TOR mode, R+X, Locked
    uint8_t cfg = PMP_R | PMP_X | PMP_TOR | PMP_L;
    write_csr(pmpcfg0, cfg << 8);  // Entry 1 config

    // Now S-mode cannot access 0x80000000 - 0x80100000
}

Enhanced PMP (ePMP)

ePMP extends PMP with additional security features:

  • Rule locking: Prevent modification of PMP entries
  • M-mode lockdown: Restrict M-mode access to specific regions
  • Whitelist mode: Default deny, explicit allow

ePMP adds mseccfg CSR:

Bits:
  [2]: RLB (Rule Locking Bypass) - Allow M-mode to modify locked entries
  [1]: MMWP (Machine Mode Whitelist Policy) - Enforce whitelist for M-mode
  [0]: MML (Machine Mode Lockdown) - Restrict M-mode access

Example: M-mode lockdown

void enable_m_mode_lockdown(void) {
    // Set MML bit: M-mode can only access regions with L=1 and X=0
    write_csr(CSR_MSECCFG, MSECCFG_MML);

    // Now M-mode is restricted to explicitly allowed regions
}

Comparison with ARM TrustZone

RISC-V PMP vs ARM TrustZone:

FeatureRISC-V PMPARM TrustZone
IsolationRegion-based (up to 64 regions)World-based (Secure/Non-secure)
Granularity4 bytes to 2^64 bytes4 KB minimum
PrivilegeM-mode enforcedEL3 enforced
Secure worldNo built-in secure worldDedicated S-EL0/S-EL1
ComplexitySimple, flexibleComplex, rich features
Use caseFirmware protection, basic TEEFull TEE, secure boot, DRM

RISC-V security philosophy: Provide minimal hardware mechanisms (PMP), build rich security features in software (TEE frameworks like Keystone, Penglai).

ARM TrustZone philosophy: Provide rich hardware support for secure world, standardize TEE architecture.


🛠️ Hands-on Lab: Lab 10.1 — Saying Hello Through the Counter (SBI Call)

This lab demonstrates the standard SBI call flow. We’ll use OpenSBI (bundled with QEMU) and place our kernel at 0x80200000 (the default payload address OpenSBI jumps to).

Lab Objectives

  1. Wrap an sbi_call Assembly function
  2. Call Legacy Extension (EID=1) to output characters
  3. Call Base Extension (EID=0x10) to query SBI version

Project Structure

lab10/
├── link.ld     # Memory layout (note the different start address)
├── sbi.S       # SBI Wrapper and entry point
└── kernel.c    # Main program

Code

File 1: link.ld

Note: When using QEMU -bios default (OpenSBI), it loads itself at 0x80000000 and jumps to 0x80200000 to execute our kernel.

OUTPUT_ARCH( "riscv" )
ENTRY( _start )

SECTIONS
{
  /* OpenSBI default jump address */
  . = 0x80200000;

  .text : {
    *(.text.boot)
    *(.text)
  }
  .data : { *(.data) }
  .bss : { *(.bss) }

  . = . + 0x1000;
  _stack_top = .;
}

File 2: sbi.S (SBI Wrapper)

.section .text.boot
.global _start
.global sbi_call

# Program entry
_start:
    la sp, _stack_top
    call kernel_main
loop:
    j loop

# long sbi_call(long ext, long fid, long arg0, long arg1, long arg2)
# C calling: a0=ext, a1=fid, a2=arg0, a3=arg1, a4=arg2
sbi_call:
    mv a7, a0       # ext -> a7 (EID)
    mv a6, a1       # fid -> a6 (FID)
    mv a0, a2       # arg0 -> a0
    mv a1, a3       # arg1 -> a1
    mv a2, a4       # arg2 -> a2

    ecall           # Trigger Environment Call (trap to M-mode)

    ret             # Return a0 (return value)

File 3: kernel.c

#include <stdint.h>

// SBI Extension IDs
#define SBI_EID_CONSOLE_PUTCHAR 0x01  // Legacy Console
#define SBI_EID_BASE            0x10  // Base Extension

// Base Extension Function IDs
#define SBI_FID_GET_SPEC_VERSION 0

// External Assembly function
long sbi_call(long ext, long fid, long arg0, long arg1, long arg2);

// Character output (via SBI)
void putchar(char c) {
    sbi_call(SBI_EID_CONSOLE_PUTCHAR, 0, c, 0, 0);
}

void print_str(const char *s) {
    while (*s) {
        putchar(*s++);
    }
}

// Query SBI version
void print_sbi_version(void) {
    long version = sbi_call(SBI_EID_BASE, SBI_FID_GET_SPEC_VERSION, 0, 0, 0);
    long major = (version >> 24) & 0x7f;
    long minor = version & 0xffffff;

    print_str("SBI Spec Version: ");
    putchar('0' + major);
    putchar('.');
    putchar('0' + minor);
    putchar('\n');
}

void kernel_main(void) {
    print_str("Hello from S-mode via SBI!\n");
    print_sbi_version();

    while (1) {}
}

Compile and Run

# Compile
riscv64-unknown-elf-gcc -nostdlib -nostartfiles -T link.ld \
    -o kernel sbi.S kernel.c

# Run (QEMU with OpenSBI)
qemu-system-riscv64 -machine virt -nographic -bios default -kernel kernel

Expected Output:

OpenSBI v1.2
   ...
Hello from S-mode via SBI!
SBI Spec Version: 1.0

Comparison: Lab 9 vs Lab 10

ItemLab 9 (Bare-metal)Lab 10 (SBI)
Start Address0x800000000x80200000
UART AccessDirect MMIO writeVia SBI ecall
Portability❌ Hardware-dependent✅ Cross-platform
ComplexitySimple but fragileRequires SBI spec knowledge

danieRTOS Reference: The danieRTOS console uses SBI calls for portable output across different RISC-V platforms.


⚠️ Common Pitfalls

Pitfall 1: Confusing EID and FID

Error Scenario: Putting Extension ID in a6 and Function ID in a7.

Consequence: Calls wrong service, may crash or hang the system.

# ❌ Wrong: EID and FID positions swapped
    li a6, 0x10      # This should be EID, but it's in a6
    li a7, 0         # This should be FID, but it's in a7
    ecall

# ✅ Correct
    li a7, 0x10      # a7 = EID (Extension ID)
    li a6, 0         # a6 = FID (Function ID)
    ecall

Pitfall 2: Forgetting OpenSBI Occupies 0x80000000

Error Scenario: When using OpenSBI, still placing kernel at 0x80000000.

Consequence: Kernel overwrites OpenSBI’s memory, causing SBI calls to fail.

/* ❌ Wrong: Conflicts with OpenSBI */
. = 0x80000000;

/* ✅ Correct: Use OpenSBI's default payload address */
. = 0x80200000;

Pitfall 3: Misusing Legacy Extensions

Error Scenario: Using deprecated Legacy Extensions that newer OpenSBI may not support.

Consequence: SBI call returns -2 (SBI_ERR_NOT_SUPPORTED).

// ⚠️ Legacy Extensions (EID 0-8) are marked deprecated
// Newer OpenSBI may not support them
sbi_call(0x01, 0, 'A', 0, 0);  // console_putchar

// ✅ Recommended: Use newer Extensions
// Debug Console Extension (EID = 0x4442434E)
#define SBI_EID_DBCN  0x4442434E
#define SBI_FID_DBCN_WRITE_BYTE 2
sbi_call(SBI_EID_DBCN, SBI_FID_DBCN_WRITE_BYTE, 'A', 0, 0);

💡 Tip: In production, check if an SBI Extension is available:

// Use Base Extension's probe_extension
#define SBI_FID_PROBE_EXTENSION 3
long result = sbi_call(SBI_EID_BASE, SBI_FID_PROBE_EXTENSION,
                       target_eid, 0, 0);
// result > 0 means the Extension is available

Summary

Machine mode and SBI provide the foundation for RISC-V system software. This chapter covered five key areas:

M-mode firmware is designed to be minimal and platform-specific. It initializes hardware, sets up memory protection, and provides runtime services through the SBI interface. Unlike ARM’s extensive EL3 firmware, RISC-V keeps M-mode simple and delegates most functionality to S-mode.

SBI interface provides a standard ecall-based interface between M-mode firmware and S-mode operating systems. The calling convention uses registers a0-a7 for parameters and returns error codes in a0. This standardization ensures that OS kernels can run on any RISC-V platform without modification.

SBI extensions cover essential system services: Timer extension for scheduling, IPI extension for inter-processor communication, RFENCE extension for TLB synchronization, HSM extension for hart lifecycle management, and System Reset extension for reboot and shutdown. Each extension is identified by an Extension ID (EID) and provides multiple functions.

Hypervisor extension adds virtualization support through VS-mode and VU-mode, enabling guest operating systems to run under a hypervisor. Two-stage address translation (GVA → GPA → HPA) isolates guest memory, while virtual interrupt injection allows the hypervisor to deliver interrupts to guests. VM entry and exit are managed through CSR manipulation and the SRET instruction.

PMP and ePMP provide memory protection by defining access permissions for memory regions. PMP is simpler than ARM TrustZone but sufficient for protecting M-mode firmware and implementing basic trusted execution environments. ePMP adds enhanced security features like M-mode lockdown and rule locking.

In the next chapter, we’ll explore RISC-V ISA extensions, starting with the standard extensions (M, A, F, D, C) and moving to advanced features like Vector and Bit Manipulation.