Risc-V Architecture

Risc-V64 has 32 Registers. There’s also a 128 bit version with 64 registers, and a Risc-V32E (E for embedded) with 16 registers.

Some notes:

Each register can be referred to as x0-x31, or its abi name, which corresponds more directly to what it does:

i.e. zero always returns zero.

In machine/supervisor mode, the registers x8-x12 have different meanings to be used. As well, mhartid exists, which can be read using csrr. You can place it into a variable, like csrr a0, mhartid to get the core number of the current core, and place it in a0.

RegisterABI NameCallee/Caller SavedNotesMachine ModeSupervisor Mode
x0zero-Constant 000
x1raCaller-savedReturn addressReturn addressReturn address
x2spCallee-savedStack pointerStack pointerStack pointer
x3gp-Global pointerGlobal pointerGlobal pointer
x4tp-Thread pointerThread pointerThread pointer
x5t0Caller-savedTemporaryScratchScratch
x6t1Caller-savedTemporaryScratchScratch
x7t2Caller-savedTemporaryScratchScratch
x8s0/fpCallee-savedSaved register/Frame pointerSavedSaved
x9s1Callee-savedSaved registerSavedSaved
x10a0Caller-savedFunction argument / return valueArgument/return valueArgument/return value
x11a1Caller-savedFunction argument / return valueArgument/return valueArgument/return value
x12a2Caller-savedFunction argumentArgumentArgument
x13a3Caller-savedFunction argumentArgumentArgument
x14a4Caller-savedFunction argumentArgumentArgument
x15a5Caller-savedFunction argumentArgumentArgument
x16a6Caller-savedFunction argumentArgumentArgument
x17a7Caller-savedFunction argument / system call IDArgumentArgument
x18s2Callee-savedSaved registerSavedSaved
x19s3Callee-savedSaved registerSavedSaved
x20s4Callee-savedSaved registerSavedSaved
x21s5Callee-savedSaved registerSavedSaved
x22s6Callee-savedSaved registerSavedSaved
x23s7Callee-savedSaved registerSavedSaved
x24s8Callee-savedSaved registerSavedSaved
x25s9Callee-savedSaved registerSavedSaved
x26s10Callee-savedSaved registerSavedSaved
x27s11Callee-savedSaved registerSavedSaved
x28t3Caller-savedTemporaryScratchScratch
x29t4Caller-savedTemporaryScratchScratch
x30t5Caller-savedTemporaryScratchScratch
x31t6Caller-savedTemporaryScratchScratch
---Program Counterpc (Next instruction address)pc (Next instruction address)
---Hart IDmhartid (Hart ID)-
x8s0/fpCallee-savedStatus Registermstatussstatus
x9s1Callee-savedException Program Countermepcsepc
x10a0Caller-savedTrap Vectormtvecstvec
x11a1Caller-savedTrap Causemcausescause
x12a2Caller-savedScratch Registermscratchsscratch
---Trap Valuemtval (Faulting address or instruction)stval (Faulting address or instruction)
---Cycle Countermcycle (Counts CPU cycles)-
---Machine Timermtime (Real-time clock ticks)-

Registers

  • zero always returns zero. This is useful when you might need to get a zero value for an operation. You can always use zero, e.g. for XORing, or copying, i.e. add x5, x1, x0 copies x1 to x5.
  • ra is the return address jal ra, func would jump to func and store the return address in ra, so you can ret out of func and get back to ra without having to spill to the stack.
  • sp is the stack pointer. You can use it to set up the stack addi sp, sp, -16 (allocate two double words of stack space).
  • gp is the global pointer. It points to static data, la gp, _global. It’s used by the compiler to jump to data via offsets quickly.
  • tp is the thread pointer. In xv6 it’s used to hold the mhartid value, the core number for the thread.
  • pc is the program counter. It holds the next instruction address.

Function Args/Working Args

  • a0 and a1 are for function args and return values.
  • a0 through a7 are for function arguments in general.

Temporary Registers

  • t0 through t6 are temporary registers, and can be used inside a function or to do anything. They’re all caller saved, so a function can do anything to them.

Callee-Saved

  • s0 through s11 are callee-saved registers, so if a function uses them, it must save them to the stack and restore them before returning.

Context Switches

During a context switch, the kernel has to save all the user mode registers, and then restore them when choosing another process to run.

This can be seen in kernelvec.S, which saves registers onto the stack, calls kerneltrap to process the trap, and then restores all registers except the tp.

.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
        # make room to save registers.
        addi sp, sp, -256
 
        # save caller-saved registers.
        sd ra, 0(sp)
        sd sp, 8(sp)
        sd gp, 16(sp)
        sd tp, 24(sp)
        sd t0, 32(sp)
        sd t1, 40(sp)
        sd t2, 48(sp)
        sd a0, 72(sp)
        sd a1, 80(sp)
        sd a2, 88(sp)
        sd a3, 96(sp)
        sd a4, 104(sp)
        sd a5, 112(sp)
        sd a6, 120(sp)
        sd a7, 128(sp)
        sd t3, 216(sp)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)
 
        # call the C trap handler in trap.c
        call kerneltrap
 
        # restore registers.
        ld ra, 0(sp)
        ld sp, 8(sp)
        ld gp, 16(sp)
        # not tp (contains hartid), in case we moved CPUs
        ld t0, 32(sp)
        ld t1, 40(sp)
        ld t2, 48(sp)
        ld a0, 72(sp)
        ld a1, 80(sp)
        ld a2, 88(sp)
        ld a3, 96(sp)
        ld a4, 104(sp)
        ld a5, 112(sp)
        ld a6, 120(sp)
        ld a7, 128(sp)
        ld t3, 216(sp)
        ld t4, 224(sp)
        ld t5, 232(sp)
        ld t6, 240(sp)
 
        addi sp, sp, 256
 
        # return to whatever we were doing in the kernel.
        sret

Risc-V Modes

There are three modes in Risc-V, machine, supervisor, and user.

Machine Mode

  • The highest, most powerful mode.
  • This mode is entered after turning on the computer, and is used for startup of the kernel, but is quickly left to get into supervisor mode to run the kernel. entry.S is the only machine mode code that gets executed.
  • Timer interrupts require using machine mode.

Supervisor Mode

  • Kernel code runs in supervisor mode.
  • Some instructions are privileged, and can only be run in machine or supervisor mode.

User Mode

  • Userspace runs in this mode.
  • Privileged instructions cause a trap + abort.

Control and Status Registers (CSRs)

There are special instructions that start with csr, and can only be accessed in a privileged mode.

  • csrr a0, sstatus to read sstatus and a0
  • csrw sstatus, a0 to write sstatus to a0
  • csrrw a0, mscratch, a0 to swap atomically
Machine Mode CSRSupervisor Mode CSRPurpose in xv6
mstatussstatusTracks CPU status, privilege mode, and interrupt enable bits.
mepcsepcStores the return address after an exception or system call.
mcausescauseIndicates the reason for a trap, exception, or interrupt.
mtvecstvecHolds the address of the trap handler function.
mtvalstvalStores faulting instruction address or memory address in exceptions.
mscratchsscratchTemporary storage for traps (e.g., saving registers).
mideleg-Delegates interrupts from machine mode to supervisor mode.
medeleg-Delegates exceptions from machine mode to supervisor mode.
mhartid-Stores the hardware thread (hart) ID, used for multi-core processing.
mipsipPending interrupts (Supervisor Timer Interrupt - STIP is used in xv6).
miesieEnables interrupts (Supervisor Timer Interrupt - STIE is used in xv6).
satpsatpHolds the physical address of the page table root, used for virtual memory.
pmpcfg0-PMP configuration register (used to control memory access rules).
pmpaddr0-First PMP address register (used to define memory access boundaries).

Traps, Exceptions and Interrupts

In Risc-V, traps occur when user mode code loses control.

Exceptions

Exceptions are synchronous and generally caused by user code. For example, syscalls willingly give up control to the kernel, and program errors in user code traps to the kernel, where it can handle those faults.

Some program errors include:

  • Illegal Instructions
  • Alignment Error
  • Memory Access/Page Fault

Interrupts

Interrupts are generally asynchronous and come from outside user code. They can come from:

  • Timers
  • Software Interrupts (caused by timer interrupts)
  • Devices (UART)

Special “Registers”

These aren’t registers because you can’t write to them, but you can read them to a register:

Mhartid

mhartid contains the core number, and is moved to tp during startup. cpuid() returns the value of tp, and thus, mhartid in xv6.

In user mode, tp can be overwritten, but the kernel will restore tp from the stack after a trap, so user mode code can happily write to tp, and the kernel will always have the value of it in supervisor mode.

Pmpcfg0 and Pmpaddr0

pmp stands for Physical Memory Protection and limits access to physical memory for code in S and M mode.

This is used for secure bootstrapping and VMs.