RISC-V Field Guide
The RISC-V Field Guide
A searchable RISC-V assembly incantation instruction reference for arcanists programmers | Using this book | About this book
How to read an instruction
RISC-V conventionally writes the destination first:
instruction destination, source1, source2
add rd, rs1, rs2
rd is the destination register; rs1 and rs2 are source registers. An imm operand is encoded directly in the instruction. Unless a page says otherwise, the destination may be the same register as a source.
Availability labels
- RV32I / RV64I: base integer ISA for 32- or 64-bit registers.
- RV64-only: requires 64-bit integer registers.
- M, A, F, D, C, B, V…: optional standard extensions.
- Pseudo: assembler syntax expanded into one or more real instructions.
A word on pseudoinstructions
The assembler instruction mv a0, a1 is convenient syntax, not a distinct machine instruction. It normally becomes addi a0, a1, 0. Search results include pseudoinstructions because programmers encounter both forms.
About the RISC-V Field Guide
Welcome to the RISC-V Field Guide, a searchable reference manual and learning resource for RISC-V assembly arcanists programmers. I basically designed it for my own needs but I hope this field guide is useful to others in some way.
My intent for this guide is to provide a reasonably accurate, useful, sampling of the important parts of the RISC-V specification, for programmers, by a programmer. I do not intend this site to be an authoritative source, however.
At times, you will want to refer to the resources in the References section, particularly if you need reference a source of truth.
Coverage
At this time I’m working on covering the base instructions for RV32 and RV64 and common pseudointructions.
Instruction References
For your convenience, this entire site is searchable via the magnifying glass at the top of the page. The most useful part of this site are the instruction reference sections that should give you a simple lookup for any instructions you are looking at.
Incantations
Assembly programming isn’t for the faint of hart (pun intended). Explore the incantations to reveal the deepest secrets of the RISC-V specification.
Rituals
We learn best when we learn by doing. Perform the rituals regularly and you will turn what starts as a passing familiarity, into a deep understanding, a memory.
Use of AI Declaration
Because I am but one human, and the breadth of knowledge hidden in the RISC-V specifications is vast, I have used AI to assist me in building this site. That doesn’t mean that I auto-generated every line though. I have written this entire page, for example, and there are plenty of areas where I have taken the time to explain things, human to human.
As a result, some of the content could be misleading or outright incorrect. If you spot something, please let me know via a GH issue and I promise I will do my best to address it.
About the curator
Hi, I’m Jim. I’ve been programming professionally since the late 90s, and am currently working in cybersecurity risk management for a large tech company where I spend time worrying about the safety of other peoples data. In my spare time I am an independent security researcher working on kernels and vulnerabilties in various things.
You can find me on socials and in other places under the pseudonym akses.
Getting started
This gets a small RISC-V assembly program running on your current computer. You do not need RISC-V hardware.
1. Install the tools
Choose your operating system.
Linux (Ubuntu or Debian)
Open a terminal:
sudo apt update
sudo apt install gcc-riscv64-linux-gnu qemu-user
Windows
Open PowerShell as Administrator and install WSL:
wsl --install
Restart Windows, open Ubuntu, then run:
sudo apt update
sudo apt install gcc-riscv64-linux-gnu qemu-user
macOS
Install Docker Desktop. Open Terminal in the directory where you want to work, then run:
docker run --rm -it -v "$PWD":/work -w /work ubuntu:24.04 bash
apt update
apt install -y gcc-riscv64-linux-gnu qemu-user
Keep this container terminal open for the remaining steps.
2. Write Hello World
Create a file named hello.s:
.section .rodata
message:
.ascii "Hello, RISC-V!\n"
.equ message_len, . - message
.section .text
.globl _start
_start:
li a0, 1
la a1, message
li a2, message_len
li a7, 64
ecall
li a0, 0
li a7, 93
ecall
3. Build and run it
Run these commands in the directory containing hello.s:
riscv64-linux-gnu-gcc -nostdlib -static -o hello hello.s
qemu-riscv64 ./hello
You should see:
Hello, RISC-V!
4. Change it
Edit the text inside .ascii, then build and run the program again.
Useful next pages:
- Registers and ABI names
li— load an integer constantla— load an addressECALL— ask the environment for a service
Signedness and width guide
RISC-V does not consistently encode signedness in mnemonics. Some operations have separate signed and unsigned forms; others use the same instruction regardless of interpretation. This page is a quick-reference for which is which.
Quick-reference table
| Operation | Signed form | Unsigned form | Notes |
|---|---|---|---|
| byte load | LB | LBU | LB sign-extends to XLEN; LBU zero-extends |
| halfword load | LH | LHU | same pattern as byte loads |
| word load | LW | LWU | both load 32 bits; LW sign-extends to XLEN, LWU zero-extends; LWU is RV64-only |
| less-than branch | BLT | BLTU | same register encoding, different comparison |
| less-than set | SLT / SLTI | SLTU / SLTIU | produces a 0/1 boolean |
| comparison branches | BGE | BGEU | greater-or-equal counterpart |
| division | DIV | DIVU | quotient; remainder follows (REM / REMU) |
| multiply high | MULH | MULHU | high half of XLEN × XLEN product |
Operations that are always the same
Addition, subtraction, bitwise logic, left shift, and low-half multiplication
produce the same bit pattern regardless of signed or unsigned interpretation.
Right shift is the exception: choose logical SRL for zero-fill or arithmetic
SRA for sign propagation.
| Instruction | Why no separate form |
|---|---|
ADD / ADDI | Two’s-complement addition is bitwise identical for signed and unsigned |
SUB | Same as addition — a − b = a + (~b + 1) works the same way |
AND / OR / XOR | Bitwise operations have no signedness |
SLL | Left shift has the same bit result for signed and unsigned values |
SRL / SRA | Right shift requires an explicit choice: zero-fill or sign propagation |
MUL (low half) | Low XLEN bits of a product are identical for signed and unsigned operands |
Width extension
Load instructions extend the loaded value to fill the destination register. The extension direction (sign or zero) is the key difference between the signed and unsigned forms.
LB rd, off(rs1) # load byte, sign-extend to XLEN
LBU rd, off(rs1) # load byte, zero-extend to XLEN
LH rd, off(rs1) # load halfword, sign-extend to XLEN
LHU rd, off(rs1) # load halfword, zero-extend to XLEN
LW rd, off(rs1) # load word, sign-extend to XLEN
LWU rd, off(rs1) # load word, zero-extend to XLEN (RV64 only)
LW is available on both RV32 and RV64. LWU is RV64-only (GNU as rejects it for RV32 targets). On RV32 both forms would be identical — there is no extension to perform when loading 32 bits into a 32-bit register — but the assembler does not accept LWU as a mnemonic.
The SLTIU gotcha
SLTIU performs an unsigned comparison but still sign-extends its 12-bit immediate before comparing. This means a negative immediate represents a large unsigned value:
sltiu rd, rs, 0 # rd = (rs < 0) — always 0 (unsigned)
sltiu rd, rs, -1 # rd = (rs < 0xFFFF…FF) — always 1 unless rs is max value
sltiu rd, rs, 1 # rd = (rs < 1) — equivalent to seqz (rs == 0)
The immediate is sign-extended because SLTIU reuses the same immediate encoding as every other I-type instruction — the sign-extension is a property of the encoding, not the comparison logic.
x86 connection
x86 expresses signedness through different mnemonics for almost every operation: jl vs jb, setl vs setb, idiv vs div, imul vs mul, movsx vs movzx. RISC-V collapses many of these pairs into a single instruction (addition, subtraction, low-half multiply) because the bit pattern is the same either way.
Where RISC-V does use separate forms (loads, branches, set-less-than, division), the U suffix consistently means “unsigned,” which is simpler than x86’s historical naming (jb = jump below, jl = jump less, setb = set below, setl = set less, etc.).
Related: Loads, Stores, BLT / BGE / BLTU / BGEU, SLT / SLTI / SLTU / SLTIU, *W — RV64 word operations.
Registers and ABI names
Integer registers have architectural names x0–x31 and conventional ABI names. x0 always reads as zero; writes to it are discarded.
| Register | ABI | Conventional purpose | Preserved across calls? |
|---|---|---|---|
x0 | zero | hard-wired zero | n/a |
x1 | ra | return address | no |
x2 | sp | stack pointer | yes |
x3 | gp | global pointer | special |
x4 | tp | thread pointer | special |
x5–x7 | t0–t2 | temporaries | no |
x8 | s0 / fp | saved register / frame pointer | yes |
x9 | s1 | saved register | yes |
x10–x17 | a0–a7 | arguments / return values | no |
x18–x27 | s2–s11 | saved registers | yes |
x28–x31 | t3–t6 | temporaries | no |
About the rituals
These rituals will serve you well on your journey to programming in assembly for RISC-V platforms. Perform them regularly for best effect, for example, pick one or two per day before you start a coding session. Explore different ways of achieving the same thing.
Each ritual begins with a goal that is grounded in the harsh realities of low level programming. Each time you solve one, you make important connections and learn important skills.
Why perform these dark arts?
While it is true we are now in an era when machines can write their own code, by practicing programming by hand you are training your brain to know what you want. You want mastery over the machine, not dependency. While a carpenter benefits from their power tools, they still need to learn the intrinsic properties of the wood which they build from. This is the same for a programmer using an LLM. Machines are great assistants, but terrible masters.
Doing so enables several positive outcomes:
- You will be more likely to understand what you are asking for when you ask an LLM for assistance.
- You will be more likely to notice when a machine learning system has misled you.
Try to complete each ritual before revealing its solution. Then close the solution and perform it again from memory.
Ritual I: Hello World
Ritual I: Hello World
Words wait in silence.
The kernel carries their voice.
Return without trace.
Write an RV64 Linux program that prints Hello, RISC-V! followed by a newline, then exits successfully.
Begin at _start. Use only Linux write and exit system calls (i.e. no C library).
Reveal solution
.section .rodata
message:
.ascii "Hello, RISC-V!\n"
.equ message_len, . - message
.section .text
.globl _start
_start:
li a0, 1
la a1, message
li a2, message_len
li a7, 64
ecall
li a0, 0
li a7, 93
ecall
write receives the file descriptor, address, and length in a0–a2. The syscall number goes in a7. The second ecall exits with status zero.
Ritual II: Sum an array
Ritual II: Sum an Array
Eight bytes mark each step.
A dwindling count guides the path.
One register remains.
Write an RV64 function named sum. It receives the address of an array of 64-bit integers in a0 and its element count in a1. Return their wrapping sum in a0.
An empty array must return zero. Use only caller-saved registers and do not modify memory.
Reveal solution
.globl sum
sum:
mv t0, a0
mv t1, a1
li a0, 0
.Lsum_loop:
beqz t1, .Lsum_done
ld t2, 0(t0)
add a0, a0, t2
addi t0, t0, 8
addi t1, t1, -1
j .Lsum_loop
.Lsum_done:
ret
t0 walks through memory while t1 counts down. Initializing a0 before the loop handles an empty array and leaves the result in the return register.
Ritual III: Change the case
Ritual III: Change the Case
Read until nothing.
Only the small letters change.
The path stays the same.
Write an RV64 function named uppercase. It receives a pointer to a null-terminated ASCII string in a0 and converts each lowercase letter to uppercase in place.
Leave every other byte unchanged. Return the original pointer in a0.
Reveal solution
.globl uppercase
uppercase:
mv t0, a0
.Lcase_loop:
lbu t1, 0(t0)
beqz t1, .Lcase_done
li t2, 97
bltu t1, t2, .Lcase_next
li t2, 123
bgeu t1, t2, .Lcase_next
addi t1, t1, -32
sb t1, 0(t0)
.Lcase_next:
addi t0, t0, 1
j .Lcase_loop
.Lcase_done:
ret
The half-open range [97, 123) selects lowercase ASCII. Subtracting 32 produces the corresponding uppercase byte. Because only t0 advances, a0 still holds the original address.
Incantations
This section is inspired by the puzzle-box spirit of xorpd’s brilliant xchg rax,rax.
This is not a translation or reproduction of that book. However, you will find a similar collection of RISC-V curiosities.
These examples are here to develop intuition about x0, sign extension,
instruction aliases, exceptional arithmetic results, relocations, compressed
encodings, and other corners of the ISA. Some are useful idioms. Others are
deliberately perverse ways to reach an ordinary result.
I like that xorpd’s approach is to provide you the examples without explanation. This forces the curious to explore, and therefore learn, far better than documentation can teach.
Unless an entry says otherwise, examples operate on integer registers and use GNU-compatible assembler syntax. Instruction size refers to the final encoding, not the number of source-code characters. Linker relaxation and the C extension can make a familiar pseudoinstruction smaller than its canonical expansion.
Incantation: I
Incantation: I
andi t0, x0, -1
sltu t0, t1, t1
lui t0, 0
mulhsu t0, x0, t1
rem t0, x0, t1
Incantation: II
Incantation: II
beq x0, x0, target
bge x0, x0, target
bgeu x0, x0, target
jal x0, target
j target
Incantation: III
Incantation: III
div t0, t1, x0
divu t0, t1, x0
rem t0, t1, x0
remu t0, t1, x0
Incantation: IV
Incantation: IV
auipc t0, 0
Arithmetic
RISC-V integer arithmetic is normally three-operand and does not update a flags register. Use explicit branches or set-less-than instructions for comparisons.
ADD — add registers
add rd, rs1, rs2
Computes rd = rs1 + rs2. Available in RV32I and RV64I. Overflow wraps modulo XLEN; it does not trap and no carry/overflow flags are produced.
add a0, a1, a2 # a0 = a1 + a2
x86 connection: closest to lea dst, [src1 + src2] when you need flag-neutral addition. Unlike two-operand x86 add, neither source must be overwritten.
ADDI — add immediate
addi rd, rs1, imm12
Adds a sign-extended 12-bit immediate: rd = rs1 + sign_extend(imm12). The range is −2048 through 2047. Available in RV32I and RV64I.
addi sp, sp, -32 # allocate 32 bytes of stack space
addi a0, a0, 1 # increment a0
Common pseudoinstructions: mv rd, rs uses an immediate of zero; nop is addi x0, x0, 0.
x86 connection: resembles add reg, imm, but produces no flags and keeps separate source/destination operands.
SUB — subtract
sub rd, rs1, rs2
Computes rd = rs1 - rs2. Overflow wraps modulo XLEN. There is no base subi; use addi with a negative immediate when it fits.
sub a0, a0, a1
addi sp, sp, -16 # effectively subtract 16
x86 connection: x86 sub is a two-operand instruction and sets flags; RISC-V sub has three operands and does not.
SLT, SLTI, SLTU, SLTIU — set less than
slt rd, rs1, rs2 # signed: rd = (rs1 < rs2) ? 1 : 0
slti rd, rs1, imm12 # signed, immediate
sltu rd, rs1, rs2 # unsigned: rd = (rs1 < rs2) ? 1 : 0
sltiu rd, rs1, imm12 # unsigned, immediate
Set rd to 1 when the comparison condition holds, otherwise 0. The result is a zero-extended XLEN-wide value (never a single-bit flag). Available in RV32I and RV64I.
Immediate
SLTI and SLTIU sign-extend a 12-bit immediate to XLEN before comparing. This matters for SLTIU: a negative immediate represents a large unsigned comparison target.
| Assembly | Immediate after sign-extension | Effect |
|---|---|---|
sltiu rd, rs, 0 | 0 | rd = (rs < 0) — always 0, since no unsigned value is below 0 |
sltiu rd, rs, -1 | 0xFFFF…FFFF (max XLEN) | rd = (rs < max) — equivalent to rd = (rs != UINT_MAX) |
sltiu rd, rs, 1 | 1 | rd = (rs < 1) — equivalent to rd = (rs == 0) |
slti a0, a1, 10 # a0 = (a1 < 10), signed
sltiu t1, t0, 1 # t1 = (t0 == 0), unsigned
sltiu t2, t0, -1 # t2 = (t0 != UINT_MAX), unsigned
Pseudoinstructions built on SLT
| Pseudo | Expansion | Effect |
|---|---|---|
sltz rd, rs | slt rd, rs, x0 | set if less than zero |
sgtz rd, rs | slt rd, x0, rs | set if greater than zero |
seqz rd, rs | sltiu rd, rs, 1 | set if equal to zero |
snez rd, rs | sltu rd, x0, rs | set if not equal to zero |
Common idioms
Materialise a comparison result — standard way to turn a condition into a 0/1 value:
slt a0, a1, a2 # a0 = 1 if a1 < a2 (signed), else 0
Min and max:
# signed min of t0, t1
slt t2, t0, t1 # t2 = (t0 < t1)
neg t2, t2 # t2 = -1 if true, 0 if false → mask of all-ones or zero
and t3, t0, t2 # t3 = t0 & mask (t0 when t0 < t1, else 0)
not t2, t2 # t2 = ~mask
and a0, t1, t2 # a0 = t1 & ~mask (t1 when t0 >= t1, else 0)
or a0, a0, t3 # a0 = min(t0, t1)
Detect borrow after subtraction:
sub t0, a0, a1 # t0 = a0 - a1 (wrapping)
sltu t1, a0, t0 # t1 = 1 if a0 < (a0 - a1), i.e. borrow occurred
x86 connection: closest to setl/setb (or xor/cmp/setcc sequences), but writes a full-width register instead of a single byte and does not require a separate flags check. There is no x86 equivalent to sltiu with a negative immediate — x86 comparisons always use a fixed width.
Related: BLT / BGE / BLTU / BGEU, SUB.
*W — RV64 word operations
addw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] + rs2[31:0])
addiw rd, rs1, imm12 # rd = sign_extend(rs1[31:0] + imm12)
subw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] - rs2[31:0])
sllw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] << rs2[4:0])
slliw rd, rs1, shamt5 # rd = sign_extend(rs1[31:0] << shamt5)
srlw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] >> rs2[4:0]) (zero-fill)
srliw rd, rs1, shamt5 # rd = sign_extend(rs1[31:0] >> shamt5) (zero-fill)
sraw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] >>> rs2[4:0]) (arithmetic)
sraiw rd, rs1, shamt5 # rd = sign_extend(rs1[31:0] >>> shamt5) (arithmetic)
RV64I only. These instructions treat their source operands as 32-bit values and write a sign-extended 64-bit result to rd — bit 31 of the result is copied into bits 63–32.
Shifts use only the low 5 bits of the shift amount (word-size shift), not 6 as in the 64-bit SLL/SRL/SRA.
When to use them
Use *W when source values are 32-bit and the result needs to be 32-bit. The sign-extension is free on most implementations; some even eliminate the operation entirely when the consumer also reads a word-width result.
| You write … | Instead of … | Why |
|---|---|---|
addiw rd, rs, 0 | slli + srai, or sext.w pseudo | sign-extend bit 31 to fill bits 63–32 (the + 0 is a no-op) |
addw rd, rs1, rs2 | add + slli + srai | single instruction, truncates then sign-extends inline |
slliw rd, rs, 0 | slli + srai, or sext.w pseudo | shift by zero leaves the low word unchanged, then sign-extends it |
# 32-bit addition with overflow ignored (common in Java/.NET)
addw a0, a1, a2
# Sign-extend a 32-bit value to 64 bits
addiw a0, a1, 0
# 32-bit left shift
slliw a0, a1, 8
# 32-bit arithmetic right shift
sraiw a0, a1, 3
# Subtract using word width
subw a0, a1, a2
subw a0, a0, a3 # chained 32-bit subtract
Not available
There is no sltiw, sltuw, or sltiuw — comparison results are naturally
0/1 and need no truncation. There is also no andw, orw, or xorw. A
full-width bitwise operation preserves the RV64 convention that 32-bit values
are held sign-extended when both inputs already have that canonical form; if
they do not, explicitly sign-extend the result when required.
x86 connection
Closest to 32-bit operand-size on x86-64 — e.g. add eax, ebx is 32-bit addition. The critical difference: x86 zero-extends the 32-bit result into RAX for most instructions, while RISC-V *W sign-extends. There is no x86 instruction that sign-extends a 32-bit arithmetic result into 64 bits as a side effect — movsxd (x86-64) sign-extends a move but cannot combine with an arithmetic operation.
Related: ADD / ADDI, SUB, SLL / SLLI, SRL, SRA.
MUL, DIV, REM — multiplication and division (M extension)
mul rd, rs1, rs2 # rd = low XLEN bits of rs1 × rs2
mulh rd, rs1, rs2 # rd = high XLEN bits of rs1 × rs2, signed × signed
mulhu rd, rs1, rs2 # rd = high XLEN bits of rs1 × rs2, unsigned × unsigned
mulhsu rd, rs1, rs2 # rd = high XLEN bits of rs1 × rs2, signed × unsigned
div rd, rs1, rs2 # rd = rs1 ÷ rs2, signed (truncate toward zero)
divu rd, rs1, rs2 # rd = rs1 ÷ rs2, unsigned
rem rd, rs1, rs2 # rd = rs1 mod rs2, signed (remainder same sign as rs1)
remu rd, rs1, rs2 # rd = rs1 mod rs2, unsigned
Available in RV32M and RV64M. The M extension is typically included alongside the base ISA; most real cores implement it.
Multiplication — high half
A full XLEN × XLEN product is 2·XLEN bits wide. RISC-V splits the result across two destination registers:
| Instruction | rd gets | Use |
|---|---|---|
mul | product[XLEN-1 : 0] | standard multiply, low half |
mulh | product[2·XLEN-1 : XLEN] | signed × signed high half |
mulhu | product[2·XLEN-1 : XLEN] | unsigned × unsigned high half |
mulhsu | product[2·XLEN-1 : XLEN] | signed × unsigned high half |
When the low half alone is sufficient (e.g., boolean multiplication, pointer arithmetic, or any result that fits in XLEN bits), mul alone is correct regardless of signedness — the low bits of a product are the same for signed and unsigned operands.
RV32 (32-bit) example
rs1 = 0x8000_0001 (-2_147_483_647 signed / 2_147_483_649 unsigned)
rs2 = 0x0000_0002 ( 2)
mul mulh mulhu mulhsu
product[31:0] product[63:32]
signed × signed 0x0000_0002 0xFFFF_FFFF — —
unsigned × unsigned 0x0000_0002 — 0x0000_0001 —
signed × unsigned 0x0000_0002 — — 0xFFFF_FFFF
mul always writes the same 32-bit low-half result. Only the high-half instructions differ by interpretation.
MULHSU — signed × unsigned high half
mulhsu treats rs1 as signed and rs2 as unsigned. This is the instruction you reach for when you have a signed value that you want to multiply by a positive fraction stored as an unsigned fixed-point number.
rs1 (signed, XLEN bits) × rs2 (unsigned, XLEN bits)
╭────────────────────────╮ ╭────────────────────────╮
│ s │ magnitude │ │ magnitude │
╰────┬──────────────────╯ ╰────────────────────────╯
│ │
│ signed × unsigned │
│ ─────────────────────── │
│ treat rs1 as signed │
│ zero-extend rs2 to 2·XLEN │
│ multiply, sign-extend rs1 │
│ result is signed, 2·XLEN bits │
│ │
╰──────────────┬───────────────────╯
│
┌──────────┴──────────┐
│ │
mul ──┤product[XLEN-1:0] │ same low half as mul/mulhu
│ │
mulhsu ───┤product[2·XLEN-1:XLEN]│ high half (signed interpretation)
└─────────────────────┘
Without mulhsu, software must derive the mixed-sign high half from other
multiply and correction operations. mulhsu expresses it directly in one
instruction.
# Multiply a signed 64-bit value (a0) by a 64-bit unsigned fraction (a1)
# stored as a Q64 fixed-point number (i.e., the unit bit is at 2^−64).
# The product is signed with 64 fractional bits.
mulhsu t0, a0, a1 # t0 = high half
mul t1, a0, a1 # t1 = low half; sources remain unchanged
Division — edge cases
Division by zero
| Instruction | rd value |
|---|---|
div rs1, x0 | −1 (all bits set) |
divu rs1, x0 | (2^XLEN) − 1 (max unsigned value, all bits set) |
rem rs1, x0 | rs1 (dividend) |
remu rs1, x0 | rs1 (dividend) |
No exception is raised. Software must check for zero divisor beforehand if it wants to trap.
Signed overflow: DIV with min / −1
When rs1 = INT_MIN (signed minimum, 0x8000_0000 in RV32 or 0x8000_0000_0000_0000 in RV64) and rs2 = −1, the mathematical quotient is −(INT_MIN) = INT_MAX + 1, which overflows the signed range. RISC-V defines this to produce rs1 (the dividend) rather than raising an exception.
| Instruction | rd value |
|---|---|
div rs1, -1 | rs1 (the dividend — overflow) |
rem rs1, -1 | 0 |
# Safe signed division that handles both edge cases
li t0, -1
beq a1, x0, handle_zero # skip if divisor == 0
bne a1, t0, 1f # skip if divisor != -1
# divisor is -1: check for overflow
li t0, 1
slli t0, t0, (XLEN - 1) # INT_MIN
bne a0, t0, 1f # if dividend != INT_MIN, safe
j overflow_path
1:
div a0, a0, a1 # safe division
Remainder sign
The remainder always has the same sign as the dividend (rs1):
(rs1 ÷ rs2) × rs2 + (rs1 mod rs2) = rs1 # identity
sign(rs1 mod rs2) = sign(rs1) # convention
li a0, -7
li a1, 3
div t0, a0, a1 # t0 = -2 (truncate toward zero)
rem t1, a0, a1 # t1 = -1 (same sign as dividend)
# check: (-2 × 3) + (-1) = -7 ✓
RV64 word operations
mulw rd, rs1, rs2 # rd = sign_extend(product[31:0] of rs1[31:0] × rs2[31:0])
divw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] ÷ rs2[31:0]), signed
divuw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] ÷ rs2[31:0]), unsigned
remw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] mod rs2[31:0]), signed
remuw rd, rs1, rs2 # rd = sign_extend(rs1[31:0] mod rs2[31:0]), unsigned
The *W variants treat their sources as 32-bit values and write a
sign-extended 64-bit result to rd. They exist only in RV64M. There are no
mulhw, mulhuw, or mulhsuw: when the operands have first been correctly
sign- or zero-extended to 64 bits, ordinary RV64 mul produces the complete
32-by-32 product. If their upper halves are unknown, extend them first or use
the shift-and-mulh technique described by the ISA specification.
Edge cases for word division follow the same rules as the full-width instructions but operate on 32-bit values:
# 32-bit signed division, result sign-extended to 64 bits
divw a0, a1, a2 # compute a1[31:0] ÷ a2[31:0] → a0 = sign_extend(32-bit result)
# 32-bit division by zero → 0xFFFFFFFF → sign-extended to 0xFFFFFFFFFFFFFFFF
divw a0, a1, x0 # a0 = −1
# 32-bit signed overflow: INT32_MIN / −1 → INT32_MIN → sign-extended
li a1, 0x80000000 # INT32_MIN in a low 32 bits
li a2, -1
divw a0, a1, a2 # a0 = sign_extend(0x80000000) = 0xFFFFFFFF80000000
x86 connection
| RISC-V | x86 | Notes |
|---|---|---|
mul | two-/three-operand imul | both can write the low half to a general register; low-half signedness is irrelevant |
mulh / mulhu | one-operand imul / mul high half (rdx) | x86 uses the fixed rdx:rax pair; RISC-V writes the selected half to any rd |
mulhsu | no direct single instruction | x86 has no signed-by-unsigned high-half form |
div / divu | idiv / div | x86 uses a fixed dividend width (e.g., rdx:rax); RISC-V divides a single register by another |
rem / remu | idiv / div remainder (rdx) | x86 returns remainder in rdx as a side effect of division; RISC-V has separate rem |
mulw | imul eax, ebx | Both compute a 32-bit product; x86 zero-extends, RISC-V sign-extends |
divw | idiv ecx with dividend in edx:eax | x86 uses a 64-bit dividend and writes quotient/remainder to eax/edx; RISC-V takes two low-32-bit sources and sign-extends its result |
The main structural difference: x86 computes quotient and remainder
together; RISC-V uses separate div / rem instructions. When both are
needed, emitting div followed by rem with the same sources allows a
supporting implementation to fuse them into one divide operation. The quotient
destination must not overwrite either source before rem reads it.
Related: *W — RV64 word operations, SUB.
Compressed integer forms (C extension)
The C extension adds 16-bit encodings for common operations. These are not smaller registers or different arithmetic semantics: after decoding, they have the same architectural effect as their 32-bit counterparts. Mixing 16- and 32-bit instructions improves code density.
Assemblers normally choose compressed encodings automatically when the target
architecture includes C and the operands satisfy an encoding’s restrictions.
Explicit c.* mnemonics are useful when the exact encoding matters.
Zero-producing forms
c.li rd, 0 # rd ≠ x0; load signed six-bit immediate zero
c.sub rd', rs2' # rd' = rd' - rs2'
c.xor rd', rs2' # rd' = rd' XOR rs2'
c.li rd, 0 directly provides a 16-bit way to zero any integer register other
than x0. c.sub rd', rd' and c.xor rd', rd' zero a register by combining it
with itself. The primes mean the compact register set x8–x15.
c.li t0, 0
c.sub a0, a0
c.xor a1, a1
The base spellings may assemble to the same encodings:
li t0, 0 # may become c.li t0, 0
sub a0, a0, a0 # may become c.sub a0, a0
xor a1, a1, a1 # may become c.xor a1, a1
c.mv rd, x0 is not another zeroing form: the encoding requires a nonzero
source register, and the all-zero source field is reserved.
Availability and inspection
Enable C in the target ISA, for example with -march=rv64imc. Operand and
register restrictions differ between compressed instructions, so do not assume
every base instruction can shrink. Use objdump -dr -M no-aliases when exact
instruction width matters.
Related: Pseudoinstructions,
ADDI, SUB, XOR.
Bitwise operations and shifts
Register forms take two sources. Immediate forms use a suffix i. Shifts use only the low five bits of the shift amount in RV32 and low six in RV64.
AND / ANDI
and rd, rs1, rs2
andi rd, rs1, imm12
Compute a bitwise AND. ANDI sign-extends its 12-bit immediate before applying it.
andi a0, a0, 0xff # retain the low byte
and a0, a0, a1 # mask using a register
x86 connection: equivalent bit operation to and, without flags and with a separate destination.
OR / ORI
or rd, rs1, rs2
ori rd, rs1, imm12
Compute a bitwise inclusive OR. The immediate is sign-extended.
x86 connection: equivalent bit operation to or, without flags and with a separate destination.
XOR / XORI
xor rd, rs1, rs2
xori rd, rs1, imm12
Compute bitwise exclusive OR. The immediate is sign-extended. not rd, rs is the pseudoinstruction xori rd, rs, -1.
x86 connection: equivalent bit operation to xor, but xor rd, rd, rd is unnecessary for producing zero—you can use x0 directly or li rd, 0.
SLL / SLLI — shift left logical
sll rd, rs1, rs2
slli rd, rs1, shamt
Shift left and fill low bits with zero. Shift amounts are masked to the register width.
x86 connection: corresponds to shl/sal, but does not produce carry or overflow flags.
SRL, SRA and immediate forms
srl rd, rs1, rs2 # logical: insert zeroes
sra rd, rs1, rs2 # arithmetic: replicate sign bit
srli rd, rs1, shamt
srai rd, rs1, shamt
Use logical right shift for unsigned values and arithmetic right shift when you intend signed sign propagation.
x86 connection: SRL corresponds to shr; SRA corresponds to sar. RISC-V shifts do not expose the shifted-out bit through a carry flag.
Memory operations
RISC-V is load/store: arithmetic instructions operate on registers, not memory operands. Addresses use offset(base) with a signed 12-bit byte offset.
LB, LH, LW, LD — loads
lb rd, offset(rs1) # 8-bit, sign-extended
lh rd, offset(rs1) # 16-bit, sign-extended
lw rd, offset(rs1) # 32-bit, sign-extended
ld rd, offset(rs1) # 64-bit, RV64 only
Unsigned variants lbu, lhu, and RV64 lwu zero-extend instead.
ld t0, 16(sp)
lbu a0, 0(a1)
x86 connection: combines an x86 memory read with movsx or movzx; unlike x86 ALU instructions, add, and, etc. cannot directly consume a memory operand.
SB, SH, SW, SD — stores
sb rs2, offset(rs1)
sh rs2, offset(rs1)
sw rs2, offset(rs1)
sd rs2, offset(rs1) # RV64 only
Stores write the low 8, 16, 32, or 64 bits of rs2. They do not modify either register.
sd ra, 24(sp)
sw a0, 0(a1)
x86 connection: closest to mov [base + displacement], src, but base-plus-index addressing must be calculated separately.
FENCE / FENCE.I — memory ordering
fence [pred], [succ] # ordering (RV32I)
fence.i # instruction-fence sync (Zifencei)
Orders memory accesses observed by other harts (hardware threads) or external devices. Without FENCE, a RISC-V hart may reorder memory operations with respect to other harts—even stores that appear sequentially in program order.
Ordinary single-threaded code usually does not need an explicit FENCE.
Concurrent code should normally use language or compiler atomic operations,
which select the required ISA ordering. Device drivers and low-level runtime or
kernel code use fences directly when ordering memory or I/O observations.
Predecessor and successor sets
The optional pred and succ arguments each name a subset of I, O, R, W. FENCE pred, succ orders all accesses in pred before all accesses in succ from the perspective of the observing hart or device.
| Mnemonic | Meaning |
|---|---|
W | Write (store) |
R | Read (load) |
O | Device output |
I | Device input |
Note: I and O refer to device input and output (memory-mapped I/O), not instruction fetch. Instruction-fetch ordering requires FENCE.I (Zifencei).
| Common form | Ordering guarantee |
|---|---|
fence | fence iorw, iorw — all memory and device-I/O accesses before all subsequent ones |
fence w, w | store before store |
fence r, r | load before load |
fence r, w | load before store |
fence rw, rw | load/store before load/store |
fence # full ordering of memory and device-I/O accesses
fence w, w # store-store ordering (weaker than full fence)
fence r, rw # all loads before subsequent loads and stores
FENCE.I (Zifencei)
fence.i ensures that prior stores to instruction-memory regions are observed
by subsequent instruction fetches on the same hart. It is required when a
program writes machine code and then executes it. It does not by itself make
another hart’s instruction fetches coherent; the execution environment must
arrange for each affected hart to perform the required instruction-fetch
synchronization, commonly through an operating-system service or IPI.
FENCE.I belongs to the Zifencei extension on both RV32 and RV64.
# a0 points to instructions this hart has just written
fence.i # make those stores visible to later instruction fetches
jalr ra, 0(a0) # execute the generated code
x86 connection
x86 uses a stronger memory model than RVWMO, so direct instruction-for- instruction fence translations are often misleading. At a high level:
| RISC-V | x86 |
|---|---|
fence rw, rw | broadly comparable to mfence, with different architectural details |
fence.i | __builtin___clear_cache (no direct instruction) |
Related: ECALL / EBREAK.
Control flow
RISC-V compares registers as part of a branch. There is no condition-code register and usually no separate cmp instruction.
BEQ / BNE
beq rs1, rs2, label
bne rs1, rs2, label
Branch when registers are equal or unequal. beqz rs, label and bnez rs, label are pseudoinstructions comparing with x0.
x86 connection: these usually replace cmp lhs, rhs followed by je/jne; RISC-V performs the comparison inside the branch.
BLT, BGE, BLTU, BGEU
blt rs1, rs2, label # signed <
bge rs1, rs2, label # signed >=
bltu rs1, rs2, label # unsigned <
bgeu rs1, rs2, label # unsigned >=
Greater-than and less-or-equal forms are assembler aliases that reverse operands.
x86 connection: roughly replaces cmp plus jl/jge or jb/jae. Signedness is explicit in the branch mnemonic, not inferred from shared flags.
JAL / JALR
jal rd, label
jalr rd, offset(rs1)
Both write the address of the following instruction to rd and jump. call label conventionally writes ra; j label discards the link in x0; ret is a jalr through ra with its link discarded.
x86 connection: call, jmp, and ret are conventions over these two instructions. Unlike x86 call, JAL does not push a return address to memory.
Address construction
Because ordinary immediates are small, RISC-V builds large constants and addresses with an upper-immediate instruction followed by another operation.
LUI — load upper immediate
lui rd, imm20
Places the immediate into bits 31:12 and zeroes the low 12 bits; on RV64 the 32-bit result is sign-extended. Usually paired with ADDI, or generated by the li pseudoinstruction.
x86 connection: no close everyday equivalent; x86 commonly embeds a full-width immediate in one instruction.
AUIPC — add upper immediate to PC
auipc rd, imm20
Adds the upper immediate (imm20 << 12) to the address of the AUIPC instruction. It is the foundation for PC-relative address construction, calls, and position-independent code.
auipc t0, 0 # t0 receives the current PC
Relocation-aware la or call syntax is usually preferable to hand-writing the pair.
x86 connection: conceptually related to RIP-relative address formation, though RISC-V materialises the base in a general register.
Environment
These instructions transfer control to, or inspect the state of, the execution environment rather than performing ordinary application computation.
ECALL / EBREAK
ecall requests service from the execution environment. Its ABI-defined meaning depends on whether you are in Linux userspace, an SBI-aware supervisor, firmware, or another environment.
ebreak requests a breakpoint exception, commonly for a debugger.
x86 connection: ecall resembles syscall only when the surrounding ABI assigns it that role. ebreak is closest to int3.
CSRRW, CSRRS, CSRRC — CSR access
csrrw rd, csr, rs1 # write csr ← rs1, old value → rd
csrrs rd, csr, rs1 # set bits in csr from rs1
csrrc rd, csr, rs1 # clear bits in csr from rs1
csrrwi rd, csr, uimm5 # write csr ← uimm5
csrrsi rd, csr, uimm5 # set bits in csr from uimm5
csrrci rd, csr, uimm5 # clear bits in csr from uimm5
Read and write control and status registers (Zicsr extension). The 12-bit CSR address encodes both the register number and the privilege level required to access it.
Reading a CSR produces the old value in rd. Writing to a CSR (via rs1 or uimm5) can have immediate side effects defined by the specific CSR — updating timer compare values, enabling interrupts, switching address spaces, etc.
| Form | Effect | Use case |
|---|---|---|
csrrw | atomic read-write | switching a CSR to a known value |
csrrs | atomic read-set (OR) | enabling individual flags |
csrrc | atomic read-clear (AND~) | disabling individual flags |
csrrw rd, csr, x0 | read + write zero | reads csr and writes zero to it — may trigger side effects |
csrrs rd, csr, x0 | read-only | read csr, no bits set (write suppressed because rs1 = x0) |
csrrc rd, csr, x0 | read-only | read csr, no bits cleared (write suppressed because rs1 = x0) |
csrrwi | immediate read-write | always writes the zero-extended uimm5 value |
csrrsi / csrrci | immediate read-set/read-clear | a zero uimm5 suppresses the write |
# Read cycle counter when the execution environment permits access
csrr a0, cycle
# Enable machine-timer interrupts in mie, then globally enable M-mode interrupts
li t0, 1 << 7 # MTIE bit in mie
csrrs x0, mie, t0
li t0, 1 << 3 # MIE bit (machine interrupt enable)
csrrs x0, mstatus, t0
# Disable machine-mode interrupts
li t0, 1 << 3
csrrc x0, mstatus, t0
# Write satp (supervisor address translation) with a page-table root
li t0, (SATP_MODE_SV39 << SATP_MODE_OFFSET) | phys_pa4k
csrrw x0, satp, t0
Common pseudoinstructions
| Pseudo | Expansion | Effect |
|---|---|---|
csrr rd, csr | csrrs rd, csr, x0 | read CSR |
csrw csr, rs1 | csrrw x0, csr, rs1 | write CSR (discard old value) |
csrs csr, rs1 | csrrs x0, csr, rs1 | set bits |
csrc csr, rs1 | csrrc x0, csr, rs1 | clear bits |
csrwi csr, uimm5 | csrrwi x0, csr, uimm5 | write immediate |
csrsi csr, uimm5 | csrrsi x0, csr, uimm5 | set bits immediate |
csrci csr, uimm5 | csrrci x0, csr, uimm5 | clear bits immediate |
CSR address layout
The 12-bit CSR number encodes both writability and the minimum privilege level:
| Bits[11:10] | Writable? |
|---|---|
11 | read-only |
10 | read/write |
01 | read/write |
00 | read/write |
| Bits[9:8] | Minimum privilege |
|---|---|
00 | U — unprivileged |
01 | S — supervisor |
10 | H — hypervisor (if H extension present) |
11 | M — machine |
Attempting to read a CSR from an insufficient privilege level raises an illegal-instruction exception. Writes may also be read-only depending on the specific CSR.
Notable CSRs include cycle, time, instret (unprivileged counters), mtvec, mstatus, mie, mtval (machine), stvec, satp, sepc (supervisor).
x86 connection
Closest to MOV CRn / MOV DRn or RDMSR / WRMSR, but with atomic read-modify-write built into the instruction (no separate AND/OR needed). CSRs replace the role of x86 model-specific registers (MSRs) and control registers, but with a unified 12-bit address space shared across all privilege levels.
Related: ECALL / EBREAK, FENCE.
Pseudoinstructions
Pseudoinstructions are assembler syntax, not additional ISA operations. Some are fixed aliases for one real instruction. Others are assembler macros whose length depends on the operands, target architecture, position-independent-code setting, relocations, or linker relaxation.
In the tables below, one instruction describes the canonical expansion before optional compressed-instruction selection or linker relaxation. Temporary means a register in addition to the pseudo’s explicit result or source registers.
Register and arithmetic conveniences
| Pseudo | Canonical expansion | One instruction? | Relocation or code-model dependent? | Temporary? |
|---|---|---|---|---|
mv rd, rs | addi rd, rs, 0 | yes | no | no |
nop | addi x0, x0, 0 | yes | no | no |
not rd, rs | xori rd, rs, -1 | yes | no | no |
neg rd, rs | sub rd, x0, rs | yes | no | no |
negw rd, rs (RV64) | subw rd, x0, rs | yes | no | no |
sext.w rd, rs (RV64) | addiw rd, rs, 0 | yes | no | no |
li rd, imm | operand-dependent constant construction | not always | operand and XLEN; no symbol relocation | no |
Comparisons that write 0 or 1
| Pseudo | Canonical expansion | One instruction? | Relocation or code-model dependent? | Temporary? |
|---|---|---|---|---|
seqz rd, rs | sltiu rd, rs, 1 | yes | no | no |
snez rd, rs | sltu rd, x0, rs | yes | no | no |
sltz rd, rs | slt rd, rs, x0 | yes | no | no |
sgtz rd, rs | slt rd, x0, rs | yes | no | no |
These names make the intended relation to zero explicit. For two arbitrary
registers, use slt or sltu and swap operands or invert the Boolean when the
opposite relation is needed.
Branch conveniences
| Pseudo | Canonical expansion | One instruction? | Relocation or code-model dependent? | Temporary? |
|---|---|---|---|---|
beqz rs, label | beq rs, x0, label | yes | no 1 | no |
bnez rs, label | bne rs, x0, label | yes | no 1 | no |
bltz rs, label | blt rs, x0, label | yes | no 1 | no |
bgez rs, label | bge rs, x0, label | yes | no 1 | no |
bgtz rs, label | blt x0, rs, label | yes | no 1 | no |
blez rs, label | bge x0, rs, label | yes | no 1 | no |
bgt rs, rt, label | blt rt, rs, label | yes | no 1 | no |
ble rs, rt, label | bge rt, rs, label | yes | no 1 | no |
bgtu rs, rt, label | bltu rt, rs, label | yes | no 1 | no |
bleu rs, rt, label | bgeu rt, rs, label | yes | no 1 | no |
The reversed forms work by swapping the two source operands; they do not need a separate comparison instruction.
Calls, jumps, and returns
| Pseudo | Canonical expansion | One instruction? | Relocation or code-model dependent? | Temporary? |
|---|---|---|---|---|
j label | jal x0, label | yes | relocation resolves target | no |
jr rs | jalr x0, 0(rs) | yes | no | no |
ret | jalr x0, 0(ra) | yes | no | no |
call symbol | auipc ra, ...; jalr ra, ... | not always | yes; linker may relax it | no extra register |
tail symbol | auipc t1, ...; jalr x0, ... | not always | yes; linker may relax it | yes, t1 (x6) |
call rd, symbol is also accepted by GNU-compatible assemblers when a link
register other than ra is intentional. A plain call symbol uses ra.
CSR conveniences
These require the Zicsr extension. Each is one instruction, has no symbol relocation or code-model choice, and uses no temporary register.
| Pseudo | Canonical expansion | Effect |
|---|---|---|
csrr rd, csr | csrrs rd, csr, x0 | read CSR without changing it |
csrw csr, rs | csrrw x0, csr, rs | write CSR; discard old value |
csrs csr, rs | csrrs x0, csr, rs | set selected bits |
csrc csr, rs | csrrc x0, csr, rs | clear selected bits |
csrwi csr, uimm5 | csrrwi x0, csr, uimm5 | write a 5-bit immediate |
csrsi csr, uimm5 | csrrsi x0, csr, uimm5 | set bits from an immediate |
csrci csr, uimm5 | csrrci x0, csr, uimm5 | clear bits from an immediate |
See CSR access for the side-effect rules that distinguish
csrrw from csrrs/csrrc when a source or destination is x0.
Addresses and symbols
| Pseudo | Canonical expansion | One instruction? | Relocation or code-model dependent? | Temporary? |
|---|---|---|---|---|
lla rd, symbol | PC-relative auipc + addi | no | relocations; PIC setting does not change it | no extra register |
lga rd, symbol | GOT-relative auipc + lw/ld | no | relocations, ABI, and XLEN | no extra register |
la rd, symbol | lla in non-PIC code; lga in PIC code | no | yes; .option pic selects the form | no extra register |
Here the destination register doubles as the working address register, so it is modified by the first instruction but no additional temporary is consumed.
Symbol loads and stores
GNU-compatible assemblers accept address-forming macros that combine a symbol reference with a load or store. They are convenient, but they are not ordinary base load/store syntax and are not guaranteed by every assembler.
| Assembler form | Canonical expansion | One instruction? | Relocation or code-model dependent? | Temporary? |
|---|---|---|---|---|
lb/lbu/lh/lhu/lw/lwu/ld rd, symbol | auipc rd, %pcrel_hi(symbol); matching load into rd | no | PC-relative relocations; width/XLEN constraints | no extra register |
sb/sh/sw/sd rs, symbol, tmp | auipc tmp, %pcrel_hi(symbol); matching store via tmp | no | PC-relative relocations; width/XLEN constraints | yes, explicit tmp |
flw/fld fd, symbol, tmp | auipc tmp, %pcrel_hi(symbol); FP load via tmp | no | PC-relative relocations and enabled FP extension | yes, explicit tmp |
fsw/fsd fs, symbol, tmp | auipc tmp, %pcrel_hi(symbol); FP store via tmp | no | PC-relative relocations and enabled FP extension | yes, explicit tmp |
The second instruction carries the matching %pcrel_lo relocation. Integer
loads can reuse rd for address construction because the load overwrites it.
Stores and floating-point loads cannot, so their syntax names a temporary
integer register. These forms address the symbol directly; use la/lga plus
an explicit load when GOT-based interposition semantics must be clear.
Inspect the emitted code
Canonical expansions explain the meaning, not necessarily the final bytes.
The C extension can provide a compressed encoding for some aliases, and linker
relaxation can shorten relocation-marked sequences. Use objdump -dr on object
files to see relocations and on the linked executable to see the final code.
-
The label is still resolved by an assembler or linker relocation. That changes the encoded displacement, not the canonical pseudoinstruction expansion. An out-of-range conditional branch generally requires the programmer or another tool to provide a longer sequence. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
li — load an integer constant
li rd, constant
li constructs an integer constant in rd. It never loads from memory and it
does not refer to a symbol. Its expansion is determined by the constant, XLEN,
and the assembler’s instruction-selection policy.
Typical expansions
| Constant | Typical expansion | Always this expansion? | Temporary? |
|---|---|---|---|
| signed 12-bit value | addi rd, x0, constant | one base instruction, though it may compress | no |
| suitable 32-bit value | lui rd, upper; optional addi/addiw rd, rd, lower | no | no |
| general RV64 value | lui/addi plus one or more shifts and additions | no | no |
li t0, 42
li t1, 0x12345678
li t2, 0x123456789abcdef0 # RV64: usually several instructions
The assembler accounts for sign extension when splitting a value into upper and lower pieces. Hand-splitting a constant with a simple shift and mask can be wrong when the low 12-bit part is negative.
What can change the expansion?
- The value itself and whether the target is RV32 or RV64.
- Enabled extensions, especially compressed instructions and extensions that offer another efficient constant-building operation.
- Assembler version and optimization policy.
No symbol relocation or code model is involved. rd is used throughout; li
does not consume a separate temporary register. Because the sequence is not
fixed, use objdump -dr when exact size matters.
Use la rather than li for a symbol address.
la, lla, and lga — load a symbol address
These macros materialize an address and attach the relocations needed for the linker. Their canonical two-instruction forms may later be relaxed.
lla — local address
lla rd, symbol
lla requests a PC-relative address directly, conventionally:
.Lanchor:
auipc rd, %pcrel_hi(symbol)
addi rd, rd, %pcrel_lo(.Lanchor)
It is normally two instructions, uses no register besides rd, and is not
selected by the PIC setting. The paired relocations are essential: the low
relocation refers to the auipc anchor, not independently to symbol.
lga — global address
lga rd, symbol
lga obtains a potentially preemptible global symbol through the global offset
table (GOT):
.Lanchor:
auipc rd, %got_pcrel_hi(symbol)
ld rd, %pcrel_lo(.Lanchor)(rd) # RV64
RV32 uses lw instead of ld. The destination doubles as the GOT-address
temporary, so no extra register is consumed. The expansion depends on the ABI,
XLEN, relocations, symbol binding, and linker relaxation.
la — load address
la rd, symbol
la chooses between the two policies:
- With
.option nopic(the usual default), it behaves likella. - With
.option pic, it behaves likelga.
That makes la convenient but deliberately not a fixed instruction sequence.
Use lla or lga when the distinction is important to the reader.
Relocation and relaxation
All three forms are relocation-dependent and normally begin as two
instructions. The linker can sometimes relax the sequence when symbol binding
and distance permit. Disassemble the linked output, not only the .o file, to
measure the final sequence.
call — call a symbol
call symbol
call rd, symbol # GNU-compatible explicit link-register form
call symbol transfers control and writes the return address to ra. Its
canonical relocation-aware expansion can reach beyond jal’s direct range:
auipc ra, %pcrel_hi(symbol)
jalr ra, %pcrel_lo(symbol)(ra)
The actual object uses a call relocation that keeps the pair together; the relocation spelling above is explanatory rather than a recommendation to write the pair by hand.
Is it always one instruction?
No. The assembler normally emits an auipc/jalr pair. If the final target is
in range and symbol binding permits it, the linker may relax that pair to
jal ra, symbol, or to an applicable compressed form when the target ISA and
register constraints allow one.
Symbol binding, target distance, enabled extensions, and linker relaxation can
therefore change the final expansion. The .option pic setting does not select
a different canonical call form.
Register use
The ordinary form uses ra both as the link register and as the intermediate
PC-relative address register. It does not consume an additional temporary. The
explicit call rd, symbol form uses rd in those roles instead.
Unlike an x86 call, no return address is pushed to memory. Saving ra and
building a stack frame are separate ABI responsibilities.
tail — tail-call a symbol
tail symbol
tail transfers control without creating a new return address. The current
function’s caller remains the eventual return destination. Its canonical
relocation-aware form is:
auipc t1, %pcrel_hi(symbol)
jalr x0, %pcrel_lo(symbol)(t1)
GNU-compatible assemblers reserve t1 (x6) as the scratch register for this
macro. The call relocation on the emitted pair carries the exact linker
semantics; the explicit modifiers above show the address calculation.
Is it always one instruction?
No. It normally starts as two instructions. The linker may relax it to
jal x0, symbol, or to an applicable compressed jump, when the final target and
enabled ISA allow it. Symbol binding, distance, enabled extensions, and
relaxation can therefore change the final sequence; .option pic does not
select a different canonical tail form.
ABI consequences
tail deliberately does not write ra, but it clobbers t1. Before using it,
the function must restore any stack frame and callee-saved registers just as it
would before returning. Arguments must already be arranged according to the
calling convention.
RISC-V for x86 developers
The safest translation unit is an operation, not a mnemonic. Some operations map one-to-one; others become short sequences or disappear because RISC-V exposes different architectural state.
Three differences explain most surprises:
- RISC-V integer instructions normally use separate destination and source operands.
- Arithmetic operates on registers; loads and stores handle memory.
- There is no general flags register. Branches compare values directly.
Start with the instruction cross-reference, then read conditionals and flags.
x86 instruction cross-reference
This table maps programmer intent. “Sequence” means there is no single direct equivalent.
| x86 concept | RISC-V | Mapping | Important difference |
|---|---|---|---|
mov reg, reg | mv pseudo | direct | expands to an integer instruction |
mov reg, imm | li pseudo | direct intent | may expand to several instructions |
mov reg, [mem] | lb/lh/lw/ld, unsigned variants | direct | extension behaviour is explicit |
mov [mem], reg | sb/sh/sw/sd | direct | no general memory-to-memory move |
movzx / movsx load | unsigned/signed load variant | direct | selected by load mnemonic |
add | add / addi | direct | three operands; no flags |
sub | sub; addi with negative immediate | direct/near | no subi in the base ISA |
inc / dec | addi rd, rs, 1/-1 | direct intent | no dedicated opcode; no flags |
and / or / xor | same names, or andi/ori/xori | direct | immediate forms are separate |
not / neg | same-name pseudos | direct intent | aliases over base instructions |
shl / sal | sll / slli | direct | no carry flag |
shr | srl / srli | direct | logical right shift |
sar | sra / srai | direct | arithmetic right shift |
lea dst, [a+b] | add dst, a, b | often direct | complex scale/index forms need a sequence |
lea dst, [rip+disp] | auipc pair / la | near | relocation-aware sequence |
cmp a, b; je/jne | beq / bne | fused intent | no stored flags |
cmp; jl/jge | blt / bge | fused intent | signedness in mnemonic |
cmp; jb/jae | bltu / bgeu | fused intent | unsigned comparison |
setl / setb / setcc | slt / sltu and immediate forms | direct | writes a full register, not a byte; no flags needed |
test r, r; jz | beqz r, label pseudo | fused intent | compares directly with x0 |
jmp label | j label pseudo | direct intent | expands to jal x0 |
jmp reg | jr reg pseudo | direct intent | expands to jalr x0 |
call | call pseudo / jal ra | direct intent | return address goes in ra, not on stack |
ret | ret pseudo | direct intent | jumps through ra |
push / pop | adjust sp plus stores/loads | sequence | deliberately not single instructions |
syscall | ecall | environment-dependent | ABI supplies syscall convention |
int3 | ebreak | near | architectural exception model differs |
nop | nop pseudo | direct intent | canonical encoding is addi x0,x0,0 |
cmovcc | branch sequence or Zicond operations | sequence/extension | base ISA has no flags or conditional move |
rep movsb | loop or vector/library implementation | sequence | no base string engine |
Conditionals and flags
x86 often separates comparison from use:
cmp rax, rbx
jl smaller
RISC-V puts the comparison into the branch:
blt a0, a1, smaller
This means there is no stale-flags hazard, but it also means carry, borrow and overflow must be represented explicitly when needed. For example, unsigned addition carry can be detected by comparing the sum with an operand:
add t0, a0, a1
sltu t1, t0, a0 # t1 = carry-out
See SLT / SLTI / SLTU / SLTIU.
Do not translate cmp in isolation. Translate the compare-plus-consumer operation.
Calls, returns and the stack
x86 call implicitly pushes a return address. RISC-V jal ra, target writes it to ra. A non-leaf function saves ra itself if it will make another call.
addi sp, sp, -16
sd ra, 8(sp)
call child
ld ra, 8(sp)
addi sp, sp, 16
ret
There are no base push and pop instructions. Stack movement and memory access remain visible and independently schedulable.
Addressing and memory
x86 can fold base, index, scale and displacement into many operations:
mov rax, [rbx + rcx*8 + 16]
RISC-V memory instructions accept a base register and signed 12-bit byte offset. Calculate the indexed address first:
slli t0, a1, 3
add t0, a0, t0
ld t1, 16(t0)
This is a normal expression of the load/store design, not necessarily three serialized hardware operations; implementations may overlap or fuse work internally.
What deliberately has no direct equivalent
Do not hunt indefinitely for single-instruction matches to these x86 features:
- implicit flags and condition-code consumers;
- general memory operands on arithmetic instructions;
pushandpop;loopandjrcxz-style specialised branches;repstring operations;- multi-component effective addresses in a load/store;
- variable-length prefix-driven instruction variants;
- a hardware-maintained return address on the ordinary stack.
Some optional RISC-V extensions add higher-level operations, but the base translation is usually a short, explicit sequence.
Instructions x86 has and RISC-V doesn’t
Several x86 staples have no direct single-instruction equivalent in the base RISC-V ISA. The table below shows what you might instinctively reach for and what you should write instead.
Arithmetic
| You want … | In x86 | RISC-V replacement |
|---|---|---|
| subtract immediate | sub r, imm | addi rd, rs, -imm (no subi exists; negate the immediate) |
ADDI sign-extends its 12-bit immediate, so a single addi covers any small subi. For subtract-immediate with an out-of-range constant, load the immediate then use SUB.
# x86: sub eax, 42
addi a0, a0, -42
# x86: sub eax, 100000 (fits in 12-bit signed? -100000 = 0xFFFE7960, no)
li t0, 100000
sub a0, a0, t0
Moves
| You want … | In x86 | RISC-V replacement |
|---|---|---|
| register-to-register copy | mov r1, r2 | mv rd, rs (pseudo: addi rd, rs, 0) |
| immediate-to-register | mov r, imm32 | li rd, imm (pseudo, may expand to addi + lui + shifts) |
| memory-to-register | mov r, [addr] | load instruction (lb/lh/lw/ld) with address in a register |
| register-to-memory | mov [addr], r | store instruction (sb/sh/sw/sd) with address in a register |
x86’s mov is overloaded across widths, addressing modes, and source types. RISC-V separates each concern into distinct instructions. The only “move” in the base ISA is ADDI with a zero immediate — everything else is a pseudoinstruction or an explicit load/store.
# x86: mov eax, ebx
mv a0, a1 # actually addi a0, a1, 0
# x86: mov eax, 0x1234
li a0, 0x1234
# x86: mov eax, [ebx]
lw a0, 0(a1)
# x86: mov [ebx], eax
sw a0, 0(a1)
Logical
| You want … | In x86 | RISC-V replacement |
|---|---|---|
| bitwise NOT | not r | not rd, rs (pseudo: xori rd, rs, -1) |
| two’s-complement negate | neg r | neg rd, rs (pseudo: sub rd, x0, rs) |
Neither NOT nor NEG are real RISC-V instructions. The assembler accepts them as pseudoinstructions that expand to a single XORI or SUB with x0.
not a0, a1 # actually xori a0, a1, -1
neg a0, a1 # actually sub a0, x0, a1
Branches
| You want … | In x86 | RISC-V replacement |
|---|---|---|
| branch if zero | jz label | beqz rs, label (pseudo: beq rs, x0, label) |
| branch if not zero | jnz label | bnez rs, label (pseudo: bne rs, x0, label) |
RISC-V branches always take two register operands — there is no single-operand branch in the base ISA. The common beqz/bnez mnemonics are pseudoinstructions supplied by the assembler.
beqz a0, target # actually beq a0, x0, target
bnez a0, target # actually bne a0, x0, target
Stack
| You want … | In x86 | RISC-V replacement |
|---|---|---|
| push register | push r | addi sp, sp, -8 / sd rs, 0(sp) |
| pop register | pop r | ld rd, 0(sp) / addi sp, sp, 8 |
RISC-V has no stack-oriented instructions. The stack pointer (sp = x2) is a regular register with no hardware-backed push/pop semantics. Stack frames are managed explicitly.
# push a0
addi sp, sp, -8
sd a0, 0(sp)
# pop a0
ld a0, 0(sp)
addi sp, sp, 8
The standard calling convention designates sp as the stack pointer and requires it to remain 16-byte aligned at function-call boundaries, but no instruction enforces this — it is purely ABI convention.
Summary
| x86 mnemonic | Exists in RISC-V? | What to use |
|---|---|---|
sub with immediate | No real subi | addi with negated immediate |
mov | No single instruction | mv, li, loads, stores |
not | No real instruction | not pseudo (xori) |
neg | No real instruction | neg pseudo (sub rd, x0, rs) |
jz / jnz | No single-operand branch | beqz / bnez pseudo |
push / pop | No | explicit sp adjustment + load/store |
Related: What deliberately has no direct equivalent, Pseudoinstructions.
Glossary of acronyms and terms
This glossary expands abbreviations used throughout the guide. Instruction
mnemonics such as ADDI and JALR are documented on their instruction pages
rather than repeated here.
| Term | Meaning |
|---|---|
| ABI | Application Binary Interface. Conventions that let separately built code interoperate, including register use, argument passing, stack layout, and object-file details. See registers and ABI names. |
| ALU | Arithmetic Logic Unit. The part of a processor that performs integer arithmetic, comparisons, shifts, and bitwise operations. |
| CSR | Control and Status Register. A register in RISC-V’s separate 12-bit CSR address space, accessed with Zicsr instructions. See CSR access. |
| EEI | Execution Environment Interface. The contract between software and the environment that runs it, defining matters such as traps, memory behavior, and the meaning of environment calls. |
| ELF | Executable and Linkable Format. The common object-file, executable, and shared-library format used by RISC-V Unix-like toolchains. |
| GOT | Global Offset Table. A linker-managed table used to obtain addresses of global symbols, especially in position-independent code. See la, lla, and lga. |
| GPR | General-Purpose Register. One of the integer registers x0–x31 in the standard base ISA. |
| IPI | Interprocessor Interrupt. A notification sent from one hart to another, commonly used by operating systems for cross-hart coordination. |
| ISA | Instruction Set Architecture. The programmer-visible contract implemented by a processor: instructions, registers, exceptions, memory rules, and related architectural behavior. |
| MIE | Machine Interrupt Enable. Depending on context, the global M-mode interrupt-enable bit in mstatus, or the mie CSR that enables individual machine interrupt sources. |
| MTIE | Machine Timer Interrupt Enable. The bit in the mie CSR that enables machine-timer interrupts. |
| PC | Program Counter. The address associated with the instruction currently being executed. RISC-V has no ordinary instruction that directly reads a dedicated PC register; PC-relative operations such as AUIPC use it implicitly. See AUIPC. |
| PIC | Position-Independent Code. Code that can execute correctly after being loaded at different addresses, normally using PC-relative addressing and linker-managed tables. |
| PLT | Procedure Linkage Table. Linker-generated machinery used by some ELF environments to resolve calls to externally defined functions. |
| psABI | Processor-Specific Application Binary Interface. The architecture-specific portion of an ABI, including RISC-V calling conventions, ELF relocations, and object-file rules. |
| RISC | Reduced Instruction Set Computer. A processor design tradition favoring regular instruction encodings and explicit load/store operations. |
| RV32 / RV64 | RISC-V ISA families with 32- or 64-bit integer registers respectively. A suffix names included ISA components: for example, RV64I is the 64-bit base integer ISA and RV64IMC additionally includes M and C. |
| RVWMO | RISC-V Weak Memory Ordering. The standard relaxed memory-consistency model used by RISC-V. See FENCE. |
| SBI | Supervisor Binary Interface. The interface commonly used by a RISC-V supervisor operating system to request services from machine-mode firmware. |
| TLS | Thread-Local Storage. Per-thread data addressed using ABI- and linker-defined mechanisms; the tp register conventionally participates in locating it. |
| XLEN | Integer register width. The number of bits in an integer register for the current ISA: normally 32 in RV32 or 64 in RV64. |
Common non-acronym terms
| Term | Meaning |
|---|---|
| hart | A hardware thread: one independently executing RISC-V instruction stream with its own architectural state. |
| immediate | A constant encoded as part of an instruction rather than read from a register or memory. |
| pseudoinstruction | Assembler syntax that expands into one or more real instructions. See pseudoinstructions. |
| relocation | Metadata asking the assembler or linker to fill in or adjust a value once a symbol’s final address is known. |
Sources and further reading
Authoritative
- RISC-V Instruction Set Manual, Volume I
- RISC-V Instruction Set Manual, Volume II
- RISC-V ELF psABI specification
Programmer-oriented inspiration
Descriptions in this book are original summaries. Links are provided for verification and deeper reading; external content remains subject to its own licence.
MNEMONIC — short name
mnemonic rd, rs1, rs2
One-sentence operation. State RV32/RV64 availability and required extension.
# minimal example
Watch for
- Immediate range, signedness, width, alignment or trap caveat.
- Whether this is real or pseudo assembler syntax.
x86 connection
Direct equivalent, short sequence, or explicitly no meaningful equivalent.
See also
Related instructions and the normative specification section.