Programming lesson
ISA Assembler Design in CDA 4205L: Vom Assembler-Code zur Maschinensprache (Teil 2)
Erfahren Sie, wie Sie mit Python in JupyterLab einen Assembler entwickeln, der vorverarbeiteten RISC-V-Assembler-Code in Maschinencode umwandelt. Schritt-für-Schritt-Anleitung für Lab #4 mit praxisnahen Beispielen.
Einleitung: Der Weg vom Assembler zur Maschinensprache
Im zweiten Teil des CDA 4205L Lab #4 dreht sich alles um die finale Übersetzung von vorverarbeitetem Assembler-Code in Maschinencode. Nachdem Sie im ersten Teil die Assembler-Anweisungen bereinigt und formatiert haben, geht es nun darum, diese in binäre Maschinenbefehle umzuwandeln – die Sprache, die der Prozessor direkt versteht. Dieser Schritt ist entscheidend, um aus einer .asm-Datei eine ausführbare .bin-Datei zu erzeugen.
Stellen Sie sich vor, Sie programmieren einen KI-Assistenten, der auf einem RISC-V-Chip läuft. Jeder Befehl, den Sie in Assembler schreiben, muss präzise in Nullen und Einsen übersetzt werden, damit der Chip die gewünschte Aktion ausführt. Genau das lernen Sie in diesem Lab: die Funktionsweise eines Assemblers, der als Teil einer Toolchain arbeitet – ähnlich wie Compiler und Linker.
Die Grundlagen: Was ist ein Assembler?
Ein Assembler ist ein Programm, das menschenlesbaren Assembler-Code in Maschinencode umwandelt. Im Kontext von RISC-V (RV32IM) müssen Sie die verschiedenen Befehlstypen (R, I, S, B, U, J) erkennen und deren Felder (Opcode, Funct3, Funct7, Registeradressen, Immediates) korrekt extrahieren und binär codieren. Die Referenzdatei rv32im_isa.csv enthält die Codierungstabelle für alle unterstützten Befehle.
Ein aktuelles Beispiel: Wenn Sie einen KI-Algorithmus für eine Bilderkennungs-App optimieren, könnte ein einziger Assembler-Befehl wie lw x10, 0(x5) (Load Word) dafür sorgen, dass ein Pixelwert aus dem Speicher geladen wird. Ohne korrekte binäre Codierung würde die App abstürzen. Das zeigt, wie wichtig Präzision im Assembler-Design ist.
Vorbereitung: Die benötigten Dateien und Tools
Für das Lab benötigen Sie:
- JupyterLab (online unter https://jupyter.org/try-jupyter/lab/)
- Die Datei
Lab4_assembler_design.ipynbaus Canvas - Die Testdateien
example1_out1.txtundexample2_out1.txtsowie die ISA-Referenzrv32im_isa.csv
Laden Sie alle Dateien in dasselbe Verzeichnis in JupyterLab. Das Notebook enthält zwei Aufgaben (Tasks 1 und 2), die Sie nacheinander bearbeiten.
Aufgabe 1 (Task 1): Einlesen und Parsen der formatierten Assembler-Datei
Zunächst müssen Sie die vorverarbeiteten Assembler-Zeilen aus der .txt-Datei einlesen. Jede Zeile enthält einen Befehl im Format: Instruktion rs1, rs2, rd, immediate (je nach Typ). Ihr Code soll diese Zeilen parsen und die einzelnen Komponenten identifizieren.
# Beispiel für das Einlesen
with open('example1_out1.txt', 'r') as f:
lines = f.readlines()
for line in lines:
# Entferne Zeilenumbrüche und teile auf
parts = line.strip().split()
# parts[0] = Befehl, z.B. 'lw'
# weitere Teile: Register und Immediate
Nutzen Sie die rv32im_isa.csv, um für jeden Befehl den Opcode, Funct3, Funct7 usw. zu ermitteln. Diese CSV-Datei enthält Spalten wie instruction, opcode, funct3, funct7, type. Laden Sie sie mit pandas.read_csv() oder als Dictionary.
import csv
isa = {}
with open('rv32im_isa.csv', 'r') as f:
reader = csv.DictReader(f)
for row in reader:
isa[row['instruction']] = row
Aufgabe 2 (Task 2): Konvertierung in Maschinencode
Jetzt kommt der Kern: Für jede Assembler-Zeile müssen Sie die 32-Bit-Maschineninstruktion binär codieren. Die Felder werden je nach Befehlstyp an bestimmten Bitpositionen angeordnet. Die folgende Tabelle zeigt die Felder für R-Typ-Befehle (z.B. add, sub):
- Bits 31-25: funct7 (7 Bit)
- Bits 24-20: rs2 (5 Bit)
- Bits 19-15: rs1 (5 Bit)
- Bits 14-12: funct3 (3 Bit)
- Bits 11-7: rd (5 Bit)
- Bits 6-0: opcode (7 Bit)
Für I-Typ-Befehle (z.B. addi, lw) sieht das Layout anders aus: Immediate (12 Bit) an den oberen Bits, dann rs1, funct3, rd, opcode. Die genauen Layouts finden Sie in der RISC-V-Spezifikation oder der CSV-Datei.
Ein Beispiel: Der Befehl lw x10, 0(x5) (I-Typ) wird zu:
opcode = 0000011 (load)
funct3 = 010
rs1 = 00101 (x5)
rd = 01010 (x10)
immediate = 000000000000
Binär: 000000000000 01010 010 00101 0000011
-> 0x0000A283 (hexadezimal)
Implementieren Sie eine Funktion encode_instruction(parts, isa), die die Maschinencode-Zahl (Integer) zurückgibt. Nutzen Sie Bit-Shifts und OR-Operationen.
def encode_instruction(parts, isa):
instr = parts[0]
info = isa[instr]
typ = info['type']
# ... Felder extrahieren und zusammensetzen
return machine_code
Schreiben Sie dann die codierten 32-Bit-Werte als Binärdatei (.bin) mit struct.pack('>I', code) oder einfach als Text mit Nullen und Einsen.
Die Funktion get_2c_binary: Zweierkomplement-Darstellung
In Aufgabe 2 werden Sie vermutlich eine Hilfsfunktion get_2c_binary(value, bits) benötigen, um negative Immediates korrekt darzustellen. Die Zweierkomplement-Darstellung ist essenziell für die binäre Codierung von negativen Zahlen in B-Feldern (z.B. bei Sprungbefehlen).
def get_2c_binary(value, bits):
if value < 0:
value = (1 << bits) + value
return format(value, '0{}b'.format(bits))
Diese Funktion stellt sicher, dass z.B. -4 als 111100 (6 Bit) codiert wird, was dem Prozessor erlaubt, rückwärts zu springen – vergleichbar mit einem „Rückgängig“-Button in einer App, der auf eine vorherige Aktion verweist.
Fehlerbehandlung und Tests
Testen Sie Ihren Code mit den bereitgestellten Beispielen. Die Ausgabe für example1_out1.txt sollte wie im Lab-Text aussehen. Vergleichen Sie Ihre Binärausgabe mit den erwarteten Werten. Häufige Fehlerquellen sind:
- Falsche Bitbreite für Immediate (z.B. 12 Bit statt 20 Bit bei U-Typ)
- Verwechseln von rs1 und rs2 bei S-Typ-Befehlen
- Vergessen des Funct3 bei I-Typ-Befehlen
Nutzen Sie Debug-Ausgaben, um die Felder zu prüfen. Ein guter Tipp: Schreiben Sie eine Funktion, die den Maschinencode als Hex-String ausgibt, um ihn mit erwarteten Werten zu vergleichen.
Zusatzfragen (T3–T6) verstehen und beantworten
Im Lab müssen Sie auch Fragen schriftlich beantworten. Hier eine Kurzanleitung:
T3: Wie wird die rv32im_isa.csv verwendet?
Die CSV-Datei dient als Nachschlagewerk: Für jeden Befehl speichert sie Opcode, Funct3, Funct7 und den Befehlstyp. Ihr Code liest diese Tabelle ein und nutzt sie, um die Felder für die binäre Codierung zu bestimmen. Ohne diese Referenz müssten Sie die Werte hartcodieren, was fehleranfällig ist.
T4: Zweck der get_2c_binary-Funktion
Wie oben erklärt, wandelt diese Funktion negative Zahlen in ihre Zweierkomplement-Darstellung um. Dies ist notwendig, da Immediates in RISC-V vorzeichenbehaftet sind und der Prozessor negative Werte als Zweierkomplement interpretiert.
T6: Wie viele Befehlstypen und Gemeinsamkeiten?
Sie müssen sechs Formate unterscheiden: R, I, S, B, U, J. Gemeinsam ist allen, dass die unteren 7 Bits (Opcode) den Befehlstyp identifizieren. Die oberen Bits variieren je nach Typ. In Ihrem Code bedeutet das, dass Sie für jeden Typ eine separate Verarbeitungslogik benötigen, aber der grundlegende Ablauf (Felder auslesen, zusammensetzen) bleibt gleich.
T5: Zweck der Vorverarbeitung in Teil 1
Im ersten Lab haben Sie den Assembler-Code bereinigt: Kommentare entfernt, Labels aufgelöst und Pseudoinstruktionen in echte Befehle umgewandelt. Diese Vorverarbeitung vereinfacht die Codierung, da Sie nun nur noch standardisierte Befehle ohne syntaktische Abweichungen parsen müssen.
Praktische Tipps für die Abgabe
- Führen Sie Ihr Notebook Schritt für Schritt aus und dokumentieren Sie Zwischenergebnisse.
- Machen Sie Screenshots der Ausgaben für
example2_out1.txt– sowohl von Task 1 als auch Task 2. - Erstellen Sie einen abschließenden Report als PDF mit allen Antworten und Screenshots.
- Packen Sie das .ipynb, die .bin-Datei (für example2) und den Report in ein ZIP-Archiv.
Fazit
Mit diesem Lab haben Sie einen voll funktionsfähigen Assembler für RISC-V implementiert. Sie verstehen nun, wie aus menschenlesbarem Code binäre Maschinenbefehle werden – eine Fähigkeit, die in eingebetteten Systemen, KI-Chips und modernen Prozessoren unerlässlich ist. Die gleichen Prinzipien finden Sie in Compilern und Linkern wieder, die komplexe Programme in ausführbare Dateien verwandeln.
Denken Sie daran: Jeder Befehl, den Sie codieren, könnte Teil einer App sein, die auf Ihrem Smartphone läuft, oder eines KI-Modells, das in der Cloud trainiert wird. Die Präzision, die Sie hier lernen, ist die Grundlage für zuverlässige Software.
„Ein Assembler ist die Brücke zwischen menschlichem Denken und maschineller Ausführung – bauen Sie sie sorgfältig.“
Viel Erfolg bei der Abgabe Ihres Labs!