#315 – Befehle unter DOS 3.30 zurückholen – epic fail

Vorwort: Der Titel verrät es bereits – heute hat mal wieder alles prima funktioniert! 😛

Es ist eines dieser Themen, bei denen ich unverblümt, geradezu naiv an die Sache herangegangen bin, nur um mit jedem weiteren Rückschlag feststellen zu müssen, dass man manchmal einfach auch etwas „gut sein lassen“ können muss. Ihr wisst schon, Lebenszeit und so. 😉

Heute tauchen wir in die Untiefen von MS-DOS-und BIOS-Interrupts ab und wagen uns sogar an ein paar Codeschnipsel Assembler. Wow, na das klingt ja spannend – nicht! 😛

Bevor ihr jetzt direkt den Browser wieder schließt: Lasst euch von dem – zugegeben etwas trockenen – Thema nicht abschrecken. Ich hoffe, dass der Beitrag trotzdem für den ein oder anderen interessant ist und wie immer habe ich auch versucht, das ganze möglichst zugänglich zu schreiben, also so, dass man auch als „normaler Mensch“ (ohne IT-Dachschaden) durchblicken kann. 😉

In diesem Sinne – fest anschnallen, Taucherbrillen aufsetzen, tief Luft holen und los geht’s!

********

Nachdem wir den Philips-PC erfolgreich zum Laufen gebracht haben, wird es Zeit, dass wir uns einem neuen Thema widmen. Na ja, zumindest so halb! 😉

Bei der Installation von Spielen auf dem betagten PC ist mir aufgefallen, dass sich die Befehle nicht „wiederholen“ lassen. Also damit meine ich, dass sich der jeweils letzte durchgeführte Befehl per Knopfdruck auf die „Pfeil-nach-oben-Taste“ nicht zurückholen lässt. Es passiert einfach nichts:

Komisch, bei allen bisher von mir genutzten MS-DOS-PCs war das aber so? Hier z.B. der Vergleich mit dem Tema TC-PC aus Artikel 272. Gibt man dort einen Befehl ein, lässt dieser sich bequem über die obere Pfeiltaste zurückholen:

Damit sich MS-DOS die Befehle merkt und man sie jederzeit zurückholen kann, wird eine Datei namens „DOSKEY.COM“ benötigt. Seit MS-DOS-Version 5.0 wird das Programm standardmäßig ausgeliefert und mitinstalliert. Lädt man es beim Systemstart (AUTOEXEC.BAT) oder startet es manuell über die Eingabe des Befehls „doskey“, stehen einem die Rückholfunktion sowie weitere Features (wie z.B. das Anzeigen der letzten Befehle über die Taste „F7“) zur Verfügung.

Spätestens jetzt ist klar, warum das Zurückholen der Befehle im Fall des Philips-PCs nicht funktioniert. Tja, hätten wir uns mal für eine aktuellere DOS-Version entschieden. Aber nein – der Herr retrololo wollte mal wieder nicht hören und hat stattdessen das altertümliche (und eher selten eingesetzte) MS-DOS 3.31 installiert.

Ach, kein Problem. Dann kopieren wir uns einfach DOSKEY von MS-DOS 5 und lassen es auf dem Philips-PC laufen, oder? Fehlanzeige. Beim Versuch das Programm manuell zu starten, bekommen wir die Quittung: Die alte Betriebssystemversion wird tatsächlich nicht unterstützt, jetzt haben wir den Salat!

Na, so was blödes aber auch. Wie kapiert dieses DOSKEY-Programm eigentlich, dass es auf einem veralteten System läuft? Um das herauszufinden, müssen wir uns ansehen, wie DOS-Anwendungen mit dem Betriebssystem interagieren und Informationen (u.a. die verwendete Systemversion) abfragen. In Artikel 285 haben wir uns ja bereits etwas in die kryptische Welt der MS-DOS-Interrupts eingearbeitet. Einfach gesagt bietet MS-DOS eine Reihe von „APIs“ (genannt Interrupts), welche Zugriff auf Hardware- und Systemkomponenten ermöglichen.

Der vermutlich interessanteste Interrupt ist „21h“ (DOS Function Dispatcher), denn über ihn können mehr als hundert verschiedene Systemfunktionen angezapft werden. Mit der Funktion „30h“ gibt es auch tatsächlich eine Methode, welche Programmen die MS-DOS-Version zurückgibt. Na, das klingt ja genau nach dem, was wir suchen!

Um auszuprobieren, wie so etwas in der Praxis aussieht, sollten wir ein kleines Assembler-Programm schreiben, welches die Version ermittelt und auf dem Bildschirm ausgibt. Ungefähr so:

Alles klar soweit? Hat jeder alles verstanden? 😛 Okay, ich gebe es ja zu: Bei einem ersten Blick auf diesen Zahlen- und Buchstabenkauderwelsch versteht man definitiv nichts. Keine Panik – das geht auch mir so. Auch ich habe nie wirklich Assembler gelernt und musste mich entsprechend mühsam an die Lösung herantasten. Das was ihr auf dem Bild seht, ist letztendlich das Ergebnis vieler Stunden Arbeit, welche mit dem Wälzen veralteter Dokumentationen, diversen Recherchen im Internet und natürlich einer Vielzahl an missglückten Versuchen verbracht wurden. Hier haben wir das gleiche Programm nochmal mit ein paar Kommentaren – vielleicht wird so schon einiges klarer:

Fun Fact: Ein paar Interrupt-Funktionen (z.B. „4C“) haben wir bereits in Artikel 285 kennengelernt.

Immer noch zu kryptisch? Kein Problem, dann versuchen wir es mit Prosa. Im Endeffekt wird bereits in den Zeilen 5 und 6 die MS-DOS-Version abgefragt. Dafür müssen wir das obere Byte des zwei Byte großen Registers „A“ (eine Art permanent verfügbare Variable, welche auch als Übergabebereich für Funktionen verwendet wird) mit dem gewünschten Funktionswert („30“ hexadezimal) bestücken. Um die Betriebssystemversion dann auch wirklich vom System zu erhalten, wird im Anschluss das Unterprogramm „int 21h“ aufgerufen. Danach steht uns die Versionskennung als Rückgabewert im Register A zur Verfügung. Im oberen, linken Byte (AH) finden wir die Nebenversionsnummer (Minor) und im unteren, rechten Byte (AL) steht die Hauptversion (Major).

Fun Fact: Ist die Betriebssystemversion z.B. 3.31 bekommen wir im unteren Teil des Registers eine „3“ (Hauptversion) und im oberen Teil „1F“ (Nebenversion) geliefert. Nanu? Aber wieso 1F? Tja, das Ergebnis wird im hexadezimalen Zahlensystem zurück gegeben und „1F“ hexadezimal entspricht einer „31“ im dezimalen Zahlensystem. Puh, ist das alles kompliziert! 😀

Alles was danach kommt ist eigentlich nur noch „Druckaufbereitung“, also das „Zerhackstückeln“ der erhaltenen Daten in ein Format, welches wir mit weiteren Aufrufen des Interrupt-Handlers auf dem Bildschirm ausgeben können. Leider ist das nicht ganz trivial und wir müssen dafür ein paar echt kryptische Tricks anwenden. Alles zu erklären wäre zu viel, aber hättet ihr z.B. erraten, dass wir die zweistellige Nebenversionsnummer durch 10 teilen und anschließend den Quotienten sowie den Rest ausgeben müssen, um die Zeichen „3“ und „1“ auf den Bildschirm gezaubert zu bekommen? Oder hättet ihr gewusst, dass man zu einer Ziffer (z.B. „3“) den Wert „30“ (hexadezimal) addieren muss, um daraus ein ASCII-Zeichen zu machen? Ich wusste das nicht. Entsprechend lang hat es gedauert, bis das Programm fertig war! xD

Uff, wollten wir nicht eigentlich „nur“ eine Versionsnummer ausgeben? Ist ja schon gut, ich quäle euch nicht weiter mit langweiligem Assemblercode. 😉 Stattdessen wandeln wir den Quelltext lieber schnell mit dem Assembler „NASM“ (Befehl „nasm -f bin -o first.com first.asm“) in Maschinencode um. Daraus ergibt sich ein COM-Programm, welches wir direkt unter MS-DOS ausführen können.

Fun Fact: Zur Erinnerung: Im Gegensatz zu EXE-Dateien unterliegen COM-Dateien keinem bestimmten Format und enthalten lediglich Maschinenbefehle. Gerade für kleine Programme lässt sich ein COM-File wesentlich einfacher erstellen, als eine EXE-Datei samt Header, Ladeanweisungen, Sektionen, etc..

Und was soll man sagen – tatsächlich funktioniert das Programm – sei es nun in der DOSBox oder auf dem Philips-PC – und wir können uns damit die Versionsnummer des Betriebssystems anzeigen lassen. Ich weiß – im Endeffekt zeigt dieses Stück Software nur ein paar Zahlen an, aber ich kann euch gar nicht sagen, wie sehr ich mich darüber freue, das geschafft zu haben! 🙂

Hm, ok, aber wozu war das gut? Wollten wir nicht eigentlich der Frage nachgehen, wieso das DOSKEY-Programm nicht unter MS-DOS 3.31 läuft? Stimmt, da war was! 😀 Die Entwicklung unseres Beispielprogramms war aber nicht umsonst, denn mit Hilfe von NASM, bzw. „NDISASM“ lässt sich unser Programm auch disassemblieren. Dabei wird der Maschinencode wieder in für den Menschen „lesbaren“ Assembler-Code umgewandelt.

Schön dabei ist, dass neben den Assembler-Befehlen auch der Maschinencode mit ausgedruckt wird. Dadurch wissen wir jetzt, dass der Befehl „mov ah,0x30“, welcher ja den Übergabeparameter zur Abfrage der DOS-Version befüllt, den Maschinencode „B430“ hexadezimal besitzt.

Diese Information können wir nutzen, um das „DOSKEY.COM“-Programm etwas genauer unter die Lupe zu nehmen. Dazu öffnen wir die COM-Datei in einem Hexeditor. In dem assemblierten Programm gibt es tatsächlich nur eine Stelle, an welcher der Hexwert „B430“ vorkommt. Ich würde vermuten, dass hier die Systemversion, unter welcher DOSKEY gestartet wurde, ausgelesen wird:

Und wir können sogar noch mehr herausfinden! Nach dem Auslesen der Systemversion wird durch einen Vergleich des Inhalts von Register A mit dem Wert 5 (CMP AX,0x5) geprüft, ob es sich um Version 5.0 von MS-DOS handelt. Ist das nicht der Fall, wird mit einem anschließenden „Jump if Zero“ (JZ) in eine Fehlerroutine gesprungen, in welcher die Meldung „Falsche DOS-Version“ ausgegeben und das Programm letztendlich abgebrochen wird.

Not so fun Fact: Spätestens hier muss man den x86-Befehlssatz zur Hand nehmen, um anhand der Hexwerte der Befehle herauszufinden, was welcher Wert bedeutet. Dieses „Reverse Engineering“ von Software anhand von Assembler Code ist komplex, hat aber irgendwie was! 🙂

Wieder so viel Theorie. Und wie hilft uns das jetzt weiter? Nun, wir können einfach mal probieren, das Programm mit einem Befehl zu hacken! 😉 Die Idee ist, dass – unabhängig davon, welche Systemversion ermittelt wird – DOSKEY nie in die Prüfroutine zur Auswertung der Versionsnummer einsteigt. Dafür tauschen wir den „JZ“ (hexadezimal 74) durch einen „JMP“-Befehl (Hex „EB“) aus:

Not so fun Fact: Aufmerksamen Lesern wird es aufgefallen sein: Wozu haben wir das Assembler-Programm zur Versionsermittlung erstellt? Im Endeffekt hätte man auch das DOSKEY-Programm von Anfang an mit einem Disassembler auflösen oder – noch besser – direkt nach dem Befehl „mov ah,0x30“ (‚B430‘x) durchsuchen können. Stimmt, ihr Schlaumeier, aber wo bleibt da der Spaß? 😛

Und? Hat es funktioniert? Nun, zumindest haben wir wohl den Versionscheck überwunden, denn das Programm lässt sich jetzt tatsächlich auf dem Philips-PC unter DOS 3.31 starten, ohne sich über eine falsche Betriebssystemversion zu beschweren – geil! 😀

Das war es dann aber auch schon mit den guten Nachrichten, denn leider funktioniert die Rückholfunktion nicht und das System verhält sich so, als wäre DOSKEY nicht installiert, bzw. gestartet. Es passiert einfach nichts. Mist.

Aber woran könnte das liegen? Im Endeffekt klinkt sich DOSKEY ziemlich tief in das System ein und verwendet, bzw. erweitert die gleichen Systemroutinen, welche auch der Befehlszeileninterpreter „COMMAND.COM“ nutzt. Somit vermute ich, dass das Programm zur korrekten Funktion eine neuere Version des Befehlszeileninterpreters benötigt – nämlich die von MS-DOS 5.0. Klar, könnten wir die COMMAND.COM jetzt von einer MS-DOS 5.0-Installation kopieren, aber auch das Programm würde mit dem Fehler „Falsche DOS-Version“ aussteigen. Glaubt mir, ich habe es versucht! 😀

Im Endeffekt bedeutet das, dass wir wohl auch die „COMMAND.COM“ hacken müssten, sodass sie den Versionscheck übergeht. Allerdings wäre das wesentlich aufwändiger – und vermutlich nicht von Erfolg gekrönt, da die Datei ca. zehnmal so groß wie das DOSKEY-Programm ist und noch tiefer im System hängt. Will man da wirklich hin langen? Und selbst wenn wir die Versionsabfrage überlisten, ist es sehr wahrscheinlich, dass einfach ein paar Systemroutinen unter der alten DOS-Version fehlen oder gar nicht lauffähig sind. Das klingt irgendwie nach einer Sackgasse. Und jetzt?

Wir kommen wohl nicht drum herum, selbst was zu programmieren. Um ehrlich zu sein, ist ein Programm, welches prüft, ob die „Pfeil-nach-oben-Taste“ gedrückt wurde und anschließend den zuvor eingegebenen Befehl ausgibt, gar nicht so schwer zu realisieren. Gerade die Abfrage der gedrückten Taste gestaltet sich deutlich einfacher als gedacht:

Im Endeffekt muss lediglich über den BIOS-Interrupt „int16“ (Funktion 0) die gedrückte Taste eingelesen und anschließend mit dem Wert „72“ (hexadezimal „48“), welcher für „Pfeil-nach-oben“ steht, verglichen werden. Die Schwierigkeit besteht darin, für den einzulesenden Befehl einen Speicherbereich zur Verfügung zu stellen und diesen im Anschluss wieder mit der Funktion „9“auf der Konsole auszugeben. Es hat mich einige Anläufe gekostet den folgenden Code zu erstellen:

Fun Fact: In dem Programm habe ich eine „CRLF“-Variable definiert, welche letztendlich eine neue Zeile durch die Eingabe der Hex-Werte „0D“ („\r“, dezimal 13) und „0A“ („\n“, dezimal 10) erzeugt. Dies ist notwendig, damit wir den gelesenen Befehl nicht an der gleichen Stelle überschreiben.

Prinzipiell funktioniert es. Ruft man die frisch programmierte „GETCMD.COM“ auf, gibt einen Befehl ein und führt diesen mit Enter aus, wird überprüft, ob die nächste eingegebene Taste der „Pfeil-nach-oben“ ist. Nur in diesem Fall wird der Befehl erneut auf der Konsole ausgegeben. Bei jeder anderen Taste wird das Programm einfach ohne Ausgabe beendet. Na, das sieht doch eher nach einem „epic win“ aus, als nach einem „epic fail“, wie es im Titel des Beitrags angekündigt ist, oder? 😉

Leider nein, denn wir haben zwar die grundsätzliche Funktionalität (Befehl merken und wiederherstellen) entwickelt, aber den entscheidenden Teil vergessen. Das Betriebssystem müsste unser selbstgeschriebenes Programm ja permanent im Hintergrund laufen lassen und nicht erst starten, wenn wir händisch „GETCMD.COM“ aufrufen. Tatsächlich gibt es eine Funktion (int21,31), welche das Programm permanent (als sog. TSR-Programm) im Speicher verfügbar hält, allerdings ist das nur die halbe Miete, denn letztendlich müsste MS-DOS ja trotzdem bei jedem eingegebenen Befehl überprüfen, ob die Pfeil-nach-oben-Taste gedrückt wurde. Und damit das klappt, müssten wir die Tastaturabfrageroutine des Betriebssystems durch Modifikation der „Interrupt Vector Table“ so verbiegen, dass bei jedem Druck auf die „Pfeil-nach-oben-Taste“ unser Programm läuft, bevor die eigentliche Systemfunktion von DOS durchgeführt wird. Ähm, nein, danke? 😀

Spaß beiseite – dieses ganze „Interrupt-Gebastel“ ist definitiv nicht unmöglich, aber an dieser Stelle mache ich den Cut. Seht es mir nach, aber die Lebenszeit ist begrenzt und so wichtig ist es mir dann auch nicht, ob das Zurückholen eines Befehls auf der alten Philips-Büchse funktioniert oder nicht. Sorry! Habt ihr noch Lust auf einen „Fun Fact“? In meiner grenzenlosen Verzweiflung bin ich sogar so weit gegangen, ChatGPT zu befragen. Hier die – leider nicht sehr hilfreiche – Antwort:

Ich zitiere: „Dies erfordert jedoch fortgeschrittene Kenntnisse in der x86-Assembler-Programmierung und im Umgang mit dem BIOS oder dem Betriebssystem“. Damit trifft die KI den Nagel auf den Kopf, besser hätte ich es nicht zusammenfassen können. Da ich keinen der drei Punkte erfülle, ist hier erst mal Endstation. Dennoch möchte ich den heutigen Beitrag mit einem positiven Ergebnis beenden. Auf der Suche nach möglichen Lösungen bin ich über die Software „KEYDO.COM“ gestolpert:

Wie es der Zufall will, tut dieses 1987 erstellte Mini-Programm genau das, was wir brauchen! Es bietet eine Rückholfunktion (bis zu 32 Befehle) und sogar einige „Komfortfunktionen“, wie z.B. das Springen zwischen Wörtern innerhalb eines Befehls über die Tastenkombinationen STRG+LINKS oder STRG+RECHTS. Damit das Stück Software gleich beim Systemstart geladen wird, sollten wir es in der AUTOEXEC.BAT verankern:

Anschließend lassen sich tatsächlich Befehle über die „Pfeil-nach-oben-Taste“ zurückholen. Geil! 🙂

Fun Fact: Die Software wählt technisch betrachtet einen anderen Ansatz und klinkt sich direkt in den Befehlszeileninterpreter ein, anstatt die Keyboardroutinen in der Interrupt Vector Table zu verändern. Sehr clever, aber mindestens genau so kompliziert, wie die von mir, bzw. der KI angedachte Lösung! xD

Tja, da wären wir also am Ende des Beitrags angekommen und müssen uns wohl oder übel die Frage stellen: War das heute alles sinnlos? War es wirklich ein „epic fail“, wie im Titel des Beitrags angekündigt? Nüchtern betrachtet könnte man „ja“ sagen, weil wir das eigentliche Problem nicht lösen konnten und viel Zeit in die Entwicklung der beiden Assemblerprogramme gesteckt haben. Das ist zwar korrekt, aber ich sehe es trotzdem nicht so. Im Endeffekt haben wir – trotz der Rückschläge – einiges über MS-DOS als Betriebssystem sowie die x86-Assemblersprache gelernt. Das ist doch was, findet ihr nicht? Wie schon Konfuzius sagte: „Der Weg ist das Ziel“. 😉

In diesem Sinne – bis die Tage, ciao!