💻 C und Assembler mit Raspberry Pi

Multiplizieren, Dividieren und Akkumulation

In diesem Abschnitt behandeln wir die Grundlagen der Multiplikation und Division im ARM64-Assembler. Wir schauen uns an, wie man mit negativen Zahlen umgeht, wie man Überläufe bei großen Multiplikationen erkennt und wie akkumulierende Operationen funktionieren.

Einfache Multiplikation: mul-Befehl

Der mul-Befehl multipliziert zwei 64-Bit-Zahlen und speichert das Ergebnis in einem Zielregister. Dabei werden immer nur die unteren 64 Bit des Ergebnisses behalten.

Syntax: 

mul <Zielregister>, <Quellregister1>, <Quellregister2>

Beispiel:

.global _start

_start:
    mov x0, #5       // Setze x0 auf 5
    mov x1, #6       // Setze x1 auf 6
    mul x2, x0, x1   // x2 = x0 * x1 (5 * 6 = 30)

    // Programm beenden
    mov x8, #93      // exit system call
    svc 0            // system call

Multiplikation und Umgang mit negativen Zahlen

In ARM64 (wie in den meisten modernen Architekturen) funktioniert die binäre Multiplikation für vorzeichenbehaftete (signed) und vorzeichenlose (unsigned) Zahlen auf Bit-Ebene identisch. Der mul-Befehl liefert also auch für negative Zahlen im unteren 64-Bit-Teil das korrekte Ergebnis.

Beispiel mit negativen Zahlen:

.global _start

_start:
    mov x0, #-5       // Setze x0 auf -5
    mov x1, #6        // Setze x1 auf 6
    mul x2, x0, x1    // x2 = x0 * x1 (-5 * 6 = -30)

    // Programm beenden
    mov x8, #93       // exit system call
    svc 0             // system call

Umgang mit Überlauf bei 64-Bit-Multiplikation

Das mul-Kommando in ARM64-Assembler multipliziert zwei 64-Bit-Register und speichert das Ergebnis in einem einzigen 64-Bit-Register. Problematisch wird es, wenn das Resultat größer als 64 Bit wird, da mul das Ergebnis auf die unteren 64 Bit beschränkt und keine Information über einen möglichen Überlauf bereitstellt.

Beispiel für das Problem

Angenommen, wir multiplizieren zwei große 64-Bit-Zahlen, deren Produkt mehr als 64 Bit umfasst:

.global _start

_start:
    mov x0, #0xFFFFFFFFFFFFFFFF  // Setze x0 auf den maximalen 64-Bit-Wert
    mov x1, #2                  // Setze x1 auf 2
    mul x2, x0, x1              // x2 = x0 * x1 (Ergebnis würde 0x1FFFFFFFFFFFFFFFE sein, aber x2 enthält nur die unteren 64 Bits: 0xFFFFFFFFFFFFFFFE)

    // Programm beenden
    mov x8, #93                 // exit system call
    svc 0                       // system call

In diesem Fall ist die erwartete 128-Bit-Zahl 0x1FFFFFFFFFFFFFFFE, aber x2 enthält nur 0xFFFFFFFFFFFFFFFE, also die unteren 64 Bits des Ergebnisses.

Lösung mit umulh zum Feststellen des Überlaufs

Um sicherzustellen, dass das Ergebnis der Multiplikation korrekt und vollständig ist, können wir den Befehl umulh (unsigned multiply high) verwenden. Dieser Befehl multipliziert zwei 64-Bit-Zahlen und gibt die oberen 64 Bits des 128-Bit-Resultats zurück.

Gebrauch von mul und umulh für 128-Bit-Multiplikation

  • mul: Multipliziert zwei 64-Bit-Zahlen und behält die unteren 64 Bits.
  • umulh: Multipliziert zwei 64-Bit-Zahlen und gibt die oberen 64 Bits des Resultats zurück.

Beispiel:

.global _start

_start:
    mov x0, #0xFFFFFFFFFFFFFFFF  // Setze x0 auf den maximalen 64-Bit-Wert
    mov x1, #2                  // Setze x1 auf 2

    // Multipliziere x0 und x1
    mul x2, x0, x1              // x2 = untere 64-Bit des Ergebnisses
    umulh x3, x0, x1            // x3 = obere 64-Bit des Ergebnisses

    // Hiermit haben wir das volle 128-Bit-Ergebnis in x3:x2
    // x3 enthält die oberen 64-Bits: 0x1 
    // x2 enthält die unteren 64-Bits: 0xFFFFFFFFFFFFFFFE

    // Programm beenden
    mov x8, #93                  // exit system call
    svc 0                        // system call

Erklärung der Schritte:

  • Setze x0 und x1: Wir setzen zwei große Werte in die Register x0 und x1.
  • Multipliziere und speichere die unteren 64-Bits:

mul x2, x0, x1: Multipliziert x0 und x1 und speichert das Ergebnis in x2. Dies speichert die unteren 64 Bits des Produkts.

  • Multipliziere und speichere die oberen 64-Bits:

umulh x3, x0, x1: Multipliziert x0 und x1 und speichert die oberen 64 Bits des Produkts in x3.

Nun haben wir das vollständige 128-Bit-Ergebnis verteilt auf die Register x2 (untere 64 Bits) und x3 (obere 64 Bits). Auf diese Weise können wir sicherstellen, dass das gesamte Produkt genau erfasst wird.

Division: udiv-Befehl

ARM64 verfügt über Hardware-Befehle für die Ganzzahldivision. Es ist wichtig, den richtigen Befehl für den Datentyp zu wählen.

udiv (Unsigned Divide): Für vorzeichenlose Zahlen. sdiv (Signed Divide): Für vorzeichenbehaftete Zahlen.

Syntax:

udiv <Zielregister>, <Quellregister1>, <Quellregister2>

Beispiel:

.global _start

_start:
    mov x0, #30       // Setze x0 auf 30
    mov x1, #6        // Setze x1 auf 6
    udiv x2, x0, x1   // x2 = x0 / x1 (30 / 6 = 5)

    // Programm beenden
    mov x8, #93       // exit system call
    svc 0             // system call

Umgang mit negativen Zahlen bei der Division

Bei der Division von vorzeichenbehafteten Zahlen verwenden wir sdiv (signed divide).

Beispiel:

.global _start

_start:
    mov x0, #-30      // Setze x0 auf -30
    mov x1, #6        // Setze x1 auf 6
    sdiv x2, x0, x1   // x2 = x0 / x1 (-30 / 6 = -5)

    // Programm beenden
    mov x8, #93       // exit system call
    svc 0             // system call

Division mit Rest (Modulo)

Oft benötigt man nicht nur das Ergebnis der Division, sondern auch den Rest. ARM64 hat keinen direkten mod-Befehl, aber wir können den Rest effizient mit msub (Multiply-Subtract) berechnen:

Rest = Dividend - (Quotient * Divisor)

.global _start
_start:
    mov x0, #30      // Dividend
    mov x1, #7       // Divisor
    
    udiv x2, x0, x1  // x2 = Quotient (30 / 7 = 4)
    
    // msub <Ziel>, <Faktor1>, <Faktor2>, <Subtrahend>
    // Berechnet: Subtrahend - (Faktor1 * Faktor2)
    msub x3, x2, x1, x0  // x3 = 30 - (4 * 7) = 2 (Der Rest)

    mov x8, #93
    svc 0

Akkumulation

In ARM64-Assembler gibt es spezielle Befehle, die die Akkumulation unterstützen. Dies ermöglicht es, Multiplikationsoperationen mit einer zusätzlichen Additions- oder Subtraktionsoperation zu kombinieren. Diese Befehle sind besonders nützlich für komplexere arithmetische Berechnungen und Optimierungen.

Akkumulierende Multiplikation: mla und mls

Die Befehle mla (multiply-accumulate) und mls (multiply-subtract) bilden die Grundlage für akkumulative Operationen bei der Multiplikation.

Der mla-Befehl

Der mla-Befehl multipliziert zwei Register und addiert das Ergebnis zu einem dritten Register.

Syntax:

mla <Zielregister>, <Quellregister1>, <Quellregister2>, <Akkumulationsregister>

Beispiel:

.global _start

_start:
    mov x0, #2       // Setze x0 auf 2
    mov x1, #3       // Setze x1 auf 3
    mov x2, #4       // Setze x2 auf 4
    mla x3, x0, x1, x2 // x3 = (x0 * x1) + x2 = (2 * 3) + 4 = 10

    // Programm beenden
    mov x8, #93      // exit system call
    svc 0            // system call

In diesem Beispiel:

x0 * x1 ergibt 6. Dann wird 6 zu x2 (4) addiert, um das endgültige Ergebnis 10 in x3 zu speichern.

Der mls-Befehl

Der mls-Befehl multipliziert zwei Register und subtrahiert das Ergebnis von einem dritten Register.

Syntax:

mls <Zielregister>, <Quellregister1>, <Quellregister2>, <Akkumulationsregister>

Beispiel:

.global _start

_start:
    mov x0, #5       // Setze x0 auf 5
    mov x1, #3       // Setze x1 auf 3
    mov x2, #20      // Setze x2 auf 20
    mls x3, x0, x1, x2 // x3 = x2 - (x0 * x1) = 20 - (5 * 3) = 20 - 15 = 5

    // Programm beenden
    mov x8, #93      // exit system call
    svc 0            // system call

In diesem Beispiel:

x0 * x1 ergibt 15. Dann wird 15 von x2 (20) subtrahiert, um das endgültige Ergebnis 5 in x3 zu speichern.