Erklärung zum REAL-MODE (auch 16-Bit-Mode genannt) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Anmerkung: Diese Erklärung setzt voraus, daß man weiß, wie man die Register lädt und wie der Assembler-Befehl MOV funktioniert. Außerdem setzt sie voraus, daß man weiß, was Register sind. Der Real-Mode ist der älteste Modus, in dem eine 80x86er CPU betrieben werden kann. Er wird deswegen 16-Bit-Mode genannt, weil die Register alle nur 16 Bit breit sind. Daß man die Register EAX, EBX usw benutzen kann, geht erst ab dem 386er, vorher waren die wirklich nur 16 Bit und man hatte keine Wahl. Mit 16 Bit kann man 65536 verschiedene Werte darstellen, (nämlich 0 bis 65535). Will man also z.B. eine Speicherstelle angeben, so könnte man mit einem Register Speicherstellen von 0 bis 65535 angeben. Das wären genau 64kByte, das ist soviel (oder so wenig) RAM, wie z.B. der C64 hat. (Deswegen heißt er auch so...) Auf dem PC wollte man aber mehr RAM haben. Also mußte man sich etwas einfallen lassen, wie man diesen am besten ansteuern kann. Und die ersten PCs hatten halt maximal 1 MB RAM, weil man dachte, so viel könnte sowieso kein Mensch jemals verbrauchen. (Heute gibts Leute, die haben mehr Arbeitsspeicher oder auf der Grafikkarte, als damals die Festplatten hatten...) Das Problem ist, daß man, um 1 MB darzustellen, genau 20 Bit braucht, also 4 mehr, als man eigentlich hat. Die Lösung, auf die man kam, waren sogenannte SEGMENTE. Das heißt, man legte in der CPU ein paar zusätzliche Register an, (z.B. DS), die angeben, in welchem Segment man sich gerade befindet. Und jeder Zugriff bezieht sich dann nicht auf den Speicher von 0 bis 65535, sondern auf den Speicher von Segmentanfang+0 bis Segmentanfang+65535. Damit man nun diese 4 Bit bekam, machte man folgende Definition: Ein Segment kann nur an einer durch 16 teilbaren Speicherstelle anfangen. So daß also Das Segment 0 an Speicherstelle 0 liegt, das Segment 1 an Speicherstelle 16, das Segment 2 an Speicherstelle 32, usw. Oder anders ausgedrückt: Bei einem Segment sind einfach die untersten 4 Bits gleich Null. D.h. Das Segment $456E beginnt an der Stelle $456E0. Damit konnte man nämlich mit den 16-Bit-Segment-Registern eine 20-Bit Speicherstelle darstellen, weil man nämlich einfach die untersten 4 Bit (die man nicht hatte) weggelassen hat. Um jetzt z.B. an eine Speicherstelle zu schreiben oder davon zu lesen, die NICHT an einer durch 16 teilbaren Adresse liegt, wird der Offset einfach addiert. Hier kann man allerdings nicht nur 0 bis 15 addieren, sondern wirklich 0 bis 65535. D.h. wenn ich z.B. schreibe: mov AX,$4FC7 ; neuer Wert für DS (das Segmentregister) mov DS,AX ; DS mit diesem Wert laden dann zeigt mein Segmentregister DS erstmal auf die Speicherstelle $4FC70. Um jetzt auf die Speicherstelle $4FC72 zuzugreifen, schreibt man z.B. so etwas wie: mov AL,DS:[2] ; Wert aus Speicherstelle DS*16+2 holen. Würde man jetzt schreiben: mov AL,DS:[16] wäre das ein "Offset" (also Abstand zum Segmentanfang von 16). Da aber bei Segment+16 schon das nächste Segment (nämlich $4FC8, also Speicherstelle $4FC80) liegt, könnte man natürlich auch genauso schreiben: mov AX,$4FC8 ; nächstes Segment für DS mov DS,AX ; wieder laden mov AL,DS:[0] Und würde damit genau auf die gleiche Speicherstelle zugreifen. Man könnte aber auch schreiben: mov AX,$4000 mov DS,AX mov AL,DS:[$FC80] Der Wert in eckigen Klammern wird also zum Segmentanfang ADDIERT. Also mov AL,Segmentregister:[Offset] wäre in einer Hochsprache, in der der Speicher von 0 bis 1 Megabyte geht: AL = Speicher(Segmentregister*16+Offset) Ich hoffe, das ist nun soweit klar geworden. Noch eine wichtige Anmerkung: Wenn man mit Speicherzugriffen arbeitet, wird man natürlich viel öfter den Offset als das Segment ändern. Segment-Änderungen brauchen mehr Rechenzeit - und sie sind natürlich umständlicher, weil man ja den Umweg über ein Register (oder über indirekten Speicherzugriff) gehen muß. Das heißt, wenn man nur auf Bereiche zugreifen will, die insgesamt weniger als 64kByte groß sind, wird man im Real Mode ein Segmentregister an den Anfang dieses Bereichs setzen und dann nur den Offset verändern. Zumal es für den Offset auch noch einige andere Spielereien gibt, die ich im folgenden erläutere. Man kann den Offset nämlich auch in einem Register angeben - und nicht nur als festen Wert. Also: mov AL,DS:[SI] Lädt den Wert aus der Speicherstelle DS*16+SI. Dies geht mit folgenden Registern: SI, DI, BX und BP. Man kann dies auf folgende Arten benutzen: mov AL,DS:[Zahl] ; entspricht AL = Wert aus DS*16 + Zahl mov AL,DS:[SI] ; entspricht AL = Wert aus DS*16 + SI mov AL,DS:[DI] ; entspricht AL = Wert aus DS*16 + DI mov AL,DS:[BX] ; entspricht AL = Wert aus DS*16 + BX mov AL,DS:[BP] ; entspricht AL = Wert aus DS*16 + BP mov AL,DS:[SI+Zahl] ; entspricht AL = Wert aus DS*16 + SI + Zahl mov AL,DS:[DI+Zahl] ; entspricht AL = Wert aus DS*16 + DI + Zahl mov AL,DS:[BX+Zahl] ; entspricht AL = Wert aus DS*16 + BX + Zahl mov AL,DS:[BP+Zahl] ; entspricht AL = Wert aus DS*16 + BP + Zahl mov AL,DS:[BX+SI] ; entspricht AL = Wert aus DS*16 + BX + SI mov AL,DS:[BX+DI] ; entspricht AL = Wert aus DS*16 + BX + DI mov AL,DS:[BP+SI] ; entspricht AL = Wert aus DS*16 + BP + SI mov AL,DS:[BP+DI] ; entspricht AL = Wert aus DS*16 + BP + DI mov AL,DS:[BX+SI+Zahl] ; entspricht AL = Wert aus DS*16 + BX + SI + Zahl mov AL,DS:[BX+DI+Zahl] ; entspricht AL = Wert aus DS*16 + BX + DI + Zahl mov AL,DS:[BP+SI+Zahl] ; entspricht AL = Wert aus DS*16 + BP + SI + Zahl mov AL,DS:[BP+DI+Zahl] ; entspricht AL = Wert aus DS*16 + BP + DI + Zahl Das ist ziemlich viel - merken muß man sich halt nur, daß man die Register BX,BP,SI,DI dafür benutzen kann, daß man eine Zahl addieren kann, daß eine Zahl auch allein stehen kann. Und daß man auch Register miteinander kombinieren kann und auch eine Zahl mit kombinieren. Dabei ist jedoch wichtig, daß man nur maximal 2 Register kombinieren kann - und daß immer eines der Register dann BX / BP sein muß und das andere dann SI / DI. Die Reihenfolge, in der man dieses schreibt, ist allen Assemblern, die ich bisher kennengelernt habe, egal. Was noch wichtig und zu beachten ist: Der Wert in eckigen Klammern wird im Realmode IMMER als 16-Bit-Wert angenommen, d.h. wenn er einen größeren Wert ergibt, werden nur die unteren 16 Bit des Ergebnisses benutzt! Das ist wichtig und muß im Real-Mode IMMER beachtet werden: Zugriffe auf Speichersegmente können IMMER nur innerhalb eines 64 kByte großen Blocks erfolgen! Noch etwas: DS ist nicht das einzige Segmentregister, das es gibt. Es gibt insgesamt 6 davon (die zwei letzten erst ab dem 386er): DS = Daten-Segment ES = Erweitertes Datensegment CS = Code-Segment SS = Stack-Segment FS = Zusätzliches erweiteres Datensegment GS = Zusätzliches erweiteres Datensegment ES, FS und GS existieren eigentlich nur, zusätzlich zu DS, damit man nicht ständig die Segmente wechseln muß, wenn man in verschiedenen Speicherbereichen gleichzeitig "zu tun" hat. Außerdem kann man z.B. relativ einfach Speicherbereiche kopieren, indem man die Segmentadressen jeweils auf deren Anfänge setzt... CS ist das Code-Segment. D.h. in diesem Segment liegt der Programmcode, der derzeit ausgeführt wird. Dieses kann natürlich "von Hand" verändert werden - dies wäre jedoch nicht zu empfehlen. Ein "far jump" - d.h. ein Sprung an eine Speicheradresse außerhalb des 64kByte großen Segments ab CS springt an eine Speicherstelle, die mit 2x 16Bit angegeben wird, nämlich einmal das neue Segment für den Programmcode und einmal die neue Speicherstelle innerhalb dieses Segments. SS ist das Stack-Segment. Der Stack kann überall liegen, wird aber von der CPU gebraucht, um z.B. wenn ein Interrupt auftritt, oder wenn man in ein Unterprogramm springt, die aktuelle Speicherstelle, in der sich das aktuelle Programm gerade befindet, zwischenzuspeichern. Nach Ausführung wird diese Speicherstelle von dort wieder geholt. Ebenfalls auf dem Stack gelagert werden zum Beispiel die Inhalte von Registern, wenn diese verändert werden sollen, jedoch die ursprünglichen Werte evtl später wieder gebraucht werden. Der Stack wird vorher von der Größe her festgelegt und "rückwärts" benutzt. D.h. der Stack-Pointer (ebenfalls ein 16-Bit-Register) namens SP steht zu Anfang auf dem ENDE des Stacks und wird heruntergezählt. Nehmen wir also an, das Stacksegment (SS) zeigt auf $4000 und der StackPointer auf $7F00 - das wäre also insgesamt die Speicherstelle $47F00. Dann würde jemand einen Unterprogrammaufruf in ein Unterprogramm, das sich im selben Segment (also innerhalb der 64kbyte ab CS) machen. Dann würde man nur den 16bit großen Offset-Pointer retten müssen, der zeigt, an welcher Stelle sich das Programm gerade befindet. (Dieser Pointer heißt übrigens IP, also "Instruction Pointer"). Das bedeutet, daß der StackPointer um 2 verringert werden würde und dann an dieser Stelle der IP landen würde. SP würde danach auf $7EFE stehen. (also $7F00 - 2). Zu beachten ist hierbei, daß 4-Byte-Register wie EAX usw. natürlich 4 Byte bräuchten. (Und daß im Protected Mode der Instruction Pointer auch EIP ist, und ebenfalls 4 Byte braucht.) Um auf Speicherstellen zuzugreifen, ist DS das "Basis-Segment". Will sagen: Läßt man das Segment weg, nimmt die CPU automatisch DS als das gewünschte Segment an. Anders ausgedrückt: mov AL,DS:[DI] macht dasselbe wie mov AL,[DI] Will man ein anderes Segment als DS benutzen, muß man es allerdings hinschreiben! Anmerkung dazu: Es gibt da eine wichtige Ausnahme, nämlich das Register BP! Alle Adressierungsarten, in denen BP enthalten ist - also sowas wie mov AL,[BP] aber auch mov AL,[BP+SI] oder mov AL,[BP+$49F5] - haben nicht DS, sondern SS als "Basis-Segment", d.h. hier wird beim Weglassen des Segments als Segment SS statt DS angenommen. Aber man kann hier natürlich trotzdem auch DS benutzen - man muß dies dann jedoch dazuschreiben! Das Ganze funktioniert für die CPU so: Im Speicher wird einfach ein Byte vor den Befehl gesetzt, was dann bedeutet: "Benutze statt des Standard-Segments einfach dieses." Wenn ein Befehl jedoch standardmäßig sowieso schon DS benutzen würde, würde dieses Byte einfach nur ein Byte Speicher verschwenden... Allerdings "wissen" die Assembler normalerweise, welches Segment das Standard(Basis-) Segment eines Befehls ist, und werden dies wohl auch selbst weglassen, wo es nicht nötig ist. D.h. im allgemeinen kann man nichts falsch machen, wenn man DS immer dazuschreibt, selbst da, wo es nicht nötig wäre. Welchen Sinn könnte es haben, auf das Segment CS zuzugreifen, wo doch da normalerweise der Programmcode liegt? Naja. Nur weil da normalerweise das Programm liegt, heißt das ja nicht, daß man unbenutzte Bereiche (die man nicht ausführt) nicht dazu benutzen kann, dort ebenfalls Daten abzulegen. Zum Beispiel irgendwelche Tabellen, die sich nicht ändern, sondern "fest" sind. Ebenfalls möglich (aber Assembler-Anfängern nicht empfohlen!) ist es, Programme zu schreiben, die sich selbst verändern. Hierbei ist natürlich Vorsicht angemahnt, da dabei einiges zu beachten ist - das ich jetzt nicht im einzelnen aufzählen will. Besonderheiten der anderen Register: - AX,BX,CX,DX Ihre Besonderheit ist erst einmal, daß man sie "teilen" kann, so daß also jedes dieser Register aus jeweils zwei 8-Byte-Registern besteht. Mitunter braucht man nur 8 Byte und so braucht man nicht immer ein ganzes 16-Bit-Register zu seiner Zwischenspeicherung - Werte in Registern zu lagern ist übrigens immer schneller, als sie in den Speicher zu legen. Weil die CPU natürlich auf ihre eigenen Register viel schnelleren Zugriff erhält als auf den Speicher. Außerdem haben CPUs ab dem Pentium die Eigenschaft, daß, wenn zwei Befehle, die unmittelbar aufeinander folgen - und die sich nicht mit Speicherzugriffen "gegenseitig behindern" würden, quasi "gleichzeitig" statt nacheinander ausgeführt werden - was das ganze natürlich etwas schneller macht. Das heißt, daß man durch allein schon durch geschickte Wahl der Reihenfolge der Befehle Programme schneller machen kann. - BX,BP,SI,DI Wie oben beschrieben: Sind die Register, die man für diese indirekte Adressierung benutzen kann. (Anmerkung: Das einizige Register, daß gleichzeitig für indirekte Adressierung benutzt werden UND "geteilt" werden kann, ist BX. Deswegen würde ich, auf die Frage nach meinem Lieblingsregister der 80x86er CPU mit BX antworten.) - AX Für Multiplikationen und Divisionen wird unbedingt AX gebraucht. Der Multiplikations- und Divisionsbefehl gibt immer nur den zweiten Faktor, bzw den Divisor an. Der erste ist immer in AX gespeichert. Oder in DX und AX. Desweiteren - das ist vielleicht für manchen interessant zu wissen - sind manche Befehle für AL und AX noch einmal extra optimiert. Das heißt, sie haben speziell für AL und AX einen eigenen Opcode, der im Speicher ein Byte weniger benötigt. Und der auch im allgemeinen schneller ausgeführt wird. Daher sollte man, wenn man viele Werte ändern und kopiere usw muß, AL, AX und EAX bevorzugt benutzen. - DX Für Multiplikation und Divisionen wird auch DX gebraucht. Außer bei 8-Bit multiplikation/Division, für die allein AX benutzt wird. - CX Es gibt einen Befehl, der gleichzeitig CX herunterzählt und einen Sprung macht, wenn CX danach <>0 ist, der heißt LOOP. Das heißt: Wenn man vor einer Schleife CX auf die Anzahl Durchläufe der Schleife setzt, braucht man nur alles für die Schleife schreiben und am Ende der Schleife den Befehl LOOP, mit dem Schleifenanfang als Sprungziel. Der zählt CX allein runter und springt, solange CX noch nicht 0 ist, wieder zum Anfang. Eigentlich wäre das praktisch. Aber imo ist der Befehl nur gut dazu, um etwas Speicher zu sparen. Denn ab einem 286er werden die beiden Befehle dec CX jnz @Loopstart sogar schneller ausgeführt als LOOP @Loopstart der genau dasselbe macht. Schade eigentlich. Die Idee war ganz gut. (Anmerkung: Gerade bei Schleifen geht die meiste Rechenzeit verloren! Wer also eine Schleife nur deswegen langsamer macht, weil er zu faul zum Tippen ist, ist ein Lamer.) CX hat eine weitere Besonderheit: Mit shl AX,4 kann man z.B. AX um 4 Bit nach links schieben. Das gleiche funktioniert auch mit mov CL,4 shl AX,CL Nun gut, mag mancher denken. Was ist der Vorteil? Naja. Zum einen ist es der Umstand, daß man natürlich, wenn die Anzahl Verschiebungen, die man braucht, veränderlich ist, damit eine einfachere Möglichkeit hat, diese festzulegen. Das zweite ist, daß wenn man über einen längeren Zeitraum immer die gleiche Anzahl Verschiebungen braucht, diese einfach in CL eintragen kann - denn shl AX,CL wird (weil CL ein Register ist) natürlich schneller ausgeführt als shl AX,zahl. Anmerkung: Das gilt nicht für Verschiebungen um 1. Für Verschiebungen um 1 gibt es extra Opcodes, die schneller ausgeführt werden (und meines Wissens auch weniger Speicher brauchen), d.h. shl AX,1 setzt ein Assembler in etwas anderes um als shl AX,0 und shl AX,2 bis shl AX,255. Anmerkung: Verschiebungen haben natürlich nur bis 31 einen Sinn. Größer werden Register nicht. Am dem 386er werden daher nur noch die unteren 5 Bit (also 0 bis 31) von CL benutzt. Dies erzeugt natürlich einen Fehler, wenn man eine Zahl um 32 nach links schiebt (denn dann wäre sie eigentlich =0, weil alle Bits nach oben herausgeschoben wurden). Auf einem 286er steht nach shl AL,32 in AL danach der Wert 0. Auf Rechnern ab 386er steht dort der Wert, den AL schon vorher hatte. Noch eine wichtige Anmerkung: Rechner VOR dem 286er (also 8086 und 80186) haben nur Schiebebefehle mit 1 oder mit CL. Dort MÜSSEN Verschiebungen um andere Werte als 1 immer mit CL gebildet werden! (Das ist nur wichtig, wenn man Programme für Rechner unter 286er schreiben will...) - DS,ES,CX,SI,DI Manche Befehle, wie z.B. die "String"-Befehle zum Vergleichen ganzer Strings oder zum kopieren eines Strings in einen anderen, nutzen DS,ES,CX,SI und DI folgendermaßen: Vorher wird nach DS und SI die Stelle geladen, an der der erste String steht. Außerdem wird nach ES und DI die Stelle geladen, an der der zweite String steht. Nach CX wird die Anzahl Durchläufe geladen. Nun kann man mit bestimmten Spezialbefehlen (REP, REPZ, REPNZ) ein und denselben "Umkopier" oder "Vergleichs" Befehl solange wiederholen lassen, bis CX=0 ist (CX wird halt dabei runtergezählt) oder beide Strings an dieser Stelle gleich oder ungleich sind (je nachdem ob REPZ oder REPNZ). SI und DI werden danach um 1, 2 oder 4 Byte erhöht - je nachdem, ob der jeweilige Befehl für Bytes, Words oder DWords gilt. Durch Setzen eines Flags kann dies auch umgekehrt werden. (D.h. dann wird um 1, 2 oder 4 vermindert statt erhöht.) D.h. es gibt Befehle, die für die Verarbeitung ganzer Speicherblöcke ausgelegt sind und die bei richtiger Anwendung eine Menge Zeit sparen gegenüber der Schleife, die man normalerweise programmieren würde, um dasselbe zu erreichen. Hierbei ist jedoch einiges zu beachten, das ich in diesem kurzen Text nicht einzeln erläutern möchte. Das ist übrigens der Grund, warum immer gesagt wird, SI und DI wären für "Stringoperationen" gedacht. SI und DI bedeutet übrigens "Source Index" und "Destination Index" - was auf ihre Verwendung in den Vergleichs- und Kopier-Befehlen hindeutet. - FLAGS Dazu habe ich noch gar nichts gesagt. Es gibt in der CPU auch ein FLAGS Register. Dieses kann man nicht direkt als Register ansprechen - dafür kann man, abhängig von einzelnen gesetzten oder gelöschten Bits in diesem Register, bestimmte Dinge tun. Die "wichtigsten" davon sind: ZF = Das "Zero-Flag". Wenn das Ergebnis einer Rechenoperation =0 war, wird dieses Bit gesetzt. Benutzt werden kann es zB mit JZ oder JNZ (Springe, wenn Zero gesetzt oder gelöscht.) CF = Das "Carry-Flag". Wenn eine Operation einen Über- oder Unterlauf erzeugt hat, wird dieses Bit gesetzt. Beispiel dafür ist, wenn zwei Zahlen addiert und das Ergebnis nicht mehr in das Ergebnisregister paßt. Dann steht das Bit, was "übersteht", in CF. Mit JC oder JNC benutzbar. SF = Das "Sign-Flag". Wenn das oberste Bit des Ergebnisses gesetzt war, wird auch dieses Bit gesetzt. Benutzbar mit JS oder JNS. Anmerkung: Es gibt auch Sprünge, die Kombinationen dieser Bits abfragen. Solche Kombinationen entstehen auch bei Vergleichen. Ein Vergleich ist eigentlich nur eine Subtraktion. Der erste Wert wird vom zweiten intern subtrahiert, ohne sein Ergebnis zu speichern, weil nur die Flags interessant sind. Die Befehle nach einem Vergleich sind, z.B. wenn man AL mit BL verglichen hat - also nach cmp AL,BL : JA - Springt, wenn AL > BL war. JAE - Springt, wenn AL >= BL war und ist dasselbe wie JC. JB - Springt, wenn AL < BL war und ist dasselbe wie JNC. JBE - Springt, wenn AL <= BL war. JE - Springt, wenn AL = BL war und ist dasselbe wie JZ. JNE - Springt, wenn AL <> BL war und ist dasselbe wie JNZ. Die folgenden Befehle beziehen das "Sign"-Flag mit ein und gelten dafür, wenn man die Zahlen als "vorzeichenbehaftet" ansieht - oder anders ausgedrückt: 128 bis 255 werden hier dann so interpretiert, als wären sie -128 bis -1, 0 bis 127 bleiben 0 bis 127. In diesem Fall gelten die Befehle JG - Springt, wenn AL > BL, vorzeichenbehaftet. JGE - Springt, wenn AL >= BL, vorzeichenbehaftet. JL - Springt, wenn AL < BL, vorzeichenbehaftet. JLE - Springt, wenn AL <= BL, vorzeichenbehaftet. Dann gibts noch die Flags: OF - Das "Overflow-Flag". Wird gesetzt, wenn das Ergebnis einen Überlauf verursacht hat, davon ausgehend, daß man das Ergebnis als vorzeichenbehaftet ansieht. D.h. bei einer Zahl, die von -128 bis +127 gehen kann, und diese Zahl ist bereits +126, und man würde 5 addieren, würde sich ja -125 ergeben, was falsch wäre, und einen "Überlauf" erzeugen würde. Dieser wird in diesem Flag eingezeigt. Darauf reagieren kann man mit JO und JNO. Ein Überlauf in einer nicht vorzeichenbehafteten Zahl kann man, wie bereits oben erwähnt, am CF (Carry) erkennen. PF - Das "Parity-Flag". (Parität... Wo braucht man sowas...) Das Paritätsflag zählt, ob die Anzahl 1-Bits (oder 0-Bits) in einem Ergebnis gerade (1) oder ungerade (0) ist. (Welche ist egal, da ja diese Zahlen nur 8, 16 oder 32 Bit sein können, also die Gesamtzahl Bits immer gerade ist. Wenn also die Anzahl Einsen gerade ist, ist die Anzahl Nullen es auch. Und ist sie ungerade, genauso. Ist die Anzahl gleicher Bits gerade, dann ist PF gesetzt, sonst gelöscht. Regieren kann man mit JP oder JPE (für gerade) oder JPO für ungerade. (E=even, O=odd, für "gerade" und "ungerade"). Es gibt noch viel mehr Flags - auf die kann man aber nicht mit Sprüngen reagieren. Die einfachsten Arten, das Flag-Register abzufragen - falls man das mal braucht - sind: lahf ; lädt die unteren 8 bits des FLAGS registers nach AH ; (sahf speichert diese übrigens wieder) oder die andere Möglichkeit: pushf ; Flags auf den Stack legen pop AX ; Flags aus dem Stack nach AX holen Noch wichtig zu den Flags: - Das Flag-Register hat natürlich bei Rechnern ab 386 auch 32 Bits. - Einige Bits des Flag-Registers sind unbenutzt. - Ich habe (bei weitem!) nicht alle Flags aufgezählt, die es gibt. - CS,IP Man kann IP nicht direkt setzen oder holen - wobei, eigentlich schon. In dem Moment, wo man einen Sprung macht, setzt man in Wirklichkeit das IP-Register. Dieses zeigt auf die Stelle, wo der nächste auszuführende Befehl steht. Das heißt: Die Stelle, an der die CPU gerade Befehle ausführt, steht (nur im Real-Mode!!!) in CS*16+IP. - SS,SP Die Stelle, an der der nächste Wert steht, der vom Stack geholt wird, steht in SS*16+SP. Soll ein Wert auf den Stack gelegt werden, so wird SP erst um die Anzahl Bytes verringert, die dieser Wert brauchen würde (beim PC jedoch immer mindestens durch 2 teilbar - d.h. man kann z.B. AL nicht einzeln auf den Stack legen!) und dann der neue Wert geschrieben. Dies passiert auch, wenn man in ein Unterprogramm geht - dann wird die Adresse, die als nächstes (nach dem Sprung) im Hauptprogramm folgen würde, auf dem Stack abgelegt. Nach dem Rücksprung mit RET wird sie wiedergeholt. Achtung: Es gibt auch Unterprogrammaufrufe außerhalb des aktuellen CS-Segments! Diese sichern sowohl CS als auch IP auf den Stack - müssen allerdings auch mit RETF statt mit RET beendet werden (also einem "FAR RETURN"). Achtung: Wenn der Stack voll ist (d.h. wenn SP=0 oder =1 ist) und jemand einen neuen Wert auf den Stack legen will, erzeugt die CPU einen Fehler. Anmerkung: Man kann auch mehrere Stacks haben. Jedes Programm kann seinen eigenen haben - das ist sogar eher die übliche Vorgehensweise. Es sollte auch immer genügend Platz auf dem Stack vorhanden sein, damit ein Interrupt auch noch Werte darauf speichern kann - mindestens die Rücksprung-Adresse und die FLAGS, die automatisch dort abgelegt werden. Achja: Dies (daß die Interrupts den Stack benutzen) ist auch der Grund, warum man, wärend man Manipulationen von SS und SP vornimmt, die Interrupts mit CLI sperren muß (nachher wieder mit STI freigeben nicht vergessen!). - BP Da hier das Standard-Segmentregister das SS (Stack-Segment) ist, ist es eigentlich dafür gedacht, Daten aus dem Stack zu lesen. Dies wird z.B. oft von Hochsprachen benutzt, die bei einem Unterprogramm, das lokale Variablen anlegt, diese auf dem Stack anlegen. Es gibt sogar Befehle, die speziell dafür ausgelegt sind, diesen "Trick" zu benutzen (sie heißen ENTER und LEAVE). Zu den 32-Bit Registern ist folgendes zu sagen: Es gibt sie für die Register AX,BX,CX,DX,BP,SP,SI,DI, IP und FLAGS und sie werden im Assembler einfach dadurch ausgedrückt, daß man ein E vor das Register schreibt. EAX ist dabei z.B. ein Register, das 32 Bit breit ist und dessen untere 16 Bit dem Register AX entsprechen. Für die anderen Register gilt im übrigen das jeweils gleiche. Im Real-Mode sind EFLAGS und EIP allerdings wenig sinnvoll einsetzbar. EIP wird in der Regel nicht funktionieren und die oberen 16 Bit von EFLAGS sind im Real-Mode eigentlich uninteressant. Adressierungen der Art mov AL,DS:[EBX] sind im Real-Mode allerdings nicht möglich. Adressen sind nur die bewußten 20 Bits groß - Adressen-Offsets immer nur 16-Bit. Abschlußbemerkungen: Dieser Text stellt keineswegs vollständig die umfangreichen Möglichkeiten der Assembler-Programmierung dar. Es ist nur ein extrem kurzer Abriß über die wichtigsten Funktionen, die der Real-Mode so hat und über die Funktionsweise des segmentierten Speichermodells. Desweiteren empfehle ich den Text ASM86FAQ.TXT, der eigentlich recht gut und ausführlich viele Dinge zur Assemblerprogrammierung (in deutsch) erklärt. Für Fehler in diesem Text übernehme ich keinerlei Haftung. Ich bin auch nur ein Mensch. Ich kann mich mal verschreiben. Ich kann mich mal irren. Meine Website: http://www.imperial-games.de Xpyder (Imperial Games), 2. 8. 2005