10 hours ago

"Hello World" in Assembler



Mit einem Beispiel führt dieser Artikel in die Anfänge der Programmierung zurück. Viele von euch haben noch nie Maschinen-Code oder Assembler gesehen. Hier seht ihr es.

Möchte diesen Artikel wirklich jemand lesen? Ist er nützlich oder überflüssig? Ich weiss es nicht. Ich schreibe ihn trotzdem, weil er in mir nostalgische Gefühle weckt. Irgendwann in den 80er-Jahren habe ich bei der Volkshochschule Sankt Augustin einen Programmierkurs für Assembler belegt. Dort durften wir eine Ampelschaltung programmieren. Das war mein Einstieg in die Erstellung von Software. Halt, das stimmt nicht; vorher hatte ich auf einem TI-58 ein Hi-Low-Zahlenratespiel implementiert.

Seitdem hat sich in der Programmierwelt viel getan. Es wurden Layer um Layer auf die Programmiersprachen gelegt, um die Hardwareabhängigkeit und die Lesbarkeit und Effizienz bei der Programmierung zu verbessern. Heute kommt niemand mehr auf die Idee, in Assembler zu programmieren. Selbst bei zeitkritischen Anwendungen ist ein C-Code schnell genug.

Dabei ist Assembler gar nicht die unterste Schicht der Programmierung, sondern der Maschinen-Code. Egal, welche Anwendung ihr ausführt, sei es LibreOffice oder ein einfaches "Hello-World"-Beispiel; auf eurer CPU wird schlussendlich immer Maschinen-Code ausgeführt. Die CPU versteht weder Python noch Rust; sie versteht nur Nullen und Einsen.

1948 - ENIAC - Maschinen-Code

Vermutlich gab es in den Anfängen der Computerei ein paar Nerds in weissen Kitteln, die tatsächlich Maschinen-Code geschrieben haben.

Maschinen-Code

Ich beginne ganz unten. Damit ich euch zeigen kann, wie Maschinen-Code aussieht, verwende ich ein kompiliertes Assembler-Programm, welches wir im nächsten Kapitel erstellen werden. Es ist eine ausführbare Binärdatei, die den Namen gnulinux trägt. Das Ding gibt den Text "Hallo GNU/Linux.ch" aus. Um den Maschinen-Code anzuschauen, verwende ich einen Disassembler. Der Befehl lautet: objdump -d gnulinux

401000: 48 c7 c0 01 00 00 00 mov $0x1,%rax 401007: 48 c7 c7 01 00 00 00 mov $0x1,%rdi 40100e: 48 c7 c6 00 20 40 00 mov $0x402000,%rsi 401015: 48 c7 c2 13 00 00 00 mov $0x13,%rdx 40101c: 0f 05 syscall 40101e: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax 401025: 48 c7 c7 00 00 00 00 mov $0x0,%rdi 40102c: 0f 05 syscall

Ganz links seht ihr Speicheradressen, in der Mitte steht der Maschinen-Code in hexadezimaler Darstellung und rechts findet ihr den Assembler-Code in der AT&T-Schreibweise. Was ist objdump?

Der Befehl „objdump“ in Linux ist ein Dienstprogramm, mit dem Sie Informationen über Objektdateien anzeigen können. Es wird häufig für Debugging- und Reverse-Engineering-Zwecke verwendet und bietet Einblicke in die Struktur und den Inhalt kompilierter Dateien. "objdump“ kann mit einer Vielzahl von ausführbaren Formaten umgehen, einschließlich ELF-Dateien (Executable and Linkable Format), und bietet Optionen zum Disassemblieren von Code, zur Untersuchung von Headern und mehr.

Ihr müsst das nicht installieren, weil es auf jeder Linux-Distribution vorhanden ist. Das gilt auch für die weiteren Befehle, die in diesem Artikel vorkommen.

Assembler

Assembler ist eine Low-Level-Programmiersprache, die direkt mit der Hardware kommuniziert. Assemblersprachen sind für Menschen (halbwegs) lesbare Versionen von Maschinen-Code. Wir verwenden Assembler, um Assembler-Code in Maschinen-Code umzuwandeln.

Jede Prozessorfamilie hat ihre eigene Assemblersprache mit unterschiedlichen Befehlssätzen. Die x86-Assemblersprache ist zum Beispiel die Assemblersprache für Intel-Prozessoren. Neben der Prozessorarchitektur kann sich auch das Format der ausführbaren Datei zwischen den verschiedenen Betriebssystemen unterscheiden. Daher kann es mehrere Assembler für dieselbe Architektur geben.

Es gibt zwei gängige Assembler-Syntaxen: AT&T und Intel. Die vorherrschende Syntax im Linux-Bereich ist natürlich die AT&T-Syntax, da Unix in den AT&T Bell Labs entwickelt wurde. So verwendet beispielsweise GCC, der Standardcompiler von Linux, standardmäßig die AT&T-Syntax. Die vorherrschende Syntax in der Windows-Domäne ist jedoch die Intel-Syntax. In diesem Artikel verwende ich die Intel-Syntax, weil sie einfacher zu lesen ist.

Beispiel gefällig?

AT&T-Syntax : mov $1, %rax
Intel-Syntax: mov rax, 1

Beide Befehle schieben den Wert 1 in das CPU-Register RAX.

Hier ist der x86 Assembler-Quellcode, um den Text "Hallo GNU/Linux.ch" auszugeben:

.global _start .section .data message: .ascii "Hallo GNU/Linux.ch\n" .section .text _start: mov rax, 1 mov rdi, 1 mov rsi, offset message mov rdx, 19 syscall mov rax, 60 mov rdi, 0 syscall

Was passiert hier?

.global _start legt den Einstiegspunkt ins Programm fest; so ähnlich wie die main() Funktion in C-Programmen.

In .section .data können Variablen belegt werden. Im Beispiel ist es der ASCII-Text "Hallo GNU/Linux.ch" mit einem Zeilenumbruch am Ende.

.section .text kündigt den Beginn der eigentlichen Verarbeitung an. Mit _start: geht es dann los. Dann kommen ein paar mov-Befehle, mit denen Werte in CPU-Register geschrieben werden. Ich erkläre hier nicht die Bedeutung der einzelnen Register; falls euch das interessiert, könnt ihr das hier lesen. Nur so viel, im Register rdx muss die Länge des Strings stehen, wobei der Zeilenumbruch \n am Ende als ein Zeichen zählt.

Mit syscall wird der Systemaufruf sys_write() ausgelöst, der die Werte tatsächlich in die CPU-Register schreibt.

Die 60 in rax beendet das Programm (sys_exit) und die 0 in rdi ist der positive Rückgabewert für sys_exit.

Kompilieren

Bisher haben wir nur ein Stück Quellcode in Assembler geschrieben. Um daraus ein ausführbares Programm zu machen, sind zwei weitere Schritte nötig: build und link. Zum Bauen kommt dieser Befehl zum Einsatz:

as -msyntax=intel -mnaked-reg gnulinux.asm -o gnulinux.o

as ist der GNU Assembler Compiler. Die beiden Parameter dienen dazu, dem Compiler klarzumachen, dass wir die Intel-Syntax verwenden. gnulinux.asm ist der Quellcode und gnulinux.o ist der Object-Code, eine ELF-Datei (siehe oben). Um daraus eine ausführbare Datei zu machen, rufen wir den Linker ld auf:

ld -s -o gnulinux gnulinux.o

Die beiden Parameter -s und -o erkläre ich nicht. gnulinux ist das Executable und gnulinux.o ist die Input-Datei für den Linker. Danach gibt es das ausführbare Programm gnulinux, welches man mit ./gnulinux starten kann. So sieht die Ausgabe aus:

Fazit

Laut dem Tiobe-Index ist Python erneut als beliebteste Programmiersprache ausgewählt worden. Der Quellcode für das "Hello World"-Beispiel ist in Python nur eine Zeile lang: print('Hello World'). Doch darum geht es mir in diesem Artikel nicht. Ich wollte euch auf den Urahnen der Programmierung hinweisen. Es ist 'erst' vierzig Jahre her, als Assembler noch als Programmiersprache - insbesondere für zeitkritische Anwendungen - verwendet wurde.

Titelbild: https://www.osa.fu-berlin.de/informatik_lehramt/_medien/bild_assembler/assembler_930.jpg

Quellen:

https://computerhistory.org/blog/programming-the-eniac-an-example-of-why-computer-history-is-hard/

https://www.baeldung.com/linux/assembly-compile-run


GNU/Linux.ch ist ein Community-Projekt. Bei uns kannst du nicht nur mitlesen, sondern auch selbst aktiv werden. Wir freuen uns, wenn du mit uns über die Artikel in unseren Chat-Gruppen oder im Fediverse diskutierst. Auch du selbst kannst Autor werden. Reiche uns deinen Artikelvorschlag über das Formular auf unserer Webseite ein.
Gesamten Artikel lesen

© Varient 2025. All rights are reserved