"LOOP, LOOPE, LOOPZ, LOOPNE, LOOPNZ"
Jak pisałem poprzednio, przy naszych obecnych umiejętnościach spokojnie można zrobić pętlę "for .. to ..." - choćby tak, jak to demonstruje ten przykład.
Oczywiście - pętla działa co można bez trudu stwierdzić - poprawnie, lecz to jest pretekstem do wprowadzenia właśnie instrukcji LOOP.
Składnia:
LOOP ETYKIETA
Tu warto od razu wyjaśnić, że loop - z angielskiego oczywiście - znaczy "pętla".
No ale co właściwie robi LOOP? Mówiąc krótko - zaledwie dwie rzeczy: Po pierwsze - zmniejsza wartość CX o jeden (DEC CX), po drugie - jeśli CX jest większe 0 powoduje bezwarunkowy przeskok do "ETYKIETA".
Jeśli chcielibyśmy powiedzieć to językiem procesora - LOOP ETYKIETA jest skrutem poniższych komend:
DEC CX
CMP CX,0
JNE ETYKIETA
Nie trzeba tu filozofa by stwierdzić, że LOOP umożliwia tylko budowę pętli typu "downto" czy - jak w basicu - "step -1" - a po ludzku pętli, w której licznik maleje a nie rośnie.
Oczywiście jest to prawda, ale prawdą jest też, że nie warto się męczyć wykonywaniem powyżej pokazanej pętli, gdy można to rozwiązać LOOP'em.
Ponieważ postraszyłem w nagłówku pół tuzinem instrukcji, najwyższy czas zakończyć ten przydługi opis LOOP - za podsumowanie musi wystarczyć przykładowy programik.
LOOPE/LOPZ, LOOPNE/LOOPNZ
Instrukcje LOOP?? mają - jak nie trudno się domyśleć - coś wspólnego z instrukcją LOOP. Tym czymś jest choćby użycie.
Składnia:
LOOPE ETYKIETA
LOOPZ ETYKIETA
LOOPNE ETYKIETA
LOOPNZ ETYKIETA
Nie trudno się też domyśleć, że LOOP?? dotyczą w jakiś sposób instrukcji skoku warunkowego...
Kończąc więc domysły wyjaśniam, że działanie instrukcji LOOP** jest następujące:
Zmniejszyć CX o jeden (DEC CX - tak jak przy LOOP)
Jeśli CX>0 wykonać skok warunkowy do "ETYKIETA" w zależności od typu instrukcji: LOOPE - JE, LOOPNE - JNE, LOOPZ - JZ, LOOPNZ - JNZ.
Jeśli CX=0, lub nie spełniono warunku skoku warunkowego, zakończyć pętlę.
A po jakie licho jest to wszystko aż tak dokładnie zamotane? - jedną z przyczyn jest oczywiście - wyjście na przeciw programiście... teraz ma on możliwość wykonania działania w pętli, które nie tylko będzie uzależnione od wartości CX, ale nawet może on przeprowadzić porówn anie dwóch innych wartości (np. CMP AX,BX) i również na tej podstawie wykonać pętlę lub jej nie wykonać... np. procedurka upewniająca się - zadająca użytkownikowi ważne pytanie, które musi on potwierdzić 3 razy np. "czy formatować dysk" - mogłaby wyglądać tak jak to przedstawia niniejszy program.
Mam nadzieję, że po jego analizie nie będziesz mieć już żadnych wątpliwości, ale oczywiście - gdybyś jednak miał mieć - czekam na pytania.
II
Mini kurs pisania programów TSR w asemblerze
Przerwania w programach TSR, pamięć i zegar CMOS
W poprzednich odcinkach kursu dowiedzieliśmy się, co to jest TSR i jak się go instaluje w pamięci. Przyszedł czas na zaprzęgnięcie naszego rezydenta do bardziej konkretnych zadań, dobrym przykładem niech będzie napisanie prostego programu instalującego się w pamięci i pokazującego aktualną sekundę, taka mała wprawka przed pełnym zegarem, który każdy z was będzie mógł spokojnie sam napisać po przeczytaniu tego odcinka.
Co nam tym razem będzie potrzebne ? Oczywiście, przerwanie zegara, wykonywane z częstotliwością 18.2 Hz (czyli około 18 razy na sekundę), a dokładnie: 1193181/65536 Hz. Możemy "przechwycić" to przerwanie, czyli podstawić swoją własną procedurę, którą komputer będzie wywoływać ze wspomnianą częstotliwością. W naszej procedurze będziemy pobierać z komputera aktualny czas i wyświetlać liczbę sekund w lewym górnym rogu ekranu. Pojawia się tylko pytanie - po co sprawdzać czas aż 18 razy na sekundę, jeżeli mamy wyświetlać tylko sekundy, które się będą zmieniać co 18 przerwań ? Najprostszym rozwiązaniem na oszczędzenie czasu procesora jest sprawdzanie aktualnego czasu tylko co 18 wywołanie naszej procedury. Jednakże możemy postąpić jeszcze inaczej - wyświetlać sekundnik na ekranie tylko wtedy, gdy jego wskazanie jest różne od poprzedniego. To nam oszczędzi mocy procesora traconej za przez każdą sekundę na wyświetlaniu tej samej liczby 18 razy. My jednak w programie przykładowym zrezygnujemy z takiej optymalizacji, aby nie zaciemniać kodu, każdy może to sam poćwiczyć. Jeszcze jedna dygresja - po dokonaniu swoich działań nasza procedura musi zwracać sterowanie do oryginalnej (czyli pod adres, który odczytamy w czasie instalowania się naszego TSRa, dla skrócenia opisu nazywa się często ten adres "wektorem przerwania").
Teraz opis dwóch przydatnych funkcji, które nam udostępnia DOS (czyli przerwanie 21h):
Funkcja 25h
Nazwa: Ustalanie adresu kodu obsługi przerwania
Wywołanie: AH=25h
AL - numer przerwania
DS:DX - adres procedury obsługującej przerwanie
Powrót: Brak
Opis: Funkcja ustawia nową procedurę obsługi przerwania o numerze
podanym w AL. Adres procedury obsługi przerwania powinien być
przekazany w DS:DX.
Funkcja 35h
Nazwa: Pytanie o adres kodu obsługi przerwania
Wywołanie: AH=35h
AL - numer przerwania
Powrót: ES:BX - adres procedury obsługi przerwania
Opis: Funkcja zwraca adres procedury obsługi przerwania o numerze
podanym w AL.
Dobra, mamy już wiadomości o tym, jak przechwytywać przerwanie po zapamiętaniu adresu oryginalnej procedury obsługi. Pytanie: no to które to właściwie jest przerwanie zegarowe ? Otóż jest to przerwanie nr 8, czyli IRQ0. Należy się jednak drobne wyjaśnienie: IRQ0 oznacza, że do kontrolera przerwań (a są takie dwa układy na płycie głównej komputera) do linii nr 0 przychodzą informacje od układu zegarowego, który na tą linię wystawia sygnał żądania przerwania właśnie 18 razy na sekundę. Podobnie do IRQ0 podłączona jest klawiatura, IRQ5 często karta muzyczna i tak dalej. Numer przerwania obsługującego linię IRQx to x+8, czyli przerwanie zegarowe ma numer 8, przerwanie klawiatury - nr 9 i tak dalej. Drugim kontrolerem nie będziemy się na razie zajmować, zaznaczę tylko, że obsługuje on przerwania IRQ8 do IRQ15, a numery przerwań od drugiego kontrolera zaczynają się dla zmyłki od 40h.
Kolejna sprawa: jak odczytać aktualną sekundę ? Jest kilka sposobów, my skorzystamy z bezpośredniego dostępu do zegara CMOS umieszczonego na płycie głównej komputera. Jest on widziany w przestrzeni adresowej jako dwa kolejne porty: o numerze 70h oraz 71h, dostępne dla programisty poprzez instrukcje: out i in. Instrukcja 'out' służy do wysyłania danych do portu, instrukcja 'in' do czytania z portu. W naszym przypadku będą to instrukcje: out 70h,al oraz in al,71h. Pierwszą z nich wyślemy do zegara CMOS numer komórki, która nas interesuje (o tym dalej), a drugą odczytamy jej zawartość. Cały fragment kodu czytający aktualną sekundę będzie w związku z tym wyglądał tak:
xor al,al
out 70h,al
jmp $+2
in al,71h
Instrukcja jmp $+2 powoduje drobne opóźnienie wymagane do poprawnej współpracy z zegarem CMOS, natomiast xor al,al jest równoważne mov al,0 - czyli po prostu do rejestru AL wpisuje zero. Po wykonaniu wyżej podanego bloku 4 rozkazów otrzymamy aktualną sekundę w AL w kodzie BCD, który należy jeszcze przekonwertować na kody dwóch znaków liczby. Jak to jest zrobione w praktyce ujrzycie za chwilę w listingu rezydenta. Jeszcze tylko trochę więcej informacji o układzie CMOS, w którym oprócz zegara zawarta jest też pamięć przechowująca najważniejsze ustawienia naszych komputerów (czyli całą zawartość SETUPu). Oto adresy i funkcje kolejnych komórek, do których możemy się odwoływać (po opisy szczegółowe odsyłam do książek):
0 aktualna sekunda zegara czasu rzeczywistego (RTC) w kodzie BCD
1 sekunda ustawienia budzika w kodzie BCD
2 aktualna minuta w BCD
3 minuta ustawienia budzika w BCD
4 aktualna godzina RTC w BCD
5 godzina ustawienia budzika w BCD
6 dzień tygodnia (1=niedziela,2=poniedziałek itd.)
7 dzień miesiąca w BCD
8 miesiąc w BCD
9 rok w BCD (ostatnie dwie cyfry)
0ah RTC rejestr stanu A
0bh RTC rejestr stanu B
0ch RTC rejestr stanu C
0dh RTC rejestr stanu D
0eh bajt stanu ustawiany przez POST
0fh powód wyłączenia
10h typ stacji dysków w systemie
11h zarezerwowane
12h typ twardego dysku
13h zarezerwowane
14h bajt wyposażenia komputera
I tak dalej. Jest tych komórek 256 i kogo bardziej interesują, może zawsze zajrzeć do literatury (np. podanej już wcześniej książki: "Jak pisać wirusy"). Kolejna sprawa: jak wypisać wartość na ekranie nie używając do tego przerwania DOSu (używanie przerwań w naszej procedurze rezydentnej jest bardzo ryzykowne, o tym będzie powiedziane dokładniej w dalszych częściach kursu) ? Otóż jest sposób, należy kody znaków do wypisania "wcisnąć" bezpośrednio w obszar pamięci ekranu, na kartach VGA, CGA, EGA itp. zaczyna się ona od początku segmentu B800h, natomiast na karcie Hercules (HGC) od B000h. Pod tymi adresami mamy dostęp do kodu pierwszego znaku na ekranie (czyli tego w lewym górnym rogu), w następnym bajcie leży atrybut tego znaku, dalej kod drugiego znaku, jego atrybut itd. Kolory znaków możemy obliczyć podstawiając odpowiednie bity w bajcie atrybutów:
nr bitu: 7 6 5 4 3 2 1 0
znaczenie: K R G B i r g b
K - to blink, czyli migotanie znaku (znak miga gdy bit K=1), i to intensity - jasność znaku (0=ciemniejszy, 1=jaśniejszy), RGB to kolejne składowe kolorów tła, natomiast rgb to składowe kolorów znaku. Przykład: potrzebujemy bajt atrybutu oznaczający jasnoczerwone znaki na czarnym tle, nie migające:
nr bitu: 7 6 5 4 3 2 1 0
znaczenie: K R G B i r g b
wartość: 0 0 0 0 1 1 0 0
| ^^|^^ | ^^^^^-czerwony
znak nie ---+ | +jasny
miga tło czarne
Czyli wychodzi na to, że poszukiwany atrybut znaku to 0ch. Można wpisać go w pamięć ekranu oddzielnie, po wpisaniu kodu znaku, jednak my te dwie rzeczy zrobimy jednocześnie - wpisując od razu całe słowo 16-bitowe rozkazem stosw, umieszczającym wartość rejestru AX pod adresem ES:DI i zwiększającym DI o 2 - tak, że wskazuje od razu na następny znak. Po uruchomieniu programu będziecie mogli się przekonać, że czas zawarty w zegarze CMOS spieszy się nieznacznie względem czasu DOSowego (np. pokazywanego przez Dos Navigatora, Nortona Commandera itp.), ponieważ przy uruchamianiu komputera DOS odczytuje zawartość CMOSa i trochę czasu mu zajmuje ustawienie swojego zegara - przez to się spóźnia. Natomiast po wyłączeniu komputera zegar CMOS chodzi sobie jakby nigdy nic - jego zasilanie jest podtrzymywane bateryjnie. Ale dość ględzenia, przyszedł czas na listing:
.model tiny
.code
.386
org 100h
Start:
jmp Instaluj
; tutaj będą nasze zmienne:
staraproc dd 0 ; dd oznacza 4 bajty (tutaj o wartości 0)
NaszaProc:
push ax ; zapamiętujemy wartości używanych rejestrów
push bx
push di
push es
mov ax,0b800h ; B800h - segment pamięci ekranu karty VGA
mov es,ax
xor di,di ; zerujemy DI - adres w pamięci ekranu
xor al,al ; AL=0 - komórka z aktualną sekundą w BCD
out 70h,al ; wysyłamy do zegara CMOS
jmp $+2 ; małe opóźnienie
in al,71h ; odczytujemy wynik z zegara CMOS
mov bl,al
and bl,0fh ; prawa połówka bajtu - prawa cyfra w BCD
add bl,'0' ; do tego dodajemy kod zera
shr al,4 ; lewa połówka bajtu - lewa cyfra w BCD
add al,'0' ; do tego też dodajemy kod '0'
mov ah,0ch ; atrybut napisu - jasnoczerwony na czarnym tle
stosw ; i rzucamy na ekran pierwszą cyfrę
mov al,bl
stosw ; potem drugą
pop es
pop di
pop bx
pop ax
jmp dword ptr cs:[staraproc] ; skok do oryginalnej procedury
; koniec części rezydentnej
Instaluj:
mov ax,3508h ; 35h: pobranie wektora przerwania
int 21h ; wynik wpadł do ES:BX
mov word ptr cs:[staraproc],bx ; trzeba jeszcze go gdzies zapamietac
mov word ptr cs:[staraproc +2],es
mov ax,2508h ; 25h: ustawienie wektora przerwania
mov dx,offset NaszaProc ; DS:DX - wektor naszej procedury
int 21h
mov ah,9 ; 09h: wydruk napisu na ekran
mov dx,offset Napis
int 21h
mov dx,offset Instaluj ; do DX wpisujemy adres pierwszego bajtu,
int 27h ; który ma być zwolniony, wcześniejsze
; zostają w pamięci na stałe
Napis db 'Program zainstalowany w pamięci.',13,10,'$'
end Start
W następnym odcinku dowiemy się, jak naszego rezydenta wyrzucić z pamięci i do tego jeszcze kilka innych przydatnych rzeczy.
Mini kurs pisania programów TSR w asemblerze
Usuwanie rezydenta z pamięci i jakie są z tym związane problemy
W poprzednim odcinku dowiedzieliśmy się, jak napisać prosty sekundnik instalowany rezydentnie w pamięci. Cały problem w tym, że po jednorazowym zainstalowaniu takiego TSRa zabiera on nam kawałek cennej pamięci, a gdy już znudzą nam się cyferki wciąż widoczne na ekranie - pozostaje tylko reset komputera. Przyszła pora na poznanie kolejnej techniki, którą będziemy stosować, a mianowicie sposób na rozinstalowanie rezydenta, czyli powrót do stanu sprzed zainstalowania.
Na początku należy się zastanowić - co tak właściwie musimy zrobić, aby nasz komputer działał tak, jakbyśmy nigdy TSRa nie uruchamiali. Po pierwsze: należy sprawdzić, czy w ogóle nasz rezydent jest obecny w pamięci. Najprościej sprawdzić wektor przerwania, które on przechwycił podczas instalacji (czyli w przypadku sekundnika będzie to przerwanie 8), a potem upewnić się, że pod podanym adresem jest obecny nasz TSR. W tym celu możemy po prostu porównać offset (przesunięcie w segmencie) początku naszej procedury z offsetem podanym nam przez funkcję DOSu czytającą wektor przerwania (funkcja 35h przerwania 21h). Jednakże takie proste sprawdzenie może czasem nie przynieść dobrych rezultatów, gdy oprócz sekundnika w pamięci są obecne inne programy TSR o tych samych offsetach procedur podpiętych pod przerwanie zegara. Największą wiarygodność możemy uzyskać tylko przez sprawdzenie czegoś unikalnego dla naszego rezydenta. W praktyce wystarczy porównanie ciągu znaków pod znanym adresem z naszym wzorcem - kiedy się zgadzają to możemy kontunuować usuwanie TSRa z pamięci komputera.
Po stwierdzeniu obecności TSRa i sprawdzeniu przechwytywanych przez niego przerwań (w przypadku sekundnika jest to jedno przerwanie - nr 8), możemy odczytać oryginalne wektory tych przerwań (wiemy bowiem, w którym miejscu w rezydencie są one "zaszyte") i przywrócić je (funkcja 25h przerwania 21h). Pozostaje już tylko zwolnić bloki pamięci zajmowane przez sekundnik, wypisać na ekranie komunikat o pomyślnym usunięciu rezydenta i normalnie powrócić do DOSu (funkcja 4ch przerwania 21h). Oczywiście przy instalacji programu warto również sprawdzić, czy już wcześniej nie był instalowany, by uniknąć dwukrotnej instalacji. Praktyczną realizację tych kilku kroków możecie prześledzić analizując kod źródłowy podany w dalszej części.
Chwila na krótkie wyjaśnienie: DOS przydziela programom pamięć w blokach o długości będącej wielokrotnością 16 bajtów. Poza takimi blokami danych mogą wystąpić jeszcze bloki z kodem programu oraz bloki z otoczeniem (tam są przechowywane wszystkie ustawienia otoczenia programu, czyli wartości nadane przez PATH, SET, PROMPT itp. - można je wyświetlić komendą SET). Każdy program przy uruchomieniu "otrzymuje" swój blok z kopią otoczenia DOSowego, które może dowolnie modyfikować (np. zmienić ścieżkę wyszukiwania PATH) i odczytywać (chcąc pobrać parametry otoczenia). Prócz samej zawartości otoczenia na końcu bloku jest wpisywana ścieżka dostępu i nazwa pliku "właściciela", czyli programu, do którego należy dane otoczenie, np. C:\MASM\PROGS\KURS\MOJPROG1.COM. Jak czytać parametry otoczenia dowiemy się kilka odcinków dalej. Przy zakończeniu programu otoczenie jest automatycznie zwalniane - zmiany, które program w nim poczynił są tracone. Oczywiście zostawiając TSRa w pamięci fragment bloku z kodem programu zostaje (wielkość fragmentu zaznaczamy w rejestrze DX przy wywołaniu przerwania 27h), natomiast reszta jest zwalniana (czyli blok jest skracany), blok z otoczeniem również pozostaje na swoim miejscu. Dlatego często w TSRach blok otoczenia jest zwalniany już w czasie instalacji, aby zmniejszyć wielkość pamięci zajmowanej przez rezydenta. Tak też będzie w nowej wersji sekundnika. Numer segmentu otoczenia (środowiska) odczytamy ze słowa 16-bitowego umieszczonego w segmencie programu pod adresem 002ch (czyli w obszarze PSP, o tym będzie później).
A oto przydatne informacje:
Funkcja 49h
Nazwa: Zwalnianie pamięci
Wywołanie: AH=49h
ES - segment, w którym znajduje się zwalniana pamięć
Powrót: Ustawiony znacznik C : AX - kod błędu
Nie ustawiony C : OK
Po wywołaniu tej funkcji możemy stwierdzić, czy wystąpił błąd (np. podaliśmy numer segmentu, który nie zaczyna nowego bloku pamięci) poprzez sprawdzenie znacznika C:
; wcześniej nadajemy rejestrom wartości potrzebne do wywołania funkcji
int 21h
jc Blad ; skok gdy znacznik C jest ustawiony
; === nie ma błędu ===
Blad:
; === wystąpił błąd ===
Pytanie w jaki sposób rozpoznamy, czy użytkownik chce zainstalować program, czy go rozinstalować ? Oczywiście w tym celu musimy sprawdzić parametry podane w linii poleceń (czyli odróżnić uruchomienie: TEST.COM od: TEST.COM /u). Dla uproszczenia przyjmijmy, że jeżeli w linii poleceń znajdzie się litera 'u' to należy usunąć TSRa z pamięci.
Znaki podane w linii poleceń przy uruchamianiu programu są trzymane w bloku PSP (ang. Program Segment Prefix), który w zbiorach typu COM rezyduje na początku segmentu z programem (jak pamiętamy, program zaczyna się od adresu 100h, wcześniej jest właśnie PSP). Kolejne znaki parametrów podanych programowi są zapisywane począwszy od adresu 81h, pod adresem 80h leży bajt zawierający ilość znaków, a cały ciąg kończy się znakiem o kodzie 0dh (czyli CR). Literę 'u' znajdziemy porównując kolejne znaki aż do znaku CR albo wcześniejszego napotkania 'u'. I znowu - konkretną implementację znajdziecie w kodzie programu.
Przyszła pora na kolejne ulepszenie naszego sekundnika - będzie on zmieniał swój kolor w zależności od tego, czy klawiatura będzie w stanie CapsLock. Do tego celu przyda nam się opis zawartości komórek danych BIOSu pod adresami: 0040:0017h (czyli wygodniej jest napisać 0000:0417h - będzie to samo) i następnym (418h):
Adres 0:0417h
Numer bitu: Znaczenie bitu zapalonego:
0 prawy Shift wciśnięty
1 lewy Shift wciśnięty
2 dowolny Ctrl wciśnięty
3 dowolny Alt wciśnięty
4 ScrollLock zapalony
5 NumLock zapalony
6 CapsLock zapalony
7 stan Insert
Adres 0:0418h
Numer bitu: Znaczenie bitu zapalonego:
0 lewy Ctrl wciśnięty
1 lewy Alt wciśnięty
2 SysReq wciśnięty
3 stan przerwy (czyli po wciśnięciu Pause)
4 ScrollLock wciśnięty
5 NumLock wciśnięty
6 CapsLock wciśnięty
7 Insert wciśnięty
Jak widzimy, aktualny stan przełącznika CapsLock możemy odczytać sprawdzając bit nr 6 pod adresem 0:417h, gdy będzie zapalony to znaczy, że klawiatura jest w stanie CapsLock (chyba nie muszę tłumaczyć, na czym ten stan polega). Sprawdzenie jednego bitu najprościej dokonać instrukcją test, której podajemy maskę bitu (czyli jego wagę, w tym przykładzie 40h), a otrzymujemy w wyniku ustawienie lub wyzerowanie flagi ZF, czyli przepisanie do niej zawartości testowanego bitu (wyzerowanie ZF gdy bit był wyzerowany, ustawienie - gdy był ustawiony). Można też instrukcję test wykonać z parametrem nie będącym wagą jednego bitu - wtedy zostanie logicznie wymnożony (AND) bajt sprawdzany i podana wartość oraz odpowiednio ustawione flagi, podobnie jak działa instrukcja and - tylko bez zapamiętywania wyników. Dla przypomnienia podam jeszcze wagi kolejnych bitów, od 0. począwszy: 1,2,4,8,16,32,64,128, a w hex. to będzie: 1,2,4,8,10h,20h,40h,80h. Popatrzmy na fragment kodu do sprawdzenia stanu CapsLock:
xor ax,ax
mov es,ax ; zerujemy rejestr segmentowy ES
test byte ptr es:[417h],40h
jz Nie_ma_CapsLock
; CapsLock wciśnięty
Nie_ma_CapsLock:
; CapsLock nie wciśnięty
A teraz już program towarzyszący temu odcinkowi kursu pisania TSR'ów:
.model tiny
.code
.386
org 100h
Start:
jmp StartTutaj
; tutaj będą nasze zmienne:
staraproc dd 0
; znacznik potrzebny do sprawdzenia zainstalowania TSRa:
znacznik db 'Sekundnik, odc. 3'
NaszaProc:
push ax
push bx
push di
push es
xor ax,ax ; segment komórki ze stanem klawiatury
mov es,ax
mov bh,0ch ; standardowy kolor jasnoczerwony do BH
test byte ptr es:[417h],40h; sprawdzamy, czy włączony jest CapsLock
jnz CapsOn ; skok gdy CapsLock wciśnięty
mov bh,1 ; kolor niebieski - CapsLock wyłączony
CapsOn:
mov ax,0b800h
mov es,ax
xor di,di
xor al,al
out 70h,al
jmp $+2
in al,71h
mov bl,al
and bl,0fh
add bl,'0'
shr al,4
add al,'0'
mov ah,bh ; ładujemy do AH wcześniej ustalony kolor
stosw ; i rzucamy na ekran pierwszą cyfrę
mov al,bl
stosw ; potem drugą
pop es
pop di
pop bx
pop ax
jmp dword ptr cs:[staraproc] ; skok do oryginalnej procedury
; koniec części rezydentnej
StartTutaj:
mov ah,9 ; 09h: wydruk nagłówka na ekran
mov dx,offset Logo
int 21h
mov si,81h ; początek ciągu parametrów
cld
Petla:
lodsb ; wczytanie do AL jednego znaku z DS:SI, SI=SI+1
cmp al,'u' ; może to jest 'u' ?
je Rozinstaluj
cmp al,'U' ; a może duże 'U' ?
je Rozinstaluj
cmp al,0dh ; może kod ENTERa (CR) ?
je Instaluj
jmp Petla ; skok gdy nic nie trafimy
Rozinstaluj:
mov ax,3508h ; 35h: pobranie wektora przerwania
int 21h
cmp bx,offset NaszaProc ; sprawdzamy, czy się zgadzają offsety
jne NieMa
mov si,offset znacznik ; adres lokalnego znacznika do DS:SI
mov di,si ; i znacznika sprawdzanego do ES:DI
mov cx,17 ; długość znacznika w bajtach
cld
repe cmpsb ; sprawdzamy aż do różniącego się bajtu
jnz NieMa ; skok gdy się nie zgadzają znaczniki
; Teraz już nie ma przeciwwskazań do rozinstalowania TSRa
mov dx,word ptr es:[staraproc] ; czytamy oryginalny wektor
mov ax,word ptr es:[staraproc +2] ; z bloku TSRa
mov ds,ax
mov ax,2508h ; 25h: ustawienie wektora przerwania
int 21h
mov ah,49h ; 49h: zwolnienie bloku pamięci z TSRem
int 21h ; w ES mamy segment TSRa
mov ax,cs
mov ds,ax ; przywracamy do DS segment naszego programu
mov ah,9 ; 09h: wydruk napisu na ekran
mov dx,offset Uninst
int 21h ; drukujemy komunikat o pomyślym usunięciu TSRa
mov ax,4c02h ; 4ch: powrót do DOSu, w AL kod błędu
int 21h
NieMa:
mov ah,9 ; 09h: wydruk napisu na ekran
mov dx,offset Brak
int 21h
mov ax,4c04h ; 4ch: powrót do DOSu, w AL kod błędu
int 21h
Instaluj:
mov ax,word ptr ds:[2ch] ; numer segmentu środowiska odczytamy z PSP,
mov es,ax ; wrzucimy do ES
mov ah,49h ; 49h: zwolnienie bloku pamięci
int 21h
mov ax,3508h ; 35h: pobranie wektora przerwania
int 21h ; wynik wpadł do ES:BX
mov word ptr cs:[staraproc],bx ; trzeba jeszcze go gdzies zapamietac
mov word ptr cs:[staraproc +2],es
mov ax,2508h ; 25h: ustawienie wektora przerwania
mov dx,offset NaszaProc ; DS:DX - wektor naszej procedury
int 21h
mov ah,9 ; 09h: wydruk napisu na ekran
mov dx,offset Napis
int 21h
mov dx,offset StartTutaj ; do DX wpisujemy adres pierwszego bajtu,
int 27h ; który ma być zwolniony, wcześniejsze
; zostają w pamięci na stałe
Logo db 'Sekundnik 1996.',13,10
db ' parametr /u - usunięcie programu z pamięci',13,10,'$'
Napis db 'Program zainstalowany w pamięci.',13,10,'$'
Brak db 'Program nie był wcześniej instalowany w pamięci.',13,10,'$'
Uninst db 'Program usunięty z pamięci.',13,10,'$'
end Start
W zależności od stanu CapsLock ustawiamy odpowiednio kolor wpisywanych na ekran znaków - niech to będzie jasnoczerwony dla CapsLock włączonego oraz niebieski dla CapsLock nie aktywnego. Właściwie nie pozostaje już nic innego jak tylko poczytać listing. Co zrobić, gdy program jest w pamięci, ale został po nim zainstalowany inny rezydent oraz jak wykryć taką sytuację dowiemy się w następnym odcinku (przy okazji poznamy bardzo użyteczne przerwanie 2fh, zwane przez znawców tematu Multiplex Interrupt).
Mini kurs pisania programów TSR w asemblerze
Wady i zalety Multiplex Interrupt
Na początku wyjaśnijmy sobie o co w ogóle chodzi w tytule tej części. Otóż Multiplex Interrupt jest to jedno z przerwań programowych (to znaczy nie wywoływanych przez sprzęt, jak np. przerwanie zegara, ale tylko poprzez instrukcję int). Ma numer 2fh i służy do bardzo wielu przydatnych rzeczy a w szczególności niesie pomoc w programach rezydentnych. Cała istota Multiplex Interrupt (w skrócie MxI) polega na utworzeniu "łańcucha", do którego dopinają się kolejne programy wykorzystujące je - TSRy. Każdy program ma przydzielony swój numer identyfikacyjny, po którym może poznać, czy odwołanie MxI dotyczy tego rezydenta, czy jakiegoś innego. W trakcie instalacji TSR odczytuje wektor przerwania 2fh i ustawia nowy na swoją procedurę. Teraz kiedy przyjdzie przerwanie i w rejestrze AH jest jego numer identyfikacyjny to oznacza, że do niego przyszło zlecenie (o tym będzie dalej) i on je ma obsłużyć. Jeżeli w AH jest inny numer, TSR przekazuje sterowanie pod stary adres (zapamiętany podczas instalacji). Powyższe tłumaczenie jest dość zawiłe, dlatego podam trochę konkretów:
Przerwanie 2Fh
Nazwa: Obsługa równoczesnych procesów
Wywołanie: AH = numer procesu (czyli ID TSRa)
01h - rezydentna część polecenia PRINT
02h - rezydentna część polecenia ASSIGN
03h - rezydentna część polecenia SHARE
80h-0ffh - dostępne dla innych procesów
AL = 0
Powrót: AL - stan zainstalowania
00h - nie zainstalowany, można zainstalować
01h - nie zainstalowany, nie można zainstalować
0ffh - zainstalowany
Opis:
Przerwanie organizuje równoczesną pracę programów rezydentnych dostępnych z dowolnego procesu. Pierwotnie dotyczyło tylko polecenia systemowego PRINT. Każdy proces instaluje się w kolejce (poprzez kolejne przechwytywanie tego przerwania). W przypadku wywołania zlecenia proces sprawdza, czy zlecenie go dotyczy, jeśli nie to oddaje sterowanie poprzedniemu w kolejce. W rejestrze AL przekazywany jest kod zlecenia. Standardowo zlecenie numer 0 oznacza pytanie o to, czy program jest zainstalowany.
Z powyższego opisu widzimy, jak prosta jest zasada działania MxI, wszystkie rezydentne polecenia DOSu mają wbudowaną obsługę swoich funkcji przez to przerwanie (np. możemy poleceniu PRINT kazać zatrzymać wszystkie wydruki znając numer zlecenia, które mamy mu przekazać), na przykład:
mov ax,0103h ; 01-PRINT, 03-zatrzymanie drukowania
int 2fh
Działanie Multiplex Interrupt w naszym rezydencie zależy tylko od naszej inwencji, poza oczywiście zleceniem nr 0, które ma przekazać informację o zainstalowaniu programu. Wtedy możemy stwierdzić, czy TSR jest obecny w pamięci, pomimo że po nim był instalowany inny program rezydentny, który również przechwycił to samo przerwanie (w przypadku sekundnika przerwanie zegara). Poza tym możemy dodać również nasze nowe zlecenia, np. zmiana kolorów cyfr bez reinstalacji programu, podawanie segmentu, w którym jest obecny kod TSRa i tak dalej. Przykładów można znaleźć bez liku, jeśli tylko dysponuje się rozwiniętą wyobraźnią. W naszym nowym przykładowym rezydencie (ile razy można obrabiać i ulepszać sekundnik?) zastosujemy procedurę, która będzie "wrażliwa" na ID procesu nr 90h (dlaczego tak? wymyśliło mi się, można podać numer od 80h w górę) i zlecenia nr 0 oraz 1. Czasem takie założenia mogą nie przynieść spodziewanych rezultatów, gdy w pamięci będzie inny TSR reagujący również na numer 90h - przy takich obawach można napisać funkcję przeszukującą kolejne numery od 80h (sprawdzamy zleceniem 0, czy rezydent jest zainstalowany) i zatrzymującą się na pierwszym wolnym. My jednak przyjmiemy, że 90h jest dla nas wystarczający i nic się nie powtórzy. Zlecenie nr 0 będzie służyło do sprawdzania, czy nasz rezydent jest obecny w pamięci (czyli będziemy w AL zwracać 0ffh) oraz przy okazji czytania numeru segmentu, w jakim jest on zainstalowany (zwracamy w BX). Natomiast zlecenie nr 1 będzie nam podawać w ES:DI adres ciągu znaków (zakończonego znakiem dolara, tak jak w napisach w DOSie) z wersją zainstalowanego TSRa. Sam program rezydentny będzie realizował trywialne zadanie - podepniemy go pod przerwanie klawiatury i przy każdym naciśnięciu klawisza będzie generowany dźwięk o długości zależnej od czasu naciśnięcia (czyli po stwierdzeniu naciśnięcia włączymy dźwięk PC Speakera, a przy puszczeniu wyłączymy). Do tego będzie jeszcze aktywna kombinacja Alt-Ctrl-Ins, przełączająca nam dźwięk (na zasadzie włączony - wyłączony - włączony itd). I znów to samo - sam program nie jest przeznaczony do używania w konkretnych celach (czy sekundnik komuś się do czegoś przydał?), ale do zobrazowania technik pisania TSRów.
Przy usuwaniu programu rezydentnego z pamięci (nazwijmy go roboczo: beep) należy pamiętać o sprawdzeniu MxI, czy TSR jest obecny, jak również o odczytaniu wektora przerwania klawiatury (int 9h), ponieważ nie możemy usunąć TSRa i odtworzyć wykorzystywanych przez niego przerwań, kiedy po nim został zainstalowany inny rezydent (bo w ten sposób odcięlibyśmy od "funkcji życiowych" również ten inny program). Jednak jest na to sposób - odcinamy tylko działanie kliku i kombinacji Alt-Ctrl-Ins poprzez ustawienie odpowiedniej flagi w obszarze TSRa (można by było też po to wymyślić nowe zlecenie MxI, ale po co kombinować, gdy po odczytaniu numeru segmentu kodu zleceniem 0 mamy dostęp do obszaru zmiennych beepa), nazywa się ona 'niemamnie' i gdy nie jest równa 0 to TSR zachowuje się tak, jakby go w ogóle nie było. Gdyby beep rezerwował sobie dodatkowe bloki pamięci należało by je również zwolnić. Pamiętajmy również o tym, aby w czasie instalacji sprawdzić, czy przypadkiem już wcześniej beep nie był instalowany, a jeżeli tak - wyświetlić stosowny komunikat, no i oczywiście zwolnić blok pamięci zajmowany przez środowisko programu. Nasz TSR nie będzie tym razem sprawdzał parametrów podanych w linii poleceń w poszukiwaniu 'u', natomiast po uruchomieniu będzie się instalował w pamięci, a po powtórnym uruchomieniu - usuwał (lub dezaktywował).
Po wywołaniu przez system przerwania klawiatury, pod które jesteśmy podpięci możemy odczytać kod wciśniętego klawisza z portu 60h (in al,60h), jest to tzw. scan-code klawisza, czyli najogólniej mówiąc jego kolejny numer na klawiaturze. Klawisz Insert ma scan-code równy 52h, natomiast ten sam klawisz przy zwolnieniu wysyła kod o 80h większy (z ustawionym najwyższym bitem), czyli 0d2h. Klawiatura rozszerzona 101-klawiszowa wysyła ponadto dodatkowe kody informujące, czy naciśnięto Ins z klawiatury numerycznej, czy szary Ins z dodatkowego bloku - jest wtedy przed scan-code klawisza Insert wysyłany kod 0e0h - my po odebraniu takiego kodu przekazujemy sterowanie bezpośrednio do oryginalnej procedury. W programie beep będziemy reagować na puszczenie klawisza Insert ze względu na samopowtarzanie przy dłuższym naciśnięciu (gdyby TSR reagował na naciśnięcie, obserwowalibyśmy naprzemienne włączanie i wyłączanie funkcji programu). Fakt wciśnięcia jednocześnie Alt i Ctrl rozpoznamy badając obszar zmiennych BIOSu (komórka 0:417h, bity 2 i 3 ustawione, traktowała o tym szerzej część 3 kursu). Po rozpoznaniu "korzystnej" kombinacji (Alt-Ctrl-Ins) zmieniamy wartość wewnętrznego przełącznika (zmienna flipflop), po czym zwracamy sterowanie do oryginalnej procedury obsługi przerwania klawiatury. Gdybyśmy nie chcieli tego robić (np. przechwycić i "zdusić" wszystkie naciśnięcia klawisza X) i nie przesyłać sterowania pod oryginalny adres (czyli do poprzedniego programu dołączonego do int 9h, a w końcu do procedury w BIOSie wpisującej kod ASCII klawisza do bufora klawiatury), należałoby wykonać następujący fragment kodu, wymagany do poprawnego powrotu do głównego programu instrukcją iret:
in al,61h ;+ znak dla kontrolera klawiatury, że zakończyliśmy
mov ah,al ;+ obsługę przerwania
or al,80h ;+
out 61h,al ;+
mov al,ah ;+
out 61h,al ;+
mov al,20h ;- znak dla kontrolera przerwań
out 20h,al ;- (tzw. EOI - End Of Interrupt)
Do włączania dźwięku w głośniku służą instrukcje:
in al,61h
or al,3 ; ustawiamy bity: 0 i 1
out 61h,al
Do wyłączania:
in al,61h
and al,0fch ; zerujemy bity: 0 i 1
out 61h,al
Na początku w trakcie instalacji TSRa możemy jeszcze ustawić wysokość dźwięku wysyłając 2 bajty wartości licznika do timera (który obsługuje i zegar, i generator głośnika). Wartość licznika to: 1193181/f, f - częstotliwość dźwięku. Z tego wynika, że chcąc ustawić wysokość dźwięku na 440 Hz należy ustawić licznik na 1193181/440 = 2712, czyli 0a98h. Wysyłamy kolejno: kod operacji (0b6h) do portu 43h, następnie młodszy bajt licznika (98h) do portu 42h, a potem starszy bajt (0ah):
mov al,0b6h
out 43h,al
mov al,98h
out 42h,al
mov al,0ah
out 42h,al
W naszym przykładowym programie beep możliwość ustawienia wysokości dźwięku nie została wykorzystana, możecie to zrobić w swoich programach. Można też również pokusić się o napisanie rezydenta, który po każdym naciśniętym klawiszu będzie zmieniał wysokość tonu, biorąc ją na przykład z tablicy. Wtedy przy pisaniu tekstu komputer będzie grał muzykę! To tyle na dziś, zobaczymy, co przyniesie kolejny odcinek.
Listing programu dołączonego do tego odcinka kursu jest tak długi, że nie było sensu go zamieszczać w tym miejscu, razem z tekstem. Za to można go ściągnąć stąd.
Mini kurs pisania programów TSR w asemblerze
Wywoływanie przerwań dosowych w czasie pracy TSR'a
Piąty odcinek kursu pisania TSRów będzie poświęcony problemom wywoływania przerwań DOSa w trakcie działania rezydenta i sposobom radzenia sobie z tymi trudnościami. Otóż zacznijmy od tego, że w naszym rezydencie (nazwijmy go roboczo: "Grabber") przechwyciliśmy przerwanie klawiatury i chcemy, aby się uaktywnił po naciśnięciu przez użytkownika kombinacji klawiszy LewyShift+LewyCtrl+Delete, po czym zapisał do zbioru w katalogu C:\TEMP zawartość ekranu trybu graficznego 13h. Jest to tryb o rozdzielczości 320x200 w 256 kolorach, w którym od początku segmentu A000h zapisane są w kolejnych bajtach kolory punktów najwyższej linii ekranu (poczynając od lewej strony), od adresu A000h:320 kolory punktów w drugiej linii itd. W ten sposób otrzymujemy 320x200 = 64000 bajtów do zapisania w zbiorze. Do tego należy doliczyć 768 bajtów na paletę kolorów (768=3*256, mamy 256 kolorów, każdy o składowych: czerwonej, zielonej i niebieskiej). Aby nasze zbiory nie były "oderwane" od rzeczywistego świata, będziemy je zapisywać w formacie .BMP - dokładając na początku zbioru stały nagłówek (ponieważ za każdym razem zapisujemy ekran o tej samej wielkości i liczbie kolorów) oraz zgodnie z konwencją zapisu plików .BMP - będziemy zapisywali kolejne linie od najniższej do najwyższej (czyli w kolejności odwrotnej, niż ich położenie w pamięci ekranu). Kolejne pliki będą otrzymywały nazwy OBRAZ000.BMP, OBRAZ001.BMP i tak dalej.
Tutaj zaczynają się nasze problemy - nie możemy tak poprostu bezkarnie utworzyć nowego pliku w katalogu C:\TEMP, zapisać do niego nasze dane, po czym go zamknąć. W momencie naciśnięcia kombinacji klawiszy uaktywniającej naszego rezydenta będzie przecież wykonywany inny program, który może w tej chwili sam zapisywać jakieś dane. Wtedy DOSowi zrobi się "mętlik w głowie", co doprowadzi w najlepszym przypadku do zawieszenia komputera, a możemy też uszkodzić system plików lub dokonać TSRem czegoś bardziej okrutnego. I właśnie o to chodzi, aby ominąć moment, w którym inny program korzysta z usług dosowych. Z pomocą w tej sytuacji przyjdą nam mechanizmy udostępniane przez sam system operacyjny, a mianowicie flaga INDOS - jeden bajt pamięci, który informuje nas, czy właśnie w tej chwili jest wykonywana jakaś funkcja DOSa. Adres flagi INDOS możemy uzyskać poprzez odwołanie do następującej funkcji przerwania 21h:
Nazwa: Pytanie o adres sygnalizatora pracy systemu
Wywołanie: AH=34h
Powrót: ES:BX - adres sygnalizatora pracy systemu
Opis: Funkcja zwraca adres sygnalizatora pracy systemu.
Sygnalizator ten jest ustawiony (różny od zera), gdy system
wykonuje jakąś czynność, której nie należy mu przerywać.
Sygnalizator ten jest często używany przez programy TSR,
które sprawdzają, czy mogą się uaktywnić. Sygnalizator ten
jest również ustawiony podczas czekania przez system na
naciśnięcie klawisza. W takim wypadku jest wywoływane
przerwanie 28h, które TSR może przechwycić i również w ten
sposób się uaktywniać.
Przy okazji poznaliśmy kolejny ważny aspekt programowania TSRów - pomimo że jest wykonywane przerwanie DOSa, które oczekuje na wciśnięcie klawisza, nie robiąc prócz tego nic pożytecznego, flaga INDOS jest zapalona. Ten fakt jednakże możemy wykryć poprzez sprawdzenie, czy DOS wywołuje w tym czasie przerwanie 28h (tzw. przerwanie Idle). Robimy to poprzez przechwycenie tego przerwania i podstawienia w jego miejsce swojej własnej procedury. Kiedy użytkownik naciśnie odpowiednią kombinację klawiszy, sprawdzamy, czy DOS jest w tej chwili wolny - flaga INDOS=0. W przeciwnym wypadku musimy dokonać sprawdzenia, czy jest wywoływane przerwanie 28h (w naszej procedurze obsługi tego przerwania zapalamy odpowiednią flagę aktywności). Jeżeli nie jest ono wywoływane, a DOS jest zajęty - nie możemy w tej chwili nic zrobić. Wtedy mamy kilka możliwości rozwiązania tego problemu, jak np. przepisanie zawartości ekranu (wraz z paletą) do innego bloku pamięci, który zarezerwowaliśmy przy instalacji, a przy najbliższej okazji zapisanie tego bloku na dysk (tutaj okazało by się pomocne przechwycenie również przerwania zegara - INT 08h - które będzie nam dostarczało tą "najbliższą okazję" około 18 razy na sekundę). Jednakże kto by chciał używać TSRa, który przy instalacji zabiera nam ponad 64000 bajtów cennej pamięci ? Drugim rozwiązaniem jest zaalokowanie pośredniego bloku w pamięci XMS lub EMS - ale na to przyjdzie czas w kolejnych odcinkach tego cyklu. My w naszym rezydencie wykorzystamy trzecią możliwość - po prostu nic nie zrobimy, wydając tylko krótki dźwięk z głośnika informujący o naszej bezradności. I jeszcze jedna uwaga - gdy DOS czeka na naciśnięcie klawisza wywołując co chwilę przerwanie 28h, a my z tego skorzystamy, nie możemy po uaktywnieniu rezydenta korzystać z przerwań dosowych o numerach od 00h do 0Ch.
No to mamy już ogólny zarys działania naszego TSRa: w procedurze obsługi przerwania klawiatury sprawdzamy, czy naciśnięto kombinację klawiszy LShift+LCtrl+Delete, a gdy miało to miejsce, przekazujemy do sterownika klawiatury potwierdzenie odebrania znaku i w odblokowujemy kontroler przerwań (jest to szczegółowo opisane w 4. odcinku tego kursu), po czym ustawiamy naszą wewnętrzną flagę aktywności i włączamy przerwania instrukcją: "sti". Kiedy teraz użytkownik znowu naciśnie tą kombinację klawiszy, a my jeszcze nie skończyliśmy obsługi poprzedniego naciśnięcia (czyli gdy nasza wewnętrzna flaga aktywności jest zapalona) - wtedy po prostu wychodzimy z przerwania. Dalej należy sprawdzić flagę INDOS - gdy jest zapalona to dajemy sygnał dźwiękowy informujący o naszej bezradności i również wychodzimy z przerwania, nie zapominając o zgaszeniu naszej wewnętrznej flagi aktywności. W końcu gdy wszystko się powiodło - przystępujemy do rzeczy. Tworzymy nowy zbiór w katalogu C:\TEMP (lub innym, każdy może wstawić sobie w kod źródłowy to, co chce), zapisujemy do tego zbioru stały nagłówek, czytamy paletę kolorów karty VGA do naszego obszaru roboczego o wielkości 768 bajtów, zapisujemy ją do pliku, dalej nagrywamy kolejne linie obrazu poczynając od najniższej (o adresie 0A000h:0F8C0h) aż do najwyższej (o adresie 0A000h:0), zmniejszając offset nagrywanego bloku pamięci za każdym razem o 320 bajtów (długość jednej linii). Potem tylko zamykamy plik, gasimy wewnętrzną flagę aktywności i powracamy z przerwania. Cały kod tej operacji wstawimy w naszego gotowego rezydenta, korzystającego z przerwania 2Fh (Multiplex Interrupt), opisywanego w poprzednim odcinku cyklu, pomijając tylko chwilowo nam niepotrzebną część służącą do dezaktywowania TSRa bez usuwania go z pamięci. Nasz rezydent będzie "wrażliwy" na numer procesu 91h podawany przy wywoływaniu przerwania 2Fh.
Teraz czas na kilka zagadnień nie dotyczących bezpośrednio programów rezydentnych, ale bardzo nam przydatnych. Otóż musimy wiedzieć po pierwsze, w jaki sposób sprawdzić, czy karta graficzna jest w trybie 13h. Możemy tego dokonać wywołując bezpośrednio podfunkcję 0Fh przerwania video - INT 10h:
Nazwa: Pytanie o aktualny tryb wyświetlania
Wywołanie: AH=0Fh
Powrót: AL - tryb pracy
AH - liczba znaków w wierszu
BH - numer aktywnej strony
Jednakże możemy odczytać numer trybu również bez użycia przerwań - szybciej i bezpieczniej (ten sam problem, co z przerwaniem dosowym - co będzie, gdy akurat w tym momencie główny program odwołał się do przerwania video ? Rozwiązanie problemu byłoby bardziej skomplikowane), odczytując bezpośrednio odpowiednią wartość z obszaru zmiennych BIOSu, zawartość bajtu spod adresu 0040h:0049h (czyli 0:0449h) również jest numerem aktualnego trybu pracy karty graficznej. Kolejne zagadnienie to odczytanie palety kolorów karty VGA. W przestrzeni adresowej wejścia/wyjścia (I/O) całego komputera są wydzielone porty, z których korzysta karta VGA. Mają one adresy od 3C0h do 3DFh. Aby odczytać składowe RGB jednego koloru, należy do portu 3C7h wysłać bajt z numerem koloru (0..255), a następnie z portu 3C9h odczytać po kolei 3 bajty ze składowymi: czerwoną, zieloną i niebieską. Licznik koloru jest automatycznie zwiększany o 1, możemy potem od razu odczytać składowe kolejnego koloru, już bez wpisywania jego numeru do portu 3C7h. Najszybciej można odczytać całą paletę kolorów pod adres w ES:DI przy pomocy następujących instrukcji:
xor al,al ; AL=0
mov dx,3c7h
out dx,al
mov dl,0c9h
mov cx,768 ; odczytujemy 256*3 = 768 bajtów
cld
rep insb ; z portu DX odczytaj kolejno CX bajtów i umieść pod ES:DI
Na nasze nieszczęście paleta jest zapisywana w zbiorach .BMP w bardzo przedziwny sposób - każdy kolor zajmuje w niej nie 3, ale 4 bajty - i to w kolejności: niebieski, zielony, czerwony, a 4. bajt jest równy zero. Do tego jeszcze karta VGA zwraca nam składowe kolorów z zakresu 0..63, a w pliku .BMP są zapisywane składowe z zakresu 0..255. Musimy to wszystko uwzględnić przy budowie naszego rezydenta - konkretne rozwiązanie znajdziecie w kodzie źródłowym dołączonym do tego odcinka.
Aby przy bezradności naszego rezydenta (kiedy nie możemy wykorzystywać przerwań DOSa) wydać sygnał dźwiękowy nie za długi i nie za krótki, posiłkujemy się odczytem zmiennej BIOSa zawierającą ilość taktów zegara, zwiększanej w każdym przerwaniu zegarowym (INT 08h), czyli co około 55 ms (18.2 raza na sekundę). Po prostu włączymy dźwięk, odczytamy jej zawartość, poczekamy, aż ulegnie zmianie o np. 2, po czym wyłączymy dźwięk. Sposób prosty i skuteczny. Należy tylko pamiętać o włączeniu przerwań już wcześniej, aby została wykonana procedura obsługi zegara zwiększająca licznik. No i najważniejsze: licznik mieści się w pamięci od adresu 0:046Ch i zajmuje 4 bajty, w kolejności od najmłodszego do najstarszego. W naszym przypadku wystarczy sprawdzić, czy się zmienił ten najmniej znaczący (czyli pod adresem, który podałem wyżej).
Operacje na plikach wykonujemy korzystając z usług dobrze już nam znanego przerwania DOSu - 21h. Przy otwieraniu lub tworzeniu plik identyfikowany jest przez nazwę zapisaną w ASCIIZ, natomiast przy następnych odwołaniach do już otwartego zbioru (przy zapisywaniu do niego danych, zamykaniu go) wykorzystujemy tzw. file handle (uchwyt, dojście), czyli liczbę 16-bitową określającą nam w sposób jednoznaczny, z jakim wcześniej otwieranym plikiem mamy do czynienia. Oto opisy funkcji dosowych, które nam się przydadzą:
Nazwa: Tworzenie dojścia
Wywołanie: AH=3Ch
DS:DX - adres łańcucha w kodzie ASCIIZ zawierającego nazwę
pliku
CX - atrybuty pliku
Powrót: Ustawiony znacznik C: AX - kod błędu
Nie ustawiony C: AX - numer dojścia
Opis: Funkcja tworzy plik o podanej nazwie, równocześnie definiując
doń dojście z uprawnieniami do czytania i pisania w pliku.
Nowy plik ma zerową długość i atrybuty przekazane w rejestrze
CX. Jeśli plik o podanej nazwie już instnieje to zostaje
zwolniona pamięć dyskowa mu przydzielona, nadana długość 0,
ustalone nowe atrybuty i przyporządkowane dojście z uprawn.
do czytania i pisania.
Wyjaśnienia wymaga zawartość rejestru CX ustawianego przed wywołaniem funkcji 3Ch. Atrybuty pliku są reprezentowane przez kolejne bity w dolnej połówce rejestru CX (czyli w CL), górną połówkę (CH) wypełniamy zerami:
bit: 7 6 5 4 3 2 1 0 r - Read Only
- - a d v s h r h - Hidden
s - System
v - Volume ID
d - Directory
a - archive
Widać, że przy pomocy tej funkcji możemy również utworzyć nowy katalog, zapalając w CL czwarty bit, jednakże jeżeli już istnieje taki katalog, nie ulegnie automatycznemu skasowaniu, inaczej niż to się dzieje w przypadku plików. W naszym rezydencie nowo tworzonym plikom będziemy nadawać tylko atrybut Archive - czyli do rejestru CX wpisywać wartość 0020h. Po utworzeniu pliku będziemy zwiększać jego numer - 3 ostatnie cyfry nazwy stanowią licznik. Zapisu danych do otwartego pliku dokonujemy przy pomocy funkcji 40h:
Nazwa: Pisanie przez dojście
Wywołanie: AH=40h
BX - numer dojścia
CX - liczba bajtów do zapisania
DS:DX - adres bufora
Powrót: Ustawiony znacznik C: AX - kod błędu
Nie ustawiony C: AX - liczba zapisanych bajtów
Opis: Funkcja zapisuje do pliku lub urządzenia związanego z
dojściem, którego numer jest przekazany w rejestrze BX bajty
znajdujące się w buforze, którego adres zawiera DS:DX. Liczba
bajtów do zapisania jest przekazywana w rejestrze CX. Po
zapisie wewnętrzny wskaźnik pozycjipliku jest przesuwany tak,
aby wskazywał na bajt następny po ostatnio zapisanym. W ten
sposób możliwe jest sekwencyjne zapisywanie w pliku.
Wywołanie tej funkcji z zawartością CX równą 0 powoduje
zmianę wielkości pliku na taką, jaką aktualnie wskazuje
wskaźnik pozycji.
Nazwa: Zamykanie dojścia
Wywołanie: AH=3Eh
BX - numer dojścia
Powrót: Ustawiony znacznik C: AX - kod błędu
Nie ustawiony C: OK.
Opis: Funkcja zamyka dojście o numerze przekazanym w AX i czyści
wszystkie bufory związane z plikiem.
No to właściwie posiadamy już całą wiedzę potrzebną do napisania rezydenta, którym będziemy zrzucali ekran karty VGA do pliku .BMP, należy tylko dodać, że ta metoda będzie dawała dobre rezultaty tylko w przypadku programów korzystających z "czystego" trybu 13h - 320x200 w 256 kolorach, bez żadnych "upiększeń" w stylu Xmode (podnoszenie rozdzielczości na standardowej karcie VGA poprzez zmianę trybu adresowania), z czego intensywnie korzysta większość programów demonstracyjnych i część gier. Nasze eksperymenty również nie powiodą się, gdy program przechwytuje przerwanie klawiatury i nie zwraca sterowanie do oryginalnej procedury obsługi. Wtedy możemy zainstalować rezydenta w przerwaniu zegara (INT 08h) i tam sprawdzać, czy ostatnio wciskanym klawiszem był Delete, jak również uaktualniać flagi stanu klawiszy kontrolnych na podstawie informacji o wciśnięciach/puszczeniach Alt, Ctrl i Shift. Ale to już będzie tematem innego odcinka. Podobnie ma się sprawa przy naszym uproszczeniu - w przykładowym rezydencie nie sprawdzamy, czy jest wywoływane przerwanie 28h, po stwierdzeniu zajętości DOSu (flaga INDOS<>0) tylko dajemy dźwięk naszej bezradności. Można też po prostu wykomentować lub usunąć zaznaczone w kodzie linie - flaga INDOS nie będzie w ogóle sprawdzana. To chyba już wszystko na dziś, przykładowy program jest działający i sprawdzony tylko dla kilku programów, nie działająca reszta zawiera się w przypadkach opisanych powyżej. Powodzenia w samodzielnym eksperymentowaniu.
Listing programu do piątego odcinka kursu można pobrać stąd.