Kommentare werden im Quelltext mit einem Semikolon (;) eingeleitet.
Register | Name | Anwendung |
---|---|---|
EAX | accumulator register | Ziel für Rechenoperationen + Diverses |
EBX | base register | Anfangsadresse einer Datenstruktur speichern + Diverses |
ECX | count register | Zähler für Schleifen + Diverses |
EDX | data register | Datenregister für 2. Operanden + Diverses |
ESI | source index | Blockoperationen |
EDI | destination index | Blockoperationen |
EBP | base pointer | Zeigt auf ein beliebiges Element im Stack oder im Speicher |
ESP | stack pointer | Zeigt auf das oberste Element im Stack |
EIP | instruction pointer | - |
CS | code segment | - |
DS | datasegment | - |
SS | stack segment | - |
ES | extra segment | - |
... | ... | Die Tabelle ist nicht vollständig und wird erweitert. |
Für die Programmierung mit Assembler ist die Kenntnis von den folgenden 3 Zahlensysteme dringend erforderlich.
>>>Dezimalsystem:Das Dezimalsystem ist "unser" Zahlensystem. Von 0 bis 9. Wir haben also pro Stelle 10 Möglichkeiten. Das Dezimalsystem hat also die Basis 10.
Die Zahlen sind also wie folgt aufgebaut:
Bsp.: 1337,42
1000 = 1 * 10^3
300 = 3 * 10^2
30 = 3 * 10^1
7 = 7 * 10^0
0,4 = 4 * 10^-1
0,02 = 2 * 10^-2
1337,42 = 1 * 10^3 + 3 * 10^2 + 3 * 10^1 + 7 * 10^0 + 4 * 10^-1 + 2 * 10^-2
In Assembler werden die Dezimalzahlen mit einem angehängten "d" gekennzeichnet: 23d.
Das Hexadezimalsystem wird in der Welt der Computer relativ oft zur Darstellung von Zahlen verwendet.
Es geht von 0 bis F. 0-9 und A-F.
A=10
B=11
C=12
D=13
E=14
F=15
Man hat also 16 Möglichkeiten pro Stelle.
Die Zahlen sind wie folgt aufgebaut:
Bsp.: DEAD1
D0000 = D * 16^4 = 13 *16^4
E000 = E *16^3 = 14 * 16^3
A00 = A * 16^2 = 10 * 16^2
D0 = D * 16^1 = 13 * 16^1
1 = 1 * 16^0
DEAD1 = D * 16^4 + E *16^3 + A * 16^2 + D * 16^1 + 1 * 16^0
In Assembler werden Hexadezimalzahlen mit einem angehängten "h" gekenntzeichnet: 42h.
Das Binärsystem ist das altbekannte System mit den Ziffern 0 und 1. Mehr gibt es nicht.
Bsp.: 1011
1000 = 1 * 2^3 = 8
000 = 0 * 2^2 = 0
10 = 1 * 2^1 = 2
1 = 1 * 2^0 = 1
1011 = 1 * 2^3 + 0 * 2^2 + 1 * 2^1 + 1 * 2^0 = 8d + 0d + 2d +1d = 11d (Zur Erinnerung: 11d ist 11 im Dezimalsystem.)
In Assembler werden Binärzahlen mit einem angehängten "b" gekennzeichnet: 1100b.
Negative Binärzahlen bildet man mit dem Zweierkomplement der positiven Zahl
Bsp.: 00001000 (8d)
-> 11110111 (Einerkomplement bilden, also die binäre Zahl invers.)
-> 11111000 (Zweierkomplement bilden, also zum Einerkomplement die Zahl 1 addieren.)
Deklaration von Konstanten. Hier wird zu Laufzeit nichts verändert.
Beispiel: section .data>>.bss-Sektion
ein_string: db 'Hello n00bor!' ; ein_string_len: equ $-ein_string ;$ = adresse aktueller zeile, ;also ist addresse hier - startadresse von ein_string die länge in bytes ;equ reserviert platz für das ergebniss eine_zahl: dw 1337 ;declare word
Deklaration von Variablen.
Beispiel: section .bss ein_byte: resb 1 ;reserve byte, 1 stück bzw. byte viele_bytes: resb 666 ;reserve byte, 666 stück bzw. byte ein_word: resw 1 ;reserve word, 1 stück bzw. word>>.text-Sektion
Hier steht der Quelltext. Sie beginnt mit "global _start", welches dem Kernel anzeigt "Hier mit der Codeausführung beginnen". Vergleich in C wäre etwa die main()-Funktion. (Es ist aber keine Funktion sondern ein "Starpunkt".)
Beispiel: section .text global _start _start: ; Quellcode hier
section .data global _start _start: mov eax,1 ;1 ist exit() mov ebx,0 ;0 der erste Parameter int 80h ;interrupt für den Kernel C-Äquivalent: int main() { exit(0); //bzw. return 0 }Zu erst muss die "Nummer" des System Calls, hier 1 für exit(), in das Register eax kopiert werden.
Übergeben wir dem Programm Parameter beim Aufruf in der Konsole, so landen diese auf dem Stack. (Siehe unten.)
Ein kurzes Beispiel:
n00bor@somebox # ./programm hans 42
In C hätte man zum Zugriff aus "int main(int argc,char* argv[])" die Anzahl der Parameter in argc, und die Parameter selbst in argv[]. Der Programmname ist auch ein Parameter.
argv[0] wäre "programm", argv[1] wäre "hans" und argv[2] wäre "42". Die Anzahl argc würde 3 betragen.
Der Stack würde nach dem Aufruf wie folgt aussehen:
---------------- | 3 | ---------------- | programm | ---------------- | hans | ---------------- | 42 | ----------------Der Stack ist ein LIFO-Speicher. LIFO steht in diesem Fall für "last in first out". Man stapelt die Daten also aufeinander und darf immer nur das oberste wegnehmen. Der Zugriff darauf erfolgt wie folgt:
;... pop eax ;die 3, also argc, wird "gepopt" pop ebx ;"programm", also argv[0], wird "gepopt" pop ecx ;"hans", also argv[1], wird "gepopt" pop edx ;"42", also argv[2], wird "gepopt" ;...Der Befehl POP nimmt also das oberste Element des Stacks und speichert es an den Ort, den wir als Operant angeben. (In diesem Fall unsere Register eax-edx.) Das Gegenstück ist PUSH. Damit kann man etwas auf dem Stack ablegen.
Mit "nasm -f elf foo.asm" erstellt uns nasm eine binäre Objektdatei im "Executable and Linking Format" (-f elf).
Nun haben wir, wenn keine Fehler im Code enthalten sind, eine Datei names foo.o im Verzeichnis.
Ein ausführbares Programm bekommen wir mit "ld -s -o foo foo.o". Der Parameter -s steht für "Strip all Symbols", was unsere Datei kleiner macht, indem etwa Namen entfernt werden.
"-o foo" gibt die Outputdatei an, also unser fertiges Programm. Mit "foo.o" geben wir unsere Objektdatei an, welche wir zuvor erstellt haben.
Nun dürften wir eine fertige, ausführbare Binary haben, welche sich starten lässt.
Als erstes wollen wir in der .data-Sektion unseren String "Hello world!" ablegen. Dies sieht wie folgt aus:
section .data hello: db 'Hello world!',10Was "section .data" bedeutet, sollte klar sein: Die .data-Sektion beginnt hier. Mit "hello:" setzen wir eine Marke/ein Label. Dieses ist im Progamm später der Name des Strings. Es repräsentiert im Quellcode die Adresse, die der String später haben wird. Die Anweisung db (declare byte) sorgt dafür, das für die nachfolgenden Operanten, also unseren Text, Speicher reserviert und dieser mit unseren Text gefüllt wird. Der erste Operant ist unser "Hellor world!". Nach dem Komma folgt noch eine 10, welche den Linefeed Character als ASCII-Zahl darstellt. Also eine newline (\n), einen carriage return (\r) bzw. einfach eine neue Zeile darstellt.
section .text global _start _start: ;nachfolgend der weitere code ;(siehe Programmaufbau, oben im Text)
mov eax,4 ;4 ist die "Nummer" von write(), siehe google/man-pages mov ebx,1 ;Parameter 1, der file descriptor von STDOUT mov ecx,hello ;Parameter 2, hello ist unser Label aus der .data-Sektion, also die Adresse des Textes mov edx,13 ;Parameter 3, die Länge des Textes, also die Anzahl der zu schreibenden Bytes int 80h ;Unser interrupt, der dem Kernel sagt: "Führe den System Call aus dem eax-Register aus!"
mov eax,1 ;1, also exit() in eax mov ebx,0 ;als Parameter die 0 für "no error" int 80h ;interrupt 80, der den Kernel zum ausführen des System Calls anweist
Ähnlich wie in höheren Progammiersprachen haben wir in Assembler die Möglichkeit unser Programm etwas zu strukturieren. Wir können Programmcode bedingt ausf¨hren oder auch häufig genutzte Programmteile auslagern oder wiederholen. Allerdings nicht ganz so einfach wie in höheren Progammiersprachen.
>>FunktionenWährend dem Programmieren kommt es immer wieder vor, dass wir auf Programmteile stoßen, die wir mehr als einmal in verschieden Teilen unseres Programmes wiederholen wollen/müssen.
Als Beispiel werden wir die Ausgabe unseres "Hello World!"-Programmes auslagern. Ebenso unser exit(0).
Hier nochmal der alte Quelltext:
section .data hello: db 'Hello world!',10 section .text global _start _start: ;write(), hello world in /dev/stdout mov eax,4 mov ebx,1 mov ecx,hello mov edx,13 int 80h ;exit(0) mov eax,1 mov ebx,0 int 80hUnsere 5 Zeilen für den Aufruf von write() und die 3 von exit() möchten wir nun also auslagern.
section .data hello: db 'Hello world!',10 section .text global _start ;write(), hello world in /dev/stdout write_hello: mov eax,4 mov ebx,1 mov ecx,hello mov edx,13 int 80h ret ;exit(0) exit: mov eax,1 mov ebx,0 int 80h ret _start: call write_hello call exitWie wir sehen besteht unser "Hauptprogramm" nur noch aus 2 Funktionsaufrufen. Zum Aufrufen einer Funktion wird, wie man sieht, CALL genutzt.