Omówienie konwencji X64 ABI
W tym temacie opisano podstawowy interfejs binarny aplikacji (ABI) dla x64, rozszerzenie 64-bitowe do architektury x86. Obejmuje on tematy, takie jak konwencja wywoływania, układ typu, stos i rejestrowanie użycia itd.
Konwencje wywoływania x64
Istnieją dwie ważne różnice między x86 i x64:
- Możliwość adresowania 64-bitowego
- Szesnaście rejestrów 64-bitowych do użytku ogólnego.
Biorąc pod uwagę rozszerzony zestaw rejestrów, x64 używa __fastcall konwencji wywoływania i modelu obsługi wyjątków opartego na protokole RISC.
Konwencja __fastcall
używa rejestrów dla pierwszych czterech argumentów, a ramka stosu przekazuje więcej argumentów. Aby uzyskać szczegółowe informacje na temat konwencji wywoływania x64, w tym rejestrowania użycia, parametrów stosu, wartości zwracanych i odwijania stosu, zobacz konwencję wywoływania x64.
Aby uzyskać więcej informacji na __vectorcall
temat konwencji wywoływania, zobacz __vectorcall
.
Włączanie optymalizacji kompilatora x64
Poniższa opcja kompilatora pomaga zoptymalizować aplikację pod kątem architektury x64:
Typ x64 i układ magazynu
W tej sekcji opisano przechowywanie typów danych dla architektury x64.
Typy skalarne
Chociaż istnieje możliwość uzyskania dostępu do danych przy użyciu dowolnego wyrównania, wyrównania danych na granicy naturalnej lub wielu jej granic naturalnych, aby uniknąć utraty wydajności. Wyliczenia są stałymi liczbami całkowitymi i są traktowane jako 32-bitowe liczby całkowite. W poniższej tabeli opisano definicję typu i zalecany magazyn danych w odniesieniu do wyrównania przy użyciu następujących wartości wyrównania:
- Bajt — 8 bitów
- Word — 16 bitów
- Doubleword — 32 bity
- Quadword — 64 bity
- Octaword — 128 bitów
Typ skalarny | Typ danych języka C | Rozmiar magazynu (w bajtach) | Zalecane wyrównanie |
---|---|---|---|
INT8 |
char |
1 | Byte |
UINT8 |
unsigned char |
1 | Byte |
INT16 |
short |
2 | Word |
UINT16 |
unsigned short |
2 | Word |
INT32 |
int , long |
100 | Podwójna kolejność |
UINT32 |
unsigned int , unsigned long |
100 | Podwójna kolejność |
INT64 |
__int64 |
8 | Czworokąt |
UINT64 |
unsigned __int64 |
8 | Czworokąt |
FP32 (pojedyncza precyzja) |
float |
100 | Podwójna kolejność |
FP64 (podwójna precyzja) |
double |
8 | Czworokąt |
POINTER |
* | 8 | Czworokąt |
__m64 |
struct __m64 |
8 | Czworokąt |
__m128 |
struct __m128 |
16 | Octaword |
Układ agregacji i unii x64
Inne typy, takie jak tablice, struktury i związki, mają bardziej rygorystyczne wymagania dotyczące wyrównania, które zapewniają spójne agregowanie i przechowywanie w unii oraz pobieranie danych. Poniżej przedstawiono definicje tablicy, struktury i unii:
Tablica
Zawiera uporządkowaną grupę sąsiednich obiektów danych. Każdy obiekt jest nazywany elementem. Wszystkie elementy w tablicy mają ten sam rozmiar i typ danych.
Struktura
Zawiera uporządkowaną grupę obiektów danych. W przeciwieństwie do elementów tablicy elementy członkowskie struktury mogą mieć różne typy danych i rozmiary.
Unia
Obiekt, który zawiera dowolny zestaw nazwanych elementów członkowskich. Składowe nazwanego zestawu mogą być dowolnego typu. Magazyn przydzielony dla unii jest równy magazynowi wymaganemu dla największego członka tej unii, a także wszelkie wypełnienie wymagane do wyrównania.
W poniższej tabeli przedstawiono zdecydowanie zalecane wyrównanie składowych skalarnych związków i struktur.
Typ skalarny | Typ danych języka C | Wymagane wyrównanie |
---|---|---|
INT8 |
char |
Byte |
UINT8 |
unsigned char |
Byte |
INT16 |
short |
Word |
UINT16 |
unsigned short |
Word |
INT32 |
int , long |
Podwójna kolejność |
UINT32 |
unsigned int , unsigned long |
Podwójna kolejność |
INT64 |
__int64 |
Czworokąt |
UINT64 |
unsigned __int64 |
Czworokąt |
FP32 (pojedyncza precyzja) |
float |
Podwójna kolejność |
FP64 (podwójna precyzja) |
double |
Czworokąt |
POINTER |
* | Czworokąt |
__m64 |
struct __m64 |
Czworokąt |
__m128 |
struct __m128 |
Octaword |
Obowiązują następujące reguły wyrównania agregacji:
Wyrównanie tablicy jest takie samo jak wyrównanie jednego z elementów tablicy.
Wyrównanie początku struktury lub unii jest maksymalnym dopasowaniem każdego elementu członkowskiego. Każdy element członkowski w strukturze lub unii musi być umieszczony w odpowiednim wyrównaniu zgodnie z definicją w poprzedniej tabeli, co może wymagać niejawnego wypełnienia wewnętrznego, w zależności od poprzedniego elementu członkowskiego.
Rozmiar struktury musi być integralną wielokrotną jego wyrównaniem, co może wymagać wypełnienia po ostatnim elemencie. Ponieważ struktury i związki mogą być zgrupowane w tablicach, każdy element tablicy struktury lub unii musi zaczynać się i kończyć na odpowiednim wyrównaniu wcześniej określonym.
Istnieje możliwość dostosowania danych w taki sposób, aby były większe niż wymagania dotyczące wyrównania, o ile poprzednie reguły są utrzymywane.
Pojedynczy kompilator może dostosować pakowanie struktury ze względów rozmiaru. Na przykład /Zp (Wyrównanie składowe struktury) umożliwia dostosowanie pakowania struktur.
Przykłady wyrównania struktury x64
Poniższe cztery przykłady deklarują wyrównaną strukturę lub unię, a odpowiadające im liczby ilustrują układ tej struktury lub unii w pamięci. Każda kolumna na rysunku reprezentuje bajt pamięci, a liczba w kolumnie wskazuje przemieszczenie tego bajtu. Nazwa w drugim wierszu każdego rysunku odpowiada nazwie zmiennej w deklaracji . Zacienione kolumny wskazują wypełnienie wymagane do osiągnięcia określonego wyrównania.
Przykład 1
// Total size = 2 bytes, alignment = 2 bytes (word).
_declspec(align(2)) struct {
short a; // +0; size = 2 bytes
}
Przykład 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
}
Przykład 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
}
Przykład 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
}
Pola bitów
Pola bitów struktury są ograniczone do 64 bitów i mogą być typu podpisane int, niepodpisane int, int64 lub niepodpisane int64. Pola bitowe, które przekraczają granicę typu, pominie bity w celu wyrównania pola bitowego do następnego wyrównania typu. Na przykład pola bitowe liczb całkowitych mogą nie przekraczać granicy 32-bitowej.
Konflikty z kompilatorem x86
Typy danych, które są większe niż 4 bajty, nie są automatycznie wyrównane do stosu podczas kompilowania aplikacji przy użyciu kompilatora x86. Ponieważ architektura kompilatora x86 jest stosem wyrównanym do 4 bajtów, cokolwiek większego niż 4 bajty, na przykład 64-bitowej liczby całkowitej, nie można automatycznie wyrównać do adresu 8-bajtowego.
Praca z danymi nieprzyznanymi ma dwa implikacje.
Uzyskanie dostępu do nieprzygotowanych lokalizacji może potrwać dłużej niż uzyskanie dostępu do wyrównanych lokalizacji.
Nie można używać nieprzystawionych lokalizacji w operacjach połączonych.
Jeśli potrzebujesz bardziej ścisłego wyrównania, użyj __declspec(align(N))
deklaracji zmiennych. Dzięki temu kompilator dynamicznie wyrównuje stos zgodnie ze specyfikacjami. Jednak dynamiczne dostosowywanie stosu w czasie wykonywania może spowodować wolniejsze wykonywanie aplikacji.
Rejestrowanie użycia x64
Architektura x64 zapewnia 16 rejestrów ogólnego przeznaczenia (określanych tutaj jako rejestry całkowite), a także 16 rejestrów XMM/YMM dostępnych do użytku zmiennoprzecinkowego. Rejestry lotne to rejestry rysowane zakładane przez obiekt wywołujący, które mają zostać zniszczone przez wywołanie. Rejestry niewolne są wymagane do zachowania ich wartości w wywołaniu funkcji i muszą zostać zapisane przez wywoływanie, jeśli jest używane.
Rejestrowanie zmienności i zachowania
W poniższej tabeli opisano sposób użycia każdego rejestru między wywołaniami funkcji:
Zarejestruj | Stan | Używanie |
---|---|---|
RAX | Lotny | Rejestr wartości zwracanych |
RCX | Lotny | Pierwszy argument liczby całkowitej |
RDX | Lotny | Drugi argument liczby całkowitej |
R8 | Lotny | Trzeci argument liczby całkowitej |
R9 | Lotny | Czwarty argument liczby całkowitej |
R10:R11 | Lotny | Należy zachować zgodnie z potrzebami przez obiekt wywołujący; używane w instrukcjach syscall/sysret |
R12:R15 | Nieulotna | Należy zachować przez wywoływanie |
RDI | Nieulotna | Należy zachować przez wywoływanie |
RSI | Nieulotna | Należy zachować przez wywoływanie |
RBX | Nieulotna | Należy zachować przez wywoływanie |
RBP | Nieulotna | Może być używany jako wskaźnik ramki; muszą być zachowywane przez obiekt wywoływany |
RSP | Nieulotna | Wskaźnik stosu |
XMM0, YMM0 | Lotny | Pierwszy argument FP; pierwszy argument typu wektorowego, gdy __vectorcall jest używany |
XMM1, YMM1 | Lotny | Drugi argument FP; drugi argument typu wektora, gdy __vectorcall jest używany |
XMM2, YMM2 | Lotny | Trzeci argument FP; trzeci argument typu wektorowego, gdy __vectorcall jest używany |
XMM3, YMM3 | Lotny | Czwarty argument FP; czwarty argument typu wektora, gdy __vectorcall jest używany |
XMM4, YMM4 | Lotny | Należy zachować zgodnie z potrzebami przez obiekt wywołujący; piąty argument typu wektora, gdy __vectorcall jest używany |
XMM5, YMM5 | Lotny | Należy zachować zgodnie z potrzebami przez obiekt wywołujący; szósty argument typu wektora, gdy __vectorcall jest używany |
XMM6:XMM15, YMM6:YMM15 | Nonvolatile (XMM), Volatile (górna połowa YMM) | Należy zachować przez wywoływanie. Rejestry YMM muszą być zachowywane zgodnie z potrzebami przez obiekt wywołujący. |
Po zakończeniu działania funkcji i wpisie funkcji do wywołań biblioteki środowiska uruchomieniowego języka C i wywołaniach systemu Windows należy wyczyścić flagę kierunku w rejestrze flag procesora CPU.
Użycie stosu
Aby uzyskać szczegółowe informacje na temat alokacji stosu, wyrównania, typów funkcji i ramek stosu w architekturze x64, zobacz użycie stosu x64.
Prolog i epilog
Każda funkcja, która przydziela przestrzeń stosu, wywołuje inne funkcje, zapisuje rejestry niezawolone lub używa obsługi wyjątków, musi mieć prolog, którego limity adresów są opisane w danych odwijanych skojarzonych z odpowiednim wpisem tabeli funkcji i epilogami w każdym wyjściu do funkcji. Aby uzyskać szczegółowe informacje na temat wymaganego kodu prologu i epilogu na x64, zobacz x64 prolog i epilog.
Obsługa wyjątku x64
Aby uzyskać informacje na temat konwencji i struktur danych używanych do implementowania obsługi wyjątków strukturalnych i obsługi wyjątków języka C++ w architekturze x64, zobacz obsługa wyjątków x64.
Funkcje wewnętrzne i wbudowany zestaw
Jednym z ograniczeń kompilatora x64 jest brak wbudowanej obsługi asemblera. Oznacza to, że funkcje, których nie można zapisać w języku C lub C++, muszą być zapisywane jako podroutines lub jako funkcje wewnętrzne obsługiwane przez kompilator. Niektóre funkcje są wrażliwe na wydajność, a inne nie. Funkcje wrażliwe na wydajność powinny być implementowane jako funkcje wewnętrzne.
Funkcje wewnętrzne obsługiwane przez kompilator są opisane w temacie Funkcje wewnętrzne kompilatora.
Format obrazu x64
Format obrazu wykonywalnego x64 to PE32+. Obrazy wykonywalne (zarówno biblioteki DLL, jak i EXEs) są ograniczone do maksymalnego rozmiaru 2 gigabajtów, więc względne adresowanie z przemieszczeniem 32-bitowym może służyć do adresowania danych obrazów statycznych. Te dane obejmują tabelę adresów importu, stałe ciągów, statyczne dane globalne itd.