Exercises — RISC-V base ISA (RV32I - RV64I)
Before we begin, one shared vocabulary reminder — earned, not assumed:
The figure below turns that first bullet into a picture — study it now, because every exercise on this page ultimately hangs on this one move.

Level 1 — Recognition
L1.1
How many general-purpose registers (GPRs) does RV32I have, and how many does RV64I have?
Recall Solution
Both have exactly 32 GPRs named x0–x31.
The number "32" in the name RV32I is the register width (XLEN = 32 bits), not the count. RV64I also has 32 registers; they are just 64 bits wide each.
Answer: 32 and 32.
L1.2
Which register is hardwired to zero, and what happens when you write to it?
Recall Solution
x0. Reads always return ; writes are silently discarded (thrown away).
This is why nop = addi x0, x0, 0: it computes something, then dumps the result into the trash-bin register.
L1.3
How many bits long is a single RV32I instruction, and why does fixed length help?
Recall Solution
32 bits (4 bytes), fixed. Fixed length means the CPU always knows where the next instruction starts without decoding the current one — fetch and decode become trivial and fast. This is a core RISC design choice.
Level 2 — Application
L2.1
An I-type immediate is a 12-bit signed field. What is its numeric range?
Recall Solution
A 12-bit signed (two's-complement) number: the top bit is the sign, the other 11 bits carry magnitude.
- Most negative: .
- Most positive: . See figure below for how the number line wraps.

L2.2
The 12-bit field 1111 1111 1111 is used as an addi immediate. What signed value is added, and what does addi x5, x6, -1 leave in x5?
Recall Solution
Top bit is → negative. In two's complement, all-ones = . Sign-extended to XLEN bits it is still (0xFFFF...FF).
So addi x5, x6, -1 computes .
Value added: .
L2.3
A B-type (branch) immediate stores 12 bits but its bit 0 is not stored (always ). What byte range can a branch reach relative to pc?
Recall Solution
The stored bits represent a signed offset in units of 2 bytes. So the effective offset is (12-bit signed value) × 2:
Roughly KiB. Why is bit 0 free? In base RV32I every instruction is 4-byte aligned, so a branch target's low two bits are always — the encoding could even have dropped two bits. RISC-V drops only bit 0 (not bit 1) on purpose: if the optional C (compressed) extension is present, instructions may sit on any 2-byte boundary, so bit 1 can legitimately be . Dropping just the always-zero bit 0 doubles the reach while staying compatible with compressed code.
Level 3 — Analysis
L3.1
Encode addi x5, x6, -1 into its 32 bits. Give each field. (opcode = 0010011, funct3 = 000.)
Recall Solution
I-type layout: [31:20]=imm | [19:15]=rs1 | [14:12]=funct3 | [11:7]=rd | [6:0]=opcode.
imm = -1→ 12 bits1111 1111 1111.rs1 = 6→00110.funct3 = 000.rd = 5→00101.opcode = 0010011.
Concatenate:
111111111111 00110 000 00101 0010011
Group into 32 bits and read as hex:
1111 1111 1111 0011 0000 0010 1001 0011
F F F 3 0 2 9 3
Machine code: 0xFFF30293.
L3.2
The S-type format (used by stores like sw) has this field layout:
[31:25]=imm[11:5] | [24:20]=rs2 | [19:15]=rs1 | [14:12]=funct3 | [11:7]=imm[4:0] | [6:0]=opcode
Notice the 12-bit immediate is split into two chunks — a high piece imm[11:5] and a low piece imm[4:0]. Explain why RISC-V chops the immediate up like this instead of storing it as one clean field.
Recall Solution
Because the register selector fields rs1, rs2, rd are pinned to fixed bit positions across all formats. Compare S-type above with I-type ([31:20]=imm | [19:15]=rs1 | [14:12]=funct3 | [11:7]=rd): in both, rs1 sits at [19:15] and funct3 at [14:12]. A store has no rd, so those [11:7] bits (where rd would be) are recycled to hold the low 5 bits of the immediate, and the rest of the immediate goes up in [31:25].
The decoder can start reading register file entries the instant it sees the fixed rs1/rs2 bits — before the scattered immediate is understood. A small MUX re-assembles imm[11:5] : imm[4:0] into the full 12-bit value. The cost (a bit of wiring) buys parallel decode + register read, a speed win that matters for Pipelining.
L3.3
In li x10, 0xEEF, a student expects x10 = 0x00000EEF. Show what actually lands in x10 if this were a single addi x10, x0, 0xEEF, and explain.
Recall Solution
0xEEF in binary is 1110 1110 1111 — 12 bits, top bit = 1 → negative.
As a signed 12-bit number: unsigned, but .
Sign-extended to 32 bits: 0xFFFFFEEF.
So a naive addi x10, x0, 0xEEF gives 0xFFFFFEEF, not 0x00000EEF.
The real li pseudo-instruction (see Assembler & Pseudo-instructions) detects this and emits a lui+addi pair to produce the clean positive value.
Level 4 — Synthesis
Before this level we need one instruction the parent note mentioned but we must define here:
L4.1
Write the two-instruction sequence to load 0xDEADBEEF into x10, and show the arithmetic proves it's exact.
Recall Solution
Split 0xDEADBEEF into upper 20 bits and lower 12 bits:
- Lower 12 bits =
0xEEF=1110 1110 1111, bit 11 set → signed value . - Upper 20 bits would naively be
0xDEADB, solui x10, 0xDEADBgives0xDEADB000.
Because the lower add is negative (), it will subtract, landing below our target. To compensate, round the upper part up by 1: 0xDEADB → 0xDEADC.
lui x10, 0xDEADC # x10 = 0xDEADC000
addi x10, x10, -273 # + (-273)
Check the arithmetic in clean hex (no messy decimals needed):
- Note , and
0xEEF = 0x1000 - 0x111. luigives0xDEADC000.- Adding :
0xDEADC000 - 0x111 = 0xDEADBEEF.
Landing exactly on 0xDEADBEEF. ✓ (The +1 to the upper field is precisely what cancels the "borrow" caused by the negative low add.)
L4.2
Build a countdown loop that runs the body 5 times using only base RV32I instructions and the zero register. Assume x1 = 5 on entry.
Recall Solution
loop:
addi x1, x1, -1 # counter--
# ... loop body ...
bne x1, x0, loop # if x1 != 0, branch back
Why compare with x0? RV32I has no compare-to-zero opcode — but it doesn't need one, because x0 is the constant zero. bne x1, x0, loop = "branch if x1 ≠ 0". The branch offset is negative (target is behind us), sign-extended, in 2-byte steps.
With x1 = 5, let's trace: after 1st addi, x1=4, body runs (run #1), branch taken; ... after 5th addi, x1=0, body runs (run #5), branch not taken. Body executes 5 times. ✓
Level 5 — Mastery
L5.1
On RV64I, register x7 holds 0x0000_0000_8000_0000. You execute addw x8, x7, x7 (32-bit add, then sign-extend to 64). What is the final 64-bit value in x8?
Recall Solution
addw does the addition only on the low 32 bits, wrapping (modular) at 32 bits, then sign-extends the 32-bit result to 64 bits.
- Low 32 bits of
x7=0x80000000. - ; keep low 32 →
0x00000000. - Sign-extend
0x00000000(top bit 0) →0x0000_0000_0000_0000. Answer:0x0000000000000000(i.e. 0). The overflow bit above 32 is simply dropped — exactly C'sintwraparound behavior.
L5.2
Still RV64I. x7 holds 0x0000_0000_4000_0000. Execute addw x8, x7, x7. Final 64-bit value?
Recall Solution
- Low 32 bits =
0x40000000. - (fits in 32 bits, no overflow past bit 31).
- The 32-bit result
0x80000000has bit 31 = 1 → sign-extend fills the upper 32 bits with1s. Answer:0xFFFFFFFF80000000. This is the subtle case: no 32-bit overflow, but the sign bit flips, so the 64-bit view becomes negative.
L5.3
The J-type format (used by jal) packs a 20-bit immediate. Its byte offset is (20-bit signed value) × 2, i.e. bit 0 is implicit and always , exactly like the B-type branch. A student claims a jump reaches bytes. Correct them and give the true range.
Recall Solution
Where the implicit bit-0 comes from (same story as L2.3): instruction targets are at least 2-byte aligned (4-byte aligned in pure RV32I, 2-byte if the compressed C extension is present), so the least-significant bit of the target offset is always . Rather than waste a stored bit on a guaranteed zero, J-type encodes the offset in units of 2 bytes — the hardware appends the implicit when it reconstructs the address. So the 20 stored bits cover a signed offset that you must multiply by 2 to read in bytes: The student forgot the implicit-bit doubling. True reach: ±1 MiB, not ±512 KiB.
L5.4
Zero/degenerate check: what does beq x0, x0, target do, and is it ever "not taken"?
Recall Solution
beq = branch if equal. x0 == x0 is always true (), so this branch is unconditionally taken, every time. It is effectively an unconditional relative jump (within branch range). This is how assemblers can synthesize a short j/b (branch always) from base instructions using the zero register — see Addressing Modes for PC-relative targets.
Active recall (mixed)
Recall Quick self-quiz — answer, then expand
Register count in RV64I? ::: 32 (same as RV32I; only width changes).
Signed range of a 12-bit immediate? ::: .
Byte reach of a B-type branch? ::: KiB, in 2-byte steps.
Machine code of addi x5, x6, -1? ::: 0xFFF30293.
Result of addw when the 32-bit sum's bit 31 is set? ::: sign-extended → upper 32 bits all 1s.