Fluxo de controle e acesso a dados em assembly RV32
TL;DR
- Você vai aprender como fluxo de controle em C (if/else, loops, switch) vira padrões de branch e jump no RV32.
- Vai entender como RV32 faz load/store na memória (base + offset) e como o compilador representa arrays, ponteiros e structs.
- Vai praticar um método repetível para “ler” disassembly: identificar entradas, identificar referências de memória e reconstruir a estrutura de alto nível.
Important
Uma parte enorme de engenharia reversa é simplesmente reconhecer padrões: formatos de loop, checagens de limite, tabelas de salto de switch e idioms comuns de biblioteca.1. Acesso a dados no RV32: o modo de endereçamento que você mais vê
Instruções de load/store no RV32 normalmente usam:
| |
Exemplos:
lw a0, 12(sp): carrega uma word de 32 bits desp+12sw a1, 0(s0): armazena uma word de 32 bits no endereço ems0
Por que isso importa
- Locais de pilha costumam aparecer como
lw/swcomspous0/fp. - Campos de struct costumam aparecer como
lw/swcom um offset constante. - Arrays combinam um cálculo de índice com um registrador base.
2. Tamanho do load/store e signedness
Loads inteiros comuns:
lb: carrega byte (sign-extend)lbu: carrega byte (zero-extend)lh: carrega halfword (16-bit, sign-extend)lhu: carrega halfword (zero-extend)lw: carrega word (32-bit)
Stores:
sb,sh,sw
Tip
Signedness costuma revelar intenção. Se você vê lbu, isso sugere fortemente um valor uint8_t ou um caractere tratado como unsigned.3. Na prática: arrays e aritmética de ponteiros
Crie:
| |
Compile e faça o disassembly:
| |
O que você deve ver
Um padrão de loop que parece com:
- inicializar contador
- comparar contador vs limite
- calcular endereço do elemento = base + índice*4
- carregar elemento
- somar
- incrementar contador
- branch de volta
Cálculo típico de endereço
No RV32, multiplicar por 4 é frequentemente feito com um shift:
| |
Note
Quando você estiver revertendo firmware, reconhecer “index « 2” é uma forma rápida de identificar indexação de array de 32 bits.4. If/else vira compare + branch
Crie:
| |
Compile tanto -O0 quanto -O2:
| |
Compare o disassembly.
Instruções de branch RV32 que você verá com frequência
beq,bneblt,bge(signed)bltu,bgeu(unsigned)
Important
Comparações unsigned (< em uint32_t) tendem a usar bltu/bgeu. Comparações signed tendem a usar blt/bge.5. Loops: padrões de for e while
Formas comuns
Loop “testa no topo”
| |
Loop “testa no fim”
| |
O otimizador pode transformar um no outro.
6. Switch statements e tabelas de salto
Crie:
| |
Compile otimizado:
| |
O que você pode ver
- Um bounds check em
x - Um salto computado usando uma tabela de endereços
Layout conceitual de jump table:
| |
Tip
Ao reverter, jump tables costumam viver em .rodata. Se você encontrar um “carregar endereço da tabela e saltar”, provavelmente está em um switch.7. Acesso a campos de struct parece offsets constantes
Crie:
| |
Compile/disassemble e confirme que:
addré carregado de um offset constante a partir do ponteiro base da structlenusa um store halfword (sh) no seu offset
| |
Exercícios
- Para
sum_u32, identifique a instrução exata que carregap[i]. Qual é a fórmula do endereço efetivo? - Para
clamp_u32, identifique quais branches são unsigned vs signed. - Para o
switch, localize a jump table (se existir) em.rodatae faça dump dos bytes comobjdump -s -j .rodata. - Para o exemplo de struct, calcule os offsets esperados usando o padrão do macro
OFFSETOFe verifique se batem com o disassembly.
Como testar suas respostas
- Use
readelf -Spara localizar endereços de seções eobjdump -spara dump de bytes brutos. - Use
objdump -d -M numeric,no-aliasespara ver as formas reais das instruções.
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.