Udostępnij za pośrednictwem


Obsługa wyjątków ARM64

Windows na ARM64 używa tego samego mechanizmu ustrukturyzowanej obsługi wyjątków dla asynchronicznie generowanych przez sprzęt wyjątków i synchronicznie generowanych przez oprogramowanie wyjątków. Menadżery wyjątków specyficznych dla języka opierają się na strukturalnej obsłudze wyjątków Windows przy użyciu funkcji pomocniczych języka. W tym dokumencie opisano obsługę wyjątków w Windows na platformie ARM64. Przedstawia pomocnicze mechanizmy językowe używane przez kod generowany przez asembler Microsoft ARM i kompilator MSVC.

Cele i motywacja

Zasady dotyczące odwijania wyjątków i danych oraz niniejszy opis mają na celu:

  • Podaj wystarczająco dużo opisu, aby umożliwić rozwijanie bez analizy kodu we wszystkich przypadkach.

    • Analizowanie kodu wymaga załadowania kodu do pamięci. 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ć instrukcje, które mechanizm odwijania może zdekodować.

    • Jeśli nie można w pełni opisać odwijania przy użyciu kodów odwijania, to w niektórych przypadkach konieczne jest powrócenie 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 Windows nie tylko do obsługi wyjątków. Bardzo ważne jest, aby kod mógł się dokładnie rozpakować nawet wtedy, gdy znajduje się w środku sekwencji prologu lub epilogu kodu.
  • Zajmuje minimalną ilość miejsca.

    • Kody odwijań nie mogą być agregowane tak, aby znacząco zwiększyć rozmiar binarnego pliku.

    • 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 na ogół są stosunkowo niewielkie. Kilka optymalizacji przestrzeni polega na wykorzystaniu tego faktu, aby osiągnąć najbardziej wydajne pakowanie danych.

  • Nie ma kodu warunkowego w epilogach.

  • Dedykowany rejestr wskaźnika ramek: jeśli rejestr jest zapisany w innym rejestrze w prologu, ten rejestr pozostaje nienaruszony w całej funkcji. Oznacza to, że oryginał może zostać odzyskany w dowolnym momencie.

  • Chyba że zostanie zapisany w innym rejestrze, wszystkie manipulacje wskaźnikiem stosu odbywają 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

Diagram przedstawiający układ ramki stosu dla funkcji.układ ramki stosu

W przypadku funkcji łańcuchowych ramek pary i można zapisać w dowolnym miejscu w obszarze zmiennej lokalnej, w zależności od zagadnień optymalizacji. Celem jest zmaksymalizowanie liczby lokalnych, do których można dotrzeć za pomocą jednej instrukcji, korzystając z wskaźnika ramki () lub wskaźnika stosu (). Jednak w przypadku funkcji musi być połączona w łańcuch i musi wskazywać na dół stosu. Aby umożliwić lepsze pokrycie trybu adresowania pary rejestrów, obszary zapisu rejestrów nieulotnych 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 rejestrów zachowywanych przez wywoływane funkcje we wszystkich kanonicznych prologach jest w kolejności rosnącej. poniżej reprezentuje rozmiar całego stosu (z wyłączeniem obszaru). i oznaczają odpowiednio rozmiar obszaru lokalnego (w tym obszar zapisywania dla pary) i rozmiar parametru wychodzącego.

  1. Ł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)
    
  2. Skuwane, #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
    
  3. Odwiązane, funkcje liścia ( 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 . wskazuje na poprzednią ramkę. W przypadku rozmiaru ramki = 512 można je zoptymalizować, jeśli zapisany obszar regs zostanie przeniesiony na dół stosu. Wadą jest to, że nie jest zgodny z innymi układami powyżej. Zapisane rejestry biorą udział w części zakresu dla rejestrów parowanych oraz w trybie adresowania z przesunięciem przed i po indeksowaniu.

  4. Funkcje niepowiązane, nie-arkuszowe (zapisuje w obszarze zapisu 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 parzystej liczbie zapisanych rejestrów 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
    

    Tylko zapisane.

        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 dla reg nie jest włączona do ponieważ wstępnie indeksowany reg-lr nie może być reprezentowany za pomocą kodów odwijających.

    Dostęp do wszystkich ustawień lokalnych jest uzyskiwany na podstawie elementu . wskazuje na poprzednią ramkę.

  5. Połączone, #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 powyższym przykładem prologu ten przykład ma przewagę: wszystkie instrukcje zapisu rejestru są gotowe do wykonania po zaledwie jednej instrukcji alokacji stosu. To oznacza, że nie ma antyzależności od , która uniemożliwia równoległość na poziomie instrukcji.

  6. Rozmiar ramki w łańcuchu 512 (opcjonalny dla funkcji bez )

        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, aby zapewnić lepsze pokrycie dla "reg-pair" i trybu adresowania przesunięcia z wstępnym/po indeksowanym adresowaniem. Dostęp do zmiennych lokalnych poniżej wskaźników ramek można uzyskać na podstawie .

  7. Połączone, 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

Rekordy

Rekordy są uporządkowaną tablicą elementów o stałej długości, które opisują każdą funkcję manipulowania stosem w pliku binarnym PE. Wyrażenie "manipulowanie stosem" jest znaczące: funkcje liściowe, które nie wymagają żadnego magazynu lokalnego i nie muszą zapisywać/przywracać rejestrów nieulotnych, nie wymagają rekordu . 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 , aby przejść do obiektu wywołującego.

Każdy rekord arm64 ma długość 8 bajtów. Ogólny format każdego rekordu umieszcza 32-bitową wartość RVA początku funkcji w pierwszym słowie, a następnie drugie słowo, które zawiera wskaźnik do bloku o zmiennej długości lub zapakowane słowo, opisujące kanoniczną sekwencję rozwijania funkcji.

Układ rekordu ".pdata".Układ rekordu ".pdata"

Pola są następujące:

  • Start RVA funkcji to 32-bitowy RVA początku funkcji.

  • Flaga to pole 2-bitowe, które wskazuje, jak interpretować pozostałe 30 bitów drugiego słowa. Jeśli Flag to 0, wtedy pozostałe bity tworzą Exception Information RVA (z dwoma najniższymi bitami niejawnie równymi 0). Jeśli flaga ma wartość inną niż zero, pozostałe bity tworzą strukturę spakowanych danych odwijania.

  • Informacje o wyjątku RVA to adres struktury informacji o wyjątkach o zmiennej długości, przechowywanej w sekcji . Te dane muszą być wyrównane do granicy 4 bajtów.

  • Pakowane dane odmikiwania to skompresowany opis operacji potrzebnych do odmikiwania z funkcji, przy założeniu formy kanonicznej. W tym przypadku nie jest wymagany żaden rekord .

Rekordy

Gdy spakowany format rozwijania jest niewystarczający do opisania procesu rozwijania funkcji, należy utworzyć rekord o zmiennej długości . Adres tego rekordu jest przechowywany w drugim słowie rekordu . Format obiektu to spakowany zestaw wyrazów o zmiennej długości:

Układ rekordu .xdata.Układ rekordu xdata

Te dane są podzielone na cztery sekcje:

  1. 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 rekordów, 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 części . 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 :

    1. Jeśli wartość E wynosi 0, oznacza ono liczbę całkowitą zakresów epilogu, które są opisane w sekcji 2. Jeśli w funkcji istnieje więcej niż 31 zakresów, pole Kod wyrazów należy ustawić na 0, aby wskazać, że jest wymagane słowo rozszerzenia.

    2. Jeśli E to 1, to pole określa indeks pierwszego kodu odwrotnego rozwijania, który opisuje jedyny epilog.

    f. Kodowe słowa to pole 5-bitowe, które określa liczbę 32-bitowych wyrazów potrzebnych do przechowywania wszystkich kodów rozwijania 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 kodowe w pierwszym słowie nagłówka mają wartość 0.

  2. Jeśli liczba epilogów nie jest zerowa, po nagłówku i opcjonalnym rozszerzonym nagłówku pojawia się lista informacji o zakresach epilogu, przy czym każda jest spakowana do jednego słowa. Są one przechowywane w kolejności zwiększania przesunięcia początkowego. Każdy zakres zawiera następujące bity:

    a. Epilog Start Offset to 18-bitowe pole określające przesunięcie epilogu względem początku funkcji, wyrażone w bajtach i podzielone przez 4.

    b. Res to 4-bitowe pole zarezerwowane do przyszłego rozszerzenia. Jego wartość musi być 0.

    c. Epilog Start Index jest polem 10-bitowym (2 bity więcej niż Extended Code Words). Wskazuje indeks bajtu pierwszego kodu rozwijania, który opisuje ten epilog.

  3. Po liście zakresów epilogu znajduje się tablica bajtów zawierająca kody odwijania, szczegółowo opisane w dalszej sekcji. Ta tablica jest dopełniona na końcu najbliższej pełnej granicy słowa. Kody odwijania 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 odwijania są przechowywane w kolejności big-endian, więc najważniejszy bajt jest pobierany pierwszy, co identyfikuje operację oraz określa długość pozostałej części kodu.

  4. 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 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 mają swoje własne indeksy do kodów odwijania, tabela jest współdzielona przez nie. 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.

Rozwiąż kody

Tablica kodów odwracania jest zestawem 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 odwijania można uważać za 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 . Wszystkie rejestry nietrwałe są przywracane do ich wartości w momencie wywołania funkcji.

Gdyby wyjątki były gwarantowane wyłącznie w ciele funkcji i nigdy w prologu ani epilogu, konieczna byłaby tylko jedna sekwencja. Jednak model odwijania Windows wymaga, aby kod mógł się odwinąć z poziomu częściowo wykonanego prologu lub epilogu. Aby spełnić to wymaganie, kody cofania zostały starannie zaprojektowane tak, aby jednoznacznie mapowały 1:1 na każdy odpowiedni kod operacji 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 zakończyć częściowe odwijanie realizowane 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 zostały już wykonane.

Instrukcje odwijań są kodowane zgodnie z poniższą tabelą. Wszystkie kody rozwijania są pojedynczymi lub podwójnymi bajtami, z wyjątkiem tego, który przydziela ogromny stos (). Łą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 parę na , wstępnie indeksowane przesunięcie = -248
save_fplr 01zzzzzz: zapisz parę w , przesunięcie wynosi = 504.
save_fplr_x 10zzzzzz: zapisz parę na , offset wcześniej zindeksowany = -512
alloc_m 11000xxx'xxxxxxxx: przydziel duży stos o rozmiarze 32K (2^11 * 16).
save_regp 110010xx'xxzzzzzz: zapisz parę w , offset = 504
save_regp_x 110011xx'xxzzzzzz: zapisz parę z wstępnie indeksowanym przesunięciem , = -512
save_reg 110100xx'xxzzzzzz: zapisz rejestr przy , offset = 504
save_reg_x 1101010x'xxxzzzzz: zapisz rejestr w , wstępnie indeksowane przesunięcie = -256
save_lrpair 1101011x'xxzzzzzz: zapisz parę przy , przesunięcie = 504
save_fregp 1101100x'xxzzzzzz: zapisz parę w , offset = 504
save_fregp_x 1101101x'xxzzzzzz: zapisz parę na , przesunięcie wstępnie indeksowane = -512
save_freg 1101110x'xxzzzzzz: zapisz rejestr przy , przesunięcie = 504
save_freg_x 11011110'xxxzzzzz: save reg at , wstępnie indeksowane przesunięcie = -256
alloc_z 11011111'zzzzzzzz: przydziel stos o rozmiarze
alloc_l 11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx: przydziel duży stos o rozmiarze 256M (2^24 * 16)
set_fp 11100001: skonfiguruj z
add_fp 11100010'xxxxxxxx: skonfigurowanie z użyciem
nop 11100011: nie jest wymagana żadna operacja cofania.
end 11100100: zakończenie kodu odwijania. Oznacza w epilogu.
end_c 11100101: koniec kodu odwijania w bieżącym zakresie powiązań.
save_next 11100110: zapisz następną parę rejestrów.
save_any_xreg 11100111'0pxrrrrr'00oooooo: zapisz rejestr(y)
  • : 0/1 = pojedyncza vs para
  • : 0/1 = dodatnie vs ujemne wstępnie indeksowane przesunięcie stosu
  • : przesunięcie = * 16, jeśli x=1 lub p=1, w przeciwnym razie * 8
(Windows 11 >= wymagane)
save_any_dreg 11100111'0pxrrrrr'01oooooo: zapisz rejestry
  • : 0/1 = pojedyncza vs para
  • : 0/1 = dodatnie vs ujemne wstępnie indeksowane przesunięcie stosu
  • : przesunięcie = * 16, jeśli x=1 lub p=1, w przeciwnym razie * 8
(Windows >= 11 wymagany)
save_any_qreg 11100111'0pxrrrrr'10oooooo: zapisz rejestry
  • : 0/1 = pojedyncza vs para
  • : 0/1 = dodatnie vs ujemne wstępnie indeksowane przesunięcie stosu
  • : przesunięcie wynosi * 16
(Windows 11 >= wymagane)
save_zreg 11100111'0oo0rrrr'11oooooo: zapisz reg w , ( przez )
save_preg 11100111'0oo1rrrr'11oooooo: zapisz reg w , ( do ; wartości są zarezerwowane)
11100111'1yyyyyy': zarezerwowane
11101xxx: zarezerwowane dla niestandardowych przypadków stosu poniżej wygenerowane tylko dla procedur asm
11101000: Niestandardowy stos dla
11101001: niestandardowy stos dla
11101010: niestandardowy stos dla
11101011: niestandardowy stos dla
11101100: niestandardowy stos dla
11101101: zarezerwowane
11101110: zarezerwowane
11101111: zarezerwowane
11110xxx: zarezerwowane
11111000'yyyyyyyy : zarezerwowane
11111001'yyyyyyyy'yyyyyyyy : zarezerwowane
11111010'yyyyyyyy'yyyyyyyy'yyyyyyyy' : zarezerwowane
11111011'yyyyyyyy'yyyyyyyy'yyyyyyyy'yyyyyyyy : zarezerwowane
pac_sign_lr 11111100: podpisz adres zwrotny w za pomocą
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 odwijania dokładnie odpowiada instrukcji w prologu lub epilogu, możesz obliczyć rozmiar prologu lub epilogu. Przejdź od początku sekwencji do końca i użyj tabeli wyszukiwania lub podobnego mechanizmu, aby określić długość odpowiedniego kodu operacyjnego.

Adresowanie przesunięcia po indeksowaniu nie jest dozwolone w prologu. Wszystkie zakresy przesunięcia (#Z) pasują do kodowania adresowania z wyjątkiem , w przypadku którego 248 jest wystarczające dla wszystkich obszarów zapisu (10 rejestrów Int + 8 rejestrów FP + 8 rejestrów wejściowych).

musi następować po zapisie dla pary rejestrów: , , , , lub innej . Można go również używać w połączeniu z , lub tylko wtedy, gdy . Zapisuje następną parę rejestrów w kolejności liczbowej rosnącej do kolejnego miejsca w stosie. nie może być używany poza ostatnim rejestrem tego samego typu.

Ponieważ rozmiary zwykłych instrukcji powrotu i skoku są takie same, nie ma potrzeby stosowania oddzielnego kodu odwijania w sytuacjach wywołań ogonowych.

jest przeznaczony do obsługi nieciągliwych fragmentów funkcji na potrzeby optymalizacji. Element wskazujący koniec kodów odwijanych w bieżącym zakresie musi być następowany przez kolejną serię kodów odwijanych, które kończą się rzeczywistym . Kody odwijające między elementami i 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 formą kanoniczną opisaną poniżej, można użyć spakowanych danych. Eliminuje to całkowitą konieczność posiadania rekordu i znacznie zmniejsza koszt dostarczania danych do odwijań. Kanoniczne prologi i epilogi zostały zaprojektowane tak, aby spełniały typowe wymagania prostej funkcji: takiej, która nie wymaga obsługi wyjątków i wykonuje operacje konfiguracji i usuwania w standardowej kolejności.

Format rekordu z wypełnionymi danymi unwind wygląda następująco:

Zapis pdata z zapakowanymi danymi odwróconymi.Zapis pdata z zapakowanymi danymi odwróconymi

Pola są następujące:

  • Start RVA funkcji to 32-bitowy RVA początku funkcji.
  • Flaga jest polem 2-bitowym, jak opisano powyżej, z następującymi znaczeniami:
    • 00 = niewykorzystane dane do odwijania; pozostałe bity wskazują na rekord
    • 01 = spakowane dane odwijania używane z pojedynczym prologiem i epilogiem na początku i 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.
  • 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 rekordu.
  • Rozmiar ramki to pole 9-bitowe wskazujące liczbę bajtów stosu przydzielonego dla tej funkcji, podzielone przez 16. Funkcje, które przydzielają więcej niż (8k-16) bajtów na stosie, muszą korzystać z pełnego 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 = niezwiązana funkcja, para nie jest zapisywana w stosie
    • 01 = funkcja niezależna, jest zapisywana na stosie
    • 10 = funkcja łańcuchowa z podpisanym adresem zwrotnym
    • 11 = funkcja łańcuchowa, instrukcja pary zapisu/odczytu jest używana w prologu/epilogu
  • 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.

Kanoniczne prologi, które należą do kategorii 1, 2 (bez obszaru parametrów wychodzących), 3 i 4 w powyższej sekcji, mogą być reprezentowane przez skompresowany format odwijania. Epilogi dla funkcji kanonicznych są zgodne z podobną formą, z wyjątkiem tego, że H nie ma efektu, pomija się instrukcję , a w epilogu kroki oraz instrukcje w każdym z kroków są odwrócone. Algorytm opakowania odbywa się zgodnie z poniższymi krokami, które są szczegółowo opisane 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 w początkowych sekcjach. jest zapisywany na końcu obszaru Int.

Krok 4: Zapisz rejestry zachowywane przez funkcję FP.

Krok 5. Zapisywanie argumentów wejściowych w obszarze parametrów domowych.

Krok 6: Przydziel pozostały stos, w tym lokalny obszar, parę i obszar wychodzących parametrów. 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 Kod operacyjny 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 4 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) &&
= 512
2 stp x29,lr,[sp,#-locsz]!
mov x29,sp***
save_fplr_x
set_fp
6b (CR == 10 || CR == 11) &&
512 = 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) &&
4080
4 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
6 dni (CR == 00 || CR == 01) &&
= 4080
1 sub sp,sp,#locsz alloc_s/alloc_m
6e (CR == 00 || CR == 01) &&
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 3 i ostatni w kroku 2 są scalane w jeden .

** Jeśli RegI == 0, CR != 01 i RegF != 0, pierwsza operacja dla zmiennoprzecinka wykonuje przedecrementację, aby dostosować sp w celu przydzielenia miejsca dla obszaru zapisu FP/SIMD.

Żadna instrukcja odpowiadająca nie jest obecna w epilogu. Spakowane dane nie mogą być używane, jeśli funkcja wymaga przywrócenia z .

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: odwijacz po prostu wykonuje kody w tablicy odwijania. Rozpoczyna się od indeksu 0 i trwa aż do 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 rama stosu jest zbudowana 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 rozwijania dla prologu są przechowywane w odwrotnej kolejności względem kolejności wykonywania prologu.

Tak więc zarówno dla prologu, jak i epilogu, pozostaje wspólny zestaw kodów rozwijania:

, , , , ,

Przypadek epilogowy 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ć istnienie mapowania 1:1 między kodami operacyjnymi a kodami odwijania. Następnie, aby rozpocząć odwijanie z instrukcji n w epilogu, powinniśmy pominąć pierwsze n kody odwijania i rozpocząć wykonywanie od tego miejsca.

Okazuje się, że podobna logika działa dla prologu, ale działa odwrotnie. Jeśli rozpoczniemy cofanie od przesunięcia 0 w prologu, chcemy nie wykonywać niczego. 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:

  1. W przypadku odwijania się z wewnątrz treści funkcji rozpocznij wykonywanie kodów odwijania w indeksie 0 i kontynuuj, aż osiągniesz kod opcode.

  2. 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 dzieli omawiany komputer 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 od tego punktu.

  3. Aby odwinąć się z 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ź przez kody odwijania, pomijając je, dopóki nie zostaną uwzględnione wszystkie instrukcje, które nie zostały jeszcze wykonane. Następnie wykonaj od tego punktu.

Te zasady oznaczają, że kody rozwijania 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. Wszelkie sekwencje kodu specyficzne dla epilogu powinny nastąpić 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 swojego własnego, oddzielnego rekordu (a być może również ).

Dla każdego oddzielonego fragmentu pomocniczego, który ma własny prolog, oczekuje się, że w jego prologu korekta stosu nie jest wykonywana. 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 poza funkcję-gospodarza. 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)

  1. 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 . W pełnym przypadku można to przedstawić, ustawiając Epilog Count = 0. Zobacz region 1 w powyższym przykładzie.

    Rozwiń kody: , , , .

  2. Tylko epilogi (obszar 2: prolog znajduje się w obszarze hosta)

    Zakłada się, że gdy sterowanie przechodzi do tej sekcji, wszystkie kody prologu są już wykonane. Częściowe rozwinięcie może wystąpić w epilogach w ten sam sposób, jak w normalnej funkcji. Ten typ regionu nie może być reprezentowany przez kompaktowanie . W pełnym rekordzie można go kodować, używając "phantom" prologu, w ramach pary kodów odwijania i . Wiodący wskazuje, że rozmiar prologu wynosi zero. Indeks początku pojedynczego epilogu wskazuje na .

    Odwij kod dla regionu 2: , , , , .

  3. Brak prologów ani epilogów (region 3: prologi i wszystkie epilogi znajdują się w innych fragmentach):

    Format kompaktowy można stosować za pomocą ustawienia Flaga = 10. Mając pełny zapis, liczba epilogów = 1. Kod odwija jest taki sam jak kod dla regionu 2 powyżej, ale indeks początkowy epilogu wskazuje również na . Częściowe odwinięcie nigdy nie nastąpi w tym obszarze kodu.

Innym bardziej skomplikowanym przypadkiem fragmentów funkcji jest "shrink-wrapping." Kompilator może zdecydować się na opóźnienie zapisywania niektórych rejestrów zachowywanych przez wywoływaną funkcję do momentu poza prologiem 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 przestrzeń stosu jest wstępnie przydzielona. Widać, że region 2 będzie miał ten sam kod rozwijania, nawet jeśli zostanie przeniesiony z funkcji macierzystej.

Region 1: , , , . Epilog Start Index wskazuje jak zwykle.

Region 2: , , , , , . Epilog Start Index wskazuje pierwszy kod cofania .

Duże funkcje

Fragmenty mogą służyć do opisywania funkcji większych niż limit 1M narzucony przez pola bitowe w nagłówku . 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 (i ewentualnie ) rekordu, aby opisać sposób odwijenia się z treści funkcji.

Przykłady

Przykład 1: połączone w ramki, forma kompaktowa

|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: Łańcuchowa ramka, forma pełna z lustrzanym prologiem i epilogiem

|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: Funkcja zmiennej liczby argumentów bez łańcuchowania

|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 centrum kodu odwijania dla Prologu (częściowo ponownie użyj tablicy odwijania).

Zobacz też

Omówienie konwencji ABI arm64
Obsługa wyjątków ARM