Compartilhar via


Tratamento de exceção do ARM64

O Windows baseado em ARM64 usa o mesmo mecanismo de tratamento de exceção estruturado para exceções geradas por hardware assíncronas e exceções geradas por software síncronas. Os manipuladores de exceção específicos da linguagem são compilados sobre o tratamento de exceção estruturado do Windows usando funções de auxiliares da linguagem. Este documento descreve o tratamento de exceções no Windows no ARM64. Ele ilustra os auxiliares de linguagem usados pelo código gerado pelo assembler Microsoft ARM e pelo compilador do MSVC.

Metas e motivação

As convenções de dados de desenrolamento de exceção e esta descrição têm os seguintes objetivos:

  • Fornecer descrição suficiente para permitir o desenrolamento sem sondagem de código em todos os casos.

    • Analisar o código exige que o código seja paginado. Impede o desenrolamento em algumas circunstâncias em que é útil (rastreamento, amostragem, depuração).

    • Analisar o código é complexo; o compilador deve ter cuidado para gerar apenas instruções que o desenrolador pode decodificar.

    • Se o desenrolamento não puder ser totalmente descrito usando códigos de desenrolamento, em alguns casos, ele deverá voltar à decodificação de instrução. A decodificação de instrução aumenta a complexidade geral e, idealmente, deve ser evitada.

  • Suporte ao desenrolamento no meio do prólogo e no meio do epílogo.

    • O desenrolamento é usado no Windows para mais do que tratamento de exceções. É fundamental que o código possa desenrolar com precisão inclusive no meio de uma sequência de código de prólogo ou epílogo.
  • Ocupar uma quantidade mínima de espaço.

    • Os códigos de desenrolamento não devem contribuir para aumentar significativamente o tamanho binário.

    • Como os códigos de desenrolamento provavelmente serão bloqueados na memória, um pequeno volume garante uma sobrecarga mínima para cada binário carregado.

Pressuposições

Essas suposições são feitas na descrição de tratamento de exceção:

  • Prólogos e epílogos tendem a espelhar um ao outro. Ao aproveitar essa característica comum, é possível reduzir muito o tamanho dos metadados necessários para descrever o desenrolamento. Dentro do corpo da função, não importa se as operações do prólogo são desfeitas ou se as operações do epílogo são feitas em um modo de avanço. Ambas devem produzir resultados idênticos.

  • As funções tendem, de um modo geral, a serem relativamente pequenas. Várias otimizações de espaço dependem desse fato para obter o empacotamento mais eficiente de dados.

  • Não há código condicional em epílogos.

  • Registro de ponteiro de quadro dedicado: se o sp for salvo em outro registro (x29) no prólogo, esse registro permanecerá intocado em toda a função. Isso significa que o original sp pode ser recuperado a qualquer momento.

  • A menos que o sp seja salvo em outro registro, toda a manipulação do ponteiro de pilha ocorre estritamente dentro do prólogo e do epílogo.

  • O layout do registro de ativação é organizado conforme descrito na próxima seção.

Layout do registro de ativação do ARM64

Diagrama que mostra o layout do quadro de pilha para funções.

Para funções encadeadas de quadros, o par e lr pode fp ser salvo em qualquer posição na área da variável local, dependendo das considerações de otimização. O objetivo é maximizar o número de locais que podem ser alcançados por uma única instrução com base no ponteiro de quadro (x29) ou no ponteiro de pilha (sp). No entanto, para alloca funções, ele deve ser encadeado e x29 deve apontar para a parte inferior da pilha. Para permitir uma cobertura melhor do modo de endereçamento de par de registros, as áreas de salvamento de registro não voláteis ficam no topo da pilha de área local. Aqui estão exemplos que ilustram várias das sequências de prólogos mais eficientes. Para clareza e melhor localidade de cache, a ordem para armazenar registros salvos por receptor em todos os prólogos canônicos está em ordem "crescente". #framesz abaixo representa o tamanho de toda a pilha (excluindo alloca a área). #localsz e #outsz denotam o tamanho da área local (incluindo a área de salvamento do par) e o tamanho do <x29, lr> parâmetro de saída, respectivamente.

  1. Encadeado, #localsz <= 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        stp    x29,lr,[sp,#-localsz]!   // save <x29,lr> at bottom of local area
        mov    x29,sp                   // x29 points to bottom of local
        sub    sp,sp,#outsz             // (optional for #outsz != 0)
    
  2. Encadeado, #localsz > 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        sub    sp,sp,#(localsz+outsz)   // allocate remaining frame
        stp    x29,lr,[sp,#outsz]       // save <x29,lr> at bottom of local area
        add    x29,sp,#outsz            // setup x29 points to bottom of local area
    
  3. Unchained, funções leaf (lr não salvas)

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]
        str    x23,[sp,#32]
        stp    d8,d9,[sp,#40]           // save FP regs (optional)
        stp    d10,d11,[sp,#56]
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    Todos os locais são acessados com base em sp. <x29,lr> aponta para o quadro anterior. Para o tamanho <do quadro = 512, o sub sp, ... pode ser otimizado se a área salva de regs for movida para o final da pilha. A desvantagem é que isso não é consistente com outros layouts acima. E os regs salvos fazem parte do intervalo para o modo de endereçamento de deslocamento pré e pós-indexado.

  4. Funções não folha desencadeadas (salva lr na área salva Int)

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        stp    x23,lr,[sp,#32]          // save last Int reg and lr
        stp    d8,d9,[sp,#48]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#64]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    Ou, com registros Int salvos de número par,

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        str    lr,[sp,#32]              // save lr
        stp    d8,d9,[sp,#40]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#56]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    Salvo apenas x19 :

        sub    sp,sp,#16                // reg save area allocation*
        stp    x19,lr,[sp]              // save x19, lr
        sub    sp,sp,#(framesz-16)      // allocate the remaining local area
    

    * A alocação da área de salvamento de registro não é dobrada no stp porque um reg-lr stp pré-indexado não pode ser representado com os códigos de desenrolamento.

    Todos os locais são acessados com base em sp. <x29> aponta para o quadro anterior.

  5. Encadeado, #framesz <= 512, #outsz = 0

        stp    x29,lr,[sp,#-framesz]!       // pre-indexed, save <x29,lr>
        mov    x29,sp                       // x29 points to bottom of stack
        stp    x19,x20,[sp,#(framesz-32)]   // save INT pair
        stp    d8,d9,[sp,#(framesz-16)]     // save FP pair
    

    Em comparação ao primeiro exemplo de caixa de diálogo acima, este exemplo tem uma vantagem: todas as instruções de salvamento de registro estão prontas para serem executadas depois de apenas uma instrução de alocação de pilha. Isso significa que não há antidependência que impeça o paralelismo no sp nível de instrução.

  6. Encadeado, tamanho de quadro > 512 (opcional para funções sem alloca)

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        sub    sp,sp,#(framesz-80)          // allocate the remaining local area
    

    Para fins de otimização, x29 pode ser colocado em qualquer posição na área local para fornecer uma melhor cobertura para o modo de endereçamento offset "reg-pair" e pré/pós-indexado. Os locais abaixo dos ponteiros de quadro podem ser acessados com base em sp.

  7. Encadeado, tamanho de quadro > 4K, com ou sem aloca(),

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        mov    x15,#(framesz/16)
        bl     __chkstk
        sub    sp,sp,x15,lsl#4              // allocate remaining frame
                                            // end of prolog
        ...
        sub    sp,sp,#alloca                // more alloca() in body
        ...
                                            // beginning of epilog
        mov    sp,x29                       // sp points to top of local area
        ldp    d10,d11,[sp,#64]
        ...
        ldp    x29,lr,[sp],#80              // post-indexed, reload <x29,lr>
    

Informações sobre tratamento de exceção do ARM64

Registros .pdata

Os registros .pdata são uma matriz ordenada de itens de comprimento fixo que descrevem cada função de manipulação de pilha em um binário PE. A frase "manipulação de pilha" é relevante: as funções folha que não exigem nenhum armazenamento local e não precisam salvar/restaurar registros não voláteis, não exigem um registro .pdata. Esses registros devem ser explicitamente omitidos para poupar espaço. Um desenrolamento de uma dessas funções pode obter o endereço de retorno diretamente de lr para mover até o chamador.

Cada registro .pdata para ARM64 tem um comprimento de 8 bytes. O formato geral de cada registro coloca o RVA de 32 bits do início da função na primeira palavra, seguido por uma segunda palavra que contém um ponteiro para um bloco .xdata de comprimento variável, ou uma palavra compactada que descreve uma sequência de desenrolamento de função canônica.

Layout de registro .pdata.

Os campos são os seguintes:

  • RVA de Início da Função é o RVA de 32 bits do início da função.

  • Flag é um campo de 2 bits que indica como interpretar os 30 bits restantes da segunda palavra .pdata. Se Flag for 0, os bits restantes formarão um RVA de informações de exceção (sendo os dois bits mais baixos implicitamente 0). Se Flag for diferente de zero, os bits restantes formarão uma estrutura de Dados de desenrolamento empacotados.

  • RVA de informações de exceção é o endereço da estrutura de informações de exceção de comprimento variável, armazenado na seção .xdata. Esses dados devem ser alinhados para 4 bytes.

  • Dados de desenrolamento empacotados são uma descrição compactada das operações necessárias para desenrolar uma função, supondo uma forma canônica. Nesse caso, nenhum registro .xdata é necessário.

Registros .xdata

Quando o formato de desenrolamento empacotado for insuficiente para descrever o desenrolamento de uma função, um registro .xdata de comprimento variável deverá ser criado. O endereço desse registro é armazenado na segunda palavra do registro .pdata. O formato de .xdata é um conjunto de palavras de comprimento variável empacotado:

Layout do registro .xdata.

Esses dados são divididos em quatro seções:

  1. Um cabeçalho de 1 ou 2 palavras que descreve o tamanho geral da estrutura e fornece dados de função chave. A segunda palavra estará presente somente se os campos Contagem de epílogos e Palavras de campo estiverem definidos como 0. O cabeçalho tem estes campos de bits:

    a. O Comprimento da Função é um campo de 18 bits. Indica o comprimento total da função em bytes dividido por 4. Se uma função tiver mais de 1 M, vários registros de .pdata e .xdata deverão ser usados para descrever a função. Para mais informações, confira a seção Funções grandes.

    b. Vers é um campo de 2 bits. Ele descreve a versão do .xdata restante. No momento, apenas a versão 0 é definida, portanto, os valores de 1 a 3 não são permitidos.

    c. X é um campo de 1 bit. Indica a presença (1) ou a ausência (0) de dados de exceção.

    d. E é um campo de 1 bit. Indica que as informações que descrevem um só epílogo são empacotadas no cabeçalho (1), em vez de precisarem de mais palavras de escopo depois (0).

    e. Contagem de epílogos é um campo de 5 bits que tem dois significados, dependendo do estado do bit E:

    1. Se E for 0, ele especificará a contagem do número total de escopos do epílogo descritos na seção 2. Se houver mais de 31 escopos na função, o campo Palavras de código deverá ser configurado como 0 para indicar que uma palavra de extensão é necessária.

    2. Se E for 1, esse campo especificará o índice do primeiro código de desenrolamento que descreve um, e apenas um, epílogo.

    f. Palavras de código é um campo de 5 bits que especifica o número de palavras de 32 bits necessário para conter todos os códigos de desenrolamento na seção 3. Se mais de 31 palavras (ou seja, 124 códigos de desenrolamento) forem necessárias, esse campo deverá ser 0 para indicar que uma palavra de extensão é necessária.

    g. Contagem de Epílogos Estendidos e Palavras de Código Estendido são campos de 16 bits e 8 bits, respectivamente. Eles fornecem mais espaço para codificar um número extraordinariamente grande de epílogos ou um número extraordinariamente grande de palavras de código desenroladas. A palavra de extensão que contém esses campos estará presente apenas se os campos Contagem de Epílogos e Palavras de Código da primeira palavra do cabeçalho forem 0.

  2. Se a contagem de epílogos não for zero, uma lista de informações sobre escopos de epílogo, empacotadas uma em uma palavra, virá após o cabeçalho e o cabeçalho estendido opcional. Eles são armazenados em ordem crescente de deslocamento inicial. Cada escopo contém os seguintes bits:

    a. Deslocamento de início do epílogo é um campo de 18 bits que tem o deslocamento em bytes, divididos por 4, do epílogo com relação ao início da função.

    b. Res é um campo de 4 bits reservado para expansão futura. Seu valor deve ser 0.

    c. Índice Inicial do Epílogo é um campo de 10 bits (2 bits a mais do que Palavras de Código Estendido). Indica o índice de bytes do primeiro código de desenrolamento que descreve este epílogo.

  3. Após a lista de escopos de epílogo, é fornecida uma matriz de bytes que contém códigos de desenrolamento, que são descritos em detalhes em uma seção posterior. Essa matriz é preenchida no final, o mais próximo possível do limite da palavra completa. Os códigos de desenrolamento são gravados nessa matriz. Eles começam com o mais próximo do corpo da função e se movem em direção às bordas da função. Os bytes para cada código de desenrolamento são armazenados em ordem big-endian, de modo que o byte mais significativo é buscado primeiro, o que identifica a operação e o comprimento do restante do código.

  4. Por fim, após o cancelamento de bytes de código, se o bit X no cabeçalho foi definido como 1, virão as informações do manipulador de exceção. Ele consiste em um só RVA do Manipulador de Exceção que fornece o endereço do manipulador de exceção em si. Ele é seguido imediatamente por uma quantidade de dados de comprimento variável exigidos pelo manipulador de exceção.

O registro .xdata foi projetado para que seja possível buscar os oito primeiros bytes e usá-los para computar o tamanho total do registro, menos o comprimento dos dados de exceção de tamanho variável que seguem. O seguinte snippet de código calcula o tamanho do registro:

ULONG ComputeXdataSize(PULONG Xdata)
{
    ULONG Size;
    ULONG EpilogScopes;
    ULONG UnwindWords;

    if ((Xdata[0] >> 22) != 0) {
        Size = 4;
        EpilogScopes = (Xdata[0] >> 22) & 0x1f;
        UnwindWords = (Xdata[0] >> 27) & 0x1f;
    } else {
        Size = 8;
        EpilogScopes = Xdata[1] & 0xffff;
        UnwindWords = (Xdata[1] >> 16) & 0xff;
    }

    if (!(Xdata[0] & (1 << 21))) {
        Size += 4 * EpilogScopes;
    }

    Size += 4 * UnwindWords;

    if (Xdata[0] & (1 << 20)) {
        Size += 4;  // Exception handler RVA
    }

    return Size;
}

Embora o prólogo e cada epílogo tenham um índice próprio nos códigos de desenrolamento, a tabela é compartilhada entre eles. É perfeitamente possível (e não de todo incomum) que todos possam compartilhar os mesmos códigos. (Para um exemplo, confira o Exemplo 2 na seção Exemplos.) Os gravadores do compilador devem otimizar para esse caso em particular. Isso acontece porque o maior índice que pode ser especificado é 255, o que limita o número total de códigos de desenrolamento para uma função específica.

Códigos de desenrolamento

A matriz de códigos de desenrolamento é um pool de sequências que descreve exatamente como desfazer os efeitos do prólogo. Eles são armazenados na mesma ordem em que as operações precisam ser desfeitas. Os códigos de desenrolamento podem ser considerados um pequeno conjunto de instruções, codificados como uma cadeia de caracteres de bytes. Quando a execução é concluída, o endereço de retorno para a função de chamada está no lr registro. E todos os registros não voláteis serão restaurados para seus valores no momento em que a função foi chamada.

Se fosse garantido que exceções ocorressem sempre somente dentro de um corpo da função e nunca dentro do prólogo ou qualquer epílogo, apenas uma sequência seria necessária. No entanto, o modelo de desenrolamento do Windows requer que o código possa desenrolar de dentro de um prólogo ou epílogo parcialmente executado. Para atender a esse requisito, os códigos de desenrolamento foram cuidadosamente projetados para que mapeiem 1:1 de modo inequívoco para cada código de opções relevante no prólogo e no epílogo. Esse design tem várias implicações:

  • Ao contar o número de códigos de desenrolamento, é possível computar o comprimento do prólogo e do epílogo.

  • Pela contagem do número de instruções passadas no início de um escopo de epílogo, é possível ignorar o número equivalente de códigos de desenrolamento. Podemos executar o restante de uma sequência para concluir o desenrolamento parcialmente executado feito pelo epílogo.

  • Pela contagem do número de instruções antes do fim do prólogo, é possível ignorar o número equivalente de códigos de desenrolamento. Podemos executar o restante da sequência para desfazer apenas as partes do diálogo que concluíram a execução.

Os códigos de desenrolamento são codificados de acordo com a tabela abaixo. Todos os códigos de desenrolamento são um byte único/duplo, exceto aquele que aloca uma pilha enorme (alloc_l). Existem 22 códigos de desenrolamento no total. Cada código de desenrolamento mapeia exatamente uma instrução no prólogo/epílogo, para permitir o desenrolamento de prólogos e epílogos parcialmente executados.

Desenrolar código Bits e interpretação
alloc_s 000xxxxx: alocar uma pilha pequena com tamanho < 512 (2^5 * 16).
save_r19r20_x 001zzzzz: salvar <x19,x20> par em [sp-#Z*8]!, deslocamento >pré-indexado = -248
save_fplr 01zzzzzz: salvar <x29,lr> par em [sp+#Z*8], offset <= 504.
save_fplr_x 10zzzzzz: salvar <x29,lr> par em [sp-(#Z+1)*8]!, deslocamento >pré-indexado = -512
alloc_m 11000xxx'xxxxxxxx: alocar pilha grande com tamanho < 32K (2^11 * 16).
save_regp 110010xx'xxzzzzzz: salvar x(19+#X) par em [sp+#Z*8], deslocamento <= 504
save_regp_x 110011xx'xxzzzzzz: salvar par x(19+#X) em [sp-(#Z+1)*8]!, deslocamento >pré-indexado = -512
save_reg 110100xx'xxzzzzzz: salvar registro x(19+#X) em [sp+#Z*8], deslocamento <= 504
save_reg_x 1101010x'xxxzzzzz: salvar registro x(19+#X) em [sp-(#Z+1)*8]!, deslocamento >pré-indexado = -256
save_lrpair 1101011x'xxzzzzzz: salvar par <x(19+2*#X),lr> em [sp+#Z*8], deslocamento <= 504
save_fregp 1101100x'xxzzzzzz: salvar par d(8+#X) em [sp+#Z*8], deslocamento <= 504
save_fregp_x 1101101x'xxzzzzzz: salvar par d(8+#X) em [sp-(#Z+1)*8]!, deslocamento >pré-indexado = -512
save_freg 1101110x'xxzzzzzz: salvar registro d(8+#X) em [sp+#Z*8], deslocamento <= 504
save_freg_x 11011110'xxxzzzzz: salvar registro d(8+#X) em [sp-(#Z+1)*8]!, deslocamento >pré-indexado = -256
alloc_l 11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx: alocar pilha grande com tamanho < 256M (2^24 * 16)
set_fp 11100001: configurar x29 com mov x29,sp
add_fp 11100010'xxxxxxxx: configurar x29 com add x29,sp,#x*8
nop 11100011: nenhuma operação de desenrolamento é necessária.
end 11100100: fim do código de desenrolamento. Implica ret em epílogo.
end_c 11100101: fim do código de desenrolamento no escopo encadeado atual.
save_next 11100110: salve o próximo par de registros int ou FP não voláteis.
11100111: reservado
11101xxx: reservado para casos de pilha personalizados abaixo gerados apenas para rotinas de asm
11101000: Pilha personalizada para MSFT_OP_TRAP_FRAME
11101001: Pilha personalizada para MSFT_OP_MACHINE_FRAME
11101010: Pilha personalizada para MSFT_OP_CONTEXT
11101011: Pilha personalizada para MSFT_OP_EC_CONTEXT
11101100: Pilha personalizada para MSFT_OP_CLEAR_UNWOUND_TO_CALL
11101101: reservado
11101110: reservado
11101111: reservado
11110xxx: reservado
11111000'yyyyyyyy : reservado
11111001'yyyy: reservado
11111010'yyyy
11111011'yyyy
pac_sign_lr 11111100: assine o endereço do remetente com lrpacibsp
11111101: reservado
11111110: reservado
11111111: reservado

Em instruções com valores grandes que abrangem vários bytes, os bits mais significativos são armazenados primeiro. Esse design possibilita localizar o tamanho total em bytes do código de desenrolamento pesquisando apenas o primeiro byte do código. Como cada código de desenrolamento é mapeado exatamente para uma instrução em um prólogo ou epílogo, você pode computar o tamanho do prólogo ou do epílogo. Percorra a sequência do início ao fim e use uma tabela de pesquisa ou um dispositivo semelhante para determinar o comprimento do opcode correspondente.

O endereçamento de deslocamento pós-indexado não é permitido em um prólogo. Todas as faixas de deslocamento (#Z) correspondem à codificação do stp/str endereçamento, exceto save_r19r20_x, em que 248 é suficiente para todas as áreas de salvamento (10 registros Int + 8 registros FP + 8 registros de entrada).

save_next deve seguir um salvamento para o par de registro volátil int ou FP: save_regp, save_regp_x, save_fregp, save_fregp_x, save_r19r20_x ou outro save_next. Ele salva o próximo par de registros no próximo slot de 16 bytes na ordem "crescente". Um save_next se refere ao primeiro par de registro FP quando ele segue o save-next que denota o último par de registro int.

Como os tamanhos das instruções regulares de retorno e salto são os mesmos, não há necessidade de um código de desenrolamento separado end em cenários de chamada final.

end_c foi projetado para lidar com fragmentos de função não contíguos para fins de otimização. Um end_c que indica o fim dos códigos de desenrolamento no escopo atual deve ser seguido por outra série de códigos de desenrolamento que terminam com um .end Os códigos de desenrolamento entre end_c e end representam as operações de prólogo na região pai (um prólogo "fantasma"). Mais detalhes e exemplos são descritos na seção abaixo.

Dados de desenrolamento empacotados

Para funções cujos prólogos e epílogos seguem a forma canônica descrita abaixo, os dados de desenrolamento compactados podem ser usados. Isso elimina por completo a necessidade de um registro .xdata e reduz significativamente o custo de fornecimento de dados de desenrolamento. Prólogos e epílogos canônicos foram projetados para atender aos requisitos comuns de uma função simples: uma que não requer um manipulador de exceção e executa operações de configuração e desinstalação em uma ordem padrão.

O formato de um registro .pdata com os dados de desenrolamento empacotados tem esta aparência:

.pdata com dados de desenrolamento compactados.

Os campos são os seguintes:

  • RVA de Início da Função é o RVA de 32 bits do início da função.
  • Flag é um campo de 2 bits, conforme descrito acima, com os seguintes significados:
    • 00 = dados de desenrolamento empacotados não usados; os bits restantes apontam para um registro .xdata
    • 01 = dados de desenrolamento empacotados usados com um só prólogo e epílogo no início e no final do escopo
    • 10 = dados de desenrolamento empacotados usados para código sem nenhum prólogo e epílogo. Útil para descrever segmentos de função separados
    • 11 = reservado.
  • Comprimento da Função é um campo de 11 bits que fornece o comprimento da função inteira em bytes dividido por 4. Se a função tiver mais de 8k, um registro .xdata completo precisará ser usado no lugar.
  • Tamanho do Quadro é um campo de 9 bits que indica o número de bytes de pilha alocados para essa função dividido por 16. Funções que alocam mais de (8-16) bytes de pilha devem usar um registro .xdata completo. Inclui a área de variável local, a área de parâmetro de saída, a área de Int e FP salva pelo receptor e a área do parâmetro inicial. Exclui a área de alocação dinâmica.
  • CR é um sinalizador de 2 bits que indica se a função inclui instruções extras para configurar uma cadeia de quadros e um link de retorno:
    • 00 = função desencadeada, <x29,lr> o par não é salvo na pilha
    • 01 = função desencadeada, <lr> é salva na pilha
    • 10 = função encadeada com um pacibsp endereço de retorno assinado
    • 11 = função encadeada, uma instrução de par de armazenamento / carga é usada no prólogo / epílogo <x29,lr>
  • H é um sinalizador de 1 bit que indica se a função abriga os registros de parâmetro inteiro (x0-x7) armazenando-os no início da função. (0 = não hospeda registros, 1 = hospeda registros).
  • RegI é um campo de 4 bits que indica o número de registros INT não voláteis (x19-x28) salvos no local da pilha canônica.
  • RegF é um campo de 3 bits que indica o número de registros FP não voláteis (d8-d15) salvos no local da pilha canônica. (RegF=0: nenhum registro FP é salvo; RegF>0: RegF+1 registros FP são salvos). Os dados de desenrolamento empacotados não podem ser usados para a função que salva apenas um registro FP.

Os prólogos canônicos que se enquadram nas categorias 1, 2 (sem área de parâmetro de saída), 3 e 4 na seção acima podem ser representados pelo formato de desenrolamento empacotado. Os epílogos para funções canônicas seguem uma forma semelhante, exceto que H não tem efeito, a instrução set_fp é omitida e a ordem das etapas e as instruções em cada etapa são revertidas no epílogo. O algoritmo para .xdata empacotado segue estas etapas, detalhadas na seguinte tabela:

Etapa 0: pré-computar o tamanho de cada área.

Passo 1: Assine o endereço do remetente.

Etapa 2: Salve os registros salvos pelo receptor Int.

Passo 3: Esta etapa é específica para o tipo 4 nas primeiras seções. lr é salvo no final da área Int.

Etapa 4: Salve os registros salvos pelo receptor FP.

Etapa 5: salve os argumentos de entrada na área de parâmetros da página inicial.

Etapa 6: Aloque a pilha restante, incluindo área local, <x29,lr> par e área de parâmetro de saída. 6a corresponde ao tipo canônico 1. 6b e 6c são para o tipo canônico 2. 6d e 6e são para o tipo 3 e o tipo 4.

Etapa nº Valores de sinalizador Número de instruções Opcode Desenrolar código
0 #intsz = RegI * 8;
if (CR==01) #intsz += 8; // lr
#fpsz = RegF * 8;
if(RegF) #fpsz += 8;
#savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf)
#locsz = #famsz - #savsz
1 CR == 10 1 pacibsp pac_sign_lr
2 0 <RegI<= 10 RegI / 2 +
RegI % 2
stp x19,x20,[sp,#savsz]!
stp x21,x22,[sp,#16]
...
save_regp_x
save_regp
...
3 CR == 01* 1 str lr,[sp,#(intsz-8)]* save_reg
4 0 <RegF<= 7 (RegF + 1) / 2 +
(RegF + 1) % 2)
stp d8,d9,[sp,#intsz]**
stp d10,d11,[sp,#(intsz+16)]
...
str d(8+RegF),[sp,#(intsz+fpsz-8)]
save_fregp
...
save_freg
5 H == 1 4 stp x0,x1,[sp,#(intsz+fpsz)]
stp x2,x3,[sp,#(intsz+fpsz+16)]
stp x4,x5,[sp,#(intsz+fpsz+32)]
stp x6,x7,[sp,#(intsz+fpsz+48)]
nop
nop
nop
nop
6a (CR == 10 || CR == 11) &&
#locsz<= 512
2 stp x29,lr,[sp,#-locsz]!
mov x29,sp***
save_fplr_x
set_fp
6 bilhões (CR == 10 || CR == 11) &&
512 <#locsz<= 4080
3 sub sp,sp,#locsz
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
save_fplr
set_fp
6c (CR == 10 || CR == 11) &&
#locsz> 4080
4 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
alloc_s/alloc_m
save_fplr
set_fp
6d (CR == 00 || CR == 01) &&
#locsz<= 4080
1 sub sp,sp,#locsz alloc_s/alloc_m
6e (CR == 00 || CR == 01) &&
#locsz> 4080
2 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
alloc_m
alloc_s/alloc_m

* Se CR == 01 e RegI for um número ímpar, o passo 3 e o último save_reg no passo 2 são mesclados em um save_regp.

** Se RegI == CR == 0 e RegF != 0, o primeiro stp para o ponto flutuante fará o pré-decremento.

Nenhuma instrução correspondente a mov x29,sp está presente no epílogo. Os dados de desenrolamento compactados não poderão ser usados se uma função exigir a restauração de sp de .x29

Como desenrolar prólogos e epílogos parciais

Nas situações de desenrolamento mais comuns, a exceção ou chamada ocorre no corpo da função, longe do prólogo e de todos os epílogos. Nessas situações, o desenrolamento é simples: o desenrolador simplesmente executa os códigos na matriz de desenrolamento. Ele começa no índice 0 e continua até que um end opcode seja detectado.

É mais difícil desenrolar corretamente no caso em que ocorre uma exceção ou interrupção durante a execução de um prólogo ou epílogo. Nessas situações, o registro de ativação é construído apenas parcialmente. O problema é determinar exatamente o que foi feito para desfazê-lo do modo correto.

Por exemplo, use esta sequência de prólogo e epílogo:

0000:    stp    x29,lr,[sp,#-256]!          // save_fplr_x  256 (pre-indexed store)
0004:    stp    d8,d9,[sp,#224]             // save_fregp 0, 224
0008:    stp    x19,x20,[sp,#240]           // save_regp 0, 240
000c:    mov    x29,sp                      // set_fp
         ...
0100:    mov    sp,x29                      // set_fp
0104:    ldp    x19,x20,[sp,#240]           // save_regp 0, 240
0108:    ldp    d8,d9,[sp,224]              // save_fregp 0, 224
010c:    ldp    x29,lr,[sp],#256            // save_fplr_x  256 (post-indexed load)
0110:    ret    lr                          // end

Próximo a cada opcode existe o código de desenrolamento apropriado que descreve esta operação. Você pode ver como a série de códigos de desenrolamento para o prólogo é uma imagem de espelho exata dos códigos de desenrolamento para o epílogo (sem contar a instrução final do epílogo). É uma situação comum: é por isso que sempre presumimos que os códigos de desenrolamento do prólogo são armazenados em ordem inversa da ordem de execução do prólogo.

Portanto, para o prólogo e o epílogo, ficamos com um conjunto comum de códigos de desenrolamento:

set_fp, save_regp 0,240, save_fregp,0,224, save_fplr_x_256, end

O caso do epílogo é simples, já que está em ordem normal. Começando no deslocamento 0 dentro do epílogo (que começa no deslocamento 0x100 na função), esperamos que a sequência completa de desenrolamento seja executada, pois nenhuma limpeza foi feita até o momento. Se encontrarmos uma instrução (no deslocamento 2 no epílogo), poderemos desenrolar com êxito ignorando o primeiro código de desenrolamento. Podemos generalizar essa situação e supor um mapeamento 1:1 entre códigos opcodes e códigos de desenrolamento. Em seguida, para começar a desenrolar a instrução n no epílogo, devemos ignorar os primeiros n códigos de desenrolamento e começar a executar daí.

Acontece que uma lógica semelhante funciona para o prólogo, porém, no sentido inverso. Se começamos a desenrolar do deslocamento 0 no prólogo, não queremos executar nada. Se desenrolamos do deslocamento 2, que é uma instrução, queremos começar a executar a sequência de desenrolamento de um código de desenrolamento começando pelo final. (Lembre-se, os códigos são armazenados na ordem inversa.) E aqui também, podemos generalizar: se começarmos a desenrolar a partir da instrução n no prólogo, devemos começar a executar n códigos de desenrolamento a partir do final da lista de códigos.

Os códigos de prólogo e epílogo nem sempre correspondem exatamente, assim, pode ser necessário que a matriz de desenrolamento contenha várias sequências de códigos. Para determinar o deslocamento de em que ponto começar a processar códigos, use a seguinte lógica:

  1. Se estiver desenrolando de dentro do corpo da função, comece a executar códigos de desenrolamento no índice 0 e continue até atingir um end opcode.

  2. Se você estiver desenrolando de dentro de um epílogo, use o índice inicial específico do epílogo fornecido com o escopo do epílogo como ponto de partida. Compute quantos bytes o PC em questão tem desde o início do epílogo. Então avance pelos códigos de desenrolamento, pulando códigos de desenrolamento até que todas as instruções já executadas sejam consideradas. Então execute começando nesse ponto.

  3. Se você estiver desenrolando de dentro do prólogo, use o índice 0 como seu ponto de partida. Compute o comprimento do código do prólogo com base na sequência e compute quantos bytes o PC está do final do prólogo. Então avance pelos códigos de desenrolamento, pulando códigos de desenrolamento até que todas as instruções ainda não executadas sejam consideradas. Então execute começando nesse ponto.

Essas regras significam que os códigos de desenrolamento para o prólogo sempre devem ser os primeiros da matriz. E eles também são os códigos usados para desenrolar no caso geral de desenrolamento de dentro do corpo. Todas as sequências de código específicas do epílogo devem vir imediatamente após.

Fragmentos de função

Para fins de otimização de código e outros motivos, pode ser preferível dividir uma função em fragmentos separados (também chamados de regiões). Quando dividido, cada fragmento de função resultante requer um registro próprio separado .pdata (e possivelmente .xdata).

Para cada fragmento secundário separado que tem o próprio prólogo, espera-se que nenhum ajuste de pilha seja feito em seu prólogo. Todo o espaço de pilha exigido por uma região secundária deve ser pré-alocado por sua região pai (ou chamada de região de host). Essa pré-localização mantém a manipulação de ponteiro de pilha estritamente no prólogo original da função.

Um caso típico de fragmentos de função é a "separação de código", em que o compilador pode mover uma região de código para fora de sua função host. Existem três casos incomuns que podem resultar da separação de código.

Exemplo

  • (região 1: início)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (região 1: fim)

  • (região 3: início)

        ...
    
  • (região 3: fim)

  • (região 2: início)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (região 2: fim)

  1. Somente prólogo (região 1: todos os epílogos estão em regiões separadas):

    Somente o prólogo deve ser descrito. Esse prólogo não pode ser representado no formato .pdata compacto. No caso de .xdata completo, ele pode ser representado definindo Contagem de Epílogos = 0. Veja a região 1 no exemplo acima.

    Códigos de desenrolamento: set_fp, save_regp 0,240, save_fplr_x_256, end.

  2. Somente epílogos (região 2: o prólogo está na região do host)

    Supõe-se que, no momento em que o controle salta para essa região, todos os códigos de prólogo tenham sido executados. O desenrolamento parcial pode ocorrer em epílogos da mesma forma que em uma função normal. Esse tipo de região não pode ser representado pelo .pdata compacto. Em um registro completo .xdata , ele pode ser codificado com um prólogo "fantasma", entre parênteses por um end_c par de códigos e end desenrolamento. O end_c à esquerda indica que o tamanho do prólogo é zero. O índice de início do epílogo referente ao epílogo único aponta para set_fp.

    Código de desenrolamento para a região 2: end_c, set_fp, save_regp 0,240, save_fplr_x_256, end.

  3. Sem prólogos ou epílogos (região 3: os prólogos e todos os epílogos estão em outros fragmentos):

    O formato .pdata compacto pode ser aplicado por meio da configuração sinalizador = 10. Com registro .xdata completo, Contagem de epílogos = 1. O código de desenrolamento é o mesmo que o código da região 2 acima, mas o Índice de Início do epílogo também aponta para end_c. O desenrolamento parcial nunca ocorrerá nesta região de código.

Outro caso mais complicado de fragmentos de função é "embalar a vácuo". O compilador pode optar por adiar o salvamento de alguns registros salvos pelo receptor até que estejam fora do prólogo da entrada da função.

  • (região 1: início)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (região 2: início)

        stp     x21,x22,[sp,#224]       // save_regp 2, 224
        ...
        ldp     x21,x22,[sp,#224]       // save_regp 2, 224
    
  • (região 2: fim)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (região 1: fim)

Na caixa de diálogo da região 1, o espaço de pilha é pré-alocado. Você pode ver que a região 2 terá o mesmo código de desenrolamento, mesmo que ele seja movido para fora de sua função de host.

Região 1: set_fp, save_regp 0,240, save_fplr_x_256, end. O Epilog Start Index aponta para set_fp como de costume.

Região 2: save_regp 2, 224, end_c, set_fp, save_regp 0,240, save_fplr_x_256, end. O Índice de Início do Epílogo aponta para o primeiro código de desenrolamento save_regp 2, 224.

Funções grandes

Fragmentos podem ser usados para descrever funções maiores que o limite de 1 M imposto pelos campos de bits no cabeçalho .xdata. Para descrever uma função extraordinariamente grande como esta, é preciso dividi-la em fragmentos menores que 1M. Cada fragmento deve ser ajustado para que não divida um epílogo em várias partes.

Apenas o primeiro fragmento da função contém um prólogo e todos os outros fragmentos são marcados como não tendo prólogo. Dependendo do número de epílogos presentes, cada fragmento pode conter zero ou mais epílogos. Lembre-se que cada escopo de epílogo em um fragmento especifica seu deslocamento inicial com relação ao início do fragmento, e não ao início da função.

Se um fragmento não tiver prólogo nem epílogo, ele ainda exigirá o próprio registro .pdata (e, possivelmente, .xdata) para descrever como desenrolar de dentro do corpo da função.

Exemplos

Exemplo 1: forma compactada e encadeada por quadros

|Foo|     PROC
|$LN19|
    str     x19,[sp,#-0x10]!        // save_reg_x
    sub     sp,sp,#0x810            // alloc_m
    stp     fp,lr,[sp]              // save_fplr
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...

|$pdata$Foo|
    DCD     imagerel     |$LN19|
    DCD     0x416101ed
    ;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]

Exemplo 2: Encadeamento de quadros, forma completa com espelho Prolog & Epilog

|Bar|     PROC
|$LN19|
    stp     x19,x20,[sp,#-0x10]!    // save_regp_x
    stp     fp,lr,[sp,#-0x90]!      // save_fplr_x
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...
                                    // begin of epilog, a mirror sequence of Prolog
    mov     sp,fp
    ldp     fp,lr,[sp],#0x90
    ldp     x19,x20,[sp],#0x10
    ret     lr

|$pdata$Bar|
    DCD     imagerel     |$LN19|
    DCD     imagerel     |$unwind$cse2|
|$unwind$Bar|
    DCD     0x1040003d
    DCD     0x1000038
    DCD     0xe42291e1
    DCD     0xe42291e1
    ;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
    ;Epilog Start Index[0], Epilog Start Offset[56]
    ;set_fp
    ;save_fplr_x
    ;save_r19r20_x
    ;end

O Índice de Início do Epílogo [0] aponta para a mesma sequência de código de desenrolamento do prólogo.

Exemplo 3: função não encadeada variádica

|Delegate| PROC
|$LN4|
    sub     sp,sp,#0x50
    stp     x19,lr,[sp]
    stp     x0,x1,[sp,#0x10]        // save incoming register to home area
    stp     x2,x3,[sp,#0x20]        // ...
    stp     x4,x5,[sp,#0x30]
    stp     x6,x7,[sp,#0x40]        // end of prolog
    ...
    ldp     x19,lr,[sp]             // beginning of epilog
    add     sp,sp,#0x50
    ret     lr

    AREA    |.pdata|, PDATA
|$pdata$Delegate|
    DCD     imagerel |$LN4|
    DCD     imagerel |$unwind$Delegate|

    AREA    |.xdata|, DATA
|$unwind$Delegate|
    DCD     0x18400012
    DCD     0x200000f
    DCD     0xe3e3e3e3
    DCD     0xe40500d6
    DCD     0xe40500d6
    ;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
    ;Epilog Start Index[4], Epilog Start Offset[15]
    ;nop        // nop for saving in home area
    ;nop        // ditto
    ;nop        // ditto
    ;nop        // ditto
    ;save_lrpair
    ;alloc_s
    ;end

O Índice de Início do Epílogo [4] aponta para o meio do código de desenrolamento do Prólogo (matriz de desenrolamento parcialmente reutilizada).

Confira também

Visão geral das convenções ARM64 ABI
Tratamento de exceção do ARM