- Eine technische Analyse untersucht den Prozess, mit dem der Kernel über den Systemaufruf
execve einen Prozess erzeugt und initialisiert, bevor ein Programm ausgeführt wird
- Dieser Aufruf übergibt den Pfad der ausführbaren Datei, Argumente und Umgebungsvariablen; auf dieser Basis lädt der Kernel eine ausführbare Datei im ELF-Format
- Eine ELF-Datei enthält Code, Daten, Symbole und Informationen für das dynamische Linken; der Kernel interpretiert diese und führt Speichermapping und Stack-Initialisierung durch
- Danach übergibt der Kernel die Kontrolle an den Entry-Point
_start, und erst nachdem die sprachspezifische Runtime initialisiert wurde, wird die benutzerdefinierte main-Funktion aufgerufen
- Dieser Ablauf zeigt die Zusammenarbeit von Betriebssystem, Compiler und Runtime und ist wichtig, um zu verstehen, wie Programmausführung auf Systemebene funktioniert
Der Startpunkt der Programmausführung: der execve-Aufruf
- Unter Linux beginnt die Programmausführung mit dem Systemaufruf
execve
- In der Form
execve(const char *filename, char *const argv[], char *const envp[]) werden Name der ausführbaren Datei, Argumentliste und Liste der Umgebungsvariablen übergeben
- Der Kernel entscheidet damit, welches Programm in welcher Umgebung ausgeführt wird
- In Hochsprachen ist dieser Aufruf durch die Prozessausführungs-API der Standardbibliothek gekapselt
- Beispiel: Rusts
std::process::Command ruft intern execve auf
- Ähnlich wie bei der PATH-Suche einer Shell wird dabei ein Befehlsname in einen vollständigen Pfad umgewandelt
- Bei Skripten mit Shebang (
#!) führt der Kernel das Programm mit dem angegebenen Interpreter aus
- Beispiel:
#!/usr/bin/python3 → Ausführung mit dem Python-Interpreter
ELF: die Struktur der ausführbaren Datei
- Ausführbare Dateien unter Linux verwenden das ELF-Format (Executable and Linkable Format)
- ELF ist ein standardisiertes Format für ausführbare Dateien, das Code, Daten, Symbole und Relocation-Informationen enthält
- Andere Betriebssysteme verwenden eigene Formate wie Mach-O (macOS) oder PE (Windows)
- Der ELF-Header enthält Informationen über die Struktur der Datei und ihre Speicheranordnung
- Beispielhafte Felder:
ELF Magic, Class, Entry point address, Program headers, Section headers
Entry point address ist die Adresse der Instruktion, mit der das Programm startet
- Im gezeigten Beispiel handelt es sich um eine ELF32-Datei für die RISC-V-Architektur, deren Entry-Point auf die Adresse
0x10358 gesetzt ist
Interne Bestandteile von ELF
- Eine ELF-Datei besteht aus mehreren Sections
.text: ausführbarer Code
.data: initialisierte globale Variablen
.bss: nicht initialisierte globale Variablen
.plt: Tabelle für Aufrufe gemeinsam genutzter Bibliotheken
.symtab, .strtab: Symbol- und String-Tabellen
- Die PLT (Procedure Linkage Table) unterstützt Aufrufe von Funktionen aus Shared Libraries
- Beispiel:
printf, malloc aus libc
- Die
PT_INTERP-Section in ELF gibt den dynamischen Linker (Interpreter) an
- Der Kernel liest ELF, ordnet ladbare Sections im Speicher an und aktiviert bei Bedarf Sicherheitsfunktionen wie ASLR und NX-Bit
Symboltabelle und Runtime-Linking
- Die Symboltabelle (
symtab) von ELF enthält Adressinformationen zu Funktionen und Variablen
- Beispielhaft gibt es Einträge wie
_start, main, __libc_start_main
- Selbst ein einfaches „Hello, World!“-Programm kann mehr als 2300 Symbole enthalten
- Das stammt größtenteils aus der Standardbibliothek und dem Runtime-Initialisierungscode
- Der Grund ist, dass
libc-Implementierungen wie musl oder glibc eingebunden sind
- Nachdem der Kernel die einzelnen ELF-Sections geladen hat, übergibt er die Kontrolle an den Interpreter (dynamischen Linker)
- Der Interpreter verarbeitet Relocations, Address Space Layout Randomization (ASLR), das Setzen von Ausführungsrechten (NX-Bit) usw.
Der Ablauf der Stack-Initialisierung
- Vor der Programmausführung muss der Kernel den Stack direkt aufbauen
- Der Stack wird für lokale Variablen, Funktions-Call-Frames und die Übergabe von Argumenten verwendet
- Die beim Aufruf von
execve übergebenen argv und envp werden auf dem Stack abgelegt
- Darüber greift das Programm auf Kommandozeilenargumente und Umgebungsvariablen zu
- Der Kernel legt außerdem den ELF Auxiliary Vector (
auxv) auf dem Stack ab
- Er enthält rund 30 Einträge, darunter Seitengröße, ELF-Metadaten und Systeminformationen
- Beispiel:
AT_PAGESZ gibt die Größe einer Speicherseite an, etwa 4 KiB
- Im Beispiel eines RISC-V-Emulators beginnt der Stack-Pointer (
sp) an einer hohen Adresse, und Argumente, Umgebungsvariablen sowie Auxiliary Vector werden in umgekehrter Reihenfolge abgelegt
Der Entry-Point und die Funktion _start
- Der Entry-Point von ELF ist auf die Adresse der Funktion
_start gesetzt
_start ist der erste User-Space-Code, an den der Kernel die Kontrolle übergibt
- Die meisten Sprachen führen in
_start zunächst eine Runtime-Initialisierung durch und rufen danach main auf
- Beispiel: Rusts
std::rt::lang_start, Cs __libc_start_main
- Im Rust-Beispiel lässt sich mit den Attributen
#![no_std] und #![no_main] _start auch direkt ohne Runtime definieren
- Innerhalb von
_start werden argc, argv und envp vom Stack gelesen und anschließend der main-Pointer aufgerufen
- Die sprachspezifische Runtime übernimmt sprachspezifische Initialisierungsschritte wie globale Konstruktoren, Thread-Local Storage oder Exception-Handling
Der vollständige Ablauf bis zum Aufruf von main()
- Der Gesamtprozess lässt sich wie folgt zusammenfassen
- Aufruf von
execve → der Kernel lädt die ELF-Datei
- Interpretation von ELF → Mapping der Code-/Daten-Sections, Festlegung des Interpreters
- Aufbau des Stacks → Speicherung von Argumenten, Umgebungsvariablen und Auxiliary Vector
- Ausführung des Entry-Points
_start
- Aufruf von
main() nach der Runtime-Initialisierung
- Diese Abfolge zeigt die Kooperationsstruktur von Betriebssystem-Kernel, ELF-Format und Sprach-Runtime
- Der reale Linux-Kernel enthält zusätzlich interne Logik für Adressräume, Prozesstabellen, Gruppenverwaltung usw., doch dieser Beitrag erklärt den Kernablauf davor
Fazit und Korrektur
- Der Ablauf vor
main() ist eine Kombination aus Initialisierung auf Kernel-Ebene und Runtime-Setup
- Selbst ein einfaches „Hello, World!“-Programm wird erst nach einer komplexen ELF-Struktur und Runtime-Initialisierung ausgeführt
- In einer früheren Version des Artikels wurde ein Teil der Section-Ladelogik dem Kernel zugeschrieben; dies wurde korrigiert, da es sich tatsächlich um die Aufgabe des ELF-Interpreters handelt
- Diese Analyse ist eine nützliche Grundlage für das Verständnis von Systemprogrammierung, Compilern und OS-Architektur
Noch keine Kommentare.