Instrumentation uprobe

Dans la section précédente, nous avons vu comment il est possible d’instrumenter le code du noyau Linux pour en extraire des informations. Nous allons maintenant voir comment une variante de cette instrumentation, les uprobes, peut être utilisée pour extraire des informations du code utilisateur : exécutables, bibliothèques…

Utilisation simple des uprobes

Dans l’ensemble, l’utilisation de perf probe sur un binaire utilisateur est très similaire à celle sur le noyau Linux. Il suffit de rajouter un paramètre --exec, qui s’abbrévie en -x, pour désigner le binaire cible. Par exemple, on peut énumérer les fonctions instrumentables avec --funcs/-F.

perf probe -x /bin/bash --funcs
Début de la sortie de perf probe -x /bin/bash --funcs

Si le code source de l’exécutable est disponible (ce qui est le cas pour bash sur srv-calcul-ambulant), on peut afficher les lignes instrumentables d’une fonction avec l’option --line/-L :

perf probe -x /bin/bash --line absolute_pathname

Code source de la fonction absolute_pathname de bash

Si des symboles de déboguages sont disponibles, on peut aussi toujours afficher le nom des variables accessibles par l’instrumentation avec --vars/-V :

perf probe -x /bin/bash --vars main

Variables de la fonction main de bash

Bref, vous l’aurez compris, à un paramètre -x près, l’utilisation de perf probe pour les uprobes est quasiment identique à celle pour les kprobes. Notez juste les préconditions : pour avoir toutes les fonctionalités, il faut avoir accès à la fois à des symboles de déboguage et au code source du binaire, ce qui est rarement le cas par défaut et nécessite un peu de préparation de l’administrateur.

Maintenant, puisqu’on parle d’administrateur, invitez-le justement à activer une instrumentation au niveau de la fonction main() de bash avec la commande suivante :

# Nécessite des privilèges administrateur !
sudo perf probe -x /bin/bash main argc argv[0]:string \
&& sudo chown -R root:perf /sys/kernel/tracing/events/probe_bash

Ce qui est l’occasion d’aborder quelques nouveaux points de syntaxe :

  • Il est possible à perf probe d’accéder aux éléments d’un tableau C, par contre les pointeurs char* ne sont pas automatiquement traités comme des chaînes de caractère, il faut explicitement le demander avec le suffixe :string.
  • Pour limiter les collisions de noms, les sondes sont regroupées en groupes. Par défaut, les kprobes appartiennent au groupe probe, et les uprobes à un groupe appelé probe_xyz où xyz est le nom du binaire instrumenté. Il est possible d’utiliser un nom de groupe personnalisé si on le souhaite, ce qui est par exemple utile si plusieurs personnes avec des droits administrateurs veulent utiliser perf probe sur le même binaire en même temps sans se marcher sur les pieds.

Exercice : En surveillant la sonde probe_bash:main nouvellement créée avec perf trace, essayez de lancer bash quelques fois en variant le chemin d’accès utilisé (détection automatique via PATH vs absolu…) et en ajoutant des paramètres, et notez comment chaque nouvelle exécution de bash est désormais suivie par perf trace.

Considérations spécifiques aux bibliothèques GNU

Nous l’avons vu, l’utilisation de base des uprobes est, dans l’ensemble, très similaire à celle des kprobes. Toutefois, quand on commence à les utiliser en pratique, on remarque qu’on a besoin de prendre des précautions qui ne sont pas souvent nécessaires avec le code noyau.

Pour être plus précis, lorsqu’on utilise des uprobes, il est très tentant d’instrumenter des bibliothèques de base telles que la libc, la libstdc++ (bibliothèque standard C++ de GCC), la libm (implémentation des fonctions mathématiques comme sin, cos…), ou la libpthread (implémentation des primitives de synchronisation comme les mutexes). Mais si on essaie, on se rendra rapidement compte qu’il ne suffit pas forcément d’instrumenter la fonction ayant le nom le plus évident (ex: memcpy dans la libc), pour les raisons suivantes :

  • Les compilateurs tendent à inliner une partie de l’implémentation de ces fonctions au sein du programme appelant, et donc la fonction en elle-même n’est presque jamais appelée, seule des sous-fonctions utilitaires internes sont éventuellement appelées.
  • Les bibliothèques GNU utilisent des techniques compliquées pour optimiser la compatibilité binaire des programmes et le compromis portabilité/performance, et l’analyse des binaires de perf probe a du mal à voir à travers ces couches d’abstraction.

La manière la plus simple de contourner ces aléas est d’utiliser un profileur CPU pour repérer le nom de la fonction qui est réellement appelée par le binaire final, et instrumenter celle-là. Mais parfois, on aura aussi la chance de disposer d’instrumentation statique USDT prête à l’emploi, ce que nous allons aborder dans la section suivante.