À la fin de cette page, vous aurez chargé un vrai programme eBPF dans le noyau et vu défiler, en direct, chaque commande lancée sur votre machine. On commence par la voie sans code (bpftrace, une ligne suffit), puis on écrit un vrai programme en C avec BCC, et on regarde ce qui se passe sous le capot : compilation, vérificateur, chargement, accrochage. Public : administrateur ou développeur à l'aise avec le terminal Linux. Aucune connaissance préalable d'eBPF n'est requise, mais lire d'abord eBPF, c'est quoi aide à situer les termes.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Installer la chaîne d'outils eBPF sur une distribution récente
- Tracer les exécutions de programmes avec une seule ligne de bpftrace
- Écrire un programme eBPF en C et le charger avec BCC
- Identifier chaque étape : compilation en bytecode, vérification, chargement, accrochage
- Lire un refus du vérificateur et comprendre pourquoi il protège la machine
Prérequis
Section intitulée « Prérequis »- Une VM Linux avec un noyau récent (5.x ou 6.x). Ici Ubuntu 24.04, noyau
6.8.0. - Un accès root ou
sudo: charger un programme eBPF est une opération privilégiée. - De quoi provoquer un peu d'activité (lancer des commandes) pour voir passer des événements.
Installer la chaîne d'outils
Section intitulée « Installer la chaîne d'outils »Trois briques suffisent pour commencer : bpftrace (traçage en une ligne), BCC (BPF Compiler Collection, pour écrire des programmes en C pilotés depuis Python) et bpftool (l'outil d'inspection officiel du noyau). On ajoute clang et les en-têtes du noyau, car BCC compile le programme C directement sur la machine cible.
sudo apt updatesudo apt install -y bpftrace bpfcc-tools python3-bpfcc \ linux-headers-$(uname -r) linux-tools-$(uname -r) \ clang llvm libbpf-devsudo dnf install -y bpftrace bcc-tools python3-bcc \ kernel-devel bpftool clang llvm libbpf-develVérifiez que les versions répondent. Sur la VM de test :
bpftrace --version # bpftrace v0.20.2clang --version # Ubuntu clang version 18.1.3sudo bpftool version # bpftool v7.4.0Voie 1 : tracer sans écrire une ligne de C
Section intitulée « Voie 1 : tracer sans écrire une ligne de C »Le moyen le plus rapide de voir eBPF à l'œuvre est bpftrace. Vous décrivez ce que vous voulez observer dans un langage court, et bpftrace se charge de produire le programme eBPF, de le vérifier, de le charger et d'afficher le résultat. Aucune compilation manuelle.
L'exemple canonique : afficher chaque fois qu'un programme en démarre un autre. En Linux, lancer un programme passe par l'appel système execve (c'est lui qui remplace le contenu d'un processus par un nouveau programme). On s'accroche au tracepoint correspondant, un point d'observation stable prévu par le noyau :
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%-16s -> %s\n", comm, str(args->filename)); }'Décortiquons la ligne :
tracepoint:syscalls:sys_enter_execve: le point de déclenchement. « À l'entrée de l'appel systèmeexecve».comm: une variable fournie par bpftrace, le nom du processus courant (comm = command, sur 16 caractères).args->filename: l'argument de l'appel, ici le chemin du programme lancé.str(...): convertit le pointeur en chaîne lisible.
Pendant que cette commande tourne, lancez quelques commandes dans un autre terminal. La sortie réelle, capturée sur la VM :
bash -> /usr/bin/unamebash -> /usr/bin/idbash -> /usr/bin/sleepbash -> /usr/bin/unamebash -> /usr/bin/idbash -> /usr/bin/sleepVous venez d'observer, depuis le noyau, chaque programme lancé sur la machine, sans modifier ni bash ni les commandes tracées. C'est toute la promesse d'eBPF, en une ligne. Le guide bpftrace explore ce langage en détail.
Voie 2 : écrire un vrai programme en C
Section intitulée « Voie 2 : écrire un vrai programme en C »bpftrace cache la mécanique. Pour la comprendre, écrivons le même genre de sonde « à la main » avec BCC. Le principe de BCC : le programme eBPF est écrit en C, embarqué dans une chaîne de caractères, et un script Python pilote sa compilation, son chargement et la lecture des résultats.
Créez le fichier hello.py :
#!/usr/bin/env python3from bcc import BPF
# Le programme eBPF, en C, embarque dans une chaine Python.programme = r"""int au_demarrage_execve(void *ctx) { char nom[16]; bpf_get_current_comm(&nom, sizeof(nom)); bpf_trace_printk("execve appele par %s\n", nom); return 0;}"""
b = BPF(text=programme) # compile + verifie + chargeb.attach_kprobe(event=b.get_syscall_fnname("execve"), # accroche a l'entree de execve fn_name="au_demarrage_execve")
print("En ecoute. Chaque commande lancee sur la machine apparait ci-dessous.\n")for _ in range(5): # on capture 5 evenements puis on sort (task, pid, cpu, flags, ts, msg) = b.trace_fields() print(f"{ts:14.6f} PID {pid:<6d} {msg.decode()}")Chaque terme du programme C mérite une explication :
int au_demarrage_execve(void *ctx): la fonction eBPF. Son argumentctx(context) est le contexte fourni par le noyau au point d'accrochage ; ici on ne s'en sert pas.bpf_get_current_comm: une fonction d'assistance (helper), une fonction que le noyau met à disposition des programmes eBPF. Celle-ci copie le nom du processus courant dans une variable.bpf_trace_printk: un autre helper, qui écrit une ligne de texte dans un canal de traçage du noyau nommétrace_pipe. C'est le « printf » du monde eBPF, pratique pour débuter (à ne pas utiliser en production, on verra pourquoi).return 0: un programme eBPF renvoie toujours une valeur ; ici elle n'a pas d'effet.
Côté Python :
b.attach_kprobe(...)accroche notre fonction à une kprobe surexecve. Une kprobe (kernel probe) est un point d'accrochage dynamique sur n'importe quelle fonction du noyau, à son entrée.get_syscall_fnname("execve")récupère le nom exact de la fonction noyau derrièreexecve, qui varie selon l'architecture.b.trace_fields()lit une ligne depuistrace_pipe.
Lancez-le (root obligatoire) en provoquant un peu d'activité à côté :
sudo python3 hello.pySortie réelle capturée sur la VM :
En ecoute. Chaque commande lancee sur la machine apparait ci-dessous.
422.733187 PID 22427 execve appele par bash 422.733882 PID 22428 execve appele par bash 422.734436 PID 22429 execve appele par bash 422.935559 PID 22430 execve appele par bash 422.936251 PID 22431 execve appele par bashLe premier nombre est l'horodatage noyau (secondes depuis le démarrage), suivi du PID (Process IDentifier, le numéro du processus) et de notre message. Vous avez écrit, compilé, chargé et exécuté un programme dans le noyau.
Ce qui vient de se passer, étape par étape
Section intitulée « Ce qui vient de se passer, étape par étape »L'appel BPF(text=programme) a enchaîné, en une fraction de seconde, tout le cycle de vie décrit dans eBPF, c'est quoi :
-
Compilation : clang traduit le C en bytecode eBPF, un jeu d'instructions propre à la machine virtuelle du noyau (pas du code machine classique).
-
Vérification : le noyau confie le bytecode au vérificateur, qui prouve que le programme est sûr avant de l'accepter. S'il échoue, rien n'est chargé.
-
Compilation JIT : une fois accepté, le bytecode est traduit en code machine natif par le compilateur JIT (Just-In-Time) du noyau, pour tourner à pleine vitesse.
-
Accrochage :
attach_kproberelie le programme à la kprobe surexecve. À partir de là, chaqueexecvedéclenche notre code.
Pendant qu'un programme BCC tourne, vous pouvez lister les programmes eBPF réellement chargés avec bpftool, l'outil d'inspection livré avec le noyau :
sudo bpftool prog showExtrait réel (le système en charge déjà plusieurs, ici ceux de systemd) :
147: cgroup_device name sd_devices tag fbee7646fdd03110 gpl loaded_at 2026-07-14T11:41:50+0000 uid 0 xlated 184B jited 103B memlock 4096B152: cgroup_skb name sd_fw_egress tag 6deef7357e7b4530 gpl loaded_at 2026-07-14T11:41:50+0000 uid 0 xlated 64B jited 56B memlock 4096BDeux champs éclairent ce qu'on vient de voir : xlated est la taille du bytecode vérifié (translated), jited celle du code machine produit par le JIT. La colonne de gauche est le type de programme (cgroup_skb, tracing, etc.), qui détermine où il peut s'accrocher.
Quand le vérificateur refuse
Section intitulée « Quand le vérificateur refuse »Le point qui inquiète les débutants, c'est le vérificateur. Voyons-le dire « non », pour constater qu'il protège sans rien casser. La règle la plus fondamentale, documentée par le noyau : un programme eBPF doit pouvoir être prouvé fini. La documentation du vérificateur le formule ainsi : « First step does DAG check to disallow loops ». Une boucle sans condition de sortie est donc rejetée d'office.
Écrivons volontairement une boucle infinie :
programme = r"""int boucle_sans_fin(void *ctx) { #pragma clang loop unroll(disable) for (;;) { bpf_ktime_get_ns(); // appel noyau : empeche la suppression du code } return 0;}"""Au chargement, le noyau refuse, et il explique précisément pourquoi. Journal réel du vérificateur :
0: (85) call bpf_ktime_get_ns#51: (05) goto pc-2...infinite loop detected at insn 1processed 10 insns (limit 1000000)
Failed to load BPF program 'boucle_sans_fin': Invalid argumentTrois choses à retenir de cette sortie. D'abord, infinite loop detected : le vérificateur a repéré le retour en arrière (goto pc-2) et refusé. Ensuite, limit 1000000 : le noyau borne l'analyse à un million d'instructions, c'est sa garantie de terminer son propre travail. Enfin, la machine n'a pas bronché : le programme n'a jamais été chargé, le noyau est intact, on récupère juste une erreur Invalid argument. Le vérificateur est un videur, pas un piège.
Ce n'est qu'une de ses règles. La page le vérificateur eBPF expliqué détaille les autres (accès mémoire bornés, pile initialisée, valeurs de map testées contre NULL) et comment lire ses messages.
Sécurité et points de vigilance
Section intitulée « Sécurité et points de vigilance »Charger un programme eBPF est une opération privilégiée, et pour de bonnes raisons : un programme mal intentionné peut observer des données sensibles. Quelques réflexes dès le premier jour.
- Ne pas exiger
rootcomplet en production. Depuis le noyau 5.8, la capacitéCAP_BPF(associée àCAP_PERFMONpour le traçage) permet de charger des programmes sans tous les pouvoirs du super-utilisateur. On limite ainsi la casse si l'outil est compromis. bpf_trace_printkest réservé au débogage. Il écrit dans un canal unique et partagé (trace_pipe) : pratique pour apprendre, inadapté dès qu'il y a du volume ou plusieurs programmes. En vrai, on transmet les données via des maps, décrites dans maps : le pont noyau / espace utilisateur.- BCC compile sur la cible. C'est simple pour apprendre, mais cela impose clang et les en-têtes du noyau sur chaque machine. La voie de production, libbpf + CO-RE, compile une fois et tourne partout ; voir BCC et libbpf.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
Failed to compile BPF module | En-têtes du noyau absents | Installer linux-headers-$(uname -r) (ou kernel-devel) |
Permission denied au chargement | Programme lancé sans privilège | Lancer avec sudo, ou accorder CAP_BPF/CAP_PERFMON |
infinite loop detected / back-edge | Boucle non bornée | Borner la boucle (compteur fixe) ou la supprimer |
| Aucune ligne ne s'affiche | Aucun événement ne se produit | Provoquer l'activité tracée (lancer des commandes) |
bpftrace: command not found | Paquet non installé | Installer bpftrace (voir plus haut) |
À retenir
Section intitulée « À retenir »- bpftrace trace en une ligne : idéal pour voir eBPF fonctionner sans écrire de C.
- BCC laisse écrire le programme en C et le piloter en Python ; il compile sur la machine cible.
- Charger un programme enchaîne compilation en bytecode, vérification, JIT, accrochage ; c'est instantané mais bien réel.
- Une kprobe s'accroche à une fonction du noyau, un tracepoint à un point d'observation stable.
- Les helpers (
bpf_get_current_comm,bpf_trace_printk...) sont les seules portes d'entrée vers le noyau offertes aux programmes. - Le vérificateur refuse proprement tout programme qu'il ne peut prouver sûr (ici, une boucle infinie), sans jamais mettre la machine en danger.
- Charger de l'eBPF est privilégié : préférer
CAP_BPFàrootcomplet en production.