2.3 Pierwszy program

Pierwszym programem będzie oczywiście "Hello world !". Jednak biorąc pod uwagę to iż asembler jest językiem w którym jesteśmy zmuszeni odwoływać się osobiście do każdego urządzenia to powinniśmy poznać najpierw budowę karty graficznej następnie napisać odpowiednie podprogramy obsługujące jej podstawowe funkcje wypisywania tekstu na ekran. To jest niewątpliwie bardzo trudne zadanie, ale od czego są funkcje oferowane przez system operacyjny. Każdy system operacyjny przychodzi nam z pomocą w postaci szeregu bibliotek z procedurami, które realizują za nas część kodu odpowiadającego za komunikowanie się ze sprzętem. Biorąc pod uwagę, że piszemy pierwszy program będziemy korzystać właśnie z takiego zestawu procedur. Procedury te w przypadku systemu DOS oferowane są w postaci przerwań, a ich kod jest na stałe umieszczony w pamięci przez system. Przerwania zostaną szczegółowo opisane w dalszych rozdziałach tak więc tutaj skupimy się jedynie na praktycznym ich wykorzystaniu.

Głównym przerwaniem systemu DOS jest przerwanie o numerze 0x21. Zawiera ono wszystkie podstawowe procedury jakie oferują systemy operacyjne czyli między innymi operowanie na plikach, obsługa ekranu czy klawiatury. Pełny opis wszystkich funkcji tego przerwania możemy znaleźć pod tym adresem [LINK]. W pierwszym programie skorzystamy z dwóch funkcji tego przerwania, a konkretnie wypisanie tekstu na ekran i zakończenie programu.

org 0x100
	mov	ah,	9
	mov	dx,	hello_world
	int	0x21
 
	mov	ax,	0x4C00
	int	0x21
 
hello_world	db	"Hello world !$"
 

Kompilujemy powyższy program, a następnie wykonujemy go.

C:\>nasm -f bin -o prog02.com prog02.asm C:\>prog02.com Hello world !

Program rozpoczyna się od dyrektywy org 0x100. Celowo piszę dyrektywa gdyż nie jest to instrukcja procesora. Dyrektywa jest poleceniem dla kompilatora i ma na celu określenie w jaki sposób poniższy kod ma zostać przekompilowany. org informuje iż kod ma zostać tak przekompilowany aby jego offset bazowy wynosił 0x100. Jest to właściwie jedyny wymóg jaki stawia przed nami format pliku wykonywalnego *.com. Programy wykonywalne zapisane w tym formacie wczytywane są do pamięci pod adres 0x100 względem początku segmentu, a dodanie dyrektywy org informuje o tym kompilator tak aby poprawnie przeliczył adresy wszystkich zmiennych.

Pierwsza instrukcja programu wpisuje do rejestru ah wartość 9. Jak spojrzymy w dokumentację przerwania 0x21 to znajdziemy tam następujące informacje:

DOS 1+ - WRITE STRING TO STANDARD OUTPUT AH = 09h DS:DX -> '$'-terminated string

Przerwania są pewnymi podprogramami umieszczonymi w pamięci. Jeżeli przed wywołaniem przerwania 0x21 w rejestrach umieścimy ściśle okreśłone wartości to przerwanie to wykona odpowiednią funkcję. Rejestr ah służy do wyboru funkcji przerwania. W przupadku funkcji numer 9 powyżej przedstawiłem jej opis z dokumentacji. Jest to funkcja wypisywania na ekran ciągu znaków gdzie znakiem kończącym ciąg jest znak ASCII "$". Musimy również przekazać procedurze przerwania adres tego ciągu znaków i  jak czytamy w dokumentacji musi to być wykonane przez zestaw rejestrów DS:DX. Temu właśnie służy druga instrukcja programu "mov dx, hello_world".

Trzecia instrukcja int 0x21 jest instrukcją wywołania kodu przerwania 0x21. Gdzie się znajduje ten kod w pamięci i jak jest wywoływany opiszę w dalszym rozdziale odpowiedzialnym za przerwania sprzętowe.

mov	ax,	0x4C00
int	0x21

W dalszej części programu wywołujemy funkcję 0x4C przerwania 0x21 z parametrem 0 przekazanym przez rejestr al. Oznacza on iż chcemy zakończyć działanie programu z kodem błędu równym 0 co oznacza prawidłowe wykonanie programu. Powyższy kod równoznaczny jest następującemu:

mov	ah,	0x4C
mov	al,	0
int	0x21

Na samym końcu możemy umieścić wyświetlany przez nas ciąg znaków. Umieszczamy go po instrukcjach oznaczających zakończenie programu ponieważ to miejsce nie będzie już traktowane przez procesor jako instrukcje. Warto spojrzeć jak wygląda ten program po przekompilowaniu.

C:\>debug prog02.com -r AX=FFFF BX=0000 CX=001A DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0100 NV UP EI PL NZ NA PO NC 075E:0100 B409 MOV AH,09 -u 075E:0100 B409 MOV AH,09 075E:0102 BA0C01 MOV DX,010C 075E:0105 CD21 INT 21 075E:0107 B8004C MOV AX,4C00 075E:010A CD21 INT 21 075E:010C 48 DEC AX 075E:010D 65 DB 65 075E:010E 6C DB 6C 075E:010F 6C DB 6C 075E:0110 6F DB 6F 075E:0111 20776F AND [BX+6F],DH 075E:0114 726C JB 0182 075E:0116 64 DB 64 075E:0117 2021 AND [BX+DI],AH 075E:0119 2463 AND AL,63 075E:011B 63 DB 63 075E:011C 67 DB 67 075E:011D E7E6 OUT E6,AX 075E:011F C0 DB C0

Powyżej znajduje się przekompilowany program uruchomiony w debugerze. Wszystkie rejestry segmentowe już od początku działania programu ustawione są na tą samą wartość 0x075E. Jest to cecha wszystkich programów typu *.com. Takie programy mogą w pamięci zajmować maksymalnie 65536 bajtów co dla asemblerowych programów jest dość dużą wartością. Dodatkowo kod programu został wgrany pod offset 0x100 względem początku tego segmentu. Początkowe 256 bajtów segmentu zarezerwowane jest dla struktur opisujących uruchomiony program w pamięci. Te struktury tworzone są przez system operacyjny tuż przed wgraniem kodu programu do pamięci.

Pierwsze 5 zdeasemblowanych instrukcji wygląda prawie identycznie jak kod źródłowy programu. Wszystkie instrukcje, które występują po nich to tak naprawdę ciąg znaków "Hello world !" i na szczęście nie będą potraktowane jako kod ponieważ procesor zakończy wykonywanie instrukcji po ostatnim wywołaniu przerwania 0x21. Normalnym jest, że po kompilacji w kodzie nie znajdują się już etykiety zmiennych ponieważ wszystkie zostały zamienione na ich adresy. To jest ta zasadnicza różnica w składni względem przykładowo języka C w którym problematyczne jest zrozumienie co jest wartością zmiennej, a co jest wskaźnikiem. W przypadku asemblera sprawa jest prosta ponieważ za każdym razem gdy wstawiamy gdzieś etykietę zmiennej to jest to jednoznacznie traktowane jako jej adres (offset).

W programie adres zmiennej hello_world wynosi 0x10C i jest to właściwie tylko część adresu ponieważ należy jeszcze dodać do niego część segmentową ds. Pod tym adresem znajduje się wypisywany ciąg znaków, ale w takiej reprezentacji jak na powyższym wydruku trudno jest to zauważyć dlatego użyję teraz komendy d.

-d 75e:10c 075E:0100 48 65 6C 6C Hell 075E:0110 6F 20 77 6F 72 6C 64 20-21 24 63 63 67 E7 E6 C0 o world !$ccg... 075E:0120 00 00 00 00 18 18 DB 3C-E7 3C DB 18 18 00 00 00 .......<.<...... 075E:0130 00 00 80 C0 E0 F8 FE F8-E0 C0 80 00 00 00 00 00 ................ 075E:0140 02 06 0E 3E FE 3E 0E 06-02 00 00 00 00 00 18 3C ...>.>.........< 075E:0150 7E 18 18 18 7E 3C 18 00-00 00 00 00 66 66 66 66 ~...~<......ffff 075E:0160 66 66 00 66 66 00 00 00-00 00 7F DB DB DB 7B 1B ff.ff.........{. 075E:0170 1B 1B 1B 00 00 00 7C C6-60 38 6C C6 6C 38 0C C6 ......|.`8l.l8.. 075E:0180 7C 00 00 00 00 00 00 00-00 00 00 00 |...........

Dyrektywa org znajdująca się na początku programu modyfikuje wszystkie odwołania do pamięci po przez dodanie do nich definiowanej wartości. Gdyby przekompilować ten sam program bez dyrektywy org to po kompilacji wyglądał by następująco:

-u 075E:0100 B409 MOV AH,09 075E:0102 BA0C00 MOV DX,000C 075E:0105 CD21 INT 21 075E:0107 B8004C MOV AX,4C00 075E:010A CD21 INT 21 -r AX=FFFF BX=0000 CX=001A DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0747 ES=0747 SS=0747 CS=0747 IP=0100 NV UP EI PL NZ NA PO NC 0747:0100 B409 MOV AH,09 -p AX=09FF BX=0000 CX=001A DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0747 ES=0747 SS=0747 CS=0747 IP=0102 NV UP EI PL NZ NA PO NC 0747:0102 BA0C00 MOV DX,000C

W tym przypadku adres ciągu znaków podany do przerwania wynosi 0x75E:0xC. Niestety to nie jest adres miejsca w którym znajduje się nasz ciąg znaków. Gdyby cały program został wczytany na sam początek segmentu 0x75E to wszystko by się wykonało poprawnie. Możemy oczywiście zmodyfikować wartości rejestrów ds i cs w ten sposób aby wskazywały początek segmentu 0x76E i wtedy nie będzie potrzeby używania dyrektywy org. Jednak zmiana wartości rejestrów segmentowych niesie za sobą wstawienie do kodu programu dodatkowych instrukcji co jest zbędne więc przyjęło się stosować wstawianie linii "org 0x100" na początek programu.