Obsługa wyjątków ARM64
System Windows w usłudze ARM64 używa tego samego mechanizmu obsługi wyjątków ustrukturyzowanych dla asynchronicznych wyjątków generowanych przez sprzęt i synchronicznych wyjątków generowanych przez oprogramowanie. Programy obsługi wyjątków specyficznych dla języka są oparte na obsłudze wyjątków ustrukturyzowanych systemu Windows przy użyciu funkcji pomocnika języka. W tym dokumencie opisano obsługę wyjątków w systemie Windows w usłudze ARM64. Ilustruje pomocników języka używanych przez kod generowany przez asembler usługi Microsoft ARM i kompilator MSVC.
Cele i motywacja
Wyjątki odwijające konwencje danych i ten opis mają na celu:
Podaj wystarczająco dużo opisu, aby umożliwić odwijanie bez sondowania kodu we wszystkich przypadkach.
Analizowanie kodu wymaga stronicowania kodu. Zapobiega to odwijaniu się w pewnych okolicznościach, gdy jest to przydatne (śledzenie, próbkowanie, debugowanie).
Analizowanie kodu jest złożone; kompilator musi być ostrożny, aby wygenerować tylko instrukcje, które może dekodować odwijanie.
Jeśli nie można w pełni opisać odwijania przy użyciu kodów odwijania, w niektórych przypadkach musi on wrócić do dekodowania instrukcji. Dekodowanie instrukcji zwiększa ogólną złożoność i najlepiej unikać.
Obsługa odwijania w połowie prologu i połowie epilogu.
- Odwijanie jest używane w systemie Windows w celu obsługi więcej niż wyjątków. Bardzo ważne jest, aby kod mógł się dokładnie odwinąć nawet wtedy, gdy w środku sekwencji kodu prologu lub epilogu.
Zajmuje minimalną ilość miejsca.
Kody odwijań nie mogą być agregowane w celu znacznego zwiększenia rozmiaru binarnego.
Ponieważ kody odwijane mogą być zablokowane w pamięci, niewielki ślad zapewnia minimalne obciążenie dla każdego załadowanego pliku binarnego.
Założenia
Te założenia są podejmowane w opisie obsługi wyjątków:
Prologi i epilogi mają tendencję do dublowania się nawzajem. Korzystając z tej typowej cechy, rozmiar metadanych potrzebnych do opisania odwijania może być znacznie zmniejszony. W treści funkcji nie ma znaczenia, czy operacje prologu są cofnięte, czy operacje epilogu są wykonywane w przyszłości. Oba powinny generować identyczne wyniki.
Funkcje mają tendencję do stosunkowo małych. Kilka optymalizacji przestrzeni polega na tym, aby osiągnąć najbardziej wydajne pakowanie danych.
Nie ma kodu warunkowego w epilogach.
Rejestr dedykowanych wskaźników ramek: jeśli
sp
element jest zapisany w innym rejestrze (x29
) w prologu, rejestr pozostaje nienaruszony w całej funkcji. Oznacza to, że oryginałsp
może zostać odzyskany w dowolnym momencie.O ile element nie
sp
zostanie zapisany w innym rejestrze, wszystkie manipulowanie wskaźnikiem stosu odbywa się ściśle w ramach prologu i epilogu.Układ ramki stosu jest zorganizowany zgodnie z opisem w następnej sekcji.
Układ ramki stosu ARM64
W przypadku funkcji łańcuchowych ramek fp
pary i lr
można zapisać w dowolnym miejscu w obszarze zmiennej lokalnej, w zależności od zagadnień optymalizacji. Celem jest zmaksymalizowanie liczby ustawień lokalnych, które można uzyskać za pomocą jednej instrukcji na podstawie wskaźnika ramki (x29
) lub wskaźnika stosu (sp
). Jednak w przypadku alloca
funkcji musi być w łańcuchu i x29
musi wskazywać dół stosu. Aby umożliwić lepsze pokrycie trybu rejestrowania par adresowania, obszary zapisywania rejestru nieuwolnego są umieszczone w górnej części stosu obszaru lokalnego. Poniżej przedstawiono przykłady ilustrujące kilka najbardziej wydajnych sekwencji prologów. Ze względu na jasność i lepszą lokalność pamięci podręcznej kolejność przechowywania zapisanych na wywoływanych rejestrach we wszystkich kanonicznych prologach jest w kolejności "dorastania". #framesz
poniżej reprezentuje rozmiar całego stosu (z wyłączeniem alloca
obszaru). #localsz
i #outsz
oznaczają odpowiednio rozmiar obszaru lokalnego (w tym obszar zapisywania <x29, lr>
dla pary) i rozmiar parametru wychodzącego.
Łańcuchowe, #localsz <= 512
stp x19,x20,[sp,#-96]! // pre-indexed, save in 1st FP/INT pair stp d8,d9,[sp,#16] // save in FP regs (optional) stp x0,x1,[sp,#32] // home params (optional) stp x2,x3,[sp,#48] stp x4,x5,[sp,#64] stp x6,x7,[sp,#82] stp x29,lr,[sp,#-localsz]! // save <x29,lr> at bottom of local area mov x29,sp // x29 points to bottom of local sub sp,sp,#outsz // (optional for #outsz != 0)
Łańcuchowe, #localsz > 512
stp x19,x20,[sp,#-96]! // pre-indexed, save in 1st FP/INT pair stp d8,d9,[sp,#16] // save in FP regs (optional) stp x0,x1,[sp,#32] // home params (optional) stp x2,x3,[sp,#48] stp x4,x5,[sp,#64] stp x6,x7,[sp,#82] sub sp,sp,#(localsz+outsz) // allocate remaining frame stp x29,lr,[sp,#outsz] // save <x29,lr> at bottom of local area add x29,sp,#outsz // setup x29 points to bottom of local area
Niezapisane, funkcje liścia (
lr
niezapisane)stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] str x23,[sp,#32] stp d8,d9,[sp,#40] // save FP regs (optional) stp d10,d11,[sp,#56] sub sp,sp,#(framesz-80) // allocate the remaining local area
Dostęp do wszystkich ustawień lokalnych jest uzyskiwany na podstawie elementu
sp
.<x29,lr>
wskazuje poprzednią ramkę. W przypadku rozmiaru <ramki = 512 można je zoptymalizować,sub sp, ...
jeśli zapisany obszar regs zostanie przeniesiony na dół stosu. Wadą jest to, że nie jest zgodny z innymi układami powyżej. Zapisane regs biorą udział w zakresie par-regs i wstępnie i po indeksowaniu trybu adresowania przesunięcia.Funkcje nieuwzględniane (zapisuje
lr
w zapisanym obszarze Int)stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] // ... stp x23,lr,[sp,#32] // save last Int reg and lr stp d8,d9,[sp,#48] // save FP reg-pair (optional) stp d10,d11,[sp,#64] // ... sub sp,sp,#(framesz-80) // allocate the remaining local area
Lub, przy numerze parzysty zapisanym w rejestrach int,
stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] // ... str lr,[sp,#32] // save lr stp d8,d9,[sp,#40] // save FP reg-pair (optional) stp d10,d11,[sp,#56] // ... sub sp,sp,#(framesz-80) // allocate the remaining local area
Zapisane tylko
x19
:sub sp,sp,#16 // reg save area allocation* stp x19,lr,[sp] // save x19, lr sub sp,sp,#(framesz-16) // allocate the remaining local area
* Alokacja obszaru zapisywania reg nie jest składana do
stp
elementu , ponieważ wstępnie indeksowany reg-lrstp
nie może być reprezentowany za pomocą kodów odwijanych.Dostęp do wszystkich ustawień lokalnych jest uzyskiwany na podstawie elementu
sp
.<x29>
wskazuje poprzednią ramkę.Łańcuchowe, #framesz <= 512, #outsz = 0
stp x29,lr,[sp,#-framesz]! // pre-indexed, save <x29,lr> mov x29,sp // x29 points to bottom of stack stp x19,x20,[sp,#(framesz-32)] // save INT pair stp d8,d9,[sp,#(framesz-16)] // save FP pair
W porównaniu z pierwszym przykładem prologu powyżej ten przykład ma przewagę: wszystkie instrukcje zapisu rejestru są gotowe do wykonania po tylko jednej instrukcji alokacji stosu. Oznacza to, że nie ma zależności od
sp
tego, co uniemożliwia równoległość na poziomie instrukcji.Rozmiar ramki > w łańcuchu 512 (opcjonalny dla funkcji bez
alloca
)stp x29,lr,[sp,#-80]! // pre-indexed, save <x29,lr> stp x19,x20,[sp,#16] // save in INT regs stp x21,x22,[sp,#32] // ... stp d8,d9,[sp,#48] // save in FP regs stp d10,d11,[sp,#64] mov x29,sp // x29 points to top of local area sub sp,sp,#(framesz-80) // allocate the remaining local area
W celu optymalizacji można umieścić w dowolnym miejscu w obszarze lokalnym,
x29
aby zapewnić lepsze pokrycie dla "reg-pair" i wstępnie/po indeksowanym trybie adresowania przesunięcia. Dostęp do ustawień lokalnych poniżej wskaźników ramek można uzyskać nasp
podstawie elementu .Łańcuchowe, ramka o rozmiarze > 4K, z alloca(lub bez alloca(),
stp x29,lr,[sp,#-80]! // pre-indexed, save <x29,lr> stp x19,x20,[sp,#16] // save in INT regs stp x21,x22,[sp,#32] // ... stp d8,d9,[sp,#48] // save in FP regs stp d10,d11,[sp,#64] mov x29,sp // x29 points to top of local area mov x15,#(framesz/16) bl __chkstk sub sp,sp,x15,lsl#4 // allocate remaining frame // end of prolog ... sub sp,sp,#alloca // more alloca() in body ... // beginning of epilog mov sp,x29 // sp points to top of local area ldp d10,d11,[sp,#64] ... ldp x29,lr,[sp],#80 // post-indexed, reload <x29,lr>
Informacje o obsłudze wyjątków arm64
.pdata
Rekordy
Rekordy .pdata
są uporządkowaną tablicą elementów o stałej długości, które opisują każdą funkcję manipulowania stosem w pliku binarnym PE. Fraza "manipulowanie stosem" jest znacząca: funkcje liścia, które nie wymagają żadnego magazynu lokalnego i nie muszą zapisywać/przywracać rejestrów nietrwałych, nie wymagają rekordu .pdata
. Te rekordy powinny zostać jawnie pominięte, aby zaoszczędzić miejsce. Odwijenie z jednej z tych funkcji może uzyskać adres zwrotny bezpośrednio z lr
, aby przejść do obiektu wywołującego.
Każdy .pdata
rekord arm64 ma długość 8 bajtów. Ogólny format każdego rekordu umieszcza 32-bitową wartość RVA funkcji rozpoczynającej się w pierwszym słowie, a następnie drugie słowo zawierające wskaźnik do bloku o zmiennej długości .xdata
lub zapakowane słowo opisujące sekwencję funkcji kanonicznej.
Pola są następujące:
Funkcja Start RVA jest 32-bitowym RVA początku funkcji.
Flaga to pole 2-bitowe, które wskazuje, jak interpretować pozostałe 30 bitów drugiego
.pdata
słowa. Jeśli flaga to 0, pozostałe bity tworzą RVA informacji o wyjątku (z dwoma najniższymi bitami niejawnie 0). Jeśli flaga ma wartość inną niż zero, pozostałe bity tworzą strukturę Spakowane dane unwind.Informacje o wyjątku RVA to adres struktury informacji o wyjątkach o zmiennej długości przechowywanej
.xdata
w sekcji. Te dane muszą być wyrównane do 4 bajtów.Pakowane dane unwind to skompresowany opis operacji potrzebnych do odwijania się z funkcji przy założeniu formy kanonicznej. W takim przypadku nie
.xdata
jest wymagany żaden rekord.
.xdata
Rekordy
Gdy spakowany format unwind jest niewystarczający do opisania odwijania funkcji, należy utworzyć rekord o zmiennej długości .xdata
. Adres tego rekordu jest przechowywany w drugim słowie rekordu .pdata
. Format obiektu .xdata
to spakowany zestaw wyrazów o zmiennej długości:
Te dane są podzielone na cztery sekcje:
Nagłówek 1-wyrazowy lub 2-wyrazowy opisujący ogólny rozmiar struktury i dostarczający dane funkcji klucza. Drugie słowo jest obecne tylko wtedy, gdy dla pól Liczba epilogów i Wyrazy kodu ustawiono wartość 0. Nagłówek zawiera następujące pola bitowe:
a. Długość funkcji jest polem 18-bitowym. Wskazuje całkowitą długość funkcji w bajtach, podzieloną przez 4. Jeśli funkcja jest większa niż 1M, należy użyć wielu
.pdata
rekordów,.xdata
aby opisać tę funkcję. Aby uzyskać więcej informacji, zobacz sekcję Duże funkcje .b. Vers to pole 2-bitowe. Opisuje on wersję pozostałej
.xdata
wersji . Obecnie zdefiniowano tylko wersję 0, więc wartości od 1 do 3 nie są dozwolone.c. X to pole 1-bitowe. Wskazuje obecność (1) lub brak (0) danych wyjątku.
d. E to pole 1-bitowe. Wskazuje, że informacje opisujące pojedynczy epilog są pakowane w nagłówek (1), a nie wymagają więcej słów zakresu później (0).
e. Liczba epilogów jest polem 5-bitowym, które ma dwa znaczenia, w zależności od stanu bitu E :
Jeśli wartość E wynosi 0, określa liczbę całkowitych zakresów epilogu opisanych w sekcji 2. Jeśli w funkcji istnieje więcej niż 31 zakresów, należy ustawić pole Wyrazy kodu na 0, aby wskazać, że słowo rozszerzenia jest wymagane.
Jeśli wartość E to 1, to pole określa indeks pierwszego odwijanego kodu, który opisuje ten i tylko epilog.
f. Słowa kodu to pole 5-bitowe, które określa liczbę 32-bitowych wyrazów potrzebnych do przechowywania wszystkich kodów odwijanych w sekcji 3. Jeśli wymagane jest więcej niż 31 wyrazów (czyli 124 kodów odwijaczania), to to pole musi zawierać wartość 0, aby wskazać, że wymagane jest słowo rozszerzenia.
g. Rozszerzona liczba epilogów i rozszerzone wyrazy kodu są odpowiednio polami 16-bitowymi i 8-bitowymi. Zapewniają one więcej miejsca na kodowanie niezwykle dużej liczby epilogów lub niezwykle dużej liczby rozprężonych słów kodu. Słowo rozszerzenia zawierające te pola jest obecne tylko wtedy, gdy zarówno pola Liczba epilogów , jak i Wyrazy kodu w pierwszym słowie nagłówka to 0.
Jeśli liczba epilogów nie jest równa zero, lista informacji o zakresach epilogu, spakowana jeden do słowa, pojawia się po nagłówku i opcjonalnym rozszerzonym nagłówku. Są one przechowywane w kolejności zwiększania przesunięcia początkowego. Każdy zakres zawiera następujące bity:
a. Przesunięcie początkowe epilogu jest 18-bitowym polem, które ma przesunięcie w bajtach, podzielone przez 4, epilogu względem początku funkcji.
b. Res to 4-bitowe pole zarezerwowane do przyszłego rozszerzenia. Jego wartość musi być 0.
c. Indeks początkowy epilogu jest polem 10-bitowym (2 bity więcej niż rozszerzone wyrazy kodu). Wskazuje indeks bajtów pierwszego kodu odwijanego, który opisuje tę epilogię.
Po utworzeniu listy zakresów epilogu znajduje się tablica bajtów zawierających kody odwijające, szczegółowo opisaną w dalszej sekcji. Ta tablica jest dopełniona na końcu najbliższej pełnej granicy słowa. Kody odwijane są zapisywane w tej tablicy. Zaczynają się od tej znajdującej się najbliżej treści funkcji i przechodzą w kierunku krawędzi funkcji. Bajty dla każdego kodu odwijanego są przechowywane w kolejności big-endian, więc najważniejszy bajt jest pobierany jako pierwszy, który identyfikuje operację i długość pozostałej części kodu.
Na koniec po odwijaniu bajtów kodu, jeśli bit X w nagłówku został ustawiony na 1, pojawia się informacja obsługi wyjątków. Składa się z pojedynczej procedury obsługi wyjątków RVA , która udostępnia adres samego programu obsługi wyjątków. Następnie natychmiast następuje zmienna długość danych wymaganych przez program obsługi wyjątków.
Rekord .xdata
został zaprojektowany tak, aby można było pobrać pierwsze 8 bajtów i użyć ich do obliczenia pełnego rozmiaru rekordu, pomniejszonego o długość danych wyjątku o zmiennym rozmiarze, które następują poniżej. Poniższy fragment kodu oblicza rozmiar rekordu:
ULONG ComputeXdataSize(PULONG Xdata)
{
ULONG Size;
ULONG EpilogScopes;
ULONG UnwindWords;
if ((Xdata[0] >> 22) != 0) {
Size = 4;
EpilogScopes = (Xdata[0] >> 22) & 0x1f;
UnwindWords = (Xdata[0] >> 27) & 0x1f;
} else {
Size = 8;
EpilogScopes = Xdata[1] & 0xffff;
UnwindWords = (Xdata[1] >> 16) & 0xff;
}
if (!(Xdata[0] & (1 << 21))) {
Size += 4 * EpilogScopes;
}
Size += 4 * UnwindWords;
if (Xdata[0] & (1 << 20)) {
Size += 4; // Exception handler RVA
}
return Size;
}
Mimo że prolog i każdy epilog ma swój własny indeks w kodach odwijanie, tabela jest udostępniana między nimi. Jest to całkowicie możliwe (i nie jest zupełnie rzadkie), że wszystkie mogą współdzielić te same kody. (Na przykład zobacz przykład 2 w Sekcja przykłady ). Autorzy kompilatora powinni w szczególności zoptymalizować ten przypadek. Jest to spowodowane tym, że największy indeks, który można określić, wynosi 255, co ogranicza łączną liczbę kodów odwijenia dla określonej funkcji.
Odwij kody
Tablica kodów odwijanych jest pulą sekwencji, które opisują dokładnie, jak cofnąć efekty prologu. Są one przechowywane w tej samej kolejności, w której operacje muszą zostać cofnięte. Kody odwijane można traktować jako mały zestaw instrukcji zakodowany jako ciąg bajtów. Po zakończeniu wykonywania adres powrotny do funkcji wywołującej znajduje się w rejestrze lr
. Wszystkie rejestry nietrwałe są przywracane do ich wartości w momencie wywołania funkcji.
Gdyby wyjątki były gwarantowane tylko w obrębie treści funkcji i nigdy w prologu lub w żadnym epilogu, konieczne byłoby tylko jedna sekwencja. Jednak model odwijania systemu Windows wymaga, aby kod mógł się cofnąć z poziomu częściowo wykonanego prologu lub epilogu. Aby spełnić to wymaganie, kody odwijające zostały starannie zaprojektowane tak, aby jednoznacznie mapowały 1:1 na każdy odpowiedni kod opcode w prologu i epilogu. Ten projekt ma kilka konsekwencji:
Zliczając liczbę kodów odwijania, można obliczyć długość prologu i epilogu.
Zliczając liczbę instrukcji po rozpoczęciu zakresu epilogu, można pominąć równoważną liczbę kodów odwijania. Możemy wykonać pozostałą część sekwencji, aby ukończyć częściowo wykonane odwijęcie wykonane przez epilog.
Zliczając liczbę instrukcji przed końcem prologu, można pominąć równoważną liczbę kodów odwijania. Możemy wykonać pozostałą część sekwencji, aby cofnąć tylko te części prologu, które zakończyły wykonywanie.
Kody odwijań są kodowane zgodnie z poniższą tabelą. Wszystkie kody odwijane są pojedynczym/podwójnym bajtem, z wyjątkiem tego, który przydziela ogromny stos (alloc_l
). Łącznie istnieje 22 kody odwijań. Każdy unwind code mapuje dokładnie jedną instrukcję w prologu/epilogu, aby umożliwić odwijanie częściowo wykonanych prologów i epilogów.
Odwij kod | Bity i interpretacja |
---|---|
alloc_s |
000xxxxx: przydziel mały stos o rozmiarze < 512 (2^5 * 16). |
save_r19r20_x |
001zzz: zapisz <x19,x20> parę na [sp-#Z*8]! , wstępnie indeksowane przesunięcie >= -248 |
save_fplr |
01zzzzzz: zapisz <x29,lr> parę w [sp+#Z*8] , przesunięcie <= 504. |
save_fplr_x |
10zzzzzz: zapisz <x29,lr> parę na [sp-(#Z+1)*8]! , wstępnie indeksowane przesunięcie >= -512 |
alloc_m |
11000xxx'xxxxxxxx: przydziel duży stos o rozmiarze < 32K (2^11 * 16). |
save_regp |
110010xx'xxzzzzzz: save x(19+#X) pair at [sp+#Z*8] , offset <= 504 |
save_regp_x |
110011xx'xxzzzzzz: zapisz parę x(19+#X) z przesunięciem >indeksowanym wstępnie [sp-(#Z+1)*8]! = -512 |
save_reg |
110100xx'xxzzzzzz: save reg x(19+#X) at [sp+#Z*8] , offset <= 504 |
save_reg_x |
1101010x'xxxzzzzz: save reg x(19+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -256 |
save_lrpair |
1101011x'xxzzzzzz: save pair <x(19+2*#X),lr> at [sp+#Z*8] , offset <= 504 |
save_fregp |
1101100x'xxzzzzzz: save pair d(8+#X) at [sp+#Z*8] , offset <= 504 |
save_fregp_x |
1101101x'xxzzzzzz: zapisz parę d(8+#X) na [sp-(#Z+1)*8]! , wstępnie indeksowane przesunięcie >= -512 |
save_freg |
1101110x'xxzzzzzz: save reg d(8+#X) at [sp+#Z*8] , offset <= 504 |
save_freg_x |
11011110'xxxzzzzz: save reg d(8+#X) at [sp-(#Z+1)*8]! , wstępnie indeksowany przesunięcie >= -256 |
alloc_l |
11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx: przydziel duży stos o rozmiarze < 256M (2^24 * 16) |
set_fp |
11100001: konfigurowanie za pomocą x29 polecenia mov x29,sp |
add_fp |
11100010'xxxxxxxx: konfigurowanie za pomocą x29 polecenia add x29,sp,#x*8 |
nop |
11100011: nie jest wymagana żadna operacja odwijana. |
end |
11100100: zakończenie odwijanego kodu. Implikuje ret w epilogu. |
end_c |
11100101: koniec kodu odwijanego w bieżącym zakresie łańcuchowym. |
save_next |
11100110: zapisz następną nietrwałą parę rejestrów int lub FP. |
11100111: zarezerwowane | |
11101xxx: zarezerwowane dla niestandardowych przypadków stosu poniżej wygenerowane tylko dla procedur asm | |
11101000: Niestandardowy stos dla MSFT_OP_TRAP_FRAME |
|
11101001: niestandardowy stos dla MSFT_OP_MACHINE_FRAME |
|
11101010: niestandardowy stos dla MSFT_OP_CONTEXT |
|
11101011: niestandardowy stos dla MSFT_OP_EC_CONTEXT |
|
11101100: niestandardowy stos dla MSFT_OP_CLEAR_UNWOUND_TO_CALL |
|
11101101: zarezerwowane | |
11101110: zarezerwowane | |
11101111: zarezerwowane | |
11110xxx: zarezerwowane | |
11111000'yyy : zarezerwowane | |
11111001'yy'yyy : reserved | |
11111010'y'y : zarezerwowane | |
11111011'y'y'y'yy : reserved | |
pac_sign_lr |
11111100: podpisywanie adresu zwrotnego za lr pomocą polecenia pacibsp |
11111101: zarezerwowane | |
11111110: zarezerwowane | |
11111111: zarezerwowane |
W instrukcjach z dużymi wartościami obejmującymi wiele bajtów najważniejsze bity są najpierw przechowywane. Ten projekt umożliwia znalezienie całkowitego rozmiaru w bajtach kodu odwijanego, wyszukując tylko pierwszy bajt kodu. Ponieważ każdy kod odwijaczania jest dokładnie mapowany na instrukcję w prologu lub epilogu, można obliczyć rozmiar prologu lub epilogu. Przejdź od początku sekwencji do końca i użyj tabeli odnośników lub podobnego urządzenia, aby określić długość odpowiedniego kodu opcode.
Adresowanie przesunięcia po indeksowaniu nie jest dozwolone w prologu. Wszystkie zakresy przesunięcia (#Z) pasują do kodowania adresowania stp
/str
z wyjątkiem save_r19r20_x
, w którym 248 jest wystarczające dla wszystkich obszarów zapisywania (10 rejestrów int + 8 rejestrów FP + 8 rejestrów wejściowych).
save_next
musi postępować zgodnie z zapisem dla pary rejestrów nietrwałych Int lub FP: save_regp
, , save_regp_x
save_fregp
, save_fregp_x
, save_r19r20_x
lub innej save_next
. Zapisuje następną parę rejestrów w następnym 16-bajtowym miejscu w kolejności "dorastania". Element save_next
odnosi się do pierwszej pary rejestru FP, gdy następuje po save-next
niej, która oznacza ostatnią parę rejestru Int.
Ponieważ rozmiary zwykłych instrukcji powrotu i skoku są takie same, nie ma potrzeby oddzielnego end
kodu odwijanego w scenariuszach wywołania końcowego.
end_c
jest przeznaczony do obsługi nieciągliwych fragmentów funkcji na potrzeby optymalizacji. Element end_c
wskazujący koniec kodów odwijanych w bieżącym zakresie musi być zgodny z kolejną serią kodów odwijanych kończących się rzeczywistym end
. Kody odwijające między elementami end_c
i end
reprezentują operacje prologu w regionie nadrzędnym (prolog "phantom"). Więcej szczegółów i przykładów opisano w poniższej sekcji.
Spakowane dane odwijaj
W przypadku funkcji, których prologi i epilogi są zgodne z formularzem kanonicznym opisanym poniżej, można użyć spakowanych danych. Eliminuje to całkowite zapotrzebowanie na .xdata
rekord i znacznie zmniejsza koszt dostarczania danych. Kanoniczne prologi i epilogi zostały zaprojektowane tak, aby spełniały typowe wymagania prostej funkcji: jeden, który nie wymaga obsługi wyjątków, i który wykonuje operacje konfiguracji i usuwania w standardowej kolejności.
Format rekordu .pdata
z wypełnionymi danymi unwind wygląda następująco:
Pola są następujące:
- Funkcja Start RVA jest 32-bitowym RVA początku funkcji.
- Flaga jest polem 2-bitowym, jak opisano powyżej, z następującymi znaczeniami:
- 00 = nieużytkowane dane unwind; pozostałe bity wskazują
.xdata
rekord - 01 = spakowane dane unwind używane z pojedynczym prologem i epilogiem na początku i na końcu zakresu
- 10 = spakowane dane unwind używane do kodu bez żadnego prologu i epilogu. Przydatne do opisywania segmentów funkcji rozdzielonych
- 11 = zarezerwowane.
- 00 = nieużytkowane dane unwind; pozostałe bity wskazują
- Długość funkcji to pole 11-bitowe zapewniające długość całej funkcji w bajtach, podzielone przez 4. Jeśli funkcja jest większa niż 8 tys., należy zamiast tego użyć pełnego
.xdata
rekordu. - Rozmiar ramki to pole 9-bitowe wskazujące liczbę bajtów stosu przydzielonego dla tej funkcji, podzielone przez 16. Funkcje przydzielające więcej niż (8k-16) bajtów stosu muszą używać pełnego
.xdata
rekordu. Obejmuje on obszar zmiennych lokalnych, obszar parametrów wychodzących, wywoływany obszar Int i FP oraz obszar parametrów domowych. Wyklucza on dynamiczny obszar alokacji. - CR to flaga 2-bitowa wskazująca, czy funkcja zawiera dodatkowe instrukcje dotyczące konfigurowania łańcucha ramek i łącza zwrotnego:
- 00 = nieuwzględniana funkcja,
<x29,lr>
para nie jest zapisywana w stosie - 01 = funkcja nieuwzględniana,
<lr>
jest zapisywana w stosie - 10 = funkcja łańcuchowa z podpisanym adresem zwrotnym
pacibsp
- 11 = funkcja łańcuchowa, instrukcja pary magazynu/ładowania jest używana w prologu/epilogu
<x29,lr>
- 00 = nieuwzględniana funkcja,
- H jest flagą 1-bitową wskazującą, czy funkcja mieści rejestry parametrów całkowitych (x0-x7), przechowując je na samym początku funkcji. (0 = nie rejestruje domu, 1 = rejestry domów).
- RegI to pole 4-bitowe wskazujące liczbę nietrwałych rejestrów INT (x19-x28) zapisanych w lokalizacji stosu kanonicznego.
- RegF to pole 3-bitowe wskazujące liczbę nietrwałych rejestrów FP (d8-d15) zapisanych w lokalizacji stosu kanonicznego. (RegF=0: nie zapisano żadnego rejestru FP; RegF 0: Rejestry RegF>+1 FP są zapisywane). Spakowane dane unwind nie mogą być używane dla funkcji, która zapisuje tylko jeden rejestr FP.
Canonical prologs, które należą do kategorii 1, 2 (bez obszaru parametrów wychodzących), 3 i 4 w powyższej sekcji mogą być reprezentowane przez zapakowany format rozpakowania. Epilogi dla funkcji kanonicznych są zgodne z podobną formą, z wyjątkiem H nie ma efektu, set_fp
instrukcja zostanie pominięta, a kolejność kroków i instrukcje w każdym kroku są odwrócone w epilogu. Algorytm spakowany .xdata
wykonuje następujące kroki, szczegółowo w poniższej tabeli:
Krok 0. Wstępne obliczanie rozmiaru każdego obszaru.
Krok 1. Podpisywanie adresu zwrotnego.
Krok 2. Zapisz zapisane rejestry int wywoływane.
Krok 3. Ten krok jest specyficzny dla typu 4 we wczesnych sekcjach. lr
jest zapisywany na końcu obszaru Int.
Krok 4. Zapisz zapisane rejestry w formacie FP.
Krok 5. Zapisywanie argumentów wejściowych w obszarze parametrów domowych.
Krok 6. Przydziel pozostały stos, w tym obszar lokalny, <x29,lr>
parę i obszar parametrów wychodzących. 6a odpowiada typowi kanonicznym 1. 6b i 6c są przeznaczone dla typu kanonicznego 2. 6d i 6e są przeznaczone zarówno dla typu 3, jak i typu 4.
Krok # | Flaga wartości | Liczba instrukcji | Opcode | Odwij kod |
---|---|---|---|---|
0 | #intsz = RegI * 8; if (CR==01) #intsz += 8; // lr #fpsz = RegF * 8; if(RegF) #fpsz += 8; #savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf) #locsz = #famsz - #savsz |
|||
1 | CR == 10 | 1 | pacibsp |
pac_sign_lr |
2 | 0 <RegI<= 10 | RegI /2 + RegI % 2 |
stp x19,x20,[sp,#savsz]! stp x21,x22,[sp,#16] ... |
save_regp_x save_regp ... |
3 | CR == 01* | 1 | str lr,[sp,#(intsz-8)] * |
save_reg |
4 | 0 <RegF<= 7 | (RegF + 1) / 2 + (RegF + 1) % 2) |
stp d8,d9,[sp,#intsz] **stp d10,d11,[sp,#(intsz+16)] ... str d(8+RegF),[sp,#(intsz+fpsz-8)] |
save_fregp ... save_freg |
5 | H == 1 | 100 | stp x0,x1,[sp,#(intsz+fpsz)] stp x2,x3,[sp,#(intsz+fpsz+16)] stp x4,x5,[sp,#(intsz+fpsz+32)] stp x6,x7,[sp,#(intsz+fpsz+48)] |
nop nop nop nop |
6a | (CR == 10 || CR == 11) &&#locsz <= 512 |
2 | stp x29,lr,[sp,#-locsz]! mov x29,sp *** |
save_fplr_x set_fp |
6b | (CR == 10 || CR == 11) && 512 < #locsz <= 4080 |
3 | sub sp,sp,#locsz stp x29,lr,[sp,0] add x29,sp,0 |
alloc_m save_fplr set_fp |
6c | (CR == 10 || CR == 11) &&#locsz > 4080 |
100 | sub sp,sp,4080 sub sp,sp,#(locsz-4080) stp x29,lr,[sp,0] add x29,sp,0 |
alloc_m alloc_s /alloc_m save_fplr set_fp |
6d | (CR == 00 || CR == 01) &&#locsz <= 4080 |
1 | sub sp,sp,#locsz |
alloc_s /alloc_m |
6e | (CR == 00 || CR == 01) &&#locsz > 4080 |
2 | sub sp,sp,4080 sub sp,sp,#(locsz-4080) |
alloc_m alloc_s /alloc_m |
* Jeśli CR == 01 i RegI jest liczbą nieparzysta, krok 2 i ostatni save_rep
w kroku 1 są scalane w jeden save_regp
.
** Jeśli regI == CR == 0, i RegF != 0, pierwszy stp
dla zmiennoprzecinku wykonuje predektację.
Żadna instrukcja odpowiadająca mov x29,sp
nie jest obecna w epilogu. Spakowane dane nie mogą być używane, jeśli funkcja wymaga przywrócenia sp
z x29
klasy .
Odwijanie częściowych prologów i epilogów
W najbardziej typowych sytuacjach odwijania wyjątek lub wywołanie występuje w treści funkcji, z dala od prologu i wszystkich epilogów. W takich sytuacjach odwijanie jest proste: odwijanie po prostu wykonuje kody w tablicy odwijania. Rozpoczyna się od indeksu 0 i trwa do momentu end
wykrycia kodu opcode.
W przypadku, gdy wyjątek lub przerwanie występuje podczas wykonywania prologu lub epilogu, trudniej jest prawidłowo odwinąć się. W takich sytuacjach ramka stosu jest skonstruowana tylko częściowo. Problem polega na określeniu dokładnie tego, co zostało zrobione, aby prawidłowo go cofnąć.
Na przykład weźmy tę sekwencję prologu i epilogu:
0000: stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store)
0004: stp d8,d9,[sp,#224] // save_fregp 0, 224
0008: stp x19,x20,[sp,#240] // save_regp 0, 240
000c: mov x29,sp // set_fp
...
0100: mov sp,x29 // set_fp
0104: ldp x19,x20,[sp,#240] // save_regp 0, 240
0108: ldp d8,d9,[sp,224] // save_fregp 0, 224
010c: ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load)
0110: ret lr // end
Obok każdego kodu opcode znajduje się odpowiedni kod odwijania opisujący tę operację. Można zobaczyć, jak seria kodów odwijania dla prologu jest dokładnym lustrzanym obrazem kodów odwijania dla epilogu (nie licząc końcowej instrukcji epilogu). Jest to powszechna sytuacja: dlatego zawsze zakładamy, że kody odwijające dla prologu są przechowywane w odwrotnej kolejności od kolejności wykonywania prologu.
Tak więc zarówno dla prologu, jak i epilogu, pozostaje nam wspólny zestaw kodów odwijanych:
set_fp
, , save_regp 0,240
, save_fregp,0,224
, , save_fplr_x_256
end
Przypadek epilogu jest prosty, ponieważ jest w normalnej kolejności. Począwszy od przesunięcia 0 w epilogu (który rozpoczyna się od przesunięcia 0x100 w funkcji), spodziewalibyśmy się wykonania pełnej sekwencji odwijenia, ponieważ nie wykonano jeszcze oczyszczania. Jeśli znajdziemy się w jednej instrukcji (z przesunięciem 2 w epilogu), możemy pomyślnie odwinąć, pomijając pierwszy kod odwijania. Możemy uogólnić tę sytuację i założyć mapowanie 1:1 między kodami opcode i unwind codes. Następnie, aby rozpocząć odwijanie z instrukcji n w epilogu, powinniśmy pominąć pierwsze n unwind codes i rozpocząć wykonywanie z tego miejsca.
Okazuje się, że podobna logika działa dla prologu, z wyjątkiem odwrotnie. Jeśli rozpoczniemy odwijanie od przesunięcia 0 w prologu, chcemy wykonać nic. Jeśli odwiniemy przesunięcie 2, czyli jedną z instrukcji w pliku, chcemy rozpocząć wykonywanie sekwencji odwijania z końca. (Pamiętaj, że kody są przechowywane w odwrotnej kolejności). I tutaj też możemy uogólnić: jeśli zaczniemy odwijać się od instrukcji n w prologu, powinniśmy rozpocząć wykonywanie n odwijania kodów z końca listy kodów.
Kody prologu i epilogu nie zawsze są dokładnie zgodne, dlatego tablica unwind może wymagać kilku sekwencji kodów. Aby określić przesunięcie miejsca rozpoczęcia przetwarzania kodów, użyj następującej logiki:
W przypadku odwijania się z wewnątrz treści funkcji rozpocznij wykonywanie kodów odwijania w indeksie 0 i kontynuuj, aż osiągniesz
end
kod opcode.W przypadku odwijania się z poziomu epilogu użyj indeksu początkowego specyficznego dla epilogu dostarczonego z zakresem epilogu jako punktem wyjścia. Oblicz, ile bajtów komputera, o których mowa, od początku epilogu. Następnie przejdź do przodu przez kody odwijania, pomijając kody odwijania, aż zostaną uwzględnione wszystkie już wykonane instrukcje. Następnie wykonaj polecenie rozpoczynające się w tym momencie.
W przypadku odwijania z poziomu prologu użyj indeksu 0 jako punktu początkowego. Oblicz długość kodu prologu z sekwencji, a następnie oblicz, ile bajtów danego komputera pochodzi od końca prologu. Następnie przejdź do przodu przez kody odwijania, pomijając kody odwijania, dopóki nie zostaną uwzględnione wszystkie nieukończone instrukcje. Następnie wykonaj polecenie rozpoczynające się w tym momencie.
Te reguły oznaczają, że kody odwijające dla prologu muszą być zawsze pierwszymi w tablicy. I są to również kody używane do odwijania się w ogólnym przypadku odwijania się z wewnątrz ciała. Wszystkie sekwencje kodu specyficzne dla epilogu powinny być zgodne natychmiast po.
Fragmenty funkcji
Ze względów optymalizacji kodu i innych powodów warto podzielić funkcję na oddzielne fragmenty (nazywane również regionami). W przypadku dzielenia każdy wynikowy fragment funkcji wymaga własnego oddzielnego .pdata
rekordu (i ewentualnie .xdata
) .
Dla każdego oddzielonego fragmentu pomocniczego, który ma własny prolog, należy się spodziewać, że w prologu nie jest wykonywana korekta stosu. Wszystkie miejsca stosu wymagane przez region pomocniczy muszą być wstępnie przydzielone przez jego region nadrzędny (lub nazywany regionem hosta). Ta wstępna alokacja utrzymuje manipulowanie wskaźnikiem stosu ściśle w oryginalnym prologu funkcji.
Typowy przypadek fragmentów funkcji to "separacja kodu", gdzie kompilator może przenieść region kodu z funkcji hosta. Istnieją trzy nietypowe przypadki, które mogą wynikać z separacji kodu.
Przykład
(region 1: początek)
stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store) stp x19,x20,[sp,#240] // save_regp 0, 240 mov x29,sp // set_fp ...
(region 1: koniec)
(region 3: początek)
...
(region 3: koniec)
(region 2: początek)
... mov sp,x29 // set_fp ldp x19,x20,[sp,#240] // save_regp 0, 240 ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load) ret lr // end
(region 2: koniec)
Tylko prolog (region 1: wszystkie epilogi znajdują się w oddzielnych regionach):
Należy opisać tylko prolog. Ten prolog nie może być reprezentowany w formacie kompaktowym
.pdata
. W pełnym.xdata
przypadku może być reprezentowana przez ustawienie Epilog Count = 0. Zobacz region 1 w powyższym przykładzie.Odwij kody:
set_fp
, ,save_regp 0,240
save_fplr_x_256
,end
.Tylko epilogi (region 2: prolog znajduje się w regionie hosta)
Zakłada się, że przez przejście kontroli czasu do tego regionu wszystkie kody prologu zostały wykonane. Częściowe odwije się w epilogach tak samo jak w normalnej funkcji. Ten typ regionu nie może być reprezentowany przez kompaktowanie
.pdata
. W pełnym.xdata
rekordzie można go kodować za pomocą prologu "phantom", w nawiasach kwadratowych paryend_c
kodu iend
odwijać. Wiodącyend_c
wskazuje, że rozmiar prologu wynosi zero. Epilog rozpoczyna indeks pojedynczego epilogu wskazuje naset_fp
.Odwij kod dla regionu 2:
end_c
, ,save_regp 0,240
set_fp
,save_fplr_x_256
,end
.Brak prologów ani epilogów (region 3: prologi i wszystkie epilogi znajdują się w innych fragmentach):
Format kompaktowy
.pdata
można stosować za pomocą ustawienia Flaga = 10. Z pełnym.xdata
rekordem, Epilog Count = 1. Odwij kod jest taki sam jak kod dla regionu 2 powyżej, ale indeks startowy epilogu wskazuje również wartośćend_c
. Częściowe odwijnie nigdy nie nastąpi w tym regionie kodu.
Innym bardziej skomplikowanym przypadkiem fragmentów funkcji jest "kurczenie się". Kompilator może zdecydować się na opóźnienie zapisywania niektórych zarejestrowanych przez wywoływane rejestry do czasu spoza prologu wpisu funkcji.
(region 1: początek)
stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store) stp x19,x20,[sp,#240] // save_regp 0, 240 mov x29,sp // set_fp ...
(region 2: początek)
stp x21,x22,[sp,#224] // save_regp 2, 224 ... ldp x21,x22,[sp,#224] // save_regp 2, 224
(region 2: koniec)
... mov sp,x29 // set_fp ldp x19,x20,[sp,#240] // save_regp 0, 240 ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load) ret lr // end
(region 1: koniec)
W prologu regionu 1 miejsce na stos jest wstępnie przydzielone. Widać, że region 2 będzie miał ten sam kod odwijanego, nawet jeśli został przeniesiony z funkcji hosta.
Region 1: set_fp
, , save_regp 0,240
save_fplr_x_256
, end
. Epilog Start Index wskazuje set_fp
jak zwykle.
Region 2: save_regp 2, 224
, , end_c
set_fp
, save_regp 0,240
, save_fplr_x_256
, end
. Epilog Start Index wskazuje pierwszy odwij kod save_regp 2, 224
.
Duże funkcje
Fragmenty mogą służyć do opisywania funkcji większych niż limit 1M narzucony przez pola bitowe w nagłówku .xdata
. Aby opisać nietypowo dużą funkcję, należy ją podzielić na fragmenty mniejsze niż 1M. Każdy fragment należy dostosować tak, aby nie dzielił epilogu na wiele fragmentów.
Tylko pierwszy fragment funkcji będzie zawierać prolog; wszystkie inne fragmenty są oznaczone jako bez prologu. W zależności od liczby obecnych epilogów każdy fragment może zawierać zero lub więcej epilogów. Należy pamiętać, że każdy zakres epilogu w fragmentcie określa jego przesunięcie początkowe względem początku fragmentu, a nie początku funkcji.
Jeśli fragment nie ma prologu i nie epilog, nadal wymaga własnego .pdata
(i ewentualnie .xdata
) rekordu, aby opisać sposób odwijenia się z treści funkcji.
Przykłady
Przykład 1: oprawa łańcuchowa, kompaktowana
|Foo| PROC
|$LN19|
str x19,[sp,#-0x10]! // save_reg_x
sub sp,sp,#0x810 // alloc_m
stp fp,lr,[sp] // save_fplr
mov fp,sp // set_fp
// end of prolog
...
|$pdata$Foo|
DCD imagerel |$LN19|
DCD 0x416101ed
;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]
Przykład 2: Ramka z pełnym łańcuchem z dublowaniem Prolog i Epilog
|Bar| PROC
|$LN19|
stp x19,x20,[sp,#-0x10]! // save_regp_x
stp fp,lr,[sp,#-0x90]! // save_fplr_x
mov fp,sp // set_fp
// end of prolog
...
// begin of epilog, a mirror sequence of Prolog
mov sp,fp
ldp fp,lr,[sp],#0x90
ldp x19,x20,[sp],#0x10
ret lr
|$pdata$Bar|
DCD imagerel |$LN19|
DCD imagerel |$unwind$cse2|
|$unwind$Bar|
DCD 0x1040003d
DCD 0x1000038
DCD 0xe42291e1
DCD 0xe42291e1
;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
;Epilog Start Index[0], Epilog Start Offset[56]
;set_fp
;save_fplr_x
;save_r19r20_x
;end
Epilog Start Index [0] wskazuje tę samą sekwencję kodu odwijanego prologu.
Przykład 3: Variadic unchained, funkcja
|Delegate| PROC
|$LN4|
sub sp,sp,#0x50
stp x19,lr,[sp]
stp x0,x1,[sp,#0x10] // save incoming register to home area
stp x2,x3,[sp,#0x20] // ...
stp x4,x5,[sp,#0x30]
stp x6,x7,[sp,#0x40] // end of prolog
...
ldp x19,lr,[sp] // beginning of epilog
add sp,sp,#0x50
ret lr
AREA |.pdata|, PDATA
|$pdata$Delegate|
DCD imagerel |$LN4|
DCD imagerel |$unwind$Delegate|
AREA |.xdata|, DATA
|$unwind$Delegate|
DCD 0x18400012
DCD 0x200000f
DCD 0xe3e3e3e3
DCD 0xe40500d6
DCD 0xe40500d6
;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
;Epilog Start Index[4], Epilog Start Offset[15]
;nop // nop for saving in home area
;nop // ditto
;nop // ditto
;nop // ditto
;save_lrpair
;alloc_s
;end
Epilog Start Index [4] wskazuje środek kodu odwijanego Prolog (częściowo ponownie odwij tablicy).
Zobacz też
Opinia
https://aka.ms/ContentUserFeedback.
Dostępne już wkrótce: W 2024 r. będziemy stopniowo wycofywać zgłoszenia z serwisu GitHub jako mechanizm przesyłania opinii na temat zawartości i zastępować go nowym systemem opinii. Aby uzyskać więcej informacji, sprawdź:Prześlij i wyświetl opinię dla