Introduction
There are several major differences between a RISC processor (such as the 32-bit RISC-V executing the RV32I or RV32G instruction sets) and a CISC processor (such as the i386).
The first difference that comes to mind is, of course, the size of the instruction set: between 1,000 and 1,500 instructions for the i386 compared to around 40 for a RISC-V processor running the base RV32I instruction set.
However, there is another, more subtle and less obvious difference: in a CISC processor, instruction length is variable and can range from 1 byte to more than 10 bytes. In contrast, on a RISC processor, the instruction size is fixed (except when using compressed instructions) and equals exactly 4 bytes (32 bits) for an RV32I processor.
Being limited to 4 bytes to encode an instruction introduces specific hardware and assembly constraints. The most immediate one leads to a simple question:
How do you load a 32-bit immediate value into a 32-bit register on a 32-bit RISC-V processor?
To answer this, let us first examine how this is handled on an i386 processor:
.intel_syntax noprefix
.section .text # Indicates that the following section contains executable code
.global _start # Defines the entry point of the program
_start:
mov eax, 0x12345678 # Loads the hexadecimal value 0x12345678 into the eax register
After assembling with GNU as and disassembling with objdump, we obtain:
x86_32: file format elf32-i386
Disassembly of section .text:
08049000 <_start>:
8049000: b8 78 56 34 12 mov eax,0x12345678
This is a direct translation of the assembly instruction: the opcode b8 is followed by the full 32-bit constant 0x12345678 (encoded in little-endian as 78 56 34 12), producing a single 5-byte instruction.
What happens on a 32-bit RISC-V processor?
.section .text
.global _start
_start:
li t0, 0x12345678 # load 0x12345678 into register t0
The assembly file above is translated into a binary that disassembles into the following code:
riscv: file format elf32-littleriscv
Disassembly of section .text:
00010074 <_start>:
10074: 123452b7 lui t0,0x12345
10078: 67828293 addi t0,t0,1656 # 0x678 in hex
Because a 32-bit RISC-V instruction must fit within exactly 32 bits, it cannot contain a 32-bit immediate payload while simultaneously reserving bits for the opcode and the target register field. As a result, the assembler splits the operation into two hardware instructions:
-
lui t0, 0x12345(Load Upper Immediate, U-Type format): loads the upper 20 bits intot0(shifted left by 12 bits). -
addi t0, t0, 1656(Add Immediate, I-Type format): adds the remaining 12-bit value (1656in decimal =0x678in hex) tot0.
Pseudo-Instructions vs Real Instructions
It is important to note that li (Load Immediate) does not actually exist in the RISC-V hardware specification. It is a pseudo-instruction provided by the assembler to simplify programming. When writing RISC-V assembly, the toolchain automatically translates high-level constructs like li or la (Load Address) into standard physical instruction sequences (lui + addi).
The Sign Extension Trap
A critical subtlety arises when loading arbitrary 32-bit values: the 12-bit immediate field in the addi instruction is sign-extended.
If bit 11 of the lower 12-bit payload is 1 (i.e., if the lower 12 bits fall in the range 0x800–0xFFF), addi treats it as a negative value and subtracts it from the upper value. To compensate for this arithmetic effect, a compliant assembler must automatically detect when bit 11 is set and add 1 to the upper 20 bits loaded by lui.
Architectural Trade-offs: Hardware vs. Binary Size
This structural difference highlights the core design philosophy separating CISC and RISC architectures:
| Feature | CISC (x86_32) | RISC (RISC-V RV32I) |
|---|---|---|
| Instruction Length | Variable (1 to 15 bytes) | Fixed (4 bytes / 32 bits) |
| 32-bit Immediate Loading | Single 5-byte instruction | Two 4-byte instructions (8 bytes total) |
| Hardware Decoder Complexity | High (must determine instruction boundaries dynamically) | Very Low (fixed alignment simplifies instruction fetch and pipeline) |
| Execution Impact | Compact code, complex decoding logic | Slightly larger code size, streamlined pipelining and execution |
Epilogue
Building an assembler for a RISC-V processor may seem straightforward at first glance due to the reduced instruction set. However, subtleties arising from fixed-width instruction encodings, sign-extension compensation, and pseudo-instruction expansion introduce unexpected layers of complexity that every compiler and assembler author must carefully handle.
Top comments (0)