Sdílet prostřednictvím


x64 – konvence volání

Tento článek popisuje standardní procesy a konvence, které jedna funkce (volající) používá k volání do jiné funkce (volaný) v kódu x64.

Další informace o __vectorcall konvenci volání najdete v tématu __vectorcall.

Výchozí nastavení konvence volání

x64 Application Binary Interface (ABI) ve výchozím nastavení používá čtyřregistrovou rychlovolací konvenci volání. V zásobníku volání je přidělen prostor jako stínové úložiště pro volané, aby se tyto registry uložily.

Mezi argumenty volání funkce a registry použitými pro tyto argumenty existuje striktní korespondence 1:1. Všechny argumenty, které se nevejdou do 8 bajtů nebo nejsou 1, 2, 4 nebo 8 bajtů, musí být předány odkazem. Jeden argument se nikdy nerozprostírá mezi více registry.

Zásobník registru x87 se nepoužívá. Může ho používat volaný, ale mezi voláními funkcí ho zvažte jako proměnlivé. Všechny operace s plovoucí desetinnou čárkou se provádějí pomocí 16 registrů XMM.

Celočíselné argumenty se předávají v registrech RCX, RDX, R8 a R9. Argumenty s plovoucí desetinou čárkou se předávají v XMM0L, XMM1L, XMM2L a XMM3L. 16-bajtové argumenty se předávají odkazem. Předávání parametrů je podrobně popsáno při předávání parametrů. Tyto registry, RAX, R10, R11, XMM4 a XMM5, jsou považovány za nestálé nebo potenciálně změněné volaným při vrácení. Použití registrů je podrobně popsáno v používání registrů x64 a uložených registrů volajícího a volaného.

U prototypovaných funkcí se všechny argumenty před předáním převedou na očekávané volané typy. Volající zodpovídá za přidělování prostoru pro parametry volané osoby. Volající musí vždy přidělit dostatek místa pro uložení čtyř parametrů registru, i když volaný nepřebere tento počet parametrů. Tato konvence zjednodušuje podporu neprotypovaných funkcí jazyka C a funkcí jazyka Vararg C/C++. Pro funkce vararg nebo funkce bez prototypu musí být všechny hodnoty pohyblivé řádové čárky duplikovány v odpovídajícím registru pro obecné účely. Všechny parametry nad rámec prvních čtyř musí být uloženy v zásobníku za stínovým úložištěm před voláním. Podrobnosti funkce Vararg najdete v nástroji Varargs. Informace o neprotypované funkci jsou podrobně popsány v neprotypovaných funkcích.

Zarovnání

Většina struktur je zarovnaná podle svého přirozeného zarovnání. Primárními výjimkami jsou ukazatel zásobníku a paměť malloc nebo alloca, které jsou 16-bajtově zarovnané tak, aby pomohly výkonu. Zarovnání nad 16 bajty musí být provedeno ručně. Vzhledem k tomu, že 16 bajtů je běžnou velikost zarovnání pro operace XMM, měla by tato hodnota fungovat pro většinu kódu. Další informace o rozložení struktury a zarovnání najdete v tématu x64 – typ a rozložení úložiště. Informace o rozložení zásobníku najdete v tématu o využití zásobníku x64.

rozvinutelnost

Listové funkce jsou funkce, které nemění žádné nezměnitelné registry. Nelistová funkce může změnit například nevolatilní registr RSP tím, že zavolá jinou funkci. Nebo by se mohlo změnit RSP přidělením více prostoru zásobníku pro místní proměnné. Pokud se při zpracování výjimky obnovují nevolatilní registry, jsou ne-listové funkce opatřeny anotacemi se statickými daty. Data popisují, jak správně uvolnit funkci pomocí libovolné instrukce. Tato data jsou uložena jako pdata, nebo procedurní data, která následně odkazují na xdata, data pro zpracování výjimek. xdata obsahuje informace o rozbalování a může odkazovat na další pdata nebo funkci obslužné rutiny výjimky.

Prology a epilogy jsou vysoce omezené, aby je bylo možné správně popsat v xdata. Ukazatel zásobníku musí zůstat zarovnaný na 16 bajtů v jakékoli části kódu, která není součástí epilogu nebo prologu, s výjimkou koncových funkcí. Listové funkce lze jednoduše rozvinout simulací návratu, takže pdata a xdata nejsou požadovány. Podrobnosti o správné struktuře prologů a epilogů funkcí najdete v tématu x64 prolog a epilog. Další informace o zpracování výjimek, a o zpracování a rozbalování pdata a xdata, najdete v tématu zpracování výjimek x64.

Předávání parametrů

Ve výchozím nastavení konvence volání x64 předává první čtyři argumenty funkci v registrech. Registry použité pro tyto argumenty závisí na pozici a typu argumentu. Zbývající argumenty se předávají v zásobníku v pořadí zprava doleva. Volající si rezervuje požadovaný prostor zásobníku a zapíše tyto argumenty do paměti zásobníku pomocí instrukcí pro uložení nebo přesun, přičemž pro každý argument zachová zarovnání na 8 bajtů.

Celočíselné hodnotné argumenty ve čtyřech pozicích úplně vlevo jsou předány v pořadí zleva doprava ve formátu RCX, RDX, R8 a R9. Páté a vyšší argumenty se předávají v zásobníku, jak jsme popsali dříve. Všechny celočíselné argumenty v registrech jsou správné, takže volaný může ignorovat horní části registru a přistupovat pouze k části registru potřebné.

Argumenty s plovoucí desetinnou čárkou a dvojitou přesností, které jsou součástí prvních čtyř parametrů, se předávají v registrech XMM0 - XMM3 v závislosti na jejich pořadí. Hodnoty s plovoucí desetinou čárkou jsou umístěny pouze v celočíselných registrech RCX, RDX, R8 a R9, pokud existují argumenty varargs. Podrobnosti najdete v tématu Varargs. Podobně jsou registru XMM0 – XMM3 ignorovány, pokud je odpovídající argument celé číslo nebo typ ukazatele.

__m128 typy, pole a řetězce se nikdy nepředávají jako přímá hodnota. Místo toho se ukazatel předá do paměti přidělené volajícím. Struktury a sjednocení velikosti 8, 16, 32 nebo 64 bitů a __m64 typů se předávají jako celá čísla stejné velikosti. Struktury nebo unie jiných velikostí jsou předány jako ukazatele na paměť přidělenou volajícím. U těchto agregačních typů předaných jako ukazatel, včetně __m128, musí být dočasná paměť přidělená volajícím zarovnaná na 16 bajtů.

Vnitřní funkce, které nepřidělují prostor zásobníku a nevolají jiné funkce, někdy používají jiné nestálé registry k předání dalších argumentů registru. Tato optimalizace je možná úzkou vazbou mezi kompilátorem a implementací vnitřní funkce.

Volaný je zodpovědný za uložení parametrů registru do svého stínového prostoru v případě potřeby.

Následující tabulka shrnuje předávání parametrů podle typu a pozice zleva:

Typ parametru pátý a výše čtvrtý třetí sekunda nejvíce vlevo
plovoucí řádová čárka stack XMM3 XMM2 XMM1 XMM0
integer stack R9 R8 RDX RCX
Agregáty (8, 16, 32 nebo 64 bitů) a __m64 stack R9 R8 RDX RCX
Další agregace jako ukazatele stack R9 R8 RDX RCX
__m128, jako ukazatel stack R9 R8 RDX RCX

Příklad předání argumentu 1 – všechna celá čísla

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

Příklad předání argumentu 2 – všechny plovoucí hodnoty

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

Příklad předávání argumentů 3 – smíšená celá čísla a plovoucí hodnoty

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

Příklad předávání argumentů 4 - __m64, __m128, a agregáty

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

Vararg

Pokud jsou parametry předány prostřednictvím varargs (například argumenty se třemi tečkami), použije se normální konvence předávání parametrů registru. Tato konvence zahrnuje přesunutí pátého a pozdějšího argumentu do zásobníku. Je odpovědností volaného zajistit výpis argumentů, u kterých byla získána jejich adresa. Pouze pro hodnoty s plovoucí desetinnou čárkou musí jak celočíselný registr, tak registr s plovoucí desetinnou čárkou obsahovat hodnotu, pro případ, že volaný očekává hodnotu v celočíselném registru.

Neprotypované funkce

U funkcí, které nejsou plně prototypované, volající předává celočíselné hodnoty jako celá čísla a hodnoty s plovoucí desetinnou čárkou jako hodnoty s dvojitou přesností. Pouze pro hodnoty s plovoucí řádovou čárkou obsahují jak celočíselné, tak i plovoucí registry hodnotu, pokud volaný očekává hodnotu v celočíselných registrech.

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

Vrácené hodnoty

Skalární návratová hodnota, která se může vejít do 64 bitů včetně __m64 typu, se vrátí prostřednictvím RAX. Nescalarové typy, včetně typů float, double a vektorových typů jako __m128, __m128i a __m128d, jsou vráceny v XMM0. Stav nepoužívaných bitů v hodnotě vrácené v RAX nebo XMM0 není definován.

Uživatelem definované typy mohou být vráceny hodnotou z globálních funkcí a statických členských funkcí. Pokud chcete vrátit uživatelem definovaný typ podle hodnoty v RAX, musí mít délku 1, 2, 4, 8, 16, 32 nebo 64 bitů. Nesmí obsahovat ani uživatelem definovaný konstruktor, destruktor nebo přiřazovací operátor kopírování. Nemůže mít žádné soukromé nebo chráněné nestatické datové členy a žádné nestatické datové členy referenčního typu. Nemůže mít základní třídy ani virtuální funkce. A může mít pouze datové členy, které tyto požadavky splňují. Tato definice je v podstatě stejná jako typ POD C++03. Vzhledem k tomu, že se definice ve standardu C++11 změnila, nedoporučujeme pro tento test používat std::is_pod . Jinak volající musí přidělit paměť pro návratovou hodnotu a předat jí ukazatel jako první argument. Zbývající argumenty se pak posunou o jeden argument doprava. Stejný ukazatel musí vrátit volaný v RAX.

Tyto příklady ukazují, jak se parametry a návratové hodnoty předávají pro funkce se zadanými deklaracemi:

Příklad návratové hodnoty 1 – 64bitový výsledek

__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.

Příklad návratové hodnoty 2 – 128bitový výsledek

__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.

Příklad návratové hodnoty 3 – výsledek typu uživatele podle ukazatele

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.

Příklad návratové hodnoty 4 – výsledek typu uživatele podle hodnoty

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.

Uložené registry volajícího nebo volaný

x64 ABI považuje registry RAX, RCX, RDX, R8, R9, R10, R11 a XMM0-XMM5 za volatilní. Jsou-li přítomny, jsou horní části YMM0-YMM15 a ZMM0-ZMM15 také nestálé. Na AVX512VL jsou registry ZMM, YMM a XMM v rozsahu 16-31 také nestálé. Pokud je k dispozici podpora AMX, registry dlaždic TMM jsou nestálé. Zvažte volatilní registry jako zničené u volání funkcí, pokud není, že by se dalo prokázat bezpečí analýzou, jako je optimalizace celého programu.

ABI pro x64 považuje registry RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 a XMM6-XMM15 za nevolatilní. Musí být uloženy a obnoveny funkcí, která je používá.

Ukazatelé funkcí

Ukazatele funkce jsou jednoduše ukazatele na popisek příslušné funkce. Pro ukazatele funkcí neexistuje žádný požadavek na tabulku obsahu (TOC).

Podpora pro čísla s plovoucí desetinnou čárkou pro starší kód

Rejstříky zásobníku MMX a plovoucí desetiny (MM0-MM7/ST0-ST7) se zachovají napříč kontextovými přepínači. Pro tyto registry neexistuje žádná explicitní konvence volání. Použití těchto registrů je přísně zakázáno v kódu režimu jádra.

FPCSR

Stav registrů obsahuje také ovládací slovo FPU x87. Konvence volání určuje, aby tento registr byl nevolatilní.

Ovládací prvek X87 FPU word register se nastaví pomocí následujících standardních hodnot na začátku provádění programu:

Zaregistrovat[bity] Nastavení
FPCSR[0:6] Masky výjimek všech 1 (všechny výjimky maskované)
FPCSR[7] Rezervováno – 0
FPCSR[8:9] Přesné ovládání – 10B (dvojitá přesnost)
FPCSR[10:11] Nastavení zaokrouhlování – 0 (zaokrouhlení na nejbližší)
FPCSR[12] Ovládání nekonečna – 0 (nepoužívá se)

Volaný, který upravuje některá pole v rámci FPCSR, musí je obnovit před návratem ke svému volajícímu. Volající, který změnil některá z těchto polí, navíc musí před vyvoláním volaného obnovit jejich standardní hodnoty, pokud volaný neočekává změněné hodnoty.

Existují dvě výjimky pravidel o nevolatilitě příznaků ovládacího prvku:

  • Ve funkcích, kde zdokumentovaný účel dané funkce je upravit nevolatilní příznaky FPCSR.

  • Pokud je prokazatelně správné, že porušení těchto pravidel vede k tomu, že program má stejné chování jako program, který tato pravidla neporušuje, například prostřednictvím analýzy celého programu.

MXCSR

Stav registru zahrnuje také MXCSR. Konvence volání rozdělí tento registr na nestálou část a nevolatilní část. Těkavá část se skládá ze šesti příznaků stavu v MXCSR[0:5], zatímco zbytek registru MXCSR[6:15] se považuje za nevolatilní.

Nevolatilní část je nastavena na následující standardní hodnoty na začátku provádění programu:

Zaregistrovat[bity] Nastavení
MXCSR[6] Denormální čísla jsou nuly
MXCSR[7:12] Maskování výjimek všech jedniček (všechna výjimky jsou maskované)
MXCSR[13:14] Nastavení zaokrouhlování – 0 (zaokrouhlení na nejbližší)
MXCSR[15] Nastavení na nulu při maskovaném podtečení – 0 (vypnuto)

Volaný, který změní některá z nezměnitelných polí v rámci MXCSR, je musí obnovit před návratem ke svému volajícímu. Volající, který změnil některá z těchto polí, navíc musí před vyvoláním volaného obnovit jejich standardní hodnoty, pokud volaný neočekává změněné hodnoty.

Existují dvě výjimky pravidel o nevolatilitě příznaků ovládacího prvku:

  • Ve funkcích, kde zdokumentovaný účel dané funkce je upravit nevolatilní příznaky MXCSR.

  • Pokud je prokazatelně správné, že porušení těchto pravidel vede k tomu, že program má stejné chování jako program, který tato pravidla neporušuje, například prostřednictvím analýzy celého programu.

Nepředpokládáme žádné předpoklady o stavu nestálé části registru MXCSR v rámci hranice funkce, pokud ji dokumentace k funkci explicitně nepopisuje.

setjmp/longjmp

Když zahrnete setjmpex.h nebo setjmp.h, všechna volání setjmp nebo longjmp způsobí proces uvolnění, který vyvolá destruktory a volání __finally. Toto chování se liší od x86, kde zahrnutí setjmp.h způsobí, že __finally klauzule a destruktory nejsou vyvolány.

Volání setjmp zachovává aktuální ukazatel zásobníku, nevolatilní registry a registry MXCSR. Volání longjmp se vracejí na nejnovější místo volání setjmp a obnovují ukazatel zásobníku, nevolatilní a MXCSR registry zpět do stavu, který je zachován posledním voláním setjmp.

Viz také