Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

E20-Assembler in Java: Von der Theorie zur Praxis – Ein vollständiger Leitfaden

Lerne, wie du mit Java einen E20-Assembler schreibst, der Assembly-Code in Maschinensprache übersetzt. Schritt-für-Schritt-Anleitung mit Codebeispielen und praxisnahen Erklärungen.

E20 Assembler Java E20 Assembly in Maschinensprache Java Assembler Tutorial E20 Befehlssatz kodieren E20 Assembler Projekt ATOM Assignment 2 Lösung E20 Assembler Schritt für Schritt Java Programmierung Assembler E20 Prozessor Assembler E20 Maschinensprache Übersetzer E20 Assembler Codebeispiele E20 Assembler Testfälle E20 Assembler Fehlerbehandlung E20 Assembler mit Labels E20 Assembler Symboltabelle E20 Assembler Bit-Shifting Java

Einführung in den E20-Assembler

Der E20-Assembler ist ein Programm, das E20-Assembly-Befehle in 16-Bit-Maschinensprache umwandelt. Diese Aufgabe ist ein zentraler Bestandteil vieler Informatikkurse und wird oft als Prüfungsleistung gestellt. In diesem Tutorial zeigen wir dir, wie du einen funktionierenden E20-Assembler in Java implementierst – ohne die gesamte Lösung vorwegzunehmen, aber mit genug Details, um den Prozess zu verstehen. Wir verwenden aktuelle Beispiele aus der Welt der KI und Apps, um die Konzepte greifbar zu machen.

Grundlagen der E20-Architektur

Der E20-Prozessor ist ein einfacher 16-Bit-Prozessor mit 8 Registern ( $0 bis $7). Jeder Befehl besteht aus einem Opcode (3 Bit), Quellregister (3 Bit), Zielregister (3 Bit) und einem Immediate-Wert (7 Bit). Es gibt verschiedene Befehlstypen: R-Typ (Register), I-Typ (Immediate) und J-Typ (Jump). Dein Assembler muss diese Formate erkennen und korrekt kodieren. Stell dir vor, du entwickelst eine KI-App, die Befehle in Echtzeit übersetzt – ähnlich wie ein Assembler Assembly in Maschinencode umwandelt.

Projektstruktur und Aufbau

Ein typischer E20-Assembler in Java besteht aus mehreren Komponenten:

  • Main-Klasse: Liest die Eingabedatei und steuert den Ablauf.
  • Parser: Zerlegt jede Zeile in Tokens (Opcode, Register, Immediate).
  • Assembler: Übersetzt Tokens in 16-Bit-Binärwerte.
  • Ausgabe: Formatiert die Ausgabe als Verilog-Syntax.

Diese modulare Struktur erleichtert Tests und Wartung – ähnlich wie bei einer gut designten App.

Schritt 1: Datei einlesen und vorbereiten

Dein Programm erhält den Dateinamen als Kommandozeilenargument. Du liest die Datei zeilenweise ein und entfernst Kommentare (alles nach einem Semikolon). Beispiel: Aus addi $1, $2, 3 ; Kommentar wird addi $1, $2, 3. Verwende BufferedReader und FileReader für effizientes Einlesen.

Schritt 2: Parser für Assembly-Befehle

Der Parser zerlegt jede Zeile in Bestandteile. Für addi $1, $2, 3 extrahierst du: Opcode = addi, Quellregister = 1, Zielregister = 2, Immediate = 3. Beachte, dass Register mit einem Dollarzeichen beginnen. Ein regulärer Ausdruck wie ^(\w+)\s+\$(\d+)\s*,\s*\$(\d+)\s*,\s*(-?\d+)$ hilft dir, die Felder zu trennen. Für Jump-Befehle (z.B. j beginning) benötigst du eine Symboltabelle, um Labels in Adressen umzurechnen.

Schritt 3: Symboltabelle für Labels

In einem ersten Durchlauf sammelst du alle Labels (z.B. beginning:) und notierst ihre Adresse (Zeilennummer * 2? Nein, die Adressen werden fortlaufend vergeben: erste Instruktion bei Adresse 0, nächste bei 1 usw.). Speichere sie in einer HashMap. Im zweiten Durchlauf ersetzt du Labels durch die entsprechenden Adressen. Dies ist vergleichbar mit einer KI, die aus Kontext lernt und später darauf zurückgreift.

Schritt 4: Befehlskodierung

Jeder Befehlstyp hat ein eigenes Format. Hier eine Übersicht:

  • R-Typ (add, sub, etc.): Opcode (3 Bit) | Quellregister (3 Bit) | Zielregister (3 Bit) | 0 (7 Bit).
  • I-Typ (addi, movi, jeq, etc.): Opcode (3 Bit) | Quellregister (3 Bit) | Zielregister (3 Bit) | Immediate (7 Bit, vorzeichenbehaftet).
  • J-Typ (j, jal, etc.): Opcode (3 Bit) | 0 (3 Bit) | 0 (3 Bit) | Zieladresse (7 Bit).

Beispiel: addi $1, $2, 3 hat Opcode 001 (laut E20-Manual), Quellregister 010 (Register 2), Zielregister 001 (Register 1), Immediate 0000011 (3). Zusammengesetzt: 0010100010000011. Nutze Bit-Shifting in Java: int machineCode = (opcode << 13) | (srcReg << 10) | (dstReg << 7) | immediate;. Achte darauf, dass Immediate negativ sein kann – verwende eine Maske mit 0x7F, um nur die unteren 7 Bits zu behalten.

Schritt 5: Ausgabe formatieren

Die Ausgabe erfolgt zeilenweise im Format: ram[adresse] = 16'bxxxxxxxxxxxxxxxx; // assembly. Die Adresse beginnt bei 0 und wird für jeden Befehl um 1 erhöht. Verwende String.format oder System.out.printf für die binäre Darstellung: String binary = String.format("%16s", Integer.toBinaryString(machineCode)).replace(' ', '0');.

Vollständiges Beispiel: Java-Code-Snippet

import java.io.*;
import java.util.*;

public class Asm {
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage: java Asm file.s");
            System.exit(1);
        }
        BufferedReader br = new BufferedReader(new FileReader(args[0]));
        List<String> lines = new ArrayList<>();
        Map<String, Integer> labels = new HashMap<>();
        String line;
        int addr = 0;
        while ((line = br.readLine()) != null) {
            line = line.replaceAll(";.*", "").trim();
            if (line.isEmpty()) continue;
            if (line.contains(":")) {
                String[] parts = line.split(":");
                labels.put(parts[0].trim(), addr);
                line = parts.length > 1 ? parts[1].trim() : "";
                if (line.isEmpty()) continue;
            }
            lines.add(line);
            addr++;
        }
        br.close();
        addr = 0;
        for (String instr : lines) {
            int machineCode = assemble(instr, labels);
            System.out.printf("ram[%d] = 16'b%16s; // %s%n", addr, 
                String.format("%16s", Integer.toBinaryString(machineCode)).replace(' ', '0'), instr);
            addr++;
        }
    }
    
    static int assemble(String instr, Map<String,Integer> labels) {
        // Vereinfachte Implementierung – muss erweitert werden
        String[] tokens = instr.split("[ ,]+");
        String op = tokens[0];
        int opcode = getOpcode(op);
        int src = 0, dst = 0, imm = 0;
        // ... Tokens parsen je nach Befehlstyp
        return (opcode << 13) | (src << 10) | (dst << 7) | (imm & 0x7F);
    }
    
    static int getOpcode(String op) {
        switch(op) {
            case "addi": return 0b001;
            case "movi": return 0b001; // gleicher Opcode? Prüfe Manual!
            // ... weitere Opcodes
            default: return 0;
        }
    }
}

Hinweis: Der Code ist unvollständig und dient als Gerüst. Du musst die Logik für alle Befehlstypen ergänzen.

Testen und Debuggen

Nutze die mitgelieferten Beispieldateien (z.B. loop2.s) und vergleiche deine Ausgabe mit der erwarteten. Schreibe eigene Testfälle, um Randfälle abzudecken: negative Immediate, mehrere Labels, leere Dateien, etc. Ein guter Test ist wie ein Bug-Report in einer App – er deckt versteckte Fehler auf.

Häufige Fehler und Tipps

  • Opcode-Tabelle: Stelle sicher, dass du die korrekten Opcodes aus dem E20-Manual verwendest. Ein Fehler hier führt zu falscher Maschinensprache.
  • Register-Indizes: Register $0 bis $7 entsprechen den Zahlen 0 bis 7. Verwende keine 1-basierte Indizierung.
  • Immediate-Vorzeichen: Negative Zahlen müssen im Zweierkomplement dargestellt werden. In Java: imm & 0x7F extrahiert die unteren 7 Bits korrekt.
  • Labels: Vergiss nicht, Labels im ersten Durchlauf zu sammeln und im zweiten zu ersetzen. Ein häufiger Fehler ist, Labels als Instruktionen zu zählen.

Erweiterungsmöglichkeiten

Dein Assembler kann um Funktionen wie Fehlerbehandlung, Optimierungen oder eine interaktive Oberfläche erweitert werden. Denkbar ist auch die Integration in eine IDE – ähnlich wie KI-gestützte Code-Vervollständigung. Diese Erweiterungen zeigen ein tiefes Verständnis und können in Projekten oder im Berufsleben nützlich sein.

Zusammenfassung

Ein E20-Assembler in Java zu schreiben, ist eine hervorragende Übung, um die Verbindung zwischen Hochsprache und Maschinensprache zu verstehen. Mit einer klaren Struktur aus Parser, Symboltabelle und Kodierung kannst du die Aufgabe erfolgreich meistern. Denke daran: Der Schlüssel liegt im Verständnis des Befehlssatzes und in sorgfältigem Testen. Viel Erfolg!