Nous avons vu ce qu’était un programme de type uProbe dans la première partie : un moyen de sonder les fonctions de vos programmes.
Dans cette partie, nous allons d’abord créer un programme très simple en Go et nous allons le faire réagir avec deux programmes eBPF :
- Le premier de type uProbe
- Le deuxième de type uRetProbe
Je suppose que vous êtes déjà dans un environnement pour développer avec Aya et que vous avez installé le compilateur go et bpftrace. Si ce n’est pas le cas, vous pouvez utiliser le lab Killercoda :
Créons un programme pour tester les uProbes#
Un code tout simple#
Pour changer du Rust, nous allons créer un petit programme en Go :
// hello.go
package main
import "fmt"
func hello() int {
fmt.Println("Hello, world!")
return 3
}
func main() {
ret := hello()
fmt.Println("Returned:", ret)
}
Comment va-t-on activer des uProbes ?#
Le but de l’article est d’activer :
- un programme eBPF de type uProbe à chaque fois que l’on rentre dans la fonction
hello()
:
- un programme eBPF de type uRetProbe à chaque fois que l’on sort de la fonction
hello()
:
Maintenant qu’on a vu les tenants et les aboutissants de l’article, passons à la compilation du programme.
Compilons le programme#
Pour le compiler, il suffit de taper :
go build -gcflags="all=-N -l" -o hello hello.go
On peut remarquer qu’on a ajouté une option -gcflags="all=-N -l"
. Normalement, on n’a pas besoin d’utiliser cette option pour compiler un programme Go :
go build -o hello hello.go
Cela fonctionne également. Mais alors pourquoi utiliser cette option ?
-gcflags
veut dire go compiler flags ce sont des options passées au compilateur Go.all
permet de dire que les options s’appliquent à tous les packages compilés.
Les options spécifiées au compilateur Go sont :
-N
: par défaut, le compilateur modifie (mangle) le nom de la fonction. Cette option permet de désactiver cette modification.
-l
: par défaut, le compilateur inline les fonctions c’est à dire qu’elle intègre directement le contenu de la fonction dans le code appelant permettant ainsi aux programmes d’être plus performants. Cette option permet de désactiver cet inline.
Ainsi ces options vont nous permettre de garder les fonctions et de les rendre lisible, ce qui va nous simplifier pour trouver le point d’attache pour notre programme eBPF.
Comment trouver le point d’attache ?#
Maintenant qu’on a créé et compilé le petit programme, il faut trouver comment déclencher le programme eBPF de type uProbe ou uRetProbe. Lorsqu’on utilise cargo generate
pour le repo aya, il faut répondre à deux questions : où se trouve le nom du binaire et quelle fonction observée. Voyons cela en détail.
Nom du binaire#
🤷 Target to attach the (u|uret)probe? (e.g libc):
Pour garantir la portabilité du programme eBPF, il faut répondre le chemin absolu du binaire. Par exemple je l’ai créé là : /home/cloud_user/hello
.
Nom de la fonction#
Une fois que tu as répondu à la première question, il reste une seconde question :
🤷 Function name to attach the (u|uret)probe? (e.g getaddrinfo):
Que faut-il répondre ? On serait tenté de répondre hello
vu qu’on a mis l’option -N
qui désactive la décoration de nom lors de la compilation.
Mais c’est un peu plus compliqué. Le compilateur Go modifie tout de même légèrement le nom de la fonction pendant l’étape de compilation.
Nous l’avons vu dans le précédent épisode pour trouver toutes les fonctions d’un binaire, il suffit d’utiliser bpftrace
:
bpftrace -l 'uprobe:/home/cloud_user/hello:*'
- l’option
-l
permet de lister toutes les probes disponibles uprobe
: le type de programme eBPF/home/cloud_user/hello
: l’emplacement du binaire*
: le joker (0 ou plusieurs caractères)
Le format d’affichage est alors :
uprobe:/home/cloud_user/hello:[fonction1]
uprobe:/home/cloud_user/hello:[fonction2]
uprobe:/home/cloud_user/hello:[fonction3]
uprobe:/home/cloud_user/hello:[fonction4]
etc
C’est pas de bol : le nom de la fonction est le même que le nom de mon fichier… On ne peut pas faire de | grep hello
. Comment s’en sortir ? avec awk
, cut
ou pire une regex ?
Faisons simple :
bpftrace -l 'uprobe:/home/cloud_user/hello:*hello*'
Et ça va vous répondre :
uprobe:/home/cloud_user/hello:main.hello
Ainsi le nom réel de la fonction est main.hello
nm
. Elle permet de voir tous les symboles qui sont présents dans un fichier binaire.Maintenant qu’on a la réponse aux deux questions, nous pouvons créer nos programmes eBPF.
Commençons par le programme de type uProbe.
Créons un programme eBPF de type uProbe#
Testons avec bpftrace#
Avant de foncer tête baissée sur un programme Aya, vérifions que ça fonctionne bien avec bpftrace
, le programme qui crée en une ligne de commande un programme eBPF. Ça tombe bien : on a déjà trouvé le début dans la section précédente :
uprobe:/home/cloud_user/hello:main.hello
Il reste plus qu’à compléter par un hello world :
sudo bpftrace -e \
'uprobe:/home/cloud_user/hello:main.hello { printf("Hello go\n"); }'
CAP_BPF
. Cependant bpftrace ne gère pas cela : il faut être en root.Dans un autre terminal, lançons alors le programme hello
(sans les droits root) :
./hello
Sur le terminal où est lancé bpftrace, vous voyez Hello go
à chaque fois que vous lancez le programme hello
.
Verifions qu’on peut créer un programme similaire avec Aya.
Génération du programme Aya#
Définissons déjà les différentes réponses pour le point d’attache dans des variables, pour mon cas ça sera :
target=/home/cloud_user/hello
fn_name=main.hello
Lançons maintenant la commande pour générer le programme Aya :
cargo generate --name test-uprobe \
-d program_type=uprobe \
-d uprobe_target=$target \
-d uprobe_fn_name=$fn_name \
https://github.com/aya-rs/aya-template
Sans les options qu’on lui a indiqué, cargo generate
fonctionnerait en mode interactif : on devrait répondre à des questions.
uprobe_target
et uprobe_fn_name
), vous pouvez regarder le fichier test.sh dans le repo aya-template.Vous aurez la sortie suivante :
🔧 program_type: "uprobe" (value from CLI)
🔧 uprobe_target: "/home/cloud_user/hello" (value from CLI)
🔧 uprobe_fn_name: "main.hello" (value from CLI)
🔧 Destination: /home/cloud_user/test-uprobe ...
🔧 project-name: test-uprobe ...
🔧 Generating template ...
[ 1/23] Done: .gitignore
[ 2/23] Done: Cargo.toml
[ 3/23] Done: LICENSE-APACHE
[ 4/23] Done: LICENSE-GPL2
[ 5/23] Done: LICENSE-MIT
[ 6/23] Done: README.md
[ 7/23] Ignored: pre-script.rhai
[ 8/23] Done: rustfmt.toml
[ 9/23] Done: test-uprobe/Cargo.toml
[10/23] Done: test-uprobe/build.rs
[11/23] Done: test-uprobe/src/main.rs
[12/23] Done: test-uprobe/src
[13/23] Done: test-uprobe
[14/23] Done: test-uprobe-common/Cargo.toml
[15/23] Done: test-uprobe-common/src/lib.rs
[16/23] Done: test-uprobe-common/src
[17/23] Done: test-uprobe-common
[18/23] Done: test-uprobe-ebpf/Cargo.toml
[19/23] Done: test-uprobe-ebpf/build.rs
[20/23] Done: test-uprobe-ebpf/src/lib.rs
[21/23] Done: test-uprobe-ebpf/src/main.rs
[22/23] Done: test-uprobe-ebpf/src
[23/23] Done: test-uprobe-ebpf
🔧 Initializing a fresh Git repository
✨ Done! New project created /home/cloud_user/test-uprobe
Compilation et installation dans le noyau#
Maintenant qu’on a généré le programme, il faut compiler le programme et l’installer dans le noyau linux :
cd test-uprobe/
RUST_LOG=info cargo run
Cela va prendre un peu de temps la première fois :
Updating crates.io index
Locking 103 packages to latest compatible versions
Adding which v6.0.3 (available: v8.0.0)
Downloaded anstyle v1.0.11
Downloaded cfg-if v1.0.1
Downloaded anyhow v1.0.98
Downloaded either v1.15.0
Downloaded cargo_metadata v0.19.2
Downloaded version_check v0.9.5
Downloaded which v6.0.3
Downloaded socket2 v0.6.0
Downloaded mio v1.0.4
[...]
warning: test-uprobe@0.1.0: Finished `release` profile [optimized] target(s) in 19.59s
Finished `dev` profile [unoptimized + debuginfo] target(s) in 50.16s
Running `/root/build-cache/debug/test-uprobe`
Waiting for Ctrl-C...
Testons maintenant#
Laisser le programme Aya tourné et sur un autre terminal, lancer le programme que vous voulez examiner. Pour mon cas :
./hello
Sur le terminal où on a lancé la commande cargo run
, Vous devriez voir la sortie suivante à chaque fois que vous lancez le programme :
[INFO test_uprobe] function main.hello called by /home/cloud_user/hello
Vous voyez que la difficulté est la même qu’avec bpftrace mais que le temps de compilation est beaucoup plus long.
Créons un programme eBPF de type uRetProbe#
Nous avons vu un exemple de programme eBPF de type uProbe où on n’avait pas à modifier le code généré. Nous allons maintenant complexifier légèrement avec un programme eBPF de type uRetProbe et nous allons récupérer la valeur de retour.
Testons avec bpftrace#
Le code bpftrace est alors légèrement modifié :
sudo bpftrace -e \
'uretprobe:/home/cloud_user/hello:main.hello { printf("retval=%d\n", retval); }'
uprobe
est donc remplacé paruretprobe
- on a rajouté la variable de retour :
retval
Lancer le programme hello
dans un autre terminal et vous verrez dans le terminal bpftrace :
retval=3
Regardons comment produire le programme équivalent en Aya.
Génération du programme Aya#
La génération du code se fait de manière similaire qu’avec une uProbe :
target=/home/cloud_user/hello
fn_name=main.hello
cargo generate --name test-uretprobe \
-d program_type=uretprobe \
-d uprobe_target=$target \
-d uprobe_fn_name=$fn_name \
https://github.com/aya-rs/aya-template
Compilation et installation#
De la même manière on va compiler et installer le programme eBPF :
cd test-uretprobe/
RUST_LOG=info cargo run
À la sortie de chaque fonction hello()
, le programme eBPF affiche bien :
[INFO test_uretprobe] function main.hello called by /home/cloud_user/hello
Jusqu’à présent, nous avons fait un peu près la même chose que pour l’uProbe. Regardons maintenant comment récupérer la valeur de retour 3.
Modification du code#
Il faut modifier le fichier test-uretprobe-ebpf/src/main.rs
: le code de l’espace noyau. En particulier ce bout de code :
fn try_test_uretprobe(ctx: RetProbeContext) -> Result<u32, u32> {
info!(&ctx, "function main.hello called by /home/cloud_user/hello");
Ok(0)
}
Il faut voir comment utiliser la structure RetProbeContext
pour afficher le code retour.
Regardons la documentation :
Il faut donc utiliser la méthode ret()
. On va rajouter un truc comme ça :
let retval: u32 = ctx.ret().ok_or(1u32)?;
1u32
(entier non signé de 32 bits) car la signature de la fonction est Result<u32, u32>
.Il faut également modifier la macro Aya info!
pour afficher cette valeur.
Ce qui donne au final :
fn try_test_uretprobe(ctx: RetProbeContext) -> Result<u32, u32> {
let retval: u32 = ctx.ret().ok_or(1u32)?;
info!(&ctx, "retval={}", retval);
Ok(0)
}
Testons maintenant#
Appliquons les modifications :
RUST_LOG=info cargo run
Lançons la commande hello
dans un autre terminal :
./hello
Et du côté du programme Aya, on a l’affichage suivant :
[INFO test_uretprobe] retval=3
Ce qui est cohérent avec ce qu’on avait trouvé avec bpftrace.
Cet épisode est maintenant terminé ! Nous avons vu les bases pour faire réagir un programme eBPF de type uProbe et uRetProbe lors du lancement d’un programme Go.
Cependant, nous n’avons pas encore exploré comment récupérer les arguments d’une fonction.
Ça tombe bien ! Dans le prochain épisode, nous allons sonder une bibliothèque bien connue en récupérant les arguments de fonction et en les traitant !