ABI do RV32 e Tipos de Dados em C: Tamanhos, Alinhamento e Layout
TL;DR
- Você vai aprender as regras do ABI RV32 ILP32 (Application Binary Interface) que fazem o C e o assembly “concordarem” sobre:
- uso de registradores,
- passagem de parâmetros,
- valores de retorno,
- alinhamento da pilha,
- e layout de dados.
- Você vai medir e verificar tamanhos de tipos, padding de structs e endianness no RV32.
- Você vai produzir pequenos experimentos que pode inspecionar tanto em C quanto em assembly.
O ABI é o contrato. Se você violá-lo (mesmo sem querer), surgem “bugs estranhos” que parecem corrupção de pilha, ponteiros ruins ou travamentos aleatórios.
1. RV32 em uma frase
- RV32: registradores e endereços são 32-bit.
- ABI mais comum em ensino: ILP32 (Integer/Long/Pointer são 32-bit).
Flags típicas do compilador
-march=rv32im(RV32I + multiply/divide)-mabi=ilp32
2. Funções dos registradores (a parte que você precisa memorizar)
RISC-V tem 32 registradores inteiros: x0..x31.
x0: sempre zerox1:ra(Return Address)x2:sp(Stack Pointer)x3:gp(Global Pointer)x4:tp(Thread Pointer)x5..x7:t0..t2(Temporários)x8:s0/fp(Saved / Frame Pointer)x9:s1(Saved)x10..x17:a0..a7(Argumentos / retornos)x18..x27:s2..s11(Saved)x28..x31:t3..t6(Temporários)
2.1. Quem preserva o quê? (A regra da “responsabilidade”)
Caller-saved (registradores temporários
t*, argumentosa*): São como “rascunho”. Se você (o caller) tem algo importante neles e chama outra função, você precisa salvá-los primeiro. A função chamada pode sobrescrevê-los sem pedir licença.Callee-saved (registradores salvos
s*): São como “ferramentas emprestadas”. Se a função (o callee) quiser usá-los, ela deve devolvê-los exatamente como encontrou antes de retornar.Valor de retorno: Por convenção, o resultado vai em
a0(ea1se for 64-bit).
3. Regras da pilha (a segunda parte que você precisa memorizar)
3.1. A pilha cresce para baixo
| |
3.2. Alinhamento
O ABI exige que o ponteiro de pilha (sp) esteja alinhado a 16 bytes sempre que você chamar uma função.
Por que 16 bytes? Por que não só 4? Pense na pilha como um caminhão de entrega.
- Eficiência: a CPU muitas vezes move dados em blocos de 128-bit (16 bytes) (para SIMD - Single Instruction, Multiple Data - operações vetoriais ou tipos
long double). Se você estacionar o caminhão torto (desalinhado), a empilhadeira não consegue carregar o pallet de uma vez; ela precisa fazer duas cargas parciais. - Prevenção de travamento: algumas instruções crasham se o endereço não for múltiplo de 16.
“Nos limites de chamada” significa:
Antes de saltar para uma nova função, você precisa garantir que sp seja múltiplo de 16. Se você empilhar 1 word (4 bytes), precisa adicionar 12 bytes de padding para que a próxima função comece alinhada em 16 bytes.
4. Tamanhos de tipos C no RV32 (ILP32)
Estes são os tamanhos típicos (confirme na sua toolchain):
| Tipo C | Bytes típicos (RV32 ILP32) |
|---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
long long | 8 |
void* | 4 |
size_t | 4 |
float | 4 |
double | 8 |
5. Na prática: medir tamanhos e alinhamento
Crie:
| |
Compile e rode no QEMU:
| |
5.1. O que você deve observar
- A tabela de
sizedeve bater com as expectativas ILP32. struct Ageralmente tem padding entreaebpara quebfique alinhado em 4 bytes.- Reordenar campos (
struct B) costuma reduzir padding.
5.2. Mergulho profundo: alinhamento de tipo vs. alinhamento da pilha
Você observou double align=8, mas o ABI exige que sp esteja alinhado em 16 bytes. Essa confusão é comum. Vamos separar o Conteúdo do Contêiner.
5.2.1. A regra do conteúdo (alinhamento de tipo)
Cada variável tem um “alinhamento natural”.
char(1 byte) pode morar em qualquer lugar (endereço divisível por 1).int(4 bytes) precisa morar em um endereço divisível por 4 (0x1000, 0x1004…).double(8 bytes) precisa morar em um endereço divisível por 8 (0x1000, 0x1008…).
Se você violar isso, a CPU gera uma exceção de acesso desalinhado (ou faz uma leitura em duas partes, mais lenta).
5.2.2. A regra do contêiner (alinhamento da pilha)
O stack frame é o contêiner para todas essas variáveis locais. Para ser um “contêiner universal”, a pilha precisa estar alinhada ao requisito mais estrito de qualquer variável que possa guardar.
- Se a pilha estivesse alinhada só em 4 bytes, ela poderia começar em
0x1004. - Se você tentasse colocar um
double(precisa de alinhamento 8) na pilha, poderia ser forçado a colocá-lo em0x1004relativo à memória 0, o que é ilegal paradouble.
A solução:
O ABI do RISC-V força a pilha a estar alinhada em 16 bytes (divisível por 16).
Como 16 é divisível por 1, 2, 4 e 8, um stack frame novo é garantido como ponto de partida seguro para qualquer tipo de dado padrão, incluindo vetores SIMD de 128-bit (float128 ou v128), sem ajustes complexos.
6. Padding em structs explicado (com diagrama)
6.1. Exemplo: struct A
| |
Um layout comum em RV32:
Pense na memória como uma grade de palavras de 4 bytes (32-bit).
| Offset | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Conteúdo |
|---|---|---|---|---|---|
| +0 | a | pad | pad | pad | a ocupa 1 byte. Pulamos 3 bytes para a próxima linha começar alinhada. |
| +4 | b | b | b | b | b (4 bytes) encaixa perfeitamente em uma nova palavra. |
| +8 | c | c | pad | pad | c (2 bytes) fica aqui. Preenchemos o final para alinhar o tamanho total da struct. |
6.2. Exemplo complexo: misturando char, int, long, long long, double
Vamos ver uma struct usando todos os tipos que você perguntou, distinguindo long (32-bit) de long long (64-bit).
| |
Análise de layout:
cfica em +0.iprecisa de alinhamento 4. O próximo slot disponível é +1, então pulamos 3 bytes.icomeça em +4.lprecisa de alinhamento 4. Ele encaixa perfeitamente em +8. Termina em +12.llprecisa de alinhamento 8. +12 não é divisível por 8 (12 % 8 = 4). Precisamos de 4 bytes de padding.llcomeça em +16.dprecisa de alinhamento 8.lltermina em +24. 24 é divisível por 8.dcomeça em +24 imediatamente.
Grade de memória:
| Offset | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Conteúdo |
|---|---|---|---|---|---|
| +0 | c | pad | pad | pad | Alinhando para i |
| +4 | i | i | i | i | |
| +8 | l | l | l | l | long tem 4 bytes no RV32 |
| +12 | pad | pad | pad | pad | Alinhando para ll (precisa ser % 8) |
| +16 | ll (lo) | ll | ll | ll | long long (primeira metade) |
| +20 | ll (hi) | ll | ll | ll | long long (segunda metade) |
| +24 | d (lo) | d | d | d | double (primeira metade) |
| +28 | d (hi) | d | d | d | double (segunda metade) |
Tamanho total: 32 bytes.
Por que o padding existe:
- Muitas CPUs carregam/armazenam de forma mais eficiente (ou só corretamente) quando alinhado.
- O ABI escolhe regras que equilibram desempenho e compatibilidade.
7. Endianness e o que isso significa para C
A maioria dos alvos RV32 é little-endian.
Se você armazenar 0x11223344 na memória, os bytes aparecem como:
| address | +0 | +1 | +2 | +3 |
| bytes | 44 | 33 | 22 | 11 |
7.1. Na prática: confirmar endianness
| |
Compile/rode:
| |
8. ABI encontra o assembly: parâmetros e valores de retorno
Considere:
| |
No nível do ABI:
achega ema0bchega ema1- valor de retorno volta em
a0
No disassembly, você costuma ver:
- cálculo em um registrador
- garantir que o resultado termine em
a0 - retorno via
jalrusandora
9. Exercícios
- Altere
struct Aadicionando umuint8_t d;no final. Preveja o novo tamanho antes de compilar. - Crie uma versão empacotada:
1struct __attribute__((packed)) P { u8 a; u32 b; }; - Compare
sizeof(struct P)com a versão não empacotada. - Escreva uma função que retorne um
u64. Observe quais registradores carregam o valor de retorno.
__attribute__((packed)) pode causar acessos desalinhados. Algumas CPUs lidam com isso lentamente; outras podem falhar. Use structs empacotadas só quando você controla todo acesso e precisa de um layout exato (ex.: formatos de rede).9.1. Como testar suas respostas
- Verifique tamanhos e offsets usando
sizeofe o padrão do macroOFFSETOF. - Use
objdump -d -M numeric,no-aliasespara confirmar quais registradores são usados.
10. Resumo
Você aprendeu o “contrato” do ABI RV32 ILP32: funções dos registradores, regras da pilha e como tipos C mapeiam para bytes.
A seguir: C → assembly + otimizações - você vai ver como -O muda o que aparece no disassembly, e como volatile realmente afeta o código gerado.