Assembly RISC-V

RV32, endereços, formatos de instrução, saltos e chamadas com a pilha.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir
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.

Vídeo de apoio: RISC-V Assembly

Introdução em vídeo de Rodrigo Dias:

Abrir no YouTube

Registos e convenção de chamadas

NomeRegistosUso e preservação
zerox0Sempre zero; escritas são ignoradas
rax1Endereço de retorno; uma chamada pode alterá-lo
spx2Apontador da pilha; repor antes de retornar
gp, tpx3, x4Reservados pela convenção de execução
t0 a t6x5 a x7, x28 a x31Temporários, não preservados pelo chamado
a0 a a7x10 a x17Argumentos; a0 e a1 também transportam retornos
s0 a s11x8, x9, x18 a x27O 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çãoEndereço usadoAlteração
lw t0,0(a0)0x1000t0=7
add t1,t0,t2Sem acesso a dadost1=12
sw t1,4(a0)0x1004A 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çãoTamanhoEfeito
lb, lh, lw1, 2, 4 bytesLer; lb/lh estendem o sinal
lbu, lhu1, 2 bytesLer; preencher os bits altos com zeros
sb, sh, sw1, 2, 4 bytesGuardar 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çãoEfeito
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.sCalculam com operandos de precisão simples
fcvt.s.w ft0,t0Converte o inteiro com sinal em t0 para precisão simples
fcvt.d.s ft1,ft0Converte um valor simples para dupla
fcvt.s.d fa0,ft1Converte de dupla para simples, arredondando quando necessário
flt.s t0,ft1,ft2Escreve 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 lidot0: valor menor que zero?SomaContagem
1,501,51
−2,011,51
4,506,02
0,006,03

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:

FormatoCampos, do bit 31 para o 0Exemplos
Rfunct7 rs2 rs1 funct3 rd opcodeadd, sub
Iimm[11:0] rs1 funct3 rd opcodeaddi, lw, jalr
Simm[11:5] rs2 rs1 funct3 imm[4:0] opcodesw
Bimm[12,10:5] rs2 rs1 funct3 imm[4:1,11] opcodebeq
Uimm[31:12] rd opcodelui, auipc
Jimm[20,10:1,11,19:12] rd opcodejal

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:

(40≪20)  ∣  (31≪15)  ∣  (2≪12)  ∣  (7≪7)  ∣  3=0x028FA383.(40\ll20)\;|\;(31\ll15)\;|\;(2\ll12)\;|\;(7\ll7)\;|\;3 =\mathtt{0x028FA383}.

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.

Chamada RV32 com frame de pilha
.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
    ret
Resultado

Executa 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.

Abrir no YouTube

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 ativasp relativamente ao inicialn conservado neste frame
5−16 bytes5
4−32 bytes4
3−48 bytes3
2−64 bytes2
1, caso base−80 bytes1

O caso base também reserva o frame, porque a reserva ocorre antes do teste. O pico é 5×16=805\times16=80 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
O bit 7 é o sinal.
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.

Voltar à explicação
Construir uma constante de 32 bits

Queremos 0x23456FED por lui e addi. Qual é o imediato de lui, em decimal?

Pista
O imediato baixo tem 12 bits com sinal.
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.

Voltar à explicação
Montar os campos de um load

Codifica lw t1,−8(a0), indicando campos e palavra hexadecimal.

Pista
t1 é x6; a0 é x10.
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.

Voltar à explicação
Preservar uma chamada aninhada

Uma rotina usa s0 e chama outra. Descreve um frame RV32 mínimo alinhado 16 e o retorno.

Pista
A chamada interior substitui ra.
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.

Voltar à explicação
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
Escreve −3 como padrão de 32 bits.
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.

Voltar à explicação
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
O deslocamento já está reconstruído em bytes.
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.

Voltar à explicação
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
O load lê 7; o add soma 5.
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.

Voltar à explicação
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.
Voltar à explicação
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.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.