==Die Redaktion== Hier ist sie also, die neue Ausgabe von "Lowlevel". Nachdem Roshl in letzter Zeit wegen seinem Abitur keine Zeit mehr für das Magazin hatte und die letzte Ausgabe schon fast ein halbes Jahr zurück liegt habe ich mich bereit erklärt, mein Glück zu versuchen und hier ist das Ergebniss, diese Ausgabe. Zuerst möchte ich mich mal vorstellen, ich heiße Joachim Neu, bin 14 Jahre alt und wohne in München. In meiner Freizeit spiele ich gerne Computer, treffe mich mit Freunden/Freundinnen und programmiere an meinem Betriebssystem "HighOS". So, nun aber wieder zurück zu dieser Ausgabe. In ihr steht so einiges über Multitasking für die etwas weiteren unter uns aber auch etwas über den Wechsel in den Protected Mode ist dabei. Wer in den letzten Tagen und Wochen wie Roshl wenig Zeit hatte wird das aktuelle im Bericht ausm Forum und dem Newsbereich erfahren. Aber da die (Computer-)Welt nicht nur aus Betriebssystemen besteht gibt es für gelangweilte Programmierer ein bisschen Abwechslung in unserer Kolumne beziehungsweise dem Vermischten Bereich. An dieser Stelle möchte ich mich nun bei allen bedanken, die bei dieser Ausgabe mitgewirkt haben. Sie haben geholfen, dieses Magazin fertig zu stellen und nicht zu Letzt ihnen ist es zu verdanken, dass es diese Ausgabe gibt. Ich hoffe sie sind auch für andere ein Vorbild, sich ein bisschen für das Magazin einzusetzen und den ein oder anderen Artikel zu schreiben. Alle Daten über die Verfasser der Artikel gibt es im Impressum. Jetzt möchte ich noch Roshl danken, der es glücklicherweise geschafft hat, uns/mir eine Testplattform für das Design zusammen zu bauen, sodass diese Ausgabe nicht alzu grauslig erscheint, und für das Einbauen einer Möglichkeit bei den BB-Codes, mit der man Text farbig hinterlegen kann, ohne die ich den Artikel im Thema der Ausgabe hätte neu schreiben müssen. So, nun reichts aber mit dem ganzen Gerede. Viel Spaß beim Lesen der siebten Ausgabe von Lowlevel diesmal unter der Regie von mir, Joachim Neu. ==News== * bochs: Das 2. Pre-Release des Beliebten x86er-PC-Emulators Bochs ist freigegeben worden. * ReactOS: Vom Windows NT kompatiblen ReactOS Betriebssystem wurde die Version 0.2.5 freigegeben. * MenuetOS: Das Final Realease 0.78 des komplett in Assembler geschriebenen MenuetOS wurde mit vielen Neuerungen freigegeben. Außerdem ist wenig später das 1. Pre Release der 0.79er Version erschienen. * E/OS LX: Die Version 0.2.5 des E/OS LX Projektes ist erschienen. Dieses Betriebssystem kann sowohl Win32/16, Linux, DOS als auch FreeBSD Programme ausführen. Es basiert hauptsächlich auf anderen Projekten, wie Linux, FreeBSD, ReactOS und Wine. * TinyOS: TinyOS ist in der Version 1.1.11 erschienen.
==Thema der Ausgabe: Multitasking mit TSS== ===Einleitung=== In diesem Tutorial möchte ich jedem, der gewillt ist, Multitasking in seinem Betriebssystem mit TSS auf Hardwarebasis zu machen, erklären, wie er dies machen kann, bzw. anderen, die nicht beabsichtigen dies zu tun, zeigen, wie Multitasking in der Theorie funktioniert. In diesem Tutorial werde ich zuerst auf die verschiedenen Formen von Multitasking und einen Teil der Theorie, und danach auf TSS-Multitasking speziell eingehen. Ich hoffe es macht Spaß und wünsche nun viel Verknügen. ===Überblick=== Bei Multitasking gibt es 2 wichtige Teile, die man immer braucht. Der erste Teil ist eine Tabelle, in der alle Tasks aufgelistet sind. Im zweiten Teil wird der aktuelle Taskzustand gespeichert. Es ist wichtig, dass der Computer alle Inhalte der CPU-Register speichert, sodass das Programm vom Taskwechsel nichts merkt und davon nicht beeinflusst wird.
Jede Zeile der Tabelle gibt ein Word wieder, eine 16 Bit lange Zahl. Eigentlich gibt es
nichts, was unterschiedlich zu dem normalen Aufbau eines Deskriptors wäre,
deswegen erkläre ich nur kurz alle Felder.
* Alle, die mit ''Limit'' bezeichnet sind geben die Größe des Segments an. * Alle mit ''Basis'' bezeichneten geben die Basisadresse im
Speicher an. Das Bit ''P'' gibt an, ob sich das Segment im Speicher (present) befindet. Es sollte immer auf 1 gesetzt sein, da es meiner Meinung nach nicht zu den schlausten
Lösungen zählt, TSSs auszulagern, die man ständig braucht, zumal diese nicht sehr viel
Speicher sparen.
*Die 2 Bit lange Stelle ''DPL'' gibt das DescriptorPrivilegeLevel an.
Damit kann man kontrollieren, welche Privilegstufe Programme brauchen, die auf
dieses Segment zugreifen wollen.
*Das Bit ''S'' ist das System-Bit. Es unterscheidet
spezielle Segmente (wie TSS und alle Gates) von Standartsegmenten (Codesegment,
Datensegment). Bei speziellen Segmenten ist es nicht gesetzt, sprich 0.
* Dann gibt es noch das ''G''(ranularity)-Bit, das angibt, ob die Größenangabe des Deskriptors in Bytes (=0) oder in Kbytes (=1) zu verstehen ist. Da ich noch kein TSS mit 1 Kbyte oder mehr Speicherverbrauch gesehen habe, sollte dies den Wert 0 haben.
* Das ''D''-Bit gibt an, ob
der Deskriptor zu dem 286er kompatibel ist, oder nicht. Dort empfehle ich den Wert 1,
also inkompatibel, dann kann man das Segment vergrößern, ohne sich irgendwann man
damit wieder auseinander setzen zu müssen.
* Das ''AVL''-Flag steht zur freien Verfügung.
* Das ''Typ''-Feld haben wir ja schon besprochen, es muss bei einem TSS den Wert 1001b beinhalten.
Wenn ein Task aktiviert wird, so wird sein ''Typ''-Feld mit dem Wert ''1011'' als
Binärzahl belegt. Das zeigt, dass das aktuelle Segment aktiv ist und man nicht zu ihm
wechseln kann. Davon wissen wir schon, dass man mindestens zwei Tasks braucht,
damit Multitasking mit TSS überhaupt richtig funktioniert. Ebenso wird beim Verlassen
eines Tasks der Wert wieder zu 1001b.
===Task-Tabelle===
Desweiteren braucht man noch, wie besprochen, eine Tabelle, die die
Deskriptornummern und TaskIDs speichert. Auch der Status wird dort oft gespeichert,
da das System ihn braucht, um zum Beispiel pausierte Tasks zu übergehen, und es
umständlich währe, diesen Wert erst aus dem TSS zu lesen, zumal dies nicht als Daten-
Segment lesbar ist, was heißt, dass man erst den Platz im Speicher finden und dann über
ein anderes Segment auslesen müsste. Außerdem beinhaltet er oft noch eine TaskID,
damit man den Task nicht über seine TSS-Nummer identifizieren muss. Da man
maximal 8192 TSSs haben kann (maximale Größe der GDT ist 0x10000), allerdings
aber noch ein paar Einträge fürs System abgehen, reicht ein Word zum Speichern der
TaskID und TSS-Nummer. Als Statusbereich dürfte auch ein Word (16 Bits) reichen.
Auch in dieser Tabelle ist eine Zeile ein Word.
http://www.joachim-neu.de/public/lowlevel/7/3_3.png
Es gibt 2 Möglichkeiten, das Ende dieser Tabelle aus Task-Tabellen-Einträgen zu
finden.
# Man speichert in einer Variable, wie viele Einträge man hat und überprüft das aktuelle und das maximale Offset, um einen WrapArround (beim Anfang anfangen) zu vollziehen.
# Man gibt am Ende der Tabelle dem nächsten leeren Eintrag die ID 0xFFFF oder ein sonstiges "Endezeichen" und führt einen WrapAround aus, sobald man als ID den Wert des Endezeichens gefunden hat.
Das war eigentlich schon das Wichtigste über die Task-Tabelle.
===TaskGates===
Außerdem gehören zu TSS-Multitasking noch TaskGates. TaskGates kann man sowohl
in der GDT als auch in der IDT eintragen. Wenn man sie an erste Stelle stellt, so wird
der durch das TaskGate angegebene Task bei der 0. Exception gestartet. Ebenso ist es
bei allen anderen Interrupts und/oder Exceptions. Ein TaskGate ist wie folgt aufgebaut:
(wieder ist jede Zeile ein Word)
http://www.joachim-neu.de/public/lowlevel/7/3_4.png
Alle Bereiche, die ''Reserviert'' sind, sollten den Wert 0 beinhalten. Im Bereich ''TSS-Deskriptor-Nummer'' wird der Deskriptor festgehalten, auf den das TaskGate verweist.
Die Felder ''P'', ''DPL'' und ''S'' habe ich vorher schon erklärt, sodass dies
hier überflüssig ist. Der ''Typ'' muss bei einem TaskGate den Wert 0101 als Binärzahl beinhalten.
TaskGates werden an sich eigentlich beim Multitasking selten verwendet, sondern
lediglich als Einträge in der IDT, damit bei einer Exception oder einem Interrupt ein
eigener Task aufgerufen wird. Dann wird das NT-Flag gesetzt und die CPU wechselt bei
IRET zu dem Task, dessen TSS-Deskriptor im BackLink-Feld eingetragen ist.
===Dispatcher===
So, nun aber nach dem ganzen Aufbauzeug zur Theorie. Ein Taskwechselalgorithmus
sollte idealerweise aus zwei Teilen bestehen, dem Scheduler und dem Dispatcher, um
den wir uns nun kümmern wollen.
Der Dispatcher ist dafür verantwortlich, dass der
Task die CPU zugeteilt und entzogen bekommt. Er muss alle Register laden bzw
speichern und den eigentlichen Taskwechsel veranlassen. Beim softwarebasierenden
Multitasking besteht diese Aufgabe des Dispatchers darin, zuerst die Werte des
aktuellen Tasks auf dessen Stack zu pushen, dann den neuen Stack zu laden und die
Werte des Tasks zu popen. Bei hardwarebasierendem Multitasking besteht die Aufgabe
lediglich darin, einen Task-Switch zu veranlassen, in dem die CPU dann eigenständig
die Register sichert und die neuen lädt.
===Scheduler===
Und hier setzt auch schon der Scheduler an. Während der Dispatcher nur die Taskwerte
sichert und die neuen setzt besteht die Aufgabe des Schedulers darin, den nächsten Task
auszuwählen. Dies tut er anhand der Scheduling-Strategie. Es gibt verschiedene dieser
Strategien, welche wir später noch genauer besprechen werden. Bei der einfachsten
Strategie sucht der Scheduler einfach nur den nächsten Task in der Task-Tabelle raus
und gibt dessen Werte an den Dispatcher weiter. Bei hardwarebasierendem Multitasking
ist dies nur die Nummer des TSS-Deskriptors, bei softwarebasierendem Multitasking ist
dies SS und ESP des Tasks.
===Scheduling-Strategien===
Dies ist einer der umfangreichsten Punkte und der schwierigste, so meinen Experten.
Das Problem sei ihrer Meinung nach nicht das Wechseln der Tasks sondern das
Auswählen des Tasks, welcher als nächstes zu bearbeiten ist. Ich bin allerdings der
Meinung, dass das Problem anfangs eher das Wechseln an sich ist, weshalb man sich
nicht mit warscheinlichkeitsberechnenden Algorithmen rumschlagen sollte, sondern
sich anfangs mit dem einfachsten Verfahren abgeben sollte, einfach den nächsten Task
zu nehmen, der in der Tabelle kommt. Allgemein werden an eine Scheduling Strategie
folgende Anforderungen gestellt:
# Sie muss fair sein, was heißt, dass alle Tasks von Grund auf gleich zu behandeln sind, sodass kein Task ständig dran kommt und kein Task nie dran kommt.
# Sie darf nicht zu komplex sein, sodass siche die CPU mehr mit dieser als mit den Tasks beschäftigen würde.
# Sie muss unabhängig sein, das heißt, sie darf nicht mit der Anzahl der Tasks zusammenhängen, zum Beispiel nur bei 5 laufenden Tasks funktionieren.
# Sie sollte berechenbar sein, also nicht auf Zufallswerten basieren oder auf jedem System anderst funktionieren.
# Sie darf nicht beeinflussbar oder manipulierbar sein.
Es gibt viele verschiedene Strategien. Die einen benutzen Prioritätsstufen zum Einteilen
der Tasks, andere benutzen die Länge der Tasks. Bei Prioritätseinteilungen gibt es
entweder die Möglichkeit, den Tasks die Prioritäten nach ihrem Einsatzbereich (Treiber,
Programm,...) zu geben, und dadurch zum Beispiel den Treibern mehr Rechenzeit
zuzuteilen, oder aber das Privileglevel zu erhöhen, wenn der Task sich selber pausiert
und damit anderen den Vortritt lässt, wenn er gerade keine Rechenzeit benötigt. Sehr
häufige Scheduling Strategien sind ''Round Robin'', ''First Task First'' und ''Shortest
Time Left''. Diese und einige andere werde ich nun ein bisschen erläutern:
===Round Robin===
Bei Round Robin werden alle Tasks in einer Kette hintereinander abgearbeitet und
jeder bekommt die gleiche Rechenzeit. Wenn man Prioritäten benutzt, so hat man
mehrere Task-Tabellen nach denen man die Tasks aussucht. Die andere Möglichkeit
ist, dass es jedes Prioritätlevel nur einmal gibt und ein Task beim Erhöhen seines
Levels einfach mit seinem direkten oberen Nachbarn tauscht, und dadruch nach oben
kommt.
====First Task First====
Dieses Verfahren ist eigendlich mehr für Batchbetrieb, also dem Abarbeiten der
Tasks nacheinander als gleichzeitig gedacht. Es folgt dem Sprichwort ''wer zuerst
kommt mahlt zuerst'', sodass ein Programm also den ersten Task ausführt und aber
derzeit auch noch andere annimmt. Und nach Beendigung dieses Tasks wird der
nächste genommen. Das Verfahren ist unbrauchbar für Systeme mit Mikrokerneln,
bei denen Treiber und Module auch Tasks darstellen, da diese nicht aufgerufen
werden können. Desweiteren scheiden sich die Geister, ob diese Methode überhaupt
den Namen "Multitasking" verdient, da die Tasks nicht gleichzeitig ausgeführt
werden, jedoch aber hintereinander.
====Shortest Time Left====
Dieses Konzept besagt, dass der Task, der die kleinste Zeit in Anspruch nehmen wird
zuerst an die Reihe kommt. Meist bemisst man die Zeit anhand der Größe des Tasks.
Jedoch müssen diese Werte ständig aktualisiert werden und es ist schwer für das OS
zu sagen, was Code und was Daten sind. Ein Task könnte nur ein Ladeprogramm
beinhalten, dass den richtigen Task lädt und würde somit als erstes ausgeführt und
umgeht damit das System. Somit ist einer der Grundsätze nicht erfüllt.
====Longest Time Left====
Dieses Konzept ist exakt das Gegenteil des vorherigen. Es bevorzugt den längsten
Task und wieder gelten hier alle Einwände, welche bei „Shortest Time Left“ genannt
wurden.
Das waren auch schon die meiner Meinung nach wichtigsten. Es steht jedem frei,
weitere dazu zu entwickeln und bestehende zu verfeinern. Ich selber benutze Round
Robin ohne jegliche Form von Prioritäten.
===Taskwechsel===
Nun aber zum meiner Meinung kompliziertesten Punkt der Praxis, dem Wechsel selber.
Das Implementieren eines Round-Robin-Schedulers sollte nicht alzu schwer gewesen
sein, aber der Wechsel hat es in sich, er ist der schwierigste Teil, den ich zu bewältigen
hatte, als ich TSS für mein Betriebssystem implementierte.
Zuerst möchte ich schildern, wodurch ein Taskwechsel an sich ausgelöst wird. Es gibt
mehrere Möglichkeiten:
# Ein Far-Jump oder Far-Call bei dem als Selektor ein TSS angegeben wird. Der Offsetteil wird hierbei ignoriert.
# Ein IRET nachdem ein Task durch ein TaskGate in der IDT aufgerufen wird, also das NT-Flag gesetzt ist.
# Ein Far-Jump oder Far-Call auf ein Task-Gate. Auch hier wird der Offsetteil ignoriert.
Es gibt also 2 Möglichkeiten:
* Man macht einen Far-Jump auf ein TSS.
* Man manipuliert das NT-Flag und simuliert den Aufruf als ein Interrupt durch ein
TaskGate.
Die zweite Methode habe ich noch nie erfolgreich zum Laufen gebracht und sie ist
zweifellos die schwierigere, da man nicht nur die Flags sondern auch BackLink-Felder
und Stacks manipulieren muss. Deswegen werde ich nun auch die erste Methode
ausgiebig erläutern.
Nun, man möchte sich nun denken "nichts leichter als das" und schreibt munter in
seinen Code JMP AX,0x00000000, wobei man gedenkt, AX mit der
Deskriptornummer zu beladen. Spätestens beim Versuch zu Assemblieren wird man
feststellen: das geht nicht. Man muss sich also was anderes ausdenken. Erstmal möchte
ich sicherheitshalber das ganze zu diesem ausbauen: JMP DWORD AX:0x12345678.
So wird erzwungen, dass der Assembler auch den kompletten Offsetteil mit übersetzt und
nicht Teile durch Abkürzen unter den Tisch kehrt. Das Offset habe ich zu dieser
Zahlenfolge gemacht, da ich es für noch ein Schritt sicherer halte, dass das ganze nicht
gekürzt wird. Noch haben wir aber das Problem mit dem AX nicht geklärt. Da dies auf
jeden Fall nicht geht wandeln wir auch das um: JMP DWORD 0x1234:0x12345678
Dies wird der Assembler zwar problemlos schlucken, aber leider bringt es uns nicht
viel. Wie man vielleicht schon merkt setze ich viel darauf, dass der Assembler dies
vollständig und ungekürzt in die Opcodes umwandelt, denn das ist unser Angriffspunkt:
HexDump. Wir manipulieren einfach den vom Assembler erstellten Maschinencode
während der Ausführung. Ein paar Versuche haben ergeben: JMP DWORD
0x1234:0x12345678 wird vom Assembler in die Bytes 0xED, 0x78, 0x56, 0x34, 0x12,
0x34 und 0x12 umgewandelt. Wir sehen also, dass die letzten zwei Bytes den Selektor
angeben. Wir bleiben also weiterhin dabei, in AX unsere TSS-Deskriptornummer zu
haben und bauen uns dann also eine Routine, die die Opcodes manipuliert. Da wir die
Bytes in anderen Datentypen auslesen als sie normalerweise abgelegt sind kommt es zu
derartigen Verdrehungen, da die CPU andere Zahlendarstellungsverfahren benutzt, als
der Mensch und die Zahlen konvertiert werden (näheres in meinem Tutorial
''Datenveränderungen im RAM''). Wir können also getrost einfach ein Word an die
Stelle mit dem Selektor schreiben, der sich 5 Bytes hinter Befehlbeginn befindet. Den
Befehlsbeginn finden wir anhand eines Labels und schon haben wir alles zusammen um
folgenden Code zu bauen:
PUSH EDI MOV EDI, tss_switch_label ; Offset laden ADD EDI,0x00000005 ; zum Operanten navigieren STOSW ; neuen Wert eintragen tss_switch_label: JMP DWORD 0x1234:0x12345678 ; springen! POP EDIDer Code verlangt lediglich, dass sich in AX die TSS-Deskriptornummer des Tasks befindet, zu dem gewechselt werden soll. Alles andere macht die CPU. Somit dürfte das Wechseln auch kein Problem mehr sein. ===Implementierung und Praxis=== Tja, wir haben jetzt alles wichtige besprochen, Zeit, zusammen mal einen Code zu coden, den ihr in euer TimerIRQ einbauen könnt, und der für euch den Taskwechsel macht. Zuerst einmal brauchen wir den Scheduler. Ich gehe davon aus, dass die Task- Tabelle an der Adresse 0x00005000 liegt. Die Größe wird anhand einer Variable angegeben, die das Offset des Endes beinhaltet, welche ''tasking_end'' heißt und vom Typ Double Word ist. Außerdem existiert eine Variable mit dem Namen ''tasking_current'', welche ebenfalls vom Typ Double Word ist und den aktuellen Punkt in der Tabelle festhält. In unserem Code bauen wir reines Round Robin, wir brauchen also Task-Tabellen- Einträge nach der Form, welche unter „4. Task-Tabelle“ gezeigt ist. Ein Eintrag ist also 6 Bytes groß. Coden wir nun einfach mal den Scheduler. Da wir am Anfang des IRQs keinen Dispatcher brauchen, können wir gleich mit dem Scheduler anfangen.
ADD DWORD [tasking_current],0x00000006 ;Werte des ; aktuellen Tasks überspringen MOV ESI,[tasking_end] ; ask-Tabellen-Ende laden CMP ESI,[tasking_current] ;mit Position des zu ladenden ; Tasks vergleichen JNE scheduler_no_resetting ;falls nicht gleich, also ; das Ende noch nicht erreicht ist spingen MOV DWORD [tasking_current],0x00005000 ;sonst auf den ; Anfang zurücksetzen scheduler_no_resetting: ; hier haben wir in „tasking_current“ die Adresse der ; Daten des zu ladenden Tasks Da wir in diesem Scheduler einige Register verändern und dies natürlich nicht sein darf, müssen wir diese (nur ESI) noch auf dem Stack sichern: PUSH ESI ADD DWORD [tasking_current],0x00000006 ;Werte des ; aktuellen Tasks überspringen MOV ESI,[tasking_end] ; ask-Tabellen-Ende laden CMP ESI,[tasking_current] ;mit Position des zu ladenden ; Tasks vergleichen JNE scheduler_no_resetting ;falls nicht gleich, also ; das Ende noch nicht erreicht ist spingen MOV DWORD [tasking_current],0x00005000 ;sonst auf den ; Anfang zurücksetzen scheduler_no_resetting: POP ESI ; hier haben wir in „tasking_current“ die Adresse der ; Daten des zu ladenden Tasks Nun müssen wir aus der Tasktabelle die TSS-Deskriptornummer des zu landenden Tasks raussuchen und in AX bekommen: PUSH ESI ADD DWORD [tasking_current],0x00000006 ;Werte des ; aktuellen Tasks überspringen MOV ESI,[tasking_end] ; ask-Tabellen-Ende laden CMP ESI,[tasking_current] ;mit Position des zu ladenden ; Tasks vergleichen JNE scheduler_no_resetting ;falls nicht gleich, also ; das Ende noch nicht erreicht ist spingen MOV DWORD [tasking_current],0x00005000 ;sonst auf den ; Anfang zurücksetzen scheduler_no_resetting: POP ESI ; hier haben wir in „tasking_current“ die Adresse der ; Daten des zu ladenden Tasks MOV ESI,[tasking_current] ; Offset laden ADD ESI,0x00000002 ; zur Deskriptornummer navigieren LODSW ; Nummer in AX laden Da wir wieder die Register verändern müssen wir auch das Stacksichern verändern: PUSH EAX PUSH ESI ADD DWORD [tasking_current],0x00000006 ;Werte des ; aktuellen Tasks überspringen MOV ESI,[tasking_end] ; ask-Tabellen-Ende laden CMP ESI,[tasking_current] ;mit Position des zu ladenden ; Tasks vergleichen JNE scheduler_no_resetting ;falls nicht gleich, also ; das Ende noch nicht erreicht ist spingen MOV DWORD [tasking_current],0x00005000 ;sonst auf den ; Anfang zurücksetzen scheduler_no_resetting: ; hier haben wir in „tasking_current“ die Adresse der ; Daten des zu ladenden Tasks MOV ESI,[tasking_current] ; Offset laden ADD ESI,0x00000002 ; zur Deskriptornummer navigieren LODSW ; Nummer in AX laden POP ESI POP EAX Nun müssen wir nurnoch unseren Dispatcher dahinter hängen und die Stacksicherung wieder verschieben und schon ist der Code fertig: PUSH EAX PUSH ESI ADD DWORD [tasking_current],0x00000006 ;Werte des ; aktuellen Tasks überspringen MOV ESI,[tasking_end] ; ask-Tabellen-Ende laden CMP ESI,[tasking_current] ;mit Position des zu ladenden ; Tasks vergleichen JNE scheduler_no_resetting ;falls nicht gleich, also ; das Ende noch nicht erreicht ist spingen MOV DWORD [tasking_current],0x00005000 ;sonst auf den ; Anfang zurücksetzen scheduler_no_resetting: ; hier haben wir in „tasking_current“ die Adresse der ; Daten des zu ladenden Tasks MOV ESI,[tasking_current] ; Offset laden ADD ESI,0x00000002 ; zur Deskriptornummer navigieren LODSW ; Nummer in AX laden POP ESI PUSH EDI MOV EDI, tss_switch_label ; Offset laden ADD EDI,0x00000005 ; zum Operanten navigieren STOSW ; neuen Wert eintragen tss_switch_label: JMP DWORD 0x1234:0x12345678 ; springen! POP EDI POP EAXSo, fertig ist der Praxisteil. Ich bitte alle Leser, diesen Code nicht einfach nur blind zu kopieren, denn er lässt sich sicher nicht fehlerlos in das System integrieren, und so schwer ist das Implementieren nicht. Also selber versuchen und bei Fragen mich einfach kontaktieren. Damit dieser Code richtig geht brauchen wir aber auch einen Task, der gerade läuft. Angezeigt wird dieser Task durch das TR, das TaskRegister. Dieses beinhaltet einen Selektor auf den Deskriptor des aktuellen TSS. Um der CPU vorzugaukeln, welcher Task gerade aktiv ist, benutzen wir den Befehl „LTR Operant“. Als Operant wird die Deskriptornummer des Tasks, der gerade aktiv ist angegeben. Beim Wechsel aktualisiert die CPU das TR dann automatisch. Man braucht den Befehl also nur einmal. ===Tipps & Tricks=== Jetzt habe ich zu Schluss noch ein paar Tipps zum Programmieren von Multitasking und zum Programmieren allgemein. # Programmiere auf Sicherheit! Es geht nicht um einen Geschwindigkeitsrekord, sondern um ein stabiles Betriebssystem. Deswegen beachte und bedenke jeden Fehler, der auftreten könnte und richte Fehlerbehandlungen ein. Du musst sie nicht gleich rein implementieren, aber schaffe (wenn auch nur leere) Funktionen dafür, die du später erweitern kannst. Somit hast du ein sicheres Gerüst und gerätst nicht in die Falle, dass ein Code nicht geht, weil ein unerwarteter Fehler aufgetreten ist. # Schreibe nicht alles ab! Code den Code selber. Es wird dir helfen, das Zeug zu verstehen und eigener Code lässt sich besser ins System integrieren. # Entscheide dich fest für ein System und mach keine Abstriche. Versuche deine Gedanken durchzusetzen und gib nicht auf, bloß, weil was nicht klappt. # Achte auch die Optimierung deines Codes. Schon kleine Teile könnte vieles verschnellern und dein System sollte das Minimum an Leistung verbrauchen. # Halte alles Modular, sprich baue auch für deinen Scheduler eine Funktion. Das wird es später erleichtern, die Funktion auszutauschen, wenn du eine andere Strategie verwenden willst. # Und als letztes: Kommentiere deinen Code gut. Gerade in Assembler ist das sehr wichtig. Tja, das waren auch schon meine Tipps. ===Schluss=== Wir sind nun schon am Ende dieses Tutorials angelangt. Es ist etwas lang geworden, warscheinlich deshalb, dass Multitasking nunmal ein komplexes Thema ist, dass man nicht so einfach erklären kann. Ich hoffe es hat Spaß gemacht, ein bisschen in die Welt des Multitasking einzutauchen und davon zu hören. Bei Fragen kann ich nur wieder ermuntern, sich bei mir zu melden. Ich helfe, so gut ich kann. Meine Kontaktdaten gibt es im Impressum. ==Designtechnisches: Multitasking== ===Einleitung=== Dies ist eine Abwägung zwischen zwe Designideen. Ich will die beiden verschiedenen Möglichkeiten von Multitasking (hardwarebasierendes und softwarebasierendes) miteinander abwiegen. Dieser Text soll keine Antwort auf die Frage geben, welches Multitasking besser oder schlechter sei, sondern lediglich die verschiedenen Vor- und Nachteile aufzeigen und abwiegen. ===Grundlagen=== Was ist Multitasking? Dieser Text soll sich zwar darum nicht kümmern, trotzdem werde ich die grundlegendsten Grundlagen aufzeigen. Multitasking ist die Fähigkeit eines Betriebssystems, mehrere Programme, sogenannte Tasks gleichzeitig/parallel laufen zu lassen. Dies ist auf einem mehrprozessorigen System leicht zu verstehen, jeder Prozessor bekommt einen Task zugewiesen, was aber, wenn nur ein Prozessor vorhanden ist? Dann muss man diesen unter den verschiedenen Tasks aufteilen, de gerade bereit sind, ausgeführt zu werden. Man muss also den aktuellen Prozessorzustand speichern und den des nächsten Tasks laden. Wichtig dabei ist, dass alle Werte gespeichert werden, die einen Task beschreiben, also Registerinhalte, Flags, aktuelle Position im Code und so weiter. Der Task an sich darf davon nix merken, er darf nicht bemerken, dass er unterbrochen wurde oder, dass etwaige Register nicht die selben Werte wie vorher haben. Es gibt zwei verschiedene Methoden, Multitasking durchzuführen, kooperativ oder preemptiv. Kooperatives Multitasking besagt, dass das Programm die Kontrolle über den PC von alleine wieder an das Betriebssystem zurück gibt, welches diese dann neu verteilt, preemptives Multitasking besagt, dass dem Task die Kontrolle in bestimmten Zeitabständen vom System entzogen und neu verteilt wird. Meist wird heutzutage preemptives Multitasking verwendet, welches der Meinung aller nach das sicherste ist, weil es im heutigen Zeitalter der Viren und Trojaner sehr unsicher ist, einem Programm die Kontrolle zu übergeben, bis dieses sie selber zurück gibt. Zum regelmäßigen Entzug der CPU braucht man ein immer wiederkehrendes Ereigniss, welches die CPU auslöst und man ungestört nutzen kann. Dazu nimmt man den ersten Timer des PIC, der standartmäßig 18,2 mal pro Sekunde einen Takt auslöst, aber auch auf höhere Frequenzen programmiert werden kann. Dieser Timer löst immer IRQ 0 aus, welches das System umbiegen kann, und somit in regelmäßigen Abständen die Kontrolle über das System zurück erlangt.
void switch_on()
{
outp(0x61,inp(0x61) | 3);
}
Okay. In Assembler baue ich das schon alles mit in die _sound-Funktion ein.
So, an können wir ihn machen, nur aus noch nicht, aber wir wissen, wenn die beiden letzten Bits in
Port 0x61 auf 00b stehen, dann ist der Lautsprecher aus. Das geht also ganz leicht:
void switch_off()
{
outp(0x61,inp(0x61) &~3);
}
Und in Assembler:
_switch_off: IN AL,61h AND AL,11111100b OUT 61h,AL RETFertig. An und aus können wir ihn schon machen, aber das Herzstückt kommt noch! Einen Ton ausgeben. Das geht mit folgendem, diesmal zuerst in Assembler ;-):
_sound: MOV AX,34DDH ;Die Frequenz wird in BX übergeben. MOV DX,0012H CMP DX,BX JNC DONE1 DIV BX MOV BX,AX IN AL,61H TEST AL,3 JNZ A99 OR AL,3 OUT 61H,AL MOV AL,0B6H OUT 43H,AL A99: MOV AL,BL OUT 42H,AL MOV AL,BH OUT 42H,AL DONE1: RETSo, jetzt in C:
void sound(unsigned frequenz)
{
unsigned teiler;
teiler = 1193180L/frequenz;
outp(0x43,0xB6);
outp(0x42,teiler&0xFF);
outp(0x42,teiler >> 8);
switch_on()
//wenn der Sound aufhören soll, muss die Funktion
//switch_off aufgerufen werden.
}
===Schluss===
So, geschafft! Das waren die Geheimnisse des PC-Lautsprechers! Zwei Sachen muss beim
Nachmachen noch beachtet werden: 1. Es muss zwischen den Tönen eine Funktion benutzt werden,
die das Programm anhält, denn sonst werden die Töne viel zu schnell abgespielt, und man hört
nichts. 2. Die Assembler-Funktionen kann man nicht so in C übernehmen. Dazu muss man sich
noch um die C-Calling-Convention kümmern, und die Funktionen ein bisschen umbauen.
Ich hoffe ich konnte alles so erklären, dass es verständlich war. Und die ein oder anderen
Unklarheiten durch den letzten Praxisteil klären. Ich bedanke mich bei meinem C-Buch, aus dem
ich die Informationen hab, bei ASHLEY4 aus dem FlatAssemblerForum
(http://board.flatassembler.net/) für die Hilfe beim Code und bei allen anderen, die mir geholfen
haben. Meine Kontaktdaten zum Melden bei Problemen finden sich im Impressum.
==Software- & Treibertechnisches: FAT12 Dateizugriff==
===Einleitung===
Ein Betriebssystem zu schreiben ist wirklich nicht einfach. Will man es jedoch machen,
und man hat einen Bootloader und einen simplen Kernel, der einen Text ausgibt, geschrieben,
wird man schnell zum nächsten Problem kommen: Dateien! 'Wie/wo/was mache ich?' sind wohl die
ersten Fragen. Mit diesem Tutorial möchte ich hier ein bisschen helfen. Wichtig: Ich gehe davon
aus, dass ihr 'OS Dev-Tutorial' ([Ausgabe 1]) und 'Das FAT12 Dateisystem' ([Ausgabe 4])
gelesen und verstanden habt. OK, blabla, let's go!
===Dateien finden===
Der erste Schritt, wenn man Dateizugriff machen will, ist natürlich die Dateien finden. Und wie?
Hier brauchen wir das 'Root-Directory' (=RD). Kurz: Das RD startet im 19. Sektor. Ein Dateieintrag ist
0x20 (32) Bytes lang, und der Dateiname ist 12 Zeichen lang: Byte 0 - 11. OK, jetzt haben wir keine
Probleme mehr. Naja, fast: Wenn eine Datei gelöscht oder umbenannt wird, bleibt der alte Eintrag,
nur wird das erste Byte des Namens auf 0xE5 gesetzt. Außerdem gibt es noch etwas seltsame Einträge,
hab aber nicht herausgefunden was es für welche sind. Auf alle Fälle ist das 3. Byte dieser Einträge
immer 0x00, und so werden sie auch ignoriert. OK, jetzt verstehen wir, wie man theoretisch Dateien
finden kann. Und praktisch? Keine Angst, hier ist der Code (Der Code ist der Kernel, den Bootloader
müsste man dann schon selber machen. Danke an mastermesh für das Tutorial in LL#1 - Für den Grund hab
ich den Code verwendet.):
; - - - - - [ Dateien finden - CODE ] - - - - -
start:
mov ax, 0x2000 ; eax=0x2000
mov es, ax ; Daten werden nach ES:BX geschrieben, ES=0x2000
mov ds, ax
mov bx, 0 ; BX=0x0
loadk:
mov ah, 0x2 ; ah=2: Sektoren lesen
mov al, 0x12 ; Wie viele Sektoren wir lesen (18)
mov ch, 0 ; Cylinder 0
mov cl, 1 ; Starten bei Sektor 1
xor dx, dx ; dx=0
inc dh ; Head 1:
; Soweit ich das mitbekommen habe, besteht ein
; Cylinder aus 2 Teilen Head0 und Head1. Head0
; sind die ersten 18 Bytes (0-17) und Head1 sind
; die zweiten 18 Bytes (18-36). Da Root Entry
; auf Sektor 19 liegt, brauchen wir head1.
int 0x13 ; BIOS-Call
jc loadk ; Wenn Fehler: Nochmal
mov bx, 0x200 ; bx=Adresse des 2. Sektors, den wir in den
; Speicher gelesen haben
fat12read:
mov ah, [bx] ; ah=erstes Byte des Dateinamen
cmp ah, 0x0 ; Checken ob das Byte 0 ist: Ende der Einträge
je endfat12read ; Wenn es das Ende ist, aufhören zum Lesen
cmp ah, 0xE5 ; Checken ob das Byte 0xE5 ist: Gelöschte Datei
je fat12next ; Wenn es eine gelöschte Datei ist: Nächster Eintrag
push bx ; Speicher bx am Stack, weil es durch 'call putstr'
; geändert wird
mov al, [bx+2] ; al=3. Buchstabe des Namens
test al, al ; Testen ob null
jz fat12next ; Wenn null, dann nächster Eintrag
mov cx, 11 ; cx=11, weil ein Dateiname aus 11 Zeichen besteht
mov si, bx ; si=Adresse des Dateinamens im Speicher
call putstr ; Schreibe Dateiname
pop bx ; Stellt bx wieder her
fat12next:
add bx, 0x20 ; Nächster Eintrag
jmp fat12read
endfat12read:
mov ax, 0x1000 ; es und ds aktualisieren, da wir jetzt wieder
; im Speicher auf 0x1000 arbeiten.
mov es, ax
mov ds, ax
mov cx, 0xFFFF ; cx=0xFFFF: Gibt die Stringlänge an, weil ich aber
; zu faul war, die Zeichenlänge zu überprüfen,
; hab ichs auf das höchste gestellt. Weil
; ein String aber mit 0x00 beendet wird, ist es egal
mov si, msg ; si=Adresse des Strings
call putstr ; Schreib String
mov cx, 0xFFFF ; Nochmal das Gleiche
mov si, m_boot ; Nochmal das Gleiche
call putstr ; Nochmal das Gleiche
call getkey ; Call zu GetKey
jmp reboot ; Springe zu reboot
msg db 'Welcome to SPTH-OS 1.1',0 ; String 1
m_boot db 'Press any key...',0 ; String 2
putstr: ; Schreib-Funktion
lodsb ; [si]->al
or al, al ; Teste al auf 0x0 (=Ende)
jz putstrd ; Wenn Ende des Strings, dann
mov ah, 0xE ; ah=0xE: Schreibe Zeichen
mov bx, 0x7 ; Zeichenfarbe: 0x7 = weißes Zeichen/schwarzer
; Hintergrund
int 0x10 ; call
loop putstr ; Nächstes Zeichen
putstrd:
mov al, 13 ; Schreibe 13
mov ah, 0xE
mov bx, 0x7
int 0x10
mov al, 10 ; Schreibe 10 (13,10=Zeilenumbruch)
mov ah, 0xE
mov bx, 0x7
int 0x10
retn
getkey:
mov ah, 0 ; ah=0: Get Key BIOS Funktion
int 0x16 ; Call
ret
reboot:
db 0xEA ; Hexdump für Reboot: jmp 00FF:FF00
dw 0x0
dw 0xFFFF
; - - - - - [ Dateien finden - CODE ] - - - - -
Der Code tut, wie es sein sollte, das, was man von ihm erwartet: Er schreibt alle Dateinamen
im Root der Diskette untereinander auf. Also: Passt!
===Dateien erstellen===
Nachdem wir jetzt die Dateien gelesen haben, dürften wir kein Problem mehr haben, eine neue Datei
zu erstellen. Was ist wichtig? Zuerst müssen wir einen leeren Platz im Root-Directory für den
Dateieintrag finden. Haben wir den gefunden, müssen wir den Sektor für die Daten suchen. Hier
nehmen wir einfachheitshalber den Sektor des letzten Eintrags+1. Natürlich könnten so nur 512
Byte Dateien gespeichert werden, weil sonst Fehler auftreten können. Aber da ich nicht hunderte
von Zeilen schreiben will, um alles zu perfektionieren, hab ich nur die Einfachlösung hier.
; - - - - - [ Dateien erstellen - CODE ] - - - - -
start:
mov ax, 0x2000 ; ax=0x2000
mov es, ax ; Daten werden nach ES:BX geschrieben, ES=0x2000
mov ds, ax
mov bx, 0 ; BX=0x0
loadk:
mov ah, 0x2 ; ah=2: Sektoren lesen
mov al, 0x12 ; Wie viele Sektoren wir lesen (18)
mov ch, 0 ; Cylinder 0
mov cl, 1 ; Starten bei Sektor 1
xor dx, dx ; dx=0
inc dh ; Head 1:
; Soweit ich das mitbekommen hab, besteht ein
; Cylinder aus 2 Teilen Head0 und Head1. Head0
; sind die ersten 18 Bytes (0-17) und Head1 sind
; die zweiten 18 Bytes (18-36). Da Root Entry auf
; Sektor 19 liegt, brauchen wir head1.
int 0x13 ; Interupt 13
jc loadk ; Wenn Fehler: Nochmal
mov bx, 0x200-0x20 ; bx=Adresse des 2. Sektors, den wir in den Speicher
; gelesen haben, -0x20 deshalb, weil wir den ersten
; Eintrag auch checken müssen, und da bx am Anfang
; des loops um 0x20 erhöht wird, müssen wir es verkleinern
fat12emptyentry:
add bx, 0x20 ; Nächster Eintrag
mov al, [bx] ; al=Erstes Zeichen des Eintrags
test al, al ; Checken ob null
jnz fat12emptyentry ; Wenn Eintrag besetzt ist, dann suche weiter
mov ax, [bx-6] ; ax enthält jetzt den Sektor des letzten Eintrags
inc ax ; und wir nehmen den nächsten. ACHTUNG: Ich gehe
; davon aus, dass keine Datei größer ist als
; 512 Bytes ist. Wenn man es professionell
; machen will, muss man die Größe der vorherigen
; Datei checken.
mov ax, 0x1000 ; Code im Speicher
mov es, ax
mov ds, ax
mov [FAT12Sector], ax ; ax in den vorbestimmten Bereich schreiben
mov di, bx ; di=unser Teil des Sektors
mov bx, NewEntry ; bx=Offset vom neuen Eintrag
Data2Mem:
mov ax, 0x1000 ; Code im Speicher
mov es, ax
mov ds, ax
mov al, byte [bx] ; al=Byte zu schreiben
mov cx, 0x2000 ; Sektor im Speicher
mov es, cx
mov ds, cx
stosb ; AL -> ES:DI
inc bx ; Nächstes Byte
cmp bx, 0x20+NewEntry ; Check ob wir alle haben
jne Data2Mem ; Wenn nicht, nochmal
mov bx, 0x0 ; ES:BX -> Databuffer
Mem2Disk:
mov ah, 0x3 ; AH=3: Schreibe Sektor(en)
mov al, 0x12 ; Wir schreiben 18 Sektor
mov ch, 0x0 ; Cylinder 0
mov cl, 0x1 ; Sektor 1
xor dx,dx ; dx=0
inc dh ; Head 1!
int 0x13 ; Interrupt 13
mov ax, 0x1000 ; es und ds aktualisieren, da wir jetzt wieder
; im Speicher auf 0x1000 arbeiten.
mov es, ax
mov ds, ax
string:
mov cx, 0xFFFF ; cx=0xFFFF: Gibt die Stringlänge an, weil ich aber zu
; faul war, die Zeichenlänge zu überprüfen, hab ichs
; auf das höchste gestellt. Weil
; ein String aber mit 0x0 beendet wird, ist es egal
mov si, msg ; si=Adresse des Strings
call putstr ; Schreib String
mov cx, 0xFFFF ; Nochmal das Gleiche
mov si, m_boot ; Nochmal das Gleiche
call putstr ; Nochmal das Gleiche
call getkey ; Call zu GetKey
jmp reboot ; Springe zu reboot
msg db 'Welcome to SPTH-OS 1.1',0 ; String 1
m_boot db 'Press any key...',0 ; String 2
NewEntry: db 'FILE TXT' ; DIR_NAME
db 0x20 ; DIR_Attribute: Archiv
db 0x0 ; DIR_NTRes: Reserviert
db 0x0 ; DIR_CrtTimeTenth: Millisekunden des Speicherns
; - ist mir egal :)
db 0x6F,0x3E ; Wahrscheinlich Erstellungsdatum,
; ist hier aber egal
db 0x9C,0x2B,0x9C,0x2B ; Wahrscheinlich: Letzter Zugriff, letzte Änderung
; - ist hier auch egal
db 0x0,0x0 ; Reserviert
dd 0x733E9C2B ; Wieder 2 mal Datum - das ist in diesem Tutorial
; aber egal
FAT12Sector dw 0x0000 ; Das hier ist wichtig: Der Sektor, in dem die
; Daten liegen
dd 0x00000000 ; Dateigröße
putstr: ; Schreib-Funktion
lodsb ; [si]->al
or al, al ; Teste al auf 0x0 (=Ende)
jz putstrd ; Wenn Ende des Strings, dann
mov ah, 0xE ; ah=0xE: Schreib Zeichen
mov bx, 0x7 ; Zeichenfarbe: 0x7 = weißes Zeichen/schwarzer
; Hintergrund
int 0x10 ; call
loop putstr ; Nächstes Zeichen
putstrd:
mov al, 13 ; Schreibe ASCII 13
mov ah, 0xE
mov bx, 0x7
int 0x10
mov al, 10 ; Schreibe ASCII 10 (13,10=Zeilenumbruch)
mov ah, 0xE
mov bx, 0x7
int 0x10
retn
getkey:
mov ah, 0 ; ah=0: Get Key BIOS Funktion
int 0x16 ; Call
ret
reboot:
db 0xEA ; Hexdump für Reboot: jmp 00FF:FF00
dw 0x0
dw 0xFFFF
; - - - - - [ Dateien erstellen - CODE ] - - - - -
Es ist alles so einfach und klein wie möglich gehalten. Es fehlen einige JumpCarry, um sich vor
Abstürzen zu schützen. Da ich hier aber kein vollständiges OS schreiben will, sondern nur die
Infos, wie man so etwas machen kann, hab ich es nicht perfektioniert. Trotzdem: Der Code macht
genau was er soll.
===Dateien lesen===
Nachdem wir wissen, wie man Dateien im Root-Directory finden kann und neu erstellen kann, kommt
jetzt endlich das zum Spiel, wofür Dateien eigentlich gut sind: Daten.
Ich habe hier davon abgesehen, nocheinmal die Root-Directory auszulesen, und so weiter, deswegen
habe ich einfach einen Eintrag der RootDirectory als Daten in den Code geschrieben. Der Code kann
natürlich trotzdem verwendet werden, muss nur Root_Entry in die echte gefundene Adresse geändert
werden. Außerdem ist es noch einfacher, da nicht immer zwischen Code (ES=0x1000) und dem gelesenen
Sektoren Daten (ES=0x2000) geändert werden.
Der Code ließt die Daten einer Datei, aber höchstens 512 Bytes. Das kann aber einfach geändert werden,
habs hier nicht gemacht, weil es eigentlich nichts mit dem zu tun hat, was ich vermitteln möchte:
Dateien lesen.
Wie gesagt, der Code ist ganz kurz, bis aufs äußerste reduziert und so weiter. Funktioniert trotzdem :)
; - - - - - [ Dateien lesen - CODE ] - - - - -
start:
mov ax, 0x2000 ; ax=0x2000
mov es, ax
mov ds, ax
mov bx, 0 ; BX=0x0
loadk:
mov ah, 0x2 ; ah=2: Sektoren lesen
mov al, 0x12 ; Wie viele Sektoren wir lesen (18)
mov ch, 0 ; Cylinder 0
mov cl, 1 ; Starten bei Sektor 1
xor dx, dx ; dx=0
inc dh ; Head 1
int 0x13 ; Interupt 13
mov ax, 0x1000 ; ax=0x1000
mov es, ax
mov ds, ax
mov bx, 0 ; BX=0x0
mov cl, byte [Root_entry+26] ; Der Sektor, in dem unsre Daten liegen
add cl, 13 ; Der erste Datensektor ist anscheinend immer
; Sektor 31: -18, weil Head1
mov ax, 0x200 ; 0x200=512: Ein Sektor
xor ch, ch ; ch=0
mul cx ; cx*ax: Sektornummer*Sektorgröße: Adresse, wo wir lesen
mov si, ax ; Resultat der Multiplikation in ax, und jetzt in si
mov ax, 0x2000 ; ax=0x2000
mov es, ax
mov ds, ax
mov bx, 0 ; BX=0x0
mov cx, 512 ; 512 Bytes schreiben, wenn ein 0x0 ist, wird abgebrochen
call putstr ; Schreibe!
mov ax, 0x1000 ; ax=0x1000
mov es, ax
mov ds, ax
mov bx, 0 ; BX=0x0
mov cx, 0xFFFF ; cx=0xFFFF: Gibt die Stringlänge an, weil ich aber zu
; faul war, die Zeichenlänge zu überprüfen, hab ichs
; auf das höchste gestellt. Weil ein String aber mit
; 0x0 beendet wird, ist es egal
mov si, msg ; si=Adresse des Strings
call putstr ; Schreib String
mov cx, 0xFFFF ; Nochmal das Gleiche
mov si, m_boot ; Nochmal das Gleiche
call putstr ; Nochmal das Gleiche
call getkey ; Call zu GetKey
jmp reboot ; Springe zu reboot
msg db 'Welcome to SPTH-OS 1.1',0 ; String 1
m_boot db 'Press any key...',0 ; String 2
big_file db 'Die Datei ist größer als 512 Bytes...',0
Root_entry db 0x53,0x50,0x54,0x48 ; Das ist ein Root_Entry
db 0x20,0x20,0x20,0x20 ; Wie man den bekommt, sieht man
db 0x54,0x58,0x54,0x20 ; Oben schon
db 0x18,0x9F,0x5D,0x2B
db 0x9D,0x2B,0x9D,0x2B
db 0x00,0x00,0xC7,0xA0
db 0x9C,0x2B,0x02,0x00
db 0x0D,0x00,0x00,0x00
putstr: ; Schreib-Funktion
lodsb ; [si]->al
or al, al ; Teste al auf 0x0 (=Ende)
jz putstrd ; Wenn Ende des Strings, dann
mov ah, 0xE ; ah=0xE: Schreib Zeichen
mov bx, 0x7 ; Zeichenfarbe: 0x7 = weißes Zeichen/schwarzer
; Hintergrund
int 0x10 ; call
loop putstr ; Nächstes Zeichen
putstrd:
mov al, 13 ; Schreibe ASCII 13
mov ah, 0xE
mov bx, 0x7
int 0x10
mov al, 10 ; Schreibe ASCII 10 (13,10=Zeilenumbruch)
mov ah, 0xE
mov bx, 0x7
int 0x10
retn
too_big:
mov si, big_file
mov cx, 0xFFFF
call putstr
getkey:
mov ah, 0 ; ah=0: Get Key BIOS Funktion
int 0x16 ; Call
ret
reboot:
db 0xEA ; Hexdump für Reboot: jmp 00FF:FF00
dw 0x0
dw 0xFFFF
; - - - - - [ Dateien lesen - CODE ] - - - - -
Kurz - macht aber, was er soll. Man sollte natürlich, wenn man ein ersthaftes Projekt hat,
auch Dateien, die größer sind als 1 Sektor lesen. Das ist aber nicht schwer. Man muss
die Sektoren bekommen (shr dateigröße, 9 | inc), und auf die Cylinder/Head aufpassen. Wenn
man es ganz toll machen möchte, könnte man natürlich auch die Schrift in einer schönen Farbe
ausgeben lassen. Sieht sicher besonders gut aus ;)
===Dateien schreiben===
Was bringt ein Betriebssystem, dass keine Dateien schreiben kann? Nichts. Darum hier der
Beispielcode, wie man in eine Datei schreibt. Dieser Code ähnelt sehr dem Lese-Code. Wenn
man nun ein OS macht, dann kann man die gleichen Sachen einfach rausnehmen, und spart damit sehr
viele Bytes, und es wird wahrscheinlich (kommt drauf an, wie mans macht) viel übersichtlicher.
; - - - - - [ Dateien schreiben - CODE ] - - - - -
start:
mov ax, 0x2000 ; ax=0x2000
mov es, ax
mov ds, ax
mov bx, 0 ; BX=0x0
loadk:
; mov ax, contentend-content ; ax=Länge des Strings zu schreiben
; mov [Root_entry+28], ax ; In den Root_Dir_entry schreiben
;
; Hier das neue Root_Directory schreiben. Wird oben schon erklärt wie!
;
mov ah, 0x2 ; ah=2: Sektoren lesen
mov al, 0x12 ; Wieviele Sektoren wir lesen (18)
mov ch, 0 ; Cylinder 0
mov cl, 1 ; Starten bei Sektor 1
xor dx, dx ; dx=0
inc dh ; Head 1
int 0x13 ; Interupt 13
mov ax, 0x1000 ; ax=0x1000
mov es, ax
mov ds, ax
mov bx, 0 ; BX=0x0
mov cl, byte [Root_entry+26] ; Der Sektor, in den wir schreiben
add cl, 13 ; Der erste Datensektor ist anscheinend immer
; Sektor 31: -18, weil Head1
mov ax, 0x200 ; 0x200=512: Ein Sektor
xor ch, ch ; ch=0
mul cx ; cx*ax: Sektornummer*Sektorgröße: Adresse,
; wo wir lesen
mov di, ax ; Wo wir schreiben
mov bx, content ; bx zeigt auf den String, den wir schrieben werden
Data2Mem:
mov ax, 0x1000 ; Code im Speicher
mov es, ax
mov ds, ax
mov al, byte [bx] ; al=Byte zu schreiben
mov cx, 0x2000 ; Sektor im Speicher
mov es, cx
mov ds, cx
stosb ; AL -> ES:DI
inc bx ; Nächstes Byte
cmp bx, contentend ; Check ob wir alles haben
jne Data2Mem ; Wenn nicht, nochmal
xor bx, bx ; bx=0
mov ah, 0x3 ; AH=3: Schreibe Sektor(en)
mov al, 0x12 ; Wir schreiben 1 Sektor
mov ch, 0x0 ; Cylinder 0
mov cl, [Root_entry+26] ; Wo wir schreiben, aus Root_entry
add cl, 1 ; Jetzt haben wir ihn!
mov cl, 0x1 ; Sektor 1
xor dx,dx ; dx=0
inc dh ; Head 1!
int 0x13 ; Interrupt 13
mov cx, 0x1000 ; Sektor im Speicher
mov es, cx
mov ds, cx
mov cx, 0xFFFF ; cx=0xFFFF: Gibt die Stringlänge an, weil ich aber zu
; faul war, die Zeichenlänge zu überprüfen, hab ichs
; auf das höchste gestellt. Weil ein String aber mit
; 0x0 beendet wird, ist es egal
mov si, msg ; si=Adresse des Strings
call putstr ; Schreib String
mov cx, 0xFFFF ; Nochmal das Gleiche
mov si, m_boot ; Nochmal das Gleiche
call putstr ; Nochmal das Gleiche
call getkey ; Call zu GetKey
jmp reboot ; Springe zu reboot
msg db 'Welcome to SPTH-OS 1.1',0 ; String 1
m_boot db 'Press any key...',0 ; String 2
content db 'Das habe ich mit meinem Betriebssystem geschrieben!',0
contentend:
Root_entry db 0x53,0x50,0x54,0x48 ; Root Entry von einer Datei mit einem Byte
db 0x20,0x20,0x20,0x20
db 0x54,0x58,0x54,0x20
db 0x18,0x34,0xBA,0xB5
db 0x9D,0x2B,0x9D,0x2B
db 0x00,0x00,0x0F,0xB6
db 0x9D,0x2B,0x02,0x00
db 0x01,0x00,0x00,0x00
putstr: ; Schreib-Funktion
lodsb ; [si]->al
or al, al ; Teste al auf 0x0 (=Ende)
jz putstrd ; Wenn Ende des Strings, dann
mov ah, 0xE ; ah=0xE: Schreib Zeichen
mov bx, 0x7 ; Zeichenfarbe: 0x7 = weißes Zeichen/schwarzer
; Hintergrund
int 0x10 ; call
loop putstr ; Nächstes Zeichen
putstrd:
mov al, 13 ; Schreibe ASCII 13
mov ah, 0xE
mov bx, 0x7
int 0x10
mov al, 10 ; Schreibe ASCII 10 (13,10=Zeilenumbruch)
mov ah, 0xE
mov bx, 0x7
int 0x10
retn
getkey:
mov ah, 0 ; ah=0: Get Key BIOS Funktion
int 0x16 ; Call
ret
reboot:
db 0xEA ; Hexdump für Reboot: jmp 00FF:FF00
dw 0x0
dw 0xFFFF
; - - - - - [ Dateien schreiben - CODE ] - - - - -
Macht genau was er soll, darum: Passt :)
===Nachwort===
Mit diesem Tutorial sollte man in der Lage sein, ein Betriebssystem zu schreiben, das mit
Dateien kein Problem hat. Ich hoffe, du hast damit etwas gelernt, und fandest es interessant
zu lesen. Wenn nicht, mach es besser oder schick mir Geld, dann mach ichs besser. (verstehst,
worauf ich hinaus will?) Egal, ich hatte Spaß beim schreiben und programmieren, und hab natürlich
selber auch das ganze dazugelernt (hab vor dem Schreiben keinen Schimmer gehabt, wie ich auf
Dateien zugreife - dann hab ich es mir einfach als Ziel gesetzt, das zu schreiben).;;; Ein kleines Beispiel, wie man in den 32-Bit Protected Mode wechselt ;;; Bei Fragen kann man sich einfach an stefan.marcik@web.de oder ;;; an die ICQ-Nummer 338417614 wenden. [BITS 16] ... ... cli ; Interrupts ausschalten lgdt [gdtr] ; GDT Pointer laden mov eax,cr0 ; In PMode wechseln, indem das niedrigste or al,1 ; Steuerungsbit von cr0 geändert wird mov cr0,eax ; muss über Umweg über ein anderes Register gemacht werden jmp codesel:PMode ; FarJump zu einer 32-Bit PMode Funktion [BITS 32] PMode: mov ax,datasel ; Segmentregister laden mov ds,ax mov ss,ax mov esp,0x90000 ; Stack aufsetzen ... ... ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; == GDT == ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; gdtr: ; Desktiptortabelle dw gdt_end-gdt-1 ; Limit dd gdt ; Basisadresse gdt: dd 0,0 ; Null-Deskriptor codesel equ $-gdt dw 0xFFFF ; Segmentgröße 0..15 dw 0x0000 ; Segmentadresse 0..15 db 0x00 ; Segmentadresse 16..23 db 0x9A ; Zugriffsberechtigung und Typ db 0xCF ; Zusatzinformationen und Segmentgröße 16...19 db 0x00 ; Segmentadresse 24..31 datasel equ $-gdt dw 0xFFFF ; Segmentgröße 0..15 dw 0x0000 ; Segmentadresse 0..15 db 0x00 ; Segmentadresse 16..23 db 0x92 ; Zugriffsberechtigung und Typ db 0xCF ; Zusatzinformationen und Segmentgröße 16...19 db 0x00 ; Segmentadresse 24..31 gdt_end==OS-Showcase: ReactOS== ===Screenshots=== http://www.joachim-neu.de/public/lowlevel/7/10_1.png http://www.joachim-neu.de/public/lowlevel/7/10_2.png ===Geschichte=== Nun, ReactOS ist schon eines der älteren Betriebssystemprojekte. Die Entwickler haben schon sehr viel erreicht. Das Betriebssystem ist seit 1996 in Entwicklung. Aber wie es halt meistens so ist (man sieht es an unserem Comm-OS) funktioniert das nicht so leicht. Ursprünglich kam das System als "FreeWin95". Es sollte die Aufgabe eines Windows-Clones erfüllen, aber die Mitglieder verliefen und verstrickten sich in tiefen Design-Theoretischem-Gerede. Es kam nix zustande. 1997 griff Jason Filby das ganze wieder auf und nannte das Projekt um zu "ReactOS", was von dem damals wie heute tief verwurzelten Unbehagen gegen Microsofts Monopolstellung kam. Im Februar 1998 beginnt also der steinige Aufstieg von ReactOS. Langsam aber sicher erstellen die Programmierer massig Kernelcode, jedoch war es nur wenigen möglich, wirklich am Kernel zu arbeiten. Nachdem der Kernel nach einiger Zeit stabil lief wurde das Schreiben von IDE- und anderen Treibern unerlässlich. Von nun an war es mehr Leuten möglich, in die Entwicklung von ReactOS einbezogen zu werden. Heute ist ReactOS mit einem stabilen Kernel versehen und die API ist soweit ausgebaut, Möglichkeiten für eine höhere Programmierung von Programmen freizugeben. Heute schreiben viele Leute an Programmen und Funktionsbibliotheken für ReactOS und ihre Zahl ist steigend. Die Entwicklung einfacher Grafiktreiber ebnen den Weg für eine GUI, eine grafische Bedienoberfläche. Aber immernoch sieht man auch anhand dieser GUI das Ziel, für das ReactOS bestimmt war: Ein Clone von Windows zu werden. Die Oberfläche sieht der der Microsoft-Produkte sehr ähnlich.
| Person | Text |
|---|---|
| sz | Du leitest die Entwicklung von LegendOS. Was machst du, wenn du gerade nicht am planen oder programmieren bist? |
| L. | Ich studiere Informatik an der Uni Bonn im 2. Semester. |
| sz | Welches Hauptziel verfolgt LegendOS? Wie sieht es mit der allgemeinen Architektur aus? |
| L. | Nun, ich hatte vor kurzem einen kompletten Richtungswechsel, weswegen ich vom aktuellen Design noch nichts präsentieren kann. |
| sz | Bist du vom Microkernel abgekommen? |
| L. | Nun, insofern dass ich nun den "Safe Language"-Ansatz verfolge. Dies gibt einem noch ganz andere Möglichkeiten, auch Treiber zu kontrollieren. Der Microkernelansatz war zwar sehr nett und hätte sicher Vorteile gehabt, er wurde jedoch auch sehr kompliziert. |
| sz | Also wird der Kernel nun doch Monolithisch, jedenfalls zum Teil... |
| L. | Nun, der grösste Vorteil meiner Meinung vom Microkernel ist dass ich auch Treiber kontrollieren kann, was z.B. bei Linux nicht geht, da dort jeder Treibver eigendlich die Kontrolle übernehmen kann. |
| sz | An was arbeitest du momentan besonders hart? Zeichnen sich noch keine Probleme ab , nach der "Richtungsänderung"? |
| L. | Im Moment arbeite ich daran, Java für Treiber benutzen zu können, die kann ich dann auch so kontrollieren, wie einen Treiber im User Land von einem Microkernel. Die Implementierung sollte so besser sein als eine VM unter einem anderen OS, da ich mir dadurch z.B. das Page-Directory-Switching ersparen kann beim Task Switch. |
| sz | Also verankerst du schon tief im Kernel eine Java VM? |
| L. | Richtig. Und im Gegensatz zu anderen ähnlichen Projekten habe ich auch vor, parallel eine CLR zu implementieren, ähnlich wie Mono oder .NET. |
| sz | Ganz allgemein, wie lange programmierst du etwa die Woche? |
| L. | Das ist immer sehr unterschiedlich, die letzten Wochen kam ich kaum dazu. |
| sz | Wann kann man nächste Releases erwarten? |
| L. | Ich habe vor, die Virtual Machines für Java und eine CLR zu implementieren. Dann geht die Entwicklung richtig los, dem würde ich aber noch ein bis zwei Monate geben, da ich noch Techniken wie z.B. Corba umsetzen muss. |
Artikel:
| Nummer | Name | Redakteur |
|---|---|---|
| 1. | Die Redaktion | Neu, Joachim |
| 2. | News | Urbanietz, Christoph |
| 3. | Thema der Ausgabe | Neu, Joachim |
| 4. | Designtechnisches | Neu, Joachim |
| 5. | Architekturtechnisches | Neu, Joachim |
| 6. | Hardwaretechnisches | Neu, Joachim |
| 7. | Software- & Treibertechnisches | Sperl, Thomas |
| 8. | Tipps & Tricks | Neu, Joachim |
| 9. | Codeschnippsel | Marcik, Stefan |
| 10. | OS-Showcase | Neu, Joachim |
| 11. | Interview | Zurfluh, Stefan |
| 12. | Kolumne | Görlich, Markus |
| 13. | Bericht ausm Forum | Robertz, Michael |
| 14. | Vermischtes | Kienz, Peter |
Redakteure:
| Name | Nick | MSN | ICQ | WWW | |
|---|---|---|---|---|---|
| Neu, Joachim | joachim_neu | joachim_neu@web.de | joachim_neu@web.de | 247-390-343 | http://www.joachim-neu.de |
| Robertz, Michael | Silent{Bob} | silentbob@michaelrobertz.de | --- | 323-417-183 | http://www.michaelrobertz.de |
| Marcik, Stefan | Stefan2005 | stefan.marcik@web.de | --- | 338-417-614 | --- |
| Görlich, Markus | The-Programmerfish | the-programmerfish@web.de | --- | 346-950-608 | --- |
| Sperl, Thomas | SPTH | spth@priest.com | --- | --- | http://www.spth.de.vu |
| Urbanietz, Christoph | chr15 | chr12@web.de | --- | 311-405-426 | http://www.clinux.de.vu |
| Zurfluh, Stefan | sz/elfish_rider | stefan@stefan-zurfluh.biz | elfish_rider@hotmail.com | 330-960-734 | --- |
| Kienz, Peter | BlueB | peterkienz@web.de | --- | 250-161-210 | --- |