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:
{ Linux używa składni AT&T do asemblera - jak zauważycie, argumenty instrukcji są odwrócone. } program pas1; begin asm movl $4,%eax 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ącym 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ę słowami __asm (
a kończą nawiasem zamykającym )
.
W Linuksie wyglądają one nieco dziwnie i to nie tylko ze względu na odwrotną
składnię
__asm ("movl $4,%eax\n" "movb $0xff,%bl\n");
Jak widać, po każdej instrukcji trzeba dać znak przejścia do nowej linii (w jednej linii może
być tylko 1 instrukcja asemblera). Można dorzucić też znak tabulacji \t
.
Wygląd bloków __asm
jest złożony. Po szczegóły odsyłam do stron przeznaczonych temu
zagadnieniu. W szczególności, możecie poczytać podręcznik GCC (sekcje: 5.34 i 5.35),
strony DJGPP oraz (w języku polskim)
stronę pana Danileckiego.
U siebie też mam krótkie porównanie tych składni.
W C i C++ można, podobnie jak w Pascalu, deklarować zmienne reprezentujące rejestry procesora. Plik nagłówkowy BIOS.H (niestety tylko w Windows) 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.
Język zwykle C dekoruje nazwy, dodając znak podkreślenia z przodu,
ale nie w Linuksie, gdzie po prostu nic nie jest dorabiane.
W Linuksie deklaracja funkcji zewnętrznej wygląda po prostu tak:
extern void naszafunkcja (int parametr, char* parametr2);
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ą 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 (od LEWEJ do PRAWEJ):
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 (GCC), aby wykonał za nas łączenie (nie musimy uruchamiać linkera LD).
Teraz krótki 32-bitowy przykładzik (użyję NASMa i GCC):
; NASM - casm1l.asm ; use32 nie jest potrzebne w Linuksie, ale też nie zaszkodzi 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
I jeszcze plik casml.c:
#include <stdio.h> extern int suma (int a, int b); int c=1, d=2; int main() { printf("%d\n", suma(c,d)); return 0; }
Kompilacja wygląda tak:
nasm -f elf casm1l.asm gcc -o casm casml.c casm1l.o
Po uruchomieniu programu na ekranie pojawia się oczekiwana cyfra 3.
Jeśli NASM wyświetla ostrzeżenie odnośnie atrybutu use32
(np.
Unknown section attribute 'use32' ignored on declaration of section `.text'
),
można go usunąć lub przenieść na górę pliku:
use32 section .text
Ten sam 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 int suma (long int a, long int b); static long int c=1, d=2; int main() { printf("%ld\n", suma(c,d)); return 0; }
Kompilacja wygląda tak:
nasm -f elf64 casm1l.asm gcc -o casm casm1l.c casm1l.o
Po uruchomieniu programu na ekranie pojawia się oczekiwana cyfra 3.
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, 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.o -f elf casm2.asm gcc -o casm2 casm2.o
64-bitowy przykładzik (też NASM):
use64 section .text global main extern printf main: ; printf("Liczba jeden to: %ld\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: %ld", 10, 0
Kompilacja powinna odbyć się tak:
nasm -o casm2l.o -f elf casm2l.asm gcc -o casm2l casm2l.o
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.
W tym języku nie wiem nic o wstawkach asemblerowych, więc przejdziemy od razu do łączenia modułów.
Fortran 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).
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_
(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 kompilatorem F77/G77).
Podam teraz przykład łączenia Fortrana 77 i asemblera (użyję NASMa i F77):
; NASM - asm1fl.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 ; 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:
nasm -f elf asm1fl.asm g77 -o asmfl asmfl.f asm1fl.o
i uruchomieniu, na ekranie ponownie pojawia się cyfra 3.
I ponownie, jeśli NASM wyświetla ostrzeżenie odnośnie atrybutu use32
,
można go usunąć lub przenieść do osobnej linii na górę pliku:
use32 section .text
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.
Powyższy program w wersji 64-bitowej wyglądałby to tak:
; 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
Po skompilowaniu (NASM i GNU Fortran):
nasm -f elf64 asm2fl.asm gfortran -o asm2fl asm2fl.f asm2fl.o
i uruchomieniu, na ekranie też pojawia się cyfra 3.
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.