Konwencja wywoływania x64

W tym artykule opisano standardowe procesy i konwencje używane przez jedną funkcję (obiekt wywołujący) do tworzenia wywołań do innej funkcji (wywoływanej) w kodzie x64.

Aby uzyskać więcej informacji na temat __vectorcall konwencji wywoływania, zobacz __vectorcall.
Aby uzyskać więcej informacji na temat __preserve_none konwencji wywoływania, zobacz __preserve_none.

Domyślne konwencje wywoływania

Interfejs binarny aplikacji x64 (ABI) domyślnie używa konwencji wywoływania z czterema rejestrami i szybką obsługą. Miejsce jest przydzielane na stosie wywołań jako magazyn cieniowy dla funkcji wywoływanych, aby zapisać te rejestry.

Istnieje ścisła korespondencja jeden do jednego między argumentami wywołania funkcji a rejestrami używanymi dla tych argumentów. Każdy argument, który nie mieści się w 8 bajtach lub nie jest 1, 2, 4 lub 8 bajtów, musi zostać przekazany przez referencję. Jeden argument nigdy nie jest rozłożony na wiele rejestrów.

Stos rejestru x87 jest nieużywany. Może być używany przez obiekt wywoływany, ale należy wziąć pod uwagę, że nietrwały między wywołaniami funkcji. Wszystkie operacje zmiennoprzecinkowe są wykonywane przy użyciu 16 rejestrów XMM.

Argumenty liczb całkowitych są przekazywane w rejestrach RCX, RDX, R8i R9. Argumenty zmiennoprzecinkowe są przekazywane w XMM0L, XMM1L, XMM2L i XMM3L. Argumenty 16-bajtowe są przekazywane przez referencję. Przekazywanie parametrów zostało szczegółowo opisane w artykule Przekazywanie parametrów. Te rejestry oraz RAX, R10, R11, XMM4 i XMM5 są uznawane za ulotne, czyli takie, które mogą zostać zmienione przez funkcję wywoływaną po jej zakończeniu. Rejestrowanie użycia jest szczegółowo udokumentowane w zastosowaniu rejestrów x64 i rejestrach zachowywanych przez wywołującego/wywoływanego.

W przypadku funkcji prototypowych wszystkie argumenty są konwertowane na oczekiwane typy wywoływane przed przekazaniem. Obiekt wywołujący jest odpowiedzialny za przydzielanie miejsca dla parametrów obiektu wywoływanego. Wywołujący musi zawsze przydzielić wystarczającą ilość pamięci do przechowywania czterech parametrów rejestru, nawet jeśli wywoływany nie wymaga tylu parametrów. Ta konwencja upraszcza obsługę nietypowych funkcji języka C i funkcji vararg C/C++. W przypadku funkcji vararg lub nietypowych wszystkie wartości zmiennoprzecinkowe muszą być zduplikowane w odpowiednim rejestrze ogólnego przeznaczenia. Wszystkie parametry wykraczające poza pierwsze cztery muszą być przechowywane na stosie po pamięci cienia przed wywołaniem. Szczegóły funkcji Vararg można znaleźć w temacie Varargs. Nietypowe informacje o funkcji są szczegółowo opisane w temacie Funkcje nietypowe.

Wyrównanie

Większość struktur jest ustawiona zgodnie z ich naturalnym wyrównaniem. Główne wyjątki to wskaźnik stosu i pamięć malloc lub alloca, które są wyrównane do 16 bajtów, aby wspomóc wydajność. Wyrównanie powyżej 16 bajtów należy wykonać ręcznie. Ponieważ 16 bajtów jest typowym rozmiarem wyrównania dla operacji XMM, ta wartość powinna działać dla większości kodu. Aby uzyskać więcej informacji na temat układu struktury i wyrównania, zobacz Typ x64 i układ pamięci masowej. Aby uzyskać informacje o układzie stosu, zobacz użycie stosu x64.

Możliwość rozwijania

Funkcje liścia to funkcje, które nie zmieniają żadnych rejestrów niewolnych. Funkcja nieliściasta może na przykład zmienić wartość nieulotną RSP, wywołując funkcję. Może też zmienić wartość RSP, przydzielając więcej miejsca na stosie dla zmiennych lokalnych. Aby odzyskać rejestry nieulotne w przypadku obsługi wyjątku, funkcje nieliściaste są oznaczone danymi statycznymi. W danych opisano sposób prawidłowego odwijania funkcji przy dowolnej instrukcji. Te dane są przechowywane jako dane pdata lub dane procedury, które z kolei odnoszą się do xdata, danych obsługi wyjątków. Dane xdata zawierają informacje o odwijeniu i mogą wskazywać dodatkowe dane pdata lub funkcję obsługi wyjątków.

Prologi i epilogi są wysoce ograniczone, aby można je było właściwie opisać w xdata. Wskaźnik stosu musi pozostać wyrównany na 16 bajtów w dowolnym regionie kodu, który nie jest częścią epilogu ani prologu, z wyjątkiem funkcji liściowych. Funkcje liścia mogą być rozwiane po prostu przez symulowanie powrotu, więc pdata i xdata nie są wymagane. Aby uzyskać szczegółowe informacje na temat właściwej struktury prologów funkcji i epilogów, zobacz x64 prolog i epilog. Aby uzyskać więcej informacji na temat obsługi wyjątków oraz odwijania i interpretacji danych pdata i xdata, zobacz obsługę wyjątków x64.

Przekazywanie parametrów

Domyślnie konwencja wywoływania x64 przekazuje pierwsze cztery argumenty do funkcji w rejestrach. Rejestry używane dla tych argumentów zależą od pozycji i typu argumentu. Pozostałe argumenty są umieszczane na stosie w kolejności od prawej do lewej. Obiekt wywołujący rezerwuje wymaganą przestrzeń stosu i zapisuje te argumenty w pamięci stosu przy użyciu instrukcji zapisu lub przemieszczania, zachowując 8-bajtowe wyrównanie dla każdego argumentu.

Argumenty liczb całkowitych w najbardziej lewej czwórce pozycji są przekazywane w kolejności od lewej do prawej odpowiednio w RCX, , RDXR8i R9. Piąte i wyższe argumenty są przekazywane na stosie zgodnie z wcześniejszym opisem. Wszystkie argumenty całkowite w rejestrach są uzasadnione prawem, więc obiekt wywoływany może zignorować górne bity rejestru i uzyskać dostęp tylko do części rejestru niezbędnej.

Wszelkie argumenty zmiennoprzecinkowe i argumenty podwójnej precyzji znajdujące się wśród pierwszych czterech parametrów są przekazywane w XMM0 - XMM3, w zależności od ich pozycji. Wartości zmiennoprzecinkowe są umieszczane tylko w rejestrach liczb całkowitych RCX, RDX, R8i R9 , gdy istnieją argumenty varargs. Aby uzyskać szczegółowe informacje, zobacz Varargs. Podobnie rejestry XMM0 - XMM3 są ignorowane, gdy odpowiedni argument jest liczbą całkowitą lub typem wskaźnikowym.

__m128 typy, tablice i ciągi nigdy nie są przekazywane przez wartość bezpośrednią. Zamiast tego wskaźnik jest przekazywany do pamięci przydzielonej przez obiekt wywołujący. Struktury i związki o rozmiarze 8, 16, 32 lub 64 bitach i __m64 typach są przekazywane tak, jakby były liczbą całkowitą o tym samym rozmiarze. Struktury lub związki innych rozmiarów są przekazywane jako wskaźnik do pamięci przydzielonej przez obiekt wywołujący. W przypadku tych typów agregatowych przekazywanych jako wskaźnik, w tym __m128, pamięć tymczasowa przydzielona przez wywołującego musi być wyrównana do 16 bajtów.

Funkcje wewnętrzne, które nie przydzielają miejsca na stosie i nie wywołują innych funkcji, czasami używają innych rejestrów tymczasowych do przekazywania dodatkowych argumentów rejestrów. Ta optymalizacja jest możliwa przez ścisłe powiązanie między kompilatorem a implementacją funkcji wewnętrznej.

Obiekt wywoływany jest odpowiedzialny za zrzucenie parametrów rejestru do obszaru cienia w razie potrzeby.

Poniższa tabela zawiera podsumowanie sposobu przekazywania parametrów według typu i pozycji po lewej stronie:

Typ parametru piąty lub wyższy czwarty trzeci drugi Po lewej stronie
zmiennoprzecinkowa stos XMM3 XMM2 XMM1 XMM0
liczba całkowita stos R9 R8 RDX RCX
Agregaty (8, 16, 32 lub 64 bity) i __m64 stos R9 R8 RDX RCX
Inne agregacje jako wskaźniki stos R9 R8 RDX RCX
__m128, jako wskaźnik stos R9 R8 RDX RCX

Przykład przekazywania argumentu 1 — wszystkie liczby całkowite

func1(int a, int b, int c, int d, int e, int f);
// a in RCX, b in RDX, c in R8, d in R9, f then e passed on stack

Przykład przekazywania argumentów 2 — wszystkie liczby zmiennoprzecinkowe

func2(float a, double b, float c, double d, float e, float f);
// a in XMM0, b in XMM1, c in XMM2, d in XMM3, f then e passed on stack

Przykład przekazywania argumentów 3 — zmieszane liczby całkowite i zmiennoprzecinkowe

func3(int a, double b, int c, float d, int e, float f);
// a in RCX, b in XMM1, c in R8, d in XMM3, f then e passed on stack

Przykład przekazywania argumentów 4 — __m64, __m128 i agregacji

func4(__m64 a, __m128 b, struct c, float d, __m128 e, __m128 f);
// a in RCX, ptr to b in RDX, ptr to c in R8, d in XMM3,
// ptr to f passed on stack, then ptr to e passed on stack

Varargs

Jeśli parametry są przekazywane przez varargs (na przykład argumenty wielokropka), ma zastosowanie normalna konwencja przekazywania parametrów rejestru. Konwencja ta obejmuje umieszczenie piątego i kolejnych argumentów na stosie. To odpowiedzialność wywoływanego za zrzucanie argumentów, które mają pobrany adres. Tylko w przypadku wartości zmiennoprzecinkowych zarówno rejestry liczb całkowitych, jak i rejestry zmiennoprzecinkowe muszą zawierać daną wartość, jeśli obiekt wywoływany oczekuje jej w rejestrach liczb całkowitych.

Funkcje nietypowe

W przypadku funkcji, które nie są w pełni prototypowane, obiekt wywołujący przekazuje wartości całkowite jako liczby całkowite i wartości zmiennoprzecinkowe jako podwójną precyzję. Tylko w przypadku wartości zmiennoprzecinkowych zarówno rejestr liczb całkowitych, jak i rejestr zmiennoprzecinkowy zawierają wartość zmiennoprzecinkową, jeśli obiekt wywoływany oczekuje wartości w rejestrach liczb całkowitych.

func1();
func2() {   // RCX = 2, RDX = XMM1 = 1.0, and R8 = 7
   func1(2, 1.0, 7);
}

Wartości zwracane

Skalarna wartość zwracana, która mieści się w 64 bitach, w tym typ __m64, jest zwracana przez RAX. Typy nieskalarne, w tym liczby zmiennoprzecinkowe, liczby podwójnej precyzji i typy wektorowe, takie jak __m128, __m128i, __m128d, są zwracane w XMM0. Stan nieużywanych bitów w wartości zwracanej przez RAX lub XMM0 jest niezdefiniowany.

Typy zdefiniowane przez użytkownika mogą być zwracane przez wartość z funkcji globalnych i statycznych funkcji składowych. Aby zwrócić typ zdefiniowany przez użytkownika według wartości w RAX, musi mieć długość 1, 2, 4, 8, 16, 32 lub 64 bitów. Nie musi również mieć zdefiniowanego przez użytkownika konstruktora, destruktora ani operatora przypisania kopiowania. Nie może mieć prywatnych ani chronionych niestatycznych składowych danych, ani niestatycznych składowych danych typu referencyjnego. Nie może mieć klas podstawowych ani funkcji wirtualnych. Ponadto może mieć tylko pola danych, które spełniają te wymagania. Ta definicja jest zasadniczo taka sama jak typ POD w C++03. Ponieważ definicja zmieniła się w standardzie C++11, nie zalecamy używania std::is_pod tego testu. W przeciwnym razie wywołujący musi przydzielić pamięć dla wartości zwracanej i przekazać wskaźnik do niej jako pierwszy argument. Pozostałe argumenty są następnie przesuwane o jeden argument w prawo. Ten sam wskaźnik musi być zwracany przez obiekt wywoływany w pliku RAX.

W tych przykładach pokazano, jak parametry i zwracane wartości są przekazywane dla funkcji z określonymi deklaracjami:

Przykład zwracanej wartości 1 — wynik 64-bitowy

__int64 func1(int a, float b, int c, int d, int e);
// Caller passes a in RCX, b in XMM1, c in R8, d in R9, e passed on stack,
// callee returns __int64 result in RAX.

Przykład wartości zwracanej 2 — wynik 128-bitowy

__m128 func2(float a, double b, int c, __m64 d);
// Caller passes a in XMM0, b in XMM1, c in R8, d in R9,
// callee returns __m128 result in XMM0.

Przykład wartości zwracanej 3 — wynik typu użytkownika według wskaźnika

struct Struct1 {
   int j, k, l;    // Struct1 exceeds 64 bits.
};
Struct1 func3(int a, double b, int c, float d);
// Caller allocates memory for Struct1 returned and passes pointer in RCX,
// a in RDX, b in XMM2, c in R9, d passed on the stack;
// callee returns pointer to Struct1 result in RAX.

Przykład wartości zwracanej 4 — wynik typu użytkownika według wartości

struct Struct2 {
   int j, k;    // Struct2 fits in 64 bits, and meets requirements for return by value.
};
Struct2 func4(int a, double b, int c, float d);
// Caller passes a in RCX, b in XMM1, c in R8, and d in XMM3;
// callee returns Struct2 result by value in RAX.

Rejestry zapisane przez funkcję wywołującą/wywoływaną

ABI x64 uznaje rejestry RAX, RCX, RDX, R8, R9, R10, R11 i XMM0-XMM5 za nietrwałe. W przypadku obecności górne części YMM0-YMM15 i ZMM0-ZMM15 są również niestabilne. Na AVX512VL rejestry ZMM, YMMi XMM 16-31 również są niestabilne. Gdy dostępna jest obsługa AMX, rejestry kafelków TMM są ulotne. Rozważ, że ulotne rejestry są niszczone podczas wywołań funkcji, chyba że można to bezpiecznie udowodnić poprzez analizę, na przykład dzięki optymalizacji całego programu.

Interfejs ABI x64 uznaje rejestry RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15, XMM6 oraz -XMM15 za nieulotne. Należy je zapisać i przywrócić za pomocą funkcji, która ich używa.

Gdy jest obecna obsługa APX, rejestry są nietrwałe R16-R29 . R30 i R31 nie sąvolatile.

Wskaźniki funkcji

Wskaźniki funkcji to po prostu wskaźniki do etykiety odpowiedniej funkcji. Nie ma wymagania spisu treści (TOC) dla wskaźników funkcji.

Obsługa zmiennoprzecinkowa dla starszego kodu

Rejestry MMX i rejestry stosu zmiennoprzecinkowego (MM0-MM7/ST0-ST7) są zachowywane podczas przełączania kontekstu. Nie ma jawnej konwencji wywoływania tych rejestrów. Korzystanie z tych rejestrów jest ściśle zabronione w kodzie trybu jądra.

FPCSR

Stan rejestru zawiera również słowo kontrolne x87 FPU. Konwencja wywołania wymaga, aby ten rejestr był nieulotny (nonvolatile).

Rejestr wyrazów sterujących x87 FPU jest ustawiany przy użyciu następujących standardowych wartości na początku wykonywania programu:

Rejestrowanie[bity] Ustawienie
FPCSR\[0:6] Wyjątki maskuje wszystkie 1s (wszystkie wyjątki maskowane)
FPCSR\[7] Zarezerwowane — 0
FPCSR\[8:9] Kontrolka precyzji — 10B (podwójna precyzja)
FPCSR\[10:11] Kontrola zaokrąglania — 0 (zaokrąglanie do najbliższej wartości)
FPCSR\[12] Kontrolka Nieskończoność — 0 (nie jest używana)

Wywoływana funkcja, która modyfikuje dowolne pola w obrębie FPCSR, musi przywrócić ich pierwotny stan przed zwróceniem sterowania do funkcji wywołującej. Ponadto obiekt wywołujący, który zmodyfikował dowolne z tych pól, musi przywrócić je do ich standardowych wartości przed wywołaniem, chyba że wywoływany zgadza się na zmodyfikowane wartości.

Istnieją dwa wyjątki od reguł dotyczących nieulotności flag kontroli:

  • W funkcjach, w których udokumentowanym celem danej funkcji jest modyfikowanie nieulotnych flag FPCSR.

  • Gdy jest udowadnialnie poprawne, że naruszenie tych reguł powoduje, że program zachowuje się tak samo jak program, który nie narusza reguł, na przykład poprzez analizę całego programu.

Mimo że jest uznawany za nieulotny, nie istnieje statyczny deskryptor rozwijania stosu określający, gdzie został zapisany i skąd należy go przywrócić. Kod odporny na wyjątki, który modyfikuje FPCSR, powinien używać finalizatora wyjątku (np. destruktora C++ lub klauzuli __finally), aby jawnie go przywrócić podczas odwijania stosu.

MXCSR

Stan rejestru obejmuje również MXCSR. Konwencja wywołania dzieli ten rejestr na ulotną część oraz trwałą część. Część ulotna obejmuje sześć flag stanu oznaczonych w MXCSR\[0:5], podczas gdy pozostałą część rejestru, MXCSR\[6:15], uznaje się za nieulotną.

Część nonvolatile jest ustawiona na następujące wartości standardowe na początku wykonywania programu:

Rejestrowanie[bity] Ustawienie
MXCSR\[6] Denormalizacje to zera - 0
MXCSR\[7:12] Wyjątki maskuje wszystkie 1s (wszystkie wyjątki maskowane)
MXCSR\[13:14] Kontrola zaokrąglania — 0 (zaokrąglanie do najbliższej wartości)
MXCSR\[15] Opróżnij do zera dla zamaskowanego podpełnienia — 0 (wyłączone)

Obiekt wywoływany, który modyfikuje dowolne z pól nienalotnych w obrębie programu MXCSR , musi je przywrócić przed powrotem do obiektu wywołującego. Ponadto obiekt wywołujący, który zmodyfikował dowolne z tych pól, musi przywrócić je do ich standardowych wartości przed wywołaniem, chyba że wywoływany zgadza się na zmodyfikowane wartości.

Istnieją dwa wyjątki od reguł dotyczących nieulotności flag kontroli:

  • W funkcjach, których udokumentowanym przeznaczeniem jest modyfikowanie nieulotnych flag MXCSR.

  • Gdy jest udowadnialnie poprawne, że naruszenie tych reguł powoduje, że program zachowuje się tak samo jak program, który nie narusza reguł, na przykład poprzez analizę całego programu.

Nie należy przyjmować żadnych założeń dotyczących stanu ulotnej części rejestru MXCSR po obu stronach granicy funkcji, chyba że dokumentacja funkcji wyraźnie to opisuje.

Mimo że niektóre części MXCSR są uznawane za nieulotne, nie istnieje statyczny deskryptor rozwijania stosu, który określa, gdzie został on zapisany i skąd należy go przywrócić. Kod odporny na wyjątki, który modyfikuje nielotne części elementu MXCSR, powinien używać finalizatora wyjątku (np. destruktora w języku C++ lub klauzuli __finally), aby jawnie przywrócić jego stan podczas odwijania stosu.

setjmp/longjmp

Gdy dołączasz setjmpex.h lub setjmp.h, wszystkie wywołania do setjmp lub longjmp powodują proces odwijania, który wywołuje destruktory oraz __finally wywołania. To zachowanie różni się od x86, gdzie uwzględnienie setjmp.h powoduje, że klauzule __finally i destruktory nie są wywoływane.

Wywołanie setjmp zachowuje bieżący wskaźnik stosu, rejestry nieulotne i rejestry MXCSR. Wywołania funkcji longjmp powodują powrót do miejsca ostatniego wywołania setjmp i przywracają wskaźnik stosu, rejestry nieulotne oraz rejestry MXCSR do stanu zachowanego przy ostatnim wywołaniu setjmp.

Jeśli APX jest obsługiwany, R30 i R31 nie powinny być modyfikowane w funkcji od momentu wywołania setjmp do momentu wykonania wywołania, które ostatecznie skutkuje longjmp. To ograniczenie wynika z tego, że elementy R30 i R31 nie są zapisywane jako część jmp_buf — tej definicji struktury nie można zmienić. Zamiast tego są przywracane za pośrednictwem odwijacza. W poniższym przykładzie pokazano, jak różnica w sposobie przywracania danych wpływa na to ograniczenie:

jmp_buf jmpbuffer;

void function_a() {
    ...

    int val = setjmp(jmpbuffer);  // At this time R30 is 10

    ...

    if (val == 0) {
        function_b();  // At this time R30 is 20
    }

    ...
}

void function_b() {
    ...

    longjmp(jmpbuffer, 1);

    ...
}

W tym przykładzie wartość R30 zmienia się od momentu wywołania setjmp do momentu wywołania function_b. W function_blongjmp odwija stos, aż dotrze do funkcji, która wywołała setjmp (w tym przypadku function_a). Wartość przywrócona dla R30 będzie wynosić 20 (wartość w momencie wywołania function_b), a nie 10 (wartość z momentu wywołania setjmp). Oznacza to, że gdy setjmp zwróci wartość po raz drugi (w wyniku longjmp), wartość R30 zostanie ustawiona na 20 zamiast na 10, co jest niepoprawne. Dlatego kompilatory muszą zapewnić, że R30 i R31 pozostają niezmienne od momentu wywołania setjmp do ostatniego miejsca w funkcji, które może ostatecznie doprowadzić do wywołania longjmp.

Ponieważ longjmp można wywołać w filtrze wyjątków (nie tylko z podprogramu), oznacza to w praktyce, że R30 i R31 powinny pozostać stałe od momentu wywołania setjmp aż do końca funkcji.

Zobacz też