1 Punkte von GN⁺ 2025-10-26 | Noch keine Kommentare. | Auf WhatsApp teilen
  • 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
    1. Aufruf von execve → der Kernel lädt die ELF-Datei
    2. Interpretation von ELF → Mapping der Code-/Daten-Sections, Festlegung des Interpreters
    3. Aufbau des Stacks → Speicherung von Argumenten, Umgebungsvariablen und Auxiliary Vector
    4. Ausführung des Entry-Points _start
    5. 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.

Noch keine Kommentare.