Grundlegendes zu Arm64EC ABI und Assemblycode
Arm64EC ("Emulation compatible") ist eine neue Binäre Anwendung (Application Binary Interface, ABI) zum Erstellen von Apps für Windows 11 auf Arm. Eine Übersicht über Arm64EC und das Erstellen von Win32-Apps als Arm64EC finden Sie unter Verwenden von Arm64EC zum Erstellen von Apps für Windows 11 auf Arm-Geräten.
Zweck dieses Dokuments ist es, eine detaillierte Ansicht der Arm64EC ABI mit ausreichendEn Informationen für einen Anwendungsentwickler bereitzustellen, um Code zu schreiben und zu debuggen, der für Arm64EC kompiliert wurde, einschließlich Low-Level-/Assembler-Debugging und Schreiben von Assemblycode für die Arm64EC ABI.
Arm64EC wurde entwickelt, um Funktionen und Leistung auf systemeigener Ebene bereitzustellen und gleichzeitig transparente und direkte Interoperabilität mit x64-Code bereitzustellen, der unter emulation ausgeführt wird.
Arm64EC ist hauptsächlich zum Klassischen Arm64 ABI addiert. Sehr wenig von der klassischen ABI wurde geändert, aber Teile wurden hinzugefügt, um die x64-Interoperabilität zu ermöglichen.
In diesem Dokument wird der Originalstandard Arm64 ABI als "Classic ABI" bezeichnet. Dadurch wird die Mehrdeutigkeit vermieden, die überladenen Ausdrücken wie "Native" inhärent ist. Arm64EC, um klar zu sein, ist jedes Mal so systemisch wie die ursprüngliche ABI.
In der folgenden Liste wird darauf hingewiesen, wo Arm64EC sich von Arm64 Classic ABI unterscheidet.
- Registrieren von Zuordnungen und blockierten Registern
- Anrufprüfer
- Stapelprüfer
- Variadische Anrufkonvention
Dies sind kleine Änderungen, wenn man sieht, wie viel die gesamte ABI definiert.
Damit die Interoperabilität auf Typebene mit x64-Code erfolgen kann, wird Arm64EC-Code mit den gleichen Definitionen der Präprozessorarchitektur wie x64-Code kompiliert.
Mit anderen Worten, _M_AMD64
und _AMD64_
sie werden definiert. Einer der Typen, die von dieser Regel betroffen sind, ist die CONTEXT
Struktur. Die CONTEXT
Struktur definiert den Zustand der CPU an einem bestimmten Punkt. Sie wird für Elemente wie Exception Handling
und GetThreadContext
APIs verwendet. Der vorhandene x64-Code erwartet, dass der CPU-Kontext als x64-Struktur CONTEXT
dargestellt wird, d. h. die CONTEXT
Struktur, wie sie während der x64-Kompilierung definiert ist.
Diese Struktur muss verwendet werden, um den CPU-Kontext während der Ausführung von x64-Code sowie Arm64EC-Code darzustellen. Bestehender Code würde kein neues Konzept verstehen, z. B. das CPU-Register, das von Funktion zu Funktion wechselt. Wenn die x64-Struktur CONTEXT
verwendet wird, um Arm64-Ausführungszustände darzustellen, bedeutet dies, dass Arm64-Register effektiv in x64-Registern zugeordnet werden.
Es bedeutet auch, dass alle Arm64-Register, die nicht in die x64 CONTEXT
integriert werden können, nicht verwendet werden dürfen, da ihre Werte jederzeit verloren gehen können, wenn ein Vorgang verwendet CONTEXT
wird (und einige können asynchron und unerwartet sein, z. B. der Garbage Collection-Vorgang einer verwalteten Sprachlaufzeit oder ein APC).
Die Zuordnungsregeln zwischen Arm64EC- und x64-Registern werden durch die Struktur in den ARM64EC_NT_CONTEXT
Windows-Headern dargestellt, die im SDK vorhanden sind. Diese Struktur ist im Wesentlichen eine Vereinigung der CONTEXT
Struktur, genau wie sie für x64 definiert ist, aber mit einer zusätzlichen Arm64-Registerüberlagerung.
Beispiel: RCX
Zuordnungen zu X0
, RDX
zu X1
, RSP
zu SP
, RIP
zu PC
, zu , usw. Wir können auch sehen, wie die Register x13
, , x14
, x24
x23
, x28
keine v16
-v31
Vertretung haben und somit nicht in Arm64EC verwendet werden können.
Diese Registernutzungseinschränkung ist der erste Unterschied zwischen arm64 Classic und EC ABIs.
Anrufprüfer sind seit der Einführung von Control Flow Guard (CFG) in Windows 8.1 Teil von Windows. Anrufprüfer sind Adress-Sanitizer für Funktionszeiger (bevor diese Dinge als Adress-Sanitizer bezeichnet wurden). Jedes Mal, wenn Code mit der Option /guard:cf
kompiliert wird, generiert der Compiler einen zusätzlichen Aufruf der Checker-Funktion direkt vor jedem indirekten Aufruf/Sprung. Die Prüffunktion selbst wird von Windows bereitgestellt, und für CFG führt sie eine Gültigkeitsprüfung für die bekannten Aufrufziele durch. Diese Informationen sind auch in Binärdateien enthalten, die mit /guard:cf
kompiliert wurden.
Dies ist ein Beispiel für die Verwendung einer Anrufprüfer in 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
Im CFG-Fall gibt die Anrufprüfer einfach zurück, wenn das Ziel gültig ist, oder wenn dies nicht der Fall ist. Anrufprüfer verfügen über benutzerdefinierte Anrufkonventionen. Sie übernehmen den Funktionszeiger in einem Register, das nicht von der normalen Aufrufkonvention verwendet wird, und bewahren alle normalen Aufrufkonventionsregister auf. Auf diese Weise führen sie keine Registerüberlaufe um sie herum ein.
Anrufprüfer sind für alle anderen Windows-ABIs optional, aber für Arm64EC obligatorisch. Auf Arm64EC sammeln Anrufprüfer die Aufgabe, die Architektur der aufgerufenen Funktion zu überprüfen. Sie überprüfen, ob der Aufruf eine andere EC-Funktion ("Emulationskompatible") oder eine x64-Funktion ist, die unter Emulation ausgeführt werden muss. In vielen Fällen kann dies nur zur Laufzeit überprüft werden.
Arm64EC-Anrufprüfer bauen auf den vorhandenen Arm64-Prüfern auf, haben jedoch eine etwas andere benutzerdefinierte Anrufkonvention. Sie nehmen einen zusätzlichen Parameter, und sie können das Register ändern, das die Zieladresse enthält. Wenn das Ziel z. B. x64-Code ist, muss das Steuerelement zuerst in die Emulationsgerüstlogik übertragen werden.
In Arm64EC würde die gleiche Anrufprüfer-Verwendung zu:
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
Zu den geringfügigen Unterschieden von Classic Arm64 gehören:
- Der Symbolname für die Anrufprüfer unterscheidet sich.
- Die Zieladresse wird
x11
anstelle vonx15
. - Die Zieladresse (
x11
) ist[in, out]
anstelle von[in]
. - Es gibt einen zusätzlichen Parameter, der über
x10
" Exit Thunk" bereitgestellt wird.
Ein Exit Thunk ist ein Funclet, das Funktionsparameter aus der Arm64EC-Aufrufkonvention in x64-Aufrufkonvention transformiert.
Die Arm64EC-Anrufüberprüfung befindet sich über ein anderes Symbol als für die anderen ABIs in Windows. Auf dem klassischen Arm64 ABI ist __guard_check_icall_fptr
das Symbol für die Anrufprüfer . Dieses Symbol wird in Arm64EC vorhanden sein, aber es ist für x64 statisch verknüpften Code vorhanden, nicht Arm64EC-Code selbst. Arm64EC-Code verwendet entweder __os_arm64x_check_icall
oder __os_arm64x_check_icall_cfg
.
Bei Arm64EC sind Anrufprüfer nicht optional. CFG ist jedoch weiterhin optional, wie es bei anderen ABIs der Fall ist. CFG kann zur Kompilierungszeit deaktiviert werden, oder es gibt einen legitimen Grund, eine CFG-Überprüfung auch dann nicht auszuführen, wenn CFG aktiviert ist (z. B. der Funktionszeiger befindet sich nie im RW-Speicher). Für einen indirekten Anruf mit CFG-Überprüfung sollte die __os_arm64x_check_icall_cfg
Prüfer verwendet werden. Wenn CFG deaktiviert oder unnötig ist, __os_arm64x_check_icall
sollte stattdessen verwendet werden.
Nachfolgend finden Sie eine Zusammenfassungstabelle der Verwendung der Anrufprüfer auf classic Arm64, x64 und Arm64EC, wobei angegeben wird, dass eine Arm64EC-Binärdatei je nach Architektur des Codes zwei Optionen haben kann.
Binär | Code | Ungeschützter indirekter Aufruf | CFG-geschützter indirekter Aufruf |
---|---|---|---|
x64 | x64 | keine Anrufprüfer | __guard_check_icall_fptr oder __guard_dispatch_icall_fptr |
Arm64 Classic | ARM64 | keine Anrufprüfer | __guard_check_icall_fptr |
Arm64EC | x64 | keine Anrufprüfer | __guard_check_icall_fptr oder __guard_dispatch_icall_fptr |
Arm64EC | __os_arm64x_check_icall |
__os_arm64x_check_icall_cfg |
Unabhängig von der ABI bedeutet CFG-aktivierter Code (Code mit Verweis auf die CFG-Aufrufprüfer) zur Laufzeit keinen CFG-Schutz. CFG-geschützte Binärdateien können auf Systemen, die CFG nicht unterstützen, herunterlaufen: Die Anrufprüfung wird zur Kompilierungszeit mit einem No-Op-Hilfsprogramm initialisiert. Ein Prozess kann auch CFG durch Konfiguration deaktiviert haben. Wenn CFG deaktiviert ist (oder die Betriebssystemunterstützung nicht vorhanden ist) auf früheren ABIs aktualisiert das Betriebssystem einfach nicht die Anrufüberprüfung, wenn die Binärdatei geladen wird. Wenn der CFG-Schutz auf Arm64EC deaktiviert ist, legt __os_arm64x_check_icall_cfg
das Betriebssystem das gleiche fest wie __os_arm64x_check_icall
, das weiterhin die erforderliche Überprüfung der Zielarchitektur in allen Fällen bereitstellt, jedoch keinen CFG-Schutz.
Wie bei CFG in Classic Arm64 muss der Aufruf der Zielfunktion (x11
) sofort dem Aufruf der Anrufprüfung folgen. Die Adresse der Anrufüberprüfung muss in ein veränderliches Register eingefügt werden, und weder die Adresse der Zielfunktion noch die Adresse der Zielfunktion sollte jemals in ein anderes Register kopiert oder in den Speicher übergelaufen werden.
__chkstk
wird automatisch vom Compiler verwendet, wenn eine Funktion einen Bereich auf dem Stapel zuweist, der größer als eine Seite ist. Um zu vermeiden, dass die Stack Guard-Seite übersprungen wird, die das Ende des Stapels schützt, aufgerufen, um sicherzustellen, __chkstk
dass alle Seiten im zugeordneten Bereich durchsucht werden.
__chkstk
wird in der Regel aus dem Prolog der Funktion aufgerufen. Aus diesem Grund und für eine optimale Codegenerierung wird eine benutzerdefinierte Aufrufkonvention verwendet.
Dies bedeutet, dass x64-Code und Arm64EC-Code eigene, unterschiedliche __chkstk
Funktionen benötigen, da Entry- und Exit-Thunks standardmäßige Aufrufkonventionen annehmen.
x64 und Arm64EC verwenden denselben Symbolnamespace, sodass keine zwei Funktionen benannt __chkstk
werden können. Um die Kompatibilität mit bereits vorhandenem x64-Code zu berücksichtigen, __chkstk
wird der Name der x64-Stapelprüfung zugeordnet. Stattdessen wird Arm64EC-Code verwendet __chkstk_arm64ec
.
Die benutzerdefinierte Anrufkonvention ist __chkstk_arm64ec
identisch mit dem klassischen Arm64 __chkstk
: x15
stellt die Größe der Zuordnung in Byte bereit, dividiert durch 16. Alle nicht veränderliche Register sowie alle veränderliche Register, die an der Standardanrufkonvention beteiligt sind, bleiben erhalten.
Alles, was oben gesagt wurde __chkstk
, gilt gleichermaßen für __security_check_cookie
und sein Arm64EC-Gegenstück: __security_check_cookie_arm64ec
.
Arm64EC folgt der klassischen Arm64 ABI-Aufrufkonvention, mit Ausnahme von variadischen Funktionen (aka varargs, aka-Funktionen mit den Auslassungszeichen (. .) Parameterschlüsselwort).
Für den variadischen Spezifischen Fall folgt Arm64EC einer Aufrufkonvention, die mit nur wenigen Unterschieden sehr ähnlich wie x64 variadic ist. Im Folgenden finden Sie die wichtigsten Regeln für arm64EC variadic:
- Nur die ersten 4 Register werden für die Parameterübergabe verwendet:
x0
,x1
, ,x2
.x3
Verbleibende Parameter werden auf den Stapel übergelaufen. Dies folgt genau der x64 variadischen Aufrufkonvention und unterscheidet sich von Arm64 Classic, wo Registerx0
>x7
verwendet werden. - Gleitkomma-/SIMD-Parameter, die von register übergeben werden, verwenden ein General-Purpose-Register, kein SIMD-Parameter. Dies ähnelt Arm64 Classic und unterscheidet sich von x64, wobei FP/SIMD-Parameter sowohl in einem General-Purpose- als auch im SIMD-Register übergeben werden. For example, for a function
f1(int, …)
being called asf1(int, double)
, on x64, the second parameter will be assigned to bothRDX
andXMM1
. Auf Arm64EC wird der zweite Parameter nurx1
zugewiesen. - Beim Übergeben von Strukturen nach Wert über ein Register gelten x64-Größenregeln: Strukturen mit genau 1, 2, 4 und 8 werden direkt in das Register für allgemeine Zwecke geladen. Strukturen mit anderen Größen werden auf den Stapel übergelaufen, und dem Register wird ein Zeiger auf die übergelaufene Position zugewiesen. Dies herabgestuft im Wesentlichen den Nachwert in einen Nachverweis auf niedriger Ebene. Auf dem klassischen Arm64 ABI werden Strukturen beliebiger Größe bis zu 16 Byte direkt den General-Purposed-Registern zugewiesen.
- X4-Register wird mit einem Zeiger auf den ersten Parameter geladen, der über stapel übergeben wird (der 5. Parameter). Dies schließt keine Strukturen ein, die aufgrund der oben beschriebenen Größenbeschränkungen übergelaufen sind.
- Das X5-Register wird in Bytes mit der Größe aller Parameter geladen, die vom Stapel übergeben werden (Größe aller Parameter beginnend mit dem 5. ). Dies schließt keine Strukturen ein, die aufgrund der oben beschriebenen Größenbeschränkungen übergeben werden.
Im folgenden Beispiel: pt_nova_function
Unten werden Parameter in einer nicht variadischen Form verwendet, daher nach der klassischen Arm64-Aufrufkonvention. Anschließend wird pt_va_function
er mit genau denselben Parametern aufgerufen, aber stattdessen in einem variadischen Aufruf.
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
akzeptiert fünf Parameter, die nach den Klassischen Arm64-Anrufkonventionsregeln zugewiesen werden:
- 'f' ist ein Double. Sie wird d0 zugewiesen.
- "tc" ist eine Struktur mit einer Größe von 3 Bytes. Sie wird x0 zugewiesen.
- ull1 ist eine ganze Zahl mit 8 Byte. Sie wird x1 zugewiesen.
- ull2 ist eine ganze Zahl von 8 Byte. Sie wird x2 zugewiesen.
- ull3 ist eine ganze Zahl von 8 Byte. Sie wird x3 zugewiesen.
pt_va_function
ist eine variadische Funktion, sodass sie den oben beschriebenen Variadischen Regeln von Arm64EC folgt:
- 'f' ist ein Double. Sie wird x0 zugewiesen.
- "tc" ist eine Struktur mit einer Größe von 3 Bytes. Es wird auf den Stapel übergelaufen und seine Position in x1 geladen.
- ull1 ist eine ganze Zahl mit 8 Byte. Sie wird x2 zugewiesen.
- ull2 ist eine ganze Zahl von 8 Byte. Sie wird x3 zugewiesen.
- ull3 ist eine ganze Zahl von 8 Byte. Sie wird direkt dem Stapel zugewiesen.
- x4 wird mit der Position von "ull3" im Stapel geladen.
- x5 wird mit der Größe von "ull3" geladen.
Im Folgenden finden Sie eine mögliche Kompilierungsausgabe, pt_nova_function
die die oben beschriebenen Parameterzuweisungsunterschiede veranschaulicht.
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
Um eine transparente Interoperabilität mit x64-Code zu erzielen, wurden viele Ergänzungen an der klassischen Arm64 ABI vorgenommen. Sie behandeln die Unterschiede zwischen Arm64EC und x64.
Die folgende Liste enthält diese Ergänzungen:
Entry and Exit Thunks kümmern sich um die Übersetzung der Arm64EC-Anrufkonvention (meist identisch mit Classic Arm64) in die x64-Anrufkonvention und umgekehrt.
Ein häufiges Missverständnis besteht darin, dass Aufrufkonventionen durch Ausführen einer einzelnen Regel konvertiert werden können, die auf alle Funktionssignaturen angewendet wird. Die Realität ist, dass Aufrufkonventionen Über Parameterzuweisungsregeln verfügen. Diese Regeln hängen vom Parametertyp ab und unterscheiden sich von ABI zu ABI. Eine Folge ist, dass die Übersetzung zwischen ABIs für jede Funktionssignatur spezifisch ist, je nach Typ der einzelnen Parameter.
Betrachten Sie die folgende Funktion:
int fJ(int a, int b, int c, int d);
Die Parameterzuweisung erfolgt wie folgt:
- Arm64: a -> x0, b -> x1, c -> x2, d -> x3
- x64: a -> RCX, b -> RDX, c -> R8, d -> r9
- Arm64 -> x64 Übersetzung: x0 -> RCX, x1 -> RDX, x2 -> R8, x3 -> R9
Betrachten Sie nun eine andere Funktion:
int fK(int a, double b, int c, double d);
Die Parameterzuweisung erfolgt wie folgt:
- Arm64: a -> x0, b -> d0, c -> x1, d -> d1
- x64: a -> RCX, b -> XMM1, c -> R8, d -> XMM3
- Arm64 -> x64 Übersetzung: x0 -> RCX, d0 -> XMM1, x1 -> R8, d1 -> XMM3
Diese Beispiele veranschaulichen, dass die Parameterzuweisung und -übersetzung je nach Typ variieren, aber auch die Typen der vorherigen Parameter in der Liste sind abhängig. Dieses Detail wird durch den 3. Parameter veranschaulicht. In beiden Funktionen ist der Parametertyp "int", die resultierende Übersetzung ist jedoch unterschiedlich.
Aus diesem Grund gibt es Eingangs- und Ausgangs-Thunks und sind speziell auf jede einzelne Funktionssignatur zugeschnitten.
Beide Arten von Thunks sind selbst Funktionen. Entry Thunks werden automatisch vom Emulator aufgerufen, wenn x64-Funktionen Arm64EC-Funktionen aufrufen (Ausführung Enters Arm64EC). Exit Thunks werden automatisch von den Anrufprüfern aufgerufen, wenn Arm64EC-Funktionen x64-Funktionen aufrufen (Ausführung Exits Arm64EC).
Beim Kompilieren von Arm64EC-Code generiert der Compiler für jede Arm64EC-Funktion einen Eintrags-Thunk, der seine Signatur abgleicht. Der Compiler generiert für jede Funktion auch einen Exit Thunk für jede Funktion, die eine Arm64EC-Funktionsaufrufe aufruft.
Betrachten Sie das folgende Beispiel:
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);
}
Beim Kompilieren des obigen Codes für Arm64EC generiert der Compiler Folgendes:
- Code für 'fA'.
- Eintrag Thunk für 'fA'
- Exit Thunk für 'fB'
- Exit Thunk für 'fC'
Der fA
Eintrag Thunk wird für den Fall fA
generiert und aus x64-Code aufgerufen. Exit Thunks for fB
and fC
are generated in case fB
and/or fC
and turn out to be x64 code.
Dasselbe Exit Thunk kann mehrmals generiert werden, da der Compiler sie an der Aufrufwebsite generiert, anstatt die Funktion selbst. Dies kann zu einer beträchtlichen Menge redundanter Thunks führen, sodass der Compiler in Wirklichkeit triviale Optimierungsregeln anwendet, um sicherzustellen, dass nur die erforderlichen Thunks es in die endgültige Binärdatei umwandeln.
In einer Binärdatei, in der die Arm64EC-Funktion A
arm64EC aufruft, B
wird die Arm64EC-Funktion B
nicht exportiert, und ihre Adresse ist außerhalb von A
. Es ist sicher, den Exit Thunk A
von zu B
beseitigen, zusammen mit der Entry Thunk für B
. Es ist auch sicher, alle Exit- und Entry-Thunks zusammen zu aliasen, die denselben Code enthalten, auch wenn sie für unterschiedliche Funktionen generiert wurden.
Mit den Beispielfunktionen fA
fB
und fC
oben wird der Compiler sowohl als fC
auch fB
Exit Thunks generieren:
Exit Thunk nach 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
Exit Thunk nach 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
In diesem fB
Fall können wir sehen, wie das Vorhandensein eines "double"-Parameters dazu führt, dass die verbleibende GP-Registerzuweisung erneut eingeblendet wird, ein Ergebnis der verschiedenen Zuordnungsregeln von Arm64 und x64. Wir können auch sehen, dass x64 nur vier Parameter zu Registern zuweist, sodass der 5. Parameter auf den Stapel übergelaufen werden muss.
fC
Im Fall ist der zweite Parameter eine Struktur von 3-Byte-Länge. Arm64 ermöglicht die direkte Zuweisung einer Größenstruktur zu einem Register. x64 lässt nur größen 1, 2, 4 und 8 zu. Dieser Exit Thunk muss dies dann vom Register auf den Stapel übertragen struct
und stattdessen einen Zeiger auf das Register zuweisen. Dies verwendet weiterhin ein Register (um den Zeiger zu tragen), sodass keine Zuordnungen für die verbleibenden Register geändert werden: Für den 3. und 4. Parameter erfolgt keine Registerumbruch. Genau wie für den fB
Fall muss der 5. Parameter auf den Stapel übergelaufen werden.
Zusätzliche Überlegungen für Exit Thunks:
- Der Compiler trägt sie nicht nach dem Funktionsnamen, von> dem sie übersetzen, sondern der Signatur, die sie adressieren. Dies erleichtert das Auffinden von Redundanzen.
- Die Exit Thunk wird mit dem Register
x9
aufgerufen, das die Adresse der Zielfunktion (x64) trägt. Dies wird von der Anrufprüfer festgelegt und durchläuft die Exit Thunk, unbestreitet, in den Emulator.
Nach dem Neuanordnen der Parameter ruft die Exit Thunk dann über den Emulator __os_arm64x_dispatch_call_no_redirect
auf.
An diesem Punkt lohnt es sich, die Funktion der Anrufprüfer zu überprüfen und details zu seiner eigenen benutzerdefinierten ABI. So würde ein indirekter Aufruf fB
aussehen:
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
Beim Aufrufen der Anrufprüfer:
x11
stellt die Adresse der Zielfunktion bereit, die aufgerufen werden soll (fB
in diesem Fall). Es ist möglicherweise nicht bekannt, zu diesem Zeitpunkt, wenn die Zielfunktion Arm64EC oder x64 ist.x10
liefert einen Exit Thunk, der der Signatur der aufgerufenen Funktion entspricht (fB
in diesem Fall).
Die von der Aufrufprüfer zurückgegebenen Daten hängen von der Zielfunktion ab, die Arm64EC oder x64 ist.
Wenn das Ziel Arm64EC lautet:
x11
gibt die Adresse des Arm64EC-Codes zurück, der aufgerufen werden soll. Dies kann oder nicht derselbe Wert sein, in dem angegeben wurde.
Wenn das Ziel x64-Code ist:
x11
gibt die Adresse des Exit Thunk zurück. Dies wird aus der Eingabe kopiert, die inx10
.x10
gibt die Adresse des Exit Thunk zurück, die nicht von der Eingabe entfernt wurde.x9
gibt die x64-Zielfunktion zurück. Dies kann oder nicht derselbe Wert sein, über den sie bereitgestelltx11
wurde.
Anrufprüfer lassen immer die Aufrufkonventionsparameter register unbeachtet, sodass der aufrufende Code dem Aufruf der Anrufprüfer sofort folgen blr x11
sollte (oder br x11
bei einem Tail-Call). Hierbei handelt es sich um die Registrierten Anrufprüfer. Sie werden immer über standardmäßige nicht veränderliche Register hinaus beibehalten: x0
-x8
, x15
(chkstk
) und .q0
-q7
Entry Thunks kümmern sich um die Transformationen, die von der x64 in die Arm64-Aufrufkonventionen erforderlich sind. Dies ist im Wesentlichen die Umgekehrte von Exit Thunks, aber es gibt ein paar weitere Aspekte zu berücksichtigen.
Sehen Sie sich das vorherige Beispiel für die Kompilierung fA
an, ein Entry Thunk wird generiert, fA
sodass dieser durch x64-Code aufgerufen werden kann.
Eintrag Thunk für 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
Die Adresse der Zielfunktion wird vom Emulator in x9
bereitgestellt.
Vor dem Aufrufen des Eintrags Thunk füllt der x64-Emulator die Absenderadresse aus dem Stapel in das LR
Register ein. Es wird dann erwartet, dass LR
beim Übertragen der Steuerung auf den Entry Thunk auf x64 Code verweist.
Der Emulator kann auch eine weitere Anpassung am Stapel vornehmen, je nach folgendem: Sowohl Arm64- als auch x64-ABIs definieren eine Stapelausrichtungsanforderung, bei der der Stapel an dem Punkt, an dem eine Funktion ausgerichtet werden muss, ausgerichtet werden muss. Wenn Arm64-Code ausgeführt wird, erzwingt Hardware diese Regel, aber es gibt keine Hardwareerzwingung für x64. Beim Ausführen von x64-Code gehen fehlerhafte Aufrufe von Funktionen mit einem nicht ausgerichteten Stapel möglicherweise unbemerkt unbemerkt, bis einige 16-Byte-Ausrichtungsanweisung verwendet wird (einige SSE-Anweisungen tun) oder Arm64EC-Code aufgerufen wird.
Um dieses potenzielle Kompatibilitätsproblem zu beheben, richtet der Emulator vor dem Aufrufen des Entry Thunk immer den Stack Pointer auf 16 Byte aus und speichert seinen ursprünglichen Wert im x4
Register. Auf diese Weise beginnen Entry Thunks immer mit der Ausführung eines ausgerichteten Stapels, kann aber trotzdem korrekt auf die parameter verweisen, die am Stapel übergeben werden, via x4
.
Bei nicht volatilen SIMD-Registern besteht ein erheblicher Unterschied zwischen den Konventionen für Arm64- und x64-Aufrufe. Bei Arm64 gelten die niedrigen 8 Bytes (64 Bit) des Registers als nicht veränderlich. Mit anderen Worten, nur der Dn
Teil der Qn
Register ist nicht veränderlich. Bei x64 gilt die gesamte 16 Bytes des XMMn
Registers als nicht veränderlich. Darüber hinaus sind auf x64 und XMM7
nicht veränderliche Register vorhanden, XMM6
während D6 und D7 (die entsprechenden Arm64-Register) veränderlich sind.
Um diese SIMD Register Manipulation asymmetrien zu adressieren, muss Entry Thunks explizit alle SIMD-Register speichern, die in x64 als nicht veränderlich angesehen werden. Dies ist nur bei Entry Thunks (nicht Exit Thunks) erforderlich, da x64 strenger als Arm64 ist. Mit anderen Worten, register saving/preservation rules in x64 überschreiten die Arm64-Anforderungen in allen Fällen.
Um die richtige Wiederherstellung dieser Registerwerte beim Abwickeln des Stapels zu beheben (z. B. setjmp + longjmp, oder throw + catch), wurde ein neuer Abspann opcode eingeführt: save_any_reg (0xE7)
. Mit diesem neuen 3-Byte-Relax-Opcode können Alle Register für allgemeine Zwecke oder SIMD (einschließlich der als veränderlich eingestuften) und einschließlich vollwertiger Qn
Register gespeichert werden. Dieser neue Opcode wird für die Qn
oben genannten Registerüberlauf-/Ausfüllvorgänge verwendet. save_any_reg
ist kompatibel mit save_next_pair (0xE6)
.
Im Folgenden finden Sie die entsprechenden Entspanninformationen, die dem oben vorgestellten Eintrag Thunk gehören:
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)
Nachdem die Arm64EC-Funktion zurückgegeben wurde, wird die __os_arm64x_dispatch_ret
Routine verwendet, um den Emulator wieder einzugeben, zurück zum x64-Code (auf den verwiesen von LR
).
Arm64EC-Funktionen verfügen über die 4 Byte vor der ersten Anweisung in der Funktion, die zum Speichern von Informationen reserviert ist, die zur Laufzeit verwendet werden sollen. In diesen 4 Byte ist die relative Adresse des Eintrags Thunk für die Funktion zu finden. Beim Ausführen eines Aufrufs von einer x64-Funktion an eine Arm64EC-Funktion liest der Emulator die vier Bytes vor dem Start der Funktion, maskieren Sie die unteren beiden Bits, und fügen Sie diesen Betrag der Adresse der Funktion hinzu. Dies wird die Adresse des Eintrags Thunk liefern, der angerufen wird.
Adjustor Thunks sind Signaturenlose Funktionen, die einfach die Steuerung an eine andere Funktion übertragen (Tail-Call), nachdem sie eine Transformation zu einem der Parameter durchgeführt haben. Der Typ der transformierten Parameter ist bekannt, aber alle verbleibenden Parameter können alles sein und in einer beliebigen Zahl – Adjustor Thunks berührt kein Register, das potenziell einen Parameter enthält und den Stapel nicht berührt. Dies macht Adjustor Thunks signaturlose Funktionen.
Adjustor Thunks können automatisch vom Compiler generiert werden. Dies ist z. B. bei C++-Vererbung mit mehreren Vererbungen üblich, wobei jede virtuelle Methode an die übergeordnete Klasse delegiert werden kann, die nicht geändert wird, abgesehen von einer Anpassung an den this
Zeiger.
Im Folgenden finden Sie ein Beispiel für eine praxisnahe Anwendung:
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
b CObjectContext::Release
Der Thunk subtrahiert 8 Bytes an den this
Zeiger und leitet den Aufruf an die übergeordnete Klasse weiter.
Zusammenfassend müssen arm64EC-Funktionen, die von x64-Funktionen aufgerufen werden können, über einen zugeordneten Entry Thunk verfügen. Der Eintrag Thunk ist unterschriftsspezifisch. Arm64-Funktionen ohne Signaturen, z. B. Adjustor Thunks, benötigen einen anderen Mechanismus, der signaturlose Funktionen verarbeiten kann.
Der Entry Thunk eines Adjustor Thunk verwendet den __os_arm64x_x64_jump
Helfer, um die Ausführung der echten Entry Thunk-Arbeit zu verzögern (die Parameter von einer Konvention auf die andere anzupassen) auf den nächsten Aufruf zurückstellen. Es ist zu diesem Zeitpunkt, dass die Signatur offensichtlich wird. Dazu gehört auch die Möglichkeit, überhaupt keine Anrufkonventionsanpassungen durchzuführen, wenn sich das Ziel des Adjustor Thunk als x64-Funktion herausstellt. Denken Sie daran, dass sich die Parameter in ihrer x64-Form befinden, wenn ein Entry Thunk gestartet wird.
Überlegen Sie sich im obigen Beispiel, wie der Code in Arm64EC aussieht.
Adjustor Thunk in 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
Adjustor Thunks Einstiegstrunk
[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
Einige Anwendungen nehmen Laufzeitänderungen an Funktionen vor, die sich in Binärdateien befinden, die sie nicht besitzen, sondern von – häufig Betriebssystem-Binärdateien – abhängig sind, um die Ausführung zu umleiten, wenn die Funktion aufgerufen wird. Dies wird auch als Hooking bezeichnet.
Auf hoher Ebene ist der Hakenprozess einfach. Im Detail ist hooking jedoch architekturspezifisch und ziemlich komplex, da die potenziellen Variationen der Hookinglogik adressiert werden müssen.
Im Allgemeinen umfasst der Prozess Folgendes:
- Bestimmen Sie die Adresse der zu verbindenden Funktion.
- Ersetzen Sie die erste Anweisung der Funktion durch einen Sprung zur Hook-Routine.
- Wenn der Haken fertig ist, kehren Sie zur ursprünglichen Logik zurück, die das Ausführen der vertriebenen ursprünglichen Anweisung umfasst.
Die Variationen ergeben sich aus Folgenden:
- Die Größe der 1. Anweisung: Es empfiehlt sich, sie durch einen JMP zu ersetzen, der die gleiche Größe oder kleiner ist, um zu vermeiden, dass der obere Rand der Funktion ersetzt wird, während ein anderer Thread sie im Test-Flight ausführt.
- Der Typ der ersten Anweisung: Wenn die erste Anweisung über eine relative PC-Art verfügt, müssen Sie sie möglicherweise ändern, z. B. die Verdrängungsfelder. Da sie wahrscheinlich überlaufen werden, wenn eine Anweisung an einen entfernten Ort verschoben wird, kann dies eine gleichwertige Logik mit unterschiedlichen Anweisungen insgesamt erfordern.
Aufgrund dieser Komplexität ist robuste und generische Hookinglogik selten zu finden. Häufig kann die logik, die in Anwendungen vorhanden ist, nur mit einer begrenzten Anzahl von Fällen umgehen, auf die die Anwendung in den spezifischen APIs stoßen wird, an denen sie interessiert ist. Es ist nicht schwierig, sich vorzustellen, wie viel von einem Anwendungskompatibilitätsproblem dies ist. Selbst eine einfache Änderung der Code- oder Compileroptimierungen kann Anwendungen unbrauchbar machen, wenn der Code nicht mehr genau wie erwartet aussieht.
Was passiert mit diesen Anwendungen, wenn sie beim Einrichten eines Hooks auf Arm64-Code stoßen würden? Sie würden sicherlich scheitern.
Fast-Forward Sequence (FFS)-Funktionen erfüllen diese Kompatibilitätsanforderung in Arm64EC.
FFS sind sehr kleine x64-Funktionen, die keine echte Logik und einen Tail-Aufruf der echten Arm64EC-Funktion enthalten. Sie sind optional, aber standardmäßig für alle DLL-Exporte und für jede Funktion aktiviert, die mit __declspec(hybrid_patchable)
.
In diesen Fällen enthält der resultierende Code x64-Code, wenn code einen Zeiger auf eine bestimmte Funktion abruft, entweder durch GetProcAddress
den Exportfall oder durch &function
den __declspec(hybrid_patchable)
Fall. Dieser x64-Code wird für eine legitime x64-Funktion übergeben und erfüllt die meisten der derzeit verfügbaren Hookinglogik.
Betrachten Sie das folgende Beispiel (Fehlerbehandlung wird aus Platzgründen weggelassen):
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);
Der Wert des Funktionszeigers in der pgma
Variablen enthält die Adresse des GetMachineTypeAttributes
FFS.
Dies ist ein Beispiel für eine Fast-Forward-Sequenz:
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
Die FFS x64-Funktion verfügt über einen kanonischen Prolog und Epilog, der mit einem Tail-Call (Jump) endet, bis zur realen GetMachineTypeAttributes
Funktion im Arm64EC-Code:
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
[...]
Es wäre ziemlich ineffizient, wenn es erforderlich wäre, 5 emulierte x64-Anweisungen zwischen zwei Arm64EC-Funktionen auszuführen. FFS-Funktionen sind besonders. FFS-Funktionen werden nicht wirklich ausgeführt, wenn sie unverändert bleiben. Die Anrufprüferhilfe überprüft effizient, ob das FFS nicht geändert wurde. Wenn dies der Fall ist, wird der Anruf direkt an das eigentliche Ziel weitergeleitet. Wenn der FFS auf eine mögliche Weise geändert wurde, ist es kein FFS mehr. Die Ausführung wird an den geänderten FFS übertragen und den Code ausführen, der den Umweg und jede Hooking-Logik emuliert.
Wenn der Hook die Ausführung an das Ende des FFS zurückgibt, wird er schließlich den Tail-Aufruf des Arm64EC-Codes erreichen, der dann nach dem Hook ausgeführt wird, genau wie die Anwendung es erwartet.
Windows SDK-Header und der C-Compiler können die Erstellung der Arm64EC-Assembly vereinfachen. Beispielsweise kann der C-Compiler verwendet werden, um Entry and Exit Thunks für Funktionen zu generieren, die nicht aus C-Code kompiliert wurden.
Betrachten Sie das Beispiel einer Entsprechung mit der folgenden Funktion fD
, die in Assembly (ASM) erstellt werden muss. Diese Funktion kann sowohl von Arm64EC- als auch von x64-Code aufgerufen werden, und der pfE
Funktionszeiger kann auch auf Arm64EC- oder x64-Code zeigen.
typedef int (PF_E)(int, double);
extern PF_E * pfE;
int fD(int i, double d) {
return (*pfE)(i, d);
}
Das Schreiben fD
in ASM würde etwa wie folgt aussehen:
#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
Auf das obige Beispiel trifft Folgendes zu:
- Arm64EC verwendet dieselbe Prozedurdeklaration und Prolog-/Epilog-Makros wie Arm64.
- Funktionsnamen sollten durch das
A64NAME
Makro umbrochen werden. Beim Kompilieren von C/C++-Code als Arm64EC markiert der Compiler denOBJ
Code "ARM64EC
Arm64EC". Dies geschieht nicht mitARMASM
. Beim Kompilieren von ASM-Code gibt es eine alternative Möglichkeit, den Linker darüber zu informieren, dass der erstellte Code Arm64EC ist. Dies ist das Präfix des Funktionsnamens mit#
dem Präfix . DasA64NAME
Makro führt diesen Vorgang aus, wenn_ARM64EC_
er definiert ist und den Namen unverändert lässt, wenn_ARM64EC_
er nicht definiert ist. Dies ermöglicht das Freigeben von Quellcode zwischen Arm64 und Arm64EC. - Der
pfE
Funktionszeiger muss zuerst über die EC-Anrufprüfer ausgeführt werden, zusammen mit dem entsprechenden Exit Thunk, falls die Zielfunktion x64 ist.
Der nächste Schritt besteht darin, die Entry Thunk für fD
und die Exit Thunk für pfE
. Der C-Compiler kann diese Aufgabe mit minimalem Aufwand ausführen, indem das _Arm64XGenerateThunk
Compilerschlüsselwort verwendet wird.
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;
}
Das _Arm64XGenerateThunk
Schlüsselwort weist den C-Compiler an, die Funktionssignatur zu verwenden, den Text zu ignorieren und entweder einen Exit Thunk (wenn der Parameter 1 ist) oder einen Entry Thunk (wenn der Parameter 2 ist) zu generieren.
Es wird empfohlen, die Thunk-Generation in einer eigenen C-Datei zu platzieren. Wenn Sie sich in isolierten Dateien befinden, ist es einfacher, die Symbolnamen zu bestätigen, indem sie die entsprechenden OBJ
Symbole verwerfen oder sogar zerlegen.
Makros wurden dem SDK hinzugefügt, um die Erstellung von benutzerdefinierten, handcodierten, Entry Thunks zu unterstützen. Ein Fall, in dem dies verwendet werden kann, ist die Erstellung von benutzerdefinierten Adjustor Thunks.
Die meisten Adjustor Thunks werden vom C++-Compiler generiert, können aber auch manuell generiert werden. Dies kann in Fällen gefunden werden, in denen ein generisches Rückrufsteuerelement an den tatsächlichen Rückruf überträgt, der durch einen der Parameter identifiziert wird.
Im Folgenden finden Sie ein Beispiel in Arm64 Classic Code:
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
In diesem Beispiel wird die Zielfunktionsadresse aus dem Element einer Struktur abgerufen, die durch Verweis über den 1. Parameter bereitgestellt wird. Da die Struktur schreibbar ist, muss die Zieladresse über Control Flow Guard (CFG) überprüft werden.
Im folgenden Beispiel wird veranschaulicht, wie der entsprechende Adjustor Thunk aussehen würde, wenn er nach Arm64EC portiert wurde:
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
Der obige Code liefert keinen Exit Thunk (im Register x10). Dies ist nicht möglich, da der Code für viele verschiedene Signaturen ausgeführt werden kann. Dieser Code nutzt den Aufrufer, der x10 auf "Exit Thunk" festgelegt hat. Der Anrufer hätte den Anruf auf eine explizite Signatur ausgerichtet.
Der obige Code benötigt einen Eintrag Thunk, um den Fall zu beheben, wenn der Aufrufer x64-Code ist. So erstellen Sie den entsprechenden Eintrag Thunk unter Verwendung des Makros für benutzerdefinierte Entry Thunks:
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
Im Gegensatz zu anderen Funktionen überträgt dieser Eintrags-Thunk schließlich keine Steuerung an die zugeordnete Funktion (der Adjustor Thunk). In diesem Fall wird die Funktionalität selbst (die Parameteranpassung durchführen) in den Entry Thunk eingebettet und die Steuerung direkt an das Endziel über den __os_arm64x_x64_jump
Helfer übertragen.
In Arm64EC-Prozessen gibt es zwei Arten von ausführbarem Speicher: Arm64EC-Code und x64-Code.
Das Betriebssystem extrahiert diese Informationen aus den geladenen Binärdateien. x64 Binärdateien sind alle x64 und Arm64EC enthalten eine Bereichstabelle für Arm64EC vs x64 Code pages.
Was ist mit dynamisch generierten Code? Just-in-Time(JIT)-Compiler generieren Zur Laufzeit Code, der nicht durch eine Binärdatei gesichert wird.
In der Regel bedeutet dies:
- Zuordnen des schreibbaren Speichers (
VirtualAlloc
). - Der Code wird im zugewiesenen Speicher erstellt.
- Erneutes Schützen des Speichers vor Lese-/Schreibzugriff auf "Read-Execute" (
VirtualProtect
). - Fügen Sie Entspannfunktionseinträge für alle nicht trivialen (nicht blattfreien) generierten Funktionen (
RtlAddFunctionTable
oderRtlAddGrowableFunctionTable
) hinzu.
Aus trivialen Kompatibilitätsgründen führt jede Anwendung, die diese Schritte in einem Arm64EC-Prozess ausführt, dazu, dass der Code als x64-Code betrachtet wird. Dies geschieht für jeden Prozess, der die nicht geänderte x64-Java-Runtime, .NET-Runtime, javaScript-Engine usw. verwendet.
Zum Generieren von dynamischem Arm64EC-Code ist der Prozess meist mit nur zwei Unterschieden identisch:
- Verwenden Sie beim Zuweisen des Speichers neuere
VirtualAlloc2
(anstelle oderVirtualAlloc
VirtualAllocEx
) und geben Sie das Attribut anMEM_EXTENDED_PARAMETER_EC_CODE
. - Beim Hinzufügen von Funktionseinträgen:
- Sie müssen im Arm64-Format vorliegen. Beim Kompilieren von Arm64EC-Code entspricht der
RUNTIME_FUNCTION
Typ dem x64-Format. Verwenden Sie für das Arm64-Format beim Kompilieren von Arm64EC stattdessen denARM64_RUNTIME_FUNCTION
Typ. - Verwenden Sie nicht die ältere
RtlAddFunctionTable
API. Verwenden Sie stattdessen immer die neuereRtlAddGrowableFunctionTable
API.
- Sie müssen im Arm64-Format vorliegen. Beim Kompilieren von Arm64EC-Code entspricht der
Nachfolgend sehen Sie ein Beispiel für die Speicherzuweisung:
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);
Und ein Beispiel für das Hinzufügen eines Abspannfunktionseintrags:
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)
);
Feedback zu Windows on Arm
Windows on Arm ist ein Open Source-Projekt. Wählen Sie einen Link aus, um Feedback zu geben: