Przegląd konwencji ABI ARM64
Podstawowy interfejs binarny aplikacji (ABI) dla systemu Windows podczas kompilowania i uruchamiania na procesorach ARM w trybie 64-bitowym (ARMv8 lub nowszych) w większości przypadków jest zgodny ze standardową architekturą AArch64 EABI usługi ARM. W tym artykule opisano niektóre kluczowe założenia i zmiany dotyczące tego, co zostało udokumentowane w ramach umowy EABI. Aby uzyskać informacje o 32-bitowej architekturze ABI, zobacz Omówienie konwencji usługi ARM ABI. Aby uzyskać więcej informacji na temat standardowego interfejsu ARM EABI, zobacz Application Binary Interface (ABI) for the ARM Architecture (link zewnętrzny).
Definicje
Wraz z wprowadzeniem obsługi 64-bitowej usługa ARM zdefiniowała kilka terminów:
- AArch32 — starsza architektura zestawu instrukcji 32-bitowych (ISA) zdefiniowana przez usługę ARM, w tym wykonywanie trybu kciuka.
- AArch64 — nowa architektura zestawu instrukcji 64-bitowych (ISA) zdefiniowana przez usługę ARM.
- ARMv7 — specyfikacja sprzętu ARM "7. generacji", który obejmuje tylko obsługę usługi AArch32. Ta wersja sprzętu ARM jest pierwszą wersją systemu Windows obsługiwaną przez usługę ARM.
- ARMv8 — specyfikacja sprzętu ARM "8. generacji", który obejmuje obsługę zarówno AArch32, jak i AArch64.
System Windows używa również następujących terminów:
- ARM — odnosi się do 32-bitowej architektury ARM (AArch32), czasami określanej jako WoA (Windows w usłudze ARM).
- ARM32 — taka sama jak w przypadku usługi ARM powyżej; używana w tym dokumencie w celu zapewnienia przejrzystości.
- ARM64 — odnosi się do 64-bitowej architektury ARM (AArch64). Nie ma czegoś takiego jak WoA64.
Na koniec podczas odwoływania się do typów danych odwołania do następujących definicji z usługi ARM:
- Short-Vector — typ danych, który można bezpośrednio przedstawić w SIMD, wektor 8 bajtów lub 16 bajtów wartości elementów. Jest on wyrównany do rozmiaru , 8 bajtów lub 16 bajtów, gdzie każdy element może mieć 1, 2, 4 lub 8 bajtów.
- HFA (Homogeniczny agregacja zmiennoprzecinkowa) — typ danych z 2 do 4 identycznych składowych zmiennoprzecinkowych, zmiennoprzecinkowych lub podwójnych.
- HVA (homogeniczne agregacja wektorów krótkich) — typ danych z 2 do 4 identycznych składowych wektorów krótkich.
Wymagania podstawowe
Wersja ARM64 systemu Windows wstępnie zakłada, że jest uruchomiona w architekturze ARMv8 lub nowszej przez cały czas. Zarówno zmiennoprzecinkowa, jak i obsługa NEONu są uważane za obecne w sprzęcie.
Specyfikacja ARMv8 opisuje nowe opcjonalne kody kryptograficzne i pomocnicze CRC zarówno dla AArch32, jak i AArch64. Obsługa ich jest obecnie opcjonalna, ale zalecana. Aby korzystać z tych kodów operacji, aplikacje powinny najpierw sprawdzić środowisko uruchomieniowe pod kątem ich istnienia.
Endianness
Podobnie jak w przypadku wersji ARM32 systemu Windows, na arm64 system Windows jest wykonywany w trybie mało endianu. Przełączanie endianness jest trudne do osiągnięcia bez obsługi trybu jądra w AArch64, więc łatwiej jest wymusić.
Wyrównanie
System Windows uruchomiony w usłudze ARM64 umożliwia sprzętowi procesora CPU obsługę niezrównoważonego dostępu w sposób niewidoczny. W ulepszeniu usługi AArch32 ta obsługa działa teraz również dla wszystkich dostępu liczb całkowitych (w tym dostępu do wielu wyrazów) i dostępu do zmiennoprzecinkowych.
Jednak dostęp do pamięci niecached (urządzenia) nadal musi być wyrównany. Jeśli kod może odczytywać lub zapisywać nieprawidłowo dopasowane dane z niebuforowanej pamięci, należy upewnić się, że wszystkie dostępy są wyrównane.
Domyślne wyrównanie układu dla ustawień lokalnych:
Rozmiar w bajtach | Wyrównanie w bajtach |
---|---|
1 | 1 |
2 | 2 |
3, 4 | 100 |
> 4 | 8 |
Domyślne wyrównanie układu dla wartości globalnych i statycznych:
Rozmiar w bajtach | Wyrównanie w bajtach |
---|---|
1 | 1 |
2 - 7 | 100 |
8 - 63 | 8 |
>= 64 | 16 |
Rejestry liczb całkowitych
Architektura AArch64 obsługuje 32 rejestry całkowite:
Zarejestruj | Lotność | Rola |
---|---|---|
x0-x8 | Lotny | Rejestry podstaw parametrów/wyników |
x9-x15 | Lotny | Rejestry plików tymczasowych |
x16-x17 | Lotny | Rejestry zdrapki wewnątrz procedury |
x18 | Nie dotyczy | Rejestr platformy zarezerwowanej: w trybie jądra wskazuje wskaźnik KPCR dla bieżącego procesora; W trybie użytkownika wskazuje TEB |
x19-x28 | Nietrwałe | Rejestry plików tymczasowych |
x29/fp | Nietrwałe | Wskaźnik ramki |
x30/lr | Oba | Rejestr linków: funkcja Wywoływana musi zachować ją dla własnego zwrotu, ale wartość obiektu wywołującego zostanie utracona. |
Każdy rejestr może być dostępny jako pełna wartość 64-bitowa (za pośrednictwem x0-x30) lub jako wartość 32-bitowa (za pośrednictwem w0-w30). 32-bitowe operacje zero-rozszerzają wyniki do 64 bitów.
Aby uzyskać szczegółowe informacje na temat używania rejestrów parametrów, zobacz sekcję Przekazywanie parametrów.
W przeciwieństwie do AArch32 licznik programu (PC) i wskaźnik stosu (SP) nie są indeksowane rejestry. Są one ograniczone w sposobie ich uzyskiwania dostępu. Należy również pamiętać, że nie ma rejestru x31. To kodowanie jest używane do celów specjalnych.
Wskaźnik ramki (x29) jest wymagany do zapewnienia zgodności z szybkim chodzeniem stosu używanym przez ETW i inne usługi. Musi wskazywać poprzednią parę {x29, x30} na stosie.
Rejestry zmiennoprzecinkowe/SIMD
Architektura AArch64 obsługuje również 32 rejestry zmiennoprzecinkowe/SIMD, podsumowane poniżej:
Zarejestruj | Lotność | Rola |
---|---|---|
v0-v7 | Lotny | Rejestry podstaw parametrów/wyników |
v8-v15 | Oba | Niskie 64 bity są nietrwałe. Wysokie 64 bity są nietrwałe. |
v16-v31 | Lotny | Rejestry plików tymczasowych |
Każdy rejestr może być dostępny jako pełna wartość 128-bitowa (za pośrednictwem v0-v31 lub q0-q31). Dostęp do niej można uzyskać jako wartość 64-bitową (za pośrednictwem d0-d31) jako wartość 32-bitową (za pośrednictwem s0-s31), jako wartość 16-bitową (za pośrednictwem h0-h31) lub jako wartość 8-bitową (za pośrednictwem b0-b31). Uzyskuje dostęp do mniejszych niż 128 bitów tylko do niższych bitów pełnego rejestru 128-bitowego. Pozostawiają pozostałe bity nietknięte, chyba że określono inaczej. (AArch64 różni się od AArch32, gdzie mniejsze rejestry były pakowane na podstawie większych rejestrów).
Rejestr sterowania zmiennoprzecinkowego (FPCR) ma pewne wymagania dotyczące różnych pól bitowych w nim:
Bity | Znaczenie | Lotność | Rola |
---|---|---|---|
26 | AHP | Nietrwałe | Alternatywna kontrolka o połowie precyzji. |
25 | DN | Nietrwałe | Domyślna kontrolka trybu NaN. |
24 | FZ | Nietrwałe | Sterowanie trybem opróżniania do zera. |
23-22 | Tryb RMode | Nietrwałe | Sterowanie trybem zaokrąglania. |
15,12-8 | IDE/IXE/etc | Nietrwałe | Pułapka wyjątków włącza bity, zawsze musi być 0. |
Rejestry systemowe
Podobnie jak AArch32, specyfikacja AArch64 udostępnia trzy rejestry sterowane przez system "identyfikator wątku":
Zarejestruj | Rola |
---|---|
TPIDR_EL0 | Zarezerwowany. |
TPIDRRO_EL0 | Zawiera numer procesora dla bieżącego procesora. |
TPIDR_EL1 | Wskazuje strukturę KPCR dla bieżącego procesora. |
Wyjątki zmiennoprzecinkowe
Obsługa wyjątków zmiennoprzecinkowych IEEE jest opcjonalna w systemach AArch64. W przypadku wariantów procesora, które mają sprzętowe wyjątki zmiennoprzecinkowe, jądro systemu Windows dyskretnie przechwytuje wyjątki i niejawnie wyłącza je w rejestrze FPCR. Ta pułapka zapewnia znormalizowane zachowanie w różnych wariantach procesora. W przeciwnym razie kod opracowany na platformie bez obsługi wyjątków może uznać za nieoczekiwane wyjątki podczas uruchamiania na platformie z pomocą techniczną.
Przekazywanie parametrów
W przypadku funkcji innych niż wariadyczne usługa Windows ABI jest zgodna z regułami określonymi przez usługę ARM na potrzeby przekazywania parametrów. Te reguły są fragmentowane bezpośrednio z procedury Call Standard dla architektury AArch64:
Etap A — inicjowanie
Ten etap jest wykonywany dokładnie raz, przed rozpoczęciem przetwarzania argumentów.
Następny numer rejestru ogólnego przeznaczenia (NGRN) jest ustawiony na zero.
Następny numer SIMD i liczba rejestrów zmiennoprzecinkowych (NSRN) ma wartość zero.
Następny skumulowany adres argumentu (NSAA) jest ustawiony na bieżącą wartość wskaźnika stosu (SP).
Etap B — wstępne wypełnienie i rozszerzenie argumentów
Dla każdego argumentu na liście zostanie zastosowana pierwsza zgodna reguła z poniższej listy. Jeśli żadna reguła nie jest zgodna, argument jest używany niezmodyfikowany.
Jeśli typ argumentu jest typem złożonym, którego rozmiar nie może być statycznie określony zarówno przez obiekt wywołujący, jak i wywoływany, argument jest kopiowany do pamięci, a argument jest zastępowany przez wskaźnik do kopii. (Nie ma takich typów w języku C/C++, ale istnieją w innych językach lub w rozszerzeniach języka).
Jeśli typ argumentu jest uwierzytelnianiem HFA lub HVA, argument jest używany niezmodyfikowany.
Jeśli typ argumentu jest typem złożonym większym niż 16 bajtów, argument jest kopiowany do pamięci przydzielonej przez obiekt wywołujący, a argument jest zastępowany wskaźnikiem do kopii.
Jeśli typ argumentu jest typem złożonym, rozmiar argumentu jest zaokrąglany do najbliższej wielokrotności 8 bajtów.
Etap C — przypisywanie argumentów do rejestrów i stosu
Dla każdego argumentu na liście następujące reguły są stosowane z kolei do momentu przydzieleniu argumentu. Po przypisaniu argumentu do rejestru wszystkie nieużywane bity w rejestrze mają nieokreśloną wartość. Jeśli argument jest przypisany do miejsca stosu, wszystkie nieużywane dopełnienie bajtów mają nieokreśloną wartość.
Jeśli argument jest typu zmiennoprzecinkowego typu Half-, Single-, Double-or Quad-precision lub Short Vector, a NSRN jest mniejszy niż 8, argument jest przydzielany do najmniej znaczących bitów rejestru v[NSRN]. NSRN jest zwiększana o jeden. Argument został teraz przydzielony.
Jeśli argument jest uwierzytelnianiem HFA lub HVA i istnieje wystarczająca liczba nieprzydzielonych rejestrów SIMD i zmiennoprzecinkowych (NSRN + liczba elementów członkowskich ≤ 8), argument jest przydzielany do rejestrów SIMD i zmiennoprzecinkowych, jeden rejestr na członka uwierzytelniania HFA lub HVA. NSRN jest zwiększana przez liczbę używanych rejestrów. Argument został teraz przydzielony.
Jeśli argument jest HFA lub HVA, NSRN jest ustawiony na 8, a rozmiar argumentu jest zaokrąglany do najbliższej wielokrotności 8 bajtów.
Jeśli argument jest HFA, HVA, quad-precision Zmiennoprzecinkowa lub Krótki typ wektora, NSAA jest zaokrąglany do większego od 8 lub naturalnego wyrównania typu argumentu.
Jeśli argument jest typem zmiennoprzecinkowa o wartości pół lub pojedynczej precyzji, rozmiar argumentu jest ustawiony na 8 bajtów. Efekt jest taki, jakby argument został skopiowany do najmniej znaczących bitów rejestru 64-bitowego, a pozostałe bity wypełnione nieokreślonymi wartościami.
Jeśli argumentem jest HFA, HVA, Half-, Single-, Double-lub Quad-precision Zmiennoprzecinkowa lub Krótki typ wektora, argument jest kopiowany do pamięci w skorygowanej NSAA. NSAA jest zwiększana przez rozmiar argumentu. Argument został teraz przydzielony.
Jeśli argument jest typem całkowitoliczbowym lub wskaźnikowym, rozmiar argumentu jest mniejszy lub równy 8 bajtów, a NGRN jest mniejszy niż 8, argument jest kopiowany do najmniej znaczących bitów w x[NGRN]. NGRN jest zwiększana o jeden. Argument został teraz przydzielony.
Jeśli argument ma wyrównanie 16, to NGRN jest zaokrąglany do następnej liczby parzystej.
Jeśli argument jest typem integralnym, rozmiar argumentu jest równy 16, a NGRN jest mniejsza niż 7, argument jest kopiowany do x[NGRN] i x[NGRN+1]. x[NGRN] zawiera dolne adresowane podwójne słowo pamięci reprezentacji argumentu. NGRN jest zwiększana o dwa. Argument został teraz przydzielony.
Jeśli argument jest typem złożonym, a rozmiar w podwójnych słowach argumentu nie przekracza 8 minus NGRN, argument jest kopiowany do kolejnych rejestrów ogólnego przeznaczenia, zaczynając od x[NGRN]. Argument jest przekazywany tak, jakby został załadowany do rejestrów z adresu wyrównanego z podwójnym wyrazem, z odpowiednią sekwencją instrukcji LDR, które ładują kolejne rejestry z pamięci. Zawartość wszelkich nieużywanych części rejestrów nie jest określona przez ten standard. NGRN jest zwiększana przez liczbę używanych rejestrów. Argument został teraz przydzielony.
NGRN jest ustawiona na 8.
NSAA jest zaokrąglany do większego od 8 lub naturalnego wyrównania typu argumentu.
Jeśli argument jest typem złożonym, argument jest kopiowany do pamięci w skorygowanym NSAA. NSAA jest zwiększana przez rozmiar argumentu. Argument został teraz przydzielony.
Jeśli rozmiar argumentu jest mniejszy niż 8 bajtów, rozmiar argumentu jest ustawiony na 8 bajtów. Efekt jest taki, jakby argument został skopiowany do najmniej znaczących bitów rejestru 64-bitowego, a pozostałe bity zostały wypełnione nieokreślonymi wartościami.
Argument jest kopiowany do pamięci w skorygowanej NSAA. NSAA jest zwiększana przez rozmiar argumentu. Argument został teraz przydzielony.
Addendum: funkcje Variadic
Funkcje, które przyjmują zmienną liczbę argumentów, są obsługiwane inaczej niż powyżej, w następujący sposób:
Wszystkie złożone są traktowane podobnie; nie ma specjalnego traktowania HFAs lub HVAs.
Rejestry SIMD i Zmiennoprzecinkowe nie są używane.
W rzeczywistości jest to samo, co następujące reguły C.12–C.15 do przydzielenia argumentów do wyimaginowanego stosu, gdzie pierwsze 64 bajty stosu są ładowane do x0-x7, a pozostałe argumenty stosu są umieszczane normalnie.
Wartości zwracane
Wartości całkowite są zwracane w x0.
Wartości zmiennoprzecinkowe są zwracane odpowiednio w s0, d0 lub v0.
Typ jest uważany za HFA lub HVA, jeśli wszystkie następujące blokady:
- To niepuste,
- Nie ma żadnych nietrywialnych konstruktorów domyślnych ani konstruktorów kopiowania, destruktorów ani operatorów przypisania,
- Wszystkie jego elementy członkowskie mają ten sam typ HFA lub HVA albo są zmiennoprzecinkowe, podwójne lub neonowe, które pasują do typów HFA lub HVA innych elementów członkowskich.
Wartości HVA z czterema lub mniejszą liczbą elementów są zwracane odpowiednio w s0-s3, d0-d3 lub v0-v3.
Typy zwracane przez wartość są obsługiwane inaczej w zależności od tego, czy mają określone właściwości i czy funkcja jest funkcją niestacyjną składową. Typy, które mają wszystkie te właściwości,
- są agregowane przez standardową definicję języka C++14, czyli nie mają konstruktorów dostarczanych przez użytkownika, nie mają prywatnych ani chronionych niestacjonowanych składowych danych, nie klas bazowych i nie mają żadnych funkcji wirtualnych oraz
- mają trywialny operator przypisania kopiowania i
- mają trywialny destruktor,
i są zwracane przez funkcje inne niż składowe lub statyczne funkcje składowe, należy użyć następującego stylu zwracanego:
- Typy, które są HFA z czterema lub mniejszą liczbą elementów, są zwracane odpowiednio w s0-s3, d0-d3 lub v0-v3.
- Typy mniejsze niż lub równe 8 bajtów są zwracane w x0.
- Typy mniejsze niż lub równe 16 bajtów są zwracane w x0 i x1, z x0 zawierającym 8 bajtów niższej kolejności.
- W przypadku innych typów agregacji obiekt wywołujący zastrzega sobie blok pamięci o wystarczającej wielkości i wyrównaniu do przechowywania wyniku. Adres bloku pamięci jest przekazywany jako dodatkowy argument funkcji w x8. Obiekt wywoływany może modyfikować blok pamięci wynikowej w dowolnym momencie podczas wykonywania podroutyny. Obiekt wywoływany nie jest wymagany do zachowania wartości przechowywanej w x8.
Wszystkie inne typy używają tej konwencji:
- Obiekt wywołujący zastrzega sobie blok pamięci o wystarczającej wielkości i wyrównaniu do przechowywania wyniku. Adres bloku pamięci jest przekazywany jako dodatkowy argument funkcji w x0 lub x1, jeśli $this jest przekazywany w x0. Obiekt wywoływany może modyfikować blok pamięci wynikowej w dowolnym momencie podczas wykonywania podroutyny. Obiekt wywoływany zwraca adres bloku pamięci w x0.
Stos
Po utworzeniu ABI przez usługę ARM stos musi pozostać wyrównany do 16 bajtów przez cały czas. AArch64 zawiera funkcję sprzętową, która generuje błędy wyrównania stosu, gdy sp nie jest wyrównany 16 bajtów, a obciążenie względne sp lub magazyn jest wykonywane. System Windows działa z włączoną tą funkcją przez cały czas.
Funkcje, które przydzielają 4k lub więcej wartości stosu, muszą mieć pewność, że każda strona przed końcową stroną jest dotykana w kolejności. Ta akcja gwarantuje, że żaden kod nie może "przeskoczyć" stron ochrony używanych przez system Windows do rozwinięcia stosu. Zazwyczaj dotknięcie odbywa się przez __chkstk
pomocnika, który ma niestandardową konwencję wywoływania, która przekazuje łączną alokację stosu podzieloną przez 16 w x15.
Czerwona strefa
Obszar 16 bajtów bezpośrednio poniżej bieżącego wskaźnika stosu jest zarezerwowany do użycia przez scenariusze analizy i dynamicznego stosowania poprawek. Ten obszar pozwala starannie wygenerować kod do wstawienia, który przechowuje dwa rejestry w [sp, #-16] i tymczasowo używa ich do dowolnych celów. Jądro systemu Windows gwarantuje, że te 16 bajtów nie zostaną zastąpione, jeśli wystąpi wyjątek lub przerwanie w trybie użytkownika i jądra.
Stos jądra
Domyślny stos trybu jądra w systemie Windows to sześć stron (24k). Zwróć szczególną uwagę na funkcje z dużymi stosu w trybie jądra. Nieprzeterminowane przerwanie może pojawić się z małą ilością miejsca pracy i utworzyć kontrolę usterek paniki stosu.
Chodzenie stosem
Kod w systemie Windows jest kompilowany z włączonymi wskaźnikami ramek (/Oy-), aby umożliwić szybkie przechodzenie stosu. Ogólnie rzecz biorąc, x29 (fp) wskazuje na następny link w łańcuchu, który jest parą {fp, lr}, wskazując wskaźnik do poprzedniej ramki na stosie i adres zwrotny. Zaleca się również włączenie wskaźników ramek w celu umożliwienia ulepszonego profilowania i śledzenia kodu innej firmy.
Odwijanie wyjątku
Odwijanie podczas obsługi wyjątków jest wspomagane za pomocą kodów odwijania. Kody odwijane to sekwencja bajtów przechowywanych w sekcji .xdata pliku wykonywalnego. Opisują one działanie prologu i epilogu w abstrakcyjny sposób, tak aby efekty prologu funkcji można cofnąć w ramach przygotowań do tworzenia kopii zapasowej do ramki stosu wywołującego. Aby uzyskać więcej informacji na temat kodów odwijanych, zobacz Arm64 exception handling (Obsługa wyjątków ARM64).
Usługa ARM EABI określa również model odwijania wyjątku, który używa kodów odwijania. Jednak specyfikacja przedstawiona jest niewystarczająca do odwijania w systemie Windows, co musi obsługiwać przypadki, w których komputer znajduje się w środku prologu funkcji lub epilogu.
Kod generowany dynamicznie należy opisać za pomocą dynamicznych tabel funkcji za pośrednictwem RtlAddFunctionTable
i skojarzonych funkcji, aby wygenerowany kod mógł uczestniczyć w obsłudze wyjątków.
Licznik cyklu
Wszystkie procesory ARMv8 są wymagane do obsługi rejestru licznika cyklu, rejestru 64-bitowego, który system Windows konfiguruje do odczytu na dowolnym poziomie wyjątku, w tym w trybie użytkownika. Dostęp do niego można uzyskać za pośrednictwem specjalnego rejestru PMCCNTR_EL0, przy użyciu kodu opcode MSR w kodzie zestawu lub wewnętrznego _ReadStatusReg
kodu C/C++.
Licznik cyklu w tym miejscu jest prawdziwym licznikiem cyklu, a nie zegarem ściennym. Częstotliwość zliczania będzie się różnić w zależności od częstotliwości procesora. Jeśli uważasz, że musisz znać częstotliwość licznika cyklu, nie należy używać licznika cyklu. Zamiast tego chcesz zmierzyć zegara ściany, dla którego należy użyć .QueryPerformanceCounter
Zobacz też
Typowe problemy przy migracji Visual C++ ARM
Obsługa wyjątków ARM64