Einführung in die moderne Assembler-Programmierung - Scot W. Stevenson - E-Book
SONDERANGEBOT

Einführung in die moderne Assembler-Programmierung E-Book

Scot W. Stevenson

0,0
36,90 €
Niedrigster Preis in 30 Tagen: 36,90 €

-100%
Sammeln Sie Punkte in unserem Gutscheinprogramm und kaufen Sie E-Books und Hörbücher mit bis zu 100% Rabatt.
Mehr erfahren.
Beschreibung

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:

EPUB
MOBI

Seitenzahl: 332

Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



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

Einführung in diemoderne Assembler-Programmierung

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:

 

Print

978-3-98889-007-8

PDF

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

Inhaltsübersicht

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

Inhaltsverzeichnis

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

Vorwort

»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ß.

Thema

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.

Was ist Assembler (ganz kurz)?

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.

Der unvermeidliche geschichtliche Überblick

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.

Also warum dann jetzt noch Assembler?

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!

Was ist RISC-V?

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.

Formalitäten

So weit ein erster Überblick. Leider kommt kein Buch ohne Bürokratie aus. Bringen wir sie schnell hinter uns.

Englisch

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.

Code

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.

Werkzeuge

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)

Teil I

Grundlagen

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.

1Das Abstrakte

Wir fangen mit den rein theoretischen Grundlagen an – Zahlen und Zeichen.

1.1Zahlen

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

1.1.1Addition von Binärzahlen

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.

1.1.2Ein Vorgriff auf die Wortbreite

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.

1.1.3Vorzeichen und Zweierkomplement

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

1.1.4Vorzeichenerweiterung

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.

1.2Zeichen

Computer verstehen nur Zahlen, Menschen wollen Texte. Wir brauchen eine Möglichkeit, Buchstaben mit Zahlen darzustellen.

1.2.1ASCII

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

1.2.2Besser als ASCII

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.

1.2.3Zeilenende

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.

1.2.4Stringformate

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.

2Hardware

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]

2.1Ein Überblick

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.

2.2Der Prozessor

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.

2.2.1Aufbau

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