Instruções básicas de assembly RISC-V

1. Objetivo

Aprender as famílias centrais de instruções do RV32I e praticar com um pequeno programa escrito à mão.

2. Famílias de instruções

3. Exemplo: somar dois números e armazenar o resultado

Arquivo: 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. Linker script mínimo (e por que isso importa)

Quando você roda assembly bare-metal, não há sistema operacional nem runtime C para decidir onde o seu código e dados vivem na memória. O C Runtime (CRT) é o código que roda antes do seu main() e depois que ele termina. Ele prepara o ambiente de execução de um programa C: pilha, argumentos, variáveis globais, bibliotecas, e só então chama main().

Nota curta sobre CRT (o que é e o que não é):

Sem CRT (C Runtime):

O linker precisa de instruções explícitas sobre:

4.1. Crie um linker script mínimo:

Arquivo: 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. O linker script

Estas são as instruções para o Linker. Pense no Linker como um construtor que pega pilhas de matéria-prima (as linhas compiladas do seu código) e as organiza em um prédio finalizado (o binário executável).

4.3. Build e execução no 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

O terminal vai travar por causa do loop infinito no final do programa. Isso é esperado. Para verificar o resultado, abra outro terminal e use gdb-multiarch para inspecionar a memória como explicado na próxima seção.

5. Conectar com gdb-multiarch

Para depurar, inicie o QEMU com o stub do GDB habilitado e pausado no reset:

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

Em outro terminal, conecte-se manualmente usando gdb-multiarch e siga cada passo abaixo:

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

Se preferir um comando único, você pode rodar:

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"

Agora você pode inspecionar o resultado:

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

A saída deve mostrar 0x0000002a, que é 42 em 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. Exercícios

  1. Altere as constantes para calcular 7 + 13 e verifique na memória.
  2. Substitua add por sub e observe o novo resultado.
  3. Adicione um loop que incrementa g_result cinco vezes.

7. Resumo