Einige Folgen von Befehlen kehren häufig wieder, da sie elementare Aufgaben bewältigen. Gerade bei diesen ist es wichtig, sie weitestgehend zu vereinfachen, um die Programme kurz zu halten.
Eine dieser Folgen lädt ein Segmentregister mit dem Wert Null, um z.B. auf die Tabelle der Interruptvektoren oder einige Systemvariablen im BIOS-Datenbereich (ab 40h:0) zugreifen zu können. Weil es der 8088 nicht erlaubt, konstante Werte in Segmentregister laden, werden dafür zumeist zwei Befehle der Art
XOR AX,AX MOV DS,AX
verwendet. Die erste Zeile ist gleichbedeutend mit »MOV AX,0«, benötigt jedoch ein Byte weniger Programmcode. Macht man sich hingegen die eben erläuterten »äußeren Umstände« bei COM-Programmen zunutze, ergeben sich gleich zwei einzeilige Methoden, um beim Programmstart den gleichen Effekt zu erreichen. Die erste davon lädt mit »POP DS« die oben auf dem Stapel liegende Null in das Segmentregister. Zu beachten ist dabei, daß die Beendigungsmethode mit »RETN« nun nicht mehr funktioniert, weil der dafür benötigte Wert ja nicht mehr auf dem Stapel liegt. Außer dem Quelltext reduziert sich dabei auch die Länge des Codes um 2-3 Byte, je nachdem, ob man die andere Methode zur Programmbeendigung berücksichtigt. Die zweite Variante funktioniert nur, wenn das Programm keine Parameter mit Laufwerksbezeichnungen erhält. Dann nämlich kann man davon ausgehen, daß das Register AX bereits den Wert 0 enthält — die Anweisung lautet nur noch »MOV DS,AX«.
Häufig ist es auch nötig, den im PSP enthaltenen Parameter als sogenannte ASCIIZ-Folge zu formatieren, weil DOS als Endekennzeichen anstelle des Wagenrücklaufs ein Nullbyte benötigt. Die kürzeste Methode, um diese Aufgabe zu lösen, besteht in den folgenden beiden Zeilen, die wir daher häufig verwendet haben:
MOV BX,[80] MOV By[BX+E081],0
Die erste Zeile besetzt dabei das Register BL mit der Länge der Kommandozeile. Gleichzeitig wird BH mit dem ersten Byte dieses Textes geladen. Da aber stets ein Leerzeichen zur Trennung zwischen Programmnamen und Parameter erforderlich ist, erhält BH den ASCII-Wert des Leerzeichens, 20h. Der indizierte Zugriff auf die Speicherstelle [BX+E081] subtrahiert bei der Adressberechnung gleichzeitig den Wert 20h von BH und verwendet als Basis die Speicherstelle 81h. Der Wagenrücklauf wird so durch eine Null ersetzt.
Andere elementare Befehlsfolgen treten bei residenten Programmen auf. Einige von ihnen (z. B. LDR.DEB im Kapitel »Festplatten und Disketten«) erlauben es, durch einen zweiten Aufruf wieder disaktiviert zu werden. Die allererste Aufgabe eines solchen Programmes ist es daher festzustellen, ob sich das Programm selbst bereits resident im Speicher befindet. Da residente Programme mindestens einen Interruptvektor verändern, um irgendwann in Aktion treten zu können, bietet es sich an, den Inhalt dieses Vektors als Kennzeichen für die erfolgte Installation des Programmes zu verwenden. Die typische Befehlsfolge für diese Aufgabenstellung lautet daher:
MOV AX,3513 ;100 Interruptvektor 13h nach ES:BX lesen INT 21 ;103 MOV AX,2513 ;105 Wert zum Setzen des Vektors MOV DX,133 ;108 auf dieses Offset soll der Vektor zeigen CMP BX,DX ;10B macht er's bereits? JE 11E ;10D dann disaktivieren INT 21 ;10F sonst neuen Interruptvektor setzen MOV [135],BX ;111 außerdem muß der alte Interruptvektor MOV [137],ES ;115 gespeichert werden MOV DX,13A ;119 das Programm resident machen INT 27 ;11C
Die Befehlsfolge ab Offset 11Eh, die das bereits installierte Programm wieder disaktiviert, lautet dann:
ES:LDS DX,[135] ;11E DS:DX mit dem gesicherten Vektor laden INT 21 ;123 und diesen Vektor setzen MOV AH,49 ;125 das residente Programm freigeben INT 21 ;127 ES:MOV ES,[2C] ;129 wichtig: auch das Environment des MOV AH,49 ;12E Programmes freigeben! INT 21 ;130 RET ;132 Programm beenden
Während die meisten Programmierer diesen Abschnitt zum Installieren einer Interruptroutine sinnvollerweise nicht im Speicher belassen, indem sie ihn an das Programmende verlagern, zu Beginn eine Sprung-Anweisung einfügen und die Endadresse entsprechend reduzieren, haben wir konsequent darauf verzichtet. Denn dies spart zwar einige zehn Bytes an Arbeitsspeicher ein, »kostet« aber zumindest eine zusätzliche Programmzeile, nämlich den erwähnten »JMP«-Befehl. In unseren Augen wiegt Ihre Zeitersparnis beim Abtippen einer Routine nämlich mehr, als diese wenigen Byte, die übrigens vom gesamten Abeitsspeicher nur den verschwindend geringen Anteil von rund 0,007 Prozent ausmachen.
Benötigt das auf diese Weise installierte Programm den gesicherten Interruptvektor, um die Routine, auf die er zuvor gezeigt hat, aufzurufen, hat es sich bewährt, das Programm »selbstmodifizierend« zu schreiben. Dabei steht an der Stelle, an der das Programm die alte Interrupt-Behandlungsroutine aufrufen will, der Befehl »JMP 0:0« oder »CALL 0:0«, je nachdem ob eine Rückkehr zum aufrufenden Programm erwünscht ist oder nicht. Vor dem CALL-Befehl muß noch die Anweisung »PUSHF« erfolgen, weil die aufgerufene Routine schließlich mit »IRET« endet und die Flags auf dem Stapel erwartet. Die aufgerufene Adresse »0:0« löst zwar beim Lesen des Programmes leichtes Unbehagen aus, hat aber durchaus ihre Berechtigung. Damit wird jedoch nicht die Interruptvektortabelle aufgerufen, wie es auf den ersten Blick scheint. Tatsächlich ist die Adresse 0:0 lediglich ein Platzhalter, der eigentlich einen beliebigen Wert enthalten darf. Er wird bei der Installation des Programmes durch den richtigen Wert, den Inhalt des Interruptvektors, ersetzt. Zur Verdeutlichung dieses Sachverhaltes setzen wir unser Musterprogramm ab Offset 133h fort:
PUSHF ;133 Flags müssen auf den Stapel CALL 0:0 ;134 ab 135 befindet sich die Adresse der Routine IRET ;139 Interrupt beendet (Leerroutine)
Vor dem Programmstart sieht der disassemblierte Programmausschnitt folgendermaßen aus:
6410:0133 9C PUSHF 6410:0134 9A00000000 CALL 0000:0000 6410:0139 CF IRET
Nach der Installation der Routine könnte sich der Abschnitt so darstellen:
6410:0133 9C PUSHF 6410:0134 9A04016422 CALL 2264:0104 6410:0139 CF IRET
Das Operandenfeld in der Anweisung »CALL 0:0« wird hier gleichzeitig als Speicher für die Adresse der Routine verwendet, die ja beim Übersetzen noch nicht feststeht. Das erspart es, diese Adresse in einer eigenen Variablen abzulegen und den CALL indirekt auszuführen, z.B. mit CALL far [135]. Daß unser Musterprogramm keine Funktion ausübt, soll uns hier nicht weiter stören. Verdeutlicht es doch die Technik residenter Programme und das optimierte Verketten von Interrupt-Routinen.
Quelle: 200 Utilities für PC-/MS-DOS von Gerhard Schild und Thomas Jannot
Dies ist eine von Unterstützern finanzierte Veröffentlichung. Um neue Beiträge zu erhalten und meine Arbeit zu fördern, werden Sie Abonnent: