Basic RISC-V Assembly Instructions

1. Goal

Learn the core RV32I instruction families and practice with a tiny hand-written program.

2. Instruction families

3. Example: add two numbers and store the result

File: src/basic_add.s

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.section .text
.globl _start

_start:
  li t0, 40
  li t1, 2
  add t2, t0, t1

  la t3, g_result
  sw t2, 0(t3)

1:
  j 1b

.section .bss
.align 4
g_result:
  .word 0

4. Minimal linker script (and why it matters)

When you run bare-metal assembly, there is no operating system or C runtime to decide where your code and data live in memory. The C Runtime (CRT) is the code that runs before your main() function and after it finishes. It prepares the execution environment of a C program: stack, arguments, global variables, libraries, and only then calls main().

Short CRT note (what it is and is not):

Without CRT (C Runtime):

The linker needs explicit instructions about:

4.1. Create a tiny linker script:

File: src/minimal_link.ld

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ENTRY(_start)

SECTIONS
{
  . = 0x80000000;
  .text : { *(.text .text.*) }
  .rodata : { *(.rodata .rodata.*) }
  .data : { *(.data .data.*) }
  .bss : { *(.bss .bss.* COMMON) }
}

4.2. The Linker Script

This is the instructions for the Linker. Think of the Linker as a builder who takes piles of raw materials (your compiled code lines) and arranges them into a finished building (the executable binary).

4.3. Build and run in QEMU

1
2
riscv64-unknown-elf-gcc -O0 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/minimal_link.ld src/basic_add.s -o build/basic_add.elf

The terminal will hang because of the infinite loop at the end of the program. This is expected. To verify the result, open another terminal and use gdb-multiarch to inspect memory as explained in the next section.

5. Connect with gdb-multiarch

To debug, start QEMU with its GDB stub enabled and paused at reset:

1
2
qemu-system-riscv32 -M virt -nographic -bios none -kernel build/basic_add.elf \
  -S -gdb tcp::1234

In another terminal, manually connect using gdb-multiarch and follow each step below:

1
2
3
4
5
gdb-multiarch build/basic_add.elf
(gdb) set architecture riscv:rv32
(gdb) target remote :1234
(gdb) break _start
(gdb) continue

If you prefer a single command, you can run:

1
2
3
4
5
gdb-multiarch build/basic_add.elf -q \
  -ex "set arch riscv:rv32" \
  -ex "target remote localhost:1234" \
  -ex "break _start" \
  -ex "continue"

Now you can inspect the result:

1
2
3
4
(gdb) info variables g_result
(gdb) print &g_result
(gdb) next
(gdb) x/wx &g_result

Output should show 0x0000002a, which is 42 in decimal (40 + 2).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Reading symbols from build/basic_add.elf...
The target architecture is set to "riscv:rv32".
Remote debugging using localhost:1234
0x00001000 in ?? ()
Breakpoint 1 at 0x80000000: file src/basic_add.s, line 5.
Continuing.

Breakpoint 1, _start () at src/basic_add.s:5
5         li t0, 40
(gdb) ni
6         li t1, 2
(gdb) ni
7         add t2, t0, t1
(gdb) ni
9         la t3, g_result
(gdb) ni
0x80000010      9         la t3, g_result
(gdb) ni
10        sw t2, 0(t3)
(gdb) ni
13        j 1b
(gdb) x/wd &g_result
0x80000020:     42
(gdb) q

6. Exercises

  1. Change the constants to compute 7 + 13 and verify in memory.
  2. Replace the add with sub and observe the new result.
  3. Add a loop that increments g_result five times.

7. Summary