__near, __far, wherever you are
« | 11 Mar 2023 | »Während Kate rebelliert, Leo stirbt und Céline von “near” und “far” singt (siehe Titanic), überlege ich gerade, wo ich überall “spezielle” Pointer-Attribute unterbringen muss.
Na … da blutet fast das Herz des Ozeans …
Vorgeschichte
Nachdem alle modernen Systeme mit Windows oder Linux ausgestattet sind und mit einem Flat-Memory-Modell arbeiten, versteht heute keiner mehr, wenn ich mich zu folgender Aussage hinreißen lasse:
Pointer ist nicht gleich Pointer.
In C++ finden viele typedef
s statt,
um Pointer zu kapseln. z.B.: typedef X* X_ptr;
Das ist eigentlich ineffizient, weil X*
viel kürzer als X_ptr
zu schreiben
ist.
Doch früher … viel früher gab es mal mehrere Implementierungen der gleichen
Sache abhängig von gewünschten Datenmodell, nämlich:
typedef X __near* X_ptr;
oder typedef X __far* X_ptr;
Und schon macht es wieder total Sinn, dass man mit dem richtigen X_ptr
arbeitet.
Segmentierung
X86-16-bit und DOS arbeiteten mit Segment:Offset Adressierung. Das Datensegment-Register wurde auf einen 64-KByte großen Block gesetzt und man adressierte die Daten über einen relativen Offset.
Wenn ein Programm mit 64 KByte auskam, konnte man ein “kleineres”
Speichermodell wählen. Das Segment-Register wurde einmal global gesetzt
und alle Adressierung liefen nur über den Offset.
Das nennt man dann einen near
Pointer, und der ist nur 16-Bit groß,
also klein und damit effizient.
Kommt man 64 KByte nicht aus, kann man weitere 64-KByte Blöcke allokieren
und das Datensegment einfach auf den Block zeigen lassen, den man haben
möchte. Solche Pointer sind dann 32-Bit breit (weil Segment + Offset)
und die werden eben als far
Pointer bezeichnet.
Das gewählte Speichermodell entscheidet also, ob
sizeof(void*) == 2
oder sizeof(void*) == 4
gilt. Das könnte dem
Programmierer jetzt theoretisch egal sein und er könnte mit dem far
Modell immer alles erreichen, doch wenn stets das Segment-Register bei
Datenzugriffen neu ausgerichtet werden muss, dann läuft das Programm
plötzlich nur noch halb so schnell.
Also Augen auf bei der Wahl des Speichermodells!
Ausnahmen von der Regel
Das GATE Projekt ist leider zu groß, als dass es mit near
recht weit
kommen würde. Kleine Konsolen-Apps würden sich zwar ausgehen, aber ich
mache eben nichts kleines.
Ich nutze daher das large
Modell, in dem Code und Daten mehrere Segmente
nutzen dürfen, damit sind sowohl Code, als auch Daten-Pointer vom Typ far
.
Schließlich geht es mir bei der DOS-Portierung des Projektes nicht um Hocheffizienz sondern um den Beweis, dass man auch heutige Programme noch für DOS kompilieren kann, wenn man sie “richtig” aufsetzt.
Allerdings sind mir einige gebrochene Unit-Tests im Projekt aufgefallen, bei denen ich Pointer für andere Zwecke einsetze, und einer davon heißt “Alignment”.
Weder der C noch C++ Standard garantieren, dass Pointer einem bestimmten Alignment folgen, doch die Praxis lehrte mich:
All Datentypen haben im Zweifelsfall mindestens die Ausrichtung des Pointer-Typs, weil der “am besten” an das Speichermodell angepasst ist.
Soll heißen,
void func(char x, int y);
hat die gleiche ABI-Signatur wie:
void func(void* x, void* y);
Denn alle Datentypen kleiner void*
werden auf die Größe von void*
aufgepumpt, weil void*
mit hoher Wahrscheinlichkeit genau die
Größe eines normalen CPU Allzweckregisters hat.
Generische Funktionsaufrufe
Im gate/functions
Modul werden beliebige C-Funktionsaufrufe über einen
Dispatcher geleitet und der arbeitete bisher nach der Idee, dass alle
Parameter einer Funktion durch void*
repräsentierbar sind (mit ein
paar Sonderfällen, die aus mehreren void*
Typen zusammengesetzt
sein können.)
Das Problem ist, dass void*
im Large-Modell 4 Bytes breit ist, doch das
Default-Alignment ist auf 16-bit Systemen eben nur 16-bit breit.
Ein void foo(int, int)
belegt 4 Bytes (2x int16) am Stack, doch ein
void bar(void*, void*)
hätte wegen far
bereits 8 Bytes (2x int32) belegt
und stimmt nicht mehr mit dem Original überein.
Mein Unit-Test Fehler entstand also durch eine Verschiebung des Speichers.
Lösung: 16-bit Parameter nutzen, anstatt von void *
Nun musste also auch ich ein typedef
für generische Parameter einsetzen und
eben dieses typedef
sieht unter DOS nun anders aus, als unter anderen
Betriebssystemen.
Kaum war die Anpassung vorgenommen, funktionierten auch die Tests wieder erfolgreich.
Andere near/far Pointer erzeugen
Wenn man unter DOS direkt auf die Hardware zugreifen möchte, dann funktioniert
das unter anderem auch über feste Speicheradressen.
Ein Beispiel wäre A0000
des VGA Standards.
Pointer zu solchen Segmenten sind ebenso als __far
Pointer deklariert.
Sie werden über Hilfsmakros wie MK_FP
explizit erzeugt:
Fazit
Ein weiteres Mysterium ist gelöst und eine notwendige Codeanpassung wurde
vorgenommen. Anstatt vieler void*
Parameter wurde nun ein neuer Typ angelegt,
den man an einer Stelle ändern kann.
So kann man auf jeder Plattform den passenden nativen Datentypen über
Compiler-spezifische Makros ermitteln.
Ich werde vorerst mit dem LARGE
Modell weiterarbeiten, doch es tut sich nun
die Frage auf, ob es auch möglich wäre die kleineren Modelle einzusetzen.
Spätestens dann müsste ich weitere Anpassungen in vermutlich vielen Modulen
vornehmen, denn dann wird die Unterscheidung zwischen near
und far
explizit umgesetzt werden müssen.