DOS Development

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:

  1. ein 32-Bit Aufruf den Prozessor in den 16-Bit Mode schalten,
  2. ein DOS Interrupt aufgerufen werden und dessen Ergebnis zwischengespeichert werden,
  3. in den Protected Mode zurück gewechselt werden,
  4. 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.

CMAKE-Watcom-DOS-1

Im folgenden Dialog genügt es beim Feld Operating System einfach DOS einzutragen, was der CMAKE-Variablen CMAKE_SYSTEM_NAME entspricht.

CMAKE-Watcom-DOS-2

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-Reste
  • wmake my_project: Baut nur das Projekt namens my_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.