"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,0JNE 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 ETYKIETALOOPZ ETYKIETALOOPNE 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:
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 asemblerzePrzerwania 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
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,alout 70h,aljmp $+2in 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:
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
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 asemblerzeUsuwanie 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
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
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:
jmp StartTutaj
staraproc dd 0
; znacznik potrzebny do sprawdzenia zainstalowania TSRa:
znacznik db 'Sekundnik, odc. 3'
push ax
xor ax,ax ; segment komórki ze stanem klawiatury
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
xor di,di
xor al,al
out 70h,al
jmp $+2
...
malutky