- Lernen
- Informatik & Informationswissenschaften
- Informatik
- Rechnerarchitektur – Prozessoren, Speicher und RISC-V
- Befehlssätze und Assemblersprache
Befehlssätze und Assemblersprache
MittelEinheit 2 von 6 in Kurs: Rechnerarchitektur – Prozessoren, Speicher und RISC-V
Was du lernst
- 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 istBeschreibe 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 ausBenenne 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 darinVerwende 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 DirektoperandenVerwende Direktoperanden für kleine Konstanten und erstelle vollständige 32-Bit-Konstanten und -Adressen mit lui, auipc, addi und den Pseudobefehlen li und la.
- Erkläre, was eine Befehlssatzarchitektur ist
- 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 SchiebebefehleRechne 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 SprungbefehleVerwende 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 AssemblerspracheWandle if-else-Anweisungen, zusammengesetzte Bedingungen, while- und for-Schleifen sowie switch-Anweisungen in Sprünge um und verfolge den entstehenden Code mit konkreten Werten.
- Verwende arithmetische Befehle, logische Befehle und Schiebebefehle
- 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 SpeicherbefehleStelle 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 MaschinencodeKodiere 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.
- Kodiere Registerbefehle, Befehle mit Direktoperanden und Speicherbefehle
- 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 ProzeduraufrufeReserviere 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 AssemblerspracheWandle Zeigerdereferenzierungen, Zeigerarithmetik, Strukturfelder, Arrayparameter und nullterminierte Zeichenketten in Ladebefehle, Speicherbefehle und Adressberechnungen um.
- Rufe Prozeduren auf und kehre unter Einhaltung der Aufrufkonvention zurück
- 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 erzeugenBeschreibe 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 startenBeschreibe, 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.
- Erkläre, wie Compiler und Assembler Objektdateien erzeugen
- 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überBeschreibe 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ätVergleiche 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.
- Stelle die Entwurfsmerkmale von RISC und CISC gegenüber
Befehlssätze und Assemblersprache einfach erklärt
1Befehle, 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.
| Register | ABI-Namen | Üblicher Zweck |
|---|---|---|
| x0 | zero | Liefert beim Lesen immer 0 |
| x1, x2 | ra, sp | Rückadresse und Stackpointer |
| x3, x4 | gp, tp | Globaler Zeiger und Thread-Zeiger |
| x5–x7, x28–x31 | t0–t6 | Temporäre Werte |
| x8–x9, x18–x27 | s0–s11 | Über Aufrufe hinweg erhaltene Werte |
| x10–x17 | a0–a7 | Argumente; 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:
| Text | Basisbefehl oder Erweiterung |
|---|---|
mv rd, rs | addi rd, rs, 0 |
nop | addi x0, x0, 0 |
not rd, rs | xori rd, rs, -1 |
neg rd, rs | sub rd, x0, rs |
li rd, c, für ein kleines c | addi 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.
2Arithmetik 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
| Befehl | Sprungbedingung |
|---|---|
beq, bne | Gleich, ungleich |
blt, bge | Vorzeichenbehaftet kleiner als, vorzeichenbehaftet größer oder gleich |
bltu, bgeu | Vorzeichenlos 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.
3Befehlsfelder 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:
| Versatzbits | Befehlsbits |
|---|---|
| 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.
4Prozeduraufrufe 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.
5Vom 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.
6Zielkonflikte 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
1Verfolge Operanden von einem Ausdruck bis zum Speicher
15 Schritte
- 1Einfü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.
- 2ErklärungLies die Befehlssatzvereinbarung
Die ISA legt fest, was
add a0, a1, a2tun 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. - 3HerausforderungZähle Befehle nach dem Assemblieren
Ein Assembler erweitert ein
lizu zwei Maschinenbefehlen, gefolgt von einemadd. 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.
- 4BeispielHalte die Zwischensumme fest
Berechne
r = (u + v) - wmitu = 18,v = 7,w = 6ina1,a2,a3:add t0, a1, a2 sub a0, t0, a3Nach dem ersten Befehl enthält
t0den Wert 25. Nach dem zweiten enthälta0den Wert 19. Die drei Eingaben bleiben verfügbar, weil keiner der Befehle ihre Register beschreibt.t0hat einen Zweck: das erste Ergebnis zu erhalten, bis die Subtraktion es liest. - 5HerausforderungÄndere das Ziel und erhalte die Quellen
Mit denselben Eingaben wird nach der Addition nun
sub a3, t0, a3ausgeführt. Was ändert sich gegenüber dem Schreiben des Ergebnisses nacha0?---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.a0bleibt unverändert, sodass ein Aufrufer, der dort das Ergebnis erwartet, dieses neue Ergebnis nicht erhalten würde. - 6ErklärungWandle einen Index in eine Adresse um
Ein Array ganzer Zahlen beginnt an der Adresse in
s1. Jedesintin RV32 ILP32 belegt 4 Bytes, daher beginnt Element 2 genau 8 Bytes nach der Basis und Element 5 genau 20 Bytes danach. Der Ladebefehllw t0, 8(s1)liest den Wert bei Element 2; er schreibt nicht die Adresse selbst nacht0. Das Basisregister bleibt unverändert. - 7BeispielLaden, berechnen, speichern
Implementiere
A[5] = A[2] + rmitr = 19ina0: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 nachA[5]. Das Lesen und Schreiben des Speichers benötigt separate Befehle um die arithmetische Operation herum. Der Wert vonA[2]bleibt 11. - 8HerausforderungErkenne die falsche Einheit in einem Versatz
Jemand ändert die erste Zeile zu
lw t0, 2(s1), weil der AusdruckA[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.
- 9BeispielLies ein Byte aus einem gespeicherten Wort
Speichere
0x7193D2A6an einer ausgerichteten Adresse auf einem Little-Endian-Rechner. Seine Bytes in aufsteigender Adressreihenfolge sind0xA6,0xD2,0x93,0x71. Ein Byte-Ladebefehl bei Versatz 1 wählt0xD2aus.lbuliefert 210, währendlbdurch Erweiterung von Bit 7 in die oberen Registerpositionen −46 liefert. Das Speicherbyte ist identisch; der Ladebefehl bestimmt die Interpretation. - 10ErklärungBestimme den vorzeichenbehafteten unteren Teil
Erstelle nun
0x00003D20in einem Register. Seine unteren 12 Bits sind0xD20, was als vorzeichenbehafteter 12-Bit-Direktoperand −736 darstellt. Der obere Schritt muss daher einen 4096-Byte-Block höher beginnen, als die sichtbare0x3nahelegt. Die Wahl von0x4ergibt0x4000, wovon die Subtraktion von0x2E0den gewünschten Wert erreicht. - 11BeispielErstelle und prüfe die Konstante
Verwende:
lui t1, 0x4 addi t1, t1, -736Der 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. - 12HerausforderungKorrigiere die Aufteilung einer Konstante
Eine Aufteilung verwendet
lui t1, 0x3undaddi t1, t1, -736. Wie weit liegt ihr Ergebnis von0x3D20entfernt, und welches Feld muss korrigiert werden?---reveal---
Sie ergibt
0x2D20, genau 4096 zu wenig. Erhöhe das obere Feld von0x3auf0x4und behalte den vorzeichenbehafteten unteren Summanden. Würde der Summand in das positive0xD20geändert, überschritte er den positiven vorzeichenbehafteten Bereich vonaddi. - 13TippVerfolge 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.
- 14HerausforderungVerbinde Adress- und Wertschritte
Bei der Arraybasis
s1ist das ElementA[6]gleich 17.t1enthält die korrekt erstellte Konstante0x3D20. Was verändertlw 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 inA[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 ins1bleiben unverändert. - 15ZusammenfassungVerfolge 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.
2Verfolge Arithmetik durch Verzweigungen und Schleifen
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMache den Ausführungspfad sichtbar
- 2BeispielVerschiebe das Feld an die richtige Position
- 3BeispielBehalte nur die Feldbreite
- 4HerausforderungSage eine vorzeichenbehaftete Verschiebung voraus
- 5ErklärungVergleiche mit der richtigen Interpretation
- 6BeispielUnterscheide einen Sprung von einem Aufruf
- 7HerausforderungPrüfe ein Muster auf zwei Arten
- 8BeispielZeichne die beiden Pfade einer if-Anweisung
- 9ErklärungPrüfe die Schranke vor dem Laden
- 10BeispielVerfolge drei Ausführungen des Schleifenrumpfs
- 11HerausforderungZähle den gesamten ausgeführten Pfad
- 12VerbindungErsetze wiederholte Skalierung durch eine fortlaufende Adresse
- 13HerausforderungSichere einen Ladezugriff durch Kurzschlussauswertung ab
- 14ZusammenfassungPrüfe Werte und Pfade gemeinsam
3Kodiere einen Befehl und rekonstruiere einen bedingten Sprung
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungÜbersetze in beide Richtungen
- 2ErklärungBestimme Format und Operanden
- 3BeispielSchreibe die Felder in voller Breite
- 4BeispielWandle das Wort um und dekodiere es
- 5HerausforderungBestimme die Wertquelle eines Speicherbefehls
- 6ErklärungMiss den Rückwärtsabstand
- 7BeispielTeile den Direktoperanden des Sprungs auf
- 8BeispielSetze den vollständigen Sprungbefehl zusammen
- 9HerausforderungErgänze das fehlende Nullbit
- 10VerbindungErhalte den relativen Abstand beim Verschieben von Code
- 11HerausforderungErkenne eine falsche Dekodierung am Operationscode
- 12ZusammenfassungPrüfe ein Maschinenwort rückwärts
4Erhalte Werte über verschachtelte Aufrufe hinweg
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungSichere, was der Aufrufer noch benötigt
- 2BeispielLege das Ergebnis der Wrapper-Prozedur fest
- 3ErklärungWende die Zusagen zum Erhalt von Werten an
- 4HerausforderungWähle den Zustand, der den Aufruf überdauern muss
- 5BeispielReserviere und fülle den Frame
- 6BeispielVerbinde die Ergebnisse vor der Speicherfreigabe
- 7ErklärungWiederhole das Muster rekursiv
- 8HerausforderungLöse die gesicherten Argumente beim Zurückkehren auf
- 9ErklärungUnterscheide eine Adresse von ihrem Objekt
- 10BeispielWähle Byteoperationen für eine Zeichenkette
- 11HerausforderungEnde an den richtigen Adressen
- 12TippStelle den gesicherten Wert wieder her, nicht das neue Ergebnis
- 13VerbindungDie Objektlebensdauer folgt dem Frame
- 14HerausforderungErhalte einen Zielzeiger über einen Hilfsaufruf hinweg
- 15ZusammenfassungPrüfe eine vollständige Aufrufgrenze
5Verfolge einen Aufruf vom Quellcode bis zum Programmstart
11 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungFrag dich, was jedes Werkzeug weiß
- 2ErklärungKompiliere für eine Ziel-ISA
- 3BeispielLass den externen Aufruf unvollständig
- 4HerausforderungUnterscheide Datenausgabe und Ausführung
- 5BeispielPlatziere das Zielmodul
- 6BeispielPasse den relativen Verweis an
- 7ErklärungWähle, wann Bibliothekscode bereitgestellt wird
- 8HerausforderungBestimme die Phase des Fehlers
- 9VerbindungErzeuge einen Prozess vor dem Aufruf von main
- 10HerausforderungErkläre, warum eine Objektdatei nicht das fertige Programm ist
- 11ZusammenfassungOrdne einen neuen Fehler seiner Phase zu
6Vergleiche Entwürfe mit dem richtigen Kennwert
10 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungHalte drei Messgrößen auseinander
- 2BeispielVergleiche die sichtbare Speicheroperation
- 3ErklärungSetze Kodierung und Entwurfsentscheidung in Beziehung
- 4HerausforderungOrdne ein Merkmal sorgfältig ein
- 5BeispielBerechne die erste Ausführungszeit
- 6BeispielVergleiche eine größere Befehlsanzahl
- 7HerausforderungUnterscheide Byteeinsparung und Speedup
- 8VerbindungKompatibilität reicht über die ISA hinaus
- 9HerausforderungBeurteile eine Aussage zur Assembleroptimierung
- 10ZusammenfassungNenne die Belege für den Vergleich
Befehlssätze und Assemblersprache: Übungsaufgaben
Set 1: Register, Speicher und Konstanten10 Übungsfragen
Frage 1 von 10
Antwort zeigenAntwort ausblenden
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
sub a2, a0, a1 enthält a0 den Wert 28 und a1 den Wert 9. Welcher Wert wird nach a2 geschrieben?Antwort zeigenAntwort ausblenden
a2. Vertauschte Quellen würden −19 ergeben, aber das ist hier nicht ihre Reihenfolge.Frage 3 von 10
A[7]?Antwort zeigenAntwort ausblenden
Frage 4 von 10
sw den Wert 0x86C4217D bei 0x3000. Welchen Wert liefert lbu t0, 2(s0), wenn s0 = 0x3000?Antwort zeigenAntwort ausblenden
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
lui und welcher vorzeichenbehaftete Direktoperand für addi erzeugen in RV32 den Wert 0x00002ABC?Antwort zeigenAntwort ausblenden
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
t3, ruft eine konforme Hilfsprozedur auf und benötigt danach den ursprünglichen Wert. Was muss der Aufrufer veranlassen?Antwort zeigenAntwort ausblenden
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
auipc t0, 0x2 bei 0x4000 ausgeführt, gefolgt von addi t0, t0, -24. Welcher Wert steht am Ende in t0?Antwort zeigenAntwort ausblenden
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
0xB2 wird einmal mit lb und einmal mit lbu geladen. Welche Dezimalergebnisse liefern die beiden Ladebefehle in dieser Reihenfolge?Antwort zeigenAntwort ausblenden
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
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?Antwort zeigenAntwort ausblenden
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)
Antwort zeigenAntwort ausblenden
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
add 1 zu 0xFFFFFFFF addiert?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
bgt a2, a3, Done?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
srli t0, a0, 8 und andi t0, t0, 0x1F in t0, wenn a0 = 0x0000D6B9?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
a0 = -11 der Befehl srai t0, a0, 2 und die Ganzzahldivision -11 / 4 in C, jeweils in dieser Reihenfolge?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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
while (A[i] == 6) i++; beginnt bei i = 1 mit A = {6, 6, 6, 4, 6}. Welchen Wert hat i beim Verlassen der Schleife?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)
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?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
sw a2, 20(sp) das Feld rs1?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
addi t2, s1, -36?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
0x308C hat das Ziel 0x3068. Welcher vorzeichenbehaftete Byteversatz wird kodiert?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
jal bei 0x1200 hat den vorzeichenbehafteten Versatz +84 und das Zielregister ra. Welche Ziel- und Rückadresse ergeben sich?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
addi t2, s1, -36 mithilfe der Felder des I-Formats? Hier gilt t2 = x7, s1 = x9, der ADDI-Operationscode ist 0x13 und funct3 = 000.Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
0xFEC12623. Der Operationscode 0x23 wählt einen Speicherbefehl und funct3 = 010 ein Wort. Welchen Assemblerbefehl stellt das Wort dar?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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.Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
0x30 und 0x54. Der Abschnitt wird von 0x9000 nach 0xB000 verschoben, ohne seine interne Anordnung zu ändern. Welcher Sprungversatz und welche Zieladresse ergeben sich?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
s4. Welchen Wert muss s4 bei ihrer Rückkehr enthalten?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
a0 auf das nächste Element weiter, wenn das Register einen int * enthält?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
sp = 0x8000, reserviert 32 Bytes und führt dann sw ra, 28(sp) aus. An welcher Adresse beginnt das gesicherte Wort?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
ra und s2 sichern, jeweils ein Wort. Was ist die kleinste positive Framegröße, die die erforderliche Stackausrichtung erhält?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?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
gcc -c module.c, ohne zu linken?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
main, ruft aber mix auf, das in einer zweiten Objektdatei definiert ist. Was ermöglicht dem Linker, diesen Aufruf mit der Definition zu verbinden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
a7, 7 nach a0 und führt dann ecall aus. Welcher Dienst wird angefordert?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
main fehl. Was erklärt den Fehler, und was wäre bei statisch eingebundenem Code aus dieser Bibliothek anders?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
add eax, [ebx+12] aus, wenn eax dem Register a0 und ebx dem Register s1 entspricht?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?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
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Adresse laden
la t0, Item ist ein separater Ladebefehl nötig, um den bei Item gespeicherten Wert zu lesen.Arithmetische Rechtsverschiebung
Assembler
call und kann Relokationseinträge für eine in einem anderen Modul definierte Funktion hinterlassen.Assemblerdirektive
.word 5 gibt einen 4-Byte-Wert aus, während .text den Abschnitt für den folgenden Code auswählt.Assemblersprache
add a0, a1, a2 bezeichnet eine Operation mit einer festgelegten RISC-V-Maschinencodekodierung.Aufrufkonvention
a0 bis a7.B
Basisadressierung mit Versatz
lw t0, 20(s1) beginnt das geladene Wort 20 Bytes hinter der in s1 gespeicherten Adresse.Basisblock
Bedingter Sprung
bne t0, t1, Loop kehrt nur zu Loop zurück, solange sich die beiden Registermuster unterscheiden.Befehlsformat
Befehlssatzarchitektur
Befehlszähler
Binärkompatibilität
Binärschnittstelle (ABI)
Bitmaske
0xFF behält das untere Byte und löscht alle höheren Registerpositionen.Blattprozedur
Byteadressierung
C
Callee-saved-Register
s1 intern verwenden, wenn sie vor der Rückkehr den ursprünglichen s1-Wert des Aufrufers wiederherstellt.Caller-saved-Register
t2 muss vor einem Hilfsaufruf gesichert werden, dessen Prozedur dieses Register frei verwenden darf.CISC
Codedichte
Compiler
D
Dereferenzierung
lw t0, 0(a0) liest eine 4 Byte große ganze Zahl über die aktuell in a0 gehaltene Adresse.Direktoperand
addi t0, t0, 1 ist als Direktoperand im Befehl kodiert.Disassemblierung
Dynamische Befehlsanzahl
Dynamisches Linken
E
Elementabstand
Epilog
sp wiederhergestellt, bleiben ihre Versätze relativ zum reservierten Frame.F
Fall-through
Funktionsfeld
funct7-Bits ADD oder SUB darstellen.G
Ganzzahlregister
I
Implizites Bit
Indirekter Sprung
jalr kann einen Aufruf über einen Funktionszeiger, eine Rückkehr oder einen Sprung über eine Sprungtabelle implementieren.J
Just-in-time-Kompilierung
K
Kleiner-als-Vergleich
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
Kurzschlussauswertung
&& verhindert in C die Auswertung des zweiten Operanden.L
Lader
Linker
Little-Endian-Byte-Reihenfolge
0x12345678 legt 0x78 im ersten und 0x12 im vierten Byte ab.Load-Store-Architektur
Logische Rechtsverschiebung
M
Mikroarchitektur
Mikrooperation
N
Natürliche Ausrichtung
Nibble
Nullerweiterung
0xF0 mit lbu ergibt den positiven Wert 240 in einem 32-Bit-Register.Nullregister
zero schreiben, wenn das Programm keine Rückadresse sichern muss.Nullterminator
O
Oberer Direktoperand
lui beim Erstellen eines RV32-Registerwerts über 12 untere Nullen setzt.addi kann einen vorzeichenbehafteten unteren Teil liefern, um eine vollständige 32-Bit-Konstante zu bilden.Objektlebensdauer
Operationscode
P
PC-relative Adresse
auipc-Folge kann lokale Daten erreichen, selbst wenn die gesamte Gruppe aus Code und Daten verschoben wird.Positionsunabhängiger Code
Prolog
ra vor einem verschachtelten Aufruf schützt die Adresse, die zur Rückkehr zum äußeren Aufrufer benötigt wird.Pseudobefehl
li erzeugten Maschinenbefehle hängt vom Wert seines konstanten Operanden ab.R
Registerallokation
Registerfeld
a0 als Registernummer 10 dar, die binär als 01010 geschrieben wird.Rekursion
Relative Relokation
Relokierbare Objektdatei
-c stoppt vor der für eine ausführbare Datei nötigen Kombination von Bibliotheken und Modulen.RISC
S
Schleifenschranke
Skalierte Indexadressierung
Speicherprogrammkonzept
Spilling
Sprung mit Rückadressensicherung
jal ra, Helper erfasst, wo der Aufrufer nach der Rückkehr von Helper fortfahren soll.Sprungtabelle
Sprungversatz
Stackframe
Stackpointer
sp reserviert gemäß RISC-V-Konvention einen ausgerichteten Frame bei niedrigeren Adressen.Statisches Linken
Statusflags
Symboltabelle
Systemaufruf
ecall mit 93 in a7 die Beendigung an und verwendet a0 für den Status.T
Taktzyklen pro Befehl
Ü
Überlauf
V
Versatz eines Strukturfelds
Verzögerte Bindung
Vorzeichenerweiterung
0xF0 mit lb ergibt 0xFFFFFFF0, die 32-Bit-Darstellung von −16.Z
Zeiger
- 1Abstraktion und Rechenleistung
- 2Befehlssätze und Assemblersprache
- 3Rechnerarithmetik
- 4Prozessor-Datenpfad und Pipelining
- 5Speicherhierarchie und Caches
- 6Mehrkernprozessoren
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.