What is the Lux RISC-V Simulator?
Lux is a free, browser-based RISC-V simulator and assembler. It lets you write, assemble, and run RV32IM assembly language directly online — no installation, no toolchain, and no account required. Whether you are learning computer architecture, studying for an exam, or prototyping low-level code, this online RISC-V simulator runs entirely in your browser.
Built as a modern successor to the venus simulator, Lux supports the base RV32I integer instruction set together with the RV32M multiply and divide extension, a full register file, byte-addressable little-endian memory, and system calls through the standard a7 ecall convention.
Online RISC-V Assembler and Step Debugger
The integrated assembler translates RISC-V assembly into an executable program, resolving labels, pseudo-instructions, and .data / .text directives automatically. You can run a program to completion or single-step through every instruction to watch the program counter, registers, and memory change in real time — an ideal way to learn how a RISC-V CPU actually executes code.
Supported instructions include arithmetic and logic operations (add, sub, and, or, xor, sll, srl, sra, slt), immediates (addi, andi, ori, slli), loads and stores (lw, lh, lb, sw, sh, sb), branches (beq, bne, blt, bge), jumps (jal, jalr), the multiply/divide extension (mul, mulh, div, rem), and common pseudo-instructions such as li, la, mv, j, call, and ret.
Understanding RISC-V Assembly Language
RISC-V is an open-standard reduced instruction set computer (RISC) architecture used across academia and industry, from microcontrollers to high-performance processors. Its clean, modular instruction set makes it the most popular choice for teaching computer organization, assembly programming, and computer architecture.
In the RISC-V calling convention, function arguments are passed in registers a0–a7, return values come back in a0, and system calls place their service number in register a7 before executing ecall. Lux follows these standard RISC-V patterns so what you learn here transfers directly to real hardware and toolchains.
Who is it for?
Students and educators in computer science and computer engineering use online RISC-V simulators to write and test RV32IM assembly without setting up GCC, QEMU, or Spike. Lux is a fast, free alternative for classrooms, homework, lab assignments, and self-study in computer architecture.
A Beginner's Guide to RISC-V Assembly
Assembly is the human-readable form of the actual instructions a processor runs. RISC-V is a clean, modern, open instruction set, which makes it a great place to learn. This guide starts from zero — no prior assembly experience required.
The big picture
A program is just a list of tiny instructions. The CPU reads them one at a time, top to bottom, unless an instruction tells it to jump somewhere else. Each instruction does one small job: add two numbers, copy a value from memory, compare two values, and so on. Real programs are simply thousands of these small steps stacked together.
Registers: the CPU's workbench
The processor cannot do math directly on memory. Instead it has 32 registers — think of them as 32 small, lightning-fast boxes, each holding one 32-bit whole number. The normal rhythm of assembly is: move data into registers, do the work there, then move results back out.
Every register has a number (x0–x31) and a friendlier ABI name that hints at its usual job:
| Name |
Number |
Typical use |
zero |
x0 |
Always 0 — writes to it are ignored |
ra |
x1 |
Return address (where a function goes back to) |
sp |
x2 |
Stack pointer |
a0–a7 |
x10–x17 |
Function arguments and system-call numbers |
t0–t6 |
x5–x7, x28–x31 |
Temporary scratch values |
s0–s11 |
x8–x9, x18–x27 |
Saved values that survive function calls |
That zero register looks useless but is everywhere: it gives you a constant 0 without spending a real register.
How to read an instruction
Most instructions share the same shape — the destination comes first, then the inputs:
operation destination, source1, source2
So add t2, t0, t1 reads as "add the value in t0 to the value in t1 and put the result in t2." Once you internalise "destination first," the whole language gets easier.
Step 1 — putting numbers into registers
li means load immediate. "Immediate" is just jargon for a fixed number written directly in the code:
li t0, 5 # t0 now holds 5
li t1, 12 # t1 now holds 12
Step 2 — doing arithmetic
Now we can compute. Read the comments to see each register update:
li t0, 3 # t0 = 3
li t1, 4 # t1 = 4
add t2, t0, t1 # t2 = 3 + 4 = 7
sub t3, t1, t0 # t3 = 4 - 3 = 1
Many operations come in two flavours: a register version (add) and an immediate version whose name ends in i (addi) that takes a constant directly. Counting up by one is the classic example:
addi t0, t0, 1 # t0 = t0 + 1
Step 3 — talking to the outside world with ecall
Your program cannot print by itself; it has to ask the operating system. That request is a system call, made with ecall. Before calling, you fill in two registers:
- a7 chooses which service you want, by number.
- a0 carries the argument for that service.
To print the number 7:
li a0, 7 # the value we want to print
li a7, 1 # service 1 means "print integer"
ecall # make the request
Putting the service number in a7 is the real RISC-V standard (the same one RARS uses), so the habits you build here carry over to actual hardware and toolchains.
Step 4 — making decisions with branches
A branch compares two registers and, only if the condition holds, jumps to a label. A label is just a name for a spot in your code, written with a colon at the end:
li t0, 5
li t1, 5
beq t0, t1, equal # if t0 == t1, jump down to "equal"
li a0, 0 # this line is skipped when they match
equal:
li a0, 1
The everyday branches are beq (equal), bne (not equal), blt (less than), and bge (greater than or equal).
Step 5 — loops
A loop is nothing more than a branch that jumps backward. Here is the classic "add up 1 + 2 + … + 10," explained line by line:
main:
li t0, 0 # sum = 0 (our running total)
li t1, 1 # i = 1 (the counter)
loop:
li t2, 11
bge t1, t2, done # if i >= 11, leave the loop
add t0, t0, t1 # sum = sum + i
addi t1, t1, 1 # i = i + 1
j loop # go back up and test again
done:
mv a0, t0 # copy the final sum into a0
li a7, 1 # print it
ecall
li a7, 10 # service 10 means "exit"
ecall
j jumps unconditionally, mv copies one register into another, and done: marks where to land when the loop is finished. Paste this into the editor and press Step to watch t0 climb 1, 3, 6, 10, 15 … all the way to 55.
Step 6 — working with memory
There are only 32 registers, so anything bigger lives in memory. You shuttle values between memory and registers using loads (memory → register) and stores (register → memory). The address is written offset(base) — a base register plus a small constant offset:
lw a0, 0(sp) # load the 32-bit word at address sp into a0
sw a0, 4(sp) # store a0 into memory at address sp + 4
A "word" is 32 bits (4 bytes). Smaller sizes exist too: lh/sh move 16-bit halves and lb/sb move single bytes.
Full Instruction Reference
Once the ideas above click, this is the cheat-sheet you keep coming back to.
Arithmetic & logic (register, R-type)
| Instruction |
Operation |
Meaning |
add rd, rs1, rs2 |
rd = rs1 + rs2 |
Signed addition |
sub rd, rs1, rs2 |
rd = rs1 − rs2 |
Subtraction |
and rd, rs1, rs2 |
rd = rs1 & rs2 |
Bitwise AND |
or rd, rs1, rs2 |
rd = rs1 | rs2 |
Bitwise OR |
xor rd, rs1, rs2 |
rd = rs1 ^ rs2 |
Bitwise XOR |
sll rd, rs1, rs2 |
rd = rs1 << rs2 |
Shift left |
srl rd, rs1, rs2 |
rd = rs1 >> rs2 |
Shift right (zero-fill) |
sra rd, rs1, rs2 |
rd = rs1 >> rs2 |
Shift right (sign-keeping) |
slt / sltu |
rd = rs1 < rs2 ? 1 : 0 |
Set if less than (signed / unsigned) |
Arithmetic with a constant (immediate, I-type)
| Instruction |
Operation |
addi rd, rs1, imm |
rd = rs1 + imm |
andi / ori / xori |
bitwise op with a constant |
slli / srli / srai |
shift by a constant amount |
slti / sltiu |
set if less than a constant |
Loads & stores
| Instruction |
Meaning |
lw rd, off(rs1) |
Load 32-bit word |
lh / lhu |
Load 16-bit half (signed / unsigned) |
lb / lbu |
Load 8-bit byte (signed / unsigned) |
sw rs2, off(rs1) |
Store 32-bit word |
sh / sb |
Store half / byte |
Branches & jumps
| Instruction |
Jumps when |
beq / bne |
equal / not equal |
blt / bge |
less / greater-or-equal (signed) |
bltu / bgeu |
less / greater-or-equal (unsigned) |
jal rd, label |
jump and save the return address in rd |
jalr rd, rs1, imm |
jump to rs1 + imm and save the return address |
Multiply & divide (RV32M)
| Instruction |
Operation |
mul |
low 32 bits of the product |
mulh / mulhu / mulhsu |
high 32 bits (signed / unsigned / mixed) |
div / divu |
quotient (signed / unsigned) |
rem / remu |
remainder (signed / unsigned) |
System calls — ecall (service in a7, argument in a0)
| a7 |
Service |
Argument |
| 1 |
print integer |
a0 = value |
| 4 |
print string |
a0 = address of text |
| 11 |
print char |
a0 = character code |
| 34 |
print hex |
a0 = value |
| 10 |
exit |
— |
| 17 |
exit with code |
a0 = exit code |
Handy pseudo-instructions
These are shortcuts the assembler expands into real instructions for you:
| Pseudo |
Does |
li rd, imm |
load a constant into rd |
mv rd, rs |
copy one register to another |
nop |
do nothing for one step |
j label |
jump, no condition |
call label / ret |
call a function / return from one |
beqz / bnez |
branch if a register is (not) zero |