02_RISCV
I. Introduction to RISC-V
RISC-V (pronounced "risk-five") is an open-source Instruction Set Architecture based on Reduced Instruction Set Computing (RISC) principles.
-
Origins: Developed in 2010 by researchers at UC Berkeley (Krste Asanović, David Patterson, and students).
-
Motivation: Most traditional ISAs (x86, ARM, MIPS) are commercially protected by patents, preventing practical reproduction. RISC-V is completely open, permitting anyone to construct compatible hardware and software.
-
Design Goals:
-
Suitable for direct native hardware implementation (not just simulation).
-
Avoids "over-architecting" for specific microarchitecture styles (e.g., out-of-order execution) or implementation technologies (FPGA, ASIC).
-
Supports the revised 2008 IEEE-754 floating-point standard.
-
II. RISC-V Principles and Processor State
1. Modular and Extendable Design
RISC-V is kept highly simple by separating its specifications into modular components:
-
User-level ISA Spec: For compute instructions.
-
Privileged ISA Spec: For supervisor-mode instructions.
-
Naming Convention:
RV + Word Width + Extensions.- Example: RV32IMAFD (or RV32G) represents a 32-bit architecture with Integer base, Multiply/Divide, Atomic, Single FP, and Double FP extensions.
2. Processor State (Registers)
The RISC-V machine state consists of:
-
Program Counter (PC): Points to the current instruction.
-
32 Integer Registers (
x0-x31): 32-bit or 64-bit wide.-
x0: Hardwired to0(always contains the value 0). -
x1: By convention, holds the return address on a function call.
-
-
32 Floating-Point Registers (
f0-f31): Can hold single or double-precision values. -
FP Status Register (
fsr): Used for rounding modes and exception reporting.
III. Instruction Encoding Formats
RISC-V uses a hybrid instruction encoding. While it supports variable lengths (16, 32, 48, 64 bits), the base ISA (RV32I) strictly uses 32-bit fixed-length instructions aligned on four-byte memory boundaries. The lowest two bits of standard 32-bit instructions are always 11.
There are six core instruction formats:
| Format Name | Primary Use | Field Structure / Comments |
|---|---|---|
| R-type | Register-to-Register Arithmetic | funct7 | rs2 | rs1 | funct3 | rd | opcode |
| I-type | Immediate Arithmetic & Loads | imm[11:0] | rs1 | funct3 | rd | opcode |
| S-type | Stores to Memory | imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode (Immediate field is split) |
| B-type | Conditional Branches | Similar to S-type, but immediate encodes a branch offset. |
| U-type | Upper Immediates | imm[31:12] | rd | opcode |
| J-type | Unconditional Jumps | imm[20:10, 1, 11, 19:12] | rd | opcode |
IV. Core Instruction Categories (RV32I Base)
1. ALU Instructions (Arithmetic & Logic)
-
R-Type (Register): Uses two source registers (
rs1,rs2) and one destination register (rd).-
Examples:
add,sub,xor,or,and,sll(shift left logical),srl(shift right logical),sra(shift right arithmetic),slt(set less than). -
Bitwise Tricks:
ANDis used to mask/clear bits.XORwith all 1s acts as aNOToperation. Left shifting by \(i\) mathematically multiplies the value by \(2^i\).
-
-
I-Type (Immediate): Uses one source register and a sign-extended 12-bit immediate.
- Examples:
addi,slti,andi,slli. (Note: There is nosubi; you simply useaddiwith a negative immediate).
- Examples:
-
U-Type (Upper Immediate):
-
lui(Load Upper Immediate): Copies a 20-bit constant to the upper 20 bits[31:12]of the destination register and clears the lower 12 bits. -
Loading 32-bit Constants: Requires two steps because a 32-bit constant cannot fit inside a single 32-bit instruction word. You must use
luito set the top 20 bits, followed byaddito set the bottom 12 bits.
-
2. Memory Access (Loads and Stores)
-
Memory Characteristics: RISC-V utilizes Little-Endian byte ordering (the least significant byte is stored at the lowest memory address). Certain data types may have alignment restrictions.
-
Loads (I-Type): Reads data from memory into a register.
-
Syntax:
lw rd, offset(rs1)\(\rightarrow\) \(rd = Memory[rs1 + offset]\) -
Examples:
ld(double),lw(word),lh(halfword),lb(byte),lbu(byte unsigned).
-
-
Stores (S-Type): Writes data from a register into memory.
-
Syntax:
sw rs2, offset(rs1)\(\rightarrow\) \(Memory[rs1 + offset] = rs2\) -
Examples:
sd,sw,sh,sb.
-
3. Control Transfer (Branches and Jumps)
RISC-V does not utilize architecturally visible delay slots. All targets are PC-relative (Target Address = PC + offset).
-
Conditional Branches (B-Type): Compare two registers.
- Examples:
beq(equal),bne(not equal),blt(less than),bge(greater/equal).
- Examples:
-
Unconditional Jumps (J-Type / I-Type):
-
jal(Jump and Link): J-Type. Jumps to a PC-relative target and saves the return address (PC + 4) in the destination register (usuallyx1). -
jalr(Jump and Link Register): I-Type. Jumps to an address calculated byrs1 + immediate, saving the return address inrd.
-
V. Compilation and Assembly Techniques
Compiling Arrays and Loops
When iterating through an array, the index must be scaled by the byte-size of the data type.
- For an array of doublewords (8 bytes), an index \(i\) must be multiplied by 8 (achieved efficiently via
slli reg, reg, 3because \(2^3 = 8\)) to calculate the correct memory offset.
Bounds Check Shortcut
To check if an array index is out of bounds (\(i \ge length\) OR \(i < 0\)), you can use a single unsigned branch instruction:
-
bgeu x20, x11, IndexOutOfBounds -
Why it works: In two's complement, negative numbers appear as extremely large positive numbers when evaluated as unsigned. Therefore,
bgeucatches both the greater-than condition and the negative condition simultaneously.
Handling Far Branches
Because branch instructions (B-type) only have a 12-bit immediate field for the offset, they cannot jump to extremely distant memory addresses.
-
Solution: The assembler will automatically invert the branch condition to skip a jump instruction, and insert an unconditional jump (
jal- which has a larger 20-bit range) to reach the far target. -
Example:
beq x10, x0, L1(where L1 is too far) becomes:
VI. Advanced Control Flow
1. Compiling switch / case Statements
Instead of using a long chain of if-else (conditional branches) for a switch statement, compilers often optimize this using a Jump Address Table (an array of memory addresses corresponding to different case blocks).
-
Mechanism:
-
Check bounds: Ensure the
switchvariable \(k\) is within the valid range (e.g., \(0 \le k < 4\)). If not, branch to the exit. -
Calculate memory offset: Multiply \(k\) by 8 (using a left shift
slli reg, k, 3) because memory addresses in a 64-bit architecture are 8 bytes long. -
Add the offset to the base address of the Jump Table.
-
Load the target address from memory (
ld). -
Execute an unconditional jump to that loaded address using
jalr(Jump and Link Register).
-
-
NOP Instruction: A "No Operation" instruction does nothing but advance the PC. In RISC-V, this is implemented as a pseudo-instruction:
addi x0, x0, 0.
VII. Decoding Machine Language & Addressing Modes
1. Decoding Example
Translating a 32-bit machine instruction (e.g., 0x00578833) back to assembly:
-
Convert Hex to Binary:
0000 0000 0101 0111 1000 1000 0011 0011 -
Look at the lowest 7 bits (opcode:
0110011). This identifies it as an R-type arithmetic instruction. -
Parse the remaining fields based on the R-type format:
-
funct7(0000000) &funct3(000) \(\rightarrow\) indicates theaddinstruction. -
rs2= 00101 (Registerx5) -
rs1= 01111 (Registerx15) -
rd= 10000 (Registerx16)
-
-
Assembly Result:
add x16, x15, x5
2. Immediate Encoding Variants
RISC-V shuffles the bits of immediate values depending on the instruction format (S-type, B-type, U-type, J-type).
-
Goal: To simplify hardware decoding. By shuffling bits, the hardware can keep the sign bit always located at the highest bit position (
inst[31]) and maximize the physical overlap of immediate bits across different formats. -
Note: Branch (B) and Jump (J) immediates are implicitly shifted left by 1 (since instructions are aligned on halfword/word boundaries, the lowest bit is always 0).
3. RISC-V Addressing Modes Summary
-
Immediate Addressing: The operand is a constant located directly inside the instruction itself.
-
Register Addressing: The operand is the data contained within a specified register.
-
Base (Displacement) Addressing: The memory address is calculated by adding a constant immediate (offset) to the value in a base register. (Used for loads/stores).
-
PC-Relative Addressing: The address is calculated by adding a constant immediate to the Program Counter (PC). (Used for branches and jumps).
VIII. Privileged ISA and Software Stack
1. Privilege Modes
RISC-V defines different levels of privilege to safely isolate system components. Processors spend most of their time in the lowest privilege mode, using interrupts/exceptions to transfer control to higher modes.
-
Level 3 - Machine Mode (M): The highest privilege level. It has absolute access to hardware. This is the only strictly mandatory mode in a RISC-V implementation.
-
Level 1 - Supervisor Mode (S): Used by Operating Systems (like Linux).
-
Level 0 - User/Application Mode (U): The lowest privilege level, used for standard application software.
2. Software Interface Stack
-
Applications communicate with the OS via the ABI (Application Binary Interface).
-
The OS communicates with the execution environment (or hypervisor) via the SBI (Supervisor Binary Interface).
IX. Function Calls and Procedure Conventions
When a Caller (e.g., main()) invokes a Callee (e.g., sum()), the system must follow a strict 6-step convention:
-
Put arguments where the Callee can access them.
-
Jump to the Callee.
-
Acquire local storage resources (save existing register values).
-
Perform the function's calculations.
-
Place the result where the Caller expects it, and restore any saved registers.
-
Return control to the origin point in the Caller.
1. Procedure Call Instructions
-
Call (
jal x1, Target): Jumps to the target address and saves the address of the next instruction (PC + 4) into the return address register (x1/ra). -
Return (
jalr x0, 0(x1)): Jumps to the address stored inx1. It writes its own return address tox0(which is hardwired to zero, effectively discarding it).
2. Register Conventions (The Contract)
RISC-V utilizes 32 registers (x0-x31), each with specific ABI roles during function calls:
-
x10 - x17(Argument/Result Registers): Used to pass parameters and return values. -
x1(ra): Return address register. -
x2(sp): Stack pointer register. -
x8 - x9, x18 - x27(Saved Registers): Callee-saved. If the Callee wants to use these, it must save their original values to the stack first and restore them before returning. The Caller expects these to remain unchanged across a call. -
x5 - x7, x28 - x31(Temporary Registers): Caller-saved. The Callee can use and overwrite these freely. If the Caller needs the data in these registers to survive the function call, the Caller must save them before invoking the procedure.
3. Stack Management
The Stack is the ideal data structure for "spilling" registers (saving data when you run out of registers).
-
Direction: The stack grows from Higher Addresses to Lower Addresses.
-
Push (Allocate): Subtract from the stack pointer (e.g.,
addi sp, sp, -24). -
Pop (Deallocate): Add to the stack pointer (e.g.,
addi sp, sp, 24).
4. Anatomy of a Procedure
-
Prologue: The start of the function. Allocates space on the stack (
sp = sp - framesize) and saves necessary registers (likeraand any Saved Registers). -
Body: The actual computation.
-
Epilogue: The end of the function. Restores saved registers from the stack, de-allocates the stack frame (
sp = sp + framesize), and returns (ret).
5. Leaf vs. Non-Leaf Procedures
-
Leaf Procedure: A function that does not call any other functions.
-
Non-Leaf Procedure: A function that calls other functions (e.g., Nested calls or Recursion).
- Crucial Rule: A non-leaf procedure must save its own return address (
x1/ra) to the stack before calling the inner function; otherwise, the inner function call will overwritex1, and the program will be unable to return to the original Caller.
- Crucial Rule: A non-leaf procedure must save its own return address (
-
Recursion Disadvantages: Recursive functions require heavy use of the stack (frequent pushes and pops). This is memory inefficient and can lead to a Stack Overflow. Where possible, iteration (loops) or tail-call optimization is preferred.
X. Memory Layout and the Stack Frame
A typical program's memory space is organized into specific segments:
-
Text Segment: Holds the compiled program code (machine instructions).
-
Static Data: Holds global variables and constants. The Global Pointer (
gp/x3) is initialized to point to the middle of this segment for fast access. -
Dynamic Data (Heap): Used for dynamically allocated memory (e.g.,
mallocin C,newin Java). It grows upward toward higher addresses. -
Stack: Used for automatic storage (local variables, procedure frames). It grows downward toward lower addresses.
The Stack Frame and Frame Pointer (fp)
When a procedure is called, it allocates a private block of memory on the stack called a Procedure Frame (or Activation Record). This holds saved registers, local arrays, and variables that don't fit in registers.
-
Frame Pointer (
fp/x8): While the Stack Pointer (sp) points to the bottom (lowest address) of the current frame and might move dynamically as data is pushed/popped, the Frame Pointer points to the top (base/highest address) of the current frame. -
Purpose: The
fpprovides a stable, unchanging base address to reference local variables and parameters throughout the lifecycle of the function call.