n00bor's notes on NASM and Linux System Calls


Inhalt:
>Allgemeines
>>Register
>>Zahlensysteme
>>>Dezimalsystem
>>>Hexadezimalsystem
>>>Binärsystem
>Programmaufbau
>>.data-Sektion
>>.bss-Sektion
>>.text-Sektion
>System Calls anhand eines Beispielprogramms
>Kommandozeilenparameter
>Den Quelltext kompilieren
>Erstes Programm: Hello world!
>Programmablauf
>>Funktionen



>Allgemeines:

Kommentare werden im Quelltext mit einem Semikolon (;) eingeleitet.

>>Register:

RegisterNameAnwendung
EAXaccumulator registerZiel für Rechenoperationen + Diverses
EBXbase registerAnfangsadresse einer Datenstruktur speichern + Diverses
ECXcount registerZähler für Schleifen + Diverses
EDXdata registerDatenregister für 2. Operanden + Diverses
ESIsource indexBlockoperationen
EDIdestination indexBlockoperationen
EBPbase pointerZeigt auf ein beliebiges Element im Stack oder im Speicher
ESPstack pointerZeigt auf das oberste Element im Stack
EIPinstruction pointer-
CScode segment-
DSdatasegment-
SSstack segment-
ESextra segment-
......Die Tabelle ist nicht vollständig und wird erweitert.

Das E vor den Namen steht für "extended". Extended Register sind 32 Bit lang, wie zB das EAX.
Ohne das E kann man auf das 16 Bit lange AX zugreifen.
Dieses ist wiederum in AH (A High) und AL (A Low) einteilbar, welche jeweils 8 Bit lang sind.

Die Register EAX, EBX, ECX und EDX sind "Multifunktionsregister". Diese können wir nutzen um diverse Daten abzulegen ohne extra Speicher zu reservieren. Auch werden sie für die Parameterübergabe an System Calls genutzt.

>>Zahlensysteme:

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.

>>>Hexadezimalsystem:

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.

>>>Binärsystem:

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




>Programmaufbau:

>>.data-Sektion

Deklaration von Konstanten. Hier wird zu Laufzeit nichts verändert.

Beispiel:	section .data
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

>>.bss-Sektion

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




>System Calls anhand eines Beispielprogramms:

		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.
(MOV ziel, quelle kopiert den Wert von quelle in ziel.)
Darauf folgt der erste Paramter, welcher in ebx kopiert wird.
Würde es einen zweiten Paramter geben, würde er in ecx kopiert werden. Ein Dritter in edx (...).
Der Befehl "int 80h" löst den Interrupt aus, der dem Kernel sagt: "Führe den System Call aus, den ich dir in eax gespeichert habe und verwende als Parameter die Werte/Adressen die ich dir in ebx und den folgenden Registern abgelegt habe!".
In diesem Fall wird das Programm einfach korrekt beendet.
Die jeweilige "Nummer" des System Calls kann man den man-pages oder google entnehmen. (Bsp: "man 2 exit", "man 2 write",...)




>Kommandozeilenparameter:

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




>Den Quelltext kompilieren:

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.




>Erstes Programm: Hello world!

Als erstes wollen wir in der .data-Sektion unseren String "Hello world!" ablegen. Dies sieht wie folgt aus:

	section .data
			hello: db 'Hello world!',10
Was "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.

Variablen brauchen wir für unser Programm nicht. Somit ist die .bss-Sektion hier nicht nötig.

Nun zum eigentlichen Programmcode, also der .text-Sektion. Wir beginnen also wie folgt:
	section .text
	global _start
	       _start: ;nachfolgend der weitere code
		       ;(siehe Programmaufbau, oben im Text)

Nun stellt sich die Frage, wie wir in NASM auf den Bildschirm schreiben können. Google oder die man-pages führen einen eventuell zu der Systemfunktion write().
Doch damit schreiben wir doch in Dateien? Vielleicht ist einem der Satz "Everything in a UNIX system is a file." bekannt. Wir haben also zur Ausgabe in unsere Konsole die Devicedatei /dev/stdout in die wir schreiben können.
Zurück zu write(), deren Aufruf in C so aussieht: ssize_t write(int fd, const void *buf, size_t count);.
Wir haben also 3 Parameter:
Die Ganzzahl/der Integer fd stellt den file descriptor der Datei da, in die wir schreiben wollen. Diesen bekommt man z.B. von der Funktion open() als Rückgabewert. Da dieser für stdout aber normal immer 1 ist, können wir diesen so übernehmen.
Der zweite Parameter, also *buf, ist ein Pointer auf unseren Buffer. Einfacher gesagt ist es die Adresse von dem Text, den wir ausgeben wollen.
Als dritten Parameter verlangt die Funktion noch einen count/Zähler. Dieser ist eine Zahl, die die Anzahl an Bytes angiebt, die geschrieben werden sollen. Also die Länge unseres Textes, hier 13 ("Hello world!" + unser Linefeed character).

Nun zu unserem Aufruf von write():
	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!"

Somit dürfte nun schon auf die Konsole geschrieben worden sein. Der Befehl MOV kopiert den Wert des zweiten Operanten in den ersten.
(MOV ziel, quelle)
Der dritte Parameter, also die Textlänge lässt sich natürlich nicht immer so leicht abzählen. Bei konstanten Texten lässt sich ein kleiner Trick anwenden, der im Punkt "Programmaufbau > .data-Sektion" in den Kommentaren beschrieben ist.
Um unser Programm ordnungsgemäß zu beenden müssen wir noch den System Call exit() ausführen, dem wir als einzigen Paramter den Wert 0 für "no error" übergeben. exit() hat die "Nummer" 1.

Das dürfte nun ein Kinderspiel sein:
	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

Somit hätten wir "Hello world!" bewerkstelligt. Gar nicht so schwer oder? :-)




>Programmablauf

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

>>Funktionen

Wä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 80h
Unsere 5 Zeilen für den Aufruf von write() und die 3 von exit() möchten wir nun also auslagern.
Dazu benötigen wir 2 neue Anweisung: CALL und RET.
Zur Veranschaulichung hier der Quelltext mit einer Funktion zur Ausgabe:
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 exit
			
Wie wir sehen besteht unser "Hauptprogramm" nur noch aus 2 Funktionsaufrufen. Zum Aufrufen einer Funktion wird, wie man sieht, CALL genutzt.
CALL bekommt als Operand ein Label. (Wie oben gesagt repräsentiert ein Label eine Adresse.) Dieses Label müssen wir irgendwo gesetzt haben. Genau an dieser Stelle, an der wir das Label gesetzt haben, beginnt der Quellcode unserer Funktion.
Ist der Quellcode der Funktion abgearbeitet so kommt unsere zweite neue Anweisung ins Spiel: RET.
RET steht für return und kehrt zu den Ort/der Speicheradresse zurück von der aus die Funktion aufgerufen wurde. Doch woher weis RET woher die Funktion aufgerufen wurde?
Ganz einfach: Bei dem Aufruf von CALL wird der Inhalt des EIP (Extended Instruction Pointer) auf den Stack gelegt. Der Inhalt des EIP ist die Adresse des nächsten auszuführenden Befehls.
Wird RET aufgerufen so wird diese Adresse zurück in das EIP-Register geschrieben und das Programm läuft normal weiter.




n00bor [ n00bor.org | sys-flaw.com ]