Befehlssätze und Assemblersprache

Lerne, wie Software über einen Befehlssatz mit der Hardware kommuniziert. Lies und schreibe am Beispiel von RISC-V Assemblersprache mit Registern, Speicheroperanden, Konstanten, arithmetischen und logischen Operationen sowie Sprüngen. Übersetze C-Ausdrücke, Verzweigungen, Schleifen, Zeiger und Zeichenketten in Befehle. Kodiere Befehle als 32-Bit-Maschinencode und dekodiere sie wieder, erstelle Prozeduraufrufe mit dem Stack und verfolge ein Programm beim Kompilieren, Assemblieren, Linken und Laden. Vergleiche abschließend RISC-V mit komplexen Befehlssätzen wie x86 und beurteile, was der Befehlssatzentwurf für die Rechenleistung bedeutet.

MittelEinheit 2 von 6 in Kurs: Rechnerarchitektur – Prozessoren, Speicher und RISC-V

Was du lernst

  1. Erkläre, was ein Befehlssatz ist, lies RISC-V-Assemblersprache und verwende Register, Speicher und Konstanten als Operanden von Befehlen.
    • Erkläre, was eine Befehlssatzarchitektur ist
      Beschreibe den Befehlssatz als Vereinbarung zwischen Software und Hardware, das Speicherprogrammkonzept und den Zusammenhang zwischen Assemblersprache, Maschinencode und dem RISC-V-Basisbefehlssatz.
    • Werte Ausdrücke mithilfe von Registern aus
      Benenne die 32 RISC-V-Ganzzahlregister und ihre üblichen Rollen und übersetze arithmetische Ausdrücke in Folgen von Registerbefehlen.
    • Lade Daten aus dem Speicher und speichere Daten darin
      Verwende Byteadressen, Datengrößen, Ausrichtung und Byte-Reihenfolge, um Wörter, Halbwörter und Bytes einschließlich Arrayelementen zwischen Speicher und Registern zu übertragen.
    • Arbeite mit Konstanten und Direktoperanden
      Verwende Direktoperanden für kleine Konstanten und erstelle vollständige 32-Bit-Konstanten und -Adressen mit lui, auipc, addi und den Pseudobefehlen li und la.
  2. Verwende arithmetische Befehle, logische Befehle, Schiebe- und Vergleichsbefehle und übersetze Verzweigungen und Schleifen aus C in bedingte und unbedingte Sprünge.
    • Verwende arithmetische Befehle, logische Befehle und Schiebebefehle
      Rechne mit Addition, Subtraktion, bitweiser Logik, Verschiebungen und Kleiner-als-Vergleichen. Berücksichtige Überlauf, vorzeichenbehaftete und vorzeichenlose Interpretation, Masken sowie Multiplikation und Division durch Potenzen von 2.
    • Verwende bedingte und unbedingte Sprungbefehle
      Verwende Vergleichs- und Sprungbefehle, vorzeichenbehaftete und vorzeichenlose Bedingungen, unbedingte Sprünge und ihre Pseudobefehle, um zu ändern, welcher Befehl als Nächstes ausgeführt wird.
    • Übersetze Verzweigungen und Schleifen aus C in Assemblersprache
      Wandle if-else-Anweisungen, zusammengesetzte Bedingungen, while- und for-Schleifen sowie switch-Anweisungen in Sprünge um und verfolge den entstehenden Code mit konkreten Werten.
  3. Wandle mithilfe der sechs RISC-V-Befehlsformate zwischen Assemblerbefehlen und 32-Bit-Maschinencode um und erkläre, warum die Formate so entworfen sind.
    • Kodiere Registerbefehle, Befehle mit Direktoperanden und Speicherbefehle
      Stelle die Felder von Befehlen im R-, I- und S-Format dar und wandle Befehle wie add, sub, addi, Schiebebefehle, lw und sw in Binär- und Hexadezimaldarstellung um.
    • Kodiere bedingte Sprünge, unbedingte Sprünge und obere Direktoperanden und dekodiere Maschinencode
      Kodiere Befehle im B-, J- und U-Format mit PC-relativen Versätzen und oberen Direktoperanden und dekodiere jedes 32-Bit-RV32I-Wort zurück in Assemblersprache.
  4. Halte die RISC-V-Aufrufkonvention ein, um Argumente zu übergeben, Ergebnisse zurückzugeben und Register zu erhalten. Verwende Stackframes für verschachtelte und rekursive Prozeduren und übersetze Zeiger, Arrays und Zeichenketten aus C.
    • Rufe Prozeduren auf und kehre unter Einhaltung der Aufrufkonvention zurück
      Übergib Argumente und Ergebnisse in Registern, rufe mit jal auf und kehre mit ret zurück. Entscheide, welche Register der Aufrufer oder die aufgerufene Prozedur erhalten muss.
    • Erstelle Stackframes für verschachtelte und rekursive Prozeduraufrufe
      Reserviere und gib Stackframes frei, sichere die Rückadresse und Register bei verschachtelten und rekursiven Aufrufen und ordne den Stack in die Speicheraufteilung eines Programms ein.
    • Übersetze Zeiger, Arrays und Zeichenketten aus C in Assemblersprache
      Wandle Zeigerdereferenzierungen, Zeigerarithmetik, Strukturfelder, Arrayparameter und nullterminierte Zeichenketten in Ladebefehle, Speicherbefehle und Adressberechnungen um.
  5. Erkläre, wie Compiler, Assembler, Linker und Lader C-Quellcode in ein im Speicher laufendes Programm umwandeln. Berücksichtige Objektdateien, Relokation und dynamisches Linken.
    • Erkläre, wie Compiler und Assembler Objektdateien erzeugen
      Beschreibe die Übersetzungshierarchie, die Aufgaben von Compilern und Assemblern, Assemblerdirektiven und Symbole sowie die Bestandteile einer Objektdatei.
    • Erkläre, wie Linker eine ausführbare Datei erstellen und Lader sie starten
      Beschreibe, wie ein Linker Module platziert und Adressen anpasst, vergleiche statisches und dynamisches Linken und erkläre, wie ein Lader und Systemaufrufe ein Programm starten und beenden.
  6. Stelle RISC-V komplexen Befehlssätzen wie x86 gegenüber und beurteile Zielkonflikte beim Befehlssatzentwurf hinsichtlich Rechenleistung, Codegröße und Kompatibilität.
    • Stelle die Entwurfsmerkmale von RISC und CISC gegenüber
      Beschreibe die kennzeichnenden Merkmale von RISC- und CISC-Befehlssätzen und vergleiche RISC-V mit x86 hinsichtlich Operanden, Kodierung, Adressierungsarten, Registern und Zustandskennungen.
    • Beurteile Zielkonflikte beim Befehlssatzentwurf hinsichtlich Rechenleistung, Codegröße und Kompatibilität
      Vergleiche Entwürfe mithilfe der CPU-Leistungsgleichung, erkläre, wie moderne Prozessoren die Trennlinie zwischen RISC und CISC verwischen, und beurteile verbreitete Fehlschlüsse über Befehlssätze.

Befehlssätze und Assemblersprache einfach erklärt

6 Kapitel

1

Befehle, Register und Speicher

Ein Befehlssatz verbindet ein Programm mit einem Prozessor. Lies zunächst das Ziel und die Quellen eines RISC-V-Befehls und verfolge dann Werte durch Register und Speicher. Byteadressen erklären, wie auf Arrays und Strukturen zugegriffen wird; Direktoperandenfelder erklären, warum manche Konstanten mehr als einen Befehl benötigen. Wenn du Werte, Adressen und Speicherinhalte auseinanderhältst, kannst du jede Assemblerzeile leichter nachvollziehen.

1.1 Der Befehlssatz als Softwarevereinbarung

Ein Prozessor führt Befehle aus, die durch Bits dargestellt werden. Eine Befehlssatzarchitektur (ISA) gibt diesen Bits eine gemeinsame Bedeutung, sodass Software dieselben Operationen auf unterschiedlichen Implementierungen anfordern kann.

Was die ISA festlegt

Die ISA legt Befehle, sichtbare Register, Datengrößen, das Speicherverhalten und Befehlskodierungen fest. Ein Befehl wie add a0, a1, a2 muss auf jedem Prozessor, der ihn unterstützt, das festgelegte Ergebnis liefern. Die internen Schaltungen und die zeitliche Planung gehören zur Mikroarchitektur. Zwei Chips können sich stark in Geschwindigkeit und Energieverbrauch unterscheiden und dennoch dieselbe ISA implementieren.

Für Binärkompatibilität muss auch die Softwareumgebung übereinstimmen. Ein Programm, das einen Betriebssystemdienst oder eine Bibliothek aufruft, hängt von mehr ab als der Bedeutung von Befehlen. Registerkonventionen und Datenlayouts sind Teil der Binärschnittstelle, kurz ABI.

Von lesbarem Code zu Bits

Für C wählt ein Compiler Assemblerbefehle aus; ein Assembler kodiert sie als Maschinencode. Assemblersprache ist Text mit Befehlsnamen, Registern, Kommentaren und Labels. Maschinencode ist die Binärdarstellung, die der Prozessor holt. Ein Label wie Start: bezeichnet eine Befehlsadresse; es ist kein Befehl, der zur Laufzeit ausgeführt wird. Ein Kommentar beginnt mit #.

Start:
    add a0, a1, a2   # a0 receives a1 + a2

Das erste Register ist das Ziel. Enthält a1 den Wert 14 und a2 den Wert 5, schreibt der Befehl 19 nach a0; die Quellen bleiben unverändert. Komplexere Ausdrücke benötigen mehrere Operationen. Die Addition von vier Registerwerten benötigt drei Additionen, wobei Zwischenergebnisse in Registern gehalten werden.

Die meisten Befehlsnamen entsprechen einem Maschinenbefehl. Ein Pseudobefehl ist eine Kurzform des Assemblers: mv fordert eine Registerkopie an und li eine Konstante. Ein Pseudobefehl kann zu einem oder mehreren echten Befehlen erweitert werden, sodass das Zählen von Assemblerzeilen nicht immer die Codegröße ergibt.

Im Speicher abgelegte Befehle

Nach dem Speicherprogrammkonzept werden sowohl Befehle als auch Daten als Zahlen im Speicher dargestellt. Der Befehlszähler pc bezeichnet den zu holenden Befehl. Bei einer Folge von 32-Bit-Befehlen wird er bei der normalen Ausführung um 4 Bytes weitergesetzt. Bedingte und unbedingte Sprünge wählen eine andere nächste Adresse. Der Prozessor führt die Kodierung aus, nicht die ursprüngliche Schreibweise in Assemblersprache, aus der sie entstand.

RISC-V-Basisbefehlssätze und -Erweiterungen

RISC-V ist ein offener, lizenzgebührenfreier ISA-Standard, der von RISC-V International gepflegt wird. RV32I stellt 40 Basisbefehle für ganze Zahlen und 32-Bit-Ganzzahlregister bereit; RV64I verwendet 64-Bit-Ganzzahlregister. Beide bieten 32 Ganzzahlregister. Optionale Erweiterungen ergänzen Funktionen: M bietet Multiplikation und Division ganzer Zahlen, A atomare Operationen, F und D Gleitkommaarithmetik, C komprimierte Befehle und V Vektoren. Die Beispiele verwenden RV32I und M dort, wo mul vorkommt.

RISC-V-Assemblersprache ist an RISC-V gebunden. Ein C-Programm kann für x86 oder ARM neu kompiliert werden, aber sein RISC-V-Maschinencode kann nicht direkt als Code dieser ISAs ausgeführt werden. Ein Simulator kann RISC-V-Befehle auf einem Host mit einer anderen ISA ausführen und Änderungen an Registern und Speicher Schritt für Schritt sichtbar machen.

Wichtige Erkenntnisse

  • Die ISA legt das sichtbare Verhalten fest; die Mikroarchitektur implementiert es.
  • Ein Assembler kodiert Assemblertext; Pseudobefehle können erweitert werden.
  • Bei 32-Bit-Befehlen setzt die sequenzielle Ausführung den pc um 4 Bytes weiter.
  • RISC-V kombiniert eine Basis-ISA mit optionalen Erweiterungen.

1.2 Register und Ergebnisse von Ausdrücken

Register halten die Werte, die ein Befehl unmittelbar verwenden kann. In RV32I enthält jedes Ganzzahlregister 32 Bits, und ein Registername bezeichnet einen Speicherplatz, keine C-Variable und keinen Datentyp.

Registernamen und Rollen

Es gibt 32 Ganzzahlregister, x0 bis x31. Der Befehlszähler ist separat. Fünf Bits bezeichnen ein Ganzzahlregister, denn es gibt 32 mögliche 5-Bit-Muster. Ein kleiner Registersatz begrenzt außerdem den Zugriffsaufwand und den Platz, den Registerfelder in Befehlen belegen.

RegisterABI-NamenÜblicher Zweck
x0zeroLiefert beim Lesen immer 0
x1, x2ra, spRückadresse und Stackpointer
x3, x4gp, tpGlobaler Zeiger und Thread-Zeiger
x5–x7, x28–x31t0–t6Temporäre Werte
x8–x9, x18–x27s0–s11Über Aufrufe hinweg erhaltene Werte
x10–x17a0–a7Argumente; a0–a1 geben auch Ergebnisse zurück

Register s0 kann auch als fp, der Framepointer, dienen. Schreibzugriffe auf zero werden verworfen, sodass es einen Nulloperanden liefern kann, ohne vorher eine Konstante zu laden. Beispielsweise kopiert add a0, a1, zero den Wert aus a1.

Diese Namen drücken eine Softwarevereinbarung aus. Insbesondere erhält die Hardware ein s-Register nicht automatisch. Eine Prozedur, die es verändert, muss dafür sorgen, dass sein Eingangswert wiederhergestellt wird. Die Standard-ABI reserviert gp und tp außerdem für ihre Aufgaben in der Umgebung; sie sind keine gewöhnlichen Arbeitsregister.

Einen Ausdruck in Operationen zerlegen

Übersetze f = (g + h) - (i + j); mit g, h, i, j in s1–s4 und dem Ergebnis in s0:

add t0, s1, s2
add t1, s3, s4
sub s0, t0, t1

Bei den Werten 9, 4, 6 und 1 hinterlässt der erste Befehl 13 in t0. Der zweite hinterlässt 7 in t1. Die Subtraktion schreibt 6 nach s0. Jedes temporäre Register hält ein Ergebnis, bis der nächste Befehl es benötigt.

Bei der Subtraktion ist die Reihenfolge der beiden Quelloperanden wichtig: sub s0, t1, t0 würde −6 ergeben. Das Ziel steht immer zuerst, sodass sub t0, s0, t1 stattdessen ein temporäres Register überschreiben und den alten Wert von s0 lesen würde. Notiere beim Nachverfolgen nach jeder Zeile das Ziel; aktualisiere nicht beide Quellregister.

Bits erhalten durch ihre Verwendung Bedeutung

Ein Register trägt keine Kennzeichnung als vorzeichenbehaftet oder vorzeichenlos. Das Muster 0xFFFFFFFF kann vorzeichenbehaftet −1 oder vorzeichenlos 4.294.967.295 bedeuten. Ein vorzeichenbehafteter und ein vorzeichenloser Vergleich können daher für dieselben gespeicherten Bits unterschiedliche Ergebnisse liefern. Auch Adressen sind Bitmuster in Registern; ein Ladebefehl interpretiert seine Basis als Adresse.

Werte so lange wie nötig verfügbar halten

Ein Compiler weist häufig verwendete Werte Registern zu. Benötigt er mehr gleichzeitig verfügbare Werte, als Register aufnehmen können, speichert er einige im Speicher und lädt sie später erneut. Dieser Vorgang heißt Spilling. Dabei entstehen tatsächlich Kosten durch Speicher- und Ladezugriffe.

Aufrufe bilden eine weitere Grenze. Eine aufgerufene Prozedur darf t0–t6 und a0–a7 überschreiben. Halte einen nach dem Aufruf benötigten Wert in einem ordnungsgemäß erhaltenen s-Register oder sichere ihn vor dem Aufruf auf dem Stack. Nur weil t0 frei war, überdauert sein Wert den Aufruf nicht automatisch.

Wichtige Erkenntnisse

  • RV32I hat 32 Ganzzahlregister mit je 32 Bits; der pc ist separat.
  • Schreibzugriffe auf zero werden verworfen, und Lesezugriffe liefern immer 0.
  • Registerarithmetik schreibt in den ersten Operanden und liest die folgenden Operanden.
  • Zwischenergebnisse müssen bis zu ihrer letzten Verwendung verfügbar bleiben.
  • Caller-saved-Register können sich bei einem Prozeduraufruf ändern.

1.3 Übertrage Werte zwischen Speicher und Registern

Speicheradressen bezeichnen Bytes. Ladebefehle bringen Daten in Register, und Speicherbefehle kopieren Registerbits zurück in den Speicher. In einer Load-Store-Architektur liest die Arithmetik ihre Operanden nicht direkt aus dem Speicher.

Größen und Byteversätze

Ein Byte belegt eine Adresse, ein Halbwort zwei und ein 32-Bit-Wort vier. RV32 hat einen 32-Bit-Adressraum von bis zu 4 GiB. In der C-ABI ILP32 belegt char 1 Byte, short 2 und int, long und Zeiger 4.

Die Befehle lw und sw übertragen Wörter. Ladebefehle für Halbwörter sind lh und lhu, für Bytes lb und lbu. Speicherbefehle verwenden sh und sb für die kleineren Größen. Ein Speicherbefehl kopiert die unteren Bits seiner Quelle; sb schreibt nur die unteren 8 Bits.

Eine Adresse ergibt sich aus einem Basisregister plus einem vorzeichenbehafteten 12-Bit-Byteversatz:

lw t0, 32(s3)
sw t0, 48(s3)

Der Ladebefehl liest das Wort an der Adresse in s3 plus 32. Er addiert nicht 32 zum geladenen Wert und verändert s3 nicht. Der Speicherbefehl schreibt diesen Wert an die Basisadresse plus 48. Der Versatzbereich reicht von −2048 bis 2047.

Einen Arrayzugriff übersetzen

Bei einem int-Array mit der Basisadresse in s3 beginnt das Element A[i] an der Basisadresse plus dem 4-Fachen des Index. Um A[12] = h + A[8]; mit h in s1 zu implementieren:

lw t0, 32(s3)      # A[8]
add t0, s1, t0
sw t0, 48(s3)      # A[12]

Ist A[8] gleich 80 und h gleich 3, wird A[12] zu 83. Ein Versatz von 8 würde A[2] auswählen, da die Adresse Bytes zählt. Bilde bei einem variablen Index in s4 zuerst die Adresse:

slli t1, s4, 2
add t1, s3, t1
lw t0, 0(t1)

Die Skalierung richtet sich nach der Elementgröße, nicht nach dem Prozessornamen. Ein Zeiger oder long in RV64 LP64 belegt 8 Bytes und verwendet ld/sd; ein int in RV64 belegt weiterhin 4.

Einen kleinen geladenen Wert erweitern

Ein Register muss 32 Bits enthalten, auch wenn ein Ladebefehl nur 8 liest. lb kopiert das Vorzeichenbit des Bytes in jedes höherwertige Bit; lbu füllt die höherwertigen Bits mit Nullen. So wird das Byte 0xF0 mit lb zu 0xFFFFFFF0 (−16), mit lbu dagegen zu 240. Derselbe Unterschied gilt für lh und lhu. Für vorzeichenlose Bytes, einschließlich Bytes von Zeichenketten, wird lbu verwendet.

Die Byte-Reihenfolge lesen

Im Little-Endian-Speicher belegt das niederwertigste Byte die niedrigste Adresse. Wird 0x12345678 bei 0x1000 gespeichert, liegen 0x78, 0x56, 0x34, 0x12 an den Adressen 0x1000 bis 0x1003. Ein Byte-Ladebefehl von der ersten Adresse liest 0x78. Bei Big-Endian steht 0x12 zuerst; RISC-V-Systeme können beide Reihenfolgen festlegen, während die üblichen Lehrsysteme Little-Endian verwenden.

Eine natürlich ausgerichtete Adresse ist ein Vielfaches der Zugriffsgröße: 4 für ein Wort und 2 für ein Halbwort. Natürlich ausgerichtete Zugriffe werden von der Architektur unterstützt. Ein nicht ausgerichteter Zugriff kann je nach Ausführungsumgebung funktionieren, länger dauern oder eine Ausnahme auslösen. Platziere Wörter an 4-Byte-Grenzen und gehe um 4 weiter, um das nächste Wort zu erreichen.

Wichtige Erkenntnisse

  • Versätze von Lade- und Speicherbefehlen zählen Bytes, keine Arrayelemente.
  • Berechne bei einem variablen Index die Adresse, bevor du den Lade- oder Speicherbefehl ausführst.
  • Vorzeichenbehaftete Ladebefehle erweitern mit dem Vorzeichen, vorzeichenlose mit Nullen.
  • Im Little-Endian-Speicher steht das niederwertigste Byte zuerst.
  • Die natürliche Ausrichtung richtet sich nach der Zugriffsgröße.
  • RV64 LP64 verbreitert Zeiger und long-Werte, während int 4 Bytes groß bleibt.

1.4 Passe Konstanten in Befehlsfelder ein

Ein Direktoperand ist eine Konstante, die innerhalb eines Befehls kodiert ist. Er ermöglicht bei einer häufigen Operation wie dem Erhöhen eines Zählers, einen separaten Ladezugriff zu vermeiden.

Kleine Konstanten

addi t0, t0, 1 addiert 1 zu t0. Sein Direktoperand ist eine vorzeichenbehaftete 12-Bit-Zahl im Zweierkomplement von −2048 bis 2047. Der Prozessor erweitert sie vor der Addition mit Vorzeichen auf 32 Bits. Subtrahiere eine kleine Konstante mit einem negativen Direktoperanden, wie in addi t0, t0, -5; RV32I hat keinen Subtraktionsbefehl mit Direktoperand.

Auch Ladebefehle, jalr und gewöhnliche arithmetische und logische Befehle mit Direktoperand verwenden vorzeichenbehaftete 12-Bit-Direktoperanden. Insbesondere erweitern andi, ori und xori ihre Konstanten nicht mit Nullen. Schiebebefehle mit Direktoperand kodieren in RV32 stattdessen eine Verschiebungsweite von 0 bis 31.

Nützliche Kurzformen des Assemblers sind:

TextBasisbefehl oder Erweiterung
mv rd, rsaddi rd, rs, 0
nopaddi x0, x0, 0
not rd, rsxori rd, rs, -1
neg rd, rssub rd, x0, rs
li rd, c, für ein kleines caddi rd, x0, c

Die Konstante −1 besteht nach der Vorzeichenerweiterung nur aus Einsen, weshalb xori mit −1 jedes Bit umkehrt.

Alle 32 Bits aufbauen

lui setzt ein 20-Bit-Feld in die Bits 31–12 und löscht die unteren 12 Bits. Für 0x12345678:

lui t0, 0x12345
addi t0, t0, 0x678

Der Zwischenwert ist 0x12345000; die Addition von 1.656 ergänzt den unteren Teil. Bei einem unteren Teil mit gesetztem Bit 11 musst du genauer aufpassen. Sein 12-Bit-Muster stellt einen negativen Summanden dar, erhöhe also den oberen Teil, bevor du ihn addierst.

Für 0x00001800 bedeutet das untere Muster 0x800 den Wert −2048. Verwende:

lui t0, 0x2
addi t0, t0, -2048

Das erste Ergebnis ist 0x2000. Die Subtraktion von 0x800 ergibt 0x1800. Mit dem oberen Feld 0x1 entstünde 0x800, also ein Block von 4096 zu wenig.

Lies bei jeder 32-Bit-Konstante die unteren 12 Bits als vorzeichenbehafteten Wert. Subtrahiere diesen vorzeichenbehafteten Wert von der gewünschten Konstante, dividiere durch 4096 und behalte die unteren 20 Bits für lui. Bei 0xDEADBEEF ist der vorzeichenbehaftete untere Teil −273. Verwende also das obere Feld 0xDEADC und danach addi mit −273. Assembler führen diese Aufteilung für li aus.

Konstanten, Adressen und Inhalte

li t0, 100 schreibt die Zahl 100 in ein Register. lw t0, 100(zero) liest den Speicherinhalt an Adresse 100. Eine Adresse kann selbst als Wert geladen werden: la t0, Item fordert vom Assembler die durch Item bezeichnete Adresse an.

auipc addiert einen um 12 Bits nach links verschobenen oberen Direktoperanden zur Adresse dieses Befehls. In Kombination mit einer Operation mit Direktoperand oder einem Ladebefehl ermöglicht es PC-relativen Zugriff über etwa ±2 GiB. Ein lokales la verwendet häufig auipc und addi; andere Erweiterungen unterstützen den Zugriff auf gemeinsam genutzte Bibliotheken.

Eine Konstante wie 5.000 kann kein Operand von addi sein. Lade sie mit li in ein zweites Register und verwende dann add. Ein größerer Wert erhält keinen breiteren Direktoperanden, nur weil er hexadezimal geschrieben wird.

Wichtige Erkenntnisse

  • Ein vorzeichenbehafteter 12-Bit-Direktoperand reicht von −2048 bis 2047.
  • lui liefert die oberen 20 Bits und setzt die unteren 12 Bits auf null.
  • Ist der untere 12-Bit-Teil negativ, gleiche dies im oberen Feld aus.
  • li lädt einen Wert, la bildet eine Adresse und lw liest den Speicher.
  • auipc bildet eine Adresse relativ zu seinem eigenen pc.
2

Arithmetik und Kontrollfluss

Sobald Werte in Registern stehen, berechnen Befehle Ergebnisse und entscheiden, was als Nächstes ausgeführt wird. Bitweise Logik und Verschiebungen arbeiten auf Darstellungen; vorzeichenbehaftete und vorzeichenlose Vergleiche geben diesen Mustern unterschiedliche Bedeutungen. Bedingte und unbedingte Sprünge setzen Verzweigungen und Schleifen aus C in ausdrückliche Pfade um. Einen Pfad nachzuverfolgen bedeutet, sowohl die aktuellen Werte als auch die Adresse des nächsten Befehls festzuhalten, einschließlich der letzten fehlgeschlagenen Schleifenprüfung.

2.1 Rechne mit Bits und vorzeichenbehafteten Werten

Ein arithmetischer Befehl liest Registerbits und schreibt ein neues Bitmuster. Um dieses Muster zu verstehen, musst du sowohl die Operation als auch die beabsichtigte vorzeichenbehaftete oder vorzeichenlose Interpretation berücksichtigen.

Addition, Subtraktion und Überlauf

In RV32I behalten add, sub und addi die unteren 32 Ergebnisbits. Die Addition von 1 zu 0x7FFFFFFF ergibt 0x80000000, was als vorzeichenbehaftete ganze Zahl −2.147.483.648 darstellt. Der Befehl löst keine Überlaufausnahme aus.

Bei vorzeichenloser Addition kann Software einen Überlauf erkennen:

add t0, a0, a1
bltu t0, a0, Overflow

Eine vorzeichenlose Summe nach einem Überlauf ist kleiner als ein Eingabewert. Dieser vorzeichenlose Test ist nicht die Regel für vorzeichenbehafteten Überlauf, bei dem die Vorzeichen von Eingaben und Ergebnis entscheidend sind.

Die M-Erweiterung stellt mul, mulh, mulhu, div, divu, rem und remu bereit. mul behält die unteren Produktbits; die Befehle für die obere Produkthälfte unterscheiden vorzeichenbehaftete und vorzeichenlose Eingaben. Eine Division durch null liefert einen Quotienten, der nur aus Einsen besteht, und den ursprünglichen Dividenden als Rest, ohne eine Divisionsausnahme auszulösen.

Bits auswählen, setzen und umkehren

and, or und xor arbeiten unabhängig auf jedem Bit. UND mit 1 erhält ein Bit; UND mit 0 löscht es. ODER mit 1 setzt ein Bit, und exklusives ODER mit 1 kehrt es um.

srli t0, a0, 4
andi t0, t0, 0xF

Bei a0 = 0xABCD ergibt das Verschieben nach rechts 0xABC. Die Maskierung mit 0xF lässt 0xC übrig, die ursprünglichen Bits 7–4. Die Verschiebung setzt das Feld an das untere Ende; die Maske entfernt alles darüber. Um Bit 4 zu setzen, ohne die anderen zu verändern, verwende ori a0, a0, 0x10. xori mit −1 kehrt alle 32 Bits um.

Eine Verschiebung nach ihrer Auffüllregel wählen

Eine Linksverschiebung mit sll oder slli füllt niederwertige Stellen mit Nullen auf. Eine logische Rechtsverschiebung mit srl oder srli füllt höherwertige Stellen mit Nullen auf. Eine arithmetische Rechtsverschiebung mit sra oder srai kopiert das Vorzeichenbit in die höherwertigen Stellen. Registerverschiebungen in RV32 verwenden nur die unteren 5 Bits des Registers mit der Verschiebungsweite.

Wird 13 um 3 nach links verschoben, ergibt sich 104. Das entspricht einer Multiplikation mit 8, weil keine benötigten Bits verloren gehen. Bei Linksverschiebungen, die signifikante Bits verwerfen, bleibt dagegen nur der Teil des Ergebnisses erhalten, der ins Register passt.

Bei vorzeichenlosen Werten dividiert eine Rechtsverschiebung durch eine Potenz von 2 und rundet ab. Bei vorzeichenbehafteten Werten rundet die arithmetische Rechtsverschiebung gegen minus unendlich: Wird −7 um 1 nach rechts verschoben, ergibt sich −4. Die vorzeichenbehaftete Division in C rundet gegen null und ergibt für -7 / 2 den Wert −3. Ein Compiler benötigt eine Korrektur, bevor er diese vorzeichenbehaftete Division durch eine Verschiebung ersetzt. Eine logische Verschiebung desselben negativen Musters füllt mit Nullen auf und ergibt ein großes positives Ergebnis.

Vergleichen, um einen Wert zu erzeugen

slt schreibt 1, wenn seine erste Quelle bei vorzeichenbehafteter Interpretation kleiner ist, und sonst 0. sltu interpretiert die Werte vorzeichenlos. Mit 0xFFFFFFFF und 1 als Eingaben schreibt slt den Wert 1, sltu dagegen 0. Die Versionen mit Direktoperand sind slti und sltiu; auch Letzterer erweitert seinen Direktoperanden vor dem vorzeichenlosen Vergleich mit Vorzeichen.

Das bitweise UND unterscheidet sich von && in C. Ein bitweises Ergebnis kann viele gesetzte Bits enthalten; das logische UND liefert 0 oder 1 und kann die Auswertung des zweiten Ausdrucks überspringen.

Wichtige Erkenntnisse

  • RV32-Arithmetik behält die unteren 32 Bits und löst bei Überlauf keinen Trap aus.
  • UND wählt Bits aus, ODER setzt sie und exklusives ODER kehrt sie um.
  • Die arithmetische Rechtsverschiebung erhält das Vorzeichen; die logische füllt mit Nullen auf.
  • srai und die vorzeichenbehaftete Division in C runden negative Werte, die keine Vielfachen des Divisors sind, unterschiedlich.
  • slt und sltu interpretieren dieselben Registermuster unterschiedlich.
  • Multiplikation und Division der M-Erweiterung sind von RV32I getrennt.

2.2 Wähle den nächsten Befehl

Ein bedingter Sprung vergleicht zwei Register und ändert den Befehlszähler, wenn seine Prüfung erfolgreich ist. Andernfalls wird der nächste Befehl ausgeführt.

Den Vergleich wählen
BefehlSprungbedingung
beq, bneGleich, ungleich
blt, bgeVorzeichenbehaftet kleiner als, vorzeichenbehaftet größer oder gleich
bltu, bgeuVorzeichenlos kleiner als, vorzeichenlos größer oder gleich

RISC-V kombiniert Vergleich und Sprung in einem Befehl. Es gibt keine Ganzzahl-Statusflags, die zuerst gesetzt werden müssten. Um gegen null zu prüfen, verwende zero als eine Quelle. beqz a0, L wird zu beq a0, zero, L erweitert.

Formen für „größer als“ und „kleiner oder gleich“ sind Kurzformen des Assemblers mit vertauschten Quellen. bgt a0, a1, L wird zu blt a1, a0, L erweitert. Die Reihenfolge der Quellen richtig zu tauschen ist ebenso wichtig wie die Wahl des richtigen Befehlsnamens.

Vorzeichenbehafteter und vorzeichenloser Kontrollfluss

Enthält a0 den Wert 0xFFFFFFFF und a1 den Wert 1, führt blt a0, a1, L den Sprung aus, weil die vorzeichenbehaftete Zahl −1 kleiner als 1 ist. bltu tut das nicht, weil das erste Muster einen großen vorzeichenlosen Wert darstellt. Verwende Vergleiche, die zur Bedeutung der Daten passen, darunter vorzeichenlose Vergleiche für Adressen.

Eine vorzeichenlose Bereichsprüfung kann sowohl negative vorzeichenbehaftete Indizes als auch Indizes ab einer kleinen nicht negativen Länge zurückweisen:

bgeu a0, a1, OutOfBounds

Bei einer Länge von 20 erscheint der Index −1 vorzeichenlos als 4.294.967.295 und löst den Sprung aus. Der Index 19 tut das nicht, der Index 20 dagegen schon. Die Länge muss die tatsächliche nicht negative Länge des Arrays darstellen.

Unbedingte Sprünge und Rückadressen

jal ra, f schreibt die Adresse des nächsten Befehls, pc + 4, nach ra und springt dann zu f. Ein einfaches j L wird zu jal zero, L erweitert; der Rückadresswert wird verworfen. Das eignet sich für einen Rücksprung in einer Schleife, legt aber keine Rückadresse für einen Aufruf an.

jalr rd, offset(rs1) addiert einen vorzeichenbehafteten 12-Bit-Versatz zu einem Registerwert, löscht Bit 0 des Ziels und schreibt sein eigenes pc + 4 nach rd. ret wird zu jalr zero, 0(ra) erweitert und kehrt zur gesicherten Adresse zurück, ohne eine neue Rückadresse zu behalten.

Bedingte Sprünge und jal speichern Versätze relativ zu ihrer eigenen Befehlsadresse. Ihre Reichweite beträgt etwa ±4 KiB beziehungsweise ±1 MiB. jalr verwendet ein registerbasiertes Ziel und kann den gesamten Adressraum erreichen. Für einen entfernten bedingten Sprung kann die Prüfung umgekehrt werden, um einen weiter reichenden Sprung zu überspringen:

bne a0, a1, Skip
jal zero, Far
Skip:

Hier liegt Far in der Reichweite von jal. Bei Gleichheit wird der kurze bedingte Sprung nicht ausgeführt und die Ausführung läuft in den unbedingten Sprung weiter; andernfalls wird dieser übersprungen.

Einen Basisblock erkennen

Ein Basisblock hat einen einzigen Eintritt an seinem Anfang und keine Kontrollflussübertragung vor seinem Ende. Innerhalb des Blocks werden Befehle der Reihe nach ausgeführt. Sprungziele beginnen neue Blöcke; bedingte und unbedingte Sprünge beenden sie. So lässt sich untersuchen, welche Zuweisungen vor einer bestimmten Prüfung oder einem Austritt immer ausgeführt werden.

Wichtige Erkenntnisse

  • Wähle vorzeichenbehaftete oder vorzeichenlose Sprünge passend zur Bedeutung der Operanden.
  • Bedingte RISC-V-Sprünge vergleichen Register direkt und ohne Statusflags.
  • jal sichert eine Rückadresse; j verwirft sie.
  • jalr verwendet ein Register plus einen Direktoperanden und löscht Bit 0 des Ziels.
  • Ein bedingter Sprungversatz ist relativ zum Sprungbefehl selbst.

2.3 Wandle den Kontrollfluss aus C in Sprünge um

Assemblersprache macht jeden Pfad durch eine Verzweigung oder Schleife ausdrücklich sichtbar. Labels markieren Ziele, bedingte Sprünge wählen Pfade, und unbedingte Sprünge verhindern unerwünschtes Weiterlaufen.

Eine if-else-Anweisung braucht einen Austrittspfad

Übersetze if (i == j) f = g + h; else f = g - h; mit f, g, h, i, j in s0–s4:

bne s3, s4, Else
add s0, s1, s2
j Exit
Else:
sub s0, s1, s2
Exit:

Der erste Sprung prüft das Gegenteil der C-Bedingung, sodass die Ausführung bei Gleichheit in die Addition weiterläuft. Der Sprung nach der Addition verhindert die Ausführung der Subtraktion. Bei g = 10 und h = 3 ergeben gleiche Indizes 13; ungleiche Indizes ergeben 7. Wird j Exit entfernt, läuft die Subtraktion auch nach der Addition.

Bei a < b && b < c springst du zum else-Teil, sobald einer der Vergleiche fehlschlägt. Bei a < b || b < c springst du zum then-Teil, sobald einer erfolgreich ist. Das implementiert die Kurzschlussauswertung und überspringt auch einen zweiten Ausdruck, dessen Wert nicht benötigt wird.

Eine while-Schleife prüft vor ihrem Rumpf

Für while (save[i] == k) i++; halte i in s3, k in s5 und die Arraybasis in s6:

Loop:
slli t1, s3, 2
add t1, t1, s6
lw t0, 0(t1)
bne t0, s5, Exit
addi s3, s3, 1
j Loop
Exit:

Bei jedem Durchlauf wird die Adresse des aktuellen Elements gebildet, das Element geladen und geprüft. Beim Array {7, 7, 7, 2, 7}, dem Anfangsindex 0 und k = 7 endet die Schleife mit Index 3. Die fehlgeschlagene Prüfung erhöht den Index nicht. Das Array muss innerhalb seines gültigen Bereichs ein Element enthalten, das die Schleife beendet.

Ein Array mit bekannter Elementanzahl summieren

Für eine nicht negative Elementanzahl in a1 und die Arraybasis in a0:

li t1, 0
li t0, 0
Loop:
bge t0, a1, Done
slli t2, t0, 2
add t2, a0, t2
lw t3, 0(t2)
add t1, t1, t3
addi t0, t0, 1
j Loop
Done:
mv a0, t1
ret

Die Prüfung am Anfang behandelt eine Länge von null, ohne etwas zu laden. Bei vier Elementen {5, -2, 10, 7} beträgt die laufende Summe zunächst 5, dann 3, 13 und schließlich 20. Die Folge führt 33 Befehle aus: 2 zur Vorbereitung, 7 für jeden der vier erfolgreichen Durchläufe, eine fehlgeschlagene Prüfung und 2 zum Abschluss.

Eine Zeigervariante bildet einmal eine Endadresse und erhöht die aktuelle Adresse um 4. Für einen gültigen Arraybereich, dessen Endadresse keinen Überlauf aufweist, kann sie vorzeichenlose Adressvergleiche verwenden:

li t1, 0
slli t0, a1, 2
add t0, a0, t0
bgeu a0, t0, Done
Loop:
lw t3, 0(a0)
add t1, t1, t3
addi a0, a0, 4
bltu a0, t0, Loop
Done:
mv a0, t1
ret

Es gibt 4 Befehle zur Vorbereitung, 4 pro Durchlauf und 2 zum Abschluss, insgesamt 22 für vier Elemente. Der erste Sprung überspringt den Rumpf bei einem leeren Bereich; der Zeiger wird nach jedem Laden um ein Element weitergesetzt.

Mehrfachverzweigungen

Ein switch kann wiederholte Vergleiche oder bei dicht beieinanderliegenden Fallwerten eine Sprungtabelle verwenden. Der Sprung über die Tabelle prüft den Fallbereich, multipliziert einen Index mit der Eintragsgröße von 4 Bytes, lädt die Falladresse und springt mit jalr zero, 0(t0) dorthin. Die Bereichsprüfung muss vor dem Laden aus der Tabelle erfolgen, damit ein ungültiger Fall keine fremde Adresse auswählen kann.

Wichtige Erkenntnisse

  • Um den then-Teil zu überspringen, wird häufig die entgegengesetzte Bedingung verwendet.
  • Springe nach Abschluss des then-Teils über den else-Teil hinweg.
  • Zusammengesetzte Bedingungen müssen die Kurzschlussauswertung erhalten.
  • Prüfe eine Zählschleife, bevor du ihr erstes Element liest.
  • Zeigerschleifen können wiederholte Indexskalierung vermeiden.
  • Eine Sprungtabelle benötigt vor dem Sprung eine Bereichsprüfung.
3

Befehlsfelder und Maschinencode

Assemblernamen werden zu festen Feldern innerhalb von Maschinenbefehlen. Gehe von der Operation zu ihrem Format über und ordne dann Register und Direktoperanden ihren Bitpositionen zu. Ladebefehle, Speicherbefehle, bedingte und unbedingte Sprünge verwenden unterschiedliche Anordnungen, weil sie unterschiedliche Informationen enthalten. Das Kodieren und Dekodieren in beide Richtungen legt Fehler bei Versatz, Vorzeichen und Feldreihenfolge offen, die lesbare Assemblersprache verbergen kann.

3.1 Kodiere Registeroperationen, Lade- und Speicherbefehle

Ein Maschinenbefehl ist ein 32-Bit-Wort, das in Felder aufgeteilt ist. Kodieren bedeutet, die richtige Anordnung zu wählen, jeden Operanden einem Feld zuzuweisen und jedes Feld in seiner vollen Breite zu schreiben.

Feste Positionen vereinfachen die Dekodierung

Die unteren 7 Bits, 6–0, enthalten den Operationscode. Wo vorhanden, belegt rd die Bits 11–7, funct3 die Bits 14–12, rs1 die Bits 19–15 und rs2 die Bits 24–20. Fünf Bit breite Registerfelder bezeichnen x0 bis x31. Funktionsfelder unterscheiden Operationen mit demselben Operationscode.

RV32I verwendet die Formate R, I, S, B, U und J. Sie bringen unterschiedliche Operanden in denselben 32 Bits unter und halten dabei die Registerpositionen fest. Bei vorzeichenbehafteten Direktoperanden trägt Bit 31 das Vorzeichen.

R-Format: zwei Quellen und ein Ziel

Von Bit 31 abwärts bis Bit 0 lautet die R-Anordnung:

funct7 | rs2 | rs1 | funct3 | rd | opcode
   7      5     5       3     5       7 bits

Für add s0, a0, a1 gilt rd = 8, rs1 = 10, rs2 = 11. ADD verwendet den Operationscode 0110011, funct3 = 000, funct7 = 0000000:

0000000 | 01011 | 01010 | 000 | 01000 | 0110011
0000 0000 1011 0101 0000 0100 0011 0011

Das Hexadezimalwort ist 0x00B50433. SUB ändert nur funct7 zu 0100000 und ergibt für sub s0, a0, a1 den Wert 0x40B50433. Die Quellfelder stehen in einer anderen Reihenfolge als die Assembleroperanden. Hänge daher nicht einfach die ausgegebenen Registernamen aneinander.

I-Format: eine Quelle und ein Direktoperand

Die I-Anordnung lautet imm[11:0] | rs1 | funct3 | rd | opcode. addi verwendet den Operationscode 0010011 und funct3 = 000. In addi sp, sp, -16 enthalten beide Registerfelder 00010. Der 12-Bit-Direktoperand ist 111111110000 (0xFF0) und ergibt 0xFF010113.

Auch ein Ladebefehl verwendet das I-Format, mit der Adressbasis als rs1 und dem geladenen Register als rd. lw t0, 32(s3) hat den Direktoperanden 32, die Basis x19, das Ziel x5, den Operationscode 0000011 und funct3 = 010; das ergibt 0x0209A283.

Schiebeoperationen mit Direktoperand verwenden die unteren 5 Bits des Direktoperandenbereichs als Verschiebungsweite. Die oberen Steuerbits unterscheiden die Verschiebungen: srli t0, t1, 3 ist 0x00335293, während srai t0, t1, 3 gleich 0x40335293 ist. Bit 30 wählt die arithmetische Auffüllregel.

S-Format: einen Quellwert speichern

Ein Speicherbefehl hat kein Register als Ziel. Die S-Anordnung verwendet diesen Platz für einen Teil ihres Direktoperanden:

imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode

Bei sw t0, 48(s3) liefert rs2 = 5 den gespeicherten Wert und rs1 = 19 die Basis. Der 12-Bit-Direktoperand 48 wird in 0000001 und 10000 aufgeteilt. Mit funct3 = 010 und dem Operationscode 0100011 ergibt sich 0x0259A823. Es gibt kein rd, das mit t0 zu füllen wäre.

Gruppiere nach dem Zusammenfügen der Felder die vollständige 32-Bit-Folge in acht 4-Bit-Nibbles und wandle jedes in eine Hexadezimalziffer um. Dekodiere dann das Ergebnis und prüfe Operationscode, Registernummern und vorzeichenbehafteten Direktoperanden. Ein Registerfeld auf 5 Bits aufzufüllen ist unerlässlich; das Weglassen einer führenden Null verändert die Positionen späterer Felder.

Wichtige Erkenntnisse

  • Bestimme anhand der Operation das Format, bevor du Operandenfelder zuweist.
  • Registerfelder sind 5 Bits breit und haben feste Positionen.
  • Das R-Format verwendet funct7, um add von sub zu unterscheiden.
  • Das I-Format hält einen 12-Bit-Direktoperanden zusammen.
  • Das S-Format teilt den Versatz auf und hat rs2, aber kein rd.
  • Prüfe ein kodiertes Wort, indem du es wieder in Operanden dekodierst.

3.2 Setze Versätze zusammen und dekodiere Maschinenwörter

Kodierungen für bedingte und unbedingte Sprünge sparen Platz, indem sie ein Versatzbit weglassen, das immer null ist. Ihre Felder stellen weiterhin Abstände in Bytes dar, und das Vorzeichen muss vor der Berechnung des Ziels rekonstruiert werden.

Sprungversätze im B-Format

Berechne den Versatz als Zieladresse minus Adresse des Sprungbefehls selbst. Ein bne bei 0x1010 mit dem Ziel 0x1000 verwendet −16 Bytes, nicht −20 und nicht −4 Befehle.

Das B-Format verwendet den Operationscode 1100011. Sein vorzeichenbehafteter Versatz hat 13 Bits, einschließlich eines nicht gespeicherten Bits 0 mit dem Wert null. Der Bereich reicht von −4096 bis +4094 Bytes in Schritten von 2. Die Anordnung lautet:

VersatzbitsBefehlsbits
imm[12]31
imm[10:5]30–25
imm[4:1]11–8
imm[11]7

Die Quellfelder bleiben an ihren gewohnten Positionen. Die funct3-Werte sind 000 für beq, 001 für bne, 100 für blt, 101 für bge, 110 für bltu und 111 für bgeu.

Für bne t0, t1, -16 lautet der 13-Bit-Versatz 1 1111 1111 0000. Seine verstreuten Teile sind in der Reihenfolge der Tabelle 1, 111111, 1000 und 1. Mit rs1 = 5 und rs2 = 6 lautet der Befehl 0xFE6298E3. Ersetzt du den Versatz durch +12, ergibt sich 0x00629663.

Versätze unterstützen 2-Byte-Schritte, weil komprimierte Befehle 2 Bytes lang sein können. Ein angesprungenes Ziel muss dennoch die geltende Befehlsausrichtung erfüllen: RV32I ohne komprimierte Befehle verlangt eine Ausrichtung auf 4-Byte-Grenzen.

J-Format und U-Format

jal verwendet den Operationscode 1101111. Sein vorzeichenbehafteter 21-Bit-Byteversatz lässt Bit 0 weg und ordnet die Bits in der Reihenfolge imm[20|10:1|11|19:12] über die Befehlsbits 31–12 an. Der Bereich reicht von −1.048.576 bis +1.048.574 Bytes. jal ra, 192 wird zu 0x0C0000EF.

Das U-Format setzt seinen oberen 20-Bit-Wert direkt in die Befehlsbits 31–12, gefolgt von rd und dem Operationscode. lui verwendet 0110111; auipc verwendet 0010111. Daher ist lui t0, 0x12345 gleich 0x123452B7.

Zuerst anhand des Operationscodes dekodieren

Bei 0x00A12423 lauten die unteren 7 Bits 0100011: ein Speicherbefehl. funct3 = 010 wählt sw. Die Quellfelder ergeben die Basis x2 (sp) und den Wert x10 (a0). Die beiden Teile des Direktoperanden ergeben zusammen 8, also lautet der Assemblerbefehl sw a0, 8(sp).

Setze bei einem bedingten Sprung die verstreuten Teile in der Reihenfolge der Versatzbits zusammen, hänge die implizite Null an und erweitere das vollständige 13-Bit-Ergebnis mit Vorzeichen. Werden die 12 gespeicherten Bits wie ein gewöhnlicher Direktoperand des I-Formats behandelt, gehen sowohl die richtige Anordnung als auch die Skalierung verloren. Die feste Position des Vorzeichenbits und die Wiederverwendung anderer Feldpositionen verringern den Aufwand für den Dekodierer, obwohl die dargestellte Anordnung von Hand weniger bequem zu lesen ist.

Relokation erhält Beziehungen

Werden ein Sprung und sein Ziel beide um denselben Abstand verschoben, ergibt die Subtraktion ihrer neuen Adressen den alten Versatz. Ein Verweis in einen anderen Abschnitt kann sich ändern, wenn Abschnitte getrennt angeordnet werden. Ein Linker kann daher einen internen relativen Sprung erhalten und zugleich einen Verweis auf ein externes Symbol anpassen.

Wichtige Erkenntnisse

  • Versätze bedingter Sprünge und von jal zählen Bytes ab der Befehlsadresse.
  • B- und J-Format lassen Bit 0 weg, das null ist.
  • Setze einen Versatz zusammen, bevor du sein Vorzeichen interpretierst.
  • Der Operationscode bestimmt, wie die übrigen Bits aufgeteilt werden.
  • Werden ein Befehl und sein Ziel gemeinsam verschoben, bleibt ihr relativer Abstand erhalten.
4

Prozeduraufrufe und Speicherung auf dem Stack

Ein Aufruf muss Argumente übergeben, benötigte Zustände erhalten und an die richtige Stelle zurückkehren. Registerkonventionen ermöglichen die Zusammenarbeit getrennter Prozeduren; Stackframes halten die Zustände verschachtelter und rekursiver Aufrufe auseinander. Dieselben Adressberechnungen wie bei Speicheroperanden implementieren auch Zeiger, Strukturfelder und Zeichenketten. Verfolge einen Wert über einen Aufruf hinweg ebenso sorgfältig wie über einen arithmetischen Befehl.

4.1 Übergib Argumente und erhalte Registerwerte

Ein Prozeduraufruf überträgt Steuerung und Daten zwischen unabhängig geschriebenen Codeteilen. Die Aufrufkonvention legt für beide Seiten fest, wohin Argumente und Ergebnisse gehören und welche Registerwerte erhalten bleiben müssen.

Die Aufruf- und Rückkehrfolge

Der Aufrufer legt die ersten acht Ganzzahlargumente in a0–a7 ab; weitere Argumente kommen auf den Stack. Die Prozedur gibt ein ganzzahliges Ergebnis in a0 zurück und verwendet bei einem 64-Bit-Ergebnis in RV32 zusätzlich a1.

jal ra, f speichert seine eigene Adresse plus 4 in ra und springt dann zu f. jal f ist die Kurzschreibweise. Der Pseudobefehl call f wird für größere Reichweite normalerweise zu auipc und jalr erweitert; der Linker kann ihn zu einem jal verkürzen. In beiden Fällen folgt die Rückadresse auf die tatsächliche Aufruffolge.

ret entspricht jalr zero, 0(ra). Der Befehl springt über die gesicherte Adresse zurück und verwirft eine neue Rückadresse. Auch ein einfaches j f verwirft seine Rückadresse und richtet daher keinen gewöhnlichen Aufruf mit Rückkehr ein.

Entscheiden, wer einen Wert sichert

Zu den Caller-saved-Registern gehören t0–t6, a0–a7 und ra. Die aufgerufene Prozedur darf sie verändern. Benötigt der Aufrufer einen ihrer aktuellen Werte später, sichert er diesen vor dem Aufruf.

Zu den Callee-saved-Registern gehören s0–s11 und sp. Eine aufgerufene Prozedur, die ein s-Register verändert, muss den Eingangswert sichern und vor der Rückkehr wiederherstellen. Durch das Wiederherstellen von sp gelangt der Aufrufer zu seinem eigenen Frame zurück. Der Prozessor erzwingt diese Zusagen nicht; kompilierte Module arbeiten zusammen, weil ihre Software die ABI einhält.

Angenommen, ein Aufrufer hat einen Schleifenzähler in t0 und ruft eine Hilfsprozedur auf. Diese darf t0 für ihre eigene Berechnung verwenden. Wird der Zähler nach s1 verschoben, kann er Hilfsaufrufe überdauern. Die umgebende Prozedur muss dann aber den s1-Wert erhalten, den sie von ihrem eigenen Aufrufer erhalten hat.

Eine Blattprozedur benötigt möglicherweise keinen Frame

Eine Blattprozedur ruft keine andere Prozedur auf. Diese Funktion berechnet (g + h) - (i + j) aus Argumenten in a0–a3:

leaf:
add t0, a0, a1
add t1, a2, a3
sub a0, t0, t1
ret

Sie verwendet nur Caller-saved-Arbeitsregister und lässt ra unverändert. Für die Eingaben 9, 4, 6 und 1 gibt sie 6 zurück. Sie benötigt keinen Stackframe.

Verwendet die Berechnung stattdessen s0, muss sie dieses Register erhalten:

addi sp, sp, -16
sw s0, 12(sp)
add t0, a0, a1
add t1, a2, a3
sub s0, t0, t1
mv a0, s0
lw s0, 12(sp)
addi sp, sp, 16
ret

Das Ergebnis wird nach a0 verschoben, bevor das alte s0 wiederhergestellt wird. War der Eingangswert von s0 gleich 1234, ist er danach noch immer 1234. Der reservierte Frame ist 16 Bytes groß, um die standardmäßige Stackausrichtung einzuhalten, obwohl das gesicherte Wort nur 4 Bytes belegt.

Ein Aufruf lässt sich als sechs Handlungen lesen: Argumente ablegen, Steuerung übertragen, Speicher beschaffen, Aufgabe ausführen, Ergebnis ablegen und zurückkehren. Unterscheide an jeder Grenze den zurückgegebenen Wert von den älteren Werten, die die Prozedur nach der Konvention erhalten muss.

Wichtige Erkenntnisse

  • Ganzzahlargumente beginnen in a0–a7, und das erste Ergebnis steht in a0.
  • ra zeigt hinter den tatsächlichen Aufrufbefehl oder die erweiterte Aufruffolge.
  • Aufrufer sichern weiterhin benötigte Caller-saved-Werte; aufgerufene Prozeduren stellen veränderte gesicherte Register wieder her.
  • Eine Blattprozedur, die nur Caller-saved-Register verwendet, benötigt möglicherweise keinen Frame.
  • Verschiebe das Ergebnis, bevor du ein gesichertes Register wiederherstellst, das es enthielt.

4.2 Gib jedem Aufruf einen eigenen Stackframe

Verschachtelte Aufrufe brauchen einen Platz für die Daten des äußeren Aufrufs. Der Stack speichert nach dem Prinzip „zuletzt hinein, zuerst heraus“: Der jüngste Aufruf endet zuerst und gibt seinen eigenen Frame frei.

Unterhalb des alten Stackpointers reservieren

Der Stack wächst zu niedrigeren Adressen hin. addi sp, sp, -16 reserviert 16 Bytes; addi sp, sp, 16 gibt sie frei. Die Standard-ABI hält sp während der gesamten Ausführung auf einem Vielfachen von 16. Ein Frame kann gesicherte Register, die Rückadresse, ausgelagerte Werte, lokale Objekte und Platz für ausgehende Stackargumente enthalten.

Ein häufiges Muster für eine Prozedur, die weitere Prozeduren aufruft, ist:

addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
# body, including calls
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret

Die Versätze beziehen sich auf das neue sp. Ist es 0x2000, belegt das gesicherte ra die Adressen 0x200C bis 0x200F. Der zugehörige Epilog stellt Werte wieder her, solange der Frame noch existiert, und gibt ihn dann frei. Jeder Rückkehrpfad muss die Reservierung ausgleichen.

Ein Framepointer, üblicherweise s0, kann lokalen Objekten feste Versätze geben, wenn sich sp innerhalb einer Prozedur ändert. Sein Eingangswert muss wie bei jeder anderen Verwendung von s0 erhalten bleiben.

Rekursion hält getrennte Kopien

Die Fakultät benötigt das ursprüngliche n, nachdem fact(n - 1) berechnet wurde. Außerdem benötigt sie ihre eigene Rückadresse, nachdem das rekursive jal den Wert in ra überschrieben hat:

fact:
addi sp, sp, -16
sw ra, 12(sp)
sw a0, 8(sp)
li t0, 2
blt a0, t0, Base
addi a0, a0, -1
jal ra, fact
lw t1, 8(sp)
mul a0, t1, a0
j Done
Base:
li a0, 1
Done:
lw ra, 12(sp)
addi sp, sp, 16
ret

Der Befehl mul aus der M-Erweiterung multipliziert das zurückgegebene innere Ergebnis mit dem ursprünglichen n. Bei fact(3) existieren Frames für 3, 2 und 1 gleichzeitig. Der Basisaufruf gibt 1 zurück; der nächste gibt 2 zurück; der äußere Aufruf gibt 6 zurück. Bei anfänglich sp = 0x7FFFFF00 senken drei Frames den Wert um 48 Bytes auf 0x7FFFFED0. Bei der Rückkehr werden sie in umgekehrter Reihenfolge freigegeben.

Die Basisprüfung behandelt die nicht negativen Werte 0 und 1. Für dieses 32-Bit-Ergebnis ist die Fakultät bis einschließlich 12! exakt: 479.001.600. Bei 13!, also 6.227.020.800, stellen die unteren Produktbits nicht mehr das vollständige Ergebnis dar.

Den Stack im Programmspeicher einordnen

Eine typische Aufteilung enthält Code im Textsegment, globale Daten in den statischen Daten, dynamisch reservierte Objekte im Heap und Aufrufframes im Stack. Der Heap wächst oft nach oben und der Stack nach unten. Die tatsächliche Platzierung bestimmt die Ausführungsumgebung.

Ein Zeiger auf ein lokales Stackobjekt wird ungültig, wenn die Lebensdauer dieses Objekts bei der Rückkehr endet. Das Wiederherstellen von sp löscht die alten Bytes nicht, aber spätere Aufrufe können sie erneut verwenden. Ebenso kann das Verändern von sp bei weiterer Verwendung alter Sicherungsversätze dazu führen, dass Bytes aus dem falschen Frame gelesen werden.

Wichtige Erkenntnisse

  • Subtrahiere von sp zum Reservieren; addiere denselben Betrag zum Freigeben.
  • Die Standard-ABI verlangt eine Stackausrichtung auf 16 Bytes.
  • Ein verschachtelter Aufruf überschreibt ra; sichere daher die äußere Rückadresse.
  • Jede rekursive Aktivierung besitzt getrennte gesicherte Argumente und einen eigenen Rückkehrzustand.
  • Alle Austrittspfade müssen den Frame und die erforderlichen Register wiederherstellen.
  • Ein lokales Objekt überdauert seinen Stackframe nicht.

4.3 Verwende Adressen für Arrays, Strukturen und Zeichenketten

Ein Zeiger ist ein Adresswert. Assemblersprache implementiert eine Dereferenzierung, indem sie diese Adresse als Basis für einen Lade- oder Speicherzugriff verwendet, und Zeigerarithmetik durch Addition eines Byteabstands.

Dem Zeiger folgen und dann auf den Wert zugreifen

Mit int *p in a0 liest lw t0, 0(a0) den Wert von *p, während sw t0, 0(a0) ihn schreibt. Das Register enthält die Adresse; der Speicher an dieser Adresse enthält die ganze Zahl. Ist eine Zeigervariable selbst im Speicher abgelegt, lade zuerst diesen Zeigerwert und verwende ihn dann als Basis für einen zweiten Ladezugriff.

Um die Adresse eines lokalen C-Objekts zu ermitteln, wird adressierbarer Speicher benötigt, normalerweise ein Stackplatz. Ein Hardwareregister hat keine Speicheradresse, die als &x übergeben werden kann.

C-Zeigerarithmetik skaliert automatisch. Bei RV32 ILP32 addiert das Erhöhen von int *p den Wert 4 Bytes, während das Erhöhen von char *p 1 addiert. Assemblersprache hat keinen Zeigertyp, der die Skalierung vorgibt:

addi a0, a0, 4    # next int
addi a1, a1, 1    # next char

Ein Arrayargument ist seine Basisadresse, keine Kopie seiner Elemente. Übergib die Elementanzahl separat, wenn eine Prozedur sie benötigt; der Zeiger allein kodiert die Arraylänge nicht.

Strukturfelder haben Byteversätze

Für struct item { int id; int score; }; belegt id die Bytes 0–3 und score die Bytes 4–7. Mit der Adresse des Objekts in a0 liest lw t0, 4(a0) den Punktwert. Im Allgemeinen folgen Feldversätze den Layout- und Padding-Regeln der ABI. Leite nicht jeden Feldversatz allein durch Zählen der Quelldeklarationen ab.

Bei einem Array dieser 8-Byte-Strukturen beginnt das nächste Element 8 Bytes später. Innerhalb jedes Elements bleibt der Punktwert 4 Bytes vom Beginn dieses Elements entfernt. Elementabstand und Feldversatz sind getrennte Teile der Adressberechnung.

Eine nullterminierte Zeichenkette kopieren

Eine C-Bytezeichenkette endet mit einem Nullbyte. Um sie zu kopieren, lies und speichere ein Byte, bevor du entscheidest, ob der Kopiervorgang abgeschlossen ist:

Loop:
lbu t0, 0(a1)
sb t0, 0(a0)
addi a0, a0, 1
addi a1, a1, 1
bnez t0, Loop
ret

Hier ist a1 die Quelle und a0 das Ziel. Das Ziel muss genügend Platz haben. lbu liest ein vorzeichenloses Byte; sb speichert nur dessen untere 8 Bits. Beide Zeiger werden um 1 weitergesetzt, auch im letzten Durchlauf. Der bedingte Sprung prüft das kopierte Byte, sodass auch die abschließende Null gespeichert wird.

Das Kopieren von „RISC“ benötigt fünf Durchläufe für vier Buchstaben und das Nullbyte. Bei fünf Befehlen pro Durchlauf plus ret werden 26 Befehle ausgeführt. Eine leere Zeichenkette benötigt dennoch einen Durchlauf, um ihren Nullterminator zu kopieren. Ein Ladebefehl für Wörter würde vier Bytes auf einmal lesen und diesen byteweisen Algorithmus nicht implementieren.

UTF-8 kann ein Zeichen in mehreren Bytes kodieren. Diese Schleife kopiert diese Bytes unverändert; sie zählt keine angezeigten Zeichen. Zeigerbasierte Arrayschleifen können entsprechend um den Elementabstand weitergehen, ohne jedes Mal einen skalierten Index neu zu berechnen. Optimierende Compiler können diese Umwandlung auch vornehmen, wenn das C-Programm gewöhnliche Arrayindizes verwendet.

Wichtige Erkenntnisse

  • Die Dereferenzierung verwendet die im Zeiger enthaltene Adresse.
  • Zeigerarithmetik in Assemblersprache muss die Elementgröße berücksichtigen.
  • Arrayparameter benötigen eine separate Länge, wenn die Prozedur eine Anzahl verwendet.
  • Strukturabstand und Feldversatz erfüllen unterschiedliche Aufgaben in einer Adresse.
  • Eine Schleife zum Kopieren von Zeichenketten muss das abschließende Nullbyte speichern.
5

Vom Quellcode zur Ausführung

Ein Programm durchläuft Werkzeuge, die Unterschiedliches über seine endgültige Form wissen. Ein Compiler wählt Befehle aus, ein Assembler kodiert sie, ein Linker fügt Module zusammen und ein Lader bereitet die Ausführung vor. Symbole und Relokationen tragen unvollständige Verweise zwischen den Phasen weiter. Wenn du einen Aufruf vom Quellcode bis zu seiner endgültigen Adresse verfolgst, werden die Rollen von Objektdateien, gemeinsam genutzten Bibliotheken und Startcode anschaulich.

5.1 Übersetze Quellcode in eine relokierbare Objektdatei

Eine Quelldatei wird durch mehrere Übersetzungsphasen zu ausführbaren Befehlen. Jede Phase hat eine andere Aufgabe, und Zwischendateien enthalten Informationen, die spätere Phasen benötigen.

Der Compiler wählt Befehle

Für C führt der übliche Weg vom Quellcode über Assemblersprache, Objektdatei und ausführbare Datei zu einem laufenden Prozess. Ein Compiler analysiert Quellcode, optimiert ihn, weist Register zu und wählt Befehle für eine Ziel-ISA. Ein Compiler-Treiber kann mehrere Phasen mit einem Befehl ausführen.

gcc -S program.c stoppt nach dem Erzeugen von Assemblercode. gcc -c program.c erzeugt eine Objektdatei, ohne zu linken. Der Zielcompiler bestimmt, welche ISA der Assemblercode verwendet; ein RISC-V-Programm benötigt eine auf RISC-V ausgerichtete Werkzeugkette.

Bei -O0 laufen Werte häufig über Stackplätze, und der Assemblercode folgt den einzelnen Quellanweisungen eng. Bei -O2 können Registerallokation, entfernte Arbeitsschritte und umstrukturierte Schleifen diese Entsprechung verschleiern. Eine C-Anweisung hat keine einzelne feste Übersetzung in Assemblersprache.

Der Assembler kodiert und erfasst

Der Assembler wandelt Befehls- und Registernamen in Bits um, erweitert Pseudobefehle und löst Labels auf, deren Position er bestimmen kann. Ein Vorwärtssprung verweist auf ein später auftretendes Label. Ein Assembler kann zuerst Positionen sammeln und dann Verweise kodieren oder Stellen vormerken, die ausgefüllt werden, sobald das Ziel bekannt ist.

Direktiven teilen dem Assembler mit, wie er die Ausgabe organisieren soll; der Prozessor führt sie nicht aus:

.data
.align 2
count:
.word 5
message:
.string "hi"
.text
.globl main
main:
# instructions follow

.word 5 gibt einen 4-Byte-Datenwert aus. .string "hi" gibt die Zeichenbytes und eine abschließende Null aus. Beim GNU-RISC-V-Assembler richtet .align 2 das nächste Element an einer 4-Byte-Grenze aus. .globl macht ein Symbol für andere Objektdateien sichtbar. Labels bezeichnen Positionen; sie reservieren selbst keine Bytes.

Was eine Objektdatei enthält

Eine relokierbare Objektdatei enthält typischerweise einen Header, Code in Textabschnitten, statische Daten, eine Symboltabelle und Relokationsinformationen. Die Symboltabelle unterscheidet hier definierte Namen von Verweisen, die anderswo definiert sind. Optionale Debuginformationen verbinden Befehle mit Quellcodezeilen; -g fordert sie an.

Zu den üblichen Objektformaten gehören ELF, Mach-O und PE/COFF. Das Format organisiert Maschinencode und Metadaten; es unterscheidet sich von der ISA, die die Bits jedes Befehls festlegt.

Für call printf kann der Assembler eine auipc/jalr-Folge ausgeben, ohne die endgültige Funktionsadresse zu kennen. Er erfasst ein undefiniertes Symbol und Relokationseinträge, damit der Linker den Verweis vervollständigen kann. Der Abstand eines lokalen Sprungs kann bereits bekannt sein, wenn sich sein Ziel mit demselben Abschnitt bewegt; spätere Layoutänderungen wie eine Verkürzung können dennoch eine Anpassung erfordern.

Andere Ausführungswege

Ein Interpreter führt Operationen durch ein anderes laufendes Programm aus. Ein Just-in-time-Compiler erzeugt während der Ausführung Maschinencode, häufig nachdem er oft verwendeten Code erkannt hat. Java verwendet gewöhnlich Bytecode mit anschließender Interpretation und JIT-Kompilierung; Python verwendet gewöhnlich einen Interpreter. Ein JIT-Compiler nimmt Kompilieraufwand während der Ausführung in Kauf, um wiederholte Ausführungen zu beschleunigen.

Eine Objektdatei ist also mehr als eine reine Befehlsliste, aber noch kein vollständiger laufender Prozess. Nicht aufgelöste Symbole und die endgültige Anordnung müssen noch von Linker und Lader bearbeitet werden.

Wichtige Erkenntnisse

  • Der Compiler wählt Befehle für eine Ziel-ISA aus und optimiert sie.
  • Der Assembler kodiert Befehle und erweitert Pseudobefehle.
  • Direktiven ordnen die Ausgabe; Labels bezeichnen Positionen.
  • Symbole und Relokationen erhalten Verweise, die spätere Phasen auflösen müssen.
  • -S stoppt bei Assemblersprache, während -c bei einer Objektdatei stoppt.
  • Interpreter und JIT-Compiler bieten unterschiedliche Wege zur Ausführung.

5.2 Linke Module und starte einen Prozess

Ein Linker erstellt aus Objektmodulen und Bibliotheken eine ausführbare Datei. Ein Lader verwendet diese Datei, um einen laufenden Prozess zu erzeugen. Wenn du weißt, welche Phase für eine Adresse zuständig ist, kannst du sowohl Relokations- als auch Startfehler erklären.

Module anordnen und Verweise anpassen

Der Linker platziert Eingabeabschnitte im Adressraum der ausführbaren Datei, löst Symboldefinitionen auf und passt Verweise anhand von Relokationseinträgen an. Ein Name, den kein gelinktes Modul definiert, erzeugt einen Fehler „undefined reference“.

Angenommen, Text beginnt bei 0x00010000 und Daten bei 0x10000000. Objekt A steuert 0x100 Textbytes und 0x20 Datenbytes bei; B folgt unmittelbar. Der Text von B beginnt bei 0x00010100 und seine Daten bei 0x10000020.

Ein jal in A beim Textversatz 0x40 hat die Adresse 0x00010040. Beginnt procB am Textanfang von B, beträgt der Versatz des Aufrufs 0xC0 oder 192 Bytes, mit ra kodiert als 0x0C0000EF. Eine Variable beim Versatz 0x10 in den Daten von B landet bei 0x10000030. Bei gp = 0x10000800 beträgt ihr Versatz −2000, was in einen vorzeichenbehafteten 12-Bit-Ladeversatz passt. Dies sind getrennte Berechnungen für Text und Daten.

Wählen, wie Bibliothekscode bereitgestellt wird

Statisches Linken kopiert benötigten Bibliotheks-Objektcode in die ausführbare Datei. Das Programm führt diesen Code mit sich, wodurch die Datei größer wird; um eine Bibliothekskorrektur zu übernehmen, muss es erneut gelinkt werden. Statische Bibliotheken beseitigen nicht die Abhängigkeit des Programms von seiner Betriebssystemschnittstelle.

Dynamisches Linken hält Bibliothekscode in gemeinsam genutzten Dateien wie .so, .dll oder .dylib. Kompatible Bibliothekscodeseiten können von Prozessen gemeinsam genutzt werden. Eine kompatible aktualisierte Bibliothek kann Programme bedienen, ohne jede ausführbare Datei erneut zu linken; die benötigte Version muss aber verfügbar sein, wenn sie gebraucht wird.

Bei verzögerter Bindung in ELF führt der erste Aufruf über ein Verbindungsstück der Prozedurverknüpfungstabelle (PLT). Der dynamische Linker löst das Ziel auf und erfasst es in der globalen Versatztabelle (GOT); spätere Aufrufe verwenden diese erfasste Adresse. Andere Konfigurationen lösen Symbole beim Laden auf.

Positionsunabhängiger Code erreicht lokale Ziele über relative Adressierung, einschließlich auipc-Folgen, und verwendet für externe Ziele Indirektion. Die eigenen relativen Abstände einer Bibliothek bleiben gültig, wenn sie als Einheit verschoben wird. Das Binden externer Verweise kann weiterhin Relokationsarbeit erfordern.

Der Lader erzeugt den laufenden Zustand

Der Lader liest Metadaten der ausführbaren Datei, erstellt einen Adressraum, bildet Code und Daten ab oder kopiert sie, richtet Argumente und den anfänglichen Stack ein, initialisiert den erforderlichen Registerzustand und überträgt die Steuerung an den Startcode. Der Startcode ruft main auf; nach dessen Rückkehr fordert der Laufzeitcode das Beenden des Prozesses an. Moderne Systeme können Dateiseiten abbilden und bei Bedarf in den Speicher laden.

ecall fordert einen Dienst der Umgebung an. Unter RISC-V-Linux enthält a7 die Systemaufrufnummer und a0–a5 enthalten Argumente. li a7, 93, li a0, 0, ecall fordert die Beendigung mit Status 0 an; Dienst 64 ist write. Die Dienstnummern eines Simulators stammen aus dessen eigener Schnittstelle.

Eine beim Linken fehlende Bibliothek verursacht einen Linkfehler. Eine benötigte gemeinsam genutzte Bibliothek, die nach dem Linken entfernt wird, kann dagegen das Laden verhindern oder einen Fehler bei der Bindung zur Laufzeit verursachen. Die ausführbare Datei kann erfolgreich erstellt worden sein, obwohl sie jetzt nicht starten kann.

Wichtige Erkenntnisse

  • Der Linker ordnet eine ausführbare Datei an und löst Symbolverweise auf und passt sie an.
  • Der Lader bildet diese ausführbare Datei ab und erzeugt den anfänglichen Prozesszustand.
  • Statisches Linken kopiert Bibliothekscode; dynamisches Linken benötigt kompatible gemeinsam genutzte Bibliotheken.
  • Verzögerte Bindung erfasst ein aufgelöstes Ziel für spätere Aufrufe.
  • Positionsunabhängigkeit erhält lokale relative Verweise beim Verschieben.
  • Die Dienstnummern von ecall gehören zur Ausführungsumgebung.
6

Zielkonflikte beim Befehlssatzentwurf

Befehlssätze treffen unterschiedliche Entscheidungen darüber, wie viel Arbeit ein einzelner Befehl ausdrückt. Vergleiche RISC-V und x86 anhand ihrer Operanden, Kodierungen und Adressberechnungen und verknüpfe diese Entscheidungen dann mit realen Einschränkungen. Für die Rechenleistung müssen Befehlsanzahl, CPI und Taktperiode gemeinsam betrachtet werden; Codedichte und Softwarekompatibilität beantworten andere Fragen. Eine nützliche Beurteilung des Entwurfs nennt die verglichene Arbeitslast und Implementierung.

6.1 Vergleiche RISC-V- und x86-Befehle

RISC und CISC bezeichnen allgemeine Ansätze zum Befehlssatzentwurf. Ein sinnvoller Vergleich betrachtet, wie jeder Ansatz Arbeit darstellt: durch Operandenpositionen, Befehlslänge, Adressberechnung und Kontrollfluss.

Einheitliche Operationen oder umfangreichere Befehle

RISC-Entwürfe bevorzugen einfache Operationen, viele Register und Load-Store-Zugriffe. Der Ansatz entstand aus der Arbeit am IBM-Projekt 801 Mitte der 1970er-Jahre sowie an Berkeley RISC und Stanford MIPS Anfang der 1980er-Jahre. RISC-V, ARM AArch64, MIPS und Power folgen dieser allgemeinen Tradition.

CISC-Entwürfe bieten Befehle variabler Länge, vielfältigere Adressierung und Operationen, die mehrere Schritte verbinden können. x86 entstand aus dem 1978 eingeführten 8086; zu früheren CISC-Familien gehören VAX und Motorola 68000.

In der Intel-x86-Syntax liest add eax, [ebx+8] einen 32-Bit-Wert aus dem Speicher und addiert ihn zu eax. Die entsprechende RISC-V-Operation benötigt einen Ladebefehl mit anschließender Addition:

lw t0, 8(s1)
add a0, a0, t0

Der x86-Befehl verbindet einen Speicherzugriff mit einer Addition. RISC-V stellt sie als separate Befehle dar. x86 nennt einen 32-Bit-Wert Doppelwort; sein Wort ist 16 Bit breit. Ein Wort in den RV32-Beispielen ist 32 Bit breit.

Kodierung und Adressierung

Ein x86-Befehl ist 1 bis 15 Bytes lang. RISC-V-Basisbefehle belegen 4 Bytes; über C sind komprimierte Formen mit 2 Bytes verfügbar. Einheitliche Feldpositionen vereinfachen die Dekodierung. Variable Längen können Platz sparen, indem sie kurze Kodierungen für häufige Operationen verwenden.

RISC-V verwendet Registeroperanden, Direktoperanden, Basisadressen mit Versatz und PC-relative Adressen. jalr gehört zur registerbasierten Adressierung, während bedingte Sprünge, jal und auipc den pc verwenden.

x86 kombiniert außerdem Basis, skalierten Index und Versatz. mov eax, [ebx + esi*4 + 8] liest ein indiziertes 4-Byte-Element mit einem zusätzlichen Versatz in einem Befehl. RISC-V bildet vor dem Laden den skalierten Index und die effektive Adresse mit separaten Befehlen. Die Arbeit muss in beiden Prozessoren intern dennoch stattfinden.

Register und Vergleiche

RISC-V hat 32 Ganzzahlregister einschließlich seines fest auf null gesetzten Registers. Herkömmliches x86-64 hat 16 Allzweckregister; 32-Bit-x86 hat 8. Viele arithmetische x86-Befehle verwenden ein Ziel, das zugleich Quelle ist: add eax, ebx verändert eax. Das add mit drei Operanden von RISC-V kann beide Quellwerte erhalten und zugleich in ein drittes Register schreiben.

Arithmetische x86-Befehle und cmp setzen Flags; ein späterer bedingter Sprung wie jl prüft diese Flags. RISC-V verwendet blt, um direkt zu vergleichen und zu springen. ARM AArch64 verwendet ebenfalls Flags, daher lässt sich eine ISA allein anhand vorhandener Flags nicht als CISC einordnen.

Was „reduziert“ bedeutet

RISC bedeutet nicht, dass eine moderne ISA insgesamt sehr wenige Befehle haben muss. RV32I hat 40 Basisbefehle, aber optionale Erweiterungen ergänzen viele weitere. Die zentrale Idee sind einheitliche, relativ einfache Operationen und Kodierungen. Komprimierte Kodierungen zeigen außerdem, dass eine feste Befehlslänge eine Entwurfstendenz ist und keine Definition, die jeder RISC-Befehl erfüllen muss.

Wichtige Erkenntnisse

  • RISC-V trennt Speicherzugriff und Arithmetik.
  • x86 kann Speicheroperanden und Arithmetik in einem Befehl verbinden.
  • Befehlslänge und Adressierungsarten beeinflussen Dekodierung und Codedichte.
  • RISC-V vergleicht innerhalb eines Sprungbefehls; x86 verwendet häufig Statusflags.
  • Die Befehlsanzahl allein definiert keinen RISC-Entwurf.

6.2 Beurteile Geschwindigkeit, Codegröße und Kompatibilität

Ein Befehlssatz prägt die Arbeit, die ein Programm ausdrückt, doch die Ausführungsgeschwindigkeit hängt auch vom Prozessor ab, der ihn implementiert. Vergleiche das gemessene oder modellierte Verhalten eines Programms, nicht die Anzahl der Operationen, die ein einzelner Befehl verspricht.

Alle drei Leistungsfaktoren verbinden

Die CPU-Zeit entspricht der Befehlsanzahl mal den mittleren Taktzyklen pro Befehl (CPI) mal der Taktperiode. Gleichwertig kannst du die Befehlsanzahl mal CPI durch die Taktfrequenz dividieren. Eine geringere Befehlsanzahl kann durch einen höheren CPI-Wert oder einen langsameren Takt aufgewogen werden.

Bei 2 GHz dauert jeder Taktzyklus 0,5 ns. Eine Version mit 1,0 Milliarden Befehlen und einem CPI-Wert von 2,0 benötigt 2,0 Milliarden Taktzyklen und dauert 1,0 s. Eine andere mit 1,3 Milliarden Befehlen und einem CPI-Wert von 1,2 benötigt 1,56 Milliarden Taktzyklen und dauert 0,78 s. Die zweite führt 30 % mehr Befehle aus, ist aber etwa 1,28-mal so schnell. Der CPI-Wert ist ein Durchschnitt für dieses Programm auf dieser Implementierung, keine allgemeingültige Eigenschaft einer ISA.

Komplexe Befehle benötigen weiterhin interne Arbeit

Viele moderne x86-Prozessoren übersetzen die meisten Befehle in einfachere interne Mikrooperationen. Diese können anschließend Ausführungstechniken nutzen, die denen moderner RISC-Kerne ähneln. Die externe ISA bleibt für Kodierung, Dekodierung und Kompatibilität wichtig, selbst wenn die internen Operationen ähnlich aussehen.

Ein komplexer Befehl garantiert keine feste Anzahl von Taktzyklen. Ein Befehl zum Kopieren eines Blocks und eine einfache Kopierschleife können je nach Prozessor und Arbeitslast unterschiedlich schnell sein. Prozessormodell, Datengröße und Speicherverhalten sind wichtig; eine Auflistung mit nur einem Quellbefehl kann das Ergebnis nicht entscheiden.

Codedichte ist ein eigener Kennwert

Kürzere Maschinencodefolgen können Speicherplatz sparen und den Befehlscache entlasten. Variable CISC-Befehlslängen erhöhen oft die Dichte. Die RISC-V-Erweiterung C bietet neben 32-Bit-Befehlen 16-Bit-Kodierungen für häufige Operationen. Die Spezifikation nennt eine typische Nutzung für 50–60 % der Befehle, wodurch der Code ungefähr 25–30 % kleiner wird.

Von 100 Befehlen, von denen die Hälfte komprimiert ist, verwenden 50 je 2 Bytes und 50 je 4: insgesamt 300 statt 400 Bytes. Die Anzahl der ausgeführten Befehle bleibt 100. Eine Größenverringerung allein sagt keinen Speedup voraus, weil die Laufzeit von mehr als den geholten Bytes abhängt.

Kompatibilität und Werkzeuge prägen die Wahl

Vorhandene x86-Binärdateien und -Bibliotheken machen Kompatibilität besonders wertvoll. RISC-V friert Basisbefehlssätze ein und ergänzt Erweiterungen. RV32I-Code kann auf einer Implementierung mit RV32I-Unterstützung laufen, wenn ABI und Ausführungsumgebung übereinstimmen; die Verwendung von M oder V stellt zusätzliche Anforderungen. RV32E ist ein anderer Basisbefehlssatz mit reduzierter Registerzahl und verspricht keine RV32I-Kompatibilität.

ARM wird verbreitet in Mobilgeräten sowie Laptops und Servern eingesetzt; x86 wird häufig in PCs und Servern verwendet; RISC-V dient eingebetteten Systemen, Beschleunigern, Forschung und Lehre. Softwareverfügbarkeit, Lizenzierung und die Qualität bestimmter Chips sind bei der Plattformwahl alle wichtig.

Handgeschriebene Assemblersprache kann bei einer kleinen kritischen Routine helfen oder einen speziellen Befehl zugänglich machen, bindet Code aber an eine ISA und erhöht den Wartungsaufwand. Optimierende Compiler beherrschen Registerallokation und Codeumstrukturierung gut. Begründe anhand von Messungen, wenn du klaren Quellcode durch Assemblersprache ersetzt.

Wichtige Erkenntnisse

  • Vergleiche Befehlsanzahl, CPI und Taktperiode gemeinsam.
  • Mikrooperationen ändern die für Software sichtbare ISA nicht.
  • Komprimierte Kodierungen verringern die Bytezahl, ohne notwendigerweise die Anzahl ausgeführter Befehle zu verringern.
  • Binärkompatibilität erfordert auch eine passende ABI und Umgebung.
  • Miss ein bestimmtes Programm und eine bestimmte Implementierung, bevor du behauptest, eine ISA sei schneller.

Befehlssätze und Assemblersprache: Beispiele

6 Interaktive Lektionen

1

Verfolge Operanden von einem Ausdruck bis zum Speicher

15 Schritte
Lies einen Befehl, verfolge temporäre Werte, aktualisiere ein Array und erstelle eine größere Konstante.
  1. 1
    EinführungVerfolge jeweils einen Wert

    Ein Assemblerbefehl nennt sowohl seine Operation als auch seine Operanden. Beginne mit einer lesbaren Registerberechnung, lade dann einen Wert aus dem Speicher und speichere ein aktualisiertes Ergebnis. Erstelle schließlich eine Konstante, die für einen einzelnen Direktoperanden zu groß ist. Verfolge bei jedem Schritt genau, was sich ändert: ein Zielregister, eine Gruppe von Speicherbytes oder die nächste Befehlsadresse.

  2. 2
    ErklärungLies die Befehlssatzvereinbarung

    Die ISA legt fest, was add a0, a1, a2 tun muss: die beiden Quellen lesen und ihre Summe in das Ziel schreiben. Die Mikroarchitektur bestimmt, wie der Chip diese Anforderung ausführt. Assemblersprache gibt der Anforderung lesbare Namen; der Assembler erzeugt ihre kodierten Bits. Unterschiedliche Chips, die dieselbe Befehlssatzvereinbarung implementieren, müssen beim sichtbaren Ergebnis übereinstimmen.

  3. 3
    HerausforderungZähle Befehle nach dem Assemblieren

    Ein Assembler erweitert ein li zu zwei Maschinenbefehlen, gefolgt von einem add. Wie viele Befehlsbytes ergeben diese beiden Assemblerzeilen, wenn nur 32-Bit-Kodierungen verwendet werden?

    ---reveal---

    Es gibt drei tatsächliche Befehle mit jeweils 4 Bytes, also 12 Bytes. Der Text hat zwei Zeilen, aber die Erweiterung bestimmt die Größe des Maschinencodes. Labels und Kommentare fügen keine ausgeführten Befehle hinzu.

  4. 4
    BeispielHalte die Zwischensumme fest

    Berechne r = (u + v) - w mit u = 18, v = 7, w = 6 in a1, a2, a3:

    add t0, a1, a2
    sub a0, t0, a3
    

    Nach dem ersten Befehl enthält t0 den Wert 25. Nach dem zweiten enthält a0 den Wert 19. Die drei Eingaben bleiben verfügbar, weil keiner der Befehle ihre Register beschreibt. t0 hat einen Zweck: das erste Ergebnis zu erhalten, bis die Subtraktion es liest.

  5. 5
    HerausforderungÄndere das Ziel und erhalte die Quellen

    Mit denselben Eingaben wird nach der Addition nun sub a3, t0, a3 ausgeführt. Was ändert sich gegenüber dem Schreiben des Ergebnisses nach a0?

    ---reveal---

    Das numerische Ergebnis ist weiterhin 19, ersetzt aber den alten Wert 6 in a3. Der Befehl liest die Quellwerte, bevor er sein Ziel beschreibt. a0 bleibt unverändert, sodass ein Aufrufer, der dort das Ergebnis erwartet, dieses neue Ergebnis nicht erhalten würde.

  6. 6
    ErklärungWandle einen Index in eine Adresse um

    Ein Array ganzer Zahlen beginnt an der Adresse in s1. Jedes int in RV32 ILP32 belegt 4 Bytes, daher beginnt Element 2 genau 8 Bytes nach der Basis und Element 5 genau 20 Bytes danach. Der Ladebefehl lw t0, 8(s1) liest den Wert bei Element 2; er schreibt nicht die Adresse selbst nach t0. Das Basisregister bleibt unverändert.

  7. 7
    BeispielLaden, berechnen, speichern

    Implementiere A[5] = A[2] + r mit r = 19 in a0:

    lw t0, 8(s1)
    add t0, t0, a0
    sw t0, 20(s1)
    

    Ist A[2] gleich 11, liefert der Ladebefehl 11, die Addition ergibt 30, und der Speicherbefehl schreibt 30 nach A[5]. Das Lesen und Schreiben des Speichers benötigt separate Befehle um die arithmetische Operation herum. Der Wert von A[2] bleibt 11.

  8. 8
    HerausforderungErkenne die falsche Einheit in einem Versatz

    Jemand ändert die erste Zeile zu lw t0, 2(s1), weil der Ausdruck A[2] nennt. Was ist schiefgegangen?

    ---reveal---

    Der Versatz beträgt 2 Bytes, nicht zwei Elemente. Element 2 benötigt 8 Bytes ab der Basis. Die veränderte Adresse ist bei ausgerichteter Basis außerdem für ein Wort nicht natürlich ausgerichtet, sodass der Zugriff fehlschlagen oder eine besondere Behandlung benötigen kann, bevor er überhaupt brauchbare Daten liefern könnte.

  9. 9
    BeispielLies ein Byte aus einem gespeicherten Wort

    Speichere 0x7193D2A6 an einer ausgerichteten Adresse auf einem Little-Endian-Rechner. Seine Bytes in aufsteigender Adressreihenfolge sind 0xA6, 0xD2, 0x93, 0x71. Ein Byte-Ladebefehl bei Versatz 1 wählt 0xD2 aus. lbu liefert 210, während lb durch Erweiterung von Bit 7 in die oberen Registerpositionen −46 liefert. Das Speicherbyte ist identisch; der Ladebefehl bestimmt die Interpretation.

  10. 10
    ErklärungBestimme den vorzeichenbehafteten unteren Teil

    Erstelle nun 0x00003D20 in einem Register. Seine unteren 12 Bits sind 0xD20, was als vorzeichenbehafteter 12-Bit-Direktoperand −736 darstellt. Der obere Schritt muss daher einen 4096-Byte-Block höher beginnen, als die sichtbare 0x3 nahelegt. Die Wahl von 0x4 ergibt 0x4000, wovon die Subtraktion von 0x2E0 den gewünschten Wert erreicht.

  11. 11
    BeispielErstelle und prüfe die Konstante

    Verwende:

    lui t1, 0x4
    addi t1, t1, -736
    

    Der erste Befehl schreibt 16.384; der zweite subtrahiert 736, sodass 15.648 oder 0x3D20 übrig bleibt. Der untere Teil ist ein Summand mit Vorzeichen und nicht nur eine einzufügende Ziffernfolge. li t1, 0x3D20 überlässt dem Assembler die Wahl des Aufbaus, doch seine Erweiterung belegt weiterhin echte Befehle.

  12. 12
    HerausforderungKorrigiere die Aufteilung einer Konstante

    Eine Aufteilung verwendet lui t1, 0x3 und addi t1, t1, -736. Wie weit liegt ihr Ergebnis von 0x3D20 entfernt, und welches Feld muss korrigiert werden?

    ---reveal---

    Sie ergibt 0x2D20, genau 4096 zu wenig. Erhöhe das obere Feld von 0x3 auf 0x4 und behalte den vorzeichenbehafteten unteren Summanden. Würde der Summand in das positive 0xD20 geändert, überschritte er den positiven vorzeichenbehafteten Bereich von addi.

  13. 13
    TippVerfolge ein Register bis zu seiner letzten Verwendung

    Schreibe beim Verfolgen einer Folge einen Wert neben das Register, das ihn gerade enthält. Streiche ihn erst durch, wenn dieses Register überschrieben wird. Ein späterer Prozeduraufruf birgt das Risiko, temporäre und Argumentregister zu überschreiben, selbst wenn neben dem Aufruf keine ausdrückliche Zuweisung steht. Sichere einen noch benötigten Wert, bevor du diese Grenze überschreitest.

  14. 14
    HerausforderungVerbinde Adress- und Wertschritte

    Bei der Arraybasis s1 ist das Element A[6] gleich 17. t1 enthält die korrekt erstellte Konstante 0x3D20. Was verändert lw t0, 24(s1); add t0, t0, t1; sw t0, 4(s1)?

    ---reveal---

    Die Folge liest A[6], addiert 17 zu 15.648 und speichert 15.665 in A[1]. Die beiden Versätze zählen Bytes, während die Konstante ein arithmetischer Wert ist. Das ursprüngliche Element bei Index 6 und die Basisadresse in s1 bleiben unverändert.

  15. 15
    ZusammenfassungVerfolge die nächste Folge selbst

    Markiere bei einer neuen Assemblerfolge die gelesenen und beschriebenen Register, erweitere alle Pseudobefehle, deren Größe wichtig ist, und übersetze jeden Speicherversatz in Bytes. Prüfe die Erweiterung beim Laden kleiner Werte getrennt von der Byte-Reihenfolge. Prüfe beim Erstellen einer Konstante den oberen Zwischenwert plus seinen vorzeichenbehafteten unteren Summanden, bevor du das Ergebnis als Datum oder Adresse verwendest.

2

Verfolge Arithmetik durch Verzweigungen und Schleifen

14 Schritte
Extrahiere ein Feld, wähle vorzeichenbehaftete Vergleiche und verfolge jeden Pfad durch eine Zählschleife.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMache den Ausführungspfad sichtbar
  2. 2
    BeispielVerschiebe das Feld an die richtige Position
  3. 3
    BeispielBehalte nur die Feldbreite
  4. 4
    HerausforderungSage eine vorzeichenbehaftete Verschiebung voraus
  5. 5
    ErklärungVergleiche mit der richtigen Interpretation
  6. 6
    BeispielUnterscheide einen Sprung von einem Aufruf
  7. 7
    HerausforderungPrüfe ein Muster auf zwei Arten
  8. 8
    BeispielZeichne die beiden Pfade einer if-Anweisung
  9. 9
    ErklärungPrüfe die Schranke vor dem Laden
  10. 10
    BeispielVerfolge drei Ausführungen des Schleifenrumpfs
  11. 11
    HerausforderungZähle den gesamten ausgeführten Pfad
  12. 12
    VerbindungErsetze wiederholte Skalierung durch eine fortlaufende Adresse
  13. 13
    HerausforderungSichere einen Ladezugriff durch Kurzschlussauswertung ab
  14. 14
    ZusammenfassungPrüfe Werte und Pfade gemeinsam
3

Kodiere einen Befehl und rekonstruiere einen bedingten Sprung

12 Schritte
Ordne Operanden Feldern zu, prüfe ein kodiertes Wort und setze einen verstreuten Sprungversatz wieder zusammen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungÜbersetze in beide Richtungen
  2. 2
    ErklärungBestimme Format und Operanden
  3. 3
    BeispielSchreibe die Felder in voller Breite
  4. 4
    BeispielWandle das Wort um und dekodiere es
  5. 5
    HerausforderungBestimme die Wertquelle eines Speicherbefehls
  6. 6
    ErklärungMiss den Rückwärtsabstand
  7. 7
    BeispielTeile den Direktoperanden des Sprungs auf
  8. 8
    BeispielSetze den vollständigen Sprungbefehl zusammen
  9. 9
    HerausforderungErgänze das fehlende Nullbit
  10. 10
    VerbindungErhalte den relativen Abstand beim Verschieben von Code
  11. 11
    HerausforderungErkenne eine falsche Dekodierung am Operationscode
  12. 12
    ZusammenfassungPrüfe ein Maschinenwort rückwärts
4

Erhalte Werte über verschachtelte Aufrufe hinweg

15 Schritte
Erstelle eine korrekte Wrapper-Prozedur, verfolge rekursive Frames und eine Bytezeichenkette im Speicher.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungSichere, was der Aufrufer noch benötigt
  2. 2
    BeispielLege das Ergebnis der Wrapper-Prozedur fest
  3. 3
    ErklärungWende die Zusagen zum Erhalt von Werten an
  4. 4
    HerausforderungWähle den Zustand, der den Aufruf überdauern muss
  5. 5
    BeispielReserviere und fülle den Frame
  6. 6
    BeispielVerbinde die Ergebnisse vor der Speicherfreigabe
  7. 7
    ErklärungWiederhole das Muster rekursiv
  8. 8
    HerausforderungLöse die gesicherten Argumente beim Zurückkehren auf
  9. 9
    ErklärungUnterscheide eine Adresse von ihrem Objekt
  10. 10
    BeispielWähle Byteoperationen für eine Zeichenkette
  11. 11
    HerausforderungEnde an den richtigen Adressen
  12. 12
    TippStelle den gesicherten Wert wieder her, nicht das neue Ergebnis
  13. 13
    VerbindungDie Objektlebensdauer folgt dem Frame
  14. 14
    HerausforderungErhalte einen Zielzeiger über einen Hilfsaufruf hinweg
  15. 15
    ZusammenfassungPrüfe eine vollständige Aufrufgrenze
5

Verfolge einen Aufruf vom Quellcode bis zum Programmstart

11 Schritte
Verfolge ein Symbol durch Kompilierung, Assemblierung, Linken, dynamische Auflösung und Laden des Prozesses.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungFrag dich, was jedes Werkzeug weiß
  2. 2
    ErklärungKompiliere für eine Ziel-ISA
  3. 3
    BeispielLass den externen Aufruf unvollständig
  4. 4
    HerausforderungUnterscheide Datenausgabe und Ausführung
  5. 5
    BeispielPlatziere das Zielmodul
  6. 6
    BeispielPasse den relativen Verweis an
  7. 7
    ErklärungWähle, wann Bibliothekscode bereitgestellt wird
  8. 8
    HerausforderungBestimme die Phase des Fehlers
  9. 9
    VerbindungErzeuge einen Prozess vor dem Aufruf von main
  10. 10
    HerausforderungErkläre, warum eine Objektdatei nicht das fertige Programm ist
  11. 11
    ZusammenfassungOrdne einen neuen Fehler seiner Phase zu
6

Vergleiche Entwürfe mit dem richtigen Kennwert

10 Schritte
Unterscheide sichtbare Befehle, kodierte Bytes und Ausführungszeit, wenn du die Wahl einer ISA beurteilst.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungHalte drei Messgrößen auseinander
  2. 2
    BeispielVergleiche die sichtbare Speicheroperation
  3. 3
    ErklärungSetze Kodierung und Entwurfsentscheidung in Beziehung
  4. 4
    HerausforderungOrdne ein Merkmal sorgfältig ein
  5. 5
    BeispielBerechne die erste Ausführungszeit
  6. 6
    BeispielVergleiche eine größere Befehlsanzahl
  7. 7
    HerausforderungUnterscheide Byteeinsparung und Speedup
  8. 8
    VerbindungKompatibilität reicht über die ISA hinaus
  9. 9
    HerausforderungBeurteile eine Aussage zur Assembleroptimierung
  10. 10
    ZusammenfassungNenne die Belege für den Vergleich

Befehlssätze und Assemblersprache: Übungsaufgaben

60 Übungsfragen in 6 Sets

Set 1: Register, Speicher und Konstanten10 Übungsfragen

Frage 1 von 10

Zwei Prozessoren implementieren RV32I mit unterschiedlichen internen Pipelines. Welche gemeinsame Spezifikation bestimmt das Ergebnis eines RV32I-add-Befehls?
  1. Die Prozessaufteilung des Betriebssystems
  2. Die Befehlssatzarchitektur
  3. Die Registerallokation des Compilers
  4. Die Taktfrequenz des Prozessors
Antwort zeigenAntwort ausblenden
Antwort: B
Die ISA legt die sichtbare Wirkung von add fest, daher müssen beide Implementierungen dieses Ergebnis liefern. Ihre internen Pipelines können sich unterscheiden. Die Taktfrequenz beeinflusst die Zeit, definiert aber nicht neu, was Addition für ein Programm bedeutet.

Frage 2 von 10

Vor sub a2, a0, a1 enthält a0 den Wert 28 und a1 den Wert 9. Welcher Wert wird nach a2 geschrieben?
  1. 37
  2. −19
  3. 9
  4. 19
Antwort zeigenAntwort ausblenden
Antwort: D
Der erste Operand ist das Ziel, und die Subtraktion berechnet den zweiten Operanden minus den dritten. Der Befehl schreibt daher 28 minus 9, also 19, nach a2. Vertauschte Quellen würden −19 ergeben, aber das ist hier nicht ihre Reihenfolge.

Frage 3 von 10

Ein RV32-ILP32-Array enthält 4 Byte große ganze Zahlen. Wie viele Bytes nach der Basis beginnt das Element A[7]?
  1. 7 Bytes
  2. 11 Bytes
  3. 28 Bytes
  4. 24 Bytes
Antwort zeigenAntwort ausblenden
Antwort: C
Jedes vorhergehende Element belegt 4 Bytes, daher beginnt Index 7 genau 28 Bytes nach der Basis. Wer 7 als Byteversatz verwendet, verwechselt einen Elementindex mit einem Adressversatz. Index null liegt bereits an der Basis und fügt der Anzahl kein weiteres Element hinzu.

Frage 4 von 10

Im Little-Endian-Speicher speichert sw den Wert 0x86C4217D bei 0x3000. Welchen Wert liefert lbu t0, 2(s0), wenn s0 = 0x3000?
  1. 0x000000C4
  2. 0x00000021
  3. 0xFFFFFFC4
  4. 0x00000086
Antwort zeigenAntwort ausblenden
Antwort: A
Die gespeicherten Bytes sind 0x7D, 0x21, 0xC4 und 0x86 bei aufsteigenden Adressen. Versatz 2 wählt 0xC4, und lbu erweitert es mit Nullen zu 0x000000C4. Das vorzeichenerweiterte Ergebnis würde lb statt des hier verwendeten vorzeichenlosen Byte-Ladebefehls erfordern.

Frage 5 von 10

Welches obere Feld für lui und welcher vorzeichenbehaftete Direktoperand für addi erzeugen in RV32 den Wert 0x00002ABC?
  1. 0x2 und +2748
  2. 0x2 und −1348
  3. 0x3 und −1348
  4. 0x3 und +2748
Antwort zeigenAntwort ausblenden
Antwort: C
Das untere 12-Bit-Muster 0xABC stellt −1348 dar. Ausgehend von 0x3000 führt die Subtraktion von 1348 zu 0x2ABC, also ist das obere Feld 0x3. Wird 2748 als positiver addi-Operand behandelt, schlägt dies fehl, weil der Wert die positive Schranke einer vorzeichenbehafteten 12-Bit-Zahl überschreitet.

Frage 6 von 10

Ein Aufrufer hält einen Wert in t3, ruft eine konforme Hilfsprozedur auf und benötigt danach den ursprünglichen Wert. Was muss der Aufrufer veranlassen?
  1. t3 nur innerhalb der Hilfsprozedur wiederherstellen
  2. t3 als Callee-saved-Register behandeln
  3. t3 nach der Rückkehr der Hilfsprozedur sichern
  4. Den Wert vor dem Aufruf sichern
Antwort zeigenAntwort ausblenden
Antwort: D
t3 ist ein Caller-saved-Register, daher darf eine konforme Hilfsprozedur es überschreiben. Der Aufrufer muss den noch benötigten Wert vor dem Aufruf sichern oder einen gleichwertigen erhaltenen Speicherort vorsehen. Die Hilfsprozedur ist nicht verpflichtet, temporäre Register wiederherzustellen, nur weil ihr Aufrufer sie benötigt.

Frage 7 von 10

In RV32 wird auipc t0, 0x2 bei 0x4000 ausgeführt, gefolgt von addi t0, t0, -24. Welcher Wert steht am Ende in t0?
  1. 0x00005FEC
  2. 0x00005FE8
  3. 0x00006018
  4. 0x00002000
Antwort zeigenAntwort ausblenden
Antwort: B
auipc addiert 0x2000 zu seiner eigenen Adresse und ergibt 0x6000. Nach der Subtraktion von 24 bleibt 0x5FE8. Würde die Adresse des folgenden Befehls als pc-Basis verwendet, wäre das Ergebnis fälschlich um 4 Bytes größer.

Frage 8 von 10

Ein Byte mit dem Inhalt 0xB2 wird einmal mit lb und einmal mit lbu geladen. Welche Dezimalergebnisse liefern die beiden Ladebefehle in dieser Reihenfolge?
  1. −78 und 178
  2. 178 und 178
  3. −78 und −78
  4. −50 und 178
Antwort zeigenAntwort ausblenden
Antwort: A
Als vorzeichenbehafteter 8-Bit-Wert ist 0xB2 gleich 178 minus 256, also −78. lb erweitert diesen Wert mit Vorzeichen, während lbu den vorzeichenlosen Wert 178 liefert. Sind beide Ergebnisse positiv, wird die Vorzeichenerweiterung des vorzeichenbehafteten Ladebefehls übersehen.

Frage 9 von 10

Ein Array von Strukturen verwendet unter RV32 ILP32 struct rec { int key; int value; }; ohne zusätzliches Padding. Seine Basisadresse ist 0x5000. Welche Adresse wird für A[3].value gelesen, und welches vorzeichenbehaftete Byte liefert lb von dieser Adresse, wenn das Feld im Little-Endian-Speicher 0x000000E9 enthält?
  1. 0x5018 und −23
  2. 0x501C und 233
  3. 0x5010 und −23
  4. 0x501C und −23
Antwort zeigenAntwort ausblenden
Antwort: D
Jede Struktur belegt 8 Bytes, daher beginnt Element 3 genau 24 Bytes nach der Basis; sein zweites Feld fügt 4 hinzu und ergibt 0x501C. Das erste Byte dort ist 0xE9, das lb mit Vorzeichen zu −23 erweitert. Das Ergebnis 233 würde der vorzeichenlosen Erweiterungsregel entsprechen.

Frage 10 von 10

In RV32 läuft die folgende Folge mit t1 = 0x7000; das Wort bei 0x7030 ist 6. Welcher Wert wird bei 0x7034 gespeichert?

lui t0, 0x4
addi t0, t0, -1984
lw t2, 48(t1)
add t2, t2, t0
sw t2, 52(t1)
  1. 0x00003840
  2. 0x00003846
  3. 0x00004846
  4. 0x00004006
Antwort zeigenAntwort ausblenden
Antwort: B
Die Konstante wird zu 0x4000 minus 0x7C0, also 0x3840. Der Ladebefehl liefert 6, sodass die Addition 0x3846 ergibt, was der letzte Speicherbefehl schreibt. 0x3840 ist nur die erzeugte Konstante und lässt den geladenen Operanden aus.
Set 2: Arithmetik und Pfade durch ein Programm10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welches 32-Bit-Hexadezimalergebnis liefert RV32I, wenn add 1 zu 0xFFFFFFFF addiert?
  1. 0xFFFFFFFF
  2. 0x80000000
  3. 0x00000000
  4. 0x7FFFFFFF
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welcher echte Befehl implementiert bgt a2, a3, Done?
  1. blt a2, a3, Done
  2. blt a3, a2, Done
  3. bge a3, a2, Done
  4. bge a2, a3, Done
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Welcher Wert steht nach srli t0, a0, 8 und andi t0, t0, 0x1F in t0, wenn a0 = 0x0000D6B9?
  1. 0x00000016
  2. 0x000000D6
  3. 0x00000019
  4. 0x00000006
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Welche Ergebnisse liefern bei vorzeichenbehaftetem a0 = -11 der Befehl srai t0, a0, 2 und die Ganzzahldivision -11 / 4 in C, jeweils in dieser Reihenfolge?
  1. −3 und −2
  2. −2 und −3
  3. −3 und −3
  4. −2 und −2
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

a0 = 0xFFFFFFFE und a1 = 0x00000003. Welcher der Befehle blt a0, a1, L und bltu a0, a1, L führt seinen Sprung aus?
  1. Nur bltu
  2. Beide Sprünge
  3. Keiner der Sprünge
  4. Nur blt
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Übersetzung von if (x != y) r = 8; else r = 3; springt bei Gleichheit zu Else. Was folgt auf die Zuweisung von 8, damit r auf dem Pfad für Ungleichheit 8 bleibt?
  1. Ein Sprung zurück zum Vergleich
  2. Ein Sprung hinter den else-Teil
  3. Eine zweite Zuweisung von 3
  4. Ein erneutes Laden von x vor Else
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Die Schleife while (A[i] == 6) i++; beginnt bei i = 1 mit A = {6, 6, 6, 4, 6}. Welchen Wert hat i beim Verlassen der Schleife?
  1. 2
  2. 4
  3. 3
  4. 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Blattprozedur führt den folgenden Code für ein Array aus 4 Byte großen ganzen Zahlen {2, -5, 9} aus. Die Basisadresse steht in a0 und die Anzahl 3 in a1. Jeder dargestellte Befehl ist ein Maschinenbefehl. Welches Ergebnis und welche dynamische Befehlsanzahl liefert sie?

addi t0, zero, 0
addi t1, zero, 0
L: bge t0, a1, D
slli t2, t0, 2
add t2, a0, t2
lw t3, 0(t2)
add t1, t1, t3
addi t0, t0, 1
jal zero, L
D: addi a0, t1, 0
jalr zero, 0(ra)
  1. 6 und 25
  2. 9 und 26
  3. 6 und 24
  4. 6 und 26
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Bei 0x8000 wird jalr ra, 7(t0) mit t0 = 0x903A ausgeführt. Welches Ziel wird verwendet, was kommt in ra, und welcher Befehl kann über diese Rückadresse zurückkehren?
  1. 0x9041; 0x8004; jalr zero, 0(ra)
  2. 0x9040; 0x8004; jalr zero, 0(ra)
  3. 0x9040; 0x8000; jalr zero, 0(ra)
  4. 0x9040; 0x8004; jal zero, 0(ra)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Die C-Bedingung lautet p != 0 && *p == 9, wobei p in a0 steht. Welche Kontrollflussreihenfolge erhält für den then-Pfad die Bedingung und vermeidet die Dereferenzierung eines Nullzeigers?
  1. Null als p verwerfen; *p laden; Ungleichheit mit 9 verwerfen
  2. *p laden; Null als p verwerfen; mit 9 vergleichen
  3. p ungleich null verwerfen; *p laden; Ungleichheit mit 9 verwerfen
  4. Null als p verwerfen; p mit 9 vergleichen; *p laden
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Befehlsfelder und relative Ziele10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welches Register liefert in sw a2, 20(sp) das Feld rs1?
  1. a2 (x12)
  2. zero (x0)
  3. ra (x1)
  4. sp (x2)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Wie lautet das 12-Bit-Muster des Direktoperanden für addi t2, s1, -36?
  1. 1111 1101 1100
  2. 0000 0010 0100
  3. 1111 1101 1011
  4. 1111 1110 1100
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Welche einzelne Änderung wandelt ein ADD im R-Format mit denselben Registern in SUB um?
  1. opcode auf 0100011 setzen
  2. funct3 auf 100 setzen
  3. funct7 auf 0100000 setzen
  4. rd auf die erste Quelle setzen
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein bedingter Sprung bei 0x308C hat das Ziel 0x3068. Welcher vorzeichenbehaftete Byteversatz wird kodiert?
  1. −40
  2. −36
  3. −9
  4. +36
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein jal bei 0x1200 hat den vorzeichenbehafteten Versatz +84 und das Zielregister ra. Welche Ziel- und Rückadresse ergeben sich?
  1. 0x1258 und 0x1204
  2. 0x1254 und 0x1204
  3. 0x1254 und 0x1258
  4. 0x1350 und 0x1204
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Welches Wort kodiert addi t2, s1, -36 mithilfe der Felder des I-Formats? Hier gilt t2 = x7, s1 = x9, der ADDI-Operationscode ist 0x13 und funct3 = 000.
  1. 0xFDC48393
  2. 0x02448393
  3. 0xFDC38493
  4. 0xFDC483B3
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Dekodiere 0xFEC12623. Der Operationscode 0x23 wählt einen Speicherbefehl und funct3 = 010 ein Wort. Welchen Assemblerbefehl stellt das Wort dar?
  1. sw a2, -12(sp)
  2. lw a2, -20(sp)
  3. sw sp, -20(a2)
  4. sw a2, -20(sp)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein bne a2, zero, Loop im B-Format steht bei 0x4018, wobei Loop bei 0x400C liegt. Welches mit dem Operationscode 0x63 und funct3 = 001 kodierte Wort erreicht Loop?
  1. 0xFE0618E3
  2. 0x00061663
  3. 0xFE061AE3
  4. 0xFE061CE3
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

An Adresse 0x6000 folgt auf das Wort 0x000022B7 bei 0x6004 das Wort 0xFE028293. Welcher Wert bleibt in t0? Die Wörter werden als LUI im U-Format beziehungsweise ADDI im I-Format dekodiert.
  1. 0x00002020
  2. 0x00001FDC
  3. 0x00007FE0
  4. 0x00001FE0
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein bedingter Sprung und sein Ziel beginnen im selben Abschnitt bei den Versätzen 0x30 und 0x54. Der Abschnitt wird von 0x9000 nach 0xB000 verschoben, ohne seine interne Anordnung zu ändern. Welcher Sprungversatz und welche Zieladresse ergeben sich?
  1. +0x24 und 0x9054
  2. +0x2024 und 0xB054
  3. +0x24 und 0xB054
  4. +0x20 und 0xB054
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Aufrufe, Rekursion und Zeigerzugriff10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Eine konforme RISC-V-Prozedur verändert s4. Welchen Wert muss s4 bei ihrer Rückkehr enthalten?
  1. Den eingehenden s4-Wert des Aufrufers
  2. Das erste Ergebnis der Prozedur
  3. Die Adresse nach dem Aufruf
  4. Die aktuelle Adresse der Stackspitze
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welcher Befehl setzt in RV32 ILP32 den Wert in a0 auf das nächste Element weiter, wenn das Register einen int * enthält?
  1. addi a0, a0, 1
  2. addi a0, a0, 2
  3. addi a0, a0, 8
  4. addi a0, a0, 4
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine Prozedur beginnt mit sp = 0x8000, reserviert 32 Bytes und führt dann sw ra, 28(sp) aus. An welcher Adresse beginnt das gesicherte Wort?
  1. 0x7FE0
  2. 0x801C
  3. 0x7FFC
  4. 0x7FF0
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine Blattprozedur erhält ein Argument in a0, berechnet ein Ergebnis in s1 und stellt vor der Rückkehr den Eingangswert von s1 wieder her. Was muss sie vor dieser Wiederherstellung tun, um die berechnete ganze Zahl zurückzugeben?
  1. Den Wert von a0 des Aufrufers bei jedem Austritt sichern
  2. Den berechneten Wert aus s1 nach a0 verschieben
  3. s1 wiederherstellen und dann s1 nach a0 kopieren
  4. Mit dem berechneten Wert in s1 zurückkehren
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine Zeichenkettenschleife speichert jeweils ein Byte, erhöht beide Zeiger und springt zurück, wenn dieses Byte nicht null ist. Wie viele Durchläufe kopieren „logic“ einschließlich seines C-Nullterminators?
  1. 5
  2. 6
  3. 7
  4. 10
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Prozedur nach Standard-ABI, die weitere Prozeduren aufruft, muss nur ra und s2 sichern, jeweils ein Wort. Was ist die kleinste positive Framegröße, die die erforderliche Stackausrichtung erhält?
  1. 4 Bytes
  2. 8 Bytes
  3. 16 Bytes
  4. 12 Bytes
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine Funktion erhält n in a0 und muss helper(n) + n zurückgeben. helper hält die ABI ein und darf alle Caller-saved-Register überschreiben. Auch das eingehende ra der Funktion muss erhalten bleiben. Welcher Plan funktioniert mit einem 16-Byte-Frame?
  1. ra und n sichern; aufrufen; n nach t0 laden; addieren; ra und sp wiederherstellen
  2. Nur ra sichern; n nach t0 kopieren; aufrufen; t0 addieren; ra und sp wiederherstellen
  3. Nur n sichern; aufrufen; n erneut laden; addieren; sp wiederherstellen; über ra zurückkehren
  4. ra und n sichern; aufrufen; n in a0 wiederherstellen; a0 zu sich selbst addieren; ra wiederherstellen
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine rekursive Funktion reserviert bei jedem Eintritt einen 16-Byte-Frame, sichert n und ra, gibt für n < 2 den Wert 1 und sonst n * f(n - 1) zurück. Welches Ergebnis und welcher niedrigste sp-Wert treten bei anfänglich n = 4 und sp = 0x9000 auf?
  1. 24 und 0x8FD0
  2. 6 und 0x8FC0
  3. 24 und 0x8FB0
  4. 24 und 0x8FC0
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Unter RV32 ILP32 enthält der Speicher bei 0xA000 den Zeigerwert 0xB000. Bei 0xB000 beginnt ein Array aus 4 Byte großen ganzen Zahlen, dessen Element 4 gleich 27 ist. Welche Folge hinterlässt dieses Element bei a0 = 0xA000 in a0?
  1. lw a0, 16(a0); lw a0, 0(a0)
  2. lw t0, 0(a0); lw a0, 16(t0)
  3. addi t0, a0, 16; lw a0, 0(t0)
  4. lw t0, 0(a0); lw a0, 4(t0)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Prozedur reserviert 32 Bytes, speichert ra bei 28(sp) und s0 bei 24(sp) und ruft dann eine Hilfsprozedur auf. Beim Austritt addiert sie zuerst 16 zu sp, lädt dann von 24(sp) und 28(sp), addiert weitere 16 und kehrt zurück. Warum kann das fehlschlagen, obwohl sp am Ende seinen ursprünglichen Wert hat?
  1. Die Ladebefehle verwenden um 16 Bytes zu hohe Adressen
  2. Die Hilfsprozedur muss alle temporären Werte erhalten
  3. Ein Frame darf keine ungenutzten Bytes enthalten
  4. Der abschließende Stackpointer ist falsch ausgerichtet
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 5: Erstelle, linke und starte ein Programm10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welche Datei erzeugt gcc -c module.c, ohne zu linken?
  1. Ein ausführbares Prozessabbild
  2. Eine Assembler-Quellcodeauflistung
  3. Eine relokierbare Objektdatei
  4. Ein Symbolcache für gemeinsam genutzte Bibliotheken
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welche Komponente bildet ausführbaren Code und Daten in einem neuen Prozess ab und bereitet dessen anfänglichen Stack vor?
  1. Der Compiler
  2. Der Lader
  3. Der Assembler
  4. Der statische Linker
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine Objektdatei definiert main, ruft aber mix auf, das in einer zweiten Objektdatei definiert ist. Was ermöglicht dem Linker, diesen Aufruf mit der Definition zu verbinden?
  1. Die Debug-Zeilentabelle und die Positionen in der Quelldatei
  2. Die Operationscodes und eine gemeinsame Registerkonvention
  3. Die anfängliche Stackaufteilung und der Eintrittspunkt der ausführbaren Datei
  4. Die Symbolinformationen und Relokationseinträge für den Aufruf
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Auf ein lokales Label wird verwiesen, bevor es später im selben Assemblerabschnitt definiert wird. Wie kann ein Assembler diesen Sprung fertigstellen?
  1. Den Verweis erfassen und seine Position später auflösen
  2. Den Lader verpflichten, das Label im Quelltext zu analysieren
  3. Den Labelnamen als ganzzahligen Direktoperanden behandeln
  4. Den Sprung mit einem dauerhaft auf null gesetzten Versatz ausgeben
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Programm wurde statisch mit der Implementierung von filter aus einer Bibliothek gelinkt. Danach wird das Bibliotheksarchiv korrigiert, die ausführbare Datei aber nicht verändert. Welchen Code für filter enthält die ausführbare Datei?
  1. Den beim Linken kopierten ursprünglichen Code
  2. Beim nächsten Aufruf den korrigierten Archivcode
  3. Eine neu interpretierte Version des Quellcodes
  4. Ein PLT-Verbindungsstück, das gemeinsam genutzten Ersatzcode lädt
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein RISC-V-Programm für Linux schreibt 93 nach a7, 7 nach a0 und führt dann ecall aus. Welcher Dienst wird angefordert?
  1. 7 Bytes in einen Standarddatenstrom schreiben
  2. 93 an den aktuellen Aufrufer zurückgeben
  3. Den Prozess mit Status 7 beenden
  4. Einen Stackframe von 7 Bytes reservieren
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein Linker platziert Text bei 0x20000 und Daten bei 0x30000000. Modul P steuert 0x180 Textbytes und 0x30 Datenbytes bei; Q folgt unmittelbar. Ein Aufruf in P liegt beim Textversatz 0x70; sein Ziel in Q liegt bei Textversatz 0x20 innerhalb von Q. Die Variable von Q liegt bei Datenversatz 0x14 innerhalb von Q. Wie lauten der Byteversatz des Aufrufs und die Adresse der Variablen?
  1. +0x1A0 und 0x30000044
  2. +0x130 und 0x30000194
  3. +0x12C und 0x30000044
  4. +0x130 und 0x30000044
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine RV32I-Auflistung bei 0x1000 enthält folgende Erweiterungen: li t0, 5000 verwendet zwei Befehle, mv a0, t0 einen und call Helper ein nicht verkürztes Paar aus zwei Befehlen. Helper kehrt normal zurück. Bei welcher Adresse wird die Ausführung nach dem Aufruf fortgesetzt, und wie viele Codebytes gehen dieser Fortsetzung voraus?
  1. 0x100C und 12 Bytes
  2. 0x1014 und 20 Bytes
  3. 0x1010 und 16 Bytes
  4. 0x1018 und 24 Bytes
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Bibliotheksfunktion beginnt beim Versatz 0x2C0 in einer gemeinsam genutzten Bibliothek. Ein lokaler Aufrufer liegt beim Versatz 0x90; ein Funktionszeiger außerhalb der Bibliothek ist in einem GOT-Eintrag gespeichert. Die Basisadresse der Bibliothek ändert sich von 0x40000000 zu 0x50000000. Welche Beschreibung behandelt beide Verweise korrekt?
  1. 0x10000000 zum lokalen Versatz addieren und jeden GOT-Eintrag beibehalten
  2. Das lokale Ziel bei 0x400002C0 belassen; nur den aufrufenden Befehl relokieren
  3. 0x10000000 vom lokalen Abstand subtrahieren und die ganze GOT löschen
  4. Den lokalen Abstand +0x230 beibehalten; das externe GOT-Ziel bei Bedarf binden
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine ausführbare Datei wurde erfolgreich gegen eine gemeinsam genutzte Bibliothek gelinkt. Später fehlt diese Bibliothek, und das Laden der ausführbaren Datei schlägt vor main fehl. Was erklärt den Fehler, und was wäre bei statisch eingebundenem Code aus dieser Bibliothek anders?
  1. Das Laden benötigt die gemeinsame Datei; eingebundener Code wäre bereits in der ausführbaren Datei
  2. Das Linken wiederholt sich beim Start; eingebundener Code bräuchte weiterhin das ursprüngliche Archiv
  3. Die Eingabeobjektdatei fehlt; eingebundener Code bräuchte weiterhin jede Eingabeobjektdatei
  4. Die Datei enthält keinen Maschinencode; eingebundener Code würde den Prozesslader umgehen
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 6: Beurteile Entwürfe und untersuche Assemblercode10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welches RISC-V-Befehlspaar führt die Speicheraddition des x86-Befehls add eax, [ebx+12] aus, wenn eax dem Register a0 und ebx dem Register s1 entspricht?
  1. addi a0, a0, 12; add a0, a0, s1
  2. lw a0, 12(s1); add a0, a0, a0
  3. sw a0, 12(s1); add a0, a0, s1
  4. lw t0, 12(s1); add a0, a0, t0
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Prozessor hat eine Load-Store-ISA und Statusflags. Was lässt sich über die Behauptung schließen, dass Flags ihn zu CISC machen?
  1. Flags bedeuten Arithmetik mit zwei Operanden, daher stimmt die Behauptung.
  2. Flags können in RISC-Entwürfen vorkommen, daher stimmt die Behauptung nicht.
  3. Eine Load-Store-ISA kann keine Flags nutzen, daher widerspricht sich die Beschreibung.
  4. Flags bedeuten kombinierte Speicherarithmetik, daher stimmt die Behauptung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Version P führt 0,8 Milliarden Befehle bei einem CPI-Wert von 1,5 und 2 GHz aus. Version Q führt 1,0 Milliarden Befehle bei einem CPI-Wert von 1,0 und 2 GHz aus. Welche Ausführungszeiten ergeben sich für P und Q?
  1. 0,60 s und 0,50 s
  2. 0,40 s und 0,50 s
  3. 0,60 s und 1,00 s
  4. 1,20 s und 1,00 s
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein Programm hat 120 Maschinenbefehle. Wenn 72 davon 16-Bit-Kodierungen und die übrigen 32-Bit-Kodierungen verwenden, wie viele Codebytes belegt es?
  1. 240 Bytes
  2. 384 Bytes
  3. 336 Bytes
  4. 408 Bytes
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Entwurf P führt 1,2 Milliarden Befehle bei einem CPI-Wert von 1,1 und 2,2 GHz aus. Entwurf Q führt 0,9 Milliarden Befehle bei einem CPI-Wert von 1,8 und 2,7 GHz aus. Eine vorgeschlagene Optimierung senkt die Befehlsanzahl von Q um 10 %, erhöht aber seinen CPI-Wert um 20 %, während die Taktfrequenz gleich bleibt. Wie schneidet das optimierte Q im Vergleich zu P ab?
  1. Q benötigt 0,648 s und ist langsamer als P
  2. Q benötigt 0,54 s und ist schneller als P
  3. Q benötigt 0,60 s und ist gleich schnell wie P
  4. Q benötigt 0,72 s und ist langsamer als P
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Routine mit 200 Befehlen verwendet ursprünglich nur 32-Bit-Kodierungen. Nachdem 110 Kodierungen zu 16-Bit-Kodierungen werden, bleiben die Befehlsfolge und die gemessene Laufzeit unverändert. Wie groß ist der Code nun, und welche Schlussfolgerung zur Rechenleistung ist gerechtfertigt?
  1. 580 Bytes; 27,5 % schnellere Ausführung
  2. 620 Bytes; weniger Bytes bei unveränderter gemessener Geschwindigkeit
  3. 400 Bytes; halb so viele ausgeführte Befehle
  4. 580 Bytes; weniger Bytes bei unveränderter gemessener Geschwindigkeit
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine Binärdatei verwendet RV32I plus mul, erwartet die ILP32-Aufrufkonvention und ruft RISC-V-Linux-Systemaufrufe auf. Eine Platine weist RV32I aus, verwendet ILP32 und führt einen Simulator mit eigener Dienstschnittstelle aus. Welche zusätzlichen Anforderungen müssen erfüllt sein, damit die Binärdatei wie vorgesehen laufen kann?
  1. Unterstützung für mul und die aktuelle Dienstschnittstelle des Simulators
  2. Unterstützung für mul und die erwartete Linux-Dienstschnittstelle
  3. Nur RV32I und die erwartete Linux-Dienstschnittstelle
  4. Unterstützung für mul und allein die passende ILP32-Konvention
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein RV32-ILP32-Array enthält struct sample { int id; int reading; }; ohne zusätzliches Padding. Ein Zeiger auf Element 2 steht in a0. Der Messwert des nächsten Elements ist 31. Welche Adressberechnung und welcher Ladebefehl holen diesen Messwert, ohne a0 zu verändern?
  1. addi t0, a0, 4; lw t1, 4(t0)
  2. addi t0, a0, 8; lw t1, 0(t0)
  3. addi t0, a0, 8; lw t1, 4(t0)
  4. addi t0, a0, 12; lw t1, 4(t0)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Diese Blattprozedur muss die Summe von n ganzen Zahlen zurückgeben, wobei n nicht negativ ist. Sie wird mit n = 0 und einer Adresse aufgerufen, die nicht gelesen werden darf. Welche kleinste Korrektur des Kontrollflusses vermeidet den ungültigen Zugriff und erhält das Verhalten für positive Anzahlen?

addi t1, zero, 0
L: lw t0, 0(a0)
add t1, t1, t0
addi a0, a0, 4
addi a1, a1, -1
bne a1, zero, L
D: addi a0, t1, 0
ret
  1. Einen vorzeichenlosen Byte-Ladebefehl in der Schleife verwenden
  2. beq a1, zero, D vor L einfügen
  3. Das letzte bne zu einem vorzeichenbehafteten blt ändern
  4. t1 mit dem ersten Arrayelement initialisieren
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine handgeschriebene Routine verringert auf demselben 2-GHz-Prozessor die ausgeführten Befehle von 50 Millionen auf 40 Millionen. Der mittlere CPI-Wert steigt von 1,2 auf 1,6. Was geschieht mit der Zeit, und was zeigt das über die Aussage zur Befehlsanzahl?
  1. Sie sinkt von 30 ms auf 24 ms; die geringere Anzahl bestimmt die Zeit
  2. Sie steigt von 60 ms auf 64 ms; der CPI-Wert gleicht die geringere Anzahl aus
  3. Sie steigt von 30 ms auf 32 ms; der CPI-Wert gleicht die geringere Anzahl aus
  4. Sie steigt von 24 ms auf 32 ms; der CPI-Wert gleicht die geringere Anzahl aus
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

82 Schlüsselbegriffe, jeweils mit Definition

A

Adresse laden
Adresse laden ist eine Assembleroperation, die die Adresse eines Labels als Registerwert bildet, ohne das dort gespeicherte Objekt zu lesen.
Nach la t0, Item ist ein separater Ladebefehl nötig, um den bei Item gespeicherten Wert zu lesen.
Arithmetische Rechtsverschiebung
Eine arithmetische Rechtsverschiebung bewegt Bits zu niedrigeren Positionen und füllt obere Positionen mit dem ursprünglichen Vorzeichenbit.
Die Rechtsverschiebung der vorzeichenbehafteten Zahl −7 um 1 ergibt −4, weil das Ergebnis gegen minus unendlich gerundet wird.
Assembler
Ein Assembler wandelt Assemblertext in kodierte Befehle und Daten um und erfasst dabei Symbole und Verweise zur späteren Auflösung.
Ein Assembler erweitert call und kann Relokationseinträge für eine in einem anderen Modul definierte Funktion hinterlassen.
Assemblerdirektive
Eine Assemblerdirektive steuert Datenausgabe, Abschnittsauswahl, Ausrichtung oder Symbolmetadaten, ohne vom Prozessor ausgeführt zu werden.
Die Direktive .word 5 gibt einen 4-Byte-Wert aus, während .text den Abschnitt für den folgenden Code auswählt.
Assemblersprache
Assemblersprache ist lesbarer, ISA-spezifischer Text, der Befehle und Operanden bezeichnet, bevor ein Assembler sie als Bits kodiert.
Der Text add a0, a1, a2 bezeichnet eine Operation mit einer festgelegten RISC-V-Maschinencodekodierung.
Aufrufkonvention
Eine Aufrufkonvention ist die Vereinbarung, die Ablageorte für Argumente und Ergebnisse sowie den Erhalt von Registern über Prozeduraufrufe hinweg festlegt.
RISC-V-Ganzzahlprozeduren übergeben ihre ersten Argumente in a0 bis a7.

B

Basisadressierung mit Versatz
Die Basisadressierung mit Versatz bildet eine effektive Speicheradresse, indem sie einen vorzeichenbehafteten konstanten Versatz zum Wert eines Basisregisters addiert.
In lw t0, 20(s1) beginnt das geladene Wort 20 Bytes hinter der in s1 gespeicherten Adresse.
Basisblock
Ein Basisblock ist eine geradlinige Befehlsfolge, die im normalen Kontrollfluss an ihrem Anfang betreten und erst an ihrem Ende verlassen wird.
Ein Label als Ziel eines bedingten Sprungs beginnt einen neuen Block, den ein Compiler separat analysieren kann.
Bedingter Sprung
Ein bedingter Sprung ändert die nächste Befehlsadresse nur, wenn sein Vergleich erfolgreich ist; andernfalls wird die Ausführung sequenziell fortgesetzt.
bne t0, t1, Loop kehrt nur zu Loop zurück, solange sich die beiden Registermuster unterscheiden.
Befehlsformat
Ein Befehlsformat ist die Anordnung von Feldern innerhalb eines kodierten Befehls, die Operanden und Steuerinformationen Bitpositionen zuweist.
Das S-Format hat einen aufgeteilten Direktoperanden dort, wo das R-Format ein Funktionsfeld und ein Zielregister hätte.
Befehlssatzarchitektur
Eine Befehlssatzarchitektur ist die Vereinbarung, die die für Software sichtbaren Befehle, Register und das Maschinenverhalten festlegt.
Zwei Prozessoren können dieselbe ISA mit unterschiedlichen internen Schaltungen und Ausführungsgeschwindigkeiten implementieren.
Befehlszähler
Der Befehlszähler ist das Register, das die Adresse des in der Befehlsfolge gerade geholten oder ausgeführten Befehls enthält.
Ein ausgeführter bedingter Sprung ersetzt das sequenzielle Weitersetzen des pc durch die für sein Ziel berechnete Adresse.
Binärkompatibilität
Binärkompatibilität ermöglicht es, vorhandene Maschinencode-Software ohne Neukompilierung auszuführen, wenn ISA, ABI und Umgebung die Anforderungen erfüllen.
Die Verwendung einer optionalen Erweiterung stellt eine zusätzliche Hardwareanforderung über die Unterstützung der Basis-ISA hinaus.
Binärschnittstelle (ABI)
Eine Binärschnittstelle ist die Softwarevereinbarung über Aufrufkonventionen, Datenlayout und weitere Regeln für das binäre Zusammenwirken.
Eine übereinstimmende ISA kann eine Abweichung darüber, welches Register den Rückgabewert einer Funktion enthält, nicht beheben.
Bitmaske
Eine Bitmaske ist ein Muster, das mit einer bitweisen Operation verwendet wird, um bestimmte Positionen in einem anderen Muster auszuwählen, zu löschen, zu setzen oder umzukehren.
Eine UND-Verknüpfung mit 0xFF behält das untere Byte und löscht alle höheren Registerpositionen.
Blattprozedur
Eine Blattprozedur ist eine Prozedur, die keine anderen Prozeduren aufruft und dadurch ihre eingehende Rückadresse unverändert lassen kann.
Eine Blattprozedur, die nur Argument- und temporäre Register verwendet, kann möglicherweise ohne Reservierung eines Stackframes zurückkehren.
Byteadressierung
Byteadressierung ist eine Speicherorganisation, bei der jede Adresse ein Byte bezeichnet, sodass größere Werte aufeinanderfolgende Adressen belegen.
Zwei benachbarte 32-Bit-Arrayelemente beginnen an Adressen, die sich um 4 unterscheiden, nicht um 1.

C

Callee-saved-Register
Ein Callee-saved-Register muss nach einem Aufruf seinen Eingangswert behalten, sodass eine Prozedur, die es verändert, diesen Wert wiederherstellen muss.
Eine Funktion kann s1 intern verwenden, wenn sie vor der Rückkehr den ursprünglichen s1-Wert des Aufrufers wiederherstellt.
Caller-saved-Register
Ein Caller-saved-Register darf von einer aufgerufenen Prozedur überschrieben werden, daher muss ihr Aufrufer einen danach benötigten aktuellen Wert sichern.
Ein noch benötigter Wert in t2 muss vor einem Hilfsaufruf gesichert werden, dessen Prozedur dieses Register frei verwenden darf.
CISC
CISC ist ein Ansatz zum Befehlssatzentwurf, der vielfältigere Operationen und Adressierungsformen unterstützt, gewöhnlich durch Kodierungen variabler Länge.
Ein x86-Befehl kann das Lesen eines Speicheroperanden mit dessen Addition zu einem Register verbinden.
Codedichte
Codedichte beschreibt, wie kompakt Maschinenbefehle ein Programm darstellen. Sie wird häufig anhand der für seinen Code benötigten Bytes verglichen.
Komprimierte Befehle können die Anzahl der Codebytes verringern und dabei die Anzahl ausgeführter Operationen erhalten.
Compiler
Ein Compiler übersetzt ein Programm in eine andere Darstellung und wählt und optimiert dabei häufig Maschinenbefehle für eine Ziel-ISA.
Ein auf RISC-V ausgerichteter Compiler kann wiederholte Indexberechnungen durch eine Zeigererhöhung ersetzen.

D

Dereferenzierung
Eine Dereferenzierung greift auf das durch einen Zeigerwert bezeichnete Objekt zu und wird durch einen passenden Lade- oder Speicherbefehl implementiert.
lw t0, 0(a0) liest eine 4 Byte große ganze Zahl über die aktuell in a0 gehaltene Adresse.
Direktoperand
Ein Direktoperand ist eine durch Bits innerhalb eines Befehls dargestellte Konstante, die verfügbar ist, ohne sie zuerst aus dem Speicher zu holen.
Der letzte Operand in addi t0, t0, 1 ist als Direktoperand im Befehl kodiert.
Disassemblierung
Disassemblierung ist die Rückumwandlung kodierter Befehlsbits in Befehlsnamen und Operanden anhand der Dekodierungsregeln der ISA.
Ein Disassembler bestimmt den Operationscode, bevor er andere Bits als Register oder Teile von Direktoperanden behandelt.
Dynamische Befehlsanzahl
Die dynamische Befehlsanzahl ist die Anzahl der tatsächlich auf einem Programmpfad ausgeführten Befehle, einschließlich wiederholter Schleifendurchläufe.
Die letzte fehlgeschlagene Prüfung einer Schleife trägt zur Ausführungsanzahl bei, obwohl ihr Rumpf nicht erneut läuft.
Dynamisches Linken
Dynamisches Linken verbindet ein Programm beim Laden oder Ausführen mit separatem Code gemeinsam genutzter Bibliotheken und setzt voraus, dass kompatible Bibliotheken verfügbar sind.
Eine fehlende gemeinsam genutzte Bibliothek kann den Start einer zuvor gelinkten ausführbaren Datei verhindern.

E

Elementabstand
Ein Elementabstand ist der Byteabstand zwischen entsprechenden Positionen aufeinanderfolgender Elemente eines Arrays oder einer wiederholten Anordnung.
Ein Array aus Strukturen mit je zwei int-Feldern hat einen Elementabstand von 8 Bytes, während das zweite Feld jeder Struktur den Versatz 4 hat.
Epilog
Ein Epilog ist die Austrittsfolge, die gesicherten Zustand wiederherstellt, den aktuellen Stackframe freigibt und die Steuerung an den Aufrufer zurückgibt.
Werden Register vor dem Erhöhen von sp wiederhergestellt, bleiben ihre Versätze relativ zum reservierten Frame.

F

Fall-through
Fall-through ist die Fortsetzung der Ausführung beim nächsten Befehl oder Codeblock, weil keine Kontrollflussübertragung ihn überspringt.
Eine if-else-Übersetzung benötigt nach dem then-Teil einen Sprung, damit die Ausführung nicht in den else-Teil weiterläuft.
Funktionsfeld
Ein Funktionsfeld verfeinert einen Operationscode, um eine bestimmte Operation auszuwählen, etwa Addition oder Subtraktion innerhalb der Registerarithmetik.
Dieselben Register und derselbe Operationscode können abhängig von den funct7-Bits ADD oder SUB darstellen.

G

Ganzzahlregister
Ein Ganzzahlregister ist ein kleiner Speicherplatz im Prozessor, der ein Bitmuster für Ganzzahloperationen oder Adressberechnungen enthält.
In RV32I enthält jedes der 32 Ganzzahlregister 32 Bits, unabhängig davon, ob es für einen Wert oder eine Adresse verwendet wird.

I

Implizites Bit
Ein implizites Bit ist ein Wert, der aus einer Kodierungsregel rekonstruiert wird, ohne eine eigene gespeicherte Bitposition zu belegen.
Das niederwertigste Bit eines Sprungversatzes im B-Format ist implizit null, weil der Byteabstand gerade ist.
Indirekter Sprung
Ein indirekter Sprung bezieht sein Ziel aus einer registerbasierten Berechnung, sodass eine gespeicherte Adresse den nächsten Befehl auswählen kann.
jalr kann einen Aufruf über einen Funktionszeiger, eine Rückkehr oder einen Sprung über eine Sprungtabelle implementieren.

J

Just-in-time-Kompilierung
Just-in-time-Kompilierung erzeugt während der Ausführung Maschinencode und konzentriert den Kompilieraufwand häufig auf wiederholt ausgeführten Code.
Eine virtuelle Maschine kann Bytecode zunächst interpretieren und anschließend eine häufig ausgeführte Methode kompilieren.

K

Kleiner-als-Vergleich
Ein Kleiner-als-Vergleich ist eine Vergleichsoperation, die 1 schreibt, wenn die erste Quelle kleiner als die zweite ist, und sonst 0.
slt und sltu können unterschiedliche Ergebnisse liefern, wenn das höchstwertige Bit einer Quelle ihr bei vorzeichenbehafteter und vorzeichenloser Interpretation unterschiedliche Werte gibt.
Komprimierter Befehl
Ein komprimierter Befehl ist eine kürzere Kodierung einer Operation, etwa eine 16-Bit-Form der RISC-V-Erweiterung C, die neben 32-Bit-Befehlen verwendet wird.
Wenn die Hälfte von 100 Befehlen je 2 Bytes verwendet, beträgt die gesamte Codegröße 300 statt 400 Bytes.
Kurzschlussauswertung
Kurzschlussauswertung beendet die Prüfung einer zusammengesetzten Bedingung, sobald ihr gesamter Wahrheitswert bereits feststeht.
Ein falscher erster Operand von && verhindert in C die Auswertung des zweiten Operanden.

L

Lader
Ein Lader bereitet eine ausführbare Datei als laufenden Prozess vor, indem er ihren Inhalt abbildet und den anfänglichen Speicher- und Registerzustand herstellt.
Der Programmstart beginnt erst, nachdem Code, Daten und der anfängliche Stack des Programms vorbereitet wurden.
Linker
Ein Linker kombiniert Objektmodule und Bibliotheken, ordnet den Inhalt einer ausführbaren Datei an, löst Symbole auf und passt adressabhängige Verweise an.
Ein Fehler wegen eines undefinierten Verweises bedeutet, dass der abschließende Linkvorgang eine benötigte Symboldefinition nicht finden konnte.
Little-Endian-Byte-Reihenfolge
Die Little-Endian-Byte-Reihenfolge speichert das niederwertigste Byte eines Werts aus mehreren Bytes an dessen niedrigster Speicheradresse.
Das Speichern des Wortes 0x12345678 legt 0x78 im ersten und 0x12 im vierten Byte ab.
Load-Store-Architektur
Eine Load-Store-Architektur verwendet eigene Befehle zur Speicherübertragung, während Arithmetik und Logik mit Registerwerten und Direktoperanden arbeiten.
Um ein Arrayelement zu addieren, lädt RISC-V es in ein Register, bevor ein arithmetischer Befehl ausgeführt wird.
Logische Rechtsverschiebung
Eine logische Rechtsverschiebung bewegt Bits zu niedrigeren Positionen und füllt frei werdende obere Positionen mit Nullen; das ermöglicht vorzeichenlose Skalierung.
Eine logische Rechtsverschiebung eines negativen vorzeichenbehafteten Musters kann ein großes positives Ergebnis liefern.

M

Mikroarchitektur
Eine Mikroarchitektur ist der interne Aufbau eines Prozessors, mit dem er die von seinem Befehlssatz verlangten Operationen und Verhaltensweisen implementiert.
Eine Änderung der Ausführungshardware kann die Geschwindigkeit eines Chips verbessern und dabei seine bestehende Maschinencode-Schnittstelle erhalten.
Mikrooperation
Eine Mikrooperation ist eine interne Operation, in die ein Prozessor einen ISA-Befehl zur Ausführung durch seinen Kern übersetzen kann.
Ein nach außen sichtbarer x86-Befehl kann mehrere einfachere interne Mikrooperationen benötigen.

N

Natürliche Ausrichtung
Bei natürlicher Ausrichtung liegt ein Speicherzugriff an einer durch die Zugriffsgröße teilbaren Adresse, etwa einem Vielfachen von 4 für ein Wort.
Ein Compiler kann Padding einfügen, damit ein Halbwort oder Wort an einer passend ausgerichteten Adresse beginnt.
Nibble
Ein Nibble ist eine Gruppe von 4 Bits, die einer Hexadezimalziffer entspricht und die Umwandlung binärer Befehlswörter in Hexadezimaldarstellung erleichtert.
Ein vollständiger 32-Bit-Befehl lässt sich in acht Nibbles aufteilen, nachdem alle seine Felder zusammengefügt wurden.
Nullerweiterung
Nullerweiterung verbreitert ein Bitmuster, indem sie neue obere Positionen mit Nullen füllt und so den vorzeichenlos interpretierten Wert erhält.
Das Laden des Bytes 0xF0 mit lbu ergibt den positiven Wert 240 in einem 32-Bit-Register.
Nullregister
Das Nullregister ist das RISC-V-Register x0, das beim Lesen immer null liefert und jeden Wert verwirft, den ein Befehl hineinschreibt.
Ein Sprung kann seine Rückadresse nach zero schreiben, wenn das Programm keine Rückadresse sichern muss.
Nullterminator
Ein Nullterminator ist das Nullbyte, das das Ende einer C-Bytezeichenkette markiert und nach dem letzten Byte ungleich null Speicher belegt.
Eine Schleife zum Kopieren von Zeichenketten speichert den Nullterminator, damit auch das Ziel ein erkennbares Ende hat.

O

Oberer Direktoperand
Ein oberer Direktoperand liefert ein 20-Bit-Feld, das lui beim Erstellen eines RV32-Registerwerts über 12 untere Nullen setzt.
Ein anschließendes addi kann einen vorzeichenbehafteten unteren Teil liefern, um eine vollständige 32-Bit-Konstante zu bilden.
Objektlebensdauer
Die Objektlebensdauer ist der Zeitraum, in dem der Speicher eines Objekts ein gültiges Objekt darstellt, auf das das Programm zugreifen darf.
Die Rückgabe eines Zeigers auf ein lokales Objekt erhält dieses Objekt nach der Rückkehr seiner Funktion nicht am Leben.
Operationscode
Ein Operationscode ist ein Befehlsfeld, das eine Operationsgruppe bezeichnet und mitbestimmt, welche Anordnung zum Dekodieren der übrigen Bits verwendet wird.
Die unteren 7 Bits unterscheiden einen RV32I-Wortspeicherbefehl von einem arithmetischen Registerbefehl.

P

PC-relative Adresse
Eine PC-relative Adresse wird aus der eigenen Adresse eines Befehls plus einem kodierten Versatz gebildet und hält so eine Position relativ zum Code.
Eine auipc-Folge kann lokale Daten erreichen, selbst wenn die gesamte Gruppe aus Code und Daten verschoben wird.
Positionsunabhängiger Code
Positionsunabhängiger Code verwendet relative Verweise und Indirektion, sodass er an unterschiedlichen Ladeadressen korrekt ausgeführt werden kann.
Werden eine Bibliothek und ihre lokalen Ziele gemeinsam verschoben, bleiben die von ihren lokalen Verweisen verwendeten Abstände erhalten.
Prolog
Ein Prolog ist die Eintrittsfolge, die den Frame einer Prozedur einrichtet und den eingehenden Registerzustand sichert, den sie erhalten muss.
Das Sichern von ra vor einem verschachtelten Aufruf schützt die Adresse, die zur Rückkehr zum äußeren Aufrufer benötigt wird.
Pseudobefehl
Ein Pseudobefehl ist eine Kurzform des Assemblers, die zu einem oder mehreren Befehlen erweitert wird, die tatsächlich von der ISA des Prozessors definiert sind.
Die Anzahl der von li erzeugten Maschinenbefehle hängt vom Wert seines konstanten Operanden ab.

R

Registerallokation
Registerallokation ist die Zuweisung von Programmwerten durch den Compiler zu den an jedem Punkt der Ausführung begrenzt verfügbaren Registern.
Eine optimierte Schleife kann ihren Index und ihre aufsummierte Summe über viele Durchläufe hinweg in Registern halten.
Registerfeld
Ein Registerfeld ist ein Teil fester Breite einer Befehlskodierung, der ein Quell- oder Zielregister anhand seiner Nummer bezeichnet.
Ein 5-Bit-Feld stellt a0 als Registernummer 10 dar, die binär als 01010 geschrieben wird.
Rekursion
Rekursion tritt auf, wenn eine Prozedur sich direkt oder indirekt selbst aufruft und getrennte aktive Aufrufe mit eigenem Zustand erzeugt.
Die Fakultätsberechnung lädt das gesicherte Argument eines äußeren Aufrufs erneut, nachdem der innere Aufruf sein Ergebnis zurückgegeben hat.
Relative Relokation
Relative Relokation ist die Anpassung eines Verweises anhand des Abstands zwischen einem platzierten Befehl oder einer Datenposition und dessen Ziel.
Wird nur ein Abschnitt verschoben, kann sich ein Aufrufversatz ändern, selbst wenn die Befehlsbytes zunächst unverändert waren.
Relokierbare Objektdatei
Eine relokierbare Objektdatei enthält Maschinencode, Daten und Metadaten, deren endgültige Platzierung und externe Verweise später vervollständigt werden können.
Das Erzeugen einer Objektdatei mit -c stoppt vor der für eine ausführbare Datei nötigen Kombination von Bibliotheken und Modulen.
RISC
RISC ist ein Ansatz zum Befehlssatzentwurf, der einheitliche Kodierungen, relativ einfache Operationen und separate Befehle zur Speicherübertragung bevorzugt.
Erweiterungen können eine RISC-ISA vergrößern, ohne ihre Load-Store-Organisation aufzuheben.

S

Schleifenschranke
Eine Schleifenschranke begrenzt die durch den Vergleich einer Schleife erlaubten Durchläufe und bestimmt, auf welche Indizes oder Adressen ihr Rumpf zugreifen kann.
Eine Prüfung, die beim Erreichen der Länge durch den Index die Schleife beendet, hält einen Arrayindex unter dieser Länge.
Skalierte Indexadressierung
Skalierte Indexadressierung kombiniert eine Basis mit einem um eine Schrittweite skalierten Index und addiert häufig einen Versatz, um eine Speicheradresse zu bilden.
Ein x86-Speicheroperand kann Basis plus das Vierfache eines Index plus 8 in einem Befehl ausdrücken.
Speicherprogrammkonzept
Das Speicherprogrammkonzept stellt Befehle als Binärwerte im Speicher dar, sodass ein Prozessor sie holen und ausführen kann.
Der Befehlszähler liefert die Adresse, von der der nächste kodierte Befehl geholt wird.
Spilling
Spilling ist das Speichern eines noch benötigten Registerwerts im Speicher, damit sein Register erneut verwendet werden kann; bei Bedarf wird der Wert wieder geladen.
Zusätzliche noch benötigte Werte können Speicher- und Ladezugriffe auf den Stack verursachen, selbst wenn der ursprüngliche C-Code keinen ausdrücklichen Speicherzugriff enthält.
Sprung mit Rückadressensicherung
Ein Sprung mit Rückadressensicherung ist eine Kontrollflussübertragung, die zugleich die Adresse des folgenden Befehls zur späteren Verwendung in ein Zielregister schreibt.
jal ra, Helper erfasst, wo der Aufrufer nach der Rückkehr von Helper fortfahren soll.
Sprungtabelle
Eine Sprungtabelle ist eine indizierte Sammlung von Codeadressen, mit der die Steuerung effizient zwischen mehreren Fällen übertragen wird.
Eine switch-Verzweigung muss ihren Fallbereich prüfen, bevor sie eine Adresse aus ihrer Sprungtabelle liest.
Sprungversatz
Ein Sprungversatz ist der vorzeichenbehaftete Byteabstand von der eigenen Adresse des Sprungbefehls zu seiner vorgesehenen Zieladresse.
Ein rückwärts liegendes Ziel ergibt einen negativen Versatz, selbst wenn beide Adressen positiv sind.
Stackframe
Ein Stackframe ist der für gesicherten Zustand, lokale Objekte, ausgelagerte Werte und Stackargumente einer Prozeduraktivierung reservierte Bereich.
Drei aktive Aufrufe können drei getrennte Kopien des gesicherten Arguments derselben Funktion enthalten.
Stackpointer
Der Stackpointer ist das Register, das die aktuelle Grenze des reservierten Stackspeichers markiert und sich beim Reservieren oder Freigeben von Frames bewegt.
Die Subtraktion von 16 von sp reserviert gemäß RISC-V-Konvention einen ausgerichteten Frame bei niedrigeren Adressen.
Statisches Linken
Statisches Linken nimmt benötigten Bibliotheks-Objektcode in die ausführbare Datei auf und macht diesen eingebundenen Code zu einem Teil der programmeigenen Datei.
Das Ersetzen einer statischen Bibliotheksdatei aktualisiert keinen Code, der bereits in eine ältere ausführbare Datei kopiert wurde.
Statusflags
Statusflags sind Zustandsbits, die Eigenschaften eines Ergebnisses oder Vergleichs festhalten, die spätere Befehle prüfen können.
x86 kann Register vergleichen und anschließend abhängig von Flags springen, während bedingte RISC-V-Sprünge direkt vergleichen.
Symboltabelle
Eine Symboltabelle erfasst benannte Definitionen und nicht aufgelöste Verweise, mit denen Befehle und Daten über übersetzte Module hinweg verbunden werden.
Eine Funktion kann in einer Objektdatei undefiniert und in einer anderen für den Linker bereitgestellten Objektdatei definiert sein.
Systemaufruf
Ein Systemaufruf ist die Anforderung eines Betriebssystemdienstes durch ein Programm über die von seiner Ausführungsumgebung definierte Schnittstelle.
Unter RISC-V-Linux fordert ecall mit 93 in a7 die Beendigung an und verwendet a0 für den Status.

T

Taktzyklen pro Befehl
Taktzyklen pro Befehl ist die mittlere Anzahl der pro ausgeführtem Befehl benötigten Taktzyklen für ein bestimmtes Programm und eine bestimmte Implementierung.
Ein Programm mit mehr Befehlen kann früher fertig sein, wenn sein CPI-Wert ausreichend kleiner ist.

Ü

Überlauf
Ein Überlauf tritt auf, wenn ein arithmetisches Ergebnis außerhalb des darstellbaren Bereichs für die gewählte Breite und Vorzeicheninterpretation liegt.
Die Addition in RV32 behält die unteren Ergebnisbits, selbst wenn die vorzeichenbehaftete Summe nicht in 32 Bits passt.

V

Versatz eines Strukturfelds
Der Versatz eines Strukturfelds ist der Byteabstand vom Anfang einer Struktur zu einem ihrer Felder, bestimmt durch ihr ABI-Layout.
Ein Ladebefehl addiert den Feldversatz zur Adresse des aktuellen Objekts, um dessen Feld zu erreichen.
Verzögerte Bindung
Verzögerte Bindung verschiebt die Auflösung einer Prozedur aus einer gemeinsam genutzten Bibliothek bis zu ihrem ersten Aufruf und erfasst das Ergebnis für spätere Aufrufe.
Der erste Aufruf kann den dynamischen Linker starten, während spätere Aufrufe eine bereits gespeicherte Adresse verwenden.
Vorzeichenerweiterung
Vorzeichenerweiterung verbreitert ein vorzeichenbehaftetes Bitmuster, indem sie sein höchstwertiges Bit in die neuen oberen Positionen kopiert und so seinen vorzeichenbehafteten Wert erhält.
Das Laden des Bytes 0xF0 mit lb ergibt 0xFFFFFFF0, die 32-Bit-Darstellung von −16.

Z

Zeiger
Ein Zeiger ist ein Wert, der eine Objekt- oder Codeadresse darstellt und angibt, worauf ein Lade- oder Speicherzugriff oder eine indirekte Kontrollflussübertragung zugreift.
Ein Zeigerregister liefert eine Adresse, während ein Ladebefehl den an dieser Adresse gespeicherten Wert holt.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten