Estendere il kernel con eBPF

Extended Berkeley Packet Filter (eBPF) è una macchina virtuale in-kernel che esegue programmi eBPF forniti dall'utente per estendere la funzionalità del kernel. Questi programmi possono essere collegati a sonde o eventi nel kernel e utilizzati per raccogliere statistiche, monitorare e eseguire il debug del kernel. Un programma viene caricato nel kernel utilizzando la chiamata di sistema bpf(2) e viene fornito dall'utente come blob binario di istruzioni macchina eBPF. Il sistema di compilazione di Android supporta la compilazione di programmi C in eBPF utilizzando una sintassi semplice del file di compilazione descritta in questo documento.

Ulteriori informazioni sugli aspetti interni e sull'architettura di eBPF sono disponibili nella pagina di Brendan Gregg su eBPF.

Android include un caricatore e una libreria eBPF che caricano i programmi eBPF al momento dell'avvio.

Caricatore BPF Android

Durante l'avvio di Android, vengono caricati tutti i programmi eBPF situati in /system/etc/bpf/. Questi programmi sono oggetti binari creati dal sistema di build di Android da programmi C e sono accompagnati da file Android.bp nell'albero di origine di Android. Il sistema di compilazione memorizza gli oggetti generati in /system/etc/bpf e questi oggetti diventano parte dell'immagine di sistema.

Formato di un programma C eBPF per Android

Un programma C eBPF deve avere il seguente formato:

#include <bpf_helpers.h>

/* Define one or more maps in the maps section, for example
 * define a map of type array int -> uint32_t, with 10 entries
 */
DEFINE_BPF_MAP(name_of_my_map, ARRAY, int, uint32_t, 10);

/* this also defines type-safe accessors:
 *   value * bpf_name_of_my_map_lookup_elem(&key);
 *   int bpf_name_of_my_map_update_elem(&key, &value, flags);
 *   int bpf_name_of_my_map_delete_elem(&key);
 * as such it is heavily suggested to use lowercase *_map names.
 * Also note that due to compiler deficiencies you cannot use a type
 * of 'struct foo' but must instead use just 'foo'.  As such structs
 * must not be defined as 'struct foo {}' and must instead be
 * 'typedef struct {} foo'.
 */

DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
   <body-of-code
    ... read or write to MY_MAPNAME
    ... do other things
   >
}

LICENSE("GPL"); // or other license

Dove:

  • name_of_my_map è il nome della variabile mappa. Questo nome informa il caricatore BPF del tipo di mappa da creare e con quali parametri. Questa definizione di struct è fornita dall'intestazione bpf_helpers.h.
  • PROGTYPE/PROGNAME indica il tipo di programma e il nome del programma. Il tipo di programma può essere uno di quelli elencati nella tabella seguente. Quando un tipo di programma non è elencato, non esiste una convenzione di denominazione rigorosa per il programma; il nome deve essere noto solo al processo che lo associa.

  • PROGFUNC è una funzione che, una volta compilata, viene inserita in una sezione del file risultante.

kprobe Esegue il hooking PROGFUNC su un'istruzione del kernel utilizzando l'infrastruttura kprobe. PROGNAME deve essere il nome della funzione del kernel sottoposto a kprobe. Per ulteriori informazioni sui kprobe, consulta la documentazione del kernel kprobe.
punto di traccia Esegue il collegamento PROGFUNC a un punto traccia. PROGNAME deve essere nel formato SUBSYSTEM/EVENT. Ad esempio, una sezione di tracepoint per l'associazione di funzioni agli eventi di cambio di contesto dell'scheduler è SEC("tracepoint/sched/sched_switch"), dove sched è il nome del sottosistema di traccia e sched_switch è il nome dell'evento di traccia. Consulta la documentazione del kernel relativa agli eventi di traccia per ulteriori informazioni sui tracepoint.
skfilter Il programma funziona come filtro delle socket di rete.
schedcls Il programma funziona come un classificatore del traffico di rete.
cgroupskb, cgroupsock Il programma viene eseguito ogni volta che i processi in un CGroup creano una socket AF_INET o AF_INET6.

Altri tipi sono disponibili nel codice sorgente del caricatore.

Ad esempio, il seguente programma myschedtp.c aggiunge informazioni sull'ultimo PID dell'attività eseguito su una determinata CPU. Questo programma raggiunge il suo obiettivo creando una mappa e definendo una funzione tp_sched_switch che può essere collegata all'evento traccia sched:sched_switch. Per ulteriori informazioni, consulta Collegare i programmi ai tracepoint.

#include <linux/bpf.h>
#include <stdbool.h>
#include <stdint.h>
#include <bpf_helpers.h>

DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024);

struct switch_args {
    unsigned long long ignore;
    char prev_comm[16];
    int prev_pid;
    int prev_prio;
    long long prev_state;
    char next_comm[16];
    int next_pid;
    int next_prio;
};

DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_SYSTEM, tp_sched_switch)
(struct switch_args *args) {
    int key;
    uint32_t val;

    key = bpf_get_smp_processor_id();
    val = args->next_pid;

    bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY);
    return 1; // return 1 to avoid blocking simpleperf from receiving events
}

LICENSE("GPL");

La macro LICENSE viene utilizzata per verificare se il programma è compatibile con la licenza del kernel quando utilizza le funzioni di assistenza BPF fornite dal kernel. Specifica il nome della licenza del programma sotto forma di stringa, ad esempio LICENSE("GPL") o LICENSE("Apache 2.0").

Formato del file Android.bp

Affinché il sistema di compilazione Android possa compilare un programma .c eBPF, devi creare una voce nel file Android.bp del progetto. Ad esempio, per compilare un programma C eBPF denominato bpf_test.c, inserisci la seguente voce nel file Android.bp del progetto:

bpf {
    name: "bpf_test.o",
    srcs: ["bpf_test.c"],
    cflags: [
        "-Wall",
        "-Werror",
    ],
}

Questa voce compila il programma C, generando l'oggetto /system/etc/bpf/bpf_test.o. All'avvio, il sistema Android carica automaticamente il programma bpf_test.o nel kernel.

File disponibili in sysfs

Durante l'avvio, il sistema Android carica automaticamente tutti gli oggetti eBPF da/system/etc/bpf/, crea le mappe necessarie per il programma e blocca il programma caricato con le relative mappe nel file system BPF. Questi file possono essere utilizzati per un'ulteriore interazione con il programma eBPF o per leggere le mappe. Questa sezione descrive le convenzioni utilizzate per la denominazione di questi file e le relative posizioni in sysfs.

Vengono creati e fissati i seguenti file:

  • Per tutti i programmi caricati, supponendo che PROGNAME sia il nome del programma e FILENAME sia il nome del file C eBPF, il caricatore Android crea e blocca ogni programma in /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Ad esempio, per l'esempio precedente del tracepoint sched_switch in myschedtp.c, viene creato un file di programma e bloccato in /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch.

  • Per tutte le mappe create, supponendo che MAPNAME sia il nome della mappa e FILENAME sia il nome del file C eBPF, il caricatore Android crea e blocca ogni mappa su /sys/fs/bpf/map_FILENAME_MAPNAME.

    Ad esempio, per l'esempio precedente del tracepoint sched_switch in myschedtp.c, viene creato un file mappa e fissato su /sys/fs/bpf/map_myschedtp_cpu_pid_map.

  • bpf_obj_get() nella libreria BPF di Android restituisce un descrittore file dal file /sys/fs/bpf bloccato. Questo descrittore file può essere utilizzato per ulteriori operazioni, come la lettura delle mappe o l'associazione di un programma a un punto traccia.

Libreria BPF per Android

La libreria BPF di Android si chiama libbpf_android.so e fa parte dell'immagine di sistema. Questa libreria fornisce all'utente le funzionalità eBPF di basso livello necessarie per creare e leggere mappe, creare sonde, tracepoint e buffer di prestazioni.

Collega i programmi ai tracepoint

I programmi dei punti traccia vengono caricati automaticamente all'avvio. Dopo il caricamento, il programma dei punti di traccia deve essere attivato seguendo questa procedura:

  1. Chiama bpf_obj_get() per ottenere il programma fd dalla posizione del file bloccato. Per ulteriori informazioni, consulta la sezione File disponibili in sysfs.
  2. Chiama bpf_attach_tracepoint() nella libreria BPF, passando il programmafd e il nome del punto di traccia.

Il seguente esempio di codice mostra come collegare il punto di traccia sched_switch definito nel file di origine myschedtp.c precedente (il controllo degli errori non viene mostrato):

  char *tp_prog_path = "/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch";
  char *tp_map_path = "/sys/fs/bpf/map_myschedtp_cpu_pid";

  // Attach tracepoint and wait for 4 seconds
  int mProgFd = bpf_obj_get(tp_prog_path);
  int mMapFd = bpf_obj_get(tp_map_path);
  int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch");
  sleep(4);

  // Read the map to find the last PID that ran on CPU 0
  android::bpf::BpfMap<int, int> myMap(mMapFd);
  printf("last PID running on CPU %d is %d\n", 0, myMap.readValue(0));

Leggere dalle mappe

Le mappe BPF supportano tipi o strutture di chiavi e valori arbitrari complessi. La libreria BPF di Android include una classe android::BpfMap che utilizza i modelli C++ per creare istanze di BpfMap in base al tipo di chiave e valore per la mappa in questione. L'esempio di codice precedente mostra l'utilizzo di un BpfMap con chiave e valore come numeri interi. Gli interi possono anche essere strutture arbitrarie.

Pertanto, la classe BpfMap basata su modelli consente di definire un oggetto BpfMap personalizzato adatto alla mappa specifica. È quindi possibile accedere alla mappa utilizzando le funzioni generate personalizzate, che sono sensibili al tipo, con un codice più pulito.

Per ulteriori informazioni su BpfMap, consulta le origini Android.

Eseguire il debug di problemi.

Durante l'avvio vengono registrati diversi messaggi relativi al caricamento di BPF. Se la procedura di caricamento non va a buon fine per qualsiasi motivo, in logcat viene fornito un messaggio di log dettagliato. Se filtri i log di logcat per bpf, vengono stampati tutti i messaggi e eventuali errori dettagliati durante il caricamento, ad esempio gli errori del verificatore eBPF.

Esempi di eBPF in Android

I seguenti programmi in AOSP forniscono ulteriori esempi di utilizzo di eBPF:

  • Il netd programma C eBPF viene utilizzato dal daemon di rete (netd) in Android per vari scopi, come il filtraggio delle socket e la raccolta delle statistiche. Per scoprire come viene utilizzato questo programma, controlla le origini del monitor del traffico eBPF.

  • Il time_in_state programma C eBPF calcola il tempo che un'app per Android trascorre a diverse frequenze della CPU, che viene utilizzato per calcolare la potenza.

  • In Android 12, il programma C eBPF gpu_mem monitora l'utilizzo totale della memoria GPU per ogni processo e per l'intero sistema. Questo programma viene utilizzato per il profiling della memoria GPU.