Looking for a shorter, English version of this?
Try tldr.zotteljedi.de/pc-serial-loader/.
Neuer alter Rechner, neue Herausforderungen: nach dem Kauf eines IBM Personal Computer XTs befand ich mich in der Situation einen Rechner zu besitzen, der mit keinem meiner anderen Rechner "sprechen" konnte. Der XT hat ein 5,25"-Floppy-Laufwerk mit 360 KB Kapazität, also Double-Density-Medien. Diese sind auf Grund ihrer größeren Spurbreite nicht mit High-Density-Laufwerken (1,2 MB) kompatibel. Daraus ergaben sich eine Reihe von Optionen:
Ich habe mich (natürlich) für den aufwendigsten, aber interessantesten Weg entschieden: die Datenübertragung via Nullmodem, aber mal anders. Ich hätte natürlich über den ersten Weg initial mein zjlink auf den XT bringen können, aber ich habe mal so getan als hätte ich gar keine Wechseldatenträger zur Verfügung (was zeitweise auch so war, siehe die Geschichte zu meinem IBM PC XT). Gesucht war also ein Weg, der mit den Mitteln auskam, die auf dem PC bereits verfügbar waren. Neben ein paar Benchmarks und Diagnose-Programmen sowie einer Textverarbeitung war das im Wesentlichen ein MS-DOS 3.30 und ein Windows 1.0.
Das MS-DOS war leider unvollständig und hatte kein BASIC dabei, das wäre sonst ein Heimspiel gewesen. Dafür aber war DEBUG.COM dabei, sodass ich kurz darauf ein erstes "Hello World"-Programm in Assembler laufen hatte. Im späteren Verlauf der Entwicklung ergab sich jedoch eine interessante Idee, die sogar mit den in den Befehlsinterpreter COMMAND.COM eingebauten Mitteln auskommt.
pc-serial-loader setzt auf ein mehrstufiges Konzept, das ich in ähnlicher Weise bei der Software Amiga Explorer gesehen hatte:
Der Initial Loader ist in Assembler geschrieben und umfasst gerade mal 22 Instruktionen. Zu Beginn hatte ich diesen per DEBUG.COM auf dem Zielsystem erstellt und übersetzt (wenn man das überhaupt so nennen kann). Später habe ich ihn auf meinem Windows-PC erstellt und über die Tastatur am Ziel-PC eingegeben. Dazu gibt es den schönen Trick aus der DOS-Steinzeit, die ALT-Taste zu drücken, gedrückt zu halten, einen Zeichencode über den Nummernblock einzugeben, und die ALT-Taste wieder loszulassen. Damit lassen sich alle 256 möglichen Werte, die ein Byte annehmen kann, als Zeichen darstellen (mit kleinen Einschränkungen, doch dazu später mehr). Diese Zeichen kann man direkt in eine Datei speichern und schon ist das Programm auf dem Zielsystem.
Die zweite Stufe nenne ich Toolbox, da sie universelle Grundfunktionen beinhaltet, die zur Umsetzung komplexerer Funktionen genutzt werden können. Als eine solche Grundfunktion betrachte ich z.B. das Öffnen einer Datei oder das Lesen eines Datenblocks. Eine komplexere Funktion wäre das Übertragen einer Datei vom Zielsystem auf das Hostsystem. Die Toolbox ist ebenfalls in Assembler geschrieben, aber da sie nicht mühevoll eingetippt werden muss, sondern in einem Rutsch via Nullmodem übertragen werden kann, besteht hier nicht der gleiche Zwang, sich möglichst kurz zu fassen. Assembler bot sich nicht zuletzt deshalb an, weil einige der benötigten Funktionen ohnehin nur als BIOS- oder DOS-Interrupt zur Verfügung stehen (z.B. einzelne Sektoren von Disketten lesen/schreiben).
Als Client kommt ein Windows-Programm mit grafischer Benutzeroberfläche zum Einsatz. Dieses habe ich in C geschrieben, unter direkter Verwendung der Win32-API. Das entstandene Programm ist mit ca. 60 KB ziemlich klein und hat keine weiteren Abhängigkeiten, sodaß es unverändert unter allen Windows-Versionen seit Windows 95 lauffähig sein sollte. Das Client-Programm ermöglicht zum einen das Lesen und Schreiben von Disk-Images unter Verwendung der Floppy-Laufwerke des Zielsystems, zum anderen die Übertragung von Dateien in beide Richtungen. Damit besteht sowohl die Möglichkeit, bereits auf dem Zielsystem vorhandene Daten und Programme zu sichern, als auch aus anderen Quellen stammende Software auf das Zielsystem zu bringen.
Als Assembler-Programm mit nur 22 Instruktionen kann der Initial Loader nicht viel. Er besteht im Wesentlichen aus der Konfiguration des COM-Ports und einer Endlosschleife für Empfang und Ausführung der zweiten Stufe.
Die Funktion 2 des BIOS-Interrupts 14h wartet zunächst blockierend auf eingehende Daten. Wenn für ca.
1 Sekunde keine Daten eintreffen, dann kehrt der Aufruf zurück und zeigt an, dass nichts empfangen wurde.
Der Initial Loader nutzt dies, indem er zu Beginn solange im Zustand idle verbleibt, bis ein erstes
gültiges Byte empfangen wurde. Danach wechselt er in den Zustand receive und schreibt das
empfangene Byte an eine statisch konfigurierte Adresse. Danach wird versucht das nächste Byte zu empfangen.
Solange gültige Bytes empfangen werden, verbleibt der Loader im Zustand receive und schreibt die
eingehenden Daten an fortlaufende Speicheradressen. Sobald der
Datenstrom aufhört und die BIOS-Funktion per Timeout zurückkehrt, wechselt der Loader in den Zustand
execute und ruft den empfangenen Code mit einer CALL
-Instruktion auf. Wenn das aufgerufene
Programm zurückkehrt, wird der Rückgabewert im Register AL
geprüft. Ist er 0, so
beginnt der Vorgang durch den Wechsel in den Zustand start von neuem. Bei einem Rückgabewert
ungleich 0 wird der Loader verlassen, wobei der Rückgabewert des empfangenen Programms auch als Exit-Code
des Loaders an das Betriebssystem weitergegeben wird.
Durch dieses sehr einfache Protokoll benötigt der Loader keinerlei Kenntnis über die Länge und die Art der Daten, die er erhält. Es muss lediglich sichergestellt sein, dass die zweite Stufe an einem Stück übertragen wird, also ohne Unterbrechungen oder Stockungen die zeitlich in die Größenordnung der Timeout-Zeit kommen.
Tatsächlich ruft der Loader sogar nicht einfach nur das Programm auf, sondern legt zuvor den Index des verwendeten COM-Ports (z.B. 0 für COM1) als Wort auf den Stack. Das geladene Programm kann somit die bereits konfigurierte Verbindung nutzen, und sollte man den COM-Port oder die Baudrate einmal ändern wollen, so muss dies nur im Code des Loaders geschehen. Dort ist es dafür umso umständlicher: sowohl die Baudrate als auch die Einstellungen für Anzahl Datenbits, Stopbits und Parität sind in einem einzigen 8-Bit-Wert kodiert.
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Baudrate | Parity | Stop bits | Data bits |
Die Eingabe des Initial Loaders über die Tastatur erfordert noch ein paar weitere Kniffe. Der Index des
COM-Ports 1 ist wie erwähnt der Wert 0. Dieser wird im Register DX erwartet, d.h. irgendwo im Code muss die
Instruktion MOV DX, 0
auftauchen. Dies erzeugt den Maschinencode BA 00 00
, d.h. er
beinhaltet zwei Null-Bytes. Bei der Eingabe des Loader-Codes über die Tastatur via ALT+nnn gibt es jedoch
ein paar Zeichencodes, die problematisch sind, und dazu gehört neben der 9 (Tabulator) und der 27 (Escape)
auch die 0. Um dieses Problem zu umschiffen müssen Instruktionen, die solche ungünstigen Codes
erzeugen, durch Instruktionen mit gleicher Wirkung aber anderen Codes ersetzt werden. Am Beispiel des
COM-Port-Index verwendet mein Loader z.B. XOR DX, DX
, was dem Maschinencode 31 D2
entspricht und damit problemlos darstellbar ist. Das gleiche Problem ergibt sich auch mit der Speicheradresse,
an die der empfangene Code gespeichert wird: glatte Adressen wie DS:0400
erzeugen ebenfalls
Null-Bytes. Daher wird bei mir das Programm an die Adresse DS:0402
geladen.
Wenn der Code breinigt ist, dann kann er über folgendes Konstrukt genutzt werden:
C:\>echo [Zeichenfolge] > initld.com
Damit wird die Zeichenfolge auf die Konsole ausgegeben (echo
), aber die Ausgabe in eine Datei
umgeleitet. Diese beinhaltet danach die Zeichenfolge (und damit das Programm), gefolgt von den zwei Bytes
0D 0A
. Diese sind auf den Zeilenumbruch (CR LF bzw. "\r\n"
) zurückzuführen,
die echo
automatisch anhängt. So etwas möchte man normalerweise nicht im Programmcode
haben, aber in diesem Fall ist das egal, da es hinten anhängt und somit nach der Instruktion folgt, die
das Ausführen des Programms beendet und zu DOS zurückkehrt, d.h. das 0D 0A
wird niemals
ausgeführt. Glück gehabt. :-)
Die Toolbox wird als Binärdatei übertragen und auf dem Zielsystem ausgeführt. Sie verwendet ein deutlich komplexeres Protokoll, das eine Kommunikation in beide Richtungen erlaubt. Jeder Übertragung wird eine Längenangabe in Bytes vorangestellt, sodass der Empfänger genau weiß, wie viel Bytes er noch lesen muss. Außerdem ermöglicht dies, unbekannte Befehle zu überspringen, ohne dass die Kommunikation danach gestört ist.
Jeder Befehl ist durch einen Code identifizierbar, der zur Auswahl einer spezifischen Bearbeitungsfunktion genutzt wird. Alle darauf folgenden Bytes sind spezifisch für den jeweiligen Befehl und können weitere Parameter beinhalten. In Version 1 des Kommunikationsprotokolls werden folgende Befehle unterstützt:
Befehl | Code | Nutzdaten in Request | Nutzdaten in Response |
---|---|---|---|
Exit | 0 | Exitcode | --- |
GetVersion | 1 | --- | Versionsnummer der Toolbox |
GetDriveInfo | 2 | Laufwerkindex | Statuscode, Laufwerktyp, Anzahl Spuren, Anzahl Seiten, Anzahl Sektoren |
ReadSector | 3 | Laufwerkindex, Spur, Seite, Sektor | Statuscode, 512 Byte Daten |
WriteSector | 4 | Laufwerkindex, Spur, Seite, Sektor, 512 Byte Daten | Statuscode |
OpenFile | 5 | Zugriffsmodus (lesen/schreiben), Dateiname | Statuscode, Datei-Handle |
GetFileSize | 6 | Datei-Handle | Statuscode, Dateigröße |
ReadFile | 7 | Datei-Handle | Statuscode, Anzahl gelesener Bytes, bis 512 Byte Daten |
WriteFile | 8 | Datei-Handle, Anzahl zu schreibender Bytes, bis 512 Byte Daten | Statuscode, Anzahl geschriebener Bytes |
CloseFile | 9 | Datei-Handle | Statuscode |
Die Toolbox-Binärdatei ist ca. 1,2 KB groß, was mich persönlich mal wieder staunen lässt, wie kompakt Assembler-Programme sind. (Und ich bin wahrscheinlich noch nicht mal besonders gut in Assembler, da kann man bestimmt noch was rausquetschen.)
Anders als bei zjlink
wollte ich diesmal eine grafische Benutzeroberfläche haben. Ich
schätze, dass selbst unter Berücksichtigung der ganzen Grundlagenforschung für Loader und Toolbox
trotzdem mehr als die Hälfte der Arbeit in das Client-Programm geflossen ist. Dies liegt nicht einmal so
sehr daran, dass der Weg über die Win32-API in C etwas steinig ist, sondern einfach daran, dass sich viele
Fragen zu Gestaltung und Bedienphilosophie ergeben, die bei einem Kommandozeilen-Tool einfach nicht relevant
sind. Zum Beispiel die flüssige Bedienbarkeit eines Cancel-Buttons bei gleichzeitiger Darstellung eines
Fortschrittbalkens während langwieriger Vorgänge -- denn dies bedeutet Multi-Threading.
Außerdem wollte ich diesmal völlig auf static
-Variablen verzichten und jeder
Dialog-Instanz eine eigene Instanz-Struktur mit den Laufzeitinformationen mitgeben, was tatsächlich
ziemlich hübsch geworden ist.
Das Client-Programm besteht im Wesentlichen aus einem Dialogfenster mit Menüleiste und diversen Unterdialogen zum Abfragen von Parametern und Dateinamen.
Hinter den Menüpunkten verbergen sich die im Laufe einer Sitzung weniger oft benötigten Funktionen für das Öffnen und Schließen der Verbindung, Download und Beeenden der Toolbox, etc. sowie ein Einstellungsdialog für diverse Timings und andere experimentelle Dinge, die evtl. bei anderen Zielsystemen nützlich sein könnten.
Der Refresh-Button aktualisiert die Liste der Laufwerke, wobei diese durch das Abfragen der Laufwerkinformationen für die Indizes 0 bis 3 entsteht. Mein IBM PC XT mit nur einem Laufwerk meldet hierbei für jeden Laufwerk-Index die gleichen Informationen. Ich habe noch nicht ausprobiert, was auf anderen Systemen passiert, aber eigentlich sollte Index 0 = A:, Index 1 = B: usw. gelten.
Die Betätigung der Buttons Read image... und Write image... öffnet jeweils einen Standard-Dialog zum Auswählen einer Image-Datei und führt darauf folgend den Transfer von bzw. zu dem in der Dropdown-Combobox ausgewählten Laufwerk aus. Der Transfer eines Images mit 360 KB dauert etwa 7 - 8 Minuten und ist damit schon ziemlich nah an dem Maximum, das man bei 9600 Baud erwarten kann:
9600 Baud = 9600 Bit/s.
Bei 1 Startbit, 8 Datenbits und 1 Stopbit: 9600 Bit/s / 10 Bit/Byte = 960 Byte/s.
360 * 1024 Byte / 960 Byte/s = 384 s (= ca. 6,4 Minuten).
Der Button List files... ist im Moment eigentlich nur dort, damit die Buttons Receive... und Send... auf der jeweils gleichen Höhe wie Read/Write image sind. Vielleicht wird es in einer späteren Version aber tatsächlich mal die Möglichkeit geben, Dateien auf der Target-Seite aufzulisten. Bis dahin sehen die Dialoge, die bei einem Klick auf Receive... und Send... erscheinen, etwas unhandlich aus.
Die Datei im lokalen Dateisystem kann jeweils über einen "Öffnen"- bzw. "Speichern unter"-Dialog
gewählt werden, die Datei im entfernten Dateisystem muss von Hand eingetippt werden. Dafür werden
jedoch relative und absolute Pfade unterstützt, d.h. man kommt unabhängig vom aktuellen
Arbeitsverzeichnis des Loaders an alle Dateien (auch auf anderen Laufwerken!).
Bevor ich mich aber zu sehr mit fremden Federn schmücke: das ist ein Beiprodukt der Verwendung der
Funktionen 3Ch und 3Dh des DOS-Interrupts 21h. Hätten die das nicht hergegeben, dann hätte ich mich
wahrscheinlich mit dem aktuellen Arbeitsverzeichnis begnügt.
Ich habe es tatsächlich sehr genossen eine readme.txt mit etwas ASCII-Art und einer Zeilenlänge von 80 Zeichen zu erstellen. Solche Dateien haben eine seltsame Faszination auf mich und erinnern mich an so gruselige Dinge wie BIOS-Updates und Patches für Spiele aus den späten 90ern, bei denen nach der Installation häufig solche Textdateien im Windows-eigenen Notepad und dem hässlichen System-Font aufgeploppt sind. Jetzt habe ich auch mal so etwas gemacht; Eintrag kann von der wollte-ich-schon-immer-mal-tun-Liste gestrichen werden. ;-)
Bei systematischen Tests mit unterschiedlichen Systemen (vielen Dank an Markus!) sind ein paar Fehler und
Unzulänglichkeiten aufgefallen. Insbesondere war der Toolbox-Client nicht so plattformunabhängig wie
beabsichtigt. Dies lag an diversen "Convenience-Funktionen" die über die Zeit Einzug in die Win32-API
erhalten haben, wie etwa RegGetValue
, das es erst seit Windows XP gibt. Nach ein bisschen Umbau ist
das Programm nun tatsächlich unter Windows 95/98/NT4 lauffähig, wobei man unter Windows 95 entweder
das
Windows Desktop Update (bzw. Windows 95c aka.
OSR2.5) benötigt, oder die Datei MSVCRT.DLL
aus anderer Quelle beschaffen muss.
Funktional hat sich auch die eine oder andere Lücke aufgetan. So funktioniert beispielsweise die Funktion 8
von BIOS-Interrupts 13h nicht immer zuverlässig und liefert im dümmsten Fall gar keine
Laufwerksinformationen, sodass die Funktionen "Read Image" und "Write Image" nicht zur Verfügung stehen. Um
dies zu beheben habe ich den Drive Editor gebaut, mit dem man nun manuell Laufwerke definieren kann,
sofern man deren Index (0 = A:, 1 = B:) und Geometrie kennt (siehe README.TXT
). In diesem Atemzug
habe ich auch den Button im Hauptfenster von "Refresh" in "Detect drives" umbenannt. Der Drive Editor ist
über ein Menü namens "Tweaks" zugängig, in das auch die exotischeren Einstellmöglichkeiten
des ehemaligen Tweaks-Dialogs verschoben wurden (Einträge "Delays and timeouts" und "Flow control").
Die wohl bedeutendste Änderung ist jedoch die Resize-Funktion für FAT12-Images. Mit dieser Funktion
ist es möglich, z.B. 360K-Images so auf ein 1.44M-Laufwerk zu schreiben, dass die entstehenden Disketten
bootfähig und vollständig les- und schreibbar sind. Was vielleicht nach schlimmen Dateisystem-Voodoo
klingt ist hinter den Kulissen tatsächlich relativ einfach. Man muss sich ein Disk-Image lediglich als eine
Folge von Sektoren vorstellen und ein paar Einträge im
BIOS Parameter Block anpassen, sodass die
Abbildung auf Spur und Seite wieder stimmt. Als Beiprodukt ist ein Kommandozeilen-Tool abgefallen, mit dem
Images auch konvertiert werden können ohne sie auf ein Target-Laufwerk zu schreiben (z.B. für den
Einsatz in einer virtuellen Maschine).
Dieses Kommandozeilen-Tool habe ich auch für einen automatisierten Test genutzt, um sicherzustellen dass
alle möglichen Kombinationen aus Image und Laufwerkgröße bestmöglich funktionieren. Dazu
habe ich die entstandenen Disk-Images unter Linux via mount -o loop
gemountet und inhaltlich mit
den
ursprünglichen Images verglichen. Doch was ist mit "bestmöglich" gemeint? Nun, die Abbildung in die
eine Richtung ist relativ klar: kleines Image auf großem Laufwerk muss natürlich funktionieren. Aber
mein Programm erlaubt auch die umgekehrte Richtung, großes Image auf kleinem Laufwerk. Natürlich muss
das zu Datenverlust führen, aber dennoch sind die am Anfang des Images gelegenen Dateien benutzbar, was je
nach Anwendungsfall vielleicht heißbegehrt sein könnte.
Natürlich gibt es auch die Option, ein Image ohne "intelligente Verfummelung" einfach 1:1 zu schreiben,
solange bis das Ende der Diskette bzw. des Images erreicht ist. Dies könnte für andere Dateisysteme
sinnvoll sein und sollte daher nicht verboten sein.
Neu in Version 1.2 ist der Target Explorer. Dieser erlaubt das komfortable Browsen durch das Dateisystem auf dem Target, sowohl auf Festplatten-Laufwerken als auch auf Disketten-Laufwerken. Dabei werden alle Dateien und Unterverzeichnisse aufgelistet, zusammen mit zusätzlichen Informationen zu Größe, Datum und Uhrzeit der letzten Änderung, sowie den gesetzten Datei-Attributen (schreibgeschützt, versteckt, etc.).
Verzeichnisse können bequem per Doppelklick gewechselt werden, oder bei bekanntem Zielpfad direkt durch die
Eingabe in die Location Bar am oberen Rand. Durch Eingabe eines anderen Filters als *.*
können
auch eingeschränkte Listen erzeugt werden, z.B. zeigt C:\DOS\*.BAS
bei meiner Installation von
MS-DOS 5.0 nur die 4 Beispiel-Programme (GORILLA.BAS, MONEY.BAS, NIBBLES.BAS und REMLINE.BAS) an. Das ist
insofern ganz interessant, als dass die Auflistung vieler Einträge eine gewisse Zeit benötigt (bei
9600 Baud ca. 3-4 Einträge pro Sekunde; das ganzes DOS-Verzeichnis braucht ca. 25 Sekunden). Da der Filter
bereits auf dem Target wirkt, entsteht somit eine entsprechend kürzere Liste die abgefragt werden muss.
Durch einen Doppelklick auf eine Datei wird der bereits bekannte "Receive file from target"-Dialog geöffnet, vorausgefüllt mit der ausgewählten Datei. Für Übertragungen in die umgekehrte Richtung können einzelne oder mehrere Dateien per Drag & Drop auf das Target-Explorer-Fenster gezogen werden. Auch hier wird der bereits vorhandene "Send file to target"-Dialog geöffnet und vorausgefüllt.
Diese Nicht-Interaktivität der Dateiübertragung (manuelle Bestätigung der Angaben durch den Benutzer) ergibt sich aus einer kleinen Gemeinheit der Geschichte: unter MS-DOS sind bei Dateinamen maximal 8 Zeichen für den Namen und 3 Zeichen für die Dateinamen-Erweiterung erlaubt. Unter Windows ist diese Beschränkung seit Windows 95 bzw. NT4 aufgehoben. Somit ist nicht sichergestellt, dass alle Dateien in einem Windows-Dateisystem direkt auf ein MS-DOS-Dateisystem abgebildet werden können. Als mir dies im Rahmen der Target-Explorer-Umsetzung bewusst geworden ist, habe ich die Dialoge "Send file to target" und "Receive file from target" um eine entsprechende Prüfung des Remote-Pfads erweitert. Wollte man nun die Übertragung automatisch ablaufen lassen, müsste im Fall eines Namenskonflikts eine automatische Lösung gefunden werden. Der Windows-typische Umgang besteht in der Vergabe von Kurznamen (MSDN: 8.3 alias or short name), wie z.B. "PROGRA~1" anstelle von "Programme". Um allerdings sicherstellen zu können, dass hier kein weiterer Konflikt entsteht (weil z.B. "PROGRA~1" im Ziel bereits existiert, und die nächste freie Darstellung "PROGRA~2" wäre), ist noch mehr Intelligenz erforderlich -- und das erschien mir für den ersten Wurf zu aufwendig. Allerdings ziehe ich inzwischen für eine spätere Version in Erwägung, den Prozess etwas zu straffen, indem entweder die Dateinamen schon im Voraus geprüft werden, oder erst im "Notfall" eine Konflikt-Behebungs-Aufforderung angezeigt wird; damit werden all jene Nutzer belohnt, die bereits im Vorfeld auf eine kompatible Benennung der zu übertragenden Dateien geachtet haben.
Eine vollständige Liste der Änderungen ist in der Datei CHANGES zu finden.
Datum | Version | Datei | Beschreibung |
---|---|---|---|
2020-05-30 | 1.0 | pc-serial-loader-bin-1.0.zip | Binaries für MS-DOS und Win32 |
pc-serial-loader-src-1.0.zip | Quellcode | ||
2020-11-17 | 1.1 | pc-serial-loader-bin-1.1.zip | Binaries für MS-DOS und Win32 |
pc-serial-loader-src-1.1.zip | Quellcode | ||
2020-12-19 | 1.2 | pc-serial-loader-bin-1.2.zip | Binaries für MS-DOS und Win32 |
pc-serial-loader-src-1.2.zip | Quellcode |