No artigo anterior — Representação de Dados — vimos como o computador codifica caracteres, inteiros e números de ponto flutuante como padrões de bits. Mas um programa não é apenas dados: é uma sequência de instruções que dizem ao processador o que fazer com esses dados. Como essas instruções são codificadas? Onde o processador encontra os operandos que elas precisam?
Essas perguntas são respondidas pela ISA — Instruction Set Architecture — e pelos modos de endereçamento que ela define. A ISA é o ponto de encontro entre o hardware e o software: tudo que um programa faz deve ser expresso em termos que o processador conheça.
A ISA: o contrato entre hardware e software #
A ISA (Instruction Set Architecture) é a interface entre o hardware e o software. Ela define o conjunto de instruções que o processador entende e o modelo visível para compiladores, montadores, sistemas operacionais e programas.
A ISA especifica, entre outras coisas:
- o formato das instruções;
- os tipos de dados suportados;
- os modos de endereçamento;
- os registradores e seus papéis;
- o conjunto de operações que a CPU executa nativamente.
É útil pensar na ISA como um contrato de compatibilidade: programas traduzidos para um ISA só executam diretamente em processadores que implementam esse ISA, salvo uso de emulação, tradução binária ou máquinas virtuais.
ISA não é a mesma coisa que microarquitetura
Dois processadores podem implementar o mesmo ISA e, ainda assim, serem internamente muito diferentes. É o caso de diferentes CPUs x86-64 da Intel e AMD: o software enxerga o mesmo ISA, mas o hardware interno pode variar bastante.
Linguagens de programação e ISA
Nem toda linguagem de alto nível vira diretamente instruções do ISA da máquina.
- Em linguagens compiladas nativamente, como C e C++, o compilador geralmente gera código de máquina para o ISA-alvo.
- Em linguagens ou plataformas como Java e Python, pode haver etapas intermediárias, como bytecode, máquina virtual ou compilação JIT, antes da execução final no hardware.
Ainda assim, em última instância, a execução real sempre termina em instruções do ISA implementado pelo processador.
ISA no mercado atual
A família x86-64 continua dominante em desktops e muitos servidores. A família ARM hoje vai muito além de smartphones, aparecendo também em sistemas embarcados, notebooks, nuvem e datacenters. O RISC-V, por sua vez, ganhou relevância por ser um ISA aberto, especialmente em pesquisa, ensino e sistemas embarcados.
Uma classificação útil das instruções #
Uma forma didática de organizar as instruções de máquina é agrupá-las por função:
| # | Categoria | Exemplos de mnemônicos |
|---|---|---|
| 1 | Movimentação de dados | LOAD, STORE, MOV |
| 2 | Aritmética inteira | ADD, SUB, MPY, DIV |
| 3 | Aritmética de ponto flutuante | FADD, FMPY, FDIV |
| 4 | Lógica | AND, OR, NOT, XOR |
| 5 | Deslocamentos | SHL, SHR, SAR |
| 6 | Controle de fluxo | JMP, JZ, CALL, RET |
| 7 | Entrada/Saída | IN, OUT |
Essa classificação é útil para estudo, mas não deve ser tratada como uma taxonomia universal. ISAs reais modernas também incluem, por exemplo, instruções vetoriais, atômicas, criptográficas, de sistema e de sincronização.
Arquiteturas RISC tendem a ter poucas instruções por categoria, cada uma simples e regular. Arquiteturas CISC podem ter dezenas de variantes por categoria, incluindo operações complexas em uma única instrução.
Mas independentemente da categoria ou da complexidade, a CPU precisa saber ler essa ordem. Toda instrução precisa ser fisicamente empacotada em bits, e é nesse empacotamento que entra o design dos formatos de instrução.
Formatos de instrução: quantos operandos? #
Uma das decisões fundamentais no projeto de um ISA é quantos operandos cada instrução carrega. Há um trade-off claro: mais operandos permitem instruções mais expressivas — e portanto programas mais curtos — mas cada instrução ocupa mais espaço na memória.
Antigamente, a preocupação com o espaço era tão extrema que projetos como o SEAC (1949) utilizavam até 4 operandos: três para a operação e um quarto apenas para indicar o endereço da próxima instrução. Com a popularização do registrador CI (Contador de Instrução), que aponta automaticamente para a próxima tarefa, economizamos esses bits preciosos, permitindo formatos mais enxutos.
Para ilustrar, vamos calcular a expressão:
$$X = A \times (B + C \times D - E / F)$$em três formatos diferentes.
Formato com 3 operandos #
Forma geral: destino ← operando1 OP operando2
MPY T1, C, D ; T1 = C × D
ADD T2, B, T1 ; T2 = B + T1 (= B + C × D)
DIV T3, E, F ; T3 = E / F
SUB T4, T2, T3 ; T4 = T2 − T3 (= B + C×D − E/F)
MPY X, A, T4 ; X = A × T4 (resultado final)Total: 5 instruções. Cada instrução é autocontida — especifica entrada, operação e destino. As instruções são mais longas (três endereços codificados em cada uma), mas o programa é compacto.
Formato com 2 operandos #
Forma geral: Op1 ← Op1 OP Op2 — o primeiro operando é também o destino.
MOV X, C
MPY X, D ; X = C × D
ADD X, B ; X = B + C × D
MOV T, E
DIV T, F ; T = E / F
SUB X, T ; X = B + C×D − E/F
MPY X, A ; X = A × (B + C×D − E/F)Total: 7 instruções. É necessário inserir MOVs para preservar valores
originais antes de sobrescrevê-los. Este é o formato mais comum em arquiteturas
reais — a família x86 é baseada em instruções de 2 operandos.
Formato com 1 operando (acumulador) #
Forma geral: ACC ← ACC OP Operando — todas as operações passam por um
registrador especial chamado acumulador.
LDA C ; ACC = C
MPY D ; ACC = C × D
ADD B ; ACC = B + C × D
STR T ; T = ACC (salva temporário)
LDA E ; ACC = E
DIV F ; ACC = E / F
STR T2 ; T2 = ACC
LDA T ; ACC = T (= B + C × D)
SUB T2 ; ACC = B + C×D − E/F
MPY A ; ACC = A × (B + C×D − E/F)
STR X ; X = ACC (resultado final)Total: 11 instruções. Cada instrução é compacta (um único endereço), mas são necessárias mais delas e mais acessos à memória para salvar resultados intermediários. Este formato foi comum nos primeiros microprocessadores.
Comparativo #
| Formato | Nº de instruções | Espaço por instrução | Uso típico |
|---|---|---|---|
| 3 operandos | 5 | Grande (3 endereços) | Arquiteturas RISC modernas |
| 2 operandos | 7 | Médio (2 endereços) | x86, a maioria das arquiteturas |
| 1 operando (ACC) | 11 | Pequeno (1 endereço) | Primeiros microprocessadores |
Mais do que o número de linhas, o projetista de hardware olha para o balanço entre bits e ciclos. Uma instrução de 3 operandos é mais “pesada” (ex: 68 bits), mas resolve a conta com menos viagens à memória (acessos). Já instruções de 1 operando são “leves” (ex: 28 bits), mas exigem muito mais acessos para carregar e salvar o acumulador constantemente. É o eterno dilema entre gastar memória com o código ou gastar tempo com a execução.
Estratégia para calcular o número de instruções
-
3 operandos: cada operação da expressão gera, em geral, uma instrução com destino explícito.
\[ X = A \times (B + C \times D - E/F) \]
Na expressãohá 5 operações aritméticas: uma multiplicação \(C \times D\), uma soma, uma divisão, uma subtração e a multiplicação final por \(A\).
Portanto, o programa com 3 operandos usa 5 instruções, sem precisar de uma instrução extra de “atribuição”. -
2 operandos: acrescente
MOVs quando precisar preservar um valor antes de sobrescrevê-lo. -
1 operando: acrescente
LDAeSTAsempre que for necessário carregar dados no acumulador e salvar resultados intermediários.
Instruções como SQRT, NEG ou deslocamentos imediatos podem aparecer em ISAs
reais ou hipotéticos, mas sua presença isolada não define uma arquitetura
como CISC ou RISC.
A distinção entre RISC e CISC envolve um conjunto maior de decisões de projeto:
- regularidade dos formatos de instrução;
- quantidade de modos de endereçamento;
- complexidade média das instruções;
- facilidade de pipeline e decodificação.
Portanto, o exemplo a seguir é apenas um exemplo de ISA hipotético estendido, e não como prova de uma “filosofia CISC”.
Exemplo — Calcular \(X_1 = (-B + \sqrt{B^2 - 4C}) / 2\) com 1 operando
Usando instruções de 1 operando (acumulador) com endereçamento direto e imediato,
e com a instrução especial SQRT para raiz quadrada:
LDA B ; ACC = B
MPY B ; ACC = B²
SUB #4 ; ACC = B² − 4 (endereçamento imediato)
MPY C ; ACC = B² − 4×C
SQRT ; ACC = √(B² − 4×C)
STR TEMP ; TEMP = ACC
LDA B ; ACC = B
NEG ; ACC = −B
ADD TEMP ; ACC = −B + √(...)
SHR #1 ; ACC = ACC / 2 (shift right = dividir por 2)
STR X1 ; X1 = ACCModos de endereçamento #
Os modos de endereçamento definem como o processador obtém o operando de uma instrução.
O ponto central é este: o endereço efetivo (EA, Effective Address) é o endereço final do operando na memória, quando a instrução realmente acessa a memória. Em alguns modos, não há EA de memória, porque o dado já está na própria instrução ou em um registrador.
flowchart TD
A([Instrução]) --> B{Modo de
Endereçamento}
B --> C["Imediato
operando = constante"]
B --> D["Direto
EA = endereço"]
B --> E["Indireto
EA = M[endereço]"]
B --> F["Reg. Direto
operando = Rn"]
B --> G["Reg. Indireto
EA = Rn"]
B --> H["Indexado
EA = endereço + Ri"]
B --> I["Base + Deslocamento
EA = RB + desl"]
Depois de calcular o EA, quando houver acesso à memória, o operando será:
$$ \text{Operando} = M[EA] $$Para o processador, existem dois caminhos para identificar qual desses modos
usar: ou o Opcode já carrega essa informação (o código 1010 significa LDA Direto
e 1011 significa LDA Indireto) ou a instrução reserva um campo de bits
específico apenas para sinalizar o modo de endereçamento ao decodificador.
Modo imediato #
O operando é a constante codificada diretamente na instrução — não é um endereço, é o próprio valor.
$$\text{Operando} = \text{constante na instrução}$$- Vantagem: rapidíssimo — nenhum acesso adicional à MP
- Desvantagem: limitado pelo tamanho do campo; serve apenas para constantes pequenas e fixas
- Exemplo:
LDA #5→ ACC = 5
Modo direto (absoluto) #
O campo da instrução contém o endereço de memória onde está o operando.
$$ EA = \text{endereço} \qquad\Rightarrow\qquad \text{Operando} = M[EA] $$- Vantagem: simples e direto; fácil de entender e depurar
- Desvantagem: o endereço é fixo no código-objeto — dificulta a recolocação do programa
- Exemplo:
LDA 100→ ACC = M[100]
Modo indireto #
O campo contém um endereço que guarda o endereço do operando — um nível extra de indireção. O campo aponta para um ponteiro.
$$ EA = M[\text{endereço}] \qquad\Rightarrow\qquad \text{Operando} = M[EA] $$- Vantagem: flexível para ponteiros e estruturas dinâmicas
- Desvantagem: exige 2 acessos à MP — um para buscar o endereço, outro para buscar o dado
- Exemplo:
LDA @100→ ACC = M[M[100]]
Modo indireto: 2 acessos à memória
O modo indireto é frequentemente confundido com o modo registrador indireto. A diferença está no número de acessos à MP:
- Indireto (memória): 2 acessos — o endereço do ponteiro está na memória, então é preciso buscar o ponteiro (1º acesso) e depois o dado (2º acesso)
- Registrador indireto: 1 acesso — o ponteiro já está num registrador (sem acesso para buscá-lo), então há apenas o acesso ao dado
Modo registrador direto #
O operando está em um registrador do processador — não há EA de memória.
$$EA = R_n$$- Vantagem: o mais rápido possível — acesso a registrador é dezenas de ciclos mais rápido que acesso à MP
- Desvantagem: número limitado de registradores; programas precisam gerenciar quais dados mantêm em registradores
- Exemplo:
ADD R1, R2→ R1 = R1 + R2
Modo registrador indireto #
O registrador contém o endereço de memória onde está o operando. O registrador funciona como ponteiro.
$$ EA = R_n \qquad\Rightarrow\qquad \text{Operando} = M[EA] $$- Vantagem: muito flexível para percorrer arrays usando um registrador como ponteiro incrementável
- Exemplo:
LDA (R1)→ ACC = M[R1]
Modo indexado #
O endereço base está codificado na instrução; o registrador índice \(R_i\) é somado a ele para calcular o endereço efetivo.
$$ EA = \text{endereço} + R_i \qquad\Rightarrow\qquad \text{Operando} = M[EA] $$- Vantagem: ideal para percorrer arrays sequencialmente — basta incrementar \(R_i\) a cada iteração
- Exemplo:
LDA 100(R1)→ ACC = M[100 + R1]
Imagine a tarefa de somar 100 números em um vetor. No modo direto, você teria que escrever a mesma instrução 100 vezes, mudando apenas o endereço. Com o Modo Indexado, você escreve a instrução uma única vez apontando para a base do vetor e usa um registrador de índice para “caminhar” pelos elementos. É a diferença entre um código de 300 linhas e um loop eficiente de 5 linhas.
Modo base + deslocamento #
O registrador contém a base; a instrução traz um deslocamento fixo.
$$ EA = R_B + \text{deslocamento} \qquad\Rightarrow\qquad \text{Operando} = M[EA] $$- Exemplo conceitual: acessar um campo de estrutura ou uma variável local na pilha
- Uso típico: código relocável, pilha, ativação de funções, acesso a campos de structs
Esse modo é muito comum em arquiteturas modernas porque separa bem o endereço base dinâmico do deslocamento fixo conhecido em tempo de compilação.
Exemplos modernos de base + deslocamento
Em arquiteturas atuais, esse modo aparece o tempo todo.
-
x86-64:
mov eax, [rbp-8]
Acessa uma variável local a partir do ponteiro de base da pilha. -
AArch64:
ldr x0, [x1, #16]
Acessa a posição de memória cujo endereço éx1 + 16.
Isso torna o modo base + deslocamento mais fácil de reconhecer em código real do que exemplos antigos baseados em segmentação histórica.
Indexado × base + deslocamento
Nos dois casos, o endereço final é a soma de dois valores. A diferença é:
-
Indexado: base na instrução + índice no registrador
Ideal para arrays e iteração. -
Base + deslocamento: base no registrador + deslocamento na instrução
Ideal para pilha, structs e código relocável.
Tabela comparativa #
| Modo | EA | Operando | Acessos à MP para obter o dado | Vantagem principal |
|---|---|---|---|---|
| Imediato | — | valor na instrução | 0 | Constantes — rapidez máxima |
| Direto | \(\text{endereço}\) | \(M[EA]\) | 1 | Simples; variáveis globais |
| Indireto | \(M[\text{endereço}]\) | \(M[EA]\) | 2 | Ponteiros em memória |
| Reg. Direto | — | \(R_n\) | 0 | Operação rápida em registradores |
| Reg. Indireto | \(R_n\) | \(M[EA]\) | 1 | Ponteiro em registrador |
| Indexado | \(\text{endereço} + R_i\) | \(M[EA]\) | 1 | Percorrer arrays |
| Base + Deslocamento | \(R_B + \text{desl}\) | \(M[EA]\) | 1 | Pilha, structs, relocação |
Exemplo — Modos de endereçamento com tabela de memória
Considere a memória:
| Endereço | Conteúdo |
|---|---|
| 1AC1 | 1AC6 |
| 1AC2 | 1AC4 |
| 1AC3 | 1AC1 |
| 1AC4 | 1AC3 |
| 1AC5 | 1AC1 |
| 1AC6 | 1AC5 |
a) Direto, operando = 1AC1
$$ EA = 1AC1 \qquad\Rightarrow\qquad ACC \leftarrow M[1AC1] = \mathbf{1AC6} $$b) Imediato, operando = 1AC5
O campo operando já contém o valor:
$$ ACC \leftarrow \mathbf{1AC5} $$c) Indireto, operando = 1AC3
Primeiro calcula-se o endereço efetivo:
$$ EA = M[1AC3] = 1AC1 $$Depois busca-se o operando:
$$ ACC \leftarrow M[EA] = M[1AC1] = \mathbf{1AC6} $$d) Se o conteúdo de 1AC1 muda para 0
- Direto: \(ACC = M[1AC1] = \mathbf{0}\)
- Imediato: \(ACC = \mathbf{1AC5}\) (inalterado)
- Indireto: \(EA = M[1AC3] = 1AC1 \Rightarrow ACC = M[1AC1] = \mathbf{0}\)
O modo imediato é o único que não depende do conteúdo atual da memória.
Bônus Python: calculando endereços efetivos #
Entender o hardware lendo diagramas e fórmulas matemáticas pode parecer muito abstrato em um primeiro momento. Mas, no fim das contas, os modos de endereçamento são apenas lógicas de cálculo de ponteiros. Para um programador, a forma mais fácil de desmistificar o que o silício faz é traduzir essa mecânica para o código. Veja como o hardware calcularia os endereços efetivos usando funções simples em Python:
# Simula os principais modos de endereçamento
def ea_imediato(valor):
"""Modo imediato: o operando é o próprio valor."""
return valor, 0 # (dado, acessos à MP)
def ea_direto(memoria: dict, endereco: int):
"""Modo direto: EA = M[endereco]."""
return memoria[endereco], 1
def ea_indireto(memoria: dict, endereco: int):
"""Modo indireto: EA = M[M[endereco]]."""
ptr = memoria[endereco] # 1º acesso: busca o ponteiro
return memoria[ptr], 2 # 2º acesso: busca o dado
def ea_reg_indireto(memoria: dict, registrador: int):
"""Modo registrador indireto: EA = M[Rn]."""
return memoria[registrador], 1 # registrador já tem o endereço
def ea_indexado(memoria: dict, base: int, indice: int):
"""Modo indexado: EA = M[base + Ri]."""
return memoria[base + indice], 1
def ea_base_deslocamento(memoria: dict, rb: int, deslocamento: int):
"""Modo base + deslocamento: EA = M[RB + deslocamento]."""
return memoria[rb + deslocamento], 1
# Tabela de memória do exemplo anterior
mem = {
0x1AC1: 0x1AC6,
0x1AC2: 0x1AC4,
0x1AC3: 0x1AC1,
0x1AC4: 0x1AC3,
0x1AC5: 0x1AC1,
0x1AC6: 0x1AC5,
}
dado, acessos = ea_direto(mem, 0x1AC1)
print(f"Direto (1AC1): {dado:04X} ({acessos} acesso(s) à MP)")
# Direto (1AC1): 1AC6 (1 acesso(s) à MP)
dado, acessos = ea_imediato(0x1AC5)
print(f"Imediato(1AC5): {dado:04X} ({acessos} acesso(s) à MP)")
# Imediato(1AC5): 1AC5 (0 acesso(s) à MP)
dado, acessos = ea_indireto(mem, 0x1AC3)
print(f"Indireto(1AC3): {dado:04X} ({acessos} acesso(s) à MP)")
# Indireto(1AC3): 1AC6 (2 acesso(s) à MP)
# Array com modo indexado
vetor = {100: 10, 101: 20, 102: 30, 103: 40}
for i in range(4):
val, _ = ea_indexado(vetor, 100, i)
print(f"vetor[{i}] = {val}")
# vetor[0] = 10, vetor[1] = 20, vetor[2] = 30, vetor[3] = 40Conclusão e próximos artigos #
A ISA define o vocabulário que o processador entende — e os formatos de instrução e os modos de endereçamento são a gramática desse vocabulário. Com três operandos por instrução e sete modos de endereçamento, um processador consegue expressar desde simples adições até acesso dinâmico a estruturas de dados complexas. A escolha entre mais operandos (CISC) e menos operandos (RISC) reflete um trade-off entre expressividade e regularidade que o hardware moderno resolve de formas sofisticadas — tema do próximo artigo.
No próximo artigo, vamos acompanhar o ciclo de vida de um programa: desde o código-fonte em uma linguagem de alto nível, passando pela compilação, ligação e carregamento, até se tornar as instruções de máquina que o processador executa. Veremos como o ISA é a ponte entre o código que escrevemos e o silício que o executa, e como as decisões de projeto da ISA impactam a eficiência e a flexibilidade dos programas.
Até lá!