Mit debug die Intel x86 Prozessoren erkunden

Home

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.

Starten und Beenden von debug

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

Anzeigen der Register

-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 Programm

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

Der Assembler-Code für dieses Programm

_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 Code assemblieren

Mit debug können Sie Code Zeile für Zeile eingeben mit dem 'a'-Befehl:

-a 100
0CED:0100 mov ax,1
0CED:0103 push ax
0CED:0104 call 10c
0CED:0107
-

Etwas gewöhnungsbedürftig ist der Ausstieg: einfach Return drücken...

Mit debug Code disassemblieren

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	                                   
-

Nicht eintippen, sondern besser herunterladen

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.

Schritt für Schritt durch den Code steppen

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                            
-