Artyku³ ten zaadresowany jest do osób chc±cych
po³±czyæ wysoki
poziom jêzyka Pascal z efektywno¶ci± Assemblera - jêzykiem
niskiego poziomu. Nie trudno jest zauwa¿yæ korzy¶ci wynikaj±ce z
tego zwi±zku. Szybko¶æ w grafice i obliczeniach matematycznych,
zwiêz³o¶æ i ma³a objêto¶æ kodu wynikowego, no i panowanie nad wszystkimi
elementami komputera. Za to Pascal wnosi ³atwo¶æ
pisania interfejsów dla aplikacji i udogodnienia wynikaj±ce z wysokiego
poziomu jêzyka. Wady we wstawkach assemblerowych
widzê tylko dwie, odziedziczone po samym Assemblerze:
trudno¶æ w pisaniu w jêzyku niskiego poziomu i nieczytelno¶æ
kodu.
Wiêc je¶li potrzebujesz du¿ej szybko¶ci, precyzji dzia³ania i
totalnej kontroli nad swoimi programami pisanym w Pascalu, to
nie widzê przeszkód aby wykorzystaæ wstawki Assemblera. No chyba,
¿e nie wiesz nic o programowaniu w Assemblerze. Wtedy
prawdopodobnie nie zrozumiesz tego tekstu, to nie jest kurs dla
(t)opornych typu "Nie tylko dla or³ów" ;) Powiniene¶
najpierw
przeczytaæ jakie¶ kursy dla pocz±tkuj±cych. Taka wiedza wystarczy
aby dobrze zrozumieæ tre¶æ tego artyku³u. Co trudniejsze rzeczy
wyt³umaczê.
Je¶li jaki¶ fragment kodu nie chce siê skompilowaæ na twoim kompilatorze,
to albo zosta³ wyrwany z kontekstu, albo u¿ywamy innej implementacji
jêzyka Pascal. Moja to Borland Turbo Pascal 7 - wersja szkoleniowa
;)
1. O Assemblerze.
Przypomnê najwa¿niejsze rzeczy, jakie bêd± nam potrzebne,
aby w ogóle skorzystaæ ze wstawek. Nie jest tego du¿o ale jest
to nieodzowna czê¶æ wstawek Assemblerowych (no mo¿e z wyj±tkiem
kilku zagadnieñ, które wstawi³em jako ciekawostkê ;).
a) Skoki warunkowe
Instrukcje do procesora przekazywane s± w kolejno¶ci
wystêpowania w pamiêci po kolei. Instrukcja, wskazywana przez
CS:IP (lub CS:EIP dla programów 32 bitowych w trybie chronionym
procesora - trudno jednak jest znale¼æ implementacjê Pascala,
która to obs³uguje, wiêc nie bêdê tego tu omawia³) przekazywana
jest procesorowi, nastêpnie zwiêkszana jest warto¶æ IP o wielko¶æ
poprzedniej instrukcji, tak aby wskazywa³ na nastêpn±. Sêk w tym,
¿e "trudno" jest zmieniæ samemu warto¶æ IP (z za³o¿enia
jest on
nie dostêpny dla programisty, lecz mo¿na to omin±æ ;). Aby manipulowaæ
skokami procesora w pamiêci, mo¿na wykorzystaæ
skoki warunkowe lub bezwarunkowe.
Ogólna instrukcja skoku bezwarunkowego ma postaæ:
JMP @SKACZ
//to omijamy
@SKACZ:
//to robimy
Jest to jedna z czê¶ciej wykorzystywanych instrukcji. Inne skoki
(ju¿ warunkowe) to: JA, JB, JE (je¿eli nich nie pamiêtasz, to
powtórz materia³ z innych kursów ;), wykorzystuje siê je tak samo
jak skoki bezwarunkowe:
CMP AX, BX
JA @WIEKSZE
//no i co ?
@WIEKSZE
//rób co¶
Jak zapewne zauwa¿y³e¶, samo tworzenie etykiet - jak i skoki
- wygl±daj± tak samo jak w standardowym kompilatorze
Assemblera. My¶lê, ¿e nie powinno byæ z ich obs³ug± we wstawkach
problemów.
Skoków warunkowych jest wiêcej, wybra³em moje ulubione. Warto
tak¿e podkre¶liæ, ¿e na nowszych procesorach (Pentium) te skoki
zajmuj± tylko jeden takt procesora.
b) Stos
Sama obs³uga stosu we wstawkach wygl±da tak samo, jak w
czystym Assemblerze. Z t± ró¿nic±, ¿e tu nie trzeba martwiæ siê
deklaracj± stosu, ani przepe³nieniem. O wszystko zatroszczy siê
kompilator - najczê¶ciej program zakañcza siê z komunikatem przepe³nienia
stosu ;)
Mam nadziejê, ¿e ka¿dy pamiêta zastosowanie i obs³ugê instrukcji
PUSH reg, POP reg, PUSHF, POPF, PUSHA, POPA. Generalna zasada
to: "ile razy push`ujesz, tyle pop`ujesz" ;). Je¿eli
nie zdejmiesz push`owanej warto¶ci ze stosu, to wywo³ana funkcja
nie powróci
w miejsce wywo³ania. Najgorsze jest to, ¿e trudno jest wykryæ
takie b³êdy (co sprytniejsi pisz± programy do zliczania liczby
PUSH`ów i POP`ów w kodzie ¼ród³owym ;).
c) Wywo³ywanie procedur
Aby we wstawce wywo³aæ procedurê bez argumentów, napisan±
w Assemblerze lub w Pascalu, stosuje siê instrukcjê CALL
nazwa. Np.:
//...
MOV AL, BH
OUT 42h, AL.
//delay
CALL NOSOUND
Nie ma w tym wiele filozofii. Problemy mog± byæ w wywo³ywaniu
funkcji (lub procedur) z argumentami (lub parametrami - bardziej
znane okre¶lenie dla Pascala), a pó¼niej w pobraniu wyników od
nich. Ale o tym pó¼niej.
Trzeba jednak pamiêtaæ, ¿e ka¿de skoki procesora w inny segment
zajmuj± cenne takty, im wiêcej takich wywo³añ, tym wolniej dzia³a
program. Tak samo nie mo¿na przedobrzyæ ze przerwaniami.
d) Koprocesor (FPU)
FPU (Floating Point Unit) jest bardzo przydatny gdy chcemy
zwiêkszyæ szybko¶æ wykonywania obliczeñ matematycznych. Jego instrukcje
najczê¶ciej mo¿na spotkaæ we wstawkach Assemblera.
Nie bêdê wymienia³ listy komend koprocesora. Trzeba tylko
pamiêtaæ, ¿e na pocz±tku stawia siê F..., pó¼niej nazwa instrukcji
(czêsto analogiczna do instrukcji CPU) np. FADD. Gdy chcemy u¿yæ
liczb ca³kowitych (co zreszt± jest nie naturalne dla koprocesora,
musi on wykonaæ konwersjê do liczb rzeczywistych - co zajmuje
czas) dodajemy po F.. I(i) np.: FIADD.
Przed u¿yciem FPU trzeba go najpierw zainicjowaæ (zresetowaæ)
(FINIT). Czêsto jest to jednak pomijane.
2. Co z tymi wstawkami?
Przejd¼my do sedna sprawy. S± trzy mo¿liwe sposoby pisania wstawek.
Wszystkie maj± swoje plusy i minusy ;)
a) Asm statement
Nie jest to polska nazwa, ale nigdzie nie widzia³em innej
(wprawdzie tekstu o wstawkach Assemblera te¿ nigdzie nie widzia³em
;).
Wstawka ta ma postaæ:
//tekst w Pascalu
Asm
//instrukcje, nie potrzeba ¶rednika
End;
//tekst w Pascalu
Np.:
Begin
WriteLn('Przed asm...');
Asm
Mov AX, BX
Mov CX, BX; SHL AX, CX {komentarz}
End;
WriteLn('Po end;');
End.
Ten przyk³ad praktycznie nic nie robi. Ale ilustruje sposób u¿ycia
wstawek ;) Zauwa¿, ¿e nie trzeba stawiaæ ¶redników po
pojedynczej instrukcji (w jednej linijce). Gdy chcemy wstawiæ
wiele
instrukcji w jednej linijce trzeba je odseparowaæ ¶rednikiem.
Komentarze maj± styl Pascala.
Ten typ wstawek wykorzystuje siê zw³aszcza, gdy chcemy w kodzie
Pascala, bez wiêkszej zabawy, u¿yæ instrukcje Assemblera. Mo¿na
je wykorzystaæ wszêdzie: w g³ównym bloku, w funkcjach i procedurach.
Ich liczba nie jest ograniczona, mog± nastêpowaæ po sobie.
Ma ona jednak swoje minusy. Rozwa¿my przyk³ad:
Asm
Mov AX, BX
Jmp @ETYKIETA
End;
WriteLn('Tekst');
Asm
@ETYKIETA:
Mov DS, AX
End;
WriteLn('Tekst');
Wbrew pozorom ten kod nie skompiluje siê. Etykiety stawiane w
jednym bloku asm-end nie s± znane w innych blokach.
W tych wstawkach mog± byæ dowolnie modyfikowane nastêpuj±ce rejestry
CPU: AX, BX, CX, DX, SI, DI, ES i flagi. Poprzedni przyk³ad
mia³ jeszcze jeden b³±d. Rejestr DS zosta³ zmieniony (choæ samo
w sobie to nie jest b³±d), ale jego warto¶æ nie zosta³a przywrócona
do pierwotnego stanu. Nastêpna instrukcja WriteLn() prawdopodobnie
wypisa³a by bzdury - dla niej DS mia³ wskazywaæ
na segment w którym jest 'Tekst'. Mo¿na siê przed tym uchroniæ
stosuj±c PUSHA, np.:
Asm
PUSHA
Mov AX, 01234h
Mov DS, AX
POPA
End;
WriteLn('Tekst2');
Teraz wszystko jest ok. Rejestr DS nie jest jedynym, którego wykorzystuje
Pascal, gdy inicjuje wstawki. Reszta to: BP, SP, SS,
no i DS. Te regu³y odnosz± siê te¿ do reszty wstawek.
b) Funkcja Assemblerowa
Jest to rodzaj wstawki, który mo¿e wystêpowaæ tylko w funkcjach
i procedurach. Obejmuje ca³± funkcjê (procedurê). Ma nastêpuj±ce
w³a¶ciwo¶ci: nie jest generowany kod inicjacji zmiennych, je¿eli
nie wystêpuj± parametry, nie jest inicjowany stos, odwo³anie siê
do @Result jest b³êdem (o @Result pó¼niej).
Jako, ¿e parametry nie s± inicjowane wszystkie s± traktowane
tak,
jak by mia³y znacznik VAR i nie mog± byæ zmieniane (a przynajmniej
nie powinny).
A oto jak wygl±da Funkcja Assemblerowa:
Function Nazwa(x,y :integer); Assembler;
Asm
Mov AX, x
End;
Zaraz po ¶redniku wystêpuje dyrektywa Assembler informuj±ca kompilator
o rodzaju funkcji. Zamiast Begin wystêpuje s³owo
kluczowe Asm. Funkcja koñczy siê - jak ka¿da - End`em.
c) Inline
Instrukcja Inline jest przeznaczona do umieszczania w programie
kodu maszynowego. Pos³ugiwanie siê funkcj± inline (tak naprawdê
to nie jest funkcja, tylko tak wygl±da ;) jest bardzo skomplikowane,
czasoch³onne i podatne na b³êdy. Ponadto nigdy nie znalaz³em sensownego
zastosowania dla niej. Kod maszynowy bardziej wygodnie - jak dla
mnie - jest wstawiaæ w zwyk³y blok Assemblera
(o tym pó¼niej).
Zdecydowa³em siê ni± tu opisaæ, poniewa¿ czêsto pojawia siê we
wstawkach Pascala. Oto przyk³ad takiej instrukcji:
Begin
inline(
$B8/$00/$4C/ {mov ax, 4c00h}
$CD/$21 {int 21h}
);
Write('Co??'); {tu nigdy nie dojdzie ;}
End.
Najpierw opiszê przyk³ad, nastêpnie wyja¶niê konstrukcjê instrukcji
inline.
W kodzie Pascala ca³a instrukcja inline (ka¿da z osobna oczywi¶cie)
jest traktowana jako ca³o¶æ. Nie wykonuje (podczas np.: debbuging`u)
pojedynczo ka¿dej fizycznej instrukcji procesora
tylko wszystkie na raz - ca³y blok inline. Tutaj napisa³em
maszynowo instrukcjê DOS`a zamykaj±c± program. Procedura
Write() nigdy nie zostanie wykonana.
Wa¿ne: Kompilator ca³± zawarto¶æ instrukcji inline "wszywa"
w kod programu NIE sprawdzaj±c jej zawarto¶ci. Niewa¿ne jakie
bzdury napiszemy, zawsze zostanie skompilowane. (Mo¿na to oczywi¶cie
wykorzystaæ do pisania najnowszych instrukcji naszego procesora
w starych kompilatorach Pascala).
Konstrukcja instrukcji inline jest bardzo prosta. Ilo¶æ danych
jest nieograniczona.
Inline(kod/kod
kod/kod);
Wciêæ nie musimy robiæ, tak jak komentarzy, s± dla naszej wygody.
3. Zwracanie wyniku funkcji.
Ze wstawkami czêsto tak jest, ¿e w 80% przypadków s± to
funkcje. Rozwa¿my nastêpuj±cy przyk³ad:
Function Dodaj(a, b:word):word;
Var temp:word;
Begin
Asm
Mov ax, a
Mov bx, b
Add ax, bx
Mov temp, ax
End;
Dodaj := temp;
End;
Jest to oczywi¶cie poprawny kod, ale ma swoje wady. Po pierwsze
trzeba tworzyæ zmienn± tymczasow± (w tym wypadku Temp).
Mo¿e to byæ nie po¿±dane, gdy operujemy na du¿ych zmiennych. Po
drugie trzeba przerywaæ Asm-end; Tak¿e nie wygl±da to najlepiej
;)
a) @Result
Podstawow± form± zwracania wyniku z funkcji, w której
zastosowano Asm-statement jest zmienna @Result . Jest ona
powi±zana referencj± do zmiennej trzymaj±cej wynik funkcji.
U¿ywa siê jej tak, jakby by³ zwyk³± zmienn±. Mo¿na wykonywaæ
na niej wszystkie operacje matematyczne. Jej typ jest ustalany
na podstawie typu funkcji. Mo¿na wiêc napisaæ:
function dodaj(a,b :single):single;
begin
asm
fld a {w³o¿enie na stos zmiennej 'a'}
fld b
fadd
fst @Result
{zdjêcie wierzcho³ka stosu do zmiennej @Result}
end;end;
Powiedzmy wprost. Konstruktorzy Pascala nie mogli wymy¶liæ nic
lepszego nad @Result.
b) Funkcje Assemblerowe
Sprawa nie wygl±da ju¿ tak prosto i wspaniale w odniesieniu do
funkcji Assemblerowych. Tutaj konstruktorzy Pascala dali plamê
wycofuj±c referencjê @Result. Pewnie mieli jakie¶ powody, ale
ja
nie znalaz³em ¿adnej dobrej strony tej decyzji (lub niedopatrzenia).
Z tego powodu jednak nie mo¿na zrezygnowaæ z funkcji Assemblerowych,
trzeba daæ sobie radê inaczej ;)
Zwracanie wyniku zale¿y od jego typu.
Typy 8-bitowe zwraca siê w rejestrze AL. Zmienne 16-bitowe
zwraca siê w rejestrze AX. Natomiast wyniki 32-bitowe zwraca
siê w parze rejestrów DX-AX. Tzn.: DX jest starszym s³owem
zmiennej, AX m³odszym. Gdy zwraca siê liczbê rzeczywist±,
to wynik pobierany jest ze szczytu stosu koprocesora.
Je¿eli funkcja zwraca typ String, to nale¿y zdj±æ ze stosu dwie
warto¶ci, najpierw segment, potem offset. Razem to tworzy adres
do Stringa. (Pamiêtaj, ¿e na pocz±tku Stringa jest bajt okre¶laj±cy
ilo¶ci± elementów).
Na pocz±tek wydaje siê to skomplikowane, w rzeczywisto¶ci tylko
zwracanie zmiennych 32-bitowych przysparza ma³e k³opoty.
Poka¿ê jeszcze ma³± funkcjê prezentuj±c± g³ówn± ideê:
function dodaj(a,b :word):word;Assembler;
asm
mov cx, a
mov bx, b
add cx, bx
mov ax, cx {Wynik w AX}
end;
4. Obs³uga funkcji we wstawkach.
Poprzednio przy omawianiu instrukcji CALL, nie wspomnia³em
(zreszt± specjalnie) jak wywo³ywaæ funkcje (lub procedury) z
parametrami. Teraz napiszê o wszystkim, co jest zwi±zane z
obs³ug± funkcji we wstawkach.
a) Wywo³ywanie funkcji / procedur z parametrem.
Ogólnie argumenty funkcj± i procedur± podaje siê poprzez
po³o¿enie ich na stos. Pierwsza po³o¿ona na stos warto¶æ zostaje
przypisana pierwszemu argumentowi. Czasem jednak jest
odwrotnie (tak jak w C++), w du¿ej mierze zale¿y to od
implementacji. Jednak gdy kompilator jest 100% zgodny ze
standardem, to kolejno¶æ jest taka, jak± poda³em na pocz±tku
(np.: Borland Turbo Pascal).
b) Odbieranie wyniku funkcji
Je¿eli jeszcze nie wiesz dobrze jak zwracaæ wynik z funkcji,
to przypomnij sobie to. Odebranie wyniku funkcji polega na
odwróceniu poprzedniego procesu ;)
Pisz±c kod wywo³uj±cy funkcjê trzeba znaæ typ, jaki ona zwraca
i
gdzie go zwraca (patrz 3.b ). Nie ma tu wiele do wyja¶niania -
popatrzmy wiêc na przyk³ad.
asm
push 1000d {pierwszy argument 'a'}
push 2000d {drugi argument 'b'}
call dodaj {wywo³anie naszej funkcji 'dodaj'}
mov c, ax {odebranie wyniku do zmiennej 'c'}
end;
writeln( c );
5. Instrukcje 32-bitowe.
Stare wersje Turbo Pascala, jak i innych kompilatorów, nie
pozwalaj± na u¿ywanie najnowszych instrukcji procesorów
(np.: MMX, itp). Wiêkszo¶æ implementacji nie "zna" nawet
wszystkich instrukcji procesorów z rodziny 30486.
Najpro¶ciej jest za³atwiæ sobie nowy kompilator lub uaktualnienia.
Jednak nie zawsze mo¿na, np. gdy chcemy zachowaæ zgodno¶æ z poprzednimi
wersjami? W³a¶nie tym teraz siê zajmiemy.
Dla rozwiania w±tpliwo¶ci, nawet je¶li zakodujemy pewn±
instrukcjê dostêpn± dopiero od procesora np.: Pentium, to na wcze¶niejszych
procesorach program najprawdopodobniej siê
zawiesi lub wykona parê innych instrukcji, gdy procesor jej nie
zna.
a) Pisanie i wykorzystanie instrukcji 32-bitowych.
Przyjrzyjmy siê przyk³adowej procedurze Pascala.
Procedure ClearSeg(Color:byte;var MSeg); Assembler;
Asm
mov AX, word ptr [MSeg+2]
mov ES, AX
mov Al, Color
mov ah, al
mov bx, ax
db 66h
shl ax, 16 {shl eax, 16}
mov ax, bx
mov DI, 0
mov CX, 16080
db 0F3h, 66h, 0ABh; {Rep Stosd}
end;
Pewnie ju¿ zauwa¿y³e¶ nieznane ci (jeszcze) znaczki wtr±cone do
kodu ;) S± to reprezentacje instrukcji w kodzie maszynowym
(rozumianych przez cz³owieka). 'DB' oznacza jeden bajt. Mo¿na
tak¿e wykorzystywaæ 'DW', 'DD', itd. Jednak gdy u¿ywany danych
wiêkszych od 'DB' to trzeba odwracaæ warto¶ci (co bajt). Sprawa
jeszcze bardziej komplikuje siê gdy chcemy u¿yæ danych
wiêkszych, np.: 'DD'. Po oznaczeniu wielko¶ci wpisywanych
instrukcji (najlepiej zawsze u¿ywaj 'DB') trzeba wypisaæ kod
instrukcji maszynowych, oddzielonych przecinkami.
'Db 66h' oznacz ¿e w tym miejscu pamiêci, miêdzy instrukcj±
mov bx, ax a shl ax, 16, zostanie umieszczony bajt o warto¶ci
66h. Ten bajt razem z instrukcj± shl ax, 16 zostanie przeczytany
przez procesor jako shl eax, 16. W tym przypadku wykorzystanie
rejestru
EAX przy¶pieszy³o program - w innym wypadku trzeba by
pisaæ wiêcej instrukcji.
Ci±g 'Db 0F3h, 66h, 0Abh' jest instrukcj± Rep Stosd, tak¿e nie
dostêpn± w wiêkszo¶ci implementacji, gdy¿ wykorzystuje rejestr
EAX.
Szczególnie wa¿ne jest wykorzystanie instrukcji 32-bitowych w
operacjach na koprocesorze, gdy¿ tam liczy siê czas.
Wcze¶niej pisa³em o odwracaniu kodu, teraz poka¿ê przyk³ad.
Db 0DAh, 0E9h {fucompp}
Dw 0E9DAh {fucompp}
Oba te zapisy przeczytane zostan± jako instrukcja koprocesora
'fucompp'.
Pisz±c w kodzie maszynowym bardzo wa¿ny jest odpowiedni komentarz.
Po pewnym czasie zapis 'db 26h, 67h, 66h, 8Bh, 03h' bêdzie dla
nas nic nie znacz±cym zapisem, którego NA PEWNO nie bêdziesz rozumia³!
b) Sk±d braæ kod maszynowy.
Najpierw co to jest kod maszynowy? S± to instrukcje dla
procesora w postaci kodu binarnego (lub hex'ów jak kto woli).
Zwyk³y cz³owiek nie jest w stanie zapamiêtaæ takich rozkazów
(np.: 8CC8h), ³atwiej jest zapamiêtaæ mnemoniki (np.: mov ax,
cs) wymy¶lone specjalnie dla nas.
Kod maszynowy mo¿emy ³atwo zaczerpn±æ z ró¿nych debugerów
(np.: Turbo Debuger ).
Aby to zrobiæ nale¿y w³±czamy debugera i wybraæ opcje
Assemble... z manu podrêcznego. Nastêpnie wpisujemy komendê, której
kod chcemy spisaæ (musi to byæ w miarê nowy debuger). I spisujemy
zwrócon± komendê w postaci hex'ów, pojawi siê na lewo od
naszej instrukcji (o ile nie wyst±pi b³±d). Tak¿e w ró¿nych ksi±¿kach
mo¿na znale¼æ kod maszynowy instrukcji.
Gdy potrzebujemy wiêkszy kawa³ kodu, to wpisywanie wszystkich
instrukcji by³o by co najmniej pracoch³onne. W takim wypadku
potrzebujemy kompilatora Assemblera (np.: Turbo Assembler i
Turbo Linkera). Kompilujemy nim tylko nasze instrukcje w
Assemblerze (mog± byæ wyjête z kontekstu), najlepiej do postaci
czystego kodu (*.COM). Nastêpnie nasz plik wynikowy otwieramy
programem typu Hex Edit (np.: Hex Workshop) i kopiujemy
wybrany fragment do kodu Pascala. Taki kod maszynowy najlepiej
w³o¿yæ w klauzurê Inline(). Oczywi¶cie TRZEBA opisaæ w
komentarzach kod maszynowy, aby go rozumieæ po jakim¶ czasie!