Architektura systemu DBI

https://chacker.pl/

Silniki DBI dynamicznie instrumentują procesy, monitorując i kontrolując wszystkie wykonywane instrukcje. Silnik DBI udostępnia API, które umożliwia pisanie zdefiniowanych przez użytkownika narzędzi DBI (często w formie biblioteki współdzielonej ładowanej przez silnik), które określają, który kod powinien zostać instrumentowany i w jaki sposób. Na przykład, narzędzie DBI pokazane po prawej stronie rysunku 9-4 implementuje (w pseudokodzie) prosty profiler, który zlicza liczbę wykonanych bloków podstawowych. W tym celu wykorzystuje API silnika DBI do instrumentacji ostatniej instrukcji każdego bloku podstawowego za pomocą wywołania zwrotnego do funkcji, która zwiększa licznik. Zanim silnik DBI uruchomi główny proces aplikacji (lub wznowi go, jeśli zostanie podłączony do istniejącego procesu), umożliwia zainicjowanie narzędzia DBI. Na rysunku 9-4 funkcja inicjalizacyjna narzędzia DBI rejestruje w silniku DBI funkcję o nazwie instrument_bb (1). Funkcja ta informuje silnik DBI, jak instrumentować każdy blok podstawowy; W tym przypadku dodaje wywołanie zwrotne do bb_callback po ostatniej instrukcji w bloku podstawowym. Następnie funkcja inicjalizacji informuje silnik DBI, że zakończył inicjalizację i jest gotowy do uruchomienia aplikacji (2). Silnik DBI nigdy nie uruchamia procesu aplikacji bezpośrednio, lecz uruchamia kod w pamięci podręcznej kodu, która zawiera cały zinstrumentowany kod. Początkowo pamięć podręczna kodu jest pusta, więc silnik DBI pobiera blok kodu z procesu (3) i instrumentuje ten kod (4) zgodnie z instrukcjami narzędzia DBI (5). Należy zauważyć, że silniki DBI niekoniecznie pobierają i instrumentują kod z podstawową granularnością bloku, co wyjaśnię szerzej w rozdziale 9.4. Jednak w tym przykładzie założę, że silnik instrumentuje kod z podstawową granularnością bloku, wywołując instrument_bb. Po instrumentacji kodu silnik DBI kompiluje go za pomocą kompilatora just-in-time (JIT) (6), który ponownie optymalizuje zinstrumentowany kod i zapisuje skompilowany kod w pamięci podręcznej kodu (7). Kompilator JIT przepisuje również instrukcje przepływu sterowania, aby zapewnić silnikowi DBI zachowanie kontroli, zapobiegając dalszemu wykonywaniu transferów sterowania w nieinstrumentowanym procesie aplikacji. Należy pamiętać, że w przeciwieństwie do większości kompilatorów, kompilator JIT w silniku DBI nie tłumaczy kodu na inny język; kompiluje z natywnego kodu maszynowego do natywnego kodu maszynowego. Instrumentacja i kompilacja JIT kodu jest konieczna tylko przy pierwszym uruchomieniu. Następnie kod jest przechowywany w pamięci podręcznej kodu i ponownie wykorzystywany. Zinstrumentowany i skompilowany kod JIT jest teraz wykonywany w pamięci podręcznej kodu, dopóki nie pojawi się instrukcja przepływu sterowania wymagająca pobrania nowego kodu lub wyszukania innego fragmentu kodu w pamięci podręcznej (8). Silniki DBI, takie jak Pin i DynamoRIO, zmniejszają narzut czasu wykonania poprzez przepisywanie instrukcji przepływu sterowania, gdy jest to możliwe, tak aby przeskakiwały one bezpośrednio do następnego bloku w pamięci podręcznej kodu bez pośrednictwa silnika DBI. Gdy nie jest to możliwe (na przykład w przypadku wywołań pośrednich), przepisane instrukcje zwracają sterowanie do silnika DBI, aby mógł on przygotować i uruchomić kolejny fragment kodu. Podczas gdy większość instrukcji działa natywnie w pamięci podręcznej kodu, silnik DBI może emulować niektóre instrukcje zamiast uruchamiać je bezpośrednio. Na przykład Pin robi to w przypadku wywołań systemowych, takich jak execve, które wymagają specjalnej obsługi przez silnik DBI. Zinstrumentowany kod zawiera wywołania zwrotne do funkcji w narzędziu DBI, które obserwują lub modyfikują zachowanie kodu (9). Na przykład na rysunku 9-4 funkcja instrument_bb narzędzia DBI dodaje wywołanie zwrotne na końcu każdego bloku podstawowego, który wywołuje funkcję bb_callback, zwiększającą licznik bloków podstawowych narzędzia DBI. Silnik DBI automatycznie zapisuje i przywraca stan rejestrów podczas przekazywania sterowania do lub z funkcji zwrotnej w narzędziu DBI. Teraz, gdy znasz już działanie silników DBI, omówmy Pin, silnik DBI, którego będę używać później

Dynamiczna Instrumentacja Binarna

https://chacker.pl/

Ponieważ silniki DBI monitorują pliki binarne (a raczej procesy) podczas wykonywania i instrumentowania strumienia instrukcji, nie wymagają deasemblacji ani przepisywania plików binarnych, tak jak SBI, co czyni je mniej podatnymi na błędy. Rysunek przedstawia architekturę nowoczesnych systemów DBI, takich jak Pin i DynamoRIO.

Wszystkie te systemy opierają się na tym samym podejściu wysokiego poziomu, chociaż różnią się szczegółami implementacji i optymalizacji. W dalszej części rozdziału skupię się na „czystych” systemach DBI pokazanych na rysunku, a nie na platformach hybrydowych, takich jak Dyninst, które obsługują zarówno SBI, jak i DBI, wykorzystując techniki łatania kodu, takie jak trampoliny.

Niezawodność podejścia trampoliny

https://chacker.pl/

Jak widać na podstawie problemów z obsługą instrukcji switch, podejście trampoliny jest podatne na błędy. Podobnie jak w przypadku przypadków switch, które są zbyt małe, aby pomieścić normalną trampolinę, programy mogą (choć jest to mało prawdopodobne) zawierać bardzo krótkie funkcje, które nie mają wystarczająco dużo miejsca na 5-bajtowy jmp, co wymaga od silnika SBI zastosowania innego rozwiązania, takiego jak podejście int 3. Co więcej, jeśli plik binarny zawiera jakiekolwiek dane inline wymieszane z kodem, trampoliny mogą nieumyślnie nadpisać część tych danych, powodując błędy podczas korzystania z nich przez program. Wszystko to zakłada, że ​​użyty deasembler jest poprawny; jeśli nie jest, wszelkie zmiany wprowadzone przez silnik SBI mogą uszkodzić plik binarny. Niestety, nie istnieje znana technika SBI, która byłaby jednocześnie wydajna i poprawna, co sprawia, że ​​SBI jest niebezpieczne w przypadku plików binarnych produkcyjnych. W wielu przypadkach rozwiązania DBI są preferowane, ponieważ nie są podatne na błędy, z którymi boryka się SBI. Choć nie są tak szybkie jak SBI, nowoczesne platformy DBI działają wystarczająco wydajnie w wielu praktycznych zastosowaniach. Pozostała część tego rozdziału koncentruje się na DBI, a konkretnie na znanej platformie DBI o nazwie Pin. Przyjrzyjmy się bliżej szczegółom implementacji DBI, a następnie omówimy praktyczne przykłady.

Obsługa pośredniego przepływu sterowania

https://chacker.pl/

Ponieważ instrukcje pośredniego przepływu sterowania są kierowane na adresy obliczane dynamicznie, nie ma niezawodnego sposobu, aby silniki SBI mogły je statycznie przekierować. Podejście trampolinowe pozwala na przepływ pośrednich transferów sterowania do oryginalnego, nieinstrumentowanego kodu i wykorzystuje trampoliny umieszczone w oryginalnym kodzie do przechwytywania i przekierowywania przepływu sterowania z powrotem do kodu zinstrumentowanego. Rysunek  pokazuje, jak podejście trampolinowe obsługuje dwa typy pośredniego przepływu sterowania: pośrednie wywołania funkcji i pośrednie skoki używane do implementacji instrukcji switch języka C/C+

+.

Rysunek a pokazuje, jak podejście trampoliny obsługuje wywołania pośrednie. Silnik SBI nie zmienia kodu obliczającego adresy, więc adresy docelowe używane przez wywołania pośrednie wskazują na funkcję oryginalną (1). Ponieważ na początku każdej funkcji oryginalnej znajduje się trampolina, sterowanie jest natychmiast przekazywane z powrotem do zinstrumentowanej wersji funkcji (2). W przypadku skoków pośrednich sytuacja jest bardziej skomplikowana, jak widać na rysunku b. Na potrzeby tego przykładu załóżmy, że skok pośredni jest częścią instrukcji switch języka C/C++. Na poziomie binarnym instrukcje switch są często implementowane za pomocą tablicy skoków zawierającej wszystkie adresy możliwych przypadków switch. Aby przejść do konkretnego przypadku, switch oblicza odpowiedni indeks tablicy skoków i używa pośredniego jmp, aby przejść do adresu tam zapisanego (1). Domyślnie wszystkie adresy zapisane w tablicy skoków wskazują na kod oryginalny (2). W ten sposób pośredni jmp trafia w środek oryginalnej funkcji, gdzie nie ma trampoliny, i wznawia tam wykonywanie (3). Aby uniknąć tego problemu, silnik SBI musi albo załatać tablicę skoków, zmieniając oryginalne adresy kodu na nowe, albo umieścić trampolinę w każdym przypadku przełączania w oryginalnym kodzie. Niestety, podstawowe informacje symboliczne (w przeciwieństwie do obszernych informacji DWARF) nie zawierają informacji o układzie instrukcji przełączania, co utrudnia ustalenie, gdzie umieścić trampoliny. Ponadto może nie być wystarczająco dużo miejsca między instrukcjami przełączania, aby pomieścić wszystkie trampoliny. Łatanie tablic skoków jest również niebezpieczne, ponieważ ryzykujesz błędną zmianę danych, które akurat są prawidłowym adresem, ale tak naprawdę nie są częścią tablicy skoków.

Przepływ sterowania trampoliną

https://chacker.pl/

Aby lepiej zrozumieć przepływ sterowania programu zinstrumentowanego za pomocą podejścia trampoliny, wróćmy do prawej strony rysunku 9-2, przedstawiającego zinstrumentowany plik binarny i załóżmy, że oryginalna funkcja f1 została właśnie wywołana. Zaraz po wywołaniu f1, trampolina przeskakuje do f1_copy (1), zinstrumentowanej wersji f1. Po trampolinie (2) mogą znajdować się pewne niepotrzebne bajty, ale nie są one wykonywane. Silnik SBI wstawia kilka instrukcji nop w każdym możliwym punkcie instrumentacji w f1_copy (3). W ten sposób, aby zinstrumentować instrukcję, silnik SBI może po prostu nadpisać instrukcje nop w tym punkcie instrumentacji za pomocą jmp lub wywołania fragmentu kodu instrumentacji. Należy pamiętać, że zarówno wstawianie instrukcji nop, jak i instrumentacja są wykonywane statycznie, a nie w czasie wykonywania. Na rysunku 9-2 wszystkie regiony nop są nieużywane, z wyjątkiem ostatniego, tuż przed instrukcją ret, co wyjaśnię za chwilę. Aby zachować poprawność skoków względnych pomimo przesunięcia kodu spowodowanego nowo wstawionymi instrukcjami, silnik SBI poprawia przesunięcia wszystkich instrukcji względnych jmp. Dodatkowo silnik zastępuje wszystkie 2-bajtowe instrukcje względne jmp, które mają 8-bitowe przesunięcie, odpowiadającą im 5-bajtową wersją z 32-bitowym przesunięciem (4). Jest to konieczne, ponieważ podczas przesuwania kodu w funkcji f1_copy przesunięcie między instrukcjami jmp a ich obiektami docelowymi może stać się zbyt duże, aby zakodować je w 8 bitach. Podobnie silnik SBI przepisuje wywołania bezpośrednie, takie jak wywołanie f2, tak aby wskazywały one na funkcję zinstrumentowaną, a nie na oryginalną (5). Biorąc pod uwagę to przepisanie wywołań bezpośrednich, można się zastanawiać, dlaczego trampoliny na początku każdej oryginalnej funkcji są w ogóle potrzebne. Jak wyjaśnię za chwilę, są one niezbędne do obsługi wywołań pośrednich. Załóżmy teraz, że poleciłeś silnikowi SBI instrumentację każdej instrukcji ret. W tym celu silnik SBI nadpisuje instrukcje nop zarezerwowane do tego celu poleceniem jmp lub wywołaniem kodu instrumentacji (6). W przykładzie z rysunku kod instrumentacji to funkcja o nazwie hook_ret, umieszczona w bibliotece współdzielonej i dostępna poprzez wywołanie umieszczone przez silnik SBI w punkcie instrumentacji. Funkcja hook_ret ​​najpierw zapisuje stan (7), taki jak zawartość rejestrów, a następnie uruchamia dowolny określony kod instrumentacji. Na koniec przywraca zapisany stan (8) i wznawia normalne wykonywanie, powracając do instrukcji następującej po punkcie instrumentacji. Teraz, gdy wiesz już, jak podejście trampoliny przepisuje instrukcje bezpośredniego przepływu sterowania, przyjrzyjmy się, jak obsługuje ono pośredni przepływ sterowania.

Podejście trampoliny

https://chacker.pl/

W przeciwieństwie do podejścia int 3, podejście trampoliny nie podejmuje próby bezpośredniej instrumentacji oryginalnego kodu. Zamiast tego tworzy kopię całego oryginalnego kodu i instrumentuje tylko ten skopiowany kod. Idea polega na tym, aby nie uszkodzić żadnego kodu ani odwołań do danych, ponieważ wszystkie one nadal wskazują na oryginalne, niezmienione lokalizacje. Aby zapewnić, że plik binarny uruchamia zinstrumentowany kod, a nie kod oryginalny, podejście trampoliny wykorzystuje instrukcje jmp zwane trampolinami, aby przekierować oryginalny kod do zinstrumentowanej kopii. Za każdym razem, gdy wywołanie lub skok przekazuje kontrolę do części oryginalnego kodu, trampolina w tym miejscu natychmiast przeskakuje do odpowiedniego zinstrumentowanego kodu. Aby wyjaśnić podejście trampoliny, rozważmy przykład pokazany na rysunku . Rysunek przedstawia niezainstrumentowany plik binarny po lewej stronie, a po prawej stronie pokazuje, jak ten plik binarny przekształca się po instrumentacji.

Załóżmy, że oryginalny, niezainstrumentowany plik binarny zawiera dwie funkcje o nazwach f1 i f2. Rysunek pokazuje, że f1 zawiera następujący kod. Zawartość f2 nie jest istotna w tym przykładzie.

<f1>:

test edi,edi

jne _ret

xor eax,eax

call f2

_ret:

ret

Podczas instrumentacji pliku binarnego za pomocą metody trampoliny, silnik SBI tworzy kopie wszystkich oryginalnych funkcji, umieszcza je w nowej sekcji kodu (nazwanej .text.instrum na rysunku) i nadpisuje pierwszą instrukcję każdej oryginalnej funkcji trampoliną jmp, która przeskakuje do odpowiadającej jej skopiowanej funkcji. Na przykład silnik SBI przepisuje oryginalną funkcję f1 w następujący sposób, aby przekierować ją do f1_copy:

<f1>:

jmp f1_copy

; junk bytes

Instrukcja trampoliny to 5-bajtowy jmp, więc może częściowo nadpisać i uszkodzić wiele instrukcji, tworząc „śmieciowe bajty” tuż za trampoliną. Jednak zazwyczaj nie stanowi to problemu w przypadku podejścia trampoliny, ponieważ gwarantuje, że te uszkodzone instrukcje nigdy nie zostaną wykonane. Pod koniec tej sekcji zobaczysz kilka przypadków, w których może to się nie udać.

Rozwiązywanie problemu skoków wielobajtowych za pomocą int 3

https://chacker.pl/

Instrukcja int 3 w architekturze x86 generuje przerwanie programowe, które programy działające w przestrzeni użytkownika, takie jak biblioteki SBI lub debugery, mogą przechwycić (w systemie Linux) w postaci sygnału SIGTRAP dostarczanego przez system operacyjny. Kluczową cechą int 3 jest to, że ma on tylko 1 bajt długości, dzięki czemu można nadpisać nim dowolną instrukcję bez obawy o nadpisanie sąsiedniej instrukcji. Kod operacji dla int 3 to 0xcc. Z punktu widzenia SBI, aby zainstrumentować instrukcję za pomocą int 3, wystarczy nadpisać pierwszy bajt tej instrukcji wartością 0xcc. W przypadku wystąpienia błędu SIGTRAP można użyć interfejsu API ptrace systemu Linux, aby dowiedzieć się, pod którym adresem wystąpiło przerwanie, podając adres punktu instrumentacji. Następnie można wywołać odpowiedni kod instrumentacji dla tego punktu. Z czysto funkcjonalnego punktu widzenia int 3 jest idealnym sposobem na implementację SBI, ponieważ jest łatwy w użyciu i nie wymaga relokacji kodu. Niestety, przerwania programowe, takie jak int 3, są powolne, co powoduje nadmierne obciążenie w instrumentowanej aplikacji. Ponadto podejście z int 3 jest niezgodne z programami, które są już debugowane z użyciem int 3 dla punktów przerwania. Dlatego w praktyce wiele platform SBI korzysta ze skomplikowanych, ale szybszych metod przepisywania, takich jak podejście trampoliny.

Podejście int 3

https://chacker.pl/

Podejście int 3 wzięło swoją nazwę od instrukcji x86 int 3, której debuggery używają do implementacji punktów przerwania oprogramowania. Aby zilustrować potrzebę int 3, rozważmy najpierw podejście SBI, które nie działa w ogólnym przypadku. Naiwna implementacja SBI Biorąc pod uwagę praktyczną niemożność naprawienia wszystkich odwołań do przeniesionego kodu, oczywiste jest, że SBI nie może przechowywać kodu instrumentacji w istniejącej sekcji kodu. Ponieważ nie ma miejsca na dowolną ilość nowego kodu w istniejących sekcjach kodu, wynika z tego, że podejścia SBI muszą przechowywać kod instrumentacji w oddzielnej lokalizacji, takiej jak nowa sekcja lub biblioteka współdzielona, ​​a następnie w jakiś sposób przekazać kontrolę do kodu instrumentacji, gdy wykonywanie osiągnie punkt instrumentacji. Aby to osiągnąć, można opracować rozwiązanie pokazane na rysunku .

Lewa kolumna na Rysunku  przedstawia fragment oryginalnego, nieinstrumentowanego kodu. Załóżmy, że chcesz zinstrumentować instrukcję mov edx,0x1 (1), dodając kod instrumentacji do uruchomienia przed i po tej instrukcji. Aby obejść problem braku miejsca na dodanie nowego kodu w linii, nadpisujesz mov edx,0x1 poleceniem jmp do kodu instrumentacji (2), przechowywanego w oddzielnej sekcji kodu lub bibliotece. Kod instrumentacji najpierw uruchamia dodany kod przedinstrumentacyjny (3), czyli kod uruchamiany przed oryginalną instrukcją. Następnie uruchamia oryginalną instrukcję mov edx,0x1 (4), a następnie kod poinstrumentacyjny (5). Na koniec kod instrumentacji powraca do instrukcji następującej po punkcie instrumentacji (6), wznawiając normalne wykonywanie. Należy pamiętać, że jeśli kod przedinstrumentacyjny lub poinstrumentacyjny zmieni zawartość rejestrów, może to nieumyślnie wpłynąć na inne części programu. Dlatego platformy SBI przechowują stan rejestrów przed uruchomieniem dodanego kodu i przywracają go później, chyba że wyraźnie poinstruujesz platformę SBI o chęci zmiany stanu rejestru. Jak widać, podejście przedstawione na rysunku 9-1 to prosty i elegancki sposób na uruchomienie dowolnej ilości kodu przed lub po dowolnej instrukcji. Jaki jest więc problem z tym podejściem? Problem polega na tym, że instrukcje jmp zajmują wiele bajtów; aby przejść do kodu instrumentacji, zazwyczaj potrzebna jest 5-bajtowa instrukcja jmp, która składa się z 1 bajtu kodu operacyjnego z 32-bitowym przesunięciem. Podczas instrumentacji krótkiej instrukcji, instrukcja jmp do kodu instrumentacji może być dłuższa niż instrukcja, którą zastępuje. Na przykład instrukcjaxor esi,esi w lewym górnym rogu rysunku 9-1 ma tylko 2 bajty, więc jeśli zastąpisz ją 5-bajtową instrukcją jmp, instrukcja jmp nadpisze i uszkodzi część następnej instrukcji. Nie można rozwiązać tego problemu, umieszczając tę ​​kolejną nadpisaną instrukcję w kodzie instrumentacji, ponieważ instrukcja ta może być celem rozgałęzienia. Wszelkie rozgałęzienia celujące w tę instrukcję trafiłyby do środka wstawionej instrukcji jmp, powodując uszkodzenie pliku binarnego. To prowadzi nas z powrotem do instrukcji int 3. Instrukcji int 3 można użyć do instrumentacji krótkich instrukcji, w których nie można wykonać skoków wielobajtowych.

Statyczna instrumentacja binarna

https://chacker.pl/

Statyczna instrumentacja binarna polega na deasemblacji pliku binarnego, a następnie dodaniu kodu instrumentacji w razie potrzeby i trwałym zapisaniu zaktualizowanego pliku binarnego na dysku. Znane platformy SBI to PEBIL i Dyninst (obsługujące zarówno DBI, jak i SBI). PEBIL wymaga symboli, podczas gdy Dyninst ich nie wymaga. Należy pamiętać, że zarówno PEBIL, jak i Dyninst to narzędzia badawcze, dlatego nie są tak dobrze udokumentowane, jak narzędzia o jakości produkcyjnej. Głównym wyzwaniem we wdrażaniu SBI jest znalezienie sposobu na dodanie kodu instrumentacji i przepisanie pliku binarnego bez naruszania istniejącego kodu ani odwołań do danych. Rozważmy dwa popularne rozwiązania tego problemu, które nazywam podejściem int 3 i podejściem trampoliny. Należy zauważyć, że w praktyce silniki SBI mogą zawierać elementy obu tych technik lub wykorzystywać zupełnie inną technikę.

Statyczna a dynamiczna instrumentacja binarna

https://chacker.pl/

Statyczna i dynamiczna instrumentacja binarna rozwiązują problemy związane z wstawianiem i relokacją kodu, stosując różne podejścia. SBI wykorzystuje techniki przepisywania binarnego do trwałej modyfikacji plików binarnych na dysku. Z drugiej strony, DBI w ogóle nie modyfikuje plików binarnych na dysku, lecz monitoruje je podczas wykonywania i wstawia nowe instrukcje do strumienia instrukcji na bieżąco. Zaletą tego podejścia jest uniknięcie problemów z relokacją kodu. Kod instrumentacji jest wstrzykiwany tylko do strumienia instrukcji, a nie do sekcji kodu pliku binarnego w pamięci, dzięki czemu nie powoduje to uszkodzenia odwołań. Wadą jest jednak to, że instrumentacja w czasie wykonywania DBI jest bardziej kosztowna obliczeniowo, co powoduje większe spowolnienia w instrumentowanym pliku binarnym niż w SBI. Tabela 9-1 podsumowuje główne zalety i wady SBI i DBI, przedstawiając zalety symbolem + i wady symbolem –.

Instrumentacja dynamiczna: Instrumentacja statyczna

– Stosunkowo wolna (4 razy lub więcej): + Stosunkowo szybka (10% do 2 razy)

– Zależna od biblioteki i narzędzia DBI: + Samodzielny plik binarny

+ Przezroczysta instrumentacja bibliotek: – Wymaga jawnego instrumentowania bibliotek

+ Obsługa dynamicznie generowanego kodu: – Kod generowany dynamicznie nie jest obsługiwany

+ Możliwość dynamicznego dołączania/odłączania: – Instrumentacja całego wykonania

+ Brak konieczności deasemblacji: – Podatność na błędy deasemblacji

+ Przezroczysta, brak konieczności modyfikowania pliku binarnego: – Podatne na błędy przepisywanie pliku binarnego

+ Brak konieczności używania symboli: – Symbole preferowane w celu minimalizacji błędów

Jak widać, zapotrzebowanie DBI na analizę i instrumentację w czasie wykonywania powoduje spowolnienia czterokrotnie lub więcej, podczas gdy SBI powoduje jedynie spowolnienie od 10% do dwukrotności. Należy pamiętać, że są to wartości orientacyjne, a rzeczywiste spowolnienie może się znacznie różnić w zależności od potrzeb instrumentacji i jakości implementacji narzędzia. Co więcej, pliki binarne z instrumentacją DBI są trudniejsze w dystrybucji: należy dostarczyć nie tylko sam plik binarny, ale także platformę i narzędzie DBI zawierające kod instrumentacji. Z drugiej strony, pliki binarne z instrumentacją SBI są autonomiczne i można je dystrybuować normalnie po zakończeniu instrumentacji. Główną zaletą DBI jest to, że jest znacznie łatwiejszy w użyciu niż SBI. Ponieważ DBI korzysta z instrumentacji w czasie wykonywania, automatycznie uwzględnia wszystkie wykonywane instrukcje, niezależnie od tego, czy są one częścią oryginalnego pliku binarnego, czy bibliotek używanych przez plik binarny. Natomiast w przypadku SBI należy jawnie zinstrumentować i rozpowszechnić wszystkie biblioteki używane przez plik binarny, chyba że chce się pozostawić te biblioteki bez instrumentacji. Fakt, że DBI działa na wykonywanym strumieniu instrukcji, oznacza również, że obsługuje dynamicznie generowany kod, którego SBI nie obsługuje, taki jak kod kompilowany metodą JIT lub kod samomodyfikujący. Ponadto platformy DBI zazwyczaj mogą dynamicznie dołączać się do procesów i odłączać się od nich, tak jak debugery. Jest to wygodne, na przykład, jeśli chcesz obserwować część wykonywania długotrwałego procesu. W przypadku DBI wystarczy po prostu dołączyć się do procesu, zebrać potrzebne informacje, a następnie odłączyć się, pozostawiając proces działający normalnie. W przypadku SBI nie jest to możliwe; albo instrumentujesz całe wykonanie, albo w ogóle nie instrumentujesz. Wreszcie, DBI jest znacznie mniej podatne na błędy niż SBI. SBI instrumentuje pliki binarne, deasemblując je, a następnie wprowadzając wszelkie niezbędne zmiany. Oznacza to, że błędy deasemblacji mogą łatwo spowodować błędy w instrumentacji, potencjalnie powodując nieprawidłowe wyniki, a nawet uszkodzenie pliku binarnego. DBI nie ma tego problemu, ponieważ nie wymaga deasemblacji; Po prostu obserwuje instrukcje w trakcie ich wykonywania, co gwarantuje poprawny strumień instrukcji. Aby zminimalizować ryzyko błędów deasemblacji, wiele platform SBI wymaga symboli, podczas gdy DBI nie ma takiego wymogu. Jak wspomniałem wcześniej, istnieją różne sposoby implementacji przepisywania binarnego w SBI i instrumentacji środowiska wykonawczego w DBI. W kolejnych dwóch sekcjach przyjrzymy się najpopularniejszym sposobom implementacji odpowiednio SBI i DBI.