Omówienie konwencji ABI arm32
Interfejs binarny aplikacji (ABI) dla kodu skompilowanego dla systemu Windows na procesorach ARM jest oparty na standardowej architekturze ARM EABI. W tym artykule przedstawiono kluczowe różnice między systemem Windows w usłudze ARM i standardem. Ten dokument zawiera omówienie usługi ARM32 ABI. Aby uzyskać informacje o architekturze ARM64 ABI, zobacz Omówienie konwencji ABI ARM64. Aby uzyskać więcej informacji na temat standardowego interfejsu ARM EABI, zobacz Application Binary Interface (ABI) for the ARM Architecture (link zewnętrzny).
Podstawowe wymagania
System Windows w usłudze ARM zawsze zakłada, że działa w architekturze ARMv7. Obsługa zmiennoprzecinkowa w postaci VFPv3-D32 lub nowszej musi być obecna na sprzęcie. VFP musi obsługiwać zarówno pojedynczą precyzję, jak i zmiennoprzecinkę o podwójnej precyzji w sprzęcie. Środowisko uruchomieniowe systemu Windows nie obsługuje emulacji zmiennoprzecinkowych w celu włączenia działania na sprzęcie innych niż VFP.
Zaawansowana obsługa rozszerzeń SIMD (NEON), w tym zarówno operacji całkowitych, jak i zmiennoprzecinkowych, musi być również obecna na sprzęcie. Nie jest dostępna obsługa emulacji w czasie wykonywania.
Obsługa dzielenia liczb całkowitych (UDIV/SDIV) jest zalecana, ale nie jest wymagana. Platformy, które nie obsługują dzielenia liczb całkowitych, mogą spowodować karę za wydajność, ponieważ te operacje muszą zostać uwięzione i ewentualnie poprawione.
Endianness
System Windows w usłudze ARM jest wykonywany w trybie mało endianu. Zarówno kompilator MSVC, jak i środowisko uruchomieniowe systemu Windows zawsze oczekują małych danych endiańskich. Instrukcja SETEND w architekturze zestawu instrukcji ARM (ISA) umożliwia nawet kod trybu użytkownika w celu zmiany bieżącej endianness. Jednak jest to zniechęcane, ponieważ jest niebezpieczne dla aplikacji. Jeśli wyjątek jest generowany w trybie big-endian, zachowanie jest nieprzewidywalne. Może to prowadzić do błędu aplikacji w trybie użytkownika lub sprawdzanie błędów w trybie jądra.
Wyrównanie
Mimo że system Windows umożliwia sprzętowi ARM obsługę nieprawidłowo wyrównanej liczby całkowitej w sposób niewidoczny, błędy wyrównania nadal mogą być generowane w niektórych sytuacjach. Postępuj zgodnie z następującymi regułami w celu wyrównania:
Nie musisz wyrównywać liczb całkowitych i liczb całkowitych o rozmiarze pół wyrazu (16-bitowym) i rozmiarze wyrazu (32-bitowym). Sprzęt obsługuje je wydajnie i w sposób niewidoczny.
Obciążenia zmiennoprzecinkowe i magazyny powinny być wyrównane. Jądro obsługuje nieprzygotowane obciążenia i przechowuje je w sposób niewidoczny, ale ze znacznym obciążeniem.
Należy wyrównać operacje ładowania lub przechowywania podwójnego (LDRD/STRD) i wielu operacji (LDM/STM). Jądro obsługuje większość z nich w sposób niewidoczny, ale ze znacznym obciążeniem.
Wszystkie dostępy do pamięci niebuforowanej muszą być wyrównane, nawet w przypadku dostępu do liczby całkowitej. Nieprzygotowane dostępy powodują usterkę wyrównania.
Zestaw instrukcji
Zestaw instrukcji dla systemu Windows w usłudze ARM jest ściśle ograniczony do Thumb-2. Oczekuje się, że cały kod wykonywany na tej platformie będzie uruchamiany i zawsze pozostaje w trybie kciuka. Próba przełączenia się do starszego zestawu instrukcji usługi ARM może zakończyć się powodzeniem. Jeśli jednak tak się stanie, wszelkie wyjątki lub przerwania, które występują, mogą prowadzić do błędu aplikacji w trybie użytkownika lub sprawdzanie błędów w trybie jądra.
Efektem ubocznym tego wymagania jest to, że wszystkie wskaźniki kodu muszą mieć zestaw bitów o niskim poziomie. Następnie po załadowaniu i rozgałęzieniu ich za pośrednictwem programu BLX lub BX procesor pozostaje w trybie kciuka. Nie próbuje wykonać kodu docelowego jako 32-bitowych instrukcji usługi ARM.
Instrukcje SDIV/UDIV
Korzystanie z instrukcji dzielenia liczb całkowitych SDIV i UDIV jest w pełni obsługiwane, nawet na platformach bez natywnego sprzętu do ich obsługi. Dodatkowe obciążenie na dziele SDIV lub UDIV na procesor Cortex-A9 wynosi około 80 cykli. Jest to dodawane do ogólnego czasu podziału 20–250 cykli, w zależności od danych wejściowych.
Rejestry liczb całkowitych
Procesor ARM obsługuje rejestry 16 liczb całkowitych:
Zarejestruj | Lotny? | Rola |
---|---|---|
r0 | Lotny | Parametr, wynik, rejestr podstaw 1 |
r1 | Lotny | Parametr, wynik, rejestr tymczasowy 2 |
r2 | Lotny | Parametr, rejestr podstaw 3 |
r3 | Lotny | Parametr, rejestr podstaw 4 |
R4 | Nietrwałe | |
r5 | Nietrwałe | |
r6 | Nietrwałe | |
r7 | Nietrwałe | |
R8 | Nietrwałe | |
r9 | Nietrwałe | |
r10 | Nietrwałe | |
r11 | Nietrwałe | Wskaźnik ramki |
r12 | Lotny | Rejestr zdrapek wewnątrz procedury |
r13 (SP) | Nietrwałe | Wskaźnik stosu |
r14 (LR) | Nietrwałe | Rejestr linków |
r15 (PC) | Nietrwałe | Licznik programów |
Aby uzyskać szczegółowe informacje na temat używania rejestru parametru i zwracanej wartości, zobacz sekcję Przekazywanie parametrów w tym artykule.
System Windows używa r11 do szybkiego chodzenia ramką stosu. Aby uzyskać więcej informacji, zobacz sekcję Stack Walking. Ze względu na to wymaganie r11 musi zawsze wskazywać górny link w łańcuchu. Nie używaj r11 do celów ogólnych, ponieważ kod nie będzie generować poprawnych przewodników stosu podczas analizy.
Rejestry VFP
System Windows obsługuje tylko warianty arm z obsługą współprocesora VFPv3-D32. Oznacza to, że rejestry zmiennoprzecinkowe są zawsze obecne i mogą być oparte na przekazywaniu parametrów. Pełny zestaw 32 rejestrów jest dostępny do użycia. Rejestry VFP i ich użycie zostały podsumowane w tej tabeli:
Singli | podwojenia | Quady | Lotny? | Rola |
---|---|---|---|---|
s0-s3 | d0-d1 | q0 | Lotny | Parametry, wynik, rejestr plików tymczasowych |
s4-s7 | d2-d3 | Pytanie 1. | Lotny | Parametry, rejestr plików tymczasowych |
s8-s11 | d4-d5 | Pytanie 2. | Lotny | Parametry, rejestr plików tymczasowych |
s12-s15 | d6-d7 | q3 | Lotny | Parametry, rejestr plików tymczasowych |
s16-s19 | d8-d9 | q4 | Nietrwałe | |
s20-s23 | d10-d11 | q5 | Nietrwałe | |
s24-s27 | d12-d13 | q6 | Nietrwałe | |
s28-s31 | d14-d15 | q7 | Nietrwałe | |
d16-d31 | q8-q15 | Lotny |
W następnej tabeli przedstawiono pola bitowe ze stanem zmiennoprzecinku i rejestrem sterowania (FPSCR):
Bity | Znaczenie | Lotny? | Rola |
---|---|---|---|
31-28 | NZCV | Lotny | Flagi stanu |
27 | QC | Lotny | Nasycenie skumulowane |
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 |
21-20 | Kroczyć | Nietrwałe | Wektor, zawsze musi mieć wartość 0 |
18-16 | Len | Nietrwałe | Długość wektora, zawsze musi mieć wartość 0 |
15, 12-8 | IDE, IXE itd. | Nietrwałe | Pułapka wyjątków włącza bity, zawsze musi mieć wartość 0 |
7, 4-0 | IDC, IXC itd. | Lotny | Flagi wyjątków skumulowanych |
Wyjątki zmiennoprzecinkowe
Większość sprzętu ARM nie obsługuje wyjątków zmiennoprzecinkowych IEEE. W wariantach procesora, które mają sprzętowe wyjątki zmiennoprzecinkowe, jądro systemu Windows dyskretnie przechwytuje wyjątki i niejawnie wyłącza je w rejestrze FPSCR. Ta akcja zapewnia znormalizowane zachowanie w różnych wariantach procesora. W przeciwnym razie kod opracowany na platformie, która nie ma obsługi wyjątków, może otrzymać nieoczekiwane wyjątki, gdy jest uruchomiony na platformie, która ma obsługę wyjątków.
Przekazywanie parametrów
System Windows w usłudze ARM ABI jest zgodny z regułami arm dotyczącymi przekazywania parametrów dla funkcji innych niż variadyczne. Reguły ABI obejmują rozszerzenia VFP i Advanced SIMD. Te reguły są zgodne z procedurą Call Standard dla architektury arm w połączeniu z rozszerzeniami VFP. Domyślnie pierwsze cztery argumenty całkowite i do ośmiu argumentów zmiennoprzecinkowych lub wektorowych są przekazywane w rejestrach. Wszelkie dalsze argumenty są przekazywane na stosie. Argumenty są przypisywane do rejestrów lub stosu przy użyciu tej procedury:
Etap A: Inicjowanie
Inicjowanie jest wykonywane dokładnie raz, zanim rozpocznie się przetwarzanie argumentów:
Numer rejestracji następnego rdzenia (NCRN) jest ustawiony na r0.
Rejestry VFP są oznaczone jako nieprzydzielone.
Następny skumulowany adres argumentu (NSAA) jest ustawiony na bieżący sp.
Jeśli zostanie wywołana funkcja zwracająca wynik w pamięci, adres wyniku zostanie umieszczony w r0, a ncRN jest ustawiona na r1.
Etap B: wstępne wypełnienie i rozszerzenie argumentów
Dla każdego argumentu na liście jest stosowana pierwsza zgodna reguła z poniższej listy:
Jeśli argument jest typem złożonym, którego rozmiar nie może być statycznie określany zarówno przez obiekt wywołujący, jak i wywoływany, argument jest kopiowany do pamięci i zastępowany wskaźnikiem do kopii.
Jeśli argument jest bajtem lub 16-bitowym pół wyrazem, oznacza to, że jest on rozszerzony o zero lub znak rozszerzony do 32-bitowego pełnego słowa i traktowany jako argument 4-bajtowy.
Jeśli argument jest typem złożonym, jego rozmiar jest zaokrąglany do najbliższej wielokrotności 4.
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:
Jeśli argument jest typem VFP i istnieje wystarczająca liczba kolejnych nieprzydzielonych rejestrów VFP odpowiedniego typu, argument jest przydzielany do najniższej liczby takich rejestrów.
Jeśli argument jest typem VFP, wszystkie pozostałe nieprzydzielone rejestry są oznaczone jako niedostępne. NSAA jest dostosowywana w górę, dopóki nie zostanie poprawnie wyrównana dla typu argumentu i argument zostanie skopiowany do stosu w skorygowanym NSAA. NSAA jest następnie zwiększana o rozmiar argumentu.
Jeśli argument wymaga wyrównania 8 bajtów, ncRN jest zaokrąglany do następnej parzystej liczby rejestrów.
Jeśli rozmiar argumentu w 32-bitowych słowach nie jest większy niż r4 minus NCRN, argument jest kopiowany do rejestrów podstawowych, począwszy od NCRN, z najmniej znaczącymi bitami zajmującymi rejestry niższej liczby. NcRN jest zwiększana przez liczbę używanych rejestrów.
Jeśli NCRN jest mniejsza niż r4, a NSAA jest równa sp, argument jest podzielony między rejestry rdzeni i stos. Pierwsza część argumentu jest kopiowana do rejestrów podstawowych, począwszy od NCRN, do i w tym r3. Pozostała część argumentu jest kopiowana do stosu, zaczynając od NSAA. NcRN jest ustawiona na r4, a NSAA jest zwiększana o rozmiar argumentu minus kwotę przekazaną w rejestrach.
Jeśli argument wymaga wyrównania 8 bajtów, NSAA jest zaokrąglany do następnego adresu wyrównanego 8 bajtów.
Argument jest kopiowany do pamięci w NSAA. NSAA jest zwiększana przez rozmiar argumentu.
Rejestry VFP nie są używane do funkcji wariadycznych, a reguły etapu C 1 i 2 są ignorowane. Oznacza to, że funkcja wariadyczna może zaczynać się od opcjonalnego wypychania {r0-r3}, aby poprzedzać argumenty rejestru dodatkowymi argumentami przekazanymi przez obiekt wywołujący, a następnie uzyskać dostęp do całej listy argumentów bezpośrednio ze stosu.
Wartości typu liczba całkowita są zwracane w r0, opcjonalnie rozszerzone do r1 dla 64-bitowych wartości zwracanych. Wartości typu VFP/NEON zmiennoprzecinkowe lub SIMD są zwracane odpowiednio w s0, d0 lub q0.
Stos
Stos musi zawsze pozostać wyrównany do 4 bajtów i musi być wyrównany do 8 bajtów w dowolnej granicy funkcji. Jest to wymagane do obsługi częstego używania operacji połączonych ze zmiennymi stosu 64-bitowego. Usługa ARM EABI stwierdza, że stos jest 8-bajtowy wyrównany do dowolnego interfejsu publicznego. W celu zapewnienia spójności system Windows w usłudze ARM ABI uważa, że każda granica funkcji jest interfejsem publicznym.
Funkcje, które muszą używać wskaźnika ramki — na przykład funkcje alloca
wywołujące lub zmieniające wskaźnik stosu dynamicznie — muszą skonfigurować wskaźnik ramki w r11 w prologu funkcji i pozostawić go bez zmian do czasu epilogu. Funkcje, które nie wymagają wskaźnika ramki, muszą wykonywać wszystkie aktualizacje stosu w prologu i pozostawić wskaźnik stosu bez zmian do epilogu.
Funkcje przydzielające co najmniej 4 KB na stosie muszą zapewnić, że każda strona przed ostatnim dotknięciem strony jest w kolejności. Ta kolejność gwarantuje, że żaden kod nie może "przeskoczyć" stron ochrony używanych przez system Windows do rozwinięcia stosu. Zazwyczaj rozszerzenie jest wykonywane przez __chkstk
pomocnik, który jest przekazywany łączną alokację stosu w bajtach podzielonych przez 4 w r4 i który zwraca ostateczną ilość alokacji stosu w bajtach z powrotem w r4.
Czerwona strefa
Obszar 8-bajtowy bezpośrednio poniżej bieżącego wskaźnika stosu jest zarezerwowany do analizy i dynamicznego stosowania poprawek. Umożliwia on starannie wygenerowanego kodu, który przechowuje 2 rejestry w i [sp, #-8]
tymczasowo używa ich do dowolnych celów. Jądro systemu Windows gwarantuje, że te 8 bajtów nie zostaną zastąpione, jeśli wystąpi wyjątek lub przerwanie zarówno w trybie użytkownika, jak i w trybie jądra.
Stos jądra
Domyślny stos trybu jądra w systemie Windows to trzy strony (12 KB). Należy zachować ostrożność, aby nie tworzyć funkcji, które mają duże stosu w trybie jądra. Przerwanie może pojawić się z bardzo małą salą stosu i spowodować błąd paniki stosu.
Specyfika języka C/C++
Wyliczenia to 32-bitowe typy całkowite, chyba że co najmniej jedna wartość w wyliczenie wymaga 64-bitowego magazynu dwu word. W takim przypadku wyliczenie jest promowane do 64-bitowej liczby całkowitej.
wchar_t
jest definiowany jako odpowiednik unsigned short
elementu , aby zachować zgodność z innymi platformami.
Chodzenie stosem
Kod systemu Windows jest kompilowany z włączonymi wskaźnikami ramek (/Oy (pominięcie wskaźnika ramki)), aby umożliwić szybkie przechodzenie stosu. Ogólnie rzecz biorąc, rejestr r11 wskazuje na następne łącze w łańcuchu, czyli parę {r11, lr}, która określa wskaźnik do poprzedniej ramki na stosie i adres zwrotny. Zalecamy również włączenie wskaźników ramek w kodzie w celu ulepszenia profilowania i śledzenia.
Odwijanie wyjątku
Odwijanie stosu podczas obsługi wyjątków jest włączone za pomocą kodów odwijania. Kody odwijane to sekwencja bajtów przechowywanych w sekcji .xdata obrazu wykonywalnego. Opisują one działanie prologu funkcji i kodu epilogu w abstrakcyjny sposób, tak aby efekty prologu funkcji można cofnąć w ramach przygotowania do odwijania się do ramki stosu obiektu wywołującego.
Arm EABI określa model odwijania wyjątku, który używa kodów odwijania. Jednak ta specyfikacja nie jest wystarczająca do odwijania się w systemie Windows, co musi obsługiwać przypadki, w których procesor znajduje się w środku prologu lub epilogu funkcji. Aby uzyskać więcej informacji na temat systemu Windows na danych wyjątków usługi ARM i odwijania ich, zobacz Arm Exception Handling (Obsługa wyjątków arm).
Zalecamy, aby dynamicznie wygenerowany kod był opisywany przy użyciu dynamicznych tabel funkcji określonych w wywołaniach RtlAddFunctionTable
i skojarzonych funkcji, aby wygenerowany kod mógł uczestniczyć w obsłudze wyjątków.
Licznik cyklu
Procesory ARM z systemem Windows są wymagane do obsługi licznika cyklu, ale użycie licznika bezpośrednio może powodować problemy. Aby uniknąć tych problemów, system Windows w usłudze ARM używa niezdefiniowanego kodu opcode w celu żądania znormalizowanej wartości licznika cyklu 64-bitowego. W języku C lub C++użyj __rdpmccntr64
funkcji wewnętrznej, aby emitować odpowiedni kod opcode; z zestawu użyj instrukcji __rdpmccntr64
. Odczytywanie licznika cyklu trwa około 60 cykli na Cortex-A9.
Licznik jest rzeczywistym licznikiem cyklu, a nie zegarem; w związku z tym częstotliwość zliczania różni się w zależności od częstotliwości procesora. Jeśli chcesz zmierzyć czas zegara, użyj polecenia QueryPerformanceCounter
.
Zobacz też
Typowe problemy przy migracji Visual C++ ARM
Obsługa wyjątków ARM