BPF para os máis pequenos, primeira parte: BPF estendido

Ao principio había unha tecnoloxía e chamábase BPF. Mirámola anterior, artigo do Antigo Testamento desta serie. En 2013, grazas aos esforzos de Alexei Starovoitov e Daniel Borkman, desenvolveuse e incluíuse no núcleo de Linux unha versión mellorada, optimizada para máquinas modernas de 64 bits. Esta nova tecnoloxía chamouse brevemente Internal BPF, logo renomeada Extended BPF, e agora, varios anos despois, todos a chaman simplemente BPF.

En liñas xerais, BPF permítelle executar código arbitrario proporcionado polo usuario no espazo do núcleo de Linux, e a nova arquitectura resultou ser tan exitosa que necesitaremos unha ducia de artigos máis para describir todas as súas aplicacións. (O único que os desenvolvedores non fixeron ben, como podes ver no código de rendemento a continuación, foi crear un logotipo decente.)

Este artigo describe a estrutura da máquina virtual BPF, interfaces do núcleo para traballar con BPF, ferramentas de desenvolvemento, así como unha breve e moi breve descrición das capacidades existentes, é dicir. todo o que necesitaremos no futuro para un estudo máis profundo das aplicacións prácticas de BPF.
BPF para os máis pequenos, primeira parte: BPF estendido

Resumo do artigo

Introdución á arquitectura BPF. En primeiro lugar, tomaremos a vista de paxaro da arquitectura BPF e esbozaremos os compoñentes principais.

Rexistros e sistema de mando da máquina virtual BPF. Xa tendo unha idea da arquitectura no seu conxunto, describiremos a estrutura da máquina virtual BPF.

Ciclo de vida dos obxectos BPF, sistema de ficheiros bpffs. Nesta sección, analizaremos o ciclo de vida dos obxectos BPF: programas e mapas.

Xestionar obxectos mediante a chamada ao sistema bpf. Con un pouco de comprensión do sistema xa instalado, finalmente veremos como crear e manipular obxectos desde o espazo do usuario usando unha chamada especial ao sistema - bpf(2).

Пишем программы BPF с помощью libbpf. Por suposto, pode escribir programas usando unha chamada do sistema. Pero é difícil. Para un escenario máis realista, os programadores nucleares desenvolveron unha biblioteca libbpf. Crearemos un esqueleto de aplicación BPF básico que usaremos en exemplos posteriores.

Axudantes do núcleo. Aquí aprenderemos como os programas BPF poden acceder ás funcións auxiliares do núcleo, unha ferramenta que, xunto cos mapas, amplía fundamentalmente as capacidades do novo BPF en comparación co clásico.

Acceso a mapas dos programas BPF. Ata este punto, saberemos o suficiente para comprender exactamente como podemos crear programas que utilicen mapas. E incluso imos dar unha ollada rápida ao gran e poderoso verificador.

Ferramentas de desenvolvemento. Sección de axuda sobre como montar as utilidades e o núcleo necesarios para os experimentos.

Conclusión. Ao final do artigo, os que lean ata aquí atoparán palabras motivadoras e unha breve descrición do que acontecerá nos seguintes artigos. Tamén enumeraremos unha serie de ligazóns para o autoestudo para aqueles que non teñan o desexo ou a capacidade de esperar a continuación.

Introdución á Arquitectura BPF

Antes de comezar a considerar a arquitectura BPF, referirémonos por última vez a (oh). BPF clásico, que foi desenvolvido como resposta á aparición das máquinas RISC e resolveu o problema do filtrado eficiente de paquetes. A arquitectura resultou ser tan exitosa que, nado nos noventa en Berkeley UNIX, foi portada á maioría dos sistemas operativos existentes, sobreviviu ata os tolos anos vinte e aínda está a atopar novas aplicacións.

O novo BPF foi desenvolvido como resposta á ubicuidade das máquinas de 64 bits, os servizos na nube e á maior necesidade de ferramentas para crear SDN (Ssoftware-ddefinido ntraballando). Desenvolvido por enxeñeiros de rede do núcleo como un substituto mellorado do clásico BPF, o novo BPF literalmente seis meses despois atopou aplicacións na difícil tarefa de rastrexar sistemas Linux, e agora, seis anos despois da súa aparición, necesitaremos un artigo completo para enumera os distintos tipos de programas.

Imaxes divertidas

No seu núcleo, BPF é unha máquina virtual sandbox que che permite executar código "arbitrario" no espazo do núcleo sen comprometer a seguridade. Os programas BPF créanse no espazo do usuario, cárganse no núcleo e conéctanse a algunha fonte de eventos. Un evento pode ser, por exemplo, a entrega dun paquete a unha interface de rede, o lanzamento dalgunha función do núcleo, etc. No caso dun paquete, o programa BPF terá acceso aos datos e metadatos do paquete (para lectura e, posiblemente, escritura, segundo o tipo de programa); no caso de executar unha función do núcleo, os argumentos de a función, incluíndo punteiros á memoria do núcleo, etc.

Vexamos máis de cerca este proceso. Para comezar, imos falar da primeira diferenza co clásico BPF, programas para os cales foron escritos en ensamblador. Na nova versión, a arquitectura foi ampliada para que os programas se puidesen escribir en linguaxes de alto nivel, principalmente, por suposto, en C. Para iso desenvolveuse un backend para llvm, que permite xerar bytecode para a arquitectura BPF.

BPF para os máis pequenos, primeira parte: BPF estendido

A arquitectura BPF foi deseñada, en parte, para funcionar de forma eficiente en máquinas modernas. Para que isto funcione na práctica, o bytecode BPF, unha vez cargado no núcleo, tradúcese a código nativo mediante un compoñente chamado compilador JIT (JUst In Time). A continuación, se lembras, no BPF clásico o programa cargouse no núcleo e adxuntouse á fonte do evento atomicamente, no contexto dunha única chamada ao sistema. Na nova arquitectura, isto ocorre en dúas etapas: primeiro, o código cárgase no núcleo mediante unha chamada ao sistema. bpf(2)e despois, posteriormente, a través doutros mecanismos que varían segundo o tipo de programa, o programa adxuntase á fonte do evento.

Aquí o lector pode ter unha pregunta: foi posible? Como se garante a seguridade de execución deste código? A seguridade na execución está garantida pola fase de carga dos programas BPF chamado verificador (en inglés esta etapa chámase verificador e seguirei usando a palabra inglesa):

BPF para os máis pequenos, primeira parte: BPF estendido

Verifier é un analizador estático que garante que un programa non perturbe o funcionamento normal do núcleo. Isto, por certo, non significa que o programa non poida interferir co funcionamento do sistema: os programas BPF, dependendo do tipo, poden ler e reescribir seccións da memoria do núcleo, devolver valores de funcións, recortar, engadir, reescribir. e incluso reenviar paquetes de rede. Verifier garante que a execución dun programa BPF non fallará o núcleo e que un programa que, segundo as regras, teña acceso de escritura, por exemplo, os datos dun paquete saínte, non poderá sobrescribir a memoria do núcleo fóra do paquete. Observaremos o verificador con máis detalle na sección correspondente, despois de que nos familiaricemos con todos os outros compoñentes de BPF.

Entón, que aprendemos ata agora? O usuario escribe un programa en C, cárgao no núcleo mediante unha chamada ao sistema bpf(2), onde é verificado por un verificador e traducido ao bytecode nativo. A continuación, o mesmo usuario ou outro conecta o programa á fonte do evento e comeza a executarse. Separar o arranque e a conexión é necesario por varias razóns. En primeiro lugar, executar un verificador é relativamente caro e ao descargar o mesmo programa varias veces perdemos tempo no ordenador. En segundo lugar, a forma en que se conecta un programa depende do seu tipo, e unha interface "universal" desenvolvida hai un ano pode non ser axeitada para novos tipos de programas. (Aínda que agora que a arquitectura está cada vez máis madura, hai unha idea de unificar esta interface a nivel libbpf.)

O lector atento pode notar que aínda non rematamos coas imaxes. De feito, todo o anterior non explica por que BPF cambia fundamentalmente a imaxe en comparación co BPF clásico. Dúas innovacións que amplían significativamente o ámbito de aplicabilidade son a capacidade de usar memoria compartida e funcións auxiliares do núcleo. En BPF, a memoria compartida implícase mediante os chamados mapas: estruturas de datos compartidas cunha API específica. Probablemente recibiron este nome porque o primeiro tipo de mapa que apareceu foi unha táboa hash. Entón apareceron matrices, táboas hash locais (por CPU) e matrices locais, árbores de busca, mapas que conteñan punteiros a programas BPF e moito máis. O que nos interesa agora é que os programas BPF agora teñen a capacidade de manter o estado entre chamadas e compartilo con outros programas e co espazo do usuario.

Accédese a Maps desde os procesos do usuario mediante unha chamada ao sistema bpf(2), e de programas BPF que se executan no núcleo usando funcións auxiliares. Ademais, existen axudantes non só para traballar con mapas, senón tamén para acceder a outras capacidades do núcleo. Por exemplo, os programas BPF poden usar funcións auxiliares para reenviar paquetes a outras interfaces, xerar eventos de perf, acceder ás estruturas do núcleo, etc.

BPF para os máis pequenos, primeira parte: BPF estendido

En resumo, BPF ofrece a capacidade de cargar código de usuario arbitrario, é dicir, probado polo verificador, no espazo do núcleo. Este código pode gardar estado entre chamadas e intercambiar datos co espazo do usuario, e tamén ten acceso aos subsistemas do núcleo permitidos por este tipo de programas.

Isto xa é semellante ás capacidades proporcionadas polos módulos do núcleo, en comparación coas que BPF ten algunhas vantaxes (por suposto, só podes comparar aplicacións similares, por exemplo, o rastrexo do sistema; non podes escribir un controlador arbitrario con BPF). Pódese observar un limiar de entrada máis baixo (algunhas utilidades que usan BPF non requiren que o usuario teña coñecementos de programación do núcleo, ou habilidades de programación en xeral), seguridade no tempo de execución (levante a man nos comentarios para aqueles que non romperon o sistema ao escribir). ou módulos de proba), atomicidade: hai un tempo de inactividade ao recargar módulos e o subsistema BPF garante que non se perda ningún evento (para ser xustos, isto non é certo para todos os tipos de programas BPF).

A presenza de tales capacidades fai que BPF sexa unha ferramenta universal para expandir o núcleo, o que se confirma na práctica: cada vez engádense máis novos tipos de programas a BPF, cada vez máis grandes empresas usan BPF nos servidores de combate 24x7, cada vez máis. as startups constrúen o seu negocio en solucións baseadas en BPF. BPF utilízase en todas partes: na protección contra ataques DDoS, na creación de SDN (por exemplo, a implementación de redes para kubernetes), como principal ferramenta de rastrexo do sistema e colector de estatísticas, en sistemas de detección de intrusos e sistemas sandbox, etc.

Rematemos aquí a parte xeral do artigo e vexamos a máquina virtual e o ecosistema BPF con máis detalle.

Digresión: utilidades

Para poder executar os exemplos das seguintes seccións, é posible que necesite unha serie de utilidades, polo menos llvm/clang con soporte bpf e bpftool. Na sección Ferramentas de desenvolvemento Podes ler as instrucións para montar as utilidades, así como o teu núcleo. Esta sección colócase a continuación para non perturbar a harmonía da nosa presentación.

Sistema de instrucción e rexistros de máquinas virtuais BPF

A arquitectura e o sistema de mando de BPF foron desenvolvidos tendo en conta o feito de que os programas se escribirán en linguaxe C e, tras cargalos no núcleo, traduciranse a código nativo. Polo tanto, o número de rexistros e o conxunto de comandos elixíronse coa atención á intersección, no sentido matemático, das capacidades das máquinas modernas. Ademais, impuxéronse varias restricións aos programas, por exemplo, ata hai pouco tempo non era posible escribir bucles e subrutinas, e o número de instrucións limitouse a 4096 (agora os programas con privilexios poden cargar ata un millón de instrucións).

BPF ten once rexistros de 64 bits accesibles polo usuario r0-r10 e un contador de programas. Rexístrate r10 contén un punteiro de marco e é só de lectura. Os programas teñen acceso a unha pila de 512 bytes en tempo de execución e a unha cantidade ilimitada de memoria compartida en forma de mapas.

Os programas BPF poden executar un conxunto específico de axudantes do núcleo de tipo programa e, máis recentemente, funcións habituais. Cada función chamada pode levar ata cinco argumentos, pasados ​​en rexistros r1-r5, e pásase o valor de retorno a r0. Garántese que despois de volver da función, o contido dos rexistros r6-r9 Non cambiará.

Para unha tradución eficiente do programa, rexístrase r0-r11 para todas as arquitecturas compatibles están mapeadas de forma única a rexistros reais, tendo en conta as características ABI da arquitectura actual. Por exemplo, para x86_64 rexistros r1-r5, que se usan para pasar parámetros de función, aparecen activados rdi, rsi, rdx, rcx, r8, que se usan para pasar parámetros ás funcións activadas x86_64. Por exemplo, o código da esquerda tradúcese ao código da dereita así:

1:  (b7) r1 = 1                    mov    $0x1,%rdi
2:  (b7) r2 = 2                    mov    $0x2,%rsi
3:  (b7) r3 = 3                    mov    $0x3,%rdx
4:  (b7) r4 = 4                    mov    $0x4,%rcx
5:  (b7) r5 = 5                    mov    $0x5,%r8
6:  (85) call pc+1                 callq  0x0000000000001ee8

Rexistrarse r0 tamén se usa para devolver o resultado da execución do programa, e no rexistro r1 pásase ao programa un punteiro ao contexto; dependendo do tipo de programa, este podería ser, por exemplo, unha estrutura struct xdp_md (para XDP) ou estrutura struct __sk_buff (para diferentes programas de rede) ou estrutura struct pt_regs (para diferentes tipos de programas de rastrexo), etc.

Así, tiñamos un conxunto de rexistros, axudantes do núcleo, unha pila, un punteiro de contexto e memoria compartida en forma de mapas. Non é que todo isto sexa absolutamente necesario na viaxe, pero...

Continuemos coa descrición e falemos do sistema de comandos para traballar con estes obxectos. Todos (Case todos) As instrucións BPF teñen un tamaño fixo de 64 bits. Se miras unha instrución nunha máquina Big Endian de 64 bits verás

BPF para os máis pequenos, primeira parte: BPF estendido

Aquí Code - esta é a codificación da instrución, Dst/Src son as codificacións do receptor e da fonte, respectivamente, Off - Sangría asinada de 16 bits e Imm é un enteiro con signo de 32 bits usado nalgunhas instrucións (semellante á constante K de cBPF). Codificación Code ten un dos dous tipos:

BPF para os máis pequenos, primeira parte: BPF estendido

As clases de instrucións 0, 1, 2, 3 definen comandos para traballar coa memoria. Eles chámanse, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respectivamente. Clases 4, 7 (BPF_ALU, BPF_ALU64) constitúen un conxunto de instrucións ALU. Clases 5, 6 (BPF_JMP, BPF_JMP32) conteñen instrucións de salto.

O plan adicional para estudar o sistema de instrucións BPF é o seguinte: en lugar de enumerar meticulosamente todas as instrucións e os seus parámetros, veremos un par de exemplos nesta sección e a partir deles quedará claro como funcionan realmente as instrucións e como desmontar manualmente calquera ficheiro binario para BPF. Para consolidar o material máis adiante no artigo, tamén atoparémonos con instrucións individuais nas seccións sobre Verificador, compilador JIT, tradución de BPF clásico, así como ao estudar mapas, chamar funcións, etc.

Cando falamos de instrucións individuais, referirémonos aos ficheiros principais bpf.h и bpf_common.h, que definen os códigos numéricos das instrucións BPF. Ao estudar arquitectura por conta propia e/ou analizar binarios, podes atopar a semántica nas seguintes fontes, ordenadas por orde de complexidade: Especificación eBPF non oficial, Guía de referencia BPF e XDP, conxunto de instrucións, Documentación/redes/filter.txt e, por suposto, no código fonte de Linux: verificador, JIT, intérprete BPF.

Exemplo: desmontar BPF na cabeza

Vexamos un exemplo no que compilamos un programa readelf-example.c e mira o binario resultante. Desvelaremos o contido orixinal readelf-example.c a continuación, despois de restaurar a súa lóxica a partir de códigos binarios:

$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Hex dump of section '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................

Primeira columna na saída readelf é unha sangría e, polo tanto, o noso programa consta de catro comandos:

Code Dst Src Off  Imm
b7   0   0   0000 01000000
15   0   1   0100 00000000
b7   0   0   0000 02000000
95   0   0   0000 00000000

Os códigos de comando son iguais b7, 15, b7 и 95. Lembre que os tres bits menos significativos son a clase de instrución. No noso caso, o cuarto bit de todas as instrucións está baleiro, polo que as clases de instrucións son iguais a 7, 5, 7, 5, respectivamente. A clase 7 é BPF_ALU64, e 5 é BPF_JMP. Para ambas as clases, o formato de instrución é o mesmo (ver arriba) e podemos reescribir o noso programa así (ao mesmo tempo reescribiremos as columnas restantes en forma humana):

Op S  Class   Dst Src Off  Imm
b  0  ALU64   0   0   0    1
1  0  JMP     0   1   1    0
b  0  ALU64   0   0   0    2
9  0  JMP     0   0   0    0

Funcionamento b clase ALU64 - É BPF_MOV. Atribúe un valor ao rexistro de destino. Se o bit está configurado s (fonte), entón o valor tómase do rexistro de orixe e, se, como no noso caso, non está definido, entón o valor tómase do campo Imm. Así que na primeira e na terceira instrución realizamos a operación r0 = Imm. Ademais, a operación JMP clase 1 é BPF_JEQ (saltar se é igual). No noso caso, dende o bit S é cero, compara o valor do rexistro de orixe co campo Imm. Se os valores coinciden, a transición ocorre a PC + Offonde PC, como é habitual, contén o enderezo da seguinte instrución. Finalmente, a operación JMP Class 9 é BPF_EXIT. Esta instrución remata o programa, volvendo ao núcleo r0. Engademos unha nova columna á nosa táboa:

Op    S  Class   Dst Src Off  Imm    Disassm
MOV   0  ALU64   0   0   0    1      r0 = 1
JEQ   0  JMP     0   1   1    0      if (r1 == 0) goto pc+1
MOV   0  ALU64   0   0   0    2      r0 = 2
EXIT  0  JMP     0   0   0    0      exit

Podemos reescribir isto nunha forma máis conveniente:

     r0 = 1
     if (r1 == 0) goto END
     r0 = 2
END:
     exit

Se lembramos o que está no rexistro r1 pásase ao programa un punteiro ao contexto desde o núcleo e no rexistro r0 o valor devólvese ao núcleo, entón podemos ver que se o punteiro ao contexto é cero, entón devolvemos 1, e en caso contrario - 2. Comprobamos que temos razón mirando a fonte:

$ cat readelf-example.c
int foo(void *ctx)
{
        return ctx ? 2 : 1;
}

Si, é un programa sen sentido, pero tradúcese en só catro instrucións sinxelas.

Exemplo de excepción: instrución de 16 bytes

Mencionamos anteriormente que algunhas instrucións ocupan máis de 64 bits. Isto aplícase, por exemplo, ás instrucións lddw (Código = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — carga unha palabra dobre dos campos no rexistro Imm. O feito é que Imm ten un tamaño de 32 e unha palabra dobre é de 64 bits, polo que cargar un valor inmediato de 64 bits nun rexistro nunha instrución de 64 bits non funcionará. Para iso, utilízanse dúas instrucións adxacentes para almacenar a segunda parte do valor de 64 bits no campo Imm. Un exemplo:

$ cat x64.c
long foo(void *ctx)
{
        return 0x11223344aabbccdd;
}
$ clang -target bpf -c x64.c -o x64.o -O2
$ llvm-readelf -x .text x64.o
Hex dump of section '.text':
0x00000000 18000000 ddccbbaa 00000000 44332211 ............D3".
0x00000010 95000000 00000000                   ........

Só hai dúas instrucións nun programa binario:

Binary                                 Disassm
18000000 ddccbbaa 00000000 44332211    r0 = Imm[0]|Imm[1]
95000000 00000000                      exit

Reunirémonos de novo con instrucións lddw, cando falamos de traslados e de traballo con mapas.

Exemplo: desmontaxe de BPF utilizando ferramentas estándar

Entón, aprendemos a ler códigos binarios BPF e estamos preparados para analizar calquera instrución se é necesario. Non obstante, paga a pena dicir que na práctica é máis cómodo e rápido desmontar programas usando ferramentas estándar, por exemplo:

$ llvm-objdump -d x64.o

Disassembly of section .text:

0000000000000000 <foo>:
 0: 18 00 00 00 dd cc bb aa 00 00 00 00 44 33 22 11 r0 = 1234605617868164317 ll
 2: 95 00 00 00 00 00 00 00 exit

Ciclo de vida dos obxectos BPF, sistema de ficheiros bpffs

(Primeiro aprendín algúns dos detalles descritos nesta subsección de publicación Alexei Starovoitov en Blog BPF.)

Os obxectos BPF (programas e mapas) créanse a partir do espazo do usuario mediante comandos BPF_PROG_LOAD и BPF_MAP_CREATE chamada do sistema bpf(2), falaremos de como ocorre exactamente isto na seguinte sección. Isto crea estruturas de datos do núcleo e para cada unha delas refcount (reconto de referencias) establécese en un e devólvese ao usuario un descritor de ficheiro que apunta ao obxecto. Despois de pechar o mango refcount o obxecto redúcese nun, e cando chega a cero, o obxecto destrúese.

Se o programa usa mapas, entón refcount estes mapas increméntanse un despois de cargar o programa, é dicir. os seus descritores de ficheiros pódense pechar desde o proceso do usuario e aínda así refcount non pasará a ser cero:

BPF para os máis pequenos, primeira parte: BPF estendido

Despois de cargar correctamente un programa, adoitamos anexar a algún tipo de xerador de eventos. Por exemplo, podemos poñelo nunha interface de rede para procesar paquetes entrantes ou conectalo a algún tracepoint no núcleo. Neste punto, o contador de referencia tamén aumentará nun un e poderemos pechar o descritor de ficheiros no programa cargador.

Que pasa se agora pechamos o cargador de arranque? Depende do tipo de xerador de eventos (gancho). Todos os ganchos de rede existirán despois de que se complete o cargador, estes son os chamados ganchos globais. E, por exemplo, os programas de rastrexo lanzaranse despois de que finalice o proceso que os creou (e, polo tanto, chámanse locais, de "local ao proceso"). Tecnicamente, os hooks locais sempre teñen un descritor de ficheiro correspondente no espazo do usuario e, polo tanto, péchanse cando se pecha o proceso, pero os hooks globais non. Na seguinte figura, usando cruces vermellas, intento mostrar como a terminación do programa cargador afecta a vida útil dos obxectos no caso dos ganchos locais e globais.

BPF para os máis pequenos, primeira parte: BPF estendido

Por que hai unha distinción entre ganchos locais e globais? Executar algúns tipos de programas de rede ten sentido sen un espazo de usuario, por exemplo, imaxina a protección DDoS: o cargador de arranque escribe as regras e conecta o programa BPF á interface de rede, despois de que o cargador de arranque pode matar a si mesmo. Por outra banda, imaxina un programa de rastrexo de depuración que escribiches de xeonllos en dez minutos; cando remate, desexa que non quede lixo no sistema e os ganchos locais garantirán iso.

Por outra banda, imaxine que quere conectarse a un punto de rastrexo no núcleo e recoller estatísticas durante moitos anos. Neste caso, querería completar a parte do usuario e volver ás estatísticas de cando en vez. O sistema de ficheiros bpf ofrece esta oportunidade. É un pseudosistema de ficheiros só en memoria que permite a creación de ficheiros que fan referencia a obxectos BPF e, polo tanto, refcount obxectos. Despois diso, o cargador pode saír e os obxectos que creou permanecerán vivos.

BPF para os máis pequenos, primeira parte: BPF estendido

A creación de ficheiros en bpffs que fan referencia a obxectos BPF denomínase "fixación" (como na seguinte frase: "o proceso pode fixar un programa ou mapa BPF"). Crear obxectos de ficheiro para obxectos BPF ten sentido non só para estender a vida útil dos obxectos locais, senón tamén para a usabilidade dos obxectos globais; volvendo ao exemplo do programa de protección global DDoS, queremos poder ver estatísticas de cando en cando.

O sistema de ficheiros BPF adoita estar instalado /sys/fs/bpf, pero tamén se pode montar localmente, por exemplo, así:

$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint

Os nomes do sistema de ficheiros créanse mediante o comando BPF_OBJ_PIN Chamada ao sistema BPF. Para ilustralo, imos coller un programa, compilalo, cargalo e fixalo bpffs. O noso programa non fai nada útil, só presentamos o código para que poidas reproducir o exemplo:

$ cat test.c
__attribute__((section("xdp"), used))
int test(void *ctx)
{
        return 0;
}

char _license[] __attribute__((section("license"), used)) = "GPL";

Compilemos este programa e creemos unha copia local do sistema de ficheiros bpffs:

$ clang -target bpf -c test.c -o test.o
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint

Agora imos descargar o noso programa usando a utilidade bpftool e mira as chamadas do sistema que se acompañan bpf(2) (elimináronse algunhas liñas irrelevantes da saída de strace):

$ sudo strace -e bpf bpftool prog load ./test.o bpf-mountpoint/test
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="test", ...}, 120) = 3
bpf(BPF_OBJ_PIN, {pathname="bpf-mountpoint/test", bpf_fd=3}, 120) = 0

Aquí cargamos o programa usando BPF_PROG_LOAD, recibiu un descritor de ficheiro do núcleo 3 e usando o comando BPF_OBJ_PIN fixou este descritor de ficheiro como ficheiro "bpf-mountpoint/test". Despois diso, o programa do cargador de arranque bpftool rematou de funcionar, pero o noso programa permaneceu no núcleo, aínda que non o conectamos a ningunha interface de rede:

$ sudo bpftool prog | tail -3
783: xdp  name test  tag 5c8ba0cf164cb46c  gpl
        loaded_at 2020-05-05T13:27:08+0000  uid 0
        xlated 24B  jited 41B  memlock 4096B

Podemos eliminar o obxecto ficheiro normalmente unlink(2) e despois eliminarase o programa correspondente:

$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory

Eliminando obxectos

Falando sobre a eliminación de obxectos, é necesario aclarar que despois de desconectar o programa do gancho (xerador de eventos), nin un novo evento activará o seu lanzamento, non obstante, todas as instancias actuais do programa completaranse na orde normal. .

Algúns tipos de programas BPF permítenche substituír o programa sobre a marcha, é dicir. proporcionar atomicidade de secuencia replace = detach old program, attach new program. Neste caso, todas as instancias activas da versión antiga do programa rematarán o seu traballo e crearanse novos controladores de eventos a partir do novo programa, e aquí "atomicidade" significa que non se perderá ningún evento.

Anexando programas a fontes de eventos

Neste artigo, non describiremos por separado a conexión de programas a fontes de eventos, xa que ten sentido estudar isto no contexto dun tipo específico de programa. Cm. exemplo a continuación, no que mostramos como están conectados programas como XDP.

Manipulación de obxectos mediante a chamada ao sistema bpf

Programas BPF

Todos os obxectos BPF son creados e xestionados desde o espazo do usuario mediante unha chamada ao sistema bpf, tendo o seguinte prototipo:

#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

Aquí está o equipo cmd é un dos valores de tipo enum bpf_cmd, attr — un punteiro a parámetros para un programa específico e size — tamaño do obxecto segundo o punteiro, é dicir. xeralmente isto sizeof(*attr). No núcleo 5.8 a chamada do sistema bpf admite 34 comandos diferentes e determinación de union bpf_attr ocupa 200 liñas. Pero isto non debe deixarnos intimidar, xa que nos familiarizaremos cos comandos e parámetros ao longo de varios artigos.

Comezamos polo equipo BPF_PROG_LOAD, que crea programas BPF: toma un conxunto de instrucións BPF e cárgao no núcleo. No momento da carga, lánzase o verificador e, a continuación, o compilador JIT e, tras a execución exitosa, devólvese ao usuario o descritor do ficheiro do programa. Vimos o que lle pasa a continuación na sección anterior sobre o ciclo de vida dos obxectos BPF.

Agora escribiremos un programa personalizado que cargará un programa BPF sinxelo, pero primeiro debemos decidir que tipo de programa queremos cargar; teremos que seleccionar тип e no marco deste tipo, escribe un programa que supere a proba verificadora. Non obstante, para non complicar o proceso, aquí tes unha solución preparada: tomaremos un programa como BPF_PROG_TYPE_XDP, que devolverá o valor XDP_PASS (saltar todos os paquetes). No ensamblador BPF parece moi sinxelo:

r0 = 2
exit

Despois de decidirnos que cargaremos, podemos dicirche como o faremos:

#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

static inline __u64 ptr_to_u64(const void *ptr)
{
        return (__u64) (unsigned long) ptr;
}

int main(void)
{
    struct bpf_insn insns[] = {
        {
            .code = BPF_ALU64 | BPF_MOV | BPF_K,
            .dst_reg = BPF_REG_0,
            .imm = XDP_PASS
        },
        {
            .code = BPF_JMP | BPF_EXIT
        },
    };

    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_XDP,
        .insns     = ptr_to_u64(insns),
        .insn_cnt  = sizeof(insns)/sizeof(insns[0]),
        .license   = ptr_to_u64("GPL"),
    };

    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

Os eventos interesantes nun programa comezan coa definición dunha matriz insns - o noso programa BPF en código máquina. Neste caso, cada instrución do programa BPF está empaquetada na estrutura bpf_insn. Primeiro elemento insns cumpre coas instrucións r0 = 2, o segundo - exit.

Retirada. O núcleo define macros máis convenientes para escribir códigos de máquina e usar o ficheiro de cabeceira do núcleo tools/include/linux/filter.h poderiamos escribir

struct bpf_insn insns[] = {
    BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
    BPF_EXIT_INSN()
};

Pero dado que escribir programas BPF en código nativo só é necesario para escribir probas no núcleo e artigos sobre BPF, a ausencia destas macros non complica realmente a vida do programador.

Despois de definir o programa BPF, pasamos a cargalo no núcleo. O noso conxunto minimalista de parámetros attr inclúe o tipo de programa, o conxunto e o número de instrucións, a licenza necesaria e o nome "woo", que utilizamos para atopar o noso programa no sistema despois da descarga. O programa, como prometeu, cárgase no sistema mediante unha chamada do sistema bpf.

Ao final do programa acabamos nun bucle infinito que simula a carga útil. Sen el, o programa será eliminado polo núcleo cando se peche o descritor de ficheiros que nos devolveu a chamada do sistema. bpf, e non o veremos no sistema.

Ben, estamos preparados para a proba. Imos montar e executar o programa baixo stracepara comprobar que todo funciona como debería:

$ clang -g -O2 simple-prog.c -o simple-prog

$ sudo strace ./simple-prog
execve("./simple-prog", ["./simple-prog"], 0x7ffc7b553480 /* 13 vars */) = 0
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0x7ffe03c4ed50, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

Todo está ben, bpf(2) devolveunos o manexo 3 e entramos nun bucle infinito con pause(). Imos tentar atopar o noso programa no sistema. Para iso iremos a outro terminal e utilizaremos a utilidade bpftool:

# bpftool prog | grep -A3 woo
390: xdp  name woo  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-31T24:66:44+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        pids simple-prog(10381)

Vemos que hai un programa cargado no sistema woo cuxo ID global é 390 e está actualmente en proceso simple-prog hai un descritor de ficheiro aberto que apunta ao programa (e se simple-prog rematará o traballo, entón woo desaparecerá). Como era de esperar, o programa woo leva 16 bytes -dúas instrucións- de códigos binarios na arquitectura BPF, pero na súa forma nativa (x86_64) xa son 40 bytes. Vexamos o noso programa na súa forma orixinal:

# bpftool prog dump xlated id 390
   0: (b7) r0 = 2
   1: (95) exit

sen sorpresas. Agora vexamos o código xerado polo compilador JIT:

# bpftool prog dump jited id 390
bpf_prog_3b185187f1855c4c_woo:
   0:   nopl   0x0(%rax,%rax,1)
   5:   push   %rbp
   6:   mov    %rsp,%rbp
   9:   sub    $0x0,%rsp
  10:   push   %rbx
  11:   push   %r13
  13:   push   %r14
  15:   push   %r15
  17:   pushq  $0x0
  19:   mov    $0x2,%eax
  1e:   pop    %rbx
  1f:   pop    %r15
  21:   pop    %r14
  23:   pop    %r13
  25:   pop    %rbx
  26:   leaveq
  27:   retq

non moi eficaz para exit(2), pero para ser xusto, o noso programa é demasiado sinxelo, e para programas non triviais, o prólogo e o epílogo engadidos polo compilador JIT son, por suposto, necesarios.

Mapas

Os programas BPF poden usar áreas de memoria estruturada accesibles tanto para outros programas BPF como para programas no espazo de usuario. Estes obxectos chámanse mapas e nesta sección mostraremos como manipulalos mediante unha chamada ao sistema bpf.

Digamos de inmediato que as capacidades dos mapas non se limitan só ao acceso á memoria compartida. Hai mapas de propósitos especiais que conteñen, por exemplo, punteiros a programas BPF ou punteiros a interfaces de rede, mapas para traballar con eventos perf, etc. Non falaremos delas aquí, para non confundir ao lector. Ademais, ignoramos os problemas de sincronización, xa que isto non é importante para os nosos exemplos. Pódese atopar unha lista completa dos tipos de mapas dispoñibles en <linux/bpf.h>, e neste apartado tomaremos como exemplo o primeiro tipo historicamente, a táboa hash BPF_MAP_TYPE_HASH.

Se creas unha táboa hash en, por exemplo, C++, dirías unordered_map<int,long> woo, que en ruso significa “Necesito unha mesa woo tamaño ilimitado, cuxas claves son de tipo int, e os valores son o tipo long" Para crear unha táboa hash BPF, necesitamos facer o mesmo, excepto que temos que especificar o tamaño máximo da táboa e, en lugar de especificar os tipos de claves e valores, necesitamos especificar os seus tamaños en bytes. . Para crear mapas use o comando BPF_MAP_CREATE chamada do sistema bpf. Vexamos un programa máis ou menos mínimo que crea un mapa. Despois do programa anterior que carga programas BPF, este debería parecerche sinxelo:

$ cat simple-map.c
#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

int main(void)
{
    union bpf_attr attr = {
        .map_type = BPF_MAP_TYPE_HASH,
        .key_size = sizeof(int),
        .value_size = sizeof(int),
        .max_entries = 4,
    };
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

Aquí definimos un conxunto de parámetros attr, no que dicimos "Necesito unha táboa hash con claves e valores de tamaño sizeof(int), no que podo poñer un máximo de catro elementos". Ao crear mapas BPF, pode especificar outros parámetros, por exemplo, do mesmo xeito que no exemplo co programa, especificamos o nome do obxecto como "woo".

Compilemos e executemos o programa:

$ clang -g -O2 simple-map.c -o simple-map
$ sudo strace ./simple-map
execve("./simple-map", ["./simple-map"], 0x7ffd40a27070 /* 14 vars */) = 0
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=4, max_entries=4, map_name="woo", ...}, 72) = 3
pause(

Aquí está a chamada do sistema bpf(2) devolveunos o número do mapa descritor 3 e entón o programa, como era de esperar, espera máis instrucións na chamada do sistema pause(2).

Agora imos enviar o noso programa a un segundo plano ou abrir outro terminal e mirar o noso obxecto usando a utilidade bpftool (podemos distinguir o noso mapa dos outros polo seu nome):

$ sudo bpftool map
...
114: hash  name woo  flags 0x0
        key 4B  value 4B  max_entries 4  memlock 4096B
...

O número 114 é o ID global do noso obxecto. Calquera programa do sistema pode usar este ID para abrir un mapa existente mediante o comando BPF_MAP_GET_FD_BY_ID chamada do sistema bpf.

Agora podemos xogar coa nosa táboa hash. Vexamos o seu contido:

$ sudo bpftool map dump id 114
Found 0 elements

Baleiro. Poñémoslle un valor hash[1] = 1:

$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0

Vexamos de novo a táboa:

$ sudo bpftool map dump id 114
key: 01 00 00 00  value: 01 00 00 00
Found 1 element

Hurra! Conseguimos engadir un elemento. Teña en conta que temos que traballar a nivel de bytes para facelo, xa que bptftool non sabe de que tipo son os valores da táboa hash. (Este coñecemento pódese transferir a ela usando BTF, pero agora máis sobre iso).

Como exactamente bpftool le e engade elementos? Vexamos debaixo do capó:

$ sudo strace -e bpf bpftool map dump id 114
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=NULL, next_key=0x55856ab65280}, 120) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=3, key=0x55856ab65280, value=0x55856ab652a0}, 120) = 0
key: 01 00 00 00  value: 01 00 00 00
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=0x55856ab65280, next_key=0x55856ab65280}, 120) = -1 ENOENT

Primeiro abrimos o mapa polo seu ID global usando o comando BPF_MAP_GET_FD_BY_ID и bpf(2) devolveunos o descritor 3. Seguindo usando o comando BPF_MAP_GET_NEXT_KEY atopamos a primeira clave na táboa ao pasar NULL como un punteiro á tecla "anterior". Se temos a chave podemos facelo BPF_MAP_LOOKUP_ELEMque devolve un valor a un punteiro value. O seguinte paso é tentar atopar o seguinte elemento pasando un punteiro á chave actual, pero a nosa táboa só contén un elemento e o comando BPF_MAP_GET_NEXT_KEY volve ENOENT.

Está ben, cambiemos o valor pola clave 1, digamos que a nosa lóxica empresarial require rexistrarse hash[1] = 2:

$ sudo strace -e bpf bpftool map update id 114 key 1 0 0 0 value 2 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x55dcd72be260, value=0x55dcd72be280, flags=BPF_ANY}, 120) = 0

Como era de esperar, é moi sinxelo: o comando BPF_MAP_GET_FD_BY_ID abre o noso mapa por ID e o comando BPF_MAP_UPDATE_ELEM sobrescribe o elemento.

Así, despois de crear unha táboa hash dun programa, podemos ler e escribir o seu contido desde outro. Teña en conta que se puidésemos facelo desde a liña de comandos, calquera outro programa do sistema pode facelo. Ademais dos comandos descritos anteriormente, para traballar con mapas desde o espazo do usuario, A seguir:

  • BPF_MAP_LOOKUP_ELEM: atopar o valor por clave
  • BPF_MAP_UPDATE_ELEM: actualizar/crear valor
  • BPF_MAP_DELETE_ELEM: eliminar a chave
  • BPF_MAP_GET_NEXT_KEY: busca a seguinte (ou primeira) clave
  • BPF_MAP_GET_NEXT_ID: permíteche percorrer todos os mapas existentes, así é como funciona bpftool map
  • BPF_MAP_GET_FD_BY_ID: abre un mapa existente polo seu ID global
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: actualiza atomicamente o valor dun obxecto e devolve o antigo
  • BPF_MAP_FREEZE: fai que o mapa sexa inmutable desde o espazo de usuario (esta operación non se pode desfacer)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: operacións masivas. Por exemplo, BPF_MAP_LOOKUP_AND_DELETE_BATCH - Esta é a única forma fiable de ler e restablecer todos os valores do mapa

Non todos estes comandos funcionan para todos os tipos de mapas, pero, en xeral, traballar con outros tipos de mapas desde o espazo do usuario parece exactamente o mesmo que traballar con táboas hash.

Por mor da orde, imos rematar os nosos experimentos de táboas hash. Lembras que creamos unha táboa que pode conter ata catro claves? Engadimos algúns elementos máis:

$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0

Ata aquí todo ben:

$ sudo bpftool map dump id 114
key: 01 00 00 00  value: 01 00 00 00
key: 02 00 00 00  value: 01 00 00 00
key: 04 00 00 00  value: 01 00 00 00
key: 03 00 00 00  value: 01 00 00 00
Found 4 elements

Tentemos engadir un máis:

$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long

Como era de esperar, non o conseguimos. Vexamos o erro con máis detalle:

$ sudo strace -e bpf bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=80, info=0x7ffe6c626da0}}, 120) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x56049ded5260, value=0x56049ded5280, flags=BPF_ANY}, 120) = -1 E2BIG (Argument list too long)
Error: update failed: Argument list too long
+++ exited with 255 +++

Todo está ben: como era de esperar, o equipo BPF_MAP_UPDATE_ELEM tenta crear unha nova quinta clave, pero falla E2BIG.

Así, podemos crear e cargar programas BPF, así como crear e xestionar mapas desde o espazo do usuario. Agora é lóxico ver como podemos usar mapas dos propios programas BPF. Poderiamos falar disto na linguaxe de programas difíciles de ler en códigos macro de máquinas, pero de feito chegou o momento de mostrar como se escriben e manteñen os programas BPF, usando libbpf.

(Para lectores que non estean satisfeitos coa falta dun exemplo de baixo nivel: analizaremos polo miúdo programas que utilizan mapas e funcións auxiliares creadas mediante libbpf e contarche o que ocorre a nivel de instrución. Para lectores insatisfeitos moito, engadimos exemplo no lugar correspondente do artigo).

Escribir programas BPF usando libbpf

Escribir programas BPF usando códigos de máquina pode ser interesante só a primeira vez, e despois comeza a saciedade. Neste momento debes centrar a túa atención llvm, que ten un backend para xerar código para a arquitectura BPF, así como unha biblioteca libbpf, que permite escribir o lado do usuario das aplicacións BPF e cargar o código dos programas BPF xerados mediante llvm/clang.

De feito, como veremos neste artigo e nos seguintes, libbpf fai bastante traballo sen el (ou ferramentas similares - iproute2, libbcc, libbpf-go, etc.) é imposible vivir. Unha das características asasinas do proxecto libbpf é BPF CO-RE (Compile Once, Run Everywhere): un proxecto que che permite escribir programas BPF que son portátiles dun núcleo a outro, coa capacidade de executarse en diferentes API (por exemplo, cando a estrutura do núcleo cambia desde a versión). á versión). Para poder traballar con CO-RE, o seu núcleo debe estar compilado con soporte BTF (describimos como facelo na sección Ferramentas de desenvolvemento. Podes comprobar se o teu núcleo está construído con BTF ou non de forma moi sinxela, coa presenza do seguinte ficheiro:

$ ls -lh /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 2.6M Jul 29 15:30 /sys/kernel/btf/vmlinux

Este ficheiro almacena información sobre todos os tipos de datos utilizados no núcleo e úsase en todos os nosos exemplos de uso libbpf. Falaremos en detalle sobre CO-RE no seguinte artigo, pero neste, só constrúe un núcleo con CONFIG_DEBUG_INFO_BTF.

biblioteca libbpf vive directamente no directorio tools/lib/bpf kernel e o seu desenvolvemento realízase a través da lista de correo [email protected]. Non obstante, mantense un repositorio separado para as necesidades das aplicacións que viven fóra do núcleo https://github.com/libbpf/libbpf na que a biblioteca do núcleo está reflectida para o acceso de lectura máis ou menos como está.

Nesta sección veremos como podes crear un proxecto que utilice libbpf, escribamos varios programas de proba (máis ou menos sen sentido) e analicemos polo miúdo como funciona todo. Isto permitiranos explicar con máis facilidade nas seguintes seccións exactamente como interactúan os programas BPF con mapas, axudantes do núcleo, BTF, etc.

Normalmente se usan proxectos libbpf engade un repositorio de GitHub como submódulo git, faremos o mesmo:

$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Initialized empty Git repository in /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Cloning into '/tmp/libbpf-example/libbpf'...
remote: Enumerating objects: 200, done.
remote: Counting objects: 100% (200/200), done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 3354 (delta 101), reused 118 (delta 79), pack-reused 3154
Receiving objects: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, done.
Resolving deltas: 100% (2176/2176), done.

Indo a libbpf moi sinxelo:

$ cd libbpf/src
$ mkdir build
$ OBJDIR=build DESTDIR=root make -s install
$ find root
root
root/usr
root/usr/include
root/usr/include/bpf
root/usr/include/bpf/bpf_tracing.h
root/usr/include/bpf/xsk.h
root/usr/include/bpf/libbpf_common.h
root/usr/include/bpf/bpf_endian.h
root/usr/include/bpf/bpf_helpers.h
root/usr/include/bpf/btf.h
root/usr/include/bpf/bpf_helper_defs.h
root/usr/include/bpf/bpf.h
root/usr/include/bpf/libbpf_util.h
root/usr/include/bpf/libbpf.h
root/usr/include/bpf/bpf_core_read.h
root/usr/lib64
root/usr/lib64/libbpf.so.0.1.0
root/usr/lib64/libbpf.so.0
root/usr/lib64/libbpf.a
root/usr/lib64/libbpf.so
root/usr/lib64/pkgconfig
root/usr/lib64/pkgconfig/libbpf.pc

O noso seguinte plan nesta sección é o seguinte: escribiremos un programa BPF como BPF_PROG_TYPE_XDP, o mesmo que no exemplo anterior, pero en C, compilámolo usando clang, e escriba un programa auxiliar que o cargará no núcleo. Nas seguintes seccións ampliaremos as capacidades tanto do programa BPF como do programa asistente.

Exemplo: crear unha aplicación completa usando libbpf

Para comezar, usamos o ficheiro /sys/kernel/btf/vmlinux, que se mencionou anteriormente, e crea o seu equivalente en forma de ficheiro de cabeceira:

$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

Este ficheiro almacenará todas as estruturas de datos dispoñibles no noso núcleo, por exemplo, así é como se define a cabeceira IPv4 no núcleo:

$ grep -A 12 'struct iphdr {' vmlinux.h
struct iphdr {
    __u8 ihl: 4;
    __u8 version: 4;
    __u8 tos;
    __be16 tot_len;
    __be16 id;
    __be16 frag_off;
    __u8 ttl;
    __u8 protocol;
    __sum16 check;
    __be32 saddr;
    __be32 daddr;
};

Agora escribiremos o noso programa BPF en C:

$ cat xdp-simple.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

SEC("xdp/simple")
int simple(void *ctx)
{
        return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

Aínda que o noso programa resultou moi sinxelo, aínda temos que prestar atención a moitos detalles. En primeiro lugar, o primeiro ficheiro de cabeceira que incluímos é vmlinux.h, que acabamos de xerar usando bpftool btf dump - agora non necesitamos instalar o paquete kernel-headers para descubrir como son as estruturas do núcleo. O seguinte ficheiro de cabeceira chéganos desde a biblioteca libbpf. Agora só o necesitamos para definir a macro SEC, que envía o carácter á sección apropiada do ficheiro de obxecto ELF. O noso programa está contido na sección xdp/simple, onde antes da barra, definimos o tipo de programa BPF - esta é a convención utilizada en libbpf, segundo o nome da sección, substituirá o tipo correcto ao iniciar bpf(2). O propio programa BPF é C - moi sinxelo e consta dunha liña return XDP_PASS. Por último, unha sección separada "license" contén o nome da licenza.

Podemos compilar o noso programa usando llvm/clang, versión >= 10.0.0, ou mellor aínda, superior (ver sección Ferramentas de desenvolvemento):

$ clang --version
clang version 11.0.0 (https://github.com/llvm/llvm-project.git afc287e0abec710398465ee1f86237513f2b5091)
...

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o

Entre as características interesantes: indicamos a arquitectura de destino -target bpf e o camiño ata as cabeceiras libbpf, que instalamos recentemente. Ademais, non te esquezas -O2, sen esta opción podes ter sorpresas no futuro. Vexamos o noso código, conseguimos escribir o programa que queriamos?

$ llvm-objdump --section=xdp/simple --no-show-raw-insn -D xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       r0 = 2
       1:       exit

Si, funcionou! Agora, temos un ficheiro binario co programa e queremos crear unha aplicación que o cargue no núcleo. Para este fin a biblioteca libbpf ofrécenos dúas opcións: usar unha API de nivel inferior ou unha API de nivel superior. Imos polo segundo camiño, xa que queremos aprender a escribir, cargar e conectar programas BPF cun mínimo esforzo para o seu posterior estudo.

En primeiro lugar, necesitamos xerar o "esqueleto" do noso programa a partir do seu binario usando a mesma utilidade bpftool — o coitelo suízo do mundo BPF (que se pode tomar literalmente, xa que Daniel Borkman, un dos creadores e mantedores de BPF, é suízo):

$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h

En arquivo xdp-simple.skel.h contén o código binario do noso programa e funcións para xestionar - cargar, anexar, eliminar o noso obxecto. No noso caso sinxelo, isto parece excesivo, pero tamén funciona no caso de que o ficheiro obxecto contén moitos programas e mapas BPF e para cargar este ELF xigante só necesitamos xerar o esqueleto e chamar a unha ou dúas funcións desde a aplicación personalizada que temos. están escribindo Sigamos agora.

En rigor, o noso programa de carga é trivial:

#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    pause();

    xdp_simple_bpf__destroy(obj);
}

Aquí struct xdp_simple_bpf definido no ficheiro xdp-simple.skel.h e describe o noso ficheiro obxecto:

struct xdp_simple_bpf {
    struct bpf_object_skeleton *skeleton;
    struct bpf_object *obj;
    struct {
        struct bpf_program *simple;
    } progs;
    struct {
        struct bpf_link *simple;
    } links;
};

Podemos ver vestixios dunha API de baixo nivel aquí: a estrutura struct bpf_program *simple и struct bpf_link *simple. A primeira estrutura describe especificamente o noso programa, escrito na sección xdp/simple, e o segundo describe como se conecta o programa á fonte do evento.

Función xdp_simple_bpf__open_and_load, abre un obxecto ELF, analízao, crea todas as estruturas e subestruturas (ademais do programa, ELF tamén contén outras seccións: datos, datos de só lectura, información de depuración, licenza, etc.), e despois cárgao no núcleo mediante un sistema. chamar bpf, que podemos comprobar compilando e executando o programa:

$ clang -O2 -I ./libbpf/src/root/usr/include/ xdp-simple.c -o xdp-simple ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_BTF_LOAD, 0x7ffdb8fd9670, 120)  = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0xdfd580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 8, 0), prog_flags=0, prog_name="simple", prog_ifindex=0, expected_attach_type=0x25 /* BPF_??? */, ...}, 120) = 4

Vexamos agora o noso programa usando bpftool. Imos buscar o seu DNI:

# bpftool p | grep -A4 simple
463: xdp  name simple  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-01T01:59:49+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        btf_id 185
        pids xdp-simple(16498)

e volcado (utilizamos unha forma abreviada do comando bpftool prog dump xlated):

# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
   0: (b7) r0 = 2
   1: (95) exit

Algo novo! O programa imprimiu anacos do noso ficheiro fonte C. Isto foi feito pola biblioteca libbpf, que atopou a sección de depuración no binario, compilouno nun obxecto BTF, cargouno no núcleo usando BPF_BTF_LOAD, e despois especificou o descritor do ficheiro resultante ao cargar o programa co comando BPG_PROG_LOAD.

Axudantes do núcleo

Os programas BPF poden executar funcións "externas": axudantes do núcleo. Estas funcións auxiliares permiten aos programas BPF acceder ás estruturas do núcleo, xestionar mapas e tamén comunicarse co "mundo real": crear eventos de perf, controlar hardware (por exemplo, paquetes de redirección), etc.

Exemplo: bpf_get_smp_processor_id

No marco do paradigma "aprender co exemplo", consideremos unha das funcións auxiliares, bpf_get_smp_processor_id(), certo en arquivo kernel/bpf/helpers.c. Devolve o número do procesador no que se está a executar o programa BPF que o chamou. Pero non nos interesa tanto a súa semántica como o feito de que a súa implementación toma unha liña:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

As definicións das funcións auxiliares de BPF son similares ás definicións de chamadas ao sistema Linux. Aquí, por exemplo, defínese unha función que non ten argumentos. (Unha función que toma, por exemplo, tres argumentos defínese usando a macro BPF_CALL_3. O número máximo de argumentos é cinco.) Non obstante, esta é só a primeira parte da definición. A segunda parte é definir a estrutura do tipo struct bpf_func_proto, que contén unha descrición da función auxiliar que o verificador comprende:

const struct bpf_func_proto bpf_get_smp_processor_id_proto = {
    .func     = bpf_get_smp_processor_id,
    .gpl_only = false,
    .ret_type = RET_INTEGER,
};

Rexistro de funcións auxiliares

Para que os programas BPF dun tipo determinado utilicen esta función, deben rexistrala, por exemplo para o tipo BPF_PROG_TYPE_XDP unha función está definida no núcleo xdp_func_proto, que determina a partir do ID da función auxiliar se XDP admite esta función ou non. A nosa función é soportes:

static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
    switch (func_id) {
    ...
    case BPF_FUNC_get_smp_processor_id:
        return &bpf_get_smp_processor_id_proto;
    ...
    }
}

Os novos tipos de programas BPF están "definidos" no ficheiro include/linux/bpf_types.h usando unha macro BPF_PROG_TYPE. Defínese entre comiñas porque é unha definición lóxica, e en termos da linguaxe C a definición de todo un conxunto de estruturas concretas dáse noutros lugares. En particular, no expediente kernel/bpf/verifier.c todas as definicións do ficheiro bpf_types.h úsanse para crear unha matriz de estruturas bpf_verifier_ops[]:

static const struct bpf_verifier_ops *const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) 
    [_id] = & _name ## _verifier_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
};

É dicir, para cada tipo de programa BPF, defínese un punteiro a unha estrutura de datos do tipo struct bpf_verifier_ops, que se inicializa co valor _name ## _verifier_ops, é dicir, xdp_verifier_ops para xdp. Estrutura xdp_verifier_ops determinado en arquivo net/core/filter.c do seguinte xeito:

const struct bpf_verifier_ops xdp_verifier_ops = {
    .get_func_proto     = xdp_func_proto,
    .is_valid_access    = xdp_is_valid_access,
    .convert_ctx_access = xdp_convert_ctx_access,
    .gen_prologue       = bpf_noop_prologue,
};

Aquí vemos a nosa función familiar xdp_func_proto, que executará o verificador cada vez que atope un desafío algún tipo funcións dentro dun programa BPF, ver verifier.c.

Vexamos como un hipotético programa BPF usa a función bpf_get_smp_processor_id. Para iso, reescribimos o programa da nosa sección anterior do seguinte xeito:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

SEC("xdp/simple")
int simple(void *ctx)
{
    if (bpf_get_smp_processor_id() != 0)
        return XDP_DROP;
    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

Símbolo bpf_get_smp_processor_id determinado в <bpf/bpf_helper_defs.h> bibliotecas libbpf como

static u32 (*bpf_get_smp_processor_id)(void) = (void *) 8;

é dicir, bpf_get_smp_processor_id é un punteiro de función cuxo valor é 8, onde 8 é o valor BPF_FUNC_get_smp_processor_id tipo enum bpf_fun_id, que se define para nós no ficheiro vmlinux.h (arquivo bpf_helper_defs.h no núcleo é xerado por un script, polo que os números "máxicos" están ben). Esta función non toma argumentos e devolve un valor de tipo __u32. Cando o executamos no noso programa, clang xera unha instrución BPF_CALL "o tipo correcto" Compilemos o programa e vexamos a sección xdp/simple:

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ llvm-objdump -D --section=xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       bf 01 00 00 00 00 00 00 r1 = r0
       2:       67 01 00 00 20 00 00 00 r1 <<= 32
       3:       77 01 00 00 20 00 00 00 r1 >>= 32
       4:       b7 00 00 00 02 00 00 00 r0 = 2
       5:       15 01 01 00 00 00 00 00 if r1 == 0 goto +1 <LBB0_2>
       6:       b7 00 00 00 01 00 00 00 r0 = 1

0000000000000038 <LBB0_2>:
       7:       95 00 00 00 00 00 00 00 exit

Na primeira liña vemos instrucións call, parámetro IMM que é igual a 8, e SRC_REG - cero. Segundo o acordo ABI utilizado polo verificador, esta é unha chamada á función auxiliar número oito. Unha vez que se lanza, a lóxica é sinxela. Valor de retorno do rexistro r0 copiado a r1 e nas liñas 2,3 convértese a tipo u32 — Borráranse os 32 bits superiores. Nas liñas 4,5,6,7 devolvemos 2 (XDP_PASS) ou 1 (XDP_DROP) dependendo de se a función auxiliar da liña 0 devolveu un valor cero ou distinto de cero.

Probámonos a nós mesmos: carga o programa e mira a saída bpftool prog dump xlated:

$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914

$ sudo bpftool p | grep simple
523: xdp  name simple  tag 44c38a10c657e1b0  gpl
        pids xdp-simple(10915)

$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
   0: (85) call bpf_get_smp_processor_id#114128
   1: (bf) r1 = r0
   2: (67) r1 <<= 32
   3: (77) r1 >>= 32
   4: (b7) r0 = 2
; }
   5: (15) if r1 == 0x0 goto pc+1
   6: (b7) r0 = 1
   7: (95) exit

Ok, o verificador atopou o asistente do núcleo correcto.

Exemplo: pasar argumentos e, finalmente, executar o programa!

Todas as funcións auxiliares de nivel de execución teñen un prototipo

u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)

Os parámetros das funcións auxiliares pásanse nos rexistros r1-r5, e o valor devólvese no rexistro r0. Non hai funcións que leven máis de cinco argumentos e non se espera que se engadan soporte para elas no futuro.

Vexamos o novo axudante do núcleo e como BPF pasa os parámetros. Imos reescribir xdp-simple.bpf.c do seguinte xeito (o resto das liñas non cambiaron):

SEC("xdp/simple")
int simple(void *ctx)
{
    bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
    return XDP_PASS;
}

O noso programa imprime o número da CPU na que se está a executar. Compilámolo e vexamos o código:

$ llvm-objdump -D --section=xdp/simple --no-show-raw-insn xdp-simple.bpf.o

0000000000000000 <simple>:
       0:       r1 = 10
       1:       *(u16 *)(r10 - 8) = r1
       2:       r1 = 8441246879787806319 ll
       4:       *(u64 *)(r10 - 16) = r1
       5:       r1 = 2334956330918245746 ll
       7:       *(u64 *)(r10 - 24) = r1
       8:       call 8
       9:       r1 = r10
      10:       r1 += -24
      11:       r2 = 18
      12:       r3 = r0
      13:       call 6
      14:       r0 = 2
      15:       exit

Nas liñas 0-7 escribimos a cadea running on CPU%un, e despois na liña 8 executamos a coñecida bpf_get_smp_processor_id. Nas liñas 9-12 preparamos os argumentos auxiliares bpf_printk - rexistros r1, r2, r3. Por que son tres e non dous? Porque bpf_printkeste é un envoltorio de macros arredor do verdadeiro axudante bpf_trace_printk, que precisa pasar o tamaño da cadea de formato.

Engadimos agora un par de liñas a xdp-simple.cpara que o noso programa se conecte á interface lo e comezou de verdade!

$ cat xdp-simple.c
#include <linux/if_link.h>
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    __u32 flags = XDP_FLAGS_SKB_MODE;
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    bpf_set_link_xdp_fd(1, -1, flags);
    bpf_set_link_xdp_fd(1, bpf_program__fd(obj->progs.simple), flags);

cleanup:
    xdp_simple_bpf__destroy(obj);
}

Aquí usamos a función bpf_set_link_xdp_fd, que conecta programas BPF como XDP a interfaces de rede. Codificamos o número da interface lo, que sempre é 1. Executamos a función dúas veces para desconectar primeiro o programa antigo se estaba adxunto. Teña en conta que agora non necesitamos un desafío pause ou un bucle infinito: o noso programa cargador sairá, pero o programa BPF non se eliminará xa que está conectado á fonte do evento. Despois da descarga e conexión exitosas, o programa lanzarase para cada paquete de rede que chegue lo.

Descarguemos o programa e vexamos a interface lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  name simple  tag 4fca62e77ccb43d6  gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

O programa que descargamos ten o ID 669 e vemos o mesmo ID na interface lo. Enviaremos un par de paquetes a 127.0.0.1 (solicitude + resposta):

$ ping -c1 localhost

e agora vexamos o contido do ficheiro virtual de depuración /sys/kernel/debug/tracing/trace_pipe, en que bpf_printk escribe as súas mensaxes:

# cat /sys/kernel/debug/tracing/trace_pipe
ping-13937 [000] d.s1 442015.377014: bpf_trace_printk: running on CPU0
ping-13937 [000] d.s1 442015.377027: bpf_trace_printk: running on CPU0

Avistaron dous paquetes lo e procesado en CPU0: o noso primeiro programa BPF completo e sen sentido funcionou!

Paga a pena notalo bpf_printk Non por nada escribe no ficheiro de depuración: este non é o axudante máis exitoso para o seu uso na produción, pero o noso obxectivo era mostrar algo sinxelo.

Acceso a mapas desde programas BPF

Exemplo: utilizando un mapa do programa BPF

Nas seccións anteriores aprendemos a crear e usar mapas desde o espazo do usuario, e agora vexamos a parte do núcleo. Comecemos, como é habitual, cun exemplo. Imos reescribir o noso programa xdp-simple.bpf.c do seguinte xeito:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 8);
    __type(key, u32);
    __type(value, u64);
} woo SEC(".maps");

SEC("xdp/simple")
int simple(void *ctx)
{
    u32 key = bpf_get_smp_processor_id();
    u32 *val;

    val = bpf_map_lookup_elem(&woo, &key);
    if (!val)
        return XDP_ABORTED;

    *val += 1;

    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

Ao comezo do programa engadimos unha definición de mapa woo: Esta é unha matriz de 8 elementos que almacena valores como u64 (en C definiríamos unha matriz como u64 woo[8]). Nun programa "xdp/simple" obtenemos o número de procesador actual nunha variable key e despois usando a función auxiliar bpf_map_lookup_element obtemos un punteiro á entrada correspondente na matriz, que aumentamos nun. Traducido ao ruso: calculamos estatísticas sobre que CPU procesou os paquetes entrantes. Imos tentar executar o programa:

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple

Comprobamos que está ligada lo e enviar uns paquetes:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 108

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done

Agora vexamos o contido da matriz:

$ sudo bpftool map dump name woo
[
    { "key": 0, "value": 0 },
    { "key": 1, "value": 400 },
    { "key": 2, "value": 0 },
    { "key": 3, "value": 0 },
    { "key": 4, "value": 0 },
    { "key": 5, "value": 0 },
    { "key": 6, "value": 0 },
    { "key": 7, "value": 46400 }
]

Case todos os procesos procesáronse en CPU7. Isto non é importante para nós, o principal é que o programa funciona e entendemos como acceder aos mapas dos programas BPF, usando хелперов bpf_mp_*.

Índice místico

Así, podemos acceder ao mapa desde o programa BPF usando chamadas como

val = bpf_map_lookup_elem(&woo, &key);

onde se parece a función auxiliar

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

pero estamos pasando un punteiro &woo a unha estrutura sen nome struct { ... }...

Se observamos o ensamblador do programa, vemos que o valor &woo non está realmente definido (liña 4):

llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
...

e está contido en traslados:

$ llvm-readelf -r xdp-simple.bpf.o | head -4

Relocation section '.relxdp/simple' at offset 0xe18 contains 1 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name
0000000000000020  0000002700000001 R_BPF_64_64            0000000000000000 woo

Pero se observamos o programa xa cargado, vemos un punteiro ao mapa correcto (liña 4):

$ sudo bpftool prog dump x name simple
int simple(void *ctx):
   0: (85) call bpf_get_smp_processor_id#114128
   1: (63) *(u32 *)(r10 -4) = r0
   2: (bf) r2 = r10
   3: (07) r2 += -4
   4: (18) r1 = map[id:64]
...

Así, podemos concluír que no momento de lanzar o noso programa de carga, a ligazón a &woo foi substituído por algo cunha biblioteca libbpf. Primeiro veremos a saída strace:

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=8, max_entries=8, map_name="woo", ...}, 120) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="simple", ...}, 120) = 5

Vemos iso libbpf creou un mapa woo e despois descargamos o noso programa simple. Vexamos máis de cerca como cargamos o programa:

  • chamar xdp_simple_bpf__open_and_load do ficheiro xdp-simple.skel.h
  • que provoca xdp_simple_bpf__load do ficheiro xdp-simple.skel.h
  • que provoca bpf_object__load_skeleton do ficheiro libbpf/src/libbpf.c
  • que provoca bpf_object__load_xattr de libbpf/src/libbpf.c

A última función, entre outras cousas, chamará bpf_object__create_maps, que crea ou abre mapas existentes, converténdoos en descritores de ficheiros. (Aquí é onde vemos BPF_MAP_CREATE na saída strace.) A continuación chámase a función bpf_object__relocate e é ela a que nos interesa, xa que lembramos o que vimos woo na táboa de traslados. Explorándoo, finalmente atopámonos na función bpf_program__relocate, que trata sobre os desprazamentos de mapas:

case RELO_LD64:
    insn[0].src_reg = BPF_PSEUDO_MAP_FD;
    insn[0].imm = obj->maps[relo->map_idx].fd;
    break;

Así que tomamos as nosas instrucións

18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll

e substituír o rexistro de orixe nel por BPF_PSEUDO_MAP_FD, e o primeiro IMM ao descritor de ficheiros do noso mapa e, se é igual a, por exemplo, 0xdeadbeef, entón, como resultado, recibiremos a instrución

18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll

Así é como se transfire a información do mapa a un programa BPF cargado específico. Neste caso, o mapa pódese crear usando BPF_MAP_CREATE, e aberto por ID usando BPF_MAP_GET_FD_BY_ID.

Total, ao usar libbpf o algoritmo é o seguinte:

  • durante a compilación, créanse rexistros na táboa de traslados para ligazóns a mapas
  • libbpf abre o libro de obxectos ELF, atopa todos os mapas usados ​​e crea descritores de ficheiros para eles
  • os descritores de ficheiros cárganse no núcleo como parte da instrución LD64

Como podedes imaxinar, queda máis por vir e teremos que mirar o núcleo. Afortunadamente, temos unha pista: anotamos o significado BPF_PSEUDO_MAP_FD no rexistro da fonte e podemos enterralo, o que nos levará ao santo de todos os santos - kernel/bpf/verifier.c, onde unha función cun nome distintivo substitúe un descritor de ficheiro polo enderezo dunha estrutura de tipo struct bpf_map:

static int replace_map_fd_with_map_ptr(struct bpf_verifier_env *env) {
    ...

    f = fdget(insn[0].imm);
    map = __bpf_map_get(f);
    if (insn->src_reg == BPF_PSEUDO_MAP_FD) {
        addr = (unsigned long)map;
    }
    insn[0].imm = (u32)addr;
    insn[1].imm = addr >> 32;

(O código completo pódese atopar по ссылке). Así podemos ampliar o noso algoritmo:

  • mentres carga o programa, o verificador verifica o uso correcto do mapa e escribe o enderezo da estrutura correspondente struct bpf_map

Ao descargar o binario ELF usando libbpf Hai moito máis a suceder, pero discutirémolo noutros artigos.

Cargando programas e mapas sen libbpf

Como prometeron, aquí tes un exemplo para os lectores que queiran saber como crear e cargar un programa que utiliza mapas, sen axuda libbpf. Isto pode ser útil cando estás a traballar nun ambiente para o que non podes construír dependencias, ou gardar cada bit ou escribir un programa como ply, que xera código binario BPF sobre a marcha.

Para facilitar o seguimento da lóxica, reescribiremos o noso exemplo para estes propósitos xdp-simple. O código completo e lixeiramente ampliado do programa discutido neste exemplo pódese atopar neste esencia.

A lóxica da nosa aplicación é a seguinte:

  • crear un mapa de tipos BPF_MAP_TYPE_ARRAY usando o comando BPF_MAP_CREATE,
  • crear un programa que use este mapa,
  • conectar o programa á interface lo,

que se traduce en humano como

int main(void)
{
    int map_fd, prog_fd;

    map_fd = map_create();
    if (map_fd < 0)
        err(1, "bpf: BPF_MAP_CREATE");

    prog_fd = prog_load(map_fd);
    if (prog_fd < 0)
        err(1, "bpf: BPF_PROG_LOAD");

    xdp_attach(1, prog_fd);
}

Aquí map_create crea un mapa do mesmo xeito que fixemos no primeiro exemplo sobre a chamada ao sistema bpf - "núcleo, faime un novo mapa en forma de matriz de 8 elementos como __u64 e devolveme o descritor do ficheiro":

static int map_create()
{
    union bpf_attr attr;

    memset(&attr, 0, sizeof(attr));
    attr.map_type = BPF_MAP_TYPE_ARRAY,
    attr.key_size = sizeof(__u32),
    attr.value_size = sizeof(__u64),
    attr.max_entries = 8,
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}

O programa tamén é fácil de cargar:

static int prog_load(int map_fd)
{
    union bpf_attr attr;
    struct bpf_insn insns[] = {
        ...
    };

    memset(&attr, 0, sizeof(attr));
    attr.prog_type = BPF_PROG_TYPE_XDP;
    attr.insns     = ptr_to_u64(insns);
    attr.insn_cnt  = sizeof(insns)/sizeof(insns[0]);
    attr.license   = ptr_to_u64("GPL");
    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
}

A parte complicada prog_load é a definición do noso programa BPF como unha matriz de estruturas struct bpf_insn insns[]. Pero como estamos a usar un programa que temos en C, podemos facer trampas un pouco:

$ llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
       7:       b7 01 00 00 00 00 00 00 r1 = 0
       8:       15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2>
       9:       61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0)
      10:       07 01 00 00 01 00 00 00 r1 += 1
      11:       63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1
      12:       b7 01 00 00 02 00 00 00 r1 = 2

0000000000000068 <LBB0_2>:
      13:       bf 10 00 00 00 00 00 00 r0 = r1
      14:       95 00 00 00 00 00 00 00 exit

En total, necesitamos escribir 14 instrucións en forma de estruturas como struct bpf_insn (consello: colle o vertedoiro de arriba, volve ler a sección de instrucións, abre linux/bpf.h и linux/bpf_common.h e tentar determinar struct bpf_insn insns[] por conta propia):

struct bpf_insn insns[] = {
    /* 85 00 00 00 08 00 00 00 call 8 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 8,
    },

    /* 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 */
    {
        .code = BPF_MEM | BPF_STX,
        .off = -4,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_10,
    },

    /* bf a2 00 00 00 00 00 00 r2 = r10 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_10,
        .dst_reg = BPF_REG_2,
    },

    /* 07 02 00 00 fc ff ff ff r2 += -4 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_2,
        .imm = -4,
    },

    /* 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll */
    {
        .code = BPF_LD | BPF_DW | BPF_IMM,
        .src_reg = BPF_PSEUDO_MAP_FD,
        .dst_reg = BPF_REG_1,
        .imm = map_fd,
    },
    { }, /* placeholder */

    /* 85 00 00 00 01 00 00 00 call 1 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 1,
    },

    /* b7 01 00 00 00 00 00 00 r1 = 0 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 0,
    },

    /* 15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2> */
    {
        .code = BPF_JMP | BPF_JEQ | BPF_K,
        .off = 4,
        .src_reg = BPF_REG_0,
        .imm = 0,
    },

    /* 61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0) */
    {
        .code = BPF_MEM | BPF_LDX,
        .off = 0,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_1,
    },

    /* 07 01 00 00 01 00 00 00 r1 += 1 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 1,
    },

    /* 63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1 */
    {
        .code = BPF_MEM | BPF_STX,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* b7 01 00 00 02 00 00 00 r1 = 2 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 2,
    },

    /* <LBB0_2>: bf 10 00 00 00 00 00 00 r0 = r1 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* 95 00 00 00 00 00 00 00 exit */
    {
        .code = BPF_JMP | BPF_EXIT
    },
};

Un exercicio para aqueles que non escribiron isto eles mesmos: atopar map_fd.

Queda unha parte máis sen revelar no noso programa: xdp_attach. Desafortunadamente, programas como XDP non se poden conectar mediante unha chamada do sistema bpf. As persoas que crearon BPF e XDP eran da comunidade Linux en liña, o que significa que usaron a máis familiar para eles (pero non para normal people) interface para interactuar co núcleo: sockets de netlink, Ver tamén RFC3549. O xeito máis sinxelo de implementar xdp_attach está copiando o código de libbpf, é dicir, do arquivo netlink.c, que é o que fixemos, acurtándoo un pouco:

Benvido ao mundo dos sockets netlink

Abre un tipo de socket netlink NETLINK_ROUTE:

int netlink_open(__u32 *nl_pid)
{
    struct sockaddr_nl sa;
    socklen_t addrlen;
    int one = 1, ret;
    int sock;

    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;

    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (sock < 0)
        err(1, "socket");

    if (setsockopt(sock, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)) < 0)
        warnx("netlink error reporting not supported");

    if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
        err(1, "bind");

    addrlen = sizeof(sa);
    if (getsockname(sock, (struct sockaddr *)&sa, &addrlen) < 0)
        err(1, "getsockname");

    *nl_pid = sa.nl_pid;
    return sock;
}

Lemos dende este socket:

static int bpf_netlink_recv(int sock, __u32 nl_pid, int seq)
{
    bool multipart = true;
    struct nlmsgerr *errm;
    struct nlmsghdr *nh;
    char buf[4096];
    int len, ret;

    while (multipart) {
        multipart = false;
        len = recv(sock, buf, sizeof(buf), 0);
        if (len < 0)
            err(1, "recv");

        if (len == 0)
            break;

        for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);
                nh = NLMSG_NEXT(nh, len)) {
            if (nh->nlmsg_pid != nl_pid)
                errx(1, "wrong pid");
            if (nh->nlmsg_seq != seq)
                errx(1, "INVSEQ");
            if (nh->nlmsg_flags & NLM_F_MULTI)
                multipart = true;
            switch (nh->nlmsg_type) {
                case NLMSG_ERROR:
                    errm = (struct nlmsgerr *)NLMSG_DATA(nh);
                    if (!errm->error)
                        continue;
                    ret = errm->error;
                    // libbpf_nla_dump_errormsg(nh); too many code to copy...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

Finalmente, aquí está a nosa función que abre un socket e envíalle unha mensaxe especial que contén un descritor de ficheiro:

static int xdp_attach(int ifindex, int prog_fd)
{
    int sock, seq = 0, ret;
    struct nlattr *nla, *nla_xdp;
    struct {
        struct nlmsghdr  nh;
        struct ifinfomsg ifinfo;
        char             attrbuf[64];
    } req;
    __u32 nl_pid = 0;

    sock = netlink_open(&nl_pid);
    if (sock < 0)
        return sock;

    memset(&req, 0, sizeof(req));
    req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
    req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    req.nh.nlmsg_type = RTM_SETLINK;
    req.nh.nlmsg_pid = 0;
    req.nh.nlmsg_seq = ++seq;
    req.ifinfo.ifi_family = AF_UNSPEC;
    req.ifinfo.ifi_index = ifindex;

    /* started nested attribute for XDP */
    nla = (struct nlattr *)(((char *)&req)
            + NLMSG_ALIGN(req.nh.nlmsg_len));
    nla->nla_type = NLA_F_NESTED | IFLA_XDP;
    nla->nla_len = NLA_HDRLEN;

    /* add XDP fd */
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FD;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(int);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &prog_fd, sizeof(prog_fd));
    nla->nla_len += nla_xdp->nla_len;

    /* if user passed in any flags, add those too */
    __u32 flags = XDP_FLAGS_SKB_MODE;
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FLAGS;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(flags);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &flags, sizeof(flags));
    nla->nla_len += nla_xdp->nla_len;

    req.nh.nlmsg_len += NLA_ALIGN(nla->nla_len);

    if (send(sock, &req, req.nh.nlmsg_len, 0) < 0)
        err(1, "send");
    ret = bpf_netlink_recv(sock, nl_pid, seq);

cleanup:
    close(sock);
    return ret;
}

Entón, todo está listo para probar:

$ cc nolibbpf.c -o nolibbpf
$ sudo strace -e bpf ./nolibbpf
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, map_name="woo", ...}, 72) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=15, prog_name="woo", ...}, 72) = 4
+++ exited with 0 +++

A ver se o noso programa conectouse a lo:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 160

Enviamos pings e miramos o mapa:

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
$ sudo bpftool m dump name woo
key: 00 00 00 00  value: 90 01 00 00 00 00 00 00
key: 01 00 00 00  value: 00 00 00 00 00 00 00 00
key: 02 00 00 00  value: 00 00 00 00 00 00 00 00
key: 03 00 00 00  value: 00 00 00 00 00 00 00 00
key: 04 00 00 00  value: 00 00 00 00 00 00 00 00
key: 05 00 00 00  value: 00 00 00 00 00 00 00 00
key: 06 00 00 00  value: 40 b5 00 00 00 00 00 00
key: 07 00 00 00  value: 00 00 00 00 00 00 00 00
Found 8 elements

Vaia, todo funciona. Teña en conta, por certo, que o noso mapa volve mostrarse en forma de bytes. Isto débese a que, a diferenza libbpf non cargamos información de tipo (BTF). Pero disto falaremos máis a próxima vez.

Ferramentas de desenvolvemento

Nesta sección, veremos o kit de ferramentas para desenvolvedores BPF mínimo.

En xeral, non precisa nada especial para desenvolver programas BPF - BPF execútase en calquera núcleo de distribución decente e os programas constrúense usando clang, que se pode subministrar dende o paquete. Non obstante, debido ao feito de que BPF está en desenvolvemento, o núcleo e as ferramentas están cambiando constantemente, se non queres escribir programas BPF usando métodos anticuados a partir de 2019, terás que compilar

  • llvm/clang
  • pahole
  • o seu núcleo
  • bpftool

(Para referencia, esta sección e todos os exemplos do artigo executáronse en Debian 10.)

llvm/clang

BPF é amigable con LLVM e, aínda que recentemente se poden compilar programas para BPF usando gcc, todo o desenvolvemento actual realízase para LLVM. Polo tanto, en primeiro lugar, imos construír a versión actual clang de git:

$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86" 
                      -DLLVM_ENABLE_PROJECTS="clang" 
                      -DBUILD_SHARED_LIBS=OFF 
                      -DCMAKE_BUILD_TYPE=Release 
                      -DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$

Agora podemos comprobar se todo chegou correctamente:

$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM version 11.0.0git
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: znver1

  Registered Targets:
    bpf    - BPF (host endian)
    bpfeb  - BPF (big endian)
    bpfel  - BPF (little endian)
    x86    - 32-bit X86: Pentium-Pro and above
    x86-64 - 64-bit X86: EM64T and AMD64

(Instrucións de montaxe clang tomado por min de bpf_devel_QA.)

Non instalaremos os programas que acabamos de construír, senón que engadímolos PATH, por exemplo:

export PATH="`pwd`/bin:$PATH"

(Isto pódese engadir a .bashrc ou nun ficheiro separado. Persoalmente, engado cousas como esta ~/bin/activate-llvm.sh e cando é necesario fágoo . activate-llvm.sh.)

Pahole e BTF

Utilidade pahole usado ao construír o núcleo para crear información de depuración en formato BTF. Non entraremos en detalles neste artigo sobre os detalles da tecnoloxía BTF, salvo o feito de que é conveniente e queremos usalo. Polo tanto, se vas construír o teu núcleo, constrúeo primeiro pahole (sen pahole non poderás construír o núcleo coa opción CONFIG_DEBUG_INFO_BTF:

$ git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
$ cd pahole/
$ sudo apt install cmake
$ mkdir build
$ cd build/
$ cmake -D__LIB=lib ..
$ make
$ sudo make install
$ which pahole
/usr/local/bin/pahole

Núcleos para experimentar con BPF

Ao explorar as posibilidades de BPF, quero montar o meu propio núcleo. Isto, en xeral, non é necesario, xa que poderá compilar e cargar programas BPF no núcleo de distribución, non obstante, ter o seu propio núcleo permítelle utilizar as funcións BPF máis recentes, que aparecerán na súa distribución nuns meses. , ou, como no caso dalgunhas ferramentas de depuración, non se empaquetarán en absoluto nun futuro previsible. Ademais, o seu propio núcleo fai que se sinta importante experimentar co código.

Para construír un núcleo necesitas, en primeiro lugar, o propio núcleo e, en segundo lugar, un ficheiro de configuración do núcleo. Para experimentar con BPF podemos usar o habitual vainilla núcleo ou un dos núcleos de desenvolvemento. Históricamente, o desenvolvemento de BPF ten lugar dentro da comunidade de redes Linux e, polo tanto, todos os cambios tarde ou cedo pasan por David Miller, o mantedor de redes Linux. Dependendo da súa natureza (edicións ou novas funcións), os cambios de rede caen nun dos dous núcleos: net ou net-next. Os cambios para BPF distribúense do mesmo xeito entre bpf и bpf-next, que despois se agrupan en net e net-next, respectivamente. Para máis detalles, consulte bpf_devel_QA и netdev-FAQ. Polo tanto, escolla un núcleo en función do seu gusto e das necesidades de estabilidade do sistema no que está a probar (*-next os núcleos son os máis inestables dos listados).

Está fóra do alcance deste artigo falar de como xestionar os ficheiros de configuración do núcleo; suponse que xa sabes como facelo ou listo para aprender por conta propia. Non obstante, as seguintes instrucións deberían ser máis ou menos suficientes para proporcionarche un sistema compatible con BPF.

Descarga un dos núcleos anteriores:

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next

Construír unha configuración mínima do núcleo de traballo:

$ cp /boot/config-`uname -r` .config
$ make localmodconfig

Activar as opcións BPF no ficheiro .config da súa propia elección (probablemente CONFIG_BPF xa estará habilitado xa que systemd o usa). Aquí tes unha lista de opcións do núcleo utilizadas para este artigo:

CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_LSM=y
CONFIG_BPF_SYSCALL=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_IPV6_SEG6_BPF=y
# CONFIG_NETFILTER_XT_MATCH_BPF is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y

Entón podemos montar e instalar facilmente os módulos e o núcleo (por certo, podes montar o núcleo usando o recén ensamblado). clangengadindo CC=clang):

$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install

e reinicie co novo núcleo (eu uso para iso kexec do paquete kexec-tools):

v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

bpftool

A utilidade máis utilizada no artigo será a utilidade bpftool, fornecido como parte do núcleo de Linux. Está escrito e mantido polos desenvolvedores de BPF para desenvolvedores de BPF e pódese usar para xestionar todo tipo de obxectos BPF: cargar programas, crear e editar mapas, explorar a vida do ecosistema BPF, etc. Pódese atopar documentación en forma de códigos fonte para páxinas de manual no núcleo ou, xa compilado, na rede.

No momento de escribir este artigo bpftool vén preparado só para RHEL, Fedora e Ubuntu (consulte, por exemplo, este fío, que conta a historia inacabada dos envases bpftool en Debian). Pero se xa construíu o seu núcleo, entón constrúeo bpftool tan fácil como unha torta:

$ cd ${linux}/tools/bpf/bpftool
# ... пропишите пути к последнему clang, как рассказано выше
$ make -s

Auto-detecting system features:
...                        libbfd: [ on  ]
...        disassembler-four-args: [ on  ]
...                          zlib: [ on  ]
...                        libcap: [ on  ]
...               clang-bpf-co-re: [ on  ]

Auto-detecting system features:
...                        libelf: [ on  ]
...                          zlib: [ on  ]
...                           bpf: [ on  ]

$

(Aquí ${linux} - este é o directorio do seu núcleo.) Despois de executar estes comandos bpftool recollerase nun directorio ${linux}/tools/bpf/bpftool e pódese engadir ao camiño (en primeiro lugar ao usuario root) ou simplemente copia a /usr/local/sbin.

Recoller bpftool o mellor é usar este último clang, montado como se describe anteriormente, e comprobe se está montado correctamente, usando, por exemplo, o comando

$ sudo bpftool feature probe kernel
Scanning system configuration...
bpf() syscall for unprivileged users is enabled
JIT compiler is enabled
JIT compiler hardening is disabled
JIT compiler kallsyms exports are enabled for root
...

que mostrará que funcións BPF están activadas no seu núcleo.

Por certo, o comando anterior pódese executar como

# bpftool f p k

Isto faise por analoxía coas utilidades do paquete iproute2, onde podemos, por exemplo, dicir ip a s eth0 en vez de ip addr show dev eth0.

Conclusión

BPF permítelle calzar unha pulga para medir eficazmente e cambiar sobre a marcha a funcionalidade do núcleo. O sistema resultou ser moi exitoso, nas mellores tradicións de UNIX: un mecanismo sinxelo que permite (re)programar o núcleo permitiu experimentar a un gran número de persoas e organizacións. E, aínda que os experimentos, así como o desenvolvemento da propia infraestrutura de BPF, están lonxe de rematar, o sistema xa ten un ABI estable que permite construír unha lóxica empresarial fiable e, sobre todo, eficaz.

Gustaríame sinalar que, na miña opinión, a tecnoloxía fíxose tan popular porque, por unha banda, pode xogar (a arquitectura dunha máquina pódese entender máis ou menos nunha noite), e por outra banda, para resolver problemas que non se podían resolver (fermosamente) antes da súa aparición. Estes dous compoñentes xuntos obrigan á xente a experimentar e soñar, o que leva á aparición de solucións cada vez máis innovadoras.

Este artigo, aínda que non é especialmente breve, é só unha introdución ao mundo de BPF e non describe características "avanzadas" e partes importantes da arquitectura. O plan para o futuro é algo así: o seguinte artigo será unha visión xeral dos tipos de programas BPF (hai 5.8 tipos de programas compatibles no núcleo 30), entón finalmente veremos como escribir aplicacións BPF reais usando programas de rastrexo do núcleo. como exemplo, é hora de realizar un curso máis profundo sobre arquitectura BPF, seguido de exemplos de aplicacións de seguridade e redes BPF.

Artigos anteriores desta serie

  1. BPF para os máis pequenos, parte cero: BPF clásico

Ligazóns

  1. Guía de referencia de BPF e XDP — documentación sobre BPF de cilium, ou máis precisamente de Daniel Borkman, un dos creadores e mantedores de BPF. Esta é unha das primeiras descricións serias, que se diferencia das outras en que Daniel sabe exactamente sobre o que está escribindo e non hai erros alí. En particular, este documento describe como traballar con programas BPF dos tipos XDP e TC usando a coñecida utilidade ip do paquete iproute2.

  2. Documentación/redes/filter.txt — arquivo orixinal con documentación para BPF clásico e despois estendido. Unha boa lectura se queres afondar na linguaxe ensambladora e nos detalles técnicos arquitectónicos.

  3. Blog sobre BPF desde facebook. Actualízase raramente, pero acertadamente, como escriben alí Alexei Starovoitov (autor de eBPF) e Andrii Nakryiko - (mantedor). libbpf).

  4. Segredos de bpftool. Un entretido fío de twitter de Quentin Monnet con exemplos e segredos do uso de bpftool.

  5. Mergullo en BPF: unha lista de material de lectura. Unha lista xigante (e aínda mantida) de ligazóns á documentación de BPF de Quentin Monnet.

Fonte: www.habr.com

Engadir un comentario