Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 17 Min.

Die Selbstbau-CPU programmieren

Von der Hochsprache über den Assembler bis zum Binärcode aus Nullen und Einsen.
Die ersten beiden Teile dieser Serie [1] [2] haben erklärt, wie eine CPU aufgebaut ist und wie man ein einfaches Exemplar selbst programmieren kann und diesem via In­struction Set und Instruction Decoder die ersten Funktionen  spendiert. Wie Sie am Ende des zweiten Teils [2] gesehen haben, wird der CPU über Nullen und Einsen im Rahmen von Opcodes mitgeteilt, welche Befehle ausgeführt werden sollen. So schreibt zum Beispiel der Opcode 01000001 den Inhalt des X-Registers in das M-Register.

Der Assembler

Selbstverständlich will niemand die CPU den ganzen Tag lang mit Nullen und Einsen füttern, wenngleich das in den Anfängen der Informatik genau so gemacht wurde. Deshalb wird auch die Selbstbau-CPU nicht direkt über Opcodes und Binärcode programmiert, sondern über sogenannte Mnemonics auf Assembly-Ebene. Der Opcode 01000001 lässt sich zum Beispiel auch als MOV M, X darstellen.
Dabei handelt es sich um einen Assembler-Befehl, welcher Opcode und Binärcode der Ziel-CPU abstrahiert. Assembler-Befehle werden von einem Assembler-Programm in den Binärcode der Ziel-CPU übersetzt.Vor den Assembler kann auch noch eine Hochsprache geschaltet werden, wie etwa C oder C++. Diese müsste dann passenden Assembler-Code generieren. Bild 1 veranschaulicht dieses Abstraktionskonzept.Im Rahmen dieses dritten Teils der CPU-Serie möchte ich Ihnen nun zeigen, wie der Assembler meiner Selbstbau-CPU implementiert ist und wie Sie damit konkrete Programme entwickeln können, ohne dabei mit Nullen und Einsen hantieren zu müssen.Wie bereits in Teil 2 erwähnt, versteht die Selbstbau-CPU auf Maschinenebene RISC-Befehle. Jeder RISC-Befehl steht für eine einzelne Hardware-Operation, die vom Instruction Decoder ausgeführt werden kann.
Auf den RISC-Befehlen aufbauend habe ich ein CISC-basierendes Instruction Set entwickelt, mit dem die CPU mit schon etwas mächtigeren Befehlen programmiert werden kann. Mein CISC Instruction Set ist angelehnt an die Assembler-Sprache für Intels x86-CPUs, da sehr viele Entwickler mit dieser Sprache zumindest ansatzweise vertraut sind. Die Aufgabe des Assemblers besteht darin, die CISC-Befehle in RISC-Befehle zu konvertieren und die generierten RISC-Befehle anschließend in Binärcode umzuwandeln (Bild 2).Jetzt soll es zunächst darum gehen, wie die RISC-Befehle vom Assembler in Binärcode konvertiert werden. Der darauf folgende Abschnitt widmet sich dann den CISC-Befehlen.

RISC Assembly Language

Der komplette Assembler zur Selbstbau-CPU ist in C# implementiert und steht auf GitHub unter der MIT-Lizenz zur Verfügung [3].Für das Parsing der Assembler-Sprache habe ich mich für ANTLR 4.5 entschieden [4], da eine Eigenentwicklung eines Parsers zu komplex gewesen wäre. ANTLR 4.5 ist die einzige Abhängigkeit, die der Assembler zu anderen Komponenten aufweist.
ANTLR bietet über sogenannte g4-Dateien die Möglichkeit, die Syntax einer Programmiersprache zu beschreiben. Bild 3 zeigt einen Ausschnitt der g4-Datei, die die RISC-Befehle des Assemblers beschreibt.Aus dieser g4-Datei generiert ANTLR passende Parser, Lexer und Visitors in C#-Klassen, gegen die man schlussendlich programmieren kann. Ich habe mich für das Generieren von Visitor-Klassen entschieden, da das Implementieren eines passenden Assemblers mithilfe des Visitor-Patterns am einfachsten erschien.
Listing 1: Binärcode zu MOV16 erzeugen
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> Result <span class="hljs-title">VisitMOV16</span>(</span><br/><span class="hljs-function"><span class="hljs-params">  LowLevelAssemblyParser.MOV16Context context</span>) </span><br/>{ <br/>   <span class="hljs-keyword">string</span> destinationRegister = <br/>     context.register_16bit(<span class="hljs-number">0</span>).GetText(); <br/>   <span class="hljs-keyword">string</span> sourceRegister = <br/>     context.register_16bit(<span class="hljs-number">1</span>).GetText(); <br/> <br/>   <span class="hljs-keyword">string</span> opcode = <span class="hljs-string">"01"</span>; <br/>   opcode += GetRegisterOpCode_GeneralPurpose_MOV16(<br/>     destinationRegister); <br/>   opcode += GetRegisterOpCode_GeneralPurpose_MOV16(<br/>     sourceRegister); <br/>   opcode += <span class="hljs-string">" ; MOV16 "</span> + destinationRegister + <br/>     <span class="hljs-string">", "</span> + sourceRegister; <br/>   assembly.Add(opcode); <br/> <br/>   <span class="hljs-keyword">return</span> <span class="hljs-keyword">base</span>.VisitMOV16(context); <br/>}  
Für jeden RISC-Befehl, den der Assembler verarbeiten muss, habe ich die entsprechende Visitor-Methode überschrieben. Innerhalb der überschriebenen Methode wird dann einfach der für den jeweiligen RISC-Befehl erforderliche Binärcode erzeugt. Listing 1 zeigt das Generieren von Binärcode für den RISC-Befehl MOV16. Wie Sie erkennen können, handelt es sich um ein 1:1-Mapping zwischen RISC-Befehl und dazugehörigem Binärcode. Bei Befehlen wie MOV16 müssen zusätzlich Quell- und Zielregister decodiert werden. Das erledigen entsprechende Hilfsfunktionen.

CISC Assembly Language

Um einiges interessanter ist die CISC Assembly Language, die auf der RISC Assembly Language aufbaut. Die CISC Assembly Language erlaubt das Programmieren der CPU auf der nächsthöheren Abstraktionsebene.Aufgabe des Assemblers ist es, einen CISC-Befehl in mehrere RISC-Befehle zu zerlegen. Dazu muss ein 1:n-Mapping durchgeführt werden. Hier ein konkretes Beispiel eines CISC-Befehls, der zerlegt werden soll:
<span class="hljs-keyword">ADD</span><span class="bash"> D, E </span>
<span class="bash"> </span> 
Dieser Befehl soll den Wert des E-Registers zum aktuellen Wert des D-Registers addieren und das Ergebnis ins D-Register schreiben. Auf CPU-Ebene sind dafür mehrere Einzelschritte auszuführen:
  • Setzen des Input A der ALU auf den Wert des Registers D
  • Setzen des Input B der ALU auf den Wert des Registers E
  • Durchführen der Addition
  • Zurückschreiben des Ergebnisses in das Register D
Mit RISC-Befehlen wird die Selbstbau-CPU also wie folgt programmiert:

<span class="hljs-keyword">MOV_ALU_IN </span>A, D 
<span class="hljs-keyword">MOV_ALU_IN </span><span class="hljs-keyword">B, </span>E 
<span class="hljs-keyword">ADD </span>
<span class="hljs-keyword">MOV_ALU_OUT </span>D 
 
Aufgabe des Assemblers ist es nun, obigen CISC-Code in diese Folge von RISC-Befehlen zu wandeln. Alle unterstützten CISC-Befehle sind wieder in einer g4-Datei beschrieben, siehe Bild 4. Auch für diese Datei erzeugt ANTLR passende Lexer, Parser und Visitor-Klassen, die anschließend zum Generieren von Code verwendet werden. Listing 2 zeigt die Umsetzung des gerade geschilderten Beispiels. Tabelle 1 listet die CISC-Befehle auf, die der Assembler der Selbstbau-CPU aktuell unterstützt.
Listing 2: RISC-Code erzeugen
public override Result VisitADD_R8_R8(&lt;br/&gt;  HighLevelAssemblyParser.ADD_R8_R8Context context) &lt;br/&gt;{ &lt;br/&gt;  string sourceRegister = &lt;br/&gt;    context.register_8bit(&lt;span class="hljs-number"&gt;1&lt;/span&gt;).GetText(); &lt;br/&gt;  string destinationRegister = &lt;br/&gt;    context.register_8bit(&lt;span class="hljs-number"&gt;0&lt;/span&gt;).GetText(); &lt;br/&gt; &lt;br/&gt;  // &lt;span class="hljs-keyword"&gt;Add&lt;/span&gt; the assembly opcodes &lt;br/&gt;  assembly.&lt;span class="hljs-keyword"&gt;Add&lt;/span&gt;(&lt;span class="hljs-string"&gt;";; BEGIN ADD "&lt;/span&gt; + &lt;br/&gt;    destinationRegister + &lt;span class="hljs-string"&gt;", "&lt;/span&gt; + sourceRegister + &lt;br/&gt;    &lt;span class="hljs-string"&gt;" (ADD_R8_R8)"&lt;/span&gt;); &lt;br/&gt;  assembly.&lt;span class="hljs-keyword"&gt;Add&lt;/span&gt;(&lt;span class="hljs-string"&gt;"MOV_ALU_IN A, "&lt;/span&gt; + &lt;br/&gt;    destinationRegister); &lt;br/&gt;  assembly.&lt;span class="hljs-keyword"&gt;Add&lt;/span&gt;(&lt;span class="hljs-string"&gt;"MOV_ALU_IN B, "&lt;/span&gt; + sourceRegister); &lt;br/&gt;  assembly.&lt;span class="hljs-keyword"&gt;Add&lt;/span&gt;(&lt;span class="hljs-string"&gt;"ADD"&lt;/span&gt;); &lt;br/&gt;  assembly.&lt;span class="hljs-keyword"&gt;Add&lt;/span&gt;(&lt;span class="hljs-string"&gt;"MOV_ALU_OUT "&lt;/span&gt; + &lt;br/&gt;    destinationRegister); &lt;br/&gt;  assembly.&lt;span class="hljs-keyword"&gt;Add&lt;/span&gt;(&lt;span class="hljs-string"&gt;";; END ADD "&lt;/span&gt; + destinationRegister + &lt;br/&gt;    &lt;span class="hljs-string"&gt;", "&lt;/span&gt; + sourceRegister + &lt;span class="hljs-string"&gt;" (ADD_R8_R8)"&lt;/span&gt;); &lt;br/&gt; &lt;br/&gt;  &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; base.VisitADD_R8_R8(context); &lt;br/&gt;}  
Neben dem Erzeugen von RISC-Code auf Basis von CISC-Befehlen hat der Assembler noch andere wichtige Aufgaben:
  • Verarbeiten von Include Files.
  • Generieren von Hauptspeicheradressen
  • Berechnen der Ziel-Hauptspeicheradresse bei Sprüngen (bedingte und unbedingte Sprünge)
  • Erzeugen einer C-Datei, über die mithilfe des angeschlossenen Arduino-Boards der Hauptspeicher der CPU mit dem generierten Programmcode initialisiert werden kann
  • Erstellen einer Map-Datei, die zeigt, in welchen Haupt­speicheradressen welche Funktionen geladen worden sind (kann für die Visualisierung von Call Stacks verwendet werden)

Addition von 32-Bit-Zahlen

Nachdem Sie nun einen groben Überblick gewonnen haben, wie der Assembler implementiert und wie aus Assembler-Programmen ausführbarer Binärcode erzeugt wird, möchte ich noch einige wichtige Assembler-Programme vorstellen, die meine CPU bereits ausführen kann.

Tabelle 1: CISC-Befehle

Befehl Beschreibung
ADC Führt eine Addition mit Vorzeichenübertrag aus.
ADD Führt eine 8-Bit-Addition aus.
AND Führt ein logisches AND aus.
CMP Vergleicht zwei 8-Bit-Werte miteinander und setzt das Flags-Register.
DEC Dekrementiert ein Register um 1.
ENTER Bereitet den Stack für einen Funktionsaufruf vor (generiert den Prolog).
HLT Stoppt die CPU-Ausführung.
IN Liest einen 8-Bit-Wert von einem Input-Port.
INC Inkrementiert ein Register um 1.
INT Löst einen Software-Interrupt aus.
JMP Führt einen unbedingten Sprung aus.
JNC Führt einen bedingten Sprung aus, wenn das Carry-Flag nicht gesetzt ist.
JNS Führt einen bedingten Sprung aus, wenn das Sign-Flag nicht gesetzt ist.
JNZ Führt einen bedingten Sprung aus, wenn das Zero-Flag nicht gesetzt ist.
JZ Führt einen bedingten Sprung aus, wenn das Zero-Flag gesetzt ist.
CALL Führt einen Funktionsaufruf aus.
RET Beendet einen Funktionsaufruf.
LEAVE Räumt den Stack nach einem Funktionsaufruf auf (generiert den Epilog).
MOV Führt Register-Transfers aus.
NEG Führt ein arithmetisches NOT aus.
NOP Führt eine No-Operation aus.
NOT Führt ein logisches NOT aus.
OR Führt ein logisches OR aus.
OUT Schreibt einen 8-Bit-Wert in einen Output-Port.
POP Nimmt den aktuellen Wert vom Stack und schreibt diesen in ein Register.
POPF Liest die Flags vom Stack und schreibt diese in ein Register.
PUSH Schreibt einen Wert von einem Register auf den Stack.
PUSHF Schreibt die Flags vom Flags-Register auf den Stack.
RCL Führt ein Rotate Left aus.
RCR Führt ein Rotate Right aus.
SAR Führt ein arithmetisches Shift Right durch.
SBB Führt eine Subtraktion mit Vorzeichenübertrag aus.
SHL Führt ein Shift Left aus.
SHR Führt ein Shift Right aus.
SUB Führt eine 8-Bit-Subtraktion aus.
XOR Führt ein logisches XOR aus.
Das erste Beispiel zeigt, wie Sie mit einer 8-Bit-CPU zwei 32-Bit-Zahlen addieren. Vor der gleichen Herausforderung steht zum Beispiel eine 32-Bit-CPU, wenn Sie zwei 64-Bit-Zahlen addieren soll. In Tabelle 1 sehen Sie, dass es für die Addition zwei verschiedene Befehle gibt: ADD und ADC. Die beiden Befehle können gemeinsam verwendet werden, um zwei beliebige n-Bit-Zahlen zu addieren. Die Idee dahinter ist folgende:
  • Die ersten acht Bit der beiden Zahlen werden regulär mit dem ADD-Befehl addiert.
  • Gibt es bei der ersten Addition einen Überlauf, wird dieser über den Befehl ADC (Add with Carry) zu den beiden nächsten 8-Bit-Zahlen hinzugefügt.
  • Tritt bei der Addition der zweiten acht Bit der Zahl ein Überlauf auf, wird auch dieser per ADC-Befehl bei der Addition der nächsten acht Bit berücksichtigt.
Listing 3: 32-Bit-Zahlen addieren
; Initialize the Stack Pointer &lt;br/&gt;; and the Base Pointer &lt;br/&gt;MOV XL, &lt;span class="hljs-number"&gt;0xFF&lt;/span&gt; &lt;br/&gt;MOV XH, &lt;span class="hljs-number"&gt;0xFF&lt;/span&gt; &lt;br/&gt;MOV SP, X &lt;br/&gt;MOV XL, &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;br/&gt;MOV XH, &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;br/&gt;MOV BP, X &lt;br/&gt; &lt;br/&gt;; Initialize an address register &lt;br/&gt;MOV XL, &lt;span class="hljs-number"&gt;0x00&lt;/span&gt; &lt;br/&gt;MOV XH, &lt;span class="hljs-number"&gt;0xFF&lt;/span&gt; &lt;br/&gt;MOV Y, X &lt;br/&gt; &lt;br/&gt;; Initialize the D and E register with the &lt;span class="hljs-number"&gt;1&lt;/span&gt;st byte &lt;br/&gt;MOV D, [Y] &lt;br/&gt;MOV E, [Y + &lt;span class="hljs-number"&gt;4&lt;/span&gt;] &lt;br/&gt;ADD D, E &lt;br/&gt;PUSHF &lt;br/&gt; &lt;br/&gt;; Write register D to the &lt;br/&gt;; Output Port &lt;br/&gt;OUTB D &lt;br/&gt; &lt;br/&gt;; Initialize the D and E register with the &lt;span class="hljs-number"&gt;2&lt;/span&gt;nd byte &lt;br/&gt;MOV D, [Y + &lt;span class="hljs-number"&gt;1&lt;/span&gt;] &lt;br/&gt;MOV E, [Y + &lt;span class="hljs-number"&gt;5&lt;/span&gt;] &lt;br/&gt;POPF &lt;br/&gt;ADC D, E &lt;br/&gt;PUSHF &lt;br/&gt; &lt;br/&gt;; Write register D to the &lt;br/&gt;; Output Port &lt;br/&gt;OUTB D &lt;br/&gt; &lt;br/&gt;; Initialize the D and E register with the &lt;span class="hljs-number"&gt;3&lt;/span&gt;rd byte &lt;br/&gt;MOV D, [Y + &lt;span class="hljs-number"&gt;2&lt;/span&gt;] &lt;br/&gt;MOV E, [Y + &lt;span class="hljs-number"&gt;6&lt;/span&gt;] &lt;br/&gt;POPF &lt;br/&gt;ADC D, E &lt;br/&gt;PUSHF &lt;br/&gt; &lt;br/&gt;; Write register D to the &lt;br/&gt;; Output Port &lt;br/&gt;OUTB D &lt;br/&gt; &lt;br/&gt;; Initialize the D and E register with the &lt;span class="hljs-number"&gt;4&lt;/span&gt;th byte &lt;br/&gt;MOV D, [Y + &lt;span class="hljs-number"&gt;3&lt;/span&gt;] &lt;br/&gt;MOV E, [Y + &lt;span class="hljs-number"&gt;7&lt;/span&gt;] &lt;br/&gt;POPF &lt;br/&gt;ADC D, E &lt;br/&gt; &lt;br/&gt;; Write register D to the &lt;br/&gt;; Output Port &lt;br/&gt;OUTB D &lt;br/&gt; &lt;br/&gt;; Stops the CPU execution &lt;br/&gt;HLT &lt;br/&gt; &lt;br/&gt;; &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;span class="hljs-number"&gt;094&lt;/span&gt; &lt;span class="hljs-number"&gt;967&lt;/span&gt; &lt;span class="hljs-number"&gt;296&lt;/span&gt;d &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000000&lt;/span&gt;b, &lt;span class="hljs-number"&gt;00000000&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;1&lt;/span&gt;st Byte &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000001&lt;/span&gt;b, &lt;span class="hljs-number"&gt;11100000&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;2&lt;/span&gt;nd Byte &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000010&lt;/span&gt;b, &lt;span class="hljs-number"&gt;01000011&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;3&lt;/span&gt;rd Byte &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000011&lt;/span&gt;b, &lt;span class="hljs-number"&gt;01000001&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;4&lt;/span&gt;th Byte &lt;br/&gt; &lt;br/&gt;; &lt;span class="hljs-number"&gt;3&lt;/span&gt; &lt;span class="hljs-number"&gt;100&lt;/span&gt; &lt;span class="hljs-number"&gt;000&lt;/span&gt; &lt;span class="hljs-number"&gt;000&lt;/span&gt;d &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000100&lt;/span&gt;b, &lt;span class="hljs-number"&gt;00000000&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;1&lt;/span&gt;st Byte &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000101&lt;/span&gt;b, &lt;span class="hljs-number"&gt;00111111&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;2&lt;/span&gt;nd Byte &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000110&lt;/span&gt;b, &lt;span class="hljs-number"&gt;11000110&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;3&lt;/span&gt;rd Byte &lt;br/&gt;DATA &lt;span class="hljs-number"&gt;1111111100000111&lt;/span&gt;b, &lt;span class="hljs-number"&gt;10111000&lt;/span&gt;b   ; &lt;span class="hljs-number"&gt;4&lt;/span&gt;th Byte  
Den gleichen Ansatz nutzen x86/x64-CPUs. Listing 3 zeigt ein Beispiel, in dem die Zahlen 1094967296 und 3100000000 über die Befehle ADD und ADC addiert werden. Hier können Sie den Nachteil einer 8-Bit-CPU gut erkennen: Sobald Sie mehr als acht Bit gemeinsam verarbeiten möchten, müssen Sie die erforderlichen Operationen auf 8-Bit-Arbeitspakete aufteilen, was bedeutet, dass die Operation entsprechend länger dauert. Eine 32-Bit-CPU führt eine solche Addition mit einem einzelnen ADD-Befehl aus.

Indirect Memory Addressing

Wie Sie in Listing 3 gesehen haben, unterstützt der Assembler auch verschiedene Möglichkeiten, um Hauptspeicher zu ­adressieren. Einerseits können Sie über eckige Klammern ­eine direkte Hauptspeicheradresse angeben, die in einem Register gespeichert wird:

MOV D, <span class="hljs-string">[Y]</span> 
In diesem Fall würde der 8-Bit-Wert aus dem Hauptspeicher (die Adresse steht im Register Y) in das Register D geladen. Man spricht hier von Direct Addressing.Wie Sie mit indirekten Speicheradressen arbeiten, zeigt dieser Befehl:

MOV D, <span class="hljs-string">[Y + 1]</span> 
 
In diesem Fall muss die Selbstbau-CPU die korrekte Hauptspeicheradresse erst ausrechnen (Y+1), um den dort hinterlegten Wert ins Register D zu schreiben. Also wird im resultierenden Low-Level Assembly Code die aktuelle Adresse des Registers Y um 1 erhöht und so die effektive Hauptspeicheradresse ermittelt.Damit das klappt, muss die CPU gut mit 16-Bit-Hauptspeicheradressen rechnen können. Dazu gibt es in der Selbstbau-CPU eine Komponente namens 16BIT_ADDER. Das ist ein 16 Bit breiter Ripple Carry Adder, der Hauptspeicheradressen berechnen kann. Schreiben Sie zum Beispiel auf CISC-Assembly-Ebene ein MOV D, [Y + 1], erzeugt der Assembler folgenden RISC-Code:

<span class="hljs-symbol">SET</span> A, <span class="hljs-string">"0000"</span> 
SET <span class="hljs-keyword">B, </span><span class="hljs-string">"0000"</span> 
<span class="hljs-keyword">MOV8 </span>
<span class="hljs-keyword">MOV_ALU_OUT </span>XH 
SET A, <span class="hljs-string">"0001"</span> 
SET <span class="hljs-keyword">B, </span><span class="hljs-string">"0000"</span> 
<span class="hljs-keyword">MOV8 </span>
<span class="hljs-keyword">MOV_ALU_OUT </span>XL 
<span class="hljs-keyword">MOV16 </span>J, X 
<span class="hljs-keyword">MOV16 </span>X, Y 
<span class="hljs-number">16</span>BIT_ADDER 
<span class="hljs-keyword">MOV16 </span>M, X 
LOAD D 
 
Hier wird im Register J der zu addierende Wert gespeichert (in diesem Fall der Binärwert 00000001) und im Register X der Ausgangswert (in diesem Fall der Wert des Registers Y). Anschließend wird das Ergebnis der Addition in das M-Register geschrieben und über den LOAD-Befehl der eigentliche Hauptspeicherzugriff durchgeführt.

Tabelle 2: Status-Flags

Status-Flag Beschreibung
Sign Ist auf 1 gesetzt, wenn das Ergebnis der letzten ALU-Operation eine negative Zahl ist.
Zero Ist auf 1 gesetzt, wenn das Ergebnis der letzten ALU-Operation die Zahl 0 ist.
Carry Ist auf 1 gesetzt, wenn das Ergebnis der letzten ALU-Operation einen Überlauf ausgelöst hat.
Overflow Ist auf 1 gesetzt, wenn das Ergebnis der letzten vorzeichenbehafteten ALU-Operation einen Überlauf ausgelöst hat.

Verzweigungen

Wenn Sie sich den bisher abgedruckten Assembly-Code ansehen, fällt auf, dass immer nur linearer Code ausgeführt wurde. Verzweigungen kamen bislang nicht vor. Komplette Programme kommen allerdings kaum ohne Verzweigungen aus. Deshalb bietet jede CPU sowohl bedingte als auch unbedingte Sprünge an. Der Unterschied: Ein unbedingter Sprung wird immer durchgeführt. Ein bedingter Sprung wird nur ausgeführt, wenn ein Status-Flag gesetzt ist. Die Selbstbau-CPU implementiert für bedingte Sprünge das Flags-Register, das die vier in Tabelle 2 aufgeführten Status-Flags als Bit-Werte speichert.Die Status-Flags werden von der ALU berechnet und im Flags-Register gespeichert, sobald das Ergebnis der ALU-Operation zurückgeliefert wird. Abhängig vom Flags-Register können bedingte Sprünge im aktuellen Ausbaustand der CPU über die folgenden Assembler-Befehle durchgeführt werden: JNC, JNS, JNZ und JZ.Mit bedingten Sprüngen lassen sich nun sämtliche Kon­trolllogiken abbilden, die Sie von höheren Programmiersprachen gewohnt sind: If Then/Else, For, While, Do/While, Switch/Case.
Listing 4: Bedingte Sprünge
; The &lt;span class="hljs-built_in"&gt;result&lt;/span&gt; is stored &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; &lt;br/&gt;; register F &lt;br/&gt;MOV E, &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;br/&gt; &lt;br/&gt;; Initial &lt;span class="hljs-built_in"&gt;value&lt;/span&gt; &lt;br/&gt;MOV D, &lt;span class="hljs-number"&gt;9&lt;/span&gt; &lt;br/&gt; &lt;br/&gt;; Add &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; current &lt;span class="hljs-built_in"&gt;value&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;br/&gt;; register D &lt;span class="hljs-built_in"&gt;to&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-built_in"&gt;result&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; &lt;br/&gt;; register F &lt;br/&gt;:LOOP &lt;br/&gt;ADD E, D &lt;br/&gt; &lt;br/&gt;; Decrement &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-built_in"&gt;value&lt;/span&gt; &lt;span class="hljs-keyword"&gt;by&lt;/span&gt; &lt;span class="hljs-literal"&gt;one&lt;/span&gt; &lt;br/&gt;DEC D &lt;br/&gt; &lt;br/&gt;; Conditional Jump &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Zero- &lt;br/&gt;; Flag &lt;span class="hljs-built_in"&gt;from&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; ALU is &lt;span class="hljs-keyword"&gt;not&lt;/span&gt; &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;br/&gt;JNZ :LOOP &lt;br/&gt; &lt;br/&gt;; Write register F &lt;span class="hljs-built_in"&gt;to&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;br/&gt;; Output Port &lt;br/&gt;; OUTB E &lt;br/&gt; &lt;br/&gt;; Stops &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; CPU execution &lt;br/&gt;; HLT  
Listing 4 zeigt, wie Sie über eine einfache Schleife und den bedingten Sprungbefehl JNZ die Summe der Zahlen 1 bis 9 berechnen können.

Der Stack

Der Stack – unendliche Weiten. Wir schreiben nun endlich Funktionen! Der Stack ist eines der wichtigsten Konzepte auf Programmier- und CPU-Ebene. Der Stack selbst ist nur eine klar definierte Hauptspeicherregion, in der Daten in einer organisierten Art und Weise abgelegt und wieder ausgelesen werden. Auf dem Stack können Sie zwei elementare Operationen durchführen:
  • Über den Befehl PUSH wird ein Wert auf dem Stack abgelegt.
  • Über den Befehl POP wird der Wert wieder vom Stack gelesen.
Einen Stack können Sie sich wie einen Stapel von Papierblättern vorstellen: Mit dem Befehl PUSH legen Sie ein Blatt obendrauf und mit POP nehmen Sie das oberste Blatt wieder herunter.Zum Implementieren eines Stacks benötigen Sie einen Stack Pointer, also ein Register, das die aktuelle Adresse des Stacks speichert. Dazu bietet die Selbstbau-CPU das 16-Bit-Register SP an. Den Stack können Sie über die Befehle PUSH und POP manipulieren, indem Sie Daten (8-Bit-Werte) auf den Stack schreiben und wieder lesen. Der Befehl PUSH führt Folgendes aus:
  • Der aktuelle Wert des Stack Pointers wird um 1 verringert.
  • Der Wert des angegebenen Registers wird auf den Stack geschrieben.
Der Befehl POP führt dagegen die folgenden Schritte aus:
  • Der aktuelle Wert des Stacks wird in das angegebene Register geschrieben.
  • Der aktuelle Wert des Stack Pointers wird um 1 erhöht.
Wie Sie sehen, wächst der Stack im Hauptspeicher immer rückwärts: Die Hauptspeicheradressen im Register SP werden kleiner, wenn Sie mit dem PUSH-Befehl Daten auf dem Stack ablegen.
Dieses Stack-Design wurde vor Urzeiten gewählt, damit der Heap (auf dem dynamische Hauptspeicher-Allokierungen vorgenommen werden) und der Stack sauber voneinander getrennt sind. Dies ist insbesondere dann sehr wichtig, wenn Sie nur einen beschränkten Adressraum zur Verfügung haben – wie im Fall der Selbstbau-CPU mit 64 KByte Arbeitsspeicher. Bild 5 veranschaulicht dieses Konzept.
Listing 5: Der Stack
; Initialize the &lt;span class="hljs-keyword"&gt;Stack&lt;/span&gt; Pointer &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV&lt;/span&gt; XL, 0xFF &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV&lt;/span&gt; XH, 0xFF &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV&lt;/span&gt; SP, X &lt;br/&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV&lt;/span&gt; &lt;span class="hljs-keyword"&gt;D&lt;/span&gt;, 00001111b &lt;br/&gt;PUSH &lt;span class="hljs-keyword"&gt;D&lt;/span&gt; &lt;br/&gt;POP &lt;span class="hljs-keyword"&gt;E&lt;/span&gt;  
Damit die Stack-Pointer-Arithmetik über die Befehle PUSH und POP richtig arbeiten kann, muss der Stack Pointer beim Start der CPU initialisiert werden. Listing 5 zeigt, wie Sie den Stack Pointer initialisieren und einen einfachen Register-Transfer über den Stack abbilden können. Im Listing wird der Stack Pointer auf den Wert 0xFFFF initialisiert, das heißt, er liegt ganz am Ende des 64K-Adressraums. Im ersten Schritt wird der Binärwert 00001111 in das Register D geladen. Im nächsten Schritt wird über den Befehl PUSH D der Stack Pointer um 1 reduziert, das heißt, dass dieser nun den Wert 0xFFFE hat. Und auf diese Hauptspeicheradresse wird der Wert des Registers D geschrieben – nämlich 00001111.
Listing 6: PUSH-Implementierung mit RISC-Befehlen
&lt;span class="hljs-symbol"&gt;SET&lt;/span&gt; A, &lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;XL &lt;br/&gt;SET A, &lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;XH &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;&lt;span class="hljs-built_in"&gt;SP&lt;/span&gt;, X &lt;br/&gt;SET A, &lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"0000"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;D &lt;br/&gt;SAVE_FLAGS &lt;br/&gt;SET A, &lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;XL &lt;br/&gt;SET A, &lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"1111"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;XH &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;J, X &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;X, &lt;span class="hljs-built_in"&gt;SP&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-number"&gt;16&lt;/span&gt;BIT_ADDER &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;&lt;span class="hljs-built_in"&gt;SP&lt;/span&gt;, X &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;M, &lt;span class="hljs-built_in"&gt;SP&lt;/span&gt; &lt;br/&gt;STORE D &lt;br/&gt;RESTORE_FLAGS &lt;br/&gt;SAVE_FLAGS &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;M, &lt;span class="hljs-built_in"&gt;SP&lt;/span&gt; &lt;br/&gt;LOAD E &lt;br/&gt;SET A, &lt;span class="hljs-string"&gt;"0001"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"0000"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;XL &lt;br/&gt;SET A, &lt;span class="hljs-string"&gt;"0000"&lt;/span&gt; &lt;br/&gt;SET &lt;span class="hljs-keyword"&gt;B, &lt;/span&gt;&lt;span class="hljs-string"&gt;"0000"&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV8 &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV_ALU_OUT &lt;/span&gt;XH &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;J, X &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;X, &lt;span class="hljs-built_in"&gt;SP&lt;/span&gt; &lt;br/&gt;&lt;span class="hljs-number"&gt;16&lt;/span&gt;BIT_ADDER &lt;br/&gt;&lt;span class="hljs-keyword"&gt;MOV16 &lt;/span&gt;&lt;span class="hljs-built_in"&gt;SP&lt;/span&gt;, X &lt;br/&gt;RESTORE_FLAGS  
Mit der Ausführung des Befehls POP E wird nun der Wert, auf den der aktuelle Stack Pointer zeigt, vom Hauptspeicher in das Register E geladen. Das heißt, dass nun das Register E den Wert 00001111 enthält. Schlussendlich wird noch der Stack Pointer um 1 erhöht, sodass dieser wieder den Wert 0xFFFF hat. All diese Operationen werden wieder über viele einzelne RISC-Operationen auf CPU-Ebene abgebildet. Listing 6 zeigt den RISC-Code zum PUSH-Befehl.Wie Sie sehen, wird die Komponente 16BIT_ADDER verwendet, um den Wert des Stack Pointers zu dekrementieren beziehungsweise zu inkrementieren – Wiederverwendung ist doch was Schönes!
Listing 7: PUSHF & POPF
; Initialize &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Stack Pointer &lt;br/&gt;MOV XL, &lt;span class="hljs-number"&gt;0xFF&lt;/span&gt; &lt;br/&gt;MOV XH, &lt;span class="hljs-number"&gt;0xFF&lt;/span&gt; &lt;br/&gt;MOV SP, X &lt;br/&gt; &lt;br/&gt;; Sets &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Zero Flag &lt;span class="hljs-built_in"&gt;to&lt;/span&gt; &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;br/&gt;MOV D, &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;br/&gt;MOV E, &lt;span class="hljs-number"&gt;1&lt;/span&gt; &lt;br/&gt;SUB D, E &lt;br/&gt; &lt;br/&gt;; Store &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Flags onto &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Stack &lt;br/&gt;PUSHF &lt;br/&gt; &lt;br/&gt;; Load &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Flags &lt;span class="hljs-built_in"&gt;from&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; Stack &lt;br/&gt;; &lt;span class="hljs-keyword"&gt;into&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; G Register &lt;br/&gt;POP G  
Zusätzlich besteht über das Befehlspaar PUSHF und POPF die Möglichkeit, den aktuellen Inhalt des Flags-Registers auf den Stack zu schreiben (über PUSHF) beziehungsweise über POPF vom Stack in ein General Purpose Register zurückzuschreiben. Schreibend wird immer nur über eine ALU-Operation auf das Flags-Register zugegriffen. Listing 7 zeigt ein einfaches Beispiel.

Funktionsaufrufe

Und nun geht es ans Eingemachte: Funktionsaufrufe auf Assembly-Ebene! Nachdem ein Stack implementiert ist, steht auch die notwendige Basis für Funktionsaufrufe. Dadurch sind Sie auf Assembly-Ebene in der Lage, Funktionen zu schreiben, die Sie zwecks Wiederverwendbarkeit an einer beliebigen anderen Stelle aufrufen können. Listing 8 zeigt ein Assembly-Programm, das einen einfachen Funktionsaufruf durchführt.
Listing 8: Ein einfacher Funktionsaufruf
; Initialize the Stack &lt;span class="hljs-keyword"&gt;Pointer&lt;/span&gt; &lt;br/&gt;MOV XL, 0xFF &lt;br/&gt;MOV XH, 0xFF &lt;br/&gt;MOV SP, X &lt;br/&gt; &lt;br/&gt;MOV F, 11110000b &lt;br/&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;; C&lt;/span&gt;all&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt; a subrout&lt;/span&gt;i&lt;/span&gt;ne.&lt;span class="hljs-keyword"&gt;.. &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;C&lt;/span&gt;AL&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;L :SUBROUT&lt;/span&gt;&lt;/span&gt;INE &lt;br/&gt; &lt;br/&gt;MOV F, 10101010b &lt;br/&gt; &lt;br/&gt;; St&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;ops pro&lt;/span&gt;&lt;/span&gt;gram execution &lt;br/&gt;HL&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;T &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;:SUBROU&lt;/span&gt;&lt;/span&gt;TINE &lt;br/&gt;MOV F, 01010101b &lt;br/&gt;RET  
Wie Sie anhand des Assembler-Codes erkennen können, wird die Funktion SUBROUTINE aufgerufen, die den Wert des Registers F verändert, und anschließend wird über den Befehl RET wieder zum aufrufenden Code zurückverzweigt.Das sieht jetzt freilich nach elegantem Assembly-Code auf CISC-Ebene aus. Auf RISC-Ebene müssen dafür jedoch eine Vielzahl unterschiedlicher Problematiken gelöst werden:
  • Wie werden Parameter-Werte an eine Funktion übergeben?
  • Wie werden Ergebniswerte einer Funktion zurückgeliefert?
  • Woher weiß der RET-Befehl, an welche Programmstelle er zurückverzweigen soll?
  • Wohin werden die lokalen Variablen einer Funktion gespeichert?
Das sind sehr viele Fragen mit sehr vielen Details, die gelöst werden müssen, aber die Antwort zu jeder Frage ist immer wieder die gleiche: der Stack. Der Stack ist einfach ein Wundermittel!Damit Funktionsaufrufe auf Assembly-Ebene implementiert und realisiert werden können, werden sogenannte Stack Frames auf dem Stack angelegt. Dazu werden die folgenden Informationen für einen Funktionsaufruf in einer fixen Reihenfolge auf dem Stack abgelegt:
  • Parameterwerte,
  • die Rücksprungadresse sowie
  • lokale Variablen.
Diese Informationen werden als Stack Frame bezeichnet. In welcher Reihenfolge Parameterwerte an eine Funktion übergeben werden (von links nach rechts oder von rechts nach links) oder ob Parameterwerte über Register übergeben werden, hängt von der gewählten Calling Convention des Compilers ab.Jeder Compiler implementiert seine eigene Calling Convention. Ich habe mich dafür entschieden, die Parameterwerte einfach von rechts nach links an die aufrufende Funktion zu übergeben.Stellen Sie sich zum Beispiel die fiktive Funktion atoi(char *ptr, int cnt) vor, die einen String in einen Integer-Wert konvertiert. Dazu müssen Sie die Hauptspeicheradresse über­geben, an der sich der gewünschte String befindet, und die Anzahl der Zeichen. Der Funktionsaufruf auf CISC-Ebene würde wie folgt aussehen:

<span class="hljs-keyword">PUSH</span> <span class="hljs-number">10</span>     ; Number of characters 
<span class="hljs-keyword">PUSH</span> <span class="hljs-number">0</span>x00   ; <span class="hljs-number">16</span>-bit memory address – Low <span class="hljs-keyword">Byte</span> 
<span class="hljs-keyword">PUSH</span> <span class="hljs-number">0</span>xFA   ; <span class="hljs-number">16</span>-bit memory address – High <span class="hljs-keyword">Byte</span> 
<span class="hljs-keyword">CALL</span> :_ATOI 
In diesem Fall befindet sich der zu konvertierende String an der Hauptspeicheradresse 0xFA00 und das Register D enthält die Anzahl der zu konvertierenden Zeichen. Bild 6 zeigt den Stack Frame dazu. 
Der eigentliche Funktionsaufruf wird über den Befehl CALL durchgeführt. Dieser berechnet die Rücksprungadresse – das ist die Hauptspeicheradresse direkt nach dem CALL-Befehl – und schreibt diese ebenfalls auf den Stack. Hier müssen jedoch wieder zwei 8-Bit-Werte auf den Stack geschrieben werden, weil eine Hauptspeicheradresse 16 Bit lang ist. In Bild 7 sehen Sie den vollständigen Stack Frame.
Nun wird zur Funktion verzweigt, indem ein unbedingter Sprung zur Hauptspeicheradresse durchgeführt wird, an der die Funktion­ abgelegt ist (der Assembler weiß, an welcher Hauptspeicheradresse die Funktion abgelegt wird, da der Assembler auch für das Generieren der Hauptspeicheradressen zuständig ist). Diese Hauptspeicheradresse wird im Assembler-Code über einen sogenanntes Label identifiziert – eingeleitet durch einen Doppelpunkt (etwa :SUBROUTINE). Nun sind wir in der Implementierung der Funktion selbst angekommen. Jede Funktion besteht immer aus drei Teilen:
  • Prolog (Einleitung)
  • Implementierung (Hauptteil)
  • Epilog (Schluss)
Bevor es um die Details von Prolog und Epilog geht, lernen Sie noch ein weiteres Register der Selbstbau-CPU kennen, den Base Pointer, abgekürzt BP.Der Base Pointer speichert am Anfang der Funktion den ­aktuellen Stack Pointer, da sich dieser – während die Funk­tion ausgeführt wird – durch Aufrufe von PUSH- und/oder POP-Befehlen verändern kann. Nach Ausführen der Funktion­ benötigen Sie jedoch den Stack-Pointer-Wert, der zum Beginn des Funktionsaufrufs galt, damit Sie einfach auf die übergebenen Parameter und die ebenfalls auf dem Stack gespeicherten lokalen Variablen zugreifen können. Im Prolog der Funktion geschieht daher immer Folgendes:
  • Der aktuelle Base Pointer wird auf dem Stack gesichert.
  • Der aktuelle Stack Pointer wird im Base Pointer gespeichert (der alte Base­ Pointer wurde bereits im vorigen Schritt auf dem Stack zwischengespeichert und kann damit überschrieben werden).
  • Der Stack Pointer wird abhängig von dem von den lokalen Variablen benötigten Speicherplatz minimiert. Dadurch wird auf dem Stack Platz für lokale Variablen geschaffen.
Der folgende Assembler-Code zeigt einen typischen Prolog, der drei Byte Speicherplatz für lokale Variablen reserviert. Dieser Prolog kann über den CISC-Befehl ENTER 3 auch automatisch generiert werden.

<span class="hljs-keyword">PUSH </span><span class="hljs-keyword">BP </span>
<span class="hljs-keyword">MOV </span><span class="hljs-keyword">BP, </span><span class="hljs-built_in">SP</span> 
<span class="hljs-keyword">SUB </span><span class="hljs-built_in">SP</span>, <span class="hljs-number">3</span> 
 
In Bild 8 sehen Sie den Stack Frame, nachdem der Prolog ausgeführt wurde. Wie Sie in der Abbildung erkennen können, wurden am Ende des Stacks (grafisch gesehen oben) drei Byte für lokale Variablen reserviert, die in obigem Listing vom aktuellen Stack Pointer abgezogen wurden. Anhand dieser Grafik lässt sich zudem erkennen, wie mithilfe des Base Pointers auf Übergabeparameter und lokale Variablen zugegriffen werden kann.
Wenn Sie einen positiven Offset beim Base Pointer angeben, referenzieren Sie einen Übergabeparameter; geben Sie einen negativen Offset an, wird eine lokale Variable referenziert. Und genau aus diesem Grund wurde vorher im Rahmen des Prologs der Base Pointer initialisiert: Er bietet eine stabile, nicht veränderbare Referenz auf dem Stack.Freilich könnten Sie – rein theoretisch – auch ohne einen Base Pointer auf die Übergabewerte und die lokalen Variablen zugreifen, indem Sie sich stets direkt auf den aktuellen Stack Pointer beziehen. Allerdings wäre der in diesem Fall  erforderliche Code erheblich komplexer, da sich der Stack Pointer mit jeder Zeile Code verändern kann (durch ein PUSH oder POP) und Sie die Änderungen permanent mitprotokollieren müssten.Dieses Konzept nennt sich Frame Pointer Omission (FPO) und wird zum Beispiel auf x64-Architekturen durchgeführt. Dadurch steht das BP-Register als weiteres General Purpose Register zur Verfügung, das der Compiler auch für andere Zwecke verwenden kann.Nun, wie gesagt arbeitet der Assembler zur Selbstbau-CPU mit Base Pointer, der im Rahmen des Prologs initialisiert wird. Danach können Sie per indirekter Adressierung sehr leicht auf Übergabeparameter und lokale Variablen innerhalb der Funktion zugreifen. Hier ein einfaches Beispiel dazu:
MOV <span class="hljs-keyword">D</span>, [BP + <span class="hljs-number">5</span>] ; <span class="hljs-number">2.</span> <span class="hljs-keyword">Parameter</span> 
MOV <span class="hljs-keyword">E</span>, [BP + <span class="hljs-number">4</span>] ; <span class="hljs-number">1.</span> Paramter 
ADD <span class="hljs-keyword">D</span>, <span class="hljs-keyword">E</span> 
 
; Schreiben einer lokalen 
; Variablen 
MOV [BP - <span class="hljs-number">1</span>], <span class="hljs-keyword">D</span> 
 
Das Funktionsergebnis kann über den Stack an den Aufrufer zurückgeliefert oder in einem spezifischen Register abgelegt werden. Dies ist abhängig von der gewählten Calling Convention. In meinem Fall liefere ich das Ergebnis – sofern es sich um einen 8-Bit-Wert handelt – immer im Register D zurück.Nachdem die eigentliche Implementierung der Funktion abgearbeitet wurde, wird noch der Epilog ausgeführt, der den Stack Frame der aktuellen Funktion verwirft. Sehen Sie sich dazu den folgenden Assembly-Code näher an. Der Epilog kann wieder über den CISC-Befehl LEAVE automatisch generiert werden.

<span class="hljs-keyword">MOV</span> <span class="hljs-built_in">SP</span>, <span class="hljs-built_in">BP</span> 
<span class="hljs-keyword">POP</span> <span class="hljs-built_in">BP</span> 
<span class="hljs-keyword">RET</span> 
 
Im Rahmen des Epilogs werden die drei folgenden Aufgaben erledigt:
  • Herstellen des alten Stack Pointers (MOV SP, BP)
  • Herstellen des alten Base Pointers (POP BP)
  • Unbedingter Sprung zurück zum Aufrufer (RET)
Beim Ausführen des RET-Befehls wird die Rücksprungadresse vom Stack genommen (auf die jetzt der Stack Pointer zeigt!) und im Program Counter gespeichert. Dadurch wird die Code-Ausführung genau an der Speicheradresse weitergeführt, bei der vorher der Funktionsaufruf durchgeführt wurde (plus ein Byte, damit der Funktionsaufruf nicht nochmals ausgeführt wird). Wie der Stack Frame nun aussieht, zeigt Bild 9.
Wie Sie im Bild jetzt sehr schön erkennen können, sind die Übergabeparameter des Funktionsaufrufes noch immer auf dem Stack gespeichert. Daher muss der Code, welcher ­zuvor den Funktionsaufruf durchgeführt hat, jetzt noch die Übergabeparameter vom Stack entfernen:

<span class="hljs-keyword">ADD </span><span class="hljs-built_in">SP</span>, <span class="hljs-number">3</span> 
 
Dazu werden einfach drei Byte zum Stack Pointer hinzugezählt und der Stack Pointer steht wieder genau dort, wo er vor dem Funktionsaufruf war. Cool, oder?Wie Sie anhand dieses Abschnitts gesehen haben, handelt es sich beim Stack um ein wirklich leistungsfähiges Konzept, durch das Funktionsaufrufe erst möglich werden. Das Schöne daran ist auch, dass es für jeden Funk­tionsaufruf einen eigenen Stack Frame gibt. Sie können daher innerhalb einer Funktion ohne Probleme eine weitere Funktion aufrufen – es wird einfach ein neuer Stack Frame erzeugt, genau nach dem gleichen Muster wie vorher.
Listing 9: Rekursive Funktionsaufrufe
; Initialize the Stack &lt;span class="hljs-keyword"&gt;Pointer&lt;/span&gt; &lt;br/&gt;MOV XL, 0xFF &lt;br/&gt;MOV XH, 0xFF &lt;br/&gt;MOV SP, X &lt;br/&gt; &lt;br/&gt;MOV F&lt;span class="hljs-number"&gt;,&lt;/span&gt; 1 &lt;br/&gt;MOV G, 00010000b &lt;br/&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;; C&lt;/span&gt;all&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt; a subrout&lt;/span&gt;i&lt;/span&gt;ne.&lt;span class="hljs-keyword"&gt;.. &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;C&lt;/span&gt;AL&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;L :SUBROUT&lt;/span&gt;&lt;/span&gt;INE &lt;br/&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;;&lt;/span&gt; Do somethi&lt;span class="hljs-keyword"&gt;ng e&lt;/span&gt;lse after the&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;; recurs&lt;/span&gt;i&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;ve funct&lt;/span&gt;&lt;/span&gt;i&lt;span class="hljs-keyword"&gt;on c&lt;/span&gt;all... &lt;br/&gt;INC G &lt;br/&gt;&lt;span class="hljs-built_in"&gt; &lt;/span&gt;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-built_in"&gt;; Wr&lt;/span&gt;ite register G to the &lt;br/&gt;; Output Port &lt;br/&gt;OUTB G &lt;br/&gt; &lt;br/&gt;; Sto&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;ps prog&lt;/span&gt;&lt;/span&gt;ram execution &lt;br/&gt;HLT&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;:SUBROUT&lt;/span&gt;&lt;/span&gt;INE &lt;br/&gt;INC F &lt;br/&gt;CMP F, G &lt;br/&gt;JZ :SUBROUTINE_RETURN &lt;br/&gt;&lt;span class="hljs-keyword"&gt; &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;; Recurs&lt;/span&gt;i&lt;span class="hljs-keyword"&gt;ve c&lt;/span&gt;all.&lt;span class="hljs-keyword"&gt;.. &lt;/span&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;C&lt;/span&gt;AL&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;L :SUBROUT&lt;/span&gt;&lt;/span&gt;INE &lt;br/&gt; &lt;br/&gt;:SUBROUTINE_RETURN &lt;br/&gt;RET  
Mit diesem Ansatz sind sogar rekursive Funktionen möglich, da jeder rekursive Funktionsaufruf auch seine eigenen lokalen Variablen innerhalb seines eigenen Stack Frames speichert. Listing 9 zeigt ­einen einfachen rekursiven Funktionsaufruf.Eine weitere wichtige Funktion, die Stack Frames erfüllen, ist das Generieren eines Call Stacks, den Ihnen jeder Debugger zurückliefern kann. Ein Call Stack zeigt ganz genau auf, welche Funktion welche Funktion aufgerufen hat und wie die einzelnen Übergabeparameter und lokalen Variablen ausgesehen haben. Diese Informationen werden aus den einzelnen Stack Frames ermittelt.Wie Sie beim Prolog gesehen haben, wird immer der alte Base Pointer auf den Stack gepusht. Und der aktuelle Base Pointer zeigt genau auf diese Adresse auf dem Stack. Daher zeigt der aktuelle Base Pointer immer genau auf die Hauptspeicheradresse, an welcher der Base Pointer der vorherigen Funktion auf dem Stack abgelegt ist.
Dadurch entsteht eine einfach verkettete Liste, mit deren Hilfe Sie sich durch die verschiedenen Stack Frames bewegen und einen vollständigen Call Stack ermitteln können.
Bild 10 veranschaulicht dieses Konzept.
Listing 10: Call-Stack-Analyse
:_STACKBACKTRACE &lt;br/&gt;ENTER &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;br/&gt; &lt;br/&gt;MOV H, &lt;span class="hljs-number"&gt;0&lt;/span&gt; &lt;br/&gt; &lt;br/&gt;; Initialize &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; registers &lt;span class="hljs-keyword"&gt;with&lt;/span&gt; &lt;br/&gt;; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; current BP &lt;br/&gt;MOV X, BP &lt;br/&gt;MOV D, XH &lt;br/&gt;MOV E, XL &lt;br/&gt; &lt;br/&gt;:BP_NOT_NULL &lt;br/&gt;; Load &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; memory content &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;br/&gt;; current BP (pointer &lt;span class="hljs-built_in"&gt;to&lt;/span&gt; &lt;br/&gt;; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; previous stack frame) &lt;span class="hljs-keyword"&gt;into&lt;/span&gt; &lt;br/&gt;; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; registers &lt;br/&gt;MOV XH, D &lt;br/&gt;MOV XL, E &lt;br/&gt;MOV Y, X &lt;br/&gt; &lt;br/&gt;; Load &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; BP &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; previous &lt;br/&gt;; stack frame &lt;span class="hljs-keyword"&gt;into&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; registers &lt;br/&gt;MOV D, [Y] &lt;br/&gt;MOV E, [Y + &lt;span class="hljs-number"&gt;1&lt;/span&gt;] &lt;br/&gt;; Write out &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; higher &lt;span class="hljs-keyword"&gt;byte&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;br/&gt;; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-literal"&gt;return&lt;/span&gt; address &lt;br/&gt;MOV XL, [Y + &lt;span class="hljs-number"&gt;2&lt;/span&gt;] &lt;br/&gt;OUTB XL &lt;br/&gt; &lt;br/&gt;; Write out &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-built_in"&gt;lower&lt;/span&gt; &lt;span class="hljs-keyword"&gt;byte&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;br/&gt;; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-literal"&gt;return&lt;/span&gt; address &lt;br/&gt;MOV XL, [Y + &lt;span class="hljs-number"&gt;3&lt;/span&gt;] &lt;br/&gt;OUTB XL &lt;br/&gt; &lt;br/&gt;; Check &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-built_in"&gt;lower&lt;/span&gt; &lt;span class="hljs-keyword"&gt;byte&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;br/&gt;; BP is &lt;span class="hljs-literal"&gt;NULL&lt;/span&gt;. &lt;br/&gt;; We should also check here &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;br/&gt;; higher &lt;span class="hljs-keyword"&gt;byte&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; BP &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;it&lt;/span&gt; &lt;br/&gt;; is &lt;span class="hljs-literal"&gt;NULL&lt;/span&gt;... &lt;br/&gt;CMP E, H &lt;br/&gt; &lt;br/&gt;; We haven‘t yet reached &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; &lt;span class="hljs-number"&gt;1&lt;/span&gt;st &lt;br/&gt;; stack frame, therefore we &lt;br/&gt;; walk again down &lt;span class="hljs-keyword"&gt;the&lt;/span&gt; stack &lt;br/&gt;JNZ :BP_NOT_NULL &lt;br/&gt; &lt;br/&gt;LEAVE &lt;br/&gt;RET  
Der Assembler-Code in Listing 10 zeigt eine einfache Implementierung, wie der Call Stack von verschachtelten Funk­tionsaufrufen über die Stack Frames analysiert werden kann. Dazu werden einfach die entsprechenden Rücksprungadressen über den OUTB-Befehl an einen der Output-Ports der CPU geschrieben.

Fazit

Im Rahmen dieses Artikels haben Sie gesehen, wie Sie bereits mit einfachen Befehlen wirkungsvolle Programme für die Selbstbau-CPU schreiben können. Einerseits haben Sie in groben Zügen erfahren, wie Sie einen einfachen Assembler implementieren können, und auf der anderen Seite habe ich Ihnen die von mir für die Selbstbau-CPU implementierte Assembler-Sprache vorgestellt.Ich hoffe, dass ich Ihnen mit dieser dreiteiligen Artikel­serie Appetit auf die Interna einer CPU gemacht habe und Sie nun ein besseres Verständnis dafür haben, was auf Bit-Ebene so alles passiert, wenn die CPU Assembler-Befehle ausführt. In diesem Sinne: PRINT("Good Bye")

Fussnoten

  1. Klaus Aschenbrenner, Eine einfache CPU entwickeln, Teil 1, Die CPU? Bau ich selber!, dotnetpro 6/2017, Seite 122 ff., http://www.dotnetpro.de/A1706CPU
  2. Klaus Aschenbrenner, Eine einfache CPU entwickeln, Teil 2, Architektur der Selbstbau-CPU, dotnetpro 7/2017, Seite 126 ff., http://www.dotnetpro.de/A1707CPU
  3. Der Assembler zur Selbstbau-CPU auf GitHub, https://github.com/KPU-RISC/KPU
  4. Objektorientierter Parsergenerator ANTLR, http://www.antlr.org/

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

Mehr Code, weniger Scrollen
Wie der BenQ RD280U und die RD-Serie die Produktivität von Entwicklern steigern (Sponsored Post)
3 Minuten
24. Jun 2025
UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige