Übersicht über ARM32-ABI-Konventionen

Die Binärschnittstelle (ABI, Application Binary Interface) für den für Windows on ARM-Prozessoren kompilierten Code basiert auf der standardmäßigen ARM EABI. Dieser Artikel zeigt die wichtigsten Unterschiede zwischen Windows on ARM und dem Standard. Thema dieses Artikels ist die ARM32-ABI. Weitere Informationen zur ARM64-ABI finden Sie unter Übersicht über ARM64-ABI-Konventionen. Weitere Informationen zur Standard-ARM-EABI finden Sie unter Application Binary Interface (ABI) für die ARM-Architektur (externer Link).

Anforderungen – einfach

Für Windows on ARM wird immer vorausgesetzt, dass es unter einer ARMv7-Architektur ausgeführt wird. Die Hardware muss eine Gleitkommaunterstützung in Form von VFPv3-D32 oder höher bieten. Das VFP muss in der Hardware sowohl ein Gleitkomma in einfacher als auch doppelter Genauigkeit unterstützen. Die Windows-Runtime unterstützt keine Emulation von Gleitkomma, um die Ausführung auf Nicht-VFP-Hardware zu ermöglichen.

Die Hardware muss auch Advanced SIMD Extensions (NEON)-Unterstützung für Ganzzahl- und Gleitkommaoperationen bieten. Es wird keine Laufzeitunterstützung für die Emulation geboten.

Die Unterstützung für die Division von ganzen Zahlen (UDIV/SDIV) wird empfohlen, ist jedoch nicht erforderlich. Plattformen, die keine Unterstützung für die Division von ganzen Zahlen bieten, weisen womöglich Leistungseinbußen auf, da diese Operationen aufgefangen und unter Umständen gepatcht werden müssen.

Endianness

Windows on ARM wird im Little-Endian-Modus ausgeführt. Sowohl der MSVC-Compiler als auch die Windows-Runtime erwarten immer Little-Endian-Daten. Die SETEND-Anweisung in der ARM-Anweisungsarchitektur (Instruction Set Architecture, ISA) ermöglicht es sogar dem Benutzermoduscode, die aktuelle Bitreihenfolge zu ändern. Es wird davon aber abgeraten, da es für eine Anwendung gefährlich sein kann. Wenn im Big-Endian-Modus eine Ausnahme generiert wird, ist das Verhalten unvorhersehbar. Dies kann zu einem Anwendungsfehler im Benutzermodus oder zu einer Fehlerprüfung im Kernel-Modus führen.

Ausrichtung

Auch wenn Windows es der ARM-Hardware ermöglicht, falsch ausgerichtete Ganzzahlzugriffe transparent zu verarbeiten, können in einigen Situationen weiterhin Ausrichtungsfehler generiert werden. Befolgen Sie diese Regeln bei der Ausrichtung:

  • Lade- und -Speichervorgänge von Ganzzahlen halber Wortgröße (16-Bit) und ganzer Wortgröße (32-Bit) müssen nicht ausgerichtet werden. Die Hardware verarbeitet sie effizient und transparent.

  • Lade- und Speichervorgänge von Gleitkommazahlen sollten ausgerichtet werden. Der Kernel verarbeitet nicht ausgerichtete Lade- und Speichervorgänge transparent, aber mit einem maßgeblichen Mehraufwand.

  • Doppelte (LDRD/STRD) und mehrfache Lade- und Speichervorgänge (LDM/STM) sollten ausgerichtet werden. Der Kernel verarbeitet die meisten transparent, aber mit einem maßgeblichen Mehraufwand.

  • Alle nicht zwischengespeicherten Arbeitsspeicherzugriffe müssen ausgerichtet werden, auch für Ganzzahlzugriffe. Nicht ausgerichtete Zugriffe verursachen einen Ausrichtungsfehler.

Instruktionssatz

Der Instruktionssatz für Windows on ARM ist streng auf Thumb-2 beschränkt. Der gesamte Code, der auf dieser Plattform ausgeführt wird, soll erwartungsgemäß im Thumb-Modus starten und dauerhaft dort verweilen. Ein Versuch, zu den älteren ARM-Anweisungen zu wechseln, kann erfolgreich sein. Wenn dies jedoch der Fall ist, können auftretende Ausnahmen oder Unterbrechungen einen Anwendungsfehler im Benutzermodus oder zu eine Fehlerprüfung im Kernelmodus verursachen.

Eine Nebenwirkung dieser Anforderung ist, dass für alle Codezeiger das niedrigwertigste Bit festgelegt sein muss. Wenn sie dann über BLX oder BX geladen und verzweigt werden, verbleibt der Prozessor im Thumb-Modus. Es wird nicht versucht, den Zielcode als 32-Bit-ARM-Anweisungen auszuführen.

SDIV/UDIV-Anweisungen

Die Nutzung von Divisionsinstruktionen für Ganzzahlen SDIV und UDIV wird vollständig unterstützt, sogar auf Plattformen ohne native Hardware zur Verarbeitung. Der Mehraufwand pro SDIV- oder UDIV-Division auf einem Cortex-A9-Prozessor beträgt etwa 80 Zyklen. Dazu wird die Gesamtzeit für die Division von 20-250 Zyklen hinzugerechnet, je nach Eingaben.

Ganzzahlregister

Der ARM-Prozessor unterstützt 16 Ganzzahlregister:

Registrieren Volatil? Role
r0 Flüchtig Parameter, Ergebnis, Scratch-Register 1
r1 Flüchtig Parameter, Ergebnis, Scratch-Register 2
r2 Flüchtig Parameter, Scratch-Register 3
r3 Flüchtig Parameter, Scratch-Register 4
r4 Nicht volatil
r5 Nicht volatil
r6 Nicht volatil
r7 Nicht volatil
r8 Nicht volatil
r9 Nicht volatil
r10 Nicht volatil
r11 Nicht volatil Frame-Pointer
r12 Flüchtig Intra-Procedure-Call / Scratch-Register
r13 (SP) Nicht volatil Stack-Pointer
r14 (LR) Nicht volatil Link-Register
r15 (PC) Nicht volatil Program-Counter

Details für die Nutzung des Parameters und der Rückgabewerteregister finden Sie im Abschnitt "Parameterübergabe" in diesem Artikel.

Windows verwendet r11 für einen schnellen Walk durch den Stack-Frame. Weitere Informationen finden Sie im Abschnitt "Stackwalk". Aufgrund dieser Anforderung muss r11 jederzeit auf den obersten Link in der Kette verweisen. Verwenden Sie r11 nicht zu allgemeinen Zwecken, da Ihr Code keine Stackwalks während der Analyse generiert.

VFP-Register

Windows unterstützt nur ARM-Varianten mit Unterstützung für VFPv3-D32-Coprozessoren. Dies bedeutet, dass Gleitkommaregister immer vorhanden sind und für die Parameterübergabe verwendet werden können. Die vollständigen 32 Register stehen zur Verwendung zur Verfügung. Die VFP-Register und deren Einsatz werden in dieser Tabelle zusammengefasst:

Einfach Doppelt Vierfach Volatil? Role
s0-s3 d0-d1 q0 Flüchtig Parameter, Ergebnis, Scratch-Register
s4-s7 d2-d3 q1 Flüchtig Parameter, Scratch-Register
s8-s11 d4-d5 q2 Flüchtig Parameter, Scratch-Register
s12-s15 d6-d7 q3 Flüchtig Parameter, Scratch-Register
s16-s19 d8-d9 q4 Nicht volatil
s20-s23 d10-d11 q5 Nicht volatil
s24-s27 d12-d13 q6 Nicht volatil
s28-s31 d14-d15 q7 Nicht volatil
d16-d31 q8-q15 Flüchtig

Die nächste Tabelle zeigt die Gleitkommastatus- und Steuerungsregister (FPSCR)-Bitfelder:

Bits Bedeutung Volatil? Role
31-28 NZCV Flüchtig Status-Flags
27 QK Flüchtig Kumulative Sättigung
26 AHP Nicht volatil Alternative-Half-Precision-Steuerung
25 DN Nicht volatil Standardmäßige NaN-Modussteuerung
24 FZ Nicht volatil Flush-to-Zero-Modussteuerung
23-22 RMode Nicht volatil Rounding-Modussteuerung
21-20 Stride Nicht volatil Vektorsprung, muss immer 0 sein
18-16 Len Nicht volatil Vektorlänge, muss immer 0 sein
15, 12-8 IDE, IXE und so weiter Nicht volatil Ausnahme-Trap-Aktivierungsbits, muss immer 0 sein
7, 4-0 IDC, IXC und so weiter Flüchtig Kumulative Ausnahme-Flags

Gleitkommaausnahmen

Die meiste ARM-Hardware unterstützt keine IEEE-Gleitkommaausnahmen. Bei Prozessorvarianten mit Hardware-Gleitkommaausnahmen fängt der Windows-Kernel stumm die Ausnahmen ab und deaktiviert sie implizit im FPSCR-Register. Durch diese Aktion wird ein normalisiertes Verhalten zwischen Prozessorvarianten sichergestellt. Anderenfalls könnte Code, der auf einer Plattform entwickelt wurde, die keine Ausnahmenunterstützung bietet, unerwartete Ausnahmen erhalten, wenn er auf einer Plattform mit Ausnahmenunterstützung ausgeführt wird.

Parameterübergabe

Die Windows on ARM-ABI folgt den ARM-Regeln für die Parameterübergabe für nicht-variadische Funktionen. Die ABI-Regeln enthalten die VFP- und Advanced SIMD-Erweiterungen. Diese Regeln folgen dem Prozeduraufrufstandard für die ARM-Architektur, kombiniert mit den VFP-Erweiterungen. Standardmäßig werden die ersten vier Ganzzahlargumente und bis zu acht Gleitkomma- oder Vektor-Argumente an Register übergeben. Alle weiteren Argumente werden an den Stapel übergeben. Argumente werden Registern oder dem Stack mithilfe der folgenden Prozedur zugewiesen:

Phase A: Initialisierung

Die Initialisierung wird genau einmal durchgeführt, bevor die Argumentsverarbeitung beginnt:

  1. Die Next Core Register Number (NCRN) wird auf r0 gesetzt.

  2. Die VFP-Register werden als nicht zugewiesen markiert.

  3. Die Next Stacked Argument Address (NSAA) wird auf die aktuelle SP gesetzt.

  4. Wenn eine Funktion aufgerufen wird, die ein Ergebnis im Speicher zurückgibt, wird die Adresse für das Ergebnis in r0 platziert und die NCRN wird auf r1 gesetzt.

Phase B: Vorabstand und Erweiterung von Argumenten

Auf jedes Argument in der Liste wird die erste übereinstimmende Regel aus der folgenden Liste angewendet:

  1. Wenn das Argument ein zusammengesetzter Typ ist, dessen Größe nicht statisch vom Aufrufer und dem Aufgerufenen bestimmt werden kann, wird das Argument in den Arbeitsspeicher kopiert und durch einen Verweis auf die Kopie ersetzt.

  2. Wenn das Argument ein Byte oder 16-Bit-Halbwort ist, wird es durch eine Null oder ein Symbol auf ein 32-Bit-Vollwort erweitert und als ein 4-Byte-Argument behandelt.

  3. Wenn das Argument ein zusammengesetzter Typ ist, wird die Größe auf das nächste Vielfache von 4 aufgerundet.

Phase C: Zuweisung von Argumenten zum Registrieren und Stapeln

Auf jedes Argument in der Liste werden die folgenden Regeln abwechselnd angewendet, bis das Argument zugeordnet wurde:

  1. Wenn das Argument ein VFP-Typ ist und es ausreichend aufeinander folgende nicht zugewiesene VFP-Register vom entsprechenden Typ gibt, wird das Argument zur Registerreihe mit der niedrigsten Nummer zugeordnet.

  2. Wenn das Argument ein VFP-Typ ist, werden alle verbleibenden nicht zugewiesenen Register als nicht verfügbar markiert. Die NSAA wird nach oben angepasst, bis sie ordnungsgemäß für den Argumententyp ausgerichtet ist und das Argument in den Stack an der angepassten NSAA kopiert wird. Die NSAA wird dann um die Größe des Arguments inkrementiert.

  3. Wenn das Argument eine 8-Byte-Ausrichtung benötigt, wird die NCRN auf die nächste gerade Registernummer aufgerundet.

  4. Wenn die Größe des Arguments in 32-Bit-Wörtern nicht mehr als r4 minus NCRN ist, wird das Argument in Core-Register kopiert, beginnend bei der NCRN, wobei die am niedrigstwertigen Bits die niedrig nummerierten Register belegen. Die NCRN wird um die Anzahl der verwendeten Register inkrementiert.

  5. Wenn die NCRN kleiner als r4 ist und die NSAA mit der SP übereinstimmt, wird das Argument in Core-Register und den Stack unterteilt. Der erste Teil des Arguments wird in die Core-Register kopiert, beginnend bei der NCRN bis hin zu einschließlich r3. Der Rest des Arguments wird in den Stapel kopiert, beginnend bei der NSAA. Die NCRN wird auf r4 gesetzt, und die NSAA wird um die Größe des Arguments minus der an die Register übergegebene Anzahl inkrementiert.

  6. Wenn das Argument eine 8-Byte-Ausrichtung benötigt, wird die NSAA auf die nächste 8-Byte-ausgerichtete Adresse aufgerundet.

  7. Das Argument wird an der NSAA in den Arbeitsspeicher kopiert. Die NSAA wird um die Größe des Arguments inkrementiert.

Die VFP-Register werden nicht für variadic-Funktionen verwendet, und die Regeln 1 und 2 aus Stufe C werden ignoriert. Das bedeutet, dass eine variadic-Funktion mit einem optionalen Push {r0-r3} beginnen kann, um die Registerargumente vor zusätzliche vom Aufrufer übergegebene Argumente voranzustellen und um dann direkt vom Stack aus auf die gesamte Argumentliste zuzugreifen.

Werte vom Typ Ganzzahl werden in r0 zurückgegeben, was optional auf r1 für 64-Bit-Rückgabewerte erweitert werden kann. Werte vom Typ VFP/NEON-Gleitkomma oder SIMD werden wie vorgegeben an s0, d0 oder q0 zurückgegeben.

Stapel

Der Stack muss auf 4-Byte ausgerichtet bleiben und an jeder Funktionsgrenze auf 8-Byte sein. Es ist erforderlich, die häufige Verwendung von Interlocked-Vorgängen für 64-Bit-Stapelvariablen zu unterstützen. Die ARM EABI besagt, dass der Stack an jeder öffentlichen Schnittstelle 8-Byte-ausgerichtet ist. Aus Konsistenzgründen interpretiert die Windows on ARM ABI jede Funktionsgrenze als öffentliche Schnittstelle.

Funktionen, die einen Frame-Pointer verwenden müssen, z. B. Funktionen, die alloca aufrufen oder die den Stack-Pointer dynamisch ändern, müssen den Frame-Pointer in r11 im Funktionsprolog einrichten und bis zum Epilog unverändert lassen. Funktionen, die keinen Frame-Pointer erfordern, müssen alle Stapelupdates im Prolog durchführen und dann den Stapelzeiger bis zum Epilog unverändert lassen.

Funktionen, die 4 KB oder mehr im Stack zuweisen, müssen sicherstellen, dass jede Seite vor der letzten Seite in der richtigen Reihenfolge verwendet wird. Dadurch wird sichergestellt, dass kein Code die Schutzseiten „überspringt“, die Windows zur Erweiterung des Stapels verwendet. Diese Erweiterung wird in der Regel mithilfe des Hilfsprogramms __chkstk erzielt, dem die Stapelgesamtzuordnung in Bytes geteilt durch 4 an r4 übergeben wird und das die letzte Stackzuordnungsanzahl in Bytes wieder an r4 zurückgibt.

Red Zone

Dieser 8-Byte-Bereich direkt unter dem aktuellen Stack-Pointer ist für die Analyse und das dynamische Patching reserviert. Dadurch kann sorgfältig generierter Code eingefügt werden, in dem 2 Register bei [sp, #-8] gespeichert werden und der sie vorübergehend für beliebige Zwecke verwendet. Der Windows-Kernel stellt sicher, dass diese 8 Bytes nicht überschrieben werden, wenn eine Ausnahme oder ein Interrupt im Benutzermodus und im Kernelmodus auftritt.

Kernelstapel

Der standardmäßige Stack im Kernel-Modus unter Windows besteht aus drei Seiten (12 KB). Achten Sie darauf, keine Funktionen zu erstellen, die über große Stack-Puffer im Kernel-Modus verfügen. Ein Interrupt kann mit sehr kleinem Stack-Spielraum auftreten und eine Panik-Stack-Fehlerprüfung verursachen.

Eigenschaften von C/C++

Enumerationen sind 32-Bit-Ganzzahl-Typen, es sei denn mindestens ein Wert in der Enumeration erfordert einen 64-Bit-Doppelwort-Speicher. In diesem Fall wird die Enumeration auf einen 64-Bit-Ganzzahl-Typ heraufgestuft.

wchar_t wurde als äquivalent zu unsigned short definiert, um die Kompatibilität mit anderen Plattformen zu bewahren.

Stackwalk

Windows-Code wird mit aktivierten Rahmenzeigern kompiliert (/Oy (Frame-Pointer Omission)), um einen schnellen Stackwalk zu ermöglichen. Im Allgemeinen verweist das r11-Register auf den nächsten Link in der Kette, bei dem es sich um ein {r11, lr}-Paar handelt, das den Pointer zum vorherigen Frame im Stack und die Rückgabeadresse spezifiziert. Für eine verbesserte Profilerstellung und Ablaufverfolgung empfehlen wir, dass Ihr Code auch Frame-Pointer aktiviert.

Ausnahmeentladung

Die Stackentladung während der Ausnahmebehandlung wird durch den Einsatz von Entladecodes aktiviert. Bei den Entladecodes handelt es sich um Bytesequenzen, die im Abschnitt .xdata des ausführbaren Bildes gespeichert sind. Sie beschreiben den Vorgang des Prolog- und Epilogcodes der Funktion auf abstrakte Weise, sodass die Effekte eines Funktionsprologs als Vorbereitung für die Entladung im Stapelrahmen des Aufrufers rückgängig gemacht werden.

Die ARM EABI gibt ein Ausnahmeentladungsmodell an, das Entladecodes verwendet. Diese Spezifikation ist jedoch nicht ausreichend zum Entladen unter Windows, wobei Fälle behandelt werden müssen, in denen der Prozessor sich in der Mitte des Prologs oder Epilogs einer Funktion befindet. Weitere Informationen zu Windows on ARM-Ausnahmedaten und zum Entladen finden Sie unter ARM-Ausnahmebehandlung.

Wir empfehlen, dass dynamisch generierter Code mithilfe von dynamischen Funktionstabellen beschrieben wird, die in Aufrufen von RtlAddFunctionTable und zugewiesenen Funktionen angegeben sind, damit der generierte Code sich an der Ausnahmebehandlung beteiligen kann.

Zykluszähler

ARM-Prozessoren mit Windows müssen einen Zyklus-Counter unterstützen. Der direkte Einsatz des Counters kann jedoch Probleme verursachen. Um diese Probleme zu vermeiden, verwendet Windows on ARM einen nicht definierten OpCode, um einen normalisierten 64-Bit-Zyklus-Counter-Wert anzufordern. Verwenden Sie aus C oder C++ den intrinsischen __rdpmccntr64, um den entsprechenden OpCode auszugeben; verwenden Sie aus der Assembly die __rdpmccntr64-Instruktion. Das Lesen des Zyklus-Counters dauert etwa 60 Zyklen auf einem Cortex-A9.

Der Counter ist ein wahrer Zyklus-Counter und keine Uhr, daher variiert die Zählerfrequenz mit der Prozessorfrequenz. Wenn Sie die verstrichene Uhrzeit messen möchten, verwenden Sie QueryPerformanceCounter.

Siehe auch

Häufig auftretende ARM-Migrationsprobleme bei Visual C++
ARM-Ausnahmebehandlung