   #Start Contents

                   Pisanie programow rezydentnych (TSR-ow)

   W tym mini-kursie zajmiemy sie sposobem pisania TSR-ow, czyli
   programow, ktore po uruchomieniu i zakonczeniu pozostaja w pamieci
   (TSR = Terminate and Stay Residend).

   Pierwsze pytanie, ktore sie nasuwa, brzmi: Po co to komu?
   Glowna przyczyna jest to, ze chcemy cos robic "w tle", czyli
   pozwalajac uzytkownikowi uruchamianie innych programow.
   A co chcielibysmy robic "w tle"?
   No coz, DOS-owe sterowniki (ktore tez sa TSR-ami) zajmuja sie wieloma
   sprawami, na przyklad zarzadzaja pamiecia (jak EMM386.EXE), kontroluja
   CD-ROMy czy karty dzwiekowe.

   Skoro juz wiemy po co, to przyszla pora, aby dowiedziec sie, jak pisac
   takie programy.
   Otoz, jak sie okazuje, nie jest to wcale takie trudne. Spojrzmy, co
   oferuje nam Lista Przerwan Ralfa Brown'a (RBIL):
   (przeskocz opis int 21h, ah=31h)
        INT 21 - DOS 2+ - TERMINATE AND STAY RESIDENT
                AH = 31h
                AL = return code
                DX = number of paragraphs to keep resident
        Return: never
        Notes:  the value in DX only affects the memory block containing the
                 PSP; additional memory allocated via AH=48h is not affected
                the minimum number of paragraphs which will remain resident
                 is 11h for DOS 2.x and 06h for DOS 3.0+
                most TSRs can save some memory by releasing their environment
                 block before terminating (see #01378 at AH=26h,AH=49h)
                any open files remain open, so one should close any files
                 which will not be used before going resident; to access a
                 file which is left open from the TSR, one must switch PSP
                 segments first (see AH=50h)

   Jeden paragraf to 16 bajtow.

   Jak widac, trzeba bedzie zadbac o kilka spraw:
    1. zamkniecie ewentualnych otwartych plikow.
    2. zwolnienie nieuzywanej pamieci
       W zwolnieniu pamieci pomoze nam funkcja:
       (przeskocz opis int 21h, ah=49h)
                INT 21 - DOS 2+ - FREE MEMORY
                        AH = 49h
                        ES = segment of block to free
                Return: CF clear if successful
                        CF set on error
                        AX = error code (07h,09h)
       Jesli uruchamiamy program typu .com, to DOS domyslnie przydziela
       mu cala dostepna pamiec. Bedziemy zwalniac segment srodowiska,
       adres ktorego znajdziemy pod ds:[2ch]. DOS sam zwolni pamiec
       przydzielona naszemu programowi po jego zakonczeniu. Jak wiemy,
       programy typu .com wczytywane sa pod adres 100h w danym segmencie,
       a wczesniej jest PSP (Program Segment Prefix), ktory zawiera
       miedzy innymi linie polecen (od offsetu 80h).
       W programach typu .exe (wczytywanych zwykle pod adresem 0), DS
       pokazuje po prostu wczesniej niz CS (zazwyczaj DS = CS - 10h,
       czyli dodatkowe 10h*10h = 100h bajtow jest przed kodem).
    3. jesli nasz TSR przejmuje jakies przerwanie (zazwyczaj tak wlasnie
       bedzie, bo po co pisac TSR, ktorego nie bedzie mozna w zaden
       sposob uruchomic?), nalezy w swojej procedurze obslugi przerwania
       (Interrupt Service Routine - ISR) uruchomic stara ISR. Oprocz
       tego, po odinstalowaniu naszego TSR trzeba przywrocic adres starej
       ISR. Nie musze chyba mowic, co by sie stalo, gdyby procesor chcial
       wykonac instrukcje pod adresem, pod ktorym nie wiadomo co sie
       znajduje.
    4. nalezy sprawdzic linie polecen, z jaka uruchomiono nasz program
       (powiedzmy, ze jesli nic tam nie ma, to uzytkownik chce
       zainstalowac nasz program w pamieci, zas jesli jest tam literka
       "u" lub "U", to uzytkownik chce odinstalowac nasz program).

   Niestety, nie mam pod reka lepszych wlasnych przykladow niz ten oto
   programik (tez moj, oczywiscie). Teoretycznie, w czasie dostepu do
   dysku twardego powinien wlaczyc diode Scroll Lock na klawiaturze.
   Uruchamiac nalezy go oczywiscie pod czystym DOSem. Moze nie zawsze
   dzialac, ale sa w nim elementy, ktore chcialbym omowic. Skladnia dla
   kompilatora NASM.
   (przeskocz przykladowy program)
; Pomysl polega na tym, aby w czasie dostepu do dysku twardego zapalac diode
; Scroll Lock na klawiaturze.
;
; Autor: Bogdan D.
;
; nasm -O999 -o scrlck.com -f bin scrlck.asm
;
; z uzyciem int 13h

; TASM:
; .model tiny
; .code

org 100h

start:
        jmp     kod


; to jest kod naszej procedury int 13h.
; Zostanie on w pamieci.

znacznik        db      "ECA135"
flagi           db      0

moje13h:
        pushf
        or      dl,dl   ; jesli nie dysk twardy (bit7 = 0) to nie ma nas tu
        js      dysk_ok

to_nie_my:
        popf
        db 0eah                         ; dlugi skok do stare13h
        stare13h dd 4ch

dysk_ok:                ; sprawdzamy, ktora komende chce wykonac uzytkownik

        test    al,al                   ; reset
        je      to_my

        cmp     ah,2                    ; czytaj
        je      to_my

        cmp     ah,3                    ; pisz
        je      to_my

;       cmp     ah,5                    ; formatuj
;       je      to_my

;       cmp     ah,6                    ; formatuj
;       je      to_my

;       cmp     ah,7                    ; formatuj
;       je      to_my

        cmp     ah,0ah                  ; czytaj
        je      to_my

        cmp     ah,0bh                  ; pisz
        je      to_my

        cmp     ah,0ch                  ; szukaj
        je      to_my

        cmp     ah,0dh                  ; reset
        je      to_my

        cmp     ah,0eh                  ; czytaj bufor sektora
        je      to_my

        cmp     ah,0fh                  ; pisz bufor
        je      to_my

        cmp     ah,21h                  ; PS/1+ czytaj sektory
        je      to_my

        cmp     ah,22h                  ; PS/1+ zapisuj sektory
        jne     to_nie_my

to_my:
        push    ax

                ;bit 2 = CapsLk, bit 1 = NumLk, bit 0 = ScrlLk,
                ; reszta bitow musi byc rowna 0


        push    es
        xor     ax, ax
        mov     es, ax
; TASM:  mov    al, byte ptr es:[0417h]
        mov     al, [es:0417h]          ; 0040:0017 - BIOS Data Area,
                                        ; bajt stanu klawiatury
; TASM:  mov    cs:[flagi], al
        mov     [cs:flagi], al          ; zachowujemy w bajcie  flagi
        pop     es

        mov     al, 0edh
        out     60h, al
        mov     al, 1                   ; zapalamy ScrLck
        out     60h, al

        pop     ax

; TASM:  call dword ptr cs:[stare13h]
        call    dword [cs:stare13h]     ; pozwol, zeby stara procedura
                                        ; int 13h tez zrobila swoje
                                        ; flagi juz sa na stosie

        pushf
        push    ax

                                        ; sprawdzamy, ktore diody byly
                                        ; wczesniej zapalone
                                        ; i zapalamy je ponownie

        xor     al, al
; TASM:  test   byte ptr cs:[flagi], 01000000b
        test    byte [cs:flagi], 01000000b
        jz      nie_caps
        or      al, 4

nie_caps:
; TASM:  test   byte ptr cs:[flagi], 00100000b
        test    byte [cs:flagi], 00100000b
        jz      nie_num
        or      al, 2

nie_num:
; TASM:  test   byte ptr cs:[flagi], 00010000b
        test    byte [cs:flagi], 00010000b
        jz      koniec
        or      al, 1

koniec:

; TASM:  mov    cs:[flagi], al
        mov     [cs:flagi], al
        mov     al, 0edh
        out     60h, al
; TASM:  mov    al, cs:[flagi]
        mov     al, [cs:flagi]
        out     60h, al                 ; zapalamy diody

        pop     ax
        popf

        iret                            ; Interrupt RETurn - wychodzimy


; poczatek wlasciwego kodu

kod:
        mov     ax, cs
        mov     ds, ax          ; DS = CS, na wszelki wypadek

        xor     bx, bx

        mov     si, 80h         ; ds:[80h] - liczba znakow w linii polecen
        mov     al, [si]

        mov     es, bx          ; ES = 0

        or      al, al          ; liczba znakow=0? To idziemy sie zainstalowac
        jz      instaluj

petla:
        inc     si              ; SI = 81h, 82h, ...

        mov     al, [si]        ; sprawdzamy kolejny znak w linii polecen

        cmp     al, 0dh
        jz      instaluj        ; Enter = koniec linii, wiec instaluj

                                ; u lub  U oznacza, ze trzeba odinstalowac
        cmp     al, "u"
        je      dezinst

        cmp     al, "U"
        jne     petla

; odinstalowanie

dezinst:
; TASM:  mov    es, word ptr es:[13h*4 + 2]
        mov     es, [es:13h*4 + 2]      ; ES = segment procedury obslugi
                                        ; int 13h (moze naszej)
; TASM:  mov    di, offset znacznik
        mov     di, znacznik
        mov     cx, 6
        mov     si, di
        repe    cmpsb                   ; sprawdzamy, czy nasz znacznik jest
                                        ; na swoim miejscu
        jne     niema                   ; jesli nie ma, to nie mozemy sie
                                        ; odinstalowac

        mov     es, bx                  ; ES = 0
; TASM:  mov    es, word ptr es:[13h*4]
        mov     bx, [es:13h*4]
; TASM:  cmp    bx, offset moje13h
        cmp     bx, moje13h             ; sprawdzamy, czy offsety aktualnego
                                        ; int13h i naszego sie zgadzaja

        jnz     niema                   ; jesli nie, to nie nasza procedura
                                        ; obsluguje int13h i nie mozemy sie
                                        ; odinstalowac


; TASM:  mov    es, word ptr es:[13h*4 + 2]
        mov     es, [es:13h*4 + 2]      ; segment naszego TSRa
        mov     ah, 49h

        cli                             ; wylaczamy przerwania, bo cos przez
                                        ; przypadek mogloby uruchomic int 13h,
                                        ; ktorego adres wlasnie zmieniamy

        int     21h                     ; zwalniamy segment naszego rezydenta

        cli
                                        ; kopiujemy adres starej procedury
                                        ; int13h z powrotem do
                                        ; Tablicy Wektorow Przerwan
                                        ; (Interrupt Vector Table - IVT)

; TASM:  mov    ax, word ptr [stare13h]
        mov     ax, [stare13h]          ; AX=offset starej procedury int 13h
; TASM:  mov    bx, word ptr [stare13h+2]
        mov     bx, [stare13h+2]        ; BX=segment starej procedury int 13h

; TASM:  mov    word ptr es:[13h*4], ax
        mov     [es:13h*4], ax
; TASM:  mov    word ptr es:[13h*4+2], bx
        mov     [es:13h*4+2], bx
        sti

; TASM:  mov    dx, offset juz_niema
        mov     dx, juz_niema           ; informujemy uzytkownika, ze
                                        ; odinstalowalismy program
        mov     ah, 9
        int     21h

        mov     ax, 4c00h
        int     21h                     ; wyjscie bez bledu

niema:                                  ; jesli adresy procedur int13h sie
                                        ; nie zgadzaja lub nie ma naszego
                                        ; znacznika, to poinformuj, ze nie
                                        ; mozna odinstalowac

; TASM:  mov    dx, offset nie_ma
        mov     dx, nie_ma
        mov     ah, 9
        int     21h

        mov     ax, 4c01h
        int     21h                     ; wyjscie z kodem bledu = 1

; zainstalowanie

instaluj:
; TASM:  mov    es, word ptr es:[13h*4 + 2]
        mov     es, [es:13h*4 + 2]      ; ES = segment procedury obslugi
                                        ; int 13h (moze naszej)
; TASM:  mov    di, offset znacznik
        mov     di, znacznik
        mov     cx, 6
        mov     si, di
        repe    cmpsb                   ; sprawdzamy, czy nasz znacznik
                                        ; juz jest w pamieci
        je      juzjest                 ; jesli tak, to drugi raz nie
                                        ; bedziemy sie instalowac

; TASM:  mov    es, word ptr cs:[2ch]
        mov     es, [cs:2ch]            ; segment srodowiska
        mov     ah, 49h
        int     21h                     ; zwalniamy


        mov     es, bx                  ; ES = 0
; TASM:  mov    ax, word ptr es:[13h*4]
        mov     ax, [es:13h*4]          ; AX=offset starej procedury int 13h
; TASM:  mov    bx, word ptr es:[13h*4+2]
        mov     bx, [es:13h*4 + 2]      ; BX=segment starej procedury int 13h

                                        ; zachowujemy adres i segment:
; TASM:  mov    word ptr [stare13h], ax
        mov     [stare13h], ax
; TASM:  mov    word ptr [stare13h+2], bx
        mov     [stare13h+2], bx

                                        ; zapisujemy nowy adres i
                                        ; segment do IVT
        cli
; TASM:  mov    word ptr es:[13h*4], offset moje13h
        mov     word [es:13h*4], moje13h
; TASM:  mov    word ptr es:[13h*4 + 2], cs
        mov     [es:13h*4 + 2], cs
        sti

; TASM: mov     dx, offset zainst
        mov     dx, zainst              ; informujemy, ze zainstalowano
        mov     ah, 9
        int     21h

; TASM:  mov    dx, offset kod
        mov     dx, kod
        mov     ax, 3100h
        shr     dx, 4                   ; DX=kod/16=liczba paragrafow do
                                        ; zachowania w pamieci
        inc     dx
        int     21h                     ; int 21h, AX = 3100h - TSR

juzjest:                                ; jesli nasz program juz jest w
                                        ; pamieci, to drugi raz sie nie
                                        ; zainstalujemy
; TASM:  mov    dx, offset juz_jest
        mov     dx, juz_jest
        mov     ah, 9
        int     21h

        mov     ax, 4c02h
        int     21h                     ; wyjscie z kodem bledu = 2


nie_ma          db "Programu nie ma w pamieci.$"
juz_niema       db "Program odinstalowano.$"
juz_jest        db "Program juz zainstalowany.$"
zainst          db "Program zainstalowano.$"

; TASM: end start

   Teraz omowie kilka spraw, o ktore moglibyscie zapytac:
     * Zaraz po starcie jest skok do kodu. Dlaczego?
       Funkcja 31h przerwania 21h musi dostac informacje, ile paragrafow
       (od miejsca, gdzie zaczyna sie program) ma zachowac w pamieci.
       Dlatego wiec najpierw w programie zapisujemy kod rezydentny a
       potem reszte (instalacja / dezinstalacja), ktora nie bedzie potem
       potrzebna w pamieci.
     * Po co ten znacznik?
       Aby upewnic sie przy probie odinstalowania, ze to rzeczywiscie
       nasza procedure chcemy odinstalowac. Niedobrze byloby, gdyby jakis
       inny program potem przejal to przerwanie, a my bysmy go wyrzucili
       z pamieci...
       Tresc znacznika moze oczywiscie byc dowolna.
     * Czemu uruchomienie starej procedury jest w srodku naszej (a nie na
       poczatku czy na koncu) i czemu jest postaci call dword ... ?
       Chodzi o to, aby najpierw zapalic Scroll Lock, potem wykonac
       operacje na dysku (do czego posluzy nam prawdziwa procedura
       int13h) i na koncu przywrocic stan diod na klawiaturze. Uzycie
       CALL a nie JMP spowoduje, ze odzyskamy kontrole po tym, jak
       uruchomimy stare przerwanie. Zas adres starego przerwania to
       segment i offset, czyli razem 4 bajty (stad: DWORD).
     * Czemu wszedzie jest CS: ?
       Gdy jestesmy w naszej procedurze, nie wiemy, ile wynosi DS. Wiemy,
       ze CS pokazuje na nasza procedure. Sa wiec 2 wyjscia:
          + Zachowac DS na stosie, po czym zmienic go na nasz segment
          + Zamiast nieznanego DS, uzywac znanego CS
       Wybralem to drugie.
     * Gdzie sie dowiedziec, jak zapalac diody na klawiaturze?
       Instrukcje znajduja sie w moim innym kursie. Polecam.
     * Co robi instrukcja IRET?
       Interrupt Return robi tyle, co zwykly RET, ale jeszcze zdejmuje
       flagi ze stosu. Polecam opis instrukcji INT z drugiej czesci
       mojego kursu.
     * Co znajduje sie pod ds:[80h] ?
       Liczba bajtow linii polecen programu.
     * Gdzie znajduje sie linia polecen programu?
       Od ds:[81h] maksymalnie do ds:[0ffh] (od ds:[100h] zwykle zaczyna
       sie kod programu). Napotkanie Carriage Return (13 = 0Dh) po drodze
       oznacza koniec linii polecen.
     * Czemu w kodzie jest [es:13h*4] zamiast [es:4ch] ?
       Czytelniejsze, bo oznacza, ze chcemy adres przerwania 13h.
     * Czemu "int 21h" jest otoczone przez CLI?
       Nie chcialem ryzykowac, ze w chwili zmiany adresu lub zwalniania
       pamieci rezydenta trafi sie jakies przerwanie, ktore mogloby
       chciec uruchomic int13h (ktorego juz nie ma po tym int21h lub
       ktorego adres jest niespojny - zmienilismy juz segment, ale
       jeszcze nie offset itp.).
     * Czemu program sprawdza znacznik itp. przy dezinstalacji ?
       Glupio byloby odinstalowac nie swoja procedure...
       Tym bardziej, ze najblizsze int13h spowodowaloby nieprzewidywalne
       skutki.
     * Czemu program sprawdza znacznik przy instalacji ?
       Nie chce, aby program instalowal sie wielokrotnie, gdyz potem
       odzyskanie adresu starej procedury zajeloby tyle samo
       dezinstalacji, co instalacji.
     * Co znajduje sie w DS:[2ch] ?
       Numer segmentu pamieci, w ktorym trzymane sa zmienne srodowiskowe
       (jak PATH, BLASTER, i wszystkie inne ustawiane komenda SET, na
       przyklad w pliku autoexec.bat). Mozemy go zwolnic, bo dla kazdego
       programu tworzona jest oddzielna kopia.
     * Paragraf to 16 bajtow, wiec dzielimy DX przez 16. Ale czemu
       dodajemy 1?
       Jezeli kod wystaje ponad adres podzielny przez 16, to czesc jego
       zostanie utracona. Procesor bedzie wykonywal nieznane instrukcje z
       nieprzewidywalnym skutkiem.

   Chociaz DOS jest juz rzadko uzywany, to jednak umiejetnosc pisania
   TSR-ow moze sie przydac, na przyklad jesli chcemy "oszukac" jakis
   program i podac mu na przyklad wiekszy/mniejszy rozmiar dysku lub cos
   innego. Mozna tez napisac DOS-owy wygaszacz ekranu jako TSR, program
   ktory bedzie wydawal dzwieki po nacisnieciu klawisza, wyswietlal czas
   w narozniku ekranu i wiele, wiele innych ciekawych programow. Nawet
   jesli nikomu oprocz nas sie nie przydadza lub nie spodobaja, to zawsze
   i tak zysk jest dla nas - nabieramy bezcennego doswiadczenia i pisaniu
   i znajdowaniu bledow w programach rezydentnych. Takie umiejetnosci
   moga naprawde sie przydac, a z pewnoscia nikomu nie zaszkodza.

   Spis tresci off-line (klawisz dostepu 1)
   Spis tresci on-line (klawisz dostepu 2)
   Ulatwienia dla niepelnosprawnych (klawisz dostepu 0)
