36,90 €
Niedrigster Preis in 30 Tagen: 36,90 €
Einstieg in die Assembler-Programmierung und RISC-V - Von den Grundlagen der Assembler-Programmierung bis zu verfeinerten Anwendungsmöglichkeiten - Die gängigsten RISC-V-Befehle und das Prozessor-Model - Umsetzung von höheren Assembler-Strukturen (Schleifen, Stapel, Sprungtabellen, Rekursion, ... etc) in effektivem RISC-V-Code Assembler-Programmierung ist mehr als nur eine Pflichtübung während der Ausbildung zum Developer. Erfahre, wie du im Code die schnellste Schleife herausarbeitest und setze dabei den Befehlssatz RISC-V ein. Im ersten Teil bietet dieses Buch einen Überblick zu den Grundlagen, über Prozessoren, die benötigten Werkzeuge und natürlich Assembler. Allgemeines Wissen über die Programmierung reicht aus, Vorkenntnisse zu Assembler oder spezifischen Hochsprachen wie C sind nicht nötig. Wir nutzen dabei den offenen Prozessor-Standard RISC-V, der auch gezielt für Forschung und Lehre entwickelt wurde. Das macht die Sache für alle einfacher, denn der Kern-Befehlssatz, den wir hier vorstellen, umfasst weniger als 50 Instruktionen. Noch besser: Wer RISC-V lernt, lernt fürs Leben, denn der Befehlssatz ist »eingefroren« und ändert sich nicht mehr. Für alle, die speziell RISC-V-Assembler-Programmierung lernen wollen, gehen wir im Mittelteil den Aufbau des Prozessors durch, wobei der Schwerpunkt auf der Software liegt. Wir stellen die einzelnen Befehle vor, warnen vor Fallstricken und verraten Tricks. Die Schwachstellen des Standards werden beleuchtet und der Einsatz von KI als Hilfsmittel besprochen. Als offener, freier Standard wird RISC-V auchzunehmend für Hobby- und Studentenprojekte eingesetzt, wo der Compiler nur schlecht oder gar nicht an die Hardware angepasst ist, falls es überhaupt einen gibt. Der letzte Teil zeigt, dass dieses Buch auch aus schierer Begeisterung für Assembler heraus entstand. Wer sich diebisch über jedes eingesparte Byte freut, wird es lieben.
Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:
Seitenzahl: 332
Scot W. Stevenson programmiert seit den Tagen von Acht-Bit-Prozessoren, wie dem 6502 in Assembler. Vom Bytegeschiebe konnten ihn weder sein Medizinstudium, ein Graduiertenkolleg Journalismus, mehr als zwei Jahrzehnte als Nachrichtenredakteur noch ein Blog über die USA abbringen. Er behauptet trotzdem, jederzeit damit aufhören zu können.
Copyright und Urheberrechte:Die durch die dpunkt.verlag GmbH vertriebenen digitalen Inhalte sind urheberrechtlich geschützt. Der Nutzer verpflichtet sich, die Urheberrechte anzuerkennen und einzuhalten. Es werden keine Urheber-, Nutzungs- und sonstigen Schutzrechte an den Inhalten auf den Nutzer übertragen. Der Nutzer ist nur berechtigt, den abgerufenen Inhalt zu eigenen Zwecken zu nutzen. Er ist nicht berechtigt, den Inhalt im Internet, in Intranets, in Extranets oder sonst wie Dritten zur Verwertung zur Verfügung zu stellen. Eine öffentliche Wiedergabe oder sonstige Weiterveröffentlichung und eine gewerbliche Vervielfältigung der Inhalte wird ausdrücklich ausgeschlossen. Der Nutzer darf Urheberrechtsvermerke, Markenzeichen und andere Rechtsvorbehalte im abgerufenen Inhalt nicht entfernen.
Scot W. Stevenson
RISC-V spielerisch und fundiert lernen
Scot W. Stevenson
Lektorat: Gabriel Neumann, Julia Griebel
Lektoratsassistenz: Friederike Demmig
Copy-Editing: Annette Schwarz, Ditzingen
Satz: III-satz, www.drei-satz.de
Herstellung: Stefanie Weidner, Frank Heidt
Umschlaggestaltung: Eva Hepper, Silke Braun
Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
ISBN:
978-3-98889-007-8
978-3-98890-155-2
ePub
978-3-98890-156-9
1. Auflage 2024
Copyright © 2024 dpunkt.verlag GmbH
Wieblinger Weg 17
69123 Heidelberg
Schreiben Sie uns:Falls Sie Anregungen, Wünsche und Kommentare haben, lassen Sie es uns wissen: [email protected].
Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.
Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.
Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.
For my parents, for curiosity endlessly encouraged
Vorwort
Thema
Teil IGrundlagen
1Das Abstrakte
2Hardware
3Code
4Umgebung
5Konzepte
Teil IIRISC-V
6Basen, Module und Profile
7Register
8Adressierungsarten
9Speicher
10Der Befehlssatz
11Die Wahrheit
Teil IIIVertiefung
12Effizienter Code
13Systemaufrufe und Bibliotheken
14Stapel
15Sprünge und Verzweigungen
16Schleifen
17Daten
18Mathe
19Künstliche Intelligenz
Teil IVProjekte
20Eine minimale Eingabeschleife
21Eine größere REPL
Teil VAnhang
22Häufige Fehler
23Stilfibel
24Danksagungen
25Literatur
Index
Vorwort
Thema
Teil IGrundlagen
1Das Abstrakte
1.1Zahlen
1.1.1Addition von Binärzahlen
1.1.2Ein Vorgriff auf die Wortbreite
1.1.3Vorzeichen und Zweierkomplement
1.1.4Vorzeichenerweiterung
1.2Zeichen
1.2.1ASCII
1.2.2Besser als ASCII
1.2.3Zeilenende
1.2.4Stringformate
2Hardware
2.1Ein Überblick
2.2Der Prozessor
2.2.1Aufbau
2.2.2CISC vs. RISC
2.2.3Register
2.2.4Programmzähler
2.2.5Weitere Hardwarebegriffe
2.3Der Arbeitsspeicher
2.3.1Noch mal zur Wortbreite
2.3.2Speicherarchitekturen
2.3.3Ein- und Ausgabe über Adressen
2.3.4Adressierungsarten
3Code
3.1Noch mehr Geschichte
3.2Maschinensprache
3.3Assembler
3.3.1Opcode und Operand
4Umgebung
4.1Werkzeuge
4.2Künstliche Intelligenz
4.3Quellcode
4.3.1Sektionen
4.3.2Datenarten
4.3.3Definitionen
5Konzepte
5.1Sprünge
5.2Verzweigungen
5.3Schleifen
5.4Stapel
5.5Subroutinen
5.6Byte-Reihenfolge
5.6.1Umgekehrte Polnische Notation (UPN)
5.7Cache
5.8Pipeline
Teil IIRISC-V
6Basen, Module und Profile
6.1Der Kleine: RV32E
7Register
7.1Die besonderen Fünf
7.2Doppelbelegungen
7.3Vogelfreie und geschützte Register
7.4Register bei RV32E
7.5Register-Synonyme
8Adressierungsarten
9Speicher
9.1Ausrichtung
10Der Befehlssatz
10.1Laden und speichern
10.1.1Load Immediate li
10.1.2Load Address la
10.1.3Move mv
10.1.4Laden aus dem Speicher
10.1.5Kampf der Vorzeichenerweiterung
10.1.6Speicherbefehle
10.2Mathe und Logik
10.2.1Addition und Subtraktion
10.2.2Multiplikation und Division
10.2.3Division
10.2.4Das Modul M bei RV64
10.2.5Logikbefehle
10.2.6Schiebebefehle
10.3Sprünge und Verzweigungen
10.3.1Nur hin: einfache Sprünge
10.3.2There and back again: Subroutinen
10.3.3Verzweigungen
10.4Vergleichen und setzen
10.5Reste
10.5.1Systembefehle
10.5.2Kontroll- und Status-Register
10.5.3Leerbefehl
10.5.4Speicherzugriffe
10.5.5Besondere RV64- und RV128-Befehle
10.5.6Illegale Befehle
10.6Komprimierte Befehle
11Die Wahrheit
11.1Register nach der roten Pille
11.2Mikroanatomie der Befehle
11.2.1Genaueres zur Befehlslänge
11.3Pseudo-Befehle
11.3.1Ladebefehle
11.3.2Verzweigungen
11.3.3Sprungbefehle
11.3.4Weitere Pseudo-Befehle
11.4Macro-Op-Fusion
11.5Zurück ans Licht
Teil IIIVertiefung
12Effizienter Code
12.1Das Ziel
12.2Verfahren
12.2.1Strength Reduction
12.2.2Inlining
12.2.3Verzweigungen ersetzen
12.2.4Parallele Befehlsausführung
12.2.5Entkopplung
12.2.6Das große Bild lässt die Kirche im Dorf
13Systemaufrufe und Bibliotheken
13.1Systemaufrufe
13.1.1Systemaufrufe bei RARS
13.1.2Systemaufrufe bei Linux
13.1.3Von x86 zu RISC-V
13.2Die C-Bibliothek
13.2.1Ausgabe: puts
13.2.2Ausgabe: printf
13.2.3Eingabe: scanf
13.2.4Umwandlung: strtol
13.2.5Und tschüss: exit
13.3Schummeln macht klug
13.3.1Schummeln mit C
13.3.2Schummeln mit Rust
13.3.3Noch viel mehr schummeln
14Stapel
14.1Stapelnutzung im Alltag
14.1.1I love my sweet 16 stack pointer
14.2Die normative Kraft des C-Moduls
14.2.1Einschub: Die Wahrheit hinter der Wahrheit
14.3Eigene Stapel
14.4Gefahr im Überfluss
15Sprünge und Verzweigungen
15.1Mehrfachverzweigungen
15.1.1Verzweigungsketten
15.1.2Sprungtabellen
15.1.3Dispatch-Tabellen
15.1.4Ein Sonderfall am Rande der Welt
15.2Tail Calls
15.3Funktionsaufrufe
15.4Millicode
15.5Datenübergabe an Subroutinen
15.5.1Das Anhalter-Bit
15.5.2Fragwürdig bis verboten: Datenübergabe im Code
16Schleifen
16.1Allgemeines zu Schleifen
16.2Basis + Index vs. Zeiger
16.2.1Und nun ein kurzer Rant über das Fehlen eines Indexmodus
16.3Techniken für effektive Schleifen
16.3.1Ausrollen der Schleife
16.3.2Eine volle Breitseite Wortbreite
16.3.3Der Sprung in die ausgerollte Schleife
16.3.4Immutables müssen draußen warten
16.3.5Schleifen zusammenfassen
16.3.6Cache as Cache can
16.3.7Unroll and Jam
16.3.8Ein Schritt zurück
16.3.9Der Lohn der Verschwendung
16.3.10Fallbeispiel: Die beste Schleife ist keine
16.4Rekursion
16.4.1Ein heiterer Ausflug zu den wirklich großen Zahlen
17Daten
17.1Strings mit fixierter Länge
17.2Stringtabellen
17.3Bitfelder
18Mathe
18.1Überläufe, Überträge und andere Katastrophen
18.1.1Übertrag bei Addition ohne Vorzeichen
18.1.2Überlauf bei Addition mit Vorzeichen
18.1.3Überlauf bei der Subtraktion ohne Vorzeichen
18.2(M)ultiplikation
18.2.1Schieben als erste Wahl
18.2.2Addieren in der Schleife
18.2.3Wie in der Schule: Shift-Add
18.3Division
18.3.1Division durch Rechtsverschiebung
18.3.2Division durch Subtraktion
18.3.3Fallbeispiel: FizzBuzz
19Künstliche Intelligenz
19.1Allgemeines
19.2Code-Generierung
19.3Code-Analyse
19.4Andere Anwendungen
Teil IVProjekte
20Eine minimale Eingabeschleife
20.1Projektvorschlag: ed
21Eine größere REPL
21.1Ziele
21.1.1Tellerstapeln (unnötig) schwer gemacht
21.2Der Aufbau, iterativ
21.2.1Schritt I: Nur ein Zeichen
21.2.2Schritt II: Eingabe
21.2.3Schritt III: Parsing (provisorisch)
21.2.4Einschub: Byte-wise but time-foolish
21.2.5Schritt IV: Alle Befehle
21.2.6Schritt V: Dictionary
21.2.7Schritt VI: Parsing (jetzt richtig)
21.2.8Schritt VII: Kommandosuche
21.3Nächste Schritte
21.4Projektvorschlag: Forth
Teil VAnhang
22Häufige Fehler
23Stilfibel
24Danksagungen
24.1Colophon
25Literatur
Index
»But I don’t want to go among mad people,« Alice remarked. »Oh, you ca’n’t help that,« said the Cat: »we’re all mad here. I’m mad. You’re mad.« »How do you know I’m mad?« said Alice. »You must be,« said the Cat, »or you wouldn’t have come here.«
– Lewis Carroll, Alice’s Adventures in Wonderland (1865) in Originalschreibweise
Ein modernes Buch über Assembler-Programmierung, was soll das denn? Macht das heute nicht alles eine Maschine, sei es ein Compiler oder eine KI? Was für Wahnsinnige befassen sich denn noch mit so was?
Nun, einige Leute werden im Studium von gemeinen Menschen dazu gezwungen, sich mit Assembler zu beschäftigen. Aus tiefstem Mitgefühl heraus versprechen wir ihnen: Wir bringen das schnell, schmerzlos und so unterhaltsam wie möglich über die Bühne.
Entsprechend gibt es am Anfang dieses Buches etwas zu den Grundlagen, einen Überblick über Prozessoren, die benötigten Werkzeuge und natürlich Assembler. Allgemeines Wissen über die Programmierung reicht aus, Vorkenntnisse zu Assembler oder spezifischen Hochsprachen wie C sind nicht nötig.
Wir nutzen dabei den offenen Prozessorstandard RISC-V, der auch gezielt für Forschung und Lehre entwickelt wurde. Das macht die Sache für alle einfacher, denn der Kern-Befehlssatz, den wir hier vorstellen, umfasst weniger als 50 Instruktionen. Noch besser: Wer RISC-V lernt, lernt fürs Leben, denn der Befehlssatz ist »eingefroren« und ändert sich nicht mehr.
Das bringt uns zu den Leuten, die speziell RISC-V-Assembler-Programmierung lernen wollen (oder müssen). Für sie gehen wir im Mittelteil den Aufbau des Prozessors durch, wobei der Schwerpunkt auf der Software liegt. Wir stellen die einzelnen Befehle vor, warnen vor Fallstricken und verraten Tricks. Die Schwachstellen des Standards werden gnadenlos beleuchtet.
Dabei verzichten wir zwar keinesfalls auf Compiler oder KI. Aber als offener, freier Standard wird RISC-V zunehmend für Hobby- und Studenten-Projekte eingesetzt, wo der Compiler nur schlecht oder gar nicht an die Hardware angepasst ist, falls es überhaupt einen gibt. Dann muss der Mensch ran, ob mit oder ohne eine künstliche Helferin.
Aber um ehrlich zu sein: Dieses Buch entstand auch aus schierer Begeisterung für Assembler heraus. Wer es liebt, die schnellste Schleife herauszuarbeiten und sich diebisch über jedes eingesparte Byte freut, wird die hinteren Abschnitte kaum abwarten können. Damit ist es am Ende tatsächlich auch ein Buch für die Leute, die vielleicht ein klein wenig wahnsinnig sind. Hier sind sie unter Freunden.
Viel Spaß.
Ein Buch wie dieses steht vor der Herausforderung, unterschiedlichen Lesergruppen gerecht zu werden: von Neulingen an der Universität, die vielleicht zum ersten Mal in die Eingeweide eines Prozessors blicken, bis hin zu Vollprofis, die vermutlich bereits mehr über Assembler vergessen haben, als der Autor jemals wissen wird. Wir werden für die erste Gruppe am Anfang Begriffe erklären und Hintergründe erläutern. Je tiefer wir in die Materie eindringen, desto mehr Wissen setzen wir als bekannt voraus.
Eigentlich sind Computer fürchterliche Geräte. Sie verstehen nur Zahlen, was Menschen langfristig nicht guttut. Zudem gehen sie in winzigen Arbeitsschritten vor, was uns nur deswegen nicht auffällt, weil sie jeden einzelnen dieser Schritte sehr schnell ausführen. Auf dieser untersten, menschenfeindlichen Ebene der Programmierung sprechen wir von der »Maschinensprache«, auf Englisch machine language.
Um die Arbeit mit Computern für Nicht-Freaks erträglich zu machen, wird die Maschinensprache unter einem Berg von Abstraktionen versteckt. Dazu gehören Hochsprachen wie Python oder Rust, die von speziellen Programmen wie Compilern oder Interpretern über Zwischenschritte in Maschinensprache übersetzt werden. Bei einer Low-Code-Entwicklungsumgebung werden grafische Elemente zusammengeklebt, sodass – wenn überhaupt – nur »niedrige« Programmierkenntnisse erforderlich sind. Die gegenwärtig höchste Abstraktionsstufe sind die Anweisungen an eine Künstliche Intelligenz (KI), die »Prompts«.
»Assembler« ist in diesem Modell die erste Abstraktionsebene nach der Maschinensprache. Das Wort kommt von dem englischen Verb to assemble, hier im Sinne von »aus Einzelteilen zusammensetzen« und nicht von »Menschen sammeln sich« wie der Schlachtruf Avengers assemble! bei Marvel. Vereinfacht gesagt wird dabei jeder Maschinenbefehl 1:1 durch einen Assembler-Befehl abgebildet, der aus mehr oder weniger leicht zu merkenden Abkürzungen besteht. Das Problem der winzigen Arbeitsschritte bleibt. Der Katalog aller Befehle eines Prozessors ist sein »Befehlssatz«, auf Englisch instruction set.
Da jede Prozessorfamilie eine andere Maschinensprache mitbringt, die sich zum Teil auch noch von Prozessormodell zu Prozessormodell unterscheidet, gibt es viele Varianten von Assembler. Wir sprechen von dem jeweiligen Befehlssatz eines Modells oder einer Chip-Familie, der instruction set architecture (ISA). Neben dem RISC-V-Befehlssatz gibt es etwa den x86 bei Intel und AMD bei klassischen PCs sowie die Arm-Befehlssätze insbesondere bei mobilen Geräten.
Dieses Programm hätte die traumhafte Laufzeit von 32 Millisekunden, aber den alptraumhaften Speicherbedarf von 24 KByte.
– Peter-Mattias Oden, Grafik-Tuning. Schneller Bildaufbau mit 6502-Prozessoren am Beispiel des Apple II (1984)
Als Erfinderin von Assembler gilt Kathleen Booth vom Birkbeck College in London – das war 1947. [BK] Zwar wurde sogar noch die erste Version des IBM-Mainframe-Betriebssystems OS/360 1966 in Assembler geschrieben. Allerdings ging es dann wegen des Aufstiegs von höheren Sprachen wie Fortran, Lisp und Cobol bergab. Assembler galt als tot. [SD]
Der Aufstieg der Acht-Bit-Computer auf der Grundlage von Prozessoren wie dem 6502 oder Z80 Mitte der 70er Jahre brachte eine Auferstehung. [SD] Zunächst gab es keine Compiler für diese Mikroprozessoren (MPU). Wer die maximale Leistung aus der Kiste holen wollte, kam an Assembler nicht vorbei. Computerzeitschriften wie c’t waren in den 80er Jahren entsprechend noch ein Stück mehr hardcore als heute und muteten ihren Lesern seitenweise Listings von Assembler-Mathematik und -Grafikcode zu. Assembler auf diesen Prozessoren machte einfach Spaß, nicht zuletzt weil die Befehlssätze für Menschen geschrieben wurden.
Mit einer größeren Leistungsfähigkeit der Rechner wurden allerdings auch die Compiler immer besser. Der Aufwand, einen schnelleren Code per Hand zu schreiben, lohnte sich immer weniger. Zudem traten die x86-Prozessoren ihre jahrzehntelange Dominanz an mit riesigen, barocken Befehlssätzen, die für Normalsterbliche kaum zu durchdringen waren und dazu noch mit Nutzungseinschränkungen bewehrt sind. Assembler wurde bestenfalls für Bootloader verwendet, und wer das machen musste, tat es meistens fluchend. Die Befehlssätze werden seitdem und bis heute für Compiler entworfen. Wenn es um die Praxis ging, schien Assembler tot, schon wieder.
I could skip the middleman and talk right to the machine. (…) I could talk to God, just like IBM. [TK]
– Tracy Kidder, The Soul of a New Machine (1981)
Vermutlich sollte an dieser Stelle eine flammende Rede für den Nutzen von Grundwissen über Assembler in der Computerwissenschaft stehen – dass erst damit klar wird, wie die Maschine auf der untersten Ebene funktioniert, dass dieses Wissen für den Compilerbau unabdingbar ist, dass es bei der Optimierung von Code helfen kann. Das ist auch alles wahr.
Allerdings müssen wir der Realität ins Auge sehen: Eine Menge Leute lernen im 21. Jahrhundert Assembler nur, weil es Teil eines Pflichtkurses an der Uni ist. Deswegen ist dieses Buch erstens kurz und zweitens befasst es sich mit RISC-V. Wie wir gleich ausführlicher sehen werden, geht dieser Befehlssatz auf frustrierte Informatik-Dozenten zurück, die unter anderem ein besseres Werkzeug für die Lehre haben wollten. Der minimalistische Kern-Befehlssatz kann selbst den desinteressierten Massen im Grundkurs zugemutet werden, die danach nie wieder irgendwas mit Assembler zu tun haben (wollen).
Profis werden dagegen vermutlich die ersten Teile des Buches überspringen und sich auf RISC-V konzentrieren wollen. Es gibt immer noch Situationen, wo es Assembler sein muss, zum Beispiel die Portierung von Betriebssystemen auf neue Prozessoren. Der langsame, aber stetige Aufstieg von RISC-V in der Industrie zwingt mehr Leute dazu, sich mit diesem Neuling zu beschäftigen. Diese Teile des Buches sind daher auch zum Nachschlagen gedacht und absichtlich etwas nüchterner geschrieben (wenn auch nicht viel). Profis haben ja keine Zeit.
Langfristig sollten diese beiden Lesergruppen deckungsgleich werden: Ein Ziel von RISC-V ist, dass im Studium derselbe Befehlssatz gelehrt wird, der auch in der Praxis verwendet wird. Die Neueinstellungen würden dann nützliches, sofort einsetzbares Wissen von der Uni mitbringen, was einem kleinen Wunder gleichkäme.
Hobby-Coder als dritte Gruppe sind dagegen nicht nur die Spiel-, sondern auch die Glückskinder der Programmierwelt. Sie können tun, was sie wollen, in welcher Sprache sie es wollen, so lange, wie sie es wollen. Ihre Projekte können den praktischen Nutzen eines überfahrenen Stinktiers haben. Assembler coden sie entsprechend zum Spaß, auch wenn ihnen besorgte Bekannte T-Shirts schenken mit Schriftzügen wie »Hauptsache, es tut weh«.
Der große Feind des Hobby-Coders sind Updates und neue Features. Neben Familie und Job und was sonst noch im Leben wirklich wichtig ist bleibt ein Projekt manchmal so lange liegen, bis sich Staub auf der Tastatur sammelt. Wer sich dann erst in neue Frameworks, Funktionen oder Updates reinfuchsen muss, kommt weniger zum Programmieren. Lizenzbedingungen können ein weiteres Problem sein. Es ist daher kein Wunder, dass sich viele Freizeit-Assembler-Fans in die Retro-Programmierung etwa des 6502 aus dem Acht-Bit-Zeitalter geflüchtet haben. RISC-V macht jetzt damit Schluss: Der Befehlssatz ist nicht nur kurz, sondern auch »eingefroren« und ändert sich nicht wieder.
Für diese Gruppe ist insbesondere der dritte und letzte Teil des Buches gedacht. Die dort vorgestellten Routinen, Verfahren und Programme würden von vernünftigen Menschen in einer Hochsprache geschrieben (oder gar nicht), der Stoff bietet jedoch tiefere Einblicke in die Assembler-Welt. Für die ganz Harten diskutieren wir schließlich am Ende noch weitergehende Projekte, die zu umfangreich für dieses Buch wären. An ihnen dürften nur noch zwei Gruppen Freude haben: sadistische Dozenten und masochistische Hobby-Coder. T-Shirts für alle!
The most pervasive change in this edition is switching from MIPS to the RISC-V instruction set. We suspect this modern, modular, open instruction set may become a significant force in the information technology industry. It may become as important in computer architecture as Linux is for operating systems.
– Hennessy und Patterson, Computer Architecture:A Quantitative Approach (2019)
RISC-V wird auf Englisch »risk five« ausgesprochen und als »Risk fünf« eingedeutscht, auch wenn einige KIs gegenwärtig noch auf »risk vee« bestehen. Der erste Teil des Namens kommt von Reduced Instruction Set Computer und bezeichnet Prozessoren, die über vergleichsweise wenige Befehle verfügen, aber diese sehr schnell ausführen. Dabei wird etwas mehr Code benötigt als bei einem Complex Instruction Set Computer (CISC). Die römische Ziffer V verweist darauf, dass es der fünfte Anlauf der Erfinder ist. Das RISC-V-Projekt ist vergleichsweise jung, in der heutigen Form nahm es 2010 seinen Anfang. Über die Einhaltung der Standards wacht seit 2020 die Stiftung RISC-V International mit Sitz in der Schweiz.
Als Befehlssatzdefinition existiert RISC-V eigentlich nur auf dem Papier. Sie besteht aus einer Spezifikation, die nichts darüber aussagt, wie der Prozessor die Befehle umsetzt. Ob konventionelle Hardware wie Logikgatter, elektromagnetische Relaistechnik wie zu Zeiten von Konrad Zuse oder dressierte Hamster in speziellen Laufrädern, alles ist möglich.
Unter uns gesagt: Der Befehlssatz an sich ist nicht fürchterlich aufregend und schon gar nicht revolutionär. Wer sich bereits mit der ISA von anderen RISC-Prozessoren beschäftigt hat, wird vieles wiedererkennen. Vielmehr zeichnen RISC-V zwei Dinge aus:
Erstens, der Standard ist »offen« oder »frei«, denn die Spezifikation unterliegt einer Creative-Commons-Lizenz. Damit kann jeder selbst RISC-V-Prozessoren bauen, ob als Bastelfreak im Hobbykeller, multinationaler Konzern mit eigener Chip-Fertigung oder Verein für ambitionierte Hamster-Trainer. Forschung und Lehre sind keine Grenzen gesetzt, Unternehmen müssen keine Lizenzgebühren bezahlen und Freizeit-Coder bekommen keine Auflagen aufgedrückt.
Zweitens, es handelt sich um einen »modularen« Standard. Während der x86-Befehlssatz durch sein unablässiges Wachstum inzwischen bei einer vierstelligen Zahl von Instruktionen angekommen ist, werden die RISC-V-Befehle in »Module« verpackt. [HS] Diese werden nach eingehender Prüfung »eingefroren« (frozen) und nie wieder verändert. Es gibt ein Basismodul I, das alle RISC-V-Prozessoren haben müssen. Darüber hinaus entscheidet jede Chip-Schmiede und jeder Hamster-Trainer selbst, welche Module sie benötigen.
So weit ein erster Überblick. Leider kommt kein Buch ohne Bürokratie aus. Bringen wir sie schnell hinter uns.
Deutsch im Code sagt dem Leser auf den ersten Blick: Hier hat jemand nur für sich selbst programmiert, ohne damit zu rechnen, dass sich jemals jemand anders für den Code interessieren könnte. Das tun überwiegend Anfänger, also ist der Code wahrscheinlich nicht besonders gut.
– Passig und Jander, Weniger schlecht programmieren (2013)
Jedes deutschsprachige Buch über Computer muss damit klarkommen, dass Englisch die Weltsprache der Informatik ist. Besonders bei RISC-V liegt bislang viel Literatur nur auf Englisch vor. Früher oder später kommt niemand daran vorbei. Der Einsatz von Künstlicher Intelligenz verstärkt diesen Effekt nur, weil die Modelle gegenwärtig deutlich besser mit Englisch zurechtkommen als mit Deutsch. Sorry.
Die gute Nachricht ist, dass die erforderlichen Englischkenntnisse eher auf der Sprachebene von Friends liegen als von William Shakespeare. Auch Englischmuffel kommen mit etwas Übung klar. Wir führen am Anfang die englischen Fachbegriffe ein und setzen sie dann nach und nach als bekannt voraus. Auch die Kommentare in den Quelltexten (source code) sind irgendwann durchgängig auf Englisch, weil das der Situation in der wirklichen Welt entspricht.
Ein Ziel dieses Buches ist es, möglichst viele gut lesbare Code-Beispiele zur Verfügung zu stellen. Dabei steht insbesondere am Anfang die Klarheit des Designs im Vordergrund. Tricks, um ein Maximum an Leistung oder den kürzesten Code herauszukitzeln, führen wir erst ein, wenn das Grundprinzip klar ist. Die Programme sind ausführlich kommentiert. Wer schon mal versucht hat, fremden Assembler-Code zu lesen – oder nach einigen Wochen den eigenen –, weiß, warum. Ein Kommentar pro Zeile wird keine Seltenheit sein.
Eine historisch gewachsene Unsitte bei Assembler ist die Verwendung von sehr kurzen Namen oder gar einzelnen Buchstaben für Variablen und Sprungmarken (label). Dafür gibt es im 21. Jahrhundert keine Entschuldigung, wir verwenden lange Namen. Konstanten werden in VERSALIEN geschrieben, auch wenn es der Maschine egal ist.
Die Grobstruktur von Routinen wird meist so aussehen, dass wir ganz oben einsteigen und ganz unten wieder rausgehen. Anders formuliert soll jede Routine möglichst immer nur einen Eingang und einen Ausgang haben. Im Rahmen des defensive programming bauen wir hin und wieder Code ein, der nur dazu dient, das Programm robuster zu machen. Entsprechende Stellen markieren wir in den Kommentaren als paranoid. Wir sagten ja bereits, hier sind Wahnsinnige am Werk.
Die Beispielprogramme wurden entweder auf dem RARS-Simulator (https://github.com/TheThirdOne/rars) oder mit QEMU unter Ubuntu Linux mit dem GCC Compiler (https://gcc.gnu.org/) getestet. RARS ist der einfachere Weg. Das Programm wird als ausführbare jar-Datei bereitgestellt und müsste auf ziemlich jedem Betriebssystem mit
java -jar <DATEI>
von der Kommendozeile aus ausführbar sein.
GCC ist dafür deutlich mächtiger. Ubuntu bietet für QEMU ein vorgefertigtes Image unter https://wiki.ubuntu.com/RISC-V an, das ein komplettes RISC-V-System emuliert. Das heißt, wir können innerhalb dieser Umgebung mit normalen Werkzeugen arbeiten. Für dieses Buch wurde benutzt:
ubuntu-22.04.1-preinstalled-server-riscv64+unmatched.img
Wir rufen die QEMU-Instanz auf mit:
qemu-system-riscv64 \
-machine virt \
-nographic \
-m 2048 \
-smp 4 \
-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
-kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
-device virtio-net-device,netdev=eth0 \
-netdev user,id=eth0,hostfwd=tcp::10022-:22 \
-drive file=<UBUNTU_IMAGE>,format=raw,if=virtio
Wir können uns dann von einem anderen Rechner aus mit ssh-p10022ubuntu@<RECHNER> einloggen.
That said, above all this book tries not to take itself (or anything) too seriously. There is humour here, the difference is that you need to look for it.
– Doug Hoyte, Let Over Lambda (2008)
In diesem Teil geht es um die allgemeinen Grundlagen von Assembler. Außerdem wird hier das Mindestwissen vermittelt, um verstehen zu können, was ganz unten in einem Computer passiert. Wer den Stoff parat hat, kann diesen Teil überspringen.
Wir fangen mit den rein theoretischen Grundlagen an – Zahlen und Zeichen.
There is only one thing inside computers, bits. Lots of them, to be sure but when you understand bits, you understand what’s in there.
– J. Clark Scott, But How Do It Know? (2009)
Auf der untersten Ebene bestehen gängige Computer aus Stromkreisen, in denen es zwei Zustände gibt: Spannung oder keine Spannung. Nehmen wir die 1 für »Spannung« und die 0 für »keine Spannung«, haben wir zwei Ziffern, mit denen wir »Binärzahlen« darstellen können.
Die einzelnen Ziffern dieser Zahlen (binary digits, kurz bit) werden von rechts nach links durchnummeriert, die Zählung beginnt wie in der Computerwissenschaft üblich bei Null. Das bit 0 – ganz rechts – wird als least significant bit (LSB) bezeichnet, ganz links ist entsprechend das most significant bit (MSB). Um klar zu machen, dass es sich bei 100 um die 4 in Binär und nicht die Dezimalzahl 100 handelt, stellen wir ein 0b voran.
Wie kommen solche Zahlen zustande? Machen wir uns klar, wie eine normale Dezimalzahl aufgebaut ist, etwa die 513.792. Hinter dieser Schreibweise steht eigentlich diese Rechnung:
5x105 + 1x104 + 3x103 + 7x102 + 9x101 + 2x100 500.000 + 10.000 + 3.000 + 700 + 90 + 2
Bei Binärzahlen machen wir das genauso, nur mit Zweierpotenzen. Nehmen wir die binäre Darstellung der Dezimalzahl 10, also 0b1010:
1x23 + 0x22 + 1x21 + 0x20 8 + 0 + 2 + 0
So wie wir im Alltag Dezimalziffern in Dreiergruppen schreiben und durch Dezimalpunkte trennen, fassen wir immer vier Binärziffern zusammen. Im Fließtext fügen wir Unterstriche ein, um das leserlicher zu machen.
0b0000_0000_0010_1010
So etwas ist aber bestenfalls unhandlich. Deswegen dampfen wir jeweils vier Binärziffern – ein nibble – in eine Hexdezimalziffer ein. Beim Hexadezimalsystem (kurz, wenn auch eigentlich falsch, »Hex« genannt) werden die Ziffern 0 bis 9 um die Buchstaben A bis F ergänzt, ein 0x wird vorangestellt und es werden ebenfalls Vierergruppen gebildet.
0b0000_0000_0010_1010 -> 0x002A
Bleiben wir bei der Addition. In beiden Fällen ist die Vorgehensweise gleich: Wir haben einen gewissen Satz an Ziffern, also 0 bis 9 im Zehnersystem. Wenn wir eine Summe bilden, »zählen« wir um so viele Stellen in unserer Ziffernreihe weiter. Damit landen wir bei 1 + 5 bei der 6. Wenn uns die Ziffern ausgehen, machen wir links eine neue Stelle auf und fangen wieder von vorne an. Bei 1 + 9 ist es zum Beispiel so weit, weswegen wir mit 10 weitermachen.
Nichts anderes passiert bei der Addition in Zweiersystem, nur dass wir lediglich zwei Ziffern haben und sie uns deswegen sofort ausgehen. Schon bei 0b1 + 0b1 sind wir am Ende und müssen links mit 0b10 eine neue Stelle aufmachen. Aber sonst läuft alles ab wie immer:
0101 5
+ 0011 3
------
Bei der Addition im Hexdezimalsystem greift der umgekehrte Effekt. Wir haben mehr Ziffern zur Verfügung, weswegen nach 9 nicht Schluss ist: Es geht mit A, B, C, D, E, F weiter, bis wir dann kapitulieren und zu 0x10 (dezimal 16) erweitern müssen.
Beim Rechnen auf Papier können wir unsere Zahlen beliebig nach links erweitern. Im Computer ist das nicht so. Prozessoren können nur eine gewisse Anzahl von Bit auf einmal verarbeiten, weil ihre Register eine begrenzte Größe haben, die Wortbreite (word size oder word width). Passt eine Binärzahl nicht in ein Maschinenwort, fallen die überschüssigen Bit weg, und wir haben einen Überlauf (overflow).
Die Wortbreite eines Prozessors ist einer der wichtigsten Faktoren für seine Leistungsfähigkeit, weil sie sagt, wie groß die Zahlen sind, die er in einem Schritt bearbeiten kann – größere Hamster können mehr tragen. Die 8-Bit-Prozessoren der 1980er Jahre wie der 6502 haben also eine Wortbreite von einem Byte.
Drei für alle, alle für drei
Bei RISC-V gibt es drei verschiedene Wortbreiten von 32, 64 oder 128 Bit, weswegen wir von den ISA-Varianten RV32, RV64 und RV128 sprechen. In allen drei Fällen sind die Befehle trotzdem 32 Bit lang.
Während wir bei Dezimalzahlen für negative Werte ein Minus voranstellen, müssen wir bei Binärzahlen spätestens auf der Ebene des Stroms tricksen. Wir greifen dazu auf das Zweierkomplement (two’s complement) zurück. Dabei gehen wir von einer festen Wortbreite aus. Das MSB in diesem Wort signalisiert das Vorzeichen: Bei einer 0 handelt es sich um eine positive, bei einer 1 um eine negative Zahl.
Um von einer positiven zur entsprechenden negativen Zahl zu gelangen, werden zunächst alle Bit invertiert (flipped), dann eine 1 addiert. Am Beispiel der Umwandlung von 6 zu -6 bei einer Wortbreite von vier Bit:
0110 ursprüngliche Zahl 6
1001 invertiert
1010 1 addiert, ergibt -6
Als alternative Methode für die Berechnung per Hand können wir rechts bei dem LSB beginnen und zunächst alle 0 und die erste 1 – aber nur diese – abschreiben. Alle folgenden Binärziffern werden dann invertiert.
Der große Vorteil des Zweierkomplements: Negative und positive Zahlen können normal addiert werden. Aus 6 - 2 wird damit 6 + (-2):
0110 6
+ 1110 -2
----
1_0100 4 (mit Überlauf)
Der Überlauf wird ignoriert, weil er außerhalb der Wortbreite liegt.
Wir haben damit zwei verschiedene Arten, binäre Zahlen bei einer gegebenen Wortbreite darzustellen: ohne Vorzeichen (unsigned), wo wir die gesammte Breite für positive Zahlen nutzen können, und mit Vorzeichen (signed), bei denen die größte positive Zahl etwa halb so groß ist. Damit haben wir bei einem Byte eine Spanne von 0 bis 255 bei Zahlen ohne Vorzeichen und von -128 bis 127 bei Zahlen im Zweierkomplement. Allgemeiner:
Vorzeichen
Spanne
nein (unsigned)
0 bis 2n-1
ja (signed)
-2n-1 bis 2n-1-1
Beim Zweierkomplement entsteht ein Problem, wenn wir etwa eine 8-Bit-Zahl mit einem Vorzeichen auf eine größere Wortbreite übertragen wollen, etwa auf ein 16-Bit-Wort. Bei acht Bit markiert das MSB eine negative Zahl, aber das geht verloren, wenn wir nicht aufpassen:
1111_1110 -2 bei einer Wortbreite von 8 Bit
0000_0000_1111_1110 254 bei einer Wortbreite von 16 Bit
Um das zu verhindern, muss das Vorzeichen »erweitert« werden (sign extension, kurz sext). Dabei wird das MSB der ursprünglichen Zahl in alle darüber liegenden Stellen der neuen Zahl kopiert.
0000_0010 2 bei einer Wortbreite von 8 Bit
0000_0000_0000_0010 2 bei einer Wortbreite von 16 Bit
1111_1110 -2 bei einer Wortbreite von 8 Bit
1111_1111_1111_1110 -2 bei einer Wortbreite von 16 Bit
Das Gegenstück zur Vorzeichenerweiterung ist die Vorzeichenreduzierung (sign contraction). Hier wird eine Binärzahl in eine kleinere Wortbreite gepresst. Während eine Vorzeichenerweiterung stets vorgenommen werden kann, passt eine Zahl nicht immer in eine kleinere Wortbreite, etwa wie die Dezimalzahl 42, die beim besten Willen nicht in eine Dezimalstelle passt.
Eine Reduzierung kann dann vorgenommen werden, wenn die Bit, die gestrichen werden, alle dem obersten Bit in der neuen Wortbreite entsprechen. Anders formuliert, das Vorzeichen muss mindestens mit einem Bit auch in die neue, kleinere Wortbreite passen.
1111_1111_1000_0000 -128 bei einer Wortbreite von 16 Bit
1000_0000 -128 bei einer Wortbreite von 8 Bit
0000_0000_0100_0000 64 bei einer Wortbreite von 16 Bit
0100_0000 64 bei einer Wortbreite von 8 Bit
Die Binärzahl 0000_1100_0000_0000 lässt sich dagegen nicht auf eine Wortbreite von acht Bit reduzieren, da die obersten Bit verschieden sind. Am Ende passt 3.072 einfach nicht in acht Bit.
Die sättigende Bit-Diät
Was tun, wenn trotzdem unbedingt reduziert werden muss und eine Fehlerbehandlung nicht erwünscht ist? Bei der Sättigung (saturation) wird die neue Wortbreite mit der größt- oder kleinstmöglichen Zahl – je nachdem – mit dem richtigen Vorzeichen aufgefüllt. Aus 0000_1100_0000_0000 wird dann 0111_1111. Zwar ist 127 immer noch weit weg von 3.072, aber es geht zumindest in die richtige Richtung.
Bei Binärzahlen gibt es je nach Anzahl der Ziffern verschiedene Begriffe neben »Byte« und »Nibble«. Bei RISC-V haben wir:
Die Einträge für 12 und 20 Bit sind »asymmetrisch« bezogen auf eine Wortbreite von 32 Bit und damit ungewöhnlich. Wir werden nachher sehen, warum diese Breiten bei RISC-V häufig benutzt werden.
Computer verstehen nur Zahlen, Menschen wollen Texte. Wir brauchen eine Möglichkeit, Buchstaben mit Zahlen darzustellen.
Dafür wird bis heute der American Standard Code for Information Interchange (ASCII) von 1963 benutzt, der sieben Bit je Buchstabe verwendet. Damit erhalten wir 128 mögliche Zeichen. Die ersten 32 sind Steuerzeichen (control characters), von denen wir folgende häufiger brauchen:
Die übrigen ASCII-Codes beschrieben ein Gemisch aus Sonderzeichen, Ziffern und Buchstaben. Wir brauchen noch das Leerzeichen (space) mit Dezimal 32 (Hex 0x20). Bei Buchstaben und Ziffern können Spannen benutzt werden, um zu sehen, ob ein Zeichen zu einer gewissen Gruppe gehört:
Zeichenart
Spanne Dezimal
Spanne Hex
Ziffern 0–9
48–57
0x30–0x39
Großbuchstaben A–Z
65–90
0x41–0x5A
Kleinbuchstaben a–z
97–122
0x61–0x7A
Wie wir sehen, liegen diese Regionen dummerweise nicht zusammen. Insbesondere ist es für die Darstellung von Hex-Zahlen ärgerlich, dass die Buchstaben nicht direkt an die Ziffern anschließen. Auch unschön: Die Großbuchstaben kommen vor den Kleinbuchstaben, was die Sortierung schwieriger macht. Es gibt aber auch gute Nachrichten. Groß- und Kleinbuchstaben unterscheiden sich nur in Bit 5: Bei Großbuchstaben ist dies eine Null, bei Kleinbuchstaben eine 1. Das erleichtert die Umwandlung.
Zeichen
Hex
Binär
a
0x61
0b0110_0001
A
0x41
0b0100_0001
In ASCII ist alles enthalten, was die englischsprachige Welt für die Programmierung von Computern braucht. Dumm nur, dass nicht alle Menschen auf der Welt Englisch sprechen. Noch schlimmer, sie benutzen zum Teil komplett andere Zeichensätze.
Für Deutschsprachige ist der 8-Bit-Code ISO 8859-1, auch bekannt als ISO Latin 1 der International Organization for Standardization (ISO), wichtig, der Umlaute und gewisse Sonderzeichen enthält. Der ASCII-Zeichensatz ist hier als Untermenge enthalten. Allerdings fehlt hier etwa das Zeichen »€« für den Euro.
Um dieses und viele andere Probleme zu lösen, wurde Unicode entwickelt. Dabei werden bis zu vier Byte (UTF-32), in der Praxis aber meist zwei (UTF-16) für ein Zeichen benutzt, womit wir faktisch alle Zeichen der Welt abbilden können. In der Assembler-Programmierung wird Unicode bislang kaum verwendet.
Daher zurück zu ASCII. Ein ständiges Problem ist die Kennzeichnung des Zeilenendes (end-of-line, EOL). Unterschiedliche Betriebssysteme nutzen unterschiedliche Verfahren:
Betriebssystem
EOL
Linux, FreeBSD, macOS
LF
Windows, MS-DOS
CR+LF
Microsoft-Betriebssysteme benutzen zwei Zeichen, Rücklauf und Zeilenvorschub, die Betriebssysteme der Unix-Familie nur einen Zeilenvorschub.
Früher war alles schlimmer
Die Tabelle hat sich in den vergangenen Jahren vereinfacht: In den alten Apple-Betriebssystemen wie OS 9 – »Classic Mac OS« genannt – wurde CR als Zeilenende verwendet. Mit dem Umstieg auf macOS ist Apple ins Unix-Lager gewechselt. Microsoft wehrt sich noch.
Einzelne Zeichen sind nett, aber in der Praxis brauchen wir Zeichenketten. Eigentlich ganz einfach: Wir geben Zeichen für Zeichen aus, bis alle verbraucht sind, und hören dann auf. Allerdings sind wir so tief in der Maschine, dass wir uns entscheiden müssen, wie wir diesen string im Speicher ablegen. Es gibt zwei Hauptvarianten:
Mit einem besonderen Zeichen am Ende, meist die Null. Auf die Zeichenkette wird mit einem Zeiger (
pointer
) auf den ersten Buchstaben zugegriffen. Diese »Null-terminierten« (
null-
oder
zero-terminated
) Strings sind durch die Programmiersprache C bekannt. Wir nennen sie hier salopp »Null-Strings«. Die meisten Assembler und Compiler bringen dafür einen speziellen Befehl, eine »Direktive«, mit, um sie im Programmtext abzuspeichern:
first_coder:
.asciz "Ada Lovelace" # Direktive mit z am Ende für "zero"
Mit einer getrennten Angabe der Länge. Damit wird auf den String durch zwei Parameter zugegriffen: den Zeiger auf das erste Zeichen sowie die Zahl der Zeichen. Ein Ende-Zeichen gibt es nicht. Diese »gezählten« (
pointer and length
oder
counted
) Strings werden bei Rust verwendet, unser Kosename für sie lautet »Zähl-Strings«. Hier müssen wir im Code meistens selbst Hand anlegen, um die Länge voranzustellen.
first_coder:
.byte 12
.ascii "Ada Lovelace" # Direktive ohne z am Ende
Beide Stringvarianten haben ihre Vorteile: Null-Strings können eine beliebige Länge annehmen, während die Längenbestimmung bei Zähl-Strings trivial ist.
Wie wir an diesen Beispielen sehen, wollen wir fast immer ein Label – hier das first_coder mit einem Doppelpunkt dahinter – zusammen mit einem String vergeben, damit wir darauf zugreifen können.
Dies ist eine sehr allgemeine Einführung in Computerhardware für Leute, die nicht wissen, was unter der Haube geschieht. Wer schon den Unterschied zwischen einer CPU und einer ALU kennt, kann diesen Abschnitt getrost überspringen. Zwar wird auch im Vorbeigehen der RISC-V-Prozessor angerissen, aber die wichtigen Teile besprechen wir in späteren Abschnitten genauer.
Wer es auf die harte Tour mag
Wir werden die Hardware hier wirklich nur streifen und sonst mit fröhlicher Unbekümmerheit annehmen, dass alles irgendwie funktioniert. Wer das genaue Gegenteil haben will, sei im deutschsprachigen Raum auf Patrick Ritschels Embedded Systems mit RISC-V und ESP32-C3 verwiesen, das am konkreten praktischen Beispiel Mikroprozessoren, Protokolle, Schnittstellen und vieles mehr behandelt, für das wir uns in diesem Buch zu fein sind. [RP]
It is impossible to construct machinery occupying unlimited space; but it is possible to construct finite machinery, and to use it through unlimited time. It is this substitution of the infinity of time for the infinity of space which I have made use of, to limit the size of the engine and yet to retain its unlimited power.
– Charles Babbage zur Analytical Engine, Life of a Philosopher (1864)
Bei der Vorstellung des Computers für absolute Neulinge werden meistens vier Teile unterschieden:
Der
Prozessor
(CPU), der die ganze Arbeit macht. In diesem Buch ist es halt der RISC-V. Prozessoren besitzen
Register
, in denen eine sehr kleine Menge an Daten für die schnelle Bearbeitung vorübergehend festgehalten wird.
Der
Arbeitsspeicher
(RAM), wo die Daten vor und nach der Bearbeitung aufbewahrt werden. Er ist schnell, aber vergleichsweise klein und »flüchtig«: Wenn der Strom ausgeschaltet wird, ist der Inhalt weg. Jedenfalls bei der heutigen Technologie.
Massenspeicher
wie Festplatten oder SSD, die sehr viel langsamer, aber dafür auch sehr viel größer sind und auch nach dem Ausschalten des Stroms die Daten (mehr oder weniger) sicher aufbewahren.
Ein- und Ausgabegeräte
wie Tastatur, Maus, Bildschirm, WiFi oder Netzwerkverbindungen, um mit der ganzen Apparatur kommunizieren zu können.
Nach diesem Bild werden die Daten vom Massenspeicher in den Arbeitsspeicher geladen, dort vom Prozessor bearbeitet und das Ergebnis wird irgendwie mit einem Ausgabegerät angezeigt. Für die Assembler-Programmierung können wir diese Darstellung noch weiter vereinfachen: Wir brauchen fürs Erste nur den Prozessor und den Arbeitsspeicher. Wie die Daten vom Massenspeicher in den Arbeitsspeicher gelangen und wie die Maschine das Ergebnis mitteilt, ist uns erst mal egal – da vertrauen wir auf die Hamster.
Daher gehen wir hier nur auf den Prozessor und den Arbeitsspeicher genauer ein.
Bei jedem Projekt gibt es bekanntlich eine Person, die eigentlich die ganze Arbeit macht. Beim Computer ist es der Prozessor. Die Begriffe gehen inzwischen etwas durcheinander, wir benutzen hier relativ synonym »Prozessor« und als Kurzform »CPU« für Central Processing Unit (etwa: »zentrale Verarbeitungseinheit«), auch wenn dieser Begriff zunehmend weniger benutzt wird. Prozessoren können mehrere »Kerne« (cores) enthalten, die eine parallele Bearbeitung der Daten erlauben oder spezielle Aufgaben haben. So können einige Kerne für einen niedrigeren Stromverbrauch ausgelegt sein, während andere als »KI-Beschleuniger« (AI accelerator) eingesetzt werden. Ein Computer kann mehrere Prozessoren haben, die wiederum mehrere Kerne besitzen.
Das ist für uns alles viel zu kompliziert. Wir merken uns: Im Prozessor geschieht die eigentliche Arbeit, und es kann mehrere Kerne geben, die eine parallele Ausführung erlauben.
Diese Ausführung erfolgt klassischerweise über den Befehlszyklus (instruction cycle). Dabei werden Befehle aus dem Arbeitsspeicher geladen (fetch), ausgeführt (execute) und das Ergebnis wird wieder abgespeichert (write). Das ist eine sehr vereinfachte Version des Zyklus, in Wirklichkeit gibt es noch Zwischenschritte. Der Prozessor muss sich den geladenen Befehl erst mal genauer anschauen – ihn »dekodieren« (decode) –, um überhaupt zu wissen, was er tun soll. Auch hier überspringen wir die Details.
Es gibt mehrere Arten, Prozessoren zu klassifizieren.
Fangen wir mit dem internen Aufbau an, der »Architektur«. Klassischerweise gibt es vier Varianten, die die gleichen Komponenten benutzen – Register, Stapel, Speicher –, sie aber anders zusammenfügen und einsetzen. [HP] Wir stellen sie jeweils mit einem Beispiel für eine Additionsaufgabe in Pseudo-Code vor, bei der die beiden Zahlen aus den Speicherstellen 0x1000 und 0x1001 addiert werden sollen und das Ergebnis an 0x1002 abgelegt werden soll.
Register
(
general-purpose register
, GPR). In einem Kern gibt es mehrere,
explizit
ansprechbare Register, die grundsätzlich für alles benutzt werden können. Heute gibt es praktisch nur noch solche Prozessoren, zu denen auch RISC-V gehört.
load r1, 0x1000 # erste Zahl in Register 1 laden
load r2, 0x1001 # zweite Zahl in Register 2 laden
add r3, r1, r2 # addieren, Ergebnis in Register 3
store r3, 0x1002 # Register 3 im Speicher ablegen
Das sieht an dieser Stelle nach relativ viel Aufwand aus, und so ist es auch. Wir werden weiter unten erklären, warum dieser Ansatz trotzdem heute so beliebt ist.
Akkumulator
. Es gibt ein besonderes Register, auf das
implizit
alle Berechnungen einwirken und wo immer das Ergebnis abgelegt wird. Im Gegensatz zur Registerarchitektur wird dabei der Inhalt des Akkumulators zwangsweise überschrieben. Der 6502 ist einer der bekanntesten Vertreter.
load 0x1000 # Erste Zahl in den Akkumulator laden
add 0x1001 # Zweite Zahl direkt aus dem Speicher addieren
store 0x1002 # Akkumulator in Speicher sichern
Hier muss nicht ausdrücklich angegeben werden, dass wir mit dem Akkumulator arbeiten.
Stapel
(
stack
). Die zu bearbeitenden Daten werden wie Teller auf einem Stapel betrachtet. Alle Operationen betreffen
implizit