Der Speicher wird für ein Programm normalerweise vom Betriebssystem (OS) in einer allgemeinen Aufteilung abgebildet:
| Adresse | Speicherbereich |
|---|---|
| hoch | Stack |
| ... | |
| Heap | |
| Lese-/Schreibsegment | |
| Code-/Nur-Lese-Segment | |
| niedrig | reserviert |
Der Speicher ist in Segmenten organisiert, die wiederum in Abschnitte mit unterschiedlichen Berechtigungen unterteilt sind.
Die Funktionen, die wir bisher definiert haben, lagen alle in section .text. Dieser Abschnitt enthält ausführbare, schreibgeschützte Daten. Andere Abschnitte werden verwendet, um Datenvariablen zu deklarieren, die schreibgeschützt oder les- und schreibbar sein können, aber nicht ausführbar sind.
Die initialisierten Daten werden in der section .data deklariert.
In NASM (The Netwide Assembler, der Assembler, der in diesem Track verwendet wird), hat eine initialisierte Variable einen Namen, eine Direktive, die die Datengröße angibt, und eine Liste von Werten, die durch Kommas getrennt sind.
Diese Bestandteile werden jeweils durch ein Leerzeichen voneinander getrennt; auf das Label kann optional ein : folgen.
Die wichtigsten Direktiven und ihre zugehörigen Datengrößen sind:
| Direktive | Größe |
|---|---|
| db | 1 Byte |
| dw | 2 Bytes |
| dd | 4 Bytes |
| dq | 8 Bytes |
Zum Beispiel deklariert dies eine Byte-Variable namens space mit dem Wert 10:
section .data
space db 10
Variablen, die in section .data deklariert werden, sind veränderbar, d. h. sie sind les- und schreibbar.
Sie haben außerdem eine statische Speicherdauer, was bedeutet, dass sie während der gesamten Laufzeit des Programms existieren.
Die section .rodata ähnelt section .data.
Beide Abschnitte enthalten initialisierte Daten, die auf die gleiche Weise deklariert werden und die gleiche Speicherdauer haben.
Der Hauptunterschied besteht darin, dass Daten in section .rodata unveränderlich sind, d. h. schreibgeschützt.
Konstanten, die mit equ definiert werden, unterscheiden sich von denen, die in section .rodata definiert werden.
Eine mit equ definierte Konstante belegt keinen Speicherplatz und wird vom Assembler direkt durch ihren Wert ersetzt.
Sie ist tatsächlich ein Platzhalter für diesen Wert.
Andererseits werden Konstanten, die in section .rodata definiert sind, tatsächlich im Speicher abgelegt und haben eine Adresse.
Deklarierte Daten müssen einen Namen haben, der ihnen zugeordnet ist. Dieser Name wird als Label bezeichnet.
Ein Label ist ein Symbol, das die spezifische Adresse der Daten im Speicher kodiert. Adressen in x86-64 sind 64-Bit-Werte.
In NASM liefert der direkte Zugriff auf Daten über ihr Label nicht den belegten Speicher, sondern ihre Adresse:
section .data
example dq 27 ; this declares a 8-byte variable initialized with 27
section .text
fn:
mov rax, example ; this stores the address of the declared variable in rax, not its contents
...
Um auf den Inhalt einer Speicheradresse zuzugreifen, muss man sie dereferenzieren. Dies wird als Indirektion bezeichnet.
In NASM geschieht dies mit []:
section .data
example dq -27 ; this declares a 8-byte variable initialized with -27
section .text
fn:
mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
...
Es gibt jedoch Situationen, in denen die Größe des dereferenzierten Speichers mehrdeutig sein kann. In diesen Fällen muss ein Präfix verwendet werden, das diese Größe angibt.
Dies sind die wichtigsten Präfixe und ihre Größen in einem typischen x86-64-Programm:
| Präfix | Größe |
|---|---|
| byte | 1 Byte |
| word | 2 Bytes |
| dword | 4 Bytes |
| qword | 8 Bytes |
Derselbe Ladevorgang kann mit explizit angegebener Größe geschrieben werden:
mov rax, qword [example] ; same dereference, size stated explicitly
Es ist gute Praxis, beim Dereferenzieren von Speicher immer ein Präfix zu verwenden.
Das Schreiben in den Speicher erfolgt auf die gleiche Weise, indem eine Adresse dereferenziert wird:
section .data
example1 db 10 ; example1 is a 1-byte memory location initialized with value 10
example2 dq -456 ; example2 is a 8-byte memory location initialized with value -456
example3 dd 54 ; example3 is a 4-byte memory location initialized with value 54
section .text
fn:
mov byte [example1], 20 ; example1 now has value 20
mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
mov dword [example3], eax ; example3 now has value equal to the contents in eax
Beachte, dass du in den meisten Anweisungen Speicheroperanden verwenden kannst, ohne den Inhalt vorher in ein Register zu laden. Es ist jedoch normalerweise nicht möglich, sie sowohl im Quell- als auch im Zieloperanden zu verwenden, sondern nur in einem der beiden:
section .data
example4 dw 4
example5 dq -8
example6 dd 15
section .text
fn:
add word [example4], 5 ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
imul rax, qword [example5] ; rax = rax * (-8)
; this is not possible -> sub dword [example6], dword [example6]
Obwohl ein mov verwendet werden kann, um die Adresse einer Variablen in einem Register zu speichern, gibt es eine Anweisung für genau diesen Zweck: lea.
Diese Anweisung verwendet einen Operanden in Speicherform, liest aber nicht den Speicher. Stattdessen berechnet sie den Ausdruck der effektiven Adresse und schreibt das Ergebnis in den Zieloperanden:
lea rax, [example] ; this stores the address of 'example' in rax
Es ist idiomatischer, lea zu verwenden, um Speicheradressen zu berechnen und in Registern zu speichern.
Beim Zugriff auf Speicherstellen ist das Standardverhalten in NASM, absolute Adressen zu erzeugen, d. h. feste Speicheradressen.
Aus Sicherheitsgründen werden ausführbare Dateien oft als PIE (positionsunabhängige ausführbare Datei) erstellt, wobei die Speicherbereiche an zufälligen Positionen platziert werden.
In einem PIE ist die endgültige Adresse einer Variablen zur Linkzeit nicht bekannt.
Stattdessen berechnet der Code Adressen als Offset vom Wert in einem speziellen Register namens rip, das auf die nächste auszuführende Anweisung zeigt.
Dies wird üblicherweise als RIP-relative Adressierung bezeichnet.
In NASM kannst du RIP-relativen Zugriff mit dem Operator rel anfordern:
mov rax, qword [rel variable]
Relative Adressierung kann auch mit default rel am Anfang zur Standardeinstellung für eine Quelldatei gemacht werden.
Alle Übungen in diesem Track werden als PIE kompiliert und gelinkt, daher sollte rel verwendet werden, um relative Adressen zu erzeugen.
Labels (Funktionen und Daten), die in einem beliebigen Abschnitt (z. B. .text, .data, .rodata) definiert sind, sind innerhalb derselben Quelldatei sichtbar.
Wenn sie als global deklariert sind, sind sie auch für andere Quelldateien sichtbar.
Umgekehrt sind Labels, die in anderen Quelldateien definiert sind, für die aktuelle Quelldatei sichtbar, wenn sie als extern deklariert sind.
In diesem Fall gibt es in der Assembly keinen Hinweis auf die Datengröße; diese muss im Voraus bekannt sein.
default rel
section .data
global number1 ; 'number1' is a variable visible to other source files
number1 db 200
extern number2 ; 'number2' is a variable visible to the current source file, but defined in another
section .text
extern sum ; sum is a function visible to the current source file, but defined in another
fn:
mov dil, byte [number1]
mov sil, byte [number2]
call sum
...
Dein Freund José ist Lehrer an einer lokalen Schule. Er hatte eine Idee für ein paar lustige Experimente, um zu zeigen, wie Farben kombiniert werden können, um andere zu erzeugen.
Er hat dich um Hilfe bei diesen Experimenten gebeten.
Eine Farbe wird in dieser Übung durch eine 32-Bit-Zahl (4 Byte) dargestellt, die ihren RGB-Wert kodiert.
Ein RGB-Wert besteht aus 3 Kanälen, Rot, Grün und Blau, von denen jeder 8 Bit (1 Byte) belegt.
Das vierte Byte ist normalerweise für den Alpha-Kanal reserviert, aber in dieser Übung ist sein Wert leer (0).
Die Werte für jede Farbe sind bereits in einer Tabelle gespeichert, die in einer anderen Quelldatei definiert ist. Eine Farbe wird in dieser Tabelle durch eine eindeutige Adresse identifiziert.
Definiere eine Funktion get_color_value, die den 32-Bit-Wert für eine Farbe zurückgibt.
Diese Funktion nimmt als Parameter eine gültige Adresse für diese Farbe in der Farbtabelle.
get_color_value(black)
// => 0
Hinweis - 32 Bit entsprechen 4 Byte.
Um verschiedene Farben zu mischen, wird José zuerst eine Basis festlegen und nur die Sekundärfarbe ändern, die damit kombiniert wird.
Definiere eine Funktion add_base_color, die den 32-Bit-Wert für eine Farbe in der Variable base_color speichert, damit er später verwendet werden kann.
Diese Funktion hat keinen Rückgabewert und nimmt als Parameter die Adresse der Farbe in der Farbtabelle.
Die Variable base_color definierst du selbst, und sie muss aus anderen Quelldateien zugänglich sein.
Es wird nie mehr als eine Basisfarbe gleichzeitig geben. Wenn eine neue Basisfarbe hinzugefügt wird, wird die alte verworfen.
Standardmäßig sollte base_color zu Beginn des Programms mit dem 32-Bit-Wert für weiß initialisiert werden, der 0xFFFFFF00 ist.
Hinweis - NASM akzeptiert hexadezimal definierte Zahlen mit 0x am Anfang, wie etwa in 0xFFFFFF00.
José erwartet, viele Kombinationen mit Primärfarben zu erstellen, also möchte er sie für einen schnellen Zugriff getrennt haben.
Da er RGB verwendet, um Farben darzustellen, sind die Primärfarben:
RED mit dem Wert 0xFF000000.GREEN mit dem Wert 0x00FF0000.BLUE mit dem Wert 0x0000FF00.Definiere eine Konstante für jede dieser Farben. Diese Konstanten müssen aus anderen Quelldateien zugänglich sein.
Die Farben sollten gemäß einer combining_function kombiniert werden, die in einer anderen Quelldatei definiert ist.
Diese Funktion nimmt als Parameter die 32-Bit-Werte für base_color und für eine Sekundärfarbe, die damit gemischt werden soll.
Sie gibt den 32-Bit-Wert für die kombinierte Farbe zurück.
Definiere eine Funktion make_color_combination, die zwei Farben kombiniert und das Ergebnis im Speicher ablegt.
Diese Funktion hat keinen Rückgabewert und nimmt als Parameter, in dieser Reihenfolge:
Beachte, dass combining_function die Werte in den Registern ändern kann, die du gerade verwendest.
Stelle sicher, dass du jede Variable, die du brauchst, im Speicher sicherst, bevor du die Funktion aufrufst.
Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.