Liczenie instrukcji, transfery sterowania i wywołania systemowe

https://chacker.pl/

Do tej pory poznałeś/aś cały kod odpowiedzialny za inicjalizację narzędzia Pintool i wstawianie wymaganej instrumentacji w postaci wywołań zwrotnych do procedur analizy. Jedyny kod, którego jeszcze nie widziałeś/aś, to faktyczne procedury analizy, które zliczają i rejestrują statystyki podczas działania aplikacji. Listing  przedstawia wszystkie procedury analizy używane przez profilera.

Listing : profiler.cpp (ciąg dalszy)

Jak widać, procedury analizy są proste i implementują jedynie minimalny kod do śledzenia wymaganych statystyk. Jest to ważne, ponieważ procedury analizy są często wywoływane podczas działania aplikacji, co ma duży wpływ na wydajność narzędzia Pintool. Pierwsza procedura analizy, count_bb_insns (1), jest wywoływana podczas wykonywania bloku podstawowego i po prostu zwiększa wartość insn_count o liczbę instrukcji w bloku podstawowym. Podobnie, count_cflow (2) zwiększa wartość cflow_count podczas wykonywania instrukcji przepływu sterowania. Dodatkowo rejestruje adres źródłowy i docelowy gałęzi na mapie cflows oraz zwiększa licznik dla tej konkretnej kombinacji adresu źródłowego i docelowego. W Pin do przechowywania adresów używany jest typ całkowity ADDRINT (3). Procedura analizy, która rejestruje informacje o wywołaniu, count_call (4), jest analogiczna do count_cflow. Ostatnia funkcja na listingu 9-5, log_syscall (5), nie jest zwykłą procedurą analizy, lecz wywołaniem zwrotnym dla zdarzeń wejścia wywołania systemowego. W Pin procedury obsługi wywołań systemowych przyjmują cztery argumenty: THREADID identyfikujący wątek, który wykonał wywołanie systemowe; CONTEXT* zawierający takie dane, jak numer wywołania systemowego, argumenty i wartość zwracaną (tylko dla procedur obsługi wyjścia wywołania systemowego); argument SYSCALL_STANDARD identyfikujący konwencję wywołania wywołania systemowego; oraz wreszcie znany już void*, który pozwala na przekazanie zdefiniowanej przez użytkownika struktury danych. Przypomnijmy, że celem log_syscall jest rejestrowanie częstotliwości wywołań każdego wywołania systemowego. W tym celu wywołuje PIN_GetSyscallNumber, aby uzyskać numer bieżącego wywołania systemowego (6) i rejestruje trafienie dla tego wywołania systemowego na mapie wywołań systemowych. Teraz, gdy poznałeś już cały ważny kod profilera, przetestujmy go!

Instrumentacja wywołań

https://chacker.pl/

Na koniec, profiler przechowuje oddzielny licznik i mapowanie do śledzenia wywoływanych funkcji, dzięki czemu można zobaczyć, które funkcje są najbardziej opłacalne pod kątem optymalizacji aplikacji. Przypomnijmy, że aby śledzić wywoływane funkcje, należy włączyć opcję -c profilera. Aby instrumentować wywołania, instrument_insn najpierw używa INS_IsCall, aby oddzielić wywołania od innych instrukcji (9). Jeśli aktualnie instrumentowana instrukcja jest rzeczywiście wywołaniem i jeśli opcja -c została przekazana do narzędzia Pintool, profiler wstawia wywołanie zwrotne analizy przed instrukcją wywołania (w punkcie IPOINT_BEFORE) (10) do procedury analizy o nazwie count_call, przekazując źródło wywołania (IARG_INST_PTR) i adres docelowy (IARG_BRANCH_TARGET_ADDR). Należy pamiętać, że w tym przypadku można bezpiecznie użyć INS_InsertCall zamiast INS_InsertPredicatedCall, ponieważ nie ma instrukcji wywołania z wbudowanymi instrukcjami warunkowymi.

Instrumentacja krawędzi opadającej

https://chacker.pl/

Właśnie zobaczyłeś, jak profiler instrumentuje pobraną krawędź instrukcji przepływu sterowania. Jednak profiler powinien rejestrować transfery sterowania niezależnie od kierunku rozgałęzienia. Innymi słowy, powinien instrumentować nie tylko pobraną krawędź, ale także krawędź opadającą instrukcji przepływu sterowania, które ją posiadają (patrz rysunek 9-5). Należy pamiętać, że niektóre instrukcje, takie jak skoki bezwarunkowe, nie posiadają krawędzi przejściowej, dlatego należy jawnie sprawdzić stan INS_HasFallthrough przed próbą instrumentacji krawędzi przejściowej instrukcji (6). Należy również zauważyć, że zgodnie z definicją Pin, instrukcje niebędące częścią przepływu sterowania, które po prostu przechodzą do następnej instrukcji, posiadają krawędź przejściową. Jeśli okaże się, że dana instrukcja posiada krawędź przejściową, instrument _insn wstawia wywołanie zwrotne analizy do count_cflow na tej krawędzi, tak jak zrobił to w przypadku krawędzi przejętej. Jedyną różnicą jest to, że to nowe wywołanie zwrotne używa punktu wstawiania IPOINT_AFTER (7) i przekazuje adres przejścia (IARG _FALLTHROUGH_ADDR) jako adres docelowy do zapisu (8).

Instrumentacja pobranej krawędzi

https://chacker.pl/

Aby rejestrować transfery sterowania i wywołania, instrument_insn wstawia trzy różne wywołania zwrotne analizy. Najpierw używa INS_InsertPredicatedCall (2), aby wstawić wywołanie zwrotne na pobranej krawędzi instrukcji (3).

 Wstawione wywołanie zwrotne analizy do count_cflow zwiększa licznik przepływu sterowania (cflow_count) w przypadku pobranej gałęzi i rejestruje adresy źródłowy i docelowy transferu sterowania. W tym celu procedura analizy przyjmuje dwa argumenty: wartość wskaźnika instrukcji w momencie wywołania zwrotnego (IARG_INST_PTR) (4) oraz adres docelowy pobranej krawędzi rozgałęzienia (IARG_BRANCH_TARGET_ADDR) (5). Należy zauważyć, że IARG_INST_PTR i IARG_BRANCH_TARGET_ADDR to specjalne typy argumentów, dla których typ danych i wartość są niejawne. Natomiast dla argumentu IARG_UINT32, który widziałeś na Listingu, musisz osobno określić typ (IARG_UINT32) i wartość (w tym przykładzie BBL_NumIns). Jak widziałeś w , pobrana krawędź jest prawidłowym punktem instrumentacji tylko dla instrukcji rozgałęzienia lub wywołania (INS_IsBranchOrCall musi zwrócić wartość true). W tym przypadku sprawdzenie na początku instrument_insn gwarantuje, że jest to rozgałęzienie lub wywołanie. Należy zauważyć, że instrument_insn używa INS_InsertPredicatedCall do wstawiania wywołania zwrotnego analizy zamiast INS_InsertCall. Niektóre instrukcje x86, takie jak ruchy warunkowe (cmov) i operacje na ciągach znaków z prefiksami rep, mają wbudowane predykaty, które powodują powtórzenie instrukcji, jeśli spełnione są określone warunki. Wywołania zwrotne analizy wstawione za pomocą INS_InsertPredicatedCall są wywoływane tylko wtedy, gdy warunek ten jest spełniony i instrukcja jest faktycznie wykonywana. Natomiast wywołania zwrotne wstawione za pomocą INS_InsertCall są wywoływane nawet wtedy, gdy warunek powtórzenia nie jest spełniony, co prowadzi do przeszacowania liczby instrukcji.

Instrumentacja instrukcji przepływu sterowania

https://chacker.pl/

Oprócz zliczania liczby instrukcji wykonywanych przez aplikację, profiler zlicza również liczbę transferów przepływu sterowania oraz, opcjonalnie, liczbę wywołań. Wykorzystuje procedurę instrumentacji na poziomie instrukcji przedstawioną na Listingu do wstawiania wywołań zwrotnych analizy, które zliczają transfery i wywołania przepływu sterowania.

Listing: profiler.cpp (ciąg dalszy)

Procedura instrumentacji o nazwie instrument_insn otrzymuje obiekt INS jako pierwszy argument, reprezentujący instrukcję dla instrumentu. Najpierw instrument_insn wywołuje INS_IsBranchOrCall, aby sprawdzić, czy jest to instrukcja przepływu sterowania (1). Jeśli nie, nie dodaje żadnej instrumentacji. Po upewnieniu się, że ma do czynienia z instrukcją przepływu sterowania, instrument_insn sprawdza, czy instrukcja jest częścią aplikacji głównej, tak jak w przypadku podstawowej instrumentacji bloku.

Implementacja instrumentacji bloków podstawowych

https://chacker.pl/

Nie można bezpośrednio instrumentować bloków podstawowych w API Pin. Oznacza to, że nie ma funkcji BBL_AddInstrumentFunction. Aby instrumentować bloki podstawowe, należy dodać procedurę instrumentacji na poziomie śladu, a następnie wykonać pętlę po wszystkich blokach podstawowych w śladzie, instrumentując każdy z nich, jak pokazano na Listingu.

Listing : profiler.cpp (ciąg dalszy)

Pierwsza funkcja na liście, instrument_trace, to procedura instrumentacji na poziomie śladu, którą profiler zarejestrował wcześniej. Jej pierwszym argumentem jest TRACE do instrumentu. Najpierw instrument_trace wywołuje IMG_FindByAddress z adresem śladu, aby znaleźć obraz IMG, którego ślad jest częścią (1). Następnie weryfikuje, czy obraz jest poprawny i wywołuje IMG_IsMainExecutable, aby sprawdzić, czy ślad jest częścią pliku wykonywalnego głównej aplikacji. Jeśli nie, instrument_trace zwraca wynik bez instrumentacji śladu. Uzasadnieniem tego jest to, że podczas profilowania aplikacji zazwyczaj chcemy zliczać kod tylko wewnątrz samej aplikacji, a nie kod w bibliotekach współdzielonych lub dynamicznym ładowaczu. Jeśli ślad jest poprawny i stanowi część głównej aplikacji, instrument_trace wykonuje pętlę po wszystkich blokach podstawowych (obiektach BBL) w śladzie (2). Dla każdego BBL wywołuje instrument_bb (3), który wykonuje faktyczną instrumentację każdego BBL. Aby zinstrumentować dany blok BBL, funkcja instrument_bb wywołuje funkcję BBL_InsertCall (4), która jest funkcją API Pin służącą do zinstrumentowania bloku podstawowego za pomocą wywołania zwrotnego procedury analizy. Funkcja BBL_InsertCall przyjmuje trzy obowiązkowe argumenty: blok podstawowy do instrumentacji (w tym przypadku bb), punkt wstawiania oraz wskaźnik do procedury analizy, którą chcesz dodać. Punkt wstawiania określa, w którym miejscu bloku podstawowego Pin wstawia wywołanie zwrotne analizy. W tym przypadku punktem wstawiania jest IPOINT_ANYWHERE (5), ponieważ nie ma znaczenia, w którym momencie bloku podstawowego aktualizowany jest licznik instrukcji. Pozwala to Pinowi zoptymalizować rozmieszczenie wywołania zwrotnego analizy. Tabela przedstawia wszystkie możliwe punkty wstawiania.

Punkty wstawiania pinów

Punkt wstawiania: Wywołanie zwrotne analizy: Ważność

IPOINT_BEFORE: Przed obiektem zinstrumentowanym: Zawsze ważny

IPOINT_AFTER: Na krawędzi przejścia: Jeśli INS_HasFallthrough ma wartość true (dla rozgałęzienia lub „zwykłej” instrukcji)

IPOINT_ANYWHERE: Dowolne miejsce w obiekcie zinstrumentowanym: Tylko dla TRACE lub BBL

IPOINT_TAKEN_BRANCH: Na krawędzi przejścia rozgałęzienia: Jeśli INS_IsBranchOrCall ma wartość true

Dotyczą one nie tylko podstawowej instrumentacji na poziomie bloku, ale także instrumentacji instrukcji i wszystkich innych szczegółów. Nazwa procedury analizy to count_bb_insns (6), a jej implementację zobaczysz za chwilę. Pin udostępnia typ AFUNPTR, do którego należy rzutować wskaźniki funkcji podczas przekazywania ich do funkcji API Pin.

Po obowiązkowych argumentach funkcji BBL_InsertCall można dodać argumenty opcjonalne do przekazania procedurze analizy. W tym przypadku występuje argument opcjonalny typu IARG_UINT32 (1) o wartości BBL_NumIns. W ten sposób procedura analizy (count_bb_insns) otrzymuje argument UINT32 zawierający liczbę instrukcji w bloku podstawowym, co pozwala na zwiększenie licznika instrukcji w razie potrzeby. Inne typy argumentów zostaną omówione w dalszej części tego i następnego przykładu. Pełny przegląd wszystkich możliwych typów argumentów można znaleźć w dokumentacji Pin. Po przekazaniu argumentów opcjonalnych należy dodać argument specjalny IARG_END (2), aby poinformować Pin, że lista argumentów jest kompletna. Końcowym rezultatem kodu z Listingu jest to, że Pin instrumentuje każdy wykonany blok podstawowy w aplikacji głównej za pomocą wywołania zwrotnego count _bb_insns, które zwiększa licznik instrukcji profilera o liczbę instrukcji w bloku podstawowym.

Instrumentacja bloków podstawowych

https://chacker.pl/

Przypomnijmy, że profiler rejestruje między innymi liczbę instrukcji wykonywanych przez program. W tym celu profiler instrumentuje każdy blok podstawowy za pomocą wywołania funkcji analizy, która zwiększa licznik instrukcji (insn_count) o liczbę instrukcji w bloku podstawowym.

Kilka uwag na temat bloków podstawowych w programie Pin

Ponieważ Pin dynamicznie odkrywa bloki podstawowe, znalezione przez niego bloki podstawowe mogą różnić się od tych, które można znaleźć w oparciu o analizę statyczną. Na przykład, Pin może początkowo znaleźć duży blok podstawowy, a później odkryć skok w środek tego bloku, zmuszając Pin do ponowienia decyzji, podzielenia bloku podstawowego na dwie części i ponownego instrumentowania obu bloków podstawowych. Chociaż nie ma to znaczenia dla profilera, ponieważ nie interesuje go kształt bloków podstawowych, a jedynie liczba wykonanych instrukcji, ważne jest, aby o tym pamiętać, aby uniknąć pomyłki z niektórymi narzędziami Pintools. Należy również pamiętać, że alternatywną implementacją byłoby zwiększanie wartości insn_count dla każdej instrukcji. Byłoby to jednak znacznie wolniejsze niż podstawowa implementacja na poziomie bloku, ponieważ wymaga jednego wywołania zwrotnego na instrukcję funkcji analizy, która inkrementuje insn_count. Natomiast podstawowa implementacja na poziomie bloku wymaga tylko jednego wywołania zwrotnego na blok podstawowy. Podczas pisania narzędzia Pintool ważne jest, aby zoptymalizować procedury analizy tak bardzo, jak to możliwe, ponieważ są one wywoływane wielokrotnie w trakcie wykonywania, w przeciwieństwie do procedur instrumentacji, które są wywoływane tylko przy pierwszym napotkaniu fragmentu kodu.

Symbole funkcji Arsing

https://chacker.pl/

Skoro już wiesz, jak zainicjować Pintool i zarejestrować procedury instrumentacji oraz inne wywołania zwrotne, przyjrzyjmy się szczegółowo właśnie zarejestrowanym funkcjom wywołania zwrotnego. Zacznijmy od parse_funcsyms, pokazanego na Listingu .

Listing : profiler.cpp (ciąg dalszy)

static void

(1){parse_funcsyms(IMG img, void *v)

if(!IMG_Valid(img)) return;

(2)for(SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec)) {

(3) for(RTN rtn = SEC_RtnHead(sec); RTN_Valid(rtn); rtn = RTN_Next(rtn)) {

(4)}}funcnames[RTN_Address(rtn)] = RTN_Name(rtn);

}

Przypomnijmy, że parse_funcsyms to procedura instrumentacji granularności obrazu, co można poznać po tym, że otrzymuje obiekt IMG jako pierwszy argument. Procedury instrumentacji obrazu są wywoływane po załadowaniu nowego obrazu (pliku wykonywalnego lub biblioteki współdzielonej), co pozwala na instrumentację obrazu jako całości. Pozwala to między innymi na pętlenie wszystkich funkcji w obrazie i dodawanie procedur analizy uruchamianych przed lub po każdej funkcji. Należy pamiętać, że instrumentacja funkcji jest niezawodna tylko wtedy, gdy plik binarny zawiera informacje symboliczne, a instrumentacja po funkcji nie działa z niektórymi optymalizacjami, takimi jak wywołania ogonowe. Jednak parse_funcsyms w ogóle nie dodaje instrumentacji. Zamiast tego wykorzystuje inną funkcję instrumentacji obrazu, która pozwala na inspekcję nazw symbolicznych wszystkich funkcji w obrazie. Profiler zapisuje te nazwy, aby móc je później odczytać i wyświetlić w wynikach nazwy funkcji czytelne dla człowieka. Przed użyciem argumentu IMG, parse_funcsyms wywołuje funkcję IMG_Valid, aby upewnić się, że obraz jest poprawny (1). Jeśli tak, parse_funcsyms przechodzi przez wszystkie obiekty SEC na obrazie, które reprezentują wszystkie sekcje (2). IMG_SecHead zwraca pierwszą sekcję obrazu, a SEC_Next zwraca następną sekcję; pętla jest kontynuowana, aż SEC_Valid zwróci wartość false, wskazując, że nie ma już kolejnej sekcji. Dla każdej sekcji parse_funcsyms przechodzi przez wszystkie funkcje (reprezentowane przez obiekty RTN, jak w „rutynach”) (3) i mapuje adres każdej funkcji (zwrócony przez RTN_Address) w mapie funcnames na nazwę symboliczną funkcji (zwróconą przez RTN_Name) (4). Jeśli nazwa funkcji nie jest znana (na przykład, gdy plik binarny nie ma tablicy symboli), RTN_Name zwraca pusty ciąg znaków. Po zakończeniu parse_funcsyms zmienna funcnames zawiera mapowanie wszystkich znanych adresów funkcji na nazwy symboliczne.

Uruchamianie aplikacji

https://chacker.pl/

Ostatnim krokiem inicjalizacji każdego narzędzia Pintool jest wywołanie funkcji PIN_StartProgram, która uruchamia aplikację (9). Po tym nie można już rejestrować żadnych nowych wywołań zwrotnych; narzędzie Pintool odzyskuje kontrolę dopiero po wywołaniu procedury instrumentacji lub analizy. Funkcja PIN_StartProgram nigdy nie zwraca wartości, co oznacza, że ​​wartość zwracana na końcu funkcji main nigdy nie zostaje osiągnięta

Rejestrowanie funkcji Fini

https://chacker.pl/

Ostatnim wywołaniem zwrotnym rejestrowanym przez profiler jest funkcja Fini, wywoływana po zamknięciu aplikacji lub odłączeniu od niej Pin (8). Funkcje Fini otrzymują kod stanu wyjścia (INT32) oraz zdefiniowany przez użytkownika void*. Aby zarejestrować funkcję Fini, należy użyć funkcji PIN_AddFiniFunction. Należy pamiętać, że funkcje Fini mogą nie być wywoływane niezawodnie w niektórych programach, w zależności od sposobu ich zamknięcia. Funkcja Fini rejestrowana przez profiler odpowiada za drukowanie wyników profilowania. Nie będę jej tutaj omawiał, ponieważ nie zawiera ona kodu specyficznego dla Pinów, ale można zobaczyć wynik funkcji print_results podczas testowania profilera.