Notes on Operand Decoding and Implementing RISC-V Instructions

Engineering

Posted by Bruce Lee on 2024-05-08

About Me

Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.

Main Topics

  • Engineering Projects: Exploring implementation details and how technical systems work.
  • C/C++: Notes on language features and programming techniques.
  • The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.

For more, visit the categories page.

Contact

If you have questions or would like to discuss something, please get in touch through the About page.

Thank you for reading and for your support. I hope these notes help you on your own technical journey!


decode_operand

BITS extracts rs1, rs2, and rd from the instruction encoding in s. A switch on the instruction type then selects the operand-decoding operations.

src1R and src2R obtain register values through the underlying R macro. immI, immU, and immS construct immediates for their respective formats, using BITS and SEXT.

BITS, BITMASK, and SEXT

The shared helpers are defined in include/macro.h. BITS(x, hi, lo) shifts the requested field down by lo, then masks away the higher bits. The field width is:

1
(hi) - (lo) + 1

BITMASK constructs that many low-order one bits. A term such as 1ull << bits starts with an unsigned long long constant; (1ull << bits) - 1 then forms the mask for supported shift counts. The wider intermediate also accommodates fields beyond a 32-bit example.

SEXT uses a signed bit-field of the requested width in a temporary structure. Assigning the extracted value to that field and reading it back produces the signed value corresponding to the field’s sign bit.

The original notes questioned the final cast to uint64_t and proposed changing it to int64_t. The distinction needs care: converting an already sign-extended negative value to unsigned preserves the corresponding modulo-2^64 bit pattern. It does not undo the sign extension. Later arithmetic and comparisons must nevertheless use the intended signedness.

g_nr_guest_inst

This variable counts executed guest instructions.

How the PC Advances

execute creates a Decode object and passes it to exec_once. That function initializes s->pc and s->snpc from the global PC and calls isa_exec_once.

Instruction fetch fills the instruction encoding and advances s->snpc to the next sequential instruction. decode_exec initializes s->dnpc from that sequential address, then executes the selected operation. A branch or jump can replace dnpc with another destination.

On returning to exec_once, cpu.pc = s->dnpc installs the actual next instruction address. Thus snpc describes the sequential successor, while dnpc describes the successor chosen by execution.

Adding an Instruction

In src/isa/riscv32/inst.c, check whether the instruction needs a new operand format in the type enumeration. Add immediate-decoding logic and a decode_operand case if necessary. Then add its pattern and operation inside the INSTPAT_START/INSTPAT_END group.

addi

TYPE_I and immI already supply the needed format. After verifying the decoding, add the opcode 0010011 and funct3=000 pattern:

1
INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi, I, R(rd) = src1 + imm);

The li Pseudoinstruction

A small constant can be loaded with addi from x0. Larger RV32 constants may require a sequence involving lui and addi, so supporting a source-level li can require more than one real instruction implementation.

jal

Add TYPE_J and reconstruct the split immediate fields. The original sketch builds the offset without its implicit low zero bit:

1
#define immJ() do {*imm = (SEXT(BITS(i, 31, 31), 1) << 19) | BITS(i, 19, 12) << 11 | BITS(i, 20, 20) << 10 | BITS(i, 30, 21);} while(0)

Add the corresponding TYPE_J case in decode_operand. The operation has two steps:

1
2
3
4
{
R(rd) = s->snpc;
s->dnpc = s->pc + (imm << 1);
}

It records the return address and selects the jump destination.

ret and jalr

ret is an assembler pseudoinstruction for jalr x0, 0(x1). jalr uses an I-type immediate, writes the return address when the destination register is not x0, and jumps to the register-plus-immediate target with its low bit cleared.

Why the Jump Offset Has One Implicit Zero Bit

The original notes included an AI explanation about an additional shift already occurring in the instruction encoding. The reliable distinction is simpler: J- and B-format offsets are encoded in units of two bytes, whereas JALR uses a byte offset. The implicit bit supports the instruction-set alignment scheme, including compressed instructions; it is not a two-stage shift caused by four-byte alignment. See the RV32I specification.

Supplying an Operation Block to INSTPAT

For multi-step operations, I changed the supplied operation into a braced block. The original expansion appended a semicolon:

1
_\_VA_ARGS_\_;

I then made each instruction operation a complete braced block, with semicolons on its internal statements, and removed the additional semicolon from that expansion site. An extra semicolon after a block is not universally invalid; this was a convention change in the macro setup being edited.

R-Type Instructions

R-type instructions need both source registers but no immediate. Add TYPE_R, a register-decoding case, and the corresponding patterns, such as add. No immR helper is needed.

seqz

The seqz found in the add.c disassembly at 0x80000098 is a pseudoinstruction. Supporting it requires the corresponding sltiu behavior.

beqz and beq

At 0x80000010, beqz expands to beq rs1, x0, offset. If the values are equal, the branch adds the sign-extended offset to the current PC.

Add TYPE_B, reconstruct the branch immediate, and add a decoding case:

1
2
3
4
#define immB() do { *imm = (SEXT(BITS(i, 31, 31), 1) << 11) | \
(BITS(i, 7, 7)) << 10 | \
(BITS(i, 30, 25)) << 4 | \
(BITS(i, 11, 8));} while(0)

Then add the pattern:

1
2
INSTPAT("??????? ????? ????? 000 ????? 11000 11", beq    , B, {s->dnpc = (src1 == src2)? (s->pc + (imm << 1)) : \
s->dnpc;});

mul in the Factorial Test

The instruction at 0x80000094 is R-type. Its implementation needs the appropriate pattern and multiplication operation.

rem in the Prime Test

Signed division truncates toward zero, and rem writes the remainder. The early notes left division by zero unresolved and hoped the compiler would protect the operation. An emulator must instead implement the ISA’s defined exceptional results explicitly; host C division by zero is not a valid substitute. The RISC-V M extension specifies the divisor-zero and signed-overflow cases.

slli

The immediate contains a shift amount: five bits in RV32, six in RV64. RV32 takes shamt from instruction bits 24:20; RV64 also uses bit 25. The remaining immediate bits participate in identifying the operation, rather than being an ordinary signed I-type value.

One implementation approach extracts the wider field and masks the legal shift-count bits, while also validating the instruction’s fixed encoding bits.

srai

The intended operation is arithmetic right shift, retaining the sign. Unsigned C right shift is logical. A signed right shift commonly provides the desired behavior in the toolchain used here, but portability and the language version should be considered explicitly.

mulh

For RV32, signed multiplication must produce a properly widened product before selecting its upper 32 bits. The original sketch was:

1
{R(rd) = ((int64_t)src1 * src2) >> 32);}

That sketch contains an extra parenthesis and needs careful signed widening of both 32-bit operands. In particular, converting an unsigned 32-bit source directly to int64_t does not reinterpret it as a negative 32-bit value. The later differential-testing notes describe the sign-extension bug this caused.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !