Estender o kernel com eBPF

O Extended Berkeley Package Filter (eBPF) é uma máquina virtual no kernel que executa programas eBPF fornecidos pelo usuário para ampliar 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 é carregados no kernel usando a syscall bpf(2) e são fornecidas pelo usuário como um blob binário de instruções da máquina eBPF. O sistema de build do Android tem suporte para compilar programas C para eBPF usando a sintaxe simples de arquivos de build descrita neste documento.

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

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

Loader BPF do Android

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

Formato de um programa eBPF C do Android

Um programa eBPF C 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 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 do programa. e o nome do programa. O tipo do programa pode ser qualquer um dos listados no tabela a seguir. Quando um tipo de programa não está listado, não há uma nomenclatura rigorosa convenção para o programa. 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 do kprobe (em inglês) para mais informações sobre Kprobes.
tracepoint Conecte o PROGFUNC a um tracepoint. PROGNAME precisa estar no formato SUBSYSTEM/EVENT. Por exemplo, uma seção de ponto de rastreamento 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. Verificar o kernel de eventos de rastreamento documentação para mais informações sobre tracepoints.
skfilter Programe funções 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 os 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 os o PID da tarefa mais recente executado 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 tracepoints.

#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 usa funções auxiliares BPF fornecidas pelo kernel. Especifique o nome da licença do seu 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 de eBPF, é necessário criar uma entrada no arquivo Android.bp do projeto. Por exemplo, para crie um programa em C eBPF chamado bpf_test.c e faça o seguinte: entrada no arquivo Android.bp do seu projeto:

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

Essa entrada compila o programa 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 em sysfs

Durante a inicialização, o sistema Android carrega automaticamente todos os objetos eBPF do /system/etc/bpf/, cria os mapas necessários para o programa e fixa os mapas carregados com seus mapas para o sistema de arquivos BPF. Esses arquivos podem ser usados para maior interação com o programa eBPF ou leitura de mapas. Esta seção descreve as convenções usadas para nomear esses arquivos e seus locais em sysfs.

Os arquivos a seguir 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 tracepoint sched_switch em myschedtp.c, um arquivo de programa será 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 tracepoint sched_switch em myschedtp.c, um arquivo de mapa é criado e fixado em /sys/fs/bpf/map_myschedtp_cpu_pid_map

  • Na biblioteca BPF do Android, bpf_obj_get() retorna um descritor de arquivo da 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. Esta biblioteca fornece ao usuário os recursos de eBPF de baixo nível necessários para criar e ler mapas e criar sondagens, tracepoints e buffers de desempenho.

Anexar programas a pontos de rastreamento

Os programas do Tracepoint são carregados automaticamente na inicialização. Após o carregamento, o programa tracepoint precisa ser ativado seguindo estas etapas:

  1. Chame bpf_obj_get() para acessar o programa fd na o local. 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 tracepoint sched_switch. definido no arquivo de origem myschedtp.c anterior (a verificação de erros não é exibida):

  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 arbitrários de chaves e valores. A biblioteca BPF do Android inclui uma classe android::BpfMap que usa modelos C++ para instanciar BpfMap com base no tipo de chave e valor do mapa em questão. O exemplo de código anterior demonstra o uso de um BpfMap com a chave 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. Então, o mapa pode ser acessado usando o funções geradas de maneira personalizada, que reconhecem o tipo, resultando em códigos mais limpos.

Para mais informações sobre BpfMap, consulte as fontes do Android (link em inglês).

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 todos os 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 C eBPF netd é usado pelo daemon de rede (netd) no Android para vários fins, 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 time_in_state eBPF C programa calcula o tempo que um app Android passa em diferentes Frequências da CPU, usadas para calcular a potência.

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