5.1.7 · D4Instruction Set Architecture (ISA)

Exercises — RISC-V base ISA (RV32I - RV64I)

2,669 words12 min readBack to topic

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.

Figure — RISC-V base ISA (RV32I - RV64I)

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 x0x31. 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.
Figure — RISC-V base ISA (RV32I - RV64I)

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 bits 1111 1111 1111.
  • rs1 = 600110.
  • funct3 = 000.
  • rd = 500101.
  • 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 111112 bits, top bit = 1negative. 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, so lui x10, 0xDEADB gives 0xDEADB000.

Because the lower add is negative (), it will subtract, landing below our target. To compensate, round the upper part up by 1: 0xDEADB0xDEADC.

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.
  • lui gives 0xDEADC000.
  • 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's int wraparound 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 0x80000000 has bit 31 = 1 → sign-extend fills the upper 32 bits with 1s. 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.