Elementare Befehlsfolgen vereinfachen

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

💡 Sie haben einen Linkedin-Account? Dann können Sie meinen Newsletter „Der 18-Jährige, der einen Zettel schrieb und verschwand“ abonnieren ✔︎ 

Matomo