Pular para o conteúdo

CPUID

CPUID é a instrução que reporta o que uma CPU de fato suporta: quais conjuntos de instruções opcionais ela implementa, quantos bits de endereço ela expõe, e dezenas de outros detalhes que um kernel de outra forma teria que simplesmente presumir. O software carrega um número de leaf em EAX (e, para algumas leaves, uma subleaf em ECX), executa CPUID, e lê a resposta de volta em EAX, EBX, ECX, e EDX, cujo significado depende inteiramente de qual leaf foi requisitada. Caminhos de código por todo um kernel, desde decidir se o long mode sequer pode ser tentado até ler a invariância do TSC no código de timers, remontam por fim a uma checagem de CPUID feita uma vez durante o boot.

A leaf 0 (EAX = 0) é a única chamada que toda implementação garante responder: ela retorna o maior número de leaf que a CPU suporta em EAX, e uma string de identificação de fabricante de doze caracteres empacotada em EBX, EDX, e ECX, nessa ordem, quatro bytes ASCII por registrador. "GenuineIntel" e "AuthenticAMD" são as duas strings que um kernel mais provavelmente verá em hardware real, embora um hypervisor possa apresentar qualquer string que quiser aqui, o que é parte do motivo pelo qual a string de fabricante sozinha é um sinal fraco para qualquer coisa além de um primeiro palpite grosseiro sobre a identidade da CPU. O valor de leaf máxima importa mais na prática: um kernel que consulta uma leaf além do que a CPU de fato implementa não gera falha, ele silenciosamente recebe de volta o resultado da maior leaf que é implementada, então checar esse valor primeiro é o que impede uma consulta fora da faixa mais tarde de ser mal interpretada como uma resposta real.

A leaf 1 retorna os números de família, modelo, e stepping do processador em EAX, e, mais útil para um kernel decidindo o que pode habilitar com segurança, dois campos de bits de 32 bits com flags de recurso em ECX e EDX. EDX carrega o conjunto mais antigo e fundamental: bit 0 para uma FPU integrada, bit 5 para as instruções MSR/RDMSR/WRMSR, bit 6 para PAE, bit 9 para um APIC local. ECX carrega adições mais novas empilhadas depois: bit 0 para SSE3, bit 13 para CMPXCHG16B, e o bit 31, incomumente, não é um recurso de hardware algum, mas uma flag que um hypervisor define para anunciar que o código está rodando dentro de uma máquina virtual em vez de em hardware físico.

int cpu_has_feature(uint32_t leaf, uint32_t edx_bit) {
uint32_t eax, ebx, ecx, edx;
__asm__ volatile("cpuid"
: "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
: "a"(leaf));
return (edx >> edx_bit) & 1;
}
// ex.: cpu_has_feature(1, 6) checa PAE antes de habilitar long mode

Um punhado de leaves precisa de mais do que só EAX para desambiguar uma requisição: a leaf 7 empacota seus bits de recurso (AVX2, SMEP, SMAP entre eles) em múltiplas subleaves selecionadas através de ECX, e a leaf 4, que descreve a hierarquia de cache, retorna dados de um nível de cache diferente a cada subleaf sucessiva até a CPU sinalizar que não há mais retornando um tipo de cache igual a zero.

Uma faixa separada de leaves, começando em EAX = 0x80000000, existe ao lado da faixa padrão e precisa ser sondada da mesma forma: consultar 0x80000000 em si retorna a maior leaf estendida suportada, que em algumas CPUs mais antigas é simplesmente 0x80000000 de novo, significando que nenhuma leaf estendida existe além de confirmar sua própria ausência. Onde leaves estendidas estão presentes, 0x80000001 carrega um segundo conjunto de bits de recurso distinto do da leaf 1, mais notavelmente o bit 29 de EDX (o bit LM), que é a flag específica que um kernel checa antes de sequer tentar a transição para long mode. As leaves 0x80000002 até 0x80000004 juntas soletram a string de marca legível por humano da CPU (“Intel(R) Core(TM)…” ou similar) como 48 bytes de ASCII divididos entre três chamadas de EAX/EBX/ECX/EDX, texto puramente descritivo sem relação alguma com o que a CPU de fato consegue fazer.

Executar uma instrução que a CPU não implementa levanta uma exceção #UD (opcode inválido) em vez de fazer algo aproximadamente certo, e habilitar um bit de registrador de controle atrelado a um recurso ausente é igualmente propenso a falhar ou produzir comportamento genuinamente indefinido. Consultar a leaf de CPUID relevante primeiro e ramificar com base no resultado custa um punhado de ciclos e transforma o que de outra forma seria uma falha, muitas vezes ocorrendo cedo o suficiente no boot para não haver ainda tratador de exceção funcional algum para reportá-la de forma limpa, em um condicional comum. Isso importa mesmo sob emulação e virtualização, possivelmente mais do que em hardware físico: o modelo de CPU padrão do QEMU e uma VM minimamente configurada podem legitimamente ambos carecer de conjuntos de instruções que a própria máquina física do desenvolvedor de kernel tem, então código que nunca checa CPUID e só roda na própria máquina do desenvolvedor pode passar em todo teste ali e ainda assim falhar no momento em que roda em outro lugar.

CPUID sobrescreve os quatro registradores EAX, EBX, ECX, e EDX, e EBX em particular é um registrador preservado pelo chamado (callee-saved) sob a convenção de chamada System V, então assembly inline emitindo CPUID a partir de C em x86-64 precisa salvar e restaurar RBX explicitamente ao redor da chamada (ou rotear a consulta através de um pequeno wrapper escrito inteiramente em assembly) em vez de deixar o compilador presumir que ele sobrevive intocado. Um kernel voltado a ambientes de desenvolvimento virtualizados comumente empilha uma segunda checagem sobre os bits de recurso comuns: o bit de presença de hypervisor da leaf 1, seguido da leaf 0x40000000, cujos registradores EBX/ECX/EDX carregam uma string de fabricante específica do hypervisor (“KVMKVMKVM” entre elas) da mesma forma que a leaf 0 faz para o fabricante da CPU física, útil para um kernel que quer seguir um caminho de código diferente (pulando uma sondagem lenta de hardware, por exemplo) quando sabe que está rodando sob um hypervisor em vez de em bare metal.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A, Capítulo 3.3: a referência completa da instrução CPUID, incluindo toda leaf padrão e estendida.
  2. ^ AMD, AMD64 Architecture Programmer’s Manual, Volume 3, Apêndice E: a listagem de leaves do lado AMD, incluindo as leaves de recurso estendido que este artigo cobre.
  • Long Mode: a transição cuja própria disponibilidade é checada via o bit LM de leaf estendida descrito acima.
  • Timers: o bit de recurso de TSC invariante já mencionado ali como um exemplo concreto de uma flag de leaf 1.
  • Model-Specific Registers: uma segunda forma, complementar, de checar suporte a um recurso, usada depois que o CPUID já confirmou que o mecanismo subjacente existe.
  • A System V ABI: a convenção de chamada por trás da restrição de callee-saved sobre RBX observada acima.
  • Geração de Números Aleatórios em Hardware: RDRAND/RDSEED, duas instruções cuja própria disponibilidade é checada da mesma forma.