Preise

Architektur im Überblick

Quelldateien

Diese Seite wurde aus den folgenden Quelldateien erstellt:

Das Projekt ebpftracer implementiert einen Syscall-Tracer auf Basis von eBPF (Extended Berkeley Packet Filter), der speziell für das Tracing von Child-Prozessen konzipiert ist. Die Architektur folgt einem klassischen User-Space/Kernel-Space-Split, wobei der Kernel-Teil mittels eBPF-Programmen Syscall-Ereignisse erfasst und der User-Space-Teil für Prozessmanagement, Event-Verarbeitung und Ausgabe zuständig ist.

Komponenten der eBPF-Architektur

Die Architektur von ebpftracer gliedert sich in drei Hauptkomponenten: den Main-Prozess (src/main.c:19-103) für Prozess-Lebenszyklusmanagement, den Loader (src/loader.c:1-242) für BPF-Initialisierung und Event-Handling, sowie den Controller (src/controller.c:1-121) als Kernel-Space-eBPF-Programm.

正在加载图表渲染器...

Erklärung der Architekturkomponenten:

  1. User-Space-Komponenten: Der Main-Prozess (src/main.c:64-76) initialisiert den Loader, der wiederum das BPF-Skeleton-Objekt verwaltet (src/loader.c:23-27). Die Event-Verarbeitung erfolgt über Callbacks, die an spezifische Syscall-Handler dispatchen.

  2. Kernel-Space-Hooks: Zwei Tracepoint-Hooks überwachen raw_syscalls/sys_enter und raw_syscalls/sys_exit (src/controller.c:32-119). Diese werden bei jedem Syscall-Eintritt und -Austritt getriggert.

  3. BPF-Maps als Kommunikationskanäle: Drei Maps verbinden User- und Kernel-Space – target_pid_map für PID-Filterung (src/controller.c:11-16), events als Ring Buffer für Event-Streaming (src/controller.c:18-22), und syscalls_map für Metadaten-Lookup (src/controller.c:24-30).

  4. Synchronisationsmechanismus: Der Child-Prozess sendet SIGSTOP an sich selbst, wodurch der Parent das BPF-Programm attachen kann, bevor die Ausführung mit SIGCONT fortgesetzt wird (src/main.c:44-76).

Datenstrukturen und Event-Format

Die zentrale Datenstruktur für die Kernel-User-Kommunikation ist struct syscall_event (src/controller.h:19-39), die mittels eines Union speichereffizient zwischen Enter- und Exit-Events unterscheidet.

c
1// Kernstruktur für Event-Übertragung (controller.h:19-39)
2struct syscall_event {
3    event_mode mode;        // EVENT_SYS_ENTER oder EVENT_SYS_EXIT
4    long syscall_nr;        // Syscall-Nummer
5    
6    union {
7        struct {
8            char name[32];                    // Syscall-Name
9            int num_args;                     // Anzahl Argumente
10            unsigned long args[MAX_SYSCALL_ARGS];  // Roh-Argumente
11        } enter;
12        
13        struct {
14            long retval;                      // Rückgabewert
15        } exit;
16    };
17};

Wichtige Konstanten und Grenzen:

KonstanteWertBeschreibungQuelle
MAX_SYSCALL_NR335Maximale Syscall-Nummer auf x86_64src/controller.h:6
MAX_SYSCALL_ARGS6Maximale Argumentanzahl pro Syscallsrc/controller.h:9
Ring Buffer Größe256 KB256 * 1024 Bytes für Event-Puffersrc/controller.c:21

Die Metadaten-Struktur struct tracer_syscall_info (src/controller.h:42-45) speichert Name und Argumentanzahl für jeden Syscall-Typ. Diese wird sowohl im Kernel-Space als BPF-Map (src/controller.c:24-30) als auch im User-Space als statisches Array (src/loader.c:29-31) vorgehalten.

Union-basiertes Event-Design: Die Verwendung eines Union spart Speicher im Ring Buffer, da niemals Enter- und Exit-Daten gleichzeitig benötigt werden. Das mode-Feld dient als Diskriminator, um festzustellen, welcher Union-Zweig gültig ist (src/controller.h:21).

Tracepoint-Hooks im Kernel

sys_enter Tracepoint

Der sys_enter-Hook (src/controller.c:33-87) wird bei jedem Syscall-Eintritt aufgerufen und führt folgende Schritte aus:

  1. PID-Filterung: Zunächst wird die PID des aktuellen Prozesses ermittelt und gegen target_pid_map geprüft (src/controller.c:35-41). Bei Nichtübereinstimmung wird das Event verworfen (return -1).

  2. Syscall-Nummer-Validierung: Die Syscall-ID wird gegen MAX_SYSCALL_NR geprüft, um Out-of-Bounds-Zugriffe zu verhindern (src/controller.c:44-47).

  3. Metadaten-Lookup: Die syscalls_map liefert Name und Argumentanzahl für den Syscall (src/controller.c:49-54).

  4. Ring-Buffer-Reservierung: Speicher wird im Ring Buffer reserviert (src/controller.c:56-60). Bei Fehlschlag wird das Event still verworfen.

  5. Argumenterfassung: Bis zu 6 Argumente werden mittels BPF_CORE_READ sicher ausgelesen (src/controller.c:78-83). Das #pragma unroll sorgt für Compile-Time-Unrolling der Schleife.

c
1// Argumenterfassung mit Boundary-Check (controller.c:68-83)
2int num_args = info->num_args;
3if (num_args > MAX_SYSCALL_ARGS) {
4    num_args = MAX_SYSCALL_ARGS;  // Cap auf Maximum
5}
6if (num_args < 0) {
7    num_args = 0;  // Negative Werte abfangen
8}
9
10#pragma unroll
11for (int i = 0; i < MAX_SYSCALL_ARGS; i++) {
12    if (i < num_args) {
13        e->enter.args[i] = BPF_CORE_READ(ctx, args[i]);
14    }
15}

sys_exit Tracepoint

Der sys_exit-Hook (src/controller.c:90-119) ist schlanker, da nur der Rückgabewert erfasst wird:

  1. PID-Filterung: Identisch zum Enter-Hook (src/controller.c:93-99).

  2. Event-Erstellung: Nur mode, syscall_nr und retval werden gesetzt (src/controller.c:112-114).

  3. Submission: Das Event wird mittels bpf_ringbuf_submit an den User-Space gesendet (src/controller.c:117).

Fehlerbehandlung in Tracepoints: Beide Hooks verwenden "fail-silent"-Semantik – bei Fehlern (Ring Buffer voll, Map-Lookup fehlgeschlagen) wird -1 zurückgegeben, ohne das System zu beeinträchtigen. Dies ist typisch für eBPF-Programme, die keine Panics im Kernel verursachen dürfen.

Event-Verarbeitung im User-Space

Event-Handler-Dispatching

Der zentrale Event-Handler (src/loader.c:100-120) empfängt Events vom Ring Buffer und dispatcht sie an spezifische Handler:

c
1// Dispatch-Logik (loader.c:104-120)
2if (e->mode == EVENT_SYS_ENTER) {
3    if (e->syscall_nr < MAX_SYSCALL_NR && enter_handlers[e->syscall_nr]) {
4        enter_handlers[e->syscall_nr](g_target_pid, e);  // Spezifischer Handler
5    } else {
6        handle_sys_enter_default(g_target_pid, e);  // Fallback
7    }
8} else if (e->mode == EVENT_SYS_EXIT) {
9    if (e->syscall_nr < MAX_SYSCALL_NR && exit_handlers[e->syscall_nr]) {
10        exit_handlers[e->syscall_nr](g_target_pid, e);
11    } else {
12        handle_sys_exit_default(g_target_pid, e);
13    }
14}

Dispatch-Mechanismus: Zwei Funktionszeiger-Arrays enter_handlers und exit_handlers (indiziert nach Syscall-Nummer) ermöglichen spezialisierte Handler für bestimmte Syscalls. Falls kein Handler registriert ist, greift der Default-Handler.

Polling-Schleife

Die Polling-Funktion (src/loader.c:208-214) nutzt ring_buffer__poll mit 100ms Timeout:

c
1int bpf_loader_poll_events(void) {
2    if (!g_ring_buf) {
3        return -1;  // Guard gegen NULL-Pointer
4    }
5    return ring_buffer__poll(g_ring_buf, 100);
6}

Die Hauptloop in main.c (src/main.c:80-82) pollt so lange, bis der Child-Prozess terminiert:

c
1while (waitpid(pid, &status, WNOHANG) == 0) {
2    bpf_loader_poll_events();  // Non-blocking wait + event polling
3}

Prozess-Speicher lesen

Für bestimmte Syscalls (z.B. read, write) müssen String-Argumente aus dem Adressraum des Child-Prozesses gelesen werden. Die Hilfsfunktion read_string_from_process (src/loader.c:68-89) nutzt process_vm_readv:

c
1long read_string_from_process(pid_t pid, unsigned long addr, 
2                              char *buffer, size_t size) {
3    struct iovec local_iov = {.iov_base = buffer, .iov_len = size};
4    struct iovec remote_iov = {.iov_base = (void *)addr, .iov_len = size};
5    ssize_t bytes_read = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0);
6    
7    // Null-Terminierung sicherstellen
8    if ((size_t)bytes_read < size) {
9        buffer[bytes_read] = '\0';
10    } else {
11        buffer[size - 1] = '\0';
12    }
13    return bytes_read;
14}

Prozess-Lebenszyklus und Ressourcenmanagement

Fork-Exec-Muster mit SIGSTOP-Synchronisation

Der Main-Prozess verwendet ein ausgeklügeltes Synchronisationsmuster (src/main.c:26-53), um sicherzustellen, dass das BPF-Programm vor dem ersten Syscall des Child-Prozesses aktiv ist:

正在加载图表渲染器...

Synchronisationsdetails:

  1. Child-Seite (src/main.c:32-53): Nach fork() leitet der Child stdout/stderr nach /dev/null um, um Tracing-Rauschen zu vermeiden. Dann sendet er sich selbst SIGSTOP.

  2. Parent-Seite (src/main.c:58-76): Der Parent wartet mit waitpid(..., WUNTRACED) auf den gestoppten Child. Nach erfolgreichem BPF-Attach wird SIGCONT gesendet.

  3. Race-Condition-Vermeidung: Ohne SIGSTOP könnte der Child bereits Syscalls ausführen, bevor das BPF-Programm attacht ist. Die Synchronisation garantiert, dass kein einziger Syscall verpasst wird.

Ressourcen-Cleanup

Die Cleanup-Funktion (src/loader.c:220-242) gibt alle Ressourcen in korrekter Reihenfolge frei:

c
1void bpf_loader_cleanup(void) {
2    // 1. Ring Buffer freigeben
3    if (g_ring_buf) {
4        ring_buffer__free(g_ring_buf);
5        g_ring_buf = NULL;
6    }
7    
8    // 2. BPF Skeleton zerstören
9    if (g_skel) {
10        controller_bpf__destroy(g_skel);
11        g_skel = NULL;
12    }
13    
14    // 3. Syscall-spezifische State-Cleanup-Funktionen
15    read_common_cleanup();
16    write_common_cleanup();
17    open_common_cleanup();
18    handle_close_cleanup();
19    dup_common_cleanup();
20    fd_cache_cleanup();
21}

Cleanup-Reihenfolge: Zuerst werden aktive Datenstrukturen (Ring Buffer) freigegeben, dann das BPF-Objekt, schließlich zustandsbehaftete Handler. Dies verhindert Use-After-Free und Dangling-Pointer.

Modulabhängigkeiten und Abgrenzungen

正在加载图表渲染器...

Modul-Abgrenzungen:

ModulVerantwortungNicht-Zuständigkeit
main.cProzess-Lebenszyklus, CLI-Entry-PointBPF-Interna, Event-Formatierung
loader.cBPF-Initialisierung, Event-DispatchingTracepoint-Logik, Speicher lesen
controller.cKernel-Space-Hooks, Event-CaptureUser-Space-Logik, Prozessmanagement
controller.hGemeinsame DatenstrukturenImplementierung

Kern-Designentscheidungen

1. Ring Buffer vs. Perf Buffer

Entscheidung: Ring Buffer (BPF_MAP_TYPE_RINGBUF) wurde gewählt (src/controller.c:18-22).

Begründung: Ring Buffer bietet effizienteres Memory-Management, garantierte Event-Reihenfolge und bessere Performance bei hohen Event-Raten im Vergleich zum älteren Perf Buffer. Die 256KB-Größe ist ein Kompromiss zwischen Speicherverbrauch und Pufferkapazität für Burst-Szenarien.

2. Skeleton-basiertes Loading

Entscheidung: Verwendung von controller_bpf Skeleton (src/loader.c:25).

Begründung: Skeletons (generiert von bpftool gen skeleton) eliminieren Runtime-Relocation, vereinfachen Map-Zugriffe und bieten typsichere APIs. Die Alternative (manuelles Object-Loading) wäre fehleranfälliger und weniger performant.

3. SIGSTOP-Synchronisation

Entscheidung: Child-Prozess stoppt sich selbst vor exec (src/main.c:47).

Begründung: Dies garantiert vollständiges Tracing ab dem allerersten Syscall. Alternativen wie ptrace(PTRACE_TRACEME) würden mehr Overhead verursachen und komplexe Race-Conditions einführen.

4. Union-basiertes Event-Design

Entscheidung: syscall_event verwendet Union für Enter/Exit (src/controller.h:24-38).

Begründung: Reduziert Event-Größe von ~120 Bytes auf ~72 Bytes (bei separaten Structs). Im Ring Buffer ist jeder Byte wichtig für Throughput. Der Diskriminator mode macht das Design sicher.

5. Handler-Dispatch-Table

Entscheidung: Funktionszeiger-Arrays für Syscall-Handler (src/loader.c:108-119).

Begründung: O(1) Dispatch-Komplexität, einfache Erweiterbarkeit für neue Syscall-Handler, und vermeidet riesige Switch-Statements. Die Alternative (Hash-Map) würde mehr Overhead bedeuten.

6. BPF_CORE_READ für Argumente

Entscheidung: Verwendung von BPF_CORE_READ statt direkter Pointer-Dereferenzierung (src/controller.c:81).

Begründung: CO-RE (Compile Once, Run Everywhere) Kompatibilität. Das eBPF-Programm funktioniert across Kernel-Versionen ohne Neukompilierung, da BPF_CORE_READ Relocations für Kernel-Struktur-Offsets handhabt.

Technologie-Stack und Auswahlkriterien

TechnologieVerwendungszweckAuswahlgrundAlternative
eBPFKernel-TracingSicher, effizient, keine Kernel-Module nötigKernel Module, ptrace
libbpfBPF-User-Space-LibraryIndustriestandard, Skeleton-Supportbcc, cilium/ebpf
Ring Buffer MapsEvent-StreamingHohe Performance, garantierte OrdnungPerf Buffer, Hash Maps
TracepointsSyscall-HooksStabile Kernel-API, niedriger Overheadkprobes, ftrace
process_vm_readvSpeicher lesenSicher, kein ptrace nötig/proc/pid/mem, ptrace
SIGSTOP/SIGCONTProzess-SynchronisationEinfach, zuverlässigptrace-Kontrollfluss
MesonBuild-SystemSchnell, moderne Dependency-VerwaltungCMake, Make
SkeletonBPF-Objekt-ManagementTypsicher, Compile-Time-ChecksRuntime Object Loading

Startsequenz und Initialisierung

Die vollständige Startsequenz von ebpftracer gliedert sich in folgende Phasen:

Phase 1: Prozess-Fork

main() → fork() → Child: SIGSTOP → Parent: waitpid(WUNTRACED)

(src/main.c:26-59)

Phase 2: BPF-Initialisierung

bpf_loader_init() → controller_bpf__open() → Maps erstellen
bpf_loader_load_and_attach(pid) → controller_bpf__load() → 
controller_bpf__attach() → target_pid_map update

(src/loader.c:208-214)

Phase 3: Event-Loop

SIGCONT an Child → while(waitpid(WNOHANG) == 0) → 
    ring_buffer__poll(100ms) → callback → handler → Ausgabe

(src/main.c:76-83)

Phase 4: Cleanup

Child exit → bpf_loader_cleanup() → ring_buffer__free() → 
controller_bpf__destroy() → handler cleanup

(src/loader.c:220-242)

Bekannte Einschränkungen

  1. Plattformabhängigkeit: Das Projekt ist auf x86_64 Linux beschränkt, da MAX_SYSCALL_NR auf 335 hardcodiert ist (src/controller.h:6). Andere Architekturen (ARM, RISC-V) haben abweichende Syscall-Tabellen.

  2. Single-Process-Tracing: Nur ein Child-Prozess kann gleichzeitig getraced werden, da target_pid_map nur einen Eintrag fasst (src/controller.c:13).

  3. Keine Thread-Unterscheidung: Die PID-Filterung basiert auf TGID (Thread Group ID), sodass alle Threads eines Prozesses getraced werden (src/controller.c:35).

  4. Argumentgröße begrenzt: Nur 6 Argumente werden erfasst (src/controller.h:9). Syscalls mit mehr Parametern (selten) werden trunkiert.

  5. Ring Buffer Overflow: Bei extrem hoher Syscall-Rate können Events verworfen werden, da bpf_ringbuf_reserve fehlschlagen kann (src/controller.c:57-59). Es gibt keinen Backpressure-Mechanismus.