Udostępnij za pośrednictwem


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

Diagram that shows the stack frame layout for functions.

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.

  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. Ł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
    
  3. 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.

  4. 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-lr stp 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ę.

  5. Ł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.

  6. 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ć na sppodstawie elementu .

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

.pdata record layout.

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:

.xdata record layout.

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 .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 .xdatawersji . 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, 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.

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

  2. 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ę.

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

  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 .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_nextmusi postępować zgodnie z zapisem dla pary rejestrów nietrwałych Int lub FP: save_regp, , save_regp_xsave_fregp, save_fregp_x, save_r19r20_xlub 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:

.pdata record with packed unwind data.

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.
  • 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>
  • 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 x29klasy .

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_256end

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:

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

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

  3. 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)

  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 .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,240save_fplr_x_256, end.

  2. 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 pary end_c kodu i end odwijać. Wiodący end_c wskazuje, że rozmiar prologu wynosi zero. Epilog rozpoczyna indeks pojedynczego epilogu wskazuje na set_fp.

    Odwij kod dla regionu 2: end_c, , save_regp 0,240set_fp, save_fplr_x_256, end.

  3. 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,240save_fplr_x_256, end. Epilog Start Index wskazuje set_fp jak zwykle.

Region 2: save_regp 2, 224, , end_cset_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ż

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