Pular para o conteúdo

Hardware e sistemas operacionais · parte 12 de 27

ISA, CISC e RISC: o contrato entre programa e processador

O que uma ISA define e por que x86-64, AArch64 e RISC-V diferem em formato, memória e tamanho de instrução, com bytes reais montados e conferidos.

intermediárioPublicada em · 32 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

ISA, CISC e RISC: o contrato entre programa e processador

Pense num cardápio de restaurante. Ele lista os pratos que a cozinha sabe fazer, e o garçom só pode anotar pedidos que estejam ali. O cliente não precisa saber se o bife é grelhado numa chapa ou num forno, e a cozinha pode trocar de fogão sem avisar ninguém, desde que continue servindo os mesmos pratos. Na computação, o cardápio é a ISA, sigla de instruction set architecture, ou conjunto de instruções. O garçom é o compilador, que traduz o programa para pratos do cardápio. A cozinha é o processador. Uma cozinha nova pode ser mais rápida que a antiga e ainda assim receber o mesmo pedido, porque o que foi combinado é o cardápio, e não o fogão.

A analogia tem um limite que vale guardar desde o começo. Um cardápio grande, com pratos que já vêm montados, e um cardápio pequeno, em que você monta o prato em etapas, são duas formas de desenhar o contrato, e é aí que entram os nomes CISC e RISC. Nas duas, a cozinha continua sendo a parte que mais muda de uma geração para outra.

Esta aula responde a uma pergunta: o que é um conjunto de instruções, e por que x86 e ARM são diferentes? Ela parte da ULA e dos registradores da aula ULA e registradores, porque uma instrução só faz sentido como ordem para uma ULA que soma e para registradores que guardam os operandos. Como a CPU busca, decodifica e executa essa instrução ao longo de ciclos fica para Ciclo de instrução e unidade de controle. Como um laço em C vira instruções passo a passo fica para Assembly na prática.

Mapa da aula

SeçãoPergunta que responde
1. O que é uma ISAO que o contrato define, e o que fica de fora dele?
2. Para que serveQual problema levou as empresas a separar o contrato da implementação?
3. Exemplos em contextos diferentesOnde a diferença entre ISAs aparece na prática?
4. Como funcionaComo uma instrução vira bits, e por que x86 e RISC geram bytes com formas diferentes?
5. Detalhes e bordasO que mudou desde os anos 1980 e onde a divisão CISC e RISC deixa de servir?
6. Erros comuns e boas práticasQuais enganos aparecem quando alguém compara processadores pelo nome da arquitetura?
7. O programa que confere os bitsComo conferir a codificação de uma instrução sem confiar em memória?
8. ExercíciosComo praticar, com as respostas?
9. GlossárioQuais termos a aula usa?

1. O que é uma ISA

A ISA é a lista de tudo o que um programa pode pedir ao processador. Ela define as instruções disponíveis, com o nome de cada uma e o que ela faz, como são os registradores que um programa enxerga pelo nome, quais maneiras de endereçar a memória existem e como cada instrução é codificada em bits. Também faz parte do contrato o modelo de memória, que diz em que ordem as escritas de um núcleo aparecem para os outros. Cada item dessa lista é algo que um compilador ou um programador de linguagem de máquina precisa saber, e é por isso que a ISA é o que o software enxerga do hardware.

O que fica de fora do contrato é a microarquitetura. Ela é a maneira como o fabricante implementa a ISA: quantos estágios tem o pipeline, quantas instruções executam por ciclo, quanto cache existe e como o processador prevê os desvios. Duas CPUs da mesma ISA podem ter microarquiteturas muito diferentes e, ainda assim, rodar o mesmo binário. Por isso, quando alguém diz que um chip é x86-64, a frase informa qual contrato ele cumpre, e só isso. Os processadores da Intel e os da AMD implementam x86-64, mas são projetos diferentes, com desempenhos diferentes. Os chips da Apple da linha M implementam AArch64, a versão de 64 bits da família ARM. Esses fatos são conhecidos, mas esta aula não os conferiu em fonte primária de cada fabricante, e a figura abaixo os usa só como exemplo do padrão.

Código em C vira binários x86-64 ou AArch64 pelo compilador; a ISA é o contrato, e chips de fabricantes diferentes a implementam.
figura 1 · Uma ISA é o contrato entre o programa e o processador: o mesmo binário x86-64 ro

Leia a figura de cima para baixo. O código em C entra no compilador, que escolhe a ISA alvo e gera um binário. O caminho verde mostra que cada binário carrega bytes de uma única ISA, e a seta verde com um X vermelho mostra o que não acontece: um binário x86-64 não roda direto num chip que implementa só AArch64, porque o chip não entende aqueles bytes. A caixa do meio é o contrato, em roxo. Abaixo dele, os chips de fabricantes diferentes aparecem agrupados, porque todos seguem o mesmo contrato. O que muda entre eles fica fora da figura, e é justamente a parte que a ISA não define.

2. Para que serve

Antes de existir o contrato separado, cada máquina tinha o seu próprio conjunto de instruções, e um programa escrito para uma máquina não rodava em outra. Quem comprava um computador novo reescrevia o software, ou pagava para alguém reescrever. A separação entre ISA e implementação resolve esse problema de duas formas. A primeira é a compatibilidade: o fabricante pode trocar o projeto interno do chip a cada geração, sem exigir que os programas sejam refeitos, desde que a ISA continue a mesma. A segunda é a concorrência: empresas diferentes podem construir chips que obedecem à mesma ISA, e o comprador escolhe pelo preço, pelo consumo ou pelo desempenho.

O manual oficial de RISC-V traz um exemplo antigo de ISA com formatos de tamanho variável: a arquitetura IBM 360, de 1964, que aceitava instruções de 16, 32 ou 48 bits, segundo a citação de Amdahl et al. feita no próprio manual. Uma ISA só funciona como contrato se as máquinas que a implementam a cumprem à risca. Trocar a ISA é caro justamente porque ela carrega o software acumulado de anos, e por isso as mudanças costumam acrescentar instruções em vez de trocar as que já existem.

3. Exemplos em contextos diferentes

O primeiro exemplo é o computador pessoal. Um notebook com chip x86-64 e um notebook com chip da linha M da Apple instalam sistemas operacionais e aplicativos diferentes porque os binários são diferentes, mesmo que a interface pareça igual. O programa que você compila para um deles não roda no outro sem recompilação, ou sem uma camada que traduza as instruções, e a escolha da ISA aparece no nome do arquivo executável e nas mensagens de erro quando algo não bate.

O segundo exemplo é o celular. Os smartphones atuais, em sua maioria, usam chips AArch64, um fato conhecido que esta aula não conferiu em fonte, e a escolha da ISA vale para qualquer marca que use esses chips. Um app para Android compilado para AArch64 roda em qualquer celular que implemente essa ISA, sem que o desenvolvedor conheça a microarquitetura de cada modelo. É a mesma lógica do notebook, mas aplicada a milhões de aparelhos com chips de fabricantes diferentes.

O terceiro exemplo são os microcontroladores pequenos, que controlam máquinas de lavar, impressoras e sensores. Nesses chips, o tamanho do código pesa tanto quanto a velocidade, e o próprio manual do RISC-V lembra que as ISAs RISC só passaram a ter versões compactas quando o uso embarcado pediu isso. A seção 5 detalha a extensão de 16 bits e a licença aberta do RISC-V.

O quarto exemplo é o mais direto para quem programa: o mesmo código C compilado para ISAs diferentes. A função abaixo faz *p += x, ou seja, soma x no valor que p aponta, e o resultado do compilador mostra como a ISA molda a sequência de instruções.

O mesmo *p += x em x86-64, AArch64 e RISC-V: o x86 lê, soma e grava numa instrução, e as outras duas separam os passos.
figura 2 · O mesmo *p += x vira instruções diferentes em x86-64, AArch64 e RISC-V: o x86 fa

A figura mostra o trabalho em três colunas. Na coluna do x86-64, a instrução add qword ptr [rdi], rsi faz a leitura da memória, a soma e a escrita de volta, tudo numa linha só, e o ret volta ao chamador. Na coluna do AArch64, o mesmo trabalho aparece em três instruções separadas: ldr lê a memória para o registrador x8, add soma no registrador e str grava de volta. A coluna do RISC-V tem a mesma divisão, com ld, add e sd, e foi montada a partir dos campos de bits do manual, porque esta máquina não tem um compilador RISC-V. O ret do RISC-V fica de fora da figura porque depende da convenção de chamada, que esta aula não descreve.

Os textos de saída do compilador vêm da seção 7, onde o programa e a saída real aparecem completos.

4. Como funciona

Uma instrução, na memória, é uma sequência de bytes. Esses bytes precisam dizer três coisas: qual operação fazer, de quais operandos ela precisa e onde está o resultado. Essa informação é o que a ISA chama de codificação, e a regra que diz como os campos se organizam dentro dos bytes é o formato da instrução. Quando a CPU lê uma instrução, a unidade de decodificação separa esses campos e os envia à ULA e aos registradores, como a aula ULA e registradores mostrou com a instrução ADD de exemplo.

4.1 Formato de tamanho fixo

No RISC-V, a base de instruções tem tamanho fixo de 32 bits, e cada instrução precisa começar em uma posição alinhada de 32 bits. O manual diz que a codificação padrão foi pensada para extensões com instruções de tamanho variável, mas a base em si é fixa. No formato R, usado pelas operações entre dois registradores, os campos aparecem sempre na mesma ordem e com a mesma largura: funct7 com 7 bits, rs2 com 5, rs1 com 5, funct3 com 3, rd com 5 e opcode com 7. A soma é 32, e o decodificador não precisa adivinhar onde cada campo começa, porque ele sempre começa no mesmo bit.

O AArch64 também usa 32 bits em toda instrução, segundo o documento técnico da Arm sobre portabilidade de 64 bits, de 2014, citado nas fontes. O layout, porém, é outro. Na instrução ADD com registrador deslocado, o primeiro campo mistura o bit sf, que escolhe entre 32 e 64 bits, o bit op, o bit S, que altera as flags, e o código fixo 01011, que identifica a família da instrução. Depois vêm o campo shift, o registrador Rm, o deslocamento imediato imm6, o registrador Rn e o destino Rd. Os dois formatos têm o mesmo número de bits, mas as fronteiras entre os campos são diferentes, e um decodificador de RISC-V não serve para ler AArch64.

Layout de 32 bits do add em RISC-V e em AArch64: RISC-V separa funct7, registradores e opcode, e AArch64 põe sf, shift e Rm no mesmo espaço.
figura 3 · Duas instruções de 32 bits com layouts diferentes: RISC-V separa funct7, registr

A figura alinha os dois formatos com a mesma régua. Cada caixa tem a largura proporcional ao número de bits, então o campo de 7 bits do funct7 ocupa mais espaço que o de 3 bits do funct3. Em azul estão os campos que apontam para registradores, e em terracota estão os campos que dizem qual operação fazer. Na linha do RISC-V, a leitura vai da esquerda para a direita: os registradores rs2 e rs1 ocupam o centro, e o opcode é o último campo. Na linha do AArch64, o campo fixo 01011 aparece no meio da palavra, e o Rm fica depois do shift. A caixa do bit 0 foi juntada com o shift por falta de espaço, e o texto explica a leitura. Os valores são os da palavra real, e a seção 7 mostra que o capstone desmonta os dois como add a0, a1, a2 e add x0, x1, x2.

Repare que o campo funct3 do RISC-V aparece como f3 na figura, para caber numa caixa de 3 bits. Ele é o mesmo campo, e no add vale 000.

4.2 Tamanho variável no x86

O x86-64 faz o caminho inverso. Uma instrução pode ocupar de 1 a 15 bytes, e o manual do Intel 64 e IA-32 diz que o limite de 15 bytes vale para qualquer instrução, inclusive as que têm prefixo REX. O tamanho depende de quantas peças a instrução precisa: um prefixo opcional, o opcode, um byte ModRM que diz quais registradores e modos de endereçamento usar, um byte SIB quando há índice na memória, um deslocamento e um imediato. Uma instrução que só troca dois registradores cabe em poucos bytes, e uma que carrega um número de 64 bits pode passar de 10.

Essa variação tem uma consequência que a figura mostra bem. Cada instrução carrega só o que precisa, e o processador não sabe onde a próxima começa até decodificar a atual. A figura abaixo mostra cinco instruções reais, montadas com clang, com os bytes que o objdump devolveu.

Cinco instruções x86-64 de 3 a 10 bytes, com REX, opcode, ModRM, SIB, deslocamento e imediato contribuindo para o tamanho de cada uma.
figura 4 · Instruções x86-64 ocupam de 3 a 10 bytes: prefixo REX, opcode, ModRM, SIB e o im

Cada quadro é uma instrução, com os bytes na ordem em que aparecem na memória. A primeira, add rax, rbx, tem três bytes: REX 48, opcode 01 e ModRM D8. A segunda, add [rbx+8], rax, troca o ModRM por 43 e acrescenta o deslocamento 08, e por isso ocupa quatro. A terceira, mov eax, 1, não tem REX e usa o opcode B8 seguido de um imediato de 4 bytes, num total de cinco. A quarta carrega um endereço com índice e deslocamento de 4 bytes, e chega a oito. A última, movabs rax, 0x1122334455667788, carrega um número de 64 bits, com REX, opcode e 8 bytes de imediato, num total de dez. Os valores dos bytes foram lidos da saída do objdump, sem edição. O caso mais curto que a aula verificou é o ret, com 1 byte, c3, conferido na seção 7.

Os papéis de cada campo são só cores da régua do formato. A palavra “disp” na segunda instrução é o deslocamento, abreviado para caber na caixa de 1 byte.

4.3 Memória: uma instrução ou três

A diferença mais importante entre as famílias, para quem escreve código, é onde a instrução pode tocar a memória. No x86-64, a instrução add qword ptr [rdi], rsi lê o valor apontado por rdi, soma rsi e grava o resultado de volta no mesmo endereço, tudo num único comando. A CPU faz as três etapas internamente, e o programa vê uma só instrução. Essa forma de ISA, em que uma operação aritmética pode ter um operando na memória, é o que se costuma chamar de CISC, sigla de complex instruction set computer, ou computador com conjunto de instruções complexo.

No AArch64, a mesma operação vira três instruções. ldr x8, [x0] carrega o valor da memória para um registrador, add x8, x8, x1 soma dentro dos registradores e str x8, [x0] grava o resultado. Nenhuma instrução de soma toca a memória, e apenas as de leitura e escrita a acessam. Essa separação tem nome, arquitetura load-store, e é a marca usual das ISAs chamadas de RISC, sigla de reduced instruction set computer, ou computador com conjunto de instruções reduzido.

Somar um valor na memória: o x86 lê, soma e grava numa instrução, e o AArch64 separa a leitura, a soma e a escrita em três instruções.
figura 5 · Somar um valor na memória: o x86 faz o acesso dentro de uma instrução, e o AArch

A figura separa os dois casos em duas faixas. Na de cima, o x86-64 aparece com uma caixa só, ligada ao cilindro da memória por uma seta de ida e volta, rotulada como lê e grava. Na de baixo, o AArch64 tem a sequência de cinco elementos, com a memória à esquerda e à direita, e cada seta mostra um passo: a leitura, a soma nos registradores e a escrita. O número de instruções aumenta, e a instrução de soma não toca a memória em nenhum momento. Essa é a promessa da arquitetura load-store, e é o que a figura quer mostrar.

Vale saber, porém, que a fronteira entre as duas famílias é menos nítida do que os nomes sugerem. A seção 5 mostra que o AArch64 tem instruções que carregam ou gravam dois registradores de uma vez, e que o RISC-V tem uma extensão de instruções compactadas. O que sobrevive da distinção é a pergunta de quantas operações um único comando faz, e essa pergunta tem resposta para cada instrução.

4.4 Decodificar bytes

A forma da ISA afeta o trabalho do decodificador, que é a peça da CPU que lê os bytes e descobre o que cada um significa. Com tamanho fixo, o decodificador sabe que a próxima instrução começa a 4 bytes da atual, e pode buscar e decodificar várias delas em paralelo, porque cada uma ocupa uma posição conhecida. Com tamanho variável, ele precisa primeiro descobrir onde a instrução atual termina, e só então sabe onde começa a seguinte.

Decodificação de bytes: AArch64 usa 4 bytes em toda instrução, e x86-64 vai de 3 a 10, obrigando a decodificar a atual para achar a próxima.
figura 6 · Com tamanho fixo, o decodificador sabe onde começa a próxima instrução; com tama

A figura põe os dois casos lado a lado, com a largura de cada caixa proporcional ao tamanho em bytes. Na faixa de cima, as quatro instruções do AArch64 têm o mesmo tamanho, e a posição da próxima é conhecida sem nenhuma conta. Na faixa de baixo, as cinco instruções do x86-64 têm tamanhos diferentes, e a largura irregular mostra o que o decodificador precisa fazer. O tamanho variável não impede a decodificação em paralelo, mas a torna mais complicada. Esta aula não mediu esse custo em nenhum chip, e a figura é só uma representação do problema.

4.5 Micro-operações

Os processadores x86 modernos não executam as instruções exatamente como elas são escritas. O manual da Intel descreve, na seção sobre a microarquitetura P6, que o front end do processador decodifica as instruções em micro-operações, pequenas tarefas internas que o núcleo executa. Uma instrução complexa pode virar várias micro-operações, e uma instrução simples pode virar uma só. Esse é o ponto em que a distinção entre CISC e RISC fica mais difusa, porque o que o núcleo executa é parecido nas duas famílias.

A figura abaixo mostra essa ideia de forma ilustrativa. A divisão em carga, soma e escrita é uma forma de explicar o processo, e não mede a divisão de um chip real. A coluna da esquerda é a do x86, com a instrução sendo separada antes da execução, e a da direita é a do AArch64, que chega do compilador já em passos separados.

Ilustração de uma instrução x86 dividida em micro-operações de carga, soma e escrita, enquanto o AArch64 do exemplo chega em três instruções.
figura 7 · Ilustração: uma instrução x86 pode ser separada em micro-operações antes de exec

Leia a figura de cima para baixo. A instrução do programa passa pelo decodificador, que a divide em três micro-operações: a carga de [rdi], a soma com rsi e a escrita em [rdi]. Elas seguem para o núcleo, que as executa. O rótulo “divisão ilustrativa” lembra que a figura não mede o chip. Na coluna da direita, as três instruções do AArch64 aparecem como o compilador as escreveu, sem divisão extra no caminho do exemplo. A comparação mostra o que muda na escrita do programa, e não o que acontece dentro de cada núcleo.

4.6 Quantos registradores cada ISA expõe

Além do formato e da memória, a ISA define quantos registradores o programa enxerga pelo nome. Essa escolha afeta quanto o compilador consegue manter em registradores antes de recorrer à memória, e por isso aparece nas duas famílias. O manual da Intel diz que, no modo de 64 bits, existem 16 registradores de uso geral, com nomes como RAX, RBX e R8 a R15. O documento da Arm de 2014 diz que o AArch64 tem 31 registradores de uso geral de 64 bits, além de um registrador de zero. O manual do RISC-V diz que a base de inteiros tem 32 registradores, em que x0 está fixo em zero, e que escrever nele não tem efeito.

Esses números não dizem qual ISA é mais rápida. Eles dizem quanto espaço o compilador tem para trabalhar sem sair dos registradores, e um número maior de registradores costuma ajudar quando o compilador precisa guardar muitos valores ao mesmo tempo.

5. Detalhes e bordas

A primeira borda é a história da compressão de código. Nos anos 1980, as ISAs RISC escolheram desempenho antes de tamanho, porque eram pensadas para estações de trabalho, onde o espaço de memória importava menos. Quando o uso passou a incluir sistemas embarcados, ARM e MIPS ganharam versões com instruções de 16 bits, e o manual do RISC-V afirma que essas versões comprimidas reduziram o código em cerca de 25% a 30% em relação às ISAs de origem. O mesmo texto diz que o resultado surpreendeu quem esperava que a ISA de tamanho variável, a 80x86, gerasse código menor, e que o código comprimido de RISC ficou significativamente menor que o da 80x86. Essa frase é do próprio manual do RISC-V, e a aula a cita com a ressalva de que o número refere-se à comparação feita no texto de origem, não a um benchmark desta aula.

A segunda borda é que o RISC-V separa a base de instruções das extensões. A base tem 32 bits e é fixa, e a extensão de compressão, chamada C no manual, adiciona instruções de 16 bits que se misturam com as de 32, com alinhamento de 16 bits. Essa separação permite que um processador simples implemente só a base, e que um mais sofisticado acrescente as extensões. No RISC-V, a divisão CISC e RISC importa menos, e o que define o chip passa a ser o conjunto de extensões que ele implementa.

A terceira borda é o AArch64 em si. O documento da Arm de 2014 diz que o conjunto tem instruções de carga e escrita de pares de registradores de 64 bits, que substituíram as antigas instruções de carga e escrita múltipla e os antigos PUSH e POP da ARM de 32 bits. Uma instrução que carrega dois registradores de uma vez não se encaixa na ideia pura de um comando por operação, o que mostra que a família ARM não segue a divisão RISC de forma pura. Vale lembrar que o documento é de 2014, e que o manual atual da Arm, que esta aula não conseguiu baixar, pode ter detalhes diferentes.

A quarta borda é a licença. O manual do RISC-V está sob a licença Creative Commons Attribution 4.0, e o documento é mantido pela RISC-V International, como a página de créditos informa. A aula não conferiu as regras de licenciamento de x86 e de AArch64, e por isso não as descreve aqui. A diferença de licença não muda o que a instrução faz, mas muda quem pode construir um chip compatível.

6. Erros comuns e boas práticas

O erro mais frequente é tratar ISA e microarquitetura como se fossem a mesma coisa. Quem vê que dois processadores são x86-64 conclui que eles têm o mesmo desempenho, e quem vê que um é mais rápido conclui que a ISA é melhor. A ISA diz o que o programa pode pedir. A microarquitetura diz quanto tempo cada pedido leva, e é ela que muda de uma geração para outra. Antes de comparar processadores, separe as duas perguntas: qual contrato eles seguem, e quão bem cada um o cumpre.

O segundo erro é atribuir o desempenho à família. O argumento de que RISC é mais rápido, ou de que CISC é mais compacto, aparece muito em discussões de internet. O texto do RISC-V mostra que o tamanho do código depende de quais formatos compactos a ISA recebe, e não só do nome da família. Quando alguém afirma que uma ISA é mais eficiente, peça o benchmark, a versão do compilador e a microarquitetura usada. Sem isso, a afirmação não diz nada sobre a ISA.

O terceiro erro é achar que um binário serve para qualquer processador de nome parecido. Um executável para x86-64 não roda num AArch64, e um executável para AArch64 não roda num x86-64. A boa prática é verificar a arquitetura do alvo antes de copiar um binário, e lembrar que a compilação define a ISA. Em projetos com várias plataformas, o compilador deve receber o alvo explicitamente, em vez de assumir o da máquina que está compilando.

O quarto erro é ler o campo de uma instrução com a regra de outra ISA. Num RISC-V, o campo rd fica nos bits 11 a 7, e num AArch64 o Rd fica nos bits 4 a 0. Quem decodifica à mão e troca a regra obtém um registrador que nunca foi pedido. A boa prática é ter a tabela de codificação da ISA ao lado, como o manual, e conferir o resultado com um desmontador, como a seção 7 faz com o capstone.

O quinto erro, o mais sutil, é confundir o nome da flag ou do registrador com o comportamento. A aula ULA e registradores mostrou que a flag C tem convenções opostas em algumas arquiteturas, e a mesma cautela vale para os nomes de instrução. Um ld em RISC-V e um ldr em AArch64 fazem a mesma tarefa, mas não têm a mesma codificação, e cada ISA define suas próprias regras de alinhamento, que esta aula não confere.

A boa prática geral é a mesma que a aula da ULA recomendou: leia a especificação e confira com uma ferramenta que leia os bytes. O manual é a fonte primária, e o desmontador mostra se a leitura do manual foi feita corretamente.

7. O programa que confere os bits

O programa abaixo codifica três instruções do formato R do RISC-V a partir dos campos, mais o ld e o sd do exemplo de *p += x, e confere cada palavra com o desmontador capstone. Também decodifica a palavra do ADD do AArch64 gerada pelo clang e confere com o capstone. O programa não depende de nenhum compilador RISC-V, porque esta máquina não tem um. Os campos vêm do manual, e o capstone serve de árbitro independente. O comando usado foi ./v/bin/python -I codificacao.py, o Python de um ambiente virtual com capstone 5.0.7 criado só para a verificação, e a saída abaixo é a real, copiada sem edição.

# Codifica instruções a partir dos campos de bits e confere com o desmontador (capstone).
# RISC-V: formato R (ADD, SUB, AND), como na seção 2.1.4.2 do manual oficial.
# AArch64: ADD (shifted register), com a palavra que o clang gerou para "add x0, x1, x2".

from capstone import Cs, CS_ARCH_RISCV, CS_MODE_RISCV32, CS_MODE_RISCV64, CS_ARCH_ARM64, CS_MODE_ARM

# Tamanho de cada campo do formato R, em bits, na ordem de cima para baixo da palavra.
CAMPOS_R = [("funct7", 7), ("rs2", 5), ("rs1", 5), ("funct3", 3), ("rd", 5), ("opcode", 7)]


def codifica_r(funct7, rs2, rs1, funct3, rd, opcode):
    valores = [funct7, rs2, rs1, funct3, rd, opcode]
    palavra = 0
    for (nome, bits), valor in zip(CAMPOS_R, valores):
        assert 0 <= valor < 2 ** bits, f"{nome} não cabe em {bits} bits"
        palavra = (palavra << bits) | valor
    return palavra


def mostra_r(palavra):
    binario = f"{palavra:032b}"
    partes, pos = [], 0
    for nome, bits in CAMPOS_R:
        partes.append(binario[pos:pos + bits])
        pos += bits
    return " ".join(partes)


def desmonta(arq, palavra, nbytes=4):
    md = Cs(arq[0], arq[1])
    for ins in md.disasm(palavra.to_bytes(nbytes, "little"), 0):
        return f"{ins.mnemonic} {ins.op_str}"
    return "(não desmontou)"


# Registradores pelo nome ABI: a0 = x10, a1 = x11, a2 = x12.
casos_riscv = [
    ("add a0, a1, a2", codifica_r(0b0000000, 12, 11, 0b000, 10, 0b0110011)),
    ("sub a0, a1, a2", codifica_r(0b0100000, 12, 11, 0b000, 10, 0b0110011)),
    ("and a0, a1, a2", codifica_r(0b0000000, 12, 11, 0b111, 10, 0b0110011)),
]

print("RISC-V, formato R (32 bits): funct7 | rs2 | rs1 | funct3 | rd | opcode")
for esperado, palavra in casos_riscv:
    print(f"  {esperado:<16} 0x{palavra:08x}  {mostra_r(palavra)}")
    desmontado = desmonta((CS_ARCH_RISCV, CS_MODE_RISCV32), palavra)
    print(f"  {'capstone:':<16} {desmontado}")
    assert desmontado == esperado, (desmontado, esperado)

print()
# Palavra gerada pelo clang para "add x0, x1, x2" (AArch64, 64 bits, ADD shifted register).
palavra_arm = 0x8B020020
campos_arm = [("sf", 1), ("op", 1), ("S", 1), ("01011", 5), ("shift", 2), ("0", 1),
              ("Rm", 5), ("imm6", 6), ("Rn", 5), ("Rd", 5)]
binario = f"{palavra_arm:032b}"
pos = 0
print("AArch64, ADD shifted register: sf | op | S | 01011 | shift | 0 | Rm | imm6 | Rn | Rd")
saida = []
for nome, bits in campos_arm:
    saida.append(binario[pos:pos + bits])
    pos += bits
print(f"  0x{palavra_arm:08x}  {' '.join(saida)}")
print("  campos:", ", ".join(f"{nome}={int(v, 2)}" for (nome, _), v in zip(campos_arm, saida)))
desmontado_arm = desmonta((CS_ARCH_ARM64, CS_MODE_ARM), palavra_arm)
print(f"  capstone: {desmontado_arm}")
assert desmontado_arm == "add x0, x1, x2"

print()
# O mesmo *p += x em RISC-V: ld, add, sd. Como não há montador RISC-V nesta máquina,
# as palavras saem dos campos do manual (I-type para ld, R para add, S-type para sd).
def codifica_i(imm, rs1, funct3, rd, opcode):
    assert -2048 <= imm < 2048
    return ((imm & 0xFFF) << 20) | (rs1 << 15) | (funct3 << 12) | (rd << 7) | opcode

def codifica_s(imm, rs2, rs1, funct3, opcode):
    assert -2048 <= imm < 2048
    imm &= 0xFFF
    return ((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (funct3 << 12) | ((imm & 0x1F) << 7) | opcode

print("RISC-V64, *p += x (p em a0, x em a1; long tem 64 bits):")
sequencia = [
    ("ld a5, 0(a0)", codifica_i(0, 10, 0b011, 15, 0b0000011)),
    ("add a5, a5, a1", codifica_r(0b0000000, 11, 15, 0b000, 15, 0b0110011)),
    ("sd a5, 0(a0)", codifica_s(0, 15, 10, 0b011, 0b0100011)),
]
for esperado, palavra in sequencia:
    desmontado = desmonta((CS_ARCH_RISCV, CS_MODE_RISCV64), palavra)
    print(f"  0x{palavra:08x}  {esperado:<16} capstone: {desmontado}")
    assert desmontado == esperado, (desmontado, esperado)
RISC-V, formato R (32 bits): funct7 | rs2 | rs1 | funct3 | rd | opcode
  add a0, a1, a2   0x00c58533  0000000 01100 01011 000 01010 0110011
  capstone:        add a0, a1, a2
  sub a0, a1, a2   0x40c58533  0100000 01100 01011 000 01010 0110011
  capstone:        sub a0, a1, a2
  and a0, a1, a2   0x00c5f533  0000000 01100 01011 111 01010 0110011
  capstone:        and a0, a1, a2

AArch64, ADD shifted register: sf | op | S | 01011 | shift | 0 | Rm | imm6 | Rn | Rd
  0x8b020020  1 0 0 01011 00 0 00010 000000 00001 00000
  campos: sf=1, op=0, S=0, 01011=11, shift=0, 0=0, Rm=2, imm6=0, Rn=1, Rd=0
  capstone: add x0, x1, x2

RISC-V64, *p += x (p em a0, x em a1; long tem 64 bits):
  0x00053783  ld a5, 0(a0)     capstone: ld a5, 0(a0)
  0x00b787b3  add a5, a5, a1   capstone: add a5, a5, a1
  0x00f53023  sd a5, 0(a0)     capstone: sd a5, 0(a0)

As outras saídas desta aula vêm de comandos diretos, também reais. O clang da Apple, versão 21.0.0 (clang-2100.3.34.2), compilou void soma(long *p, long x) { *p += x; } com -O2 -fomit-frame-pointer para x86-64 e para AArch64. Para x86-64, a saída foi:

_soma:
	add	qword ptr [rdi], rsi
	ret

Para AArch64, a saída foi:

_soma:
	ldr	x8, [x0]
	add	x8, x8, x1
	str	x8, [x0]
	ret

O objdump do mesmo clang, aplicado às cinco instruções x86-64 da seção 4.2, devolveu os bytes que aparecem no quadro da figura 04. Num arquivo separado com ret e nop, o objdump mostrou c3 para o ret, com 1 byte, e 90 para o nop, também de 1 byte.

Três limites do programa valem o registro. Ele confere a codificação e o desmonte, mas não executa as instruções, então não prova o comportamento em tempo de execução. O mapeamento de registradores e o uso de a5 como temporário são escolhas do exemplo, e a convenção de chamada real pode usar outros registradores. E o capstone é uma biblioteca de desmontagem, e não o manual, por isso o campo de cada bit foi conferido na própria especificação da RISC-V, e o desmonte serve de segunda leitura.

8. Exercícios

Exercício 1. Calcule a palavra de 32 bits de sub a0, a1, a2 no formato R do RISC-V, sabendo que a0 é x10, a1 é x11 e a2 é x12, e que o funct7 da subtração é 0100000.

Resposta: a palavra é 0x40c58533. Os campos são funct7 = 0100000, rs2 = 01100 (x12), rs1 = 01011 (x11), funct3 = 000, rd = 01010 (x10) e opcode = 0110011. O programa da seção 7 produz a mesma palavra e o capstone desmonta como sub a0, a1, a2.

Exercício 2. Sem consultar a saída, diga quais bits da palavra 0x00c58533 formam o campo rd, e qual registrador ele aponta.

Resposta: rd ocupa os bits 11 a 7. Na palavra, esses bits valem 01010, que é 10 em decimal, ou seja, x10, que tem o nome ABI a0. Como o campo está no meio da palavra, a leitura exige um deslocamento de 7 bits e uma máscara de 5 bits.

Exercício 3. O x86-64 faz a soma na memória em uma instrução, e o AArch64 faz em três. Quantos acessos à memória cada um faz para *p += x, e quais instruções são esses acessos?

Resposta: os dois fazem um acesso de leitura e um de escrita no mesmo endereço, ou seja, dois acessos no total. No x86-64, os dois acessos estão dentro da própria instrução add qword ptr [rdi], rsi. No AArch64, são as instruções ldr x8, [x0] e str x8, [x0], enquanto add x8, x8, x1 só usa registradores. A diferença está em quantas instruções o programa escreve, e não no número de acessos.

Exercício 4. Por que a instrução add rax, rbx ocupa 3 bytes em x86-64 e a instrução add qword ptr [rbx+8], rax ocupa 4?

Resposta: a primeira não tem deslocamento, e a segunda acrescenta o byte 08 do deslocamento de 8 bytes. Os dois têm REX 48 e opcode 01, e o ModRM muda de D8 para 43, porque o modo de endereçamento passa a ser memória com deslocamento de 8 bits. Cada byte extra, como o do deslocamento, aumenta o tamanho.

Exercício 5 (sem resposta fechada). Escolha um programa seu que compile para uma máquina ARM e para uma x86, e compare o tamanho do binário de cada um com size ou ls -l. Diga se a diferença vem da ISA, do compilador, das bibliotecas ou da opção de otimização. Este exercício mede e não tem resposta pronta, porque depende do programa escolhido.

9. Glossário

ISA: conjunto de instruções, o contrato entre software e processador. Define instruções, registradores visíveis, modos de endereçamento, formato de codificação e modelo de memória.

Microarquitetura: a implementação de uma ISA dentro de um chip, com pipeline, cache e previsão de desvios. Duas microarquiteturas podem executar a mesma ISA com desempenho diferente.

CISC: sigla de complex instruction set computer. Família de ISAs em que uma instrução pode fazer várias operações, inclusive acessar a memória, como o x86.

RISC: sigla de reduced instruction set computer. Família de ISAs com instruções simples e acesso à memória concentrado em leitura e escrita, como o AArch64 e a base do RISC-V.

Arquitetura load-store: ISA em que só as instruções de leitura e escrita acessam a memória, e as aritméticas trabalham com registradores.

Codificação: regra que diz como os campos de uma instrução, como opcode e registradores, ocupam os bits da palavra.

Opcode: campo que identifica a operação da instrução.

Formato R: formato do RISC-V para operações entre dois registradores, com funct7, rs2, rs1, funct3, rd e opcode.

REX: prefixo de um byte do x86-64 que estende os registradores e o tamanho da operação para 64 bits.

ModRM: byte do x86 que indica o modo de endereçamento e os registradores usados pela instrução.

Micro-operação: tarefa interna que um núcleo executa depois de decodificar uma instrução. Uma instrução pode virar uma ou mais micro-operações.

Desmontador: programa que lê bytes de código de máquina e devolve as instruções em texto. Esta aula usou o capstone.

O que não foi confirmado ou medido

Os dados de codificação desta aula foram conferidos de duas formas: as palavras do RISC-V foram montadas a partir dos campos do manual e desmontadas pelo capstone, e as instruções x86-64 e AArch64 foram montadas pelo clang e lidas pelo objdump. Nenhuma instrução foi executada, e a aula não mediu desempenho, tamanho de binário nem tempo de decodificação em nenhum chip.

O RISC-V não tem compilador nesta máquina, por isso a sequência de *p += x em RISC-V é uma montagem a partir dos campos, e não a saída de um compilador. A convenção de chamada, que decidiria o ret final, não foi descrita.

O manual DDI 0487 da Arm, que é a fonte primária do AArch64, não foi alcançado: o site de documentação devolveu páginas sem conteúdo para esta consulta. A aula usou o documento técnico da Arm sobre portabilidade de 64 bits, de 2014, para os 32 bits fixos e os 31 registradores. Os detalhes de instruções de pares e de tudo o que mudou depois de 2014 não foram conferidos.

O SDM da Intel usado é a edição combinada de setembro de 2026. A aula conferiu o limite de 15 bytes, a codificação de ADD e a contagem de 16 registradores de uso geral nessa edição. A descrição de micro-operações citada é a da microarquitetura P6, e a aula não conferiu em documento primário como os processadores x86 atuais dividem cada instrução.

A afirmação de que o chip da Apple linha M e os processadores Intel e AMD implementam, respectivamente, AArch64 e x86-64 é conhecida, mas não foi conferida em documento de cada fabricante nesta consulta. A afirmação de que a maioria dos smartphones usa AArch64 e as regras de licenciamento de x86 e de AArch64 também não foram conferidas em fonte nesta consulta.

Os números de 25% a 30% de redução de código e a comparação com a 80x86 vêm do manual do RISC-V, e são afirmações do texto de origem. Não foram reproduzidos nesta aula.

A aula não verificou se existe, e como funciona, um tradutor de instruções entre famílias em sistemas comerciais. A figura 01 mostra apenas que o chip não executa diretamente os bytes de outra ISA.

As datas de história citadas, como 1964 para a System/360 e os anos 1980 para as primeiras ISAs RISC, vêm do manual do RISC-V, que cita as fontes originais. A aula não consultou esses artigos originais.

Fontes

Consultadas em 1 de novembro de 2026.

  • Manual oficial do RISC-V, The RISC-V Instruction Set Manual, Volume I: Unprivileged Architecture, versão 20261030 (release riscv-isa-release-8fb3965-2026-10-30): seção 1.5 sobre o comprimento das instruções base, seção 2.1.1 sobre os 32 registradores e x0 fixo em zero, seção 2.1.4.2 sobre o formato R, seção 7.1 sobre a extensão Zca e o apêndice histórico sobre a compressão de código. Licença Creative Commons Attribution 4.0, citada na página de créditos. Arquivo consultado: https://github.com/riscv/riscv-isa-manual/releases/download/riscv-isa-release-8fb3965-2026-10-30/riscv-spec.pdf
  • Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Combined Volumes (ordem 325462-093US, edição de setembro de 2026): volume 1, seção 2.2.2.1 sobre o front end que decodifica em micro-operações (página 2-9), seção 3.4.1.1 sobre os 16 registradores de uso geral em 64 bits (página 3-11); volume 2A, seção 2.3.11 sobre o limite de 15 bytes (página 2-20) e a tabela de ADD, com REX.W + 01 /r para ADD r/m64, r64 (página 3-14). Página de download: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
  • Arm, Porting to 64-bit ARM, de Chris Shore, julho de 2014: seção “Overview of AArch64 in ARMv8-A” para os 32 bits fixos e os 31 registradores, e a seção sobre o conjunto de instruções para as instruções de pares. Arquivo consultado: https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-21-42/Porting-to-ARM-64_2D00_bit-whitepaper.pdf
  • Página de documentação da Arm para a arquitetura A64, consultada sem sucesso para o manual DDI 0487: https://developer.arm.com/documentation/ddi0487/latest (a página não entregou o conteúdo do manual nesta consulta).
  • Ferramentas usadas na verificação: Apple clang 21.0.0 (clang-2100.3.34.2), objdump da LLVM do macOS, capstone 5.0.7 em ambiente virtual, e Python 3.12.8 no modo isolado.

Relacionados

  • Trilha de Hardware
  • ULA e registradores
  • Ciclo de instrução e unidade de controle
  • Assembly na prática
  • CPU
  • Como a CPU executa instruções
  • Gerações e arquiteturas
  • Pipeline, cache e paralelismo

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?