Tracks
/
x86-64 Assembly
x86-64 Assembly
/
Übungen
/
Inventarverwaltung
Inventarverwaltung

Inventarverwaltung

Lernübung

Einführung

Ganzzahlen

Binäre Notation

Eine Ganzzahl ist eine Abstraktion, die ganze Zahlen wie 4, -2, 0 oder 64532 darstellt.

Um eine Ganzzahl als Folge von Bytes darzustellen, wird die Binäre Notation verwendet. In dieser Notation repräsentiert jedes Bit in der Folge eine bestimmte Zweierpotenz, wobei der Wert zunimmt, wenn der Index des Bits von rechts nach links steigt.

Vorzeichenlose Zahlen

Kann die Zahl nur nicht-negativ sein, nennt man sie eine vorzeichenlose Zahl.

Vorzeichenlose Zahlen werden direkt als Summe der Zweierpotenzen dargestellt, die allen gesetzten Bits ihrer Folge entsprechen.

Der Bereich der darstellbaren nicht-negativen Ganzzahlen in einem Register reicht von 0 (kein Bit gesetzt) bis 2⁶⁴ - 1 (Summe aller 64 gesetzten Bits).

Eine vorzeichenlose Zahl auf eine größere Größe zu erweitern, geschieht, indem man alle oberen Bits mit 0 füllt, sodass kein neues Bit zum Wert beiträgt. Das nennt man Nullerweiterung.

Die Anweisung movzx (z steht für zero) erweitert einen 8-Bit- oder 16-Bit-Quelloperanden durch Nullerweiterung zu einem größeren Zieloperanden. Ein 32-Bit-Quelloperand wird mit einem einfachen mov immer durch Nullerweiterung auf alle 64 Bits des Zieloperanden erweitert.

Vorzeichenbehaftete Zahlen

Kann eine Ganzzahl positive oder negative Werte annehmen, nennt man sie eine vorzeichenbehaftete Zahl.

Um negative Zahlen darzustellen, verwendet x86-64 die Zweierkomplement-Darstellung.

Im Zweierkomplement werden vorzeichenbehaftete Zahlen ebenfalls als Summe der Zweierpotenzen dargestellt, die den gesetzten Bits entsprechen. Ist jedoch das oberste Bit gesetzt, wird es subtrahiert, statt zu den anderen addiert zu werden.

Da dieses Bit einem höheren Wert entspricht als die Summe aller anderen, bedeutet das in der Praxis, dass eine Zahl mit gesetztem Bit immer negativ ist. Dieses besondere Bit nennt man das Vorzeichenbit.

Eine vorzeichenbehaftete Zahl auf eine größere Größe zu erweitern, bedeutet, jedes neue obere Bit mit einer Kopie des Vorzeichenbits zu füllen, sodass der Wert erhalten bleibt. Das nennt man Vorzeichenerweiterung.

Die Anweisung movsx (s steht für sign) erweitert einen 8-Bit- oder 16-Bit-Quelloperanden durch Vorzeichenerweiterung zu einem größeren Zieloperanden. Eine Variante von movsx namens movsxd macht dasselbe von einem 32-Bit-Quelloperanden zu einem 64-Bit-Zieloperanden.

Die Anweisung neg kann verwendet werden, um das Vorzeichen einer Zahl zu ändern.

Caution

In Assembly gibt es keine Möglichkeit zu erkennen, ob eine Folge von Bytes eine vorzeichenbehaftete oder eine vorzeichenlose Zahl darstellt. Es liegt in der Verantwortung des Programmierers, diesen Bytes eine Bedeutung zu geben.

Der Einsatz von Kommentaren kann bei dieser Aufgabe eine große Hilfe sein.

Immediates

In einem früheren Konzept wurde erwähnt, dass eine konstante Zahl wie 4 oder -15 als Quelloperand für viele Anweisungen verwendet werden kann. Diese Zahlen nennt man Immediates.

Ein Immediate steht nicht in einem Register oder im Speicher: Es ist in der Anweisung selbst kodiert. In den meisten Anweisungen ist der dafür reservierte Platz nur 32 Bits breit, egal wie groß der Zieloperand ist.

Wenn der Zieloperand 64 Bits breit ist, werden diese 32 Bits vorzeichenerweitert, um ihn zu füllen. Die obere Hälfte des Operanden wird vollständig mit Kopien des obersten Bits des Immediates gefüllt, sodass nur eine Zahl im Bereich einer 32-Bit-Ganzzahl mit Vorzeichen so geschrieben werden kann:

add rax, -1          ; the immediate is sign-extended, so all 64 bits of rax are affected
add rax, 2147483647  ; the largest immediate an instruction like this accepts

Eine Zahl außerhalb dieses Bereichs kann nicht als Immediate verwendet werden. Die Ausnahme von dieser Regel ist mov, das ein volles 64-Bit-Immediate annehmen kann, wenn der Zieloperand ein Register ist. Wenn ein 64-Bit-Immediate benötigt wird, lade es zuerst mit mov in ein Register und verwende dann dieses Register:

mov rax, 3435973837           ; this works, mov can take a 64-bit immediate
mov rdx, 18446744073709551615 ; the largest immediate mov accepts
sub rdx, rax

Beachte, dass ein negatives Immediate und die vorzeichenlose Zahl mit derselben Bitdarstellung äquivalent sind und zu genau demselben Wert assemblieren:

mov rax, -1                   ; rax = 18446744073709551615
mov rax, 18446744073709551615 ; rax = -1

Summe

Die Addition zweier Zahlen kann mit der Anweisung add berechnet werden.

Es gibt auch eine inc-Anweisung mit einem Operanden, die 1 zum Wert ihres Operanden addiert:

inc rax ; rax = rax + 1

Die Summe zweier Ganzzahlen funktioniert bei vorzeichenlosen und vorzeichenbehafteten Zahlen auf dieselbe Weise.

Subtraktion

Die Subtraktion zweier Ganzzahlen erfolgt mit der Anweisung sub.

Es gibt auch eine dec-Anweisung mit einem Operanden, die 1 vom Wert ihres Operanden subtrahiert:

dec rax ; rax = rax - 1

Die Subtraktion zweier Ganzzahlen funktioniert ebenfalls bei vorzeichenlosen und vorzeichenbehafteten Zahlen auf dieselbe Weise.

Multiplikation

In x86-64 gibt es zwei verschiedene Anweisungen, um zwei Zahlen zu multiplizieren. In der Regel verwendet die vorzeichenlose Multiplikation die Anweisung mul, während die vorzeichenbehaftete Multiplikation imul verwendet.

Die Anweisung mul hat die folgende Form mit einem Operanden, wobei src der Quelloperand ist:

mul src

Die Anweisung imul kann eine Form mit einem, zwei oder drei Operanden haben:

imul src
imul dest, src
imul dest, src1, src2
Multiplikation mit einem Operanden

Bei der Form mit einem Operanden werden implizit zwei Register für die Multiplikation verwendet: rax und rdx. Wenn die Multiplikation zwei 64-Bit-Zahlen umfasst, liegen die unteren 64 Bits des Ergebnisses in rax und die oberen 64 Bits in rdx.

Dies wird üblicherweise rdx:rax genannt, um anzuzeigen, dass beide Register zusammen verwendet werden:

mul rcx ; rax = lower 64 bits of rax * rcx
        ; rdx = upper 64 bits of rax * rcx

Dasselbe geschieht bei anderen Operandengrößen. Wenn also zum Beispiel zwei 32-Bit-Zahlen multipliziert werden, werden eax und edx verwendet.

Die Ausnahme ist die Multiplikation zweier Bytes.

In diesem Fall wird statt dl:al ax verwendet. Der untere Teil von ax (al) erhält die unteren 8 Bits des Produkts, während der obere Teil (ah) die oberen 8 Bits erhält.

Caution

Register, die implizit in einer Multiplikation verwendet werden, wie rax und rdx, werden immer überschrieben. Die Werte in diesen Registern sollten vor der Operation gespeichert werden, wenn sie später noch benötigt werden.

Multiplikation mit zwei Operanden

Die Form von imul mit zwei Operanden hat einen expliziten Zieloperanden und folgt der üblichen Syntax. rdx wird nicht verwendet. Stattdessen wird das Ergebnis gekürzt, damit es in den Zieloperanden passt.

imul r8, r9 ; r8 = lower 64 bits of r8 * r9
Multiplikation mit drei Operanden

Die Form von imul mit drei Operanden hat zwei Quelloperanden, von denen der zweite immer ein Immediate (eine konstante Zahl) ist. Beide Quelloperanden werden multipliziert, und das Ergebnis wird gekürzt und im Zieloperanden abgelegt:

imul r8, r9, 100 ; r8 = lower 64 bits of r9 * 100

Beachte, dass der Zieloperand bei der Multiplikation nicht verwendet wird. Er nimmt nur das Ergebnis auf.

Umgang mit Überlauf

Sowohl die Multiplikation mit zwei als auch die mit drei Operanden kürzt das Ergebnis, damit es in die Größe des Zieloperanden passt. Eine Multiplikation mit einem Operanden bewahrt den vollen Bereich, aber sie wird üblicherweise auf zwei Register aufgeteilt, rdx und rax.

Daher ist es manchmal nützlich, die Operanden vor der Multiplikation zu erweitern, um Platz für das ganze Produkt in einem einzigen Register zu schaffen. Ein vorzeichenloser Operand wird durch Nullerweiterung erweitert, ein vorzeichenbehafteter durch Vorzeichenerweiterung:

movzx eax, di ; di and si hold unsigned 16-bit numbers
movzx ecx, si
mul ecx       ; the 32-bit product fits in eax, and edx is cleared

Division

Wie bei der Multiplikation gibt es auch zwei Anweisungen, um zwei Zahlen zu dividieren. Die vorzeichenlose Division verwendet die Anweisung div, die vorzeichenbehaftete Division idiv.

Beide Anweisungen arbeiten mit nur einem Operanden:

div src
idiv src

Die 16-Bit-, 32-Bit- und 64-Bit-Division verwenden dx:ax, edx:eax bzw. rdx:rax als Dividenden. In diesen Fällen wirken beide Register zusammen, um einen 2N-Bit-Wert zu bilden, wobei N die Größe der Operation ist (16 Bit, 32 Bit oder 64 Bit). Dieser Wert wird dann durch den Quelloperanden dividiert. Der Quotient wird in ax, eax oder rax geschrieben und der Rest in dx, edx oder rdx, je nach Größe der Operation.

Die Division zwischen Bytes ist speziell: Statt dl:al wird ax verwendet. Die unteren 8 Bits von ax (al) erhalten den Quotienten der Operation und die oberen 8 Bits (ah) den Rest.

Beachte, dass vor der Division alle Bits im Dividenden passend gesetzt sein sollten. Jedes in rdx (oder in ah bei der 8-Bit-Division) gesetzte Bit trägt zum Wert bei, der dividiert wird.

Bei der vorzeichenlosen Division sollte die obere Hälfte gelöscht werden, wenn der zu dividierende Wert in die untere Hälfte passt. Jede Anweisung, die diese Bits löscht, ist geeignet. Zum Beispiel löscht mov edx, 0 die oberen Bits bei der 32-Bit-Division.

Bei der vorzeichenbehafteten Division sollte der Wert stattdessen vorzeichenerweitert werden. Es gibt Anweisungen, die diesen Vorgang automatisieren: cbw, cwd, cdq und cqo. Die erste setzt die Bits in ah entsprechend dem Vorzeichen von al. Die anderen führen eine Vorzeichenerweiterung von ax nach dx, von eax nach edx bzw. von rax nach rdx durch.

Caution

Register, die implizit in einer Division verwendet werden, wie rax und rdx, werden immer überschrieben. Die Werte in diesen Registern sollten vor der Division gespeichert werden, wenn sie später noch benötigt werden.

Anleitung

Ein lokales Geschäft zieht mit seinem Inventar in ein größeres Lager um. Du wurdest angeheuert, um alles zu packen und umzuziehen.

Du hast vier Aufgaben, die alle mit der Abwicklung des Transports zu tun haben.

Note

Dies sind die Befehle, die in diesem Konzept erwähnt werden:

Befehl Beschreibung
add a, b a = a + b
inc a a = a + 1
sub a, b a = a - b
dec a a = a - 1
imul a rdx:rax = a * rax (signed)
imul a, b a = a * b (signed, truncated)
imul a, b, c a = b * c (signed, truncated)
mul a rdx:rax = a * rax (unsigned)
div a rax = quotient, rdx = remainder of rdx:rax / a (unsigned)
idiv a rax = quotient, rdx = remainder of rdx:rax / a (signed)
movzx a, b a = b, adding 0 to the extra bits
movsx a, b a = b, adding 1 to the extra bits if b < 0 or 0 otherwise
Note

Denk daran, dass du auf dasselbe Register mit unterschiedlichen Größen zugreifen kannst, indem du den Namen des Operanden änderst. Zum Beispiel: rax (64-Bit), eax (32-Bit), ax (16-Bit), al (8-Bit).

Die vollständige Tabelle findest du im vorherigen Konzept.

1. Ermittle das Gewicht jeder Kiste

Die Gegenstände werden in Kisten verpackt, die mit ihrem Gewicht beschriftet werden müssen. Es ist keine Waage in der Nähe, aber zum Glück weißt du, wie viel jeder Gegenstand im Durchschnitt wiegt.

Um die Sache besser zu organisieren, enthält eine Kiste nur Gegenstände von zwei verschiedenen Produkten.

Definiere eine Funktion get_box_weight, die das Gesamtgewicht einer Kiste in g zurückgibt. Diese Funktion nimmt die folgenden Parameter entgegen, und zwar in dieser Reihenfolge:

  • Die Anzahl der Gegenstände für das erste Produkt in der Kiste
  • Das Gewicht jedes Gegenstands des ersten Produkts, in g
  • Die Anzahl der Gegenstände für das zweite Produkt in der Kiste
  • Das Gewicht jedes Gegenstands des zweiten Produkts, in g

Geh davon aus, dass eine leere Kiste 500 g wiegt. Eine Konstante WEIGHT_OF_EMPTY_BOX ist am Anfang der Lösungsdatei definiert.

Beispiel:

get_box_weight(30, 40, 50, 20);
// => 2700

Alle Argumente sind nicht negative 16-Bit-Ganzzahlen, und der Rückgabewert ist eine nicht negative 32-Bit-Ganzzahl.

2. Berechne, wie viele Kisten in den Lastwagen passen

Die Kisten werden gestapelt und mit einem Lastwagen ins neue Lager gebracht. Allerdings gibt es im Lastwagen nur begrenzt Platz in der Höhe.

Definiere eine Funktion max_number_of_boxes, die zurückgibt, wie viele Kisten einer bestimmten Höhe im Lastwagen übereinander (eine auf die andere) gestapelt werden können.

Diese Funktion nimmt die Höhe der Kiste in cm als Parameter entgegen. Geh davon aus, dass die Innenhöhe des Lastwagens 300 cm beträgt. Eine Konstante TRUCK_HEIGHT ist am Anfang der Lösungsdatei definiert.

Beispiel:

max_number_of_boxes(30);
// => 10

Das Argument und der Rückgabewert sind nicht negative 8-Bit-Ganzzahlen. Die Kistenhöhe beträgt immer mindestens 2, das Ergebnis passt also in 8 Bit.

3. Prüfe, ob alle Produkte erfasst sind

Im neuen Lager gibt es eine Checkliste mit der Anzahl der Gegenstände, die für jedes Produkt noch nicht erfasst sind. Für jede neue Kiste, die dorthin gebracht wird, musst du den neuen Wert in der Checkliste für jedes Produkt in der Kiste berechnen.

Definiere eine Funktion items_to_be_moved, die zurückgibt, wie viele Gegenstände für ein bestimmtes Produkt noch ins neue Lager gebracht werden müssen. Diese Funktion nimmt die folgenden Parameter entgegen, und zwar in dieser Reihenfolge:

  • Die Anzahl der Gegenstände, die für ein Produkt noch nicht erfasst sind
  • Die Anzahl der Gegenstände für das Produkt in einer Kiste

Beispiel:

items_to_be_moved(76532, 120);
// => 76412

Die Argumente sind nicht negative 32-Bit-Ganzzahlen. Der Rückgabewert ist eine 32-Bit-Ganzzahl. Falls im Prozess ein Fehler auftritt, kann das Ergebnis eine negative Zahl sein.

4. Ermittle die Bezahlung

Deine Bezahlung richtet sich danach, wie viele Kisten bewegt wurden und wie viele Fahrten mit dem Lastwagen nötig waren. Für jede Kiste bekommst du 5 Dollar und für jede Fahrt 220 Dollar. Die Konstanten PAY_PER_BOX und PAY_PER_TRUCK_TRIP sind am Anfang der Lösungsdatei definiert.

Beachte, dass du einen Teil dieser Bezahlung im Voraus erhalten haben könntest, um Anfangskosten zu decken, und diese Vorauszahlung sollte von der endgültigen Bezahlung abgezogen werden. Außerdem sind manche Produkte nicht versichert, und deine Bezahlung wird zusätzlich um den Wert der zerbrochenen oder fehlenden Gegenstände dieser Produkte gekürzt. Wenn du nicht aufpasst, kann es passieren, dass du am Ende Geld schuldest!

Das bedeutet, der Nettobetrag, der dir zusteht oder den du schuldest, ist:

net = boxes * PAY_PER_BOX + trips * PAY_PER_TRUCK_TRIP - up_front - broken_items * item_value

Diese Bezahlung oder Schuld wird zu gleichen Teilen zwischen dir und einer Anzahl von Arbeitern aufgeteilt, die du eingestellt hast. Jegliches verbleibende Geld oder jegliche verbleibende Schuld gehört dir. Wenn der Nettobetrag zum Beispiel 100 beträgt und unter 6 Personen aufgeteilt wird (du und 5 Arbeiter), bekommst du 20 (100/(5 + 1) = 16 plus die verbleibenden 4).

Definiere eine Funktion calculate_payment, die zurückgibt, wie viel dir am Ende ausgezahlt werden sollte oder wie viel du zahlen musst. Diese Funktion nimmt die folgenden Parameter entgegen, und zwar in dieser Reihenfolge:

  • Wie viel du im Voraus erhalten hast, als nicht negative 64-Bit-Ganzzahl
  • Die Gesamtzahl der bewegten Kisten, als nicht negative 32-Bit-Ganzzahl
  • Die Anzahl der Fahrten mit dem Lastwagen, als nicht negative 32-Bit-Ganzzahl
  • Die Anzahl der zerbrochenen oder fehlenden Gegenstände, als nicht negative 32-Bit-Ganzzahl
  • Der Wert jedes verlorenen Gegenstands, als nicht negative 64-Bit-Ganzzahl
  • Die Anzahl der Arbeiter, mit denen du die Bezahlung oder Schuld aufteilst, als positive 8-Bit-Ganzzahl

Beispiel:

calculate_payment(2000, 1000, 5, 21, 2, 1);
// => 2029

Der Rückgabewert ist eine 64-Bit-Ganzzahl.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
x86-64 Assembly Exercism

Bereit, mit Inventarverwaltung zu starten?

Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.