Noções básicas do ABI Arm64EC e do código de assembly
Arm64EC ("Emulation Compatible") é uma nova interface binária de aplicativo (ABI) para criar aplicativos para o Windows 11 no Arm. Para obter uma visão geral do Arm64EC e como começar a criar aplicativos Win32 como Arm64EC, consulte Usando o Arm64EC para criar aplicativos para o Windows 11 em dispositivos Arm.
O objetivo deste documento é fornecer uma visão detalhada da ABI Arm64EC com informações suficientes para que um desenvolvedor de aplicativos escreva e depure código compilado para Arm64EC, incluindo depuração de baixo nível/assembler e escrita de código assembly visando a ABI Arm64EC.
Projeto de Arm64EC
O Arm64EC foi projetado para oferecer funcionalidade e desempenho de nível nativo, ao mesmo tempo em que fornece interoperabilidade transparente e direta com código x64 executado sob emulação.
Arm64EC é principalmente aditivo para o Classic Arm64 ABI. Muito pouco da ABI clássica foi alterada, mas partes foram adicionadas para permitir a interoperabilidade x64.
Neste documento, o ABI Arm64 padrão original deve ser referido como "ABI clássico". Isso evita a ambiguidade inerente a termos sobrecarregados como "nativo". O Arm64EC, para ser claro, é tão nativo quanto o ABI original.
Arm64EC vs Arm64 ABI clássico
A lista a seguir aponta onde o Arm64EC divergiu do Arm64 Classic ABI.
- Mapeamento de registros e registros bloqueados
- Verificadores de chamadas
- Verificadores de pilha
- Convenção de chamada variádica
São pequenas mudanças quando vistas em perspectiva do quanto todo o ABI define.
Mapeamento de registros e registros bloqueados
Para que haja interoperabilidade em nível de tipo com o código x64, o código Arm64EC é compilado com as mesmas definições de arquitetura de pré-processador que o código x64.
Em outras palavras, _M_AMD64
e _AMD64_
são definidos. Um dos tipos afetados por essa regra é a CONTEXT
estrutura. A CONTEXT
estrutura define o estado da CPU em um determinado ponto. Ele é usado para coisas como Exception Handling
e GetThreadContext
APIs. O código x64 existente espera que o contexto da CPU seja representado como uma estrutura x64 ou, em outras palavras, a estrutura como ela é definida durante a CONTEXT
compilação x64CONTEXT
.
Essa estrutura deve ser usada para representar o contexto da CPU durante a execução do código x64, bem como o código Arm64EC. O código existente não entenderia um novo conceito, como o conjunto de registros da CPU mudando de função para função. Se a estrutura x64 for usada para representar estados de execução Arm64, isso implica que os registradores Arm64 são efetivamente mapeados em registros x64 CONTEXT
.
Isso também implica que quaisquer registradores Arm64 que não podem ser encaixados no x64 CONTEXT
não devem ser usados, pois seus valores podem ser perdidos sempre que uma operação ocorre CONTEXT
(e alguns podem ser assíncronos e inesperados, como a operação de coleta de lixo de um Managed Language Runtime ou um APC).
As regras de mapeamento entre os registradores Arm64EC e x64 são representadas pela ARM64EC_NT_CONTEXT
estrutura nos cabeçalhos do Windows, presentes no SDK. Essa estrutura é essencialmente uma união da estrutura, exatamente como é definida para x64, mas com uma sobreposição de CONTEXT
registro Arm64 extra.
Por exemplo, mapas para , para , para , RCX
RSP
RDX
para X1
SP
X0
, RIP
etc.PC
Também podemos ver como os registros x13
, , , , , , não têm representação e, portanto, v16
x23
v31
x14
x24
x28
-não podem ser usados no Arm64EC.
Essa restrição de uso do registro é a primeira diferença entre o Arm64 Classic e o EC ABIs.
Verificadores de chamadas
Os verificadores de chamadas fazem parte do Windows desde que o Control Flow Guard (CFG) foi introduzido no Windows 8.1. Os verificadores de chamadas são desinfetantes de endereço para ponteiros de função (antes essas coisas eram chamadas de desinfetantes de endereço). Toda vez que o código é compilado com a opção /guard:cf
, o compilador gerará uma chamada extra para a função de verificador pouco antes de cada chamada/salto indireto. A própria função de verificação é fornecida pelo Windows e, para CFG, ela executa uma verificação de validade em relação aos destinos de chamada conhecidos como válidos. Essas informações também estão incluídas em binários compilados com /guard:cf
o .
Este é um exemplo de uso de um verificador de chamadas no Classic Arm64:
mov x15, <target>
adrp x16, __guard_check_icall_fptr
ldr x16, [x16, __guard_check_icall_fptr]
blr x16 ; check target function
blr x15 ; call function
No caso do CFG, o verificador de chamadas simplesmente retornará se o destino for válido, ou falhará rapidamente no processo, se não for. Os verificadores de chamadas têm convenções de chamada personalizadas. Eles tomam o ponteiro de função em um registro não usado pela convenção de chamada normal e preservam todos os registros normais de convenção de chamada. Dessa forma, eles não introduzem derramamento de registros ao seu redor.
Os verificadores de chamadas são opcionais em todas as outras ABIs do Windows, mas obrigatórios no Arm64EC. No Arm64EC, os verificadores de chamadas acumulam a tarefa de verificar a arquitetura da função que está sendo chamada. Eles verificam se a chamada é outra função EC ("Emulation Compatible") ou uma função x64 que deve ser executada sob emulação. Em muitos casos, isso só pode ser verificado em tempo de execução.
Os verificadores de chamadas Arm64EC são construídos sobre os verificadores Arm64 existentes, mas eles têm uma convenção de chamada personalizada ligeiramente diferente. Eles usam um parâmetro extra e podem modificar o registro que contém o endereço de destino. Por exemplo, se o destino for o código x64, o controle deverá ser transferido para a lógica de scaffolding de emulação primeiro.
No Arm64EC, o mesmo uso do verificador de chamadas se tornaria:
mov x11, <target>
adrp x9, __os_arm64x_check_icall_cfg
ldr x9, [x9, __os_arm64x_check_icall_cfg]
adrp x10, <name of the exit thunk>
add x10, x10, <name of the exit thunk>
blr x9 ; check target function
blr x11 ; call function
Ligeiras diferenças em relação ao Classic Arm64 incluem:
- O nome do símbolo para o verificador de chamadas é diferente.
- O endereço de destino é fornecido em
x11
vez dex15
. - O endereço de destino (
x11
) é[in, out]
em vez de[in]
. - Há um parâmetro extra, fornecido através do
x10
, chamado "Exit Thunk".
Um Exit Thunk é um funclet que transforma parâmetros de função de convenção de chamada Arm64EC para convenção de chamada x64.
O verificador de chamadas Arm64EC está localizado através de um símbolo diferente do que é usado para os outros ABIs no Windows. No Classic Arm64 ABI, o símbolo do verificador de chamadas é __guard_check_icall_fptr
. Este símbolo estará presente no Arm64EC, mas ele está lá para o código vinculado estaticamente x64 usar, não o código Arm64EC em si. O código Arm64EC usará ou __os_arm64x_check_icall
__os_arm64x_check_icall_cfg
.
No Arm64EC, os verificadores de chamadas não são opcionais. No entanto, a CFG ainda é opcional, como é o caso de outras ITBs. O CFG pode ser desativado em tempo de compilação, ou pode haver uma razão legítima para não executar uma verificação CFG mesmo quando o CFG está ativado (por exemplo, o ponteiro de função nunca reside na memória RW). Para uma chamada indireta com verificação CFG, o __os_arm64x_check_icall_cfg
verificador deve ser usado. Se o CFG estiver desativado ou desnecessário, __os_arm64x_check_icall
deve ser usado em seu lugar.
Abaixo está uma tabela de resumo do uso do verificador de chamadas no Classic Arm64, x64 e Arm64EC observando o fato de que um binário Arm64EC pode ter duas opções, dependendo da arquitetura do código.
Binário | Código | Chamada indireta desprotegida | Chamada indireta protegida por CFG |
---|---|---|---|
x64 | x64 | sem verificador de chamadas | __guard_check_icall_fptr ou __guard_dispatch_icall_fptr |
Arm64 Clássico | Arm64 | sem verificador de chamadas | __guard_check_icall_fptr |
Arm64EC | x64 | sem verificador de chamadas | __guard_check_icall_fptr ou __guard_dispatch_icall_fptr |
Arm64EC | __os_arm64x_check_icall |
__os_arm64x_check_icall_cfg |
Independentemente da ABI, ter o código habilitado para CFG (código com referência aos verificadores de chamadas CFG), não implica proteção CFG em tempo de execução. Os binários protegidos por CFG podem ser executados em níveis inferiores, em sistemas que não suportam CFG: o verificador de chamadas é inicializado com um auxiliar no-op em tempo de compilação. Um processo também pode ter o CFG desabilitado pela configuração. Quando o CFG está desabilitado (ou o suporte ao sistema operacional não está presente) em ABIs anteriores, o sistema operacional simplesmente não atualizará o verificador de chamadas quando o binário for carregado. No Arm64EC, se a proteção CFG estiver desativada, o sistema operacional definirá __os_arm64x_check_icall_cfg
o mesmo __os_arm64x_check_icall
que , que ainda fornecerá a verificação de arquitetura de destino necessária em todos os casos, mas não a proteção CFG.
Tal como acontece com o CFG no Classic Arm64, a chamada para a função de destino (x11
) deve seguir imediatamente a chamada para o Verificador de Chamadas. O endereço do Verificador de Chamadas deve ser colocado em um registro volátil e nem ele, nem o endereço da função de destino, devem ser copiados para outro registro ou derramados na memória.
Damas de pilha
__chkstk
é usado automaticamente pelo compilador toda vez que uma função aloca uma área na pilha maior que uma página. Para evitar pular a página de proteção de pilha que protege o final da pilha, __chkstk
é chamado para garantir que todas as páginas na área alocada sejam examinadas.
__chkstk
é geralmente chamado a partir do prólogo da função. Por esse motivo, e para a geração ideal de código, ele usa uma convenção de chamada personalizada.
Isso implica que o código x64 e o código Arm64EC precisam de suas próprias funções distintas __chkstk
, já que os thunks de entrada e saída assumem convenções de chamada padrão.
x64 e Arm64EC compartilham o mesmo namespace de símbolo, portanto, não pode haver duas funções chamadas __chkstk
. Para acomodar a compatibilidade com o código x64 pré-existente, __chkstk
o nome será associado ao verificador de pilha x64. O código Arm64EC será usado __chkstk_arm64ec
em vez disso.
A convenção de chamada personalizada para é a mesma que para __chkstk_arm64ec
o Classic Arm64 __chkstk
: x15
fornece o tamanho da alocação em bytes, dividido por 16. Todos os registros não voláteis, bem como todos os registros voláteis envolvidos na convenção de chamada padrão são preservados.
Tudo o que foi dito acima __chkstk
se aplica igualmente a __security_check_cookie
e sua contraparte Arm64EC: __security_check_cookie_arm64ec
.
Convenção de chamada variádica
Arm64EC segue a convenção de chamada Classic Arm64 ABI, exceto para funções variádicas (também conhecidas como varargs, também conhecidas como funções com a palavra-chave de parâmetro de reticências (. . .) ).
Para o caso específico variádico, Arm64EC segue uma convenção de chamada muito semelhante à variadica x64, com apenas algumas diferenças. Abaixo estão as principais regras para Arm64EC variadic:
- Apenas os 4 primeiros registradores são usados para a passagem de parâmetros:
x0
, ,x1
x2
,x3
. Os parâmetros restantes são derramados na pilha. Isso segue exatamente a convenção de chamada variádica x64, e difere do Arm64 Classic, onde registradoresx0
->x7
são usados. - Os parâmetros de Ponto Flutuante / SIMD que estão sendo passados pelo registro usarão um registro de Uso Geral, não um SIMD. Isso é semelhante ao Arm64 Classic, e difere do x64, onde os parâmetros FP/SIMD são passados em um registro de uso geral e SIMD. Por exemplo, para uma função
f1(int, …)
que está sendo chamada comof1(int, double)
, em x64, o segundo parâmetro será atribuído a ambosRDX
eXMM1
. No Arm64EC, o segundo parâmetro será atribuído a apenasx1
. - Ao passar estruturas por valor através de um registro, aplicam-se regras de tamanho x64: Estruturas com tamanhos exatos 1, 2, 4 e 8 serão carregadas diretamente no registro de uso geral. Estruturas com outros tamanhos são derramadas na pilha e um ponteiro para o local derramado é atribuído ao registro. Isso essencialmente rebaixa o subvalor para o sub-referência, no nível baixo. No Classic Arm64 ABI, estruturas de qualquer tamanho até 16 bytes são atribuídas diretamente a registradores de propósito geral.
- O registro X4 é carregado com um ponteiro para o primeiro parâmetro passado via pilha (o 5º parâmetro). Isso não inclui estruturas derramadas devido às restrições de tamanho descritas acima.
- O registro X5 é carregado com o tamanho, em bytes, de todos os parâmetros passados pela pilha (tamanho de todos os parâmetros, começando com o 5º). Isso não inclui estruturas passadas por valor derramado devido às restrições de tamanho descritas acima.
No exemplo a seguir: pt_nova_function
abaixo toma parâmetros em uma forma não variádica, portanto, seguindo a convenção de chamada Classic Arm64. Em seguida, ele chama pt_va_function
com exatamente os mesmos parâmetros, mas em uma chamada variádica.
struct three_char {
char a;
char b;
char c;
};
void
pt_va_function (
double f,
...
);
void
pt_nova_function (
double f,
struct three_char tc,
__int64 ull1,
__int64 ull2,
__int64 ull3
)
{
pt_va_function(f, tc, ull1, ull2, ull3);
}
pt_nova_function
leva 5 parâmetros que serão atribuídos seguindo as regras da convenção de chamada Classic Arm64:
- 'f' é um duplo. Ele será atribuído a d0.
- 'tc' é uma struct, com um tamanho de 3 bytes. Ele será atribuído a x0.
- ULL1 é um inteiro de 8 bytes. Ele será atribuído a x1.
- ULL2 é um inteiro de 8 bytes. Ele será atribuído a x2.
- ULL3 é um inteiro de 8 bytes. Ele será atribuído a x3.
pt_va_function
é uma função variádica, por isso seguirá as regras variádicas Arm64EC descritas acima:
- 'f' é um duplo. Ele será atribuído a x0.
- 'tc' é uma struct, com um tamanho de 3 bytes. Ele será derramado na pilha e sua localização carregada em x1.
- ULL1 é um inteiro de 8 bytes. Ele será atribuído a x2.
- ULL2 é um inteiro de 8 bytes. Ele será atribuído a x3.
- ULL3 é um inteiro de 8 bytes. Ele será atribuído diretamente à pilha.
- x4 é carregado com a localização de ull3 na pilha.
- x5 é carregado com o tamanho de ull3.
A seguir é mostrada uma possível saída de compilação para pt_nova_function
o , que ilustra as diferenças de atribuição de parâmetros descritas acima.
stp fp,lr,[sp,#-0x30]!
mov fp,sp
sub sp,sp,#0x10
str x3,[sp] ; Spill 5th parameter
mov x3,x2 ; 4th parameter to x3 (from x2)
mov x2,x1 ; 3rd parameter to x2 (from x1)
str w0,[sp,#0x20] ; Spill 2nd parameter
add x1,sp,#0x20 ; Address of 2nd parameter to x1
fmov x0,d0 ; 1st parameter to x0 (from d0)
mov x4,sp ; Address of the 1st in-stack parameter to x4
mov x5,#8 ; Size of the in-stack parameter area
bl pt_va_function
add sp,sp,#0x10
ldp fp,lr,[sp],#0x30
ret
Adições ABI
Para alcançar a interoperabilidade transparente com o código x64, muitas adições foram feitas ao Classic Arm64 ABI. Eles lidam com as diferenças de convenções de chamada entre Arm64EC e x64.
A lista a seguir inclui essas adições:
- Thunks de entrada e saída
- Sair de Thunks
- Entrada Thunks
- Ajustador Thunks
- Sequências de avanço rápido
Thunks de entrada e saída
Os Thunks de entrada e saída se encarregam de traduzir a convenção de chamada Arm64EC (principalmente a mesma que a convenção de chamada Classic Arm64) para a convenção de chamada x64 e vice-versa.
Um equívoco comum é que as convenções de chamada podem ser convertidas seguindo uma única regra aplicada a todas as assinaturas de função. A realidade é que as convenções de chamada têm regras de atribuição de parâmetros. Essas regras dependem do tipo de parâmetro e são diferentes de ABI para ABI. Uma consequência é que a tradução entre ABIs será específica para cada assinatura de função, variando de acordo com o tipo de cada parâmetro.
Considere a seguinte função:
int fJ(int a, int b, int c, int d);
A atribuição de parâmetros ocorrerá da seguinte maneira:
- Braço64: a - x0, b - x1, c - x2, d ->>>> x3
- x64: a - RCX, b - RDX, c - R8, d ->>>> r9
- Tradução Arm64 - x64: x0 - RCX, x1 - RDX, x2 - R8, x3 ->>>>> R9
Agora considere uma função diferente:
int fK(int a, double b, int c, double d);
A atribuição de parâmetros ocorrerá da seguinte maneira:
- Braço64: a - x0, b - d0, c - x1, d ->>>> d1
- x64: a - RCX, b - XMM1, c - R8, d ->>>> XMM3
- Tradução Arm64 - x64: x0 - RCX, d0 - XMM1, x1 - R8, d1 ->>>>> XMM3
Esses exemplos demonstram que a atribuição e a tradução de parâmetros variam de acordo com o tipo, mas também os tipos dos parâmetros anteriores na lista são dependentes. Esse detalhe é ilustrado pelo 3º parâmetro. Em ambas as funções, o tipo do parâmetro é "int", mas a tradução resultante é diferente.
Os Thunks de Entrada e Saída existem por esse motivo e são especificamente adaptados para cada assinatura de função individual.
Ambos os tipos de thunks são, eles mesmos, funções. Os Thunks de entrada são automaticamente invocados pelo emulador quando as funções x64 chamam as funções Arm64EC (execução Enters Arm64EC). Os Thunks de saída são automaticamente invocados pelos verificadores de chamadas quando as funções Arm64EC chamam funções x64 (execução Saídas Arm64EC).
Ao compilar o código Arm64EC, o compilador irá gerar um Entry Thunk para cada função Arm64EC, correspondendo à sua assinatura. O compilador também irá gerar um Exit Thunk para cada função que uma função Arm64EC chama.
Considere o seguinte exemplo:
struct SC {
char a;
char b;
char c;
};
int fB(int a, double b, int i1, int i2, int i3);
int fC(int a, struct SC c, int i1, int i2, int i3);
int fA(int a, double b, struct SC c, int i1, int i2, int i3) {
return fB(a, b, i1, i2, i3) + fC(a, c, i1, i2, i3);
}
Ao compilar o código acima visando Arm64EC, o compilador gerará:
- Código para «fA».
- Entrada Thunk para 'fA'
- Saia de Thunk para 'fB'
- Saia de Thunk para 'fC'
O fA
Entry Thunk é gerado no caso fA
e chamado a partir do código x64. Saia de Thunks para fB
e são gerados em caso fB
e/ou fC
e fC
acabam por ser código x64.
O mesmo Exit Thunk pode ser gerado várias vezes, dado que o compilador irá gerá-los no site de chamada em vez da função em si. Isso pode resultar em uma quantidade considerável de thunks redundantes, então, na realidade, o compilador aplicará regras de otimização triviais para garantir que apenas os thunks necessários cheguem ao binário final.
Por exemplo, em um binário onde a função Arm64EC chama a função A
B
Arm64EC , B
não é exportada e seu endereço nunca é conhecido fora do A
. É seguro eliminar o Exit Thunk de A
para , juntamente com o Entry Thunk para B
B
. Também é seguro juntar todos os thunks Exit e Entry que contêm o mesmo código, mesmo que tenham sido gerados para funções distintas.
Sair de Thunks
Usando as funções fA
de exemplo , e fC
acima, fB
é assim que o compilador geraria ambos e fB
fC
Exit Thunks:
Saia de Thunk para int fB(int a, double b, int i1, int i2, int i3);
$iexit_thunk$cdecl$i8$i8di8i8i8:
stp fp,lr,[sp,#-0x10]!
mov fp,sp
sub sp,sp,#0x30
adrp x8,__os_arm64x_dispatch_call_no_redirect
ldr xip0,[x8]
str x3,[sp,#0x20] ; Spill 5th param (i3) into the stack
fmov d1,d0 ; Move 2nd param (b) from d0 to XMM1 (x1)
mov x3,x2 ; Move 4th param (i2) from x2 to R9 (x3)
mov x2,x1 ; Move 3rd param (i1) from x1 to R8 (x2)
blr xip0 ; Call the emulator
mov x0,x8 ; Move return from RAX (x8) to x0
add sp,sp,#0x30
ldp fp,lr,[sp],#0x10
ret
Saia de Thunk para int fC(int a, struct SC c, int i1, int i2, int i3);
$iexit_thunk$cdecl$i8$i8m3i8i8i8:
stp fp,lr,[sp,#-0x20]!
mov fp,sp
sub sp,sp,#0x30
adrp x8,__os_arm64x_dispatch_call_no_redirect
ldr xip0,[x8]
str w1,[sp,#0x40] ; Spill 2nd param (c) onto the stack
add x1,sp,#0x40 ; Make RDX (x1) point to the spilled 2nd param
str x4,[sp,#0x20] ; Spill 5th param (i3) into the stack
blr xip0 ; Call the emulator
mov x0,x8 ; Move return from RAX (x8) to x0
add sp,sp,#0x30
ldp fp,lr,[sp],#0x20
ret
No caso, podemos ver como a fB
presença de um parâmetro 'double' fará com que a atribuição de registro de GP restante seja remanejada, resultado das diferentes regras de atribuição do Arm64 e x64. Também podemos ver que x64 atribui apenas 4 parâmetros aos registros, então o 5º parâmetro deve ser derramado na pilha.
No caso, o fC
segundo parâmetro é uma estrutura de 3 bytes de comprimento. O Arm64 permitirá que qualquer estrutura de tamanho seja atribuída a um registro diretamente. x64 só permite tamanhos 1, 2, 4 e 8. Esse Exit Thunk deve então transferir isso struct
do registro para a pilha e atribuir um ponteiro ao registrador. Isso ainda consome um registro (para carregar o ponteiro) para que não altere as atribuições para os registros restantes: nenhum remanejamento de registro acontece para o 3º e 4º parâmetros. Assim como para o caso, o fB
5º parâmetro deve ser derramado na pilha.
Considerações adicionais para Exit Thunks:
- O compilador irá nomeá-los> não pelo nome da função que eles traduzem para, mas sim pela assinatura que eles endereçam. Isso facilita a localização de redundâncias.
- O Exit Thunk é chamado com o registro
x9
carregando o endereço da função de destino (x64). Isso é definido pelo verificador de chamadas e passa pelo Exit Thunk, sem ser perturbado, para o emulador.
Depois de reorganizar os parâmetros, o Exit Thunk chama o emulador via __os_arm64x_dispatch_call_no_redirect
.
Vale a pena, neste momento, rever a função do verificador de chamadas, e detalhar sobre sua própria ABI personalizada. É assim que seria uma chamada indireta fB
:
mov x11, <target>
adrp x9, __os_arm64x_check_icall_cfg
ldr x9, [x9, __os_arm64x_check_icall_cfg]
adrp x10, $iexit_thunk$cdecl$i8$i8di8i8i8 ; fB function’s exit thunk
add x10, x10, $iexit_thunk$cdecl$i8$i8di8i8i8
blr x9 ; check target function
blr x11 ; call function
Ao chamar o verificador de chamadas:
x11
fornece o endereço da função de destino a ser chamada (fB
neste caso). Pode não ser conhecido, neste momento, se a função de destino é Arm64EC ou x64.x10
fornece um Exit Thunk correspondente à assinatura da função que está sendo chamada (fB
neste caso).
Os dados retornados pelo verificador de chamadas dependerão da função de destino ser Arm64EC ou x64.
Se o alvo for Arm64EC:
x11
retornará o endereço do código Arm64EC para ligar. Este pode ou não ser o mesmo valor que foi fornecido.
Se o destino for o código x64:
x11
retornará o endereço do Exit Thunk. Isso é copiado da entrada fornecida nox10
.x10
retornará o endereço do Exit Thunk, sem ser perturbado pela entrada.x9
retornará a função x64 de destino. Este pode ou não ser o mesmo valor que foi fornecido na viax11
.
Os verificadores de chamadas sempre deixarão os registradores de parâmetros de convenção de chamada intactos, portanto, o código de chamada deve seguir a chamada para o verificador de chamadas imediatamente com blr x11
(ou br x11
no caso de uma chamada de cauda). Estes são os verificadores de chamadas de registros. Eles sempre preservarão acima e além dos registros não voláteis padrão: x0
-x8
, x15
(chkstk
) e .q0
-q7
Entrada Thunks
Os Thunks de entrada cuidam das transformações necessárias do x64 para as convenções de chamada Arm64. Este é, essencialmente, o inverso de Exit Thunks, mas há mais alguns aspectos a considerar.
Considere o exemplo anterior de compilação fA
, um Entry Thunk é gerado para que fA
possa ser chamado pelo código x64.
Entrada Thunk para int fA(int a, double b, struct SC c, int i1, int i2, int i3)
$ientry_thunk$cdecl$i8$i8dm3i8i8i8:
stp q6,q7,[sp,#-0xA0]! ; Spill full non-volatile XMM registers
stp q8,q9,[sp,#0x20]
stp q10,q11,[sp,#0x40]
stp q12,q13,[sp,#0x60]
stp q14,q15,[sp,#0x80]
stp fp,lr,[sp,#-0x10]!
mov fp,sp
ldrh w1,[x2] ; Load 3rd param (c) bits [15..0] directly into x1
ldrb w8,[x2,#2] ; Load 3rd param (c) bits [16..23] into temp w8
bfi w1,w8,#0x10,#8 ; Merge 3rd param (c) bits [16..23] into x1
mov x2,x3 ; Move the 4th param (i1) from R9 (x3) to x2
fmov d0,d1 ; Move the 2nd param (b) from XMM1 (d1) to d0
ldp x3,x4,[x4,#0x20] ; Load the 5th (i2) and 6th (i3) params
; from the stack into x3 and x4 (using x4)
blr x9 ; Call the function (fA)
mov x8,x0 ; Move the return from x0 to x8 (RAX)
ldp fp,lr,[sp],#0x10
ldp q14,q15,[sp,#0x80] ; Restore full non-volatile XMM registers
ldp q12,q13,[sp,#0x60]
ldp q10,q11,[sp,#0x40]
ldp q8,q9,[sp,#0x20]
ldp q6,q7,[sp],#0xA0
adrp xip0,__os_arm64x_dispatch_ret
ldr xip0,[xip0,__os_arm64x_dispatch_ret]
br xip0
O endereço da função de destino é fornecido pelo emulador em x9
.
Antes de chamar o Entry Thunk, o emulador x64 irá colocar o endereço de retorno da pilha no LR
registro. Espera-se, então, que LR
estará apontando para o código x64 quando o controle for transferido para o Entry Thunk.
O emulador também pode executar outro ajuste na pilha, dependendo do seguinte: As ABIs Arm64 e x64 definem um requisito de alinhamento de pilha em que a pilha deve ser alinhada a 16 bytes no ponto em que uma função é chamada. Ao executar o código Arm64, o hardware impõe essa regra, mas não há imposição de hardware para x64. Ao executar o código x64, chamar erroneamente funções com uma pilha não alinhada pode passar despercebida indefinidamente, até que alguma instrução de alinhamento de 16 bytes seja usada (algumas instruções SSE o fazem) ou o código Arm64EC seja chamado.
Para resolver esse possível problema de compatibilidade, antes de chamar o Entry Thunk, o emulador sempre alinhará o Stack Pointer para 16 bytes e armazenará seu valor original no x4
registrador. Dessa forma, os Entry Thunks sempre começam a ser executados com uma pilha alinhada, mas ainda podem referenciar corretamente os parâmetros passados na pilha, via x4
.
Quando se trata de registradores SIMD não voláteis, há uma diferença significativa entre as convenções de chamada Arm64 e x64. No Arm64, os 8 bytes (64 bits) baixos do registro são considerados não voláteis. Em outras palavras, apenas a Dn
parte dos Qn
registros não é volátil. Em x64, os 16 bytes inteiros do XMMn
registro são considerados não voláteis. Além disso, em x64, e são registros não voláteis, XMM6
enquanto D6 e XMM7
D7 (os registradores Arm64 correspondentes) são voláteis.
Para resolver essas assimetrias de manipulação de registro SIMD, o Entry Thunks deve salvar explicitamente todos os registros SIMD que são considerados não voláteis em x64. Isso só é necessário em Entry Thunks (não Exit Thunks) porque o x64 é mais rigoroso que o Arm64. Em outras palavras, as regras de salvamento/preservação de registros em x64 excedem os requisitos do Arm64 em todos os casos.
Para resolver a recuperação correta desses valores de registro ao desenrolar a pilha (por exemplo, setjmp + longjmp ou throw + catch), um novo opcode de desenrolar foi introduzido: save_any_reg (0xE7)
. Este novo opcode de desdobramento de 3 bytes permite salvar qualquer registro de Uso Geral ou SIMD (incluindo os considerados voláteis) e incluir registradores de tamanho Qn
normal. Este novo opcode é usado para as Qn
operações de registro de derramamentos/enchimento acima. save_any_reg
é compatível com save_next_pair (0xE6)
.
Para referência, abaixo estão as informações de desdobramento correspondentes pertencentes ao Entry Thunk apresentado acima:
Prolog unwind:
06: E76689.. +0004 stp q6,q7,[sp,#-0xA0]! ; Actual=stp q6,q7,[sp,#-0xA0]!
05: E6...... +0008 stp q8,q9,[sp,#0x20] ; Actual=stp q8,q9,[sp,#0x20]
04: E6...... +000C stp q10,q11,[sp,#0x40] ; Actual=stp q10,q11,[sp,#0x40]
03: E6...... +0010 stp q12,q13,[sp,#0x60] ; Actual=stp q12,q13,[sp,#0x60]
02: E6...... +0014 stp q14,q15,[sp,#0x80] ; Actual=stp q14,q15,[sp,#0x80]
01: 81...... +0018 stp fp,lr,[sp,#-0x10]! ; Actual=stp fp,lr,[sp,#-0x10]!
00: E1...... +001C mov fp,sp ; Actual=mov fp,sp
+0020 (end sequence)
Epilog #1 unwind:
0B: 81...... +0044 ldp fp,lr,[sp],#0x10 ; Actual=ldp fp,lr,[sp],#0x10
0C: E74E88.. +0048 ldp q14,q15,[sp,#0x80] ; Actual=ldp q14,q15,[sp,#0x80]
0F: E74C86.. +004C ldp q12,q13,[sp,#0x60] ; Actual=ldp q12,q13,[sp,#0x60]
12: E74A84.. +0050 ldp q10,q11,[sp,#0x40] ; Actual=ldp q10,q11,[sp,#0x40]
15: E74882.. +0054 ldp q8,q9,[sp,#0x20] ; Actual=ldp q8,q9,[sp,#0x20]
18: E76689.. +0058 ldp q6,q7,[sp],#0xA0 ; Actual=ldp q6,q7,[sp],#0xA0
1C: E3...... +0060 nop ; Actual=90000030
1D: E3...... +0064 nop ; Actual=ldr xip0,[xip0,#8]
1E: E4...... +0068 end ; Actual=br xip0
+0070 (end sequence)
Depois que a função Arm64EC retorna, a __os_arm64x_dispatch_ret
rotina é usada para reinserir o emulador, de volta ao código x64 (apontado por LR
).
As funções Arm64EC têm os 4 bytes antes da primeira instrução na função reservados para armazenar informações a serem usadas em tempo de execução. É nesses 4 bytes que o endereço relativo de Entry Thunk para a função pode ser encontrado. Ao executar uma chamada de uma função x64 para uma função Arm64EC, o emulador lerá os 4 bytes antes do início da função, mascarará os dois bits inferiores e adicionará essa quantidade ao endereço da função. Isso produzirá o endereço do Thunk de Entrada para chamar.
Ajustador Thunks
Adjustor Thunks são funções sem assinatura que simplesmente transferem o controle para (tail-call) outra função, depois de executar alguma transformação para um dos parâmetros. O tipo do(s) parâmetro(s) que está sendo transformado é conhecido, mas todos os parâmetros restantes podem ser qualquer coisa e, em qualquer número – o Adjustor Thunks não tocará em nenhum registro potencialmente contendo um parâmetro e não tocará na pilha. Isso é o que torna o Adjustor Thunks funções sem assinatura.
Adjustor Thunks pode ser gerado automaticamente pelo compilador. Isso é comum, por exemplo, com a herança múltipla do C++, em que qualquer método virtual pode ser delegado à classe pai, sem modificações, além de um ajuste no this
ponteiro.
Abaixo está um exemplo do mundo real:
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
b CObjectContext::Release
O thunk subtrai 8 bytes para o this
ponteiro e encaminha a chamada para a classe pai.
Em resumo, as funções Arm64EC chamáveis a partir de funções x64 devem ter um Entry Thunk associado. O Entry Thunk é específico para assinatura. As funções sem assinatura do Arm64, como o Adjustor Thunks, precisam de um mecanismo diferente que possa lidar com funções sem assinatura.
O Entry Thunk de um Adjustor Thunk usa o __os_arm64x_x64_jump
auxiliar para adiar a execução do trabalho real do Entry Thunk (ajustar os parâmetros de uma convenção para a outra) para a próxima chamada. É neste momento que a assinatura se torna aparente. Isso inclui a opção de não fazer ajustes de convenção de chamada, se o destino do Adjustor Thunk for uma função x64. Lembre-se de que, no momento em que um Entry Thunk começa a ser executado, os parâmetros estão em sua forma x64.
No exemplo acima, considere a aparência do código no Arm64EC.
Ajustador Thunk em Arm64EC
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
adrp x9,CObjectContext::Release
add x11,x9,CObjectContext::Release
stp fp,lr,[sp,#-0x10]!
mov fp,sp
adrp xip0, __os_arm64x_check_icall
ldr xip0,[xip0, __os_arm64x_check_icall]
blr xip0
ldp fp,lr,[sp],#0x10
br x11
Porta-malas de entrada do Adjustor Thunk
[thunk]:CObjectContext::Release$entry_thunk`adjustor{8}':
sub x0,x0,#8
adrp x9,CObjectContext::Release
add x9,x9,CObjectContext::Release
adrp xip0,__os_arm64x_x64_jump
ldr xip0,[xip0,__os_arm64x_x64_jump]
br xip0
Sequências de avanço rápido
Alguns aplicativos fazem modificações em tempo de execução em funções que residem em binários que eles não possuem, mas dos quais dependem – geralmente binários do sistema operacional – com a finalidade de desviar a execução quando a função é chamada. Isso também é conhecido como gancho.
No alto nível, o processo de gancho é simples. Em detalhes, no entanto, o hooking é específico da arquitetura e bastante complexo, dadas as variações potenciais que a lógica de hooking deve abordar.
Em termos gerais, o processo envolve o seguinte:
- Determine o endereço da função a ser conectada.
- Substitua a primeira instrução da função por um salto para a rotina do gancho.
- Quando o gancho estiver pronto, volte à lógica original, que inclui executar a instrução original deslocada.
As variações surgem de coisas como:
- O tamanho da 1ª instrução: É uma boa ideia substituí-lo por um JMP que é do mesmo tamanho ou menor, para evitar substituir o topo da função enquanto outro thread pode estar executando-o em voo.
- O tipo da primeira instrução: Se a primeira instrução tiver alguma natureza relativa ao PC, realocá-la pode exigir a alteração de coisas como os campos de deslocamento. Uma vez que eles são propensos a transbordar quando uma instrução é movida para um lugar distante, isso pode exigir o fornecimento de lógica equivalente com instruções completamente diferentes.
Devido a toda essa complexidade, uma lógica de gancho robusta e genérica é rara de encontrar. Frequentemente, a lógica presente em aplicativos só pode lidar com um conjunto limitado de casos que o aplicativo espera encontrar nas APIs específicas em que está interessado. Não é difícil imaginar o quanto isso é um problema de compatibilidade de aplicativos. Mesmo uma simples alteração no código ou otimizações do compilador pode tornar os aplicativos inutilizáveis se o código não tiver mais a aparência exata do esperado.
O que aconteceria com esses aplicativos se eles encontrassem o código Arm64 ao configurar um gancho? Certamente falhariam.
As funções FFS (Fast-Forward Sequence) atendem a esse requisito de compatibilidade no Arm64EC.
FFS são funções x64 muito pequenas que não contêm lógica real e tail-call para a função Arm64EC real. Eles são opcionais, mas habilitados por padrão para todas as exportações de DLL e para qualquer função decorada com __declspec(hybrid_patchable)
.
Para esses casos, quando o código obtém um ponteiro para uma determinada função, seja no caso de exportação ou no GetProcAddress
&function
__declspec(hybrid_patchable)
caso, o endereço resultante conterá o código x64. Esse código x64 passará para uma função x64 legítima, satisfazendo a maior parte da lógica de gancho atualmente disponível.
Considere o seguinte exemplo (tratamento de erros omitido por brevidade):
auto module_handle =
GetModuleHandleW(L"api-ms-win-core-processthreads-l1-1-7.dll");
auto pgma =
(decltype(&GetMachineTypeAttributes))
GetProcAddress(module_handle, "GetMachineTypeAttributes");
hr = (*pgma)(IMAGE_FILE_MACHINE_Arm64, &MachineAttributes);
O valor do ponteiro de função na pgma
variável conterá o endereço de GetMachineTypeAttributes
's FFS.
Este é um exemplo de uma sequência de avanço rápido:
kernelbase!EXP+#GetMachineTypeAttributes:
00000001`800034e0 488bc4 mov rax,rsp
00000001`800034e3 48895820 mov qword ptr [rax+20h],rbx
00000001`800034e7 55 push rbp
00000001`800034e8 5d pop rbp
00000001`800034e9 e922032400 jmp 00000001`80243810
A função FFS x64 tem um prólogo canônico e epílogo, terminando com uma chamada de cauda (salto) para a função real GetMachineTypeAttributes
no código Arm64EC:
kernelbase!GetMachineTypeAttributes:
00000001`80243810 d503237f pacibsp
00000001`80243814 a9bc7bfd stp fp,lr,[sp,#-0x40]!
00000001`80243818 a90153f3 stp x19,x20,[sp,#0x10]
00000001`8024381c a9025bf5 stp x21,x22,[sp,#0x20]
00000001`80243820 f9001bf9 str x25,[sp,#0x30]
00000001`80243824 910003fd mov fp,sp
00000001`80243828 97fbe65e bl kernelbase!#__security_push_cookie
00000001`8024382c d10083ff sub sp,sp,#0x20
[...]
Seria bastante ineficiente se fosse necessário executar 5 instruções x64 emuladas entre duas funções Arm64EC. As funções FFS são especiais. As funções FFS realmente não são executadas se permanecerem inalteradas. O auxiliar do verificador de chamadas verificará com eficiência se o FFS não foi alterado. Se for esse o caso, a chamada será transferida diretamente para o destino real. Se o FFS tiver sido alterado de alguma forma possível, ele não será mais um FFS. A execução será transferida para o FFS alterado e executará qualquer código que esteja lá, emulando o desvio e qualquer lógica de gancho.
Quando o gancho transfere a execução de volta para o final do FFS, ele eventualmente alcançará a chamada de cauda para o código Arm64EC, que será executado após o gancho, assim como o aplicativo está esperando.
Autoria Arm64EC na Assembleia
Os cabeçalhos do SDK do Windows e o compilador C podem simplificar o trabalho de criação do assembly Arm64EC. Por exemplo, o compilador C pode ser usado para gerar Thunks de entrada e saída para funções não compiladas a partir do código C.
Considere o exemplo de um equivalente à seguinte função fD
que deve ser criada no Assembly (ASM). Esta função pode ser chamada pelo código Arm64EC e x64 e o ponteiro da função pode apontar para o pfE
código Arm64EC ou x64 também.
typedef int (PF_E)(int, double);
extern PF_E * pfE;
int fD(int i, double d) {
return (*pfE)(i, d);
}
Escrever fD
em ASM seria algo como:
#include "ksarm64.h"
IMPORT __os_arm64x_check_icall_cfg
IMPORT |$iexit_thunk$cdecl$i8$i8d|
IMPORT pfE
NESTED_ENTRY_COMDAT A64NAME(fD)
PROLOG_SAVE_REG_PAIR fp, lr, #-16!
adrp x11, pfE ; Get the global function
ldr x11, [x11, pfE] ; pointer pfE
adrp x9, __os_arm64x_check_icall_cfg ; Get the EC call checker
ldr x9, [x9, __os_arm64x_check_icall_cfg] ; with CFG
adrp x10, |$iexit_thunk$cdecl$i8$i8d| ; Get the Exit Thunk for
add x10, x10, |$iexit_thunk$cdecl$i8$i8d| ; int f(int, double);
blr x9 ; Invoke the call checker
blr x11 ; Invoke the function
EPILOG_RESTORE_REG_PAIR fp, lr, #16!
EPILOG_RETURN
NESTED_END
end
No exemplo acima:
- O Arm64EC usa a mesma declaração de procedimento e macros prólogo/epílogo que o Arm64.
- Os nomes das funções devem ser encapsulados
A64NAME
pela macro. Ao compilar o código C/C++ como Arm64EC, o compilador marca o comoARM64EC
contendo oOBJ
código Arm64EC. Isso não acontece comARMASM
o . Ao compilar o código ASM, há uma maneira alternativa de informar ao vinculador que o código produzido é Arm64EC. Isso ocorre prefixando o nome da função com#
. AA64NAME
macro executa essa operação quando é definida e deixa o nome inalterado quando_ARM64EC_
_ARM64EC_
não está definido. Isso torna possível compartilhar o código-fonte entre Arm64 e Arm64EC. - O
pfE
ponteiro de função deve primeiro ser executado através do verificador de chamadas EC, juntamente com o Exit Thunk apropriado, caso a função de destino seja x64.
Gerando Thunks de Entrada e Saída
A próxima etapa é gerar o Entry Thunk para e o Exit Thunk para fD
pfE
. O compilador C pode executar essa tarefa com o mínimo de esforço, usando a palavra-chave do _Arm64XGenerateThunk
compilador.
void _Arm64XGenerateThunk(int);
int fD2(int i, double d) {
UNREFERENCED_PARAMETER(i);
UNREFERENCED_PARAMETER(d);
_Arm64XGenerateThunk(2);
return 0;
}
int fE(int i, double d) {
UNREFERENCED_PARAMETER(i);
UNREFERENCED_PARAMETER(d);
_Arm64XGenerateThunk(1);
return 0;
}
A _Arm64XGenerateThunk
palavra-chave diz ao compilador C para usar a assinatura da função, ignorar o corpo e gerar um Exit Thunk (quando o parâmetro é 1) ou um Entry Thunk (quando o parâmetro é 2).
Recomenda-se colocar a geração thunk em seu próprio arquivo C. Estar em arquivos isolados torna mais simples confirmar os nomes dos símbolos despejando os símbolos correspondentes OBJ
ou até mesmo desmontando.
Thunks de entrada personalizados
Macros foram adicionadas ao SDK para ajudar a criar Entry Thunks personalizados, codificados à mão. Um caso em que isso pode ser usado é ao criar Thunks Ajustadores personalizados.
A maioria dos Adjustor Thunks são gerados pelo compilador C++, mas também podem ser gerados manualmente. Isso pode ser encontrado nos casos em que um retorno de chamada genérico transfere o controle para o retorno de chamada real, identificado por um dos parâmetros.
Abaixo está um exemplo no código Arm64 Classic:
NESTED_ENTRY MyAdjustorThunk
PROLOG_SAVE_REG_PAIR fp, lr, #-16!
ldr x15, [x0, 0x18]
adrp x16, __guard_check_icall_fptr
ldr x16, [x16, __guard_check_icall_fptr]
blr xip0
EPILOG_RESTORE_REG_PAIR fp, lr, #16
EPILOG_END br x15
NESTED_END
Neste exemplo, o endereço da função de destino é recuperado do elemento de uma estrutura, fornecido por referência, por meio do 1º parâmetro. Como a estrutura é gravável, o endereço de destino deve ser validado por meio do Control Flow Guard (CFG).
O exemplo abaixo demonstra como o Adjustor Thunk equivalente ficaria quando portado para Arm64EC:
NESTED_ENTRY_COMDAT A64NAME(MyAdjustorThunk)
PROLOG_SAVE_REG_PAIR fp, lr, #-16!
ldr x11, [x0, 0x18]
adrp xip0, __os_arm64x_check_icall_cfg
ldr xip0, [xip0, __os_arm64x_check_icall_cfg]
blr xip0
EPILOG_RESTORE_REG_PAIR fp, lr, #16
EPILOG_END br x11
NESTED_END
O código acima não fornece um Exit Thunk (no registro x10). Isso não é possível, pois o código pode ser executado para muitas assinaturas diferentes. Esse código está aproveitando o fato de o chamador ter definido x10 para o Exit Thunk. O interlocutor teria feito a ligação visando uma assinatura explícita.
O código acima precisa de um Entry Thunk para resolver o caso quando o chamador é o código x64. Esta é a maneira de criar o Entry Thunk correspondente, usando a macro para Entry Thunks personalizados:
ARM64EC_CUSTOM_ENTRY_THUNK A64NAME(MyAdjustorThunk)
ldr x9, [x0, 0x18]
adrp xip0, __os_arm64x_x64_jump
ldr xip0, [xip0, __os_arm64x_x64_jump]
br xip0
LEAF_END
Ao contrário de outras funções, este Entry Thunk eventualmente não transfere o controle para a função associada (o Adjustor Thunk). Nesse caso, a funcionalidade em si (realizando o ajuste de parâmetro) é incorporada ao Entry Thunk e o controle é transferido diretamente para o destino final, através do __os_arm64x_x64_jump
auxiliar.
Gerando dinamicamente (compilação JIT) código Arm64EC
Nos processos Arm64EC existem dois tipos de memória executável: código Arm64EC e código x64.
O sistema operacional extrai essas informações dos binários carregados. Os binários x64 são todos x64 e Arm64EC contêm uma tabela de intervalo para páginas de código Arm64EC vs x64.
E quanto ao código gerado dinamicamente? Os compiladores Just-in-time (JIT) geram código, em tempo de execução, que não é suportado por nenhum arquivo binário.
Normalmente, isso implica:
- Alocação de memória gravável (
VirtualAlloc
). - Produzindo o código na memória alocada.
- Protegendo novamente a memória de Leitura-Gravação para Leitura-Execução (
VirtualProtect
). - Adicione entradas de função de desbobinamento para todas as funções geradas não triviais (não-folha) (
RtlAddFunctionTable
ouRtlAddGrowableFunctionTable
).
Por razões de compatibilidade triviais, qualquer aplicativo executando essas etapas em um processo Arm64EC resultará no código sendo considerado código x64. Isso acontecerá para qualquer processo usando o x64 Java Runtime não modificado, .NET runtime, mecanismo JavaScript, etc.
Para gerar o código dinâmico Arm64EC, o processo é basicamente o mesmo, com apenas duas diferenças:
- Ao alocar a memória, use mais recente
VirtualAlloc2
(em vez deVirtualAlloc
ouVirtualAllocEx
) e forneça oMEM_EXTENDED_PARAMETER_EC_CODE
atributo. - Ao adicionar entradas de função:
- Eles devem estar no formato Arm64. Ao compilar o código Arm64EC, o
RUNTIME_FUNCTION
tipo corresponderá ao formato x64. Para o formato Arm64 ao compilar Arm64EC, use oARM64_RUNTIME_FUNCTION
tipo. - Não use a API mais antiga
RtlAddFunctionTable
. Sempre use a API mais recenteRtlAddGrowableFunctionTable
.
- Eles devem estar no formato Arm64. Ao compilar o código Arm64EC, o
Abaixo está um exemplo de alocação de memória:
MEM_EXTENDED_PARAMETER Parameter = { 0 };
Parameter.Type = MemExtendedParameterAttributeFlags;
Parameter.ULong64 = MEM_EXTENDED_PARAMETER_EC_CODE;
HANDLE process = GetCurrentProcess();
ULONG allocationType = MEM_RESERVE;
DWORD protection = PAGE_EXECUTE_READ | PAGE_TARGETS_INVALID;
address = VirtualAlloc2 (
process,
NULL,
numBytesToAllocate,
allocationType,
protection,
&Parameter,
1);
E um exemplo de adição de uma entrada de função de descontração:
ARM64_RUNTIME_FUNCTION FunctionTable[1];
FunctionTable[0].BeginAddress = 0;
FunctionTable[0].Flags = PdataPackedUnwindFunction;
FunctionTable[0].FunctionLength = nSize / 4;
FunctionTable[0].RegF = 0; // no D regs saved
FunctionTable[0].RegI = 0; // no X regs saved beyond fp,lr
FunctionTable[0].H = 0; // no home for x0-x7
FunctionTable[0].CR = PdataCrChained; // stp fp,lr,[sp,#-0x10]!
// mov fp,sp
FunctionTable[0].FrameSize = 1; // 16 / 16 = 1
this->DynamicTable = NULL;
Result == RtlAddGrowableFunctionTable(
&this->DynamicTable,
reinterpret_cast<PRUNTIME_FUNCTION>(FunctionTable),
1,
1,
reinterpret_cast<ULONG_PTR>(pBegin),
reinterpret_cast<ULONG_PTR>(reinterpret_cast<PBYTE>(pBegin) + nSize)
);
Comentários
https://aka.ms/ContentUserFeedback.
Em breve: Ao longo de 2024, eliminaremos os problemas do GitHub como o mecanismo de comentários para conteúdo e o substituiremos por um novo sistema de comentários. Para obter mais informações, consulteEnviar e exibir comentários de