Mit dem guten alten debug (bei allen mir bekannten Windows-Versionen dabei) können Sie direkt mit dem Prozessor in Ihrem PC spielen. debug ist zwar ziemlich limitiert, denn es kennt nur den 16Bit-Modus des Prozessors, aber für den ersten Kontakt mit einer CPU reicht das völlig aus.
Nichts einfacher als das:
c:\ffhs >debug
-q
c:\ffhs >
debug wird von der Kommandozeile aus gestartet und zeigt ein Minus '-' als Prompt an. Alle Kommandos sind nicht nur einsilbig sondern sogar bloss einen einzigen Buchstaben lang.
Wie Sie sicher schon erkannt haben, ist 'q' die Kurzform für das Quit-Kommando
-r
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0CED ES=0CED SS=0CED CS=0CED IP=0100 NV UP EI PL NZ NA PO NC
0CED:0100 B80100 MOV AX,0001
-
Auch dieser Befehl ist nicht schwierig zu merken, 'r' wie Register. Er zeigt den aktuellen Zustand der Register an.
Interessant sind vor allem:
Die anderen Register werden wir nicht beachten, um die Erklärungen möglichst einfach zu halten.
Auf der letzten Zeile vor dem Prompt wird der nächste Befehl angezeigt.
Das folgende Beispiel zeigt Code der in C (oder Java) etwa so aussehen würde:
for( ;; ) // Endlosschleife { add( 1 ) ; // Aufruf von add mit einem Argument } int add( int value ) // Funktion add { int result = 2 ; // Lokale Variable, wird mit 2 initialisiert result += value ; // Der Wert des Arguments wird zum Wert der lokalen Variablen addiert return result ; // Das Resultat wird zurückgegeben }
Natürlich ist die lokale Variable vollkommen überfüssig und ein vernünftiger Compiler würde sie sofort wegoptimieren. Ich habe sie aber in den Code gesetzt um den Zugriff auf die lokalen Variablen zu demonstrieren
_for: MOV AX,0001 // Konstante 1 laden PUSH AX // Auf den Stack pushen CALL _add // Funktion aufrufen ADD SP,+02 // Argument vom Stack werfen JMP _for // Zurück zur Schleife springen _add PUSH BP // BP (Framepointer) auf dem Stack sichern MOV BP,SP // BP auf den Stack Frame zeigen lassen SUB SP,+02 // Platz auf dem Stack schaffen für die lokale Variable MOV AX,0002 // Konstante 2 laden MOV [BP-02],AX // Und in die lokale Variable schreiben MOV AX,[BP-02] // Wert aus der lokalen Variable lesen ADD AX,[BP+04] // Wert des Arguments dazuzählen MOV SP,BP // Lokale Variablen wegputzen POP BP // Vorherigen BP vom Stack poppen RET // Zurück zur Instruktion nach dem Funktionsaufruf
Vermutlich haben Sie noch nie etwas von einem Frampointer gehört. Dieses Register zeigt auf den aktuellen Stack-Frame, um den Umgang mit Parametern und lokalen Variablen zu vereinfachen. Die Argumente (Parameter) liegen bei der 8086-Familie über dem Framepointer (BP), die lokalen Variablen darunter.
Am besten sehen Sie dies bei den folgenden Instruktionen:
MOV AX,[BP-2] // Hole den Wert der zwei Bytes unter dem BP liegt und speichere ihn in AX ADD AX,[BP+4] // Hole den Wert der vier Bytes über dem BP liegt und addiere ihn zu AX
Mitten in der add-Funktion sieht der Stack so aus (Hex-Zahlen):
Adresse Wert FFEC 0001 // Argument von add FFEA 0107 // Return-Adresse der Funktion FFE8 0000 // Gesicherter BP <- hierhin zeigt BP FFE6 0002 // Lokale Variable <- hierhin zeigt SP
Dass AX den Returnwert der Funktion enthält, ist eine übliche Konvention
Mit debug können Sie Code Zeile für Zeile eingeben mit dem 'a'-Befehl:
-a 100
0CED:0100mov ax,1
0CED:0103push ax
0CED:0104call 10c
0CED:0107 -
Etwas gewöhnungsbedürftig ist der Ausstieg: einfach Return drücken...
Mit dem 'u'-Kommando können Sie den eben geschriebenen Code auch wieder anzeigen:
-u 100 121
0CED:0100 B80100 MOV AX,0001
0CED:0103 50 PUSH AX
0CED:0104 E80500 CALL 010C
0CED:0107 83C402 ADD SP,+02
0CED:010A EBF4 JMP 0100
0CED:010C 55 PUSH BP
0CED:010D 89E5 MOV BP,SP
0CED:010F 83EC02 SUB SP,+02
0CED:0112 B80200 MOV AX,0002
0CED:0115 8946FE MOV [BP-02],AX
0CED:0118 8B46FE MOV AX,[BP-02]
0CED:011B 034604 ADD AX,[BP+04]
0CED:011E 89EC MOV SP,BP
0CED:0120 5D POP BP
0CED:0121 C3 RET
-
Damit Sie nicht zu viel Zeit mit dem Line-Assembler verlieren, habe ich den Code
für Sie in ein File gespeichert: stack.bin.
Nach dem herunterladen starten Sie debug einfach mit debug stack.bin
.
Das 't'-Kommando führt genau eine Instruktion aus, stoppt dann das Programm und zeigt alle Register an.
Dank der Endlosschleife kann der Code so oft durchlaufen werden wie Sie wollen
Im folgenden Trace habe ich Kommentare eingefügt, damit Sie den Ablauf besser verstehen:
// SP steht auf FFEE, das ist der Top of Stack // Register AX wurde bereits mit der Konstante 1 geladen // Als nächstes wird AX auf den Stack gepusht -t
AX=0001 BX=0000 CX=0122 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0103 NV UP EI PL NZ NA PO NC 0CED:0103 50 PUSH AX // Die Funktion wird mit call aufgerufen // -> Die Adresse hinter der Call-Instruktion wird auf den Stack gepusht // -> Der Program Counter IP wird auf die Adresse 10C gesetzt -t
AX=0001 BX=0000 CX=0122 DX=0000 SP=FFEC BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0104 NV UP EI PL NZ NA PO NC 0CED:0104 E80500 CALL 010C // Administration: BP sichern -t
AX=0001 BX=0000 CX=0122 DX=0000 SP=FFEA BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=010C NV UP EI PL NZ NA PO NC 0CED:010C 55 PUSH BP // BP in den aktuellen Stackframe zeigen lassen -t
AX=0001 BX=0000 CX=0122 DX=0000 SP=FFE8 BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=010D NV UP EI PL NZ NA PO NC 0CED:010D 89E5 MOV BP,SP // Platz für die lokale Variable schaffen -t
AX=0001 BX=0000 CX=0122 DX=0000 SP=FFE8 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=010F NV UP EI PL NZ NA PO NC 0CED:010F 83EC02 SUB SP,+02 // Lokale Variable initialisieren // -> Nach der Initialisierung ist der Stack wie oben gezeigt -t
AX=0001 BX=0000 CX=0122 DX=0000 SP=FFE6 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0112 NV UP EI NG NZ NA PO NC 0CED:0112 B80200 MOV AX,0002 -t
AX=0002 BX=0000 CX=0122 DX=0000 SP=FFE6 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0115 NV UP EI NG NZ NA PO NC 0CED:0115 8946FE MOV [BP-02],AX SS:FFE6=3302 // Wert der lokalen Variablen in AX laden // (Eigentlich überflüssig, da der Wert noch in AX gespeichert ist) -t
AX=0002 BX=0000 CX=0122 DX=0000 SP=FFE6 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0118 NV UP EI NG NZ NA PO NC 0CED:0118 8B46FE MOV AX,[BP-02] SS:FFE6=0002 // Wert des Parameters zum Returnwert dazuzählen -t
AX=0002 BX=0000 CX=0122 DX=0000 SP=FFE6 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=011B NV UP EI NG NZ NA PO NC 0CED:011B 034604 ADD AX,[BP+04] SS:FFEC=0001 // Lokale Variablen vom Stack putzen -t
AX=0003 BX=0000 CX=0122 DX=0000 SP=FFE6 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=011E NV UP EI PL NZ NA PE NC 0CED:011E 89EC MOV SP,BP // BP wieder auf den vorherigen Stackframe zeigen lassen -t
AX=0003 BX=0000 CX=0122 DX=0000 SP=FFE8 BP=FFE8 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0120 NV UP EI PL NZ NA PE NC 0CED:0120 5D POP BP // Zurück zum aufrufenden Code // -> Der Wert vom Stack wird in IP geladen -t
AX=0003 BX=0000 CX=0122 DX=0000 SP=FFEA BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0121 NV UP EI PL NZ NA PE NC 0CED:0121 C3 RET // Parameter vom Stack putzen -t
AX=0003 BX=0000 CX=0122 DX=0000 SP=FFEC BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0107 NV UP EI PL NZ NA PE NC 0CED:0107 83C402 ADD SP,+02 // Zurück zum Start springen // -> 100 in IP laden -t
AX=0003 BX=0000 CX=0122 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=010A NV UP EI NG NZ NA PE NC 0CED:010A EBF4 JMP 0100 // Nächste Runde ... -t
AX=0003 BX=0000 CX=0122 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=0CED ES=0CED SS=0CED CS=0CED IP=0100 NV UP EI NG NZ NA PE NC 0CED:0100 B80100 MOV AX,0001 -