6. Programowanie w asemblerze
6.1 Pliki źródłowe
Każdy język programowania traktuje pliki o określonym rozszerzeniu jako swoje pliki źródłowe, to jest pliki, w których wpisany jest program, a które po przetworzeniu na drodze asemblacji lub kompilacji stają się plikami uruchamialnymi programów. W asemblerze pliki źródłowe powinny posiadać rozszerzenie *.asm. Do utworzenia pliku o takim rozszerzeniu może posłużyć dowolny edytor tekstu pozwalający na zachowanie pliku w postaci tekstowej, a więc może to być program edit.com, ncedit.exe, notepad.exe, itp. W takim pliku tekstowym należy wpisać program spełniający wszystkie wymagania co do składni, a następnie poddać go procesowi przetwarzania na wersję uruchamialną.
6.2. Narzędzia programistyczne
Język asembler jest językiem niskiego poziomu, toteż narzędzia jakie programista ma do dyspozycji są na ogół proste. Można wyróżnić trzy główne programy, jakie są niezbędne do napisania i przetworzenia programu w asemblerze. Są to:
tasm.exe - (asemblacja) program ten służy do analizy programu źródłowego pod kątem poprawności ze składnią języka. Użycie tego programu jest następujące:
tasm nazwa_pliku.asm
Jeżeli program jest napisany poprawnie, pojawi się stosowny komunikat informujący nas o tym fakcie oraz powstanie plik o takiej samej nazwie, jaką posiada plik źródłowy i rozszerzeniu *.obj. W przeciwnym wypadku pojawią się komunikaty o błędach wyświetlające numer linii, w której popełniono błąd i zdawkową informację na czym ten błąd polega. Aby błąd poprawić należy edytować plik źródłowy, odszukać błędną linię, poprawić błąd i powtórzyć operację asemblacji.
tlink.exe - (konsolidacja) program ten służy do utworzenia wersji uruchamialnej programu na podstawie pliku obiektowego (*.obj). Na ogół w prostych zastosowaniach wywołanie tego programu zawsze zakończy się sukcesem i utworzeniem pliku o takiej samej nazwie, jak plik obiektowy i rozszerzeniu (najczęściej) *.exe o ile proces asemblacji przebiegł bezbłędnie. Użycie tego programu jest następujące:
tlink nazwa_pliku.obj
td.exe - (praca krokowa) programu tego używa się w sytuacji, gdy udało się utworzyć program uruchamialny, ale nie działa on poprawnie, tzn. jest w nim jakiś błąd nie składniowy, lecz funkcjonalny. Wówczas wykorzystujemy narzędzie td.exe do pracy krokowej, sprawdzając krok po kroku jak działa program, jakie wartości przyjmują wybrane zmienne, itp. Użycie tego programu jest następujące:
td nazwa_pliku.exe
Aby jednak plik *.exe mógł być uruchomiony z programem td.exe, należy dołączyć do niego pewne dodatkowe informacje. Aby tak się stało, należy dokonać procesu asemblacji i konsolidacji w sposób następujący:
tasm /zi nazwa_pliku.asm
tlink /v nazwa_pliku.obj
Odpowiednie opcje (/zi oraz /v) programów tasm.exe i tlink.exe pozwolą na dodanie do pliku wynikowego *.exe tych informacji, których potrzebuje program td.exe. Obsługę programu td.exe należy opanować we własnym zakresie.
6.3. Elementy składni
Każdy język programowania wymaga odpowiedniej organizacji pliku źródłowego, tj. odpowiedniego rozmieszczenia elementów języka, zachowania kolejności pewnych sekcji, użycia odpowiednich słów kluczowych, aby zawartość pliku źródłowego nie została uznana za list do ulubionej koleżanki, ale za program źródłowy danego języka. Tak jest też i w przypadku asemblera. W pliku źródłowym powinny się znaleźć następujące elementy (idąc od góry):
a) .model nazwa_modelu - określenie modelu pamięci. W prostych zastosowaniach możliwe modele pamięci to small i tiny.
b) .stack rozmiar_stasu_w_bajtach - określenie rozmiaru stosu dla programu. W prostych zastosowaniach wystarczy 512-bajtowy stos.
c) .data - ta dyrektywa rozpoczyna część programu, w której deklaruje się zmienne.
d) .code - ta dyrektywa rozpoczyna część programu, w której wpisuje się kod.
e) end - kończy program w sensie struktury.
Ad a)
Przyjęcie modelu pamięci determinuje, ile miejsca w pamięci pozostaje na program, dane i stos. Dla modelu tiny wszystkie segmenty są łączne, to znaczy, że program, stos i dane muszą się zmieścić w jednym segmencie, przy czym na program przypada 32kB pamięci, a łącznie na dane i stos także 32kB. Tak więc ten typ pamięci służy do pisania małych programów operujących na niewielkiej liczbie danych.
Ad b)
Stos powinien być w programie zadeklarowany, ponieważ nawet jeżeli programista świadomie go nie używa, to stos jest wykorzystywany podczas wywołań podprogramów lub podczas realizacji przerwań programowych. Jeżeli nie ma stosu, program konsolidujący wyświetli ostrzeżenie.
Ad c)
Trudno sobie wyobrazić program nie operujący na danych, toteż dane trzeba zadeklarować. W asemblerze generalnie można mówić o trzech typach danych:
typ całkowity
typ łańcuchowy
typ tablicowy
Pierwszy z typów jest traktowany jako ciąg bitów w pamięci, które to bity mogą opisywać liczby różnych typów: liczby binarne proste, liczby binarne U2 i liczby rzeczywiste. Z tym, że to programista powinien wiedzieć, jak interpretować zawartość zmiennej (jakiego typu jest zawartość zmiennej). Deklaracje tego typu zmiennej odbywa się w następujący sposób:
nazwa_zmiennej rozmiar wartosc_pocz
przy czym w miejscu rozmiar pojawić się może:
- DB - oznaczać to będzie, że wartości zmiennej będą zapisywane w pamięci na jednym bajcie,
- DW - oznaczać to będzie, że wartości zmiennej będą zapisywane w pamięci na dwóch bajtach,
- DD - oznaczać to będzie, że wartości zmiennej będą zapisywane w pamięci na czterech bajtach.
- wartosc_pocz - początkowa wartość zmiennej (może być dowolna). Nadanie wartości jest konieczne, bowiem dopiero w chwili nadania wartości zmiennej zostaje jej przydzielona w pamięci odpowiednia liczba bajtów.
Przykład.
liczba DW 0
Drugi z typów służy tylko i wyłącznie do deklaracje ciągu znaków, które w następstwie będą wyświetlane na ekranie. Deklaracja tego typu zmiennej odbywa się w sposób następujący:
nazwa_zmiennej DB `Tu wpisz wyświetlany tekst$'
Bardzo istotne jest postawienie znaku `$' na końcu wyświetlanego tekstu, bowiem funkcja wyświetlająca znak po znaku z tego ciągu znaków zakończy ich wyświetlanie, gdy napotka znak `$'.
Przykład.
tekst db `Ala ma kota$'
Trzeci z typów (tablicowy) umożliwia deklarację w programie zmiennych, które będą zajmowały w pamięci pewną liczbę kolejnych bajtów (słów, podwójnych słów), czyli tablicę. Sposób deklaracji tego typu zmiennych jest następujący:
nazwa_zmiennej rozmiar liczba_elementów dup(`znak wypełniający tablicę')
Przykład.
tab db 100 dup(`0')
Ad d), e)
Pomiędzy dyrektywami .code i end wpisujemy program. To ta część naszego pliku źródłowego będzie skojarzona z rejestrem segmentowym CS.
6.4. Wywołania systemowe
Tak się składa, że programiści bardzo często nie piszą wszystkiego w asemblerze na piechotę, lecz korzystają z faktu, że wiele rzeczy można zrealizować wywołując pewne usługi BIOS-u lub DOS-u. Chodzi tu po prostu o zasadę, że nie wyważa się drzwi otwartych, czyli że np. jeżeli ktoś chce wyczyścić ekran, to nie zabiera się do tego w ten sposób, że bezpośrednio do pamięci ekranu zapisuje 64kB spacji tylko wywołuje odpowiednią usługę i ekran jest wyczyszczony. Pozostaje już tylko odpowiedź na pytanie jak te usługi wywoływać. W telegraficznym skrócie rzecz ma się następująco:
Podczas startu systemu tworzona jest w pamięci tzw. tablica wektorów przerwań zawierająca adresy w pamięci, gdzie znajdują się rzeczone wcześniej usługi. Tworzenie tej tablicy odbywa się dwuetapowo, tzn. część adresów usług wpisuje do niej BIOS, a reszta jest dopisywana po fakcie startu systemu operacyjnego DOS. Sięgnięcie do odpowiedniej usługi jest możliwe jedynie w przypadku wywołania przerwania. A że los zdarzył, że oprócz przerwań sprzętowych (generowanych przez urządzenia wchodzące w skład systemu komputerowego) występuje także grupa przerwań programowych (te może sobie generować dowolny, nawet średnio zaawansowany programista za pomocą instrukcji INT). W momencie wystąpienia przerwania następuje odwołanie do tablicy wektorów przerwań pod adres numer_przerwania*4, tam odnajdowane są cztery bajty adresu procedury obsługi danego przerwania, czyli upragnionej usługi. Niektóre przerwania mają wiele usług i wtedy należy dodatkowo oprócz wywołania
INT nr_przerwania
podać numer usługi w ramach tego przerwania do zrealizowania. Np. przerwanie 10h to jest zbiór procedur obsługi ekranu. Można za ich pomocą zrealizować całe mnóstwo operacji od odczytu pozycji kursora na ekranie po zdefiniowanie własnej palety kolorów. Usługi te mają swoje numery i wybór konkretnej usługi w ramach danego przerwania odbywa się poprzez podanie jej numeru. Rejestrem, który jest zawsze domyślnie sprawdzany w celu określenia numeru usługi jest rejestr AH. Chcąc zatem wywołać usługę numer 10h przerwania 10h należy to zrobić w sposób następujący:
mov ah,10h
int 10h
Czasem do wywołania określonej usługi należy podać pewne dodatkowe parametry w innych rejestrach procesora, chociażby aby ustawić pozycję kursora na ekranie trzeba w jakiejś parze rejestrów tą pozycję podać. Czasem z kolei w wyniku wykonania określonej usługi zwracane są pewne parametry do wybranych rejestrów procesora, chociażby wywołując usługę odczytującą pozycję kursora na ekranie, usługa ta do jakichś rejestrów wpisze odczytaną pozycję kursora. Zmierzam do tego, że zawsze dobrze jest dokładnie poczytać:
Co przerwanie robi.
Czy przerwanie ma wiele usług, bo jeżeli tak, to istotny będzie wybór usługi do realizacji.
Czy do poprawnego wykonania usługi nie trzeba określić dodatkowych parametrów.
Czy w wyniku wykonania usługi nie są zwracane wyniki, bo jeżeli tak, to może przed wywołaniem tej usługi konieczne będzie zachowanie zawartości rejestrów, które usługa modyfikuje wpisując wyniki swojego działania.
Wszystkie powyższe kroki należy przeprowadzić zanim wywołamy przerwanie.
Bardzo istotne jest to, że numery przerwań poniżej 21h są to przerwania BIOS-u, natomiast powyżej 21h, to usługi DOS-u. Na ogół jest tak, że usługi DOS-u są dublowane przez usługi BIOS-u i odwrotnie. Słowem jest wiele dróg do szczęścia w postaci działającego programu.
6.5. Pierwszy program
Pierwszy program w asemblerze może wyglądać następująco:
.model small
.stack 512
.code
mov ah,4ch
int 21h
end
Powyższy program należy zasemblować:
tasm nazwa_programu.asm
tlink nazwa_programu.obj
Powstały plik *.exe można uruchomić. Powyższy program ma dwie zasadnicze zalety, tj. nic nie robi oraz nie zawiesza komputera, będąc przy tym w pełni zgodnym ze składnią języka.
6.6. Makra
Przy wielokrotnym powtarzaniu się różniących się nieznacznie fragmentów programu można sobie ułatwić życie stosując makra, to jest rzeczywisty ciąg operacji jakiemu podlegają abstrakcyjne zmienne przeciążane wirtualnie w momencie wywołania poprzez wykorzystanie mechanizmów inline tj. wplatania (he! he!). Lepiej (tego jestem pewien) wyjaśni to przykład:
Program 1
;definicja stalej oznaczajacej numer koloru
BIALY EQU 15
.model small
.stack 512
.data
txt1 db 'Tekst 1',10,13,'$'
txt2 db 'Tekst 2',10,13,'$'
txt3 db 'Tekst 3',10,13,'$'
txt4 db 'Tekst 4',10,13,'$'
.code
;ustalenie segmentu danych (dobrze, żeby to było zaraz na poczatku programu)
mov ax,@data
mov ds,ax
;ustawienie aktywnej strony (usluga BIOS-u: przerwanie 10h, funkcja 05h)
mov al,0 ;numer strony
mov ah,05h
int 10h
;wyczyszczenie ekranu (usluga BIOS-u: przerwanie 10h, funkcja 06)
mov al,0
mov ch,0 ;lewy gorny wiersz okna
mov cl,0 ;lewa gorna kolumna okna
mov dh,25 ;prawy gorny wierz okna
mov dl,80 ;prawa gorna kolumna okna
mov bh,BIALY
mov ah,06h
int 10h
;ustawienie kursora na poczatku (usluga BIOS-u: przerwanie 10h, funkcja 02h)
mov dh,0 ;wiersz
mov dl,0 ;kolumna
mov bh,0 ;numer strony
mov ah,02h
int 10h
;wyswietlenie pierwszego tekstu (usluga DOS-u: przerwanie 21h, funkcja 09h)
lea dx,txt1
mov ah,09h
int 21h
;wyswietlenie drugiego tekstu (usluga DOS-u: przerwanie 21h, funkcja 09h)
lea dx,txt2
mov ah,09h
int 21h
;wyswietlenie trzeciego tekstu (usluga DOS-u: przerwanie 21h, funkcja 09h)
lea dx,txt3
mov ah,09h
int 21h
;wyswietlenie czwartego tekstu (usluga DOS-u: przerwanie 21h, funkcja 09h)
lea dx,txt4
mov ah,09h
int 21h
;czekanie na wcisniecie klawisza (usluga DOS-u: przerwanie 21h, funkcja 01h)
mov ah,01h
int 21h
;zakonczenie programu (usługa DOS-u: przerwanie 21h, funkcja 4ch)
mov ah,4ch
int 21h
end
Powyższy program zajmuje 43 linie wraz z komentarzami. Gdyby nie te właśnie komentarze, to byłby on mało nieczytelny. A teraz taki sam program, ale wykorzystujący makra...
Program 2
;definicja stalej oznaczajacej numer koloru
BIALY EQU 15
.model small
.stack 512
.data
txt1 db 'Tekst 1',10,13,'$'
txt2 db 'Tekst 2',10,13,'$'
txt3 db 'Tekst 3',10,13,'$'
txt4 db 'Tekst 4',10,13,'$'
clrscr macro
;ustawienie aktywnej strony (usluga BIOS-u: przerwanie 10h, funkcja 05h)
mov al,0 ;numer strony graficznej
mov ah,05h
int 10h
;wyczyszczenie ekranu (usluga BIOS-u: przerwanie 10h, funkcja 06)
mov al,0
mov ch,0 ;lewy gorny wiersz okna
mov cl,0 ;lewa gorna kolumna okna
mov dh,25 ;prawy gorny wierz okna
mov dl,80 ;prawa gorna kolumna okna
mov bh,BIALY
mov ah,06h
int 10h
;ustawienie kursora na poczatku (usluga BIOS-u: przerwanie 10h, funkcja 02h)
mov dh,0 ;wiersz
mov dl,0 ;kolumna
mov bh,0 ;numer strony graficznej
mov ah,02h
int 10h
endm
write macro txt
;wyswietlenie tekstu (usluga DOS-u: przerwanie 21h, funkcja 09h)
lea dx,txt
mov ah,09h
int 21h
endm
readkey macro
;czekanie na wcisniecie klawisza (usluga DOS-u: przerwanie 21h, funkcja 01h)
mov ah,01h
int 21h
endm
exit macro
;zakonczenie programu (usluga DOS-u: przerwanie 21h, funkcja 4ch)
mov ah,4ch
mov al,0 ;kod wyjscia
int 21h
endm
.code
;ustalenie segmentu danych (dobrze, żeby to było zaraz na poczatku programu)
mov ax,@data
mov ds,ax
;czyszczenie ekranu
clrscr
;wyswietlenie pierwszego tekstu
write txt1
;wyswietlenie drugiego tekstu
write txt2
;wyswietlenie trzeciego tekstu
write txt3
;wyswietlenie czwartego tekstu
write txt4
;czekanie na wcisniecie klawisza
readkey
;zakonczenie programu
exit
end
Teraz program zajmuje 16 linii, a co do czytelności to odpowiedzcie sobie sami...
Istotne jest to, że w miejscu wywołania makra wstawiane są fizycznie linie, które makro opisuje, toteż nikogo nie powinno dziwić, że program 1 i 2 w wersji uruchamialnej mają dokładnie taki sam rozmiar (w moim przypadku 632 bajtów)...
6.7. Procedury
Procedury są z kolei modułem programowym, który może, w przeciwieństwie do makr, być udostępniany na zewnątrz. Udostępniać na zewnątrz można także zmienne, ale tylko globalne. Sytuacja ta dotyczy przypadku, gdy jeden program składa się z wielu plików składowych. Program działający identycznie, jak przedstawione w rozdziale poprzednim, ale zrealizowany przy użyciu procedur może wyglądać następująco:
;definicja stalej oznaczajacej numer koloru
BIALY EQU 15
.model small
.stack 512
.data
txt1 db 'Tekst 1',10,13,'$'
txt2 db 'Tekst 2',10,13,'$'
txt3 db 'Tekst 3',10,13,'$'
txt4 db 'Tekst 4',10,13,'$'
.code
;segmentu danych (dobrze, żeby to było zaraz na poczatku programu)
mov ax,@data
mov ds,ax
;czyszczenie ekranu
call clrscr
;wyswietlenie pierwszego tekstu
lea dx,txt1
push dx ;odlozenie na stosie adresu tekstu 1
call write
;wyswietlenie drugiego tekstu
lea dx,txt2
push dx ;odlozenie na stosie adresu tekstu 2
call write
;wyswietlenie trzeciego tekstu
lea dx,txt3
push dx ;odlozenie na stosie adresu tekstu 3
call write
;wyswietlenie czwartego tekstu
lea dx,txt4
push dx ;odlozenie na stosie adrseu tekstu 4
call write
;czekanie na wcisniecie klawisza
call readkey
;zakonczenie programu
call exit
;czesc programu, do ktorej nie ma mozliwosci wejscia normalnym trybem
clrscr proc
;ustawienie aktywnej strony (usluga BIOS-u: przerwanie 10h, funkcja 05h)
mov al,0 ;numer strony
mov ah,05h
int 10h
;wyczyszczenie ekranu (usluga BIOS-u: przerwanie 10h, funkcja 06)
mov al,0
mov ch,0 ;lewy gorny wiersz okna
mov cl,0 ;lewa gorna kolumna okna
mov dh,25 ;prawy gorny wierz okna
mov dl,80 ;prawa gorna kolumna okna
mov bh,BIALY
mov ah,06h
int 10h
;ustawienie kursora na poczatku (usluga BIOS-u: przerwanie 10h, funkcja 02h)
mov dh,0 ;wiersz
mov dl,0 ;kolumna
mov bh,0 ;numer strony
mov ah,02h
int 10h
ret
endp
write proc
;wyswietlenie tekstu (usluga DOS-u: przerwanie 21h, funkcja 09h)
; sutuacja na stosie jest nastepujaca (model small, wywolanie bliskie)
; XXXXh <- na wierzcholku jest adres powrotu do pr. gl.
; XXXXh <- tu jest odlozony adres tekstu
pop di ;teraz di zawiera adres powrotu
pop dx ;teraz dx zawiera adres tekstu do wyswietlenia
mov ah,09h
int 21h
push di ;koniecznie przed "ret" odlozyc adres powrotu
;na stos, bo inaczej bedzie powrot w krzaki...
ret
endp
readkey proc
;czekanie na wcisniecie klawisza (usluga DOS-u: przerwanie 21h, funkcja 01h)
mov ah,01h
int 21h
ret
endp
exit proc
;zakonczenie programu (usluga DOS-u: przerwanie 21h, funkcja 4ch)
mov ah,4ch
mov al,0 ;kod wyjscia
int 21h
ret
endp
end
Jak widać, program główny nieco się wydłużył w stosunku do poprzednich. Zwrócić należy uwagę na sposób przekazania adresu tekstu, jaki się ma wyświetlić, do procedury. Wykorzystywany jest w tym celu stos.
Istotne jest to, że w miejscu wywołania procedur wlinkowywany (nie po polskiemu) jest adres procedury, a nie jej ciało. Program zyska na objętości, lecz w zamian uzyskamy możliwość udostępnienia wybranych procedur innym składowym programu...
Autor zachęca do przeczytania i zrozumienia powyższych rozdziałów. Jako motto w chwilach zwątpienia niechaj Wam służy bardzo mądre przesłanie, którego autorstwa nie znam:
„Próbując dosięgnąć gwiazd możesz nie chwycić ani jednej, ale robiąc to zyskujesz pewność, że to co pozostanie w Twojej dłoni nigdy nie będzie garścią błota...”