A processor that runs an operating system has to protect it from the programs it runs. If any program could reprogram the memory protection or turn off interrupts, one bug or one attacker could bring down the whole machine. Hardware provides this protection through privilege levels: modes in which some instructions and registers are off limits. Control and status registers are where those protected settings live. This article explains why privilege exists, the three RISC-V levels, how CSRs work, and how software moves between levels.
12 min read | Intermediate
What You'll Learn
- Explain why a processor needs more than one privilege level
- Describe the RISC-V machine, supervisor and user modes
- Use the instructions that read and write control and status registers
- Explain what the ecall and mret instructions do
Why Privilege Exists
A small embedded program may run alone and own the whole machine. A phone or a server does not work like that. Many programs run at once, written by different people, and none of them can be fully trusted. The operating system (OS) shares the processor, the memory and the devices between them. To do that job, it needs powers the programs do not have. It must be able to stop a program that runs forever, keep one program from reading another's memory, and control the devices.
These things cannot be done by software alone. If a program can execute the same instructions as the OS, nothing stops it from doing what the OS does. So the hardware keeps a small piece of state, the current privilege level, and refuses certain operations when that level is too low. A refused operation does not run. It causes a trap to the higher level, which decides what to do.
The RISC-V Privilege Levels
RISC-V defines three standard levels:
- Machine mode (M) is the highest. It has full access to the hardware, and the processor starts in it after reset. Every RISC-V processor has it, since it is the only mandatory level. Firmware and boot code run here, as does the entire program on a small microcontroller.
- Supervisor mode (S) is for the operating system kernel. It controls virtual memory and handles most traps, but it cannot touch machine-level settings.
- User mode (U) is for applications. It can run ordinary instructions on ordinary registers and on the memory it has been given. Anything else causes a trap.
A simple microcontroller might have only M. A protected embedded system has M and U. A processor that runs a full OS such as Linux has all three. A separate, optional extension adds a hypervisor level for running virtual machines.
Control and Status Registers
The settings of the processor itself live in control and status registers (CSRs). They are separate from the 32 general registers and have their own numbers, in a 12-bit address space. They are read and written by special instructions rather than by loads and stores. RISC-V keeps them in their own extension, Zicsr. These are the main machine-level CSRs:
| CSR | Purpose |
|---|---|
| mstatus | global state: machine interrupt enable (MIE), its saved copy (MPIE), and the previous mode (MPP) |
| mtvec | the address of the trap handler |
| mepc | the PC saved when a trap happens |
| mcause | the reason for the trap |
| mtval | extra information, such as the faulting address |
| mie, mip | which interrupts are enabled, and which are pending |
| mscratch | a spare register for the trap handler |
| mcycle, minstret | 64-bit counters of cycles and retired instructions |
| misa, mhartid | which extensions exist, and which hardware thread this is |
Supervisor mode has its own, smaller set: sstatus, stvec, sepc, scause, stval and satp, the last of which controls address translation. The top bits of a CSR's number encode the lowest privilege that may access it, and whether it is read-only. So the hardware can check an access with a very small amount of logic. A program in user mode that tries to read mstatus gets an illegal instruction exception.
The CSR Instructions
Six instructions work on CSRs. All of them are atomic: each reads the old value and writes the new one as one step, so no interrupt can land between the two.
csrrw rd, csr, rs1 # read csr into rd, write rs1 into it csrrs rd, csr, rs1 # read csr into rd, set the bits that are 1 in rs1 csrrc rd, csr, rs1 # read csr into rd, clear the bits that are 1 in rs1 csrrwi, csrrsi, csrrci # the same, with a 5-bit constant instead of rs1
The assembler offers shorter names. csrr a0, mcause reads a CSR. csrw mtvec, t0 writes one. csrsi mstatus, 8 sets bit 3, which enables machine interrupts. Reading is csrrs with rs1 = x0, which sets no bits. These instructions may have side effects, and the processor must not run them on a wrong path, so a pipeline often stalls or serializes around them. The 5-Stage Pipeline explains why a change made late in the pipeline needs that care.
Quick Check
Q: In machine mode, how do you turn off machine interrupts and find out whether they were on, in one instruction?
Show Answer
csrrci t0, mstatus, 8. It clears bit 3 (MIE) and puts the old value of mstatus in t0. Because the read and the clear are a single step, nothing can happen between them.
Moving Between Levels
A program cannot simply raise its own privilege. If it could, the protection would mean nothing. There is only one way up: a trap, and a trap always goes to a fixed address chosen by the higher level. So entering the kernel always means entering at a point the kernel selected.
A program that wants a service from the OS, such as writing to a file, uses the ecall instruction. It causes a deliberate trap. On Linux on RISC-V, the program puts the number of the system call in a7 and the arguments in a0 to a5:
li a7, 64 # system call number for write li a0, 1 # file descriptor 1, standard output la a1, message # buffer li a2, 13 # length ecall # trap into the kernel
The kernel handles the trap, checks the arguments, does the work, and returns. To go down in privilege, it uses a return instruction: sret from supervisor mode, or mret from machine mode. These read the previous mode from the status register and restore it, then jump to the saved PC. An mret at machine level can start a program in user mode for the first time: the firmware sets MPP to user mode, sets mepc to the program's address, and executes mret.
Traps and Precise Exceptions goes through exactly what the hardware does on a trap.
Delegation
By default, all traps go to machine mode. That would be slow for a full OS, because every page fault would have to pass through firmware. So M mode can delegate chosen traps straight to S mode, using the CSRs medeleg for exceptions and mideleg for interrupts. Normally, the OS handles system calls and page faults directly, with firmware only involved for a few privileged services. On RISC-V, those services go through an agreed interface called the SBI, the Supervisor Binary Interface, so that an OS can run on many machines without knowing their low-level details.
Memory Protection
Privilege alone is not enough: a user program could still write over the kernel's memory, unless the hardware checks addresses. Full systems use virtual memory, where each page has permission bits, as Why Virtual Memory explains. Smaller systems without virtual memory use physical memory protection (PMP): a small set of CSRs that define address regions and what lower levels may do in each. A real-time OS on a microcontroller can use PMP to keep one task from corrupting another, or to protect firmware from everything else.
Where Is This Used?
On a RISC-V phone or server, the layers line up neatly. A firmware layer such as OpenSBI runs in M mode, Linux runs in S mode, and applications run in U mode. Arm has the same idea with its exception levels EL0 to EL3, and x86 has its privilege rings. Secure boot, explained in Boot ROM and Boot Flow, depends on this too: the highest level is set up first, by trusted code, and then it hands control to lower levels with fewer rights.
What the Hardware Must Add
Privilege support does not take much logic, but it touches many places:
- a small register holding the current mode, which programs cannot read directly;
- the CSRs themselves, with decoding and legal-value checks;
- a permission check on every CSR access;
- a check that privileged instructions such as
mretandwfirun only at a suitable level; - logic to take traps and return from them.
Some CSR fields are defined as "WARL", write any values, reads legal values: the hardware may ignore unsupported settings. So software should read back what it wrote, to find out what is really there. For example, a program can write all ones to a PMP address register and read it back to learn the smallest region size the hardware supports. The CSR misa reports which instruction extensions are present in much the same spirit.
Counters and Other Things Users May Read
Not all CSRs are dangerous. Reading the cycle counter is harmless and very useful for measuring performance, so RISC-V provides user-level read-only views of it: cycle, time and instret. On a 32-bit processor, a 64-bit counter takes two reads, for the low and high halves, and the code must handle the low half wrapping around between them. Higher levels can turn off access to those, using mcounteren. That matters for security, because precise timers help some side-channel attacks. The Performance Equation shows what these counters are for.
Quick Check
Q: A user program executes csrw mstatus, zero. What happens?
Show Answer
mstatus is a machine-level CSR, so the access is refused. The instruction does nothing to mstatus, and the processor takes an illegal instruction exception. The handler, in the kernel, will usually end the program.
A Day in the Life of a System Call
Putting the pieces together, here is what happens when a program prints a line. The program in U mode places its arguments in registers and runs ecall. Hardware saves the PC, raises the mode, turns off interrupts and jumps to the handler address set by the kernel. The kernel saves the program's registers, reads the system call number, checks that the buffer really belongs to this program, and passes the bytes to a driver. It then restores the registers, adds 4 to the saved PC, and runs sret. The program carries on at the next instruction, with no idea of how much just happened. Each such trip costs hundreds to thousands of cycles on a large core, which is why programs buffer their output and make fewer calls.
Common Mistakes
Mistake #1: Forgetting that CSR writes can have side effects
Writing a CSR can change how the next instruction behaves, for example by turning on interrupts or changing address translation. A pipeline must make sure instructions already fetched see the new setting, usually by flushing.
Mistake #2: Assuming every CSR bit exists
Many fields are optional or restricted to legal values. Portable code writes a value, then reads it back, or it checks misa before relying on a feature.
Mistake #3: Treating ecall arguments as trusted
A system call handler runs with high privilege on values chosen by a user program. Every pointer and length must be checked before use, or the kernel can be tricked into reading or writing its own memory.
Practice Exercise
Challenge: Boot Into User Mode
Reset leaves the processor in M mode. Write the steps to start a program at address 0x8000_0000 in U mode, with traps going to a handler at 0x0000_0100. MPP is bits 12:11 of mstatus, and value 00 means user mode. (a) Which CSRs do you write? (b) Which instruction starts the program? (c) What happens if the program then runs ecall? (d) Which CSR holds the reason?
Hint
mret sets the mode from MPP and the PC from mepc.
Solution
(a) Write mtvec with 0x100, clear the MPP bits in mstatus (li t0, 0x1800, then csrc mstatus, t0), and write mepc with 0x8000_0000. On a system with memory protection, PMP must also allow user access, or the first fetch faults. (b) mret. (c) The hardware traps to M mode: it saves the PC in mepc, records the old mode in MPP, and jumps to 0x100. (d) mcause, with value 8, environment call from U mode.
Summary
- Privilege levels let an operating system control a machine that runs untrusted programs.
- RISC-V has machine, supervisor and user modes, and only machine mode is mandatory.
- CSRs hold protected settings and are accessed by atomic instructions, with access checked against the current mode.
- The only way up is a trap to a fixed handler;
ecallasks for it on purpose, andmretorsretgo back down. - PMP or virtual memory protect memory, and delegation sends common traps straight to the OS.