Pular para o conteúdo

Arquitetura x86

x86 é a arquitetura de conjunto de instruções por trás de praticamente todo processador de desktop e servidor que um kernel hobby provavelmente vai ter como alvo, com linhagem que remonta ao Intel 8086 em 1978 e estendida, geração após geração, mantendo compatibilidade retroativa com software escrito para seus ancestrais mais antigos. Essa história é diretamente visível em como uma CPU x86-64 moderna inicializa: ela começa a execução em um modo de 16 bits funcionalmente semelhante ao 8086 original, e o software precisa transicioná-la explicitamente, um passo de cada vez, através das décadas intermediárias de adições arquiteturais antes de alcançar o ambiente completo de 64 bits em que a maioria dos kernels de fato roda.

x86 define vários modos de operação, cada um mudando o que a CPU é capaz de fazer e como ela interpreta endereços de memória.

O real mode, estado de power-on da CPU, é uma continuação direta do ambiente de 16 bits do 8086 original: registradores têm 16 bits de largura, memória é endereçada através de pares de segmento e offset de 16 bits combinados como segmento × 16 + offset, e não há proteção de memória ou separação de privilégio alguma: qualquer código rodando em real mode pode acessar qualquer endereço e executar qualquer instrução. O firmware e o código mais inicial de um bootloader rodam aqui, e a transição para fora dele é uma das primeiras coisas que o código de boot de um kernel realiza.

O protected mode, introduzido com o 80286 e substancialmente estendido pelo 80386, adiciona proteção de memória através de descritores de segmento, quatro níveis de privilégio (rings 0 a 3), e registradores e endereçamento de 32 bits no 80386 e posteriores. Segmentos aqui são consultados através de tabelas de descritores em vez de deslocados diretamente, dando à CPU um lugar para aplicar permissões de acesso e fronteiras de privilégio da forma que o real mode não consegue.

O long mode, adicionado com o x86-64, estende registradores e endereços virtuais para 64 bits e exige que a paginação já esteja ativa como pré-requisito para sequer entrar nele; diferente de todo modo anterior, o long mode não tem variante sem paginação. O long mode ainda se divide em modo de 64 bits propriamente dito e compatibility mode, o último permitindo que código de aplicação de 32 e 16 bits rode em grande parte sem modificação sob um kernel de 64 bits, embora o próprio kernel, uma vez em long mode, sempre execute como código de 64 bits.

Chegar ao long mode a partir do power-on não é um único passo: o real mode transiciona para protected mode carregando uma Global Descriptor Table e definindo um bit de registrador de controle, o protected mode transiciona para long mode habilitando adicionalmente a paginação e um bit de habilitação de long mode em uma model-specific register, e só depois de ambos um far jump para um segmento de código de 64 bits completa a troca. Cada estágio depende do anterior, razão pela qual o código de boot mais inicial de um kernel costuma ser o mais específico de arquitetura e menos portável.

O 8086 original definiu oito registradores de 16 bits: AX, BX, CX, DX, SI, DI, BP, SP, vários com papéis específicos de instrução além do armazenamento geral (CX como contador implícito de laço, SI/DI como ponteiros de origem e destino para instruções de string). O 80386 estendeu cada um para 32 bits prefixando um E (EAX, EBX, e assim por diante), e o x86-64 os estendeu novamente para 64 bits com um prefixo R (RAX, RBX), além de adicionar oito registradores inteiramente novos, R8 a R15, sem nome legado algum. De forma crítica, escrever em um nome de registrador mais estreito não limpa os bits superiores do registrador mais largo, exceto em um caso específico: uma escrita de 32 bits sob x86-64 (em EAX, por exemplo) estende com zeros para o RAX completo de 64 bits, mas uma escrita de 16 ou 8 bits deixa os bits superiores intocados exatamente como estavam, uma fonte frequente de bugs quando código presume que uma escrita estreita limpa o registrador inteiro.

Tanto protected mode quanto long mode organizam a execução em quatro níveis de privilégio, chamados de rings, numerados de 0 (mais privilegiado) a 3 (menos). Um kernel roda no ring 0, de onde toda instrução e todo acesso à memória são permitidos; aplicações comuns rodam no ring 3, onde instruções privilegiadas falham e o acesso à memória é restrito ao que os bits de permissão das tabelas de página atuais permitirem. Os rings 1 e 2 existem na arquitetura mas praticamente não têm uso em sistemas modernos; a maioria dos sistemas operacionais, incluindo todos os principais, usa apenas os rings 0 e 3, deixando os níveis intermediários definidos mas não utilizados. Mover-se entre rings acontece através de pontos de transição controlados, não de um salto arbitrário: uma interrupção ou exceção pode elevar o privilégio ao ring 0 através da IDT, e uma instrução de system call fornece uma transição semelhante e mais rápida, especificamente para requisições voluntárias do ring 3 para o ring 0.

Tanto a segmentação quanto a aplicação de privilégio são orientadas por tabelas de descritores que a CPU consulta, em vez de regras fixas embutidas no conjunto de instruções. A Global Descriptor Table (GDT), carregada com a instrução LGDT, define os segmentos disponíveis em todo o sistema (mesmo sob o modelo de memória plano do long mode, onde base e limite de segmento são efetivamente ignorados para a maioria dos propósitos, uma GDT mínima ainda é exigida), já que seletores de segmento continuam carregando a informação de nível de privilégio da qual uma transição de ring depende. Uma Local Descriptor Table (LDT) opcional pode complementar a GDT com segmentos por tarefa, embora tenha pouco uso na prática em sistemas que não dependem de troca de tarefas por hardware.

Instruções x86 têm comprimento variável e, particularmente considerando as extensões de cada geração, são codificadas através de um esquema bastante intrincado: um conjunto opcional de bytes de prefixo (alterando tamanho de operando, tamanho de endereço, adicionando semântica de lock, ou selecionando um segment override), um opcode de um a três bytes, um byte ModR/M opcional selecionando modos de endereçamento de operando e operandos de registrador, um byte SIB (Scale-Index-Base) opcional para endereçamento de memória complexo, um deslocamento opcional, e um valor imediato opcional. Sob long mode, um byte de prefixo opcional adicional, REX, fica entre quaisquer prefixos legados e o opcode, fornecendo os bits extras necessários para endereçar os oito novos registradores de 64 bits e para selecionar um tamanho de operando de 64 bits, já que o esquema de codificação base é anterior à própria existência de operandos de 64 bits.

A sequência de transição de modo descrita acima é implacável com erros de ordem: habilitar o bit de controle do long mode antes da paginação estar ativa, ou tentar o far jump final para um segmento de 64 bits antes da GDT ter um descritor de código de 64 bits válido carregado, geralmente produz um triple fault imediato em vez de um erro diagnosticável, já que a própria maquinaria de tratamento de falhas depende de estruturas que ainda não estão corretamente configuradas naquele ponto do boot. Registradores de segmento também carregam mais estado oculto do que seu valor de seletor de 16 bits sugere: carregar um seletor de segmento faz a CPU armazenar em cache internamente a base, o limite e os direitos de acesso do descritor correspondente, e é essa informação em cache, não uma nova consulta à tabela, que os acessos subsequentes à memória através daquele segmento de fato usam até que o seletor seja recarregado.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 1: a visão geral arquitetural que este artigo resume.
  2. ^ AMD, AMD64 Architecture Programmer’s Manual, Volume 1: a visão geral correspondente, com foco em long mode.
  • Protected Mode: um olhar mais próximo sobre segmentação, a GDT e aplicação de privilégio.
  • Long Mode: a sequência completa de transição para execução de 64 bits.