Adok's Way to Assembler
Folge 7

Begleit-Dateien:


Yo...  welcome zur ersten Kursfolge für Fortgeschrittene. :) Heute stürzen wir
uns  in das unebene und wilde Land der TSRs. Also nix wie los! Sturzhelm nicht
vergessen!

+++ Was versteht man unter TSR? +++

TSR  ist  die Abkürzung für Terminate and  Stay  Resident. Bei TSRs handelt es
sich   also  um  speicherresidente  Programme.   Die  Grundidee  hinter  einem
speicherresidenten  Programm ist es, sich  in einen Interrupt einzuklinken und
so im Hintergrund anderer Programme zu arbeiten. Auf diese Weise entsteht eine
Art Multitasking unter DOS. Das Einsatzgebiet der TSR-Programmierung liegt vor
allem  bei  Treibern  -  jeder  Treiber  unter  DOS  ist  ein  TSR.  Ihr seht,
TSR-Programme  sind  für  die Benutzung von  DOS  lebenswichtig. Aber auch bei
manchen  Utilities und den meisten Viren  handelt es sich um speicherresidente
Programme.

Wie  programmiert  man  nun eigene TSRs?  Allgemein  gesagt, muß man dazu drei
Schritte tun:

1. Man  schreibt  die  Prozedur  (Interrupt-Handler),  die  in einen Interrupt
   eingeklinkt werden  soll  (auf  die  ein  Interrupt-Vektor  verbogen werden
   soll);
2. man verbiegt den gewünschten Interrupt-Vektor auf den Interrupt-Handler;
3. man berechnet  die Anzahl  der Paragraphen  (ein Paragraph = 16 Bytes), die
   resident im Speicher bleiben sollen, und beendet das TSR-Programm mit einer
   speziellen Funktion des Interrupts 21h.

Klingt  einfach, nicht wahr? :) Nun, setzen  wir die Theorie in die Praxis um!
Hier  ein  Beispielprogramm,  das ich im  folgenden  Zeile für Zeile erläutern
werde:

.MODEL TINY              ;Speichermodell für COM
CODE SEGMENT             ;Beginn Code-Seg
ASSUME CS:CODE, DS:CODE  ;CS und DS auf Code-Seg
ORG 100h                 ;Startadresse COM

beginning:               ;Startlabel
 JMP install             ;zur Installation springen

 INTPROC PROC FAR        ;neuer INT-5-Handler
   STI                   ;Interrupts zulassen
   IRET                  ;zurückkehren
 last_byte:              ;letztes Byte des Handlers
 INTPROC ENDP            ;Ende des Handlers

;Installation des Interrupt-Handlers
 install:                ;Label install
  MOV DX,OFFSET INTPROC  ;Offset nach DX (Segment steht in DS)
  MOV AX,2505h           ;Funktion 25h, Interrupt-Vektor 5
  CLI                    ;Interrupts sperren
  INT 21h                ;auf die eigene Routine umleiten
  STI                    ;Interrupts zulassen

;Programm speicherresident beenden
  MOV DX,OFFSET last_byte;Anz. Bytes, die resident bleiben
  SHR DX,4               ;in Paragraphen umwandeln
  INC DX                 ;DX zur Sicherheit erhöhen
  MOV AX,3100h           ;Programm resident beenden
  INT 21h                ;
CODE ENDS                ;Ende Code-Seg
END beginning            ;Ende des Programms

Erzeugt  aus diesem Programm eine COM-Datei,  startet sie und probiert einmal,
die   Taste   Print  Screen  zu   drücken.   Was  passiert?  Richtig:  nichts!
Normalerweise wird mit einem Druck auf Print Screen der Inhalt des Bildschirms
auf  dem Drucker ausgegeben. Für diesen Vorgang ist der Interrupt 5 zuständig.
Obiges  Beispielprogramm  verbog jedoch den  Interrupt-Vektor des Interrupts 5
auf  einen  eigenen Interrupt-Handler. Dieser enthält  nur die Befehle STI und
IRET.  Falls  ihr  es  schon  vergessen  haben  solltet:  STI  dient dazu, das
Interrupt-Flag  zu  setzen,  so  daß  eventuell  gesperrte  Interrupts  wieder
zugelassen    werden.   Neu   ist   IRET.    Dieses   Mnemoniclein   wird   in
Interrupt-Handlern anstelle von RET verwendet.

OK,  dann  gehen wir mal Zeile für  Zeile das Programm durch... aber nicht von
Anfang   an,   denn  der  Anfang  sollte   mittlerweile   ja  schon  mehr  als
selbstverständlich sein.

Zeile 7:      Mit JMP install  überspringen wir den Interrupt-Handler,  der ja
              erst nach der Installation des Programms ausgeführt werden soll,
              und begeben uns zur Installationsroutine, also zu jener Routine,
              in der der Vektor von INT 5 auf unseren Handler verbogen wird.
Zeilen 9-13:  Jeder Interrupt-Handler  ist eine FAR-Prozedur,  wie wir sie  in
              der vorigen Folge durchgenommen haben.  Am Anfang steht der Kopf
              der Prozedur.  Die nächsten  zwei Zeilen  sind  der  Inhalt  des
              Interrupt-Handlers. Danach folgt ein Label, welches das Ende der
              Prozedur darstellt.  Wir werden es für die Berechnung der Anzahl
              der Paragraphen,  die resident  bleiben sollen,  benötigen.  Zum
              Schluß kommt der Fuß der Prozedur.  Wichtig zu erwähnen ist, daß
              Interrupt-Handler mit IRET anstelle von RET abgeschlossen werden
              müssen.
Zeilen 17-22: Die Installationsroutine! Wir verbiegen den Vektor von INT 5 auf
              unseren  Handler mit Hilfe von INT 21h,  Fkt. 25h. In DS muß das
              Segment  und  in  DX  der  Offset  des  Handlers  stehen.  Da in
              COM-Dateien  das  DS-Register  automatisch  auf  das Codesegment
              zeigt,  müssen  wir  uns  nicht  zusätzliche  Tipparbeit  antun.
              Außerdem  muß  in  AL  die  Nummer  des  zu verbiegenden Vektors
              übergeben werden. Vor dem Aufruf von INT 21h sperren wir mit CLI
              alle  übrigen  Interrupts,  um sicherzugehen,  daß  INT  5 nicht
              versehentlich während des Verbiegens aufgerufen werden kann, was
              sonst  zu  einem  Chaos  führen  würde.  Danach  lassen  wir die
              Interrupts mit STI wieder zu.
Zeilen 25-29: Nun machen wir das Proggy zu einem TSR! INT 21h, Fkt. 31h dient,
              wie  Fkt. 4Ch,  zum Beenden  des Programms.  Im  Unterschied  zu
              Fkt. 4Ch wird dabei allerdings nicht der komplette Speicher, den
              das   Programm   eingenommen  hat,   freigegeben.   Man  muß  im
              DX-Register angeben,  wie viele  Paragraphen  nach  dem  Beenden
              resident bleiben,  also nicht  freigegeben  werden  sollen. Hier
              kommt  das  Label  last_byte  ins  Spiel!   Wir  wollen  ja  den
              Speicherbereich, den unser Interrupt-Handler einnimmt,  resident
              machen. Also weisen wir DX den Offset des Ende des Handlers zu -
              wir weisen DX den Offset  von last_byte zu.  Nun haben wir in DX
              die Anzahl der Bytes, die resident bleiben müssen.  Funktion 31h
              erwartet  ihren  Parameter  jedoch  nicht  in Bytes,  sondern in
              Paragraphen.  Da ein Paragraph 16 Bytes entspricht,  shiften wir
              DX um 4 nach rechts.  (Ihr erinnert euch?)  Abschließend erhöhen
              wir DX zur Sicherheit um 1,  denn sonst  könnte es sein,  daß zu
              wenig Speicher resident gemacht wird  und das Programm abstürzt.
              (Beispiel:  18 geteilt  durch 16  ergibt,  da es  sich  um ganze
              Zahlen handelt, 1.  Um 18 Bytes  resident zu machen,  müssen wir
              jedoch  nicht  einen,   sondern  zwei  Paragraphen  an  Fkt. 31h
              übergeben.)  Nun werden noch die Funktionsnummer ins AH-Register
              geschrieben und der Interrupt gecallt - juchu! Fertig!

Dieses  Programm könnt ihr als Gerüst  für eigene TSRs verwenden. Umfangreiche
TSRs  bestehen  natürlich  nicht  nur aus  STI  und  IRET, sondern aus bunten,
hüpfenden Menüpünktchen und ähnlichem Zeug. Jedenfalls, wer etwas Vernünftiges
schreiben  will,  will  sicherlich  auch  wissen,  wie  man den ursprünglichen
Standard wiederherstellen kann, sprich: wie man einen Interrupt-Vektor auf den
Original-Handler setzen kann. Nichts leichter als das!

+++ Ermittlung des Original-Handlers +++

Zu  diesem  Zweck  benutzt  man INT 21h, Fkt.  35h.  In  AL muß die Nummer des
Interrupt-Vektors  übergeben  werden.  Nach dem Aufruf  von  INT 21h, Fkt. 35h
findet man in ES das Segment und in BX den Offset des gesuchten Handlers vor.

Das  Ermitteln  des  Original-Handlers ist auch  dann  notwendig, wenn man ein
Programm  schreiben  will,  das  per  Hotkey  "aufgeklappt"  werden soll. Dazu
verbiegt  man  den Tastatur-Interrupt (INT 9)  auf eine eigene Routine, welche
abfragt,  ob  dieser Hotkey gedrückt wurde.  Falls  nein, ruft die Routine den
Original-Handler auf.

+++ Verwenden von DOS-Funktionen in TSRs +++

Das  Verwenden von DOS-Funktionen in  Interrupt-Handlern ist leider nicht ohne
weiteres  möglich.  Wenn  nämlich  sowohl  das  Betriebssystem  als  auch  ein
TSR-Programm   gleichzeitig   eine   DOS-Funktion   aufrufen,   gibt's   einen
Systemabsturz.  Zum Glück gibt es auch für dieses Problem eine Lösung: Mit INT
21h, Fkt. 34h kann man die Adresse des INDOS-Flags ermitteln. Ihr Segment wird
in ES und ihr Offset in BX zurückgegeben. Solange das INDOS-Flag einen anderen
Wert  als 0 hat, wird gerade eine  DOS-Funktion ausgeführt. Was macht also der
Coder?   Im   nicht-residenten   Teil  des   TSRs   (dem   Installations-  und
Beendigungsteil) ermittelt er die Adresse des INDOS-Flags und speichert sie in
Variablen  ab. Wichtig: Die Variablen müssen zwischen JMP install und dem Kopf
des  Interrupt-Handlers  definiert  sein, um  sicherzugehen,  daß sie resident
bleiben!  Im  Interrupt-Handler selbst wird  vor Aufruf einer DOS-Funktion das
INDOS-Flag  abgefragt.  Ist  es  ungleich  Null,  wird  das  TSR  nicht weiter
ausgeführt.

 mov ax,word ptr indos_seg
 mov es,ax
 mov bx,word ptr indos_ofs
 cmp es:[bx],0
 jne ende

Alles klar? Gut! Dann viel Spaß mit den TSRs! Euer Adok.