Segunda, 2:53pm, 24 de Agosto de 2026.
Além do novo modelo de registradores realmente de 32 bits, os
endereços eram extremamente restritos no 8086. Essencialmente eram
necessárias combinações com [bx], [bp],
[si] e [di]:
mov word [bx + si], ax mov byte [bx + di], al mov word [bp + si], ax mov byte [bp + di], al mov byte [bx + si + offset], al
Não era permitido simplesmente usar [ax + cx], por
exemplo; muito menos [ax + bx + 4 + 123]. Foi
justamente no 386 que surgiu o modelo:
base + index * scale + offset
Com as escalas permitidas sendo 1, 2, 4 ou 8. Isso permite operações sofisticadas de endereçamento numa única instrução, como por exemplo:
mov eax, [esi + ecx * 8 + 444] mov ebx, [esi + ecx * 4 + 333] mov ecx, [esi + ecx * 2 + 222] mov edx, [esi + ecx * 1 + 222] ; O mesmo que mov edx, [esi + ecx + 222]
E o próprio manual do 386 diz explicitamente que a multiplicação do índice por 2, 4 ou 8 é feita durante o cálculo do endereço sem perda de performance, o que transforma completamente a maneira de percorrer arrays e estruturas. Por exemplo, no 8086 faríamos:
mov si, array mov cx, count again: mov ax, [si] ; Poderia ser substituído por LODSW add si, 2 ; loop again
Já no 386, é possível fazer diferente, já que o índice lógico pode ser convertido no endereço físico do elemento pelo próprio modelo de endereçamento:
mov ecx, count again: mov eax, [array + ecx * 2 + 8] ; ... dec ecx jnz again
Esse avanço no ISA (Instruction Set Architecture) no que diz
respeito ao modelo de endereçamento também torna o LEA
muito mais poderoso, já que no 8086, o LEA era
basicamente uma ferramenta de cálculo de endereço com as limitações
do modelo de endereçamento do 8086. Já no 386, ele pode até mesmo
ser utilizado para operações aritméticas:
lea eax, [eax + eax] ; Produz efetivamente EAX *= 2 lea eax, [eax + eax * 2] ; EAX *= 3 lea eax, [eax * 4] ; EAX *= 4 lea eax, [eax + eax * 4] ; EAX *= 5 lea eax, [eax * 8] ; EAX *= 8 lea eax, [eax + eax * 8] ; EAX *= 9 lea eax, [eax * 4 + 5] ; EAX = EAX * 4 + 5 lea eax, [eax + eax * 8 + 10] ; EAX = EAX * 9 + 10
Também é possível combinar duas ou mais instruções
LEA para alcançar multiplicações por outros fatores,
como 6, 7, 10, etc.:
Por 6:
mov eax, 6 lea eax, [eax + eax * 2] ; EAX = EAX * 3 lea eax, [eax + eax] ; EAX = EAX + EAX ; EAX agora é 36 (EAX * 6)
Por 10:
mov eax, 5 lea eax, [eax + eax * 4] ; EAX = EAX * 5 lea eax, [eax + eax] ; EAX = EAX + EAX ; EAX agora é 50
Essa classe inteira de idiomatismos simplesmente não existia no 8086.
Domingo, 11:49pm, 23 de Agosto de 2026.
Longe do escopo deste texto, apenas um desabafo: C++ é uma linguagem horrível. E digo isso com pesar, visto que programei C++ por longos anos, até que um ano atrás decidi que não implementaria mais nenhum projeto pessoal usando isso. C foi a escolha mais natural e, após a implementação do último game engine ao vivo em lives, no ano passado, tomei mais uma decisão quanto ao próximo projeto: esse será em assembly, como nos velhos tempos. E escrevendo as primeiras linhas, trabalhando na biblioteca base com funções para agilizar o dia a dia, lembrei de como certos detalhes denunciam a evolução da ISA (Instruction Set Architecture), versão após versão das CPUs, como do 8086 para o 286, para então o 386 e por aí vai. Decidi escrever algumas coisas sobre isso e este é o primeiro texto.
Lançado pela Intel em 1985, o 386 marcou uma das maiores mudanças na história da arquitetura x86. Mais do que uma evolução do 286, ele estabeleceu a base do que viria a ser conhecido como IA-32, ampliando os principais registradores de 16 para 32 bits e permitindo trabalhar naturalmente com valores e endereços do mesmo tamanho. Registradores como AX, BX, CX e DX passaram a ter suas versões estendidas EAX, EBX, ECX e EDX, assim como SI, DI, BP e SP deram origem a ESI, EDI, EBP e ESP. Essa ampliação aumentou enormemente a capacidade de manipular grandes estruturas de dados e espaços de memória sem recorrer aos segmentos que marcaram o dia a dia de quem programou os processadores anteriores.
Além de transformar o endereçamento de memória (no 8086, apenas determinadas combinações entre BX, BP, SI e DI eram permitidas na formação de endereços), graças ao 386 podemos combinar praticamente qualquer registrador de propósito geral como base ou índice, além de multiplicar índices por 1, 2, 4 ou 8, como vamos ver mais adiante, e acrescentar um deslocamento. Outra inovação decisiva foi a possibilidade de trabalhar com um espaço linear de endereçamento de até 4 GB. O 386 manteve a segmentação herdada das gerações anteriores, mas permitiu segmentos muito maiores e acrescentou paginação à arquitetura. Com isso, tornou-se possível construir sistemas operacionais com memória virtual, isolamento entre processos e proteção de páginas. O chamado modelo de memória plana (flat memory), no qual diferentes segmentos são configurados para abranger o mesmo espaço linear, tornou-se posteriormente a forma dominante de utilizar a arquitetura em sistemas de 32 bits.
Mesmo assim, o 386 manteve uma característica fundamental da família x86: compatibilidade com software anterior. Ele continuava capaz de executar código escrito para o 8086 e seus sucessores, mesmo acrescentando modos de operação muito mais sofisticados. Seu modo protegido de 32 bits oferecia quatro níveis de privilégio (ring 0, 1, 2 e 3), proteção de memória e mecanismos apropriados de multitarefa (mesmo que esses tenham sido abandonados pelos sistemas operacionais pouco depois, por se provarem desnecessariamente lentos), enquanto o modo Virtual 8086 permitia executar software compatível com 8086 dentro de um sistema protegido (isso foi utilizado pelo Windows 3.1, Windows 9x e Windows NT de 32 bits por décadas, para executar software para MS-DOS e Windows 3.1 nos Windows posteriores). Essa combinação de compatibilidade e expansão arquitetural foi uma das razões para a longevidade do x86.
Mas além disso, o 386 representou uma mudança muito mais profunda: ele acrescentou instruções que eliminavam vários idiomatismos do 8086 e, curiosamente, mudou até quais sequências de instruções são mais rápidas, como vamos ver com o LOOP versus DEC/JNZ logo a seguir.
No 8086, isto era perfeitamente racional:
mov cx, 100 again: ; ... loop again
O Intel 8086 Family User's Manual diz que:
INSTRUÇÃO CLOCKS --------- ------ LOOP 17 + m DEC CX 2 JNZ addr 16 + m DEC CX + JNZ 18 clocks + m
Além disso, loop again tem 2 bytes, enquanto
DEC CX + JNZ again tem 3 bytes. Ou seja: no 8086
LOOP é simultaneamente menor e ligeiramente mais
rápido.
No 386 a situação se inverte:
mov ecx, 100 again: ; ... dec ecx jnz again
O 386 dá:
INSTRUÇÃO CLOCKS --------- ------ LOOP 11 + m DEC ECX 2 JNZ addr 7 + m DEC ECX -> JNZ 9 + m
Diferença de 2 clocks por iteração (11 do LOOP - 9 do DEC/JNZ).
E aquele famigerado m das tabelas da Intel tem
definição exata: é o número de componentes da próxima instrução
executada que o 386 precisa buscar (fetch) após a transferência
de controle. Um displacement inteiro conta como um componente, um
immediate inteiro conta como outro e os demais bytes/prefixos contam
individualmente.
No nosso caso, isso não atrapalha a comparação, visto que ambos os casos saltam para a mesma instrução:
LOOP = 11 + m DEC/JNZ = 9 + m
Logo, exatamente 2 clocks de vantagem para DEC/JNZ no
caminho tomado.
Há, porém, uma diferença semântica importante: LOOP
addr não altera flag alguma, enquanto DEC ECX
altera OF, SF, ZF,
AF e PF, embora preserve CF. Logo, não é
uma substituição universal: LOOPE e LOOPNE,
principalmente, conseguem testar um ZF produzido por uma
instrução anterior sem destruí-lo.
E existe uma outra sutileza muito 386: LOOP escolhe CX
ou ECX pelo address size attribute (use16 ou use32), não pelo operand
size. Em código de 32 bits, ele naturalmente usa ECX, enquanto em
código de 16 bits, ele usa CX.