Programowanie w asemblerze x86 - podstawy
1
. Narzędzia programistyczne
Język asembler jest językiem niskiego poziomu. Można wyróżnić trzy główne programy,
jakie są niezbędne do napisania i przetworzenia programu w asemblerze. Są to:
1. 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 roz-
szerzeniu *.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.
2. tlink.exe - (konsolidacja) program ten służy do utworzenia wersji uruchamialnej pro-
gramu na podstawie pliku obiektowego (*.obj). Na ogół w prostych zastosowaniach wy-
woł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 prze-
biegł bezbłędnie. Użycie tego programu jest następujące:
tlink nazwa_pliku.obj
3. td.exe
- (debugger) programu tego używa się w sytuacji, gdy udało się utworzyć pro-
gram uruchamialny, ale nie działa on poprawnie, tzn. jest w nim jakiś błąd nie składnio-
wy, 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 zmien-
ne, 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.
2
. Elementy składni (program nierealokowalny)
Każdy język programowania wymaga odpowiedniej organizacji pliku źródłowego, tj. od-
powiedniego rozmieszczenia elementów języka, zachowania kolejności pewnych sekcji, uży-
cia odpowiednich słów kluczowych.
W pliku źródłowym powinny się znaleźć następujące elementy:
a) .model nazwa_modelu
- określenie modelu pamięci. W prostych zasto-
sowaniach możliwe modele pamięci to small i ti-
ny.
b) .stack rozmiar_stasu_w_bajtach
- określenie rozmiaru stosu dla programu. W pro-
stych 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.
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 mu-
szą się zmieścić w jednym segmencie, przy czym na program przypada 32kB pamięci, a łącz-
nie 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.
b)
Stos powinien być w programie zadeklarowany, ponieważ nawet jeżeli programista świa-
domie go nie używa, to stos jest wykorzystywany podczas wywołań podprogramów lub pod-
czas realizacji przerwań programowych. Jeżeli nie ma stosu, program konsolidujący wyświe-
tli ostrzeżenie.
c)
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ątkowa
przy czym w miejscu rozmiar pojawić się może:
- DB
- wartości zmiennej zapisana na jednym bajcie,
- DW
- wartości zmiennej zapisana na dwóch bajtach,
- DD
- wartości zmiennej zapisana na czterech bajtach,
- DQ
- wartości zmiennej zapisana na ośmiu bajtach,
- DT
- wartości zmiennej zapisana na dziesięciu bajtach,
Przykład
y:
deklaracja w asemblerze:
odpowiednik w C
liczba DB 0
char liczba=0;
liczba DW 2
int liczba=2;
liczba DW 30h
int liczba=0x30; // wartość heksadecymalna
liczba DD 23
long liczba=23;
liczba DD 2.34
float liczba=2.34;
liczba DQ 1.23456
double liczba=1.23456;
Drugi z typów służy tylko i wyłącznie do deklaracji ciągu znaków, które w następstwie będą
wyświetlane na ekranie (znaki zapisane są na jednym bajcie, co oznacza typ danych DB).
Deklaracja tego typu zmiennej odbywa się w sposób następujący:
nazwa_zmiennej
DB
‘Tu wpisz wyświetlany tekst$’
Pojawiający się znak ‘$’ na końcu typu łańcuchowego związany jest z wykorzystywaniem
funkcja wyświetlającej łańcuch znaków (funkcja 9. przerwania 21h), która rozpoznaje koniec
ciągu znaków, gdy napotka znak ‘$’. W ciąg znaków można wprowadzać również bezpośred-
nio kody ASCII.
Przykład.
tekst db ‘Ala ma kota$’
tekst db ‘Ala ma kota’,13,10,’$’
;13,10 to przejście do następnej linii
Typ tablicowy umożliwia deklarację w programie zmiennych, które będą zajmowały w pa-
mięci pewną liczbę kolejnych bajtów (słów, podwójnych słów itd.). 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’)
d)
Pomiędzy dyrektywami .code i end wpisujemy program. To ta część naszego pliku źró-
dłowego będzie skojarzona z rejestrem segmentowym CS.
3. Elementy składni programu realokowalnego
W punkcie 2 zaprezentowana została składnia programu assemblerowego w wersji niere-
alokowalnej, to znaczy w takiej wersji, w której model pamięci determinuje z góry maksy-
malny rozmiar pamięci na dane program i stos. Gdyby się okazało, że pamięci tej potrzeba
więcej, program należałoby assemblować i linkować ponownie w innym modelu pamięci.
Program w wersji realokowalnej ma tę cechę, że ilość miejsca potrzebnego na program,
dane i stos jest obliczana w trakcie procesu assemblacji i linkowania (konsolidacji) i zawsze
jest przydzielona w potrzebnym wymiarze. W najprostszym przypadku budowa programu w
wersji jest następująca:
sts segment stack 'stack'
- segment stosu
db ROZMIAR_STOSU dup(0)
- zarezerwowanie pamięci na stos
sts ends
dane segment
- segment danych
..
- tutaj kolejne deklaracje zmiennych
dane ends
program segment
- segment kodu
assume cs: program, ds: dane, ss: sts
- powiązanie rejestrów segmentowych z
odpowiednimi segmentami
start:
..
- tutaj program
program ends
end start
W powyższym przykładzie pokazano jeden z kilku sposobów konstrukcji pliku źródłowego
dla realokowalnej wersji programu. Pogrubioną czcionką wyróżnione zostały niezbędne ele-
menty programu należące do słów kluczowych assemblera, natomiast kursywa stwarza pole
do popisu dla inwencji twórczej programistów, bowiem są to nazwy części składowych pro-
gramu, które mogą być zupełnie dowolne.
3
. Wywołania systemowe
W asemblerze można wykorzystać z fakt, że wiele rzeczy można zrealizować wywołując
pewne usługi BIOS-u lub DOS-u. Numery przerwań do 20h są to przerwania BIOS-u, nato-
miast 21h i wyżej, to usługi DOS-u. Na ogół jest tak, że usługi DOS-u są dublowane przez
usługi BIOS-u i odwrotnie. W skrócie można to przedstawić 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ę wcześniej napisane procedury. Tworzenie tej tablicy
odbywa się dwuetapowo, tzn. część adresów programów wpisuje do niej BIOS, a reszta jest
dopisywana po fakcie startu systemu operacyjnego DOS. Sięgnięcie do odpowiedniej proce-
dury jest możliwe jedynie w przypadku wywołania przerwania. Przerwania można podzielić
na sprzętowe (generowanych przez urządzenia wchodzące w skład systemu komputerowego)
oraz programowe, które może wywołać programista za pomocą instrukcji INT). W momen-
cie wystąpienia przerwania następuje odwołanie do tablicy wektorów przerwań i pobrane
cztery bajty adresu procedury obsługi danego przerwania (segment oraz offset adresu). Nie-
które przerwania mają wiele procedur i wtedy należy dodatkowo oprócz wywołania przerwa-
nia podać numer procedury w ramach tego przerwania (np. przerwanie 10h to jest zbiór pro-
cedur obsługi ekranu). Rejestrem, który jest zawsze domyślnie sprawdzany w celu okre-
ślenia numeru
procedury jest rejestr AH. Chcąc zatem wywołać usługę numer 10h prze-
rwania 10h należy to zrobić w sposób następujący:
mov
ah,10h
int
10h
Czasem do wywołania określonej procedury należy podać dodatkowe parametry w innych
rejestrach procesora (np. aby ustawić pozycję kursora na ekranie należy tą podać tą pozy-
cję). W wyniku wykonania określonej procedury mogą być również zwracane pewne para-
metry do wybranych rejestrów procesora (np. wywołując usługę odczytującą pozycję kur-
sora na ekranie, procedura ta do odpowiednich rejestrów wpisze odczytaną pozycję). Infor-
mację o tym do jakich rejestrów należy wprowadzić dodatkowe parametry przed wyw
o-
łaniem procedury, lub z j
akich je odczytać po wykonaniu procedury można zdobyć na
podstawie opisu przerwań, który znajduje się w dodatkach do większości książek o pr
o-
gramowaniu w asemblerze.
Inaczej rzecz ujmując wywołanie przerwania (INT) powinno przebiegać wg następującej ko-
lejności:
1. Odnaleźć numer odpowiedniej procedury oraz numer przerwania realizującej wymagane
zadanie (np. ustawienie kursora na danej pozycji)
2. Jeżeli w wyniku wykonania procedury do rejestrów zwracane są wyniki jej działania (lub
do rejestrów konieczne jest wpisanie parametrów dodatkowych dla wywoływanej proce-
dury), a przechowują one dane istotne dla działania programu to konieczne jest zachowa-
nie zawartości tych rejestrów np. na stosie.
3. Należy wprowadzić do odpowiednich rejestrów parametry dodatkowe (np. pozycję kurso-
ra).
4. Dopiero wówczas można wywołać przerwanie
5. Z rejestrów można odczytać i wykorzystać w programie informacje zwrócone przez pro-
cedurę
Przykład:
Ustawienie kursora na pozycji 3 wiersz, 2 kolumna:
1. Funkcję ustawienia kursora realizuje funkcja 02h przerwania 10h BIOS-u. W wyniku
działania dodatkowe parametry (nr wiersza, kolumny i strony) należy umieścić w odpo-
wiednich rejestrach zgodnie z opisem przerwania (odpowiedni w rejestrze dh, dl i bh).
2. Funkcja nie zwraca informacji zwrotnej, ale należy wpisać parametry oraz nr funkcji do
rejestrów: ah, bh, dl, dh. Przyjmijmy że w programie przed wywołanie przerwania rejestry
te przechowywały istotne dla programu wartości i należy je przechować na stosie.
3. Po zabezpieczeniu wartości rejestrów można do nich wprowadzić odpowiednie parametry
dla przerwania
4. Wywołanie przerwanie
5. Procedura nie zwraca informacji zwrotnej, ale skoro zachowaliśmy wartości rejestrów na
stosie to można po wywołaniu procedury je przywrócić pobierające je ze stosu
W wyniku przeprowadzenia 5 kroków program realizujący to zadanie będzie wyglądał nastę-
pująco:
---;
; krok nr 2 – zabezpieczenie rejestrów
push ah
push bh
push dh
push dl
;krok nr 3 – wprowadzenie parametrów
mov dh,3
;wiersz
mov dl,2
;kolumna
mov bh,0
;numer strony
mov ah,02h
;numer funkcji realizującej ustawienie kursora
;krok nr 4 –
wywołanie przerwania
int
10h
;krok nr 5 –
przywrócenie poprzednich wartości w rejestrach
pop
dl
;pobranie wartości ze stosu w odwrotnej kolejności niż ich
pop
dh
;położenie na stos
pop
bh
pop
ah
---;
4. 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 nazwa_programu.exe można uruchomić. Powyższy program nic nie robi, jed-
nak ma zasadniczą zaletę – nie zawiesza komputera, będąc przy tym w pełni zgodnym ze
składnią języka.
W wersji relokowalnej program będzie wyglądał następująco:
;---
segment stosu
sts segment 'stack'
db 512 dup(0)
sts ends
;---
segment danych
dane segment
dane ends
;---
segment kodu
program segment
assume cs:program, ds: dane, ss: sts
start:
mov ah,4ch
int
21h
program ends
end start
5. 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. Najlepiej to wyjaśnić na przykładzie:
;Program 1
.model small
.stack 512
.data
txt1
db
'Tekst 1',10,13,'$'
txt2
db
'Tekst 2',10,13,'$'
txt3
db
'Tekst 3',10,13,'$'
.code
mov ax,@data
;ustalenie segmentu danych
mov ds,ax
;wyswietlenie pierwszego tekstu (przerwanie 21h, funkcja 09h, w dx offset tekstu)
lea
dx,txt1
mov ah,09h
int
21h
;wyswietlenie drugiego tekstu (przerwanie 21h, funkcja 09h, w dx offset tekstu)
lea
dx,txt2
mov ah,09h
int
21h
;wyswietlenie trzeciego tekstu (przerwanie 21h, funkcja 09h, w dx offset tekstu)
lea
dx,txt3
mov ah,09h
int
21h
;czekanie na wcisniecie klawisza (przerwanie 21h, funkcja 01h, kod ASCII
;wciśniętego klawisza zwrócony zostaje w rejestrze AL)
mov ah,01h
int
21h
;zakonczenie programu (przerwanie 21h, funkcja 4ch)
mov ah,4ch
int
21h
end
Powyższy program zajmuje 29 linii 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
.model small
.stack 512
.data
txt1
db
'Tekst 1',10,13,'$'
txt2
db
'Tekst 2',10,13,'$'
txt3
db
'Tekst 3',10,13,'$'
write macro txt
;wyswietlenie tekstu (przerwanie 21h, funkcja 09h w dx offset tekstu)
lea
dx,txt
mov ah,09h
int
21h
endm
readkey
macro
;czekanie na wcisniecie klawisza (przerwanie 21h, funkcja 01h, kod ASCII
;wciśniętego klawisza zwrócony zostaje w rejestrze AL)
mov ah,01h
int
21h
endm
exit
macro
;zakonczenie programu (przerwanie 21h, funkcja 4ch)
mov ah,4ch
int
21h
endm
.code
mov ax,@data
;ustalenie segmentu danych
mov ds,ax
write txt1
;wyswietlenie pierwszego tekstu
write txt2
;wyswietlenie drugiego tekstu
write txt3
;wyswietlenie trzeciego tekstu
readkey
;czekanie na wcisniecie klawisza
exit
;zakonczenie programu
end
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.
6. Procedury
Procedury są modułem programowym, który może, w przeciwieństwie do makr, być udo-
stę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:
.model small
.stack 512
.data
txt1
db
'Tekst 1',10,13,'$'
txt2
db
'Tekst 2',10,13,'$'
txt3
db
'Tekst 3',10,13,'$'
.code
;segmentu danych (dobrze, żeby to było zaraz na poczatku programu)
mov ax,@data
mov ds,ax
lea
dx,txt1
;pobranie adresu pierwszego tekstu
call
write
;wyswietlenie pierwszego tekstu
lea
dx,txt2
;pobranie adresu drugiego tekstu
call
write
;wyswietlenie pierwszego tekstu
lea
dx,txt3
;pobranie adresu pierwszego tekstu
call
write
;wyswietlenie pierwszego tekstu
call readkey
;czekanie na wcisniecie klawisza
;zakonczenie programu (przerwanie 21h, funkcja 4ch)
mov ah,4ch
int
21h
;czesc programu, do ktorej nie ma mozliwosci wejscia normalnym trybem
write proc
;wyswietlenie tekstu (przerwanie 21h, funkcja 09h, w dx offset tekstu)
mov ah,09h
int
21h
ret
endp
readkey
proc
mov ah,01h
int
21h
ret
endp
end
Istotne przy użyciu procedur jest to, że w miejscu wywołania procedur nie jest wpisy-
wane ciało procedury jak przy użyciu makr
.
7. Program typu com
Program typu com ma inną budowę, niż program typu exe. Aby uzyskać plik com ko-
nieczne jest spełnienie trzech warunków:
Program nie może zawierać linii sygnalizującej segment stosu ani segment danych i musi
się zaczynać od dyrektywy org 100h.
W programie musi istnieć dyrektywa (etykieta z dwukropkiem) określająca początek i
koniec segmentu kodu (w przykładzie nazwana Start).
Turbo linker (program Tlink.exe) należy koniecznie uruchomić z parametrem /t.
Proces asemblacji i konsolidacji jest więc następujący:
tasm nazwa_pliku.asm
tlink /t nazwa_pliku.obj
;przykład programu w wersji dla pliku com:
program segment
assume cs:program, ds:program
org 100h
start:
lea
dx,txt1
;wyswietlenie pierwszego tekstu
call
write
lea
dx,txt2
;wyswietlenie drugiego tekstu
call
write
lea
dx,txt3
;wyswietlenie trzeciego tekstu
call
write
call readkey
;czekanie na wcisniecie klawisza
;zakonczenie programu (przerwanie 21h, funkcja 4ch)
mov ah,4ch
int
21h
;czesc programu, do ktorej nie ma mozliwosci wejscia normalnym trybem
;wyswietlenie tekstu (przerwanie 21h, funkcja 09h, w dx offset tekstu)
mov ah,09h
int
21h
ret
endp
readkey
proc
mov ah,01h
int
21h
ret
endp
;---
deklaracje zmiennych
txt1
db
'Tekst 1',10,13,'$'
txt2
db
'Tekst 2',10,13,'$'
txt3
db
'Tekst 3',10,13,'$'
program ends
end start