Pseudo-Instructions: What the Assembler Adds
intermediateRISCVLesson 7 of 7
li t0, 5 # li -> addi t0, zero, 5mv t1, t0 # mv -> addi t1, t0, 0nop # nop -> addi zero, zero, 0j skip # j -> jal zero, skipli t2, 42 # SKIPPED - j jumped over this lineskip: mv t3, t1 # mv -> addi t3, t1, 0
registers
memory / stack
pc & flags
Every lesson so far has quietly used instructions that don’t actually exist. mv,
nop, and even ret from
functions and the calling convention
never appear in the RISC-V hardware manual. They’re pseudo-instructions —
convenient names the assembler recognizes and rewrites into the real
instructions the CPU actually decodes. This is
the first assemblers’
mnemonic idea and macros’
one-line-for-many idea, both still doing their jobs in the tool you use today.
Why the hardware doesn’t just add these
RISC-V’s base instruction set is deliberately tiny — that’s the whole point of the
RISC philosophy: keep the
real instruction set small and uniform, and let software handle convenience. A
pseudo-instruction is where that trade shows up directly: instead of the chip
growing a copy register or do nothing instruction, the assembler expands a
friendlier name into instructions that already exist.
The common ones
| You write | Assembler emits | What it really is |
|---|---|---|
mv rd, rs |
addi rd, rs, 0 |
copy a register (add zero, keep the value) |
nop |
addi zero, zero, 0 |
do nothing, but take up one instruction slot |
li rd, imm |
addi rd, zero, imm (small imm)lui+addi (large imm) |
load an immediate, however many real instructions that takes |
j label |
jal zero, label |
jump, discarding the return address |
call label |
jal ra, label (nearby)auipc+jalr (far away) |
call a function, however far away it is |
ret |
jalr zero, 0(ra) |
return, using whatever ra holds |
Two patterns run through this whole table. First, several of these are the
same trick: zero is a real register hardwired to the value 0, so “add zero”
means copy, and “link into zero” means don’t bother saving where you came from.
Second, li and call can quietly expand to one instruction or two,
depending on the argument — a 12-bit-immediate li needs just addi, but a
32-bit constant needs lui to set the high bits first. You write one line either
way; the assembler decides how many real instructions it costs.
Why this matters: reading disassembly
This is mostly invisible until you look at disassembled machine code instead of
your own source — objdump output, a debugger, a simulator’s instruction view. It
shows you the real instructions, so a ret you wrote becomes jalr zero, 0(ra)
on screen, and a li with a big constant shows up as two lines where you wrote
one. Knowing the expansion table means neither surprises you.
Step through it
Watch the trace’s pc column: five source lines produce five real instructions
at 0x00–0x14 — except li t2, 42 never executes at all. j skip jumps
straight from 0x0c to 0x14, so that line’s real instruction (addi t2, zero,
42) sits in memory but the CPU never reaches it. That’s jal zero, skip at work:
a jump with nowhere to link the return address, exactly like j in the table
above.
Try this
- What real instruction does
mv a0, a1assemble to? (addi a0, a1, 0— add zero, i.e. copy.) - Why can’t
li t0, 100000be a singleaddi? (addi’s immediate is only 12 bits — about ±2047 — so a value that large needsluito load the high bits beforeaddifills in the rest.) - You disassemble a program and see
jalr zero, 0(ra)where your source said something else. What did you write? (ret— that’s exactly what it expands to.)
Pseudo-instructions are a kindness from the assembler, not a new part of the
machine — the CPU never sees mv or ret, only the addi and jalr underneath.
With that gap closed, the next lesson turns loose instructions into a complete
program: sections, directives, and how a label becomes a real address in memory.