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.


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