Comprendre l’ABI Arm64EC et le code d’assembly
Arm64EC (« Emulation Compatible ») est une nouvelle interface binaire d’application (ABI) pour la création d’applications pour Windows 11 sur Arm. Pour obtenir une vue d’ensemble d’Arm64EC et comment commencer à créer des applications Win32 en tant qu’Arm64EC, consultez Utilisation d’Arm64EC pour créer des applications pour Windows 11 sur les appareils Arm.
L’objectif de ce document est de fournir une vue détaillée de l’ABI Arm64EC avec suffisamment d’informations pour qu’un développeur d’applications écrive et débogue le code compilé pour Arm64EC, y compris le débogage de bas niveau/assembleur et l’écriture de code d’assembly ciblant l’ABI Arm64EC.
Conception d’Arm64EC
Arm64EC a été conçu pour fournir des fonctionnalités et des performances de niveau natif, tout en fournissant une interopérabilité transparente et directe avec du code x64 exécuté sous émulation.
Arm64EC est principalement additif à l’ABI Arm64 classique. Très peu de l’ABI classique a été modifié, mais des parties ont été ajoutées pour activer l’interopérabilité x64.
Dans ce document, l’ABI Arm64 standard standard doit être appelé « ABI classique ». Cela évite l’ambiguïté inhérente aux termes surchargés comme « Natif ». Arm64EC, pour être clair, est tout aussi natif que l’ABI d’origine.
Arm64EC vs Arm64 Classic ABI
La liste suivante indique où Arm64EC a divergent d’Arm64 Classic ABI.
- Inscrire le mappage et les registres bloqués
- Vérificateurs d’appels
- Vérificateurs de pile
- Convention d’appel variadicique
Il s’agit de petits changements lorsqu’ils sont vus en perspective de la définition de l’ABI dans son ensemble.
Inscrire le mappage et les registres bloqués
Pour qu’il existe une interopérabilité au niveau du type avec du code x64, le code Arm64EC est compilé avec les mêmes définitions d’architecture de préprocesseur que le code x64.
En d’autres termes, _M_AMD64
et _AMD64_
sont définis. L’un des types affectés par cette règle est la CONTEXT
structure. La CONTEXT
structure définit l’état du processeur à un point donné. Il est utilisé pour des éléments tels que Exception Handling
des GetThreadContext
API. Le code x64 existant s’attend à ce que le contexte de l’UC soit représenté en tant que structure x64 CONTEXT
ou, en d’autres termes, la structure telle qu’elle est définie pendant la CONTEXT
compilation x64.
Cette structure doit être utilisée pour représenter le contexte de l’UC lors de l’exécution de code x64, ainsi que du code Arm64EC. Le code existant ne comprend pas un nouveau concept, tel que le jeu d’enregistrement du processeur qui passe de la fonction à la fonction. Si la structure x64 CONTEXT
est utilisée pour représenter les états d’exécution Arm64, cela implique que les registres Arm64 sont mappés efficacement dans des registres x64.
Il implique également que tous les registres Arm64 qui ne peuvent pas être intégrés dans le x64 CONTEXT
ne doivent pas être utilisés, car leurs valeurs peuvent être perdues chaque fois qu’une opération se CONTEXT
produit (et certaines peuvent être asynchrones et inattendues, telles que l’opération garbage collection d’un runtime de langage managé ou d’un APC).
Les règles de mappage entre les registres Arm64EC et x64 sont représentées par la ARM64EC_NT_CONTEXT
structure dans les en-têtes Windows, présentes dans le Kit de développement logiciel (SDK). Cette structure est essentiellement une union de la CONTEXT
structure, exactement comme elle est définie pour x64, mais avec une superposition supplémentaire de registre Arm64.
Par exemple, RCX
mappe à X0
, RDX
à X1
SP
, RSP
à , RIP
à PC
, etc. Nous pouvons également voir comment les registres x13
, , x14
, x23
x24
, , x28
n’ont v16
-v31
aucune représentation et, par conséquent, ne peuvent pas être utilisés dans Arm64EC.
Cette restriction d’utilisation de registre est la première différence entre les API Arm64 Classic et EC.
Vérificateurs d’appels
Les vérificateurs d’appels font partie de Windows depuis que Control Flow Guard (CFG) a été introduit dans Windows 8.1. Les vérificateurs d’appels sont des assainisseurs d’adresses pour les pointeurs de fonction (avant que ces éléments aient été appelés désinfecteurs d’adresses). Chaque fois que le code est compilé avec l’option /guard:cf
, le compilateur génère un appel supplémentaire à la fonction vérificateur juste avant chaque appel indirect/saut. La fonction de vérificateur elle-même est fournie par Windows et, pour CFG, elle effectue une vérification de validité par rapport aux cibles d’appel connues à bon fonctionnement. Ces informations sont également incluses dans les fichiers binaires compilés avec /guard:cf
.
Voici un exemple d’utilisation d’un vérificateur d’appel dans Arm64 classique :
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
Dans le cas de CFG, le vérificateur d’appel retourne simplement si la cible est valide, ou échoue rapidement le processus si ce n’est pas le cas. Les vérificateurs d’appels ont des conventions d’appel personnalisées. Ils prennent le pointeur de fonction dans un registre non utilisé par la convention d’appel normale et conservent tous les registres de convention d’appel normal. De cette façon, ils n’introduisent pas de déversement d’enregistrement autour d’eux.
Les vérificateurs d’appels sont facultatifs sur toutes les autres API Windows, mais obligatoires sur Arm64EC. Sur Arm64EC, les vérificateurs d’appels accumulent la tâche de vérification de l’architecture de la fonction appelée. Ils vérifient si l’appel est une autre fonction EC (« Emulation Compatible ») ou une fonction x64 qui doit être exécutée sous émulation. Dans de nombreux cas, cela ne peut être vérifié qu’au moment de l’exécution.
Les vérificateurs d’appels Arm64EC s’appuient sur les vérificateurs Arm64 existants, mais ils ont une convention d’appel personnalisée légèrement différente. Ils prennent un paramètre supplémentaire et peuvent modifier le registre contenant l’adresse cible. Par exemple, si la cible est du code x64, le contrôle doit d’abord être transféré vers la logique de génération automatique d’émulation.
Dans Arm64EC, la même utilisation du vérificateur d’appel devient :
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
Les légères différences de Classic Arm64 sont les suivantes :
- Le nom du symbole du vérificateur d’appel est différent.
- L’adresse cible est fournie au
x11
lieu dex15
. - L’adresse cible (
x11
) est[in, out]
au lieu de[in]
. - Il existe un paramètre supplémentaire, fourni via
x10
, appelé « Exit Thunk ».
Exit Thunk est un fonclet qui transforme les paramètres de fonction de la convention d’appel Arm64EC en convention d’appel x64.
Le vérificateur d’appel Arm64EC se trouve via un symbole différent de celui utilisé pour les autres API dans Windows. Sur l’ABI Arm64 classique, le symbole du vérificateur d’appel est __guard_check_icall_fptr
. Ce symbole sera présent dans Arm64EC, mais il est là pour que le code lié de manière statique x64 utilise, pas le code Arm64EC lui-même. Le code Arm64EC utilise soit __os_arm64x_check_icall
__os_arm64x_check_icall_cfg
.
Sur Arm64EC, les vérificateurs d’appels ne sont pas facultatifs. Toutefois, cfG est toujours facultatif, comme c’est le cas pour d’autres API. CfG peut être désactivé au moment de la compilation, ou il peut y avoir une raison légitime de ne pas effectuer de vérification CFG même lorsque CFG est activé (par exemple, le pointeur de fonction ne réside jamais dans la mémoire RW). Pour un appel indirect avec la vérification CFG, le __os_arm64x_check_icall_cfg
vérificateur doit être utilisé. Si cfG est désactivé ou inutile, __os_arm64x_check_icall
doit être utilisé à la place.
Voici un tableau récapitulatif de l’utilisation du vérificateur d’appel sur Arm64 classique, x64 et Arm64EC notant le fait qu’un binaire Arm64EC peut avoir deux options en fonction de l’architecture du code.
Binary | Code | Appel indirect non protégé | Appel indirect protégé par CFG |
---|---|---|---|
x64 | x64 | aucun vérificateur d’appel | __guard_check_icall_fptr ou __guard_dispatch_icall_fptr |
Arm64 Classic | Arm64 | aucun vérificateur d’appel | __guard_check_icall_fptr |
Arm64EC | x64 | aucun vérificateur d’appel | __guard_check_icall_fptr ou __guard_dispatch_icall_fptr |
Arm64EC | __os_arm64x_check_icall |
__os_arm64x_check_icall_cfg |
Indépendamment de l’ABI, le fait que CFG ait activé le code (code avec référence aux vérificateurs d’appels CFG), n’implique pas la protection CFG au moment de l’exécution. Les fichiers binaires protégés par CFG peuvent s’exécuter de bas niveau, sur les systèmes ne prenant pas en charge CFG : le vérificateur d’appel est initialisé avec un assistance sans opération au moment de la compilation. Un processus peut également avoir cfG désactivé par configuration. Lorsque CFG est désactivé (ou que la prise en charge du système d’exploitation n’est pas présente) sur les API précédentes, le système d’exploitation ne met simplement pas à jour le vérificateur d’appel lorsque le fichier binaire est chargé. Sur Arm64EC, si la protection CFG est désactivée, le système d’exploitation définit __os_arm64x_check_icall_cfg
la même chose que __os_arm64x_check_icall
, ce qui fournira toujours la vérification de l’architecture cible nécessaire dans tous les cas, mais pas la protection CFG.
Comme avec CFG dans Classic Arm64, l’appel à la fonction cible (x11
) doit immédiatement suivre l’appel au vérificateur d’appel. L’adresse du vérificateur d’appel doit être placée dans un registre volatile et ni dans l’adresse de la fonction cible, ne doit jamais être copiée dans un autre registre ou renversée en mémoire.
Vérificateurs de pile
__chkstk
est utilisé automatiquement par le compilateur chaque fois qu’une fonction alloue une zone sur la pile supérieure à une page. Pour éviter d’ignorer la page de protection de la pile protégeant la fin de la pile, __chkstk
elle est appelée pour vous assurer que toutes les pages de la zone allouée sont sondées.
__chkstk
est généralement appelé à partir du prologue de la fonction. Pour cette raison, et pour une génération de code optimale, elle utilise une convention d’appel personnalisée.
Cela implique que le code x64 et le code Arm64EC ont besoin de leurs propres fonctions, distinctes, __chkstk
car les thunks d’entrée et de sortie supposent des conventions d’appel standard.
x64 et Arm64EC partagent le même espace de noms de symboles afin qu’il ne puisse pas y avoir deux fonctions nommées __chkstk
. Pour prendre en charge la compatibilité avec le code x64 préexistant, __chkstk
le nom est associé au vérificateur de pile x64. Le code Arm64EC sera utilisé __chkstk_arm64ec
à la place.
La convention d’appel personnalisée pour __chkstk_arm64ec
est la même que pour Classic Arm64 __chkstk
: x15
fournit la taille de l’allocation en octets, divisée par 16. Tous les registres non volatiles, ainsi que tous les registres volatiles impliqués dans la convention d’appel standard sont conservés.
Tout ce qui a été dit ci-dessus s’applique __chkstk
de façon égale à __security_check_cookie
et à son équivalent Arm64EC : __security_check_cookie_arm64ec
.
Convention d’appel variadicique
Arm64EC suit la convention d’appel ABI Arm64 classique, à l’exception des fonctions Variadic (aka varargs, aka functions with the ellipsis (. .) parameter keyword).
Pour le cas spécifique variadicique, Arm64EC suit une convention d’appel très similaire à la variadicique x64, avec seulement quelques différences. Voici les principales règles d’Arm64EC variadiciques :
- Seuls les 4 premiers registres sont utilisés pour le passage de paramètre :
x0
,x1
,x2
,x3
. Les paramètres restants sont déversés sur la pile. Ceci suit la convention d’appel variadicique x64 exactement, et diffère de Arm64 Classic, où lesx0
registres ->x7
sont utilisés. - Les paramètres à virgule flottante /SIMD transmis par l’inscription utilisent un registre à usage général, et non un SIMD. Ceci est similaire à Arm64 Classic et diffère de x64, où les paramètres FP/SIMD sont transmis à la fois dans un registre à usage général et SIMD. Par exemple, pour une fonction
f1(int, …)
appeléef1(int, double)
, sur x64, le deuxième paramètre sera affecté à la foisRDX
etXMM1
. Sur Arm64EC, le deuxième paramètre sera affecté à justex1
. - Lors du passage de structures par valeur à travers un registre, les règles de taille x64 s’appliquent : les structures dont la taille est exactement 1, 2, 4 et 8 sont chargées directement dans le registre à usage général. Les structures avec d’autres tailles sont déversées sur la pile, et un pointeur vers l’emplacement déversé est affecté au registre. Cela rétrograde essentiellement par valeur en by-reference, au bas niveau. Sur l’ABI Arm64 Classique, les structures d’une taille pouvant atteindre 16 octets sont affectées directement aux registres à usage général.
- Le registre X4 est chargé avec un pointeur vers le premier paramètre passé via la pile (le 5e paramètre). Cela n’inclut pas les structures déversées en raison des restrictions de taille décrites ci-dessus.
- Le registre X5 est chargé avec la taille, en octets, de tous les paramètres passés par pile (taille de tous les paramètres, en commençant par le 5e). Cela n’inclut pas les structures transmises par valeur renversées en raison des restrictions de taille décrites ci-dessus.
Dans l’exemple suivant : pt_nova_function
ci-dessous prend des paramètres dans un formulaire non variadicique, par conséquent en suivant la convention d’appel Arm64 classique. Il appelle pt_va_function
ensuite avec les mêmes paramètres exactement, mais dans un appel variadicique à la place.
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
prend 5 paramètres qui seront attribués en suivant les règles de convention d’appel Arm64 classiques :
- 'f' est un double. Elle sera affectée à d0.
- 'tc' est un struct, avec une taille de 3 octets. Elle sera affectée à x0.
- ull1 est un entier de 8 octets. Elle sera affectée à x1.
- ull2 est un entier de 8 octets. Elle sera affectée à x2.
- ull3 est un entier de 8 octets. Elle sera affectée à x3.
pt_va_function
est une fonction variadicique, de sorte qu’elle suit les règles variadiciques Arm64EC décrites ci-dessus :
- 'f' est un double. Elle sera affectée à x0.
- 'tc' est un struct, avec une taille de 3 octets. Il sera déversé sur la pile et son emplacement chargé en x1.
- ull1 est un entier de 8 octets. Elle sera affectée à x2.
- ull2 est un entier de 8 octets. Elle sera affectée à x3.
- ull3 est un entier de 8 octets. Elle sera affectée directement à la pile.
- x4 est chargé avec l’emplacement de ull3 dans la pile.
- x5 est chargé avec la taille de ull3.
L’exemple suivant montre une sortie de compilation possible pour pt_nova_function
, qui illustre les différences d’attribution de paramètre décrites ci-dessus.
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
Ajouts ABI
Pour obtenir une interopérabilité transparente avec le code x64, de nombreux ajouts ont été apportés à l’ABI Arm64 Classique. Ils gèrent les différences de conventions d’appel entre Arm64EC et x64.
La liste suivante inclut ces ajouts :
Entrée et sortie thunks
Entrée et Sortie Thunks s’occupent de traduire la convention d’appel Arm64EC (principalement identique à Classic Arm64) dans la convention d’appel x64, et vice versa.
Une idée fausse courante est que les conventions d’appel peuvent être converties en suivant une règle unique appliquée à toutes les signatures de fonction. La réalité est que les conventions d’appel ont des règles d’attribution de paramètre. Ces règles dépendent du type de paramètre et sont différentes d’ABI à ABI. Une conséquence est que la traduction entre les API sera spécifique à chaque signature de fonction, variable avec le type de chaque paramètre.
Observez la fonction suivante :
int fJ(int a, int b, int c, int d);
L’attribution de paramètres se produit comme suit :
- Arm64 : a -> x0, b -> x1, c -> x2, d -> x3
- x64 : a -> RCX, b -> RDX, c -> R8, d -> r9
- Traduction Arm64 -> x64 : x0 -> RCX, x1 -> RDX, x2 -> R8, x3 -> R9
Considérez maintenant une fonction différente :
int fK(int a, double b, int c, double d);
L’attribution de paramètres se produit comme suit :
- Arm64 : a -> x0, b -> d0, c -> x1, d -> d1
- x64 : a -> RCX, b -> XMM1, c -> R8, d -> XMM3
- Arm64 -> traduction x64 : x0 -> RCX, d0 -> XMM1, x1 -> R8, d1 -> XMM3
Ces exemples montrent que l’attribution de paramètres et la traduction varient selon le type, mais également les types des paramètres précédents de la liste sont dépendants. Ce détail est illustré par le 3ème paramètre. Dans les deux fonctions, le type du paramètre est « int », mais la traduction résultante est différente.
Les entrées et sorties Thunks existent pour cette raison et sont spécifiquement adaptées à chaque signature de fonction individuelle.
Les deux types de thunks sont, eux-mêmes, des fonctions. Les thunks d’entrée sont automatiquement appelés par l’émulateur lorsque les fonctions x64 appellent les fonctions Arm64EC (l’exécution entre Arm64EC ). Les thunks de sortie sont automatiquement appelés par les vérificateurs d’appels lorsque les fonctions Arm64EC appellent dans des fonctions x64 (l’exécution quitte Arm64EC).
Lors de la compilation du code Arm64EC, le compilateur génère un thunk d’entrée pour chaque fonction Arm64EC, correspondant à sa signature. Le compilateur génère également un Thunk exit pour chaque fonction qu’une fonction Arm64EC appelle.
Prenons l’exemple suivant :
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);
}
Lors de la compilation du code ci-dessus ciblant Arm64EC, le compilateur génère :
- Code pour « fA ».
- Entrée Thunk pour 'fA'
- Quitter Thunk pour 'fB'
- Quitter Thunk pour 'fC'
L’entrée fA
Thunk est générée dans le cas fA
et appelée à partir du code x64. Quittez thunks pour fB
et fC
sont générés dans le cas fB
et/ou fC
s’avèrent être du code x64.
Le même Thunk de sortie peut être généré plusieurs fois, étant donné que le compilateur les générera sur le site d’appel plutôt que la fonction elle-même. Cela peut entraîner une quantité considérable de thunks redondants de sorte que, en réalité, le compilateur applique des règles d’optimisation triviales pour s’assurer que seuls les thunks requis le rendent dans le binaire final.
Par exemple, dans un fichier binaire où la fonction A
Arm64EC appelle la fonction B
Arm64EC , B
n’est pas exportée et son adresse n’est jamais connue en dehors de A
. Il est sûr d’éliminer le Thunk de sortie de A
, B
ainsi que l’Entrée Thunk pour B
. Il est également sûr d’alias ensemble tous les segments de sortie et d’entrée qui contiennent le même code, même s’ils ont été générés pour des fonctions distinctes.
Quitter thunks
À l’aide des exemples de fonctions fA
, fB
et fC
ci-dessus, il s’agit de la façon dont le compilateur génère à la fois fB
et fC
quitte les thunks :
Quitter Thunk pour 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
Quitter Thunk pour 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
Dans le fB
cas, nous pouvons voir comment la présence d’un paramètre « double » entraîne la réinscription de l’affectation d’inscription gp restante, résultat des différentes règles d’affectation d’Arm64 et x64. Nous pouvons également voir que x64 affecte uniquement 4 paramètres aux registres, de sorte que le 5e paramètre doit être déversé sur la pile.
Dans le fC
cas, le deuxième paramètre est une structure de longueur de 3 octets. Arm64 permet à toute structure de taille d’être affectée directement à un registre. x64 autorise uniquement les tailles 1, 2, 4 et 8. Ce Thunk de sortie doit ensuite transférer cette struct
opération à partir du registre sur la pile et affecter un pointeur au registre à la place. Cela consomme toujours un registre (pour transporter le pointeur) afin qu’il ne modifie pas les affectations pour les registres restants : aucun registre ne se produit pour le 3e et le 4e paramètre. Tout comme pour le fB
cas, le 5e paramètre doit être déversé sur la pile.
Considérations supplémentaires relatives à la sortie de Thunks :
- Le compilateur les nomme non pas par le nom de la fonction qu’ils traduisent>, mais plutôt par la signature qu’ils adressent. Cela facilite la recherche de redondances.
- Exit Thunk est appelé avec le registre
x9
portant l’adresse de la fonction cible (x64). Cela est défini par le vérificateur d’appel et passe par le thunk de sortie, non perturbé, dans l’émulateur.
Après avoir réorganisé les paramètres, exit Thunk appelle ensuite l’émulateur via __os_arm64x_dispatch_call_no_redirect
.
Il vaut la peine, à ce stade, d’examiner la fonction du vérificateur d’appel et de détails sur son propre ABI personnalisé. Voici à quoi ressemble un appel fB
indirect :
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
Lors de l’appel du vérificateur d’appel :
x11
fournit l’adresse de la fonction cible à appeler (fB
dans ce cas). Il peut ne pas être connu, à ce stade, si la fonction cible est Arm64EC ou x64.x10
fournit un Jeu de sortie correspondant à la signature de la fonction appelée (fB
dans ce cas).
Les données retournées par le vérificateur d’appel dépendent de la fonction cible arm64EC ou x64.
Si la cible est Arm64EC :
x11
retourne l’adresse du code Arm64EC à appeler. Il peut s’agir ou non de la même valeur que celle fournie.
Si la cible est du code x64 :
x11
retourne l’adresse du Thunk de sortie. Cette opération est copiée à partir de l’entrée fournie dansx10
.x10
retourne l’adresse du Thunk de sortie, non perturbée par l’entrée.x9
retourne la fonction x64 cible. Il peut s’agir ou non de la même valeur qu’elle a été fournie viax11
.
Les vérificateurs d’appels quittent toujours les paramètres de convention d’appel ne sont pas perturbés. Le code appelant doit donc suivre immédiatement l’appel au vérificateur d’appel avec blr x11
(ou br x11
en cas d’appel de fin). Il s’agit des vérificateurs d’appels inscrits. Ils conserveront toujours au-dessus et au-delà des registres non volatiles standard : x0
-x8
, x15
(chkstk
) et .q0
-q7
Entrée Thunks
Les thunks d’entrée prennent en charge les transformations requises de la x64 aux conventions d’appel Arm64. C’est essentiellement l’inverse de Exit Thunks, mais il y a quelques autres aspects à prendre en compte.
Considérez l’exemple précédent de compilation fA
, un thunk d’entrée est généré afin de pouvoir fA
être appelé par du code x64.
Entrée Thunk pour 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
L’adresse de la fonction cible est fournie par l’émulateur dans x9
.
Avant d’appeler l’entrée Thunk, l’émulateur x64 affiche l’adresse de retour de la pile dans le LR
registre. Il est alors prévu qu’il LR
pointe vers le code x64 lorsque le contrôle est transféré vers l’entrée Thunk.
L’émulateur peut également effectuer un autre ajustement de la pile, selon les éléments suivants : Les API Arm64 et x64 définissent une exigence d’alignement de pile où la pile doit être alignée sur 16 octets au point qu’une fonction est appelée. Lors de l’exécution du code Arm64, le matériel applique cette règle, mais il n’existe aucune application matérielle pour x64. Lors de l’exécution du code x64, les fonctions appelant de manière erronée avec une pile non alignée peuvent passer inaperçues indéfiniment, jusqu’à ce que des instructions d’alignement de 16 octets soient utilisées (certaines instructions SSE le font) ou que le code Arm64EC soit appelé.
Pour résoudre ce problème de compatibilité potentiel, avant d’appeler l’entrée Thunk, l’émulateur aligne toujours le pointeur de pile sur 16 octets et stocke sa valeur d’origine dans le x4
registre. Ainsi, l’entrée Thunks commence toujours à s’exécuter avec une pile alignée, mais peut toujours référencer correctement les paramètres transmis sur la pile, via x4
.
En ce qui concerne les registres SIMD non volatiles, il existe une différence significative entre les conventions d’appel Arm64 et x64. Sur Arm64, les 8 octets faibles (64 bits) du registre sont considérés comme non volatiles. En d’autres termes, seule la Dn
partie des Qn
registres n’est pas volatile. Sur x64, les 16 octets entiers du XMMn
registre sont considérés comme non volatiles. De plus, sur les registres x64 XMM6
et XMM7
non volatiles, les registres D6 et D7 (registres Arm64 correspondants) sont volatiles.
Pour traiter ces asymmetries de manipulation de registre SIMD, les entrées Thunks doivent enregistrer explicitement tous les registres SIMD considérés comme non volatiles dans x64. Cela n’est nécessaire que sur l’entrée Thunks (pas Exit Thunks), car x64 est plus strict que Arm64. En d’autres termes, l’enregistrement/conservation des règles dans x64 dépasse les exigences Arm64 dans tous les cas.
Pour résoudre la récupération correcte de ces valeurs d’inscription lors du déroulement de la pile (par exemple, setjmp + longjmp ou lève + catch), un nouveau décodage de déroulement a été introduit : save_any_reg (0xE7)
. Ce nouveau décodage de 3 octets permet d’enregistrer tout registre à usage général ou SIMD (y compris ceux considérés comme volatiles) et d’inclure des registres de taille Qn
complète. Ce nouvel opcode est utilisé pour les Qn
opérations de déversements/remplissage du registre ci-dessus. save_any_reg
est compatible avec save_next_pair (0xE6)
.
Pour référence, voici les informations de déroulement correspondantes appartenant à l’entrée Thunk présentée ci-dessus :
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)
Une fois la fonction Arm64EC retournée, la __os_arm64x_dispatch_ret
routine est utilisée pour entrer à nouveau l’émulateur, de retour au code x64 (pointé par LR
).
Les fonctions Arm64EC ont les 4 octets avant la première instruction de la fonction réservée pour stocker les informations à utiliser au moment de l’exécution. Il se trouve dans ces 4 octets que l’adresse relative de l’entrée Thunk pour la fonction est disponible. Lors de l’exécution d’un appel d’une fonction x64 vers une fonction Arm64EC, l’émulateur lit les 4 octets avant le début de la fonction, masque les deux bits inférieurs et ajoute cette quantité à l’adresse de la fonction. Cela génère l’adresse de l’entrée Thunk à appeler.
Ajusteur Thunks
Les ajustements Thunks sont des fonctions sans signature qui transfèrent simplement le contrôle vers (tail-call) une autre fonction, après avoir effectué une transformation vers l’un des paramètres. Le type du ou des paramètres en cours de transformation est connu, mais tous les paramètres restants peuvent être tout et, dans n’importe quel nombre , ajusteur Thunks ne touchera aucun registre pouvant contenir un paramètre et ne touchera pas la pile. C’est ce qui rend les fonctions sans signature d’Adjustor Thunks.
Les ajustements Thunks peuvent être générés automatiquement par le compilateur. C’est courant, par exemple, avec l’héritage multiple C++, où toute méthode virtuelle peut être déléguée à la classe parente, sans modification, en dehors d’un ajustement vers le this
pointeur.
Voici un exemple réel :
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
b CObjectContext::Release
Le thunk soustrait 8 octets au this
pointeur et transfère l’appel à la classe parente.
En résumé, les fonctions Arm64EC pouvant être appelées à partir de fonctions x64 doivent avoir une entrée Thunk associée. L’entrée Thunk est spécifique à la signature. Les fonctions sans signature Arm64, telles que Adjustor Thunks, ont besoin d’un mécanisme différent qui peut gérer les fonctions sans signature.
L’entrée Thunk d’un Ajusteur Thunk utilise l’assistance __os_arm64x_x64_jump
pour différer l’exécution du travail De Thunk d’entrée réel (ajuster les paramètres d’une convention à l’autre) à l’appel suivant. C’est à ce moment que la signature devient apparente. Cela inclut la possibilité de ne pas effectuer d’ajustements de convention d’appel du tout, si la cible de l’ajusteur Thunk s’avère être une fonction x64. N’oubliez pas qu’au moment où une Entrée Thunk commence à s’exécuter, les paramètres se trouvent sous leur forme x64.
Dans l’exemple ci-dessus, réfléchissez à l’apparence du code dans Arm64EC.
Ajusteur Thunk dans 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
Tronc d’entrée de Thunk de l’ajusteur
[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
Séquences de transfert rapide
Certaines applications apportent des modifications au moment de l’exécution aux fonctions résidant dans des fichiers binaires qu’elles ne possèdent pas, mais dépendent des fichiers binaires du système d’exploitation courants, pour les besoins de la déviation de l’exécution lorsque la fonction est appelée. C’est également ce qu’on appelle le raccordement.
Au niveau supérieur, le processus de raccordement est simple. En détail, toutefois, le raccordement est spécifique à l’architecture et assez complexe étant donné les variations potentielles que la logique de raccordement doit traiter.
En général, le processus implique les éléments suivants :
- Déterminez l’adresse de la fonction à raccorder.
- Remplacez la première instruction de la fonction par un saut vers la routine de crochet.
- Lorsque le crochet est terminé, revenez à la logique d’origine, qui inclut l’exécution de l’instruction d’origine déplacée.
Les variations proviennent de choses telles que :
- Taille de la 1ère instruction : il est judicieux de le remplacer par un JMP qui est de la même taille ou plus petite, pour éviter de remplacer le haut de la fonction tandis que d’autres threads peuvent l’exécuter en vol.
- Type de la première instruction : si la première instruction a une nature relative au PC, le déplacement peut nécessiter des modifications comme les champs de déplacement. Étant donné qu’elles sont susceptibles de dépasser lorsqu’une instruction est déplacée vers un endroit distant, cela peut nécessiter la fourniture d’une logique équivalente avec des instructions différentes.
En raison de toute cette complexité, la logique de raccordement robuste et générique est rare à trouver. Fréquemment, la logique présente dans les applications ne peut faire face qu’à un ensemble limité de cas que l’application s’attend à rencontrer dans les API spécifiques qui lui intéressent. Il n’est pas difficile d’imaginer la quantité d’un problème de compatibilité d’application. Même une modification simple du code ou des optimisations du compilateur peut rendre les applications inutilisables si le code ne ressemble plus exactement comme prévu.
Que se passerait-il pour ces applications s’ils rencontraient du code Arm64 lors de la configuration d’un hook ? Ils échoueraient certainement.
Les fonctions FFS (Fast-Forward Sequence) répondent à cette exigence de compatibilité dans Arm64EC.
FFS sont des fonctions x64 très petites qui ne contiennent aucune logique réelle et appel de queue à la fonction Arm64EC réelle. Ils sont facultatifs mais activés par défaut pour toutes les exportations DLL et pour n’importe quelle fonction décorée avec __declspec(hybrid_patchable)
.
Dans ce cas, lorsque le code obtient un pointeur vers une fonction donnée, soit GetProcAddress
dans le cas d’exportation, soit par &function
le __declspec(hybrid_patchable)
cas, l’adresse résultante contient du code x64. Ce code x64 passe pour une fonction x64 légitime, satisfaisant la plupart de la logique de raccordement actuellement disponible.
Prenons l’exemple suivant (gestion des erreurs omise pour la concision) :
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);
La valeur du pointeur de fonction dans la pgma
variable contiendra l’adresse de GetMachineTypeAttributes
's FFS.
Voici un exemple de séquence de transfert rapide :
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
La fonction FFS x64 a un prolog canonique et un épilogue, se terminant par un appel de fin (saut) vers la fonction réelle GetMachineTypeAttributes
dans le code 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
[...]
Il serait très inefficace s’il était nécessaire d’exécuter 5 instructions x64 émulées entre deux fonctions Arm64EC. Les fonctions FFS sont spéciales. Les fonctions FFS ne s’exécutent pas vraiment si elles restent inchangées. L’assistance de l’appelant vérifie efficacement si le FFS n’a pas été modifié. Si c’est le cas, l’appel est transféré directement vers la destination réelle. Si le FFS a été modifié de quelque manière que ce soit, il ne sera plus un FFS. L’exécution est transférée vers le FFS modifié et exécute le code qui peut y être, en émulant le détour et toute logique de raccordement.
Lorsque le hook transfère l’exécution à la fin du FFS, il atteint finalement l’appel de fin au code Arm64EC, qui s’exécute ensuite après le hook, comme l’attend l’application.
Création d’Arm64EC dans l’assembly
Les en-têtes du Kit de développement logiciel (SDK) Windows et le compilateur C peuvent simplifier le travail de création d’assembly Arm64EC. Par exemple, le compilateur C peut être utilisé pour générer des thunks d’entrée et de sortie pour les fonctions non compilées à partir du code C.
Considérez l’exemple d’un équivalent à la fonction fD
suivante qui doit être créée dans Assembly (ASM). Cette fonction peut être appelée par le code Arm64EC et x64, et le pfE
pointeur de fonction peut également pointer vers le code Arm64EC ou x64.
typedef int (PF_E)(int, double);
extern PF_E * pfE;
int fD(int i, double d) {
return (*pfE)(i, d);
}
L’écriture fD
dans ASM ressemble à ceci :
#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
Dans l’exemple ci-dessus :
- Arm64EC utilise la même déclaration de procédure et les macros prolog/épilogue que Arm64.
- Les noms de fonction doivent être encapsulés par la
A64NAME
macro. Lors de la compilation du code C/C++ en tant qu’Arm64EC, le compilateur marque leOBJ
code Arm64EC commeARM64EC
contenant. Cela ne se produit pas avecARMASM
. Lors de la compilation du code ASM, il existe un autre moyen d’informer l’éditeur de liens que le code produit est Arm64EC. Il s’agit du préfixe du nom de la fonction avec#
. LaA64NAME
macro effectue cette opération quand elle_ARM64EC_
est définie et laisse le nom inchangé lorsqu’elle_ARM64EC_
n’est pas définie. Cela permet de partager du code source entre Arm64 et Arm64EC. - Le
pfE
pointeur de fonction doit d’abord être exécuté via le vérificateur d’appel EC, ainsi que le thunk de sortie approprié, au cas où la fonction cible était x64.
Génération de thunks d’entrée et de sortie
L’étape suivante consiste à générer l’entrée Thunk pour fD
et la sortie de Thunk pour pfE
. Le compilateur C peut effectuer cette tâche avec un effort minimal, à l’aide du mot clé du _Arm64XGenerateThunk
compilateur.
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;
}
Le _Arm64XGenerateThunk
mot clé indique au compilateur C d’utiliser la signature de fonction, d’ignorer le corps et de générer un Thunk de sortie (lorsque le paramètre est 1) ou une entrée Thunk (lorsque le paramètre est 2).
Il est recommandé de placer la génération de thunk dans son propre fichier C. Le fait d’être dans des fichiers isolés simplifie la confirmation des noms de symboles en vidant les symboles correspondants OBJ
ou même en désassemblant.
Entrées personnalisées Thunks
Des macros ont été ajoutées au Kit de développement logiciel (SDK) pour faciliter la création de macros personnalisées, codées à la main, Entrée Thunks. Dans le cas où cela peut être utilisé, il s’agit de la création d’ajusteurs personnalisés Thunks.
La plupart des ajustements Thunks sont générés par le compilateur C++, mais ils peuvent également être générés manuellement. Cela se trouve dans les cas où un rappel générique transfère le contrôle au rappel réel, identifié par l’un des paramètres.
Voici un exemple de code 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
Dans cet exemple, l’adresse de fonction cible est récupérée à partir de l’élément d’une structure, fournie par référence, via le 1er paramètre. Étant donné que la structure est accessible en écriture, l’adresse cible doit être validée via Control Flow Guard (CFG).
L’exemple ci-dessous montre comment l’ajusteur Thunk équivalent ressemblerait lors du portage vers 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
Le code ci-dessus ne fournit pas de thunk de sortie (dans le registre x10). Cela n’est pas possible, car le code peut être exécuté pour de nombreuses signatures différentes. Ce code tire parti de l’appelant ayant défini x10 sur exit Thunk. L’appelant aurait effectué l’appel ciblant une signature explicite.
Le code ci-dessus a besoin d’un thunk d’entrée pour résoudre le cas lorsque l’appelant est du code x64. Voici comment créer l’entrée Thunk correspondante à l’aide de la macro pour les entrées Thunks personnalisées :
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
Contrairement à d’autres fonctions, cette entrée Thunk ne transfère pas finalement le contrôle à la fonction associée (l’ajusteur Thunk). Dans ce cas, la fonctionnalité elle-même (effectuant l’ajustement du paramètre) est incorporée dans l’entrée Thunk et le contrôle est transféré directement vers la cible de fin, via l’assistance __os_arm64x_x64_jump
.
Génération dynamique (compilation JIT) du code Arm64EC
Dans les processus Arm64EC, il existe deux types de mémoire exécutable : le code Arm64EC et le code x64.
Le système d’exploitation extrait ces informations des fichiers binaires chargés. Les fichiers binaires x64 sont tous x64 et Arm64EC contiennent une table de plages pour arm64EC et les pages de codes x64.
Qu’en est-il du code généré dynamiquement ? Les compilateurs juste-à-temps (JIT) génèrent du code, au moment de l’exécution, qui n’est pas soutenu par un fichier binaire.
Cela implique généralement :
- Allocation de mémoire accessible en écriture (
VirtualAlloc
). - Production du code dans la mémoire allouée.
- Reprotection de la mémoire de lecture-écriture en lecture-exécution (
VirtualProtect
). - Ajouter des entrées de fonction de déroulement pour toutes les fonctions générées non triviales (non feuille) (
RtlAddFunctionTable
ouRtlAddGrowableFunctionTable
).
Pour des raisons de compatibilité triviales, toute application effectuant ces étapes dans un processus Arm64EC entraîne la prise en compte du code x64. Cela se produit pour n’importe quel processus à l’aide du runtime Java x64 non modifié, du runtime .NET, du moteur JavaScript, etc.
Pour générer du code dynamique Arm64EC, le processus est principalement le même avec seulement deux différences :
- Lors de l’allocation de la mémoire, utilisez une version plus récente
VirtualAlloc2
(au lieu deVirtualAlloc
ouVirtualAllocEx
) et fournissez l’attributMEM_EXTENDED_PARAMETER_EC_CODE
. - Lors de l’ajout d’entrées de fonction :
- Ils doivent être au format Arm64. Lors de la compilation du code Arm64EC, le
RUNTIME_FUNCTION
type correspond au format x64. Pour le format Arm64 lors de la compilation d’Arm64EC, utilisez plutôt leARM64_RUNTIME_FUNCTION
type. - N’utilisez pas l’ANCIENNE
RtlAddFunctionTable
API. Utilisez toujours l’API la plus récenteRtlAddGrowableFunctionTable
à la place.
- Ils doivent être au format Arm64. Lors de la compilation du code Arm64EC, le
Voici un exemple d’allocation de mémoire :
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);
Et un exemple d’ajout d’une entrée de fonction de déroulement :
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)
);