Estender o kernel com eBPF

O Extended Berkeley Packet Filter (eBPF) é uma máquina virtual no kernel que executa programas eBPF fornecidos pelo usuário para estender a funcionalidade do kernel. Esses programas podem ser conectados a sondas ou eventos no kernel e usados para coletar estatísticas úteis, monitorar e depurar. Um programa é carregado no kernel usando a syscall bpf(2) e é fornecido pelo usuário como um blob binário de instruções de máquina eBPF. O sistema de build do Android oferece suporte à compilação de programas C para eBPF usando a sintaxe de arquivo de build simples descrita neste documento.

Mais informações sobre os componentes internos e a arquitetura do eBPF podem ser encontradas na página eBPF de Brendan Gregg (em inglês).

O Android inclui um carregador eBPF e uma biblioteca que carregam programas eBPF no tempo de inicialização.

Loader BPF do Android

Durante a inicialização do Android, todos os programas eBPF localizados em /system/etc/bpf/ são carregados. Esses programas são objetos binários criados pelo sistema de build do Android a partir de programas C e são acompanhados por arquivos Android.bp na árvore de origem do Android. O sistema de build armazena os objetos gerados em /system/etc/bpf, e esses objetos se tornam parte da imagem do sistema.

Formato de um programa eBPF C do Android

Um programa C eBPF precisa ter o seguinte 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

Em que:

  • name_of_my_map é o nome da variável do seu mapa. Esse nome informa ao carregador de BPF o tipo de mapa a ser criado e com quais parâmetros. Essa definição de struct é fornecida pelo cabeçalho bpf_helpers.h incluído.
  • PROGTYPE/PROGNAME representa o tipo e o nome do programa. O tipo do programa pode ser qualquer um dos listados na tabela a seguir. Quando um tipo de programa não está listado, não há uma convenção de nomenclatura rígida para ele. O nome só precisa ser conhecido pelo processo que anexa o programa.

  • PROGFUNC é uma função que, quando compilada, é colocada em uma seção do arquivo resultante.

kprobe O PROGFUNC é vinculado a uma instrução do kernel usando a infraestrutura do kprobe. PROGNAME precisa ser o nome da função do kernel que está sendo kprobed. Consulte a documentação do kernel kprobe para mais informações sobre kprobes.
ponto de rastreamento Conecte o PROGFUNC a um tracepoint. PROGNAME precisa estar no formato SUBSYSTEM/EVENT. Por exemplo, uma seção de tracepoint para anexar funções a eventos de alternância de contexto do programador seria SEC("tracepoint/sched/sched_switch"), em que sched é o nome do subsistema de rastreamento e sched_switch é o nome do evento de rastreamento. Consulte a documentação do kernel de eventos de rastreamento para mais informações sobre pontos de rastreamento.
skfilter O programa funciona como um filtro de soquete de rede.
schedcls O programa funciona como um classificador de tráfego de rede.
cgroupskb, cgroupsock O programa é executado sempre que processos em um CGroup criam um soquete AF_INET ou AF_INET6.

Outros tipos podem ser encontrados no código-fonte do carregador.

Por exemplo, o programa myschedtp.c a seguir adiciona informações sobre o PID da tarefa mais recente executada em uma CPU específica. Esse programa alcança o objetivo criando um mapa e definindo uma função tp_sched_switch que pode ser anexada ao evento de rastreamento sched:sched_switch. Para mais informações, consulte Como anexar programas a pontos de rastreamento.

#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");

A macro LICENSE é usada para verificar se o programa é compatível com a licença do kernel quando o programa faz uso de funções auxiliares BPF fornecidas pelo kernel. Especifique o nome da licença do programa em formato de string, como LICENSE("GPL") ou LICENSE("Apache 2.0").

Formato do arquivo Android.bp

Para que o sistema de build do Android crie um programa .c do eBPF, é necessário criar uma entrada no arquivo Android.bp do projeto. Por exemplo, para criar um programa C eBPF chamado bpf_test.c, faça a seguinte entrada no arquivo Android.bp do projeto:

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

Essa entrada compila o programa em C, resultando no objeto /system/etc/bpf/bpf_test.o. Na inicialização, o sistema Android carrega automaticamente o programa bpf_test.o no kernel.

Arquivos disponíveis no sysfs

Durante a inicialização, o sistema Android carrega automaticamente todos os objetos eBPF de /system/etc/bpf/, cria os mapas necessários para o programa e fixa o programa carregado com os mapas no sistema de arquivos BPF. Esses arquivos podem ser usados para interação adicional com o programa eBPF ou leitura de mapas. Nesta seção, descrevemos as convenções usadas para nomear esses arquivos e os locais deles no sysfs.

Os seguintes arquivos são criados e fixados:

  • Para todos os programas carregados, supondo que PROGNAME seja o nome do programa e FILENAME seja o nome do arquivo C eBPF, o carregador do Android cria e fixa cada programa em /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Por exemplo, no exemplo anterior de ponto de rastreamento sched_switch em myschedtp.c, um arquivo de programa é criado e fixado em /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch.

  • Para todos os mapas criados, supondo que MAPNAME seja o nome do mapa e FILENAME seja o nome do arquivo C eBPF, o carregador do Android cria e fixa cada mapa em /sys/fs/bpf/map_FILENAME_MAPNAME.

    Por exemplo, no exemplo anterior de ponto de rastreamento sched_switch em myschedtp.c, um arquivo de mapa é criado e fixado em /sys/fs/bpf/map_myschedtp_cpu_pid_map.

  • bpf_obj_get() na biblioteca BPF do Android retorna um descritor de arquivo do arquivo /sys/fs/bpf fixado. Esse descritor de arquivo pode ser usado para outras operações, como ler mapas ou anexar um programa a um ponto de rastreamento.

Biblioteca BPF do Android

A biblioteca BPF do Android é chamada de libbpf_android.so e faz parte da imagem do sistema. Essa biblioteca oferece ao usuário os recursos de eBPF de baixo nível necessários para criar e ler mapas, criar sondas, pontos de rastreamento e buffers de desempenho.

Anexar programas a pontos de rastreamento

Os programas de ponto de rastreamento são carregados automaticamente na inicialização. Após o carregamento, o programa de ponto de rastreamento precisa ser ativado seguindo estas etapas:

  1. Chame bpf_obj_get() para receber o fd do programa do local do arquivo fixado. Para mais informações, consulte os Arquivos disponíveis no sysfs.
  2. Chame bpf_attach_tracepoint() na biblioteca BPF, transmitindo o programa fd e o nome do ponto de rastreamento.

O exemplo de código a seguir mostra como anexar o ponto de rastreamento sched_switch definido no arquivo de origem myschedtp.c anterior (a verificação de erros não é mostrada):

  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));

Ler os mapas

Os mapas BPF são compatíveis com estruturas ou tipos complexos arbitrários de chave e valor. A biblioteca BPF do Android inclui uma classe android::BpfMap que usa modelos de C++ para instanciar BpfMap com base na chave e no tipo de valor do mapa em questão. O exemplo de código anterior demonstra o uso de um BpfMap com chave e valor como números inteiros. Os números inteiros também podem ser estruturas arbitrárias.

Assim, a classe BpfMap com modelo permite definir um objeto BpfMap personalizado adequado para o mapa específico. O mapa pode ser acessado usando as funções geradas de forma personalizada, que são sensíveis ao tipo, resultando em um código mais limpo.

Para mais informações sobre BpfMap, consulte as fontes do Android.

Depurar problemas

Durante a inicialização, várias mensagens relacionadas ao carregamento do BPF são registradas. Se o processo de carregamento falhar por qualquer motivo, uma mensagem de registro detalhada será fornecida no logcat. A filtragem dos registros do logcat por bpf imprime todas as mensagens e erros detalhados durante o tempo de carregamento, como erros do verificador eBPF.

Exemplos de eBPF no Android

Os seguintes programas no AOSP oferecem outros exemplos de uso do eBPF:

  • O programa eBPF C netd é usado pelo daemon de rede (netd) no Android para várias finalidades, como filtragem de soquetes e coleta de estatísticas. Para saber como esse programa é usado, confira as origens do monitor de tráfego eBPF.

  • O programa eBPF C time_in_state calcula a quantidade de tempo que um app Android passa em diferentes frequências de CPU, que é usada para calcular a potência.

  • No Android 12, o gpu_mem programa C eBPF monitora o uso total da memória da GPU para cada processo e para todo o sistema. Esse programa é usado para criar o perfil de memória da GPU.