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 originalsp
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
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.
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)
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
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, osub 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.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-lrstp
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.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.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 emsp
.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.
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:
Esses dados são divididos em quatro seções:
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:
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.
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.
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.
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.
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 lr pacibsp |
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:
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.
- 00 = dados de desenrolamento empacotados não usados; os bits restantes apontam para um registro
- 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>
- 00 = função desencadeada,
- 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:
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.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.
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)
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
.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 umend_c
par de códigos eend
desenrolamento. Oend_c
à esquerda indica que o tamanho do prólogo é zero. O índice de início do epílogo referente ao epílogo único aponta paraset_fp
.Código de desenrolamento para a região 2:
end_c
,set_fp
,save_regp 0,240
,save_fplr_x_256
,end
.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 paraend_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