Teilen über


Übersicht über x64-ABI-Konventionen

In diesem Artikel wird die grundlegende Anwendungsbinärschnittstelle (Application Binary Interface, ABI) für x64, die 64-Bit-Erweiterung für die x86-Architektur, erläutert. Es werden Themen wie die beispielsweise die Aufrufkonvention, das Typlayout sowie die Stapel- und Registernutzung behandelt.

x64-Aufrufkonventionen

Die zwei wichtigen Unterschiede zwischen x86 und x64 sind:

  • 64-Bit-Adressierungsfunktion
  • Sechzehn 64-Bit-Register für die allgemeine Verwendung.

Aufgrund des erweiterten Registersatzes wird bei x64 die __fastcall-Aufrufkonvention und ein RISC-basiertes Ausnahmebehandlungsmodell verwendet.

Die __fastcall-Konvention verwendet Register für die ersten vier Argumente und den Stapelrahmen zur Übergabe weiterer Argumente. Weitere Informationen zur x64-Aufrufkonvention, einschließlich Registerverwendung, Stapelparametern, Rückgabewerten und Stapelentladung, finden Sie unter x64 calling convention (x64-Aufrufkonvention).

Weitere Informationen zur __vectorcall-Aufrufkonvention finden Sie unter __vectorcall.

Aktivieren der x64-Compileroptimierung

Die folgende Compileroption hilft Ihnen, Ihre Anwendung für x64 zu optimieren:

x64-Typ- und Speicherlayout

In diesem Abschnitt wird die Speicherung von Datentypen für die x64-Architektur beschrieben.

Skalare Typen

Obwohl es möglich ist, auf Daten mit beliebiger Ausrichtung zuzugreifen, richten Sie Daten an ihrer natürlichen Grenze oder an einem Vielfachen ihrer natürlichen Grenze davon aus, um Leistungsverluste zu vermeiden. Enumerationen sind konstante Ganzzahlen und werden wie 32-Bit-Ganzzahlen behandelt. Die folgende Tabelle enthält eine Beschreibung der Typdefinition und der empfohlenen Speicherung von Daten hinsichtlich der Ausrichtung mithilfe der folgenden Ausrichtungswerte:

  • Byte: 8 Bit
  • Word (Wort): 16 Bit
  • Doubleword (Doppelwort): 32 Bit
  • Quadword (Quadwort): 64 Bit
  • Octaword (Oktawort): 128 Bit
Skalarer Type C-Datentyp Speichergröße (in Byte) Empfohlene Ausrichtung
INT8 char 1 Byte
UINT8 unsigned char 1 Byte
INT16 short 2 Word
UINT16 unsigned short 2 Word
INT32 int, long 4 Doubleword
UINT32 unsigned int, unsigned long 4 Doubleword
INT64 __int64 8 Quadword
UINT64 unsigned __int64 8 Quadword
FP32 (einfache Genauigkeit) float 4 Doubleword
FP64 (doppelte Genauigkeit) double 8 Quadword
POINTER * 8 Quadword
__m64 struct __m64 8 Quadword
__m128 struct __m128 16 Octaword

x64-Aggregat und Unionlayout

Andere Typen, wie z. B. Arrays, Strukturen und Unions, haben strengere Ausrichtungsanforderungen, die eine konsistente Speicherung und einen Datenabruf von Aggregaten und Unions gewährleisten. Im Folgenden sind die Definitionen für Array, Struktur und Union aufgeführt:

  • Array

    Enthält eine geordnete Gruppe von angrenzenden Datenobjekten. Jedes Objekt wird als Element bezeichnet. Alle Elemente in einem Array weisen dieselbe Größe und denselben Datentyp auf.

  • Struktur

    Enthält eine geordnete Gruppe von Datenobjekten. Im Gegensatz zu den Elementen eines Arrays können die Member einer Struktur unterschiedliche Datentypen und Größen aufweisen.

  • Union

    Ein Objekt, das eine Gruppe von benannten Membern enthält. Die Member der benannten Gruppe können von einem beliebigen Typ sein. Der für eine Union zugewiesene Speicherplatz entspricht dem Speicherplatz, der für das umfangreichste Member dieser Union erforderlich ist, zuzüglich der für die Ausrichtung erforderlichen Auffüllung.

Die folgende Tabelle zeigt die dringend empfohlene Ausrichtung für die skalaren Member von Unions und Strukturen.

Skalarer Type Datentyp in C# Erforderliche Ausrichtung
INT8 char Byte
UINT8 unsigned char Byte
INT16 short Word
UINT16 unsigned short Word
INT32 int, long Doubleword
UINT32 unsigned int, unsigned long Doubleword
INT64 __int64 Quadword
UINT64 unsigned __int64 Quadword
FP32 (einfache Genauigkeit) float Doubleword
FP64 (doppelte Genauigkeit) double Quadword
POINTER * Quadword
__m64 struct __m64 Quadword
__m128 struct __m128 Octaword

Es gelten die folgenden aggregierten Ausrichtungsregeln:

  • Die Ausrichtung eines Arrays entspricht der Ausrichtung eines der Elemente des Arrays.

  • Die Ausrichtung am Anfang einer Struktur oder einer Union ist die maximale Ausrichtung eines einzelnen Members. Jedes Member innerhalb der Struktur oder Union muss in der richtigen Ausrichtung, wie in der vorherigen Tabelle definiert, platziert werden. Abhängig vom vorherigen Member kann dies eine implizite interne Auffüllung erfordern.

  • Die Größe der Struktur muss ein ganzzahliges Vielfaches ihrer Ausrichtung sein, was eine Auffüllung nach dem letzten Member erforderlich machen kann. Da Strukturen und Unions in Arrays gruppiert werden können, muss jedes Arrayelement einer Struktur oder Union an der zuvor festgelegten richtigen Ausrichtung beginnen und enden.

  • Es ist möglich, Daten so auszurichten, dass sie die Ausrichtungsanforderungen übersteigen, solange die vorherigen Regeln beibehalten werden.

  • Ein einzelner Compiler kann die Zusammenstellung einer Struktur aufgrund der Größe anpassen. Zum Beispiel erlaubt /Zp (Strukturmemberausrichtung), das Packen von Strukturen anzupassen.

Beispiele für die x64-Strukturausrichtung

Die folgenden vier Beispiele deklarieren jeweils eine ausgerichtete Struktur oder Union, und die entsprechenden Abbildungen veranschaulichen das Layout dieser Struktur bzw. Union im Speicher. Jede Spalte in einer Abbildung stellt ein Byte des Speichers dar, und die Zahl in der Spalte zeigt die Verschiebung dieses Byte an. Der Name in der zweiten Zeile jeder Abbildung entspricht dem Namen einer Variablen in der Deklaration. Die schattierten Spalten geben die Auffüllung an, die zum Erreichen der angegebenen Ausrichtung erforderlich ist.

Beispiel 1

// Total size = 2 bytes, alignment = 2 bytes (word).

_declspec(align(2)) struct {
    short a;      // +0; size = 2 bytes
}

Diagramm mit dem Strukturlayout für Beispiel 1.

Beispiel 2

// Total size = 24 bytes, alignment = 8 bytes (quadword).

_declspec(align(8)) struct {
    int a;       // +0; size = 4 bytes
    double b;    // +8; size = 8 bytes
    short c;     // +16; size = 2 bytes
}

Diagramm mit dem Strukturlayout für Beispiel 2.

Beispiel 3

// Total size = 12 bytes, alignment = 4 bytes (doubleword).

_declspec(align(4)) struct {
    char a;       // +0; size = 1 byte
    short b;      // +2; size = 2 bytes
    char c;       // +4; size = 1 byte
    int d;        // +8; size = 4 bytes
}

Diagramm mit dem Strukturlayout für Beispiel 3.

Beispiel 4

// Total size = 8 bytes, alignment = 8 bytes (quadword).

_declspec(align(8)) union {
    char *p;      // +0; size = 8 bytes
    short s;      // +0; size = 2 bytes
    long l;       // +0; size = 4 bytes
}

Diagramm mit dem Vereinigungslayout für Beispiel 4.

Bitfelder

Strukturbitfelder sind auf 64 Bit begrenzt und können vom Typ „signed int“, „unsigned int“, „int64“ oder „unsigned int64“ sein. Bei Bitfeldern, die die Typgrenze überschreiten, werden Bits übersprungen, um das Bitfeld an der nächsten Typausrichtung auszurichten. Ganzzahlige Bitfelder können z. B. keinen 32-Bit-Grenzwert überschreiten.

Konflikt mit dem x86-Compiler

Datentypen mit mehr als 4 Bytes werden nicht automatisch auf dem Stapel ausgerichtet, wenn Sie den x86-Compiler zum Kompilieren einer Anwendung verwenden. Da die Architektur für den x86-Compiler ein an 4 Bytes ausgerichteter Stapel ist, wird nicht alles, was größer als 4 Bytes ist, z. B. eine 64-Bit-Ganzzahl, automatisch an einer 8-Byte-Adresse ausgerichtet.

Das Arbeiten mit nicht ausgerichteten Daten hat zwei Auswirkungen.

  • Der Zugriff auf nicht ausgerichtete Positionen kann länger dauern als der Zugriff auf ausgerichtete Positionen.

  • Nicht ausgerichtete Positionen können nicht in Interlocked-Vorgängen verwendet werden.

Wenn Sie eine strengere Ausrichtung benötigen, verwenden Sie __declspec(align(N)) für die variablen Deklarationen. Dies bewirkt, dass der Compiler den Stapel dynamisch entsprechend Ihren Spezifikationen ausrichtet. Das dynamische Ausrichten des Stapels zur Laufzeit kann jedoch zu einer langsameren Ausführung der Anwendung führen.

x64-Registerverwendung

Die x64-Architektur ermöglicht den Einsatz von 16 allgemeinen Registern (im Folgenden als Ganzzahlregister bezeichnet) sowie von 16 XMM/YMM-Registern für die Gleitkommanutzung. Volatile Register sind Scratch-Register, von denen der Aufrufer voraussetzt, dass sie während eines Aufrufs zerstört werden. Nicht volatile Register müssen ihre Werte über einen Funktionsaufruf hinweg bewahren und, sofern sie verwendet werden, vom Aufgerufenen gespeichert werden.

Registervolatilität und -beibehaltung

Die folgende Tabelle beschreibt, wie jedes Register bei Funktionsaufrufen verwendet wird:

Registrieren Status Zweck
RAX Flüchtig Rückgabewert-Register
RCX Flüchtig Erstes Ganzzahl-Argument
RDX Flüchtig Zweites Ganzzahl-Argument
R8 Flüchtig Drittes Ganzzahl-Argument
R9 Flüchtig Viertes Ganzzahl-Argument
R10:R11 Flüchtig Muss je nach Anforderung des Aufrufers bewahrt werden. Wird in syscall-/sysret-Instruktionen verwendet.
R12:R15 Nicht volatil Muss vom Aufgerufenen bewahrt werden
RDI Nicht volatil Muss vom Aufgerufenen bewahrt werden
RSI Nicht volatil Muss vom Aufgerufenen bewahrt werden
RBX Nicht volatil Muss vom Aufgerufenen bewahrt werden
RBP Nicht volatil Kann als Frame-Pointer verwendet werden; muss vom Aufgerufenen bewahrt werden
RSP Nicht volatil Stack-Pointer
XMM0, YMM0 Flüchtig Erstes FP-Argument; erstes Argument vom Typ Vektor, wenn __vectorcall verwendet wird
XMM1, YMM1 Flüchtig Zweites FP-Argument; zweites Argument vom Typ Vektor, wenn __vectorcall verwendet wird
XMM2, YMM2 Flüchtig Drittes FP-Argument; drittes Argument vom Typ Vektor, wenn __vectorcall verwendet wird
XMM3, YMM3 Flüchtig Viertes FP-Argument; viertes Argument vom Typ Vektor, wenn __vectorcall verwendet wird
XMM4, YMM4 Flüchtig Muss je nach Bedarf vom Aufrufer beibehalten werden; fünftes Argument vom Typ Vektor, wenn __vectorcall verwendet wird
XMM5, YMM5 Flüchtig Muss je nach Bedarf vom Aufrufer beibehalten werden; sechstes Argument vom Typ Vektor, wenn __vectorcall verwendet wird
XMM6:XMM15, YMM6:YMM15 Nicht volatil (XMM), Volatil (obere Hälfte von YMM) Muss vom Aufgerufenen bewahrt werden. YMM-Register müssen je nach Bedarf vom Aufrufer bewahrt werden.

Bei Funktionsende und beim Funktionseinstieg in C-Laufzeitbibliotheksaufrufen und Windows-Systemaufrufen wird erwartet, dass das Richtungsflag im CPU-Flagsregister gelöscht wird.

Stapelverwendung

Ausführliche Informationen zu Stapelzuordnung, Ausrichtung, Funktionstypen und Stapelrahmen für x64 finden Sie unter x64 Stack Usage (Verwendung von Stapeln bei x64-Systemen).

Prolog und Epilog

Jede Funktion, die Stapelspeicherplatz zuweist, andere Funktionen aufruft, nichtflüchtige Register speichert oder die Ausnahmebehandlung verwendet, muss über einen Prolog verfügen, dessen Adressgrenzen in den Entladedaten beschrieben sind, die mit dem jeweiligen Funktionstabelleneintrag verbunden sind, sowie über Epiloge bei jedem Beenden einer Funktion. Ausführliche Informationen zum erforderlichen Prolog- und Epilogcode für x64 finden Sie unter x64 prolog and epilog (Prolog- und Epilogcode bei x64-Systemen).

Ausnahmebehandlung bei x64-Systemen

Informationen zu den Konventionen und Datenstrukturen, die verwendet werden, um die strukturierte C++-Ausnahmebehandlung und das Verhalten der Ausnahmebehandlung bei x64-Systemen zu implementieren, finden Sie unter x64 exception handling (Ausnahmebehandlung bei x64-Systemen).

Intrinsische Funktionen und Inlineassemblys

Eine der Einschränkungen für den x64-Compiler besteht darin, dass keine Unterstützung für den Inlineassembler vorhanden ist. Dies bedeutet, dass Funktionen, die nicht in C oder C++ geschrieben werden können, entweder als Unterroutinen oder als intrinsische Funktionen geschrieben werden müssen, die vom Compiler unterstützt werden. Bestimmte Funktionen sind leistungsabhängig, andere hingegen nicht. Leistungsabhängige Funktionen sollten als intrinsische Funktionen implementiert werden.

Die intrinsischen Funktionen, die vom Compiler unterstützt werden, werden in intrinsischen Compilerfunktionen beschrieben.

x64-Bildformat

Das ausführbare x64-Imageformat ist das Format PE32+. Ausführbare Images (sowohl DLLs als auch EXE-Dateien) sind auf eine maximale Größe von 2 Gigabyte beschränkt, sodass eine relative Adressierung mit einer 32-Bit-Verschiebung verwendet werden kann, um statische Imagedaten zu adressieren. Diese Daten umfassen die Importadresstabelle, Zeichenfolgenkonstanten, statische globale Daten usw.

Weitere Informationen

Aufrufkonventionen