W tym mini-kursie zajmiemy się sposobem pisania TSR-ów, czyli programów, które po uruchomieniu i zakończeniu pozostają w pamięci (TSR = Terminate and Stay Residend).
Pierwsze pytanie, które się nasuwa, brzmi: Po co to komu?
Główną przyczyną jest to, że chcemy coś robić w tle
, czyli pozwalając użytkownikowi
uruchamianie innych programów.
A co chcielibyśmy robić w tle
?
No cóż, DOS-owe
sterowniki (które też są TSR-ami) zajmują się wieloma sprawami, na przykład
zarządzają pamięcią (jak EMM386.EXE), kontrolują CD-ROMy czy karty dźwiękowe.
Skoro już wiemy po co, to przyszła pora, aby dowiedzieć się, jak pisać takie programy.
Otóż, jak się okazuje, nie jest to wcale takie trudne. Spójrzmy, co oferuje nam Lista Przerwań
Ralfa Brown'a
(RBIL):
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 bajtów.
Jak widać, trzeba będzie zadbać o kilka spraw:
zamknięcie ewentualnych otwartych plików.
W zwolnieniu pamięci pomoże nam funkcja:
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)
Jeśli uruchamiamy program typu .com,
to DOS domyślnie przydziela mu całą dostępną pamięć.
Będziemy zwalniać segment środowiska, adres którego znajdziemy pod ds:[2ch]. DOS sam
zwolni pamięć przydzieloną naszemu programowi po jego zakończeniu.
Jak wiemy, programy typu .com wczytywane są pod adres 100h w danym segmencie, a wcześniej
jest PSP (Program Segment Prefix),
który zawiera między innymi linię poleceń (od offsetu 80h).
W programach typu .exe (wczytywanych zwykle pod adresem 0), DS pokazuje po prostu wcześniej
niż CS (zazwyczaj DS = CS - 10h, czyli dodatkowe 10h*10h = 100h bajtów jest przed kodem).
jeśli nasz TSR przejmuje jakieś przerwanie (zazwyczaj tak właśnie będzie, bo po co pisać TSR, którego nie będzie można w żaden sposób uruchomić?), należy w swojej procedurze obsługi przerwania (Interrupt Service Routine - ISR) uruchomić starą ISR. Oprócz tego, po odinstalowaniu naszego TSR trzeba przywrócić adres starej ISR. Nie muszę chyba mówić, co by się stało, gdyby procesor chciał wykonać instrukcje pod adresem, pod którym nie wiadomo co się znajduje.
należy sprawdzić linię poleceń, z jaką uruchomiono nasz program (powiedzmy, że jeśli nic
tam nie ma, to użytkownik chce zainstalować nasz program w pamięci, zaś jeśli jest tam
literka u
lub U
, to użytkownik chce odinstalować nasz program).
Niestety, nie mam pod ręką lepszych własnych przykładów niż ten oto programik (też mój, oczywiście). Teoretycznie, w czasie dostępu do dysku twardego powinien włączyć diodę Scroll Lock na klawiaturze. Uruchamiać należy go oczywiście pod czystym DOSem. Może nie zawsze działać, ale są w nim elementy, które chciałbym omówić. Składnia dla kompilatora NASM.
; Pomysł polega na tym, aby w czasie dostępu do dysku twardego zapalać diodę ; Scroll Lock na klawiaturze. ; ; Autor: Bogdan D. ; ; nasm -O999 -o scrlck.com -f bin scrlck.asm ; ; z użyciem int 13h ; TASM: ; .model tiny ; .code org 100h start: jmp kod ; to jest kod naszej procedury int 13h. ; Zostanie on w pamięci. znacznik db "ECA135" flagi db 0 moje13h: pushf or dl,dl ; jeśli nie dysk twardy (bit7 = 0) to nie ma nas tu js dysk_ok to_nie_my: popf db 0eah ; długi skok do stare13h stare13h dd 4ch dysk_ok: ; sprawdzamy, którą komendę chce wykonać użytkownik 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 bitów musi być równa 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] ; pozwól, żeby stara procedura ; int 13h też zrobiła swoje ; flagi już są na stosie pushf push ax ; sprawdzamy, które diody były ; wcześniej 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 ; początek właściwego kodu kod: mov ax, cs mov ds, ax ; DS = CS, na wszelki wypadek xor bx, bx mov si, 80h ; ds:[80h] - liczba znaków w linii poleceń mov al, [si] mov es, bx ; ES = 0 or al, al ; liczba znaków=0? To idziemy się zainstalować jz instaluj petla: inc si ; SI = 81h, 82h, ... mov al, [si] ; sprawdzamy kolejny znak w linii poleceń cmp al, 0dh jz instaluj ; Enter = koniec linii, więc instaluj ; u lub U oznacza, że trzeba odinstalować 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 obsługi ; int 13h (może 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 ; jeśli nie ma, to nie możemy się ; odinstalować 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 się zgadzają jnz niema ; jeśli nie, to nie nasza procedura ; obsługuje int13h i nie możemy się ; odinstalować ; TASM: mov es, word ptr es:[13h*4 + 2] mov es, [es:13h*4 + 2] ; segment naszego TSRa mov ah, 49h cli ; wyłączamy przerwania, bo coś przez ; przypadek mogłoby uruchomić int 13h, ; którego adres właśnie zmieniamy int 21h ; zwalniamy segment naszego rezydenta cli ; kopiujemy adres starej procedury ; int13h z powrotem do ; Tablicy Wektorów Przerwań ; (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 użytkownika, że ; odinstalowaliśmy program mov ah, 9 int 21h mov ax, 4c00h int 21h ; wyjście bez błędu niema: ; jeśli adresy procedur int13h się ; nie zgadzają lub nie ma naszego ; znacznika, to poinformuj, że nie ; można odinstalować ; TASM: mov dx, offset nie_ma mov dx, nie_ma mov ah, 9 int 21h mov ax, 4c01h int 21h ; wyjście z kodem błędu = 1 ; zainstalowanie instaluj: ; TASM: mov es, word ptr es:[13h*4 + 2] mov es, [es:13h*4 + 2] ; ES = segment procedury obsługi ; int 13h (może naszej) ; TASM: mov di, offset znacznik mov di, znacznik mov cx, 6 mov si, di repe cmpsb ; sprawdzamy, czy nasz znacznik ; już jest w pamięci je juzjest ; jeśli tak, to drugi raz nie ; będziemy się instalować ; TASM: mov es, word ptr cs:[2ch] mov es, [cs:2ch] ; segment środowiska 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, że zainstalowano mov ah, 9 int 21h ; TASM: mov dx, offset kod mov dx, kod mov ax, 3100h shr dx, 4 ; DX=kod/16=liczba paragrafów do ; zachowania w pamięci inc dx int 21h ; int 21h, AX = 3100h - TSR juzjest: ; jeśli nasz program już jest w ; pamięci, to drugi raz się nie ; zainstalujemy ; TASM: mov dx, offset juz_jest mov dx, juz_jest mov ah, 9 int 21h mov ax, 4c02h int 21h ; wyjście z kodem błędu = 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 omówię kilka spraw, o które moglibyście zapytać:
Funkcja 31h przerwania 21h musi dostać informację, ile paragrafów (od miejsca, gdzie zaczyna się program) ma zachować w pamięci. Dlatego więc najpierw w programie zapisujemy kod rezydentny a potem resztę (instalacja / dezinstalacja), która nie będzie potem potrzebna w pamięci.
Aby upewnić się przy próbie odinstalowania, że to rzeczywiście naszą procedurę chcemy
odinstalować. Niedobrze byłoby, gdyby jakiś inny program potem przejął to przerwanie,
a my byśmy go wyrzucili z pamięci...
Treść znacznika może oczywiście być dowolna.
call dword ...
?Chodzi o to, aby najpierw zapalić Scroll Lock,
potem wykonać operację na dysku (do czego posłuży nam prawdziwa procedura int13h)
i na końcu przywrócić stan diód na klawiaturze. Użycie CALL
a nie JMP
spowoduje, że odzyskamy kontrolę po tym, jak
uruchomimy stare przerwanie. Zaś adres starego przerwania to segment i offset, czyli
razem 4 bajty (stąd: DWORD).
CS:
?Gdy jesteśmy w naszej procedurze, nie wiemy, ile wynosi DS. Wiemy, że CS pokazuje na naszą procedurę. Są więc 2 wyjścia:
Wybrałem to drugie.
Instrukcje znajdują się w moim innym kursie. Polecam.
IRET
?Interrupt Return robi tyle, co zwykły RET
, ale jeszcze
zdejmuje flagi ze stosu. Polecam opis instrukcji INT
z drugiej części mojego kursu.
ds:[80h]
?Liczba bajtów linii poleceń programu.
Od ds:[81h] maksymalnie do ds:[0ffh] (od ds:[100h] zwykle zaczyna się kod programu). Napotkanie Carriage Return (13 = 0Dh) po drodze oznacza koniec linii poleceń.
[es:13h*4]
zamiast [es:4ch]
?Czytelniejsze, bo oznacza, że chcemy adres przerwania 13h.
int 21hjest otoczone przez
CLI
?Nie chciałem ryzykować, że w chwili zmiany adresu lub zwalniania pamięci rezydenta trafi się jakieś przerwanie, które mogłoby chcieć uruchomić int13h (którego już nie ma po tym int21h lub którego adres jest niespójny - zmieniliśmy już segment, ale jeszcze nie offset itp.).
Głupio byłoby odinstalować nie swoją procedurę...
Tym bardziej, że najbliższe int13h spowodowałoby nieprzewidywalne skutki.
Nie chcę, aby program instalował się wielokrotnie, gdyż potem odzyskanie adresu starej procedury zajęłoby tyle samo dezinstalacji, co instalacji.
DS:[2ch]
?Numer segmentu pamięci, w którym trzymane są zmienne środowiskowe (jak PATH, BLASTER, i wszystkie inne ustawiane komendą SET, na przykład w pliku autoexec.bat). Możemy go zwolnić, bo dla każdego programu tworzona jest oddzielna kopia.
Jeżeli kod wystaje ponad adres podzielny przez 16, to część jego zostanie utracona. Procesor będzie wykonywał nieznane instrukcje z nieprzewidywalnym skutkiem.
Chociaż DOS jest już rzadko używany, to jednak umiejętność pisania TSR-ów może się przydać, na przykład
jeśli chcemy oszukać
jakiś program i podać mu na przykład większy/mniejszy rozmiar dysku lub coś
innego. Można też napisać DOS-owy wygaszacz ekranu jako TSR, program który będzie wydawał
dźwięki po naciśnięciu klawisza, wyświetlał czas w narożniku ekranu i wiele, wiele innych
ciekawych programów. Nawet jeśli nikomu oprócz nas się nie przydadzą lub nie spodobają, to
zawsze i tak zysk jest dla nas - nabieramy bezcennego doświadczenia i pisaniu i znajdowaniu
błędów w programach rezydentnych. Takie umiejętności mogą naprawdę się przydać, a z pewnością
nikomu nie zaszkodzą.