ASSEMBLER
Definicje
Mnemonik - nazwa odpowiadająca każdej instrukcji maszynowej
Dyrektywa - nazwa odpowiadająca instrukcji dla programu asemblera
Operandy (argumenty) - liczby i inne nazwy z prawej strony mnemoników, oddzielamy je przecinkiem `,'
Instrukcja - mnemonik + operand, może być poprzedzona etykietą (łańcuch alfanumeryczny od tego momentu kojarzony z tą
instrukcją programu) oraz może po niej następować komentarz
Instrukcja maszynowa - binarny odpowiednik instrukcji
Komentarz - zaczyna się średnikiem `;`a kończy znakiem końca wiersza
Liczba - szesnastkowa: zapisujemy kończąc ją znakiem `H' , jeśli zaczyna się od litery poprzedzamy ją `0'
1202H, 0A010H
- dziesiętna: zwyczajnie
- ósemkowa: kończymy ją znakiem `Q' lub `O'.
- binarna: kończymy ją znakiem `B'
EOL = Carrier Return (13) + Line Feed (10) - znak końca wiersza
Wielkości pamięci :
NAZWA |
w Asemblerze |
Ilość bajtów |
słowo |
WORD |
2 |
podwójne słowo |
DWORD |
4 |
poczwórne słowo |
QWORD |
8 |
10 bajtów |
TBYTE |
10 |
paragraf |
PARA |
16 |
strona |
PAGE |
256 |
segment |
SEGMENT |
65536 |
( ! ) bity zlicza się od prawej (najmniej znaczący) do lewej i numeruje od 0
Architektura mikroprocesora
W pracy procesora wyodrębnia się cykle. Podstawowym cyklem jest cykl rozkazowy. Związany jest on z wykonaniem pojedynczej instrukcji. Na jeden cykl rozkazowy przypada kilka cykli maszynowych, z których każdy może składać się z kilku cykli zegarowych. Jeden cykl zegarowy wykonywany jest w jednym takcie zegara. Poszczególne cykle maszynowe realizują osobne układy, a ich realizacja jest równoległa. Układy te to:
BU (układ magistrali systemu) - pas transmisyjny, pobiera instrukcję z pamięci i umieszcza ją w kolejce kodów, jest odpowiedzialny za przesyłanie danych do świata zewnętrznego
IU (układ instrukcji) - pobiera instrukcje z kolejki kodów, dekoduje i umieszcza w swojej kolejce instrukcji (potok).
EU (układ wykonawczy) - wykonuje instrukcje pod kontrolą mikroprogramu zapisanego w ROM (opisane np. operacje mnożenia, dzielenia itp. składające się z kilku elementarnych operacji bitowych)
AU (układ adresowy) - zarządza pamięcią, przekształca adresy wirtualne na fizyczne, pełni funkcje ochronne.
Równoległość cykli maszynowym sprowadza się do równoczesnej pracy ww. układów, podczas gdy EU wykonuje instrukcję, IU dekoduje już następną instrukcję.
Adresy
Każdy adres jest 20-bitowy. Aby go zapamiętać, potrzeba 2 rejestrów. Pierwszy zawiera adres segmentu (okienko 65536-bajtowe, przez które patrzymy na pamięć, do każdego bajtu z tego okienka mamy równoczesny dostęp), drugi - adres przesunięcia (offset) - określa konkretną komórkę pamięci w segmencie. Adres komórki liczony jest jako przesunięcie względem początku segmentu. Zwiększenie offsetu o 1 powoduje zwiększenie adresu bezwzględnego (20 - bitowego) o jeden bajt, natomiast ( ! ) każde zwiększenie segmentu o `1' powoduje zwiększenie bezwzględnego adresu początku segmentu o 16 bajtów (adres segmentowy - adres paragrafu).
( ! ) - adres nie jest jednoznaczny segmenty mogą na siebie zachodzić. Jeśli jeden segment przesunięty jest względem drugiego o 16 bajtów, to wystarczy przesunąć offset drugiego o 16 bajtów i na 2 sposoby wskazujemy się wówczas tą samą komórkę.
Zapis adresu - w postaci: <segment> : <offset> 0100H:0001H.
Dekodowanie adresu - słowo zawierające adres segmentu przemnażane jest przez 16 (otrzymujemy 20-bitową liczbę, której najmłodsze 4 bity są wyzerowane). Do takiej liczby dodawana jest 16-bitowa liczba - adres offsetu. Wynik stanowi adres 20-bitowy.
Bufor obrazu - zapisując w nim odpowiednie znaki ASCII możemy wyświetlić je na ekranie. Adres segmentu pamięci wideo wynosi: 0B800H (kolor) lub 0B000H (mono). Na każdy wyświetlany znak przypadają 2 bajty: 1 - kod ASCII wyświetlanego znaku, 2 - atrybut (pierwsze 4 bity - kolor tła, drugie - kolor tekstu). Atrybut normalnego tekstu wynosi 07H. Pierwsze dwa bajty odnoszą się do lewego górnego rogu, kolejne to wszystkie znaki z najwyższego wiersza, potem niższe wiersze. Jeden wiersz - 160 bajtów. ( ! ) Można operować bezpośrednio na pamięci obrazu, zmieniając odpowiednie bajty (np. write() )
Data BIOS - znajduje się zawsze pod adresem 0FFFF:0005. (jest to ostatni możliwy segment, jego offset jest od 0 do 15). Data zapisana jest w postaci wartości znaków ASCII odpowiadających zapisowi: 25/04/97
Restart (zimny) - w BIOS zapisana pod adresem 0FFFF:0000 instrukcja skoku do procedury startowej.
Rejestry
Rejestry segmentowe - tylko one mogą zawierać adresy segmentów, każdy ma 16 bitów.
CS (code segment) - każda instrukcja programu znajduje się w segmencie kodu. Adres segmenu, w którym znajduje się aktualnie wykonywana część programu znajduje się w CS
DS (data segment) - zmienne i stale znajdują się w segmencie danych. Może istnieć kilka segmentów danych, ale w danej
chwili można korzystać tylko z jednego
SS (stack segment) - zawiera adres początku segmentu stosu
ES (extra segment) - zawiera adres dodatkowego segmentu danych
Dodatkowe rejestry segmentowe (386 / 486)
FS i GS - (nazwa od kolejnych liter alfabetu) - zawierają adresy dodatkowych segmentów danych
( ! ) w trybie rzeczywistym rejestry segmentowe mogą być używane (z pewnymi ograniczeniami) jak rejestry ogólnego
przeznaczenia, w trybie wirtualnym mogą przechowywać tylko adresy.
Rejestry ogólnego przeznaczenia - mogą spełniać różne funkcje
przechowywanie offsetów: BX, BP, SP, SI, DI. Adres bezwzględny zapisuje się wówczas np.: SS : SP . ( ! ) Nie można stosować do offsetów rejestrów AX, CX czy DX.
połówki rejestrów: można wykorzystywać 8-bitowe połowy następujących rejestrów: AX, BX, CX, DX. Odpowiednio wyższą i niższą połowę tworzymy zgodnie ze schematem: AX = AH + AL . Starsza połówka to MSB (most significant byte) , młodsza - LSB (least significant byte)
Rejestry 32-bitowe (386 / 486)
Wszystkie rejestry ogólnego przeznaczenia (także BP, SP, SI, DI) poszerzono do 32 bitów nazwa 32-bitowego odpowiednika tworzona jest zgodnie ze schematem: AX EAX.
( ! ) Nie ma sposobu oddzielenia i manipulowania tylko starszym słowem rejestru (bo młodsze słowo dla EAX to AX)
Wskaźnik instrukcji - rejestr IP - służy do przechowywania offsetu instrukcji maszynowej, która będzie wykonywana jako następna. Po skończeniu instrukcji CPU zwiększa IP o taką ilość bajtów, ile zajmowała aktualnie wykonywana instrukcja maszynowa.
( ! ) - adres bezwzględny następnej instrukcji jest w CS:IP
( ! ) - adresu w IP nie można bezpośrednio czytać i zmieniać
Rejestr znaczników (flag) - 16-bitowy, większość bitów wykorzystana i traktowana jako 1-bitowe rejestry, sprawdzanie jakichkolwiek warunków w programie sprawdza stan tych rejestrów. Większość instrukcji w zależności od wyniku swojego działania odpowiednio ustawia kolejne bity (każda na swój sposób). W 8086 wykorzystuje się jedynie 9 bitów z tego rejestru. Każdy taki bit (flaga) ma swoją osobną nazwę:
OF (overflow flag) - znacznik przepełnienia ustawiany, gdy wynik operacji jest za duży i nie mieści się w operandzie przeznaczenia
DF (direction flag) - znacznik kierunku nie wskazuje niczego, ( ! ) należy go samemu ustawić, ustalając jaki kierunek zmian mają przyjąć instrukcje łańcuchowe. Gdy DF jest ustawiony, instrukcje te wykonują się „od góry do dołu” - w kierunku mniejszych adresów, gdy 0 - w kierunku większych adresów. Ustawianie na 1za pomocą STD, 0 - CLD.
( ! ) znacznik ten jest inicjalizowany wartością „0”
IF (interrupt enable flag) - znacznik zezwolenia przerwań może być ustawiany przez CPU a także przez użytkownika, za pomocą instrukcji STI (na 1) oraz CLI (na 0). Jeśli jego wartość jest 0 - ignorowane występowanie przerwań.
TF (trap flag) - znacznik pułapki umożliwia wykonywanie programu krok po kroku
SF (sign flag) - znacznik znaku ustawiany, gdy wynik operacji jest ujemny (1 na najstarszym bicie), zerowany - wynik dodatni.
ZF (zero flag) - znacznik zera ustawiony, gdy wynikiem operacji jest `0' ( ! ) , zerowany, gdy cokolwiek innego.
AF (auxiliary carry flag) - znacznik przeniesienia pomocniczego wykorzystywany w arytmetyce liczb dziesiętnych kodowanych binarnie - BCD - bajt traktowany jako para 4-bitowych nybli - w każdym z nich zapisana jest cyfra 0-9.
PF (parity flag) - znacznik parzystości ustawiony, gdy liczba ustawionych bitów w najmniej znaczącym bajcie wyniku jest parzysta, zerowany - gdy nieparzysta.
CF (carry flag) - znacznik przeniesienia ustawiony, gdy w wyniku działań arytmetycznych lub przesuwania z rejestru zostanie wyniesiony jeden ustawiony bit. Flagę tą można modyfikować instrukcjami STC (CF = 1), CLC (CF = 0), CMC (CF = -CF)
AC (alignment check) (486) - wykrywa błędy wyrównania ustawiany, gdy zaszło odwołanie do pamięci pod adres niepodzielny przez 4. W 486 jednocześnie wczytuje się 32-bity najszybciej dokonuje się to gdy adres pamięci wyrównany jest do granicy podwójnych słów.
Kopiowanie rejestru flag - nie można za pomocą MOV nale¿y u¿yæ stosu - naspierw PUSHF a potem POP
Rodzaje danych i adresacje
Operandy źródła i przeznaczenia
Instrukcje wymagaj --> [Author:brak] --> [Author:brak] --> [Author:brak] ą często podania adresu źródła danych potrzebnych do ich działania (operand źródła ) i adresu przeznaczenia, gdzie ma być zapisany efekt ich działania (operand przeznaczenia). Operand przeznaczenia stoi za zwyczaj bliżej instrukcji np. MOV DX, AX DX to przeznaczenie, AX - źródło.
Rodzaje danych
Istnieją 3 rodzaje danych, które mogą być użyte jako operandy:
Dane natychmiastowe - osiągalne dzięki adresowaniu natychmiastowemu - dana zawarta jest w segmencie kodu zaraz po kodzie instrukcji (dana należy do wczytywanej instrukcji).
( ! ) - Może nią być tylko operand źródła, musi on być wielkości odpowiedniej do operandu docelowego (jak 8 bitów celu, to 8-bitowa liczba w źródle).
Dane natychmiastowe to stałe, zapisuje się je jako liczby np. MOV AX, 1
Dane te można wykorzystywać także przy adresowaniu bezpośrednim, w którym offset argumentu pamięciowego zawarty jest bezpośrednio w rozkazie jako 16-bitowa (lub 8-bitowa ze znakiem adresowanie względne) stała.
Dane rejestrowe - przechowywane w rejestrach CPU. Dostępne dzięki adresowaniu rejestrowemu (ukrytemu). Sprowadza się ono do podania nazwy rejestru: MOV AX, BX - w BX jest dana rejestrowa, która zostanie zapisana jako dana rejestrowa w AX.
( ! ) - nie można mieć w źródle 8 bitów, a celu 16-to bitowego i odwrotnie.
Dane pamięciowe - znajdują się gdzieś w 1MB obszarze pamięci. ( ! ) W większości instrukcji tylko jeden z operandów może być adresem pamięci. Dane takie podlegają adresacji pośredniej . Najpierw należy załadować do odpowiednich rejestrów wartość adresu segmentu i przesunięcia. Następnie adresuje się przy użyciu [ ], którymi wskazujemy, w jakich rejestrach jest adres komórki pamięci.
Adresacja pośrednia
Sposoby adresacji pośredniej:
( ! ) W każdym przypadku można użyć przedrostka zmiany segmentu.
[ <rejestr_offsetu> ] [BX] - dana pamięciowa z segmentu domyślnego dla BX i offsetu zawartego w BX
[ <rejestr_bazowy> + <rejestr_indeksu> ] [BX + DI] - adresowanie indeksowe względem bazy - dana pamięciowa z segmentu DS o offsecie równym sumie wartości w BX i DI
( ! ) można wykorzystać przy adresowaniu pól tablicy rejestr bazy wskazuje na początek tablicy, rejestr indeksu
modyfikujemy wskazując na kolejne elementy w tablicy (przesunięcie względem początku tablicy)
[ <rejestr_bazowy> + <dana_natychmiastowa> ] [SI + 1] - adresowanie pośrednie z przesunięciem - <dana_natychmiastowa> jest przesunięciem (stała wartość liczbowa) względem podstawowego offsetu (z rejestru).
( ! ) można wykorzystać do odwoływania się do kolejnych pól rekordu rejestr wskazuje na początek danego obiektu
(rekordu), przesunięcie wskazuje na konkretne pole rekordu (w każdym obiekcie danego typu rekordowego dane pole
jest przesunięte o stałą liczbę bajtów względem adresu obiektu)
[ <rejestr_bazowy> + <rejestr_indeksu> + <dana_natychmiastowa> ] [BX + DI + 7] - adresowanie indeksowe względem bazy z przesunięciem - dana pamięciowa z DS, offset równy sumie offsetów rejestrów i wartości liczbowej.
( ! ) można wykorzystywać do odwoływania się do konkretnych pól w tablicach rekordów <rejestr_bazowy>
wskazuje adres tablicy, <rejestr_indeksu> - element tablicy (rekord), <dana_natychmiastowa> - konkretne pole w danym
rekordzie (znajduje się ono w każdym obiekcie tego typu rekordowego o taką samą ilość bajtów od początku obiektu)
Adresowanie wielowymiarowe - dla tablic o wielu wymiarach korzystać z ostatniego typu adresowania, <rejestr_bazowy> - początek tablicy, <dana_natychmiastowa> - pole w rekordzie, <rejestr_indeksu> - konkretny element tablicy, który wcześniej należy obliczyć (i załadować do tego rejestru) ze wzoru (indeksacja od „0”) :
nr_wiersza * ilość_bajtów_wiersza + nr_kolumny
Domyślny adres segmentowy - rzadko musi się podawać pełny adres komórki. Wystarczy jedynie adres offsetu, a adres segmentu zostanie przypisany domyślnie. Wartości tego adresu zależą od instrukcji i rejestru, w którym przechowywany jest offset. Najczęściej domyślnie przyjmuje się segment:
DS dla offsetów BX, SI, DI
SS dla offsetu SP ( ! ) i nie można tego zmienić
SP dla BP
Jeśli chcemy zmienić domyślne założenia, musimy przy adresowaniu użyć przedrostka zmiany segmentu w postaci <rejestr_segmentu> : przed nazwą rejestru offsetu np. ES:[BX], CS:[SI], ES:[BX + DI] itd.
Tworzenie programu „exe”
Każdy program musi zawierać co najmniej 3 segmenty logiczne: stosu, danych i kodu. Możne być więcej segmentów.
Segment definiuje się za pomocą dyrektywy SEGMENT, zgodnie ze składnią:
<nazwa> SEGMENT [typ] [połączenie] [`nazwa_klasy'] np. MojeDane SEGMENT.
gdzie:
<nazwa> jest adresem początkowym tego segmentu
[typ] - określa gdzie rozpocznie się ten segment logiczny, przyjmuje wartości:
BYTE - od pierwszego wolnego bajtu
WORD - od pierwszego wolnego słowa
PARA - od pierwszego wolnego adresu podzielnego przez 16
PAGE - od pierwszego wolnego adresu podzielnego przez 256
( ! ) jeśli nie podamy typu domyślnie przyjęte zostanie PARA
[połączenie] - informuje linker, w jaki sposób łączyć segment z innymi segmentami, może przyjąć wartości:
PUBLIC - tworzony będzie jeden wspólny segment z segmentów o tej samej nazwie, będą one miały jeden adres
początkowy, a rozmiar segmentu będzie sumą rozmiarów wszystkich segmentów o tej nazwie
COMMON - segmenty o tej samej nazwie będą miały wspólny adres początkowy, wielkość segmentu =
wielkości największego z łączonych segmentów
MEMORY - segment zostanie umieszczony na końcu ładowanego programu, może istnieć tylko 1 taki segment
AT <wyrażenie_numeryczne> - segment zostanie umieszczony pod adresem segmentowym znanym w czasie
asemblacji określanym przez <wyrażenie_numeryczne>
STACK - wszystkie segmenty o tej nazwie i połączeniu STACK stworzą wspólny segment stosu, o wielkości
równej ich sumie
( ! ) Jeśli połączenie nie jest podane domyślnie przyjęte jest PRIVATE - segment nie będzie z żadnym innym łączony.
[`nazwa_klasy'] - wg tej nazwy linker grupuje segmenty w ramach modułów, jeśli nawet segmenty będą miały tą samą nazwę, a różne nazwy klasy nie zostaną połączone
Koniec definicji segmentu jest określany dyrektywą ENDS, przed którą stoi <nazwa>.
Każdy kod programu (definicje wszystkich segmentów, nie tylko kodu) kończymy przez wpisanie dyrektywy END poza każdym z segmentów. Asembler nie będzie czytał żadnych instrukcji ani dyrektyw, znajdujących się po END.
Wyrażenia matematyczne
Asembler umożliwia stosowanie wyrażeń matematycznych (używających operatorów matematycznych np. 20 * 15 - stała) w kodzie programu. Obliczenia ich dokonuje się w czasie asemblacji - zapis wyrażenia zostaje zastąpiony w kodzie programu przez jego wartość w wyrażeniach mogą występować tylko stałe (liczby lub etykiety zdefiniowane za pomocą EQU). Można również użyć operatora NOT <op> , traktując go wraz z operandem jak wyrażenie po obliczeniu zostanie wstawiona na jego miejsce odpowiednia wartość. NOT neguje wszystkie bity <op>.
Przełączanie w tryb 286 / 386 / 486
Należy podać asemblerowi specjalne dyrektywy (w segmencie kodu): .486 (MASM/TASM) lub P486N (TASM).
Jak rozróżnić procesory
8086 / 8088 - cztery najstarsze bity znaczników są na stałe ustawione przenieść PUSHF znaczniki na stos, zmodyfikować ostanie bity, użyć POPF, jeśli dalej będą ustawione mamy 8086, jeśli nie ...
286 - cztery najstarsze bity znaczników są wyzerowane ( w trybie rzeczywistym, w chronionym bity 12-14 mają zastosowanie) postępować analogicznie jak dla 8086. Jeśli dalej wyzerowane mamy 286, jeśli nie ...
386 - rejestr AC zawsze równy 0 jeśli nie uda się go zmienić mamy 386, w przeciwnym razie 486
Segment stosu
Adres segmentu stosu przechowuje SS. Jest on ładowany przez DOS przy uruchamianiu programu. Rozmiar stosu rzadko musi przekraczać 1KB, jednak z tego stosu nie korzysta tylko nasz program, ale również funkcje przerwań DOS i BIOS. Segment stosu zaczyna się od SS:0, ale wszelkie na nim operacje odbywają się początkowo na jego przeciwległym końcu. Na początku SP wskazuje na koniec stosu (SS + rozmiar stosu).
Między SS + rozmiar a SS:SP znajdują się dane, przy czym SS:SP wskazuje na aktualnie ostatnio zapisaną daną (Szczyt stosu). Między SS:0 a SS:SP znajduje się pusty obszar gotowej do wykorzystania pamięci.
Odkładanie na stos
Przy odkładaniu najpierw zmniejsza się SP, a potem zapisuje odpowiednią daną pod adresem SS:SP. Odbywa się to za pomocą PUSH (odkłada na stos zawartość dowolnego rejestru ,tego na co rejestr wskazuje lub (286) danej natychmiastowej) lub PUSHF (odkłada rejestr flag) np.
PUSHF
PUSH AX
PUSH [BX]
PUSH 21H
( ! ) Obie instrukcje odkładają na stos po 1 słowie, nie można odkładać na stos rejestrów 8-bitowych. Bajty słowa znajdują się pod adresami: mniej znaczący - SS:SP, bardziej znaczący SS:SP+1
( ! ) nie można odkładać na stos danych bezpośrednich (8086)
Zdejmowanie ze stosu
Zdejmując najpierw odczytuje się szczyt stosu, a potem zwiększa odpowiednio wartość SP. Realizuje się to za pomocą POP (ogólne) lub POPF (dla flag). ( ! ) Obie czytają dane o wielkości 1 słowa (nie 8-bit).
POPF
POP AX
POP [BX]
Inicjalizacja stosu
Definiuje się go jako jedną ogromną zmienną bez nazwy, przy użyciu DUP (patrz: Segment danych) np.
DB 64 DUP(`STACK!!!');
( ! ) - opłaca się rezerwować stos inicjalizując go jakimś identyfikującym napisem (np. `stack'), bo dzięki temu łatwo można go odnaleźć przez DEBUG. Tyczy się to każdego segmentu lub dużej zmiennej
Segment danych
Typ danych - w asemblerze oznacza rozmiar danych.
Definiowanie zmiennych
Definiując zmienną określa się ją etykietą (nazwa zmiennej) oraz dyrektywą definicji danych:
DB (define byte) - definiuj bajt
DW (define word) - definiuj słowo
DD (define double) - definiuj podwójne słowo
DQ - definiuje poczwórne słowo
DT - definiuje 10 bajtów (5 słów)
np. zmienna DW ; „zmienna” będzie miała rozmiar 2 bajtów.
( ! ) W przypadku gdy zmienna „zm” jest 16-bitowa, nieprawidłowe będzie: MOV AL, zm, bo niezgodność typów do 8-bitów „pakuje się” 16 bitów. Należy wówczas użyć specyfikatora zmiany typu.
Zmienne można od razu inicjalizować - po dyrektywie podaje się wartość zmiennej np. zmienna DW 0FFFFH.
Zmienne łańcuchowe
Zmienne takie to ciągi znaków (string) umieszczone po sobie w pamięci. Miejsce dla nich rezerwuje się inicjalizując je w odpowiedni sposób. Po nazwie zmiennej podaje się dyrektywę DB ( ! ) po której umieszcza się ciąg znaków. Ciąg znaków umieszcza się w „ ” lub ` `, albo podając liczbowe wartości odpowiednich znaków ASCII oddzielanych przecinkami. Tak więc DB rezerwuje tylko 1 bajt, będący początkowym znakiem łańcucha. To na niego wskazuje etykieta stojąca przed DB. Liczba znaków w apostrofach decyduje ile ma zostać zarezerwowanych bajtów. Można łączyć szeregi oddzielnych ciągów znaków inicjalizujących oddzielając je przecinkiem `,' np.
lancuch DB „Ala”, „ma kota”, `$'
Eol DB 0DH, OAH, `$'
Długość łańcucha - jeśli zainicjalizowano łańcuch i chcemy znać jego długość (bez liczenia literek, przy każdej zmianie łańcucha), w następnej linijce po jego definicji można zapisać:
<stała> EQU $ - <nazwa_łańcucha>
gdzie: <stała> - będzie zawierać długość łańcucha <nazwa_łańcucha>, $ - operator oznaczający obecne położenie - zwraca adres miejsca w kodzie, w którym on wystąpił. W tej sytuacji <nazwa_łańcucha> wskazuje na pierwszy bajt stringu, $ - na pierwszy bajt zaraz po stringu, więc <stałą> będzie ilością bajtów stringu np.
Napis DB `Ala ma kota, ale kot jest zielony'
Dl EQU $ - Napis ; „Dl” ma wartość ilości znaków „Napis”
Zmienne większych rozmiarów
Jeśli chcemy zarezerwować miejsce dla zmiennej większe niż dzięki podstawowym dyrektywom, a nie chcemy korzystać ze stringów, należy użyć dyrektywy DUP. Zmienne definiuje się zgodnie ze schematem:
<nazwa_zmiennej> DB <ile> DUP( <czym_wypelnić> )
DB - rezerwuje tylko początkowy bajt
<ile> - liczba powtórzeń argumentu w nawiasach
<czym_wypelnić> - wzorzec do inicjalizacji string, liczba lub ? - nie inicjalizuje, wypełnia dowolnym znakiem już znajdującym się w pamięci.
np. Nazwa DB 10 DUP(`Luke') ; ”Nazwa” zajmie 40B
Temp DB 10 DUP(?) ; „Temp” zajmie 10B, nie inicjalizowana
( ! ) - zmienna będzie zajmować: ile * sizeof ( <czym_wypelnić>)
Zmienne anonimowe
Tworzy się je nie podając etykiety przed dyrektywą rezerwującą miejsce np. DB 100 DUP(?). Dostęp do takiej zmiennej może odbywać się przy pomocy adresu przesunięcia tej zmiennej w segmencie danych.
( ! ) - offset zmiennej w DS = ilość bajtów, które zajęły zmienne zdefiniowane wcześniej w segmencie danych
Dyrektywa LABEL
Dyrektywa ta służy do określania wielkości pamięci kojarzonej z etykietą zmiennej. Ma ona następującą składnię:
<nazwa_zmiennej> LABEL <rozmiar_pamięci>
gdzie: <rozmiar_pamięci> jest jedną z nazw asemblera np. WORD, DWORD
Nazwa zmiennej będzie się odnosić do pierwszej komórki pamięci, która zostanie zdefiniowana w kodzie za pomocą jednej z dyrektyw definicji zmiennych (DB, DW). Z <nazwa_zmiennej> będzie odtąd kojarzony rozmiar nie trzeba będzie stosować specyfikatora zmiany typu (patrz niżej).
Tablice
Definiuje się przy użyciu dyrektywy DB, po której można inicjalizować pamięć jak w przypadku zwykłego stringu lub używając dyrektywy DUP (), w której w nawiasach można podać wzorzec o długości jednego pola, a przed DUP może stać liczba = ilości potrzebnych pól tablicy np.
Tab DB '1234567890' - tablica 10-cio bajtowa, można ją traktować jak zbiór 10-ciu elementów 1-bajtowych.
Tab DB 100 DUP(`12') - tablica, którą można używać do przechowania 100 elementów 2-bajtowych.
Można także użyć innych dyrektyw np. DW, wówczas tworzymy zmienną odpowiednią dyrektywą, a po niej tworzy się ciąg anonimowych zmiennych tymi samymi dyrektywami (w końcu chodzi tylko o zarezerwowanie miejsca, a nazwa zmiennej potrzebna jest tylko do zaadresowania początku tablicy). Przykładowo:
Tab DW 0B800H
DW 0A000H
DW 0000H . . .
( ! ) Przy odwoływaniu się do kolejnych elementów tablicy, używa się adresowania indeksowego względem bazy, wówczas rejestr bazy wskazuje początek tablicy, a rejestr indeksu - kolejny element tablicy. Jeśli elementy tablicy mają rozmiar
n-bajtów, rejestr indeksu musi być wielokrotnością n.
( ! ) Opłaca się wielkość elem. tablicy (n) definiować jako wielokrotność „2”, ponieważ szukając przesunięcia i-tego elementu dokonuje się mnożenia i*n, co można wówczas szybko wykonać za pomocą SHL
( ! ) Przy odwoływaniu się do el. tablicy można wykorzystać adresowanie pośrednie z przesunięciem wówczas rejestr bazy wskazuje na kolejny element, a daną natychmiastową stanowi (konkretna wartość) etykieta, będąca nazwą tablicy.
( ! ) odwołania do elementów tablicy mogą wyglądać następująco:
<nazwa_tablicy>[ <indeks> ] np. tab[2]
gdzie <indeks> oznacza numer komórki tej tablicy. Rodzaj (wielkość) komórki zależny jest od dyrektywy użytej do definiowania tablicy (np. DB, DW ), ewentualnie od zadeklarowanego dla <nazwa_tablicy> rozmiaru pamięci za pomocą dyrektywy LABEL.
Szybkie odwzorowania - tworzenie tablic przeglądowych tablica, obrazująca odwzorowanie, w której w i-tym elemencie znajduje się wartość odwzorowania na argumencie i. ( f ( i ) ). Taką tablicę najlepiej od razu inicjalizować (na etapie pisania kodu). Dzięki temu nie trzeba dla danego elementu mozolnie obliczać f( i ) (np. wykonywać serii sprawdzeń typu: jeśli i = 0 zwróć 7, jeśli i = 1 zwróć 2, ...
Stałe
Definicji stałych dokonuje się przy użyciu dyrektywy EQU, przed którą stoi nazwa stałej, a po której występuje wyrażenie, którym ta nazwa ma być zastąpiona (np. wyrażenie arytmetyczne, tekst w apostrofach, stała liczbowa itp.) np.
Stala EQU 2-7*4
Imie EQU `Cindy'
Data EQU 0FFFF:0005
( ! ) EQU dziala jak #define, na etapie preprocesingu zamienia każde wystąpienie nazwy stałej na odpowienią wartość.
Zmiana typu
W większości przypadków, gdy instrukcja jednoargumentowa odwołuje się do pamięci przez adres w rejestrze (adresowanie pośrednie) - np. NEG, INC, DEC, NOT - należy podać, do jakiej wielkości danych ma się odwoływać (np. czy NEG ma zanegować bajt czy całe słowo spod podanego adresu). Osiąga się to przez podanie specyfikatorów zmiany typu - BYTE PTR, WORD PTR itp. np. NEG BYTE PTR [BX]
( ! ) nie istnieje domyślny typ danych dla tych instrukcji.
( ! ) W sytuacji, gdy chcemy do 8-bitów zapisać 16-bitową daną, dodajemy przed nazwą 16-bitowej danej specyfikator zmiany BYTE PTR i zostanie tam zapisana starsza połówka tego słowa np.
MOV AL, BYTE PTR Zmienna
Segment kodu
Definiowanie Etykiet
Etykieta - nazwa symboliczna służąca do określenia miejsca w pamięci (etykieta = nazwa zmiennej, adres danej instrukcji w kodzie, adres segmentu itp. ).
W kodzie programu można zdefiniować etykiety, którym przypisany zostanie adres instrukcji stojącej po etykiecie. Umożliwia to łatwiejsze wykorzystanie instrukcji skoku skok do etykiety = skok do instrukcji w programie, mającej adres przesunięcia względem CS przypisany etykiecie. Etykietę definiuje się następująco:
<etykieta> : gdzie <etykieta> - dowolny string np. Tutaj :
( ! ) Odwołanie do etykiety odbywa się już bez pisania dwukropka np. MOV SI, Tutaj.
Definiowanie początku programu
Przed instrukcją od której chcemy rozpocząć program tworzymy etykietę np. Start. Następnie po END wypisujemy nazwę tej etykiety.
Określenie kodu dla odpowiednich segmentów
Do skojarzenia etykiet segmentów z odpowiednimi rodzajami segmentów służy dyrektywa ASSUME. Używa się jej zgodnie z następującym wzorcem: ASSUME CS:Kod, DS:Dane, SS:Stos , gdzie Kod, Dane i Stos to etykiety - nazwy segmentów. Odtąd asembler wie, że kod między Dane SEGMENT a Dane ENDS należy traktować jako kod segmentu danych - dzięki temu wszystkie zmienne definiowane w kodzie między Dane SEGMENT a Dane ENDS znajdą się w segmencie danych i będą do nich odnosić się inne instrukcje niż do zmiennych definiowanych w segmencie kodu. Na tej podstawie asembler będzie generował także wartości domyślnych adresów segmentowych.
( ! ) - Należy załadować adres segmentu danych do rejestru DS. Nie można tego dokonać jednak bezpośrednio z danej natychmiastowej czy pamięciowej. Robi to się w następujący sposób:
mov AX, Dane
mov DS, AX
Procedury
Definiowanie
Procedury wydzielamy za pomocą dyrektywy PROC, przed którą stoi etykieta = nazwa procedury. Następnie definiuje się instrukcje tej procedury i kończy się za pomocą ENDP, przed którą stoi nazwa procedury np.
Moja PROC
mov AX, Dane
mov DS, AX
dec CX
....
ret
Moja ENDP
Wywołanie procedury - za pomocą CALL <nazwa_procedury> np. call Moja. CALL odkłada na stos adres powrotu (offset = 2B = 16b) i skacze do wskazanego miejsca. ( ! ) - Procedura musi zawierać instrukcję RET umożliwiającą powrót w przypadku gdy wywołujemy ją przez CALL, RET pobiera adres ze stosu i skacze do niego.
( ! ) - wewnątrz procedury można robić wszystko co w każdej części programu także wywoływać inne proc.
Bezpieczeństwo
Poprawnie napisana procedura powinna dbać o to, aby wszystkie rejestry (też znaczniki) miały taką samą wartość po wywołaniu procedury jak przed jej wywołaniem. Innymi słowy - patrząc na procedurę z „zewnątrz” nie powinno się stwierdzić, że modyfikuje ona jakiekolwiek rejestry. Aby to zapewnić należy posłużyć się stosem. Na początku odkładamy na stos wszystkie rejestry , które w dalszej części będziemy modyfikować, a następnie tuż przed końcem procedury zdejmujemy je w odwrotnej kolejności np.
Moja PROC
; zabezpieczenie wart. rejestrow
push AX
pushf
...
XOR AX, AX ;modyfikowany rejestr AX
CMP AX, zmienna ;modyfikowane flagi...
...
; przywrocenie wartosci z momentu wywolania
popf
pop AX
ret
Moje ENDP
Przekazywanie parametrów do procedur
przez odpowiednie rejestry - można dla danej procedury ustalić listę rejestrów, w których mają przed jej wywołaniem zostać zapisane odpowiednie parametry wywołania (jako dane wej.) oraz wyspecyfikować, w jakich rejestrach procedura zwróci wynik
przez stos - procedura wymaga przed jej wywołaniem odłożenia na stos danych wejściowej w określonej kolejności. Wartość zwracana może też być odkładana na stos. ( ! ) - przy pobieraniu ze stosu należy pamiętać, że szczyt stosu zawiera adres powrotu z procedury pierwsza dana wejściowa (parametr) znajduje się pod SS:[SP+2]
Przekazywanie argumentów dla procedur przy użyciu stosu
W przypadku przekazywania przez stos należy korzystać z adresowania pośredniego z przesunięciem w celu odwołania się do komórek pamięci znajdujących się „pod” szczytem stosu. Szczyt stosu wskazywany jest przez rejestr SP. Nie może on jednak być wykorzystany jako rejestr bazowy w adresowaniu pośrednim z przesunięciem. Funkcję taką może jednak spełnić rejestr BP. W celu odczytu argumentów przekazanych do procedury należy najpierw przyrównać jego wartość do wartości SP, a następnie można odwoływać się do słów pamięci poniżej stosu o adresach odpowiednio BP+4, BP+6 ...
Poniżej znajduje się przykład procedury do której przekazuje się parametry za pomocą stosu:
Moja PROC
;zabezpieczenie rejestrów
push BP
push AX
push BX
;odczytanie argumentów wywołania
;UWAGA - pod adresem SP+2 znajduje się BX, pod SP+4 AX, pod SP+6 BP,
; pod SP+8 - adres powrotu z procedury dla RET
mov BP, SP
mov BX, [BP+10] ;do BX trafia drugi arg. wywołania
mov AX, [BP+12] ;do AX pierwszy
...
;przywrócenie wartości z momentu wywołania
pop BX
pop AX
pop BP
ret
Moja ENDP
Poprawne wywołanie takiej procedury ma postać:
...
push arg1
push arg2
call Moja
pop arg2 ;jeśli zachodzi konieczność odczytania zmodyfikowanych wartości argumentów
pop arg2 ;można je teraz odczytać...
...
( ! ) - W przypadku, gdy po wywołaniu procedury nie interesują nas wartości argumentów które odłożyliśmy na stos, nie ma potrzeby używania POP do ściągnięcia ich ze stosu. Wystarczy w następnej linii kodu napisać
ADD SP, N
gdzie N jest ilością bajtów, jaką zajmowały argumenty na stosie.
Zmienne lokalne
Jeśli zachodzi konieczność wykorzystywania zmiennych lokalnych wewnątrz procedur, zmienne takie należy odłożyć na stosie. W tym celu rezerwuje się odpowiednią ilość N - bajtów miejsca (SUB SP, N ). W tym momencie mamy do dyspozycji N bajtów stosu do wykorzystania jako zmienne lokalne. Do kolejnych zmiennych odwołujemy się także przy użyciu rejestru BP, który najpierw przyrównujemy do SP.
( ! ) Jeśli po zarezerwowaniu miejsca na zmienne lokalne chcemy odwołać się np. do argumentów wywołania procedury odłożonych na stosie, należy pamiętać, że adres BP przesunął się o N bajtów i adres tych parametrów względem nowej wartości BP jest większy o N np.
mov BP, SP
; pod BP+4 - pierwszy argument wywołania
sub SP, 4 ;zarezerwowano dwa słowa na zmienne lokalne
mov BP, SP ;SP wskazuje na ostatnią zmienną lokalną
; pod BP + 8 - pierwszy arg. wywołania
( ! ) W momencie, gdy zmienne lokalne nie są już potrzebne należy zwolnić zajmowane przez nie miejsce na stosie za pomocą ADD SP, N.
( ! ) Przy okazji rezerwowania zmiennych na stosie dobrze jest obarczyć każde rezerwowane słowo komentarzem np.
sub SP, 4; rezerwacja miejsca na 2 słowa
mov BP, SP
;OPIS ZMIENNYCH LOKALNYCH:
;BP - zmienna używana do ...
;BP+2 - zmienna używana do ...
...
Punkty wejściowe
W ramach procedury można zdefiniować etykiety, które później można traktować jak punkty wejściowe tej procedury. Dzięki temu można korzystając z 1 kodu procedury realizować różne funkcje ( omijając za pomocą punktu wejściowego kilka pierwszych instrukcji). Dzięki temu można np. zdefiniować wartości domyślne procedury, które będą inicjalizowane instrukcjami zaraz po dyrektywie PROC, a następnie zdefiniować punkt wejściowy po tych instrukcjach, pozwalający na zewnętrzne ustawienie tych parametrów np.
DodajDomysl PROC
mov AX, 2
mov BX, 2
Dodaj: add AX, BX
ret
DodajDomysl ENDP
Wywołania procedury przez jej punkt wejściowy dokonuje się używając: CALL <punkt_wejściowy>.
( ! ) Do każdej procedury dodawać komentarz:
nazwa procedury
co dana procedura robi
jakie ma punkty wejściowe
które rejestry wymagają załadowania, jako dane wejściowe
co należy odłożyć na stos, jako dane wej.
które rejestry są modyfikowane na skutek działania procedury
w jakich rejestrach zwracana jest wartość
co procedura odkłada na stosie jako zwracaną wartość
nazwy wywoływanych przez nią procedur
Makroinstrukcje
Makra traktowane są jak procedury „inline” - wstawiane w miejsce wywołania w czasie preprocesingu asemblacji. Odbywa się to na podobnej zasadzie, jak obliczanie wyrażeń matematycznych.
Definiowanie
Makra definiuje się ( ! ) poza wszelkimi segmentami, definicję makra rozpoczyna się następująco:
<nazwa_makra> MACRO <parametr1> , <parametr2>, ...
gdzie <parametr i> - etykiety oznaczające kolejne parametry makra, pod które w trakcie wywołania (preprocesingu) podstawiane są odpowiednie argumenty.
Parametr makra - etykiety (nazwy) wpisane bezpośrednio za dyrektywą MACRO przy definicji makra
Argument makra - konkretne wartości podane przy wywołaniu makra.
Odwołanie do parametrów (a w praktyce argumentów) odbywa się w kodzie makra za pomocą etykiet <parametr i>
Definicję makra kończy dyrektywa ENDM.
( ! ) - etykiety w makrach muszą być lokalne (każde wstawienie makra powoduje powielenie definicji etykiety, a ta musi być unikatowa). W tym celu należy w 2 linii kodu makra (po linii zawierającej MACRO) zadeklarować jako lokalne nazwy wszystkich używanych wewnątrz makra etykiet:
LOCAL <etykieta1> , <etykieta2>, ...
Etykiety takie widoczne są tylko w obrębie definicji makra.
Przykład (treść makra nie ma wiele wspólnego z nazwą)
Gotoxy MACRO WspX, WspY
LOCAL Petla, Koniec
mov DH, WspX
mov DL, WspY
Petla: dec CX
...
Koniec: mov AX,[BX]
ENDM
( ! ) - na końcu makra nie wstawiać RET (nie ma tu powrotu, bo nie ma skoku do makra)
( ! ) - przed ENDM nie stoi nazwa makra
( ! ) - jeśli wykorzystuje się w makrze instrukcje, które operują na parametrach makra, a mają ograniczenia co do tych parametrów (wykorzystuje się instrukcję która nie może używać danych natychmiastowych, instrukcja ta działa na parametrze jeśli za parametr zostanie podstawiony argument w postaci rejestru - makro zadziała poprawnie, gdy argumentem będzie dana natychmiastowa, asembler wykaże błąd) należy w opisie makra podać ograniczenia dotyczące parametrów lub spróbować ominąć niedogodności (przed użyciem instrukcji nie operującej na danych natychmiastowych, przenieść jej parametr do rejestru, następnie wywołać tą instrukcję nad tym rejestrem)
Wywoływanie
Zlecenia wstawienia makra w odpowiednie miejsce programu (wywołania) dokonuje się zgodnie ze schematem:
<nazwa_makra> <argument1> , <argument2>, ... np. Gotoxy 10, 20
( ! ) - można podać więcej argumentów niż parametrów (ostatnie zostaną zignorowane). Za mało arg. błąd.
( ! ) - nie używać CALL
Przekazywanie dalekiego adresu - przydatne, gdy musimy oprócz offsetu jakiejś danej przekazać adres jej segmentu (np. dla pracy z pamięcią karty graficznej), do tego używa się instrukcji LES wg. schematu:
Clrscr MACRO AdresBuf, Czymczysc
LES DI, DWORD PTR AdresBuf ;AdresBuf zostanie załadowany do ES:DI
Wywołanie takiego makro może mieć postać:
Clrscr 0B800H:0000H,` `
Wzorzec programu „exe”
;---------------------------SEGMENT STOSU - START
Stos SEGMENT STACK
DB 64 DUP (`STACK!!!')
Stos ENDS
;---------------------------SEGMENT STOSU - KONIEC
;---------------------------SEGMENT DANYCH - START
Dane SEGMENT
Zmienna DB
Dane ENDS
;---------------------------SEGMENT DANYCH - KONIEC
;---------------------------SEGMENT KODU - START
Kod SEGMENT
assume CS:Kod, DS:Dane
Main PROC
Start: ;tu zaczyna się program
mov AX, Dane
mov DS, AX ;pośrednie załadowanie rejestru segmentu danych
...
mov AH, 4CH ;wybór procedury DOS kończącej program
mov AL, 0 ;przekazanie wartości do zmiennej ERRORLEVEL
int 21H
Main ENDP
Kod ENDS
;---------------------------SEGMENT KODU - KONIEC
END Start
Program typu „com”
Właściwości
zbiory .com są krótsze (min. o 512 bajtowy nagłówek pliku .exe)
programy .com muszą zawierać się w pojedynczym segmencie 64KB
dla programów .com stos jest generowany automatycznie, miejsce na niego przydzielane jest w ramach segmentu zajmowanego przez program, a jeśli nie ma tam dość miejsca - na końcu wolnej pamięci
program .com musi definiować dane wewnątrz segmentu kodu na początku, przed wszystkimi instrukcjami poza instrukcją skoku do początku programu
wszystkie skoki w programie muszą być bliskie (także bliskie procedury tworzone przy użyciu dyrektywy NEAR)
należy zainicjalizować wszystkie rejestry segmentowe na początek segmentu kodu (jedynego segmentu)
należy zarezerwować 256B na PSP (nagłówek uruchomionego programu w pamięci DOS) przy pomocy dyrektywy: ORG 100H na początku programu
Wzorzec programu
Kod SEGMENT
assume CS:Kod, DS:Kod, SS:Kod
ORG 100H
Start: jmp Main
;tu definiuje się zmienne w segmencie:
Zmienna DB
...
Main PROC NEAR
...
ret
Main ENDP
Kod ENDS
END Start
Tworzenie programu wynikowego
dokonać asemblacji pliku źródłowego
zlinkować (nie zwracając uwagi na Warning: No STACK segment! )
używając programu EXE2BIN zamienić powstały plik .exe na .com
Tworzenie bibliotek zewnętrznych
Tworzenie modułów z procedurami i danymi
Procedury można grupować w osobnych plikach. Plik taki po zasemblowaniu będzie traktowany jako moduł. Plik modułu ma następujące cechy:
nie posiada punktu startowego (nic po END)
nie zawiera kodu poza procedurami ( = głównego programu)
nie zawiera segmentu stosu (korzysta ze stosu programu głównego)
może posiadać segment danych (choćby tylko na zmienne zewnętrzne)
jeśli jakieś procedury (punkty wejściowe) chcemy udostępnić innym modułom - należy je zadeklarować na początku segmentu kodu:
PUBLIC <etykieta1> , <etykieta2> , ... <etykieta i> - nazwa udostępnianej procedury lub punktu wejściowego
W dalszej części segmentu kodu definiujemy treść tych procedur zwyczajnie przy użyciu PROC i ENDP
jeśli chcemy udostępniać dane także należy je upublicznić - zadeklarować na początku segmentu danych:
PUBLIC <zmienna1> , <zmienna2> , ... w dalszej części segmentu należy zdefiniować w normalny sposób te zmienne
(np. przy użyciu dyrektyw DB, DW czy DUP( ) )
( ! ) jeśli w segmentach danych lub kodu znajdują się jakieś deklaracje publiczne, należy po nazwie segmentu i dyrektywie SEGMENT podać dyrektywę PUBLIC.
( ! ) segmenty kodu i danych w każdym module muszą mieć te same nazwy - wówczas linker potraktuje je jako części składowe tego samego segmentu.
( ! ) w każdym module a także programie głównym używać ASSUME.
Przykład:
;------------------moduł z procedurami dodawania------------------
Dane SEGMENT PUBLIC
PUBLIC Eol, Ekran
Eol DB 0DH, 0AH, `$' ; przykładowe zmienne nie związane z modułem
Ekran DW 184FH ; y = 18H = 24D, x = 4FH = 79D
Dane ENDS
Kod SEGMENT PUBLIC
PUBLIC DodajDomysl, Dodaj
ASSUME CS:Kod, DS:Dane
DodajDomysl PROC
mov AX, 2
mov BX, 2
Dodaj: add AX, BX
ret
DodajDomysl ENDP
Kod ENDS
END
Wykorzystanie procedur i danych z modułów
jeśli w programie chcemy wykorzystywać jakąś procedurę zewnętrzną musimy ją zadeklarować:
EXTERN <nazwa_procedury> : PROC np. EXTERN ClrScr: PROC
Dzięki temu „definiujemy” etykietę i odtąd asembler dba o jej jednoznaczność. Asembler wie, że etykieta ta oznacza
procedurę znajdującą się w zewnętrznym module.
( ! ) Deklaracji dokonuje się w segmencie kodu, poza ciałem wszelkich procedur
( ! ) Jeśli chcemy korzystać z punktów wejściowych, należy je także zadeklarować.
jeśli chcemy korzystać z danych zdefiniowanych na zewnątrz musimy je zadeklarować:
EXTERN <nazwa_zmiennej> : <specyfikator_typu> , ...
gdzie specyfikator typu określa rozmiar zmiennej (asembler musi wiedzieć jak taką zmienną traktować, w zależności
od rozmiaru zmiennych różne są wersje odpowiednich instrukcji maszynowych). Może on mieć następujące wartości:
BYTE, WORD, DWORD gdy zewnętrzne zmienne są zdefiniowane odpowiednio jako DB, DW, DD.
( ! ) Gdy zewnętrzna zmienna jest typu „string” lub deklarowana za pomocą DUP( ) jest ona definiowana przy pomocy
dyrektywy DB należy ją zadeklarować jako BYTE
Przykład deklaracji zmiennej łańcuchowej i słowa: EXTERN Eol:BYTE, Ekran:WORD
( ! ) Przy deklarowaniu procedury zewnętrznej rolę specyfikatora typu pełni PROC
( ! ) Deklaracja ta jest w segmencie danych
Łączenie modułów
Łączenia dokonuje linker, w tym celu należy najpierw stworzyć pliki .obj dla wszystkich modułów, następnie należy podać w wywołaniu linkera nazwę programu głównego i wszystkich modułów.
Biblioteki makr
Bibliotekę stanowi niezasemblowany plik zawierający tylko definicje kolejnych makr (bez segmentów danych ani segmentu kodu). Plik taki ma rozszerzenie .mac . Włączenie tego pliku (jako 1 „instrukcja” w kodzie programu głównego) następuje przy pomocy dyrektywy INCLUDE , po której występuje nazwa pliku.
Przerwania
Przerwania sprzętowe
Urządzenia zewnętrzne komunikują się z procesorem za pomocą przerwań sprzętowych. W odpowiedniej chwili oprócz wysłania sygnału przerwania wysyłany jest także sygnał identyfikujący dane urządzenie - kod typu. Mikroprocesor może rozróżnić 256 różnych kodów. Na podstawie kodu procesor uruchamia odpowiedni program obsługi - oblicza adres jego wektora przerwania mnożąc kod typu przez 4. Dzięki temu, w wyniku przerwania pochodzącego od różnych urządzeń uruchamiane są różne programy. Oprócz przerwań pochodzących od urządzeń, sam procesor jest w stanie wygenerować sygnał przerwania (np. przy dzieleniu przez 0). Programy obsługi przerwań mogą być także wywoływane jawnie w kodzie programów...
Przerwania programowe
W segmencie „0” znajduje się tabela o 256 pozycjach, zawierająca adresy 4 bajtowe (2 starsze bajty - segment + 2 młodsze - przesunięcie) adresy kodów różnych procedur (przerwań programowych) zajmuje ona pierwszy 1KB RAM. Każdy adres - wektor przerwania . Tabela ta zwana jest tabelą wektorów przerwań. Pamięć tą uzupełniają BIOS i DOS, każdy może zająć określone pozycje. W każdej wersji DOS w danym wektorze (numer wektora w tabeli = numer przerwania) znajduje się adres procedury (grupy procedur) wykonującej to samo, ale ta procedura znajduje się w różnych miejscach.
Wywołanie przerwania następuje za pomocą INT <numer_przerwania> np. INT 21H. Przed skokiem do odpowiedniej procedury (przerwania) INT wykonuje:
odkłada na stos zawartość rejestru znaczników (1 słowo)
zeruje flagę IF (w tym momencie inne przerwanie nie może zostać wywołane z zewnątrz zablokowanie możliwości przyjmowania przerwań) oraz TF
odkłada na stos adres segmentu kodu CS
przypisuje mu nowy adres z wektora przerwań
odkłada na stos adres offsetu kodu IP
przypisuje mu wartość z wektora przerwań
Aby umożliwić powrót z przerwania, w kodzie przerwania ostatnią instrukcją jest IRET, która wykonuje działania przeciwne do INT.
( ! ) - istnieje warunkowa wersja INT - INTO, przerwanie wykonane zostaje jedynie, gdy nastąpiło przepełnienie (overflow)
Przerwania zarezerwowane
Przerwania od 0 do 1FH są zarezerwowane przez Intel'a. Mają min. następujące funkcje:
0 - błąd dzielenia - wynik nie mieści się w rejestrze lub dzielenie przez 0
1 - pojedynczy krok - występuje po każdej instrukcji, umożliwia debugowanie krok po kroku
2 - niemaskowanle przerwanie - nie może być zablokowane z poziomu żadnego programu
3 - punkt zatrzymania - pozwala wykonywać program aż do osiągnięcia odpowiedniego adresu
4 - nadmiar - obsługuje sytuację wystąpienia nadmiaru.
...
Przerwania od 20H do 3FH są zarezerwowane przez DOS.
20h - zakończenie wykonywania programu
21h - dyspozytor usług DOS
22h - adres powrotu po wykonaniu programu
23h - adres wyjścia dla Ctrl-Break
24h - obsługa poważnego błędu
25h - odczyt sektorów z dysku
26h - zapis sektorów na dysk
27h - zakończenie programu z pozostawieniem w pamięci
Definiowanie własnych procedur obsługi przerwań
Procedury obsługi przerwań muszą być procedurami dalekimi. Definiuje się je zgodnie ze wzorcem:
<nazwa> PROC FAR
...
IRET
<nazwa> ENDP
Wewnątrz procedury nie trzeba się martwić o ustawianie IF, odkładanie na stos zawartości flag itd. ponieważ dba o to INT.
Podmiana istniejących procedur obsługi przerwań
przed podmianą należy zapamiętać dotychczasową zawartość wektora podmienianego przerwania
należy wstawić odpowiedni bezwzględny adres kodu procedury (własny wektor przerwania)
przed przekazaniem sterowania z programu do systemu operacyjnego należy przywrócić pierwotny wektor przerwania
Przy realizacji tych czynności korzysta się z następujących funkcji przerwania 21H DOS:
ustawienie wektora przerwania funkcja AH = 25H, AL - numer przerwania, DS:DX - adres procedury obsługi
odczyt wektora przerwania funkcja AH = 35H, AL - numer przerwania, ES:BX - tu trafia adres procedury obsługi
Przykład przechwycenia przerwania obsługi dzielenia przez zero:
; w segmencie danych istnieje linia: wektor DW
; zdefiniowano daleką procedurę MojePrzerw
; w segmencie danych istnieje linia: adres DD MojePrzerw
...
;odczytanie dotychczasowej wartości
mov AH, 35H
mov AL. 0
int 21H
;zapamiętanie starej wartości
mov wektor[0], BX
mov AX, ES
mov wektor[1], AX
;zapisanie nowego adresu
lds DX, adres ;do DS:DX trafia adres nowej proc. obsługi
mov AH, 25H
mov AL. 0
int 21H
...
;przywrócenie pierwotnych wart.
lds DX, DWORT PTR wektor
mov AH, 25H
mov AL. 0
int 21H
...
Dyspozytor usług DOS
DOS udostępnia ponad 50 procedur, znajdują się one pod przerwaniem 21H. Wybór procedury za pośrednictwem AH oraz odpowiednio ustawionych innych rejestrów, w zależności od wymagań danej usługi.
( ! ) - Funkcje obsługi dysku twardego modyfikują CF. Gdy CF = 0 - operacja zakończyła się pomyślnie, CF=1 gdy nastąpił błąd, którego kod trafia do AX.
( ! ) - wszystkie łańcuchy zawierające nazwy plików muszą kończyć się znakiem `0'.
AH |
Operacja |
Dane wejściowe |
Wyniki |
|
Obsługa ekranu |
|
|
2 |
Wyświetlanie znaku (z kontrolą Ctrl-Break) |
DL = znak |
kursor za znakiem |
5 |
Wydruk znaku |
DL = znak |
|
6 |
Wyświetlenie znaku (bez kontroli Ctrl-Break) |
DL = znak |
kursor za znakiem |
9 |
Wyświetlenie łańcucha zakończonego `$' (nie można w ten sposób wyświetlić znaku dolara) |
DS:DX = adres łańcucha ( ! ) - zakończony `$'. |
kursor za łańcuchem |
40 |
patrz Rozszerzone funkcje obsługi plików |
BX = 1, ... |
|
|
Obsługa klawiatury |
|
|
1 |
Czekanie na znak z klawiatury, następnie wyświetlenie go (z kontrolą Ctrl-Break) |
|
AL = znak |
6 |
Odczyt znaku z klawiatury (bez kontroli Ctrl-Break) |
DL = 0FFH |
AL = znak, jeśli on jest = 0, gdy brak znaku |
7 |
Czekanie na znak z klawiatury, ale bez wyświetlania (bez kontroli Ctrl-Break) |
|
AL = znak |
8 |
Czekanie na znak z klawiatury, ale bez wyświetlania (z kontrolą Ctrl-Break) |
|
AL = znak |
A |
Wczytanie łańcucha z klawiatury do bufora (rozmiar bufora powinien być o 2 większy niż ilość bajtów, jakie maksymalnie chcemy tam wczytać, koniec wczytywania następuje w momencie naciśnięcia ENTER) |
DS:DX = adres bufora, ( ! ) - pierwszy bajt bufora określa jego rozmiar w bajtach nie licząc tego bajtu, a wliczając 2-gi bajt. |
Drugi bajt bufora określa ilość przeczytanych znaków |
B |
Odczyt stanu klawiatury |
|
AL = FFH - brak znaku, = 0 - znak dostępny |
C |
Wyczyszczenie bufora klawiatury i wywołanie funkcji obsługi klawiatury |
AL = numer funkcji obsługi (1, 6, 7, 8, A) |
W zależności od wywoływanej funkcji |
|
Czas i data |
|
|
2A |
Pobranie daty |
|
CX = rok DH = miesiąc DL = dzień |
2B |
Ustawienie daty |
CX = rok (1980 - 2099) DH = miesiąc DL = dzień |
AL = 0 - data poprawna = FF - data błędna |
2C |
Pobranie czasu |
|
CH = godzina CL = minuty DH = sekundy DL = setne sekundy |
2D |
Ustawienie czasu |
CH = godzina CL = minuty DH = sekundy DL = setne sekundy |
AL = 0 - czas poprawny = FF - czas błędny |
|
Komunikacja asynchroniczna |
|
|
3 |
odczytanie znaku z wejścia asynchronicznego |
|
AL = znak |
4 |
wysłanie znaku na wyjście asynchroniczne |
DL = znak |
|
|
Obsługa plików |
|
|
D |
Zapis zmienionych buforów dyskowych z powrotem na dyskietkę |
|
|
E |
Wybranie domyślnego napędu dysku |
DL = numer dysku (0 = A, 1 = B, 2 = C ...) |
AL = ilość dysków (2 dla jednego napędu) |
19 |
Pobierz numer domyślnego dysku |
|
AL = nr dysku (j. w.) |
2E |
Zmień stan wskaźnika weryfikacji |
DL=0 AL = 0 - wyłączenie = 1 - włączenie |
|
30 |
Pobranie numeru wersji MS-DOS |
|
AL = numer wersji AH = numer zmiany BX, CX = 0 |
|
Obsługa wektora przerwań |
|
|
25 |
Ustawienie wektora przerwań |
DS:DX = adres wektora AL = numer przerwania |
|
35 |
Pobranie wektora przerwań |
AL = numer przerwania |
ES:BX = adres wektora |
|
Rozszerzone funkcje obsługi plików i katalogów |
|
|
39 |
Utworzenie katalogu |
DS:DX = adres łańcucha z nazwą katalogu |
|
3A |
Usunięcie katalogu |
DS:DX = j. w. |
|
3B |
Zmiana katalogu roboczego |
DS:DX = adres katalogu do którego chcemy przejść |
|
47 |
Pobranie nazwy katalogu roboczego - bieżącego |
DS = nr napędu (0 = domyślny, 1 = A, 2 = B ...) DS:DX = adres 64-bajtowego bufora |
DS:SI = adres łańcucha docelowego |
36 |
Pobranie wolnego obszaru na dysku |
DL = nr napędu (j. w.) |
AX = ilość sektorów na klaster, = 0FFFFH w razie błędu BX = ilość wolnych klastrów CX = ilość bajtów na sektor DX = całkowita ilość klastrów |
3C |
Utworzenie pliku |
DS:DX = adres łańcucha z nazwą CX = atrybuty pliku (patrz pod tabelką) |
AH = uchwyt do pliku (patrz pod tabelką) |
3D |
Otwarcie pliku |
DS:DX = adres łańcucha z nazwą AL =0 - otwarcie do odczytu =1 - zapisu =2 - odczyt & zapis |
AH = uchwyt do pliku (patrz pod tabelką) |
3E |
Zamknięcie pliku |
BX = uchwyt pliku (patrz pod tabelką) |
|
3F |
Odczyt z pliku (urządzenia) |
BX = uchwyt pliku (patrz pod tabelką) CX = ilość czytanych bajtów DS:DX = adres bufora |
AX = ilość odczytanych bajtów = 0 odczyt poza plik |
40 |
Zapis do pliku (urządzenia) |
BX = uchwyt pliku (patrz pod tabelką) CX = ilość bajtów zapisu DS:DX = adres źródła czyt. |
AX = ilość zapisanych bajtów |
41 |
Usunięcie pliku |
DS:DX = adres łańcucha z nazwą |
|
43 |
Nadawanie atrybutów plikowi |
AL = 1 DS:DX = adres łańcucha z nazwą CX = atrybuty (patrz pod tabelką) |
|
54 |
Pobranie znacznika sprawdzania |
|
AL = 0 - brak zapisu sprawdzania = 1 - zapis ze sprawdzaniem |
56 |
Zmiana nazwy pliku |
DS:DX = adres łańcucha ze starą nazwą ES:DI = adres łańcucha z nową nazwą |
|
|
Zarządzanie procesami |
|
|
31 |
Zakończenie programu z pozostawieniem w pamięci |
AL = kod powrotu DX = rozmiar pamięci w paragrafach |
|
4B |
Załadowanie i wykonanie programu |
AL = 0 DS:DX = adres łańcucha nazwy programu ES:BX = adres bloku parametrów |
|
4C |
Zakończenie programu |
AL = kod powrotu dla ErrorLevel |
|
4D |
Pobranie kodu błędu dla wywołanego programu |
|
AL = kod powrotu dla procesu wołającego |
62 |
Pobranie adresu bloku PSP |
|
BX = adres segmentu PSP programu |
|
Zarządzanie pamięcią |
|
|
48 |
Zarezerwowanie pamięci systemowej (RAM) |
BX = ilość rezerwowanych paragrafów |
AX = adres segmentu z zarezerwowaną pamięcią |
49 |
Zwolnienie zarezerwowanej pamięci |
ES = adres segmentu ze zwalnianą pamięcią |
|
4A |
Zmiana wielkości zarezerwowanego bloku pamięci |
ES = adres segmentu obszaru pamięci |
|
|
Pobranie rozszerzonego kodu błędu |
|
|
59 |
Pobranie rozszerzonego kodu błędu |
BX = 0 |
AX = rozszerz. kod BH = klasa błędu BL = zalecana reakcja CH = położenie |
Atrybuty pliku określa bajt atrybutów. Znaczenie odpowiednich bitów:
0 - read only, 1 - hidden, 2 - system, 3 - czy etykieta dysku, 4 - czy nazwa podkatalogu, 5 - czy zwykły plik (nie backup)
Uchwyt pliku - numer identyfikujący otwarty plik lub urządzenie I/O. Istnieją następujące przypisania uchwytów:
0 - standardowe wejście (DOS przypisuje to klawiaturze, ale można zmienić)
1 - standardowe wyjście (domyślnie - ekran)
2 - standardowe wyjście strumienia błędów (niezmienialne)
3 - standardowe urządzenie dodatkowe (plik)
4 - drukarka
Standardowo do dyspozycji użytkownika pozostają dalsze 4 uchwyty, bo DOS rezerwuje 8 uchwytów. Można to jednak zwiększyć.
Przykładowe przerwania oferowane przez DOS / BIOS
( ! ) Często w ramach przerwania istnieje kilka osobnych procedur (usług), ich wybór dla przerwań BIOS ( i chyba wszystkich DOS) dokonuje się na podstawie zawartości rejestru AH
VIDEO (BIOS) - przerwanie 10H.
gotoxy AH = 02H, do BH - strona karty graficznej (zwykle = 0), DL - współrzędna „x”, DH - współrzędna „y', współrzędne ( ! ) liczone od 0
scrolling okna/ekranu (czyszczenie okna/ekranu) AH = 06H, CX - współrzędne lewego górnego rogu okna, DX - współrzędne prawego dolnego rogu okna, współrzędne liczone od 0, do starszych bajtów - „y”, do młodszych - „x”, do AL - ilość linii scrollingu, ( ! ) AL = 0 - czyszczenie ekranu.
odczyt typu karty AH = 1AH, AL = 0 (podusługa - odczyt konfiguracji, w AL zwraca identyfikator karty)
Konfiguracja (BIOS) - przerwanie 11H, zapisuje do AX słowo, w którym ustawienie poszczególnych bitów oznacza:
0 - istnieje FDD 1 - istnieje koprocesor 2 - zainstalowana mysz 3 - ilość RAM 4,5 - tryb video 6,7 - ilość FDD
9,10,11 - ilość portów szeregowych 12 - istnieje GamePort 13 - zainstalowany modem (wew.) 14,15 - il. druk.
Programy rezydentne
Program rezydentny różni się od zwykłego programu tylko funkcją, przekazując sterowanie do systemu operacyjnego. Program nie zostaje skasowany z pamięci, jego dane także tam pozostają i inne programy nie mogą z tej pamięci korzystać. Wykorzystywane to jest głównie, jeśli podmieniania się procedury obsługi przerwań. Wówczas, jeśli podmienimy przerwanie i opuścimy program, kod procedury podmieniającej musi zostać w pamięci w postaci programu TSR. Programy rezydentne mogą również wykonywać inne funkcje oprócz zmiany obsługi przerwań. Ich praca polega wówczas na oczekiwaniu na zdarzenie (przerwanie), które budzi taki program do pracy. W związku z tym program taki musi podmienić np. procedurę obsługi przerwania klawiatury, żeby zareagować na odpowiednią kombinację klawiszy uaktywniającą jego działanie itp.
Program można uczynić rezydentnym używając jednej z następujących funkcji:
KEEP - funkcja 31H przerwania 21H. Przed wywołaniem przerwania należy ustawić:
AH - 31H
AL - kod wyjścia, jaki program przekaże do procesu rodzica (np. do zmiennej ERRORLEVEL)
DX - rozmiar pamięci zatrzymywanej dla programu rezydentnego w 16-bajtowych paragrafach
Funkcja ta powoduje wyjście do procesu rodzica (np. shell'u DOS) zostawiając kod i dane procesu w pamięci. Gdy program otrzymuje kontrolê, funkcja EXEC (4bH) alokuje blok pamiêci zaczynajacy siê od PSP i zawieraj¹cy ca³¹ dostêpn¹ pamiêæ. Pamiêæ ta przydzielana jest procesowi. Wykorzystuj¹c funkcjê 31H DOS ogranicza pamięć przeznaczoną dla procesu rezydentnego do rozmiaru zapisanego w DX (wartość DX * 16 Bajtów). Funkcja ta nie zwalnia żadnych obszarów pamięci, które proces w między czasie alokował przy użyciu funkcji 48H.
TSR - przerwanie 27H. Należy ustawić:
DX - adres ostatniej komórki pamięci do zatrzymania jako część rezydentna, podany jako offset względem DS,
powiększony o 1.
Funkcja powoduje przekazanie sterowania do DOS, przy czym w pamięci pozostaje obszar pamięci o początku na początku PSP i długości zawartej w DX. Pamięć ta nie jest udostępniana innym procesom.
( ! ) Przerwanie 27H w momencie przekazywania sterowania do DOS przywraca wartości wektorów przerwań 22H-24H użycie tej funkcji do instalowania programów przechwytujących Critical Error lub Ctrl - Break jest niemożliwe.
( ! ) Przerwanie 27H nadaje się tylko do instalowania jako rezydentne programów o rozmiarze <= 64KB (co wynika z faktu, że rozmiar podawany jest w Bajtach w 16-bitowym rejestrze DX)
Dyrektywy zaawansowane
EVEN - wymusza parzysty adres wskaźnika asemblera, dane spod parzystych adresów odczytywane są szybciej program może wykonywać się szybciej.
<nazwa> GROUP <nazwa_segmentu1> [,<nazwa_segmentu2>, ...] - grupuje segmenty pod jedną nazwą <nazwa> - segmenty te zajmują jeden 64 kilobajtowy obszar.
IF?? <wyrażenie> ... [ ELSE ] ... ENDIF - dyrektywa pozwalająca na warunkowe poddawanie asemblacji pewnych części programu w zależności od <wyrażenie>. Asemblacja instrukcji przed ELSE zachodzi w zależności od rodzaju dyrektywy IF:
IFE - wyrażenie jest 0
IF - wyrażenie nie jest 0
IFDEF - gdy wyrażeniem jest zdefiniowany symbol
IFNDEF - gdy wyrażeniem nie jest zdefiniowany symbol
Operatory
Operatorów można używać w polu argumentu asemblera lub dyrektywy.
arytmetyczne
+, -, *, /, MOD - o składni <wartość1> <operator> <wartość2>,
SHL, SHR o składni <wartość1> <operator> <wyrażenie>
logiczne
AND, OR, XOR - o składni <wartość1> <operator> <wartość2>,
NOT - o składni NOT <wartość>
relacji
wszystkie posiadają składnię <argument1> <operator> <argument2>, zwracają prawdę gdy:
EQ - a1 = a2, NE - a1 != a2, LT - a1 < a2, GT - a1 > a2, LE - a1 <= a2, GE - a1 >= a2
przekazujące wartość
$ - zwraca bieżącą wartość wskaźnika asemblera - wartość wskaźnika, gdzie stoi $
SEG - zwraca wartość segmentu zmiennej lub etykiety stojącej za tym operatorem
OFFSET - zwraca wartość offsetu zmiennej lub etykiety
LENGTH - zwraca rozmiar (w bajtach lub słowach - w zależności od tego jak zmienna została zdefiniowana)
zmiennej zdefiniowanej przy użyciu DUP
TYPE - dla zmiennych stojących za TYPE zwraca: 1 - BYTE, 2 - WORD, 4 - DWORD . Dla etykiet zwraca: -1 -
NEAR, -2 - FAR.
SIZE - zwraca iloczyn LENGTH i TYPE
atrybutu
PTR - występuje ze składnią: <typ> PTR <wyrażenie>, gdzie <typ> (BYTE, WORD, ... , NEAR, FAR,) oznacza
jak ma być traktowane wyrażenie, jakiego ma być typu.
DS:, ES:, SS:, CS: - atrybuty zmiany segmentu
SHORT - składnia: JMP SHORT <etykieta> - informuje asembler, że <etykieta> leży nie dalej niż 127 bajtów od
instrukcji aktualnej.
THIS - zwraca adres bieżącego wskaźnika asemblera
HIGH - zwraca starszy bajt 16-bitowej liczby
LOW - zwraca młodszy bajt - // -
Koprocesor
( ! ) - Aby korzystać z koprocesora, należy podać dyrektywę .287 lub podobną.
Budowa
Koprocesor posiada 8, 80-bitowych rejestrów danych, 1 słowo stanu, 1 słowo kontrolne. Słowo stanu spełnia funkcje analogiczne do rejestru flag. Słowo kontrolne określa, w jaki sposób koprocesor ma wykonywać zaokrąglenie, jak ma traktować nieskończoność i ustala dokładność obliczeń. Rejestrów koprocesora używa się jako stosu koprocesora. Stos ten wykorzystywany jest w obliczeniach (dane do obliczeń pobierane ze stosu, wynik ląduje na stosie).
Typy danych
Liczby w rejestrach zapisywane są w postaci: 1 - bit znaku, 15 - bitów wykładnika, 64 - bity mantysy. Korzystając z takiej reprezentacji koprocesor może operować na liczbach całkowitych oraz zmiennopozycyjnych. Dostępne są następujące typy danych (z uwzględnieniem ilości dokładnych cyfr):
Word Integer - 16b, -32768, 32767, 5 cyfr
Short Integer - 32b, -2 * 10^9, 2 * 10^9, 9 cyfr
Long Integer - 64b , -2 * 10^18, 9 * 10^18, 18 cyfr
Short Real - 32b, 10^-37 , 10^38, 7 cyfr
Long Real - 64b, 10^-307, 10^308 , 16 cyfr
Temporary Real - 80b, 10^-4932, 10^4932, 19 cyfr
Packed Decimal - 80b, 18 cyfr dziesiętnych + znak, 18 cyfr
Instrukcje koprocesora
Instrukcja |
Opis |
Przesyłanie danych
FBLD <źródło> FBSTP <przeznaczenie> FILD <źródło> FIST <przeznaczenie> FISTP <przeznaczenie> FLD <źródło> FST <przeznaczenie> FSTP <przeznaczenie> FXCH <przeznaczenie>
|
przenoszą liczby na i ze szczytu stosu rejestrów danych koprocesora
ładuje spakowaną dziesiętnie zapamiętuje w <przeznaczeniu> spakowaną dziesiętnie i zdejmuje ze stosu ładuje całkowitą zapamiętuje w <przeznaczeniu> całkowitą ( ! - nie zdejmuje ze stosu) zapamiętuje w <przeznaczeniu> całkowitą i zdejmuje ze stosu ładuje rzeczywistą zapamiętaj rzeczywistą zapamiętaj rzeczywistą i usuń ze stosu wymień rejestr przeznaczenia z wierzchołkiem stosu |
Arytmetyczne
FADD [<przeznaczenie>, <źródło>] FADDP <przeznaczenie>, <źródło> FIADD źródło
FSUB [<przeznaczenie>, <źródło>] FSUBP <przeznaczenie>, <źródło> FISUB <źródło> FSUBR [<przeznaczenie>, <źródło>] FSUBRP <przeznaczenie>, <źródło> FISUBR <źródło>
FMUL [<przeznaczenie>, <źródło>] FMULP <przeznaczenie>, <źródło> FIMUL <źródło>
FDIV [<przeznaczenie>, <źródło>] FDIVP <przeznaczenie>, <źródło> FIDIV <źródło> FDIVR [<przeznaczenie>, <źródło>] FDIVRP <przeznaczenie>, <źródło> FIDIVR <źródło>
FSQRT FSCALE FPREM FRNDINT XTRACT
FABS FCHS
|
( ! ) - odejmowanie i dzielenie występuje w 2 wariantach, normalnym (odejmuje się źródło od przeznaczenia, dzieli przeznaczenie przez źródło) i odwróconym (odejmuje się przeznaczenie od źródła, dzieli źródło przez przeznaczenie)
dodaje rzeczywiste dodaje rzeczywiste i zdejmuje ze stosu dodaje całkowite
odejmuje rzeczywiste odejmuje rzeczywiste i zdejmuje ze stosu odejmuje całkowite odejmuje rzeczywiste (odwrócona) odejmuje rzeczywiste i zdejmuje ze stosu (odwrócona) odejmuje całkowite (odwrócona)
mnoży rzeczywiste mnoży rzeczywiste i zdejmuje ze stosu mnoży całkowite
dzieli rzeczywiste dzieli rzeczywiste i zdejmuje ze stosu dzieli całkowite dzieli rzeczywiste (odwrócona) dzieli rzeczywiste i zdejmuje ze stosu (odwrócona) dzieli całkowite (odwrócona)
pierwiastek kwadratowy skalowanie przez potęgę 2 częściowa reszta zaokrąglenie do całkowitej wydzielenie cechy i mantysy
Wartość bezwzględna Zmiana znaku
|
Porównania
FCOM [<źródło>] FCOMP [<źródło>] FCOMPP
FICOM <źródło> FICOMP <źródło>
FTST FXAM
|
porównują liczby ze szczytu stosu z inną liczbą przechowywaną na stosie lub w pamięci
porównuje rzeczywiste porównuje rzeczywiste i zdejmuje ze stosu porównuje rzeczywiste i 2 razy zdejmuje ze stosu
porównuje całkowite porównuje całkowite i zdejmuje ze stosu
sprawdza, czy szczyt stosu jest 0 bada szczyt stosu |
Przestępne
F2XM1 FYL2X FYL2XP1 FPTAN FPATAN
|
obliczają funkcje log i trygonometryczne.
oblicza 2^X - 1 oblicza Y * log_2 (X) oblicza Y * log_2 (X+ 1) częściowy tangens częściowy arcus tangens
|
Stałych
FLDZ FLD1 FLDPI FLDL2T FLDL2E FLDLG2 FLDLN2
|
umieszczają jedną ze stałych na szczycie stosu, liczby te mają pełną dokładność (Temporary Real)
załaduj 0.0 załaduj 1.0 załaduj PI załaduj log_2 (10) załaduj log_2 (e) załaduj log_10 (2) załaduj log_e (2) |
Sterujące
FLDCW <źródło> FSTCW/ FNSTCW <przeznaczenie> FSTSW / FNSTSW <przeznaczenie> FSTSW/ FNSTSW AX FSAVE/ FNSAVE <przeznaczenie> FRSTOR <źródło> FLDENV <źródło> FSETPM FSTENV / FNSTENV <przeznaczenie> FWAIT FINIT / FNINIT FENI / FNENI FDISI / FNDISI DCLEX / FNCLEX FINCSTP FDECSTP FFREE FNOP |
udostępniają informacje o stanie, możliwości zmiany spos. zaokrągleń wyników, umożliwiają i zabraniają przerwania itp.
załaduj słowo sterujące zapamiętaj słowo sterujące zapamiętaj słowo stanu zapamiętaj słowo stanu AX zachowaj stan odtwórz stan załaduj środowisko ustaw wirtualny tryb pracy zapamiętaj środowisko wait (zatrzymuje proc., zabezp. przed korzyst. z tych samych obszarów RAM) inicjuje (resetuje) koprocesor zezwala na przerwania blokuje reakcje przerwania zeruje wyjątki zwiększa wskaźnik stosu zmniejsza wskaźnik stosu zwalnia (zeruje) rejestry nic nie rób |
Niebieskie karty
Instrukcje asemblera opisuje się często za pomocą niebieskich kart. Zawierają one opis działania instrukcji, ich ograniczenia, uwagi, listę modyfikowanych znaczników oraz poprawne formy ich użycia. Aby uogólnić dostępne formy użycia stosuje się następujące symbole operandów:
r8 - 8-bitowa połówka rejestru
r16 - 16-bitowy rejestr ogólnego przeznaczenia
sr - jeden z rejestrów segmentowych
m8 - osiem bitów danych pamięciowych (bajt)
m16 - słowo pamięciowe
i8 - bajt natychmiastowy
i16 - słowo natychmiastowe
d8 - 8-bitowe przemieszczenie ze znakiem - odległość między aktualnym położeniem a miejscem w kodzie programu, w które chcemy skoczyć. Przemieszczenie dodatnie - skok w kierunku wyższego adresu. Jest to adres względem aktualnej instrukcji.
d16 - 16-bitowe przemieszczenie ze znakiem, przy skokach i instrukcjach wywołania podprogramów
Instrukcje
Rozkazy transmisji danych
( ! ) - Instrukcje transmisji nie modyfikują znaczników.
MOV <op1>, <op2> przenosi zawartość operandy 2 (lub tego, na co ona wskazuje) do operandy 1. Operandy mogą być dowolne z wyjątkiem pewnych ograniczeń:
( ! ) - nie może przenosić wartości z jednej kom. pamięci do drugiej
( ! ) - nie może przenosić wart. jednego rejestru segmentowego do drugiego
( ! ) - nie może przenosić danych natychmiastowych do rejestrów segmentowych
( ! ) - nie może działać na mieszanych operandach różnych wielkości (MOV 16bitów 8bitów)
( ! ) - nie potrafi operować na rejestrze znaczników
W przypadku, gdy operand 2 jest etykietą zmiennej, do operandu 1 przeniesiona zostanie wartość tej zmiennej.
LEA <op1>, <op2> przenosi efektywny adres operandu 2 do operandu 1 np. LEA DX, Zmienna
LDS <op1>, <op2> przenosi daleki adres <op2> do pary rejestrów DS:<op1>
LES <op1>, <op2> przenosi daleki adres <op2> do pary rejestrów ES:<op1>
LAHF przenosi do AH młodszy bajt rejestru znaczników
SAHF przenosi do młodszego bajtu rejestru znaczników zawartość rejestru AH
IN <op1> , <op2> przenosi do <op1> zawartość port[ <op2> ]
( ! ) operandy mogą stanowić tylko pary: AL i wartość 8-bitowa albo AX i DX
OUT <op1>, <op2> przenosi do port[ <op1> ] zawartość <op2>
( ! ) operandy mogą stanowić tylko pary: wartość 8-bitowa i AL albo DX i AX
XCHG <op1>, <op2> zamienia między sobą wartości wskazywane przez operandy.
XLAT zapisuje w AL bajt pamięci spod adresu DS:[BX+AL] , stosuje siê, gdy pod DS:BX jest tablica bajtów, a do AL załadowano indeks interesuj¹cego elementu tablicy
PUSHA (286) - odkłada na stos rejestry w kolejności: AX, CX, DX, BX, SP, BP, SI, DI.
( ! ) Wartość SP odłożona na stos - wartość przed wywołaniem PUSHA jeśli chce się zdejmować przez POP kolejne rejestry, ignorować zdjęcie SP, bo po zdjęciu SP będziemy nim wskazywać poniżej (w sensie stosu a nie adresów) rejestrów AX, CX, DX i BX i stracimy do nich dostęp
POPA (286) - ściąga ze stosu rejestry w kolejności odwrotnej do PUSHA, przy czym ignoruje zapis SP
PUSHAD i POPAD (386) - jak PUSHA i POPA, tylko dla rejestrów 32-bitowych
IN <op1>, <op2> - przenosi dane z portu do akumulatora. <op1> = AL - przenoszony jest bajt, = AX - przenoszone jest słowo, <op2> - numer portu - dana natychmiastowa lub zawartość rejestru DX.
OUT <op1>, <op2> - przenosi dane z akumulatora na port, <op1> - numer portu - dana natychmiastowa lub zawartość DX, <op2> - dana do przesyłu w AL lub AX.
Operacje arytmetyczne
( ! ) O ile nie jest to opisane, instrukcje arytmetyczne mogą modyfikować: OF, SF, ZF, AF, PF, CF
INC <op> zwiększa o 1 wartość operandu, ( ! ) nie zmienia CF.
DEC <op> zmniejsza o 1 wartość operandu, ( ! ) nie zmienia CF
NEG <op> neguje wartość operandu 5 -5
ADD <op1> <op2> dodaje <op2> do <op1>, dla bez i ze znakiem
SUB <op1> <op2> odejmuje <op2> od <op1>, dla bez i ze znakiem
MUL <op> mnożenie bez znaku, wynik umieszczany w AX lub jeśli jest większy od 65535 w parze DX i AX (młodsze bity w AX). <op> może być r8, wówczas drugi mnożnik ładuje się do AL (mnożenie 8-bitowe). <op> może być <r16> lub <m16>, wówczas drugi mnożnik do AX (dwa słowa).
( ! ) MUL modyfikuje AX i DX
( ! ) <op> NIE może być daną natychmiastową
IMUL <op> tak samo jak MUL, ale dla liczb ze znakiem, wynik trafia do AX jeśli nie jest większy od 32767
DIV <op> dzieli liczbę bez znaku znajdującą się w AX przez <op>, wynik trafia do AL, a do AH trafia modulo. Możliwe jest także dzielenie 16-bitowe dzielna znajduje się w DX - AX, wynik trafia do AX, modulo do DX
IDIV <op> jak DIV, tylko dla liczb ze znakiem
ADC <op1> <op2> dodaje <op2> do <op1> wykorzystując także CF,
SBB <op1> <op2> odejmuje <op2> od <op1> korzystając z pożyczki w CF
CBW jeśli bit znaku jest w AL ustawiony wypełnia jedynkami AH, w przeciwnym wypadku wypełnia go zerami
CWD jeśli bit znaku w AX ustawiony wypełnia jedynkami DX, w przeciwnym wypadku wypełnia go zerami
AAA wprowadza poprawkę korygującą wynik dodawania dla BCD (działa na AX). W BCD w ramach bajtu zajęte są tylko 4 najmłodsze bity (4 starsze są ignorowane), przy czym dozwolone jest zapisanie na nich cyfr 0-9. AAA działa następująco: koryguje zawartość 4 młodszych bitów AL, - jeśli zawierają liczbę >9 dodaje 6 do AL., zeruje 4 starsze bity, ustawia znaczniki CF i AF, ponieważ zawartość AH została zwiększona. Jeśli w AL. była liczba <= 9 CF i AF są zerowane.
AAD poprawia wynik odejmowania dla BCD, działa na AX
AAM poprawia wynik mnożenia dla BCD, działa na AX
AAS poprawia wynik dzielenia dla BCD, działa na AX
( ! ) Znaki ASCII odpowiadające cyfrom są tak zdefiniowane, że jeśli bajt odpowiadający kodowi ASCII danego znaku potraktować jak liczbę BCD wówczas ma ona wartość symbolizowaną przez ten znak.
DAA korygowanie wyniku dodawania dla liczb traktowanych jako upakowane BCD, gdzie każdy bajt reprezentuje 2-cyfrową liczbę dziesiętną, wykorzystuje się po 4 bity na każdą cyfrę
DAS korygowanie wyniku odejmowania dla liczb traktowanych jako upakowane BCD
Operacje logiczne
( ! ) Nie można ich wykonywać na rejestrach segmentowych najpierw skopiować zawartość rejestru seg. do innego
( ! ) Działają na bajtach i na słowach
AND <op1>, <op2> dokonuje logicznego and na każdej z pary bitów pochodzących po jednym z <op1> i <op2>, wynik umieszcza w <op1>.
<opi> - może być 8 lub 16-bitowe.
OR <op1>, <op2> jak w AND, tylko dla logicznego or.
XOR <op1>, <op2> jak w AND, tylko dla logicznego xor.
NOT <op> neguje wszystkie bity <op>, wynik w <op>
( ! ) nie modyfikuje ¿adnych znaczników (nawet ZF)
Wydzielanie bitu - za pomocą maski (w której badany bit ustawiony jest na 1) i instrukcji AND między maską a badanym bajtem (słowem)
Ustawianie bitu - tworzy się maskę, w której bity do ustawienia mają wart. „1”, a następnie OR między maską a bajtem / słowem w którym chcemy ustawić bity
Zerowanie bitu - stworzyć maskę, w której bity do zerowania ustawiamy na „1”, robimy NOT na bajcie (słowie) w którym chcemy zerować bity, OR między maską i zanegowanym bajtem (słowem), wynik negujemy (NOT)
Szybkie zerowanie danej - użycie instrukcji XOR na tej samej danej np. XOR AX, AX zeruje AX.
Instrukcje przesuwania
SHL <op>, <licznik> przesuwa bity <op> w lewo, uzupe³nia 0-wym bitem, <op> - pamiêæ lub rejestr, <licznik> - liczba „1” lub rejestr CL (nie CX), w CL jest iloœæ, o jaki¹ nale¿y przesun¹æ bity, ewentualnie przesuniêcie o „1”.
( ! ) wysuwane bity trafiaj¹ do CF (a z niego donik¹d)
( ! ) przesuniêcie 16-krotne = zerowanie s³owa
(286) - <licznik> może być dowolną wartością natychmiastową np. SHL AX, 7
(286) - wartość przesunięcia <licznik> ograniczono do 31 (bo np. 32 daje to samo co 0)
SAL <op>, <licznik> działa tak samo jak SHL
SHR <op>, <licznik> jak SHL, tylko w prawo i także uzupełnia zerami (też 286).
SAR <op>, <licznik> jak SHR, ale uzupe³nia najstarszym bitem (bitem znaku)
ROL <op>, <licznik> rotuje bity <op> w lewo, wysuniêty bit trafia na koniec oraz do CF (też 286)
ROR <op>, <licznik> jak ROL, tylko w prawo, wysuniêty bit trafia na pocz¹tek oraz do CF (też 286)
RCL <op>, <licznik> rotuje bity <op> w lewo, wysuniêty bit trafia do CF a bit z CF na koniec(też 286)
RCR <op>, <licznik> jak ROL, tylko w prawo, wysuniêty bit trafia do CF, a bit z CF na pocz¹tek (też 286)
Szybciej jest zapisaæ kilka razy SH? <op>, 1 , niż raz przez załadowanie do CL liczby przesunięć (do 4 razy włącznie)
Szybkie mnożenie i dzielenie - w przypadkach mnożenia i dzielenia przez potęgę dwójki lepiej jest używać przesuwania niż instrukcji mnożenia i dzielenia. (zwykłe MUL trwa 77 cykli)
( ! ) - lepiej rezerwowaæ trochę miejsca „na wyrost” przy definiowaniu wielkości elementu tablicy ( n ), aby była ona wielokrotnością 2, ponieważ szukając przesunięcia i-tego elementu musimy mnożyć i * n (co szybko wykonamy za pomocą SHL)
Instrukcje skoku i pętli
JMP <etykieta> --> [Author:brak] skok do <etykieta> (<etykieta> - wczeœniej zdefiniowana etykieta lub konkretna wartość liczbowa - segment:offset), skok jest bezwarunkowy.
( ! ) możliwy jest skok do dalekiej etykiety (do pełnego 32-bitowego adresu)
J? <etykieta> skok do <etykiety> w zależności od `?' i stanu znaczników (skok warunkowy)
( ! ) Instrukcje skoku warunkowego umożliwiają skoki bliskie - do adresu położonego o [-128, 127] od aktualnego
( ! ) jeżeli zachodzi potrzeba wykonania skoku warunkowego dalekiego, wykonuje się wartunkowy skok bliski do instrukcji
dokonującej bezwarunkowego skoku dalekiego
( ! ) Dla każdej instrukcji skoku warunkowego istnieje instrukcja wykonująca skok, gdy jest spełniony przeciwny warunek
( ! ) każda instrukcja ma synonim (np. większy = mniejszy lub równy)
( ! ) Istnieją osobne wersje instrukcji dla liczb signed i unsigned.
Liczby ze znakiem - najstarszy bit = 1, gdy liczba ujemna
Relacje między liczbami:
mniejszy / większy (less / greater) liczby ze znakiem (signed)
ponad / poniżej (above / below) liczby bez znaku
Relacje po wykonaniu CMP:
Relacja |
dla: bez znaku |
Skocz jeśli (co testuje) |
dla: ze znakiem |
Skocz jeśli (co testuje) |
= |
JE |
ZF = 1 |
JE |
ZF = 1 |
< > |
JNE |
ZF = 0 |
JNE |
ZF = 0 |
> |
JA JNBE |
CF = 0 i ZF = 0 |
JG JNLE |
ZF = 0 lub SF = OF |
< |
JB JNAE |
CF = 1 |
JL JNGE |
SF < > OF |
>= |
JAE JNB |
CF = 0 |
JGE JNL |
SF = OF |
<= |
JBE JNA |
CF = 1 lub ZF = 1 |
JLE JNG |
ZF = 1 i SF < > OF |
Instrukcje skoku warunkowego testujące pojedyncze flagi:
mnemonik |
działanie |
co testuje |
JNS |
skok, gdy brak znaku |
SF = 0 |
JS |
skok, gdy jest znak |
SF = 1 |
JNO |
skok, gdy nie ma przepełnienia |
OF = 0 |
JO |
skok, gdy jest przepełnienie |
OF = 1 |
JP/JPE |
skok, gdy jest parzystość |
PF = 1 |
JNP/JPO |
skok, gdy nie ma parzystości |
PF = 0 |
JCXZ <etykieta> skok do <etykieta>, skacze gdy CX = 0 (nie sprawdza żadnego znacznika)
( ! ) tylko skoki bliskie [-128 - 127]
( ! ) nie istnieje instrukcja przeciwna JCXNZ - skacząca dopóty, dopóki CX <>0.
( ! ) nie pomylić się JCXZ działa jakby odwrotnie niż LOOP, tzn. NIE skacze, gdy CX <> 0 nadaje się jako instrukcja wyjścia z pętli nieskończonej - do skoków poza pętlę, gdy licznik CX osiąga 0.
CMP <op1>, <op2> - porównuje operandy i ustawia odpowiednie znaczniki, w zależności od relacji między operandami (<, <=, =, >=, > ). Działa jakby wykonywała: wynik = <op1> - <op2>, zapomina wynik a tylko ustawia znaczniki.
( ! ) gdy chcemy użyć instrukcji skoku np. J?, w zależności od relacji między dwoma argumentami, należy najpierw wykonać CMP na tych argumentach, a dopiero potem użyć J?, bo J? skaczą (lub nie) tylko na podstawie zawartości flag.
TEST <op>, <maska> - testuje wyszczególnione maską bity <op>, dokonując operacji AND, wynik „gubi”, jedynie ustawia znaczniki.
( ! ) TEST modyfikuje ZF potrafi jedynie sprawdzić, czy dany bit jest ustawiony, gdy chodzi o sprawdzenie, czy są zerami, należy najpierw użyć NOT <op>
BT <op>, <nr> (386) - sprawdza wartość bitu o numerze <nr> (od 0) w operandzie <op>, bit ten jest kopiowany do CF.
( ! ) TEST jest szybsze, ale tylko dla sprawdzania, czy bit jest `1' .
LOOP <etykieta> - instrukcja pętli, działa następująco:
zmniejsza CX o 1
jeśli CX<>0 skacze do <etykieta>
jeśli CX=0 przerywa i wykonuje się następna instrukcja pod LOOP
LOOPNZ / LOOPNE <etykieta> - instrukcja pętli podobna do LOOP, działa następująco:
zmniejsza CX o 1
jeśli CX<>0 i ZF = 0 skacze do <etykieta>
jeśli CX=0 lub ZF = 1 przerywa i wykonuje się następna instrukcja pod LOOP
LOOPZ / LOOPE <etykieta> - instrukcja pętli podobna do LOOP, działa następująco:
zmniejsza CX o 1
jeśli CX<>0 i ZF = 1 skacze do <etykieta>
jeśli CX=0 lub ZF = 0 przerywa i wykonuje się następna instrukcja pod LOOP
( ! ) Instrukcje LOOP? stosuje się, gdy oprócz wyjścia z pętli po wykonaniu zadanej ilości razy istnieje potrzeba wyjścia, gdy jakaś zmienna (rejestr) będzie równa / różna wzorcowi (stała, zmienna, rejestr). Wówczas ostatnią linią przed wywołaniem LOOP? jest instrukcja CMP porównująca odpowiednią zmienną ze wzorcem.
Instrukcje łańcuchowe
Instrukcje łańcuchowe korzystają z następujących założeń:
( ! ) łańcuch źródłowy wskazywany jest przez DS:SI (suorce index)
( ! ) łańcuch docelowy wskazywany jest przez ES:DI (destination index)
( ! ) długość obu łańcuchów określona jest w CX
( ! ) dane przechodzą przez rejestr AX (będzie on modyfikowany)
( ! ) kierunek kopiowania (modyfikowania SI i DI) zależny jest od flagi DF, gdy DF=0 adresy te są zwiększane, gdy DF=1 adresy są zmniejszane.
REP STOS{W/B} - tworzy łańcuch docelowy pod ES:DI (o długości 2*CX bajtów) wypełnia go CX razy słowem zawartym w AX (bajtem z AL). ( ! ) Przy każdym odczycie słowa z pamięci CX jest zmniejszany o 1, aż zostanie wyzerowany. Wówczas instrukcja kończy działanie z wskaźnikiem ES:DI ustawionym na pierwszym słowie (bajcie) za stworzonym łańcuchem.
( ! ) Gdy DF = 1 - kopiowanie słów (bajtów) zaczyna się od adresu ES:DI, przy czym za każdym razem adres DI zostaje zmniejszony o 2 (1) bajty, i tam kopiowane zostaje następne słowo („w tył”). Na końcu DI wskazuje początek łańcucha.
STOS{W/B} - kopiuje słowo z AX (bajt z AL) pod ES:DI, DI zwiększane (zmniejszane dla DF = 1) jest o 2, nie modyfikuje CX
REP MOVS{W/B} - odpowiednik instrukcji REP STOSW (bez REP jak STOSW), różnica polega na tym, że dane pobierane są spod adresu DS:SI i kopiowane słowo po słowie lub bajt po bajcie (przy udziale AX / AL) pod adres ES:DI
LODS{W/B} kopiuje spod DS:[SI] słowo / bajt i zapisuje je w AX / AL. Na koniec zwiększa SI o słowo / bajt.
SCAS{W/B} porównuje (odejmuje ustawiając tylko znaczniki) słowo / bajt spod ES:[DI] z zawartością AX / AL. Następnie zwiększa DI.
CMPS{W/B} porównuje słowa / bajty spod DS:[SI] oraz ES:[DI], w zależności od DF zwiększa lub zmniejsza SI i DI.
REP przedrostek powtarzający dla MOVS, STOS, jeśli CX <>0 wykonuje odpowiednią instrukcję łańcuchową, dekrementuje CX oraz zmniejsza o 1 zawartość IP (w ten sposób następną instrukcją będzie znów REP)
REPE / REPZ przedrostek dla SCAS, CMPS, gdy CX<>0 wykonuje odpowiednią instrukcję łańcuchową, dekrementuje CX, jeśli ZF = 1 wówczas powtarza pętlę (zmniejsza IP o 1)
REPNE / REPNZ przedrostek dla SCAS, CMPS, gdy CX<>0 wykonuje odpowiednią instrukcję łańcuchową, dekrementuje CX, jeśli ZF = 0 wówczas powtarza pętlę (zmniejsza IP o 1)
INS{W/B} w DX znajduje się numer portu, przesyła zawartość portu do łańcucha przeznaczenia
OUTS{B/W} w DX numer portu, przesyła na port dane z łańcucha źródłowego
Instrukcje synchronizacji zewnętrznej
NOP - nie robi nic, zapycha miejsce w kodzie programu, wprowadza drobne opóźnienia
HLT - zatrzymuje wykonywanie instrukcji, może być tylko odblokowane przez przerwanie niemaskowalne
WAIT - zatrzymanie, odblokowane na skutek spełnienia warunku na linii wejściowej procesora TEST - musi zajść zdarzenie związane z urządzeniem zewnętrznym
ASSEMBLER strona 1
SS + rozmiar
SS:SP
SS:0