Partilhar via


Tratamento de exceções ARM64

O Windows no ARM64 usa o mesmo mecanismo estruturado de tratamento de exceções para exceções assíncronas geradas por hardware e exceções síncronas geradas por software. Os manipuladores de exceção específicos do idioma são criados com base no tratamento de exceções estruturadas do Windows usando funções auxiliares de 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 MSVC.

Objetivos e motivação

A exceção desenrolando convenções de dados, e esta descrição, destinam-se a:

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

    • A análise do código requer que este seja carregado na memória. Evita o desenrolar 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 deve retornar à decodificação de instruções. A decodificação de instruções aumenta a complexidade geral e, idealmente, deve ser evitada.

  • Suporte ao desenrolar em meio-prólogo e meio-epílogo.

    • O desenrolamento é usado no Windows para mais do que o tratamento de exceções. É fundamental que o código consiga desenrolar-se com precisão, mesmo quando está no meio de uma sequência de código de prolog ou epilog.
  • Ocupe uma quantidade mínima de espaço.

    • Os códigos de descompactação não devem ser agregados de forma a aumentar significativamente o tamanho binário.

    • Como é provável que os códigos de desenrolamento estejam bloqueados na memória, uma pequena dimensão garante uma sobrecarga mínima para cada binário carregado.

Suposições

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

  • Prólogos e epílogos tendem a espelhar-se. Aproveitando essa característica comum, o tamanho dos metadados necessários para descrever o desenrolamento pode ser muito reduzido. 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 de maneira progressiva. Ambos devem produzir resultados idênticos.

  • As funções tendem a ser, em geral, relativamente pequenas. Várias otimizações no espaço dependem deste facto para alcançar o empacotamento de dados mais eficiente.

  • 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 durante toda a função. Isso significa que o sp original pode ser recuperado a qualquer momento.

  • A menos que o sp seja salvo noutro registo, toda a manipulação do ponteiro da pilha ocorre estritamente no prólogo e no epílogo.

  • O layout da estrutura de pilha é organizado conforme descrito na próxima seção.

Disposição da estrutura de pilha ARM64

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

Para funções encadeadas de quadros, o par fp e lr pode ser salvo em qualquer posição na área 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 ponteiro de pilha (sp). No entanto, para as funções alloca, estas devem ser encadeadas e x29 deve apontar para o fundo da pilha. Para permitir uma melhor cobertura do modo de endereçamento de pares de registros, as áreas salvas de registro não voláteis são posicionadas na parte superior da pilha de área local. Aqui estão exemplos que ilustram várias das sequências prolog mais eficientes. Por uma questão de clareza e melhor localização de cache, a ordem de armazenamento de registros salvos por destinatários em todos os prólogos canônicos está em ordem de "crescimento". #framesz abaixo representa o tamanho de toda a pilha (excluindo a área alloca). #localsz e #outsz denotam o tamanho da área local (incluindo a área de salvamento para o par <x29, lr>) e o tamanho do 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. Acorrentado, #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. Funções de folha não encadeadas (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 dos registos for movida para a parte inferior da pilha. A desvantagem é que não é consistente com outros layouts acima. E os registos salvos fazem parte do intervalo para registos em pares e modo de endereçamento com deslocamento pré e pós-indexado.

  4. Funções não foliares e não encadeadas (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
    

    Apenas x19 guardados:

        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 salvaguarda reg não é integrada no stp porque um stp reg-lr pré-indexado não pode ser representado com os códigos de desempacotamento.

    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 com o primeiro exemplo de prolog acima, este exemplo tem uma vantagem: todas as instruções de salvamento de registro estão prontas para serem executadas após apenas uma instrução de alocação de pilha. Isso significa que não há antidependência de sp que impeça o paralelismo no nível de instrução.

  6. Encadeado, tamanho do 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 de deslocamento "reg-pair" e pré/pós-indexado. Os locais abaixo dos ponteiros do quadro podem ser acessados com base em sp.

  7. Encadeado, tamanho do 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 de tratamento de exceções ARM64

.pdata registos

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 expressão "manipulação de pilha" é significativa: funções de folha que não exigem 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 economizar espaço. Um descontrair de uma dessas funções pode obter o endereço de retorno diretamente do lr para passar para o chamador.

Cada registro .pdata para ARM64 tem 8 bytes de comprimento. O formato geral de cada registo 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:

  • Function Start RVA é 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 igual a 0, os bits restantes formarão uma Informações de Exceção RVA (com os dois bits mais baixos implicitamente 0). Se o Flag for diferente de zero, os bits restantes formarão uma estrutura de Dados Empacotados de Desenrolamento .

  • Exception Information RVA é o endereço da estrutura de informações de exceção de comprimento variável, armazenada na seção .xdata. Os dados devem estar alinhados em 4 bytes.

  • Packed Unwind Data é uma descrição compactada das operações necessárias para desenrolar de uma função, assumindo uma forma canônica. Neste caso, não é necessário o registo .xdata.

.xdata registos

Quando o formato de desenrolar compactado é insuficiente para descrever o desenrolamento de uma função, deve ser criado um registo .xdata de comprimento variável. O endereço deste registo é armazenado na segunda palavra do registo .pdata. O formato do .xdata é um conjunto compactado de palavras de comprimento variável:

layout de registro .xdata.

Estes dados dividem-se em quatro secçõ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 só estará presente se os campos Epilog Count e Code Words estiverem definidos como 0. O cabeçalho tem estes campos de bits:

    a) Comprimento de Função é um campo de 18 bits. Ele indica o comprimento total da função em bytes, dividido por 4. Se uma função for maior que 1M, vários registros de .pdata e .xdata devem ser usados para descrever a função. Para obter mais informações, consulte a seção Funções grandes.

    b) Vers é um campo de 2 bits. Descreve a versão dos restantes .xdata. Atualmente, apenas a versão 0 é definida, portanto, valores de 1-3 não são permitidos.

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

    d. E é um campo de 1 bit. Ele indica que as informações que descrevem um único epílogo são compactadas no cabeçalho (1) em vez de exigir mais palavras de escopo posteriormente (0).

    e. O campo Contagem de Epílogos é de 5 bits e tem dois significados, dependendo do estado do bit E.

    1. Se e for igual a 0, especifica o número total de escopos de epílogo descritos na seção 2. Se existirem mais de 31 escopos na função, o campo Code Words deve ser definido como 0 para indicar que uma palavra de extensão é necessária.

    2. Se e for igual a 1, este campo especifica o índice do primeiro código de desenrolar que descreve o único e apenas epílogo.

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

    g. Extended Epilog Count e Extended Code Words são campos de 16 bits e 8 bits, respectivamente. Eles fornecem mais espaço para codificar um número excepcionalmente grande de epílogos, ou um número excepcionalmente grande de palavras de código de desenrolamento. A palavra de extensão que contém esses campos só estará presente se os campos Epilog Count e Code Words na primeira palavra do cabeçalho forem 0.

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

    a) Epilog Start Offset é um campo de 18 bits que indica o deslocamento, em bytes, do epílogo em relação ao início da função, dividido por 4.

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

    c. Epilog Start Index é um campo de 10 bits (2 bits a mais do que Extended Code Words). Ele indica o índice de byte do primeiro código de desenrolamento que descreve este epílogo.

  3. Após a lista de escopos de epílogo, vem uma matriz de bytes que contém códigos de desenrolamento, descritos em detalhe numa seção posterior. Esta matriz é preenchida no final até ao mais próximo limite de palavra completa. Os códigos de desmontagem são gravados neste array. 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 desenrolar são armazenados em ordem big-endian para que o byte mais significativo seja buscado primeiro, o que identifica a operação e o comprimento do restante do código.

  4. Finalmente, após os bytes do código de desenrolamento, se o bit X no cabeçalho estiver definido para 1, segue-se a informação do manipulador de exceções. Ele consiste em um único Exception Handler RVA que fornece o endereço do próprio manipulador de exceções. Ele é seguido imediatamente por uma quantidade de dados de comprimento variável exigida pelo manipulador de exceções.

O registro .xdata foi projetado para que seja possível buscar os primeiros 8 bytes e usá-los para calcular o tamanho total do registro, menos o comprimento dos dados de exceção de tamanho variável a seguir. O trecho de código a seguir 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 o seu próprio índice nos códigos de desenrolamento, a tabela é compartilhada entre eles. É totalmente possível (e não totalmente incomum) que todos eles possam compartilhar os mesmos códigos. (Para obter um exemplo, consulte o Exemplo 2 na seção Exemplos.) Os programadores de compiladores devem otimizar para este caso em particular. Isso deve-se ao facto de que o maior índice que pode ser especificado é 255, o que limita o número total de códigos de desenrolar para uma determinada função.

Desenrolar códigos

A matriz de códigos de desenrolamento é um pool de sequências que descrevem 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 desenrolar podem ser considerados como um pequeno conjunto de instruções, codificado como uma sequência de bytes. Quando a execução estiver concluída, o endereço de retorno para a função de chamada estará no registro lr. E, todos os registros não voláteis são restaurados para seus valores no momento em que a função foi chamada.

Se fosse garantido que as exceções ocorressem apenas dentro de um corpo de função, e nunca dentro de um prólogo ou de qualquer epílogo, então apenas uma única sequência seria necessária. No entanto, o modelo de desenrolamento do Windows exige que o código possa desenrolar-se no interior de um prólogo ou epílogo em execução parcial. Para atender a esse requisito, os códigos de desenrolar foram cuidadosamente projetados para que eles mapeem inequivocamente 1:1 para cada opcode relevante no prólogo e no epílogo. Este desenho tem várias implicações:

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

  • Ao contar o número de instruções após o 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 completar o desenrolar parcialmente realizado pelo epílogo.

  • Ao contar o número de instruções até ao final do prólogo, é possível ignorar o número equivalente de códigos de desenrolamento. Podemos executar o resto da sequência para desfazer apenas as partes do prólogo que já foram executadas completamente.

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

Código de desmontagem Bits e interpretação
alloc_s 000xxxxx: alocar uma pilha pequena com tamanho < 512 (2^5 * 16).
save_r19r20_x 001zzzzz: salve <x19,x20> par em [sp-#Z*8]!, deslocamento pré-indexado >= -248
save_fplr 01zzzzzz: salvar par de <x29,lr> em [sp+#Z*8], deslocamento <= 504.
save_fplr_x 10zzzzzz: Guardar par de <x29,lr> em [sp-(#Z+1)*8]!, com deslocamento pré-indexado >= -512
alloc_m 11000xxx'xxxxxxxx: alocar grande stack com tamanho < 32K (2^11 * 16).
save_regp 110010xx'xxzzzzzz: guardar o par x(19+#X) em [sp+#Z*8], deslocamento <= 504
save_regp_x 110011xx'xxzzzzzz: guardar par x(19+#X) em [sp-(#Z+1)*8]!, >de deslocamento pré-indexado = -512
save_reg 110100xx'xxzzzzzz: salvar o registro x(19+#X) em [sp+#Z*8], deslocamento <= 504
save_reg_x 1101010x'xxxzzzzz: Guardar reg 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 o par d(8+#X) em [sp+#Z*8], deslocamento <= 504
save_fregp_x 1101101x'xxzzzzzz: guardar par d(8+#X) em [sp-(#Z+1)*8]!, com deslocamento pré-indexado >= -512
save_freg 1101110x'xxzzzzzz: guardar o registo d(8+#X) em [sp+#Z*8], com deslocamento <= 504
save_freg_x 11011110'xxxzzzzz: guardar reg d(8+#X) em [sp-(#Z+1)*8]!, com deslocamento pré-indexado >= -256
alloc_z 11011111'zzzzzzzzzz: alocar pilha com tamanho z * SVE-VL
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 desanuviamento é 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.
save_any_xreg 11100111'0pxrrrrr'00oooooo: salvar registro(s)
  • p: 0/1 => único X(#r) vs par X(#r) + X(#r+1)
  • x: 0/1 => deslocamento de pilha pré-indexado positivo vs negativo
  • o: deslocamento = o * 16, se x=1 ou p=1, senão o * 8
(Windows >= 11 necessário)
save_any_dreg 11100111'0pxrrrrr'01oooooo: salvar registro(s)
  • p: 0/1 => único D(#r) vs par D(#r) + D(#r+1)
  • x: 0/1 => deslocamento de pilha pré-indexado positivo vs negativo
  • o: deslocamento = o * 16, se x=1 ou p=1, senão o * 8
(Windows >= 11 necessário)
save_any_qreg 11100111'0pxrrrrr'10oooooo: salvar registro(s)
  • p: 0/1 => único Q(#r) vs par Q(#r) + Q(#r+1)
  • x: 0/1 => deslocamento de pilha pré-indexado positivo vs negativo
  • o: deslocamento = o * 16
(Windows >= 11 necessário)
save_zreg 11100111'0oo0rrrr'11oooooo: salvar reg Z(#r+8) em [sp + #o * VL], (Z8 a Z23)
save_preg 11100111'0oo1rrrr'11oooooo: salvar reg P(#r) em [sp + #o * (VL / 8)], (P4 até P15; r valores [0, 3] são reservados)
11100111'1yyyyyy': reservado
11101xxx: Reservado para casos de pilha personalizados abaixo gerados apenas para rotinas 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'yyyyyy : reservado
11111001'yyyyyyyy'yyyyyyyy : reservado
11111010'yyyyyyyy'yyyyyyyy'yyyyyyyy : reservado
11111011'yyyyyyyy'yyyyyyyy'yyyyyyyy'yyyyyyyy : reservado
pac_sign_lr 11111100: assine o endereço de retorno em lr com pacibsp
11111101: reservado
11111110: reservado
11111111: reservado

Em instruções com valores grandes cobrindo vários bytes, os bits mais significativos são armazenados primeiro. Este design torna possível encontrar o tamanho total em bytes do código de desempacotamento consultando apenas o primeiro byte do código. Como cada código de desenrolar é exatamente mapeado para uma instrução em um prólogo ou epílogo, você pode calcular o tamanho do prólogo ou epílogo. Percorra a sequência do início ao fim e use uma tabela de consulta ou dispositivo semelhante para determinar o comprimento do opcode correspondente.

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

save_next deve seguir um save para um par de registros: save_regp, save_regp_x, save_fregp, save_fregp_x, save_r19r20_xou outro save_next. Ele também pode ser usado em conjunto com save_any_xreg, save_any_dreg ou save_any_qreg mas apenas quando p = 1. Ele salva o próximo par de registro em ordem numericamente crescente para o próximo espaço de pilha. save_next não deve ser usado além do último registo do mesmo tipo.

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

end_c é projetado para lidar com fragmentos de função não contíguos para fins de otimização. Um end_c que indique o fim dos códigos de desenrolar no âmbito atual deve ser seguido por outra série de códigos de desenrolar que terminem com uma endreal. Os códigos de desenrolar 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 compactados

Para funções cujos prólogos e epílogos seguem a forma canônica descrita abaixo, dados de desenrolamento compactados podem ser usados. Ele elimina totalmente a necessidade de um registo .xdata e reduz significativamente o custo de fornecimento de dados de desmontagem. Os prólogos e epílogos canônicos são projetados para atender aos requisitos comuns de uma função simples: uma que não requer um manipulador de exceções e que faz suas operações de configuração e desmontagem em uma ordem padrão.

O formato de um registo de .pdata com dados de desenrolamento compactados é o seguinte:

registro .pdata com dados de desenrolar compactados.

Os campos são os seguintes:

  • Function Start RVA é 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 embalados não utilizados; os bits restantes apontam para um registo .xdata
    • 01 = dados de desenrolamento compactados usados com um único prólogo e epílogo no início e no final do escopo
    • 10 = dados de desenrolamento compactados usados para código sem qualquer 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 de toda a função em bytes, dividido por 4. Se a função for maior que 8k, um registro de .xdata completo deve ser usado.
  • O Tamanho do Quadro é um campo de 9 bits que indica o número de bytes da pilha alocados para esta função, dividido por 16. As funções que alocam mais de (8k-16) bytes de pilha devem usar um registro de .xdata completo. Inclui a área variável local, a área de parâmetros de saída, a área Int e FP salva pelo destinatário e a área de parâmetros domésticos. 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 não encadeada, par de <x29,lr> não é armazenado na pilha
    • 01 = função desencadeada, <lr> é guardado na pilha
    • 10 = função encadeada com um endereço de retorno assinado pacibsp
    • 11 = função encadeada, uma instrução de pares de armazenamento e carga é usada em prólogo/epílogo <x29,lr>
  • H é um sinalizador de 1 bit que indica se a função abriga os registradores de parâmetros inteiros (x0-x7) armazenando-os no início da função. (0 = não regista a casa, 1 = regista a casa).
  • RegI é um campo de 4 bits que indica o número de registradores INT não voláteis (x19-x28) salvos no local canônico da pilha.
  • RegF é um campo de 3 bits que indica o número de registos FP não voláteis (d8-d15) guardados na localização canónica da pilha. (RegF=0: nenhum registro FP é salvo; RegF>0: os registos FP RegF+1 são guardados). Os dados de desenrolamento compactados não podem ser usados para funções que salvam apenas um registro FP.

Prologs 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 desenrolar compactado. Os epílogos para funções canônicas seguem uma forma semelhante, exceto H não tem efeito, a instrução set_fp é omitida e a ordem dos passos e as instruções em cada passo são invertidas no epílogo. O algoritmo para .xdata compactado segue estas etapas, detalhadas na tabela seguinte:

Passo 0: Pré-calcular o tamanho de cada área.

Passo 1: Assine o endereço de retorno.

Passo 2: Salve os registros salvos pelo destinatário Int.

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

Passo 4: Salve os registros FP salvos por destinatários.

Etapa 5: Salve os argumentos de entrada na área de parâmetros home.

Etapa 6: Aloque a pilha restante, incluindo área local, par de <x29,lr> 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 tanto para o tipo 3 como para o tipo 4.

Passo # Valores do sinalizador N.º de instruções Opcode Código de desmontagem
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
6º-A (CR == 10 || CR == 11) &&
#locsz <= 512
2 stp x29,lr,[sp,#-locsz]!
mov x29,sp***
save_fplr_x
set_fp
6 ter (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
6 cêntimos (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
6º D (CR == 00 || CR == 01) &&
#locsz <= 4080
1 sub sp,sp,#locsz alloc_s/alloc_m
6º E (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 fundidos num save_regp.

Se RegI == CR == 0 e RegF != 0, então o primeiro stp para o ponto flutuante realiza o predecréscimo.

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

Desdobramento de prólogos e epílogos parciais

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

É mais difícil realizar o deslocamento 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 quadro de pilha é apenas parcialmente construído. O problema é determinar exatamente o que foi feito, desfazê-lo corretamente.

Por exemplo, tome esta sequência de prólogos e epílogos:

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

Ao lado de cada opcode está o código de desenrolamento apropriado que descreve esta operação. Você pode ver como a série de códigos de desenrolar para o prólogo é uma imagem espelhada exata dos códigos de desenrolar para o epílogo (sem contar a instrução final do epílogo). É uma situação comum: é por isso que sempre assumimos que os códigos de desenrolamento para o prólogo são armazenados em ordem inversa à ordem de execução do prólogo.

Assim, tanto para o prólogo quanto para 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 epilog é simples, uma vez 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 ainda não foi feita qualquer limpeza. Se encontrarmos uma instrução em (no deslocamento 2 no epílogo), podemos relaxar com sucesso ignorando o primeiro código de desenrolamento. Podemos generalizar essa situação e assumir um mapeamento 1:1 entre opcodes e unwind codes. Então, para começar a desenrolar a partir da instrução n no epílogo, devemos saltar os primeiros códigos de desenrolamento n e começar a execução a partir daí.

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

Os códigos Prolog e epilog nem sempre correspondem exatamente, e é por isso que a matriz unwind pode precisar conter várias sequências de códigos. Para determinar o deslocamento a partir de onde começar a processar códigos, use a seguinte lógica:

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

  2. Se estiver a desenrolar a partir 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. Calcule quantos bytes o PC em questão tem desde o início do epílogo. Em seguida, avance pelos códigos de desenrolar, ignorando os códigos de desenrolar até que todas as instruções já executadas sejam contabilizadas. Em seguida, execute a partir desse ponto.

  3. Se desenrolar a partir do prólogo, use o índice 0 como ponto de partida. Calcule o comprimento do código prolog a partir da sequência e, em seguida, calcule quantos bytes o PC em questão está a partir do final do prolog. Em seguida, avance pelos códigos de desenrolamento, ignorando-os até que todas as instruções ainda não executadas sejam contabilizadas. Em seguida, execute a partir desse ponto.

Essas regras indicam que os códigos de desenrolar para o prólogo devem ser sempre os primeiros na matriz. E, também, são os códigos usados para desenfazer no caso geral de desenfazer dentro do corpo. Quaisquer sequências de código específicas do epílogo devem seguir-se imediatamente a seguir.

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 separado .pdata (e possivelmente .xdata).

Para cada fragmento secundário separado que tem o seu próprio prólogo, espera-se que não seja feito nenhum ajuste de pilha no seu prólogo. Todo o espaço de pilha necessário para uma região secundária deve ser pré-alocado pela sua região pai (também conhecida como região hospedeira). Esta pré-alocação mantém o ajuste do ponteiro da pilha estritamente no prólogo da função original.

Um caso típico de fragmentos de função é a "separação de código", onde o compilador pode mover uma região de código para fora de sua função host. Há 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. Apenas Prolog (região 1: todos os epílogos estão em regiões separadas):

    Apenas o prólogo deve ser descrito. Este prólogo não pode ser representado no formato de .pdata compacto. No caso de .xdata completo, ele pode ser representado definindo Epilog Count = 0. Ver região 1 no exemplo acima.

    Desenrolar códigos: set_fp, save_regp 0,240, save_fplr_x_256, end.

  2. Apenas epílogos (região 2: o prólogo está na região anfitriã)

    Supõe-se que, no momento em que o controle salta para esta região, todos os códigos de prolog tenham sido executados. O desenrolar parcial pode acontecer em epílogos da mesma forma que numa função normal. Este tipo de região não pode ser representado por um .pdatacompacto. Em um registo de .xdata completo, ele pode ser codificado com um prólogo "fantasma", entre um par de códigos de desapilhamento end_c e end. O end_c indica que o tamanho do prólogo é zero. O índice de início do epílogo do único epílogo aponta para set_fp.

    Desenrolar código 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: prólogos e todos os epílogos estão em outros fragmentos):

    O formato .pdata compacto pode ser aplicado através da definição de Flag = 10. Com registro de .xdata completo, Contagem de Epilog = 1. O código Unwind é o mesmo que o código da região 2 acima, mas o Epilog Start Index também aponta para end_c. O desanuviamento parcial nunca acontecerá nesta região do código.

Outro caso mais complicado de fragmentos de função é o "shrink wrapping". O compilador pode optar por adiar o salvamento de alguns registos guardados pelo callee até estar 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)

No prólogo da região 1, o espaço da pilha é pré-alocado. Você pode ver que a região 2 terá o mesmo código de desenrolamento, mesmo que tenha sido movida 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 Epilog Start Index aponta para o primeiro código de desenrolamento save_regp 2, 224.

Grandes funções

Os fragmentos podem ser usados para descrever funções maiores do que o limite de 1M imposto pelos campos de bits no cabeçalho .xdata. Para descrever uma função excepcionalmente grande como esta, ela precisa ser dividida em fragmentos menores que 1M. Cada fragmento deve ser ajustado para que não divida um epílogo em vários pedaços.

Apenas o primeiro fragmento da função conterá um prólogo; 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 de que cada escopo de epílogo em um fragmento especifica seu deslocamento inicial em relação ao início do fragmento, não ao início da função.

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

Exemplos

Exemplo 1: Quadros em cadeia e em forma compacta

|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 frames, completo 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 Epilog Start Index [0] aponta para a mesma sequência do código de desenrolamento do Prolog.

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 Epilog Start Index [4] aponta para o meio do código de desenrolar Prolog (reutilizar parcialmente a matriz de desenrolamento).

Ver também

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