Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RISC-V Field Guide

The RISC-V Field Guide

add immediate load 64-bit cmp call zero extend

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:

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

OperationSigned formUnsigned formNotes
byte loadLBLBULB sign-extends to XLEN; LBU zero-extends
halfword loadLHLHUsame pattern as byte loads
word loadLWLWUboth load 32 bits; LW sign-extends to XLEN, LWU zero-extends; LWU is RV64-only
less-than branchBLTBLTUsame register encoding, different comparison
less-than setSLT / SLTISLTU / SLTIUproduces a 0/1 boolean
comparison branchesBGEBGEUgreater-or-equal counterpart
divisionDIVDIVUquotient; remainder follows (REM / REMU)
multiply highMULHMULHUhigh 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.

InstructionWhy no separate form
ADD / ADDITwo’s-complement addition is bitwise identical for signed and unsigned
SUBSame as addition — a − b = a + (~b + 1) works the same way
AND / OR / XORBitwise operations have no signedness
SLLLeft shift has the same bit result for signed and unsigned values
SRL / SRARight 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 x0x31 and conventional ABI names. x0 always reads as zero; writes to it are discarded.

RegisterABIConventional purposePreserved across calls?
x0zerohard-wired zeron/a
x1rareturn addressno
x2spstack pointeryes
x3gpglobal pointerspecial
x4tpthread pointerspecial
x5x7t0t2temporariesno
x8s0 / fpsaved register / frame pointeryes
x9s1saved registeryes
x10x17a0a7arguments / return valuesno
x18x27s2s11saved registersyes
x28x31t3t6temporariesno

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:

  1. You will be more likely to understand what you are asking for when you ask an LLM for assistance.
  2. 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.

Begin the rituals.

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 a0a2. 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.

Explore the incantations.

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.

Related: ADDI, SUB.

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.

AssemblyImmediate after sign-extensionEffect
sltiu rd, rs, 00rd = (rs < 0) — always 0, since no unsigned value is below 0
sltiu rd, rs, -10xFFFF…FFFF (max XLEN)rd = (rs < max) — equivalent to rd = (rs != UINT_MAX)
sltiu rd, rs, 11rd = (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

PseudoExpansionEffect
sltz rd, rsslt rd, rs, x0set if less than zero
sgtz rd, rsslt rd, x0, rsset if greater than zero
seqz rd, rssltiu rd, rs, 1set if equal to zero
snez rd, rssltu rd, x0, rsset 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, 0slli + srai, or sext.w pseudosign-extend bit 31 to fill bits 63–32 (the + 0 is a no-op)
addw rd, rs1, rs2add + slli + sraisingle instruction, truncates then sign-extends inline
slliw rd, rs, 0slli + srai, or sext.w pseudoshift 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:

Instructionrd getsUse
mulproduct[XLEN-1 : 0]standard multiply, low half
mulhproduct[2·XLEN-1 : XLEN]signed × signed high half
mulhuproduct[2·XLEN-1 : XLEN]unsigned × unsigned high half
mulhsuproduct[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

Instructionrd value
div rs1, x0−1 (all bits set)
divu rs1, x0(2^XLEN) − 1 (max unsigned value, all bits set)
rem rs1, x0rs1 (dividend)
remu rs1, x0rs1 (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.

Instructionrd value
div rs1, -1rs1 (the dividend — overflow)
rem rs1, -10
# 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-Vx86Notes
multwo-/three-operand imulboth can write the low half to a general register; low-half signedness is irrelevant
mulh / mulhuone-operand imul / mul high half (rdx)x86 uses the fixed rdx:rax pair; RISC-V writes the selected half to any rd
mulhsuno direct single instructionx86 has no signed-by-unsigned high-half form
div / divuidiv / divx86 uses a fixed dividend width (e.g., rdx:rax); RISC-V divides a single register by another
rem / remuidiv / div remainder (rdx)x86 returns remainder in rdx as a side effect of division; RISC-V has separate rem
mulwimul eax, ebxBoth compute a 32-bit product; x86 zero-extends, RISC-V sign-extends
divwidiv ecx with dividend in edx:eaxx86 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 x8x15.

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.

MnemonicMeaning
WWrite (store)
RRead (load)
ODevice output
IDevice 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 formOrdering guarantee
fencefence iorw, iorw — all memory and device-I/O accesses before all subsequent ones
fence w, wstore before store
fence r, rload before load
fence r, wload before store
fence rw, rwload/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-Vx86
fence rw, rwbroadly 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.

FormEffectUse case
csrrwatomic read-writeswitching a CSR to a known value
csrrsatomic read-set (OR)enabling individual flags
csrrcatomic read-clear (AND~)disabling individual flags
csrrw rd, csr, x0read + write zeroreads csr and writes zero to it — may trigger side effects
csrrs rd, csr, x0read-onlyread csr, no bits set (write suppressed because rs1 = x0)
csrrc rd, csr, x0read-onlyread csr, no bits cleared (write suppressed because rs1 = x0)
csrrwiimmediate read-writealways writes the zero-extended uimm5 value
csrrsi / csrrciimmediate read-set/read-cleara 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

PseudoExpansionEffect
csrr rd, csrcsrrs rd, csr, x0read CSR
csrw csr, rs1csrrw x0, csr, rs1write CSR (discard old value)
csrs csr, rs1csrrs x0, csr, rs1set bits
csrc csr, rs1csrrc x0, csr, rs1clear bits
csrwi csr, uimm5csrrwi x0, csr, uimm5write immediate
csrsi csr, uimm5csrrsi x0, csr, uimm5set bits immediate
csrci csr, uimm5csrrci x0, csr, uimm5clear bits immediate

CSR address layout

The 12-bit CSR number encodes both writability and the minimum privilege level:

Bits[11:10]Writable?
11read-only
10read/write
01read/write
00read/write
Bits[9:8]Minimum privilege
00U — unprivileged
01S — supervisor
10H — hypervisor (if H extension present)
11M — 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

PseudoCanonical expansionOne instruction?Relocation or code-model dependent?Temporary?
mv rd, rsaddi rd, rs, 0yesnono
nopaddi x0, x0, 0yesnono
not rd, rsxori rd, rs, -1yesnono
neg rd, rssub rd, x0, rsyesnono
negw rd, rs (RV64)subw rd, x0, rsyesnono
sext.w rd, rs (RV64)addiw rd, rs, 0yesnono
li rd, immoperand-dependent constant constructionnot alwaysoperand and XLEN; no symbol relocationno

Comparisons that write 0 or 1

PseudoCanonical expansionOne instruction?Relocation or code-model dependent?Temporary?
seqz rd, rssltiu rd, rs, 1yesnono
snez rd, rssltu rd, x0, rsyesnono
sltz rd, rsslt rd, rs, x0yesnono
sgtz rd, rsslt rd, x0, rsyesnono

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

PseudoCanonical expansionOne instruction?Relocation or code-model dependent?Temporary?
beqz rs, labelbeq rs, x0, labelyesno 1no
bnez rs, labelbne rs, x0, labelyesno 1no
bltz rs, labelblt rs, x0, labelyesno 1no
bgez rs, labelbge rs, x0, labelyesno 1no
bgtz rs, labelblt x0, rs, labelyesno 1no
blez rs, labelbge x0, rs, labelyesno 1no
bgt rs, rt, labelblt rt, rs, labelyesno 1no
ble rs, rt, labelbge rt, rs, labelyesno 1no
bgtu rs, rt, labelbltu rt, rs, labelyesno 1no
bleu rs, rt, labelbgeu rt, rs, labelyesno 1no

The reversed forms work by swapping the two source operands; they do not need a separate comparison instruction.

Calls, jumps, and returns

PseudoCanonical expansionOne instruction?Relocation or code-model dependent?Temporary?
j labeljal x0, labelyesrelocation resolves targetno
jr rsjalr x0, 0(rs)yesnono
retjalr x0, 0(ra)yesnono
call symbolauipc ra, ...; jalr ra, ...not alwaysyes; linker may relax itno extra register
tail symbolauipc t1, ...; jalr x0, ...not alwaysyes; linker may relax ityes, 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.

PseudoCanonical expansionEffect
csrr rd, csrcsrrs rd, csr, x0read CSR without changing it
csrw csr, rscsrrw x0, csr, rswrite CSR; discard old value
csrs csr, rscsrrs x0, csr, rsset selected bits
csrc csr, rscsrrc x0, csr, rsclear selected bits
csrwi csr, uimm5csrrwi x0, csr, uimm5write a 5-bit immediate
csrsi csr, uimm5csrrsi x0, csr, uimm5set bits from an immediate
csrci csr, uimm5csrrci x0, csr, uimm5clear 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

PseudoCanonical expansionOne instruction?Relocation or code-model dependent?Temporary?
lla rd, symbolPC-relative auipc + addinorelocations; PIC setting does not change itno extra register
lga rd, symbolGOT-relative auipc + lw/ldnorelocations, ABI, and XLENno extra register
la rd, symbollla in non-PIC code; lga in PIC codenoyes; .option pic selects the formno 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 formCanonical expansionOne instruction?Relocation or code-model dependent?Temporary?
lb/lbu/lh/lhu/lw/lwu/ld rd, symbolauipc rd, %pcrel_hi(symbol); matching load into rdnoPC-relative relocations; width/XLEN constraintsno extra register
sb/sh/sw/sd rs, symbol, tmpauipc tmp, %pcrel_hi(symbol); matching store via tmpnoPC-relative relocations; width/XLEN constraintsyes, explicit tmp
flw/fld fd, symbol, tmpauipc tmp, %pcrel_hi(symbol); FP load via tmpnoPC-relative relocations and enabled FP extensionyes, explicit tmp
fsw/fsd fs, symbol, tmpauipc tmp, %pcrel_hi(symbol); FP store via tmpnoPC-relative relocations and enabled FP extensionyes, 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.


  1. 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

ConstantTypical expansionAlways this expansion?Temporary?
signed 12-bit valueaddi rd, x0, constantone base instruction, though it may compressno
suitable 32-bit valuelui rd, upper; optional addi/addiw rd, rd, lowernono
general RV64 valuelui/addi plus one or more shifts and additionsnono
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 like lla.
  • With .option pic, it behaves like lga.

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:

  1. RISC-V integer instructions normally use separate destination and source operands.
  2. Arithmetic operates on registers; loads and stores handle memory.
  3. 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 conceptRISC-VMappingImportant difference
mov reg, regmv pseudodirectexpands to an integer instruction
mov reg, immli pseudodirect intentmay expand to several instructions
mov reg, [mem]lb/lh/lw/ld, unsigned variantsdirectextension behaviour is explicit
mov [mem], regsb/sh/sw/sddirectno general memory-to-memory move
movzx / movsx loadunsigned/signed load variantdirectselected by load mnemonic
addadd / addidirectthree operands; no flags
subsub; addi with negative immediatedirect/nearno subi in the base ISA
inc / decaddi rd, rs, 1/-1direct intentno dedicated opcode; no flags
and / or / xorsame names, or andi/ori/xoridirectimmediate forms are separate
not / negsame-name pseudosdirect intentaliases over base instructions
shl / salsll / sllidirectno carry flag
shrsrl / srlidirectlogical right shift
sarsra / sraidirectarithmetic right shift
lea dst, [a+b]add dst, a, boften directcomplex scale/index forms need a sequence
lea dst, [rip+disp]auipc pair / lanearrelocation-aware sequence
cmp a, b; je/jnebeq / bnefused intentno stored flags
cmp; jl/jgeblt / bgefused intentsignedness in mnemonic
cmp; jb/jaebltu / bgeufused intentunsigned comparison
setl / setb / setccslt / sltu and immediate formsdirectwrites a full register, not a byte; no flags needed
test r, r; jzbeqz r, label pseudofused intentcompares directly with x0
jmp labelj label pseudodirect intentexpands to jal x0
jmp regjr reg pseudodirect intentexpands to jalr x0
callcall pseudo / jal radirect intentreturn address goes in ra, not on stack
retret pseudodirect intentjumps through ra
push / popadjust sp plus stores/loadssequencedeliberately not single instructions
syscallecallenvironment-dependentABI supplies syscall convention
int3ebreakneararchitectural exception model differs
nopnop pseudodirect intentcanonical encoding is addi x0,x0,0
cmovccbranch sequence or Zicond operationssequence/extensionbase ISA has no flags or conditional move
rep movsbloop or vector/library implementationsequenceno 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;
  • push and pop;
  • loop and jrcxz-style specialised branches;
  • rep string 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 x86RISC-V replacement
subtract immediatesub r, immaddi 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 x86RISC-V replacement
register-to-register copymov r1, r2mv rd, rs (pseudo: addi rd, rs, 0)
immediate-to-registermov r, imm32li rd, imm (pseudo, may expand to addi + lui + shifts)
memory-to-registermov r, [addr]load instruction (lb/lh/lw/ld) with address in a register
register-to-memorymov [addr], rstore 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 x86RISC-V replacement
bitwise NOTnot rnot rd, rs (pseudo: xori rd, rs, -1)
two’s-complement negateneg rneg 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 x86RISC-V replacement
branch if zerojz labelbeqz rs, label (pseudo: beq rs, x0, label)
branch if not zerojnz labelbnez 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 x86RISC-V replacement
push registerpush raddi sp, sp, -8 / sd rs, 0(sp)
pop registerpop rld 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 mnemonicExists in RISC-V?What to use
sub with immediateNo real subiaddi with negated immediate
movNo single instructionmv, li, loads, stores
notNo real instructionnot pseudo (xori)
negNo real instructionneg pseudo (sub rd, x0, rs)
jz / jnzNo single-operand branchbeqz / bnez pseudo
push / popNoexplicit 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.

TermMeaning
ABIApplication 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.
ALUArithmetic Logic Unit. The part of a processor that performs integer arithmetic, comparisons, shifts, and bitwise operations.
CSRControl and Status Register. A register in RISC-V’s separate 12-bit CSR address space, accessed with Zicsr instructions. See CSR access.
EEIExecution 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.
ELFExecutable and Linkable Format. The common object-file, executable, and shared-library format used by RISC-V Unix-like toolchains.
GOTGlobal Offset Table. A linker-managed table used to obtain addresses of global symbols, especially in position-independent code. See la, lla, and lga.
GPRGeneral-Purpose Register. One of the integer registers x0x31 in the standard base ISA.
IPIInterprocessor Interrupt. A notification sent from one hart to another, commonly used by operating systems for cross-hart coordination.
ISAInstruction Set Architecture. The programmer-visible contract implemented by a processor: instructions, registers, exceptions, memory rules, and related architectural behavior.
MIEMachine Interrupt Enable. Depending on context, the global M-mode interrupt-enable bit in mstatus, or the mie CSR that enables individual machine interrupt sources.
MTIEMachine Timer Interrupt Enable. The bit in the mie CSR that enables machine-timer interrupts.
PCProgram 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.
PICPosition-Independent Code. Code that can execute correctly after being loaded at different addresses, normally using PC-relative addressing and linker-managed tables.
PLTProcedure Linkage Table. Linker-generated machinery used by some ELF environments to resolve calls to externally defined functions.
psABIProcessor-Specific Application Binary Interface. The architecture-specific portion of an ABI, including RISC-V calling conventions, ELF relocations, and object-file rules.
RISCReduced Instruction Set Computer. A processor design tradition favoring regular instruction encodings and explicit load/store operations.
RV32 / RV64RISC-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.
RVWMORISC-V Weak Memory Ordering. The standard relaxed memory-consistency model used by RISC-V. See FENCE.
SBISupervisor Binary Interface. The interface commonly used by a RISC-V supervisor operating system to request services from machine-mode firmware.
TLSThread-Local Storage. Per-thread data addressed using ABI- and linker-defined mechanisms; the tp register conventionally participates in locating it.
XLENInteger 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

TermMeaning
hartA hardware thread: one independently executing RISC-V instruction stream with its own architectural state.
immediateA constant encoded as part of an instruction rather than read from a register or memory.
pseudoinstructionAssembler syntax that expands into one or more real instructions. See pseudoinstructions.
relocationMetadata 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

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.