Wysłany: Nie 16:05, 06 Lis 2005 |
|
|
Popiol |
Poison Headcrab |
|
|
Dołączył: 02 Lis 2005 |
Posty: 16 |
Przeczytał: 0 tematów
Ostrzeżeń: 0/5
|
Skąd: Wygiełzów |
|
|
|
|
|
|
Witam wszystkich i zapraszam na randke z assemblerem. Na dobry początek przedstawiam najkrótszą drogę do odpalenia swojego własnego programu napisanego w assemblerze.
Po pierwsze trzeba mieć kompilator. Oto [link widoczny dla zalogowanych] do kompilatora Turbo Assebler, pod którym działają zamieszczone tu programy.
Następnie otwieramy edytor tekstowy (taki, który potrafi zapisywać czysty tekst) i wpisujemy kod:
Kod: | .model tiny
.code
org 100h
start:
;wypisanie zmiennej tekst
mov ah, 9
mov dx, offset(tekst)
int 21h
;koniec programu
mov ah, 4ch
int 21h
;definicje
tekst db 'Yo!$'
end start |
Zapisujemy plik z rozszerzeniem asm w katalogu z kompilatorem (tasm). Teraz trzeba otworzyć okienko dosowe, przejść do katalogu tasm i uruchomić kompilator poleceniem:
Kod: | tasm nazwa_pliku_asm |
Dzięki temu powstanie plik z rozszerzeniem obj. Następnie odpalamy linkera wpisując:
Kod: | tlink nazwa_pliku_obj /t |
W efekcie dostaniemy plik com, który uruchamiamy i cieszymy się widokiem wypisanego na ekran tekstu. Jak widać programowanie w assemblerze nie jest takie trudne . Teraz nieco bardziej skomplikowana część artykułu, czyli próba wyjaśnienia jak to działa.
Po pierwsze program ładowany jest do pamięci operacyjnej. W naszym prostym programie sytuacja wygląda następująco. Pamięć podzielona jest na 64 KBajtowe segmenty, a nasz program mieści się w jednym takim segmencie. Znajduje się tam zarówno kod jak i dane. Taki model pamięci ustalamy pisząc .model tiny. Co więcej tworzymy program typu com. Aby tak się stało używamy opcji /t przy wywołaniu linkera. Programy typu com to właśnie małe programy mieszczące się w jednym segmencie, w których właściwy kod zaczyna się od 256 baju względem początku segmentu. Wcześniej znajdują się jakieś informacje na temat programu, ale to nas nie interesuje. Polecenie .code oznacza, że zaczynamy segment kodu. W tym przypadku jest to zarazem segment danych. Aby przejść do 256 bajtu uzywamy polecenia org 100h. Litera 'h' oznacza zapis heksadecymalny (szesnastkowy), a więc 100h = 256. W assemblerze możemy używać oprócz heksadecymalnego również zapis dziesiętny (wówczas nie trzeba dodawać żadnej litery) oraz binarny (dodajemy literę 'b'). Możemy zatem napisać równie dobrze org 256. Teraz w końcu możemy zacząć właściwy kod. O pamięci w assemblerze będzie osobny artykuł bo to dość szerokie zagadnienie.
Jak widać kod znajduje się w bloku ograniczonym przez start: i end start, które możemy potraktować jak begin i end. w pascalu. Słowo start możemy zastąpić dowolnym innym, to jest tylko nazwa modułu. Komentarze w assemblerze umieszczamy po znaku ;.
Dalej mamy polecenie mov ah, 9. Jest ono równoznaczne z pascalowym ah := 9. Trzeba pamiętać, że nie zawsze możemy podstawić wartość pod zmienną czy rejestr bespośrednio. Czasami trzeba zrobić coś takiego:
Kod: | mov ax, wartość
mov coś, ax |
gdzie coś jest zmienną lub rejestrem. O rejestrach na razie powiem tylko tyle, że są to specjalne zmienne procesora.
Aby wytłumaczyć po co robimy to i następne podstawienie należy powiedzieć pare słów o przerwaniach. Otóż programując w assemblerze mamy do dyspozycji całą listę funkcji, które np. zmieniają ustawienia BIOSu, albo wypisują tekst na ekran, albo wczytują dane z klawiatury itp. Funkcję taką wykonuje się przez wywołanie odpowiedniego przerwania. Jest tego naprawdę sporo, dlatego w kolejnych artykułach będę opisywał różne ciekawe przerwania.
Przerwania wywołuje się poleceniem int numer_przerwania. W naszym programie użyliśmy jednego przerwania o numerze 21h. Wywołuje ono jedną z funkcji dosowych. O tym która funkcja zostanie ostatecznie wywołana decyduje zawartość rejestru ah. Dlatego właśnie wykonujemy podstawienie mov ah, 9. Funkcja dosowa o numerze 9 to funkcja 'Print String'. Wypisuje ona na standardowe wyjście string, którego adres znajduje się w rejestrach ds i dx. Rejestr ds to rejestr segmentu danych (wskazuje na segment, w którym są dane). Dzięki temu, że tworzymy plik com, rejestr ten ma od razu ustawioną właściwą wartość. Rejestr dx ma natomiast, w tym przypadku, zawierać tzw. offset odpowiadający stringowi. Offset to przesunięcie w pamięci względem początku segmentu. Zatem segment i offset stanowią razem adres konkretnej komórki pamięci. W tym przypadku rejestry ds i dx stanowią adres początku stringu, który chcemy wypisać. Offset, który potrzebujemy dostajemy pisząc offset(tekst), gdzie tekst to nazwa naszego stringu. Pozostaje pytanie, gdzie jest koniec stringu. Otóż ogranicznikiem dla stringu jest znak '$'.
Przy drugim wywołaniu przerwania 21h, wartość w rejestrze ah wynosi 4ch (przypominam, że c odpowiada liczbie 12 w zapisie szesnastkowym). Jest to funkcja, która kończy działanie programu i sprząta po nim (czyści pamięc i takie tam).
Ufff doszliśmy do deklaracji zmiennych, a właściwie jednej zmiennej tekst. Kluczowym słowem jest tutaj db - skrót od define byte. Jak łatwo się domyśleć definiuje ono jeden bajt. Składnia:
Kod: | nazwa db wartość[, wartość...] |
Jeśli napiszemy kilka wartości to zdefinujemy kilka bajtów, a nazwa będzie wskazywała na pierwszy. W ten sposób utworzymy tablicę. Natomiast zapis:
jest równoważny takiemu:
Kod: | tekst db 'Y','o','!','$' |
Znaki zaś są zamieniane na kody ASCII. To zupełnie tak jakbyśmy napisali w C++: char tekst[4] = "Yo!", tylko w C++ automatycznie dodawany jest znak końca stringu. Jeśli jesteśmy przy deklarowaniu zmiennych to napiszę od razu jak zdefiniować większą zmienną:
dw - define word = 2 bajty
dd - define double word = 4 bajty
df - define far word = 6 bajtów
dq - define quad word = 8 bajtów
dt - define temp word = 10 bajtów
I jeszcze jedna ciekawa rzecz. Pokazaliśmy jak można zdefiniować kilkuelementową tablicę. Natomiast jeśli chcemy mieć większą tablicę robimy tak:
Kod: | nazwa db rozmiar dup(wartość) |
Powstanie tabilca bajtów, w której ilość elementów = rozmiar, a początkowe wartości są równe wartość. Jeśli nie chcemy ustalać początkowej wartości możemy wpisać dup(?). Aby uciąć ewentualne spekulacje dodam, że dup to skrót od duplicate . A oto tablica dwuwymiarowa n na m elementów, inicjalizowana zerami:
Kod: | nazwa db n dup(m dup (0)) |
Do wartości zmiennej odnosimy się umieszczając jej nazwę w nawiasach kwadratowych, np:
Kod: | mov [zmienna], ax ; zmienna := ax
mov ax, [tablica+10] ; ax := tablica[10]
mov ax, [tablica+5*m+4] ; ax := tablica[5,4] z tym, że m jest konkretną wartością, a nie zmienną |
Pamiętajcie, że muszą zgadzać się typy. To co podstawiamy musi mieć tyle samo bajtów co to pod co podstawiamy. Jeśli chcemy pod zmienną podstawić wartość np 2 to robimy to za pośrednictwem rejestru ax, czyli najpierw do ax i dopiero z ax do zmiennej. Skoro już przy tym jesteśmy to przyda się podstawowa wiedza na temat rejestrów ogólnego przeznaczenia. Pierwszym takim rejestrem jest akumulator. Ma on następującą konstrukcję:
rax = 64 bity = 32 starsze bity + eax
eax = 32 bity = 16 starszych bitów + ax
ax = 16 bitów = ah (8 starszych bitów) + al (8 młodszych bitów)
Analogicznie wyglądają inne rejestry ogólnego przeznaczenia: bx (bazowy), cx (licznik), dx (danych).
Zanim zaczniemy coś ciekawego programować trzeba jeszcze wspomnieć o kilku ważnych poleceniach.
Operacje arytmetyczne:
add x, y ; x := x + y
sub x, y ; x := x - y
dec x ; x := x - 1
inc x ; x := x + 1
div x ; al := ax div x, ah := ax mod x, dla x wielkości bajta
div x ; ax := dx:ax div x, dx := dx:ax mod x, dla x wielkości 2 bajtów
div x ; eax := edx:eax div x, edx := edx:eax mod x, dla x wielkości 4 bajtów
idiv x ; to samo co div ale x jest ze znakiem
mul x ; ax := al * x, dla x wielkości bajta
mul x ; dx:ax := ax * x, dla x wielkości 2 bajtów
mul x ; edx:eax := eax * x, dla x wielkości 4 bajtów
imul x ; to samo co mult ale x jest ze znakiem
neg x ; x := -x
Operacje bitowe
and x, y ; x := x and y na każdym bicie
or x, y ; x := x or y na każdym bicie
xor x, y ; x := x xor y na każdym bicie
shl x, n ; przesuwa bity w lewo o n, uzupełniając zerami
shr x, n ; przesuwa bity w prawo o n, uzupełniając zerami
Porównanie dwóch wartości:
cmp x, y ; porównuje x i y, a wynik sprawdzamy instrukcją skoku
Skoki warunkowe odnoszą się do wartości ostatniego wyrażenia i wykonują skok jeśli zachodzi warunek:
ja etykieta ; warunek := x > y (bez znaku) dla polecenia cmp albo x > 0
jb etykieta ; warunek := x < y (bez znaku) dla polecenia cmp albo x < 0
jg etykieta ; warunek := x > y (ze znakiem) dla polecenia cmp
jl etykieta ; warunek := x < y (ze znakiem) dla polecenia cmp
je etykieta ; warunek := x == y dla polecenia cmp
jz etykieta ; warunek := x == 0
jmp etykieta ; skok bezwarunkowy
Możemy też zaprzeczać powyższe warunki dodając literę 'n' w poleceniu, np:
jna etykieta ; warunek := x <= y (bez znaku) dla polecenia cmp albo x <= 0
etykiety robi się tak samo jak np w c++ czyli
Dobra, na razie wystarczy. Na koniec jeszcze jeden program.
Kod: | .model tiny
.code
org 100h
start:
;inicjalizacja grafiki 320x200 256kol
mov ax, 0013h
int 10h
rysuj:
;obliczenie funkcji
mov dx, 0000h
mov ax, [x]
imul [x]
mov bx, 0080h;
div bx;
mov dx, 200
sub dx, ax
;putpixel al=kolor cx=x dx=y
mov ah, 0ch
mov al, 100
mov cx, 00a0h
add cx, [x]
int 10h
;x-- i sprawdzenie warunku końca
dec [x]
cmp [x], -160
jnl rysuj
;koniec programu
mov ah, 4ch
int 21h
;definicje
x dw 160;
end start |
|
|
Post został pochwalony 0 razy
Ostatnio zmieniony przez Popiol dnia Pią 23:43, 11 Lis 2005, w całości zmieniany 1 raz
|
|
|
|
|
Wysłany: Nie 0:12, 13 Lis 2005 |
|
|
Popiol |
Poison Headcrab |
|
|
Dołączył: 02 Lis 2005 |
Posty: 16 |
Przeczytał: 0 tematów
Ostrzeżeń: 0/5
|
Skąd: Wygiełzów |
|
|
|
|
|
|
Musze dodać jedną rzecz na temat pętli. Jeśli chcemy, żeby dany fragment kodu wykonał się n razy to możemy napisać tak:
Kod: | mov cx, n ; n - jakaś wartość np 5 albo [x]
petla:
...
loop petla |
Polecenie loop zmniejsza zawrtość rejestru cx o 1 i wraca do podanej etykiety jeśli cx <> 0.
A teraz kolejny krok ku zdobyciu nieograniczonej władzy nad komputerem: funkcje. Wywoływanie funkcji, a właściwie procedur w assemblerze wygląda tak:
Kod: | .model tiny
.code
org 256
start:
call wypiszA ; wywołanie procedury wypiszA
jmp exit
;FUNKCJE_________________________
;procedura wypiszA
wypiszA proc
;zapisanie modyfikowanych rejestrów
push ax
push dx
;wypisanie litery A
mov ah, 02h
mov dl, 'A'
int 21h
;przywrócenie zapisanych rejestrów
pop dx
pop ax
;powrót z procedury
ret
wypiszA endp ; koniec definicji procedury
;KONIEC__________________________
exit:
mov ah, 4ch
int 21h
end start |
Pierwszym ważnym poleceniem jest call nazwa_procedury. Zapisuje ono na stosie adres następnej linii kodu, a następnie skacze do procedury. Definicja procedury zaczyna się od nazwa_procedury proc, a kończy na nazwa_procedury endp. Aby poprawnie wyjść z procedury trzeba użyć polecenia ret. Pobiera ono ze stosu adres, który wrzuciliśmy tam poleceniem call i przechodzi pod ten adres.
W tym miejscu wypada powiedzieć pare słów o stosie. Każdy program oprócz segmentu kodu i segmentu danych posiada jeszcze coś takiego jak segment stosu. Generalnie czym jest stos każdy wie (mam nadzieje). Ten stos w assemblerze służy do przechowywania pewnych informacji podczas wywoływań procedur, przede wszystkim adresu powrotnego. Aby położyć coś na stosie piszemy push coś, gdzie coś może być czymkolwiek o rozmiarze dwóch bajtów. Aby zdjąć elemant ze stosu piszemy pop cel, gdzie cel to zmienna lub rejestr o wielkości dwóch bajtów.
Wewnątrz procedury użyłem operacji push i pop, aby po jej wykonaniu zawartość rejestrów ax i dx pozostała nie zmieniona. W tym małym programie akurat nie ma to znaczenia, ale w większych programach nie zmienianie zawartości rejestrów w funkcjach jest przyjemną cechą.
Do wypisania znaku użyte zostało przerwanie dosowe z serii 21h, o numerze 2. Przerwanie to wypisuje znak, którego kod ASCII jest w rejestrze dl.
Jak widać wywoływanie procedur, które nie mają parametrów i nic nie zwracają jest całkiem proste. Przypadek z parametrami i zwracaną wartością już nie jest taki przyjemny. Nie da się przesłać parametrów tak jak np w C. Można przesłać parametr np przez rejestry, czyli przed wywołaniem procedury wstawiamy coś do rejestru, a procedura z tego korzysta. Tak samo możemy zwracać wartości, tzn procdura ustawia zawartość rejestru, a my po wywołaniu procedury z tego korzystamy. Dokładnie tak jest w przerwaniach. Możemy też wysłać parametr wrzucając go na stos. Musimy jednak pamiętać, że w momencie wywołania procedury na stos idzie jeszcze adres. W przypadku wywołania "bliskiego" idzie tylko offset, a więc dwa bajty, bo funkcja znajduje się w tym samym segmencie co kod. Wewnątrz procedury musimy najpierw ściągnąć offset do rejestru indeksowego, np di. Dopiero wtedy możemy pobrać ze stosu parametr. Na koniec, przed powrotem z procedury, musimy wrzucić na stos adres powrotny. Możemy w podobny sposób zwracać wartości. Umieszczamy je wtedy na stosie przed umieszczeniem adresu powrotnego. Wygląda to tak:
Kod: | .model tiny
.code
org 256
start:
;wrzucamy na stos parametr funkcji, 13 to kod ASCII entera
push 13
call czytajDo
cmp cx, 0
jng exit
petla:
;zdejmujemy ze stosu wartości funkcji
pop dx
call wypiszZnak
loop petla
jmp exit
;FUNKCJE_________________________
czytajDo proc
;modyfikuje di, ax, bx, cx
;cx - ilość przeczytanych znaków
;zdejmujemy adres powrotny
pop di
;zdejmujemy parametr
pop bx
mov cx, 0
czytaj:
;wczytaj znak
mov ah, 00h
int 16h
;umieść go na stosie
push ax
inc cx
cmp al, bl
jne czytaj
;ostatni wczytany znak jest niepotrzebny
pop ax
dec cx
;wrzucamy na stos adres powrotny
push di
;wracamy
ret
czytajDo endp
;_______________
wypiszZnak proc
;modyfikuje ah
mov ah, 02h
int 21h
ret
wypiszZnak endp
;KONIEC__________________________
exit:
mov ah, 4ch
int 21h
end start |
Ten program wczytuje znaki z klawiatury aż do znaku, który podajemy jako parametr w procedurze czytajDo. Tutaj jest to znak o kodzie 13 czyli enter. Kod ASCII jest wprawdzie jedno bajtowy, ale w rzeczywistości wrzucane są dwa bajty, a kod jest bajtem mniej znaczącym. Jak widać w procedurze czytajDo najpierw ściągamy ze stosu adres powrotny, a następnie parametr. Procedura ta ma zwrócić wczytane znaki przez stos. Rejestr cx będzie przechowywał ilość wczytanych znaków.
Do wczytywania używam przerwania z serii 16h (Keyboard BIOS Services) o numerze 0. Ta funkcja czeka, aż jakiś przycisk klawatury zostanie wciśnięty i zwraca do niego kod w rejestrze ax. Jeśli jest to zwykły znak to w al jest jego kod ASCII.
A więc wczytujemy znaki, umieszczając je od razu na stosie, aż wczytamy znak, który jest parametrem procedury. Po wyjściu z pętli usuwamy jeszcze ostatni wczytany znak i wrzucamy na stos adres powrotny. Po powrocie do głównego kodu programu wypisujemy znaki ze stosu. Są one ustawione w odwrotnej kolejności niż były wpisane, co wynika z własności stosu.
Kilka uwag do procedury czytajDo. Po pierwsze jeśli chcemy przekazać procedurze znak, lepiej użyć do tego celu rejestru, tak jak jest to w przypadku procedury wypiszZnak. Po drugie jeśli chcemy zwrócić wartość, która nie mieści się w rejestrach, to lepiej użyć do tego celu zmiennej. Generalnie można powiedzieć, że używanie stosu do wysyłania i zwracania wartości jest poronionym pomysłem. Nic dziwnego w końcu sam na to wpadłem . |
|
Post został pochwalony 0 razy
|
|
|
|
Wysłany: Nie 0:33, 13 Lis 2005 |
|
|
Popiol |
Poison Headcrab |
|
|
Dołączył: 02 Lis 2005 |
Posty: 16 |
Przeczytał: 0 tematów
Ostrzeżeń: 0/5
|
Skąd: Wygiełzów |
|
|
|
|
|
|
A teraz dla odmiany coś pożytecznego i ciekawego:
Kod: | .model tiny
.code
org 256
start:
mov dx, 0000h
call skocz
call cout
db 'Sterowanie: <- w lewo, -> w prawo, Esc - koniec',10,13
db 'Punkty: ',10,13
db 80 dup('_'),0
call rysujRakiete
main:
call rysujPilke
call piszPunkty
call klawiatura
mov cx, 20000
sleep:
int 1Ch;
loop sleep
cmp koniec, 0
je main
jmp exit
;FUNKCJE_________________________
;********************************
cout proc
;wypisuje string, który znajduje się po wywołaniu funkcji
pop di
xor bx, bx ; bl = 0, bl jest znakiem końca stringu
cmp [di], bl
je cout_ret
cout_petla:
mov al, [di]
mov ah, 0Eh
int 10h
inc di
cmp [di], bl
jne cout_petla
cout_ret:
inc di
push di
ret
cout endp
;********************************
;********************************
wypiszCyfre proc
;wypisuje cyfrę, która jest w al
add al, 48
mov ah, 0Ah
xor bh, bh
mov cx, 1
int 10h
ret
wypiszCyfre endp
;********************************
;********************************
skocz proc
;ustawia kursor na pozycji (dl,dh)
mov ah, 02h
mov bh, 0
int 10h
ret
skocz endp
;********************************
;********************************
piszPunkty proc
;wypisuje ilość punktów
mov ax, punkty
mov bl, 10
div bl
push ax
mov al, ah
mov dx, 010Bh
call skocz
call wypiszCyfre
pop ax
mov ah, 0
div bl
push ax
mov al, ah
mov dx, 010Ah
call skocz
call wypiszCyfre
pop ax
mov dx, 0109h
call skocz
call wypiszCyfre
ret
piszPunkty endp
;********************************
;********************************
rysujRakiete proc
;rysuje "rakietę renisową"
mov dl, rakietax
mov dh, 23
call skocz
call cout
db '======',0
ret
rysujRakiete endp
;********************************
;********************************
zmazRakiete proc
;maże "rakietę tenisową"
mov dl, rakietax
mov dh, 23
call skocz
call cout
db ' ',0
ret
zmazRakiete endp
;********************************
;********************************
klawiatura proc
;odczytuje naciśnięty klawisz
mov ah, 01h
int 16h
je klaw_ret
mov ah, 0h
int 16h
cmp ah, 4Bh ; kursor w lewo
je wlewo
cmp ah, 4Dh ; kursor w prawo
je wprawo
cmp ah, 01h ; Esc
je wyjscie
ret
wlewo:
cmp rakietax, 0
je klaw_ret
call zmazRakiete
dec rakietax
call rysujRakiete
ret
wprawo:
cmp rakietax, 73
je klaw_ret
call zmazRakiete
inc rakietax
call rysujRakiete
ret
wyjscie:
mov koniec, 1
klaw_ret:
ret
klawiatura endp
;********************************
;********************************
zmazPilke proc
;maże pilkę
mov dl, pilkax
mov dh, pilkay
call skocz
call cout
db ' ',0
ret
zmazPilke endp
;********************************
;********************************
rysujPilke proc
;rysuje piłkę
cmp czas, 0
je rysujemyPilke
dec czas
ret
rysujemyPilke:
mov ah, klatki
mov czas, ah
cmp pilkay, 23
je aut
cmp pilkay, 3
je gora
w1:
cmp pilkax, 0
je lewo
w2:
cmp pilkax, 78
je prawo
w3:
cmp pilkay, 22
je dol
jmp rysuj
aut:
call zmazRakiete
call zmazPilke
mov punkty, 0
mov rakietax, 37
mov pilkax, 40
mov pilkay, 21
mov wektorx, 1
mov wektory, -1
call pauza
call rysujRakiete
ret
dol:
mov al, pilkax
add al, wektorx
cmp rakietax, al
ja rysuj
sub al, 5
cmp rakietax, al
jl rysuj
mov wektory, -1
inc punkty
jmp rysuj
gora:
mov wektory, 1
jmp w1
lewo:
mov wektorx, 1
jmp w2
prawo:
mov wektorx, -1
jmp w3
rysuj:
call zmazPilke
mov al, wektorx
mov ah, wektory
add pilkax, al
add pilkay, ah
mov dl, pilkax
mov dh, pilkay
call skocz
call cout
db 'o',0
ret
rysujPilke endp
;********************************
;********************************
pauza proc
;"game over"
mov dx, 0C14h
call skocz
call cout
db 'GAME OVER cieniasie haha :P (nacisnij spacje)',0
czekaj:
mov ah, 10h
int 16h
cmp ah, 01h
je exit
cmp ah, 39h
jne czekaj
call skocz
call cout
db ' ',0
ret
pauza endp
;********************************
;ZMIENNE_________________________
punkty dw 0 ; ilość punktów
rakietax db 37 ; położenie rakiety tenisowej
pilkax db 40 ; polozenie pilki
pilkay db 21 ; polozenie pilki
wektorx db 1 ; wektor ruchu piłki
wektory db -1 ; wektor ruchu piłki
koniec db 0 ; jeśli <> 0 to koniec programu
czas db 2 ; odlicza klatki
klatki db 2 ; co tyle klatek rysuje się pilka
;KONIEC__________________________
exit:
mov ah, 4ch
int 21h
end start |
|
|
Post został pochwalony 0 razy
Ostatnio zmieniony przez Popiol dnia Wto 23:49, 22 Lis 2005, w całości zmieniany 2 razy
|
|
|
|