Assembler-Programmierung für x86-Prozessoren/ Das erste Assemblerprogramm
Die Befehle MOV und XCHG
BearbeitenDer mov
-Befehl (move) ist wohl einer der am häufigsten verwendeten Befehle im Assembler. Er hat die folgende Syntax:
mov op1, op2
Mit dem mov
-Befehl wird der zweite Operand in den ersten Operanden kopiert. Der erste Operand wird auch als Zieloperand, der zweite als Quelloperand bezeichnet. Beide Operanden müssen die gleiche Größe haben. Wenn der erste Operand beispielsweise die Größe von zwei Byte besitzt, muss auch der zweite Operand die Größe von zwei Byte besitzen.
Es ist nicht erlaubt, als Operanden das IP-Register zu benutzen. Wir werden später mit Sprungbefehlen noch eine indirekte Möglichkeit kennen lernen, dieses Register zu manipulieren.
Außerdem ist es nicht erlaubt, eine Speicherstelle in eine andere Speicherstelle zu kopieren. Diese Regel gilt für alle Assemblerbefehle mit zwei Operanden: Es dürfen niemals beide Operanden eine Speicherstelle ansprechen. Beim mov
-Befehl hat dies zur Folge, dass, wenn eine Speicherstelle in eine zweite Speicherstelle kopiert werden soll, dies über ein Register erfolgen muss. Beispielsweise wird mit den zwei folgenden Befehlen der Inhalt von der Speicherstelle 0110h in die Speicherstelle 0112h kopiert:
mov ax, [0110]
mov [0112], ax
Der xchg
-Befehl (exchange) hat die gleiche Syntax wie der mov
-Befehl:
xchg op1, op2
Wie sein Name bereits andeutet, vertauscht er den ersten und den zweiten Operanden. Die Operanden können allgemeine Register oder ein Register und eine Speicherstelle sein.
Das erste Programm
BearbeitenSchritt 1: Installieren des Assemblers
Laden Sie zunächst den Netwide-Assembler unter der folgenden Adresse herunter:
http://sourceforge.net/project/showfiles.php?group_id=6208
Dort sehen Sie eine Liste mit DOS 16-Bit-Binaries. Da wir unsere ersten Schritte unter DOS machen werden, laden Sie anschließend die Zip-Datei der aktuellsten Version herunter und entpacken diese.
macOS Anwender können eine aktuelle Version mittels Homebrew herunterladen.
Schritt 2: Assemblieren des ersten Programms
Anschließend kopieren Sie das folgende Programm in den Editor:
org 100h
start:
mov ax, 5522h
mov cx, 1234h
xchg cx,ax
mov al, 0
mov ah,4Ch
int 21h
Speichern Sie es unter der Bezeichnung „firstp.asm“. Anschließend starten Sie die Eingabeaufforderung "cmd.exe" und wechseln in das Verzeichnis, in das Sie den Assembler entpackt haben. Dort übersetzen Sie das Programm (wir nehmen an, dass der Quellcode im selben Verzeichnis steht wie der Assembler; ansonsten muss der Pfad natürlich mit angegeben werden):
nasm firstp.asm -f bin -o firstp.com
Mit der Option -f
kann festgelegt werden, dass die übersetze Datei eine *.COM
ist (dies ist die Voreinstellung des NASM). Bei diesem Dateityp besteht ein Programm aus nur einem Segment in dem sowohl Code, Daten wie auch der Stack liegt. Allerdings können COM-Programme auch nur maximal 64 KB groß werden. Eine komplette Liste aller möglichen Formate für -f
können Sie sich mit der Option -hf
ausgeben lassen. Über den Parameter -o
legen Sie fest, dass Sie den Namen der assemblierten Datei selbst festlegen wollen.
Sie können das Programm nun ausführen, indem Sie firstp
eingeben. Da nur einige Register hin und her geschoben werden, bekommen Sie allerdings nicht viel zu sehen.
Schritt 3: Analyse
Das Programm besteht sowohl aus Assembleranweisungen als auch Assemblerbefehlen. Nur die Assemblerbefehle werden übersetzt. Assembleranweisungen hingegen enthalten wichtige Informationen für den Assembler, die er beim Übersetzen des Programms benötigt.
Die Assembleranweisung ORG (origin) sagt dem Assembler, an welcher Stelle das Programm beginnt. Wir benötigen diese Assembleranweisung, weil Programme mit der Endung .COM immer mit Adresse 100h beginnen. Dies ist wichtig, wenn der Assembler eine Adresse beispielsweise für Sprungbefehle berechnen muss. In unserem sehr einfachen Beispiel muss der Assembler keine Adresse berechnen, weshalb wir die Anweisung in diesem Fall auch hätten weglassen können. Mit start
wird schließlich der Beginn des Programmcodes festgelegt.
Für die Analyse des Programms benutzen wir den Debugger, der bereits seit MS-DOS mitgeliefert wurde. Er ist zwar nur sehr einfach und textorientiert, hat jedoch den Vorteil, dass er nicht erst installiert werden muss und kostenlos ist.
Starten Sie den Debugger mit debug firstp.com
. Wenn Sie den Dateinamen beim Starten des Debuggers nicht mit angegeben haben, können Sie dies jederzeit über n firstp.com
(n steht für engl. name) und dem Befehl l
(load) nachholen.
Anschließend geben Sie den Befehl r
ein, um sich den Inhalt aller Register anzeigen zu lassen:
-r AX=0000 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0100 NV UP EI PL NZ NA PO NC 0CDC:0100 B82255 MOV AX,5522
In der ersten Zeile stehen die Datenregister sowie die Stackregister. Ihr Inhalt ist zufällig und kann sich von Aufruf zu Aufruf unterscheiden. Nur das BX-Register ist immer auf 0 gestellt und das CX-Register gibt die Größe des Programmcodes an. Sämtliche Angaben sind in hexadezimaler Schreibweise angegeben.
In der nächsten Zeile befinden sich die Segmentregister sowie der Befehlszähler. Die Register CS:IP zeigen auf den ersten Befehl in unserem Programm. Wie wir vorher gesehen haben können wir die tatsächliche Adresse über die Formel
Physikalische Adresse = Offsetadresse + Segmentadresse * 16dez
errechnen. Setzen wir die Adresse für das Offset und das Segment ein, so erhalten wir
Physikalische Adresse = 0x0CDC * 16dez + 0x0100 = 0xCDC0 + 0x0100 = 0xCEC0.
Dahinter befinden sich die Zustände der Flags. Vielleicht werden Sie sich wundern, dass ein Flag fehlt: Es ist das Trap Flag, das dafür sorgt, dass nach jeder Anweisung der Interrupt 1 ausgeführt wird, um es zu ermöglichen, dass das Programm Schritt für Schritt abgearbeitet wird. Es ist immer gesetzt, wenn ein Programm mit dem Debugger aufgeführt wird.
In der dritten und letzten Zeile sehen Sie zu Beginn nochmals die Adresse des Befehls, die immer mit dem Registerpaar CS:IP übereinstimmt. Dahinter befindet sich der Opcode des Befehls. Wir merken uns an dieser Stelle, dass der Opcode für den Befehl MOV AX,5522
3 Byte groß ist (zwei Hexadezimalziffern entsprechen immer einem Byte).
Mit der Anweisung t
(trace) führen wir nun den ersten Befehl aus und erhalten die folgende Ausgabe:
-t AX=5522 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0103 NV UP EI PL NZ NA PO NC 0CDC:0103 B93412 MOV CX,1234
Wir haben die Register hervorgehoben, die sich verändert haben. Der Befehl MOV
(move) hat dafür gesorgt, dass 5522
in das AX-Register kopiert wurde.
Der Prozessor hat außerdem den Inhalt des Programmzählers erhöht – und zwar um die Anzahl der Bytes, die der letzte Befehl im Arbeitsspeicher benötigte (also unsere 3 Bytes). Damit zeigt das Registerpaar CS:IP nun auf den nächsten Befehl.
Hat der Assembler nun wiederum den Befehl MOV CX,1234 abgearbeitet, erhöht er das IP-Register wiederum um die Größe des Opcodes und zeigt nun wiederum auf die Adresse des nächsten Befehls:
-t AX=5522 BX=0000 CX=1234 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0106 NV UP EI PL NZ NA PO NC 0CDC:0106 91 XCHG CX,AX
Der XCHG
-Befehl vertauscht nun die Register AX und CX, so dass jetzt AX den Inhalt von CX hat und CX den Inhalt von AX.
Wir haben nun genug gesehen und beenden deshalb das Programm an dieser Stelle. Dazu rufen wir über den Interrupt 21h die Betriebssystemfunktion 4Ch auf. Der Wert in AL (hier 0 für erfolgreiche Ausführung) wird dabei an das Betriebssystem zurückgegeben (er kann z. B. in Batch-Dateien über %ERRORLEVEL% abgefragt werden). Den Debugger können Sie nun beenden, indem Sie q
(quit) eingeben.
„Hello World“-Programm
BearbeitenEs ist ja inzwischen fast üblich geworden, ein „Hello World“-Programm in einer Einführung in eine Sprache zu entwickeln. Dem wollen wir natürlich folgen:
org 100h
start:
mov dx,hello_world
mov ah,09h
int 21h
mov al, 0
mov ah,4Ch
int 21h
section .data
hello_world: db 'hello, world', 13, 10, '$'
Nachdem Sie das Programm übersetzt und ausgeführt haben, starten Sie es mit dem Debugger:
-r AX=0000 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0100 NV UP EI PL NZ NA PO NC 0CDC:0100 BA0C01 MOV DX,010C - q
Wie Sie erkennen können, hat der Assembler hello_world
durch die Offsetadresse 010C ersetzt. Diese Adresse zusammen mit dem CS-Register entspricht der Stelle im Speicher, in der sich der Text befindet. Wir müssen diese der Betriebssystemfunktion 9h des Interrupts 21 übergeben, die dafür sorgt, dass der Text auf dem Bildschirm ausgegeben wird. Wie kommt der Assembler auf diese Adresse?
Die Anweisung „section .data“ bewirkt, dass hinter dem letzten Befehl des Programms ein Bereich für Daten reserviert wird. Am Beginn dieses Datenbereichs wird der Text 'hello, world', 13, 10, '$' bereitgestellt. Ab welcher Adresse der Datenbereich beginnt, lässt sich nicht genau vorhersagen. Wieso?
Wir können zunächst bestimmen, wo unser Programmcode beginnt und wie lang unser Programmcode ist.
COM-Programme beginnen immer erst bei der Adresse 100h (sonst kann sie das Betriebssystem nicht ausführen). Die Bytes von 0 bis FFh hat das Betriebssystem für Verwaltungszwecke reserviert. Dies muss natürlich dem Assembler mit der Anweisung org 100h
mitgeteilt werden, der sonst davon ausgehen würde, dass das Programm bei Adresse 0 beginnt. Es sei hier nochmals darauf hingewiesen, dass Assembleranweisungen nicht mit übersetzt werden, sondern lediglich dazu dienen, dass der Assembler das Programm richtig übersetzen kann.
Der Opcode von mov dx,010C
hat eine Größe von 3 Byte, der Opcode von mov ah,09
und mov ah,4Ch
jeweils 2 Byte und der Opcode der Interruptaufrufe nochmals jeweils 2 Byte. Der Programmcode hat somit eine Größe von 11 Byte dezimal oder B Byte hexadezimal. Woher ich das weiß? Entweder kompiliere ich das Programm und drucke das Listing aus, oder ich benutze das DEBUG-Programm und tippe mal eben schnell die paar Befehle ein. Wie auch immer:
Das letzte Byte des Programms hat die Adresse 10Ah. Das ist auf den ersten Blick verblüffend, aber die Zählung hat ja nicht mit 101h, sondern mit 100h begonnen. Das erste freie Byte hinter dem Programmcode hat also die Adresse 10Bh, ab dieser Adresse könnten Daten stehen. Tatsächlich beginnt der Datenbereich erst an 10Ch, weil die meisten Compiler den Speicher ab geradzahligen oder durch vier teilbaren Adressen zuweisen.
Warum ist das so? Prozessoren lesen (und schreiben) 32 Bit gleichzeitig aus dem Speicher. Wenn eine 32-Bit-Variable an einer durch vier teilbaren Speicheradresse beginnt, kann sie von der CPU mit einem Speicherzugriff gelesen werden. Beginnt die Variable an einer anderen Speicheradresse, sind zwei Speicherzyklen notwendig.
Nun hängt es vom verwendeten Assemblerprogramm und dessen Voreinstellungen ab, welcher Speicherplatz dem Datensegment zugewiesen wird. Ein „kluges“ Assemblerprogramm wird eine 16-Bit-Variable oder eine 32-Bit-Variable stets so anordnen, dass sie mit einem Speicherzugriff gelesen werden kann. Nötigenfalls bleiben einige Speicherplätze ungenutzt. Wenn der Assembler allerdings die Voreinstellung hat, kein einziges Byte zu vergeuden, obwohl dadurch das Programm langsamer wird, tut er auch das. Die meisten Assembler werden den Datenbereich ab 10Ch oder 110h beginnen lassen.
Eine weitere Anweisung in diesem Programm ist die DB-Anweisung. Durch sie wird byteweise Speicherplatz reserviert. Der NASM kennt darüber hinaus noch weitere Anweisungen, um Speicherplatz zu reservieren. Dies sind:
Anweisung | Breite | Bezeichnung |
---|---|---|
DW | 2 Byte | Word |
DD | 4 Byte | Doubleword |
DF | 6 Byte | - |
DQ | 8 Byte | Quadword |
DT | 10 Byte | Ten Byte |
Die Zahl 13 dezimal entspricht im ASCII Zeichensatz dem Wagenrücklauf, die Zahl 10 dezimal entspricht im ASCII-Zeichensatz dem Zeilenvorschub. Beide zusammen setzen den Cursor auf den Anfang der nächsten Zeile. Das $
-Zeichen wird für den Aufruf der Funktion 9h des Interrupts 21h benötigt und signalisiert das Ende der Zeichenkette.
EXE-Dateien
BearbeitenDie bisher verwendeten COM-Dateien können maximal ein Segment groß werden. EXE-Dateien können dagegen aus mehreren Segmenten bestehen und damit größer als 64 KB werden.
Im Gegensatz zu DOS unterstützt Windows überhaupt keine COM-Dateien mehr und erlaubt nur EXE-Dateien auszuführen. Trifft Windows auf eine COM-Datei, wird sie automatisch in der Eingabeaufforderung als MS-DOS-Programm ausgeführt.
Im Gegensatz zu einer COM-Datei besteht eine EXE-Datei nicht nur aus ausführbarem Code, sondern besitzt einen Header, der vom Betriebssystem ausgewertet wird. Der Header unterscheidet sich zwischen Windows und DOS. Wir wollen jedoch nicht näher auf die Unterschiede eingehen.
Ein weiterer Unterschied ist, dass EXE-Dateien in zwei Schritten erzeugt werden. Beim Übersetzen des Quellcodes entsteht zunächst eine „Objektdatei“ als Zwischenprodukt. Eine oder mehrere Objektdateien werden anschließend von einem Linker zum EXE-Programm zusammengefügt. Bei einem Teil der Objektdateien handelt es sich üblicherweise um Dateien aus Programmbibliotheken.
Der Netwide-Assembler besitzt allerdings selbst keinen Linker. Sie müssen deshalb entweder auf einen Linker eines anderen Compilers oder auf einen freien Linker wie beispielsweise ALINK zurückgreifen. ALINK kann von der Webseite http://alink.sourceforge.net/ heruntergeladen werden.
Das folgende Programm ist eine Variante unseres „Hello World“-Programms.
segment code
start:
mov ax, data
mov ds, ax
mov dx, hello
mov ah, 09h
int 21h
mov al, 0
mov ah, 4Ch
int 21h
segment data
hello: db 'Hello World!', 13, 10, '$'
Speichern Sie das Programm unter hworld.asm ab und übersetzen Sie es:
nasm hworld.asm -f obj -o hworld.obj alink hworld.obj
Beim Linken des Programms gibt der Linker möglicherweise eine Warnung aus. Bei ALINK lautet sie beispielsweise Warning – no stack
. Beachten Sie diese Fehlermeldung nicht. Wir werden in einem der nächsten Abschnitte erfahren, was ein Stack ist.
Die erste Änderung, die auffällt, sind die Assembleranweisungen segment code
und segment data
. Die Anweisung ist ein Synonym für section
. Wir haben uns hier für segment
entschieden, damit deutlicher wird, dass es sich hier im Unterschied zu einer COM-Datei tatsächlich um verschiedene Segmente handelt.
Die Bezeichnungen für die Segmente sind übrigens beliebig. Sie können die Segmente auch hase
und igel
nennen. Der Assembler benötigt die Bezeichnung lediglich für die Adressberechnung und muss dazu wissen, wo ein neues Segment anfängt. Allerdings ist es üblich das Codesegment mit code
und das Datensegment mit data
zu bezeichnen.
Die Anweisung start:
legt den Anfangspunkt des Programms fest. Diese Information benötigt der Linker, wenn er mehrere Objektdateien zusammenlinken muss. In diesem Fall muss er wissen, in welcher Datei sich der Eintrittspunkt befindet.
In einer Intel-CPU gibt es keinen direkten Befehl, eine Konstante in ein Segmentregister zu laden. Man muss deshalb entweder den Umweg über ein universelles Register gehen (im Beispiel: AX), oder man benutzt den Stack (den man vorher korrekt initialisieren muss): push data, dann pop data.
Nachdem Sie das Programm übersetzt haben, führen Sie es mit dem Debugger aus:
debug hworld.exe -r AX=0000 BX=FFFF CX=FE6F DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=0D3A ES=0D3A SS=0D4A CS=0D4A IP=0000 NV UP EI PL NZ NA PO NC 0D4A:0000 B84B0D MOV AX,0D4C -t AX=0D4C BX=FFFF CX=FE6F DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=0D3A ES=0D3A SS=0D4A CS=0D4A IP=0003 NV UP EI PL NZ NA PO NC 0D4A:0003 8ED8 MOV DS,AX -t AX=0D4B BX=FFFF CX=FE6F DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=0D4C ES=0D3A SS=0D4A CS=0D4A IP=0005 NV UP EI PL NZ NA PO NC 0D4A:0005 BA0000 MOV DX,0000
Die ersten zwei Zeilen unterscheiden sich vom COM-Programm. Der Grund hierfür ist, dass wir nun zunächst das Datensegmentregister (DS) initialisieren müssen. Bei einem COM-Programm sorgt das Betriebssystem dafür, dass alle Segmentregister einen Anfangswert haben, und zwar alle den gleichen Wert. Bei einem COM-Programm braucht man sich deshalb nicht um die Segmentregister kümmern, hat dafür aber auch nur 64k für Programm + Daten zur Verfügung. Bei einem EXE-Programm entfällt die Einschränkung auf 64 k, aber man muss sich selbst um die Segmentregister kümmern.
Wie Sie sehen, beträgt die Differenz zwischen DS- und CS-Register anschließend 2 (0D4C – 0D4A). Das bedeutet allerdings nicht, dass nur zwei Byte zwischen Daten- und Codesegment liegt - darin würde der Code auch keinen Platz finden. Wie wir im letzten Abschnitt gesehen haben, liegen die Segmente 16 Byte auseinander, weshalb DS und CS Segment tatsächlich 32 Byte auseinanderliegen.
Das DX-Register erhält diesmal den Wert 0, da die Daten durch die Trennung von Daten- und Codesegment nun ganz am Anfang des Segments liegen.
Der Rest des Programms entspricht dem einer COM-Datei. Beenden Sie deshalb anschließend den Debugger.
Hello World in anderen Betriebssystemen
BearbeitenDie oben angegebenen Programme enthalten zwei verschiedene Systemabhängigkeiten: Zum einen logischerweise die Abhängigkeit vom Prozessor (es handelt sich um Code für den 80x86, der logischerweise nicht auf einem anderen Prozessor lauffähig ist), aber zusätzlich die Abhängigkeit vom verwendeten Betriebssystem (in diesem Fall DOS; beispielsweise wird obiger Code unter Linux nicht laufen). Um die Unterschiede zu zeigen, sollen hier exemplarisch Versionen von Hello World für andere Betriebssysteme gezeigt werden. Der verwendete Assembler ist in allen Fällen nasm.
Eine Anmerkung vorweg: Die meisten Betriebssysteme verwenden den seit dem 80386 verfügbaren 32-Bit-Modus. Dieser wird später im Detail behandelt, im Moment ist nur interessant, dass die Register 32 Bits haben und ein „e“ (für extended = erweitert) am Anfang des Registernamens bekommen (z. B. eax statt ax), und dass man Segmentregister in der Regel vergessen kann.
Linux
BearbeitenUnter Linux werden statt Segmenten sections verwendet (letztlich landet alles im selben 32-Bit-Segment). Die Code-Section heißt „.text“, die Datensection „.data“ und der Einsprungpunkt für den Code „_start“. Systemaufrufe benutzen int 80h statt des 21h von DOS, Werte werden in der Regel wie unter DOS über Register übergeben (wenngleich die Einzelheiten sich unterscheiden). Außerdem wird ein Zeilenende unter Linux nur mit einem Linefeed-Zeichen (ASCII 10) gekennzeichnet. Es sollte nicht schwer sein, die einzelnen Elemente des Hello-World-Programms im folgenden Quelltext wiederzuerkennen.
section .text
global _start
_start:
mov ecx, hello
mov edx, length
mov ebx, 1 ; Dateinummer der Standard-Ausgabe
mov eax, 4 ; Funktionsnummer: Ausgabe
int 80h
mov ebx, 0
mov eax, 1 ; Funktionsnummer: Programm beenden
int 80h
section .data
hello db 'Hello World!', 10
length equ $ - hello;
Die folgenden Kommandozeilenbefehle bewirken das Kompilieren und Ausführen des Programms:
nasm -g -f elf32 hello.asm ld hello.o -o hello ./hello
Für einen 64-Bit Prozessor sind folgende Kommandozeilenbefehle notwendig
nasm -g -f elf64 hello.asm ld hello.o -o hello ./hello
Erläuterung: nasm ist wieder der Assembler. Er erzeugt hier die Datei hello.o, die man noch nicht ausführen kann. Die Option -g sorgt dafür, dass Debuginformationen in die zu erzeugende hello.o-Datei geschrieben werden. Der sogenannte Linker ld erzeugt aus hello.o die fertige, lauffähige Datei hello. Der letzte Befehl startet schließlich das Programm.
Hat man den GNU Debugger am Start, kann man per
gdb hello
ähnlich wie unter MS Windows debuggen. Der GDB öffnet eine Art Shell, in der diverse Befehle verfügbar sind. Der Befehl list gibt den von uns verfassten Assemblercode inklusive Zeilennummern zurück. Mit break 5 setzen wir einen Breakpoint in der fünften Zeile unseres Codes. Ein anschließendes run führt das Programm bis zum Breakpoint aus, und wir können nun mit info registers die Register auslesen. Einen einzelnen Schritt können wir mit stepi ausführen.
(gdb) list 1 section .text 2 global _start 3 _start: 4 mov ecx, hello 5 mov edx, length 6 mov ebx, 1 ; Dateinummer der Standard-Ausgabe 7 mov eax, 4 ; Funktionsnummer: Ausgabe 8 int 80h 9 mov ebx, 0 10 mov eax, 1 ; Funktionsnummer: Programm beenden (gdb) break 5 Breakpoint 1 at 0x8048085: file hello.asm, line 5. (gdb) run Starting program: /home/name/projects/assembler/hello_world/hello Breakpoint 1, _start () at hello.asm:5 5 mov edx, length (gdb) info registers eax 0x0 0 ecx 0x80490a4 134516900 edx 0x0 0 ebx 0x0 0 esp 0xbf9e6140 0xbf9e6140 ebp 0x0 0x0 esi 0x0 0 edi 0x0 0 eip 0x8048085 0x8048085 <_start+5> eflags 0x200292 [ AF SF IF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x0 0 (gdb) stepi _start () at hello.asm:6 6 mov ebx, 1 ; Dateinummer der Standard-Ausgabe (gdb) quit
NetBSD
BearbeitenDie NetBSD-Version unterscheidet sich von der Linux-Version vor allem dadurch, dass alle Argumente über den Stack übergeben werden. Auch der Stack wird später behandelt; an dieser Stelle ist nur wesentlich, dass man mit push-Werte auf den Stack bekommt, und dass die Reihenfolge wesentlich ist. Außerdem benötigt man für NetBSD eine spezielle .note-Section, die das Programm als NetBSD-Programm kennzeichnet.
section .note.netbsd.ident
dd 0x07,0x04,0x01
db "NetBSD",0x00,0x00
dd 200000000
section .text
global _start
_start:
push dword length
push dword hello
push dword 1
mov eax, 4
push eax
int 80h
push dword 0
mov eax, 1
push eax
int 80h
section .data
hello db 'Hello World!', 10
length equ $ - hello;
macOS (Intel 64bit)
BearbeitenmacOS erfordert je nach Betriebssystemversion einen unterschiedlichen Einstiegspunkt in das Programm.
; Dateiname: HelloWorld.macOS.asm
global _main ; exportable macOS entry 10.8 and upper
global start ; exportable macOS entry 10.7 and lower
section .text
start: ; entry point macOS entry 10.7 and lower
_main: ; entry point macOS entry 10.8 and upper
mov rax, 0x02000004
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
mov rax, 0x02000001
xor rdi, rdi
syscall
section .data
message:
db "Hello, World", 10
Um das Programm lauffähig zu machen rufen wir nacheinander den Assembler und Linker auf, wobei wir kein ELF Dateiformat sondern ein Macho-64bit Dateiformat für macOS benötigen.
nasm -f macho64 -o HW.obj HelloWorld.macOS.asm
ld -macosx_version_min 11.2.1 -static -o world.run HW.obj