Aller au contenu
Background Image
  1. Ebpf-Another-Types/

Observons une fonction simple de ton programme avec des uProbes

·1847 mots·9 mins·
Joseph Ligier
Auteur
Joseph Ligier
CNCF ambassador | Kubestronaut 🐝
Sommaire
Apprenons uProbe avec eBPF et Aya - Cet article fait partie d'une série.
Partie 2: Cet article

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 :

Killer coda screenshot


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)
}
Vous pouvez tester pour un autre programme compilé comme C/C++ ou Rust si vous préférez.

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() :

Go uprobe

  • un programme eBPF de type uRetProbe à chaque fois que l’on sort de la fonction hello() :

Go uretprobe

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.
En anglais, to mangle peut se traduire par modifier, mutiler ou même défigurer. En informatique, name mangling se traduit par décoration de nom. On a complètement gommé le côté péjoratif du terme !
  • -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.

Go 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

Si vous ne pouvez pas installer bpftrace, vous pouvez utiliser la commande 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
#

Go 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"); }'
Historiquement, il fallait avoir les droits root pour installer un programme eBPF dans le noyau. Avec un Linux moderne, un utilisateur non root peut installer un programme eBPF avec juste quelques capacités supplémentaires comme 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.

Pour trouver le nom des arguments (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
Pour simplifier, j’ai fait la compilation et l’installation du programme eBPF en root.

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.

Go uretprobe

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é par uretprobe
  • 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 :

Aya uRetProbe documentation

Il faut donc utiliser la méthode ret(). On va rajouter un truc comme ça :

let retval: u32 = ctx.ret().ok_or(1u32)?;
Si la fonction n’a pas de retour on affiche 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 !

Apprenons uProbe avec eBPF et Aya - Cet article fait partie d'une série.
Partie 2: Cet article