Pular para o conteúdo

Hardware e sistemas operacionais · parte 13 de 27

Ciclo de instrução e unidade de controle: busca, decodificação e execução

Como a CPU busca, decodifica e executa uma instrução, o que o contador de programa faz e como a unidade de controle liga os sinais em cada etapa.

intermediárioPublicada em · 39 min de leitura

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

Ciclo de instrução e unidade de controle: busca, decodificação e execução

Pense numa cozinha de restaurante em horário de pico. Chega um papelzinho com o número do próximo pedido, o chef lê o número, pega a receita certa e avisa cada bancada do que fazer. A bancada da chapa não sabe de nada além do que o chef falou, e o chef não cozinha nada com as próprias mãos. Terminado o prato, o próximo papelzinho é o próximo número da fila. Na CPU, o papelzinho é o contador de programa, o PC, que guarda o endereço da instrução a executar. O chef é a unidade de controle, que lê a instrução e decide qual peça entra em ação. As bancadas são a ULA, os registradores e a memória. A analogia tem um limite que vale guardar: o chef da cozinha improvisa quando falta um ingrediente, e a unidade de controle só segue a tabela que o projetista desenhou para ela.

Esta aula responde a uma pergunta: como a CPU busca, decodifica e executa uma instrução, e o que faz o contador de programa? A aula ULA e registradores mostrou como a ULA soma e como os registradores guardam operandos, e a aula ISA, CISC e RISC mostrou como uma instrução vira uma palavra de 32 bits. Aqui essa palavra ganha movimento. Ela sai da memória, é separada em campos e cada campo liga uma parte do caminho. A unidade de controle, que é o assunto central, é uma máquina de estados, o mesmo tipo de circuito de Máquinas de estados finitos. Como a instrução é escrita em texto e como um laço de C vira essa sequência fica para Assembly na prática, e o que acontece quando a função chama outra função fica para Pilha e chamada de função.

Um termo aparece antes da seção em que é explicado. IR é o registrador de instrução, o lugar onde a palavra buscada fica guardada enquanto a unidade de controle a decodifica. A definição completa está na seção 4.2.

Mapa da aula

SeçãoPergunta que responde
1. O que é o ciclo de instruçãoQuais peças participam e qual é o papel de cada uma?
2. Para que servePor que a CPU precisa de um contador e de uma unidade separada da ULA?
3. Exemplos em contextos diferentesOnde essa sequência de buscar, decidir e avançar aparece fora da CPU?
4. Como funcionaComo o PC avança, como a palavra é buscada, decodificada e executada, e como a unidade de controle vira uma máquina de estados?
5. Detalhes e bordasQual a diferença entre um ciclo longo e várias etapas curtas, e o que acontece quando um desvio dá errado?
6. Erros comuns e boas práticasQuais enganos aparecem quando alguém lê um rastro de execução pela primeira vez?
7. O programa que simulaComo um simulador pequeno de CPU confirma cada passo, com a saída real?
8. ExercíciosComo praticar, com as respostas?
9. GlossárioQuais termos a aula usa?

1. O que é o ciclo de instrução

A CPU executa um programa repetindo sempre a mesma sequência: pegar a instrução num endereço da memória, entender o que ela pede, fazer o pedido e escolher a instrução seguinte. Essa repetição se chama ciclo de instrução, e cada volta dele executa uma instrução do programa. Quatro grupos de peças participam dela. O contador de programa aponta o endereço. A memória entrega a palavra que está nesse endereço. A unidade de controle lê a palavra e decide o que cada peça deve fazer naquele passo. E o caminho de dados, formado pela ULA e pelos registradores, faz a conta ou a cópia pedida.

A figura abaixo mostra essas peças e as ligações entre elas. A seta verde é o caminho dos dados, e a seta laranja é o caminho do controle. O caminho dos dados carrega valores, e o caminho de controle decide o que esses valores fazem. A distinção parece pequena, mas é ela que permite ler qualquer diagrama de CPU sem se perder.

Unidade de controle, em laranja, lê os campos da instrução e liga os sinais, enquanto PC, memória, registradores e ULA executam a palavra buscada
figura 1 · Visão geral da CPU: a unidade de controle lê a instrução e liga os sinais, e o c

Siga a figura pelas setas verdes. O PC envia um endereço à memória, a memória devolve a instrução para o IR, e o IR entrega os campos à unidade de controle. Dali saem as setas laranjas tracejadas, que ligam os sinais aos registradores e à ULA. A seta de dados entre registradores e ULA carrega os operandos e volta com o resultado. A figura não desenha o ciclo como uma roda, mas a sequência de setas já mostra a ordem: primeiro a busca, depois a leitura dos campos, depois a execução.

A unidade de controle é o circuito que transforma os campos de uma instrução em sinais elétricos. Um sinal diz se o registrador de destino deve ser escrito, outro diz se a ULA deve somar ou comparar, outro diz se a memória deve ser lida ou escrita, e outro diz se o PC deve receber um valor diferente do próximo em sequência. Ela não guarda resultados de contas. Ela guarda apenas o estado em que está dentro do ciclo, e esse detalhe leva à seção 4.5.

2. Para que serve

Sem um contador, a CPU não saberia qual instrução vem depois da atual. Ela executaria a mesma palavra para sempre, ou pararia no fim da primeira. O PC resolve isso guardando um endereço que avança sozinho, e que muda só quando uma instrução manda mudar. Esse é o contrato básico do modelo de programa armazenado: o programa é uma lista de palavras na memória, e a mesma máquina, com o mesmo ciclo, segue qualquer lista que você colocar ali.

A unidade de controle existe por outro motivo. A ULA sabe somar, e os registradores sabem guardar. Nenhum dos dois sabe quando deve somar, de onde pegar o segundo operando, nem onde escrever o resultado. Alguém precisa ler a instrução e converter a informação em decisões, e fazer isso por uma tabela fixa é mais barato que uma máquina separada para cada programa. Quando a instrução mudou de tipo, a unidade de controle mudou de passo, e o hardware continuou o mesmo.

Esse é o ponto que liga a aula à máquina de estados da fase 2. A unidade de controle não toma a mesma decisão em todo instante: ela depende da etapa em que está e da instrução que tem em mãos. Um circuito que decide com base no estado atual e na entrada é exatamente o que Máquinas de estados finitos descreve. A diferença é que, aqui, o número de estados é pequeno e cada um tem um nome que o leitor pode ler no simulador da seção 7.

3. Exemplos em contextos diferentes

O primeiro exemplo é um livro de receitas com marcador de página. Você lê o passo atual, faz o que ele pede e passa o marcador para a próxima linha. Quando o livro diz “volte ao passo 2 enquanto a massa não dobrar”, o marcador volta para trás, e isso é um desvio. O marcador é o PC, e a regra escrita no livro é a instrução. A analogia funciona bem para o avanço e para o retorno, mas falha num ponto: o livro não tem uma etapa de leitura separada da de execução, e a CPU tem.

O segundo exemplo é um manual de montagem de um móvel. Os passos são numerados, e o passo 5 pode dizer “se faltar parafuso, vá ao passo 9”. Quem monta não decide sozinho para onde ir, a decisão está escrita no próprio passo, e só a condição muda de uma vez para outra. Essa é a lógica de um desvio condicional: a instrução traz o alvo e a condição, e o caminho depende do que acontece naquele momento.

O terceiro exemplo é um GPS. Enquanto a rota pede “siga em frente”, cada trecho segue o anterior. Quando a rota diz “vire à direita no retorno”, o próximo ponto deixa de ser o seguinte na lista. O GPS é um bom modelo para o PC mais 4, que é o trecho sem curva, e para o alvo do desvio, que é a curva. Ele falha ao explicar quem decide a curva, porque o GPS não compara números, e a CPU compara.

O quarto exemplo é um laço de contagem regressiva, de exatamente o tipo que o programa da seção 7 executa. O corpo do laço soma, diminui o contador e pergunta se ainda não chegou a zero. Quando a resposta é não, o caminho volta ao começo do laço. Repare que a pergunta “ainda não chegou a zero?” é respondida pela unidade de controle a partir de um valor que a ULA acabou de calcular, e não a partir de um ponteiro qualquer.

4. Como funciona

Nesta seção o ciclo é desmontado peça por peça. O caminho começa no contador de programa, passa pela busca e pela decodificação, chega à execução e termina na escrita do resultado. A unidade de controle aparece em cada etapa, e no fim a seção mostra por que ela pode ser lida como uma máquina de estados.

4.1 O contador de programa

O contador de programa, chamado PC nesta aula, é um registrador de 32 bits no RV32I. Ele guarda o endereço da instrução atual, como o manual do RISC-V define no capítulo sobre o modelo de programação da base de inteiros. Nada na memória indica qual instrução vem a seguir: essa informação vive no PC e nas regras de avanço. A convenção de nome importa, e a seção 6 volta a ela, porque o PC guarda o endereço da instrução que está sendo tratada, mas no simulador ele já aparece avançado durante a busca.

Em um caso comum, o PC avança 4 bytes a cada instrução. O número vem da aula ISA, CISC e RISC: no RV32I cada instrução ocupa 32 bits, e o manual exige que as instruções fiquem alinhadas em múltiplos de 4 bytes. Por isso, em vez de somar 1, o contador soma 4, e o endereço da instrução seguinte é sempre PC mais 4. Um desvio muda essa regra. Quando o desvio é tomado, o novo endereço é o do próprio desvio somado a um deslocamento com sinal que a instrução carrega, medido em bytes.

A figura abaixo mostra o circuito que escolhe o próximo PC. Dois somadores calculam os dois candidatos, PC mais 4 e PC mais o deslocamento do desvio, e um seletor escolhe um deles. A unidade de controle controla o seletor, e o resultado volta ao PC.

O seletor de PC recebe PC mais 4 ou o alvo do desvio, escolhido pela unidade de controle, e devolve o novo valor ao PC a cada passo
figura 2 · O contador de programa: o seletor escolhe entre PC mais 4 e o alvo do desvio, e

Leia a figura da esquerda para a direita. O PC, à esquerda, alimenta os dois somadores, que aparecem em cima e embaixo. A linha de cima, marcada como não tomado, leva o PC mais 4 ao seletor. A linha de baixo, marcada como tomado, leva o alvo do desvio. A seta laranja vinda da unidade de controle, embaixo à direita, é a decisão que escolhe entre as duas. A linha reta de volta, com o rótulo novo PC, fecha o laço: o valor escolhido entra no PC e começa o próximo passo. O diagrama não mostra a memória, para não cruzar as setas, e a seção 4.2 explica a ligação entre o PC e a memória.

4.2 A busca

A busca é o primeiro passo do ciclo, e ela tem três ações. O endereço guardado no PC vai para a memória, a memória devolve a palavra de 32 bits que está ali, e essa palavra é guardada num registrador chamado IR, de registrador de instrução. Ao mesmo tempo, o PC avança. O manual do RISC-V descreve essa leitura de forma precisa: para cada instrução executada, há uma leitura implícita da memória, chamada busca de instrução, que obtém a palavra a ser executada. Nem toda instrução faz outro acesso à memória depois, mas toda instrução faz esse.

Por que a busca é tratada como etapa própria, e não como parte da execução? Porque ela é sempre a mesma, qualquer que seja a instrução. Não importa se a palavra é uma soma, um desvio ou um acesso à memória de dados: antes de saber o que ela faz, a CPU precisa lê-la. Isso faz da busca o gargalo do ciclo em muitos projetos, e é uma das razões para a hierarquia de memória e as caches das fases 4 e 5 existirem.

Na simulação da seção 7, a linha 0x00 mostra a busca de uma instrução simples. O PC começa em 0x00, o IR recebe a palavra 0x00300293 e o PC passa a valer 0x04. Esse valor é o endereço da instrução seguinte, e ele aparece antes de a instrução ser executada, porque a busca já avançou o contador.

4.3 Decodificação e unidade de controle

Decodificar é separar a palavra do IR em campos e descobrir o que cada campo significa. A palavra de 32 bits de uma instrução do RV32I tem campos fixos: o opcode, de 7 bits, diz o formato e a família da instrução. O funct3 e o funct7 refinam a operação dentro da família. O rd é o registrador de destino, o rs1 e o rs2 são os registradores de origem, e o imediato é um número codificado no meio da palavra. A aula ISA, CISC e RISC mostrou onde cada campo fica no formato R, e a figura abaixo aplica o mesmo mapa a uma instrução real do simulador.

A instrução é add t1, t1, t0, que soma o registrador t0 ao registrador t1 e grava o resultado de volta em t1. Os nomes t0, t1 e t2 são os nomes de ABI dos registradores x5, x6 e x7, e o capstone desmonta a palavra com esses nomes. A palavra é 0x00530333. Os campos ficam assim: funct7 vale 0000000, rs2 vale 00101 (t0), rs1 vale 00110 (t1), funct3 vale 000, rd vale 00110 (t1), e o opcode vale 0110011.

A palavra 0x00530333 separa em funct7, rs2, rs1, funct3, rd e opcode, e opcode e funct3 consultam a tabela que liga os sinais do add
figura 3 · Decodificar add t1, t1, t0: os campos da palavra 0x00530333 viram endereços de r

Leia a figura da esquerda para a direita, na linha de cima. Os seis campos da palavra aparecem em caixas, com o valor binário e, quando é um registrador, o nome. Os campos azuis são endereços de registrador e vão para o banco, onde as setas verdes mostram a leitura de t1 e t0. Os campos roxos dizem o tipo da operação. O opcode e o funct3 seguem para a tabela de controle, no centro, e dali sai a linha de sinais embaixo, que é a decisão tomada para o add.

A tabela de controle é o núcleo da unidade de controle. Ela é uma função: dada a combinação de opcode e funct3 (e, quando preciso, funct7), devolve o valor de cada sinal. No projeto real essa função é implementada com portas lógicas fixas, e a seção 5.2 mostra a alternativa de microcódigo. No simulador, a tabela está escrita como um dicionário, uma linha por instrução, e cada coluna é um sinal. O quadro abaixo lista os sinais das cinco instruções que o programa usa, com os mesmos valores do código.

InstruçãoRegWriteALUSrcMemReadMemWriteMemToRegBranchALUOpEstados
addi110000somaIF, ID, EX, WB
add100000somaIF, ID, EX, WB
lw111010somaIF, ID, EX, MEM, WB
sw010100somaIF, ID, EX, MEM
bne000001comparaIF, ID, EX

Cada sinal tem uma função. RegWrite liga a escrita no registrador de destino. ALUSrc escolhe se a segunda entrada da ULA vem de um registrador ou do imediato. MemRead e MemWrite habilitam a leitura e a escrita da memória de dados. MemToReg escolhe se o valor gravado no registrador vem da ULA ou da memória. Branch diz que a instrução é um desvio condicional, e ALUOp diz a operação da ULA. Repare que o sw e o bne têm RegWrite igual a 0: eles não gravam nenhum registrador. O sw grava na memória, e o bne só decide o PC.

4.4 Execução, memória e escrita

Depois da decodificação, a instrução entra na execução, que é a etapa em que a ULA calcula. Para um add, a ULA soma os dois registradores. Para um addi, soma um registrador e o imediato. Para um lw ou um sw, a ULA calcula o endereço de memória somando o registrador base ao deslocamento. Para um bne, a ULA compara os dois registradores, e esse resultado decide se o PC muda.

A etapa de memória só existe para lw e sw. O lw lê a palavra do endereço calculado e a coloca num registrador temporário, que o simulador chama de MDR, o registrador de dados da memória. O sw faz o contrário: escreve o valor de um registrador no endereço calculado. A instrução add não passa por essa etapa, e isso explica por que os tipos têm números de estados diferentes, como a seção 4.5 mostra.

A última etapa é a escrita, em que o resultado vai para o registrador de destino. O sinal MemToReg escolhe a origem: o valor da ULA, para uma soma, ou o valor lido da memória, para o lw. Um detalhe que costuma passar despercebido é o registrador x0 (zero): a escrita nele é ignorada, e o manual diz isso explicitamente. O simulador respeita essa regra, por isso o programa nunca altera zero.

A figura abaixo acompanha um lw, que é o caso mais longo, em cinco estados. A linha do tempo mostra qual componente entra em ação em cada passo, e a ordem das mensagens é a ordem da execução.

Linha do tempo do lw t2, 64(zero): a unidade de controle chama PC, memória, registradores e ULA em cada um dos cinco estados até gravar 6 em t2
figura 4 · lw t2, 64(zero) em cinco estados: a unidade de controle chama cada componente na

Leia a figura de cima para baixo. A unidade de controle começa pedindo ao PC que aponte para 0x18, o endereço do lw, e o PC lê a palavra na memória. A memória devolve a instrução, que a unidade de controle recebe como IR. Na decodificação, os registradores entregam o valor de rs1, que é zero, e a ULA recebe A igual a zero e o imediato 64. A ULA calcula o endereço 0x40, a memória devolve o valor 6, que chega ao MDR, e a última seta grava 6 em t2. As setas tracejadas são retornos de dados, e as cheias são pedidos.

Os desvios seguem o mesmo caminho até a execução, mas não passam pela memória de dados. O bne compara t0 com zero, e o resultado decide o PC. A figura abaixo mostra as duas saídas possíveis, com o valor de t0 que leva a cada uma.

Decisão do bne: se t0 é diferente de zero, o PC recebe 0x08 e o laço continua; se é zero, o PC recebe 0x14 e o programa segue
figura 5 · bne decide o próximo PC: tomado volta ao início do laço, não tomado segue para a

Siga o losango, que é a decisão. Entrando pela esquerda, o bne em 0x10 pergunta se t0 é diferente de zero. Quando a resposta é sim, o caminho de cima leva o PC a 0x08, o início do laço, e isso é o desvio tomado. Quando a resposta é não, o caminho de baixo leva o PC a 0x14, a instrução depois do laço, e isso é o desvio não tomado. Na simulação, o primeiro bne é tomado com t0 valendo 2, e o último é não tomado com t0 valendo 0.

4.5 A unidade de controle é uma máquina de estados

Os estados da unidade de controle são IF, ID, EX, MEM e WB, sigla de busca, decodificação, execução, memória e escrita, em inglês. A instrução atual está em um desses estados, e a unidade de controle decide o próximo com base no estado e no tipo da instrução. Essa é a definição de máquina de estados que a aula Máquinas de estados finitos usa, e a diferença para um semáforo é que a entrada aqui é a instrução, e não um temporizador.

A figura abaixo desenha o percurso que cada tipo de instrução faz. O estado inicial é a busca. Depois vem a decodificação e a execução. A partir da execução, o percurso depende do tipo: uma soma vai direto à escrita, um lw passa pela memória e depois pela escrita, e um sw passa só pela memória. O bne termina na execução, porque a decisão do PC já foi tomada ali.

Máquina de estados da unidade de controle: IF, ID e EX em sequência, depois MEM ou WB conforme a instrução, com o estado atual destacado
figura 6 · A unidade de controle como máquina de estados: cada instrução percorre IF, ID e

Leia a figura da esquerda para a direita. A bolinha à esquerda é o estado inicial, e as setas seguem o percurso até a execução. Depois da execução, a seta de cima leva ao MEM, para o lw e o sw, e a seta de baixo leva ao WB, para add, addi e bne. O MEM segue para o WB apenas no caso do lw. A figura não desenha as setas que voltam de WB, MEM e EX ao estado IF, porque o retorno é igual para todas as instruções e embolaria o desenho. Toda instrução termina e a busca da próxima começa de novo.

Os números de estados são a consequência direta do desenho. Um add percorre quatro estados, um lw percorre cinco, um sw percorre quatro e um bne percorre três. Como o simulador conta cada estado como um ciclo do relógio, os números do programa da seção 7 saem desse percurso, e a seção 5.1 usa esses números para comparar modelos.

4.6 Como ler o rastro do programa

Um rastro é a lista de instruções que a CPU executou, na ordem em que executou, com o PC de cada uma e o efeito. O programa da seção 7 produz um rastro de treze instruções. A leitura é mais fácil se você seguir uma iteração do laço. O PC passa de 0x08 para 0x0c, depois para 0x10, e o bne decide se volta para 0x08. Essa sequência se repete três vezes, e na última o bne não é tomado, e o PC segue para 0x14.

O rastro também mostra a relação entre PC e alvo. O bne em 0x10 tem deslocamento -8, e o alvo dá 0x08. O simulador calcula esse alvo a partir do endereço do próprio bne, e não a partir do PC já avançado para 0x14. A seção 6 mostra o erro que aparece quando essa diferença é esquecida.

Por fim, o rastro confirma o resultado. A soma 3 mais 2 mais 1 dá 6, e o programa grava 6 na memória e o lê de volta em t2. Ao final, t1, t2 e a memória valem 6, e t0 vale zero, porque o contador chegou ao fim. A seção 7 mostra a linha que faz essa conferência.

5. Detalhes e bordas

5.1 Um ciclo longo ou várias etapas curtas

Existem dois modos clássicos de organizar o ciclo. No primeiro, uma instrução ocupa um único ciclo longo, e o relógio precisa ser lento o bastante para que a instrução mais demorada termine dentro dele. No segundo, que é o do simulador, cada instrução passa por várias etapas curtas, e o número de etapas varia conforme o tipo. O segundo modo deixa o relógio rápido, porque cada etapa é curta, mas gasta mais ciclos por instrução, como a figura abaixo mostra.

Barras com os ciclos gastos por addi, add, bne, sw e lw no modelo multiciclo, contados pelo simulador: de 3 a 5 ciclos por instrução
figura 7 · Ciclos gastos por instrução no modelo multiciclo deste capítulo, contados pelo s

As barras vêm da contagem do simulador, e não de medição em chip. O lw é o mais longo, com cinco ciclos, porque passa pela memória de dados e pela escrita. O bne é o mais curto, com três, porque não grava nada. Num modelo de ciclo único, o relógio teria de caber o lw inteiro para toda instrução, e o add, que cabe em muito menos tempo, ficaria com a mesma duração e desperdiçaria o restante do ciclo. O modelo multiciclo evita esse desperdício ao custo de uma unidade de controle mais complexa.

Os números do modelo mudam conforme o tamanho do programa. No laço de dez iterações da seção 8, a execução gasta 127 ciclos, contra 50 do programa original. O custo cresce com a quantidade de instruções executadas, e não com o tamanho do código escrito. Essa diferença é a base da equação de desempenho que a subnota Como a CPU executa instruções apresenta em outro contexto.

5.2 Fiação fixa ou microcódigo

A tabela de controle pode ser implementada de duas formas. Na primeira, chamada de lógica fixa, cada sinal é uma função booleana dos campos da instrução, e essa função vira um conjunto de portas ligadas uma vez por todas. A decodificação é rápida, porque não há nada a ler de memória, mas mudar uma instrução exige redesenhar o circuito. Na segunda forma, o microcódigo, a unidade de controle contém uma memória de leitura, a ROM, onde cada linha é uma micro-operação, e um micro-PC percorre essas linhas para gerar os sinais de cada instrução.

A figura abaixo coloca as duas formas lado a lado, com as mesmas funções de saída.

Duas formas de gerar sinais de controle: decodificador de portas fixas que vai direto aos sinais, ou micro-PC que lê uma linha de ROM por passo
figura 8 · Duas formas de gerar os sinais de controle: portas fixas ligadas à mão, ou uma R

Leia cada grupo de cima para baixo. No grupo da esquerda, o decodificador recebe o opcode e entrega os sinais sem nenhuma memória no caminho. No grupo da direita, o micro-PC aponta uma linha da ROM, a linha é lida e entrega os sinais. A diferença está no meio: portas fixas de um lado, uma memória percorrida pelo micro-PC do outro. Quem já viu o PC sabe a lógica do micro-PC, porque também é um contador que avança e pode saltar.

Essa escolha tem consequências práticas. Microcódigo facilita corrigir e estender o conjunto de instruções, porque basta reescrever a ROM sem redesenhar as portas. A subnota Como a CPU executa instruções afirma que processadores x86 atuais ainda usam microcódigo para instruções complexas. Esta aula não conferiu essa afirmação em documento primário, e a trata como não confirmada.

5.3 Quando o desvio dá errado: exceções

Um desvio pode falhar mesmo quando a decisão está correta. A regra do RV32I é que o alvo de um desvio tomado precisa estar alinhado em 4 bytes. Se não estiver, o manual gera uma exceção de endereço de instrução desalinhado, chamada instruction-address-misaligned, e ela é reportada na própria instrução de desvio, e não na instrução de destino. Um desvio não tomado não gera a exceção, mesmo que o alvo seja inválido, porque o alvo nunca é usado.

A figura abaixo mostra esse caminho com um bne cujo alvo é 0x0a, que não é múltiplo de 4.

Bne tomado com alvo 0x0a, que não é múltiplo de 4: a ULA decide tomar o desvio, e a exceção de endereço desalinhado é reportada no próprio bne
figura 9 · Desvio com alvo desalinhado: o bne tomado gera exceção de endereço desalinhado,

Siga a figura da esquerda para a direita. O bne está em 0x04, e o valor de t0 é 1, que é diferente de zero. A decisão do losango é sim, e o alvo calculado é 0x0a. Esse alvo é barrado, com o sinal de X, porque não é múltiplo de 4, e a exceção aparece à direita com o PC sem mudança. A parte de baixo mostra o caso não tomado, em que o PC segue para 0x08 sem exceção. O simulador da seção 7 produz as duas situações e confirma esse comportamento.

O programa da seção 7 confirma as duas situações. Com o bne tomado e alvo 0x0a, a execução para com a mensagem de exceção. Com o bne não tomado e o mesmo alvo, a execução segue até o fim e t2 recebe o valor 7. Os códigos de causa das exceções ficam no volume privilegiado do manual, e a aula Assembly na prática volta a esse tipo de erro quando o programa é escrito em C.

5.4 Comparar sem flags

A aula ULA e registradores mostrou que a comparação pode ser uma subtração que guarda flags, como Z, N, C e V. O RISC-V não usa esse caminho. O manual explica que os desvios condicionais comparam diretamente dois registradores, em vez de usar códigos de condição, como fazem x86, ARM e SPARC. A justificativa do próprio manual é que uma instrução de comparação e desvio combinada cabe num pipeline regular, não precisa de estado extra e reduz o tráfego de busca de instruções.

Na prática isso muda a tabela de controle. Em vez de ler uma flag da última operação, a unidade de controle liga o comparador entre os dois registradores e recebe o resultado no mesmo passo do desvio. O bne do simulador faz exatamente isso, e a figura da seção 4.4 mostra a decisão. Ao ler código de outra arquitetura, a primeira pergunta continua sendo qual convenção ela segue.

5.5 O que o modelo deixa de fora

O simulador desta aula não tem pipeline, previsão de desvios, cache nem memória separada para instruções e dados. Ele executa uma instrução de cada vez, e cada estado ocupa um ciclo. Um processador real sobrepõe os estados de instruções diferentes, para que enquanto uma instrução executa outra já está sendo buscada. Essa sobreposição é o que a aula Pipeline, cache e paralelismo descreve, e o desvio é justamente o ponto em que ela fica mais difícil, porque a busca precisa adivinhar o alvo antes de a decisão existir.

6. Erros comuns e boas práticas

O primeiro erro é achar que o PC aponta para a instrução que acabou de ser executada, ou para a próxima, sem perceber que a convenção muda de texto para texto. O manual do RISC-V diz que o pc guarda o endereço da instrução atual. No simulador, porém, o registrador já avançou para o endereço seguinte durante a busca, e por isso, enquanto a instrução do endereço 0x08 executa, o registrador já aponta para 0x0c. Quem lê o rastro sem essa distinção conclui que o programa pulou uma instrução. A boa prática é escrever, em cada texto, qual endereço o registrador representa naquele momento.

O segundo erro é calcular o alvo do desvio a partir do PC já avançado. O bne em 0x10 com deslocamento -8 tem alvo 0x08. Quem soma o deslocamento ao 0x14, que é o PC depois da busca, obtém 0x0c, que é outra instrução do laço e produz um programa que roda, mas faz a conta errada. O manual define o alvo como o endereço do próprio desvio somado ao deslocamento, e essa é a regra que o simulador segue.

O terceiro erro é tratar um desvio com alvo desalinhado como se fosse legal, desde que o encoding aceite o valor. O deslocamento de um bne precisa ser múltiplo de 2, e o valor 6 é aceito na codificação, mas o alvo 0x0a que ele produz não é múltiplo de 4. A exceção só aparece quando o desvio é tomado. Por isso o programa de teste tem os dois casos, e a boa prática é testar o caso tomado e o não tomado separadamente.

O quarto erro é confundir a ULA com a unidade de controle. A ULA calcula, e a unidade de controle decide o que a ULA deve calcular e quando. Um estudante que procura a soma dentro da unidade de controle não encontra nada, porque ela só entrega sinais. Essa confusão se desfaz quando se pergunta, para cada parte, se ela produz um valor ou uma decisão.

O quinto erro é achar que todo if vira um desvio tomado. Um desvio não tomado simplesmente deixa o PC seguir em sequência, e a maior parte das instruções de desvio de um programa normal cai nesse caso. Ao ler um rastro, a pergunta útil é se o desvio tomou o caminho, e não se ele existe.

A boa prática geral é ler um rastro real antes de confiar num diagrama. Os diagramas desta aula simplificam, e o rastro mostra o que de fato acontece, linha por linha. Quando o rastro e o diagrama não batem, o problema costuma estar na leitura do diagrama.

7. O programa que simula

O programa abaixo simula uma CPU multiciclo com um subconjunto do RV32I: addi, add, lw, sw e bne. A simulação reproduz o ciclo de instrução com os estados IF, ID, EX, MEM e WB, e a tabela de controle da seção 4.3 vem escrita como um dicionário. A codificação dos campos segue o manual oficial do RISC-V, e os bytes são conferidos com o capstone, um desmontador independente. O programa é executado em duas partes: primeiro o laço que soma 3, 2 e 1, depois um caso de desvio com alvo desalinhado.

O programa foi executado com Python 3 e o capstone 5.0.7 instalado, no modo isolado com o comando python3 -I sim.py. A saída abaixo é a real, copiada sem edição.

# Simulador didático de uma CPU multiciclo com um subconjunto do RV32I.
# Cada instrução passa pelos estados IF, ID, EX, MEM e WB, e a unidade de controle
# escolhe os sinais de cada estado a partir do tipo da instrução (tabela de controle).
# Codificação dos campos conforme o manual oficial do RISC-V (versão 20261030).
# Os bytes são conferidos com o capstone, que é um desmontador independente.

from capstone import Cs, CS_ARCH_RISCV, CS_MODE_RISCV32

ABI = {0: "zero", 5: "t0", 6: "t1", 7: "t2"}


def enc_i(imm, rs1, f3, rd, op):
    return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op


def enc_s(imm, rs2, rs1, f3, op):
    imm &= 0xFFF
    return ((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | ((imm & 0x1F) << 7) | op


def enc_r(f7, rs2, rs1, f3, rd, op):
    return (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op


def enc_b(imm, rs2, rs1, f3, op):
    imm &= 0x1FFF
    b12 = (imm >> 12) & 1
    b11 = (imm >> 11) & 1
    b10_5 = (imm >> 5) & 0x3F
    b4_1 = (imm >> 1) & 0xF
    return (b12 << 31) | (b10_5 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (b4_1 << 8) | (b11 << 7) | op


def sext(valor, bits):
    return valor - (1 << bits) if valor & (1 << (bits - 1)) else valor


def decodifica(w):
    op = w & 0x7F
    rd = (w >> 7) & 0x1F
    f3 = (w >> 12) & 0x7
    rs1 = (w >> 15) & 0x1F
    rs2 = (w >> 20) & 0x1F
    f7 = (w >> 25) & 0x7F
    if op == 0b0010011 and f3 == 0b000:
        return "addi", dict(rd=rd, rs1=rs1, imm=sext(w >> 20, 12))
    if op == 0b0110011 and f3 == 0b000 and f7 == 0:
        return "add", dict(rd=rd, rs1=rs1, rs2=rs2)
    if op == 0b0000011 and f3 == 0b010:
        return "lw", dict(rd=rd, rs1=rs1, imm=sext(w >> 20, 12))
    if op == 0b0100011 and f3 == 0b010:
        imm = ((w >> 25) << 5) | ((w >> 7) & 0x1F)
        return "sw", dict(rs1=rs1, rs2=rs2, imm=sext(imm, 12))
    if op == 0b1100011 and f3 == 0b001:
        imm = ((w >> 31) << 12) | (((w >> 7) & 1) << 11) | (((w >> 25) & 0x3F) << 5) | (((w >> 8) & 0xF) << 1)
        return "bne", dict(rs1=rs1, rs2=rs2, imm=sext(imm, 13))
    raise ValueError(f"instrução desconhecida: {w:#010x}")


# Tabela de controle: para cada instrução, o valor de cada sinal.
# É a parte da unidade de controle que a decodificação consulta.
CONTROLE = {
    "addi": dict(RegWrite=1, ALUSrc=1, ALUOp="soma", MemRead=0, MemWrite=0, MemToReg=0, Branch=0),
    "add":  dict(RegWrite=1, ALUSrc=0, ALUOp="soma", MemRead=0, MemWrite=0, MemToReg=0, Branch=0),
    "lw":   dict(RegWrite=1, ALUSrc=1, ALUOp="soma", MemRead=1, MemWrite=0, MemToReg=1, Branch=0),
    "sw":   dict(RegWrite=0, ALUSrc=1, ALUOp="soma", MemRead=0, MemWrite=1, MemToReg=0, Branch=0),
    "bne":  dict(RegWrite=0, ALUSrc=0, ALUOp="compara", MemRead=0, MemWrite=0, MemToReg=0, Branch=1),
}

# Estados de cada tipo de instrução. A unidade de controle percorre essa lista.
ESTADOS = {
    "addi": ["IF", "ID", "EX", "WB"],
    "add":  ["IF", "ID", "EX", "WB"],
    "lw":   ["IF", "ID", "EX", "MEM", "WB"],
    "sw":   ["IF", "ID", "EX", "MEM"],
    "bne":  ["IF", "ID", "EX"],
}


class Excecao(Exception):
    pass


class CPU:
    def __init__(self, programa, memoria_extra=None):
        self.pc = programa[0][0]
        self.regs = [0] * 32
        self.mem = {}
        for addr, palavra in programa:
            self.mem[addr] = palavra
        if memoria_extra:
            self.mem.update(memoria_extra)
        self.ciclos = 0
        self.contagem = {}

    def ler_reg(self, r):
        return 0 if r == 0 else self.regs[r]

    def escreve_reg(self, r, v):
        if r != 0:
            self.regs[r] = v & 0xFFFFFFFF

    def executa_uma(self, log):
        pc = self.pc
        if pc % 4:
            raise Excecao(f"instruction-address-misaligned (mcause 0) em pc={pc:#04x}")
        w = self.mem[pc]
        nome, c = decodifica(w)
        sinais = CONTROLE[nome]
        estados = ESTADOS[nome]
        efeitos = []

        # IF: o PC vai para o barramento de endereços, a memória devolve a palavra e o PC avança 4.
        ir = w
        proximo_pc = pc + 4
        # ID: os campos são separados e os registradores são lidos.
        A = self.ler_reg(c.get("rs1", 0))
        B = self.ler_reg(c.get("rs2", 0)) if "rs2" in c else 0
        alvo = pc + c["imm"] if "imm" in c and nome == "bne" else None
        # EX: a ULA calcula. Para o desvio, a comparação decide se o PC muda.
        resultado = None
        MDR = None
        if nome == "add":
            resultado = (A + B) & 0xFFFFFFFF
        elif nome == "addi":
            resultado = (A + c["imm"]) & 0xFFFFFFFF
        elif nome in ("lw", "sw"):
            endereco = (A + c["imm"]) & 0xFFFFFFFF
        elif nome == "bne":
            tomado = A != self.ler_reg(c["rs2"])
            if tomado:
                if alvo % 4:
                    raise Excecao(f"instruction-address-misaligned no bne em pc={pc:#04x}, alvo {alvo:#04x}")
                proximo_pc = alvo
            efeitos.append("desvio tomado" if tomado else "desvio não tomado")
        # MEM: só lw e sw acessam a memória de dados.
        if nome == "lw":
            MDR = self.mem.get(endereco, 0)
            efeitos.append(f"MDR <- mem[{endereco:#04x}] = {MDR}")
        elif nome == "sw":
            self.mem[endereco] = self.ler_reg(c["rs2"])
            efeitos.append(f"mem[{endereco:#04x}] <- {self.ler_reg(c['rs2'])}")
        # WB: o resultado volta para o registrador de destino, se o sinal RegWrite estiver ligado.
        if sinais["RegWrite"]:
            valor = MDR if sinais["MemToReg"] else resultado
            self.escreve_reg(c["rd"], valor)
            efeitos.append(f"{ABI[c['rd']]} <- {valor}")

        self.pc = proximo_pc
        self.ciclos += len(estados)
        self.contagem[nome] = self.contagem.get(nome, 0) + len(estados)
        if proximo_pc != pc + 4:
            efeitos.insert(0, f"PC <- {proximo_pc:#04x}")
        log.append((pc, nome, estados, efeitos, w))
        return nome


def roda(programa, memoria_extra=None, limite=200, fim=None):
    cpu = CPU(programa, memoria_extra)
    log = []
    try:
        for _ in range(limite):
            if cpu.pc not in cpu.mem or (fim is not None and cpu.pc == fim):
                break
            cpu.executa_uma(log)
    except Excecao as e:
        return cpu, log, str(e)
    return cpu, log, None


def asm_capstone(w):
    md = Cs(CS_ARCH_RISCV, CS_MODE_RISCV32)
    for ins in md.disasm(w.to_bytes(4, "little"), 0):
        return f"{ins.mnemonic} {ins.op_str}"
    return "(não desmontou)"


PROGRAMA = [
    (0x00, enc_i(3, 0, 0b000, 5, 0b0010011)),          # addi t0, zero, 3
    (0x04, enc_i(0, 0, 0b000, 6, 0b0010011)),          # addi t1, zero, 0
    (0x08, enc_r(0, 5, 6, 0b000, 6, 0b0110011)),       # add t1, t1, t0
    (0x0C, enc_i(-1, 5, 0b000, 5, 0b0010011)),         # addi t0, t0, -1
    (0x10, enc_b(-8, 0, 5, 0b001, 0b1100011)),         # bne t0, zero, -8
    (0x14, enc_s(64, 6, 0, 0b010, 0b0100011)),         # sw t1, 64(zero)
    (0x18, enc_i(64, 0, 0b010, 7, 0b0000011)),         # lw t2, 64(zero)
]

print("1. Codificação e conferência com o capstone")
for addr, w in PROGRAMA:
    desmontado = asm_capstone(w)
    print(f"  {addr:#04x}  {w:#010x}  {w:032b}  {desmontado}")
print()

print("2. Execução com a unidade de controle (estados por instrução)")
cpu, log, erro = roda(PROGRAMA, fim=0x1C)
print(f"  {'PC':<6} {'instrução':<18} {'estados':<22} efeitos")
for pc, nome, estados, efeitos, w in log:
    asm = asm_capstone(w).replace(" ", "")
    print(f"  {pc:#04x}   {asm:<18} {'/'.join(estados):<22} {'; '.join(efeitos)}")
print()
print("3. Resultado final")
print(f"  t0 = {cpu.regs[5]}, t1 = {cpu.regs[6]}, t2 = {cpu.regs[7]}, mem[0x40] = {cpu.mem[64]}")
print(f"  PC final = {cpu.pc:#04x}, instruções executadas = {len(log)}, ciclos = {cpu.ciclos}")
soma_esperada = sum(range(1, 4))
print(f"  conferência: soma de 3 + 2 + 1 = {soma_esperada}, t2 bate: {cpu.regs[7] == soma_esperada}")
print("  ciclos por tipo:", ", ".join(f"{k}={v}" for k, v in sorted(cpu.contagem.items())))
print()

print("4. Desvio com alvo desalinhado")
FALHA_TOMADO = [
    (0x00, enc_i(1, 0, 0b000, 5, 0b0010011)),          # addi t0, zero, 1
    (0x04, enc_b(6, 0, 5, 0b001, 0b1100011)),          # bne t0, zero, 6 (alvo 0x0a, desalinhado)
]
cpu2, log2, erro2 = roda(FALHA_TOMADO)
print("  bne tomado, alvo 0x0a:", erro2)
FALHA_NAO_TOMADO = [
    (0x00, enc_i(1, 0, 0b000, 5, 0b0010011)),          # addi t0, zero, 1
    (0x04, enc_b(6, 5, 5, 0b001, 0b1100011)),          # bne t0, t0, 6 (não tomado)
    (0x08, enc_i(7, 0, 0b000, 7, 0b0010011)),          # addi t2, zero, 7
]
cpu3, log3, erro3 = roda(FALHA_NAO_TOMADO)
print("  bne não tomado, alvo 0x0a:", erro3, "| t2 =", cpu3.regs[7], "| instruções:", len(log3))

Saída real do programa:

1. Codificação e conferência com o capstone
  0x00  0x00300293  00000000001100000000001010010011  addi t0, zero, 3
  0x04  0x00000313  00000000000000000000001100010011  mv t1, zero
  0x08  0x00530333  00000000010100110000001100110011  add t1, t1, t0
  0x0c  0xfff28293  11111111111100101000001010010011  addi t0, t0, -1
  0x10  0xfe029ce3  11111110000000101001110011100011  bnez t0, -8
  0x14  0x04602023  00000100011000000010000000100011  sw t1, 0x40(zero)
  0x18  0x04002383  00000100000000000010001110000011  lw t2, 0x40(zero)

2. Execução com a unidade de controle (estados por instrução)
  PC     instrução          estados                efeitos
  0x00   addit0,zero,3      IF/ID/EX/WB            t0 <- 3
  0x04   mvt1,zero          IF/ID/EX/WB            t1 <- 0
  0x08   addt1,t1,t0        IF/ID/EX/WB            t1 <- 3
  0x0c   addit0,t0,-1       IF/ID/EX/WB            t0 <- 2
  0x10   bnezt0,-8          IF/ID/EX               PC <- 0x08; desvio tomado
  0x08   addt1,t1,t0        IF/ID/EX/WB            t1 <- 5
  0x0c   addit0,t0,-1       IF/ID/EX/WB            t0 <- 1
  0x10   bnezt0,-8          IF/ID/EX               PC <- 0x08; desvio tomado
  0x08   addt1,t1,t0        IF/ID/EX/WB            t1 <- 6
  0x0c   addit0,t0,-1       IF/ID/EX/WB            t0 <- 0
  0x10   bnezt0,-8          IF/ID/EX               desvio não tomado
  0x14   swt1,0x40(zero)    IF/ID/EX/MEM           mem[0x40] <- 6
  0x18   lwt2,0x40(zero)    IF/ID/EX/MEM/WB        MDR <- mem[0x40] = 6; t2 <- 6

3. Resultado final
  t0 = 0, t1 = 6, t2 = 6, mem[0x40] = 6
  PC final = 0x1c, instruções executadas = 13, ciclos = 50
  conferência: soma de 3 + 2 + 1 = 6, t2 bate: True
  ciclos por tipo: add=12, addi=20, bne=9, lw=5, sw=4

4. Desvio com alvo desalinhado
  bne tomado, alvo 0x0a: instruction-address-misaligned no bne em pc=0x04, alvo 0x0a
  bne não tomado, alvo 0x0a: None | t2 = 7 | instruções: 3

A primeira parte da saída confere a codificação. Cada linha mostra o endereço, a palavra em hexadecimal, a mesma palavra em binário e o texto que o capstone desmonta. Repare que ele escreve mv no lugar de addi t1, zero, 0, e bnez no lugar de bne t0, zero, -8. São pseudoinstruções, nomes mais curtos para o mesmo código de máquina, e o desmontador as usa por convenção. A palavra do bne, 0xfe029ce3, é a mesma que o programa codifica a partir dos campos, e isso confirma a montagem do deslocamento.

A segunda parte é o rastro. Cada linha traz o PC, a instrução, os estados que ela percorreu e os efeitos nos registradores e na memória. A primeira iteração do laço aparece nas linhas de 0x08 a 0x10: o add grava 3 em t1, o addi diminui t0 para 2, e o bne é tomado. A última iteração, em que t0 vale 1 antes e 0 depois, mostra o bne não tomado, e o PC segue para 0x14. As linhas finais, sw e lw, gravam o valor 6 na memória e o leem de volta em t2.

A terceira parte confere o resultado e conta os ciclos. t1, t2 e a memória terminam com 6, e t0 termina com 0. A soma esperada, 3 mais 2 mais 1, dá 6, e a conferência retorna verdadeira. O total de treze instruções e cinquenta ciclos bate com a soma da tabela de estados: cinco addi em quatro ciclos cada, três add em quatro ciclos cada, três bne em três ciclos cada, um sw em quatro e um lw em cinco.

A quarta parte mostra o desvio com alvo desalinhado. No primeiro caso, o bne tomado em 0x04 com alvo 0x0a gera a exceção, reportada no bne. No segundo, com o mesmo alvo mas não tomado, a execução segue até o fim e t2 recebe 7, sem exceção. Essa diferença é exatamente a regra que o manual define.

8. Exercícios

Exercício 1. No programa da seção 7, o bne fica em 0x10 e tem deslocamento -8. Qual é o endereço do alvo, e qual PC segue quando t0 vale zero?

Resposta: o alvo é 0x10 menos 8, que dá 0x08. Quando t0 vale zero, o desvio não é tomado e o PC segue para 0x14, que é o endereço da instrução depois do bne. O rastro da seção 7 mostra essas duas situações nas linhas do bne.

Exercício 2. Quantos ciclos o programa gasta se o contador inicial de t0 for 10 em vez de 3? Explique a conta.

Resposta: 127 ciclos. Os dois addi iniciais gastam 8 ciclos. Cada iteração do laço gasta 11 ciclos, que são 4 do add, 4 do addi e 3 do bne. Com 10 iterações, o laço gasta 110. O sw e o lw do fim gastam 9. A soma é 8 mais 110 mais 9, que dá 127. A variação do programa foi executada e o total confirmado pelo simulador.

Exercício 3. Por que o bne tomado com alvo 0x0a gera exceção, e o bne não tomado com o mesmo alvo não gera?

Resposta: o manual exige que o alvo de um desvio tomado esteja alinhado em 4 bytes, e 0x0a não está. A exceção só ocorre quando o desvio é tomado, porque o alvo só é usado nesse caso. No caso não tomado, o PC segue em sequência e o alvo nunca é consultado. A exceção é reportada no próprio bne, e não na instrução de destino.

Exercício 4. Por que o sw tem RegWrite igual a 0 na tabela de controle?

Resposta: porque o sw não grava em nenhum registrador. Ele lê o valor de rs2 e grava esse valor na memória, no endereço calculado pela ULA. O sinal MemWrite é que vale 1. Se RegWrite fosse 1, a escrita iria para o registrador de destino, que a instrução sw nem tem, pois o campo que ocupa essa posição guarda parte do imediato.

Exercício 5 (sem resposta fechada). Troque o bne por um beq, que desvia quando os registradores são iguais. O que muda na tabela de controle e o que muda na máquina de estados? Use o manual para conferir o funct3 do beq. Este exercício pede leitura da tabela de codificação do manual, e a resposta depende da conferência feita por quem o resolver.

9. Glossário

PC, contador de programa: registrador que guarda o endereço da instrução atual no RV32I, e que avança a cada busca ou muda num desvio tomado.

IR, registrador de instrução: registrador que guarda a palavra de 32 bits buscada, até a unidade de controle decodificá-la.

MDR, registrador de dados da memória: registrador temporário que guarda o valor lido por um lw antes de ser gravado no registrador de destino.

Ciclo de instrução: sequência de estados que a CPU percorre para executar uma instrução, que neste modelo são busca, decodificação, execução, memória e escrita.

Busca: leitura da palavra apontada pelo PC, feita para toda instrução, e seguida do avanço do PC.

Decodificação: separação da palavra em campos, e leitura dos registradores de origem.

Unidade de controle: circuito que lê a instrução e liga os sinais que decidem o que cada peça faz em cada estado.

Tabela de controle: função que associa o opcode e o funct3 a cada sinal de controle.

Desvio tomado: desvio cuja condição é verdadeira, e que faz o PC receber o alvo.

Desvio não tomado: desvio cuja condição é falsa, e que deixa o PC seguir em sequência.

Alvo do desvio: endereço calculado somando o deslocamento do desvio ao endereço da própria instrução.

Microcódigo: sequência de micro-operações guardada numa ROM interna, percorrida por um micro-PC para gerar os sinais de controle.

Exceção de endereço desalinhado: erro gerado quando um desvio tomado ou um salto tem alvo que não é múltiplo de 4 no RV32I básico.

O que não foi confirmado ou medido

Os números de ciclo desta aula são do modelo didático do simulador. Eles não são medidos em chip, e um processador real não tem cinco ciclos fixos por instrução. O modelo não tem pipeline, cache nem previsão de desvios, e por isso não representa o desempenho de nenhum processador comercial.

O nome que o x86 usa para o registrador de endereço da instrução não foi conferido nesta aula, e o texto não afirma nada sobre ele.

A afirmação de que processadores x86 atuais usam microcódigo para instruções complexas vem da subnota Como a CPU executa instruções, e não foi conferida em documento primário nesta aula.

Os livros clássicos de arquitetura, como o de Patterson e Hennessy, não foram consultados nesta versão. O ciclo de cinco estados é uma escolha do simulador, e os nomes IF, ID, EX, MEM e WB seguem a convenção de livros didáticos, sem citação de página.

O programa foi executado, mas não foi executado em nenhum hardware real. Não há medida de tempo, de frequência nem de consumo.

Fontes

Consultadas em 1 de novembro de 2026.

  • RISC-V International, The RISC-V Instruction Set Manual, Volume I: Unprivileged Architecture, release riscv-isa-release-8fb3965-2026-10-30, versão 20261030. Seções usadas: 1.4 sobre a leitura implícita de memória na busca de instrução, 2.1.1 sobre o pc e o registrador x0, 2.1.2 sobre o alinhamento de 4 bytes e a exceção de endereço desalinhado, 2.1.5.1 sobre o alvo de JAL e JALR, 2.1.5.2 sobre o alvo dos desvios condicionais, a regra de exceção só no desvio tomado e a comparação direta entre registradores. A tabela de códigos de causa, com o código 0 para instruction-address-misaligned, está no volume privilegiado do mesmo PDF. Arquivo consultado: https://github.com/riscv/riscv-isa-manual/releases/download/riscv-isa-release-8fb3965-2026-10-30/riscv-spec.pdf
  • Capstone 5.0.7, biblioteca de desmontagem instalada via pip em um ambiente virtual. Usada para conferir a codificação das palavras e os nomes das instruções, como add, addi, lw, sw e bne, e as pseudoinstruções mv e bnez. Não é fonte da semântica das instruções, que vem do manual.
  • Os valores da tabela de controle, os estados de cada instrução e os números de ciclo são do simulador desta aula, escritos a partir das definições do manual. Eles não são citações de nenhuma fonte.

Relacionados

  • Trilha de Hardware
  • ULA e registradores
  • ISA, CISC e RISC
  • Máquinas de estados finitos
  • Assembly na prática
  • Pilha e chamada de função
  • Como a CPU executa instruções
  • Pipeline, cache e paralelismo
  • CPU

↑ 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
?