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*'
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:*'
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
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
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 conditionflags & MSG_ERRQUEUE
de la ligne 6 ou bien à l’intérieur de la fonctiontcp_recvmsg_locked
appelée ligne 15.
- En surveillant la valeur du champs de bits
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
.
- Il est aussi possible d’utiliser cette syntaxe pour instrumenter une
certaine ligne d’un fichier source, par exemple
- On peut utiliser des modificateurs comme
:x
pour contrôler l’affichage des variables extraites. Ici, le modificateurx
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înerperf probe xyz
etperf 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 donctcp_recvmsg_retval__return
.
- Attention: le noyau ajoute automatiquement
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.