Assembly RISC-V
RV32, endereços, formatos de instrução, saltos e chamadas com a pilha.
lw t0, 0(a0)
add t1, t0, t2
sw t1, 4(a0)
Com a0=0x1000, uma palavra de valor 7 nesse endereço e t2=5, este fragmento lê 7, soma 5 e guarda 12 em 0x1004. Os registos t0 e t1 guardam os valores intermédios dentro do processador; a memória conserva palavras em endereços de byte, por isso o deslocamento 4 avança exatamente uma palavra.
Cada linha é uma instrução. A ISA, arquitetura do conjunto de instruções, fixa as operações, os registos e os seus efeitos visíveis. Duas organizações internas podem executar o mesmo programa em tempos diferentes.
Trabalhamos em RV32: 32 registos inteiros de 32 bits e instruções base de 32 bits, sem instruções comprimidas. O PC identifica o endereço da instrução em execução, e um salto relativo usa o PC dessa instrução. Numa pipeline, várias instruções coexistem, cada uma com o seu PC, enquanto a obtenção já procura instruções mais novas.
Registos e convenção de chamadas
| Nome | Registos | Uso e preservação |
|---|---|---|
zero | x0 | Sempre zero; escritas são ignoradas |
ra | x1 | Endereço de retorno; uma chamada pode alterá-lo |
sp | x2 | Apontador da pilha; repor antes de retornar |
gp, tp | x3, x4 | Reservados pela convenção de execução |
t0 a t6 | x5 a x7, x28 a x31 | Temporários, não preservados pelo chamado |
a0 a a7 | x10 a x17 | Argumentos; a0 e a1 também transportam retornos |
s0 a s11 | x8, x9, x18 a x27 | O chamado preserva os que utilizar |
Não existe t7. O banco de vírgula flutuante, quando disponível, usa f0 a f31; não é o mesmo banco dos x.
Contas, imediatos e memória
No fragmento inicial, as alterações de estado são:
| Instrução | Endereço usado | Alteração |
|---|---|---|
lw t0,0(a0) | 0x1000 | t0=7 |
add t1,t0,t2 | Sem acesso a dados | t1=12 |
sw t1,4(a0) | 0x1004 | A palavra de memória passa a 12 |
O add usa o valor carregado, não o endereço 0x1000. Já o sw conserva t1 e modifica memória.
add rd,rs1,rs2 soma dois registos; addi rd,rs1,imm soma um imediato com sinal de 12 bits, entre −2048 e 2047. sub, and, or, xor, slt seguem o formato de três registos. slt compara com sinal; sltu compara sem sinal. Não há flags de comparação globais.
Uma soma inteira mantém os 32 bits baixos, sem gerar uma exceção por overflow. Com sinal, o intervalo é de −2³¹ a 2³¹−1; sem sinal, de 0 a 2³²−1. Por exemplo, 0x7FFFFFFF+1 produz 0x80000000, que se interpreta como −2³¹ com sinal. O padrão continua correto como soma modular, mas já não representa a soma matemática com sinal.
O mesmo padrão 0xFFFFFFFF representa 4294967295 sem sinal ou −1 com sinal, por isso a instrução de comparação escolhida determina a interpretação.
| Instrução | Tamanho | Efeito |
|---|---|---|
lb, lh, lw | 1, 2, 4 bytes | Ler; lb/lh estendem o sinal |
lbu, lhu | 1, 2 bytes | Ler; preencher os bits altos com zeros |
sb, sh, sw | 1, 2, 4 bytes | Guardar apenas os bits baixos correspondentes |
Em lw t0,12(a0), o endereço é a0 + 12, em bytes. Para ler vetor[i] de palavras, calcula base + 4i. ld/sd pertencem ao modelo inteiro RV64, não a este RV32.
A memória é little endian: a palavra 0x12345678 ocupa os bytes 78 56 34 12, do endereço menor para o maior. O alinhamento natural de uma palavra exige endereço múltiplo de 4. Ler um byte com sinal estende esse sinal aos 32 bits: um lb sobre o byte 0xFE produz −2, enquanto lbu produz 254.
Os deslocamentos trabalham por via aritmética ou lógica. slli desloca à esquerda e preenche os bits baixos com zeros. srli preenche os altos com zeros; srai preenche-os com o bit de sinal. Os imediatos de deslocamento em RV32 vão de 0 a 31. Para valores negativos, o arredondamento difere: dividir −3 por 2 com srai dá −2, por arredondar para baixo, enquanto div dá −1, por arredondar para zero. A multiplicação e a divisão, como div, vêm da extensão M. As instruções SIMD empacotadas têm um modelo próprio, tratado em SIMD.
Vírgula flutuante e conversões
As extensões F e D acrescentam operações de precisão simples e dupla. Os endereços e as contagens continuam nos registos inteiros; os valores em vírgula flutuante usam o banco f. O sufixo .s indica precisão simples, com 32 bits, e .d, dupla, com 64 bits. RV32 com D continua a ter registos inteiros de 32 bits; o banco de vírgula flutuante tem a largura necessária para os valores de 64 bits.
| Operação | Efeito |
|---|---|
flw ft0,0(a0) | Copia quatro bytes de memória para um registo de vírgula flutuante |
fsw ft0,0(a0) | Guarda os quatro bytes de precisão simples |
fadd.s, fsub.s, fmul.s, fdiv.s | Calculam com operandos de precisão simples |
fcvt.s.w ft0,t0 | Converte o inteiro com sinal em t0 para precisão simples |
fcvt.d.s ft1,ft0 | Converte um valor simples para dupla |
fcvt.s.d fa0,ft1 | Converte de dupla para simples, arredondando quando necessário |
flt.s t0,ft1,ft2 | Escreve 1 ou 0 no registo inteiro conforme a comparação |
Uma carga não converte um inteiro para um número real. Se a memória contém o inteiro 3, flw copia o padrão 0x00000003, que não é a representação de 3,0. Para converter, lê com lw e usa fcvt.s.w.
As instruções fmv.w.x e fmv.x.w copiam bits entre bancos, sem conversão numérica: uma apenas move o padrão, a outra faz o inverso. fmv.s copia entre registos de precisão simples; não alarga um valor para dupla.
Média de elementos não negativos
Suponhamos uma sequência de números finitos de precisão simples, base alinhada a quatro bytes, comprimento positivo e pelo menos um elemento não negativo. Não há overflow nas somas. O fragmento seguinte deixa a média em ft0; altera os temporários e avança a0 até ao fim do vetor.
mv t2, zero
fcvt.s.w ft0, zero # soma = 0,0
fmv.s ft3, ft0 # constante 0,0
ciclo_media:
flw ft1, 0(a0)
addi a0, a0, 4
addi a1, a1, -1
flt.s t0, ft1, ft3
bnez t0, seguinte # exclui negativos
fadd.s ft0, ft0, ft1
addi t2, t2, 1
seguinte:
bnez a1, ciclo_media
fcvt.s.w ft2, t2
fdiv.s ft0, ft0, ft2
Para [1,5; −2,0; 4,5; 0,0], acompanha soma e contagem:
| Valor lido | t0: valor menor que zero? | Soma | Contagem |
|---|---|---|---|
| 1,5 | 0 | 1,5 | 1 |
| −2,0 | 1 | 1,5 | 1 |
| 4,5 | 0 | 6,0 | 2 |
| 0,0 | 0 | 6,0 | 3 |
O zero entra na média, porque o teste exclui apenas valores estritamente negativos. O divisor é 3, convertido para 3,0, e a média é 2,0. Dividir a soma filtrada 6,0 pelo comprimento 4 daria 1,5, que não responde a nenhuma das duas perguntas: a média filtrada é 2,0 e a média de todos os elementos é (1,5−2,0+4,5+0,0)/4=1,0.
O comprimento zero precisa de verificação antes da primeira carga, e uma sequência só de negativos precisa de tratamento antes da divisão por zero. Esses casos estão excluídos do contrato do fragmento, não resolvidos pelo ciclo.
Para acumular em dupla dados guardados em simples, mantém o avanço de quatro bytes. Converte cada carga com fcvt.d.s, inicializa a soma com fcvt.d.w ...,zero, usa fadd.d, converte a contagem com fcvt.d.w e divide com fdiv.d. Só no retorno usa fcvt.s.d se o resultado pedido for simples. Aumentar a precisão do cálculo não aumenta o tamanho dos elementos em memória nem torna exatas todas as somas.
Codificar uma instrução
O opcode identifica a família da instrução; funct3 e funct7 distinguem operações dentro dela. rd é o destino, rs1/rs2 são origens e imm é uma constante codificada na instrução. Os campos conservam posições quando possível:
| Formato | Campos, do bit 31 para o 0 | Exemplos |
|---|---|---|
| R | funct7 rs2 rs1 funct3 rd opcode | add, sub |
| I | imm[11:0] rs1 funct3 rd opcode | addi, lw, jalr |
| S | imm[11:5] rs2 rs1 funct3 imm[4:0] opcode | sw |
| B | imm[12,10:5] rs2 rs1 funct3 imm[4:1,11] opcode | beq |
| U | imm[31:12] rd opcode | lui, auipc |
| J | imm[20,10:1,11,19:12] rd opcode | jal |
Na expressão seguinte, << desloca bits à esquerda e | combina os campos com OR. Como cada campo ocupa bits distintos, os campos não se sobrepõem.
Os tamanhos de opcode, rd, funct3, rs1, rs2 são 7, 5, 3, 5 e 5 bits. Em lw t2,40(t6), rd=7, rs1=31, funct3=2, opcode=3 e imm=40:
Em B e J, o bit zero do deslocamento é implícito. Reconstrói o deslocamento com sinal em bytes, antes de o somar ao PC da própria instrução. Para um salto de 0x100 para 0x0F0, o deslocamento é −16, não −20.
Saltos e decisões
beq/bne comparam igualdade. blt/bge comparam com sinal; bltu/bgeu, sem sinal. Se não houver salto, o PC avança 4 bytes neste modelo sem instruções comprimidas.
jal ra,funcao guarda PC+4 em ra. jalr rd,imm(rs1) guarda também PC+4 e salta para (rs1+imm) & ~1. ret é a pseudoinstrução jalr zero,0(ra).
Uma pseudoinstrução pode expandir-se em várias instruções reais. mv t0,t1 equivale a addi t0,t1,0; j L, a jal zero,L. li, la e call dependem da constante ou distância. Conta as instruções expandidas se o problema pedir ciclos ou códigos máquina.
Para formar 0x12345ABC com lui/addi, repara que a parte baixa 0xABC é negativa como imediato de 12 bits: −1348. Por isso usa lui t0,0x12346 seguido de addi t0,t0,-1348. O incremento da parte alta compensa essa extensão de sinal.
Chamadas e a pilha
O chamador guarda temporários de que precisa depois de uma chamada. O chamado guarda e repõe os s que altera. Uma função que chama outra deve proteger o seu ra. A pilha cresce para endereços menores e mantém alinhamento de 16 bytes na convenção usada aqui.
O programa seguinte calcula dobro(7)+5 = 19. s0 conserva o 5 durante a chamada; o ra exterior fica no frame. As funções não usam variáveis globais para guardar retornos, permitindo chamadas aninhadas.
.text
.globl main
main:
andi sp, sp, -16
li a0, 7
jal ra, dobro_mais_cinco
li a7, 1
ecall
li a7, 10
ecall
dobro_mais_cinco:
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 0(sp)
li s0, 5
jal ra, dobro
add a0, a0, s0
lw s0, 0(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
dobro:
slli a0, a0, 1
retExecuta o código para ver a saída e os gráficos.
As chamadas ecall deste bloco usam os serviços do simulador: imprimir inteiro com a7=1 e terminar com a7=10. São a interface de execução do exemplo, não instruções de entrada e saída universais de RV32.
Vídeo de apoio: pilha e stack pointer
A pilha e o registo sp são o tema deste vídeo de Rodrigo Dias.
Profundidade da pilha na recursão
Uma função recursiva tem vários frames ativos ao mesmo tempo. Cada chamada guarda o seu retorno e os valores de que precisa depois da chamada seguinte. O deslocamento 12(sp) repete-se no código, mas o endereço absoluto muda porque cada chamada tem outro sp.
Esta rotina calcula a soma de 1 até n. Assume 1 ≤ n ≤ 65535, resultado de 32 bits com sinal e espaço de pilha suficiente. Guarda n em s0 para o recuperar depois de a0 receber a soma da chamada interior.
soma_recursiva:
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
mv s0, a0
li t0, 1
beq a0, t0, repor_soma
addi a0, a0, -1
jal ra, soma_recursiva
add a0, a0, s0
repor_soma:
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
Com n=5, antes de começar a retornar:
| Chamada ativa | sp relativamente ao inicial | n conservado neste frame |
|---|---|---|
| 5 | −16 bytes | 5 |
| 4 | −32 bytes | 4 |
| 3 | −48 bytes | 3 |
| 2 | −64 bytes | 2 |
| 1, caso base | −80 bytes | 1 |
O caso base também reserva o frame, porque a reserva ocorre antes do teste. O pico é bytes, embora cada frame guarde apenas duas palavras, oito bytes úteis. Contam-se também os oito bytes de espaço reservado para manter o alinhamento.
O caso base retorna 1. As chamadas seguintes formam 3, 6, 10 e 15, libertando um frame em cada retorno. No fim, sp e s0 têm os valores que tinham antes da chamada inicial. Cinco chamadas sucessivas que retornassem antes da próxima reutilizariam a mesma região: o pico seria 16 bytes, não 80. A memória máxima depende das chamadas simultaneamente ativas, não só do número total de chamadas.
Exercícios
Ler o mesmo byte com e sem sinal
Um byte de memória contém 0xF0. Que inteiro com sinal se obtém com lb em RV32?
Pista
Solução
lb estende o sinal: 240−256=−16, padrão 0xFFFFFFF0. lbu produziria 240.
Erros frequentes
Interpretar lb como lbu ou preencher os bits altos com zeros.
Construir uma constante de 32 bits
Queremos 0x23456FED por lui e addi. Qual é o imediato de lui, em decimal?
Pista
Solução
A parte alta deve ser 0x23457=144471, porque (0x23457<< 12)−19=0x23456FED.
Erros frequentes
Usar 0x23456 na parte alta e ignorar a extensão de sinal.
Montar os campos de um load
Codifica lw t1,−8(a0), indicando campos e palavra hexadecimal.
Pista
Solução
imm=0xFF8, rs1=10, funct3=2, rd=6, opcode=3. A palavra é 0xFF852303. Descodificar os campos repõe lw x6,−8(x10).
Confere a tua resposta:
- Identifiquei rd=x6 e rs1=x10.
- Separei opcode, funct3, registos e imediato 12 bits.
- Conferi a extensão de sinal de −8.
Erros frequentes
Colocar rs2 em um formato I, usar uma constante positiva ou trocar rd e rs1.
Preservar uma chamada aninhada
Uma rotina usa s0 e chama outra. Descreve um frame RV32 mínimo alinhado 16 e o retorno.
Pista
Solução
addi sp, sp,−16; sw ra, 12(sp); sw s0, 0(sp). Depois da chamada: lw s0, 0(sp); lw ra, 12(sp); addi sp, sp, 16; ret. O espaço restante não precisa de ser usado.
Confere a tua resposta:
- Guardei ra antes da chamada interior.
- Guardei e repus o s0 alterado.
- Mantive sp alinhado 16 e repus o valor original.
Erros frequentes
Usar sd/ld em RV32, guardar ra depois da chamada ou retornar antes de repor sp.
Dividir um negativo
Em RV32 com extensão M, t0 contém −3 e t1 contém 2. Compara srai t2,t0,1,
div t3,t0,t1 e srli t4,t0,1. Que inteiro resulta em cada
destino?
Pista
Solução
t2=−2, padrão 0xFFFFFFFE; t3=−1, pois div arredonda para
zero; t4=2147483646, padrão 0x7FFFFFFE. Ou seja, o shift aritmético
arredonda −3/2 para baixo.
Confere a tua resposta:
- Obtive −2 com srai e −1 com div.
- Expliquei os dois arredondamentos.
- Distingui srli de srai.
Erros frequentes
Usar truncagem para zero no shift aritmético ou conservar o sinal no shift lógico.
Reconstruir o destino de um branch
Um beq em PC=1024 tem deslocamento descodificado de −12 bytes. A condição é verdadeira. Qual é o PC seguinte, em decimal? Assume RV32 sem instruções comprimidas.
Pista
Solução
PC=1024−12=1012. O PC+4=1028 só seria o próximo endereço se o branch não fosse tomado, por isso não multipliques outra vez o deslocamento descodificado.
Erros frequentes
Somar a PC+4 ou multiplicar por 2 um imediato que já inclui o bit zero implícito.
Seguir leitura, soma e escrita
Com a0=0x1000, t2=5 e palavra em 0x1000 com valor 7, segue lw t0, 0(a0); add t1, t0, t2; sw t1, 4(a0).
Pista
Solução
Endereço do load 0x1000, t0=7. O add calcula 12 em t1. O store escreve 12 em 0x1004. O PC arquitetural aponta para cada instrução atual; a obtenção calcula o seguinte a partir dele.
Confere a tua resposta:
- Calculei o endereço do load em bytes.
- Propaguei o valor lido para o add.
- Escrevi o resultado no endereço do store.
Erros frequentes
Somar deslocamentos como contagem de palavras em vez de bytes.
Conservar a precisão e o tamanho dos elementos
Uma rotina recebe em a0 a base alinhada de um vetor de números finitos de precisão simples e em a1 o seu comprimento positivo. Deve acumular e dividir em dupla, sem overflow, e retornar a média de todos os elementos em fa0, com precisão simples. Para [1,5; 2,5; 5,0], indica as instruções que inicializam a soma, carregam e convertem cada elemento, somam, convertem o divisor, dividem e preparam o retorno. Um colega propõe trocar flw por fld, avançar oito bytes e usar fmv.d para converter. Explica os erros.
Pista
Distingue o formato guardado em memória do formato dos cálculos.
Solução
Inicializa com fcvt.d.w fa0,zero. Conserva a1 para o divisor e
usa outro registo inteiro para controlar o ciclo. Cada elemento usa
flw ft0,0(a0), fcvt.d.s ft1,ft0 e
fadd.d fa0,fa0,ft1; avança a0 quatro bytes e decrementa a
contagem do ciclo. A soma é 9,0. No fim,
fcvt.d.w ft2,a1 cria 3,0,
fdiv.d fa0,fa0,ft2 dá 3,0 efcvt.s.d fa0,fa0
prepara o retorno simples. fld e avanço de oito bytes tratariam pares de
elementos simples como se fossem valores duplos. fmv.d copia um valor já
duplo, não converte o formato simples. Se o comprimento pudesse ser zero, a
rotina precisaria de definir e tratar esse caso antes de carregar e dividir.
Confere a tua resposta:
- Usei flw e avanço de 4 bytes para os elementos simples.
- Converti cada elemento para dupla antes de fadd.d.
- Converti a contagem inteira para dupla antes de fdiv.d.
- Converti o resultado para simples, em vez de copiar bits.
- Obtive média 3,0 e expliquei a pré-condição de comprimento positivo.
Contar frames ativos, incluindo o caso base
A rotina soma_recursiva da lição recebe n=5 e reserva um frame de 16 bytes antes de testar n=1; guarda duas palavras em cada frame. Partindo de sp=4096, qual é o menor valor de sp? Quantos bytes estão reservados nesse instante e quantos contêm os registos guardados? Compara com cinco chamadas não recursivas que usem esse mesmo frame e retornem antes da chamada seguinte. Assume pilha suficiente, inicialmente alinhada, sem outros frames a contar.
Solução
Há frames de n=5,4,3,2,1, todos ativos antes do primeiro retorno. O menor sp é 4096−5×16=4016. Estão reservados 80 bytes, dos quais 5×2×4=40 bytes guardam os registos. Os restantes mantêm o tamanho alinhado dos frames. No caso não recursivo, cada retorno repõe sp=4096 antes da próxima chamada; o menor sp é 4080 e o pico é apenas 16 bytes. O número total de chamadas é igual, mas o tempo de vida dos frames é diferente.
Confere a tua resposta:
- Contei cinco frames simultâneos e pico de 80 bytes.
- Incluí o frame reservado pelo caso base.
- Distingui 40 bytes úteis de 80 bytes reservados.
- Expliquei por que chamadas sucessivas usam um pico de 16 bytes.

