Fluxo de controle e acesso a dados em assembly RV32

TL;DR


1. Acesso a dados no RV32: o modo de endereçamento que você mais vê

Instruções de load/store no RV32 normalmente usam:

1
address = base_register + signed_immediate

Exemplos:

Por que isso importa


2. Tamanho do load/store e signedness

Loads inteiros comuns:

Stores:


3. Na prática: arrays e aritmética de ponteiros

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/arrays.c
#include "types.h"
#include "uart.h"

u32 sum_u32(const u32 *p, u32 n) {
  u32 s = 0u;
  for (u32 i = 0u; i < n; i++) {
    s += p[i];
  }
  return s;
}

int main(void) {
  u32 a[4] = {1u, 2u, 3u, 4u};
  u32 s = sum_u32(a, 4u);
  uart_puts("sum=");
  uart_puthex32(s);
  uart_putc('\n');
  return 0;
}

Compile e faça o disassembly:

1
2
3
4
5
6
riscv64-unknown-elf-gcc -O0 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/link.ld src/start.s src/uart.c src/arrays.c -o build/arrays_O0.elf

riscv64-unknown-elf-objdump -d -M numeric,no-aliases build/arrays_O0.elf | less

qemu-system-riscv32 -M virt -nographic -bios none -kernel build/arrays_O0.elf

O que você deve ver

Um padrão de loop que parece com:

  1. inicializar contador
  2. comparar contador vs limite
  3. calcular endereço do elemento = base + índice*4
  4. carregar elemento
  5. somar
  6. incrementar contador
  7. branch de volta

Cálculo típico de endereço

No RV32, multiplicar por 4 é frequentemente feito com um shift:

1
2
index_bytes = i << 2
addr = base + index_bytes

4. If/else vira compare + branch

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/ifelse.c
#include "types.h"
#include "uart.h"

u32 clamp_u32(u32 x, u32 lo, u32 hi) {
  if (x < lo) return lo;
  if (x > hi) return hi;
  return x;
}

int main(void) {
  u32 v = clamp_u32(42u, 10u, 30u);
  uart_puts("clamp=");
  uart_puthex32(v);
  uart_putc('\n');
  return 0;
}

Compile tanto -O0 quanto -O2:

1
2
3
4
5
6
7
riscv64-unknown-elf-gcc -O0 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/link.ld src/start.s src/uart.c src/ifelse.c -o build/if_O0.elf

riscv64-unknown-elf-gcc -O2 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/link.ld src/start.s src/uart.c src/ifelse.c -o build/if_O2.elf

qemu-system-riscv32 -M virt -nographic -bios none -kernel build/if_O0.elf

Compare o disassembly.

Instruções de branch RV32 que você verá com frequência


5. Loops: padrões de for e while

Formas comuns

Loop “testa no topo”

1
2
3
4
5
init
check
body
increment
jump to check

Loop “testa no fim”

1
2
3
4
5
init
body
increment
check
branch to body

O otimizador pode transformar um no outro.


6. Switch statements e tabelas de salto

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// src/switch.c
#include "types.h"
#include "uart.h"

u32 dispatch(u32 x) {
  switch (x) {
    case 0: return 0x1111u;
    case 1: return 0x2222u;
    case 2: return 0x3333u;
    case 3: return 0x4444u;
    default: return 0xdeadu;
  }
}

int main(void) {
  u32 r = dispatch(2u);
  uart_puts("dispatch=");
  uart_puthex32(r);
  uart_putc('\n');
  return 0;
}

Compile otimizado:

1
2
3
4
5
6
riscv64-unknown-elf-gcc -O2 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/link.ld src/start.s src/uart.c src/switch.c -o build/switch_O2.elf

riscv64-unknown-elf-objdump -d -M numeric,no-aliases build/switch_O2.elf | less

qemu-system-riscv32 -M virt -nographic -bios none -kernel build/switch_O2.elf

O que você pode ver

Layout conceitual de jump table:

1
2
3
4
.rodata:
  table[0] = &case0
  table[1] = &case1
  ...

7. Acesso a campos de struct parece offsets constantes

Crie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/structs.c
#include "types.h"
#include "uart.h"

typedef struct {
  u8  flags;
  u8  mode;
  u16 len;
  u32 addr;
} header_t;

u32 read_addr(const header_t *h) {
  return h->addr;
}

void set_len(header_t *h, u16 v) {
  h->len = v;
}

int main(void) {
  header_t h = {1u, 2u, 3u, 0x80001234u};
  set_len(&h, 0x55aau);
  uart_puts("addr=");
  uart_puthex32(read_addr(&h));
  uart_putc('\n');
  return 0;
}

Compile/disassemble e confirme que:

1
2
3
riscv64-unknown-elf-gcc -O2 -g -ffreestanding -nostdlib -march=rv32im -mabi=ilp32 \
  -T src/link.ld src/start.s src/uart.c src/structs.c -o build/structs_O2.elf
riscv64-unknown-elf-objdump -d -M numeric,no-aliases build/structs_O2.elf | less

Exercícios

  1. Para sum_u32, identifique a instrução exata que carrega p[i]. Qual é a fórmula do endereço efetivo?
  2. Para clamp_u32, identifique quais branches são unsigned vs signed.
  3. Para o switch, localize a jump table (se existir) em .rodata e faça dump dos bytes com objdump -s -j .rodata.
  4. Para o exemplo de struct, calcule os offsets esperados usando o padrão do macro OFFSETOF e verifique se batem com o disassembly.

Como testar suas respostas


Resumo

Você aprendeu os padrões comuns de RV32 para loads/stores e como fluxo de controle de alto nível vira branches e (às vezes) tabelas de salto.

A seguir: funções e a pilha, vamos mergulhar em prólogos/epílogos, stack frames, registradores salvos e como depurar bugs relacionados à pilha.