BPF pour les plus petits, première partie : BPF étendu
Au début, il existait une technologie appelée BPF. Nous l'avons regardée précédent, article de l'Ancien Testament de cette série. En 2013, grâce aux efforts d'Alexei Starovoitov et Daniel Borkman, une version améliorée, optimisée pour les machines modernes 64 bits, a été développée et incluse dans le noyau Linux. Cette nouvelle technologie a été brièvement appelée Internal BPF, puis renommée Extended BPF, et maintenant, après plusieurs années, tout le monde l'appelle simplement BPF.
En gros, BPF vous permet d'exécuter du code arbitraire fourni par l'utilisateur dans l'espace du noyau Linux, et la nouvelle architecture s'est avérée si réussie que nous aurons besoin d'une douzaine d'articles supplémentaires pour décrire toutes ses applications. (La seule chose que les développeurs n'ont pas bien fait, comme vous pouvez le voir dans le code de performance ci-dessous, a été de créer un logo décent.)
Cet article décrit la structure de la machine virtuelle BPF, les interfaces du noyau pour travailler avec BPF, les outils de développement, ainsi qu'un bref, très bref aperçu des capacités existantes, c'est-à-dire tout ce dont nous aurons besoin à l'avenir pour une étude plus approfondie des applications pratiques du BPF.
Résumé de l'article
Introduction à l'architecture BPF. Tout d’abord, nous allons avoir une vue d’ensemble de l’architecture BPF et décrire les principaux composants.
Gestion des objets à l'aide de l'appel système bpf. Avec une certaine compréhension du système déjà en place, nous verrons enfin comment créer et manipuler des objets depuis l'espace utilisateur à l'aide d'un appel système spécial - bpf(2).
Пишем программы BPF с помощью libbpf. Bien entendu, vous pouvez écrire des programmes à l’aide d’un appel système. Mais c'est difficile. Pour un scénario plus réaliste, les programmeurs nucléaires ont développé une bibliothèque libbpf. Nous allons créer un squelette d’application BPF de base que nous utiliserons dans les exemples suivants.
Aides du noyau. Ici, nous apprendrons comment les programmes BPF peuvent accéder aux fonctions d'assistance du noyau - un outil qui, avec les cartes, étend fondamentalement les capacités du nouveau BPF par rapport au classique.
Accès aux cartes des programmes BPF. À ce stade, nous en saurons suffisamment pour comprendre exactement comment créer des programmes utilisant des cartes. Et jetons même un coup d’œil rapide au grand et puissant vérificateur.
Outils de développement. Section d'aide sur la façon d'assembler les utilitaires et le noyau requis pour les expériences.
Conclusion. À la fin de l’article, ceux qui liront jusqu’ici trouveront des mots motivants et une brève description de ce qui se passera dans les articles suivants. Nous listerons également un certain nombre de liens d'auto-apprentissage pour ceux qui n'ont pas le désir ou la capacité d'attendre la suite.
Introduction à l'architecture BPF
Avant de commencer à considérer l’architecture BPF, nous ferons référence une dernière fois (oh) à FBP classique, qui a été développé en réponse à l'avènement des machines RISC et a résolu le problème du filtrage efficace des paquets. L'architecture s'est avérée si réussie que, née dans les fringantes années XNUMX sous Berkeley UNIX, elle a été portée sur la plupart des systèmes d'exploitation existants, a survécu jusque dans les folles années XNUMX et trouve encore de nouvelles applications.
Le nouveau BPF a été développé en réponse à l'omniprésence des machines 64 bits, des services cloud et au besoin croissant d'outils de création de SDN (Slogiciel-ddéfini nréseautage). Développé par les ingénieurs réseau du noyau pour remplacer le BPF classique, le nouveau BPF a trouvé littéralement six mois plus tard des applications dans la tâche difficile de traçage des systèmes Linux, et maintenant, six ans après son apparition, nous aurons besoin d'un prochain article complet juste pour lister les différents types de programmes.
Images drôles
À la base, BPF est une machine virtuelle sandbox qui vous permet d'exécuter du code « arbitraire » dans l'espace du noyau sans compromettre la sécurité. Les programmes BPF sont créés dans l'espace utilisateur, chargés dans le noyau et connectés à une source d'événements. Un événement peut être, par exemple, la livraison d'un paquet à une interface réseau, le lancement d'une fonction du noyau, etc. Dans le cas d'un package, le programme BPF aura accès aux données et métadonnées du package (en lecture et, éventuellement, en écriture, selon le type de programme) ; dans le cas de l'exécution d'une fonction noyau, les arguments de la fonction, y compris les pointeurs vers la mémoire du noyau, etc.
Examinons de plus près ce processus. Pour commencer, parlons de la première différence avec le BPF classique, dont les programmes ont été écrits en assembleur. Dans la nouvelle version, l'architecture a été étendue afin que les programmes puissent être écrits dans des langages de haut niveau, principalement, bien sûr, en C. Pour cela, un backend pour llvm a été développé, qui permet de générer du bytecode pour l'architecture BPF.
L'architecture BPF a été conçue, en partie, pour fonctionner efficacement sur des machines modernes. Pour que cela fonctionne en pratique, le bytecode BPF, une fois chargé dans le noyau, est traduit en code natif à l'aide d'un composant appelé compilateur JIT (Just In Tmoi). Ensuite, si vous vous en souvenez, dans le BPF classique, le programme était chargé dans le noyau et attaché à la source d'événement de manière atomique - dans le contexte d'un seul appel système. Dans la nouvelle architecture, cela se déroule en deux étapes : premièrement, le code est chargé dans le noyau à l'aide d'un appel système. bpf(2)puis, plus tard, via d'autres mécanismes qui varient selon le type de programme, le programme s'attache à la source de l'événement.
Ici, le lecteur peut se poser une question : était-ce possible ? Comment la sécurité d’exécution d’un tel code est-elle garantie ? La sécurité d'exécution nous est garantie par l'étape de chargement des programmes BPF appelée verifier (en anglais cette étape s'appelle verifier et je continuerai à utiliser le mot anglais) :
Verifier est un analyseur statique qui garantit qu'un programme ne perturbe pas le fonctionnement normal du noyau. Soit dit en passant, cela ne signifie pas que le programme ne peut pas interférer avec le fonctionnement du système - les programmes BPF, selon le type, peuvent lire et réécrire des sections de la mémoire du noyau, renvoyer les valeurs des fonctions, découper, ajouter, réécrire. et même transférer des paquets réseau. Verifier garantit que l'exécution d'un programme BPF ne fera pas planter le noyau et qu'un programme qui, selon les règles, a un accès en écriture, par exemple aux données d'un paquet sortant, ne pourra pas écraser la mémoire du noyau en dehors du paquet. Nous examinerons le vérificateur un peu plus en détail dans la section correspondante, après nous être familiarisés avec tous les autres composants de BPF.
Alors qu’avons-nous appris jusqu’à présent ? L'utilisateur écrit un programme en C, le charge dans le noyau à l'aide d'un appel système bpf(2), où il est vérifié par un vérificateur et traduit en bytecode natif. Ensuite, le même utilisateur ou un autre connecte le programme à la source d'événement et il commence à s'exécuter. La séparation du démarrage et de la connexion est nécessaire pour plusieurs raisons. Premièrement, exécuter un vérificateur est relativement coûteux et en téléchargeant plusieurs fois le même programme, nous perdons du temps informatique. Deuxièmement, la manière exacte dont un programme est connecté dépend de son type, et une interface « universelle » développée il y a un an peut ne pas convenir aux nouveaux types de programmes. (Même si maintenant que l'architecture devient plus mature, il y a une idée pour unifier cette interface au niveau libbpf.)
Le lecteur attentif remarquera peut-être que nous n’en avons pas encore fini avec les images. En effet, tout ce qui précède n’explique pas pourquoi le BPF change fondamentalement la donne par rapport au BPF classique. Deux innovations qui élargissent considérablement le champ d'application sont la possibilité d'utiliser la mémoire partagée et les fonctions d'assistance du noyau. Dans BPF, la mémoire partagée est implémentée à l'aide de ce que l'on appelle des cartes - des structures de données partagées avec une API spécifique. Ils tirent probablement ce nom du fait que le premier type de carte à apparaître était une table de hachage. Ensuite, des tableaux sont apparus, des tables de hachage locales (par CPU) et des tableaux locaux, des arbres de recherche, des cartes contenant des pointeurs vers des programmes BPF et bien plus encore. Ce qui nous intéresse maintenant, c'est que les programmes BPF ont désormais la capacité de conserver l'état entre les appels et de le partager avec d'autres programmes et avec l'espace utilisateur.
Maps est accessible à partir des processus utilisateur à l'aide d'un appel système bpf(2), et à partir de programmes BPF exécutés dans le noyau à l'aide de fonctions d'assistance. De plus, des assistants existent non seulement pour travailler avec des cartes, mais aussi pour accéder à d'autres fonctionnalités du noyau. Par exemple, les programmes BPF peuvent utiliser des fonctions d'assistance pour transférer des paquets vers d'autres interfaces, générer des événements de performances, accéder aux structures du noyau, etc.
En résumé, BPF offre la possibilité de charger du code utilisateur arbitraire, c'est-à-dire testé par un vérificateur, dans l'espace du noyau. Ce code peut sauvegarder l'état entre les appels et échanger des données avec l'espace utilisateur, et a également accès aux sous-systèmes du noyau autorisés par ce type de programme.
Ceci est déjà similaire aux capacités fournies par les modules du noyau, par rapport auxquelles BPF présente certains avantages (bien sûr, vous ne pouvez comparer que des applications similaires, par exemple le traçage du système - vous ne pouvez pas écrire un pilote arbitraire avec BPF). On peut noter un seuil d'entrée plus bas (certains utilitaires utilisant BPF ne nécessitent pas que l'utilisateur ait des compétences en programmation du noyau, ou des compétences en programmation en général), la sécurité d'exécution (levez la main dans les commentaires pour ceux qui n'ont pas cassé le système lors de l'écriture ou tester des modules), atomicité - il y a des temps d'arrêt lors du rechargement des modules, et le sous-système BPF garantit qu'aucun événement n'est manqué (pour être honnête, cela n'est pas vrai pour tous les types de programmes BPF).
La présence de telles capacités fait de BPF un outil universel d'extension du noyau, ce qui se confirme dans la pratique : de plus en plus de nouveaux types de programmes sont ajoutés à BPF, de plus en plus de grandes entreprises utilisent BPF sur des serveurs de combat 24h/7 et XNUMXj/XNUMX, de plus en plus les startups construisent leur activité sur des solutions basées sur BPF. BPF est utilisé partout : pour se protéger contre les attaques DDoS, créer un SDN (par exemple, implémenter des réseaux pour Kubernetes), comme principal outil de traçage du système et collecteur de statistiques, dans les systèmes de détection d'intrusion et les systèmes sandbox, etc.
Terminons ici la partie présentation de l'article et examinons plus en détail la machine virtuelle et l'écosystème BPF.
Digression : utilitaires
Afin de pouvoir exécuter les exemples des sections suivantes, vous aurez peut-être besoin d'un certain nombre d'utilitaires, au moins llvm/clang avec le support bpf et bpftool... Dans le chapitre Outils de développement Vous pouvez lire les instructions d'assemblage des utilitaires, ainsi que votre noyau. Cette section est placée en dessous afin de ne pas perturber l'harmonie de notre présentation.
Registres de machines virtuelles BPF et système d'instructions
L'architecture et le système de commande de BPF ont été développés en tenant compte du fait que les programmes seront écrits en langage C et, après chargement dans le noyau, traduits en code natif. Par conséquent, le nombre de registres et l’ensemble des commandes ont été choisis en tenant compte de l’intersection, au sens mathématique, des capacités des machines modernes. De plus, diverses restrictions ont été imposées aux programmes, par exemple, jusqu'à récemment, il n'était pas possible d'écrire des boucles et des sous-programmes, et le nombre d'instructions était limité à 4096 XNUMX (désormais, les programmes privilégiés peuvent charger jusqu'à un million d'instructions).
BPF dispose de onze registres 64 bits accessibles à l'utilisateur r0-r10 et un compteur de programme. Registre r10 contient un pointeur de trame et est en lecture seule. Les programmes ont accès à une pile de 512 octets au moment de l'exécution et à une quantité illimitée de mémoire partagée sous forme de cartes.
Les programmes BPF sont autorisés à exécuter un ensemble spécifique d'assistants de noyau de type programme et, plus récemment, des fonctions régulières. Chaque fonction appelée peut prendre jusqu'à cinq arguments, passés dans des registres r1-r5, et la valeur de retour est transmise à r0. Il est garanti qu'après le retour de la fonction, le contenu des registres r6-r9 ne changera pas.
Pour une traduction efficace du programme, enregistre r0-r11 car toutes les architectures prises en charge sont mappées de manière unique à des registres réels, en tenant compte des fonctionnalités ABI de l'architecture actuelle. Par exemple, pour x86_64 registres r1-r5, utilisés pour transmettre les paramètres de la fonction, sont affichés sur rdi, rsi, rdx, rcx, r8, qui sont utilisés pour transmettre des paramètres aux fonctions sur x86_64. Par exemple, le code de gauche se traduit par le code de droite comme ceci :
Le registre r0 également utilisé pour renvoyer le résultat de l'exécution du programme, et dans le registre r1 le programme reçoit un pointeur vers le contexte - selon le type de programme, il peut s'agir, par exemple, d'une structure struct xdp_md (pour XDP) ou structure struct __sk_buff (pour différents programmes réseau) ou structure struct pt_regs (pour différents types de programmes de traçage), etc.
Nous avions donc un ensemble de registres, des assistants du noyau, une pile, un pointeur de contexte et une mémoire partagée sous forme de cartes. Non pas que tout cela soit absolument nécessaire pendant le voyage, mais...
Continuons la description et parlons du système de commande pour travailler avec ces objets. Tous (presque tout) Les instructions BPF ont une taille fixe de 64 bits. Si vous regardez une instruction sur une machine Big Endian 64 bits, vous verrez
il est Code - c'est l'encodage de l'instruction, Dst/Src sont les codages du récepteur et de la source, respectivement, Off - Indentation signée de 16 bits, et Imm est un entier signé de 32 bits utilisé dans certaines instructions (similaire à la constante cBPF K). Codage Code a l'un des deux types suivants :
Les classes d'instructions 0, 1, 2, 3 définissent des commandes pour travailler avec la mémoire. Ils sont appelés, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respectivement. Classes 4, 7 (BPF_ALU, BPF_ALU64) constituent un ensemble d'instructions ALU. Classes 5, 6 (BPF_JMP, BPF_JMP32) contiennent des instructions de saut.
Le plan ultérieur d'étude du système d'instructions BPF est le suivant : au lieu de lister méticuleusement toutes les instructions et leurs paramètres, nous examinerons quelques exemples dans cette section et à partir d'eux, il deviendra clair comment les instructions fonctionnent réellement et comment désassemblez manuellement tout fichier binaire pour BPF. Pour consolider le matériel plus loin dans l'article, nous rencontrerons également des instructions individuelles dans les sections sur le vérificateur, le compilateur JIT, la traduction du BPF classique, ainsi que lors de l'étude des cartes, de l'appel de fonctions, etc.
Lorsque nous parlerons d'instructions individuelles, nous ferons référence aux fichiers de base bpf.h и bpf_common.h, qui définissent les codes numériques des instructions BPF. Lorsque vous étudiez l'architecture par vous-même et/ou analysez des binaires, vous pouvez trouver la sémantique dans les sources suivantes, classées par ordre de complexité : Spécification eBPF non officielle, Guide de référence BPF et XDP, jeu d'instructions, Documentation/réseau/filter.txt et, bien sûr, dans le code source Linux - vérificateur, JIT, interpréteur BPF.
Exemple : démonter le BPF dans votre tête
Regardons un exemple dans lequel nous compilons un programme readelf-example.c et regardez le binaire résultant. Nous dévoilerons le contenu original readelf-example.c ci-dessous, après avoir restauré sa logique à partir des codes binaires :
Les codes de commande sont égaux b7, 15, b7 и 95. Rappelons que les trois bits de poids faible constituent la classe d'instruction. Dans notre cas, le quatrième bit de toutes les instructions est vide, donc les classes d'instructions sont respectivement 7, 5, 7 et 5. La classe 7 est BPF_ALU64, et 5 est BPF_JMP. Pour les deux classes, le format des instructions est le même (voir ci-dessus) et nous pouvons réécrire notre programme comme ceci (en même temps nous réécrirons les colonnes restantes sous forme humaine) :
Op S Class Dst Src Off Imm
b 0 ALU64 0 0 0 1
1 0 JMP 0 1 1 0
b 0 ALU64 0 0 0 2
9 0 JMP 0 0 0 0
Opération b Classe ALU64 - Est BPF_MOV. Il attribue une valeur au registre de destination. Si le bit est défini s (source), alors la valeur est extraite du registre source, et si, comme dans notre cas, elle n'est pas définie, alors la valeur est extraite du champ Imm. Donc dans les première et troisième instructions nous effectuons l'opération r0 = Imm. De plus, le fonctionnement JMP classe 1 est BPF_JEQ (sauter si égal). Dans notre cas, depuis le moment S est nul, il compare la valeur du registre source avec le champ Imm. Si les valeurs coïncident, alors la transition se produit vers PC + OffOù PC, comme d'habitude, contient l'adresse de l'instruction suivante. Enfin, le fonctionnement JMP Classe 9 est BPF_EXIT. Cette instruction termine le programme et retourne au noyau r0. Ajoutons une nouvelle colonne à notre tableau :
Op S Class Dst Src Off Imm Disassm
MOV 0 ALU64 0 0 0 1 r0 = 1
JEQ 0 JMP 0 1 1 0 if (r1 == 0) goto pc+1
MOV 0 ALU64 0 0 0 2 r0 = 2
EXIT 0 JMP 0 0 0 0 exit
Nous pouvons réécrire cela sous une forme plus pratique :
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exit
Si on se souvient de ce qu'il y a dans le registre r1 le programme reçoit un pointeur vers le contexte depuis le noyau, et dans le registre r0 la valeur est renvoyée au noyau, alors on voit que si le pointeur vers le contexte est zéro, alors on renvoie 1, et sinon - 2. Vérifions que nous avons raison en regardant la source :
Oui, c'est un programme dénué de sens, mais il se traduit par seulement quatre instructions simples.
Exemple d'exception : instruction de 16 octets
Nous avons mentionné plus tôt que certaines instructions occupent plus de 64 bits. Cela s'applique par exemple aux instructions lddw (Code = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — charge un double mot des champs dans le registre Imm. Point est que Imm a une taille de 32 et un double mot fait 64 bits, donc le chargement d'une valeur immédiate de 64 bits dans un registre en une seule instruction de 64 bits ne fonctionnera pas. Pour ce faire, deux instructions adjacentes sont utilisées pour stocker la deuxième partie de la valeur 64 bits dans le champ Imm. Exemple:
Nous nous reverrons avec des instructions lddw, quand on parle de déménagements et de travail avec des cartes.
Exemple : démontage du BPF à l'aide d'outils standards
Nous avons donc appris à lire les codes binaires BPF et sommes prêts à analyser n'importe quelle instruction si nécessaire. Cependant, il convient de dire qu'en pratique, il est plus pratique et plus rapide de désassembler des programmes à l'aide d'outils standards, par exemple :
Cycle de vie des objets BPF, système de fichiers bpffs
(J'ai d'abord appris certains des détails décrits dans cette sous-section grâce à le jeûne Alexeï Starovoitov dans Blogue BPF.)
Les objets BPF - programmes et cartes - sont créés à partir de l'espace utilisateur à l'aide de commandes BPF_PROG_LOAD и BPF_MAP_CREATE appel système bpf(2), nous parlerons exactement de la façon dont cela se produit dans la section suivante. Cela crée des structures de données du noyau et pour chacune d'elles refcount (compte de références) est défini sur un et un descripteur de fichier pointant vers l'objet est renvoyé à l'utilisateur. Une fois la poignée fermée refcount l'objet est réduit de un, et lorsqu'il atteint zéro, l'objet est détruit.
Si le programme utilise des cartes, alors refcount ces cartes sont augmentées de un après le chargement du programme, c'est-à-dire leurs descripteurs de fichiers peuvent être fermés du processus utilisateur et toujours refcount ne deviendra pas nul :
Après avoir chargé avec succès un programme, nous l'attachons généralement à une sorte de générateur d'événements. Par exemple, nous pouvons le mettre sur une interface réseau pour traiter les paquets entrants ou le connecter à certains tracepoint dans le noyau. À ce stade, le compteur de références augmentera également de un et nous pourrons fermer le descripteur de fichier dans le programme de chargement.
Que se passe-t-il si nous arrêtons maintenant le chargeur de démarrage ? Cela dépend du type de générateur d'événements (hook). Tous les hooks réseau existeront une fois le chargeur terminé, ce sont ce qu'on appelle les hooks globaux. Et, par exemple, les programmes de trace seront publiés après la fin du processus qui les a créés (et sont donc appelés locaux, de « local au processus »). Techniquement, les hooks locaux ont toujours un descripteur de fichier correspondant dans l'espace utilisateur et se ferment donc lorsque le processus est fermé, mais pas les hooks globaux. Dans la figure suivante, à l'aide de croix rouges, j'essaie de montrer comment la fin du programme de chargement affecte la durée de vie des objets dans le cas de hooks locaux et globaux.
Pourquoi y a-t-il une distinction entre les hooks locaux et globaux ? L'exécution de certains types de programmes réseau a du sens sans espace utilisateur, par exemple, imaginez une protection DDoS - le chargeur de démarrage écrit les règles et connecte le programme BPF à l'interface réseau, après quoi le chargeur de démarrage peut se suicider. D'un autre côté, imaginez un programme de trace de débogage que vous avez écrit à genoux en dix minutes - une fois terminé, vous aimeriez qu'il ne reste plus de déchets dans le système, et les hooks locaux s'en assureront.
D'un autre côté, imaginez que vous souhaitiez vous connecter à un point de trace dans le noyau et collecter des statistiques sur plusieurs années. Dans ce cas, vous souhaiterez compléter la partie utilisateur et revenir aux statistiques de temps en temps. Le système de fichiers bpf offre cette opportunité. Il s'agit d'un système de pseudo-fichiers en mémoire uniquement qui permet la création de fichiers faisant référence à des objets BPF et augmentant ainsi refcount objets. Après cela, le chargeur peut se fermer et les objets qu'il a créés resteront vivants.
La création de fichiers dans des bpff faisant référence à des objets BPF est appelée « épinglage » (comme dans la phrase suivante : « un processus peut épingler un programme ou une carte BPF »). La création d'objets fichier pour les objets BPF a du sens non seulement pour prolonger la durée de vie des objets locaux, mais aussi pour la convivialité des objets globaux - en revenant à l'exemple du programme global de protection DDoS, nous voulons pouvoir venir consulter les statistiques de temps en temps.
Le système de fichiers BPF est généralement monté dans /sys/fs/bpf, mais il peut aussi être monté localement, par exemple, comme ceci :
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
Les noms de système de fichiers sont créés à l'aide de la commande BPF_OBJ_PIN Appel système BPF. Pour illustrer, prenons un programme, compilons-le, téléchargeons-le et épinglons-le sur bpffs. Notre programme ne fait rien d'utile, nous présentons seulement le code pour que vous puissiez reproduire l'exemple :
Téléchargeons maintenant notre programme à l'aide de l'utilitaire bpftool et regardez les appels système qui l'accompagnent bpf(2) (quelques lignes non pertinentes supprimées de la sortie strace) :
Ici, nous avons chargé le programme en utilisant BPF_PROG_LOAD, a reçu un descripteur de fichier du noyau 3 et en utilisant la commande BPF_OBJ_PIN épinglé ce descripteur de fichier en tant que fichier "bpf-mountpoint/test". Après cela, le programme du chargeur de démarrage bpftool a fini de fonctionner, mais notre programme est resté dans le noyau, même si nous ne l'avons attaché à aucune interface réseau :
$ sudo bpftool prog | tail -3
783: xdp name test tag 5c8ba0cf164cb46c gpl
loaded_at 2020-05-05T13:27:08+0000 uid 0
xlated 24B jited 41B memlock 4096B
Nous pouvons supprimer l'objet fichier normalement unlink(2) et après cela, le programme correspondant sera supprimé :
$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory
Supprimer des objets
En parlant de suppression d'objets, il est nécessaire de préciser qu'après avoir déconnecté le programme du hook (générateur d'événements), aucun nouvel événement ne déclenchera son lancement, cependant, toutes les instances actuelles du programme seront terminées dans l'ordre normal. .
Certains types de programmes BPF vous permettent de remplacer le programme à la volée, c'est-à-dire fournir une atomicité de séquence replace = detach old program, attach new program. Dans ce cas, toutes les instances actives de l'ancienne version du programme termineront leur travail et de nouveaux gestionnaires d'événements seront créés à partir du nouveau programme, et « atomicité » signifie ici qu'aucun événement ne sera manqué.
Attacher des programmes à des sources d'événements
Dans cet article, nous ne décrirons pas séparément la connexion de programmes à des sources d'événements, car il est logique d'étudier cela dans le contexte d'un type spécifique de programme. Cm. exemple ci-dessous, dans lequel nous montrons comment des programmes comme XDP sont connectés.
Manipulation d'objets à l'aide de l'appel système bpf
Programmes BPF
Tous les objets BPF sont créés et gérés depuis l'espace utilisateur à l'aide d'un appel système bpf, ayant le prototype suivant :
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
Voici l'équipe cmd est une des valeurs de type enum bpf_cmd, attr — un pointeur vers les paramètres d'un programme spécifique et size — taille de l'objet en fonction du pointeur, c'est-à-dire habituellement ceci sizeof(*attr). Dans le noyau 5.8, l'appel système bpf prend en charge 34 commandes différentes, et déterminationunion bpf_attr occupe 200 lignes. Mais cela ne devrait pas nous intimider, puisque nous nous familiariserons avec les commandes et les paramètres au fil de plusieurs articles.
Commençons par l'équipe BPF_PROG_LOAD, qui crée des programmes BPF - prend un ensemble d'instructions BPF et le charge dans le noyau. Au moment du chargement, le vérificateur est lancé, puis le compilateur JIT et, après une exécution réussie, le descripteur de fichier programme est renvoyé à l'utilisateur. Nous avons vu ce qui lui arrive ensuite dans la section précédente sur le cycle de vie des objets BPF.
Nous allons maintenant écrire un programme personnalisé qui chargera un simple programme BPF, mais nous devons d'abord décider quel type de programme nous voulons charger - nous devrons sélectionner Тип et dans le cadre de ce type, écrivez un programme qui passera le test du vérificateur. Cependant, afin de ne pas compliquer le processus, voici une solution toute faite : nous prendrons un programme comme BPF_PROG_TYPE_XDP, qui renverra la valeur XDP_PASS (ignorer tous les forfaits). En assembleur BPF, cela semble très simple :
r0 = 2
exit
Après avoir décidé que nous allons télécharger, nous pouvons vous dire comment nous allons procéder :
Les événements intéressants dans un programme commencent par la définition d'un tableau insns - notre programme BPF en code machine. Dans ce cas, chaque instruction du programme BPF est emballée dans la structure bpf_insn. Premier élément insns est conforme aux instructions r0 = 2, la deuxième - exit.
Retraite. Le noyau définit des macros plus pratiques pour écrire des codes machine et utiliser le fichier d'en-tête du noyau tools/include/linux/filter.h nous pourrions écrire
Mais comme l'écriture de programmes BPF en code natif n'est nécessaire que pour écrire des tests dans le noyau et des articles sur BPF, l'absence de ces macros ne complique pas vraiment la vie du développeur.
Après avoir défini le programme BPF, nous passons à son chargement dans le noyau. Notre ensemble minimaliste de paramètres attr comprend le type de programme, l'ensemble et le nombre d'instructions, la licence requise et le nom "woo", que nous utilisons pour trouver notre programme sur le système après le téléchargement. Le programme, comme promis, est chargé dans le système à l'aide d'un appel système bpf.
A la fin du programme on se retrouve dans une boucle infinie qui simule la charge utile. Sans cela, le programme sera tué par le noyau lorsque le descripteur de fichier que l'appel système nous a renvoyé sera fermé. bpf, et nous ne le verrons pas dans le système.
Eh bien, nous sommes prêts pour les tests. Assemblons et exécutons le programme sous stracepour vérifier que tout fonctionne comme il se doit :
Tout va bien, bpf(2) nous a rendu la poignée 3 et nous sommes entrés dans une boucle infinie avec pause(). Essayons de trouver notre programme dans le système. Pour ce faire, nous allons aller sur un autre terminal et utiliser l'utilitaire bpftool:
Nous voyons qu'il y a un programme chargé sur le système woo dont l'ID global est 390 et est actuellement en cours simple-prog il y a un descripteur de fichier ouvert pointant vers le programme (et si simple-prog je finirai le travail, alors woo disparaîtra). Comme prévu, le programme woo prend 16 octets - deux instructions - de codes binaires dans l'architecture BPF, mais dans sa forme native (x86_64) cela fait déjà 40 octets. Regardons notre programme dans sa forme originale :
pas de surprises. Regardons maintenant le code généré par le compilateur JIT :
# bpftool prog dump jited id 390
bpf_prog_3b185187f1855c4c_woo:
0: nopl 0x0(%rax,%rax,1)
5: push %rbp
6: mov %rsp,%rbp
9: sub $0x0,%rsp
10: push %rbx
11: push %r13
13: push %r14
15: push %r15
17: pushq $0x0
19: mov $0x2,%eax
1e: pop %rbx
1f: pop %r15
21: pop %r14
23: pop %r13
25: pop %rbx
26: leaveq
27: retq
pas très efficace pour exit(2), mais en toute honnêteté, notre programme est trop simple, et pour les programmes non triviaux, le prologue et l'épilogue ajoutés par le compilateur JIT sont bien sûr nécessaires.
Map
Les programmes BPF peuvent utiliser des zones de mémoire structurées accessibles à la fois aux autres programmes BPF et aux programmes de l'espace utilisateur. Ces objets sont appelés cartes et dans cette section nous montrerons comment les manipuler à l'aide d'un appel système. bpf.
Disons tout de suite que les capacités des cartes ne se limitent pas uniquement à l'accès à la mémoire partagée. Il existe des cartes spéciales contenant, par exemple, des pointeurs vers des programmes BPF ou des pointeurs vers des interfaces réseau, des cartes pour travailler avec des événements de performances, etc. Nous n’en parlerons pas ici, afin de ne pas embrouiller le lecteur. En dehors de cela, nous ignorons les problèmes de synchronisation, car cela n’est pas important pour nos exemples. Une liste complète des types de cartes disponibles peut être trouvée dans <linux/bpf.h>, et dans cette section nous prendrons comme exemple le premier type historiquement, la table de hachage BPF_MAP_TYPE_HASH.
Si vous créez une table de hachage, par exemple en C++, vous diriez unordered_map<int,long> woo, qui signifie en russe « J'ai besoin d'une table woo taille illimitée, dont les clés sont de type int, et les valeurs sont du type long" Afin de créer une table de hachage BPF, nous devons faire à peu près la même chose, sauf que nous devons spécifier la taille maximale de la table, et au lieu de spécifier les types de clés et de valeurs, nous devons spécifier leurs tailles en octets. . Pour créer des cartes, utilisez la commande BPF_MAP_CREATE appel système bpf. Regardons un programme plus ou moins minimal qui crée une carte. Après le programme précédent qui charge les programmes BPF, celui-ci devrait vous paraître simple :
Ici, nous définissons un ensemble de paramètres attr, dans lequel nous disons « J'ai besoin d'une table de hachage avec des clés et des valeurs de taille sizeof(int), dans lequel je peux mettre un maximum de quatre éléments." Lors de la création de cartes BPF, vous pouvez spécifier d'autres paramètres, par exemple, de la même manière que dans l'exemple avec le programme, nous avons spécifié le nom de l'objet comme "woo".
Voici l'appel système bpf(2) nous a renvoyé le numéro de la carte descriptive 3 puis le programme, comme prévu, attend d'autres instructions dans l'appel système pause(2).
Envoyons maintenant notre programme en arrière-plan ou ouvrons un autre terminal et regardons notre objet à l'aide de l'utilitaire bpftool (on peut distinguer notre carte des autres par son nom) :
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...
Le nombre 114 est l'identifiant global de notre objet. N'importe quel programme du système peut utiliser cet ID pour ouvrir une carte existante à l'aide de la commande BPF_MAP_GET_FD_BY_ID appel système bpf.
Nous pouvons maintenant jouer avec notre table de hachage. Regardons son contenu :
$ sudo bpftool map dump id 114
Found 0 elements
Vide. Mettons-y une valeur hash[1] = 1:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0
Regardons à nouveau le tableau :
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 element
Hourra! Nous avons réussi à ajouter un élément. Notez que nous devons travailler au niveau des octets pour ce faire, puisque bptftool ne sait pas de quel type sont les valeurs de la table de hachage. (Ces connaissances peuvent lui être transférées en utilisant BTF, mais nous en parlerons plus à ce sujet maintenant.)
Comment bpftool lit-il et ajoute-t-il exactement des éléments ? Jetons un coup d'oeil sous le capot :
Nous avons d'abord ouvert la carte par son identifiant global à l'aide de la commande BPF_MAP_GET_FD_BY_ID и bpf(2) nous a renvoyé le descripteur 3. En utilisant ensuite la commande BPF_MAP_GET_NEXT_KEY nous avons trouvé la première clé du tableau en passant NULL comme pointeur vers la clé "précédente". Si nous avons la clé, nous pouvons le faire BPF_MAP_LOOKUP_ELEMqui renvoie une valeur à un pointeur value. L'étape suivante consiste à essayer de trouver l'élément suivant en passant un pointeur vers la clé actuelle, mais notre table ne contient qu'un seul élément et la commande BPF_MAP_GET_NEXT_KEY retourne ENOENT.
D'accord, modifions la valeur par la clé 1, disons que notre logique métier nécessite un enregistrement hash[1] = 2:
Comme prévu, c'est très simple : la commande BPF_MAP_GET_FD_BY_ID ouvre notre carte par ID, et la commande BPF_MAP_UPDATE_ELEM écrase l'élément.
Ainsi, après avoir créé une table de hachage à partir d’un programme, nous pouvons lire et écrire son contenu à partir d’un autre. Notez que si nous pouvions le faire à partir de la ligne de commande, alors n'importe quel autre programme du système peut le faire. En plus des commandes décrites ci-dessus, pour travailler avec des cartes depuis l'espace utilisateur, Ce qui suit:
BPF_MAP_LOOKUP_ELEM: trouver la valeur par clé
BPF_MAP_UPDATE_ELEM: mettre à jour/créer de la valeur
BPF_MAP_DELETE_ELEM: retirer la clé
BPF_MAP_GET_NEXT_KEY: trouver la clé suivante (ou première)
BPF_MAP_GET_NEXT_ID: permet de parcourir toutes les cartes existantes, c'est comme ça que ça marche bpftool map
BPF_MAP_GET_FD_BY_ID: ouvre une carte existante par son identifiant global
BPF_MAP_LOOKUP_AND_DELETE_ELEM: met à jour atomiquement la valeur d'un objet et renvoie l'ancienne
BPF_MAP_FREEZE: rendre la carte immuable depuis l'espace utilisateur (cette opération ne peut pas être annulée)
BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: opérations de masse. Par exemple, BPF_MAP_LOOKUP_AND_DELETE_BATCH - c'est le seul moyen fiable de lire et de réinitialiser toutes les valeurs de la carte
Toutes ces commandes ne fonctionnent pas pour tous les types de cartes, mais en général, travailler avec d'autres types de cartes à partir de l'espace utilisateur ressemble exactement à travailler avec des tables de hachage.
Par souci d'ordre, terminons nos expériences sur les tables de hachage. Vous souvenez-vous que nous avons créé une table pouvant contenir jusqu'à quatre clés ? Ajoutons quelques éléments supplémentaires :
$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long
Comme prévu, nous n’avons pas réussi. Examinons l'erreur plus en détail :
$ sudo strace -e bpf bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=80, info=0x7ffe6c626da0}}, 120) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x56049ded5260, value=0x56049ded5280, flags=BPF_ANY}, 120) = -1 E2BIG (Argument list too long)
Error: update failed: Argument list too long
+++ exited with 255 +++
Tout va bien : comme prévu, l'équipe BPF_MAP_UPDATE_ELEM essaie de créer une nouvelle cinquième clé, mais plante E2BIG.
Ainsi, nous pouvons créer et charger des programmes BPF, ainsi que créer et gérer des cartes depuis l'espace utilisateur. Il est maintenant logique de voir comment nous pouvons utiliser les cartes des programmes BPF eux-mêmes. Nous pourrions en parler dans le langage des programmes difficiles à lire dans les codes de macro machine, mais en fait le moment est venu de montrer comment les programmes BPF sont réellement écrits et maintenus - en utilisant libbpf.
(Pour les lecteurs insatisfaits de l'absence d'exemple de bas niveau : nous analyserons en détail les programmes qui utilisent des cartes et des fonctions d'assistance créées à l'aide de libbpf et vous dire ce qui se passe au niveau de l'instruction. Pour les lecteurs insatisfaits beaucoup, nous avons ajouté exemple à l'endroit approprié dans l'article.)
Écrire des programmes BPF avec libbpf
Écrire des programmes BPF à l’aide de codes machine ne peut être intéressant que la première fois, puis la satiété s’installe. À ce moment, vous devez porter votre attention sur llvm, qui dispose d'un backend pour générer du code pour l'architecture BPF, ainsi que d'une bibliothèque libbpf, qui permet d'écrire le côté utilisateur des applications BPF et de charger le code des programmes BPF générés à l'aide de llvm/clang.
En fait, comme nous le verrons dans cet article et dans les suivants, libbpf fait beaucoup de travail sans lui (ou des outils similaires - iproute2, libbcc, libbpf-go, etc.) il est impossible de vivre. L'une des caractéristiques phares du projet libbpf est BPF CO-RE (Compile Once, Run Everywhere) - un projet qui vous permet d'écrire des programmes BPF portables d'un noyau à un autre, avec la possibilité de s'exécuter sur différentes API (par exemple, lorsque la structure du noyau change de version à la version). Afin de pouvoir travailler avec CO-RE, votre noyau doit être compilé avec le support BTF (nous décrivons comment faire cela dans la section Outils de développement. Vous pouvez vérifier si votre noyau est construit avec BTF ou pas très simplement - par la présence du fichier suivant :
Ce fichier stocke des informations sur tous les types de données utilisés dans le noyau et est utilisé dans tous nos exemples utilisant libbpf. Nous parlerons en détail de CO-RE dans le prochain article, mais dans celui-ci, construisez-vous simplement un noyau avec CONFIG_DEBUG_INFO_BTF.
bibliothèque libbpf vit directement dans l'annuaire tools/lib/bpf le noyau et son développement s'effectue via la liste de diffusion [email protected]. Cependant, un référentiel séparé est maintenu pour les besoins des applications vivant en dehors du noyau. https://github.com/libbpf/libbpf dans lequel la bibliothèque du noyau est mise en miroir pour un accès en lecture plus ou moins telle quelle.
Dans cette section, nous verrons comment créer un projet qui utilise libbpf, écrivons plusieurs programmes de test (plus ou moins dénués de sens) et analysons en détail comment tout cela fonctionne. Cela nous permettra d'expliquer plus facilement dans les sections suivantes exactement comment les programmes BPF interagissent avec les cartes, les assistants du noyau, les BTF, etc.
Généralement, les projets utilisent libbpf ajoutez un dépôt GitHub en tant que sous-module git, nous ferons de même :
Notre prochain plan dans cette section est le suivant : nous allons écrire un programme BPF comme BPF_PROG_TYPE_XDP, le même que dans l'exemple précédent, mais en C, on le compile en utilisant clang, et écrivez un programme d'assistance qui le chargera dans le noyau. Dans les sections suivantes, nous développerons les capacités du programme BPF et du programme assistant.
Exemple : créer une application à part entière à l'aide de libbpf
Pour commencer, nous utilisons le fichier /sys/kernel/btf/vmlinux, qui a été mentionné ci-dessus, et créez son équivalent sous la forme d'un fichier d'en-tête :
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Ce fichier stockera toutes les structures de données disponibles dans notre noyau, par exemple, voici comment l'en-tête IPv4 est défini dans le noyau :
Même si notre programme s’est avéré très simple, nous devons néanmoins prêter attention à de nombreux détails. Tout d'abord, le premier fichier d'en-tête que nous incluons est vmlinux.h, que nous venons de générer en utilisant bpftool btf dump - désormais, nous n'avons plus besoin d'installer le package kernel-headers pour découvrir à quoi ressemblent les structures du noyau. Le fichier d'en-tête suivant nous vient de la bibliothèque libbpf. Il ne nous reste plus qu'à définir la macro SEC, qui envoie le caractère à la section appropriée du fichier objet ELF. Notre programme est contenu dans la section xdp/simple, où avant la barre oblique nous définissons le type de programme BPF - c'est la convention utilisée dans libbpf, en fonction du nom de la section, il remplacera le type correct au démarrage bpf(2). Le programme BPF lui-même est C - très simple et se compose d'une seule ligne return XDP_PASS. Enfin, une section distincte "license" contient le nom de la licence.
Nous pouvons compiler notre programme en utilisant llvm/clang, version >= 10.0.0, ou mieux encore, supérieure (voir section Outils de développement):
Parmi les fonctionnalités intéressantes : nous indiquons l’architecture cible -target bpf et le chemin vers les en-têtes libbpf, que nous avons récemment installé. N'oubliez pas non plus -O2, sans cette option, vous pourriez avoir des surprises à l'avenir. Regardons notre code, avons-nous réussi à écrire le programme que nous voulions ?
Oui, ça a marché ! Maintenant, nous avons un fichier binaire avec le programme et nous voulons créer une application qui le chargera dans le noyau. A cet effet, la bibliothèque libbpf nous offre deux options : utiliser une API de niveau inférieur ou une API de niveau supérieur. Nous emprunterons la deuxième voie, car nous voulons apprendre à écrire, charger et connecter des programmes BPF avec un minimum d'effort pour leur étude ultérieure.
Tout d’abord, nous devons générer le « squelette » de notre programme à partir de son binaire en utilisant le même utilitaire bpftool — le couteau suisse du monde BPF (que l'on peut prendre au pied de la lettre, puisque Daniel Borkman, l'un des créateurs et mainteneurs de BPF, est suisse) :
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
En fichier xdp-simple.skel.h contient le code binaire de notre programme et les fonctions de gestion - chargement, attachement, suppression de notre objet. Dans notre cas simple, cela semble exagéré, mais cela fonctionne également dans le cas où le fichier objet contient de nombreux programmes et cartes BPF et pour charger cet ELF géant, il nous suffit de générer le squelette et d'appeler une ou deux fonctions depuis l'application personnalisée que nous avons. écrivent Passons à autre chose maintenant.
À proprement parler, notre programme de chargement est trivial :
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"
int main(int argc, char **argv)
{
struct xdp_simple_bpf *obj;
obj = xdp_simple_bpf__open_and_load();
if (!obj)
err(1, "failed to open and/or load BPF objectn");
pause();
xdp_simple_bpf__destroy(obj);
}
il est struct xdp_simple_bpf défini dans le fichier xdp-simple.skel.h et décrit notre fichier objet :
On peut voir ici les traces d'une API de bas niveau : la structure struct bpf_program *simple и struct bpf_link *simple. La première structure décrit spécifiquement notre programme, écrite dans la section xdp/simple, et le second décrit comment le programme se connecte à la source de l'événement.
Fonction xdp_simple_bpf__open_and_load, ouvre un objet ELF, l'analyse, crée toutes les structures et sous-structures (outre le programme, ELF contient également d'autres sections - données, données en lecture seule, informations de débogage, licence, etc.), puis le charge dans le noyau à l'aide d'un système appel bpf, que nous pouvons vérifier en compilant et en exécutant le programme :
Regardons maintenant notre programme utilisant bpftool. Trouvons sa carte d'identité :
# bpftool p | grep -A4 simple
463: xdp name simple tag 3b185187f1855c4c gpl
loaded_at 2020-08-01T01:59:49+0000 uid 0
xlated 16B jited 40B memlock 4096B
btf_id 185
pids xdp-simple(16498)
et dump (nous utilisons une forme abrégée de la commande bpftool prog dump xlated):
# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
0: (b7) r0 = 2
1: (95) exit
Quelque chose de nouveau! Le programme a imprimé des morceaux de notre fichier source C. Cela a été fait par la bibliothèque libbpf, qui a trouvé la section de débogage dans le binaire, l'a compilé dans un objet BTF, l'a chargé dans le noyau en utilisant BPF_BTF_LOAD, puis a spécifié le descripteur de fichier résultant lors du chargement du programme avec la commande BPG_PROG_LOAD.
Assistants du noyau
Les programmes BPF peuvent exécuter des fonctions « externes » – des assistants du noyau. Ces fonctions d'assistance permettent aux programmes BPF d'accéder aux structures du noyau, de gérer les cartes et également de communiquer avec le « monde réel » - créer des événements de performances, contrôler le matériel (par exemple, rediriger les paquets), etc.
Exemple : bpf_get_smp_processor_id
Dans le cadre du paradigme « apprendre par l’exemple », considérons l’une des fonctions d’assistance, bpf_get_smp_processor_id(), certain dans le fichier kernel/bpf/helpers.c. Il renvoie le numéro du processeur sur lequel s'exécute le programme BPF qui l'a appelé. Mais nous ne sommes pas tant intéressés par sa sémantique que par le fait que sa mise en œuvre tient sur une seule ligne :
Les définitions des fonctions d'assistance BPF sont similaires aux définitions des appels système Linux. Ici, par exemple, une fonction est définie sans argument. (Une fonction qui prend, disons, trois arguments est définie à l'aide de la macro BPF_CALL_3. Le nombre maximum d'arguments est de cinq.) Cependant, il ne s'agit que de la première partie de la définition. La deuxième partie consiste à définir la structure du type struct bpf_func_proto, qui contient une description de la fonction d'assistance que le vérificateur comprend :
Pour que les programmes BPF d'un type particulier puissent utiliser cette fonction, ils doivent l'enregistrer, par exemple pour le type BPF_PROG_TYPE_XDP une fonction est définie dans le noyau xdp_func_proto, qui détermine à partir de l'ID de la fonction d'assistance si XDP prend en charge cette fonction ou non. Notre fonction est soutient le:
Les nouveaux types de programmes BPF sont "définis" dans le fichier include/linux/bpf_types.h en utilisant une macro BPF_PROG_TYPE. Défini entre guillemets car il s'agit d'une définition logique, et en termes de langage C, la définition de tout un ensemble de structures concrètes se produit à d'autres endroits. En particulier, dans le dossier kernel/bpf/verifier.c toutes les définitions du fichier bpf_types.h sont utilisés pour créer un ensemble de structures bpf_verifier_ops[]:
Autrement dit, pour chaque type de programme BPF, un pointeur vers une structure de données du type est défini struct bpf_verifier_ops, qui est initialisé avec la valeur _name ## _verifier_ops, c'est à dire., xdp_verifier_ops pour xdp. Structure xdp_verifier_opsdéterminé par dans le fichier net/core/filter.c comme suit:
Ici, nous voyons notre fonction familière xdp_func_proto, qui exécutera le vérificateur chaque fois qu'il rencontrera un défi certains fonctions à l'intérieur d'un programme BPF, voir verifier.c.
Voyons comment un programme BPF hypothétique utilise la fonction bpf_get_smp_processor_id. Pour ce faire, nous réécrivons le programme de notre section précédente comme suit :
c'est bpf_get_smp_processor_id est un pointeur de fonction dont la valeur est 8, où 8 est la valeur BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, qui nous est défini dans le fichier vmlinux.h (déposer bpf_helper_defs.h dans le noyau est généré par un script, donc les nombres « magiques » sont ok). Cette fonction ne prend aucun argument et renvoie une valeur de type __u32. Lorsque nous l'exécutons dans notre programme, clang génère une instruction BPF_CALL "le bon genre" Compilons le programme et regardons la section xdp/simple:
Dans la première ligne, nous voyons des instructions call, paramètre IMM qui est égal à 8, et SRC_REG - zéro. Selon l'accord ABI utilisé par le vérificateur, il s'agit d'un appel à la fonction d'assistance numéro huit. Une fois lancé, la logique est simple. Valeur de retour du registre r0 copié vers r1 et aux lignes 2,3, il est converti en type u32 — les 32 bits supérieurs sont effacés. Aux lignes 4,5,6,7 on retourne 2 (XDP_PASS) ou 1 (XDP_DROP) selon que la fonction d'assistance de la ligne 0 a renvoyé une valeur nulle ou non nulle.
Testons-nous : chargez le programme et regardez le résultat bpftool prog dump xlated:
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914
$ sudo bpftool p | grep simple
523: xdp name simple tag 44c38a10c657e1b0 gpl
pids xdp-simple(10915)
$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
0: (85) call bpf_get_smp_processor_id#114128
1: (bf) r1 = r0
2: (67) r1 <<= 32
3: (77) r1 >>= 32
4: (b7) r0 = 2
; }
5: (15) if r1 == 0x0 goto pc+1
6: (b7) r0 = 1
7: (95) exit
Ok, le vérificateur a trouvé le bon kernel-helper.
Exemple : passer des arguments et enfin exécuter le programme !
Toutes les fonctions d'assistance au niveau de l'exécution ont un prototype
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
Les paramètres des fonctions d'assistance sont transmis dans des registres r1-r5, et la valeur est renvoyée dans le registre r0. Aucune fonction ne prend plus de cinq arguments et leur prise en charge ne devrait pas être ajoutée à l'avenir.
Jetons un coup d'œil au nouvel assistant du noyau et à la manière dont BPF transmet les paramètres. Réécrivons xdp-simple.bpf.c comme suit (le reste des lignes n'a pas changé) :
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}
Notre programme imprime le numéro du CPU sur lequel il fonctionne. Compilons-le et regardons le code :
Aux lignes 0 à 7, nous écrivons la chaîne running on CPU%un, puis à la ligne 8, nous exécutons le système familier bpf_get_smp_processor_id. Aux lignes 9 à 12, nous préparons les arguments d'assistance bpf_printk - les registres r1, r2, r3. Pourquoi y en a-t-il trois et non deux ? Parce que bpf_printk - c'est un wrapper de macro autour du véritable assistant bpf_trace_printk, qui doit transmettre la taille de la chaîne de format.
Ajoutons maintenant quelques lignes à xdp-simple.cpour que notre programme se connecte à l'interface lo et vraiment commencé !
Ici, nous utilisons la fonction bpf_set_link_xdp_fd, qui connecte les programmes BPF de type XDP aux interfaces réseau. Nous avons codé en dur le numéro d'interface lo, qui vaut toujours 1. Nous exécutons la fonction deux fois pour détacher d'abord l'ancien programme s'il était attaché. Remarquez que maintenant nous n'avons plus besoin de défi pause ou une boucle infinie : notre programme de chargement se terminera, mais le programme BPF ne sera pas tué puisqu'il est connecté à la source de l'événement. Après un téléchargement et une connexion réussis, le programme sera lancé pour chaque paquet réseau arrivant à lo.
Téléchargeons le programme et regardons l'interface lo:
$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp name simple tag 4fca62e77ccb43d6 gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 669
Le programme que nous avons téléchargé a l'ID 669 et nous voyons le même ID sur l'interface lo. Nous enverrons quelques colis à 127.0.0.1 (demande + réponse) :
$ ping -c1 localhost
et maintenant regardons le contenu du fichier virtuel de débogage /sys/kernel/debug/tracing/trace_pipe, dans lequel bpf_printk écrit ses messages :
# cat /sys/kernel/debug/tracing/trace_pipe
ping-13937 [000] d.s1 442015.377014: bpf_trace_printk: running on CPU0
ping-13937 [000] d.s1 442015.377027: bpf_trace_printk: running on CPU0
Deux colis ont été repérés lo et traité sur CPU0 - notre premier programme BPF à part entière et dénué de sens a fonctionné !
Il est à noter que bpf_printk Ce n'est pas pour rien qu'il écrit dans le fichier de débogage : ce n'est pas l'assistant le plus performant à utiliser en production, mais notre objectif était de montrer quelque chose de simple.
Accéder aux cartes des programmes BPF
Exemple : utiliser une carte du programme BPF
Dans les sections précédentes, nous avons appris comment créer et utiliser des cartes à partir de l'espace utilisateur, et regardons maintenant la partie noyau. Commençons, comme d'habitude, par un exemple. Réécrivons notre programme xdp-simple.bpf.c comme suit:
Au début du programme, nous avons ajouté une définition de carte woo: Il s'agit d'un tableau à 8 éléments qui stocke des valeurs comme u64 (en C, nous définirions un tableau tel que u64 woo[8]). Dans un programme "xdp/simple" nous obtenons le numéro de processeur actuel dans une variable key puis en utilisant la fonction d'assistance bpf_map_lookup_element nous obtenons un pointeur vers l'entrée correspondante dans le tableau, que nous augmentons de un. Traduit en russe : nous calculons des statistiques sur le processeur qui a traité les paquets entrants. Essayons d'exécuter le programme :
Vérifions qu'elle est connectée à lo et envoie quelques paquets :
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 108
$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
Presque tous les processus ont été traités sur CPU7. Ce n'est pas important pour nous, l'essentiel est que le programme fonctionne et que nous comprenions comment accéder aux cartes des programmes BPF - en utilisant хелперов bpf_mp_*.
Indice mystique
Ainsi, nous pouvons accéder à la carte depuis le programme BPF en utilisant des appels comme
$ llvm-readelf -r xdp-simple.bpf.o | head -4
Relocation section '.relxdp/simple' at offset 0xe18 contains 1 entries:
Offset Info Type Symbol's Value Symbol's Name
0000000000000020 0000002700000001 R_BPF_64_64 0000000000000000 woo
Mais si nous regardons le programme déjà chargé, nous voyons un pointeur vers la bonne carte (ligne 4) :
Ainsi, nous pouvons conclure qu'au moment du lancement de notre programme de chargement, le lien vers &woo a été remplacé par quelque chose avec une bibliothèque libbpf. Nous allons d'abord regarder le résultat strace:
On voit que libbpf créé une carte woo puis j'ai téléchargé notre programme simple. Regardons de plus près comment nous chargeons le programme :
appel xdp_simple_bpf__open_and_load de fichier xdp-simple.skel.h
qui cause xdp_simple_bpf__load de fichier xdp-simple.skel.h
qui cause bpf_object__load_skeleton de fichier libbpf/src/libbpf.c
qui cause bpf_object__load_xattr de libbpf/src/libbpf.c
La dernière fonction, entre autres, appellera bpf_object__create_maps, qui crée ou ouvre des cartes existantes, les transformant en descripteurs de fichiers. (C'est là qu'on voit BPF_MAP_CREATE dans la sortie strace.) Ensuite, la fonction est appelée bpf_object__relocate et c'est elle qui nous intéresse, puisqu'on se souvient de ce qu'on a vu woo dans le tableau des déménagements. En l'explorant, on se retrouve finalement dans la fonction bpf_program__relocate, qui et s'occupe des déplacements de cartes:
case RELO_LD64:
insn[0].src_reg = BPF_PSEUDO_MAP_FD;
insn[0].imm = obj->maps[relo->map_idx].fd;
break;
et remplacez le registre source par BPF_PSEUDO_MAP_FD, et le premier IMM au descripteur de fichier de notre carte et, s'il est égal à, par exemple, 0xdeadbeef, alors en conséquence, nous recevrons l'instruction
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll
C'est ainsi que les informations cartographiques sont transférées vers un programme BPF chargé spécifique. Dans ce cas, la carte peut être créée en utilisant BPF_MAP_CREATE, et ouvert par ID en utilisant BPF_MAP_GET_FD_BY_ID.
Total, lors de l'utilisation libbpf l'algorithme est le suivant :
lors de la compilation, des enregistrements sont créés dans la table de relocalisation pour les liens vers les cartes
libbpf ouvre le livre d'objets ELF, trouve toutes les cartes utilisées et crée des descripteurs de fichiers pour elles
les descripteurs de fichiers sont chargés dans le noyau dans le cadre de l'instruction LD64
Comme vous pouvez l’imaginer, il y a encore beaucoup à venir et nous devrons nous pencher sur l’essentiel. Heureusement, nous avons une idée : nous avons noté la signification BPF_PSEUDO_MAP_FD dans le registre source et nous pourrons l'enterrer, ce qui nous mènera au saint de tous les saints - kernel/bpf/verifier.c, où une fonction avec un nom distinctif remplace un descripteur de fichier par l'adresse d'une structure de type struct bpf_map:
(le code complet peut être trouvé lien). Nous pouvons donc étendre notre algorithme :
lors du chargement du programme, le vérificateur vérifie l'utilisation correcte de la carte et écrit l'adresse de la structure correspondante struct bpf_map
Lors du téléchargement du binaire ELF en utilisant libbpf Il se passe beaucoup plus de choses, mais nous en discuterons dans d'autres articles.
Chargement de programmes et de cartes sans libbpf
Comme promis, voici un exemple pour les lecteurs qui veulent savoir comment créer et charger un programme utilisant des cartes, sans aide libbpf. Cela peut être utile lorsque vous travaillez dans un environnement pour lequel vous ne pouvez pas créer de dépendances, ni enregistrer chaque bit, ni écrire un programme comme ply, qui génère du code binaire BPF à la volée.
Pour faciliter le suivi de la logique, nous allons réécrire notre exemple à ces fins xdp-simple. Le code complet et légèrement développé du programme discuté dans cet exemple peut être trouvé dans ce essence.
La logique de notre application est la suivante :
créer une carte de type BPF_MAP_TYPE_ARRAY en utilisant la commande BPF_MAP_CREATE,
créer un programme qui utilise cette carte,
connecter le programme à l'interface lo,
qui se traduit par humain par
int main(void)
{
int map_fd, prog_fd;
map_fd = map_create();
if (map_fd < 0)
err(1, "bpf: BPF_MAP_CREATE");
prog_fd = prog_load(map_fd);
if (prog_fd < 0)
err(1, "bpf: BPF_PROG_LOAD");
xdp_attach(1, prog_fd);
}
il est map_create crée une carte de la même manière que nous l'avons fait dans le premier exemple à propos de l'appel système bpf - "noyau, s'il te plaît, fais-moi une nouvelle carte sous la forme d'un tableau de 8 éléments comme __u64 et redonne-moi le descripteur du fichier » :
La partie délicate prog_load est la définition de notre programme BPF comme un ensemble de structures struct bpf_insn insns[]. Mais comme nous utilisons un programme que nous avons en C, nous pouvons tricher un peu :
Au total, nous devons écrire 14 instructions sous forme de structures comme struct bpf_insn (conseil: prenez la décharge d'en haut, relisez la section instructions, ouvrez linux/bpf.h и linux/bpf_common.h et essaie de déterminer struct bpf_insn insns[] tout seul):
Un exercice pour ceux qui ne l'ont pas écrit eux-mêmes - trouvez map_fd.
Il reste encore une partie non divulguée dans notre programme - xdp_attach. Malheureusement, des programmes comme XDP ne peuvent pas être connectés à l'aide d'un appel système. bpf. Les personnes qui ont créé BPF et XDP appartenaient à la communauté Linux en ligne, ce qui signifie qu'ils ont utilisé celui qui leur était le plus familier (mais pas celui qui leur était le plus familier). normal people) interface pour interagir avec le noyau : sockets netlink, voir également RFC3549. La manière la plus simple de mettre en œuvre xdp_attach copie le code de libbpf, à savoir, à partir du fichier netlink.c, c'est ce que nous avons fait, en le raccourcissant un peu :
Bienvenue dans le monde des sockets netlink
Ouvrir un type de socket netlink NETLINK_ROUTE:
int netlink_open(__u32 *nl_pid)
{
struct sockaddr_nl sa;
socklen_t addrlen;
int one = 1, ret;
int sock;
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock < 0)
err(1, "socket");
if (setsockopt(sock, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)) < 0)
warnx("netlink error reporting not supported");
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
err(1, "bind");
addrlen = sizeof(sa);
if (getsockname(sock, (struct sockaddr *)&sa, &addrlen) < 0)
err(1, "getsockname");
*nl_pid = sa.nl_pid;
return sock;
}
Nous lisons depuis cette socket :
static int bpf_netlink_recv(int sock, __u32 nl_pid, int seq)
{
bool multipart = true;
struct nlmsgerr *errm;
struct nlmsghdr *nh;
char buf[4096];
int len, ret;
while (multipart) {
multipart = false;
len = recv(sock, buf, sizeof(buf), 0);
if (len < 0)
err(1, "recv");
if (len == 0)
break;
for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);
nh = NLMSG_NEXT(nh, len)) {
if (nh->nlmsg_pid != nl_pid)
errx(1, "wrong pid");
if (nh->nlmsg_seq != seq)
errx(1, "INVSEQ");
if (nh->nlmsg_flags & NLM_F_MULTI)
multipart = true;
switch (nh->nlmsg_type) {
case NLMSG_ERROR:
errm = (struct nlmsgerr *)NLMSG_DATA(nh);
if (!errm->error)
continue;
ret = errm->error;
// libbpf_nla_dump_errormsg(nh); too many code to copy...
goto done;
case NLMSG_DONE:
return 0;
default:
break;
}
}
}
ret = 0;
done:
return ret;
}
Enfin, voici notre fonction qui ouvre une socket et lui envoie un message spécial contenant un descripteur de fichier :
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 160
Hourra, tout fonctionne. Notez au passage que notre carte est à nouveau affichée sous forme d'octets. Cela est dû au fait que, contrairement libbpf nous n'avons pas chargé les informations de type (BTF). Mais nous en reparlerons davantage la prochaine fois.
Outils de développement
Dans cette section, nous examinerons la boîte à outils minimale du développeur BPF.
D'une manière générale, vous n'avez besoin de rien de spécial pour développer des programmes BPF - BPF fonctionne sur n'importe quel noyau de distribution décent et les programmes sont construits en utilisant clang, qui peut être fourni à partir du package. Cependant, étant donné que BPF est en cours de développement, le noyau et les outils changent constamment, si vous ne souhaitez pas écrire de programmes BPF en utilisant des méthodes à l'ancienne à partir de 2019, vous devrez alors compiler
llvm/clang
pahole
son noyau
bpftool
(Pour référence, cette section et tous les exemples de l'article ont été exécutés sur Debian 10.)
llvm/clang
BPF est compatible avec LLVM et, bien que récemment les programmes pour BPF puissent être compilés en utilisant gcc, tous les développements actuels sont effectués pour LLVM. Par conséquent, nous allons tout d’abord construire la version actuelle clang depuis git :
$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86"
-DLLVM_ENABLE_PROJECTS="clang"
-DBUILD_SHARED_LIBS=OFF
-DCMAKE_BUILD_TYPE=Release
-DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$
Nous pouvons maintenant vérifier si tout s'est bien déroulé :
(Instructions de montage clang pris par moi de bpf_devel_QA.)
Nous n'installerons pas les programmes que nous venons de créer, mais nous les ajouterons simplement à PATH, Par exemple:
export PATH="`pwd`/bin:$PATH"
(Cela peut être ajouté à .bashrc ou dans un fichier séparé. Personnellement, j'ajoute des choses comme ça à ~/bin/activate-llvm.sh et quand c'est nécessaire je le fais . activate-llvm.sh.)
Pahole et BTF
Utilitaire pahole utilisé lors de la construction du noyau pour créer des informations de débogage au format BTF. Nous n’entrerons pas dans les détails dans cet article sur les détails de la technologie BTF, mis à part le fait qu’elle est pratique et que nous souhaitons l’utiliser. Donc, si vous comptez construire votre noyau, construisez d'abord pahole (sans pour autant pahole vous ne pourrez pas construire le noyau avec l'option CONFIG_DEBUG_INFO_BTF:
$ git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
$ cd pahole/
$ sudo apt install cmake
$ mkdir build
$ cd build/
$ cmake -D__LIB=lib ..
$ make
$ sudo make install
$ which pahole
/usr/local/bin/pahole
Noyaux pour expérimenter avec BPF
En explorant les possibilités de BPF, je souhaite assembler mon propre noyau. Ceci, d'une manière générale, n'est pas nécessaire, puisque vous pourrez compiler et charger des programmes BPF sur le noyau de la distribution, cependant, avoir votre propre noyau vous permet d'utiliser les dernières fonctionnalités de BPF, qui apparaîtront dans votre distribution dans le meilleur des mois. , ou, comme dans le cas de certains outils de débogage, ne seront pas du tout packagés dans un avenir prévisible. De plus, son propre noyau donne l’impression qu’il est important d’expérimenter le code.
Pour construire un noyau, vous avez besoin, d'une part, du noyau lui-même, et d'autre part, d'un fichier de configuration du noyau. Pour expérimenter avec BPF, nous pouvons utiliser l'habituel vanille noyau ou l'un des noyaux de développement. Historiquement, le développement de BPF a lieu au sein de la communauté réseau Linux et donc tous les changements passent tôt ou tard par David Miller, le mainteneur du réseau Linux. Selon leur nature - modifications ou nouvelles fonctionnalités - les modifications du réseau se répartissent en deux noyaux - net ou net-next. Les évolutions du BPF sont réparties de la même manière entre bpf и bpf-next, qui sont ensuite regroupés respectivement en net et net-next. Pour plus de détails, voir bpf_devel_QA и FAQ netdev. Choisissez donc un noyau en fonction de vos goûts et des besoins de stabilité du système sur lequel vous testez (*-next les noyaux sont les plus instables de ceux répertoriés).
Cela dépasse le cadre de cet article de parler de la façon de gérer les fichiers de configuration du noyau - on suppose que soit vous savez déjà comment faire cela, soit prêt à apprendre tout seul. Cependant, les instructions suivantes devraient être plus ou moins suffisantes pour vous fournir un système compatible BPF fonctionnel.
Téléchargez l'un des noyaux ci-dessus :
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next
Créez une configuration de noyau fonctionnelle minimale :
$ cp /boot/config-`uname -r` .config
$ make localmodconfig
Activer les options BPF dans le fichier .config de votre propre choix (très probablement CONFIG_BPF sera déjà activé puisque systemd l'utilise). Voici une liste des options du noyau utilisé pour cet article :
CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_LSM=y
CONFIG_BPF_SYSCALL=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_IPV6_SEG6_BPF=y
# CONFIG_NETFILTER_XT_MATCH_BPF is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y
Ensuite, nous pouvons facilement assembler et installer les modules et le noyau (d'ailleurs, vous pouvez assembler le noyau en utilisant le fichier nouvellement assemblé clangen ajoutant CC=clang):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install
et redémarrez avec le nouveau noyau (j'utilise pour cela kexec du paquet kexec-tools):
v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e
outil bpf
L'utilitaire le plus couramment utilisé dans l'article sera l'utilitaire bpftool, fourni dans le cadre du noyau Linux. Il est écrit et maintenu par des développeurs BPF pour des développeurs BPF et peut être utilisé pour gérer tous les types d'objets BPF - charger des programmes, créer et éditer des cartes, explorer la vie de l'écosystème BPF, etc. De la documentation sous forme de codes sources pour les pages de manuel peut être trouvée au cœur ou, déjà compilé, Réseau.
Au moment de la rédaction bpftool est livré prêt à l'emploi uniquement pour RHEL, Fedora et Ubuntu (voir, par exemple, ce fil, qui raconte l'histoire inachevée de l'emballage bpftool dans Debian). Mais si vous avez déjà construit votre noyau, alors construisez bpftool Aussi facile que la tarte:
$ cd ${linux}/tools/bpf/bpftool
# ... пропишите пути к последнему clang, как рассказано выше
$ make -s
Auto-detecting system features:
... libbfd: [ on ]
... disassembler-four-args: [ on ]
... zlib: [ on ]
... libcap: [ on ]
... clang-bpf-co-re: [ on ]
Auto-detecting system features:
... libelf: [ on ]
... zlib: [ on ]
... bpf: [ on ]
$
(ici ${linux} - c'est le répertoire de votre noyau.) Après avoir exécuté ces commandes bpftool seront rassemblés dans un répertoire ${linux}/tools/bpf/bpftool et il peut être ajouté au chemin (tout d'abord à l'utilisateur root) ou copiez simplement dans /usr/local/sbin.
recueillir bpftool il est préférable d'utiliser ce dernier clang, assemblé comme décrit ci-dessus, et vérifiez s'il est correctement assemblé - en utilisant, par exemple, la commande
$ sudo bpftool feature probe kernel
Scanning system configuration...
bpf() syscall for unprivileged users is enabled
JIT compiler is enabled
JIT compiler hardening is disabled
JIT compiler kallsyms exports are enabled for root
...
qui montrera quelles fonctionnalités BPF sont activées dans votre noyau.
À propos, la commande précédente peut être exécutée comme
# bpftool f p k
Cela se fait par analogie avec les utilitaires du package iproute2, où l'on peut par exemple dire ip a s eth0 au lieu de ip addr show dev eth0.
Conclusion
BPF vous permet de ferrer une puce pour mesurer efficacement et modifier à la volée la fonctionnalité du noyau. Le système s'est avéré très efficace, dans la meilleure tradition d'UNIX : un mécanisme simple permettant de (re)programmer le noyau a permis à un grand nombre de personnes et d'organisations d'expérimenter. Et, bien que les expériences, ainsi que le développement de l'infrastructure BPF elle-même, soient loin d'être terminés, le système dispose déjà d'un ABI stable qui vous permet de construire une logique métier fiable et, surtout, efficace.
Je voudrais souligner qu'à mon avis, la technologie est devenue si populaire parce que, d'une part, elle peut jouer (l'architecture d'une machine peut être comprise plus ou moins en une soirée), et d'autre part, résoudre des problèmes qui ne pouvaient pas être résolus (magnifiquement) avant son apparition. Ces deux composantes réunies poussent les gens à expérimenter et à rêver, ce qui conduit à l'émergence de solutions de plus en plus innovantes.
Cet article, bien que pas particulièrement court, n'est qu'une introduction au monde de BPF et ne décrit pas les fonctionnalités « avancées » ni les parties importantes de l'architecture. Le plan pour l'avenir ressemble à ceci : le prochain article sera un aperçu des types de programmes BPF (il existe 5.8 types de programmes pris en charge dans le noyau 30), puis nous verrons enfin comment écrire de vraies applications BPF à l'aide de programmes de traçage du noyau. à titre d'exemple, il est temps de suivre un cours plus approfondi sur l'architecture BPF, suivi d'exemples d'applications de mise en réseau et de sécurité BPF.
Guide de référence BPF et XDP — documentation sur BPF de cilium, ou plus précisément de Daniel Borkman, l'un des créateurs et mainteneurs de BPF. C'est l'une des premières descriptions sérieuses, qui diffère des autres en ce sens que Daniel sait exactement de quoi il parle et qu'il n'y a là aucune erreur. En particulier, ce document décrit comment travailler avec des programmes BPF de types XDP et TC à l'aide de l'utilitaire bien connu ip du paquet iproute2.
Documentation/réseau/filter.txt — fichier original avec documentation pour BPF classique puis étendu. Une bonne lecture si vous souhaitez vous plonger dans le langage assembleur et les détails architecturaux techniques.
Blog sur BPF sur Facebook. Il est rarement mis à jour, mais à juste titre, comme l'écrivent Alexei Starovoitov (auteur d'eBPF) et Andrii Nakryiko - (mainteneur) libbpf).
Les secrets de bpftool. Un fil Twitter divertissant de Quentin Monnet avec des exemples et des secrets d'utilisation de bpftool.