💻 C und Assembler mit Raspberry Pi

Laden und Speichern von Werten

In diesem Abschnitt lernen Sie, wie Daten zwischen den schnellen CPU-Registern und dem größeren, aber langsameren Hauptspeicher bewegt werden. Dies ist die Grundlage jeder Datenmanipulation in Assembler.

Register und Speicher: Die Grundlagen

  • Register sind extrem schnelle Speicherplätze direkt in der CPU. ARM64 besitzt 31 allgemeine 64-Bit-Register (`x0` bis `x30`). Die unteren 32 Bit eines Registers können auch separat als `w0` bis `w30` angesprochen werden.
  • Der Hauptspeicher (RAM) wird über Adressen angesprochen. Die CPU kann nicht direkt im Speicher rechnen. Daher ist der Standardablauf immer:

  1. Adresse der Variable ermitteln.
  2. Wert aus dem Speicher in ein Register laden (`ldr`).
  3. Wert im Register manipulieren (z. B. mit `add` oder `mov`).
  4. Wert zurück in den Speicher schreiben (`str`).

Der grundlegende Ablauf: `ldr` und `str`

Um mit einer Variable im Speicher zu arbeiten, müssen wir streng zwischen der Adresse der Variable und dem Wert an dieser Adresse unterscheiden.

Beispiel: Lesen und Schreiben einer Variable

.data
meine_zahl: 
    .quad 0          // Reserviert 8 Byte (64 Bit) im Speicher, initialisiert mit 0

.text
.global _start
_start:
    // 1. Adresse der Variable laden (Pseudo-Instruktion)
    // Der Assembler sorgt dafür, dass die Adresse von 'meine_zahl' in x0 steht.
    ldr x0, =meine_zahl      

    // 2. Einen neuen Wert vorbereiten
    mov x1, #42              // x1 enthält jetzt den Wert 42

    // 3. Wert in den Speicher schreiben (Store)
    // Schreibe den Inhalt von x1 (42) an die Adresse, die in x0 steht.
    str x1, [x0]             

    // 4. Wert aus dem Speicher lesen (Load)
    // Lese den Wert an der Adresse in x0 und speichere ihn in x2.
    ldr x2, [x0]             // x2 enthält jetzt 42

(Tipp für GDB: Um nach dem `str`-Befehl zu prüfen, ob der Wert im Speicher steht, nutzen Sie `x /1g $x0`. Das `g` steht für "giant word" (64 Bit) und `$x0` ist die Adresse, die wir ansehen wollen.)

Datentypen und Suffixe

Standardmäßig laden `ldr` und speichern `str` 64 Bit (bei `x`-Registern) oder 32 Bit (bei `w`-Registern). Um kleinere Datenmengen (Bytes oder Halfwords) zu verarbeiten, hängen wir ein Suffix direkt an den Befehl.

Suffix Befehl (Beispiel) Bedeutung Wichtiger Hinweis
(ohne) ldr x0, [x1] 64-Bit (Doubleword) Standard bei `x`-Registern
(ohne) ldr w0, [x1] 32-Bit (Word) Standard bei `w`-Registern
b ldrb / strb Byte (8 Bit) Liest/Schreibt 1 Byte.
sb ldrsb Signed Byte Nur beim Laden! Liest 1 Byte und füllt die oberen Bits mit dem Vorzeichen auf 64 Bit auf.
h ldrh / strh Halfword (16 Bit) Liest/Schreibt 2 Bytes.
sh ldrsh Signed Halfword Nur beim Laden! Liest 2 Bytes und erweitert das Vorzeichen.
sw ldrsw Signed Word Nur beim Laden! Liest 4 Bytes und erweitert das Vorzeichen auf 64 Bit.

Wichtig: Es gibt keine "Signed"-Store-Befehle (wie strsb). Beim Speichern werden einfach nur die unteren Bits des Registers in den Speicher geschrieben; das Vorzeichen ist dafür irrelevant.

Adressierungsarten (Wie die Adresse in `[...]` gebildet wird)

Der Teil in den eckigen Klammern [...] bestimmt, wo im Speicher gelesen oder geschrieben wird. ARM64 bietet hierfür flexible Modi.

Register-indirekt (Basis)

Die Adresse steht komplett in einem Register.

ldr x0, [x1]    // Lese von der Adresse, die in x1 steht

Basis + Immediate-Offset (Vorindexiert)

Ein fester Wert (Offset) wird zur Basisadresse addiert. Nützlich für Strukturen.

ldr x0, [x1, #8]    // Lese von (Adresse in x1) + 8 Bytes
str w0, [x1, #4]    // Schreibe 32-Bit-Wert an (Adresse in x1) + 4 Bytes

Register-Offset (Indizierung für Arrays)

Ein zweites Register wird als Offset verwendet. Oft wird dieser Wert noch verschoben (`lsl`), um die Größe der Array-Elemente zu berücksichtigen.

// Angenommen, x1 ist die Basisadresse eines Arrays aus 32-Bit-Wörtern (4 Byte)
// und w2 enthält den Index (z.B. 3).
ldr w0, [x1, w2, uxtw #2]  // Adresse = x1 + (w2 * 4)

(Hinweis: `uxtw` bedeutet "Unsigned Extend Word" und stellt sicher, dass das 32-Bit-Register `w2` korrekt als 64-Bit-Adresse verrechnet wird).

Post-Indexierte Adressierung

Die Adresse wird für den Zugriff genutzt, und anschließend wird das Basisregister automatisch um den Offset erhöht. Perfekt für das Durchlaufen von Arrays oder Strings.

ldr x0, [x1], #8    // 1. Lese Wert von Adresse in x1 nach x0.
                    // 2. Erhöhe danach x1 um 8 (x1 = x1 + 8).

Praxisbeispiel: Array in einer Schleife verarbeiten

Hier kombinieren wir Register-Offset und Schleifen, um ein Array zu füllen.

.data
mein_array: 
    .fill 10, 4, 0    // 10 Elemente, je 4 Byte (Word), initialisiert mit 0

.text
.global _start
_start:
    ldr x0, =mein_array   // x0 = Basisadresse des Arrays
    mov w1, #1            // w1 = Laufvariable i (Start bei 1)

forloop:
    // 1. Berechne den Wert: i * 8
    // Linksverschiebung um 3 ist dasselbe wie Multiplikation mit 8
    lsl w4, w1, #3        

    // 2. Speichere den Wert ins Array: Adresse = x0 + (i * 4)
    // Wir nutzen w1 als Index und verschieben ihn um 2 (== *4) für die Word-Größe
    str w4, [x0, w1, uxtw #2] 

    // 3. Laufvariable erhöhen: i = i + 1
    add w1, w1, #1        

    // 4. Schleifenbedingung: Wenn i <= 10, springe zurück
    cmp w1, #10           
    b.le forloop          

    // Ende der Schleife

Effiziente Mehrfachzugriffe: `ldp` und `stp`

Um die Leistung zu steigern, erlaubt ARM64 das gleichzeitige Laden oder Speichern von zwei 64-Bit-Registern (insgesamt 128 Bit) mit einem einzigen Befehl. Dies reduziert die Anzahl der Instruktionen und nutzt den Speicherbus effizienter.

  • ldp (Load Pair): Lädt zwei Register aus aufeinanderfolgenden Speicherstellen.
  • stp (Store Pair): Speichert zwei Register in aufeinanderfolgende Speicherstellen.
 .data
grosser_wert: 
    .octa 0x11111111111111112222222222222222  // 16 Byte (128 Bit)

.text
    ldr x0, =grosser_wert
    
    // Lade die unteren 64 Bit in x1, die oberen 64 Bit in x2
    ldp x1, x2, [x0]      
    
    // Modifiziere die Werte...
    add x1, x1, #1
    
    // Schreibe beide Register zurück in den Speicher
    stp x1, x2, [x0]     

Zusammenfassung der Befehle

Aufgabe Befehl Beispiel Erklärung
Konstante laden mov mov x0,#42 Lädt kleine, direkte Werte in ein Register.
Adresse laden ldr (Pseudo) ldr x0, =var Lädt die Adresse einer Variable/Konstante (Assembler-Magie).
Wert lesen ldr ldr x1,[x0] Liest den Inhalt an der Adresse in x0 nach x1.
Wert schreiben str str x1,[x0] Schreibt den Inhalt von x1 an die Adresse in x0
Teilweise lesen ldrb, ldrh ... ldrb w1, [x0] Liest nur 1 oder 2 Byte (mit oder ohne Vorzeichenerweiterung).
Paarweise zugreifen ldp / stp ldp x1, x2, [x0] Lädt/Speichert zwei 64-Bit-Register gleichzeitig für bessere Performance.