1
Programowanie komputera
Program jest algorytmem przetwarzania danych zapisanym w sposób zrozumiały dla komputera. Procesor rozumie wyłącznie rozkazy zapisane w kodzie maszynowym (ciąg 0 i 1).
Ponieważ jest to zapis bardzo trudny do zrozumienia dla człowieka już na początku ery komputerów stworzono język symboliczny, w którym rozkazom procesora, rejestrom, miejscom w pamięci przyporządkowano nazwy symboliczne. Instrukcja pisana przez programistę nie wyglądała już tak:
001 01 10010 lecz np. tak:
add A, wzrost
Był to zapis bardziej zrozumiały dla człowieka lecz instrukcje te nie są bezpośrednio zrozumiałe dla procesora – on ostatecznie rozumie tylko ciągi zer i jedynek. Program zapisany w języku asembler musi być przetłumaczony na kod maszynowy, musi przy tym spełniać ścisłe reguły składni danego języka.
Program napisany w języku asembler jest wciąż bliższy konstrukcji maszyny (odzwierciedla jej konstrukcję, zasoby sprzętowe, listę rozkazów) niż sposobowi myślenia o problemach przez człowieka i sposobami wyrażania rozwiązań tych problemów. Stąd obserwujemy gwałtowny rozwój nowych języków programowania tzw. wysokopoziomowych, dedykowanych do rozwiązywania określonych klas problemów. Programy napisane w takich językach przed wykonaniem przez procesor muszą być tłumaczone (kompilowane lub interpretowane) na język maszynowy.
Pisanie programów w języku asembler pozwala bliżej zapoznać się z działaniem procesora, poznać jego możliwości i ograniczenia. Czasami wymagane jest połączenie programu napisanego w języku wysokopoziomowym z funkcją napisaną w języku asembler aby spełnić krytyczne wymagania dotyczące wykorzystania zasobów komputera lub szybkości realizacji programu.
Struktura programu typu program.com
; program według modelu tiny
name "mycode" ; nazwa pliku wyjściowego (maksymalnie 8 znaków) org 100h ; początek programu od adresu IP = 100h
; kod programu
ret ; koniec programu i powrót do systemu operacyjnego.
msg db "press any key..." ; deklaracje zmiennych
2
Struktura programy typu program.exe
; program wykonywalny wielosegmentowy.
data segment
; tutaj umieszczamy deklaracje danych ends
stack segment
dw 128 dup(0) ; określenie wielkości stosu ends
code segment
start: ; początek programu
; ustawienie wartości rejestrów segmentowych:
mov ax, data mov ds, ax mov es, ax
; kod programu
mov ax, 4c00h ; zakończenie programu i powrót do system operacyjnego.
int 21h ends
end start ; ustawienie punktu startu programu i koniec instrukcji w assemblerze.
Program w assemblerze składa się z dyrektyw kompilatora, instrukcji, deklaracji zmiennych i stałych.
Dyrektywa kompilatora nie jest tłumaczona na kod wynikowy – określa tylko jak ma przebiegać kompilacja i jak zarządzać kodem wynikowym. Dyrektywa ORG 100h w programach typu .com określa, że instrukcje programu powinny zaczynać się od adresu 100h w segmencie kodu. Pierwsze 256 B (100h) zarezerwowane są dla potrzeb systemu operacyjnego w celu komunikacji z programem ( np. przekazanie parametrów uruchomienia programu). Dyrektywy mogą sterować warunkową kompilacją programu, definiować makroinstrukcje i in.
Zmienne określają miejsce przechowywania danych i ich typ. Dane te mogą znajdować się w rejestrach procesora, ale ze względu na ograniczoną liczbę rejestrów, miejscem ich przechowywania jest głównie pamięć operacyjne. Procesor korzysta z pamięci operacyjnej podając adres komórki z którą chce się skontaktować. Adresy są długimi liczbami binarnymi (np. 20 bitów) i ze względów praktycznych podczas deklaracji zmiennych adresom komórek przyporządkowuje się nazwy symboliczne. Określa się również typ zmiennych, który
3 decyduje o tym jak dużo miejsca zajmuje zmienna w pamięci oraz jakie operacje można na niej wykonywać. Będziemy korzystać ze zmiennych o długości 1 bajtu i 1 słowa (2 bajty).
nazwa DB wartość ; deklaracja zmiennej o długości 1B nazwa DW wartość ; deklaracja zmiennej o długości 2B
nazwa jest dowolnym ciągiem liter i cyfr zaczynającym się od litery, duże i małe litery nie są rozróżniane. Można zadeklarować zmienną bez podania nazwy lecz tracimy możliwość bezpośredniego odwołania się do zmiennej za pośrednictwem nazwy.
Wartość początkowa zmiennej może być określona w systemie binarnym, dziesiętnym, szesnastkowym, w postaci kodu ASCII. Można też nie podawać wartości początkowej (zmienne nie zainicjalizowana) pisząc znak ?.
Przykłady:
Zm1 DB 8 ; zapis dziesiętny
Zm2 DW 1234h, 0A5A5h ; zapis szesnastkowy Zm3 DB 01011100b ; zapis binarny
Zm4 DB ‘A’ ; kod ASCII
Zm5 DB ‘Witaj’, 0 ; ciąg znaków ASCII zakończony 0
Zm6 DW ? ; zmienna bez podanej wartości początkowej Zmienna typu WORD zajmuje dwa bajty. Stosowana jest zasada, że młodszy bajt zapisywany jest pod niższym adresem w pamięci. Poniższe dwie deklaracje są równoważne:
Zm DW 1234h Zm DB 34h, 12h
Wartość szesnastkowa rozpoczynająca się znakiem A..F musi być poprzedzona 0.
Można również oprócz pojedynczych zmiennych deklarować tablice. Tablicą nazywamy zmienną złożoną z określonej liczby elementów jednakowego typu. Dostęp do elementów tablicy uzyskujemy korzystając z indeksu elementu przechowywanego w rejestrze indeksowym. Jeśli wartości elementów tablicy się powtarzają możemy skorzystać z operatora DUP.
Przykłady:
Tab1 DB 1, 2, 3, 4, 5 ; tablica 5-elementowa zawiera liczby 1-5 Tab2 DB ‘Hello’ , 0 ; tablica zawiera napis zakończony 0 Tab3 DB 48h, 65h, 6Ch, 6Fh, 00h ; zapis równoważny Tab2
Tab4 DB 10 DUP (?) ; tablica 10-elementowa nie zainicjalizowana Tab5 DB 10 DUP (0) ; tablica 10-elementowa o wartościach 0 Tab6 DB 5 DUP (0,1) ; tablica 10-elementowa o wartościach 0,1,0,1 … Uwaga: DW nie może być używane przy deklaracji napisów.
Możemy dokonywać podglądu zmiennych podczas pracy symulatora otwierając okienko variables (przycisk vars).
4 Możliwy jest wybór typu zmiennej (byte, word, dword, qword, tword), ilości elementów zmiennej, sposobu interpretacji wyświetlanych danych (hex, bin, octal, signed, unsigned, ASCII). Dostępna jest również edycja zawartości pamięci danych podczas pracy symulatora.
Rodzaje instrukcji wykonywanych przez mikroprocesor Instrukcje transferu (przesłania danych)
Instrukcje obliczeniowe - arytmetyczne
- logiczne
- operacji na bitach - przesunięć i rotacji Instrukcje sterujące - skoki
- skoki warunkowe - pętle
- procedury
- operacje na rejestrach specjalnych
5
Instrukcje Arytmetyczne i Logiczne
Instrukcje arytmetyczne i logiczne działają na rejestr statusowy (rejestr znaczników –ang. Flags)
Rejestr ten ma 16 bitów, każdy nazywany jest znacznikiem (flag) i może przyjmować wartości 1 lub 0.
Znacznik przeniesienia - Carry Flag (CF) – ustawiany jest na 1 kiedy wystąpi przeniesienie dla liczb bez znaku, np. kiedy dla argumentów 8-bitowych dodamy 255 + 1. Gdy nie ma przeniesienia bit ten jest ustawiony na 0.
Znacznik zera - Zero Flag (ZF) – ustawiany na 1 kiedy wynik jest zero. Dla wartości różnej od zera bit ten ustawiany jest na 0.
Znacznik znaku - Sign Flag (SF) – ustawiany na 1 gdy wynik jest ujemny. Gdy wynik jest dodatni – ustawiany jest na 0. Znacznik ten przyjmuje wartość najbardziej znaczącego bitu wyniku.
Znacznik nadmiaru - Overflow Flag (OF) – ustawiany na 1 kiedy wystąpi przepełnienie dla liczb ze znakiem, np. kiedy dodamy 100 + 50 (wynik nie jest w przedziale -128...127).
Znacznik parzystości - Parity Flag (PF) – ustawiany jest na 1 kiedy wynik posiada parzystą liczbę jedynek, a na 0 kiedy liczba bitów o wartości jeden w wyniku jest nieparzysta. Uwaga:
analizowane jest tylko 8 bitów wyniku nawet gdy wynik jest 16-bitowy!
Znacznik przeniesienia pomocniczego - Auxiliary Flag (AF) – ustawiany na 1 kiedy wystąpi przeniesienie z bitu 3 na 4 (nibble-4 bity).
Znacznik zezwolenia na przerwania maskowalne - Interrupt enable Flag (IF) – kiedy ustawiony jest na 1 CPU przyjmuje przerwania od urządzeń zewnętrznych.
Znacznik kierunku - Direction Flag (DF) – znacznik ten wykorzystywany przez instrukcje przetwarzające łańcuchy znaków, kiedy znacznik ustawiony jest na 0 znaki przetwarzane są do przodu, kiedy ma wartość 1 – kierunek przetwarzania jest odwrotny.
Poznamy trzy grupy instrukcji arytmetycznych i logicznych.
Grupa pierwsza: ADD, SUB, CMP, AND, TEST, OR, XOR Wykorzystywane są następujące rodzaje argumentów:
6 REG, memory
memory, REG REG, REG
memory, immediate REG, immediate
REG:AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory: [BX], [BX+SI+7], zmienna, itd...
immediate: 5, -24, 3Fh, 10001101b, itd...
Po wykonaniu operacji wynik przechowywany jest w pierwszym argumencie. Rozkazy CMP i TEST ustawiają tylko znaczniki nie zapisując wyniku operacji (wykorzystywane są do podejmowania decyzji podczas wykonywania program – skoki warunkowe i wywołanie podprogramów).
Instrukcje te działają tylko na następujące znaczniki: CF, ZF, SF, OF, PF, AF.
ADD – dodaje drugi operand do pierwszego.
SUB - odejmuje drugi operand od pierwszego.
CMP - odejmuje drugi operand do pierwszego lecz nie zapisuje wyniku odejmowania, ustawia tylko znaczniki.
AND – iloczyn logiczny pomiędzy parami bitów dwóch operandów. Stosowane są zależności:
1 AND 1 = 1 1 AND 0 = 0 0 AND 1 = 0 0 AND 0 = 0
Jak widzimy wynik jest równy 1 tylko wtedy, gdy oba bity są 1.
TEST – działa tak samo jak AND ale ustawia tylko znaczniki.
OR - suma logiczna pomiędzy parami bitów dwóch operandów. Stosowane są zależności:
1 OR 1 = 1 1 OR 0 = 1 0 OR 1 = 1 0 OR 0 = 0
Jak widzimy wynik jest równy 1 jeśli przynajmniej jeden bit argumentów jest równy 1.
XOR – Suma rozłączna (eXclusive OR) pomiędzy parami bitów dwóch operandów. Stosowane są zależności:
1 XOR 1 = 0 1 XOR 0 = 1 0 XOR 1 = 1 0 XOR 0 = 0
Jak widzimy wynik jest równy 1 kiedy bity argumentów różnią się.
7 Druga grupa: MUL, IMUL, DIV, IDIV
Wykorzystywane są następujące rodzaje argumentów:
REG memory
REG:AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory: [BX], [BX+SI+7], zmienna, itd.
Instrukcje MUL i IMUL działają tylko na znaczniki: CF, OF
Kiedy wynik przekracza zakres operandów znaczniki ustawiane są na 1, w przeciwnym przypadku na 0.
Dla instrukcji DIV i IDIV znaczniki są niezdefiniowane.
MUL – mnożenie liczb bez znaku:
kiedy operand jest typu byte: AX = AL * operand.
kiedy operand jest typu word: (DX AX) = AX * operand.
IMUL – mnożenie liczb ze znakiem:
kiedy operand jest typu byte: AX = AL * operand.
kiedy operand jest typu word: (DX AX) = AX * operand.
DIV – dzielenie liczb bez znaku:
kiedy operand jest typu byte: AL = AX / operand
AH = reszta z dzielenia (moduł) kiedy operand jest typu word: AX = (DX AX) / operand
DX = reszta z dzielenia (moduł)
IDIV – dzielenie liczb ze znakiem:
kiedy operand jest typu byte: AL = AX / operand
AH = reszta z dzielenia (moduł).
kiedy operand jest typu word: AX = (DX AX) / operand
DX = reszta z dzielenia (moduł).
8 Trzecia grupa: INC, DEC, NOT, NEG
Wykorzystywane są następujące rodzaje argumentów:
REG memory
REG:AX, BX, CX, DX, AH, AL, BL, BH, CH, CL, DH, DL, DI, SI, BP, SP.
memory: [BX], [BX+SI+7], zmienna, itd.
INC, DEC Instrukcje te działają tylko na następujące znaczniki:
ZF, SF, OF, PF, AF.
NOT Instrukcja nie zmienia żadnego znacznika!
NEG Instrukcje ta działa tylko na następujące znaczniki:
CF, ZF, SF, OF, PF, AF.
NOT – Neguje każdy bit operandu.
NEG – Zamienia liczbę na ujemną (w systemie U2). Zamienia każdy bit argumentu na przeciwny i dodaje do wyniku 1.
9
Sterowanie pracą programu
Umożliwia podejmowanie decyzji w oparciu o określone warunki.
Skoki bezwarunkowe
Skok bezwarunkowy polega na wpisaniu nowej wartości do licznika rozkazów po napotkaniu instrukcji skoku. Podstawową instrukcją umożliwiającą przeniesienie sterowania do innego punktu programu oznaczonego etykietą jest JMP o następującej składni:
JMP etykieta
Ażeby zadeklarować etykietę w programie podajemy jej nazwę, a po niej dwukropek ":". Oto przykłady prawidłowych nazw etykiet:
label1:
label2:
a:
Etykieta może być zadeklarowana w osobnej linii lub przed dowolną instrukcją, np.:
x1:
mov AX, 1
x2: mov AX, 2
Przykłady wykorzystania instrukcji JMP:
org 100h
mov ax, 5 mov bx, 2 jmp calc back: jmp stop calc:
add ax, bx jmp back stop:
ret
Instrukcja JMP pozwala przenieść sterowanie do przodu lub do tyłu, w dowolne miejsce wewnątrz segmentu.
10
Skoki warunkowe bliskie
Warunkiem wykonania skoku jest wartość wybranego znacznika. Wartość znaczników w rejestrze stanu jest ustalana w wyniku ostatniej operacji arytmetycznej lub logicznej (nie są zmieniane np.
podczas przesyłania danych). Pozwalają przenieść sterowanie przy spełnieniu określonych warunków.
Podzielone są na trzy grupy: pierwsza testuje pojedyncze znaczniki, druga testuje liczby ze znakiem, a trzecia – liczby bez znaku.
Instrukcje skoku warunkowego testujące pojedyncze znaczniki
Instrukcja Opis Warunek Instrukcja przeciwna
JZ , JE Jump if Zero (Equal). ZF = 1 JNZ, JNE
JC , JB, JNAE Jump if Carry (Below, Not Above Equal). CF = 1 JNC, JNB, JAE
JS Jump if Sign. SF = 1 JNS
JO Jump if Overflow. OF = 1 JNO
JPE, JP Jump if Parity Even. PF = 1 JPO
JNZ , JNE Jump if Not Zero (Not Equal). ZF = 0 JZ, JE
JNC , JNB, JAE Jump if Not Carry (Not Below, Above Equal). CF = 0 JC, JB, JNAE
JNS Jump if Not Sign. SF = 0 JS
JNO Jump if Not Overflow. OF = 0 JO
JPO, JNP Jump if Parity Odd (No Parity). PF = 0 JPE, JP
Można zauważyć, że niektóre instrukcje o różnych nazwach działają tak samo, są one kodowane za pomocą tych samych kodów maszynowych.
Instrukcje te są dwubajtowe – pierwszy bajt zawiera kod rozkazu, a drugi określa zakres skoku od - 128 do +127 bajtów).
11 Instrukcje skoku warunkowego dla liczb ze znakiem
Instrukcja Opis Warunek Instrukcja przeciwna
JE , JZ
Jump if Equal (=).
Jump if Zero. ZF = 1 JNE, JNZ
JNE , JNZ Jump if Not Equal (<>).
Jump if Not Zero. ZF = 0 JE, JZ
JG , JNLE Jump if Greater (>).
Jump if Not Less or Equal (not <=).
ZF = 0 and SF = OF
JNG, JLE
JL , JNGE Jump if Less (<).
Jump if Not Greater or Equal (not >=). SF <> OF JNL, JGE
JGE , JNL Jump if Greater or Equal (>=).
Jump if Not Less (not <). SF = OF JNGE, JL
JLE , JNG Jump if Less or Equal (<=).
Jump if Not Greater (not >).
ZF = 1 or SF <> OF
JNLE, JG
Instrukcje skoku warunkowego dla liczb bez znaku
Instrukcja Opis Warunek Instrukcja przeciwna
JE , JZ
Jump if Equal (=).
Jump if Zero. ZF = 1 JNE, JNZ
JNE , JNZ Jump if Not Equal (<>).
Jump if Not Zero. ZF = 0 JE, JZ
JA , JNBE Jump if Above (>).
Jump if Not Below or Equal (not <=).
CF = 0 and ZF = 0
JNA, JBE
JB , JNAE, JC
Jump if Below (<).
Jump if Not Above or Equal (not >=).
Jump if Carry.
CF = 1 JNB, JAE, JNC
12 JAE , JNB, JNC
Jump if Above or Equal (>=).
Jump if Not Below (not <).
Jump if Not Carry.
CF = 0 JNAE, JB
JBE , JNA Jump if Below or Equal (<=).
Jump if Not Above (not >).
CF = 1 or ZF = 1
JNBE, JA
Dla porównania wartości numerycznych używana jest instrukcja CMP która działa jak instrukcja SUB lecz nie zapisuje wyniku odejmowania tylko ustawia odpowiednio znaczniki.
Przykłady użycia instrukcji CMP i skoków warunkowych:
include "emu8086.inc"
org 100h
mov AL, 25 ; ustaw AL na 25.
mov BL, 10 ; ustaw BL na 10.
cmp AL, BL ; porównaj AL - BL.
je equal ; skocz jeśli AL = BL (ZF = 1).
putc 'n' ; program w tym miejscu jeśli AL <> BL, jmp stop ; wyświetla 'n' i skacze do stop.
equal: ; jeśli program osiągnie to miejsce
putc 'y' ; oznacza to, że AL = BL, więc wyświetlane jest 'y'.
stop:
ret ; koniec programu.
Instukcje pętli
Instrukcja Loop et jest przykładem instrukcji warunkowej, która zmniejsza zawartość rejestru CX o 1, sprawdza, czy zawartość tego rejestru jest różna od 0 i jeśli tak - wykonuje skok do miejsca oznaczonego etykietą et, w przeciwnym razie procesor przechodzi do następnej po loop instrukcji.
mov CX, 10 //ustawienie licznika powtórzeń pętli et:
ciąg instrukcji do wykonania w pętli loop et
13
Instrukcja Operacja i warunek skoku Instrukcja przeciwna
LOOP zmniejsz CX, skocz do etykiety jeśli CX nie równa się zero. DEC CX and JCXZ
LOOPE zmniejsz CX, skocz do etykiety jeśli CX nie równa się zero i (ZF = 1). LOOPNE
LOOPNE zmniejsz CX, skocz do etykiety jeśli CX nie równa się zero i (ZF = 0). LOOPE
LOOPNZ zmniejsz CX, skocz do etykiety jeśli CX nie równa się zero i ZF = 0. LOOPZ
LOOPZ zmniejsz CX, skocz do etykiety jeśli CX nie równa się zero i ZF = 1. LOOPNZ
JCXZ skocz do etykiety jeśli CX jest równe zero. OR CX, CX and JNZ
Instrukcje pętli wykorzystują rejestr CX do zliczania liczby powtórzeń. Jest to rejestr 16-bitowy, stąd liczba powtórzeń ograniczona jest do 65535. Można organizować pętle zagnieżdżone zapamiętując rejestr CX na stosie instrukcją push CX i odzyskując zawartość kiedy pętla lokalna się kończy za pomocą pop CX, np.:
org 100h
mov BX, 0 ; całkowity licznik przeskoków do etykiet.
mov CX, 5 k1: add BX, 1 mov AL, '1' mov AH, 0Eh int 10h push CX mov CX, 5 k2: add BX, 1 mov AL, '2' mov AH, 0Eh int 10h push CX mov CX, 5 k3: add BX, 1 mov AL, '3' mov AH, 0Eh int 10h
loop k3 ; pętla wewnętrzna.
pop CX
loop k2 ; pętla środkowa.
pop CX
loop k1 ; pętla zewnętrzna.
ret
14 BX zlicza całkowitą liczbę przeskoków do etykiet. Domyślnie symulator wyświetla zawartość rejestru w postaci szesnastkowej – klikając podwójnie w rejestr możemy wyświetlać zawartość rejestru w postaci rozszerzonej, w różnych systemach liczenia.
Po zakończeniu programu w rejestrze BX zapisana jest wartość 155 = 125 + 25 + 5 125 razy wykonała się pętla wewnętrzna, 25 razy środkowa i 5 razy zewnętrzna.
W przeciwieństwie do instrukcji JMP instrukcje skoków warunkowych i pętli pozwalają przenieść sterowanie o 127 bajtów do przodu i 128 bajtów do tyłu – skoki względne (pamiętajmy, że instrukcje w kodzie maszynowym mają długość od 1 do 6 bajtów)
Możemy łatwo obejść to ograniczenie w następujący sposób:
Bierzemy z tabeli przeciwny rozkaz skoku warunkowego i podajemy adres skoku do etykiety label_x.
Używamy instrukcji JMP do przeskoczenia do pożądanej lokalizacji.
Definiujemy etykietę label_x: zaraz za instrukcją JMP.
Etykieta label_x: musi być unikalna w całym programie.
Przykład:
include "emu8086.inc"
org 100h mov AL, 5 mov BL, 5
cmp AL, BL ; porównanie AL - BL.
; je equal ; rozkaz 2-bajtowy jne not_equal ; skok, jeśli AL <> BL (ZF = 0).
jmp equal ; skok w dowolne miejsce w segmencie not_equal:
add BL, AL sub AL, 10 xor AL, BL jmp skip_data
db 256 dup(0) ; 256 bytes skip_data:
putc 'n' ; jeśli AL <> BL,
jmp stop ; wyświetlamy 'n' i skok do etykiety stop.
equal: ; jeśli AL = BL putc 'y' ; wyświetlamy 'y'.
stop:
ret