• Nie Znaleziono Wyników

Algorytm brudnych zasobów

5. Mechanizmy wykrywania i obsługi bł˛edów

5.6. Procedury naprawcze

5.6.2. Algorytm brudnych zasobów

Główn ˛a cech ˛a metody obsługi przerwa´n jest naprawa wył ˛acznie bł˛edów, które spowodowały wywołanie przerwania procesora ju˙z przy pierwszym wykonaniu zaburzonej instrukcji. Nie pokrywa ona przypadków, kiedy wprowadzony bł ˛ad skutkuje awari ˛a po wykonaniu kilku instrukcji od aktywacji.

W celu zbadania takich przypadków moduły opisane w sekcji 5.6.1 zostały wyposa˙zone w funkcj˛e zrzutu obrazu kodu zaburzonej funkcji7i obrazu jej kopii zapasowej. Uzyskane obrazy zostały poddane deasemblacji8i poddane analizie – wszystkie zamieszczone w niniejszej sekcji przykłady kodu assembler zostały uzyskane w wyniku przeprowadzonych eksperymentów.

Na listingu 5.3 zamieszczony jest przykładowy zapis porównuj ˛acy zaburzony zestaw instrukcji z oryginalnym. Zapis ten składa si˛e z kolejnych instrukcji procesora w j˛ezyku AT&T Assembly Language dla architektury x869. W liniach 1-4 przedstawione s ˛a instrukcje, które były wspólne dla obu obrazów kodu przed zaburzeniem. W liniach 5-7 po lewej stronie znaku

„|” zapisane s ˛a instrukcje zaburzonego kodu, natomiast po prawej kodu oryginalnego. Linie 8-10 zawieraj ˛a instrukcje, które były wspólne dla obu obrazów, a wyst˛epuj ˛a po zaburzonym fragmencie kodu. Znakiem „>” w linii 6 oznaczona jest instrukcja, która spowodowała zgłoszenie przerwania.

W przypadku zaburzenia kodu przedstawionego na listingu 5.3 zmieniony był 1 bit.

Instrukcje i ich argumenty w architekturze x86 kodowane s ˛a ci ˛agami bajtów o zmiennej

7 Funkcja rozumiana jest tutaj jako symbol wymieniony w pliku /proc/kallsyms

8 Proces przekształcenia kodu binarnego na odpowiadaj ˛acy mu zestaw instrukcji j˛ezyka assembler.

9 http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

1 mov %edx,%ecx

2 mov %ebx,%eax

3 mov %esi,%edx

4 call *(%edi)

5 mov (%ebx),%edx | mov 0x20(%ebx),%edx

6 > and %dh,(%ecx) | xor %eax,%eax

7 rorb $0x1,-0x3d09f3b2(%ebx) | mov 0xc(%esi),%ecx

8 je 0x53

9 test %ecx,%ecx

10 mov $0x41,%edi

Listing 5.3: Deasemblacja instrukcji zaburzonej i oryginalnej

długo´sci, co spowodowało, ˙ze zaburzenie argumentu funkcji mov w linii 5 wpłyn˛eło na zmian˛e znaczenia kolejnych instrukcji w liniach 6-7. Z analizy zamieszczonego przykładu wynika,

˙ze tu˙z przed zgłoszeniem przerwania procesor wykonał tylko zaburzon ˛a instrukcj˛e mov z linii 5. W wyniku jej działania do rejestru %edx została załadowana warto´s´c pami˛eci spod adresu (%ebx)zamiast 0x20(%ebx). Napraw ˛a takiej sytuacji byłoby przywrócenie w procedurze obsługi przerwa´n oryginalnego obrazu kodu oraz cofni˛ecie licznika instrukcji procesora do instrukcji mov. Wynika to z faktu, ˙ze jedynym zmodyfikowanym rejestrem w zaburzonym wykonaniu był rejestr %edx. Rejestr ten przy wykonaniu oryginalnego kodu zostałby na nowo zapisany prawidłow ˛a warto´sci ˛a.

Zaproponowana procedura naprawcza jest problemem odtworzenia prawidłowego stanu wykonania zadania. Opracowana została formalna definicja tego problemu:

Definicja 5.6.1. Stan wykonania Si jest to kombinacja stanu pami˛eci Mi i stanu rejestrów procesora Ri przed wykonaniem i-tej instrukcji przez procesor P . Przebiegiem wykonania procesora P jest uporz ˛adkowany zbiór stanówS = (S0, S1, ..., Sn). Stanem równowa˙znym Wi stanowiSijest kombinacja stanu pami˛eciMi0i stanu rejestrów procesoraR0i, gdzie w przebiegu wykonaniaS stan Si mo˙ze by´c zast ˛apiony stanem Wi, a zasoby nale˙z ˛ace doWi maj ˛ace inne warto´sci ni˙z ich odpowiedniki nale˙z ˛ace doSiwS nie s ˛a odczytywane lub s ˛a nadpisane nowymi warto´sciami.

Zaburzonym przebiegiem wykonania jest uporz ˛adkowany zbiór stanów Sf = (S0, S1, ..., Si−1, Qi, Qi+1, ..., Qn) gdzie stany Sj, (0 ≤ j < i) s ˛a stanami uzyskanymi poprzez wykonanie niezaburzonych instrukcji, natomiast stany Qj, (i ≤ j ≤ n) s ˛a stanami uzyskanymi poprzez wykonanie zaburzonych instrukcji. Problem odtwarzania jest to znalezienie transformacjiT (Qn, Sk) stanu Qndo stanu równowa˙znego stanowiSk, gdzie0 ≤ k < i.

Wynikiem przeprowadzenia tak zdefiniowanej operacji odtwarzania jest przepływ wykonania składaj ˛acy si˛e ze stanów S = (S0, S1, ..., Si−1, Wi, Wi+i, ..., Wj−1, Sj, Sj+1, ..., Sn. Transformacja stanu Qn do Sk mo˙ze składa´c si˛e z nast˛epuj ˛acych operacji: zmiana warto´sci

licznika instrukcji procesora, zapis zawarto´sci rejestrów oraz pami˛eci, wykonanie operacji arytmetycznych.

Zdefiniowany problem odtwarzania jest pokrewny problemowi wstecznego wykonania znanego z literatury. Wsteczne wykonanie polega na mo˙zliwo´sci odtworzenia ka˙zdego stanu poprzedzaj ˛acego stan Si w celu inspekcji stanu pami˛eci w poszukiwaniu bł˛edów w implementacji. Jednak standardowa architektura systemów komputerowych nie wspiera takiego zastosowania. Istnieje wiele propozycji implementacji systemów pozwalaj ˛acych na wsteczne wykonanie – np. programistyczne (patrz [4, 74, 22]) lub wykorzystuj ˛ace maszyny wirtualne (patrz [108]).

Kluczowym problemem przy wstecznym wykonaniu s ˛a instrukcje destrukcyjne, które powoduj ˛a niemo˙zno´s´c odtworzenia poprzedniego stanu. Instrukcje te modyfikuj ˛a stan pami˛eci lub rejestrów w ten sposób, ˙ze nie jest mo˙zliwe zastosowanie instrukcji odwrotnej pozwalaj ˛acej cofn ˛a´c efekt wykonania pierwotnej instrukcji. Instrukcje, dla których istniej ˛a instrukcje niweluj ˛ace ich efekt, s ˛a nazywane instrukcjami odwracalnymi. Przykładem instrukcji destrukcyjnej jest funkcja mov, która nadpisuje adres lub rejestr pami˛eci. Instrukcj ˛a odwracaln ˛a jest instrukcja add, dla której instrukcj ˛a odwrotn ˛a jest instrukcja sub. Warto równie˙z zaznaczy´c, ˙ze instrukcje architektury x86 mog ˛a by´c destrukcyjne lub odwracalne w zale˙zno´sci od argumentów. Przykładem jest instrukcja xor, która dla wywołania xor %eax, %eax10 jest destrukcyjna, natomiast przy wywołaniu xor %ebx, %eax jest odwracalna11. Wsteczne wykonanie w celu umo˙zliwienia cofni˛ecia efektu działania instrukcji destrukcyjnej najcz˛e´sciej stosuje metod˛e utworzenia kopii nadpisywanych danych.

W przypadku problemu odtwarzania zastosowanie wstecznego wykonania do wyznaczenia transformacji T (Qn, Sk) jest niemo˙zliwe. Wynika to z nast˛epuj ˛acych przyczyn:

— wsteczne wykonanie w rozwi ˛azaniach programistycznych polega na instrumentacji kodu, a w przypadku wykonania zaburzonego kodu dane zawarte w instrumentacji byłyby nieaktualne,

— wsteczne wykonanie oparte o maszyn˛e wirtualn ˛a wymaga zastosowania systemu gospodarza, który ju˙z nie podlegałby ochronie.

Konieczne jest wi˛ec opracowanie nowej metody uzyskania transformacji T (Qn, Sk).

Przypadek zaburzenia kodu, który mo˙ze by´c odtworzony poprzez wykonanie prawidłowego kodu został poprzednio przedstawiony na listingu 5.3. Natomiast przykład mo˙zliwo´sci odtworzenia zasobów na podstawie stanu pami˛eci i rejestrów procesora w chwili zgłoszenia przerwania przedstawiony jest na listingu 5.4. Jest to szczególnie ciekawy przypadek, poniewa˙z odtworzenie warto´sci zapisanej w rejestrze %edx mo˙zliwe jest na dwa sposoby: poprzez ponowne wykonanie instrukcji z linii 2 lub skopiowanie zawarto´sci %eax do rejestru %edx.

10 Instrukcja taka jest generowana przez kompilatory jako sposób wyzerowania warto´sci rejestru ze wzgl˛edu na krótszy wygenerowany kod binarny ni˙z instrukcja mov $0,%eax.

11 Instrukcj ˛a niweluj ˛ac ˛a jej działanie jest ona sama ze wzgl˛edu na wła´sciwo´sci działania xor.

Przykład nierozwi ˛azywalnego zaburzenia kodu przedstawiony jest na listingu 5.5, gdzie w linii 2 stracona jest bezpowrotnie zawarto´s´c rejestru %eax potrzebna do wykonania prawidłowego kodu.

Poni˙zej przedstawiony jest algorytm brudnych zasobów – podstaw ˛a jego działania jest stwierdzenie, ˙ze mo˙zliwe jest przywrócenie stanu Si, o ile zasoby zapisane podczas wykonania zaburzonych instrukcji zostan ˛a nadpisane podczas wykonania prawidłowego kodu, lub mog ˛a by´c odtworzone na podstawie stanu pami˛eci w chwili zgłoszenia przerwania. Algorytm ten potrafi wyliczy´c rozwi ˛azania dla przykładu z listingu 5.3 oraz przykładu z listingu 5.4 (w wersji wykorzystuj ˛acej zmian˛e warto´sci licznika instrukcji).

1 mov %eax,%edx

2 xor %eax,%edx | xor %eax,%eax

3 > testb $0x2,(%edx)

4 jne 0x1a

Listing 5.4: Deasemblacja instrukcji zaburzonej i oryginalnej

1 mov 0x50(%eax),%eax

2 mov 0x40194(%eax),%eax | mov 0x194(%eax),%eax

3 > movzwl 0x260(%eax),%esi

Listing 5.5: Deasemblacja instrukcji zaburzonej i oryginalnej

Zaprojektowanie algorytmu rozwi ˛azuj ˛acego problem odtwarzalno´sci wymaga bazy wiedzy o instrukcjach procesora. Potrzebne s ˛a informacje o tym, jakie zasoby s ˛a przez instrukcje modyfikowane. Obecna baza instrukcji została skonstruowana z instrukcji najcz˛e´sciej wyst˛epuj ˛acych w instrukcjach wykonanych po zaburzonej instrukcji12. Składa si˛e ona z nast˛epuj ˛acych instrukcji: mov, lea, add, sub, xor, or, and, test, cmp. Algorytm brudnych zasobów przedstawiony jest na stronie 136. Zasada działania algorytmu jest nast˛epuj ˛aca: skanowany jest zbiór zaburzonych instrukcji w zakresie od pierwszej zaburzonej instrukcji do instrukcji, która zgłosiła przerwanie i wyznaczany jest zbiór zasobów, które zostały zapisane w wyniku wykonania tego kodu (linie 6-12) – zbiór ten nazwany jest zbiorem brudnych zasobów. W przypadku napotkania instrukcji, której działanie nie jest opisane w bazie instrukcji, działanie algorytmu jest przerywane (warunek w linii 7). Nast˛epnym etapem jest zbadanie, czy w przypadku wykonania niezaburzonych instrukcji, brudne zasoby nie zostan ˛a nadpisane nowymi warto´sciami z wykorzystaniem niezabrudzonych zasobów (linie 15-20) – je˙zeli tak, to taki zasób jest usuwany ze zbioru brudnych zasobów (linia 17). Skanowanie jest przeprowadzane tak długo jak analizowane s ˛a instrukcje dost˛epne w bazie instrukcji.

12 Zbiór instrukcji wyznaczono na podstawie zrzutów zaburzonego kodu dost˛epnych w dziennikach eksperymentów przeprowadzonych w 5.6.1.

Algorytm 5.1 Algorytm brudnych zasobów

Input: beforeCode, faultCode, originalCode, failInstr Output: transform

1: if failInstr 6∈ faultCode then

2: return ∅;

3: end if

4: dirtyResources ← ∅, remainingResources ← ∅;

5: ptrInstr ← head(faultCode);

6: while ptrInstr ∈ faultCode do

7: if is_invalid(ptrInstr) then

8: return ∅;

9: end if

10: add(dirtyResources, written_resources(ptrInstr);

11: ptrInstr ← next(ptrInstr);

12: end while

13: remainingResources ← dirtyResources,

14: ptrInstr ← head(originalCode);

15: while ptrInstr ∈ originalCode ∧ is_valid(ptrInstr) do

16: if read_resources(ptrInstr) 6∈ dirtyResources then

17: remove(remainingResources, read_resources(ptrInstr);

18: end if

19: ptrInstr ← next(ptrInstr);

20: end while

21: if remainingResources == ∅ then return transform;

22: end if

23: ptrInstr ← tail(beforeCode);

24: while ptrInstr ∈ beforeCode ∧ is_valid(ptrInstr) do

25: if read_resources(ptrInstr) 6∈ dirtyResources then

26: remove(remainingResources, read_resources(ptrInstr);

27: add(transform, ptrInstr);

28: end if

29: if remainingResources == ∅ then return transform

30: end if

31: add(dirtyResources, written_resources(ptrInstr);

32: ptrInstr ← previous(ptrInstr);

33: end while

34: return ∅

Je˙zeli po zako´nczeniu tego etapu zbiór brudnych zasobów jest pusty, to rozwi ˛azaniem problemu odtwarzania dla danego przypadku jest ustawienie wska´znika instrukcji na pocz ˛atek niezaburzonego kodu – w wyniku jego wykonania wszystkie brudne zasoby zostan ˛a nadpisane nowymi warto´sciami. Je˙zeli zbiór brudnych zasobów nie jest pusty, to wykonywany jest ostatni etap polegaj ˛acy na zbadaniu instrukcji wykonywanych przed miejscem zaburzenia w analogiczny sposób, jak w poprzednim etapie z t ˛a ró˙znic ˛a, ˙ze licznik instrukcji jest „cofany”.

Je˙zeli w tym etapie uda si˛e osi ˛agn ˛a´c pusty zbiór zasobów, to rozwi ˛azaniem jest ustawienie licznika instrukcji na pierwsz ˛a instrukcj˛e, od której prawidłowe wykonanie pozwoli nadpisa´c wszystkie brudne zasoby. Algorytm ko´nczy si˛e niepowodzeniem, je˙zeli nie uda si˛e oczy´sci´c zbioru brudnych zasobów.

Niestety mo˙zliwo´sci zbadania skuteczno´sci zaproponowanego algorytmu w praktyce s ˛a ograniczone z uwagi na fakt, ˙ze podstawowa implementacja wymaga deasemblacji kodu binarnego. Jest to utrudnione z poziomu j ˛adra systemu operacyjnego z uwagi na brak bibliotek realizuj ˛acych tak ˛a funkcj˛e w kodzie systemu operacyjnego GNU/Linux13. W zwi ˛azku z tym ograniczeniem przygotowane zostały dwa scenariusze sprawdzaj ˛ace mo˙zliwo´sci adaptacji zaproponowanej metody. Jeden z nich ma na celu sprawdzenie mo˙zliwo´sci modyfikacji stanu zadania zgłaszaj ˛acego przerwanie. Drugi słu˙zy oszacowaniu liczby awarii, które potencjalnie mog ˛a zosta´c obsłu˙zone poprzez znalezienie rozwi ˛azania problemu odtwarzania.

Weryfikacja eksperymentalna

W celu zbadania mo˙zliwo´sci odtworzenia poprawnego stanu zadania w module naprawczym opisanym w sekcji 5.6.1 zaimplementowano heurystyk˛e polegaj ˛ac ˛a na nast˛epuj ˛acym scenariuszu: je˙zeli po wykonaniu procedury naprawczej zadanie ponownie zgłasza przerwanie (czyli instrukcja wywołuj ˛aca przerwanie nie jest pierwsz ˛a zaburzon ˛a instrukcj ˛a), to sprawdzane jest, czy w odległo´sci 6 bajtów przed adresem zgłaszanej instrukcji wyst˛epuje kod instrukcji mov i w tym przypadku licznik instrukcji jest ustawiany na ten adres. Heurystyka ta polega na obserwacji, i˙z wiele przypadków nieobsłu˙zonych przez moduły naprawcze z sekcji 5.6.1 jest podobna do przypadku przedstawionego na listingu 5.3, gdzie awaria jest zgłaszana w nast˛epnej instrukcji po załadowaniu danych spod niewła´sciwego adresu pami˛eci.

W wyniku przeprowadzenia eksperymentu (składaj ˛acego si˛e z 10 000 testów) identycznego z eksperymentem opisanym w sekcji 5.6.1 uzyskano 56 przypadków, gdzie przedstawiona heurystyka pozwoliła na zako´nczenie poprawnym wynikiem zadania zgłaszaj ˛acego przerwanie w wyniku zaburzenia pami˛eci. Dzi˛eki temu eksperymentowi zostało potwierdzone, ˙ze mo˙zliwe jest zwi˛ekszenie skuteczno´sci modułu naprawczego poprzez zmian˛e warto´sci licznika instrukcji w procedurze naprawczej.

13 Istnieje inicjatywa zintegrowania deasemblera w kodzie j ˛adra GNU/Linux http://www.phoronix.com/scan.php?page=news_item&px=MTA4MTI

Oszacowanie potencjalnej skuteczno´sci

Oszacowanie liczby awarii, dla których istnieje rozwi ˛azanie problemu odtwarzania zostało przeprowadzone poprzez implementacj˛e algorytmu brudnych zasobów operuj ˛acego na artefaktach eksperymentów opisanych w sekcji 5.6.1 dla eksperymentu „RM v.2 I”.

Je˙zeli w dziennikach eksperymentu istniej ˛a zrzuty zaburzonego i oryginalnego kodu, to podejmowana jest próba wyznaczenia transformacji T (Qn, Si). Zaproponowany algorytm znalazł rozwi ˛azanie problemu odtwarzania w 28% eksperymentów, które zako´nczyły si˛e awari ˛a pomimo przeprowadzenia próby naprawy metod ˛a pułapek procesora (NRD), co pozwoliłoby na zwi˛ekszenie liczby prawidłowych wyników o około 3 p.p. (oznacza to zwi˛ekszenie warto´sci współczynnika Frz 55% do 63%). Nale˙zy jednak zało˙zy´c, ˙ze w cz˛e´sci przypadków znalezienie rozwi ˛azania problemu odtwarzania nie zapobiegnie wyst ˛apieniu awarii (np. zaburzony kod został wykonany wi˛ecej ni˙z raz i algorytm brudnych zasobów z tego powodu nie wyznaczył pełnego zbioru zasobów nadpisanych przez zaburzony kod). Dodatkowo warto zaznaczy´c, ˙ze tylko w 2% przypadków algorytm brudnych zasobów wykrył zniszczenie zasobu uniemo˙zliwiaj ˛ace odtworzenie prawidłowego stanu. Dla 50% przypadków nie udało si˛e znale´z´c rozwi ˛azania z powodu wykonania zaburzonej instrukcji nieuwzgl˛ednionej w bazie instrukcji, a dla 10% przypadków niemo˙zliwe było stwierdzenie, ˙ze wszystkie brudne zasoby zostan ˛a nadpisane w trakcie wznowionego wykonania. Pozostałe przypadki niepowodze´n były zwi ˛azane z warto´sci ˛a licznika instrukcji, która nie pozwalała na zastosowanie algorytmu (np. w wyniku wykonania zaburzonego kodu został wykonany skok w losowe miejsce pami˛eci i nie jest mo˙zliwe spekulowanie jaka sekwencja instrukcji została wykonana w wyniku zaburzenia).

Wnioski

Zdefiniowanie problemu odtwarzalno´sci pozwoliło opracowa´c oryginalny algorytm brudnych zasobów, który mo˙ze istotnie zwi˛ekszy´c skuteczno´s´c obsługi bł˛edów po stronie systemu operacyjnego, a tak˙ze aplikacji u˙zytkownika. Ograniczeniem przedstawionej implementacji jest stosunkowo niewielka baza instrukcji – otwiera to potencjalny obszar bada´n automatycznego wyznaczenia wła´sciwo´sci wszystkich instrukcji danej architektury ISA. Dodatkowym obszarem bada´n mo˙ze by´c modyfikacja algorytmu brudnych zasobów o inspekcj˛e stanu stosu (jako potencjalnego miejsca przechowywania warto´sci rejestrów, które zostały nadpisane w rejestrach) oraz mo˙zliwo´s´c zastosowania instrukcji odwrotnych dla wykonanych instrukcji w celu odwrócenia efektów wykonania zaburzonych instrukcji. Efektywno´s´c algorytmu mogłaby by´c potencjalnie zwi˛ekszona dzi˛eki meta-danym dotycz ˛acym kodu, które mog ˛a by´c dost˛epne na poziomie kompilatora – np. czy wynik wykonania funkcji zale˙zy wył ˛acznie od parametrów wywołania.