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.

Powrót na główną stronę kursu.