Ich gehe in diesem Text von ein paar Grundfragen aus, die in diesem Zusammenhang immer beantwortet werden müssen,
und die teilweise eine sehr ins Detail gehende Kenntnis der Konstruktion von Computer und CPU erfordern. Deshalb
diskutiere ich Betriebssysteme im Bezug zu einem AT-PC und einer CPU vom Typ i486 oder weiter entwickelt. Ich werde
solch eine CPU im folgenden eine Intel-CPU nennen, weil sie beim Hersteller Intel konstruiert wurde. Eine CPU mit
gleichen Opcodes bekommt man aber auch von anderen Herstellern, evtl. mit Erweiterungen des Befehlssatzes für die FPU.
Wenn man ein Betriebssystem schreiben will, stellen sich einem zunächst zwei Fragen:
1.Frage: Wieviel Betriebssystem braucht man eigentlich?
Anwort: Fast garnichts!
Ein Betriebssystem muss ausser einigen Gerätetreibern vor allem eine Verwaltung von Speicher auf Medien und
Arbeitsspeicher enthalten und den Programmstart ermöglichen. Auch einige Initialisierungen muss es vornehmen.
2.Frage: Muss man ein Betriebssystem in einer "höheren" Programmiersprache schreiben?
Antwort: Nein!
Ein Betriebssystem muss immer mit einer konkreten Maschine umgehen. Diese ist immer durch einen besonderen Opcode
der CPU gekennzeichnet. Dieser wird nur mit einer Assemblersprache so abgebildet, dass wirklich keine Möglichkeit
der Programmierung verschenkt wird und viele nötige Einstellungen überhaupt erst machbar sind.
"Höhere" Programmiersprachen sind dagegen immer mit der Absicht ersonnen worden, gerade nicht eine konkrete Maschine
programmierbar zu machen, sondern eine verallgemeinerte Abstraktion. Diese "Maschinen-Unabhängigkeit" wird als
Vorteil gepriesen, ist tatsächlich aber nicht gegeben, denn alle Betriebssysteme müssen einen Teil enthalten,
der in Assembler geschrieben wurde. Dieser Teil wird gerne als "arch dependant" (="Architektur bedingt")
verniedlicht.
Die Frage kann also nur sein, ob "höhere" Programmiersprachen geeignet sind, irgendetwas in einem Betriebssystem
besser zu machen. Auch das kann verneint werden!
Diese Verneinung kann aber nur mit Bezug zu einer konkreten Maschine diskutiert werden, hier dem AT-PC.
Mal abgesehen davon, dass bei "höheren" Programmiersprachen genau die Vorteile einer konkreten Konstruktion von
Computer und CPU jedenfalls dann unerreichbar verloren gehen, wenn sie einzigartig sind, sind auch viele Abläufe
zwar im Zweck sehr ähnlich, in konkreten Maschinen aber sehr unterschiedlich zu kommandieren, weil Zwecke zunächst
schaltungstechnisch ermöglicht werden müssen, bevor sie mit einem Programm kommandiert werden können. Und es ist
eben deshalb die schaltungstechnisch gegebene Logik, die "logisch" ist und nicht die "höhere" Abstraktion, die
zwar ungeheuer mathematisch aussehen kann, tatsächlich aber nicht logisch im Sinne von "vernünftig" ist,
weil sie Ab- und Umwege erzwingt.
Was genau das heisst, mache ich jetzt an den Teilen des Betriebssystems klar, die ich als Antwort auf die 1. Frage
gab. Dabei spielt weniger die Rolle, mit welchen Controllern Gerätetreiber umzugehen haben, weil die auf sehr
verschiedenen Computern die gleichen ICs sein können. Insofern ist also Programm unabhängig von der Maschine AT-PC,
aber natürlich abhängig von der Maschine, die ein Controller darstellt.
Mit einer Intel-CPU werden die Register solcher Controller jedenfalls mit besonderen Kommandos in einem besonderen
I/O-Adressbereich erreicht. Auch die Interruptleitungen sind auf besondere Weise mit CPU und Arbeitsspeicher verknüpft.
Mittlerweile sind manche Register zwar auch auf Adressen im Arbeitsspeicher erreichbar, wenn ein PCI-Bus vorhanden ist.
Aber auch dann ist die Adressierung eigentümlich anders mit einer Intel-CPU zu machen als z.B. mit einer von Motorola.
Ausserdem muss dann der cache ausgeschaltet werden, was die Zugriffe nicht unbedingt schneller als über I/O-Adressen
werden lässt.
Ich werde nicht die Konstruktionen der Hersteller Intel und Motorola gegeneinander stellen, die lange Jahre nur
um Unterschiede zwischen ihren Konstruktionen wetteiferten. Tatsächlich sind in beiden Lagern sehr bemerkenswerte
Ideen entstanden. Diese bemerkenswerten Ideen gehen aber sofort unter, wenn man einen gemeinsamen Nenner mittels
"höherer" Programmiersprache finden muss!
Der kleinste gemeinsame Nenner führt vollkommen logisch zum Rückschritt!
Die bemerkenswerteste Idee in einer Intel-CPU ist die Segmentierung des Arbeitsspeichers, wie sie nach Umschaltung in
den protected mode gegeben ist. Mit der dann gegebenen Adressierungsweise kann allerdings keine "höhere"
Programmiersprache umgehen. Zunächst ist aber das wichtigste Problem zu betrachten, das auf diese Weise konstruktiv
gelöst wurde:
Programme müssen mit bekannter Anfangsadresse übersetzt werden, damit der Assembler Namen für Label in physische
Adressen übersetzen kann. Das kann z.B.stets die Adresse =0 sein.
Sollen nun mehrere Programme mit dieser Anfangsadresse gleichzeitig im Arbeitsspeicher arbeiten können, geht das nur,
wenn mehrere solcher 0-Adressen zur Verfügung stehen - was natürlich nur mit einem Trick möglich ist. In jeder CPU wird
das Problem gelöst, indem bei jeder Adressierung ein zweiter Adressteil zu der im Programm gegebenen Adresse hinzu
addiert wird. In der Intel-CPU sind gleich drei verschiedene Methoden eingebaut. Während im real mode ein Addend benutzt
wird, der eine durch 16 geteilte absolute Adresse ist, die "Segmentadresse", ist im protected mode eine in einen
"descriptor" eingebaute absolute Anfangsadresse eines "Segmentes" in Gebrauch, die allerdings aktuell mit einer
ebenfalls zweigeteilten Adresse (Anfangsadresse der GDT+selector) adressiert und geladen werden muss. Diese scheinbar
zu umständliche Methode macht aber gerade den grössten Vorteil dieser Tabelle GDT aus.
Während die im real mode zu verwendende Segmentadresse eine ziemlich nahe liegende Form einer Basisadresse ist, die auch
in anderen CPUs gebräuchlich ist (und dann anders heisst), ist der konstruktiv bestimmte Umgang mit Selektoren und
Deskriptoren bei einer Adressierung im protected mode eine ausserordentliche Ingenieurleistung - offenbar weit über das
geistige Fassungsvermögen von Systemphilosophen hinaus gehend.
In einer Intel-CPU ist deshalb noch eine dritte Methode eingebaut, die "höhere" Programmiersprachen wieder möglich macht.
Diese Methode ist der page mode, der erst eingestellt werden kann, wenn der protected mode bereits eingeschaltet ist.
Dazu ist aber nicht nur ein einzelnes PE-Bit im Kontroll-Register cr0 zu setzen, sondern eine Unzahl von Konsequenzen
in Kauf zu nehmen, die allesamt nicht gut tun.
Nur am Rande zu erwähnen ist der V86-mode, der allein aus Gründen der Abwärts-Kompatibilität in die Welt gesetzt wurde.
Allerdings bedingt er einstellbare Stellenzahlen für Operanden und Adressen, die für die Rückschaltung aus dem
protected mode in den real mode zu beachten sind.
Zunächst stelle ich fest, dass im page mode nichts zu haben ist, was nicht auch vor der Umschaltung schon zu haben wäre.
Der protected mode musste allerdings um zahlreiche Eigenheiten erweitert werden, um den page mode überhaupt möglich
zu machen. Vor allem musste die Adresskalkulation zur Erzeugung physischer Adressen, wie sie zur Ansteuerung realer
Speicherchips sein müssen, so erweitert werden, dass ein weiterer Adressrechner nötig wurde, der demjenigen
nachgeschaltet ist, der im protected mode mittels Selektor und Deskriptor gegebener Basisadresse und mittels vom Programm
gegebener 32-stelliger Offsetadresse die physische Adresse erzeugt.
Im page mode kann die Offsetadresse nur 12 Bit breit sein. Die restlichen 20 Bit entstammen zu je 10 Bit zwei Tabellen,
der "page directory table" und der "page table". Der Arbeitsspeicher ist dann regelmässig in 4 KByte (=1000h Byte) grosse
Bereiche, die "page frames", gestückelt, die allesamt ihren eigenen Tabelleneintrag benötigen. Auch die auf zwei
Tabellen verteilten Basis-Adressen brauchen aber weit mehr Platz als die (immer vorhandene!) Global Descriptor Table
(="GDT") - einen mindestens 1 MByte grossen Bereich. Und weil die Zweiteilung der Tabellen ein weiteres Ausufern des
Bedarfes vermeiden soll, ist das nur um den Preis von mindestens zwei separaten Zugriffen auf den Arbeitsspeicher
und mindestens drei Adresskalkulationen möglich (der Inhalt von cr3 muss auch noch eingerechnet werden) - wohlgemerkt
zusätzlich zu den durch die Segmentierung bereits nötigen Zugriffen und Kalkulationen.
Im Durchschnitt ist damit die Geschwindigkeit der Programmausführung also höchstens halb so schnell. Diese Kalkulationen
sind nämlich bei jeder Adressierung im Arbeitsspeicher nötig. Sind "task switches" nötig, muss nicht nur ein neuer
Wert für cr3 im Speicher gelesen werden, es muss zuvor auch der alte Wert im zugehörigen TSS (im Arbeitsspeicher)
gerettet werden. Noch viel mehr Aufhebens macht das swapping.
Das ist aber noch nicht der ganze Nachteil!
Woher kommen nämlich die Definitionen für die 20 Bit der Adresse, die das Programm nicht bar enthalten kann?
Und wie müssen Programme aussehen, die über viele Seiten gehen, aber nur innerhalb einer Seite adressieren können?
Prinzipiell geht das nur durch zusätzliches Programm, das nicht nur während der Übersetzung eines Programmes wirken
muss, sondern auch weiteres Programm, das zur Laufzeit nicht nur präsent sein muss, sondern auch alle
Adressierungen aktuell ergänzen muss. Das kostet Zeit und Speicher. Dazu hatten Systemphilosophen Ideen, die jedenfalls
zu viel Geld zu machen waren...
Neben der Adressierung im Arbeitsspeicher haben Programme aber oft auch mit der Adressierung im I/O-Bereich zu tun, die
in Intel-CPUs ebenfalls ganz eigenartig (und durchaus raffiniert!) zu verwenden ist. Sie hat jedenfalls den Vorteil, dass
falsche Adressierungen im Arbeitsspeicher, die auch mal durch Transferfehler oder irgendein dahergekommenes
Alphateilchen verursacht sein können, nicht gleich allzu grossen Schaden anrichten können. Auch damit können "höhere"
Programmiersprachen nicht ohne Umwege umgehen.
Immer eingebaute Controller für Timing, Interrupts, Tastatur usw... sind deshalb deutlich umständlicher regierbar.
Schliesslich ist die Verarbeitung von Unterbrechungen nicht so, dass in "höheren" Programmiersprachen alles kommandierbar
wäre. Insbesondere ist eine Interrupt Descriptor Table (="IDT") im Spiel, die auf absoluter Basisadresse, also nur mit
passendem Selektor, zu erreichen ist, und die diese Basis-Adresse nur mit einem speziellen Kommando zugewiesen bekommen
kann.
Diese Vielfalt von Möglichkeiten wird nicht nur durch einen cache und sein nur über spezielle Kommandos einstellbares
Verhalten noch berührt, sondern auch Spezialregister, über die der Maschinenzustand einzustellen ist (z.B.der
protected mode).
Auch die Deskriptoren enthalten noch jede Menge Stellen, die den Maschinenzustand für bestimmte Segmente einstellen.
Das sind neben der Charakterisierung für Daten oder Programm auch Privilegien und Adress-und Operandenbreiten.
Auch ein Präsenz-Bit ist vorhanden, das für einen Auslagerungsmechanismus (swapping) verwertet werden kann.
Es stellt sich also angesichts eines AT-PC noch eine weitere Frage, bevor man ein Betriebssystem schreibt.
3. Frage: Braucht man das alles?
Antwort: Nein!
Die Ingenieure bei Intel hatten vor allem ein Ziel im Auge, nämlich möglichst viele Ziele mit einem Mädchen für alles
erreichbar zu machen. Das geht nicht ohne sich teilweise gegenseitig ausschliessende Möglichkeiten.
Die letzte Anwort führt deshalb sofort zur nächsten Frage:
4. Was braucht man und was verschenkt man?
Diese Frage kann man sehr verschieden beantworten und berührt damit zwangsläufig quasi-religiöse Überzeugungen.
Tatsächlich kann man eine vernünftige Antwort sehr leicht finden, indem man Kriterien dieser Optimierungsfrage
findet, die unbestreibar sind:
Schnelligkeit, minimaler Bedarf an Speicher, einfache Fehlersuche und Programmerzeugung.
Es scheiden sich allerdings die Geister, wenn ausserdem noch Geschäftsideen oder sonstiger Geltungsdrang eine
Rolle spielen. Schliesslich kommen damit auch Wertvorstellungen ins Spiel, die teilweise Weltgeltung haben. Mit
diesen fange ich an:
Zwei Eigenschaften eines Betriebssystems stehen vielen Programmierern als unverzichtbar vorteilhaft im Kopf -
"paging" und insbesondere "multi tasking". Das paging macht nur Sinn im Zusammenhang mit "swapping", denn es ging
einmal darum, viel zu grosse Programme in einem viel zu kleinen, einst sehr teuren Arbeitsspeicher auszuführen.
Das Spiel konnte nur einfach genug mit vielen gleich grossen Teilbereichen, den pages, veranstaltet werden.
Die Not zu kleiner Arbeitsspeicher ist aber längst auf eine noch viel einfachere Weise aus der Welt geschafft
worden, nämlich durch mehr Speicherkapazität pro Chipfläche und damit weniger Kosten. Paging und Swapping sind
also Steinzeitmethoden, die höchstens dann noch von Interesse sein können, wenn ganze Filme vorzuführen oder
gigantische Suchmaschinen zu programmieren sind.
Eine andere Not entstand einst durch langsame und teure CPUs. Man baute deshalb "main frames" (Zentralrechner)
um die man viele einfachste Maschinen gruppierte, die gerade mal Monitor und Tastatur ansteuern konnten. Dabei
entstanden gleich weitere Probleme, die mit "multi using" und "user identification" gelöst werden mussten. Alles
in allem war dabei ebenfalls viel Zeit und Arbeitspeicher für die Verwaltung zu opfern.
Auch diese Not kam auf die noch viel einfachere Weise aus der Welt. Tatsächlich hat heute jeder, der das hier
liest, mehr Geschwindigkeit und Speicherplatz vor sich als main frames im Pentagon vor 30 Jahren. Auch multi
tasking ist also eine Steinzeitmethode, die allerdings in manchen Köpfen als Zauber vorkommt, der erlaubt, aus
einer Maschine mehrere zu machen. Solche Phantasien waren auch nicht ohne Folgen im maschinellen Bereich.
Es wurden "cubes" und "super cubes" ersonnen, in denen mehrere CPUs "cluster" bildeten. Das ist aber nur
Kubik-Blödsinn, weil die allermeisten Probleme eben nicht einfach skalierbar sind, also nicht etwa erlauben,
das Dach vor dem Haus zu bauen. Aber eine Intel-CPU ist jedenfalls auch für Mehrprozessorsysteme gebaut (mit
Pins, die Buskontrolle erlauben). Angesichts eines AT-PC kann man davon aber vollständig absehen. Auch die
Verteilung von Abläufen ist mittlerweile längst viel sinnvoller durch Entwicklung von Controllern zustande
gekommen - multi tasking ohne Konsequenzen für Betriebssysteme.
Kurz gesagt ist paging und multitasking nicht nur kein Vorteil, sondern ein Nachteil, der die Geschwindigkeit
herab setzt und den Speicherbedarf in letzter Konsequenz gigantisch erhöht (weiter unten mehr dazu).
Multi tasking erfordert, wenn es nicht "on demand" (="Bei Bedarf") ist, eine Uhr. Das kann ein AT-PC aber nicht
ohne grosse Nachteile bieten. Es gibt nur einen Timer und die CMOS-Uhr mit Weckerfunktion. Es muss also mit
weitreichenden Folgen die Herrschaft über die Uhr verwaltet werden...
Bevor ich zu einer vernünftigen Anwort auf die letzte Frage komme, ist noch abzuhandeln, wieweit Geschäftssinn
und Geltungsdrang den Verstand trüben können. Dabei komme ich auf die quasi religiöse Entscheidung für eine
"höhere" Programmiersprache und einher gehende Verdammung von Assembler zuletzt...
Zunächst soll die Frage sein, ob ein Betriebssytem unbedingt ein "supervisor" sein muss. Darunter versteht man
solche Systeme, die niemals umgangen werden können, solange irgendwas weiteres geschehen soll. In einer
Intel-CPU ist eine Menge Zeug eingebaut, das jede Begierde nach Allgewaltigkeit befriedigen kann. Dafür gibt
es keine logischen Gründe, aber Zwecke, die komischerweise niemals diskutiert werden. Es ist nämlich prinzipiell
möglich, dass ein Programm andere Programme lesen kann, dass Programme also disassemblierbar bzw. nachvollziehbar
lesbar werden können. Das kann einem garnicht passen, wenn man mit einem Programm Geld machen will und am
liebsten auch verstecken möchte, welcher Blödsinn da teuer bezahlt werden soll. Und so kommt es, dass Firmen
mit Milliardenumsätzen entstehen konnten, die ihre Geschäfte wie Verlage auf dem copyright aufbauen, aber
"Bücher" verkaufen, die niemand lesen darf oder auch nur kann.
An die Stelle von solchem Geschäftsinn kann natürlich immer auch anderes Streben nach Allgewaltigkeit treten. Es
ist deshalb festzustellen, dass kein Computer einen Sinn dafür hat (und deshalb auch nicht braucht), was der
Programmzähler oder ein Indexregister gerade adressiert und damit zur Ausführung bringt. Nur Zwecke des Programms
können wichtig machen, was als nächstes Kommando adressiert wird! Ein supervisor ist also prinzipiell absurd, wenn
nicht bestimmte Zwecke in einem bestimmten Programm, dem Betriebssystem, gehalten werden sollen. Tatsächlich gibt
es Zwecke, die besser an einer Stelle abgehandelt werden. Das gilt für jede Form der Speicherverwaltung und damit
auch für den Programmstart, der im protected mode nur in einem Programmsegment stattfinden kann, das wiederum
nur innerhalb einer Verwaltung der GDT bereit gestellt werden kann. Allerdings kann man diese Verwaltung auf
wenige Tabellen und Variable stützen, die dann prinzipiell auch aus beliebigen Anwendungsprogrammen heraus
benutzt werden können. Es ist also grundsätzlich möglich, ein Betriebssystem so zu gestalten, dass wirklich alles
auch aus Programmen heraus gegriffen werden kann - ein Blick über die Schulter durch einen supervisor ist also
nicht nur überflüssig, sondern vor allem mal störend und nicht ohne Kosten an Zeit und Code zu verwirklichen.
Inwieweit das die im Zusammenhang gerne beschworene Systemsicherheit betrifft, diskutiere ich weiter unten -
zunächst noch einige Bemerkungen zu der Frage, ob "höhere" Programmiersprachen eine so grossartige Schöpfung sind,
dass man mehr als ein paar Worte dazu verlieren muss.
Geschäftssinn hat nicht nur konstruktives Vorgehen von Ingenieuren bestimmt, er hat auch die Programmierer von
"höheren" Programmiersprachen getrieben. Selbst wenn ursprünglich eine Verallgemeinerung von Assemblersprachen
die Ausgestaltung solcher Programmiersprachen wie "BASIC", "FORTRAN" usw. prägte. Das Streben nach griffigeren
Formulierungen in Programmen trat bald zurück hinter den Griff in die Taschen von Programmierern. Und es wurde
auch noch einen weiterer Vorteil "höherer" Programmiersprachen entdeckt. Sie gestatten eine Formulierung von
Programmen, die jede Disassemblierung ins Leere laufen lässt. Wichtige Teile der Verknüpfungen können ausgelagert
werden in Abgründe von Konfigurationsdateien, Linkern und Präprozessoren. Und deren Wirkung kann in die
Programmausführung verlegt werden, wo sie unauffindbar ist - vor allem dann, wenn man auch noch
Privilegierungsstufen im protected mode nutzt.
Es ist ganz erstaunlich wie das vernebelt werden konnte mit Lobpreisungen der Vereinfachung und Beschleunigung
der Programmierung. Bei näherer Betrachtung, die über die sensationelle Vereinfachung eines "add" zu "+" hinaus
geht, stellt man nämlich fest, dass das Gegenteil wahr ist: "höhere" Programmiersprachen führen nicht nur zu mehr
Binärcode, sondern auch mehr Quellcode. Sie führen sogar zu sehr viel mehr Code, wenn man den jeweils im Programm
bezweckten Zusammenhang betrachtet! Zum Programm gehören nämlich auch die mit unscheinbaren include-Anweisungen
zur Ausführung gebrachten Codemengen in Bibliotheken und die mit unscheinbaren Makros eingefügten Abläufe.
Und das alles muss nicht nur mit gigantischen Programmen übersetzt, sondern ebenso gigantischen Umtrieben
gelinkt werden.
Aber schon ein "+" ist nicht weniger Schreibarbeit als ein "add", mit dem weitaus knapper eine Adresskalkulation
mit der Adressierung von Operanden verknüpft sein kann als mit "+" in "höheren" Programmiersprachen erlaubt ist
(z.B. ;add eax,[gs:ebx+ecx*4+displ]; ).
Nun gibt es seit dem Geschenk von Richard Stallman (GNU-Compiler für C/C++) und den vielen anderen Geschenken
unbezweifelbar liebens- und lobens-werter Mitmenschen aus der Welt der Gnus und Pinguine, keine verachtenswerten
Motive mehr bei der Programmerzeugung. Man kann die Quellen lesen - aber wer liest mehrere GByte und wann in
dieser Welt? Und muss das wirklich sein?
Antwort: Nein!
Diese Antwort drängt sich einem schon beim Auspacken der voluminösen Geschenke auf, sie auszusprechen kostet noch
mehr Nerven und Einfühlung. Man muss sich nämlich nicht nur mit C/C++ befassen, mit dem wenigstens Mikro$oft keine
allzu grossen Geschäfte mehr machen kann, sondern mit UNIX, das in einer ganz anderen Welt vor mehr als 30 Jahren
geboren wurde. Mittlerweile gibt es mindestens 100 Abkömmlinge, die auch als Quellprogramme zu haben sind. Deshalb
kann man im Detail studieren, welche Konsequenzen diese "Systemphilosophie" hat, bzw. welche Konsequenzen offenbar
garnicht erst bedacht werden...
Ein UNIX-artiges System ist die verallgemeinerte Form eines Betriebssystems, das untrennbar mit der "höheren"
Programmiersprache C/C++ vorkommt. Ursprünglich und meistens auch in den Abkömmlingen wird es selbst weitgehend
in dieser Programmiersprache formuliert. Alle anderen eventuell benutzbaren Programmiersprachen sind dann nur
Abkömmlinge von C/C++, in das sie normalerweise übersetzt werden, bevor die Assemblierung getan wird.
Mal ganz abgesehen von den im Kern durchaus vernünftigen Abstraktionen "file", "device", "process" usw. heisst
das aber im AT-PC, dass in den page mode geschaltet werden muss, weil man mit C/C++ keine Selektoren programmieren
kann. Und man braucht einen supervisor, weil IDT und GDT und auch im Arbeitsspeicher liegende I/O-Register nur
mit absoluter, physischer Adresse erreichbar sind. Schliesslich kann kein Prozedurruf mehr ohne Linker getan
werden, der ausserdem den wichtigsten Teil der Arbeit erst zur Laufzeit machen kann.
Diese Einschränkung wird einem mit der Behauptung angedient, dass diese "höhere" Programmiersprache schliesslich
erlaube, Programme zu schreiben, die maschinenunabhängig nicht nur auf dem AT-PC dienen können. Dass das nicht
wahr sein kann, lehrt ein Blick in die Liste der Optionen für den GNU-compiler. Es gilt nicht nur jede Menge
Dialekte dieser Sprache, sondern auch Linker und Dateisysteme zu unterscheiden! Statt maschinenabhängig zu sein,
ist man jedenfalls abhängig von Zielsystemen. Das gleiche C-Programm läuft noch nichtmal auf dem gleichen PC
unter zwei verschiedenen UNIX-artigen Betriebssystemen. Und auch die Maschinenunabhängigkeit ist nicht so einfach
gegeben. Das wird klar, wenn man sich durch die Treiber-Dateien gelesen hat.
Das kann auch garnicht anders sein!
Es wird aber nicht ausgeträumt. Jährlich entsteht mindestens ein neues Projekt mit dem Ehrgeiz, wenigstens den
einen wenn schon nicht den anderen Fehler nicht zu machen. Keiner will nämlich auf all die geschenkt verfügbaren
Pakete verzichten, und ziemlich sicher, weil keiner sieht, dass der grösste Teil des begehrenswerten Volumens
Verpackung und damit Müll ist.
Dieser Müll ist aber erst als solcher entlarvt, wenn man es mal ganz anders versucht hat. Unbezweifelbar sind
gigantische Linker, Compiler, Präprozessoren, Bibliotheken, bash-scripte, makefiles usw. im Rahmen dieser
"Systemphilosophie" unverzichtbar! Deshalb ist jedes UNIX-artige Betriebssystem mit den gleichen Mängeln behaftet.
Erst recht, wenn es auch noch multi tasking bietet. Erst recht, wenn es noch nichtmal reinrassig ist wie das am
meisten verbreitete Betriebssystem, das in der neuesten Ausführung so ewig beim Booten rummacht, dass die Akkus
eines Laptops allein durch ein paar Mal rauf- und runter-fahren des Systems bereits erschöpft sind.
Welch ein Fortschritt...
Und das sicherste an diesem Betriebssystem ist, dass der Akku schnellstens geleert wird...
Ganz nebenbei bemerkt schädigt solche Systemphilosophie nicht nur die Nerven, sondern macht, weil sie in Millionen
von Maschinen sitzt, auch weltweit einige grosse Kraftwerke mehr nötig.
Ich hatte allerdings eher meine Nerven als die Umwelt im Auge, als ich die Anwort auf die oben gestellten Fragen
suchte. Und mein Interesse war zunächst nur auf einige eigene Ziele gerichtet. Ich wollte sie möglichst einfach
und schnellstens erreichen und hatte schon vergeblich Lösungen unter Window$ und LINUX gesucht.
Bei der Arbeit stellte ich fest, dass nur wenig mehr als ich brauchte, nötig war, um ganze Klassen von Problemen
zu lösen. Ich stellte mir deshalb eine etwas erweiterte Aufgabe für das Betriebssystem, das ich schrieb und
fortschreibe:
1) Der Quellcode muss so kommentiert werden, dass ihn jeder auch verstehen und beliebig ergänzen kann.
2) Die Übersetzung in Opcode muss ohne Beziehung zu Datei- und Linker-Systemen schnell erledigt sein.
3) Alle Festlegungen müssen durch Programme abgeändert, umgangen oder erweitert werden können.
4) Jede Stelle in einem AT-PC muss auch ohne Code im Betriebssystem erreichbar sein.
5) Möglichst alle Abläufe im Betriebssystem müssen während der Laufzeit tauschbar sein.
Ausserdem sollte es natürlich so schnell, platzsparend und einfach wie möglich geschrieben sein.
Das kann nur wahr werden, wenn alles in Assembler programmiert wird - auch die Anwendungen.
Diese Programmiersprache ist alles, was man können muss, um wirklich alles regieren zu können! Man braucht nicht
wenigstens vier verschiedene Programmiersprachen ( C/C++, inline-Assembler, Präprozessor, bash...) zu können,
die ausserdem je nach Zielsystem verschieden zu schreiben und zu benutzen sind. Statt dessen muss man etwa zwei
Dutzend Kommandos und Regeln (z.B."effektive Adresse") im Kopf halten und kommt damit durch 99% aller Aufgaben
durch. Weitere Kommandos und Regeln kann man z.B. in den Quelltexten zu meinen Programmen ASMn, ASMnr und ASMat
nachlesen (inclusive der Methode, die Opcodes zu basteln).
Damit kann meine Antwort auf die vierte Frage jedenfalls heissen:"Es wird nichts verschenkt und nichts verbaut"!
(...abgesehen von Geschenken, die in C/C++ geschrieben sind und deshalb auch geschenkt zu teuer sind.)
Wer glaubt, dass damit irgendwas schwieriger würde, liegt schwer daneben (wahrscheinlich in einer UNIX-Welt).
Meine Antwort erfordert nicht nur weniger Binärcode als in anderen Betriebssystemen für gleiche Zwecke gebraucht
wird, sondern auch weniger Quellcode nicht nur im Betriebssystem, sondern auch allen nur denkbaren "Anwendungen".
Das muss genauer erklärt werden. Ich nenne es ...
Oben habe ich das Problem der Bereitstellung von 0-Adressen vorgestellt, das im AT-PC mit Segmentierung des
Arbeitsspeichers gelöst ist, und im protected mode auf besondere Weise. Basis-Adressen sind dabei in Deskriptoren in
der GDT definiert und über Selektoren adressierbar, die im wesentlichen eine Offsetadresse in dieser Tabelle darstellen
(Bits 0,1,2 sind Angaben über Privilegien und Zieltabelle GDT oder LDT). Und ich schalte nicht in den page mode weiter,
dessen Benutzung ich aber niemand unbelehrbarem verbaue...
Eine Konsequenz ist, dass Prozeduren nur entweder NEAR oder FAR gerufen werden können und dass jede Adressierung über
Segmentgrenzen hinweg (definiert in cs/ds) auch einen Segmentbezug enthalten muss, undzwar in den Segment-Registern,
die im protected mode Selektoren enthalten müssen und automatisch zu einer Basis-Adresse der GDT im GDT-Register
addiert werden, bevor damit ein Deskriptor adressiert wird.
Namen können dann in Assembler nicht verwendet werden, und normalerweise auch keine absoluten Adressen!
Will man aber Namen verwenden, dann muss zur Laufzeit eine aktuelle Erzeugung von Bezügen zwischen Namen und Adressen
stattfinden. In jedem Betriebssystem ist es ein Linker (es gibt viele Abarten), der die Verknüpfung von Programmen
untereinander und mit dem Kernel besorgt. In LINUX werden z.B. beim Booten über 30000 solcher Bezüge hergestellt. Weil
dort keine beliebig grossen Segmente, sondern pages benutzt werden, ist das garnicht verwunderlich.
Das muss nur sein, wenn Namen gewollt sind!
Weil das Betriebssystem zeitlich vor einem Programm geschrieben wird, das seine Dienste nutzt, und im weiteren auch
jede Dienstbarkeit vor nutzenden Programmen, lassen sich alle Offset-Anteile von Adressbezügen vollständig aus Quelltexten
ableiten (die natürlich lesbar sein müssen!). Eine aktuelle Herleitung ist prinzipiell überflüssig! Und es ist bei vielen
anderen Bezügen zu Dateinamen oder Konfigurationen auch überflüssig, sie jedesmal mit einer Initialisierung herzustellen.
Sie werden am besten während einer Installation hergestellt!
Auch die meist notwendige Übergabe von "Parametern" kann enorm vereinfacht werden, wenn sie in Register geschrieben
werden - was nur mit Assembler möglich ist. Die Zuordnung grosser Datenmengen kann über Basis-Adressen in Registern
stattfinden und muss keineswegs über den Stack gewälzt werden - der damit kleiner bleibt und kalkulierbarer schwillt.
Die durch "höhere" Programmiersprachen erzwungene Einsparung von Registertransfers (implizit in einer "Zuweisung"
enthalten) ist nämlich nicht etwa ein Vorteil, sondern ein ebenfalls entscheidender Mangel, der zusätzliche Transfers
nötig macht, die ein Assembler-Programmierer vermeiden kann. Der kann ausserdem viele Werte ohne Variablentransfer
verfügbar machen - mit immediate-Kommandos. Schliesslich kann der Adressrechner in der CPU genutzt werden, der von
Hochsprachen-Compilern nur in Ausnahmefällen nutzbar gemacht werden kann. Das allerdings geht nur einfach genug, wenn
nicht der page mode eingeschaltet ist.
Es ist aber noch ein Problem zu lösen. Dabei ist der Ausgangspunkt, dass jedes(!) Programm im wesentlichen so aussieht:
1.Adresse enthält 1.Kommando
...........Initialisierung
X.Adresse enthält 1.Kommando in einer unendlichen Schleife mit Eingabeprüfungen oder Programmabbruch
...........Prozeduren, die aus der Initialisierung oder der Schleife gerufen werden
...........Variable und Konstanten
Dabei können die mit Punkten gekennzeichneten Programmteile durchaus in der Abfolge vertauscht sein.
Dieses Programm kann das einzige in einer Maschine sein. Um alle wünschenswerten Zwecke zu erfüllen, müsste es aber sehr
gross sein. Weil gerade in der Frühzeit der Computerentwicklung Speicherplatz sehr knapp war, mussten von Anfang an Wege
gefunden werden, das grosse einzige Programm in viele Teile zu zerlegen.
Wenn die Teile aber in beliebiger Abfolge im Arbeitsspeicher liegen sollen, geht es nicht nur darum, dass ein Programm
ein anderes finden kann, mit dem es zusammen wirken soll, es muss es auch als ein bestimmtes erkennen können. Programme
müssen deshalb mit einer Individualität ausgestattet werden. Sollen also die Programme A,B,C zusammenwirken, muss dem
Programm B nicht nur eine Anfangsadresse von A vermittelt werden, sondern auch, dass es die von A ist und nicht die von C.
Die herkömmliche Lösung des Problems ist ein Dateiname für jedes Programm. Es genügt aber eine Ziffer, weil nur ein
Programm diesen Namen wirklich braucht! Je mehr Buchstaben für Namen oder gar "Pfad"-Namen verwendet werden, umso
zeit- und platzraubender werden Suche und Suchprogramm. Ideal ist ein 64-stelliger Name, der mit der Offsetadresse im
Selektor und einer anderen absoluten Basis-Adresse verfügbar wird. Der Name kann dann in den untersten 32 Stellen mit
einer einzigen ;cmp;-Anweisung selektiert werden und die weiteren Stellen können sonstwie die Bedeutung des Inhaltes
typisieren. Man braucht dann keinen Linker mehr. Jedes Programm, das in der GDT lesen kann, kann sehr einfach andere
Programme finden, die es zur Ausführung braucht, und Daten, die es selbst nicht enthält. Im weiteren nenne ich diese
zweite Tabelle "Global Nickname Table" (="GNT") und die 64-stelligen Namen "nickname" (="Spitznamen").
Da kein Linker verwendet wird, kann jedes Programm ein "standalone"-Programm sein, das mit einem ersten Opcode auf
Adresse 0 des Programm-Segmentes beginnt. Da aber jede Information über Label-Adressen in diesem Programm in fester Relation
zur Adresse 0 stehen muss, wenn weitere Programme diese adressieren können sollen, wird für die Struktur solcher
Programme folgendes vorgeschrieben:
Wie ich eingangs bemerkt habe, sind individuelle Namen für Programme nötig, wenn sie während der Laufzeit an beliebiger
Position stehen sollen ("relocation"). Nur über einen Namen kann dann die aktuelle Segmentadresse gefunden werden.
Diese Namen bestehen aus einem 32-stelligen Binärwert, der erlaubt, 4G Programme zu unterscheiden.
Damit der Offset in einer Namenstabelle direkt als Selektor verwendet werden kann, gebe ich den Namen noch ein Attribut.
Das kann zunächst mal dazu dienen, im gleichen Zug mit der Namensfindung zu entscheiden, ob das benamte Segment ein
ausführbares Binary ist oder eine Datei, editierbar oder nicht usw...
Insbesondere kann man mit einem solchen Attribut auf einen weiteren notwendigen Namensstapel verweisen, der die Namen
enthält, die der Benutzer tatsächlich lesen können muss: editierbare Dateien oder startbereite Programme.
Weitere Spitznamen werden als laufende Nummer während einer Programminstallation vergeben und in einer speziellen
Installationsdatei bezogen auf ihre eigentlichen Namen, die dann 48 Zeichen lang sein können - in ASMOS eine elementare
Länge. Die genauen Regeln für die Definition von Spitznamen entnimmt man dem Quell-Programm von ASMOS...
Diese Methode erübrigt eine insbesondere in UNIX-Systemen gepflegte Bürokratie des "process management" komplett.
Für die Installation eines Programmes genügen in ASMOS kaum mehr Programmzeilen als man in C/C++ Programmen für
%include... und %define...Anweisungen benötigt, und ausserdem einige besondere Prozeduraufrufe und Sprünge, die man aus
meinen Beispielprogrammen Kopieren&Einfügen kann. Abgesehen von einem Installationsverzeichnis, das die Namensbezüge
von Dateinamen zu Spitznamen enthält, sind keinerlei weitere Strukturen nötig!
Nur wer die Qualen hinter sich hat, eine UNIX-Bürokratie zu durchschauen, kann ermessen, welche G-Byte-Müllberge diese
Methode komplett erübrigt. Und wer jemals eine ELF-Datei gelesen hat und sich gewundert hat, warum bis zum zehnfachen
des Opcodes Pfadnamen und irgendwelche Kürzel sind, kann ermessen, wieviel Arbeitsspeicher plötzlich verfügbar wird,
wenn dieser Müll in Spitznamen umgesetzt wurde.
Eine weitere Frage ist: Wie werden Treiber in den Kernel integriert?
Davor steht die Frage, inwieweit sich ein Treiber von anderen Programmen unterscheidet. Ein Programm ist er nämlich
insofern, als er eine eigene Basis-Adresse enthält, also in einem eigenen Segment arbeiten muss.
In ASMOS werden "Treiber" nur solche Programme genannt, die in einer Installation zu einem Teil des Kernels (oder auch
eines anderen Programmes) werden und mit ihm zusammen geladen und initialisiert werden.
Das erfordert einige wenige Festlegungen, die einen Treiber dann von einem Programm unterscheiden, das separat
gestartet wird, aber dennoch exakt das gleiche bewirkt (solche Quasi-Treiber könnte man "Module" nennen - ich bleibe
aber bei "Programm", weil eine Unterscheidung sehr willkürlich wäre).
Die gängige Definition eines Treibers ist, dass er mit der Gerätschaft umgeht.
In ASMOS sind aber Treiber stets solche Programme, die beim ersten Ansprung nur initialisiert werden, dann aber nicht
mehr auf ihrer Basis-Adresse angesprungen werden. Sie bestehen aus Prozeduren, Sequenzen oder Daten, die mit der
Initialisierung Teil des Programms werden, mit dem sie verknüpft werden (insbesondere also ASMOS). Solche Treiber
werden grundsätzlich mit einem Ruf initialisiert.
Das Programm ist so gestaltet, dass es bereits ein komplettes System darstellt, in dem nicht nur Dateien bearbeitet und
in Dateisystemen gespeichert werden können, sondern auch Kontrolle von Maschine und Start bzw. Installation von
Programmen über Menüfunktionen erreichbar sind. Alles wird aus einer Tastaturabfrage heraus verzweigt, die auch durch
eine andere Schleife ersetzt werden kann. In dieser Schleife sind drei Schleifen gekapselt, die im Effekt alles
erlauben, was in anderen Betriebssystemen nur mit multi tasking erreicht werden kann. Es gibt einen Editier-Modus, der
auch in Teil-Fenstern existieren kann, einen Kopier-Modus, der z.B. auch rohe Kopien (Binärcode) erlaubt, und es gibt
schliesslich einen Menü-Modus, in dem unter anderem der Programmstart erledigt wird.
Was da alles zu haben ist, kann man in der Datei "ASMOSdocD" nachlesen. Bezogen auf die Funktionalität werden nicht nur
MSDO$ und Derivate himmelweit übertroffen, sondern auch eine einfache Installation von LINUX ohne Xserver
(=nur Text-Modus), mit C-Standard-library, mit Dateimanager "mc", Editier-Programm "vi" und Kommando-Interpreter
"bash".
Dabei besteht ASMOS aus etwa 130000 Byte Binärcode, während allein der "vi" aus mehr als 1 MByte gemacht ist (und
weniger bietet als meine zwei Editier-Modi, die ca.20000 Byte ausmachen). Ausserdem sind Prozeduren in ASMOS verfügbar,
die weit über den Funktionsumfang der C-Standard-library hinaus gehen und die denkbar einfachste Einschleifung von
Programmen in den Menü-Modus erlauben - mit Ein- und Ausgabe über die Tastatur.
Praktisch alle Teile des Programms sind aber austauschbar, weil Sequenzen oder Prozeduren indiziert und FAR angesprungen
werden. Ein Programm (oder Treiber) kann also in seiner Initialisierung die Sprungzieladressen (="pointer") umdefinieren
und auf diese Weise die in ihm enthaltenen Programmstücke zu denjenigen machen, die bei gegebener indizierter Adressierung
angesprungen werden. So können vorhandene Abläufe ersetzt oder erweitert werden. Natürlich müssen die Prozeduren, die
vorhandene ersetzen, äquivalent agieren, also z.B. bestimmte Statusbits benutzen oder bestimmte Variable definieren oder
bestimmte Parameter aufnehmen oder zurückgeben. Es kann aber auch nur irgendeine Tabelle getauscht werden, die ebenfalls
indiziert adressiert wird. Und falls das alles ist, was zu tun ist, kann ein Treiber bereits während der Installation
verschwinden. Vor allem aber können mittels Programmen oder Treibern Verzweigungen eingerichtet werden, die die
Funktionalität von ASMOS erweitern.
Das um Treiber erweiterte ASMOS kann nicht mehr vollständig re-initialisiert werden! Zukünftige Treiber können
unkalkulierbar Änderungen setzen. Bei Fehlverhalten muss deshalb ausgeschaltet werden - auch das in ASMOS ohne Ritual.
Treiber werden mit dem Kernel mittels eines besonderen Installationsprogrammes verknüpft. Dieses in FDOS2 enthaltene
Programm formt aus ASMOS-Binary und Treiber-Binaries ein kohärentes Stück, das vom Bootloader in einem Zug vom Bootmedium
in den Speicher transferiert wird und das ohne weiteren Dateitransfer initialisiert wird.
Damit sind ergreifende Bootrituale mit Willkommensgrüssen, Wolken und Fahnen zur Unterhaltung oder uferlose Textausgaben,
die keiner lesen kann, Vergangenheit. ASMOS bootet in wenigen Sekunden in einen Editor mit einem Menü-Modus, der einen
kargen "Prompt" ersetzt. Die meiste Zeit wird mit einer Speicherprüfung vertan, die äusserst gründlich ist.
Der Ablauf der Installation von Treibern (und Tabellen) sieht immer so aus:
ASMOS enthält unter "programend:" eine besondere Adresse. Sie kann im Binary auf Adresse 8 gelesen werden. Nach
"programend:" schreibt das Installationsprogramm die Anzahl der folgenden Basis-Adressen von Treiber-Binaries.
Wenn z.B. ein einziger Treiber anschliesst, steht dort eine 1, gefolgt von der Offsetadresse im Kernelsegment, auf der
der erste Opcode des Treibers steht - also so: