P-Programowanie
Tekst
zmniejsz/powiększ
Kolory
jasne/ciemne/kontrast/brak

Konwencje wywoływania funkcji

Krótki artykuł opisujący trzy podstawowe konwencje wywoływania funkcji C++ (a jest ich więcej). Konwencje wywoływania funkcji nie są tematem, na który można się szeroko rozpisać, jednak należy znać i odróżniać ich podstawowe rodzaje, szczególnie bawiąc się w reverse engineering.

Informacje wstępne

Jak to zwykle bywa, do napisania artykułu skłoniło mnie zapotrzebowanie czytelników. Na wielu forach internetowych można przeczytać o problemach osób, które nie radzą sobie z edycją jakiś prostych funkcji (chodzi o edycję na poziomie debbugera).

Zahaczając o temat inżynierii wstecznej konwencje wywoływania funkcji należy znać. O ile operowanie nimi pisząc program w C++ nie niesie większych zmian widocznych gołym okiem, o tyle ciało funkcji zmienia się podczas procesu debugowania.

Wskaźnik stosu

Praktycznie całe zamieszanie dotyczące konwencji wywoływania funkcji kręci się w okół wskaźnika stosu.

Stos jest liniową strukturą danych używaną przez procesor, do przechowywania zmiennych lokalnych, zapamiętywania stanów rejestrów oraz do przekazywania argumentów do funkcji. Dane dodane na stos jako ostatnie, muszą być ściągnięte jako pierwsze.

Rejestr ESP wskazuje na wierzchołek stosu. Aby lepiej zobrazować sytuację, wyobraź sobie rejestr ESP jako wskaźnik z jakąś wartością arytmetyczną. Jeżeli ESP wynosi 512, podczas dodania słowa na stos rozkazem POP wskaźnik stosu powiększy się z 512 na 508. Arytmetyczna wartość jest mniejsza, ale rejestr ESP rośnie w stronę zera, aż na końcu nastąpi przepełnienie stosu.

Wywołując dowolny CALL wrzucamy argumenty na stos rozkazami PUSH. Argumenty zawsze wrzucamy w odwrotnej kolejności, wynika to z zasady odczytywania ze stosu. W chwili kiedy program zaczyna wykonywać instrukcję CALL, wrzuca ona na stos adres powrotu, z którego później skorzysta funkcja RET. Biorąc ten fakt pod uwagę, niezbędne jest zachowanie porządku w rejestrze ESP wskazującym wierzchołek stosu. Po wrzuceniu kilku argumentów dla rozkazu CALL, niezbędne jest ściągnięcie ich ze stosu rozkazami POP lub arytmetyczne przesunięcie wskaźnika stosu ESP. W przeciwnym razie po wywołaniu CALL (skoku) do dowolnego miejsca, program nie znajdzie instrukcji powrotu.

Ponieważ pojedyncze słowo word jest 32 bitowe, zwiększamy wskaźnik stosu (właściwie zmniejszamy, ponieważ rośnie on w stronę zera) o 4 bajty. Dla dwóch argumentów, trzeba zmniejszyć wskaźnik stosu już o 8 bajtów itd.

Konwencje wywoływania funkcji

W różnych konwencjach wywoływania funkcji stos jest czyszczony z argumentów w różny sposób.

  • stdcall – stos za każdym razem czyści funkcja wywoływana
  • cdecl – stos za każdym razem czyści obiekt, na rzecz którego wywołujemy funkcje
  • fastcall – nie korzystamy ze stosu

Konwencja stdcall

Jak podaje MSDN, konwencja stdcall jest standardową, jeżeli chodzi o wywoływania funkcji Win32 API. Oznacza to, że nawet jeżeli nie wybierzemy jawnie żadnej konwencji, funkcja automatycznie zostanie wywołana z konwencją stdcall. Argumenty przekazywane do funkcji są standardowo umieszczane na stosie.

Cechą charakterystyczną jest to, że funkcja sama sprząta stos po jej wywołaniu. Stos czyszczony jest podczas zakończenia funkcji przez rozkaz retn x, gdzie x oznacza ilość parametrów na stosie. Oto ten sam program przepisany do assemblera (FASM):

Widać, że wskaźnik stosu został przesunięty podczas kończenia funkcji. Zaletą stdcall jest szybkość działania i mniejsza ilość zajętego miejsca. Jeżeli funkcję wywołujemy dziesiątki razy w różnych miejscach programu, stos i tak czyszczony jest tylko raz wewnątrz funkcji.

Podczas debugowania kodu, możemy zauważyć, że funkcje w konwencji stdcall zaczynają się od znaku podkreślenia, na końcu dodawana jest małpa oraz ilość przekazywanych do funkcji argumentów:

Funkcje stdcall rozpoznajemy właśnie po tym, że zawsze na ich końcu znajduje się retn x lub ret x.

Konwencja cdecl

Konwencja cdecl jest drugą najczęściej spotykaną konwencją wywoływania funkcji. Była bardzo często wykorzystywana w języku C. Charakteryzuje się tym, że stos musi zostać wyczyszczony przez program w miejscu wywołania funkcji, a nie przez samą funkcję. Argumenty do funkcji przekazywane są na stosie:

Ponieważ stos nie jest czyszczony wewnątrz funkcji podczas jej zakończenia (tak jak w przypadku stdcall), może się okazać, że kod programu znacznie wzrośnie. Stanie się tak przede wszystkim wtedy, jeżeli będziemy posiadać wiele wywołań funkcji.

Szczególnym zastosowaniem konwencji cdecl jest sytuacja, kiedy wywołujemy funkcję ze zmienną ilością parametrów. Przykładem takiej funkcji jest np. printf(). Ponieważ, możemy umieścić w niej dowolną ilość argumentów, funkcja nie może zajmować się czyszczeniem stosu – nigdy nie wie ile parametrów zostanie do niej przekazane. Czyszczeniem stosu, czyli przewijaniem wskaźnika stosu, zajmuje się obiekt, na rzecz którego została wywołana funkcja cdecl:

Podczas debugowania zauważysz, że funkcje cdecl zaczynają się od znaku podkreślenia:

Funkcje cdecl rozpoznajemy po tym, że na ich końcu ret nie posiada żadnego argumentu, jednak nie można pomylić jej z fastcall. Aby tego nie zrobić, trzeba się upewnić, że argumenty nie są pobierane z rejestrów.

Konwencja fastcall

Generalną zasadą konwencji fastcall, jest przekazywanie argumentów poprzez rejestry, a nie poprzez stos tak jak w wypadku innych funkcji. To jakie rejestry będą używane, zależy od kompilatora – wszystkie używają innych standardów. Zarówno GCC jak i MVC działają w tym wypadku na tej samej zasadzie. Dwa pierwsze parametry przesyłane są kolejno w rejestrach ECX oraz EDX, a wszystkie następne już ze stosu (jeśli zajdzie potrzeba wykorzystania większej ilości parametrów).

Używając fastcall program może znacznie przyśpieszyć, w przypadku wywoływania wielu funkcji z małą ilością parametrów. Program nie operuje wtedy na pamięci, a jedynie na rejestrach, które są szybsze.

Używając fastcall stosu nie czyści ani obiekt wywołujący ani sama funkcja wywoływana. Podczas debugowania zauważysz, że funkcje fastcall są poprzedzone znakiem małpy, a na ich końcu znajduje się ponownie znak małpy oraz liczba argumentów:

Podsumowanie

Istnieje kilka innych konwencji wywoływania funkcji w języku C++, lecz nie są one tak często używane jak trzy opisane wyżej. Mam nadzieję, że po przeczytaniu tego prostego artykułu z większą łatwością będziesz w stanie modyfikować różne funkcje zewnętrznych aplikacji, czy to podczas debugowania procesu czy zabaw z dll injection.

Nie utrzymanie porządku we wskaźniku stosu ESP zawsze skończy się niespodziewanym crashem aplikacji.

Udostępnij ten artykuł na fejsie lub zostaw komentarz!

Komentarze:

Użytkownik Piotrek napisał/a:

09 sierpnia 2014


Świetny artykuł. Czekam na więcej. Pozdrawiam.

Użytkownik Domeny napisał/a:

27 lipca 2015


Również czekam na kontynuacje!

Użytkownik Kaczus napisał/a:

21 października 2015


Taka uwaga – to nie jest konwencja wywołania w C++, ale w dowolnym języku programowania i jest to związane z systemem operacyjnym oraz platformą sprzętową. Zaskoczę Cię – te same konwencje możesz uzyć w Pascalu pod Windowsem.

Użytkownik Karol napisał/a:

21 października 2015


@Kaczus
Nie zaskoczyłeś mnie, generalnie archaiczny Pascal mnie nie interesuje. Ja zaskoczę Ciebie, nie we wszystkich językach programowania konwencje wywoływania funkcji działają tak samo. Dla frameworka .NET domyślną konwencją jest stdcall a dla C cdecl. Dodatkowo we frameworku .NET nie masz bezpośredniej kontroli nad tym w jaki sposób wywołasz funkcję (oprócz zasobów importowanych z bibliotek napisanych np. w C++ w celu zapewnienia integralności z kodem niezarządzalnym przez Garbage Collector). Kolejną różnicą jest fakt, że assembler dobrze wspólgra z C++ ponieważ jest jego niższą warstwą, jezyka C# nie potraktujesz nijak assemblerem. Możesz kombinować z edytowaniem kodu CIL jednak nie wiem jaki będzie efekt i czy w ogóle są do tego narzędzia.
Byłbym ostrożny z podciągnięciem tematu do dowolnych języków programowania.

Użytkownik gienio napisał/a:

19 lutego 2017


wreszcie jakiś mądry artykuł bez pierduł pseudo mądrych magików

Zachęcam Cię do zostawienia komentarza!

Ilość znaków: 0

Zachęcam Cię do polubienia bloga na facebooku! Dając lajka wspierasz moją pracę - wszystkie artykuły na blogu są za darmo!