Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Écrire et charger son premier programme eBPF

Par mis à jour le

Améliorer cette page

À la fin de cette page, vous aurez vérifié que votre noyau prend eBPF en charge, chargé un vrai programme 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, pour ceux qui lisent du C, on écrit un vrai programme avec BCC et on regarde ce qui se passe sous le capot : compilation, vérificateur, chargement, accrochage. Public : administrateur à 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.

  • Vérifier que le noyau de votre machine prend eBPF en charge
  • Installer la chaîne d'outils eBPF sur une distribution récente
  • Tracer les exécutions de programmes avec une seule ligne de bpftrace
  • Identifier chaque étape : compilation en bytecode, vérification, chargement, accrochage
  • Déchiffrer un refus du vérificateur et comprendre pourquoi il protège la machine
  • 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.
  • Pour la voie 2 uniquement : savoir lire du C (une fonction, un pointeur, un if) et un peu de Python. La voie 1 n'en demande aucun.

Un noyau prend eBPF en charge quand il est compilé avec CONFIG_BPF et CONFIG_BPF_SYSCALL, et il autorise la portabilité CO-RE quand il expose son BTF. Les distributions récentes (Ubuntu 24.04, Debian 13, RHEL 10) l'activent toutes, mais une image minimale ou un noyau personnalisé peut s'en passer. Mieux vaut le vérifier avant d'installer quoi que ce soit.

Premier test, le plus parlant : le noyau expose-t-il la description de ses types (le BTF, expliqué dans la leçon CO-RE et BTF) ? Si ce fichier existe, le noyau a été compilé avec CONFIG_DEBUG_INFO_BTF.

Fenêtre de terminal
ls -l /sys/kernel/btf/vmlinux

Le fichier doit apparaître, en lecture seule, avec une taille de plusieurs mégaoctets. Un No such file or directory signifie que les outils fondés sur CO-RE ne fonctionneront pas ; bpftrace et BCC, eux, continuent de marcher tant que les options BPF sont présentes.

Deuxième test, les options de compilation du noyau, que les distributions publient dans /boot :

Fenêtre de terminal
grep -E '^CONFIG_(BPF|BPF_SYSCALL|DEBUG_INFO_BTF)=' /boot/config-$(uname -r)

Vous attendez trois lignes qui se terminent par =y : CONFIG_BPF=y, CONFIG_BPF_SYSCALL=y et CONFIG_DEBUG_INFO_BTF=y. Il manque une option ? Le fichier /boot/config-* peut aussi être absent sur certaines images (conteneur, noyau d'un fournisseur de cloud) ; dans ce cas, /proc/config.gz joue le même rôle quand il existe.

Troisième test, une fois bpftool installé (étape suivante) : il interroge le noyau lui-même plutôt que sa configuration.

Fenêtre de terminal
sudo bpftool feature probe kernel | head -20

La commande liste les types de programmes, de maps et de helpers que ce noyau accepte réellement. C'est la source la plus fiable : une option compilée ne garantit pas que le type de programme dont vous avez besoin soit disponible. Sortie réelle sur AlmaLinux 10.2 (noyau 6.12, bpftool 7.7.0) :

Scanning system configuration...
bpf() syscall restricted to privileged users (admin can change)
JIT compiler is enabled
JIT compiler hardening is enabled for unprivileged users
JIT compiler kallsyms exports are enabled for root
Global memory limit for JIT compiler for unprivileged users is 528482304 bytes
CONFIG_BPF is set to y
CONFIG_BPF_SYSCALL is set to y
CONFIG_HAVE_EBPF_JIT is set to y
CONFIG_BPF_JIT is set to y
CONFIG_BPF_JIT_ALWAYS_ON is set to y
CONFIG_DEBUG_INFO_BTF is set to y
CONFIG_DEBUG_INFO_BTF_MODULES is set to y
CONFIG_CGROUPS is set to y
CONFIG_CGROUP_BPF is set to y
CONFIG_CGROUP_NET_CLASSID is set to y
CONFIG_SOCK_CGROUP_DATA is set to y
CONFIG_BPF_EVENTS is set to y
CONFIG_KPROBE_EVENTS is set to y
CONFIG_UPROBE_EVENTS is set to y

Sur Ubuntu 24.04 (noyau 6.8, bpftool 7.4.0), les lignes sont les mêmes, sauf JIT compiler hardening is disabled à la place de JIT compiler hardening is enabled for unprivileged users. Ces vingt premières lignes ne couvrent que la configuration : les types de programmes (eBPF program_type kprobe is available, par exemple) et de helpers viennent plus bas dans la sortie complète.

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.

Fenêtre de terminal
sudo apt update
sudo apt install -y bpftrace bpfcc-tools python3-bpfcc \
linux-headers-$(uname -r) linux-tools-$(uname -r) \
clang llvm libbpf-dev

Vérifiez que les versions répondent. Sur la VM de test :

Fenêtre de terminal
bpftrace --version # bpftrace v0.20.2
clang --version # Ubuntu clang version 18.1.3
sudo bpftool version # bpftool v7.4.0

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 :

Fenêtre de terminal
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ème execve ».
  • 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/uname
bash -> /usr/bin/id
bash -> /usr/bin/sleep
bash -> /usr/bin/uname
bash -> /usr/bin/id
bash -> /usr/bin/sleep

Vous 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. La leçon bpftrace explore ce langage en détail.

Voie 2 (facultative) : écrire un vrai programme en C

Section intitulée « Voie 2 (facultative) : écrire un vrai programme en C »

Cette voie est facultative pour un administrateur. Écrire et charger soi-même un programme en C est un geste de développeur d'outils, comme la leçon CO-RE : elle demande de lire du C et de Python, et vous pouvez la sauter sans perdre le fil du module. Elle reste utile pour comprendre ce que bpftrace cache, et pour reconnaître le cycle compilation, vérification, chargement quand il apparaît dans les journaux de vos outils.

Pour la suivre, é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 python3
from 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 + charge
b.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 argument ctx (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 (voir la section sécurité).
  • 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 sur execve. 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ère execve, qui varie selon l'architecture.
  • b.trace_fields() lit une ligne depuis trace_pipe.

Lancez-le (root obligatoire) en provoquant un peu d'activité à côté :

Fenêtre de terminal
sudo python3 hello.py

Sortie 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 bash

Le 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 noyau, et chaque ligne confirme qu'il a bien été accroché.

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. bpftrace suit exactement les mêmes étapes, sans que vous les voyiez.

  1. Compilation : clang traduit le C en bytecode eBPF, un jeu d'instructions propre à la machine virtuelle du noyau (pas du code machine classique).

  2. 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é.

  3. 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.

  4. Accrochage : attach_kprobe relie le programme à la kprobe sur execve. À partir de là, chaque execve déclenche notre code.

Pendant qu'un programme BCC ou bpftrace tourne, vous pouvez lister les programmes eBPF réellement chargés avec bpftool, l'outil d'inspection livré avec le noyau :

Fenêtre de terminal
sudo bpftool prog show

Extrait 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 4096B
152: cgroup_skb name sd_fw_egress tag 6deef7357e7b4530 gpl
loaded_at 2026-07-14T11:41:50+0000 uid 0
xlated 64B jited 56B memlock 4096B

Deux 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.

Que fait le vérificateur quand il refuse un programme ?

Section intitulée « Que fait le vérificateur quand il refuse un programme ? »

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#5
1: (05) goto pc-2
...
infinite loop detected at insn 1
processed 10 insns (limit 1000000)
Failed to load BPF program 'boucle_sans_fin': Invalid argument

Trois 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 leçon 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.

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.

  • Les outils de cette page exigent root. Mesuré en lab (Ubuntu 24.04 et AlmaLinux 10.2), bpftrace refuse de démarrer pour un compte non root, même doté de CAP_BPF et CAP_PERFMON, et les outils BCC échouent faute d'accès à /sys/kernel/tracing. Depuis le noyau 5.8, la capacité CAP_BPF (associée à CAP_PERFMON pour le traçage) permet à un agent conçu pour cela de charger des programmes sans tous les pouvoirs du super-utilisateur, ce qui limite la casse s'il est compromis. Cette voie n'a pas été rejouée ici ; la leçon durcir eBPF en donne les limites.
  • bpf_trace_printk est 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.

Quand le premier programme ne démarre pas, le message d'erreur désigne presque toujours l'une de ces causes, et la dernière colonne donne le geste qui la corrige.

SymptômeCause probableSolution
Failed to compile BPF moduleEn-têtes du noyau absentsInstaller linux-headers-$(uname -r) (ou kernel-devel)
Permission denied au chargementProgramme lancé sans privilègeLancer avec sudo (les capacités seules ne suffisent pas pour bpftrace ni BCC, mesuré en lab)
infinite loop detected / back-edgeBoucle non bornéeBorner la boucle (compteur fixe) ou la supprimer
/sys/kernel/btf/vmlinux introuvableNoyau sans CONFIG_DEBUG_INFO_BTFUtiliser bpftrace ou BCC, qui n'en ont pas besoin ; éviter les outils CO-RE
Aucune ligne ne s'afficheAucun événement ne se produitProvoquer l'activité tracée (lancer des commandes)
bpftrace: command not foundPaquet non installéInstaller bpftrace (voir plus haut)
  • Avant tout, vérifier le noyau : /sys/kernel/btf/vmlinux, CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_DEBUG_INFO_BTF et bpftool feature probe.
  • 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 ; c'est la voie facultative, celle d'un développeur d'outils.
  • 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.
  • Le vérificateur refuse proprement tout programme qu'il ne peut prouver sûr, sans jamais mettre la machine en danger.
  • Charger de l'eBPF est privilégié : bpftrace et BCC exigent root, et CAP_BPF n'est une voie que pour les agents conçus pour elle.

Ce quiz porte sur le geste : vérifier un noyau, choisir entre bpftrace et BCC, et interpréter ce que le chargeur et le vérificateur répondent.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn