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

Połączenie assemblera i C++

Ponownie dodaję artykuł zahaczający o temat assemblera. Na własnej skórze doświadczyłem dzisiaj problemów ze wstawką assemblerową w kodzie C++, dlatego postanowiłem stworzyć artykuł, w którym zbiorę w całość kilka często używanych trików. Artykuł jest skierowany dla użytkowników darmowego kompilatora GCC, ponoć w Visual Studio wiele problemów znika.

Assembler w C++

Jeżeli nie jesteś tylko zwykłym klepaczem kodu, który zna na pamięć wszystkie interfejsy oferowane przez Jave, prawdopodobnie nie raz próbowałeś oszukać jakieś gry lub programy – choćby dla rozrywki lub sprawdzania siebie. Po prostej edycji pamięci w końcu przychodzi czas, że nie da się ruszyć dalej bez znajomości assemblera. Assembler daje nieograniczone możliwości podczas analizowania oprogramowania a także w procesie jego wytwarzania.

Osobiście często używam środowiska Code::Blocks, ponieważ w wygodny sposób koloruję składnie i jest lekkie, a nic oprócz kolorowania składni nie potrzebuję. Jego drugą największą zaletą (w połączeniu z GCC) jest fakt, że jest to zestaw całkowicie darmowy. Dość dużym problemem z jakim się spotkałem, są wstawki assemblerowe (inline assembler). Wydaję mi się, że GCC podchodzi do swoich użytkowników trochę niedbale. W tym artykule opiszę kilka często stosowanych zabiegów.

Syntaksa AT&T oraz Intela

Kompilator GCC domyślnie narzuca dla swoich użytkowników syntaksę AT&T, która w moim osobistym odczuciu jest koszmarna. Po dokładnej analizie widzę, że różnice nie wydają się szczególnie wielkie, jednak nauka jednej i drugiej jest dla mnie stratą czasu, tym bardziej kiedy z Assemblera korzystam odświętnie.

W kompilatorze GCC można dodać odpowiednią flagę -masm=intel. Dzięki niej możemy bez (prawie) żadnych ograniczeń korzystać z syntaksy Intela. W tym celu w środowisku Code::Blocks wchodzimy w menu Project > Build Options > Other options. Po dopisaniu flagi zapisujemy zmiany.

Aby dodać do kodu C++ fragment kodu assemblera, posługujemy się rozkazem Asm lub __asm. Kod assemblera musi znajdować się w cudzysłowach. Istnieje też kilka sposobów na osiągnięcie nowych linii:

Powyższe dwie funkcje są identyczne, różni je tylko sposób przejścia do nowych linii oraz formatowanie.

Argumenty funkcji i zmienne lokalne

Tworząc wstawkę assemblera, możemy w prosty sposób uzyskać dostęp do argumentów funkcji C++, w której to wstawka się znajduje oraz zmiennych lokalnych tej funkcji. Dostęp ten zapewnia nam rejestr EBP, który jest wskaźnikiem do segmentu danych na stosie. Dane wyciągamy arytmetycznie zwiększając lub zmniejszając wskaźnik:

  • [EBP – 8] zmienna lokalna 2…
  • [EBP – 4] zmienna lokalna 1…
  • [EBP] wskaźnik segmentu stosu
  • [EBP+4] wskazuje na EIP
  • [EBP+8] argument funkcji 1..
  • [EBP+12] argument funkcji 2..

Poniżej znajdują się dwie te same funkcje, napisane w czystym C++ oraz korzystając ze wstawki assemblera:

Nie musisz zwracać wartości funkcji poprzez return. Wartość zwracana funkcji zawsze znajduje się w rejestrze EAX w każdej konwencji wywołania (stdcall, cdecl, fastcall):

Argumenty funkcji można także umieszczać na stosie, jednak nie zwolni nas to z obowiązku operowania rejestrem EBP a więc nie ma to sensu.

Funkcje w assemblerze

Czasem może się zdarzyć, że będziesz potrzebował napisać funkcję w assemblerze. Jest to zabieg dość prosty, jednak w kompilatorze GCC należy skorzystać z dyrektywy extern „C”. W przeciwnym wypadku funkcja we wstawce assemblera, nie będzie widoczna dla reszty programu w C++.

Tworząc funkcję w assemblerze piszemy w C++ jej sygnaturę. Nie zapominamy aby umieścić ją w klauzuli extern „C”. Następnie we wstawce assemblera definiujemy ją globalnie używając rozkazu .global poprzedzając etykietę funkcji znakiem podkreślenia _. Ostatnim krokiem jest napisanie definicji funkcji w assemblerze:

Ponieważ całą funkcję napisaliśmy sami, niezbędne jest umieszczenie prologu i epilogu funkcji. Kod można nieco skrócić, zastępując prolog i epilog gotowymi funkcjami enter i leave. We wstawce także działają one bez zarzutu:

Nic nie stoi na przeszkodzie aby utworzyć do wstawek assemblerowych normalne wskaźniki funkcyjne:

Podsumowanie

Nie należy się bać korzystania z assemblera. Niesie on ze sobą wiele korzyści, ponieważ daje programiście możliwość tworzenie funkcji naked, które nie są wcale zmieniane przez kompilator. Jest także niezbędny podczas pisania wszelkiego typu trainerów czy hooków.

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

Komentarze:

Użytkownik /pm napisał/a:

09 września 2014


Hej chciałem zapytać, czy znasz może jakieś dobre źródła do nauki assemblera? Polsko- lub angielskojęzyczne i właśnie w takim połączeniu z C++?

Użytkownik Karol napisał/a:

09 września 2014


Nie znam nic godnego polecenia.

Użytkownik Marcin napisał/a:

19 listopada 2014


C++ z assemblerem łączy się najczęściej na poziomie linkera, wstawki assemblerowe są często niewspierane przez kompilatory, a jeśli już to używa się ich właściwie jedynie w przypadku odsłaniania pojedynczych rozkazów SIMD, choć i to jest niepotrzebne, bo wiodące kompilatory (GCC, VC++ oraz Intel C/C++) mają intrinsicsy na wszystkie rozkazy SIMD z najpopularniejszych rozszerzeń procesorów takich jak MMX i SSE aż po SSE4 włącznie.

Użytkownik Marcin napisał/a:

19 listopada 2014


Ale żeby nie było, ciekawy artykuł i miłe, że nie wszyscy się boją asseblera :) Przy okazji uczy trochę konwencji wywołań, a to chyba coś co warto wiedzieć jeśli chce się być poważnym programistą.

Użytkownik f napisał/a:

28 maja 2015


A jakiekolwiek inne źródła do nauki?
Na łączeniu z c++ mi nie zależy

Użytkownik Karol napisał/a:

28 maja 2015


Książek do assemblera są setki. To stary język. Nic konkretnego nie umiem polecić, umiem asma tylko powierzchownie.

Użytkownik Bartosz Wójcik napisał/a:

22 października 2015


Cześć Karol,

Fajny wpis w dobie wszechobecnej Javy, Rustów i innych wynalazków ;), jednak mam kilka uwag:

– w konwencji stdcall wartości zwracane są w parze rejestrów EDX:EAX o ile funkcja zwraca 64 bitową wartość (ULONGLONG / __int64), przykładem takiej funkcji jest np. GetTickCount64() – https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms724411(v=vs.85).aspx

– prolog i epilog funkcji warto dać tylko wtedy, gdy faktycznie wykorzystujesz elementy bazujące na tym, czyli zmienne lokalne, bez tego szkoda instrukcji, a parametry przesłane do funkcji można pobrać bezpośrednio poprzez rejestr ESP, dzięki temu zyskujesz 1 rejestr dodatkowy na dowolne operacje czyli zwykle EBP, jeśli taką funkcję chcesz stworzyć w C++ to nadajesz jej atrybut naked https://msdn.microsoft.com/pl-pl/library/h5w10wxs.aspx

– w twoim przykładzie jest błąd, ponieważ uszkadzasz rejestr EBX, a w standardzie interfejsu STDCALL (WinApi na nim bazuje), cdecl pomiędzy wywołaniami funkcji nie możesz uszkadzać rejestrów ESI, EDI, EBX, EBP (i multimedialnych choć to kwestia sporna), gdyż w ten sposób możesz uszkadzać ważne wartości, które by się w nich znajdowały przed wywołaniem funkcji i z których korzysta dalsza część programu już po wywołaniu twojej funkcji

Użytkownik Łukasz napisał/a:

02 grudnia 2016


Hej

Zamiast potworka „Syntaksa” dałbym po prostu „Składnia”
;)

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!