DOS Development
« | 09 Oct 2021 | »OpenWatcom eröffnet mir also heute im Jahr 2021 ein Tor 30 Jahre zurück in die Vergangenheit, denn der Compiler unterstützt auch heute noch MS-DOS und Windows 3.x als Zielplattform.
Auf was muss man also achten, wenn man Programme für DOS schreiben will?
Ach … damals
Technisch betrachtet, war Turbo Pascal
6.0 der erste “richtige” Compiler, den ich je besessen hatte, den mir ein
Hobby-Informatiker aus der Nachbarschaft kopiert hatte.
Doch da mir diese Sprache so “fern” war, tauschte ich es quasi auf dem
Schulhof gegen QuickBASIC 4.5.
Denn das abgespeckte QBasic von DOS kannte ich schon und dessen eingebaute
“Befehlshilfe” lehrte mich damals meine ersten Programmiervokabel.
QuickBASIC ließ mich dann die ersten EXE Dateien erzeugen.
Kurz danach folgte wegen der Schule wieder Pascal und dann kaufte ich mir
Turbo-Assembler 4.0 um “DOS zu hacken”. (Meine parallelen VB-4
Erlebnisse sind Teil einer anderen Geschichte.)
Ich war damals schon von “antiker” Software mehr beeindruckt, als von moderner, da die alten Programme auf meiner veralteten gebrauchten Hardware immer “hoch-performant” lief und Windows immer nur träge wirkte. Und Linux hat mich wegen seines Schneckentempos damals so abgeschreckt, dass ich es auch Jahre später immer noch nicht anfassen wollte.
Es ist beeindruckend, was man damals in 64 KB großen Programmen alles unterbringen konnte, wo heute zig Megabytes nicht ausreichen.
Realmode DOS und Speichermodelle
C und C++ habe ich unter DOS damals nie kennen gelernt. Zwar stolperte ich auch über Turbo C++, konnte damit aber nichts anfangen. Außerdem hielt ich damals ja alle Sprachen, die Groß-Kleinschreibung unterschieden für pervers.
Um 2009 fand ich parallel zu Watcom auch den DJGPP, eine GCC Portierung für DOS, den ich aber (auch mangels einer für mich “einfachen” IDE dafür) nie betreiben konnte.
Das Problem: Für C++ ist DOS nicht gleich DOS.
Die ältesten Compiler (wie auch Turbo C/C++) sehen DOS als reine X86-Realmode Umgebung. Dort herrschen verschiedene Speichermodelle vor:
Name | Description |
---|---|
Tiny | COM: nur 64 KB für Code, Daten und Stack |
Small | EXE: 1 x 64 KB Code, 1 x 64 KB Daten |
Medium | EXE: n x 64 KB Code, 1 x 64 KB Daten |
Compact | EXE: 1 x 64 KB Code, n x 64 KB Daten |
Large | EXE: n x 64 KB Code, n x 64 KB Daten |
Huge | EXE: n x 64 KB Code, bis zu 1 MB Daten |
Ein Segment kann im Real-Mode nur 64 KB groß sein und da es nur ein Code
und ein Daten-Segment Register gibt (Das ES
Register zählt hier noch nicht),
muss man sich entscheiden, ob das Programm mehrere Segmente ansteuern können
soll oder nicht.
Braucht man mehr als 64 KB Code, muss das CS
Register bei Sprüngen wechseln,
und Code-Pointer sind dann 32 bit breit und nutzt FAR
Calls, aber wenn 64 KB
reichen, hat man nur ein Code-Segment, die Code-Pointer sind 16 bit breit und
werden durch NEAR
Calls angesprungen.
Bei den Daten verhält es sich ähnlich und weil größere Pointer mehr Rechenzeit und Platz verbrauchen, musste der Entwickler schon beim Programmieren in etwa wissen, gegen welches Speichermodell er kompilieren will.
Dass C++ std
Allokatoren unter anderem auch ein Überbleibsel aus diesem
Dilemma sind, ist heute schon in Vergessenheit geraten. Es macht eben einen
Performance- und Reichweiten- Unterschied aus, ob ein std::vector
intern
einen NEAR
oder FAR
Pointer typedef
nutzt. Und zusätzliche Allokatoren
konnten diese Grenze überwinden.
DOS Protected Mode
Die meisten C und C++ Compiler wechselten in den 90er Jahren zum DOS Protected Mode Interface und waren damit in der Lage ein 32-Bit Flat-Memory-Model zu nutzen. Und so konnten DOS-Programme (wie heutige Software) den ganzen verfügbaren RAM nutzen, sie nutzten 32-bit Pointer in genau einem Code und einem Datensegment und alles war gut.
Das wahre Problem der Technik wurde vom Compiler gut versteckt: nämlich das
andauernde Hin- und Herwechseln zwischen Real-Mode und Protected-Mode.
Denn DOS Funktionen waren nicht 32-Bit tauglich und sobald z.B. eine Datei
geöffnet wurde, musste:
- ein 32-Bit Aufruf den Prozessor in den 16-Bit Mode schalten,
- ein DOS Interrupt aufgerufen werden und dessen Ergebnis zwischengespeichert werden,
- in den Protected Mode zurück gewechselt werden,
- und das Ergebnis in die 32-Bit Welt import werden.
Der ganze Zirkus war auch für Hardware-Interrupts notwendig, womit sicher einiges an Performance verloren ging.
Doch DOS Spiele hatten keine andere Wahl, weil sie mit den segmentierten
64 KB Blöcken einfach nicht auskamen. Deshalb findet man bei ihnen immer eine
dos4gw.exe
Datei oder einen anderen 32-Bit Extender,
der diese Funktion implementiert hat.
CMake für Watcom DOS Builds
Unglaublich, aber wahr: CMake unterstützt OpenWatcom. Im Standard werden zwar nur für NT Binaries erstellt, aber mit ein paar Klicks mehr, lässt sich der 64-Bit Compiler überzeugen am anderen Ende 16-Bit Code zu generieren.
Am besten installiert man OpenWatcom 2 genau so wie vom Setup vorgeschlagen.
Damit landen alle Dateien in C:\WATCOM
und CMake findet alles selbstständig.
Wenn man in der CMake GUI den Generator Watcom WMAKE
auswählt, muss man für
DOS-Builds die Option Specify options for cross-compiling
aktivieren.
Im folgenden Dialog genügt es beim Feld Operating System
einfach DOS
einzutragen, was der CMAKE-Variablen CMAKE_SYSTEM_NAME
entspricht.
Danach richtet CMAKE automatisch den Compiler ein.
Die wichtigsten CMAKE Variablen sind hier aufgelistet.
CMAKE Variable | Value |
---|---|
CMAKE_AR:FILEPATH | C:/WATCOM/binnt64/wlib.exe |
CMAKE_CXX_COMPILER | C:/WATCOM/binnt64/wcl386.exe |
CMAKE_CXX_FLAGS | -w3 -bt=dos -xs |
CMAKE_CXX_FLAGS_DEBUG | -d2 |
CMAKE_CXX_FLAGS_MINSIZEREL | -s -os -d0 -dNDEBUG |
CMAKE_CXX_FLAGS_RELEASE | -s -ot -d0 -dNDEBUG |
CMAKE_CXX_FLAGS_RELWITHDEBINFO | -s -ot -d1 -dNDEBUG |
CMAKE_C_COMPILER | C:/WATCOM/binnt64/wcl386.exe |
CMAKE_C_FLAGS | -w3 -bt=dos |
CMAKE_C_FLAGS_DEBUG | -d2 |
CMAKE_C_FLAGS_MINSIZEREL | -s -os -d0 -dNDEBUG |
CMAKE_C_FLAGS_RELEASE | -s -ot -d0 -dNDEBUG |
CMAKE_C_FLAGS_RELWITHDEBINFO | -s -ot -d1 -dNDEBUG |
CMAKE_EXE_LINKER_FLAGS | opt map system dos4g |
CMAKE_EXE_LINKER_FLAGS_DEBUG | debug all |
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO | debug all |
CMAKE_LINKER | C:/WATCOM/binnt64/wlink.exe |
CMAKE_MAKE_PROGRAM | wmake |
CMAKE_MODULE_LINKER_FLAGS | opt map system dos4g |
CMAKE_MODULE_LINKER_FLAGS_DEBUG | debug all |
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO | debug all |
CMAKE_SHARED_LINKER_FLAGS | opt map system dos4g |
CMAKE_SHARED_LINKER_FLAGS_DEBUG | debug all |
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO | debug all |
CMAKE_SYSTEM_NAME | DOS |
Jetz braucht man nur noch in das Build-Verzeichnis zu wechseln und dort
wmake
eintippen.
Da wmake
auch nur “die Watcom-Variante” von make
ist, funktionieren
auch die üblichen Verdächtigen wie:
wmake clean
: Löscht bisherige Build-Restewmake my_project
: Baut nur das Projekt namensmy_project
and dessen Abhängigkeiten.
Am Ende liegen im Output-Directory die entsprechenden DOS-EXE Dateien.
Diese kann man dann unter Win64 mit Emulatoren wie DOSBOX
ausführen, wenn man noch zusätzlich die Datei dos4gw.exe
ins
Programmverzeichnis kopiert.
Würde man in CMAKE_MODULE_LINKER_FLAGS
den Eintrag dos4g
durch dos
ersetzen, wären reine 16-Bit DOS Programme möglich, doch das habe ich
bisher noch nicht durch den Linker gebracht, da fehlen einige Symbole.
However, für einen ersten Test reicht mir eine DOS-4GW Anwendungen vollkommen.
Fazit
Nostalgie pur! Ich fühle mich wieder wie ein glücklicher kleiner Junge in den 90ern. Ja damals, als man noch mit 3 Zeilen Basic-Code den unwissenden Gymnasium-Professor in Verlegenheit bringen konnte. 😎
Und trotzdem ärgere ich mich, dass ich erst heute, über 20 Jahre
zu spät, diese interessante Erfahrung machen durfte.
Es fehlte mir damals einfach an Software und Infos.