Compartilhar via


Visão geral das convenções do ABI do ARM32

A interface binária do aplicativo (ABI) de código compilado para Windows em processadores ARM é baseada na EABI padrão do ARM. Este artigo destaca diferenças chave entre Windows em ARM e o padrão. Este documento aborda o ABI do ARM32. Para obter informações sobre a ABI do ARM64, confira Visão geral das convenções do ABI do ARM64. 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).

Requisitos básicos

O Windows no ARM sempre presume estar sendo executado em uma arquitetura ARMv7. É necessário haver suporte a ponto flutuante na forma de VFPv3-D32 ou mais recente no hardware. O VFP deve suportar ponto flutuante de precisão única e de precisão dupla no hardware. O Windows runtime não dá suporte à emulação de ponto flutuante para habilitar execução em hardware não VFP.

Suporte a Extensões SIMD Avançadas (NEON), isso incluindo operações de ponto inteiro e flutuante, também deve estar presente no hardware. Não é fornecido suporte de tempo de execução para emulação.

Suporte a divisão de inteiros (UDIV/SDIV) é recomendável, mas não exigido. As plataformas sem suporte a divisão de inteiros podem incorrer uma penalidade de desempenho, pois essas operações devem ser capturadas e, possivelmente, atualizadas.

Endianness

O Windows em ARM é executado em modo little-endian. O compilador do MSVC e o Windows runtime sempre esperam dados little-endian. A instrução SETEND na ISA (arquitetura do conjunto de instruções) do ARM permite até mesmo código de modo de usuário para alterar a extremidade atual. No entanto, isso não é incentivado porque é perigoso para um aplicativo. Se uma exceção é gerada em modo big-endian, o comportamento é imprevisível. Isso pode levar a uma falha do aplicativo em modo de usuário ou em uma verificação de erro em modo kernel.

Alinhamento

Apesar de o Windows habilitar o hardware ARM para manipular acessos inteiros desalinhados de maneira transparente, falhas de alinhamento ainda podem ser geradas em algumas situações. Segue essas regras de alinhamento:

  • Você não precisa alinhar armazenamentos e cargas de inteiro do tamanho de meia palavra (16 bits) e tamanho de palavra (32 bits). O hardware os manipula de maneira eficiente e transparente.

  • Cargas e armazenamentos de ponto flutuante devem ser alinhados. O kernel manipula cargas e armazenamentos não alinhados de maneira transparente, porém, com sobrecarga significativa.

  • Operações Carregar ou armazenar duplo (LDRD/STRD) e múltiplo (LDM/STM) devem ser alinhadas. O kernel manipula a maioria delas de maneira transparente, porém, com sobrecarga significativa.

  • Todos os acessos a memória não armazenada em cache devem ser alinhados, mesmo para acessos inteiros. Acessos desalinhados causam uma falha de alinhamento.

Conjunto de instruções

O conjunto de instruções para Windows em ARM é estritamente limitado a Thumb-2. É esperado que todo código executado nessa plataforma comece e sempre permaneça em modo Thumb. A tentativa de alternar para o conjunto de instruções herdado do ARM pode ser bem-sucedida. Porém, qualquer exceção ou interrupção que ocorrer poderá levar a uma falha do aplicativo em modo de usuário ou a uma verificação de erro em modo kernel.

Um efeito colateral desse requisito é que todos os ponteiros de código devem conter o conjunto de bits inferior. Assim, quando eles forem carregados e ramificados via BLX ou BX, o processador permanecerá no modo Thumb. Ele não tentará executar o código de destino como instruções do ARM de 32 bits.

Instruções SDIV/UDIV

O uso de instruções de divisão de inteiros SDIV e UDIV tem suporte total, mesmo em plataformas sem hardware nativo para manipulá-las. A sobrecarga adicional da divisão SDIV ou UDIV em um processador Cortex-A9 é de aproximadamente 80 ciclos. Isso é adicionado ao tempo de divisão geral de 20 a 250 ciclos, dependendo das entradas.

Registros inteiros

O processador ARM oferece suporte a 16 registros inteiros:

Registrar-se Volátil? Função
r0 Volátil Parâmetro, resultado, registro de rascunho 1
r1 Volátil Parâmetro, resultado, registro de rascunho 2
r2 Volátil Parâmetro, registro de rascunho 3
r3 Volátil Parâmetro, registro de rascunho 4
r4 Não volátil
r5 Não volátil
r6 Não volátil
r7 Não volátil
r8 Não volátil
r9 Não volátil
r10 Não volátil
r11 Não volátil Ponteiro de quadro
r12 Volátil Registro de rascunho de chamada dentro do procedimento
r13 (SP) Não volátil Ponteiro de pilha
r14 (LR) Não volátil Registro de link
r15 (PC) Não volátil Contador de programa

Para obter detalhes sobre como usar o parâmetro e os registros de valores retornados, consulte a seção Passagem de Parâmetro neste artigo.

O Windows usa r11 para passagem rápida do quadro de pilha. Para obter mais informações, consulte a seção Passagem de Pilha. Devido a esse requisito, o r11 sempre deve apontar para o link superior na cadeia. Não use r11 para fins gerais, pois seu código não gerará exames de pilha corretos durante a análise.

Registros de VFP

O Windows tem suporte apenas a variações de ARM com suporte no coprocessador VFPv3-D32. Isso significa que registros de ponto flutuante estão sempre presentes e podem ser considerados para passagem de parâmetros. Além disso, o conjunto completo de 32 registros está disponível para uso. Os registros VFP e seu uso são resumidos nesta tabela:

Únicos Duplos Quads Volátil? Função
s0-s3 d0-d1 q0 Volátil Parâmetros, resultado, registro de rascunho
s4-s7 d2-d3 q1 Volátil Parâmetros, registro de rascunho
s8-s11 d4-d5 q2 Volátil Parâmetros, registro de rascunho
s12-s15 d6-d7 q3 Volátil Parâmetros, registro de rascunho
s16-s19 d8-d9 q4 Não volátil
s20-s23 d10-d11 q5 Não volátil
s24-s27 d12-d13 q6 Não volátil
s28-s31 d14-d15 q7 Não volátil
d16-d31 q8-q15 Volátil

A próxima tabela ilustra os campos de status do ponto flutuante e do registro de controle (FPSCR):

Bits Significado Volátil? Função
31-28 NZCV Volátil Sinalizadores de status
27 QC Volátil Saturação cumulativa
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
21-20 Passo Não volátil Passo do Vetor, deve ser sempre 0
18-16 Len Não volátil Comprimento do Vetor, deve ser sempre 0
15, 12-8 IDE, IXE e assim por diante Não volátil Bits de habilitação de captura de exceção, deve ser sempre 0
7, 4-0 IDC, IXC e assim por diante Volátil Sinalizadores de exceção cumulativa

Exceções de ponto flutuante

A maior parte do hardware do ARM não dá suporte a exceções de ponto flutuante IEEE. Em variações de processador que não contêm exceções de ponto flutuante de hardware, o kernel do Windows silenciosamente captura as exceções e implicitamente as desabilita no registro de FPSCR. Isso garante o comportamento normalizado entre variações do processador. Caso contrário, código desenvolvido em uma plataforma que não tenha suporte de exceção pode receber exceções inesperadas quando estiver executando em uma plataforma que tenha suporte de exceção.

Passagem de parâmetro

O Windows no ABI do ARM segue as regras do ARM para a passagem de parâmetros para funções não variádicas. As regras da ABI incluem as extensões VFP e SIMD Avançada. Essas regras seguem o Padrão de Chamada de Procedimento para a Arquitetura do ARM combinada com as extensões do VFP. Por padrão, os primeiros quatro argumentos inteiros e até oito argumentos de ponto flutuante ou vetor são passados em registros. Argumentos adicionais são passados na pilha. Argumentos são atribuídos a registros ou à pilha usando este procedimento:

Estágio A: inicialização

A inicialização é realizada exatamente uma vez, antes do início do processamento do argumento:

  1. O Próximo Número de Registro Principal (NCRN) é definido para r0.

  2. Os registros VFP são marcados como não alocados.

  3. O Próximo Endereço de Argumento Empilhado (NSAA) é definido para o SP atual.

  4. Se uma função que retorna um resultado na memória for chamada, o endereço do resultado é colocado em r0 e o NCRN é definido para r1.

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

Para cada argumento na lista, a primeira regra correspondente da seguinte lista é aplicada:

  1. Se o argumento for um tipo composto cujo tamanho não pode ser determinado estaticamente pelo chamador e pelo receptor, o argumento é copiado para a memória e substituído por um ponteiro para a cópia.

  2. Se o argumento for um byte ou uma meia palavra de 16 bits, será estendido em zero ou estendido em sinal para uma palavra inteira de 32 bits e tratado como um argumento de 4 bytes.

  3. Se o argumento for um tipo composto, seu tamanho é arredondado para cima para o próximo múltiplo de 4.

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

Para cada argumento na lista, as seguintes regras são aplicadas em turnos até o argumento ser alocado:

  1. Se o argumento for um tipo VFP e não houver registros VFP consecutivos não alocados suficientes do tipo adequado, o argumento será alocado para a sequência com menor número de tais registros.

  2. Se o argumento for um tipo VFP, todos os registros não alocados restantes serão marcados como não disponíveis. O NSAA é ajustado para cima até ser alinhado corretamente com o tipo de argumento e o argumento ser copiado para a pilha no NSAA ajustado. Em seguida, o NSAA é incrementado pelo tamanho do argumento.

  3. Se o argumento exige alinhamento de 8 bytes, o NCRN é arredondado para cima para o próximo número de registro par.

  4. Se o tamanho do argumento em palavras de 32 bits não for maior que r4 menos o NCRN, o argumento será copiado nos registros principais, começando no NCRN, com os bits menos significativos ocupando os registros de números inferiores. O NCRN é incrementado pelo número de registros usados.

  5. Se o NCRN for menor que r4 e o NSAA for igual ao SP, o argumento será dividido entre registros principais e a pilha. A primeira parte do argumento é copiado para os registros principais, começando no NCRN até e incluindo o r3. O restante do argumento é copiado para a pilha, começando no NSAA. O NCRN é definido para r4 e o NSAA é incrementado pelo tamanho do argumento menos a quantidade passada em registros.

  6. Se o argumento exige alinhamento de 8 bytes, o NSAA é arredondado para cima para o próximo endereço de 8 bytes alinhado.

  7. O argumento é copiado para a memória no NSAA. O NSAA é incrementado pelo tamanho do argumento.

Os registros do VFP não são usados para funções variádicas e as regras 1 e 2 do Estágio C são ignoradas. Isso significa que uma função variádica pode começar com um push opcional {r0-r3} para preceder os argumentos do registro de qualquer argumento adicional passado pelo chamador e, em seguida, acessar a lista de argumentos inteira diretamente da pilha.

Valores de tipo inteiro são retornados em r0, opcionalmente estendidos para r1 para valores retornados de 64 bits. Valores de tipo SIMD ou ponto flutuante VFP/NEON são retornados em s0, d0 ou q0 conforme adequado.

Pilha

A pilha deve permanecer sempre alinhada a 4 bytes e deve se alinhada a 8 bytes em qualquer limite de função. Isso é necessário para dar suporte ao uso frequente de operações sincronizadas em variáveis de pilha de 64 bits. A EABI do ARM declara que a pilha é alinhada a 8 bytes em qualquer interface pública. Para consistência, a ABI do Windows em ARM considera qualquer limite de função como uma interface pública.

Funções que devem usar um ponteiro de quadro—por exemplo, funções que chamam alloca ou alteram o ponteiro de pilha dinamicamente—devem manter o ponteiro de quadro em r11 no prólogo da função e deixá-lo inalterado até o epílogo. Funções que não exigem um ponteiro de quadro devem realizar todas as atualizações de pilha no prólogo e deixar o ponteiro de pilha inalterado até o epílogo.

Funções que alocam 4 KB ou mais na pilha devem garantir que cada página antes da página final será tocada em ordem. Essa ordem garante que nenhum código possa "pular por cima" das páginas de proteção que o Windows usa para expandir a pilha. Geralmente, essa expansão é feita pelo auxiliar __chkstk, para o qual é passado a alocação de pilha total em bytes dividida por 4 em r4, e que retorna a quantidade final de alocação de pilha em bytes em r4.

Zona vermelha

A área de 8 bytes imediatamente abaixo do ponteiro de pilha atual é reservada para análise e atualização dinâmica. Ela permite inserir código gerado cuidadosamente, que armazena 2 registros em [sp, #-8] e os usa temporariamente para propósitos arbitrários. O kernel do Windows garante que esses 8 bytes não serão sobrescritos se houver uma exceção ou interrupção no modo de usuário e no modo kernel.

Pilha de kernel

A pilha padrão de modo kernel no Windows é de três páginas (12 KB). Tenha cuidado para não criar funções que possuem grandes buffers de pilha em modo kernel. Uma interrupção pode ocorrer com muito pouco espaço de pilha e causar uma verificação de erros de pânico de pilha.

Particularidades do C/C++

Enumerações são tipos inteiros de 32 bits, exceto quando ao menos um valor na enumeração exigir armazenamento de palavra dupla de 64 bits. Nesse caso, a enumeração é promovida para um tipo inteiro de 64 bits.

wchar_t é definido como equivalente a unsigned short, para preservar a compatibilidade com outras plataformas.

Passagem de pilha

O código do Windows é compilado com ponteiros de quadro habilitados (/Oy (Omissão de ponteiro de quadro)) para permitir a passagem rápida de pilha. Geralmente, o registro r11 aponta para o próximo link na cadeia, que é um par {r11, lr} que especifica o ponteiro para o quadro anterior na pilha e o endereço de retorno. Também é recomendável habilitar ponteiros de quadro no código para criação de perfis e rastreamento melhorados.

Desenrolamento de exceção

O desenrolamento de pilha durante manipulação de exceções é habilitado pelo uso de códigos de desenrolamento. Os códigos de desenrolamento são uma sequência de bytes armazenada na seção .xdata da imagem executável. Eles descrevem a operação do código de prólogo e epílogo da função de maneira abstrata, de modo que os efeitos do prólogo de uma função podem ser desfeitos em preparação para o desenrolamento para o registro de ativação do chamador.

A EABI do ARM especifica um modelo de desenrolamento de exceção que usa códigos de desenrolamento. No entanto, essa especificação não é suficiente para desenrolamento no Windows, que deve manipular casos em que o processador está no meio do prólogo ou epílogo de uma função. Para obter mais informações sobre desenrolamento e dados de exceção no Windows em ARM, confiraTratamento de Exceções no ARM.

É recomendável descrever o código gerado dinamicamente usando tabelas de função dinâmica especificadas em chamas para RtlAddFunctionTable e funções associadas, para o código gerado poder participar da manipulação de exceções.

Contador de ciclos

Processadores ARM executando Windows devem suportar um contador de ciclo, porém, usar o contador diretamente pode causar problemas. Para evitar esses problemas, o Windows em ARM usa um opcode indefinido para solicitar um valor de contador de ciclo de 64 bits normalizado. A partir de C ou C++, use a intrínseca __rdpmccntr64 para emitir o opcode adequada; a partir de assembly, use a instrução __rdpmccntr64. A leitura do contador de ciclo leva aproximadamente 60 ciclos em um Cortex-A9.

O contador é um verdadeiro contador de ciclo, não um relógio; portanto, a frequência de contagem varia com a frequência do processador. Se desejar medir o tempo de relógio decorrido, use QueryPerformanceCounter.

Confira também

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