Jak wiemy, w asemblerze można napisać wszystko. Jednak nie zawsze wszystko trzeba pisać w tym języku. W tej części pokażę, jak asemblera łączyć z innymi językami. Są na to 2 sposoby:
Postaram się z grubsza omówić te dwa sposoby na przykładzie języków Pascal, C i Fortran 77. Uprzedzam jednak, że moja znajomość języka Pascal i narzędzi związanych z tym językiem jest słaba.
Wstawki asemblerowe realizuje się używając słowa asm
. Oto przykład:
{ DOS/Windows }
program pas1;
begin
asm mov eax,4
end;
end.
Można też stosować nieco inny sposób - deklarowanie zmiennej reprezentującej rejestry procesora. Poniższy wycinek kodu prezentuje to właśnie podejście (wywołuje przerwanie 13h z AH=48h, DL=80h, DS:DX wskazującymi na obiekt a):
uses crt,dos;
Var
regs: Registers;
BEGIN
clrscr();
With regs DO
Begin
Ah:=$48;
DL:=$80;
DS:=seg(a);
DX:=ofs(a);
End;
Intr($13,regs);
Teraz zajmiemy się bardziej skomplikowaną sprawą - łączenie modułów napisanych w Pascalu i asemblerze. Pascal dekoruje nazwy zmiennych i procedur, dorabiając znak podkreślenia z przodu. Jakby tego było mało, do nazwy procedury dopisywana jest informacja o jej parametrach. Tak więc z kodu
var c:integer; d:char; procedure aaa(a:integer;b:char);
otrzymujemy symbole: _C, _D oraz _AAA$INTEGER$CHAR.
Oprócz tego, zwykle w Pascalu
argumenty na stos szły od lewej do prawej, ale z tego co widzę
teraz, to Free Pascal Compiler działa odwrotnie
- argumenty idą na stos wspak. W naszym
przykładzie najpierw na stos pójdzie zmienna typu char
, a potem typu
integer
(obie
rozszerzone do rozmiaru DWORDa).
Jedno jest pewne: jeżeli Twoja procedura jest uruchamiana z programu napisanego w
Pascalu, to
Ty sprzątasz po sobie
stos - należy przy wyjściu z procedury wykonać
RET liczba
, gdzie
liczba to rozmiar wszystkich parametrów włożonych na stos (wszystkie parametry są rozmiaru co
najmniej DWORD).
Jeśli to Ty uruchamiasz procedury napisane w Pascalu, to nie musisz
się martwić o zdejmowanie parametrów ze stosu.
Samo dołączanie modułów odbywa się na linii poleceń, najlepiej w tym celu użyć linkera (po uprzednim skompilowaniu innych modułów na pliki obiektowe).
Wstawki asemblerowe zaczynają się wyrażeniem asm {
a kończą klamrą zamykającą }
(ale NIE w gcc, o tym później). Przykład:
asm { mov eax, 1 }
Wszystkie nowe kompilatory produkują programy 32- lub 64-bitowe, przypominam więc, aby we wstawkach NIE używać przerwań ( DOS-a i BIOS-u w Windows).
W C i C++ można, podobnie jak w Pascalu, deklarować zmienne reprezentujące rejestry procesora. Plik nagłówkowy BIOS.H oferuje nam kilka możliwości. Oto przykład:
#include <bios.h> ... REGS rejestry; ... rejestry.x.ax = 0x13; rejestry.h.bl = 0xFF; int86 (0x10, rejestry, rejestry);
Łączenie modułów jest prostsze niż w Pascalu.
Kompilatory języka C dla DOS dekorują nazwy, dodając
znak podkreślenia z przodu.
UWAGA - w języku C++ sprawy są trudniejsze nawet niż w
Pascalu. Dlatego, jeśli chcemy, aby
nazwa naszej funkcji była niezmieniona (poza tym, że ewentualnie dodamy podkreślenie z przodu)
i jednocześnie działała w C++, zawsze przy deklaracji funkcji w pliku nagłówkowym, należy dodać
extern "C"
, na przykład
#ifdef __cplusplus extern "C" { #endif extern void _naszafunkcja (int parametr, char* a); #ifdef __cplusplus } #endif
W systemach 32-bitowych parametry przekazywane są na stosie
OD PRAWEJ DO LEWEJ,
czyli pierwszy parametr (u nas powyżej: int)
będzie włożony na stos jako ostatni, czyli będzie najpłycej
, a ostatni (u nas:
char*)
będzie najgłębiej
.
W systemach 64-bitowych sprawa wygląda trudniej: parametry, w zależności od klasy, są przekazywane (także od prawej do lewej):
Dodatkowo, dla funkcji typu stdarg
(inaczej: vararg
),
czyli dla takich z wielokropkiem w deklaracji, jak np. printf
,
rejestr AL zawiera liczbę rejestrów SSE zużytych na parametry.
W C/C++ to funkcja uruchamiająca zdejmuje włożone parametry ze stosu, a NIE funkcja uruchamiana.
Na systemach 32-bitowych parametry całkowitoliczbowe do 32 bitów zwracane
są w rejestrze EAX (lub jego częściach: AL, AX, w zależności od rozmiaru), 64-bitowe w EDX:EAX,
zmiennoprzecinkowe w ST0. Wskaźniki
w 32-bitowych kompilatorach są 32-bitowe i są zwracane w EAX (w 16-bitowych zapewne w AX).
Struktury są wkładane na stos od ostatnich pól, a jeśli funkcja zwraca strukturę przez wartość, na przykład
struct xxx f ( struct xxx a )
to tak naprawdę jest traktowana jak taka funkcja:
void f ( struct xxx *tu_bedzie_wynik, struct xxx a )
czyli jako ostatni na stos wkładany jest adres struktury, do której ta funkcja ma włożyć
strukturę wynikową.
Na systemach 64-bitowych sprawa ponownie wygląda inaczej. Tu także klasyfikuje się typ zwracanych danych, które są wtedy przekazywane:
Polecam do przeczytania x64 ABI (na przykład dokument x64-abi.pdf, do znalezienia w Internecie).
Dołączanie modułów (te napisane w asemblerze muszą być uprzednio skompilowane) odbywa się na linii poleceń, z tym że tym razem możemy użyć samego kompilatora, aby wykonał za nas łączenie (nie musimy uruchamiać linkera).
No to krótki 32-bitowy przykładzik (użyję NASMa i Borland C++ Builder):
; NASM casm1.asm section .text use32 global _suma _suma: ; po wykonaniu push ebp i mov ebp, esp: ; w [ebp] znajduje się stary EBP ; w [ebp+4] znajduje się adres powrotny z procedury ; w [ebp+8] znajduje się pierwszy parametr, ; w [ebp+12] znajduje się drugi parametr ; itd. %idefine a [ebp+8] %idefine b [ebp+12] push ebp mov ebp, esp mov eax, a add eax, b ; LEAVE = mov esp, ebp / pop ebp leave ret
oraz plik casm.c:
#include <stdio.h> extern int _suma (int a, int b); /* deklaracja funkcji zewnętrznej */ int suma (int a, int b); /* prototyp funkcji */ int c=1, d=2; int main() { printf("%d\n", suma(c,d)); return 0; }
Kompilacja odbywa się tak:
nasm -o casm1.obj -f obj casm1.asm bcc32 casm.c casm1.obj
Uwaga: w kompilatorach GNU: DJGPP, Dev-C++, MinGW, CygWin format wyjściowy NASMa powinien być ustawiony na COFF. Możliwe, że format COFF trzeba będzie wybrać także w innych.
W wyniku otrzymujemy programik, który na ekranie elegancko wyświetla wynik równy 3.
DOS nie uruchomi programów 64-bitowych, ale dla kompletności przedstawiam powyższy kod w wersji 64-bitowej (też dla NASMa):
; NASM - casm1l.asm use64 section .text global suma suma: ; po wykonaniu push rbp i mov rbp, rsp: ; w [rbp] znajduje się stary RBP ; w [rbp+8] znajduje się adres powrotny z procedury ; w rdi znajduje się pierwszy parametr całkowitoliczbowy, ; w rsi znajduje się drugi parametr całkowitoliczbowy, ; w rdx znajduje się trzeci parametr całkowitoliczbowy, ; w rcx znajduje się czwarty parametr całkowitoliczbowy, ; w r8 znajduje się piąty parametr całkowitoliczbowy, ; w r9 znajduje się szósty parametr całkowitoliczbowy, ; w [rbp+16] znajduje się pierwszy parametr wymagający stosu, ; w [rbp+24] znajduje się drugi parametr wymagający stosu ; itd. %idefine a rdi %idefine b rsi push rbp mov rbp, rsp mov rax, a add rax, b ; LEAVE = mov rsp, rbp / pop rbp leave ret
I jeszcze plik casm1l.c:
#include <stdio.h> extern long long int suma (long long int a, long long int b); static long long int c=1, d=2; int main() { printf("%lld\n", suma(c,d)); return 0; }
Może się zdarzyć też, że chcemy tylko korzystać z funkcji języka C, ale główną część
programu chcemy napisać w asemblerze. Nic trudnego: używane funkcje deklarujemy jako
zewnętrzne (pamiętając o znaku podkreślenia), ale
uwaga - swoją funkcję główną musimy nazwać _main
.
Jest tak dlatego, że teraz punkt startu programu nie jest w naszym kodzie, lecz w
samej bibliotece języka C. Program zaczyna się między innymi ustawieniem tablic
argumentów listy poleceń i zmiennych środowiska. Dopiero po tych operacjach biblioteka
C uruchamia funkcję _main
instrukcją CALL.
Inną ważną sprawą jest to, że naszą funkcję główną powinniśmy zakończyć instrukcją RET
(zamiast normalnych instrukcji wyjścia z programu), która
pozwoli przekazać kontrolę z powrotem do biblioteki C, umożliwiając posprzątanie
(na przykład wyrzucenie buforów z wyświetlonymi informacjami w końcu na ekran).
Krótki (także 32-bitowy) przykładzik:
section .text global _main extern _printf _main: ; printf("Liczba jeden to: %d\n", 1); push dword 1 ; drugi argument push dword napis ; pierwszy argument call _printf ; uruchomienie funkcji add esp, 2*4 ; posprzątanie stosu ; return 0; xor eax, eax ret ; wyjście z programu section .data napis: db "Liczba jeden to: %d", 10, 0
Kompilacja powinna odbyć się tak:
nasm -o casm2.obj -f obj casm2.asm bcc32 casm2.obj
64-bitowy przykładzik (też NASM):
use64 section .text global main extern printf main: ; printf("Liczba jeden to: %lld\n", 1); xor al, al ; liczba argumentów wymagających SSE ; w funkcjach varargs mov rsi, 1 ; drugi argument mov rdi, napis ; pierwszy argument call printf ; uruchomienie funkcji ; sprzątanie stosu niepotrzebne ; add rsp, 2*8 ; return 0; xor rax, rax ret ; wyjście z programu section .data napis: db "Liczba jeden to: %lld", 10, 0
Jedna uwaga: funkcje biblioteki C mogą zamazać nam zawartość wszystkich rejestrów (poza EBX, EBP, ESI, EDI w systemach 32-bitowych, i RBX, RBP, R12, R13, R14, R15 na systemach 64-bitowych), więc nie wolno nam polegać na zawartości rejestrów po uruchomieniu jakiejkolwiek funkcji C.
Kompilator GNU gcc wymaga osobnego wytłumaczenia. Składnia wstawek asemblerowych różni się od powyższej dość znacznie, a jej opisy możecie znaleźć w podręczniku GCC (sekcje: 5.34 i 5.35), na stronach DJGPP oraz (w języku polskim) na stronie pana Danileckiego.
Jak zauważycie, różni się nawet sam wygląd instrukcji, gdyż domyślnie gcc używa składni AT&T języka asembler. U siebie mam krótkie porównanie tych składni.
W tym języku nie wiem nic o wstawkach asemblerowych, więc przejdziemy od razu do łączenia modułów.
Fortran 77 dekoruje nazwy, stawiając znak podkreślenia PO
nazwie funkcji lub zmiennej (wyjątkiem jest funkcja
główna - blok PROGRAM - która nazywa się MAIN__
, z dwoma podkreśleniami).
Dodatkowo, kompilatory języka Fortran 77 dla DOS dekorują nazwy, dodając
znak podkreślenia z przodu (podobnie, jak w przypadku języka C).
Tak więc, pod DOS, funkcja główna nazywa się _MAIN__
.
Nie musimy pisać externów, ale jest kilka reguł przekazywania parametrów:
Na przykład, następujący kod:
REAL FUNCTION aaa (a, b, c, i) CHARACTER a*(*) CHARACTER b*(*) REAL c INTEGER i aaa = c END [...] CHARACTER x*8 CHARACTER y*5 REAL z,t INTEGER u t=aaa (x, y, z, u) [...]
będzie przetłumaczony na asemblera tak (samo uruchomienie funkcji):
push 5 push 8 push u_ ; adres, czyli offset zmiennej "u" push z_ push y_ push x_ call _aaa_ ; podkreślenie z przodu dla DOS, z tyłu - dla Fortrana
(to niekoniecznie musi wyglądać tak ładnie, gdyż zmienne x
, y
, u
i z
są lokalne w funkcji MAIN__, czyli są na stosie, więc ich adresy mogą wyglądać jak
[ebp-28h] lub podobnie).
Funkcja uruchamiająca sprząta stos po uruchomieniu (podobnie jak w C).
Dołączać moduły można bezpośrednio z linii poleceń (w każdym razie pod Linuksem z kompilatorem F77/G77).
Podam teraz przykład łączenia Fortrana 77 i asemblera. W oryginale użyłem narzędzi Linuksowych: NASMa i F77, ale po minimalnych przeróbkach powinno to też działać pod Windows. Oto pliki:
; NASM - asm1fl.asm section .text use32 global _suma_ _suma_: ; podkreślenie z przodu dla DOS, z tyłu - dla Fortrana ; po wykonaniu push ebp i mov ebp, esp: ; w [ebp] znajduje się stary EBP ; w [ebp+4] znajduje się adres powrotny z procedury ; w [ebp+8] znajduje się pierwszy parametr, ; w [ebp+12] znajduje się drugi parametr ; itd. %idefine a [ebp+8] %idefine b [ebp+12] push ebp mov ebp, esp ; przypominam, że nasze parametry są w rzeczywistości ; wskaźnikami do prawdziwych parametrów mov edx, a ; EDX = adres pierwszego parametru mov eax, [edx] ; EAX = pierwszy parametr mov edx, b add eax, [edx] ; LEAVE = mov esp, ebp / pop ebp leave ret
I teraz plik asmfl.f:
PROGRAM funkcja_zewnetrzna INTEGER a,b,suma a=1 b=2 WRITE (*,*) suma(a,b) END
Po skompilowaniu (ewentualnie zmieniając opcję -f
u NASMa):
nasm -f obj -o asm1fl.obj asm1fl.asm f77 -o asmfl.exe asmfl.f asm1fl.obj
i uruchomieniu na ekranie powinna ponownie pojawić się cyfra 3.
W wersji 64-bitowej obowiązują powyższe reguły przekazywania parametrów, a pozostałe reguły są takie same, jak dla języka C.
Ponownie, dla kompletności, przedstawiam powyższy program w wersji 64-bitowej:
; NASM - asm2fl.asm use64 section .text global suma_ suma_: ; po wykonaniu push rbp i mov rbp, rsp: ; w [rbp] znajduje się stary RBP ; w [rbp+8] znajduje się adres powrotny z procedury ; w rdi znajduje się pierwszy parametr całkowitoliczbowy, ; w rsi znajduje się drugi parametr całkowitoliczbowy, ; w rdx znajduje się trzeci parametr całkowitoliczbowy, ; w rcx znajduje się czwarty parametr całkowitoliczbowy, ; w r8 znajduje się piąty parametr całkowitoliczbowy, ; w r9 znajduje się szósty parametr całkowitoliczbowy, ; w [rbp+16] znajduje się pierwszy parametr wymagający stosu, ; w [rbp+24] znajduje się drugi parametr wymagający stosu ; itd. %idefine a rdi %idefine b rsi push rbp mov rbp, rsp ; przypominam, że nasze parametry są w rzeczywistości ; wskaźnikami do prawdziwych parametrów mov rdx, a ; EDX = adres pierwszego parametru mov rax, [rdx] ; EAX = pierwszy parametr mov rdx, b add rax, [rdx] ; LEAVE = mov rsp, rbp / pop rbp leave ret
I teraz plik asm2fl.f:
PROGRAM funkcja_zewnetrzna INTEGER a,b,suma a=1 b=2 WRITE (*,*) suma(a,b) END
Co do innych języków, jeśli kompilator posiada taką opcję, można spróbować wygenerować kod asemblerowy i z niego dowiedzieć się, jaka jest umowa (konwencja) przekazywania parametrów, np. dla kompilatora GNU C:
gcc -S plik.c
Można też poszukać takich informacji w Internecie.
Informacji podanych w tym dokumencie NIE
należy traktować jako uniwersalnych, jedynie
słusznych reguł działających w każdej sytuacji
. Aby uzyskać kompletne informacje, należy
zapoznać się z dokumentacją posiadanego kompilatora.