image: nasm-logo.png

Netwide Assemb­ler (1996)

Simon Tatham

Assemb­ler ist eine ma­schi­nen­na­he Pro­gram­mier­spra­che, die direkt auf der Ma­schi­nen­ebene ar­bei­tet und es er­mög­licht, Pro­gramme zu schreiben, die von der CPU direkt ausgeführt werden.

Assemb­ler wird häufig für die Ent­wick­lung von Be­triebs­sys­te­men, Gerätetreibern und ein­ge­bet­te­ten Systemen ver­wen­det, da es eine hohe Ef­fi­zienz und direkte Kontrolle über die Hardware bietet. Bekannte Beispiele für die Verwendung von Assemb­ler sind der Linux-Kernel, der Teile seines Codes in Assemb­ler enthält, und die Firmware von Mikrocontrollern in ein­ge­bet­te­ten Systemen. Auch viele frühe Com­puterspiele wie »Prince of Persia« und »RollerCoaster Tycoon« wur­den in Assemb­ler geschrieben. Heutzutage ist Assemb­ler immer noch relevant, insbesondere in Be­rei­chen, die eine hohe Leistung und präzise Hard­ware­steue­rung erfordern, wie in der Sys­tem­pro­gram­mie­rung und bei der Ent­wick­lung von Echtzeitsystemen.

NASM (Netwide Assemb­ler) ist ein freier Assemb­ler, der auf vielen Plattformen verfügbar ist und die x86-Architektur un­ter­stützt. Er wur­de 1996 von Simon Tatham ent­wick­elt und ist eine der be­lieb­testen Assemb­ler-Tools in der Open-Source-Community.

1. Hello, world!

Assemb­ler-Pro­gramme werden in Textdateien mit der Endung .asm oder .s geschrieben. Diese Da­tei­en werden anschließend von einem Assemb­ler in ausführbare Da­tei­en übersetzt, die auf deinem Com­puter direkt ausgeführt werden können. Es gibt eine Vielzahl von Assemb­lern, die du verwenden kannst, aber wir werden hier den »Netwide Assemb­ler« (NASM) verwenden, der auf vielen Plattformen verfügbar ist.

Stelle zuerst sicher, dass du keinen Ordner geöffnet hast. Um sicherzugehen, drücke einfach den Shortcut für »Ordner schließen«: StrgK und dann F. Dein Work­space sollte jetzt ungefähr so aussehen:

Quelltext schreiben

Klicke auf »New File« und wähle als Dateityp »Text File«.

Schreibe nun den folgenden Code in die Datei:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
section .data
    hello db 'Hello, World!', 10, 0

section .text
    global _start

_start:
    ; write the string to stdout
    mov rax, 1
    mov rdi, 1
    mov rsi, hello
    mov rdx, 14
    syscall

    ; exit the program
    mov rax, 60
    xor rdi, rdi
    syscall

Warum einfach, wenn's auch kompliziert geht?

Wie du sehen kannst, ist der Assemb­ler-Code für ein einfaches »Hello, world!«-Programm im Vergleich zu anderen Programmierspracen relativ lang. Das liegt daran, dass Assemb­ler eine sehr ma­schi­nen­na­he Spra­che ist und du viele Details explizit angeben musst.

In den Zeilen 9 bis 13 wird ein Systemaufruf (syscall) ver­wen­det, um die Funktion write aufzurufen und den Text »Hello, world!« auf der Standardausgabe auszugeben. Die Register rax, rdi, rsi und rdx enthalten die Argumente für den Systemaufruf:

  • rax enthält die Nummer des Systemaufrufs (1 für write)
  • rdi enthält den Dateideskriptor (1 für die Standardausgabe)
  • rsi enthält die Adresse des Texts
  • rdx enthält die Länge des Texts

Eine Übersicht über alle Linux-Systemaufrufe für die x86_64-Architektur findest du hier: Linux x86_64 System Call Reference Table.

Die Frage, warum man in Assemb­ler programmieren sollte, wenn es doch so viel einfacher geht, ist berechtigt. Assemb­ler ist eine sehr mächtige Pro­gram­mier­spra­che, die es dir erlaubt, die Hardware deines Com­puters direkt zu kontrollieren. Das kann in bestimmten Situationen sehr nützlich sein, z. B. wenn du extrem schnelle oder res­sour­cen­scho­nen­de Pro­gramme schreiben möchtest. Außerdem kann es sehr lehrreich sein, Assemb­ler zu lernen, da du ein tiefes Verständnis dafür be­kommst, wie Com­puter auf der Ebene der CPU funktionieren. Unabhängig davon spielt Assemb­ler auch in der Soft­ware­ent­wick­lung eine wichtige Rolle, da die Quelltexte kompilierter Pro­gram­mier­spra­chen (z. B. C und C++) im ersten Schritt in Assemb­ler-Code übersetzt werden.

Syntax-Highlighting aktivieren

Da Visual Studio Code noch nicht weiß, dass es sich um Assemb­ler-Quelltext handelt, ist dein Programm momentan noch einfarbig, aber das wird sich gleich ändern. An dem weißen Punkt erkennst du, dass deine Änderungen noch nicht gespeichert sind.

Drücke nun StrgS, um die Datei zu speichern. Gib hello.asm ein – der vollständige Pfad zu deiner Datei lautet dann /workspace/hello.asm.

Da Assemb­ler standardmäßig nicht von Visual Studio Code un­ter­stützt wird, müssen wir noch eine passende Er­wei­te­rung installieren. Klicke dazu auf das Er­wei­te­rungs-Symbol in der Seitenleiste oder drücke StrgShiftX. Suche nach der Er­wei­te­rung »The Netwide Assemb­ler« und installiere sie.

Alternativ kannst du auch StrgP drücken und ext install rights.nas-vscode eingeben, um die Er­wei­te­rung zu installieren.

Anschließend solltest du dein Assemb­ler-Programm farbig sehen:

Kompilieren und ausführen

Bevor wir das Programm ausführen können, müssen wir es kompilieren und linken. Dazu verwenden wir den Netwide Assemb­ler nasm, um den Assemb­ler-Code in eine Objektdatei zu übersetzen, und den GNU Linker ld, um die Objektdatei anschließend in eine ausführbare Datei zu linken.

Öffne dazu ein Ter­mi­nal, indem du entweder StrgJ drückst oder das Panel-Symbol rechts oben drückst. Dein Fenster sollte jetzt ungefähr so aussehen:

Um das Programm zu kompilieren, gib folgenden Befehl ein:

nasm -f elf64 hello.asm
Du musst nicht den vollständigen Da­tei­na­men schreiben. Schreib einfach nasm -f elf64 he und drücke Tab, um den Da­tei­na­men automatisch zu hello.asm vervollständigen zu lassen. Du kannst danach ganz normal weiterschreiben.

Wenn du keinen Fehler gemacht hast, wird das Programm erfolgreich kompiliert und die Objektdatei hello.o wird im selben Verzeichnis erstellt. Du kannst dies überprüfen, indem du dir die Da­tei­en im aktuellen Verzeichnis mit ls oder ls -l anzeigen lässt:

Um eine ausführbare Datei zu erstellen, müssen wir die Objektdatei mit dem GNU Linker ld linken. Gib dazu folgenden Befehl ein:

ld hello.o -o hello

Nun sollte eine ausführbare Datei mit dem Namen hello im Verzeichnis liegen. Du kannst dies überprüfen, indem du dir die Da­tei­en im aktuellen Verzeichnis mit ls oder ls -l anzeigen lässt:

Die grüne Datei hello ist die ausführbare Datei – im Unterschied zu Win­dows, wo ausführbare Da­tei­en die Endung .exe haben, haben ausführbare Da­tei­en unter Linux keine Endung. Um das Programm auszuführen, gib folgenden Befehl ein:

./hello

Das Programm sollte die Nachricht Hello, World! im Ter­mi­nal ausgeben. Du kannst alle drei Schritte (kompilien, linken, ausführen) auch in einem Befehl kombinieren:

nasm -f elf64 hello.asm && ld hello.o -o hello && ./hello
Die Zeichenkombination && sorgt dafür, dass der nächste Befehl nur ausgeführt wird, wenn der vorherige Befehl erfolgreich war.

Fehler finden und beheben

Wenn du einen Fehler im Code machst, wird der Compiler eine Fehlermeldung ausgeben. Versuche zum Beispiel, in Zeile 9 statt rax das Wort ray zu schreiben:

    mov ray, 1

Speichere die Datei und führe den Assemb­ler erneut aus:

nasm -f elf64 hello.asm
Nutze die Pfeiltaste hoch , um einen vorherigen Befehl erneut einzugeben. So kannst du schnell dein Programm testen, nachdem du es verändert hast.

Der Assemb­ler sollte eine Fehlermeldung ausgeben, die dir hilft, den Fehler zu finden:

Es lohnt sich, die Fehlermeldungen genau zu lesen, um den Fehler zu finden und zu beheben. Achte auf die Zeilennummer (in diesem Beispiel 9) und den Text, der dir sagt, was falsch ist. Denke daran, den Fehler wieder zu beheben, bevor du das nächste Beispiel ausprobierst.

2. Primfaktorzerlegung

Im zweiten Beispiel wollen wir eine Zahl in ihre Primfaktoren zerlegen. An diesem Beispiel kannst du sehen, wie man in Assemb­ler Be­nut­zer­ein­ga­ben verar­bei­tet, Schleifen ver­wen­det sowie Strings in Zahlen umwandelt und anders herum. Erstelle eine neue Datei mit StrgAltN und schreibe den folgenden Code hinein:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
section .data
    prompt db "Enter a number: ", 0
    answer db "Prime factors: ", 0
    result db "Doubled number: %d", 0
    newline db 0x0a
    space db 0x20

section .bss
    input resb 16

section .text
    global _start

_start:
    ; Prompt the user for input
    mov rax, 1
    mov rdi, 1
    mov rsi, prompt
    mov rdx, 16
    syscall

    ; Read user input
    mov rax, 0
    mov rdi, 0
    mov rsi, input
    mov rdx, 10
    syscall

    ; Write answer
    mov rax, 1
    mov rdi, 1
    mov rsi, answer
    mov rdx, 15
    syscall

    ; Convert input to integer
    mov rdi, input
    call atoi

    mov rdi, 2          ; Initialize divisor to 2
    mov rsi, rax        ; Copy value from rax to rsi

find_factors:
    mov rdx, 0          ; Clear rdx register
    mov rax, rsi        ; Move value in rsi to rax

    div rdi             ; Divide rsi by rdi
    cmp rdx, 0          ; Check if remainder is zero
    jne failed          ; If remainder is not zero, go to next factor

    mov rsi, rax

    push rax
    push rdi
    push rsi
    push rdx

    mov rax, rdi        ; Move divisor to rax

    ; Convert the result to string
    mov byte [input+15], 0
    mov rdi, input+15
    call itoa

    ; Print the result
    mov rax, 1
    mov rsi, rdi
    mov rdi, 1
    mov rdx, input
    add rdx, 15
    sub rdx, rsi
    syscall

    ; Print a space
    mov rax, 1
    mov rdi, 1
    mov rsi, space
    mov rdx, 1
    syscall

    pop rdx
    pop rsi
    pop rdi
    pop rax

    mov rsi, rax
    jmp next_factor
failed:
    mov rax, rsi
    inc rdi             ; Increment divisor
    cmp rdi, rsi        ; Compare divisor with value in rsi
    jle find_factors    ; If divisor is less than or equal to value, continue finding factors

next_factor:
    cmp rdi, rsi        ; Compare divisor with value in rsi
    jle find_factors

    ; Print a newline
    mov rax, 1
    mov rdi, 1
    mov rsi, newline
    mov rdx, 1
    syscall

    ; Exit the program
    mov rax, 60
    xor rdi, rdi
    syscall

atoi:
    xor rax, rax
    mov rcx, 10

atoi_loop:
    xor rbx, rbx
    mov bl, byte [rdi]
    inc rdi
    cmp bl, 0
    je atoi_done
    cmp bl, 10
    je atoi_done

    sub bl, '0'
    mul rcx
    add rax, rbx

    jmp atoi_loop

atoi_done:
    ret

; itoa implementation
itoa:
    xor rcx, rcx
    mov rsi, 10

itoa_loop:
    xor rdx, rdx
    div rsi
    add dl, '0'
    dec rdi
    mov byte [rdi], dl
    test rax, rax
    jnz itoa_loop

itoa_done:
    ret

Speichere die Datei unter dem Namen factor.asm. Kompiliere und linke das Programm:

nasm -f elf64 factor.asm && ld factor.o -o factor

Falls du keine Fehlermeldung erhältst, kannst du das Programm ausführen und testen:

Das Programm hat die Zahl 123 in ihre Primfaktoren zerlegt und ausgegeben. Probiere aus, was passiert, wenn du die Zahl 3000000000 eingibst oder die Zahl 123456789123456789. Was könnte der Grund dafür sein?

3. Zusammenfassung

Wie du siehst, ist Assemb­ler eine sehr komplexe Pro­gram­mier­spra­che. Daher verzichten wir vorerst auf das Bubblesort-Beispiel. Falls du eine funktionierende NASM-Implentierung des Bubblesort-Algorihmus (für x86_64) hast, schick sie bitte an specht@gymnasiumsteglitz.de, damit wir sie hier einfügen können.

In diesem Kapitel hast du an zwei Beispielen gesehen, wie man ein einfaches Assemb­ler-Programm schreiben, kompilieren, linken und ausführen kann. Das ist natürlich nur ein erster Eindruck. Um Assemb­ler wirklich zu beherrschen, musst du noch viel mehr lernen – am besten, indem du eigene Pro­gramme schreibst und ausprobierst. Die Buchhandlungen, Bib­lio­theken und Youtube sind voll von Material für dich. Viel Spaß beim Pro­gram­mier­en!