Pisanie programów rezydentnych (TSR-ów)

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):


(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 bajtów.

Jak widać, trzeba będzie zadbać o kilka spraw:

  1. zamknięcie ewentualnych otwartych plików.

  2. zwolnienie nieużywanej pamięci

    W zwolnieniu pamięci pomoże 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)

    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).

  3. 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.

  4. 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.


(przeskocz przykładowy program)
; 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ć:

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ą.



Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)