Systemaufrufe: Unterschied zwischen den Versionen
| Zeile 137: | Zeile 137: | ||
[[Übersicht der Fehlercodes]] | [[Übersicht der Fehlercodes]] | ||
== Strukturen bei Systemcalls == | |||
einige Systemcalls in Linux (auch auf ARM64) erwarten Strukturen als Argumente – also zusammengesetzte Daten, die du im Speicher bereitstellen musst und dann per Zeiger übergibst. Das ist für Einsteiger oft ein Aha-Moment im Assembler. | |||
📦 Strukturen an Systemcalls übergeben (ARM64/Linux) – Anleitung für Anfänger | |||
🧠 Grundidee | |||
Einige Systemcalls (z. B. stat, gettimeofday, timespec, uname, poll, etc.) brauchen als Argument einen Zeiger auf eine Struktur, die im Speicher angelegt ist. | |||
Du musst: | |||
Den Speicher für die Struktur bereitstellen (z. B. über .skip oder .space) | |||
Die Struktur ggf. initialisieren (z. B. Null setzen oder Werte eintragen) | |||
Den Zeiger zur Struktur ins richtige Register laden (z. B. x1) | |||
Den passenden Systemcall ausführen (svc #0) | |||
📄 Beispiel 1: uname-Systemcall | |||
Dieser Systemcall füllt eine struct utsname mit Informationen über das System (wie uname -a in der Shell). | |||
c | |||
Kopieren | |||
Bearbeiten | |||
// C-Definition: | |||
struct utsname { | |||
char sysname[65]; | |||
char nodename[65]; | |||
char release[65]; | |||
char version[65]; | |||
char machine[65]; | |||
char domainname[65]; // oft ignoriert | |||
}; | |||
Gesamtgröße: mindestens 390 Bytes | |||
💻 Assembler-Beispiel: uname aufrufen und sysname (z. B. „Linux“) ausgeben | |||
asm | |||
Kopieren | |||
Bearbeiten | |||
.section .bss | |||
utsbuf: .skip 390 // Speicher für struct utsname | |||
outbuf: .skip 65 // Puffer zum Ausgeben von sysname | |||
.section .text | |||
.global _start | |||
_start: | |||
// Systemcall: uname(struct utsname *buf) | |||
// syscall number: 160 | |||
ldr x0, =utsbuf // Zeiger auf struct utsname | |||
mov x8, #160 // syscall number | |||
svc #0 | |||
// Rückgabewert in x0: 0 bei Erfolg, sonst -errno | |||
cmp x0, #0 | |||
b.ne uname_fail | |||
// Jetzt sysname (erstes Feld der Struktur) ausgeben: | |||
// Es ist eine nullterminierte Zeichenkette | |||
ldr x1, =utsbuf // sysname ist am Anfang | |||
bl print_string | |||
b done | |||
uname_fail: | |||
// Fehlerausgabe (optional) | |||
ldr x1, =failmsg | |||
bl print_string | |||
done: | |||
// exit(0) | |||
mov x0, #0 | |||
mov x8, #93 | |||
svc #0 | |||
// Ausgabe-Funktion (x1 = Zeiger auf 0-terminierten String) | |||
print_string: | |||
mov x2, #0 | |||
count_loop: | |||
ldrb w3, [x1, x2] | |||
cbz w3, print_now | |||
add x2, x2, #1 | |||
b count_loop | |||
print_now: | |||
mov x0, #1 // stdout | |||
mov x8, #64 // write | |||
svc #0 | |||
ret | |||
.section .data | |||
failmsg: .asciz "uname failed\n" | |||
🧪 Kompilieren & Ausführen | |||
bash | |||
Kopieren | |||
Bearbeiten | |||
as -o uname.o uname.s | |||
ld -o uname uname.o | |||
./uname | |||
Erwartete Ausgabe: | |||
bash | |||
Kopieren | |||
Bearbeiten | |||
Linux | |||
📚 Weitere Beispiele für strukturbasierte Syscalls | |||
Systemcall Struktur Zweck | |||
uname struct utsname Systeminfo | |||
statx struct statx Dateiinformation | |||
gettimeofday struct timeval aktuelle Uhrzeit | |||
nanosleep struct timespec schlafen für Zeitspanne | |||
poll struct pollfd Warten auf Dateideskriptor | |||
🛠 Beispiel 2: nanosleep mit struct timespec | |||
c | |||
Kopieren | |||
Bearbeiten | |||
// struct timespec { | |||
long tv_sec; // ganze Sekunden | |||
long tv_nsec; // Nanosekunden | |||
// }; | |||
⏱ Schlafen für 2 Sekunden: | |||
asm | |||
Kopieren | |||
Bearbeiten | |||
.section .data | |||
timespec: | |||
.quad 2 // tv_sec = 2 | |||
.quad 0 // tv_nsec = 0 | |||
.section .text | |||
.global _start | |||
_start: | |||
ldr x0, =timespec // Zeiger auf timespec | |||
mov x1, #0 // NULL für "remaining" | |||
mov x8, #101 // syscall: nanosleep | |||
svc #0 | |||
// done | |||
mov x0, #0 | |||
mov x8, #93 | |||
svc #0 | |||
== Arbeiten mit Dateien == | == Arbeiten mit Dateien == | ||
Version vom 8. April 2025, 19:12 Uhr
In diesem Kapitel werden wir uns mit Systemaufrufen unter Linux beschäftigen. Insbesondere werden wir:
Eine Einführung in Linux-Systemaufrufe geben, Grundlegende Systemaufrufe wie exit, read, write, open und close kennenlernen und Erklären, wie Parameter übergeben und Rückgabewerte entgegengenommen werden.
Einführung in Linux-Systemaufrufe
Systemaufrufe sind Schnittstellen, die es Programmen ermöglichen, Dienste und Funktionen des Betriebssystems in Anspruch zu nehmen. Diese können niedrigere Eingabe-/Ausgabe-Funktionen, Prozesssteuerung, Speicherverwaltung und mehr umfassen. In ARM64-Assembler verwenden wir den svc-Befehl, um Systemaufrufe auszuführen. Dabei tragen wir die Nummer des spezifischen Systemaufrufs in Register x8 ein und die Parameter in Registers x0 bis x5.
Grundlegende Systemaufrufe
exit
Dieser Systemaufruf beendet das Programm.
Syntax:
mov x8, #93 // syscall number for exit
mov x0, #0 // exit code (0 for success)
svc 0
read
Dieser Systemaufruf liest Daten von einer Datei.
Syntax:
mov x8, #63 // syscall number for read
mov x0, #file_desc // file descriptor (0 for stdin)
mov x1, #buffer // buffer to store the read data
mov x2, #size // number of bytes to read
svc 0
write
Dieser Systemaufruf schreibt Daten in eine Datei.
Syntax:
mov x8, #64 // syscall number for write
mov x0, #file_desc // file descriptor (1 for stdout)
mov x1, #buffer // buffer with data to write
mov x2, #size // number of bytes to write
svc 0
open
Dieser Systemaufruf öffnet eine Datei und gibt einen Datei-Deskriptor zurück.
Syntax:
mov x8, #56 // syscall number for openat
mov x0, #-100 // AT_FDCWD (current working directory)
mov x1, #filename // pointer to the filename string
mov x2, #flags // flags (e.g., O_RDONLY for read only)
svc 0
close
Dieser Systemaufruf schließt eine Datei.
Syntax:
mov x8, #57 // syscall number for close
mov x0, #file_desc // file descriptor
svc 0
Übergabe von Parametern und Entgegennahme von Rückgabewerten
Die Übergabe von Parametern erfolgt über bestimmte Register:
- x0 bis x5 für die ersten sechs Argumente
- x8 für die Systemaufrufnummer
Rückgabewerte werden in x0 zurückgegeben.
Beispiel:
.global _start
.section .data
hello_msg: .asciz "Hello, World!\n"
_start:
// write system call
mov x0, #1 // file descriptor (1 for stdout)
ldr x1, =hello_msg // buffer
mov x2, #13 // size
mov x8, #64 // syscall number for write
svc 0
// exit system call
mov x0, #0 // exit code
mov x8, #93 // syscall number for exit
svc 0
In diesem Beispiel wird die Zeichenkette "Hello, World!\n" auf die Standardausgabe geschrieben und danach das Programm mit dem Rückgabewert 0 beendet.
Verweis auf alle verfügbaren Linux-Systemaufrufe
Eine vollständige Liste der verfügbaren Linux-Systemaufrufe und deren Nummern finden Sie in den entsprechenden Header-Dateien des Systems, wie z.B. in /usr/include/asm/unistd.h oder online in den offiziellen Linux-Kernel-Dokumentationen.
Eine häufig verwendete Referenz für Systemaufrufe ist die Man-Seite (man 2 syscall). Eine umfassendere Liste spezifisch für den ARM64-Architektur habe ich hier erstellt: Übersicht der Linux ARM64 Systemaufrufen
Fehlercodes
Wenn ein Systemcall ausgeführt wird (z. B. write, open, read), prüft der Kernel, ob die Anfrage gültig ist, und führt sie aus – oder gibt einen Fehlercode zurück, wenn etwas schiefgeht.
Viele Systemcalls geben in x0 ein Ergebnis wie zum Beispiel die Anzahl von Bytes, ein Dateideskriptor, PID oder anderes zurück, wenn alles geklappt hat. Sollte allerdings ein negatives Ergebnis (errno) zurück kommen, so hat in der Regel etwas nicht funktioniert.
Beispiel:
// Datei öffnen (openat)
mov x0, AT_FDCWD // aktuelles Verzeichnis
ldr x1, =filename // Dateiname
mov x2, #0 // O_RDONLY
mov x8, #56 // syscall number für openat
svc #0
// Rückgabewert ist jetzt in x0
cmp x0, #0
b.ge open_success // x0 >= 0 → Erfolg
// Fehlerbehandlung
neg x1, x0 // Fehlercode positiv machen
// Jetzt enthält x1 z.B. 2 → ENOENT (Datei nicht gefunden)
open_success:
// normal weiter...
Strukturen bei Systemcalls
einige Systemcalls in Linux (auch auf ARM64) erwarten Strukturen als Argumente – also zusammengesetzte Daten, die du im Speicher bereitstellen musst und dann per Zeiger übergibst. Das ist für Einsteiger oft ein Aha-Moment im Assembler.
📦 Strukturen an Systemcalls übergeben (ARM64/Linux) – Anleitung für Anfänger 🧠 Grundidee Einige Systemcalls (z. B. stat, gettimeofday, timespec, uname, poll, etc.) brauchen als Argument einen Zeiger auf eine Struktur, die im Speicher angelegt ist.
Du musst:
Den Speicher für die Struktur bereitstellen (z. B. über .skip oder .space)
Die Struktur ggf. initialisieren (z. B. Null setzen oder Werte eintragen)
Den Zeiger zur Struktur ins richtige Register laden (z. B. x1)
Den passenden Systemcall ausführen (svc #0)
📄 Beispiel 1: uname-Systemcall Dieser Systemcall füllt eine struct utsname mit Informationen über das System (wie uname -a in der Shell).
c Kopieren Bearbeiten // C-Definition: struct utsname {
char sysname[65]; char nodename[65]; char release[65]; char version[65]; char machine[65]; char domainname[65]; // oft ignoriert
}; Gesamtgröße: mindestens 390 Bytes
💻 Assembler-Beispiel: uname aufrufen und sysname (z. B. „Linux“) ausgeben asm Kopieren Bearbeiten .section .bss utsbuf: .skip 390 // Speicher für struct utsname outbuf: .skip 65 // Puffer zum Ausgeben von sysname
.section .text .global _start
_start:
// Systemcall: uname(struct utsname *buf) // syscall number: 160
ldr x0, =utsbuf // Zeiger auf struct utsname mov x8, #160 // syscall number svc #0
// Rückgabewert in x0: 0 bei Erfolg, sonst -errno cmp x0, #0 b.ne uname_fail
// Jetzt sysname (erstes Feld der Struktur) ausgeben: // Es ist eine nullterminierte Zeichenkette
ldr x1, =utsbuf // sysname ist am Anfang bl print_string
b done
uname_fail:
// Fehlerausgabe (optional) ldr x1, =failmsg bl print_string
done:
// exit(0) mov x0, #0 mov x8, #93 svc #0
// Ausgabe-Funktion (x1 = Zeiger auf 0-terminierten String) print_string:
mov x2, #0
count_loop:
ldrb w3, [x1, x2] cbz w3, print_now add x2, x2, #1 b count_loop
print_now:
mov x0, #1 // stdout mov x8, #64 // write svc #0 ret
.section .data failmsg: .asciz "uname failed\n" 🧪 Kompilieren & Ausführen bash Kopieren Bearbeiten as -o uname.o uname.s ld -o uname uname.o ./uname Erwartete Ausgabe:
bash Kopieren Bearbeiten Linux 📚 Weitere Beispiele für strukturbasierte Syscalls Systemcall Struktur Zweck uname struct utsname Systeminfo statx struct statx Dateiinformation gettimeofday struct timeval aktuelle Uhrzeit nanosleep struct timespec schlafen für Zeitspanne poll struct pollfd Warten auf Dateideskriptor 🛠 Beispiel 2: nanosleep mit struct timespec c Kopieren Bearbeiten // struct timespec {
long tv_sec; // ganze Sekunden long tv_nsec; // Nanosekunden
// }; ⏱ Schlafen für 2 Sekunden: asm Kopieren Bearbeiten .section .data timespec:
.quad 2 // tv_sec = 2 .quad 0 // tv_nsec = 0
.section .text .global _start
_start:
ldr x0, =timespec // Zeiger auf timespec mov x1, #0 // NULL für "remaining" mov x8, #101 // syscall: nanosleep svc #0
// done mov x0, #0 mov x8, #93 svc #0
Arbeiten mit Dateien
https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/fcntl.h Datei öffnen:
mov x0,#-100 ldr x1,=Dateiname mov x2,#flags //O_RDONLY, O_WRONLY, =O_CREAT mov x3,#666 mov x8,#323 //#define __NR_openat 323 svc 0
Rückgabe in x0 ->
adds x10,XZR,x0 //file descriptor nach x10 bpl ok //Wenn positiv, dann hat es geklappt Fehler...
Datei schließen:
mov x0,x10 //Beispiel x10 für file descriptor mov x8,#118 //#define __NR_fsync 118 svc 0 mov x0,x10 //Beispiel x10 für file descriptor mov x8,#6 //#define __NR_close 6 svc 0
Lesen aus einer Datei:
mov x0,x10 //Beispiel x10 für file descriptor ldr x1,=buffer //Hierhin sollen die Daten geladen werden mov x2,#256 //Max Anzahl an Bytes, die geladen werden sollen (Beispiel: 256Bytes) mov x8,#3 //#define __NR_read 3 svc 0
Speichern in eine Datei:
mov x0,x10 //Beispiel x10 für file descriptor ldr x1,=buffer //Von hier sollen die Daten gespeichert werden mov x2,#256 //Anzahl an Bytes, die gespeichert werden sollen (Beispiel: 256Bytes) mov x8,#4 //#define __NR_write 4 svc 0
Ein Beispiel in ARM64-Assembler für Linux, das zeigt, wie Sie eine Datei öffnen, die Daten lesen, bearbeiten und anschließend die bearbeiteten Daten in einer anderen Datei speichern können. Ich werde dafür Systemaufrufe (syscalls) verwenden.
Überblick der Schritte: Datei öffnen (open). Daten aus der Datei lesen (read). Daten bearbeiten (In diesem Beispiel ändern wir die Daten nicht, sie könnten aber leicht modifiziert werden). Neue Datei erstellen und öffnen (open). Daten in die neue Datei schreiben (write). Beide Dateien schließen (close). Vollständiges Beispiel in ARM64-Assembler: .section .data
filename: .asciz "input.txt" newfilename:.asciz "output.txt" buffer: .space 1024 // Pufferspeicher zum Lesen der Datei
.section .bss
.lcomm result, 4 // Speicher für den Rückgabewert von syscalls
.section .text .global _start _start:
// Datei öffnen (input.txt) mov x0, 0 // stdin als File Descriptor (0) ldr x1, =filename // filename in x1 laden mov x2, 0 // O_RDONLY (0) mov x8, 2 // Syscall-Nummer für 'open' ist 2 svc 0 // Systemaufruf
mov x19, x0 // x0 enthält den File Descriptor ldr x1, =buffer // Speicheradresse des Puffers in x1 laden mov x2, 1024 // Maximale Anzahl von Bytes zu lesen mov x8, 63 // Syscall-Nummer für 'read' ist 63 svc 0 // Systemaufruf
// Ergebnis der gelesenen Bytes in x0 speichern mov x20, x0
// Datei zum Schreiben öffnen (output.txt) ldr x1, =newfilename // filename in x1 laden mov x2, 241 // O_WRONLY | O_CREAT | O_TRUNC (241) mov x3, 0644 // Modus 0644 (rw-r--r--) mov x8, 2 // Syscall-Nummer für 'open' (2) svc 0 // Systemaufruf
mov x21, x0 // File Descriptor für die Ausgabedatei
// Daten in die neue Datei schreiben ldr x1, =buffer // Speicheradresse des Puffers in x1 laden mov x2, x20 // Anzahl der gelesenen Bytes mov x0, x21 // File Descriptor der Ausgabedatei mov x8, 64 // Syscall-Nummer für 'write' (64) svc 0 // Systemaufruf
// Eingabedatei schließen mov x0, x19 // File Descriptor der Eingabedatei mov x8, 57 // Syscall-Nummer für 'close' (57) svc 0 // Systemaufruf
// Ausgabedatei schließen mov x0, x21 // File Descriptor der Ausgabedatei mov x8, 57 // Syscall-Nummer für 'close' (57) svc 0 // Systemaufruf
// Beende das Programm mov x8, 93 // Syscall-Nummer für 'exit' (93) mov x0, 0 // Rückgabewert 0 svc 0 // Systemaufruf
Erklärung: Datenabschnitt: .section .data:
filename: Speichert den Namen der Eingabedatei input.txt. newfilename: Speichert den Namen der Ausgabedatei output.txt. buffer: Ein Speicherbereich zum Lesen und Schreiben der Dateiinhalte. .section .bss:
Speicher zur Speicherung des Rückgabewerts von read. Textabschnitt: Datei öffnen (input.txt):
mov x0, 0: Bereitet den Systemaufruf vor, um stdin als Dateideskriptor zu verwenden. ldr x1, =filename: Lädt die Adresse des Dateinamens in x1. mov x2, 0: Setzt den Modus auf O_RDONLY (nur lesen). mov x8, 2: Setzt die Systemaufrufnummer für open (2). svc 0: Führt den Systemaufruf aus. Der Dateideskriptor wird in x0 zurückgegeben und in x19 gespeichert. Daten lesen:
ldr x1, =buffer: Lädt die Adresse des Puffers in x1. mov x2, 1024: Gibt an, dass bis zu 1024 Bytes gelesen werden. mov x8, 63: Setzt die Systemaufrufnummer für read (63). svc 0: Führt den Systemaufruf aus. Die Anzahl der gelesenen Bytes wird in x0 zurückgegeben und in x20 gespeichert. Datei öffnen (output.txt):
ldr x1, =newfilename: Lädt die Adresse des neuen Dateinamens in x1. mov x2, 241: Setzt den Modus auf O_WRONLY | O_CREAT | O_TRUNC (schreiben, Datei erstellen, Datei kürzen). mov x3, 0644: Setzt die Dateiberechtigungen auf rw-r--r--. mov x8, 2: Setzt die Systemaufrufnummer für open (2). svc 0: Führt den Systemaufruf aus. Der Dateideskriptor wird in x0 zurückgegeben und in x21 gespeichert. Daten schreiben:
ldr x1, =buffer: Lädt die Adresse des Puffers in x1. mov x2, x20: Setzt die Anzahl der zu schreibenden Bytes auf die Anzahl der gelesenen Bytes. mov x0, x21: Setzt den Dateideskriptor auf die Ausgabedatei. mov x8, 64: Setzt die Systemaufrufnummer für write (64). svc 0: Führt den Systemaufruf aus. Dateien schließen:
mov x0, x19: Setzt den Dateideskriptor auf die Eingabedatei. mov x8, 57: Setzt die Systemaufrufnummer für close (57). svc 0: Führt den Systemaufruf aus. mov x0, x21: Setzt den Dateideskriptor auf die Ausgabedatei. mov x8, 57: Setzt die Systemaufrufnummer für close (57). svc 0: Führt den Systemaufruf aus. Programm beenden:
mov x8, 93: Setzt die Systemaufrufnummer für exit (93). mov x0, 0: Setzt den Rückgabewert auf 0. svc 0: Führt den Systemaufruf aus und beendet das Programm. Dieses Beispiel demonstriert die wesentlichen Systemaufrufe und die Datenbearbeitung in einem ARM64-Assemblerprogramm unter Linux.
Linux Systemaufrufe
https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h https://github.com/ilbers/linux/blob/master/arch/sh/include/uapi/asm/unistd_64.h
x0–x7: Parameterübergabe x8: SystemCallNummer svc 0 -> Aufruf des Systemcalls x0: Ergebniss
Wenn x0 negativ -> negieren -> dann Fehler: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
In der Parameterübergabe erwarten viele SystemCalls eine Struktur. Ein einfacher SystemCall wie nanosleep wird als C-Code wie folgt aufgerufen:
int nanosleep(const struct timespec *req, struct timespec *rem);
Dieser Aufruf erwartet jeweils eine Struktur "timespec", die in x0 und x1 übergeben wird. Diese Struktur ist wie folgt definiert:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
Nach Assembler:
timespec: timespec_tv_sec: .dword 0 timespec_tv_nsec: .dword 100000000
Um es der Funktion zu übergeben:
ldr x0,=timespec ldr x1,=timespec