KURS ASSEMBLERA
PODSTAWY
1. Wstęp
Na łamach tego kursu postaram się wytłumaczyć podstawowe informacje na temat Assemblera. Znajdą tutaj informacje początkujący, jak i zaawansowani programiści. Ze względu, że przychodziło wiele listów na temat tego kursu, że jest mało zrozumiały, postaram się teraz tłumaczyć wszystko od podstaw, krok po kroku.
Zacznijmy od tego, co nam będzie potrzebne. Assembler mimo iż jest językiem niskiego poziomu potrzebuje kompilatora, który przetłumaczy kod zrozumiały przez nas, na kod maszynowy, czyli taki, jaki rozumie komputer, a dokładnie procesor. Są takie programy, jak HIEW, które potrafią tłumaczy kod assemblerowy na maszynowy podczas wprowadzania programu, jednak nie nadają się one dla początkujących. My nasze programy będziemy kompilować przy pomocy programu TASM i konsolidować programem TLINK. Możesz je sobie ściągnąć z mojej strony lub jeśli masz zainstalowanego Turbo Pascal-a, albo C++, znaleźć w katalogu BIN.
Kompilator TASM jest kompilatorem bardzo rozbudowanym. Posiada bardzo wiele funkcji. Można nim tworzyć programy z rozszerzeniem OBJ. Takich programów nie da się jeszcze uruchomić. Trzeba je później skonsolidować używając programu TLINK.
Jeśli więc napiszemy program w dowolnym edytorze ASCII (możesz taki znaleźć na mojej stronie) i zapiszesz pod nazwą PRG.ASM, to by go skompilować piszesz:
TASM PRG.ASM
Zostanie wygenerowany plik PRG.OBJ. Teraz trzeba go skonsolidować. Piszemy:
TLINK PRG.OBJ
Powstaje plik PRG.EXE, który już możemy normalnie uruchomić. Jeśli chcemy wygenerować plik COM, to piszemy:
TLINK /t PRG.OBJ
Teraz powstanie plik PRG.COM
Istnieje jeszcze bardzo wiele innych kompilatorów. Bardzo znanym jest A86. Potrafi on kompilować zbiory ASM odrazu na COM-y lub OBJ. Piszemy wówczas:
A86 PRG.ASM
Powstanie plik PRG.COM
2. Strukrura programu
Każda linijka programu napisanego w Assemblerze ma następującą budowę:
[ETYKIETA:] [KOD OPERACJI] [ARGUMENTY] [;KOMENTARZ]
Wszystkie elementy instrukcji mogą występować w dowolnym miejscu linii, ale w podobnej kolejności i każdy musi być oddzielony od sąsiednich przynajmniej jedną spacją (odstępem). Ogólna długość instrukcji nie może być dłuższa niż 128 znaków. Nie można również kontynuować jednej instrukcji w następnym wierszu. Każda linia, to jedno polecenie.
np.:
PRZESLIJ: MOV AX,LICZNIK ;odtwarza licznik
W powyższym przykładzie kodem operacji jest MOV, argumentami są: rejestr AX i adres pola o nazwie LICZNIK. Pierwszy argument nazywamy argumentem docelowym, zaś drugi źródłowym. Argument źródłowy nigdy nie zmienia swojej wartości, natomiast docelowy prawie zawsze. Instrukcja z przykładu ma etykietę o nazwie PRZESLIJ i komentarz opisujący jej sens. Początek komentarza zaczyna się zawsze średnikiem. Wszystko, co jest napisane za średnikiem nazywamy więc komentarzem i nie jest to brane pod uwagę podczas generowania kodu wynikowego.
Etykieta jest opcjonalna, tzn. jej obecność nie jest obowiązkowa. Służy ona do tego, by móc przekazywać sterowanie (skok) do danej instrukcji. Etykieta może zajmować również oddzielną linię, np.:
PRZESLIJ:
MOV AX,LICZNIK
Każda etykieta zakończona jest dwukropkiem. W jej nazwie można używać liter, cyfr i znaków specjalnych. Nie można natomiast zacząć nazwę etykiety cyfrą. Trzeba ją poprzedzić innym znakiem.
3. Systemy liczbowe
By biegle władać assemblerem trzeba zapoznać się z systemami:
? dwójkowym
? szesnastowym
System dwókowy
W systemie dwójkowym dostępne są tylko dwie wartości 0 i 1. Wystarczają one jednak na zapisanie dowolnej liczby. Najmniejszy składnik pamięci - bit może zawierać właśnie jedną z tych wartości. Jest wiele metod przeliczania. Ja przedstawie chyba najprostrze. Poniższa tabela powinna ułatwić przeliczanie z systemu binarnego na dziesiętny:
27
26
25
24
23
22
21
20
128
64
32
16
8
4
2
1
Liczba 1011 będzie więc:
1*20+1*21+0*22+1*23=1*1+1*2+0*4+1*8=1+2+8=11
Tłumaczyć chyba nie trzeba, jednak jak ktoś nie rozumie, to to robię:
Numer bitu zawsze liczymy od prawej w lewo. Teraz każdy bit (pokolei od prawej) mnożymy przez dwa do potęgi [numer_bitu]. Trzeba dodać, że bity numerujemy od 0.
By przeliczyć z systemu dziesiętnego na binarny można posłużyć się również tabelką. Jest jednak na to prostrzy sposób. Daną liczbę dziesiętną dzilimy przez dwa. Jeśli wynik jest całkowity, to zapisujemy sobie 0, jeśli ma resztę, to zapisujemy 1 i ucinamy daną resztę. Teraz wykonujemy tą czynność tak długo, aż dojdziemy do 0, np. liczbę 11 przeliczymy tak:
11 : 2 = 5.5 (bit0=1)
5 : 2 = 2.5 (bit1=1)
2 : 2 = 1 (bit2=0)
1 : 2 = 0.5 (bit3=1)
0 - koniec
Spisujemy bity: 1011 = 11
Jeśli chcemy zapisać liczbę binarną w assemblerze, to musimy w niej używać tylko zer i jedynek oraz zakończyć ją znakiem B (binary), np.
MOV AX,1001B
System szesnastkowy
W Assemblerze używa się głównie systemu szesnastkowego. Ma on bardzo wiele zalet. Jest przejrzysty i prosty. W systemie tym mamy do dyspozycji cyfry od 0 do 9 i litery od A do F. Poniższa tabela pokazuje wartości dziesiętne poszczególnych liter:
Dziesiętny
Szesnastkowy
0
0
1
1
2
2
3
3
4
4
5
5
6
6
7
7
8
8
9
9
10
A
11
B
12
C
13
D
14
E
15
F
Przeliczać w tym systemie możemy tak jak w systemie binarnym przy pomocy tabelki. Mnożymy dany znak (licząc od prawej) prze 16 do potęgi [numer_znaku]., np. liczba C3 w systemie dziesiętnym wynosi:
3*160+C*161=3*1+12*16=3+192=195
Każdą liczbę heksadecymalną w Assemblerze zapisujemy kończąc literką H i jeśli dana liczba zaczyna się literą poprzedzamy ją cyfrą 0, np.:
MOV AH,34H
MOV DX,0C2H
MOV CX,3FH
REJESTRY I PROCESOR
Procesor jest sercem każdego komputera. Szybkość naszego komputera zależy od rodzaju i wielkości pamięci RAM, rodzaju karty graficznej ale w głównej mierze właśnie od procesora. Procesory takie jak 8086/8088/80286/80386/80486 wyszły już dawno z użytku. Czasami można dostrzec u kogoś jeszcze 486, ale to rzadkość. Nawet PENTIUM to przeżytek, teraz na rynku górują PENTIUM II i MMX.
Procesor pobiera z pamięci i wykonuje rozkazy sterujące całą pracą komputera. Ponieważ wszystkie procesory 8086 i w górę są ze sobą kompatybilne, będziemy się zajmować 8086.
Praca procesora polega na pobieraniu z pamięci rozkazów wraz z danymi i wykonywaniu ich. Dane i rozkazy niczym między sobą w pamięci się nie różnią. Ta sama liczba może być traktowana jako kod rozkazu lub wartość innego. Procesor steruje również pracą urządzeń zewnętrznych np. klawiatura, karta graficzna, muzyczna itp. Do współpracy z nimi służą tzw. porty wejścia/wyjścia. W celu szybszego operowania danymi każdy procesor został wyposażony w kilka rejestrów, czyli bajtów pamięci (najszybszej).
Procesory 8086 (i nowsze, jednak te posiadają jeszcze inne rejestry) posiada 14 rejestrów 16-bitowych. Osiem pierwszych z nich to tzw. rejestry ogólnego przeznaczenia. Cztery pierwsze można traktować zarówno jako rejestry 16-bitowe bądź jako złożenie dwóch rejestrów 8-bitowych, i tak:
AX (ang. Accumulator)
Rejestr ten jest wykorzystywany głównie do operacji arytmetycznych i logicznych. Można go traktować jako rejestr 16-bitowy bądz jako dwa rejestry 8-bitowe: AH i AL. AH jest to bardziej znacząca część rejestru AX, AL jest to mniej znacząca część. Bardziej znacząca oznacza, że są to drugie 8-bitów rejestru AX, a mniej znacząca to pierwsze 8-bitów. Trzeba tylko dodać, że kolejność bitów jest następująca: 7,6,5,4,3,2,1,0. Np. AX=234FH, więc AH=23H, a AL=4FH.
BX (ang. Base Registers)
Rejestr bazowy, głównie wykorzystywany przy adresowaniu pamięci. Rejestr ten również dzieli się na dwa rejestru ośmio bitowe: BH i BL.
CX (ang. Counter Registers)
Rejestr ten jest bardzo często wykorzystywany jako licznik, np. przy instrukcji LOOP. Dzieli się na dwa rejestry 8-bitowe CH i CL.
DX (ang. Data Register)
Rejestr danych, wykorzystywany przy operacjach mnożenia i dzielenia, a także do wysyłania i odbierania danych z portów. Dzieli się na DH i DL.
Wszystkie te rejestry, o ile instrukcja wykonywana zezwala mogą być wykorzystywane do innych celów. Trzeba tylko zauważyć częste błedy popełniane przez początkujących. Jeśli zmieniamy zawartość rejestru 16-bitowego np. AX zmieniają się również wartości rejestrów 8-bitowych składowych tego rejestru, w tym wypadku AH i AL, oraz odwrotnie, zmiana AH czy AL zmienia wartość rejestru AX.
Następne cztery rejestry ogólnego przeznaczenia to:
SI (ang. Source Index)
Rejestr indeksujący pamięć oraz wskazujący obszar z którego przesyła się dane.
DI (ang. Destination Index)
Rejestr indeksujący pamięć oraz wskazujący obszar, do którego przesyłamy dane.
SP (ang. Stack Pointer)
Wskaźnik stosu.
BP (ang. Base Pointer)
Rejestr używany do adresowania pamięci.
Jest jeszcze jeden rejestr, ale jest on nie osiągalny bezpośrednio przez programistę. Jest to IP (Instruction Pointer). Zawiera on adres aktualnie wykonywanej instrukcji i może być modyfikowany przez rozkazy sterujące pracą programu.
Podczas wykonywania operacji korzystających z pamięci , rejestry ogólnego przeznaczenia zawierają tylko offset tych adresów, czyli przesunięcie w danym segmencie. Te przekazywane są za pomocą innych rejestrów, tzw. rejestrów segmentowych. Są to:
CS (ang. Code Segment)
Rejestr zawierający segment aktualnie wykonywanego rozkazu.
DS (ang. Data Segment)
Rejestr zawierający segment z danymi.
ES (ang. Extra Segment)
Rejestr zawierający segment np. przy operacjach przesyłania łańcuchów.
SS (ang. Stack Segment)
Rejestr zawierający segment stosu.
Rejestry te są używane niejawnie w zależności od kontekstu, np. skok do komórki o adresie 0 tak naprewdę jest to skok do komórki o adresie CS:0, a odczyt komórki 0, to odczyt komórki o adresie DS:0. Można w niektórych sytuacjach podawać samemu segment i offset.
By zapisać jakąś wartość do któregoś z tych rejestrów, należy posłużyć się rejestrem ogólnego przeznaczenia np. by ustawić segment danych na 5 należy napisać:
MOV AX,5
MOV DS,AX
Rejestry segmentowe istnieją ze względu na podział pamięci komputera właśnie na segmenty 64KB-owe (w rzeczywistym trybie pracy procesora). Z tego też względu każda komórka pamięci opisana jest dwoma wartościami: numer segmentu i przesunięcie (offset) względem początku tego segmentu. Procesor oblicza adres rzeczywisty mnożąc numer segmentu przez 16 i dodając przesunięcie.
Ostatnim rejestrem jest tzw. rejestr znaczników. Do tego rejestru również nie można odwoływać się bezpośrednio. Do tego celu używa się specjalnych instrukcji. Dla programisty rejestr ten to 9 pojedynczych bitów informujących o stanie procesora, np. by porównać dwie wartości (wykonać polecenie Pascal'owe IF) używa się instrukcji CMP. Jednak wynik tej instrukcji tzn. czy np. dana liczba jest większa od innej uzyskujemy właśnie za pomocą odpowiednio ustawionych znaczników. Instrukcja CMP ustawia je w odpowiedni sposób. Część z tych znaczników można ustawiać, a część tylko odczytywać. Położenie i znaczenie poszczególnych bitów jest następujące:
O
D
I
T
S
Z
A
P
C
O (ang. Overlow Flag)
Znacznik nadmiaru, zostaje ustawiony przy wystąpieniu nadmiaru w operacjach arytmetycznych.
D (ang. Direction Flag)
Znacznik kierunku, określa czy dane będą przesyłane w kolejności adresów rosnących, czy malejących.
I (ang. Interrupt Flag)
Znacznik zezwolenia na przerwanie, określa czy przerwanie sprzętowe ma być wykonane natychmiast po zgłoszeniu, czy dopiero po skączeniu wykonywania programu.
T (ang. Trap Flag)
Znacznik pracy krokowej. Określa, czy po każdej wykonanej instrukcji procesora wykonywane jest przerwanie pracy krokowej.
S (ang. Sign Flag)
Znacznik znaku. Zawiera znak wyniku ostatnio wykonanej operacji arytemtycznej.
Z (ang. Zero Flag)
Znacznik zera.Zostaje ustawiony, jeśli wynikiem ostatniej operacji arytmetycznej był wynik zero.
A (ang. Auxiliary Carry Flag)
Znacznik przeniesienia połówkowego.
P (ang. Parity Flag)
Znacznik parzystości.
C (ang. Carry Flag)
Znacznik przeniesienia.
Jak pisałem wcześniej, wszystkie te rejestry, są dostępne od procesora 8086. Jednak od 386 dostępne są jeszcze inne rejestry:
EAX, EBX, ECX, EDX, EBP, ESI, EDI
Są to 32 bitowymi rozszerzeniami rejestrów AX, BX itd. Zależność jest taka sama jak AX i AL. Teraz AX jest mniejznaczącym słowem (2 bajty) rejestru EAX. Bardziej znaczącego słowa nie ma. Rejestry te są często wykorzystywane w operacjach matematycznych ze względu na dużą pojemność.
SZKIELET PROGRAMU
W Assemblerze programy pisze się zazwyczaj według jednego, stałego szablonu, deklaruje się segmenty kodu, stosu i danych. Do deklaracji tych elementów używa się specjalnych dyrektyw:
? ASSUME rejestr segmentowy: nazwa segmentu [...] - informuje kompilator, z którego rejestru segmentowego ma korzystać przy odwołaniach do etykiety podanego segmentu, np:
ASSUME cs: CODE
ASSUME cs:NOTHING
Linijka pierwsza informuje kompilator, że CS zawiera numer segmentu kodu. Teraz jeśli będziemy odwoływać się do etykiety, będziemy odwoływać się do rejestru kodu CS
W drugiej linijce anulujemy powiązanie z pierwszej linijki.
? SEGMENT, ENDS - dyrektyw tych używa się do deklaracji segmentów. Sposób użycia:
nazwa_segmentu SEGMENT [typ_segmentu] [połączenie] ['klasa']
gdzie:
? nazwa_segmentu - jest to dowolna nazwa przyjmowana przez kompilator. Może być później w programie wykorzystywana jako identyfikator segmentu.
? typ_segmentu - określa sposób przydzielania pamięci:
o byte - adres dowolny - segment ładowany jest w dowolnym miejscu
o word - adres parzysty - segment ładowany jest na granicy pełnego słowa
o para - adres podzielny przez 16 - segment ładowany jest na granicy pełnego paragrafu
o page - adres podzielny przez 256 - segment ładowany jest na granicy strony (1 strona=1024 bajty)
? połączenie - określa jak kompilator ma łączyć segmenty o tej samej nazwie
o public - segmenty o tej samej nazwie łączone są w jeden ciągły segment. Wszystkie adresy w segmencie są łączone względem jego początku, np.:
DANE1 SEGMENT PUBLIC
ZMIENNA1 DB 0
ZMIENNA2 DB 0
DANE1 ENDS
DANE1 SEGMENT PUBLIC
ZMIENNA3 DB 0
ZMIENNA4 DB 0
DANE1 ENDS
;zadeklarowanie tych dwóch segmentów jest równoważne
DANE1 SEGMENT PUBLIC
ZMIENNA1 DB 0
ZMIENNA2 DB 0
ZMIENNA3 DB 0
ZMIENNA4 DB 0
DANE1 ENDS
o stock - wszystkie segmenty o tej samej nazwie łączone są w jeden ciągły segment, który przy ładowaniu programu do pamięci inicjowany jest jako stos (SP pokazuje na ostatni bajt segmentu). Jeśli chce się definiować w programie segment stosu, trzeba zadeklarować dla niego parametr 'połączenie' - STACK. Jeśli zdefiniuje się stos bez parametru STACK, trzeba będzie samemu inicjować rejestry stosu.
o ommon - nakłada wszystkie segmenty o tej samej nazwie, umieszczając początek każdego w tym samym miejcu. Powstaje w efekcie obszar o wielkości największego z segmentów.
o memory - umieszcza wszystkie segmenty o tej samej nazwie w najwyższym fizycznym segmencie pamięci. Jeśli jest więcej niż jeden segment MEMORY nakładane są one jak w przypadku COMMON.
o AT adres - adresy wszystkich zmiennych i etykiet w segmencie są obliczane względem wartości segmentu podanej przy AT
? klasa - określa kolejność segmentów
Przykładowy szablon programu w Assemblerze:
DSTACK SEGMENT STACK 'STACK' ; deklaracją segmentu stosu
DB 64 DUP ('STACK') ; wypełnienie stosu
DSTACK ENDS ; koniec segmentu
ASSUME CS:CODE, SS:DSTACK ; przypisanie rejestrów do segmentów
CODE SEGMENT
Start:
CODE ENDS
DATA SEGMENT
DATA ENDS
END Start
? ORG - dyrektywa nadaje licznikowi adresów wartość wyrażenia. Wszystkie następujące po ORG adresy zaczynają się od zadeklarowanej wartości. Dyrektywa ta jest zazwyczaj używana do nadawania danym lub instrukcjom określonego przesunięcia w segmencie. Jeśli nasz program ma być zapisany w pliku COM, to musimy napisać:
ORG 100H
Co spowoduje przesunięcie początku o 100H bajtów. Jeśli nie wiesz czemu, zajrzyć do budowy plików COM.
OPERATORY
Assembler daje nam do dsypozycji dużą ilość różnorodnych operatorów. Dzielimy je na kilka grup:
OPERATORY ARYTMETYCZNE
Mamy do dyspozycji następujące operatory arytmetyczne:
? * - mnożenie
? / - dzielenie
? MOD - modulo
? + - dodawanie
? - - odejmowanie
np.:
MOV DX,OFFSET Napis+15
DB 256*4 DUP (?)
ADD BX,50/5
X EQU 1251 mod 1000
OPERATORY SHR i SHL
Przesuwanie bitów w prawo lub w lewo. Format:
wyrażenie SHR licznik
wyrażenie SHL licznik
Przykład:
MOV AX,101001B SHL 2
OPERATORY LOGICZNE
Zaliczamy do nich; NOT, AND, OR, XOR. Sosób użycia i format zapisu tak jak przy operatorach SHR i SHL.
OPERATOR PTR
Operator ten wymusza typ wyrażenia, tak by było ono traktowane w odpowiedni sposób. Ma ono następujący format:
typ PTR wyrażenie
Mamy dostępne następujące typy:
BYTE DWORD TBYTE FAR
WORD GWORD NEAR
np.:
MOV BYTE PTR [BX],5
CALL FAR PTR PROC1
OPERATOR SEG
Podaje wartość segmentu danego wyrażenia adresowego, np.:
MOV DX,SEG Napis
OPERATOR THIS
Definuje typ danej etykiety w programie lub argumentu w instrukcji. Typy są takie same jak przy PTR. Format:
THIS typ
np. dwa poniższe wyrażenia są sobie równe:
ETYKIETA EQU THIS WORD
ETYKIETA LABEL BYTE
OPERATORY HIGH i LOW
Zwracają bardziej (HIGH) lub mniej (LOW) znaczące 8 bitów danego wyrażenia liczbowego, np.:
MOV AL,HIGH LICZBA
SUB CL,LOW LICZBA
OPERATOR TYPE
Zwraca liczbę reprezentującą typ danego wyrażenia. Jeśli jest ona typu bajt, to 2, word, to 4, itd. Jeśli wyrażenie jest etykietą typu NEAR zwraca 0FFFFH, a jeśli FAR 0FFFEH, np.:
MOV AX,TYPE DANE
CALL (TYPE TYP_ADRESU) PTR PROCEDURA
OPERATOR LENGTH
Zwraca liczbę elementów ( nie bajtów ) zadeklarowanych w definicji danej zmiennej, np.:
TAB1 DB 24 DUP (4)
TAB2 DW 75 DUP (85 DUP (0))
...
MOV CX,LENGTH TAB1 ;W CX BEDZIE 24
SUB BX,LENGTH TAB2 ;BX ZOSTANIE POMNIEJSZONY o 75
OPERATOR SIZE
Zwraca łączną liczbę bajtów zaalokowanych dla zmiennej. Używamy jak LENGTH.
OPERATOR SHORT
Ustawia typ danej etykiety na SHORT. Zazwyczaj jest wykorzystywany w instrukcjach skoku, np.:
JMP SHORT ETYKIETA1
OPERATOR MASK
Podaje maskę dla pozycji bitowych zajmowanych w rekordzie przez pole. Bity odpowiadające danemu polu są równe 1, a pozostałe 0 .
ZMIENNE I STAŁE
ZMIENNE
W Assemblerze, jak i w innych językach programowania mamy do dyspozycji kilka dyrektyw, które umożliwiają definiowanie zmiennych.
* DB (define byte) - obszar o rozmiarze jednego bajta
* DW (define word) - zmienna o rozmiarze jednego słowa (dwóch bajtów)
* DD (define doubleword) - zmienna o rozmiarze dwóch słów (cztery bajty)
* DF - zmienna o rozmiarze trzech słów
* DQ - zmienna o rozmiarze czterech słów
* DT - zmienna o rozmiarze pięciu słów
Przykładowe zmienne:
X DW 0
LICZBA DW 123H
A DD ?
LITERA DB "A"
Deklarując zmienne nadajemy im konkretne wartośći. Można również przypisać, tak jak jest w przykładzie literę, co w rezultacie jest przypisaniem liczby o kodzie ASCII danej litery. Jeśli wartość liczby nie jest wiadoma, dana zmienna będzie dopiero wykorzystana, wpisujemy znak zapytania. Wówczas zmienna nie ma określonej wartości.
W zmiennych można przetrzymywać również etykiety, np:
START:
...
...
ADRES16 DW START ; zmienna zawiera przesunięcie (offset) etykiety START
ADRES32 DD START ; zmienna zawiera segment i przesunięcie etykiety START
W Assemblerze można również deklarować łańcuchy, np:
POTEGI_LICZBY_2 DW 1, 2, 4, 8, 16, 32, 64
KODY DB 123, 54, 12, 143
Można deklarować również teksty, lub teksty i liczby, np:
NAPIS DB "Autor: Karol Wierzchołowski"
KOMUNIKAT DB "ERROR CODE 23"
COS_TAM DB 12,"123",23,56
Teksty można podawać w cudzysłowiach bądź apostrofach. Jeśli jednak otworzysz cudzysłów, to musisz później go zamknąć również cudzysłowiem.
Jeśli chcemy stworzyć tablicę o dużej wielkości, można posłużyć się dyrektywą DUP, np.
TABLICA DB 1024 DUP (?)
Zostanie stworzona tablica 1KB-owa o nieokreślonych wartościach. Można jednak nadać jej wartość, np.:
TABLICA DB 1024 DUP (0)
Zostanie stworzona taka sama tablica, tylko zostanie wyzerowana. Można również tak:
TABLICA DB 1024 DUP ('BINBOY')
Jeśli chcemy zadeklarować bardzo dużą tablicę, to deklarujemy ją w osobnym segmencie, np. jeśli będziemy przechowywać w niej obrazek, to zrobimy tak:
SCREEN SEGMENT
EKRAN DB 64000 DUP (?)
SCREEN ENDS
STRUKTURY
nazwa_struktury STRUCT
pole1
pole2
...
nazwa_struktury ENDS
Dyrektywa STRUCT umożliwia definiowanie struktur danych, tj. pewne zgrupowanie zmiennych, niekoniecznie tego samego typu. Sama definicja struktury nie powoduje włączenia danych do programu. Zapamietywana jest ona dopiero przy jej zadeklarowaniu.
Poszczególne pola struktury mają następujący format:
nazwa_pola DB wartość_początkowa
Nazwa_pola określa przesunięcie pola liczone od początku struktury. Struktur nie można zagnieżdżać.
Ogólny format deklaracji struktury jest następujący:
[nazwa] nazwa_struktury <[wart._pola1],...>
Do struktury odwołujemy się za pomocą kropki, np.:
PRACOWNIK STRUCT
NAZWISKO DB ' '; nazwisko
IMIE DB ' '; imię
WIEK DB ?
STAZ DB ?
PRACOWNIK ENDS
KOWALSKI PRACOWNIK <"Kowalski","jan",,2>
Mając juz zadeklarowaną strukturę odwołujemy się do niej np. tak:
MOV AL,KOWALSKI.STAZ
Do AL zostanie załadowany staż naszego Kowalskiego. Można również tak:
LEA BX,KOWALSKI
......
MOV AL,[BX].STAZ
REKORDY
nazwa_rekordu RECORD nazwa_pola:szerokosc [=wyrazenie],...
Dyrektywa RECORD definuje pewien rodzaj struktury danych, gdzie operujemy na bitach. Nazwa_rekordu identyfikuje nasz rokord. Nazwa_pola określa pole w rekordzie, szerokość, liczbę bitów, z których się składa pole, a opcjonalne "wyrazenie", nadaje początkową wartość, np.:
DATA RECORD ROK:7,MIESIAC:4,DZIEN:5
Tak zadeklarowany rekord tworzy strukturę postaci:
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
ROK
MIESIĄC
DZIEŃ
Format deklaracji:
[nazwa] nazwa_struktury <[wartosc],..>
np.:
DZIS DATA <87,12,31>
Z tak zdefiniowanej daty wyizolujemy wartość roku. Użymy więc operatora MASK:
MOV BX,DZIS
AND BX,MASK ROK ;IZOLUJE BITY ROKU
MOV CL,ROK ;LICZBA BITÓW Z PRAWEJ STRONY POLA
SHR BX,CL
W BX mamy naszą datę.
STAŁE I SYMBOLE
Podczas pisania programów często zachodzi potrzeba użycia tych samych wartości. Wówczas można stworzyć stałą przy pomocy dyrektywy EQU. Działanie jej polega na przypisaniu nazwie określonej stałej wartości, np:
LICZBA EQU 123
Można również przypisywać do stałych wyrażenia, np:
GETAL EQU MOV AL, DS:[SI]
Teraz w programie wpisanie:
GETAL
powoduje wykonanie instrukcji MOV AL, DS:[SI]
W Assemblerze można również przypisać inną nazwę rejestrowi, np:
REJ_AX EQU AX
STOS
Stos-jest to pewien rodzaj podręcznej pamięci. Używany jest do zapamiętywania na pewien czas pewnych wartości. Na stosie można wykonać tylko dwie operacje: położyć coś na stos (zapamiętać) i zdjąć coś ze stosu. Do zapamiętywania na stosie służy instrukcja PUSH. Do zdejmowania ze stosu POP. By położyć na stos wartość rejestru AX napiszemy:
PUSH AX
Jeśli teraz chcemy zdjąc wartość ze stosu i zapamiętać pod BX napiszemy:
POP BX
W ten sposób wykonaliśmy operację BX<-AX
Instrukcja PUSH może mieć również wartości liczbowe, np.:
PUSH 32H
Używa się tego np. do nadawania określonych wartości rejestrom segmentowym. Wiemy, że nie wolno napisać:
MOV ES,0A000H
Trzeba najpierw wartość 0A000 zapamiętać pod pewnym rejestrem, a dopiero później przesłać ten rejestr do ES, np.
MOV AX,0A000H
MOV ES,AX
Można to również wykonać stosując stos. Napiszemy:
PUSH 0A000H
POP ES
Zapis drugi jest krótszy, po skompilowaniu zajmie 4 bajty, a pierwszy 5.
Na stos można również położyć wartość odpowiedniej komórki pamięci, np:
TABLICA DW 1000 DUP (123)
...
PUSH TABLICA[BX]
Można również zapamiętać na stosie rejestr znaczników. Do tego celu używa się instrukcji PUSHF, o do zdjęcia rejestru znaczników używa się instrukcji POPF. Jeśli chcemy położyć na stosie wszystkie rejestry (oprócz segmentowych) piszemy PUSHA, a do zdjęcia POPA.
Trzeba tylko dodać, że stos można wyobrazić sobie pudełkiem, do którego wkładamy karty (karta-wartość). Jeśli położymy na stosie AX, a później BX, to na wierzchu mamy kartę BX. Jeśli teraz byśmy chcieli przywrócić wartość AX musielibyśmy zdjąć BX (zobaczylibyśmy teraz AX) i dopiero zdjęli AX. Trochę to zagmatfane, ale cóż :-))) Chyba wszyscy zrozumieli.
Ze stosem związane są jeszcze dwa rejestry SS i SP. Rejestr SS wskazuje segment stosu, natomiast SP jego wierzchołek. Jeśli więc położymy coś na stosie zmniejszymy wartość SP o dwa (na stosie kładziemy wartości 16 bitowe).
Segment stosu trzeba zadeklarować. Jeśli tego nie zrobimy, kompilator wyświetli komunikat:
Warning: No Stack.
Jeśli segment nie zostanie zadeklarowany DOS przydzieli mu gdzieś w pamięci miejsce, jednak wówczas program może nie działać poprawnie. Dlatego należy stos deklarować samemu, np. tak:
SSTACK SEGMENT STACK
DW 1000 DUP (?)
SSTACK ENDS
ASSUME SS:SSTACK
Zadeklarowaliśmy stos na 1000 bajtów. Komunikat już się nie pojawi
PROCEDURY I MAKRA
Deklarowanie procedur
Assembler, jak i inne języki programowania, daje nam możliwość tworzenia własnych procedur. Do tego celu używa się dyrektywy PROC poprzedzonej nazwą naszej procedury. Ogólna budowa jest następująca:
Nazwa PROC
RET
Nazwa ENDOP
Instrukcja RET powoduje powrót do miejsca, z którego procedura została wywołana. Do wywołania procedury używa się instrukcji CALL. Parametrem CALL może być również bezpośredni adres. Używa się tego, gdy np. chcemy wywołać odpowiednią procedurę w zależności od wartości rejestru BX. Adresy tych procedur, lub ich nazwy podajemy w tablicy, np:
tablica_procedury DD proc0, proc1, proc2
; bx zawiera wartość od 0 do 2
mov si,bx ; zachowaj BX
shl si,1 ; policz przesunięcie
shl si,1 ; adresu w tablicy
call tablica_procedur[si]
; deklaracja procedur
proc0 PROC
RET
ENDP
...
By procesor wiedział, gdzie ma później wrócić podczas wywoływania instrukcji CALL na stosie zostanie położony: w przypadku procedury NEAR rejestr IP. Później wykonany skok i po napodkaniu RET następuje zdjęcie ze stosu IP, czyli powrót do instrukcji CALL. W przypadku procedury FAR na stosie zostanie położony rejestr CS i IP, dalsze działanie jest takie same jak przy procedurach NEAR.
Deklarowanie makroinstrukcji
Makroinstrukcja jest to zbiór instrukcji Assemblera oznaczonych jedną nazwą. Jeśli więc jakąś sekwencję wykonujemy w programie wiele razy możemy stworzyć odpowienie makro. Jest to znacznie szybsze od tworzenia procedur, jednak stosowanie makroinstrukcji powoduje zwiększenie objętości programu. Jak już wiemy, procedury, są to podprogramy, do których wykonujemy skok. Skok jest wolny. Makro jest to sekwencja, która zostanie wstawiona we wskazane miejsce. Nie ma wówczas straty czasu na skoki, jednak objetość rośnie.
Makroinstrukcję deklarujemy w następujący sposób:
nazwa MACRO lista_parametrów
ENDM
Tak może wyglądać przykładowa makroinstrukcja:
Print MACRO tekst
PUSHA
MOV AH,09H
PUSH CS
POP DS
LEA DX,tekst
INT 21H
POPA
ENDM
Teraz program może wyglądać tak:
Print Napis1
Print Napis2
...
Napis1 DB "BY BINBOY",13,10,'$'
Napis2 DB "http://binboy.koti.com.pl",13,10,'$'
Podczas pisania makroinstrukcji zachodzi czasami konieczność sprawdzenia, czy jakiś argument został podany, czy też nie. Do tego celu służą dyrektywy warunkowe, i tak:
* IFB - powoduje wstawienie do programu instrukcji znajdujących się między IFB a ENDIF tylko wtedy, gdy parametr podany do IFB jest pusty.
* IFNB - powoduje wstawienie do programu kodu, gdy barametr nie jest pusty
* EXITM - powoduje zakończenie makroinstrukcji
Przykładowa makroinstrukcja DELAY
Delay MACRO ile
PUSH CX
IFB ile
MOV CX,1000
ENDIF
IFNB ile
MOV CX,ile
ENDIF
WAI:
LOOP WAI
POP CX
ENDM
Często używane makroinstrukcje wygodnie jest przechowywać w osobnym pliku, dołączanym do programu za pomocą dyrektywy include, np:
include graph.asm
PRZERWANIA
Przerwanie jest to zatrzymanie wykonującego się w danej chwili programu i przekazanie sterowania do procedury, która to przerwanie obsługuje. Po zakończeniu się tej procedury następuje powrót do wcześniej wykonywanego programu. Istnieją dwa rodzaje przerwań: przerwania sprzętowe i systemowe. Przerwania sprzętowe wywoływane są w krytycznych sytuacjach (na rządanie urządzeń zewnętrznych), np. gdy naciśnięto klawisz Ctrl+Break na klawiaturze, lub gdy jest błąd parzystości pamięci. Ze względu na to, że w jednej chwili może zgłosić kilka urządzeń rządanie o przerwanie, a procesor w danej chwili może obsłużyć tylko jedno z nich, każdemu przerwaniu sprzętowemu przyporządkowano pewien numer (priorytet). Numer ten mówi w jakiej kolejności należy wykonywać przerwania, np. jeśli w tej samej chwili zgłosi rządanie o przerwanie klawiatura i zegar, procesor najpierw wykona przerwanie zegarowe, ponieważ ma większy priorytet (0), a dopiero później przerwanie klawiatury (priorytet 1). Przerwania sprzętowe oznacza się jako IRQx, gdzie x jest numerem priorytetu. Przerwanie o najwyższym priorytecie (IRQ0) ma numer 8 i jest to przerwanie zegarowe. Tabela części przerwań sprzętowych znajduje się na końcu artykułu. Przerwania sprzętowe mogą być maskowalne lub nie. Przerwania maskowalne nie muszą być przekazywane do procesora w przeciwieństwie do przerwań niemaskowalnych. W komputerach IBM PC istnieje przerwanie maskowalne NMI (ang. Non Maskable Interrupt). Jest ono generowane w sytuacjach krytycznych, np. przy błędzie parzystości pamięci. Przerwania systemowe mogą być wywoływane tylko w sposób programowy.W procesorach rodziny 8086 adresy przerwań przechowywane są w tzw. tablicy wektorów przerwań. Tablica ta zajmuje 1024 bajty pamięci i rozpoczyna się od adresu 0000:0000, czyli na samym początku pamięci komputera. Adresy procedur przerwań są 32-bitowe, a więc adres do jednego przerwania zapisany jest w czterech bajtach. W dwóch pierwszych bajtach zapisany jest offset, a w następnych dwóch segment. Np. offset przerwania 5 zapisany jest pod adresem: 0000:0014H, a segment pod adsresem 0000:0016H. Do szybkiego znajdywania adresu przerwania można posłużyć się wzorem:
offset=4*NR
segment=4*NR+2
, gdzie NR - numer przerwania.
Jeśli chcemy np. zmienić adres przerwania zegarowego 1CH, tak aby wskazywał na naszą procedurę, zakładając, że nasza procedura zaczyna się etykietą PRZERWANIE napiszemy ciąg instrukcji:
MOV AX,0
MOV ES:AX
MOV ES:70H,OFFSET PRZERWANIE
MOV ES:72H,SEG PRZERWANIE
Można również to uczynić za pomocą przerwania 21H i funkcji 25H. W rejestrze AL podajemy numer przerwania, natomiast adres przekazujemy w DS:DX. Ten sam przypadek co wyżej uzyskany za pomocą przerwań będzie wyglądać następująco:
MOV AX,SEG PRZERWANIE
MOV DS,AX
MOV DX,OFFSET PRZERWANIE
MOV AL,01CH
MOV AH,25H
INT 21H
Za pomocą przerwania 21H i funkcji 35H możemy uzyskać informacje o adresie procedury obsługi danego przerwania. W rejestrze AL podajemy wówczas numer interesującego nas przerwania, a w odpowiedzi dostaniemy w ES:BX adres tego przerwania.
Każda procedura obsługi przerwania musi być zakończona instrukcją powrotu IRET. Działa ona na wszystkich procesorach. Powoduje zdjęcie ze stosu rejestrów IP, CS oraz rejestru znaczników. Na procesorach 80386 można użyć instrukcji IRETD.
PASCAL
Również Pascal umożliwia nam zmianę adresu procedury osługi przerwania, jak i informacje o aktualnym adresie. Do tego celu służą dwie procedury zaimplementowane w module DOS. Są to: GetIntVec oraz SetIntVec. Pierwsza służy do odczytania adresu procedury obsługi przerwania, natomiast druga ustawia adres danej procedury.
SetIntVec(IntNo : Byte; Vector : Pointer);
Procedura powoduje ustawienie adresu procedury obsługi przerwania o numerze podanym w IntNo. Adres tej procedury określony jest zmienną Vector, która jest typu Pointer..
GetIntVec(IntNo : Byte; var Vector : Pointer);
Procedura zwraca adres procedury obsługi przerwania o numerze podanym w IntNo. Adres ten jest zwracany w zmiennej Vector typu Pointer.
Każda procedura obsługi przerwania powinna mieć następującą składnie:
PROCEDURE Nazwa_procedury(Flags, CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP : Word);
INTERRUPT;
Blok
Właśnie dzięki dyrektywie INTERRUPT jest możliwe pisanie procedur obsługi przerwań za pomocą języka Turbo Pascal. Podana w nagłówku procedury lista parametrów może być krótsza i używać innych nazw, ale zawartość rejestrów jest przesyłana właśnie w takiej kolejności.
Procedura Intr(IntNo : Byte; var Regs : TRegisters);powoduje wykonanie przerwania programowego o numerze podanym w IntNo. Zmienna Regs jest zmienną rekordową zdefiniowaną w module WinDos, określającą rejestry procesora. Przed wykonaniem przerwania powinniśmy ustawić odpowiednio te rejestry.
Procedura MsDos(var Regs : TRegister); powoduje wykonanie przerwania DOS'a (21H). Zmienna Regs określa rejestry procesora.
Do pisania programów rezydentnych w Pascal'u potrzebna jest również procedura Keep, która powoduje pozostawienie programu w pamięci. Deklaracja tej procedury jest następująca:
PROCEDURE Keep(kod_wyjscia : Word);
Długość bloku pamięci jest z góry ustalona i równa całej przydzielonej wstępnie programowi pamięci, tak więc by określić jej wielkość używa się dyrektywy $M.
Przerwania sprzętowe
8
IRQ0
zegar
9
IRQ1
klawiatura
A
IRQ2
zarezerwowane
B
IRQ3
drugi port szeregowy
C
IRQ4
pierwszy port szeregowy
D
IRQ5
dysk twardy
E
IRQ6
stacja dysków elastycznych
F
IRQ7
drukarka
IMPLEMENTACJA PROCEDURY KEEEP
IMPLEMENTACJA PROCEDURY GETINTVEC
IMPLEMETACJA PROCEDURY SETINTVEC
DATA SEGMENT WORD PUBLIC
EXTRN PrefixSeg : WORD
DATA ENDS
CODE SEGMENT BYTE PUBLIC
ASSUME CS:CODE, DS:DATA
PUBLIC Keep
Keep PROC FAR
ExitCode EQU (BYTE PTR [BP+6])
PUSH BP
MOV BP,SP
MOV AX,PrefixSeg
MOV ES,AX
MOV DX, WORD PTR ES:2
SUB DX,AX
MOV AL,ExitCode
MOV AH,31H
INT 21H
POP BP
RET 2
Keep ENDP
CODE ENDS
END
CODE SEGMENT BYTE PUBLIC
ASSUME CS:CODE
PUBLIC GetIntVec
GetIntVec PROC FAR
IntNo EQU (BYTE PTR [BP+10])
VectorP EQU (DWORD PTR [BP+6])
PUSH BP
MOV BP,SP
MOV AL,IntNo
MOV AH,35H
INT 21H
MOV AX,ES
LES DI,VectorP
CLD
XCHG AX,BX
STOSW
XCHG AX,BX
STOSW
POP BP
RET 6
GetIntVec ENDP
CODE ENDS
END
CODE SEGMENT BYTE PUBLIC
ASSUME CS:CODE
PUBLIC SetIntVec
SetIntVec PROC FAR
IntNo EQU (BYTE PTR [BP+10])
Vector EQU (DWORD PTR [BP+6])
PUSH BP
MOV BP,SP
PUSH DS
LDS DX,Vector
MOV AL,IntNo
MOV AH,25H
INT 21H
POP DS
POP BP
RET 6
SetIntVec ENDP
CODE ENDS
END
OPERACJE ARYTMETYCZNE
Do operacji arytmetycznych będziemy zaliczać:
ADD rejestr, zmienna
Dodawanie
SUB rejestr, zmienna
Odejmowanie
INC rejestr
Zwiększanie o jeden
DEC rejestr
Zmniejszanie o jeden
MUL rejestr lub zmienna
Mnożenie
DIV rejestr lub zmienna
Dzielenie
Dodawanie - ADD
Instrukcja ADD powoduje dodanie do siebie dwóch rejestrów (rejestru i zmiennej) oraz umieszczenie wyniku w pierwszym operandzie. Pewną odmianą polecenia ADD jest instrukcja ADC, która wykonuje tą samą czynność, ale uwzględniając przeniesienie. Ów przeniesienie zachodzi wtedy, dgy wynik operacji nie mieści się w przewidzianych granicach. Przykładowo:
MOV AX, 0FFFFH
MOV BX, 1
ADD AX, BX
W rejestrze AX znajdzie się wartość 0, więc znacznik C zostanie ustawiony. Po takiej sytuacji przy kolejnym dodawaniu zostanie to uwzględnione (jeśli posłużymy się ADC).
Odejmowanie - SUB
Instrukcja SUB powoduje odjęcie od siebie dwóch rejestrów (rejestru i zmiennej) oraz umieszczenie wyniku w pierwszym operandzie. Pewną odmianą polecenia SUB jest instrukcja SBB, która wykonuje tą samą czynność, ale uwzględniając przeniesienie. Ów przeniesienie zachodzi wtedy, gdy wynik operacji nie mieści się w przewidzianych granicach.
Mnożenie - MUL
W przypadku mnożenia 8-bitowego drugi czynnik musi znajdować się w AL, a wynik znajdzie się w AX. Przy mnożeniu liczb 16-bitowych drugi czynnik przekazujemy a rejestrze AX, natomiast wynik otrzymujemy w DX:AX. Przykładowo:
MOV AL, 10H
MOV AH, 50H
MUL AH
W rejestrze AX znajdzie się wartość 500H
Przy mnożeniu liczb przez potęgę liczby 2 powinno stosować się instrukcji SHL . Jest ona szybsza i bezpieczniejsza.
Dzielenie - DIV
W przypadku dzielenia 8-bitowego dzielna powinna znajdować się w AX, a wynik znajdzie się w AH. Przy dzieleniu liczb 16-bitowych dzielną przekazujemy w rejestrze DX:AX, natomiast wynik otrzymujemy w AX. Jeśli wynik nie mieści się w granicach rejestru lub nastąpiło dzielenie przez 0 następuje wywołanie przerwania sprzętowego 0. Przykładowo:
MOV AX, 37
MOV DL, 12
DIV DL
W rejestrze AL znajdzie się wartość 3 (37/12), a w rejestrze AH 1 (reszta z 37/12).
Przy dzieleniu liczb przez potęgę liczby 2 powinno się stosować instrukcji SHR , gdyż jest ona szybsza i bezpieczniejsza.]
PRZESUNIĘCIA, OBROTY I ROZKAZY LOGICZNE
Operacje związane z tymi pojęciami są bardzo ważne, dlatego poświęciłem oddzielną stronę na ten temat. By sprawnie posługiwać się zawartymi tutaj opisami i instrukcjami powinieneś powtórzyć sobie lekcję systemu dwójkowego (binarnego). Każda liczba w postaci binarnej powinna być zakończona literą B.
Zacznijmy od przesunięć i obrotów. Wszystkie te operacje można wykonywać na rejestrach 8 i 16 bitowych. Drugim argumentem instrukcji jest zawsze cyfra 1 bądz zawartość rejestru CL. Trzeba tylko jeszcze przypomnieć, że kolejne pola bitowe rejestru są w następującej kolejności: 7,6,5,4,3,2,1,0 i że literka C oznacza znacznik rejestru znaczników, a dokładnie znacznik przesunięcia
Instrukcje można używać w następujący sposób, np.:
MOV AL,15H
MOV CL,2
SHL AL,CL
Są następujące instrukcje związane z obrotami i przesunięciami:
SHL rejestr lub zmienna, liczba
Przesunięcie logiczne w lewo
SAL rejestr lub zmienna, liczba
Przesunięcie arytmetyczne w lewo
SHR rejestr lub zmienna, liczba
Przesunięcie logiczne w prawo
SAR rejestr lub zmienna, liczba
Przeunięcie arytmetyczne w prawo
ROL rejestr lub zmienna, liczba
Obrót w lewo
RCL rejestr lub zmienna, liczba
Obrót w lewo z przeniesieniem
ROR rejestr lub zmienna, liczba
Obrót w prawo
RCR rejestr lub zmienna, liczba
Obrót w prawo z przeniesieniem
Tutaj są instrukcje logiczne:
NOT rejestr lub zmienna
Negacja logiczna
AND rejestr lub zmienna, rejestr lub zmienna
Iloczyn logiczny
TEST rejestr lub zmienna, rejestr lub zmienna
Iloczyn logiczny bez zmiany operandów
OR rejestr lub zmienna, rejestr lub zmienna
Suma logiczna
XOR rejestr lub zmienna, rejestr lub zmienna
Różnica symetryczna
Instrukcje SHL i SAL
C
<--
7
<--
6
<--
5
<--
4
<--
3
<--
2
<--
1
<--
0
<--
0
Przesunięcie logiczne w lewo (SHL) i przesunięcie arytmetyczne w lewo (SAL). Znacznik C jest wyzerowany.
Przesunięcie logiczne w lewo jest szybszą instrukcją pozwalającą na mnożenie liczby przez potęgę liczby 2. By np. pomnożyć 4*8, należy napisać ciąg instrukcji:
MOV AL,4 ; 00000100B
MOV CL,3 ; 23=8
SHL AL,CL
Po wykonaniu tych instrukcji w rejestrze AL znajdzie się wartość 32 (00100000B) czyli 4*8=32.
Instrukcja SHR
0
-->
7
-->
6
-->
5
-->
4
-->
3
-->
2
-->
1
-->
0
-->
C
Przesunięcie logiczne w prawo. Znacznik C zostaje ustawiony.
Przesunięcie logiczne w prawo jest często stosowano jako instrukcja do dzielenia przez potęgę liczby 2, gdyż jest dużo szybsza od innych podobnych instrukcji. By np. podzielić liczbę 32 przez 16 należy napisać ciąg instrukcji:
MOV AL,32 ; 0010000B
MOV CL,4 ; 24=16
SHR AL,CL
W rejestrze AL znajdzie się wartość 2 (00000010B) czyli 32/16.
Instrukcja SAR
bit 7
-->
7
-->
6
-->
5
-->
4
-->
3
-->
2
-->
1
-->
0
-->
C
Przesunięcie arytmetyczne w prawo. Znacznik C wyzerowany. Bit 7 jest powielany.
Instrukcja ROL
C
<--
7
<--
6
<--
5
<--
4
<--
3
<--
2
<--
1
<--
0
<--
bit 7
Obrót w lewo. Znacznik C zostaje wyzerowany.
Instrukcja RCL
C
<--
7
<--
6
<--
5
<--
4
<--
3
<--
2
<--
1
<--
0
<--
C
Obrót w lewo z przeniesieniem. Znacznik C zostaje wyzerowany.
Instrukcja ROR
bit 0
-->
7
-->
6
-->
5
-->
4
-->
3
-->
2
-->
1
-->
0
-->
C
Obrót w prawo. Znacznik C ustawiony.
Instrukcja RCR
C
-->
7
-->
6
-->
5
-->
4
-->
3
-->
2
-->
1
-->
0
-->
C
Obrót w prawo z przeniesieniem, znacznik C zostaje ustawiony.
Instrukcja NOT
Instrukcja wykonuje negację logiczna. Jej działanie polega na negowaniu każdego bitu argumentu, czyli zmianie wartości na przeciwną np.
MOV AX, 14B5H ; 0001010010110101B
NOT AX
Wynikiem powyższych instrukcji jest wartość AX=0EB4AH (1110101101001010B)
Instrukcja AND
Instrukcja wykonuje iloczyn logiczny. Parametrami jej mogą być rejestry, zmienne lub wartości liczbowe. Iloczynowi logicznemu podlegają odpowiadające sobie bity obu parametrów. Po wykonaniu tej instrukcji znaczniki C i O zostają ustawione, a znaczniki S, P, Z są ustawione zgodnie z wynikiem operacji (dla 0,0, dla 1,0 i dla 1,1). Wynik tej instrukcji przesyłany jest do pierwszego operandu. Tabela pokazuje sposób działania instrukcji AND:
A
B
Wynik
0
0
0
1
0
0
0
1
0
1
1
1
Instrukcja TEST
Instrukcja działa tak samo jak AND, tylko że nie zmienia wartości argumentów, a ustawia odpowiednio znaczniki.
Instrukcja OR
Instrukcja wykonuje sumę logiczną. Parametrami jej mogą być rejestry, zmienne lub wartości liczbowe. Sumie podlegają odpowiadające sobie bity obu argumentów. Po wykonaniu tej instrukcji znaczniki C i O zostają ustawione, a znaczniki S, P, Z są ustawione zgodnie z wynikiem operacji (dla 0,0, dla 1,0 i dla 1,1). Wynik tej instrukcji przesyłany jest do pierwszego operandu. Tabela pokazuje sposób działania instrukcji OR:
A
B
Wynik
0
0
0
1
0
1
0
1
1
1
1
1
Instrukcja XOR
Instrukcja wykonuje różnicę symetryczną. Parametrami jej mogą być rejestry, zmienne lub wartości liczbowe. Różnicy podlegają odpowiadające sobie bity obu argumentów. Po wykonaniu tej instrukcji znaczniki C i O zostają ustawione, a znaczniki S, P, Z są ustawione zgodnie z wynikiem operacji (dla 0,0, dla 1,0 i dla 1,1). Wynik tej instrukcji przesyłany jest do pierwszego operandu. Tabela pokazuje sposób działania instrukcji XOR:
A
B
Wynik
0
0
0
1
0
1
0
1
1
1
1
0
INSTRUKCJE
Warunki
Instrukcja wyboru w Assemblerze ma postać CMP. Porównuje ona dwa argumenty, a dokładniej mówiąc odejmuje zawartość pierwszego od drugiego. Wynik nie zostaje nigdzie zapisany, zostają ustawione tylko odpowiednie znaczniki. Po wykonaniu instrukcji CMP należy wykonać skok warunkowy.
Instrukcja
Opis
JB/JNAE rel8
Skok gdy mniejszy
Skok gdy nie większy lub równy
C=1
JAE/JNB rel8
Skok gdy większy lub równy
Skok gdy nie mniejszy
C=0
JBE/JNA rel8
Skok gdy mniejszy lub równy
Skok gdy nie większy
C=1 lub Z=1
JA/JNBE rel8
Skok gdy większy
Skok nie mniejszy lub równy
C=0 i Z=0
JE/JZ rel8
Skok gdy równy
Z=1
JNE/JNZ rel8
Skok gdy nie równy
Z=0
JL/JNGE rel8
Skok gdy mniejszy (liczby ze znakiem)
Skok gdy nie większy lub równy
S=0
JGE/JNL rel8
Skok gdy nie większy (liczby ze znakiem)
Skok gdy nie mniejszy
S=0
JNG/JLE rel8
Skok gdy mniejszy lub równy
Skok gdy nie większy (liczby ze znakiem)
Z=1 lub S=0
JG/JNLE rel8
Skok gdy większy (liczby ze znakiem)
Skok gdy nie mniejszy lub równy
Z=0 i S=0
JP/JPE rel8
Skok przy parzystości
P=1
JNP/JPO rel8
Skok przy braku parzystości
P=0
JS rel8
Skok gdy znak ujemny
S=1
JNS rel8
Skok gdy znak dodatni
S=0
JC rel8
Skok przy przeniesieniu
C=1
JNC rel8
Skok przy braku przeniesienia
C=0
JO rel8
Skok przy nadmiarze
O=1
JNO rel8
Skok przy braku nadmiaru
O=0
JCXZ rel8
Skok jeśli rejestr CX=0
CX=0
JECXZ rel8
Skok jeśli rejestr ECX=0 (tylko procesory 80386)
ECX=0
JB/JNAE rel16/32
Skok gdy mniejszy
Skok gdy nie większy lub równy
C=1
JAE/JNB rel16/32
Skok gdy większy lub równy
Skok gdy nie mniejszy
C=0
JBE/JNA rel16/32
Skok gdy mniejszy lub równy
Skok gdy nie większy
C=1 lub Z=1
JA/JNBE rel16/32
Skok gdy większy
Skok gdy nie mniejszy lub równy
C=0 i Z=0
JE/JZ rel16/32
Skok gdy równy
Z=1
JNE/JNZ rel16/32
Skok gdy nie równy
Z=0
JL/JNGE rel16/32
Skok gdy mniejszy (liczby ze znakiem)
Skok gdy nie większy lub równy
S=0
JGE/JNL rel16/32
Skok gdy nie większy (liczby ze znakiem)
Skok gdy nie mniejszy
S=0
JNG/JLE rel16/32
Skok gdy mniejszy lub równy
Skok gdy nie większy (liczby ze znakiem)
Z=1 i S=0
JG/JNLE rel16/32
Skok gdy nie większy (liczby ze znakiem)
Skok gdy nie mniejszy lub równy
Z=0 i S=0
JP/JPE rel16/32
Skok przy parzystości
P=1
JNP/JPO rel16/32
Skok przy braku parzystości
P=0
JS rel16/32
Skok gdy znak ujemny
S=1
JNS rel16/32
Skok gdy znak dodatni
S=0
JC rel16/32
Skok przy przeniesieniu
C=1
JNC rel16/32
Skok przy braku przeniesienia
C=0
JO rel16/32
Skok przy nadmiarze
O=1
JNO rel16/32
Skok przy braku nadmiaru
O=0
Pętle
W Assemblerze ogólna struktura pętli jest następująca:
ETYKIETA:
LOOP ETYKIETA
Instrukcja LOOP powoduje zmniejszenie o jeden wartości rejestru CX i jeśli wartość ta jest większa od zera skok bezwarunkowy do etykiety ETYKIETA. Jeśli więc chcielibyśmy wykonać pętlę, która wyświetli 10 razy napis DZIAŁA napisalibyśmy tak:
MOV AH,09H
MOV DX,OFFSET Napis
MOV CX,10
ETYK:
INT 21H
LOOP ETYK
MOV AH,4CH
INT 21H
Napis DB "DZIAŁA",13,10,'$'
; wyświetlenie napisu
; napis w zmiennej NAPIS
; pętla 10 razy
; etykieta
; wykonanie przerwania - wyświetlenie napisu
; wykonanie pętli
; funkcja 4CH
; zakończenie programu
; napis wyświetlany
Instrukcję LOOP można zamienić na:
DEC CX
JCXZ ETYKIETA
Polecenie CX zmniejsza wartość rejestru CX, a skok warunkowy (został opisany wcześniej) zostanie wykonany, gdy CX=0.
Język Assembler daje nam jeszcze kilka różnych pętli. Sposób ich wykorzystania jest taki sam co LOOP. Różnica polega na skoku warunkowym:
* LOOPE
* LOOPZ
* LOOPNE
* LOOPNZ
Końcówka dodana do LOOP jest taka jak przy skokach warunkowych (patrz tabela wyżej)
RÓŻNE OPERACJE
Operacje na plikach
Tworzenie nowego pliku
Pisząc w dowolnym języku często zachodzi potrzeba stworzenia jakieś pliku, np. z danymi. W Assemblerze nie ma do tego celu żadnych instrukcji (jak i do innych operacji z tym związanych). By stworzyć plik trzeba użyć pewnych przerwań i tak do tworzenia pliku używamy przerwania 21H i funkcji 3CH. Przerwania wykonuje się w Assemblerze za pomocą instrukcji INT, a funkcja jest to wartość rejestru AH. Przed wywołaniem tego przerwania w rejestrze CX podajemy atrybuty naszego pliku (o atrybutach powiemy później), a w DS:DX nazwę, jaką ma mieć. Nazwa ma być zapisana w kodzie ASCIZ, tzn. musi być zakończona znakiem o kodzie 0, np. jeśli chcemy stworzyć plik o nazwie "PLIK.TXT" napiszemy tak:
MOV AH,3CH
MOV CX,0
MOV DX,OFFSET Nazwa
INT 21H
...
Nazwa DB "PLIK.TXT",0
Jeśli operacja zakończy się sukcesem, to w rejestrze AX znajdzie się numer dojścia do pliku. Jest on bardzo ważny. Identyfikuje on plik, do którego będą później zapisywane dane. Jeśli jednak operacja tworzenia pliku zakończy się niepowodzeniem to zostanie ustawiony znacznik C, a rejestr AX będzie posiadał kod powstałego błędu.
Otwieranie pliku
Jeśli tworzymy plik zostaje on automatycznie otwarty, jeśli jednak mamy już plik musimy go otworzyć oddzielną procedurą. By otworzyć plik trzeba użyć również przerwania 21H oraz funkcji 3DH. Jako parametry podajemy: w DS:DX nazwę naszego zbioru oraz w AL tryb dostępu. Istnieją następujące tryby dostępu:
bity
wartość
opis
7
0
Procesy potomne dziedziczą dojście wraz z numerem
4..6
000
Każdy proces może otworzyć plik wielokrotnie z prawem do pisania i/lub czytania, ale tylko w tym trybie
4..6
001
Pełna wyłączność. Nie mogą istnieć żadne inne dojścia do pliku.
4..6
010
Wyłączność pisania, pozostałe dojścia mogą mieć tylko prawo do czytania, jednak nie mogą być otwarte w trybie 000
4..6
011
Wyłączność pisania, pozostałe dojścia mogą mieć prawo tylko do czytania i mogą być otwarte w trybie 000
4..6
100
Mogą istnieć inne dojścia z prawem do czytania i/lub pisania, jednak nie mogą być otwarte w trybie 000
0..3
000
Prawo do czytania
0..3
001
Prawo do pisania
0..3
010
Prawo do czytania i pisania
My będziemy zazwyczaj używali trzech trybów:
AL=0 - czytanie
AL=1 - pisanie
AL=2 - czytanie i pisanie
Jeśli więc chcemy otworzyć istniejący plik o nazwie PLIK.TXT napiszemy:
MOV AH,3DH
MOV AL,0
MOV DX,OFFSET Nazwa
INT 21H
...
Nazwa DB "PLIK.TXT",0
Podczas otwierania mogą wystąpić również błędy i wtedy znacznik C jest ustawiany, a AX posiada kod błędu. Jeśli operacja została wykonana poprawnie C=0, a AX zawiera numer dojścia.
Zapis do pliku
Jeśli mamy już otwarty plik i chcemy w nim coś zapisać musimy użyć funkcji 40H przerwania 21H. W rejestrze BX podajemy numer dojścia (uzyskaliśmy go podczas otwierania pliku), w rejestrach DS:DX podajemy adres początku danych, które chcemy zapisać, a w rejestrze CX podajmy ilość bajtów do zapisania. Jeśli operacja zakończy się sukcesem znacznik C jest wyzerowany, a AX zawiara liczbę bajtów zapisanych. Jeśli podczas zapisu wystąpił błąd C=1, a AX zawiara kod błędu. Jeśli więc chcemy zapisać w naszym (otworzonym wcześniej) pliku napis "KURS ASSEMBLERA" napiszemy:
MOV AH,40H
MOV BX,Numer dojścia
MOV DX,OFFSET Napis
MOV CX,15
INT 21H
...
Napis DB "KURS ASSEMBLERA"
Czytanie z pliku
Do czytania z pliku służy funkcja 3FH przerwania 21H. W rejestrze BX podajemy również numer dojścia, CX ilość bajtów, które chcemy przeczytać, a w DS:DX adres bufora, gdzie mają zostać one zapamiętane. Jeśłi operacja zakończy się sukcesem w rejestrze AX będziemy mieli ilość przeczytanych bajtów, a znacznik C=0. Ilość przeczytanych bajtów nie musi być równa ilość bajtów, które kazaliśmy przeczytać. Jeśli plik zawiera np. 30 bajtów danych, a my mu każemy przeczytać 50 bajtów, to AX będzie zawierało 30 bajtów.
Zamknięcie dojścia do pliku
Każdy plik, który został wcześniej otwarty trzeba na koniec zamknąć Do tego celu służy funkcja 3EH przerwania 21H. Podajemy w rejestrze BX numer dojścia do pliku , który chcemy zamknąć. Jeśli wystąpi jakiś błąd to znacznik C zostanie ustawiony, a AX zawiera kod powstałego błędu.
Zmiana wskaźnika w pliku
Do ustawiania wskaźnika w pliku służy funkcja 42H. W rejestrze BX podajemy numer dojścia, w AL sposób przesunięcia wskaźnika, a w CX:DX odległość na jaką chcemy przesunąć. Istnieją trzy rodzaje przesunięć:
AL=0 - licząc od początku pliku
AL=1 - licząc od bieżącej pozycji kursora
AL=2 - licząc od końca zbioru
Przesunięcie względem końca zbioru używa się również do obliczania wielkości zbioru, gdyż wynikiem funkcji 42H jest w DX:AX wartość aktualnego położenia (o ile nie wystąpił błąd, wtedy C=1, a AX kod błędu).
Usuwanie pliku
Do usuwania zbiorów z dysku służy funkcja 41H przerwania 21H. W rejestrze DS:DX podajemy nazwę zbioru w kodzie ASCIZ. Jeśłi nie wystąpi błąd znacznik C zostaje ustawiony, a AX zawiara kod powstałego błędu.
Sprawdzenie atrybutów pliku
Jeśli chcemy sprawdzić jakie dany plik ma atrybuty musimy posłużyć się fukcją 43H przerwania 21H. Podajemy wówczas w AL wartość 0, a w DS:DX nazwę pliku w kodzie ASCIZ. Po wykonaniu przerwania w CX będą atrybuty pliku.
Nadawanie atrybutów pliku
Do nadawania atrybutów dla pliku służy również funkcja 43H. Podajemy teraz w AL wartość 1, w DS:DX nazwę pliku, a w rejestrze CX atrybuty, które chcemy nałożyć.
Zmiana nazwy pliku
Czasami zachodzi potrzeba zmiany nazwy pliku. Wtedy trzeba posłużyć się funkcją 56H. W rejestrze DS:DX podajemy pierwotną nazwę pliku, a w rejestrze ES:DI nową nazwę pliku. Jeśli operacja zakończy się niepowodzeniem zostanie ustawiony znacznik C, a rejestr AX będzie zawierał kod błędu.
Operacje na katalogach
Tworzenie katalogu
Do tworzenia nowych katalogów służy funkcja 39H przerwania 21H. W rejestrze DS:DX podajemy nazwę katalogu (w kodzie ASCIZ). Jeśli operacja zakończy się błędem, zostanie on przekazany w rejestrze AX i zostanie ustawiony znacznik C
Usunięcie katalogu
Do usuwania katalogów służy funkcja 3AH. W rejestrze DS:DX podajemy wówczas adres nazwy usuwanego katalogu.
Zmiana katalogu
Do ustalania katalogu bieżącego służy funkcja 3BH przerwania 21H. Jako parametr podajemy w DS:DX adres nowej nazwy katalogu.
Pobranie nazwy aktualnego katalogu
Jeśli chcemy dowiedzieć się o aktualnym katalogu bieżącym posłużymy się przerwaniem 21H i funkcją 47H. Jako parametry podajemy w DL numer dysku (o który nam chodzi). Jeśli mamy na myśli dysk bieżący wpisujemy 0, oraz w DS:SI adres 64-bajtowego bufora, w którym ma zostać zapamietana ścieżka.. Jeśli operacja zakończy się sukcesem w buforze jest ścieżka, w przeciwnym wypadku znacznik C=1, a AX -kod błędu.
Windows
Wykrywanie Windows w pamięci
Podczas programowania zachodzi czasami potrzeba sprawdzenia, czy w pamieci znajduje się Windows, czy pracujemy w środowisku DOS. Można to uczynić wykonując przerwanie 2FH i funkcję 160AH. Jeśli po wykonaniu przerwania w rejestrze AX znajduje się wartość 0, tzn. że jest uruchomiony Windows. Wówczas w rejestrze BX znajduje się jego wersja (BH-numer wersji i BL-podnumer). Jeśli w rejestrze CX znajduje się wartość 2 tzn. że jest uruchomiony tryb standardowy. Jeśli CX=3, to uruchomiony jest tryb rozszerzony 386. Przerwanie to działa jednak tylko pod Windows 3.11 i nowszych.
Blokada klawiszy
Czasami program musi wykonać jakąś operacje, które nie może zostać mu przerwana. Jednak system operacyjny Windows umożliwia przerwanie wykonywania programu kombinacją klaiwszy ALT+TAB, lub wciskając CTRL+ESC. By zapobiec takim sytuacjom trzeba użyć funkcji 1681H przerwania 2FH. Powoduje ona włączenie blokady tych klawiszy. By wyłączyć blokadę trzeba wywołać to samo przerwanie, tylko funkcję 1682H.
Monitor
Nowe monitory posiadją możliwość włączenia tzw. trybu czuwania. Jest to znacznie lepsze od wygaszaczy ekranu. Za pomocą przerwania 10H i funkcji 4F10H można sterować pracą mnitora. By zmienić stan monitora do rejestru BL wpisujemy 1, a do rejestru BH wstawiamy jedną z następujących wartości:
? BH=0 - ON (monitor włączony, obraz widoczny)
? BH=1 - STANDBY (monitor w stanie czuwania)
? BH=2 - SUSPEND (monitor w stanie uśpienia)
? BH=4 - OFF (monitor w stanie głębokiego uśpienia)
? BH=8 0 REDUCED ON (opcja występuje tylko przy niektórych monitorach z płaskim ekranem)
DYREKTYWY
W Assemblerze jest wiele dyrektyw. Część z nich została opisana już wcześniej, m.in. SEGMENT, ENDS, ASSUME, ORG, PROC. Teraz zajmiemy się innymi, wcześniej nie opisanymi, ale również bardzo przydatnymi:
GROUP
nazwa GROUP nazwa_segmentu
Dyrektywa ta pozwala wiązać kilka segmentów zdefiniowanych dyrektywami SEGMENT tak, że adresy w każdym z nich są liczone nie względem jego początku, ale względem początku grupy. Segmenty w grupie mogą być ciągłe (max. 64KB), ale nie muszą - między segmentami grupy mogą znajdować się segmenty nie należące do danej grupy.
Aby ustalić adresację w obrębie całej grupy, należy użyć dyrektywy ASSUME, podając w niej nazwę grupy, np.:
GROUP1 GROUP DANE1, DANE2
DANE1 SEGMENT
PAR1 DB ?
DANE1 ENDS
DANE2 SEGMENT
PAR2 DW ?
DANE2 ENDS
KOD SEGMENT
ASSUME CS:KOD, DS:GROUP1 ; SEGMENT DANE1 i DANE2
; SĄ ADRESOWANE WZGLĘDEM REJESTRU DS
MOV AX,GRUP1 ; SEGMENT GRUPY GRUP1
MOV DS,AX
...
SUB CX,PAR2 ; PRZESUNIECIE PAR2 LICZONE JEST
; WZGLĘDEM POCZATKU GRUPY
KOD ENDS
EVEN
Dyrektywa ta umieszcza instrukcję lub definicję danych następującą po niej na granicy pełnego słowa, wstawiając ewentualnie instrukcję pustą (NOP). Dyrektywy EVEN nie można używać w segmentach z parametrem 'ułożenie' równym BYTE.
PUBLIC i EXTERN
PUBLIC nazwa,...
EXTRN nazwa:typ,...
Dyrektywa PUBLIC czyni podanaą przy niej nazę (etykietę, nazwę zmiennej lub procedury) dostępną dla innych modułów. Dyrektywa EXTRN natomiast deklaruje nazwę użytą w module jako zewnętrzną, tj. takąm do której odwołanie zostanie rozwiązane dopiero na etapie łączenia modułw. Typ w tej deklaracji określa rodzaj nazwy zewnętrznej i może być następujący:
BYTE WORD DWORD NEAR FAR
np. pierwszy moduł wygląda tak:
EXTRN PIERWSZA:FAR, ZMIENNA:BYTE
KOD1 SEGMENT
ASSUME CS:COD1,DS:COD1
START:
...
CALL PIERWSZA ;WYWOŁANIE BĘDZIE TYPU FAR
...
MOV AL,ZMIENNA ;ODWOŁANIE DO ZMIENNEJ BĘDZIE Z
;PODANIEM SEGMENTU
KOD1 ENDS
END START
Drugi natomiast wygląda tak:
PUBLIC PIERWSZA,ZMIENNA
KOD2 SEGMENT
ASSUME CS:KOD2,DS:KOD2
...
PIERWSZA PROC FAR ;PROCEDURA TYPU FAR
...
RET
PIERWSZA ENDS
...
ZMIENNA DB 0
...
KOD2 ENDS
ZBIORY INSTRUKCJI
Istnieje kilka dyrektyw, które pozwalają deklarować procesory, których instrukcje będą używane w programie. Dyrektywa deklarowania rodzaju procesora powinna być podana na początku zbioru, żeby objąć całą jego zawartość. Domyślnym rodzajem procesora jest 8086. My mamy do dyspozycji:
? .8086
? .8087
? .186
? .286c
? .286p
? .287
? .386
LABEL
nazwa LABEL typ
Dyrektywa ta służy do tworzenia etykiet, przyporządkowując im aktualną wartość adresu. Jest ona użyteczna przy nadawaniu zmiennym pamięciowym, już nazwanym, innej nazwy z innym kodem. Mamy do dyspozycji typy:
? BYTE
? WORD
? DWORD
? QWORD
? TBYTE
? NEAR
? FAR
np.:
SLOWO LABEL WORD
ZMIENNA1 DB 5
ZMIENNA2 DB 8
Liczby 5 i 8 znajdują się w dwóch kolejnych bajtach pamięci. Można teraz odwoływać się do nich używając ich nazw (ZMIENNA1, ZMIENNA2), wtedy nadajemy wartości bajtowe, lub odwołać się do SLOWO, gdzie nadamy im wartość słowa (dwa bajty jednocześnie), np:
MOV AX,SLOWO ; LADUJE DO AX WARTOSC 0508H
MOV CL,ZMIENNA1 ; LADUJE DO CL WARTOŚĆ 8
Podobnie można robić z etykietami:
ETYKIETA1 LABEL FAR
ETYKIETA2:MOV AX,BX
Etykieta ETYKIETA2 jest typu NEAR, a ETYKIETA1 typu FAR, a obie wskazują ten sam adres w pamięci.
TRYB 13h
Wstęp
Zacznę od wyjaśnienia, co to jest to tajemnicze 13H. Otóż tryb 13H jest to pewien tryb graficzny, który można uzyskać na chyba każdej karcie graficznej bez żadnych dodatkowych sterowników. Daje on nam rozdzielczość 320x200 i 256 kolorów. Dzięki temu można tworzyć programy wykorzystujące więcej kolorów, np. ogień. Ogień jest prostym efektem graficznym, którego nie da się uzyskać w zwylym trybie graficznym w Turbo Pascal-u. Tryb ten ma również inne zalety. Bardzo łatwo się go używa, nasze procedury zapisane w Assemblerze są szybsze i pewniejsze.
Opis
Jak już wspomniałem tryb ten daje nam rozdzielczość 320x200 i 256 kolorór. Tryb ten zajmuje więc 64000 bajtów pamięci (320x200=64000). Wiemy również, że mniej więcej tyle właśnie zajmuje jeden segment pamięci komputera. Segment pamięci ekranu w trybie 13H zaczyna się o adresie A000H. Literka H oznacza, że liczba zapisana jest w systemie szesnastkowym.
Włączenie/Wyłączenie trybu graficznego
Do obsługi trybu graficznego służy przerwanie 10H. Funcka 0H (Ah=0) ma za zadanie zmiany trybu graficznego/tekstowego. Dostępne są następujące tryby:
AL
Rodzaj
Rozdz.
Kolory
Karta
Segment pamięci
0
tekstowy
40x25
16/8
CGA/EGA
B800H
1
tekstowy
40x25
16/8
CGA/EGA
B800H
2
tekstowy
80x25
16/8 (odcieni)
CGA/EGA
B800H
3
tekstowy
80x25
16/8
CGA/EGA
B800H
4
graficzny
320x200
4
CGA/EGA
B800H
5
graficzny
320x200
4 (odcienie)
CGA/EGA
B800H
6
graficzny
640x200
2
CGA/EGA
B800H
7
tekstowy
80x25
3
MDA/EGA
B000H
0DH
graficzny
320x200
16
EGA/VGA
A000H
0EH
graficzny
640x200
16
EGA/VGA
A000H
0FH
graficzny
540x350
3
EGA/VGA
A000H
10H
graficzny
640x350
4 lub 16
EGA/VGA
A000H
11H
graficzny
640x480
2
VGA
A000H
12H
graficzny
640x480
14
VGA
A000H
13H
graficzny
320x200
156
VGA
A000H
My włączamy tryb 13H, czyli nasza procedura InitGraph wygląda tak:
Procedure Init; Assembler;
Asm;
MOV AX,0013H;
INT 10H
end;
Na koniec pracy w grafice trzeba ten tryb zamknąć, a dokładnie mówiąc wywołać tryb standardowy tekstowy. Nasza procedura CloseGraph wygląda więc tak:
Procedure Close; Assembler;
Asm;
MOV AX,0003H
INT 10H
End;
Instrukcje
Punkt o współrzędnych [0,0] ma więc adres A000H:0. Tak więc punkt o współrzędnych [X,Y] znajduje się w pamięci pod adresem A000:Y*320+X.
Turbo Pascal daje nam możliwość odwoływania się do komórek pamięci za pomocą instrukcji MEM. Jeśli chcemy odczytać wartość danej komórki piszemy:
X:=MEM[segment:offset];
Jeśli chcemy zmienić wartość danej komórki, napiszemy:
MEM[segment:offset]:=X;
Prosta procedura rysująca punkt na ekranie mogłaby wyglądać więc tak:
Procedure PutPixel(X,Y : Integer; Kolor : Byte);
Begin
MEM[$A000:Y*320+X]:=Kolor;
End;
Funkcja odczytująca kolor punktu pogłaby wyglądać tak:
Function GetPixel(X,Y : Integer) : Byte;
Begin
GetPixel:=MEM[$A000:Y*320+x];
End;
Opie te instrukcje ze względu na działanie patematyczne (Y*320+X) oraz użycie Pascal-owej instrukcji MEM są bardzo proste. Dlatego należy używać procedury do wstawiania punktu napisanej w Assemblerze. Wiemy, że ten język jest najszybszy. W Assemblerze nasza procedura PutPixel wyglądałaby tak:
Procedure PutPixel(X,Y : Integer; Kolor : Byte); Assembler;
Asm
MOV AX,0A000H;
MOV ES,AX;
MOV DX,Y
MOV DI,X
SHL DX,6
ADD DI,DX
SHL DX,2
ADD DI,DX
MOV AL,Kolor
MOV ES:[DI],AL
End;
Sprytne nie. Ta procedura jest znacznie szybsza od poprzeniej. Działa w następujący sposób:
MOV AX,0A000H
MOV ES,AX
W rejestrze ES mamy teraz segment naszego ekranu. Wystarczy wykonać działanie Y*320+X. Można to zrobić używając instrukcji MUL, która służy do mnożenia, jednak jest ona strasznie wolna. Lepiej zastosować tutać SHL, czyli przesunięcie logiczne. Jest znacznie szybsze, a można za pomocą niego wykonać mnożenie przez potęgę liczby 2. Działanie 320*Y zapisujemy jako Y*256+Y*64. Liczby 256 i 64 są potęgami liczby 2. Dlaczego akurat 256 i 64? Bo wiemy ze wzoru (a+b)*C=A*c+b*c, czyli (256+64)*Y.
Instrukcja GetPixel w Assemblerze jest bardzo podobna.
Function GetPixel(X,Y : Integer) : Byte; Assembler
Asm;
MOV AX,0A000H
MOV ES,AX
MOV DX,Y
MOV DI,X
SHL DX,6
ADD DI,DX
SHL DX,2
ADD DX,DI
MOV AL,ES:[DI]
MOV @Result,AL
End;
Przykładowe programy w Assemblerze.
Dots
Obracająca się, stworzona z punktów kula 12.2KB
???
Filld
Obracający się, wypełniony kolorami sześcian 8.6KB
???
Gouraud
Obracający się sześcian z cieniowaniem 12.4KB
???
Gwiazdy
Latające gwiazdy 3KB
???
Linie
Obracający się, wykonanany z linii sześcian 7.4KB
???
Plazma
Ciekawy efekt graficzny 91.5KB
???
Scape
Trójwymiarowa powierzchnia 103.1KB
???
Scrool
Prosty scrool z ciekawą czcionką 28KB
???
Texture
Kolejny obracający się sześcian, tym razem z teksturą 32.3KB
???
Tunel
Trójwymiarowy tunel 10.3KB
???
Moduły potrzebne do uruchomienia niektórych programów.
BGraph
Super moduł do obsługi trybu 13H.
Binboy
Graphhigh
Moduł do obsługi różnych trybów graficznych, m.in. VESA (wysokie rozdzielczości) 6.1KB
Binboy
Graphics
Moduł do obsługi trybu graficznego 13H. Umożliwia m.in. wstawianie grafiki PCX. Jest w wersji skompilowanej. 7.4KB
Binboy
Vga_lib
Szybki moduł graficznych do trybu 13H 12.7KB
???
Vlib
Moduł graficzny 2.5KB
Grend
Gif
Moduł umożliwiający wyświetlanie plików graficznych GIF 7.2KB
???
Pcx
Moduł do obsługi plików graficznych PCX 5.3KB
Bartok
Ikey
Moduł do obsługi klawiatury. Umożliwia kontrolowanie kilku klawiszy jednocześnie 1.3KB
Bartok
Mysz
Moduł do obsługi myszki 1.8KB
???
Cosin
Moduł wspomagający obliczenia SIN i COS 335B
Grend
OPTYMALIZACJA
Na tej stronie zajmiemy się optymalizacją kodu w Assemblerze. Zastanowimy się jak przyspieszyć działanie naszego programu i jak zmniejszyć jego objętość.
Programy napisane w Assemblerze z reguły są bardzo szybkie. Na pewno szybsze niż te napisane w BASIC-u, czy Pascal-u. Czasami zachodzi jednak potrzeba stworzenia programu nadzwyczaj szybkiego. Gdy np. chcemy stworzyć jakąś animację w wysokiej rozdzielczości, z teksturowaniem szybkość naszego kodu ma ogromne znaczenie.
1. Rejestry
Podczas programowania często zachodzi potrzeba zapamiętania jakiejś wartości. Wówczas niektórzy z nas tworzą sobie zmienne. Oprócz tego, że te zmienne zajmują pewną pamięc, odczytanie z nich wartości jest trudniejsze i stwarza możliwość popełnienia błędu, operacje na nich jest znacznie wolniejsze, niż na rejestrach procesora. Rejestry są najszybsze w naszym komputerze, dlatego wykorzystujmy je w 100%. Jeśli jakaś liczba ma zostać zapamiętana na krótką chwilę, a nie mamy wolnych rejestrów, to użyjmy stosu. Jest on również szybszy od zmiennych. Róbmy więc zmiennych jak najmniej.
2. Makrodefinicje
Jak działają makrodefinicje nie trzeba chyba tłumaczyć. Wstawiają w odpowienie miejsce, odpowiedni kod. Stosujmy więc makrodefinicję wszędzie tam, gdzie zależy nam na prędkości. Używanie procedur jest wygodne, jednak wywołanie procedury zajmuje wiele czasu. Położenie na stos odpowiednich rejestrów i wykonanie bardzo powolnego skoku, to dla nas za dużo. Jeśli więc użyjemy makrodefinicji, nie wykonamy żadnego skoku. Program jest czytelny i szybki. Jedyna wada, to objętość, która rośnie w zadziwiająco szybkim tępie.
3. Pętle
Istnieje taka instrukcja w Assemblerze, która się nazywa LOOP. Służy ona do zmniejszania licznika CX i wykonywania skoku, dopóty dopóki CX<>0. Jest ona bardzo wolna, zajmuje na 486 6/7 cykli, a na PENTIUM 5NP. Można czasami pętle znacznie przyspieczyć, przykładowo mamy napisać program, który wykona pętlę pięć razy i za każdym razem zwiększy rejestr AX o jeden. Normalnie napisalibyśmy tak:
MOV CX,5
ET1:
INC AX
LOOP ET1
Można go przyspieszyć pisząc tak:
MOV CX,5
ET1:
INC AX
DEC CX
JNZ ET1
Dlaczego drugi sposób jest szybszy? Bo dodatkowa instrukcja DEC (na 486 i PENTIUM) trwa 1 cykl, a JNZ na 486 trwa 1/3 cykla, a na PENTIUM 1V, co oznacza całkowity czas na 486 max. 4 cykle (poprzednie rozwiązanie 7), a na PENTIUM 1 cykl, bo za jednym razem zostanie wykonana instrukcja DEC i JNZ (podczas gdy LOOP 5NP).
Optymalizacja pod względem objętości jest również bardzo ważna. Czasami zachodzi potrzeba napisania programu, który nie ważne ile będzie wykonywał daną operację, ważne by działał i był mały. Zazwyczaj jednak programy małe są i szybkie, ale nie zawsze.
1. Procedury
Jednym ze sposobów zmniejszenia objętości programu jest stosowanie procedur. Jeśli jakiś fragment programu jest wykonywany wiele razy można zrobić z niego procedurę i w razie potrzeby wykonywać tylko skok. Ten sposób znacznie zmniejszy objętość naszego kodu, jednak skoki są powolne, przez co szybkość programu spadnie.
2. Stos
Stos jest bardzo poręcznym narzędziem. Zapamiętanie dowolnego rejestru 16 bitowego na stosie zajmuje jeden bajt, 32 bitowego dwa bajty, a zapamietanie rejestru 16 bitowego w zmiennej zajmuje 3 bajty, a 32 bitowego 4 bajty. Jeśli więc mamy do zapamietanie jakąś zmienną, na krótki czas, to jak się da to stosujmy stos. Skracamy kod i nie ma konieczności tworzenia zmiennych. Mamy również mniejsze ryzyko wystąpienia błedu.
3. Segmenty
Wiemy, że do rejestrów segmentowych nie wolno zapamiętywać wartości stałych, np. konkretnej liczby. Trzeba to uczynić korzystając np. z rejestru. Zapamiętujemy wówczas liczbę w rejestrze i ten przesyłamy do rejestru segmentowego. Przykładowy program może wyglądać tak:
MOV AX,0A000H
MOV DS,AX
Jest na to wolniejszy (o jeden cykl), ale mniejszy objętościowo sposób. Można wykorzystać tą właściwość stosu i instrukcji PUSH, że można kłaść wartość stałą na stos i to, że można zdjąć coś ze stosu i zapamiętać w rejestrze segmentowym. Nasz program z wykorzystaniem stosu wyglądać może tak:
PUSH 0A000H
POP DS
Sposób pierwszy zajmuje 5 bajtów (2 cykle), natomiast drugi 4 bajty (3 cykle).
4. Zerowanie
Bardzo często podczas programowania zachodzi potrzeba wyzerowania jakiegoś rejestru. Wówczas niektórzy piszą:
MOV AX,0
Jest to niepotrzebne zwiększanie objętości programu. Dużo lepszym sposobem jest zastosowanie instrukcji logicznej XOR lub AND. W XOR wykorzystujemy tą właściwość, że jeżeli para bitów jest taka sama (równa 0 lub 1), to wynik jest 0. Jeśli więc ksorujemy liczbę przez samą siebie, to wszystkie odpowiadające sobie bity są identyczne, więc wynik jest równy 0. Używająć AND wykorzystujemy tą właściwość, że jeżeli pomnożymy logicznie dowolny bit przez 0, to wynik będzie 0. Dlatego wystarczy pomnożyć liczbę przez 000000B, czyli 0. Powinniśmy więc napisać:
XOR AX,AX
AND AX,0
Obie te instrukcje zajmują dwa bajty, a instrukcja MOV trzy.
"Artykul sciagniety ze strony Binboy-a [BHP] Binboy HomePage
http://binboy.infohelp.com.pl"
Wyszukiwarka
Podobne podstrony:
kurs oracle podstawy sql
KURS EXCEL podstawowy
Kurs Assemblera by Skowik3
Kurs Assemblera by Skowik
Kurs Assemblera by Skowik2
Kurs komputerowy podstawowy program300408
więcej podobnych podstron