Compartilhar via


Visão geral das convenções ARM64 ABI

A ABI (interface binária do aplicativo) básica para Windows, quando compilada e executada em processadores ARM em modo de 64 bits (arquiteturas ARMv8 ou posteriores), na maior parte, segue o padrão AArch64 EABI do ARM. Este artigo destaca algumas das principais suposições e alterações do que está documentado no EABI. Para obter mais informações sobre ABI de 32 bits, consulte Visão geral das convenções ABI do ARM. Para obter mais informações sobre a EABI padrão do ARM, confira ABI (Interface Binária do Aplicativo) para a Arquitetura do ARM (link externo).

Definições

Com a introdução do suporte de 64 bits, o ARM definiu vários termos:

  • AArch32: a ISA (arquitetura de conjunto de instruções) de 32 bits herdada definida pelo ARM, incluindo a execução no modo Thumb.
  • AArch64 : a nova ISA (arquitetura de conjunto de instruções) de 64 bits definida pelo ARM.
  • ARMv7: a especificação do hardware ARM de "7ª geração", que inclui apenas suporte para AArch32. Essa versão do hardware ARM é a primeira versão com suporte pelo Windows para ARM.
  • ARMv8: a especificação do hardware ARM de "8ª geração", que inclui suporte para AArch32 e AArch64.

O Windows também usa estes termos:

  • ARM: refere-se à arquitetura ARM de 32 bits (AArch32), às vezes chamada de WoA (Windows em ARM).
  • ARM32: igual ao ARM acima, usado neste documento para maior clareza.
  • ARM64: refere-se à arquitetura ARM de 64 bits (AArch64). Não há como WoA64.

Por fim, ao se referir a tipos de dados, as seguintes definições do ARM serão referenciadas:

  • Short-Vector: um tipo de dados diretamente representável em SIMD, um vetor de 8 bytes ou 16 bytes de elementos. Ele é alinhado ao tamanho, de 8 bytes ou 16 bytes, em que cada elemento poderá ter 1, 2, 4 ou 8 bytes.
  • HFA (Agregado de Ponto Flutuante Homogêneo): um tipo de dados com 2 a 4 membros de ponto flutuante idênticos, flutuantes ou duplos.
  • HVA (Agregado de Vetor Curto Homogêneo): um tipo de dados com 2 a 4 membros de Vetor Curto idênticos.

Requisitos base

A versão ARM64 do Windows pressupõe que estará em execução em uma arquitetura ARMv8 ou posterior o tempo todo. Presume-se que há suporte NEON e de ponto flutuante presente no hardware.

A especificação ARMv8 descreve novos opcodes auxiliares de criptografia e CRC opcionais para AArch32 e AArch64. Atualmente, o suporte para eles é opcional, mas recomendado. Para tirar proveito desses opcodes, é necessário que os aplicativos primeiro executem verificações em tempo de execução para verificar se há opcodes.

Endianness

Assim como na versão ARM32 do Windows, no ARM64 o Windows executa no modo little endian. Alternar extremidade é difícil sem suporte para modo kernel no AArch64, portanto é mais fácil de impor.

Alinhamento

O Windows em execução no ARM64 permite que o hardware da CPU manipule os acessos desalinhados de forma transparente. Em uma melhoria do AArch32, esse suporte agora também funciona para todos os acessos de inteiros (incluindo acessos de várias palavras) e para acessos de ponto flutuante.

No entanto, os acessos à memória sem cache (dispositivo) ainda deverão sempre estar alinhados. Se o código puder ler ou gravar dados desalinhados de memória não armazenada em cache, ele deverá alinhar todos os acessos.

Alinhamento de layout padrão para locais:

Tamanho em Bytes Alinhamento em bytes
1 1
2 2
3, 4 4
> 4 8

Alinhamento de layout padrão para globais e estáticos:

Tamanho em Bytes Alinhamento em bytes
1 1
2 - 7 4
8 - 63 8
>= 64 16

Registros inteiros

A arquitetura AArch64 dá suporte para 32 registros inteiros:

Registrar-se Volatilidade Função
x0-x8 Volátil Registradores de rascunho de parâmetro/resultado
x9-x15 Volátil Registros de rascunho.
x16-x17 Volátil Registros de rascunho de chamada intra-procedimento.
x18 N/D Registro de plataforma reservada: no modo kernel, aponta para KPCR para o processador atual; No modo de usuário, aponta para TEB
x19-x28 Não volátil Registros de rascunho.
x29/fp Não volátil Ponteiro de quadro
x30/lr Ambos Link Register: A função Callee deve preservá-la para seu próprio retorno, mas o valor do chamador será perdido.

É possível acessar cada registro como um valor completo de 64 bits (por meio de x0-x30) ou como um valor de 32 bits (por meio de w0-w30). As operações de 32 bits estendem os resultados até 64 bits.

Consulte a seção Passagem de parâmetro para obter mais detalhes sobre o uso dos registros de parâmetro.

Ao contrário do AArch32, o PC (contador de programa) e o SP (ponteiro de pilha) não são registros indexados. Eles são limitados em como podem ser acessados. Observe também que não há registro x31. Essa codificação é usada para finalidades especiais.

O ponteiro de quadro (x29) é necessário para compatibilidade com o andamento rápido da pilha utilizada pelo ETW e por outros serviços. Ele deverá apontar para o par {x29, x30} anterior na pilha.

Registros de ponto flutuante/SIMD

A arquitetura AArch64 também dá suporte a 32 registros de ponto flutuante/SIMD, resumidos abaixo:

Registrar-se Volatilidade Função
v0-v7 Volátil Registradores de rascunho de parâmetro/resultado
v8-v15 Ambos Baixos 64 bits não são voláteis. Alta 64 bits são voláteis.
v16-v31 Volátil Registros de rascunho.

É possível acessar cada registrador como um valor completo de 128 bits (por meio de v0-v31 ou q0-q31). Pode ser acessado como valor de 64 bits (por meio de d0-d31), valor de 32 bits (por meio de s0-s31), valor de 16 bits (por meio de h0-h31) ou valor de 8 bits (por meio de b0-b31). Acessos menores que 128 bits acessarão apenas os bits inferiores do registrador completo de 128 bits. Os bits restantes permanecerão inalterados, exceto se especificado de outra forma. (AArch64 é diferente de AArch32, em que os registros menores foram empacotados em cima dos registros maiores.)

O FPCR (registrador de controle de ponto flutuante) possui determinados requisitos nos vários campos de bits dentro dele:

Bits Significado Volatilidade Função
26 AHP Não volátil Controle de meia precisão alternativo.
25 DN Não volátil Controle de modo NaN padrão.
24 FZ Não volátil Controle de modo Flush-to-zero.
23-22 RMode Não volátil Controle de modo de arredondamento.
15,12-8 IDE/IXE/etc. Não volátil Bits de habilitação de interceptação de exceção devem sempre ser 0.

Registros do sistema

Como AArch32, a especificação AArch64 fornece três registros de "ID de thread" controlados pelo sistema:

Registrar-se Função
TPIDR_EL0 Reservado.
TPIDRRO_EL0 Contém o número da CPU para o processador atual.
TPIDR_EL1 Aponta para a estrutura KPCR do processador atual.

Exceções de ponto flutuante

O suporte para exceções de ponto flutuante IEEE é opcional nos sistemas AArch64. Para variantes de processador com exceções de ponto flutuante de hardware, o kernel do Windows captura as exceções silenciosamente e as desabilita implicitamente no registro de FPCR. Essa interceptação garante um comportamento normalizado nas variantes do processador. Caso contrário, o código desenvolvido em uma plataforma sem suporte para exceções poderá receber exceções inesperadas ao executar em uma plataforma com suporte.

Passagem de parâmetro

Para funções não variádicas, a ABI do Windows segue as regras especificadas pelo ARM para passagem de parâmetros. Essas regras são extraídas diretamente do padrão de chamada de procedimento para a arquitetura AArch64:

Estágio A: inicialização

Essa etapa é feita exatamente uma vez, antes de iniciar o processamento dos argumentos.

  1. O NGRN (Próximo Número de Registro de Uso Geral) é definido como zero.

  2. O Próximo SIMD e o NSRN (Registro de Ponto Flutuante) são definidos como zero.

  3. O NSAA (próximo endereço do argumento empilhado) é definido para o valor atual de SP (ponteiro de pilha).

Estágio B: pré-preenchimento e extensão de argumentos

Para cada argumento na lista, será aplicada a primeira regra de correspondência da lista a seguir. Se nenhuma regra corresponder, o argumento será utilizado sem modificação.

  1. Se o tipo de argumento for um Tipo Composto cujo tamanho não pode ser determinado estaticamente pelo chamador e pelo receptor, o argumento será copiado para a memória e substituído por um ponteiro para a cópia. (Não há esses tipos em C/C++, mas existem em outras linguagens ou em extensões de linguagem).

  2. Se o tipo de argumento for um HFA ou um HVA, o argumento será usado sem modificações.

  3. Se o tipo de argumento for um Tipo Composto maior do 16 bytes, o argumento será copiado para a memória alocada pelo chamador e o argumento será substituído por um ponteiro para a cópia.

  4. Se o tipo de argumento for um Tipo Composto, o tamanho do argumento será arredondado para o múltiplo de 8 bytes mais próximo.

Estágio C: atribuição de argumentos para registros e pilha

Para cada argumento na lista, as seguintes regras serão aplicadas sucessivamente até que o argumento seja alocado. Quando um argumento for atribuído a um registrador, os bits não utilizados no registrador terão o valor não especificado. Se um argumento for atribuído a um slot de pilha, os bytes de preenchimento não utilizados terão o valor não especificado.

  1. Se o argumento for um tipo de ponto flutuante de precisão média, simples, dupla ou quádrupla ou de vetor curto, e o NSRN for menor que 8, o argumento será alocado para os bits menos significativos do registro v[NSRN]. O NSRN será incrementado em uma unidade. O argumento agora foi alocado.

  2. Se o argumento for um HFA ou um HVA, e houver registros SIMD e de ponto flutuante não alocados suficientes (NSRN + número de membros ≤ 8), o argumento será alocado para os registros SIMD e de ponto flutuante, um registro por membro do HFA ou HVA. O NSRN é incrementado pelo número de registros utilizados. O argumento agora foi alocado.

  3. Se o argumento for um HFA ou um HVA, o NSRN será definido como 8 e o tamanho do argumento será arredondado para o múltiplo de 8 bytes mais próximo.

  4. Se o argumento for um HFA, um HVA, um ponto flutuante de precisão quádrupla ou um tipo de vetor curto, o NSAA será arredondado para o maior de 8 ou o alinhamento natural do tipo do argumento.

  5. Se o argumento for um tipo de ponto flutuante de precisão simples ou média, o tamanho do argumento será definido como 8 bytes. O efeito é como se o argumento tivesse sido copiado para os bits menos significativos de um registro de 64 bits e os bits restantes preenchidos com os valores não especificados.

  6. Se o argumento for um HFA, um HVA, um ponto flutuante de precisão média, simples, dupla ou quádrupla ou tipo de vetor curto, o argumento será copiado para a memória no NSAA ajustado. O NSAA é incrementado pelo tamanho do argumento. O argumento agora foi alocado.

  7. Se o argumento for um tipo de ponteiro ou integral, o tamanho do argumento for menor ou igual a 8 bytes e o NGRN for menor que 8, o argumento será copiado para os bits menos significativos em x[NGRN]. O NGRN será incrementado em uma unidade. O argumento agora foi alocado.

  8. Se o argumento tiver um alinhamento de 16, o NGRN será arredondado para o próximo número par.

  9. Se o argumento for um tipo integral, o tamanho do argumento for igual a 16 e o NGRN for menor que 7, o argumento será copiado para x[NGRN] e x[NGRN+1]. x[NGRN] deverá conter a palavra dupla endereçada inferior da representação de memória do argumento. O NGRN é incrementado em duas unidades. O argumento agora foi alocado.

  10. Se o argumento for um tipo composto e o tamanho em palavras duplas do argumento não for maior que 8 menos NGRN, o argumento será copiado em registros de uso geral consecutivos, começando em x[NGRN]. O argumento será passado como se tivesse sido carregado nos registros a partir de um endereço alinhado por palavra dupla, com uma sequência apropriada de instruções LDR que carregam registros consecutivos da memória. O conteúdo de partes não utilizadas dos registros não é especificado por esse padrão. O NGRN é incrementado pelo número de registros utilizados. O argumento agora foi alocado.

  11. O NGRN é definido como 8.

  12. O NSAA é arredondado para o maior de 8 ou o alinhamento natural do tipo do argumento.

  13. Se o argumento for um tipo composto, o argumento será copiado para a memória no NSAA ajustado. O NSAA é incrementado pelo tamanho do argumento. O argumento agora foi alocado.

  14. Se o tamanho do argumento for menor que 8 bytes, o tamanho do argumento será definido como 8 bytes. O efeito é como se o argumento fosse copiado para os bits menos significativos de um registro de 64 bits e os bits restantes fossem preenchidos com valores não especificados.

  15. O argumento é copiado para a memória no NSAA ajustado. O NSAA é incrementado pelo tamanho do argumento. O argumento agora foi alocado.

Adendo: funções variádicas

Funções que recebem um número variável de argumentos são manipuladas de forma diferente das abordadas acima, conforme a seguir:

  1. Todos os compostos são tratados da mesma forma, portanto, sem tratamento especial de HFAs ou HVAs.

  2. Os registros de ponto flutuante e SIMD não são utilizados.

Efetivamente, é o mesmo que seguir as regras C.12–C.15 para alocar argumentos para uma pilha imaginária, em que os primeiros 64 bytes da pilha são carregados em x0-x7 e os argumentos restantes da pilha são colocados normalmente.

Valores de retorno

Os valores integrais são retornados em x0.

Os valores de ponto flutuante são retornados em s0, d0 ou v0, conforme apropriado.

Um tipo será considerado HFA ou HVA se todos os itens a seguir ocorrerem:

  • É não vazio,
  • Não tem padrões não triviais ou construtores de cópia, destruidores ou operadores de atribuição,
  • Todos os seus membros possuem o mesmo tipo HFA ou HVA, ou são tipos float, double ou neon que correspondem aos tipos HFA ou HVA de outros membros.

Os valores de HVA com quatro ou menos elementos são retornados em s0-s3, d0-d3 ou v0-v3, conforme apropriado.

Tipos retornados por valor serão manipulados de forma diferente, dependendo se possuem determinadas propriedades e se a função é uma função membro não estática. Tipos que possuem todas essas propriedades,

  • são agregados pela definição padrão do C++ 14, ou seja, não têm construtores fornecidos pelo usuário, membros de dados não estáticos privados ou protegidos, classes base e funções virtuais,
  • têm um operador de atribuição de cópia trivial,
  • têm um destruidor trivial,

e são retornados por funções não membro ou funções membro estáticas, usam o seguinte estilo de retorno:

  • Os tipos que são HFAs com quatro ou menos elementos são retornados em s0-s3, d0-d3 ou v0-v3, conforme apropriado.
  • Tipos menores ou iguais a 8 bytes são retornados em x0.
  • Tipos menores ou iguais a 16 bytes são retornados em x0 e x1, com x0 contendo os 8 bytes de ordem inferior.
  • Para outros tipos agregados, o chamador deve reservar um bloco de memória de tamanho e alinhamento suficientes para manter o resultado. O endereço do bloco de memória deverá ser passado como argumento adicional à função em x8. O computador chamado poderá modificar o bloco de memória resultante em qualquer ponto durante a execução da sub-rotina. O computador chamado não será obrigado a preservar o valor armazenado em x8.

Todos os outros tipos usam esta convenção:

  • O chamador deverá reservar um bloco de memória de tamanho e alinhamento suficientes para armazenar o resultado. O endereço do bloco de memória deverá ser passado como argumento adicional para a função em x0, ou x1 se $this for passado em x0. O computador chamado poderá modificar o bloco de memória resultante em qualquer ponto durante a execução da sub-rotina. O computador chamado retornará o endereço do bloco de memória em x0.

Pilha

Seguindo a ABI apresentada pelo ARM, a pilha deverá permanecer sempre alinhada com 16 bytes. O AArch64 contém um recurso de hardware que gerará falhas de alinhamento de pilha sempre que o SP não estiver alinhado com 16 bytes e um carregamento ou armazenamento relativo ao SP for concluído. O Windows executa com esse recurso habilitado o tempo todo.

Funções que alocam 4k ou mais de pilha deverão garantir que cada página antes da página final seja tocada na ordem. Essa ação garantirá que nenhum código possa "saltar" as páginas de proteção que o Windows usa para expandir a pilha. Normalmente, o toque é feito pelo auxiliar __chkstk, que possui uma convenção de chamada personalizada que passa a alocação total da pilha dividida por 16 em x15.

Zona vermelha

A área de 16 bytes imediatamente abaixo do ponteiro de pilha atual é reservada para uso em cenários análise e de aplicação de patch. Essa área permite a inserção de código cuidadosamente gerado que armazena dois registradores em [sp, #-16] e os utiliza temporariamente para propósitos arbitrários. O kernel do Windows garante que esses 16 bytes não serão substituídos se uma exceção ou interrupção for realizada, tanto no modo usuário quanto no modo kernel.

Pilha de kernel

A pilha do modo kernel padrão no Windows é de seis páginas (24k). Preste muita atenção nas funções com buffers de pilha grandes no modo kernel. Uma interrupção inoportuna poderá ocorrer com pouca reserva dinâmica e criar uma verificação de bug de emergência de pilha.

Passagem de pilha

O código dentro do Windows é compilado com ponteiros de quadro habilitados (/Oy-) para permitir o andamento rápido da pilha. Geralmente, x29 (fp) aponta para o próximo link na cadeia, que é um par {fp, lr}, indicando o ponteiro para o quadro anterior na pilha e o endereço de retorno. O código de terceiros também é recomendado para habilitar ponteiros de quadro, para permitir criação de perfil e rastreamentos aprimorados.

Desenrolamento de exceção

O desenrolamento durante o tratamento de exceção é auxiliado pelo uso de códigos de desenrolamento. Os códigos de desenrolamento são uma sequência de bytes armazenados na seção .xdata do executável. Eles descrevem a operação do prólogo e do epílogo de maneira abstrata, de modo que os efeitos do prólogo de uma função poderão ser desfeitos em preparação para o backup no quadro de pilha do chamador. Para obter mais informações sobre os códigos de desenrolamento, consulte Tratamento de exceções ARM64.

EABI do ARM também especifica um modelo de desenrolamento de exceção que usa códigos de desenrolamento. No entanto, a especificação apresentada é insuficiente para desenrolar no Windows, que deverá tratar nos casos em que o PC está no meio de um prólogo ou epílogo de função.

O código gerado dinamicamente deverá ser descrito com tabelas de funções dinâmicas por meio de RtlAddFunctionTable e funções associadas, para que o código gerado possa participar do tratamento de exceções.

Contador de ciclos

Todas as CPUs ARMv8 são necessárias para dar suporte a um registro de contador de ciclo, um registro de 64 bits que o Windows configura para ser legível em todos os níveis de exceções incluindo o modo de usuário. Ele pode ser acessado por meio do registro PMCCNTR_EL0 especial, usando o opcode do MSR em código assembly ou o _ReadStatusReg intrínseco em código C/C++.

O contador de ciclo aqui é um contador de ciclo verdadeiro, não um tempo total. A frequência de contagem variará com a frequência do processador. Se você precisar saber a frequência do contador de ciclo, não utilize o contador de ciclo. Em vez disso, se quiser medir o tempo total, utilize QueryPerformanceCounter.

Confira também

Problemas de migração ARM do Visual C++ comuns
Tratamento de exceções ARM64