Simon Tatham
Assembler ist eine maschinennahe Programmiersprache, die direkt auf der Maschinenebene arbeitet und es ermöglicht, Programme zu schreiben, die von der CPU direkt ausgeführt werden.
Assembler wird häufig für die Entwicklung von Betriebssystemen, Gerätetreibern und eingebetteten Systemen verwendet, da es eine hohe Effizienz und direkte Kontrolle über die Hardware bietet. Bekannte Beispiele für die Verwendung von Assembler sind der Linux-Kernel, der Teile seines Codes in Assembler enthält, und die Firmware von Mikrocontrollern in eingebetteten Systemen. Auch viele frühe Computerspiele wie »Prince of Persia« und »RollerCoaster Tycoon« wurden in Assembler geschrieben. Heutzutage ist Assembler immer noch relevant, insbesondere in Bereichen, die eine hohe Leistung und präzise Hardwaresteuerung erfordern, wie in der Systemprogrammierung und bei der Entwicklung von Echtzeitsystemen.
NASM (Netwide Assembler) ist ein freier Assembler, der auf vielen Plattformen verfügbar ist und die x86-Architektur unterstützt. Er wurde 1996 von Simon Tatham entwickelt und ist eine der beliebtesten Assembler-Tools in der Open-Source-Community.
Assembler-Programme werden in Textdateien mit der Endung .asm
oder .s
geschrieben. Diese Dateien werden anschließend von einem Assembler in ausführbare Dateien übersetzt, die auf deinem Computer direkt ausgeführt werden können. Es gibt eine Vielzahl von Assemblern, die du verwenden kannst, aber wir werden hier den »Netwide Assembler« (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 Workspace sollte jetzt ungefähr so aussehen:
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 |
Wie du sehen kannst, ist der Assembler-Code für ein einfaches »Hello, world!«-Programm im Vergleich zu anderen Programmierspracen relativ lang. Das liegt daran, dass Assembler eine sehr maschinennahe Sprache ist und du viele Details explizit angeben musst.
In den Zeilen 9 bis 13 wird ein Systemaufruf (syscall
) verwendet, 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 Textsrdx
enthält die Länge des TextsEine Übersicht über alle Linux-Systemaufrufe für die x86_64-Architektur findest du hier: Linux x86_64 System Call Reference Table.
Da Visual Studio Code noch nicht weiß, dass es sich um Assembler-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 Assembler standardmäßig nicht von Visual Studio Code unterstützt wird, müssen wir noch eine passende Erweiterung installieren. Klicke dazu auf das Erweiterungs-Symbol in der Seitenleiste oder drücke StrgShiftX. Suche nach der Erweiterung »The Netwide Assembler« und installiere sie.
Alternativ kannst du auch StrgP drücken und ext install rights.nas-vscode
eingeben, um die Erweiterung zu installieren.
Anschließend solltest du dein Assembler-Programm farbig sehen:
Bevor wir das Programm ausführen können, müssen wir es kompilieren und linken. Dazu verwenden wir den Netwide Assembler nasm
, um den Assembler-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 Terminal, 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
nasm -f elf64 he
und drücke Tab, um den Dateinamen 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 Dateien 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 Dateien im aktuellen Verzeichnis mit ls
oder ls -l
anzeigen lässt:
Die grüne Datei hello
ist die ausführbare Datei – im Unterschied zu Windows, wo ausführbare Dateien die Endung .exe
haben, haben ausführbare Dateien unter Linux keine Endung. Um das Programm auszuführen, gib folgenden Befehl ein:
./hello
Das Programm sollte die Nachricht Hello, World!
im Terminal 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
&&
sorgt dafür, dass der nächste Befehl nur ausgeführt wird, wenn der vorherige Befehl erfolgreich war.
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 Assembler erneut aus:
nasm -f elf64 hello.asm
Der Assembler 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.
Im zweiten Beispiel wollen wir eine Zahl in ihre Primfaktoren zerlegen. An diesem Beispiel kannst du sehen, wie man in Assembler Benutzereingaben verarbeitet, Schleifen verwendet 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?
Wie du siehst, ist Assembler eine sehr komplexe Programmiersprache. 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 Assembler-Programm schreiben, kompilieren, linken und ausführen kann. Das ist natürlich nur ein erster Eindruck. Um Assembler wirklich zu beherrschen, musst du noch viel mehr lernen – am besten, indem du eigene Programme schreibst und ausprobierst. Die Buchhandlungen, Bibliotheken und Youtube sind voll von Material für dich. Viel Spaß beim Programmieren!