Programowanie hybrydowe łączenie C/C++ z asemblerem
Konwencje Wywoływanie procedur asemblerowych w kodzie języka wysokiego poziomu wymaga: Ustalenia konwencji nazewniczej używanej w języku programowania i stwierdzenia, czy uwzględnia ona reguły związane z nazewnictwem zmiennych i procedur (np. czy asembler lub kompilator nie zmieniają nazw identyfikatorów w pliku.obj, a jeśli tak to w jaki sposób?) Stosowania nazw segmentów kompatybilnych z nazwami używanymi przez język wysokiego poziomu Pamiętania, że użyty w programie model pamięci (tiny, small, compact, medium, large, huge lub flat) decyduje o rozmiarze segmentu (16 lub 32 bity) i rodzaju odwołań (bliskie wewnątrz tego samego segmentu; dalekie pomiędzy segmentami)
Konwencja wywoływania procedur Dotyczy niskopoziomowych aspektów wywoływania procedur i wymaga określenia: Rejestrów, które należy zachować przy wywołaniu procedury Sposobu przekazywania argumentów do procedury: W rejestrach Na stosie Przez współdzieloną pamięć Inną metodą Porządku przekazywania argumentów do procedury Czy argumenty są przekazywane przez rejestry, czy przez referencję W jaki sposób odtwarzana jest zawartość wskaźnika stosu po powrocie z procedury Sposobu zwracania wyników do wywołującego programu
Konwencje nazewnicze i zewnętrzne identyfikatory Wywoływanie procedur języka asemblerowego z programu napisanego w języku wysokiego poziomu wymaga zgodności nazewniczej identyfikatorów zewnętrznych (nazwy, które są umieszczane w pliku.obj w taki sposób, że linker może udostępnić je innym modułom programu) linker rozpoznaje odwołania do identyfikatorów zewnętrznych, ale tylko wtedy, gdy używane konwencje nazewnicze są spójne Przykład: Program w języku C (np. main.c) wywołuje zewnętrzną procedurę nazywaną ArraySum, kompilator zachowuje wielkość liter i automatycznie dodaje podkreślenie na początku nazwy tj. w tym przypadku zmienia ją na _ArraySum Napisany w języku asemblerowym moduł Array.asm eksportuje nazwę procedury ArraySum jako ARRAYSUM, ponieważ w module Array.asm zastosowano dyrektywę.model określającą opcję języka Pascal Efekt: linker zwraca błąd powód: różnice wyeksportowanych nazw Starsze języki programowania, jak Cobol czy Pascal konwertują identyfikatory do wielkich liter; nowsze języki (np. C, C++, Java) zachowują wielkość liter w identyfikatorach; języki, w których można przeciążać funkcje (np. C++) stosują technikę zwaną dekorowaniem nazw dodającą dodatkowe znaki do nazw funkcji (np. Funkcja MyFunction(int a, double b) może zostać wyeksportowana jako MyFunction#int#double.
Nazwy segmentów Linkując procedurę napisaną w języku asemblerowym z programem napisanym w języku wysokiego poziomu nazwy segmentów muszą być zgodne Stosowanie uproszczonych dyrektyw segmentowych.code,.stack i.data pozwala zachować kompatybilność z nazwami segmentów generowanymi przez kompilator Microsoft C/C++
Modele pamięci Zarówno program, jak i wywoływana procedura zewnętrzna muszą używać tego samego modelu pamięci W trybie rzeczywistym do wyboru są następujące modele small, medium, compact, large oraz huge W trybie chronionym dostępny jest jedynie model flat
Dyrektywa.MODEL W trybach 16 i 32-bitowych MASM stosuje dyrektywę.model by określić m.in.: model pamięci, schemat nazywania procedur, konwencję przekazywania argumentów do procedury Składnia:.MODEL model_pamięci [,opcje]
Model pamięci W 16-bitowym trybie rzeczywistym Tiny pojedynczy segment zawierający kod i dane (pliki.com) Small pojedynczy segment danych i pojedynczy segment kodu, domyślnie cały kod i dane są bliskie Medium wiele segmentów kodu i pojedynczy segment danych Compact pojedynczy segment kodu i wiele segmentów danych Large wiele segmentów danych i kodu Huge tak samo jak large, ale pojedyncze dane mogą rozmiarem przekraczać rozmiar segmentu W 32-bitowym trybie chronionym Flat - 32-bitowe przesunięcie (offset) dla kodu i danych wszystkie dane i kod (łącznie z zasobami systemowymi) w pojedynczym 32-bitowym segmencie (rozmiar do 4 GB)
Opcje modelu Drugi (opcjonalny) parametr dyrektywy.model może zawierać: Informację o stosowanej konwencji wywołania procedur i konwencji nazewniczej dla procedur i symboli publicznych wg języka wysokiego poziomu (np. C, BASIC, FORTRAN, COBOL, PASCAL, STDCALL) Odległość odwołań do stosu: NEARSTACK domyślna FARSTACK
STDCALL Używany przy wywoływaniu funkcji systemowych MS Windows Przekazywanie argumentów odbywa się przez stos w odwróconym porządku np. dla wywołania funkcji Dodaj(5,6) wygenerowany zostanie kod: push 6 push 5 call Dodaj powrót z procedury wymaga zdjęcia ze stosu również argumentów dlatego na końcu procedury musi być instrukcja RET z operandem określającym liczbę bajtów zajmowanych przez argumenty procedury np. RET 8 Eksportowana na zewnątrz (do pliku.obj) nazwa procedury jest modyfikowana do: _nazwa@nn (np. _Dodaj@8) nn określa liczbę bajtów używaną przez argumenty procedury (zaokrągloną w górę do wielokrotności 4) linker rozróżnia wielkość liter w nazwach aby obejrzeć nazwy wszystkich procedur z pliku *.OBJ należy użyć narzędzia DUMPBIN z opcją /SYMBOLS
C Używany w językach C/C++ Wywołanie procedury tak samo jak w STDCALL (argumenty w odwrotnej kolejności na stosie) Zdejmowanie argumentów ze stosu po zakończeniu procedury leży po stronie programisty dla procedury Dodaj (5,6) czyszczenie stosu z argumentów wymaga zastosowania instrukcji add esp,8 Eksportowana nazwa procedury to _nazwa (np. _Dodaj)
Kontrola wygenerowanego kodu Opcje kompilatora związane z generowaniem kodu asemblerowego: z poziomu linii poleceń: /FA wyłącznie plik z kodem asemblera /FAc kod asemblera + kod maszynowy /FAs kod asemblera + kod źródłowy /FAcs kod asemblera + kod maszynowy + kod źródłowy z poziomu środowiska Visual Studio Tools Options Debugging General Enable address-level debugging Assembler Output = (np. Assembly With Source Code) Favour Size Or Speed = (np. Favour fast code) optymalizacja prędkości Optimization = Disabled ze względu na zastosowanie debugera (kod zoptymalizowany jest mniej czytelny dla użytkownika)
Linkowanie 32-bitowego kodu w asemblerze z kodem w C/C++ Asembler: W kodzie źródłowym asemblera należy określić model FLAT, C oraz zdefiniować prototyp dla każdej procedury wywoływanej później z poziomu programu w C/C++ Prototyp dla przykładowej funkcji IndexOf z trzema argumentami (srchval, arrayptr, count) która w tablicy zawierającej count elementów (32-bitowe wartości) wyszukuje pierwszego elementu o wartości równej srchval i zwraca indeks tej komórki jako arrayptr (lub -1, gdy element taki w tablicy nie występuje) IndexOf PROTO, srchval:dword, arrayptr:ptr DWORD, count:dword
Linkowanie 32-bitowego kodu w asemblerze z kodem w C/C++ C: zewnętrzną funkcję należy zadeklarować jako extern extern long IndexOf( long n, long array[], unsigned count ); C++: zadeklarować zewnętrzną funkcję jako extern i wymusić stosowanie konwencji nazewniczej języka C (C++ ze względu na możliwość przecążania funkcji stosuje dekorowanie nazw) extern C int IndexOf( long n, long array[], unsigned count );
Przykład moduł asm dla IndexOf.586.model flat,c IndexOf PROTO, srchval:dword, arrayptr:ptr DWORD, count:dword.code IndexOf PROC USES ecx esi edi,srchval:dword, arrayptr:ptr DWORD, count:dword NOT_FOUND = -1 mov eax,srchval mov ecx,count mov esi,arrayptr mov edi,0 L1: cmp [esi+edi*4],eax je found inc edi loop L1 notfound: mov al,not_found jmp short exit found: exit: IndexOf mov eax,edi ret ENDP END
Program w C++ z procedurą IndexOf #include <iostream> #include <time.h> #include "indexof.h" using namespace std; int main() { // Fill an array with pseudorandom integers. const unsigned ARRAY_SIZE = 100000; const unsigned LOOP_SIZE = 100000; char* boolstr[] = {"false","true"}; long array[array_size]; for(unsigned i = 0; i < ARRAY_SIZE; i++) array[i] = rand(); long searchval; time_t starttime, endtime; cout << "Enter an integer value to find: "; cin >> searchval; cout << "Please wait...\n"; // Test the Assembly language function. time( &starttime ); int count = 0; for( unsigned n = 0; n < LOOP_SIZE; n++) count = IndexOf( searchval, array, ARRAY_SIZE ); bool found = count!= -1; time( &endtime ); cout << "Elapsed ASM time: " << long(endtime - starttime) << " seconds. Found = " << boolstr[found] << endl; } return 0;
Wywoływanie funkcji C/C++ Funkcje języka C/C++ można wywoływać z poziomu języka asemblerowego Szczególnie przydatne gdy: Programista chce skorzystać z funkcji we/wy Programista chce skorzystać z funkcji matematycznych np. liczenie pierwiastków Wywoływanie funkcji C/C++ wymaga uruchomienia programu z funkcji main() w celu prawidłowego zainicjowania kodu bibliotecznego
Prototypy funkcji Wywoływanie funkcji C++ z poziomu języka asemblerowego wymaga definicji z opcją C oraz extern Składnia extern C nazwafunkcji (lista_parametrów) { } Przykład: extern C int wczytajliczbę () { cout << Podaj liczbę z zakresu 1..100: ; // } Zamiast zmieniać definicje wszystkich funkcji wywoływanych z poziomu języka asemblerowego można ująć w jednym bloku extern C prototypy tych funkcji Przykład: extern C { } int wczytajliczbę (); int dodajint(int a,int b); //
Moduł asemblerowy Dyrektywa.model flat, STDCALL umożliwia wywoływanie procedur WIn32 API, ale nie jest kompatybilna z mechanizmem wywoływania funkcji języka C/C++ Deklarowanie zewnętrznych funkcji języka C/C++ wewnątrz takiego programu asemblerowego wymaga zastosowania kwalifikatora C z dyrektywą PROTO Przykład: INCLUDE biblioteka.inc wczytajliczbę PROTO C dodajint PROTO C, a: SDWORD, b:sdword
Moduł asemblerowy cd. Procedury asemblerowe wywoływane z poziomu języka C/C++ również muszą używać kwalifikatora C Przykład: SetTextOutColor PROC C, color: DWORD SetTextOutColor ENDP Jeśli moduł asemblerowy korzysta z procedur asemblerowych z innych modułów, to korzystanie z konwencji C wymaga po każdym wywołaniu procedury usuwania parametrów ze stosu
Wartości zwracane przez funkcję języka C/C++ Sposób zwracania wartości nie jest określony w specyfikacji języka C/C++ Należy sprawdzić w dokumentacji kompilatora W MS Visual C++: bool i char w AL. short int w AX int i long int w EAX wskaźniki w EAX float, double, long double odkładane na stosie zmiennopozycyjnym jako 4, 8 lub 10 bajtowe wartości