Instrumentation kprobe

Comme nous l’avons vu dans la section théorique, les kprobes sont une fonctionnalité du noyau Linux qui permet d’instrumenter n’importe quel point du code source noyau, pour suivre quand ce code est exécuté et éventuellement en extraire de l’information (paramètres, variables…).

Il va de soi que quand on commence à se livrer à ce genre de sorcellerie, la règle d’or du suivi de l’activité système que nous avons introduite précédemment est plus importante que jamais :

Tu ne provoqueras pas, une fois par événement système surveillé, un événement système que tu surveilles également.

Pensez donc toujours bien, avant d’instrumenter une fonction noyau, à vous demander si elle ne pourrait pas être utilisée par l’outil perf que vous allez invoquer. En cas de doute, privilégiez des outils aussi peu “actifs” que possible (ex: perf record plutôt que perf trace).

Choix de fonction

La commande perf probe --funcs permet d’afficher une liste des fonctions noyau instrumentables. Sont instrumentables les fonctions qui ne sont pas inlinée et où l’instrumentation n’a pas été désactivée manuellement par les développeurs (générallement parce qu’elles sont destinées à s’exécuter dans un contexte où l’instrumentation poserait problème, ex: gestionnaire d’interruptions).

perf probe --funcs
Début de la sortie de perf probe --funcs

Vous noterez l’utilisation d’un pager. Il ne serait pas judicieux pour moi d’inclure la liste complète, car à l’heure où ces lignes sont écrites (noyau Linux 5.14.21), elle fait 40819 entrées. Ce chiffre est à comparer aux quelques 1600 tracepoints exposés par le noyau, en gardant aussi à l’esprit le fait qu’une kprobe n’est pas non plus forcée de se situer au début ou à la fin d’une fonction noyau…

Comme avec perf list, il est possible de filtrer la liste de fonctions émise par perf probe --funcs en ajoutant un motif shell. Par exemple, on peut n’afficher que les fonctions qui ont trait aux entrées/sorties asynchrones “à l’ancienne” (Linux AIO) :

perf probe --funcs '*aio*'

Fonctions d’E/S Linux AIO instrumentables

Instrumentation de l’appel d’une fonction

Pour illustrer l’intérêt des kprobes, nous allons maintenant essayer de surveiller le trafic TCP reçu par le serveur avec perf trace. Il n’y a pas de tracepoint adapté à l’heure où ces lignes sont écrites :

perf list 'tcp:*'

Tracepoints TCP fournis par le noyau linux

En effet, seul le tracepoint tcp:tcp_probe permet de surveiller le trafic TCP “normal”, et il ne fait pas de distinction entre le trafic entrant et sortant. On ne peut donc pas l’utiliser avec perf trace, puisque chaque sortie de perf trace générerait du trafic réseau pour l’envoi via notre connexion SSH, trafic réseau qui serait lui-même détecté par perf trace, et on aurait donc une boucle infinie.

A la place, nous allons instrumenter la fonction tcp_recvmsg() du noyau, qui est appelée lorsque du trafic TCP est reçu alors qu’un processus utilisateur attend des données sur un socket associé. Pour plus d’informations sur l’implémentation de la pile réseau du noyau Linux, vous pourrez vous référer à cette page quand vous aurez accès à Internet.

Signalez à l’administrateur de srv-calcul-ambulant que vous êtes arrivés à ce point du TP pour qu’il mette en place l’instrumentation et vous y donne accès si ce n’est pas déjà fait. Pour votre information, il procèdera comme suit :

# Nécessite des privilèges administrateur !
sudo perf probe tcp_recvmsg \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe

Notez que vous pouvez consulter la liste des instrumentations actives avec l’option --list de perf probe, qui s’abbrévie en -l, et que celles-ci apparaissent également dans perf list tracepoint.

Exercice : Utilisez perf trace pour faire un suivi de l’événement probe:tcp_recvmsg nouvellement créé, et observez ce qui se passe quand vous saisissez du texte dans votre terminal. Notez que ce faisant, vous serez aussi sensible à l’activité réseau issue des autres utilisateurs du serveur.

Capture de variables

A ce stade, nous ne pouvons pas différencier qui est responsable de quel trafic réseau. Le champ __probe_ip affiché par perf trace ne désigne que le pointeur d’instruction, qui sera toujours le même pour un appel de fonction ordinaire.

En revanche, avec une kprobe plus sophistiquée, il nous est possible d’enregistrer les paramètres et variables locales de la fonction, et ainsi en savoir plus sur les conditions dans lesquelles elle est appelée. Voyons la liste des variables accessibles :

perf probe --vars tcp_recvmsg

Variables accessibles de tcp_recvmsg()

Le pointeur de socket sk semble être un bon début : il sera différent pour chaque connexion réseau. Nous allons donc l’utiliser pour raffiner notre suivi.

Vous pouvez afficher également les (nombreuses) variables globales accessibles depuis le point du code source que vous êtes en train d’instrumenter en ajoutant l’option --externs avant --vars.

Signalez à l’administrateur de srv-calcul-ambulant que vous êtes arrivés à ce point du TP pour qu’il mette en place l’instrumentation et vous y donne accès si ce n’est pas déjà fait. Pour votre information, il procèdera comme suit :

# Nécessite des privilèges administrateur !
sudo perf probe tcp_recvmsg_sk=tcp_recvmsg sk \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe

Notez la nouvelle syntaxe utilisée. La syntaxe A=B permet de définir une probe appelée A qui surveille la fonction B, et les variables qu’on souhaite surveiller (“arguments” dans le jargon de perf probe) sont indiquées après un espace.

Exercice : Reprenez l’exercice précédent avec la probe tcp_recvmsg_sk nouvellement créée, et utilisez l’option --filter de perf trace pour ne plus observer que les événements TCP issus de votre propre connexion SSH.

Attention, la sortie de perf trace affiche les pointeurs comme des addresses entières signées, alors que l’option --filter attend un entier non signé. Vous pouvez faire la conversion avec la commande printf "0x%x\n" <nombre négatif à convertir>.

Notez qu’une variante simple de cette technique vous permettrait d’exclure le trafic TCP issu de votre propre connexion SSH, et ainsi d’instrumenter impunément la fonction réciproque tcp_sendmsg() qui est appelée à chaque envoi de données sur le réseau via un socket, sans créer une boucle infinie liée aux sorties de perf trace

Autres emplacements

Jusqu’à présent, pour simplifier les exemples, nos kprobes se sont toujours déclenchées au moment de l’entrée dans une fonction. Mais si nous le souhaitons, il est possible d’injecter une kprobe à peu près n’importe où dans le code de la fonction noyau, ou bien de détecter quand on sort de cette fonction et quelle est la valeur de retour émise à ce moment-là (on parlera alors de kretprobe).

Il est possible d’afficher le code source d’une fonction avec un accent visuel sur les lignes de code qui peuvent être instrumentées via l’option --line=<fonction> de perf probe, qui s’abbrévie en -L :

perf probe --line tcp_recvmsg

Lignes de code instrumentables de tcp_recvmsg()

On voit qu’il n’est pas possible de demander l’arrêt à certaines lignes du code, affichées en bleu. C’est lié au fait que les kprobe travaillant au niveau du binaire compilé, elles doivent être placées au niveau d’une instruction assembleur, or en présence d’optimisations une ligne de code n’est pas forcément clairement associée à une instruction assembleur.

Sur la base du code source affiché ci-dessus, on pourrait par exemple vouloir s’intéresser aux points du code suivant :

  • La ligne 20 représente le cas où on a effectué un cycle lock/unlock normal et s’apprête à exécuter éventuellement des fonctionnalités optionnelles contrôlées par le champ de bits cmsg_flags.
    • En surveillant la valeur de ce champ de bits, on peut savoir lesquelles de ces fonctionnalités seront utilisées ou pas.
  • La sortie de la fonction, qu’elle ait réussi ou échoué (selon une convention habituelle dans le monde UNIX, l’échec est signalé par un code de retour négatif).
    • En surveillant la valeur du champs de bits flags, on peut savoir si l’échec, le cas échéant, est survenu en raison de la condition flags & MSG_ERRQUEUE de la ligne 6 ou bien à l’intérieur de la fonction tcp_recvmsg_locked appelée ligne 15.

En croisant ces deux informations, il est possible d’étudier de façon différenciée les cas où la fonction réussit et ceux où elle échoue.

Signalez à l’administrateur de srv-calcul-ambulant que vous êtes arrivés à ce point du TP pour qu’il mette en place l’instrumentation et vous y donne accès si ce n’est pas déjà fait. Pour votre information, il procèdera comme suit :

# Nécessite des privilèges administrateur !
sudo perf probe tcp_recvmsg_sk_flags=tcp_recvmsg sk flags:x \
&& sudo perf probe tcp_recvmsg_sk_cmsg=tcp_recvmsg:20 sk cmsg_flags:x \
&& sudo perf probe tcp_recvmsg_retval=tcp_recvmsg%return '$retval' \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe

Notez la nouvelle syntaxe utilisée ici:

  • On peut suivre le nom d’une fonction de : et un nombre pour indiquer qu’on veut instrumenter une certaine ligne du code, relativement au début de la fonction.
    • Il est aussi possible d’utiliser cette syntaxe pour instrumenter une certaine ligne d’un fichier source, par exemple fichier.cpp:123.
  • On peut utiliser des modificateurs comme :x pour contrôler l’affichage des variables extraites. Ici, le modificateur x permet de forcer un affichage hexadécimal, ce qui est plus pertinent pour les données de type “flags”.
  • On peut instrumenter la sortie de la fonction avec la syntaxe %return à la fin du nom de la fonction, et utiliser la valeur spéciale $retval pour extraire le résultat renvoyé. Cette dernière nécessite un échappement car le signe $ serait sinon (mal) interprété par le shell.
    • Attention: le noyau ajoute automatiquement __return au nom des kretprobe. Ceci vise à permettre d’enchaîner perf probe xyz et perf probe xyz%return sans rencontrer d’erreur comme quoi le nom de kprobe “xyz” est déjà utilisé. La kprobe définie ci-dessus s’appelle donc tcp_recvmsg_retval__return.

Exercice : Utilisez les trois sondes définies ci-dessus (tcp_recvmsg_sk_flags, tcp_recvmsg_sk_cmsg et tcp_recvmsg_retval__return) pour en savoir plus sur la façon dont la fonction tcp_recvmsg s’exécute en pratique.

Pour une raison mystérieuse, au 13 octobre 2022, perf trace rencontre quelques difficultés avec ces sondes et n’affiche pas toutes les variables extraites. Je vous recommande donc d’utiliser à la place perf record -a suivi de perf script.

Nous reviendrons plus loin sur ce que fait perf script, mais à ce stade vous pouvez retenir que si on la lance sans argument, elle ouvre le fichier perf.data produit par perf record et affiche son contenu dans l’ordre chronologique.

Conclusion

Avec perf probe, il est possible d’instrumenter presque n’importe quel point du code source du noyau linux pour savoir quand est-ce qu’il est exécuté et extraire des valeurs de paramètres, résultats, et variables globales et locales.

Nous allons voir par la suite que ces fonctionnalités d’instrumentation fonctionnent presque pareil pour du code utilisateur (exécutables et bibliothèques).

Mentionnons pour conclure qu’on peut supprimer une kprobe avec la commande administrateur sudo perf probe --del <nom>, et qu’il est aussi possible de spécifier un point d’arrêt par une recherche textuelle dans le code source (syntaxe ;<glob>), ce qui permet d’écrire des scripts un petit peu plus pérennes par rapport aux évolutions futures du noyau Linux. Consultez perf help probe pour plus d’informations.