Assembler-Programmierung für x86-Prozessoren/ Das erste Assemblerprogramm



Die Befehle MOV und XCHG

Bearbeiten

Der 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

Bearbeiten

Schritt 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

Bearbeiten

Es 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

Bearbeiten

Die 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

Bearbeiten

Die 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.

Unter 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

Die 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)

Bearbeiten

macOS 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