BPF per als més petits, primera part: BPF ampliat

Al principi hi havia una tecnologia i es deia BPF. La vam mirar anterior, article de l'Antic Testament d'aquesta sèrie. El 2013, gràcies als esforços d'Alexei Starovoitov i Daniel Borkman, es va desenvolupar i incloure al nucli de Linux una versió millorada, optimitzada per a màquines modernes de 64 bits. Aquesta nova tecnologia es va anomenar breument Internal BPF, després es va rebatejar com a Extended BPF, i ara, després de diversos anys, tothom simplement l'anomena BPF.

A grans trets, BPF us permet executar codi arbitrari subministrat per l'usuari a l'espai del nucli de Linux, i la nova arquitectura va resultar tan exitosa que necessitarem una dotzena d'articles més per descriure totes les seves aplicacions. (L'única cosa que els desenvolupadors no van fer bé, com podeu veure al codi de rendiment següent, va ser crear un logotip decent.)

Aquest article descriu l'estructura de la màquina virtual BPF, les interfícies del nucli per treballar amb BPF, les eines de desenvolupament, així com una visió general breu i molt breu de les capacitats existents, és a dir. tot el que necessitarem en el futur per a un estudi més profund de les aplicacions pràctiques de BPF.
BPF per als més petits, primera part: BPF ampliat

Resum de l'article

Introducció a l'arquitectura BPF. Primer, farem una visió a vista d'ocell de l'arquitectura BPF i esbossarem els components principals.

Registres i sistema de comandament de la màquina virtual BPF. Tenint ja una idea de l'arquitectura en el seu conjunt, descriurem l'estructura de la màquina virtual BPF.

Cicle de vida dels objectes BPF, sistema de fitxers bpffs. En aquesta secció, analitzarem més de prop el cicle de vida dels objectes BPF: programes i mapes.

Gestió d'objectes mitjançant la crida al sistema bpf. Amb una certa comprensió del sistema ja en funcionament, finalment veurem com crear i manipular objectes des de l'espai d'usuari mitjançant una trucada especial al sistema - bpf(2).

Пишем программы BPF с помощью libbpf. Per descomptat, podeu escriure programes mitjançant una trucada al sistema. Però és difícil. Per a un escenari més realista, els programadors nuclears van desenvolupar una biblioteca libbpf. Crearem un esquelet bàsic d'aplicació BPF que utilitzarem en exemples posteriors.

Ajudants del nucli. Aquí aprendrem com els programes BPF poden accedir a les funcions d'ajuda del nucli, una eina que, juntament amb els mapes, amplia fonamentalment les capacitats del nou BPF en comparació amb el clàssic.

Accés a mapes dels programes BPF. En aquest punt, sabrem prou com per entendre exactament com podem crear programes que utilitzin mapes. I fins i tot donem un cop d'ull ràpid al gran i poderós verificador.

Eines de desenvolupament. Secció d'ajuda sobre com muntar les utilitats i el nucli necessaris per als experiments.

Conclusió. Al final de l'article, els que llegiu fins aquí trobaran paraules motivadores i una breu descripció del que passarà en els articles següents. També enumerarem una sèrie d'enllaços per a l'autoestudi per a aquells que no tinguin les ganes o la capacitat d'esperar a la continuació.

Introducció a l'Arquitectura BPF

Abans de començar a considerar l'arquitectura BPF, ens referirem BPF clàssic, que es va desenvolupar com a resposta a l'aparició de les màquines RISC i va resoldre el problema del filtratge eficient de paquets. L'arquitectura va tenir tant d'èxit que, després d'haver nascut als anys noranta a Berkeley UNIX, es va portar a la majoria de sistemes operatius existents, va sobreviure fins als bojos anys vint i encara està trobant noves aplicacions.

El nou BPF es va desenvolupar com a resposta a la ubiqüitat de les màquines de 64 bits, els serveis al núvol i la creixent necessitat d'eines per crear SDN (Sprogramari -ddefinit ntreballant). Desenvolupat per enginyers de xarxes del nucli com a reemplaçament millorat del clàssic BPF, el nou BPF literalment sis mesos després va trobar aplicacions en la difícil tasca de rastrejar sistemes Linux, i ara, sis anys després de la seva aparició, necessitarem tot un article següent només per enumerar els diferents tipus de programes.

Imatges divertides

En el seu nucli, BPF és una màquina virtual sandbox que us permet executar codi "arbitrari" a l'espai del nucli sense comprometre la seguretat. Els programes BPF es creen a l'espai d'usuari, es carreguen al nucli i es connecten a alguna font d'esdeveniments. Un esdeveniment podria ser, per exemple, el lliurament d'un paquet a una interfície de xarxa, el llançament d'alguna funció del nucli, etc. En el cas d'un paquet, el programa BPF tindrà accés a les dades i metadades del paquet (per llegir i, possiblement, escriure, segons el tipus de programa); en el cas d'executar una funció del nucli, els arguments de la funció, inclosos els punters a la memòria del nucli, etc.

Fem una ullada més de prop a aquest procés. Per començar, parlem de la primera diferència amb el clàssic BPF, programes per als quals estaven escrits en assemblador. En la nova versió, l'arquitectura es va ampliar perquè els programes es poguessin escriure en llenguatges d'alt nivell, principalment, és clar, en C. Per això, es va desenvolupar un backend per a llvm, que permet generar bytecode per a l'arquitectura BPF.

BPF per als més petits, primera part: BPF ampliat

L'arquitectura BPF es va dissenyar, en part, per funcionar de manera eficient en màquines modernes. Perquè això funcioni a la pràctica, el bytecode BPF, un cop carregat al nucli, es tradueix al codi natiu mitjançant un component anomenat compilador JIT (Just In Time). A continuació, si recordeu, al BPF clàssic el programa es va carregar al nucli i es va connectar atòmicament a la font de l'esdeveniment, en el context d'una única trucada al sistema. A la nova arquitectura, això passa en dues etapes: primer, el codi es carrega al nucli mitjançant una trucada al sistema. bpf(2)i després, posteriorment, mitjançant altres mecanismes que varien segons el tipus de programa, el programa s'adjunta a la font de l'esdeveniment.

Aquí el lector pot tenir una pregunta: va ser possible? Com es garanteix la seguretat d'execució d'aquest codi? La seguretat de l'execució ens garanteix l'etapa de càrrega dels programes BPF anomenada verificador (en anglès aquesta etapa s'anomena verificador i continuaré utilitzant la paraula anglesa):

BPF per als més petits, primera part: BPF ampliat

Verifier és un analitzador estàtic que assegura que un programa no interromp el funcionament normal del nucli. Això, per cert, no vol dir que el programa no pugui interferir amb el funcionament del sistema: els programes BPF, depenent del tipus, poden llegir i reescriure seccions de la memòria del nucli, retornar els valors de les funcions, retallar, afegir, reescriure. i fins i tot reenviar paquets de xarxa. Verifier garanteix que executar un programa BPF no bloquejarà el nucli i que un programa que, segons les regles, tingui accés d'escriptura, per exemple, les dades d'un paquet sortint, no podrà sobreescriure la memòria del nucli fora del paquet. Veurem el verificador amb una mica més de detall a la secció corresponent, després de familiaritzar-nos amb tots els altres components de BPF.

Aleshores, què hem après fins ara? L'usuari escriu un programa en C, el carrega al nucli mitjançant una trucada al sistema bpf(2), on el verifica un verificador i es tradueix al bytecode natiu. Aleshores, el mateix usuari o un altre connecta el programa a la font de l'esdeveniment i comença a executar-se. És necessari separar l'arrencada i la connexió per diversos motius. En primer lloc, executar un verificador és relativament car i en baixar el mateix programa diverses vegades perdem temps a l'ordinador. En segon lloc, exactament com es connecta un programa depèn del seu tipus, i una interfície "universal" desenvolupada fa un any pot no ser adequada per a nous tipus de programes. (Tot i que ara que l'arquitectura és cada cop més madura, hi ha una idea per unificar aquesta interfície a nivell libbpf.)

El lector atent pot notar que encara no hem acabat amb les imatges. De fet, tot l'anterior no explica com el BPF canvia fonamentalment la imatge en comparació amb el BPF clàssic. Dues innovacions que amplien significativament l'abast d'aplicabilitat són la capacitat d'utilitzar la memòria compartida i les funcions d'ajuda del nucli. A BPF, la memòria compartida s'implementa mitjançant els anomenats mapes: estructures de dades compartides amb una API específica. Probablement van rebre aquest nom perquè el primer tipus de mapa que va aparèixer va ser una taula hash. Aleshores van aparèixer les matrius, taules hash locals (per CPU) i matrius locals, arbres de cerca, mapes que contenien punters a programes BPF i molt més. El que ens interessa ara és que els programes BPF ara tenen la capacitat de mantenir l'estat entre trucades i compartir-lo amb altres programes i amb espai d'usuari.

S'accedeix a Maps des dels processos d'usuari mitjançant una trucada al sistema bpf(2), i dels programes BPF que s'executen al nucli utilitzant funcions d'ajuda. A més, existeixen ajudants no només per treballar amb mapes, sinó també per accedir a altres capacitats del nucli. Per exemple, els programes BPF poden utilitzar funcions d'ajuda per reenviar paquets a altres interfícies, generar esdeveniments perf, accedir a estructures del nucli, etc.

BPF per als més petits, primera part: BPF ampliat

En resum, BPF ofereix la possibilitat de carregar codi d'usuari arbitrari, és a dir, provat pel verificador, a l'espai del nucli. Aquest codi pot desar l'estat entre trucades i intercanviar dades amb l'espai d'usuari, i també té accés als subsistemes del nucli que permet aquest tipus de programes.

Això ja és similar a les capacitats que proporcionen els mòduls del nucli, en comparació amb les quals BPF té alguns avantatges (per descomptat, només podeu comparar aplicacions similars, per exemple, el seguiment del sistema; no podeu escriure un controlador arbitrari a BPF). Podeu observar un llindar d'entrada més baix (algunes utilitats que utilitzen BPF no requereixen que l'usuari tingui habilitats de programació del nucli, o habilitats de programació en general), seguretat en temps d'execució (aixequeu la mà als comentaris per a aquells que no han trencat el sistema en escriure). o mòduls de prova), atomicitat: hi ha temps d'inactivitat quan es recarreguen els mòduls i el subsistema BPF assegura que no es perdi cap esdeveniment (per ser justos, això no és cert per a tots els tipus de programes BPF).

La presència d'aquestes capacitats fa que BPF sigui una eina universal per expandir el nucli, cosa que es confirma a la pràctica: cada cop s'afegeixen més nous tipus de programes a BPF, cada cop més grans empreses utilitzen BPF als servidors de combat les 24 hores del dia, els 7 dies de la setmana, cada cop més. les startups construeixen el seu negoci a partir de solucions basades en les quals es basen en BPF. BPF s'utilitza a tot arreu: per protegir contra atacs DDoS, crear SDN (per exemple, implementar xarxes per a kubernetes), com a principal eina de seguiment del sistema i col·lector d'estadístiques, en sistemes de detecció d'intrusions i sistemes sandbox, etc.

Acabem aquí la part general de l'article i mirem la màquina virtual i l'ecosistema BPF amb més detall.

Digressió: utilitats

Per poder executar els exemples de les seccions següents, és possible que necessiteu diverses utilitats, almenys llvm/clang amb suport bpf i bpftool... A la secció Eines de desenvolupament Podeu llegir les instruccions per muntar les utilitats, així com el vostre nucli. Aquesta secció es col·loca a continuació per no pertorbar l'harmonia de la nostra presentació.

Sistema d'instruccions i registres de màquines virtuals BPF

L'arquitectura i el sistema de comandaments de BPF es van desenvolupar tenint en compte el fet que els programes s'escriuran en llenguatge C i, després de carregar-los al nucli, es traduiran al codi natiu. Per tant, el nombre de registres i el conjunt d'ordres es van triar tenint en compte la intersecció, en sentit matemàtic, de les capacitats de les màquines modernes. A més, es van imposar diverses restriccions als programes, per exemple, fins fa poc no era possible escriure bucles i subrutines, i el nombre d'instruccions estava limitat a 4096 (ara els programes amb privilegis poden carregar fins a un milió d'instruccions).

BPF té onze registres de 64 bits accessibles per l'usuari r0-r10 i un comptador de programes. Registra't r10 conté un punter de marc i és només de lectura. Els programes tenen accés a una pila de 512 bytes en temps d'execució i una quantitat il·limitada de memòria compartida en forma de mapes.

Els programes BPF poden executar un conjunt específic d'ajudants del nucli de tipus de programa i, més recentment, funcions regulars. Cada funció cridada pot prendre fins a cinc arguments, passats en registres r1-r5, i es passa el valor de retorn a r0. Es garanteix que després de tornar de la funció, el contingut dels registres r6-r9 No canviarà.

Per a una traducció eficient del programa, registres r0-r11 per a totes les arquitectures admeses es mapegen de manera única a registres reals, tenint en compte les característiques ABI de l'arquitectura actual. Per exemple, per x86_64 registres r1-r5, utilitzat per passar paràmetres de funció, es mostren a rdi, rsi, rdx, rcx, r8, que s'utilitzen per passar paràmetres a funcions activades x86_64. Per exemple, el codi de l'esquerra es tradueix al codi de la dreta així:

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

Registra't r0 també s'utilitza per retornar el resultat de l'execució del programa, i al registre r1 al programa se li passa un punter al context; depenent del tipus de programa, aquest podria ser, per exemple, una estructura struct xdp_md (per a XDP) o estructura struct __sk_buff (per a diferents programes de xarxa) o estructura struct pt_regs (per a diferents tipus de programes de traça), etc.

Així doncs, teníem un conjunt de registres, ajudants del nucli, una pila, un punter de context i memòria compartida en forma de mapes. No és que tot això sigui absolutament necessari durant el viatge, però...

Continuem la descripció i parlem del sistema d'ordres per treballar amb aquests objectes. Tots (Gairebé tots) Les instruccions BPF tenen una mida fixa de 64 bits. Si mireu una instrucció en una màquina Big Endian de 64 bits, veureu

BPF per als més petits, primera part: BPF ampliat

Aquí Code - aquesta és la codificació de la instrucció, Dst/Src són les codificacions del receptor i la font, respectivament, Off - Sagnat signat de 16 bits i Imm és un nombre enter amb signe de 32 bits utilitzat en algunes instruccions (similar a la constant K de cBPF). Codificació Code té un de dos tipus:

BPF per als més petits, primera part: BPF ampliat

Les classes d'instrucció 0, 1, 2, 3 defineixen ordres per treballar amb memòria. Ells són anomenats, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respectivament. Classes 4, 7 (BPF_ALU, BPF_ALU64) constitueixen un conjunt d'instruccions ALU. Classes 5, 6 (BPF_JMP, BPF_JMP32) conté instruccions de salt.

El pla addicional per estudiar el sistema d'instruccions BPF és el següent: en comptes d'enumerar meticulosament totes les instruccions i els seus paràmetres, veurem un parell d'exemples en aquesta secció i a partir d'ells quedarà clar com funcionen realment les instruccions i com desmunteu manualment qualsevol fitxer binari per a BPF. Per consolidar el material més endavant en l'article, també ens trobarem amb instruccions individuals a les seccions sobre Verificador, compilador JIT, traducció de BPF clàssic, així com a l'hora d'estudiar mapes, cridar funcions, etc.

Quan parlem d'instruccions individuals, ens referirem als fitxers bàsics bpf.h и bpf_common.h, que defineixen els codis numèrics de les instruccions BPF. Quan estudieu arquitectura pel vostre compte i/o analitzeu binaris, podeu trobar la semàntica a les fonts següents, ordenades per ordre de complexitat: Especificació eBPF no oficial, Guia de referència BPF i XDP, conjunt d'instruccions, Documentació/xarxes/filter.txt i, per descomptat, al codi font de Linux: verificador, JIT, intèrpret BPF.

Exemple: desmuntar BPF al cap

Vegem un exemple en què compilem un programa readelf-example.c i mireu el binari resultant. Desvelarem el contingut original readelf-example.c a continuació, després de restaurar la seva lògica a partir de codis binaris:

$ 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 ................

Primera columna a la sortida readelf és un sagnat i, per tant, el nostre programa consta de quatre ordres:

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

Els codis de comandament són iguals b7, 15, b7 и 95. Recordeu que els tres bits menys significatius són la classe d'instrucció. En el nostre cas, el quart bit de totes les instruccions està buit, de manera que les classes d'instruccions són 7, 5, 7, 5, respectivament. La classe 7 és BPF_ALU64, i 5 és BPF_JMP. Per a ambdues classes, el format d'instrucció és el mateix (vegeu més amunt) i podem reescriure el nostre programa així (al mateix temps reescriurem les columnes restants 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

Operació b classe ALU64 - És BPF_MOV. Assigna un valor al registre de destinació. Si el bit està configurat s (font), aleshores el valor s'agafa del registre d'origen, i si, com en el nostre cas, no està establert, el valor s'agafa del camp Imm. Així que a la primera i tercera instruccions realitzem l'operació r0 = Imm. A més, l'operació JMP classe 1 és BPF_JEQ (saltar si és igual). En el nostre cas, des del bit S és zero, compara el valor del registre d'origen amb el camp Imm. Si els valors coincideixen, es produeix la transició a PC + OffOn PC, com és habitual, conté l'adreça de la següent instrucció. Finalment, l'operació JMP Classe 9 és BPF_EXIT. Aquesta instrucció finalitza el programa i torna al nucli r0. Afegim una nova columna a la nostra taula:

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

Podem reescriure això d'una forma més convenient:

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

Si recordem el que hi ha al registre r1 al programa se li passa un punter al context des del nucli i al registre r0 el valor es retorna al nucli, aleshores podem veure que si el punter al context és zero, retornem 1, i en cas contrari - 2. Comprovem que tenim raó mirant la font:

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

Sí, és un programa sense sentit, però es tradueix en només quatre instruccions senzilles.

Exemple d'excepció: instrucció de 16 bytes

Hem esmentat anteriorment que algunes instruccions ocupen més de 64 bits. Això s'aplica, per exemple, a les instruccions lddw (Codi = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — carregueu una paraula doble dels camps al registre Imm. El fet és que Imm té una mida de 32 i una paraula doble és de 64 bits, de manera que carregar un valor immediat de 64 bits en un registre en una instrucció de 64 bits no funcionarà. Per fer-ho, s'utilitzen dues instruccions adjacents per emmagatzemar la segona part del valor de 64 bits al camp Imm... Exemple:

$ 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                   ........

Només hi ha dues instruccions en un programa binari:

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

Ens tornarem a trobar amb instruccions lddw, quan parlem de trasllats i de treball amb mapes.

Exemple: desmuntatge de BPF amb eines estàndard

Per tant, hem après a llegir codis binaris BPF i estem preparats per analitzar qualsevol instrucció si cal. Tanmateix, val la pena dir que a la pràctica és més còmode i ràpid desmuntar programes amb eines estàndard, per exemple:

$ 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

Cicle de vida dels objectes BPF, sistema de fitxers bpffs

(Primer vaig aprendre alguns dels detalls descrits en aquesta subsecció de publicació Alexei Starovoitov a Bloc BPF.)

Els objectes BPF - programes i mapes - es creen des de l'espai d'usuari mitjançant ordres BPF_PROG_LOAD и BPF_MAP_CREATE trucada al sistema bpf(2), parlarem exactament de com passa això a la secció següent. Això crea estructures de dades del nucli i per a cadascuna d'elles refcount (recompte de referències) s'estableix en un i es retorna a l'usuari un descriptor de fitxer que apunta a l'objecte. Després de tancar el mànec refcount l'objecte es redueix en un, i quan arriba a zero, l'objecte es destrueix.

Si el programa utilitza mapes, aleshores refcount aquests mapes s'incrementen en un després de carregar el programa, és a dir. els seus descriptors de fitxers es poden tancar des del procés d'usuari i encara refcount no es convertirà en zero:

BPF per als més petits, primera part: BPF ampliat

Després de carregar correctament un programa, normalment l'adjuntem a algun tipus de generador d'esdeveniments. Per exemple, el podem posar en una interfície de xarxa per processar paquets entrants o connectar-lo a alguns tracepoint al nucli. En aquest punt, el comptador de referència també augmentarà en un i podrem tancar el descriptor del fitxer al programa carregador.

Què passa si ara tanquem el carregador d'arrencada? Depèn del tipus de generador d'esdeveniments (ganxo). Tots els ganxos de xarxa existiran un cop finalitzat el carregador, aquests són els anomenats ganxos globals. I, per exemple, els programes de traça es publicaran després que finalitzi el procés que els va crear (i, per tant, s'anomenen locals, de "local al procés"). Tècnicament, els ganxos locals sempre tenen un descriptor de fitxer corresponent a l'espai d'usuari i, per tant, es tanquen quan es tanca el procés, però els ganxos globals no. A la figura següent, fent servir creus vermelles, intento mostrar com la terminació del programa carregador afecta la vida útil dels objectes en el cas dels ganxos locals i globals.

BPF per als més petits, primera part: BPF ampliat

Per què hi ha una distinció entre ganxos locals i globals? L'execució d'alguns tipus de programes de xarxa té sentit sense un espai d'usuari, per exemple, imagineu la protecció DDoS: el carregador d'arrencada escriu les regles i connecta el programa BPF a la interfície de xarxa, després de la qual cosa el carregador d'arrencada es pot matar. D'altra banda, imagineu-vos un programa de traça de depuració que hàgiu escrit de genolls en deu minuts; quan s'hagi acabat, us agradaria que no quedés escombraries al sistema i els ganxos locals ho garantiran.

D'altra banda, imagineu que voleu connectar-vos a un punt de traça al nucli i recopilar estadístiques durant molts anys. En aquest cas, voldríeu completar la part d'usuari i tornar a les estadístiques de tant en tant. El sistema de fitxers bpf ofereix aquesta oportunitat. És un pseudosistema de fitxers en memòria que permet la creació de fitxers que fan referència a objectes BPF i, per tant, augmenten refcount objectes. Després d'això, el carregador pot sortir i els objectes que va crear es mantindran vius.

BPF per als més petits, primera part: BPF ampliat

La creació de fitxers en bpffs que fan referència a objectes BPF s'anomena "fixació" (com en la frase següent: "el procés pot fixar un programa o un mapa BPF"). La creació d'objectes de fitxer per a objectes BPF té sentit no només per allargar la vida dels objectes locals, sinó també per a la usabilitat dels objectes globals: tornant a l'exemple amb el programa de protecció global DDoS, volem poder veure les estadístiques. de tant en tant.

El sistema de fitxers BPF sol estar muntat /sys/fs/bpf, però també es pot muntar localment, per exemple, així:

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

Els noms del sistema de fitxers es creen mitjançant l'ordre BPF_OBJ_PIN Trucada al sistema BPF. Per il·lustrar-ho, agafem un programa, el compilem, el pengem i el fixem bpffs. El nostre programa no fa res útil, només us presentem el codi perquè pugueu reproduir l'exemple:

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

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

Compilem aquest programa i creem una còpia local del sistema de fitxers bpffs:

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

Ara descarreguem el nostre programa mitjançant la utilitat bpftool i mireu les trucades del sistema que l'acompanyen bpf(2) (algunes línies irrellevants s'han eliminat de la sortida de l'estrace):

$ 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í hem carregat el programa utilitzant BPF_PROG_LOAD, va rebre un descriptor de fitxer del nucli 3 i utilitzant l'ordre BPF_OBJ_PIN ha fixat aquest descriptor de fitxer com a fitxer "bpf-mountpoint/test". Després d'això, el programa del carregador d'arrencada bpftool va acabar de funcionar, però el nostre programa va romandre al nucli, tot i que no el vam connectar a cap interfície de xarxa:

$ 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

Podem suprimir l'objecte fitxer amb normalitat unlink(2) i després d'això s'eliminarà el programa corresponent:

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

Esborrar objectes

Parlant de la supressió d'objectes, cal aclarir que després d'haver desconnectat el programa del ganxo (generador d'esdeveniments), ni un sol esdeveniment nou activarà el seu llançament, però, totes les instàncies actuals del programa es completaran en l'ordre normal. .

Alguns tipus de programes BPF permeten substituir el programa sobre la marxa, és a dir. proporcionar atomicitat de la seqüència replace = detach old program, attach new program. En aquest cas, totes les instàncies actives de l'antiga versió del programa acabaran la seva feina i es crearan nous controladors d'esdeveniments a partir del nou programa, i "atomicitat" aquí significa que no es perdrà cap esdeveniment.

Adjuntar programes a fonts d'esdeveniments

En aquest article, no descriurem per separat la connexió de programes amb fonts d'esdeveniments, ja que té sentit estudiar-ho en el context d'un tipus específic de programa. Cm. exemple a continuació, en què mostrem com es connecten programes com XDP.

Manipulació d'objectes mitjançant la crida al sistema bpf

Programes BPF

Tots els objectes BPF es creen i gestionen des de l'espai d'usuari mitjançant una trucada al sistema bpf, amb el següent prototip:

#include <linux/bpf.h>

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

Aquí teniu l'equip cmd és un dels valors del tipus enum bpf_cmd, attr — un punter als paràmetres d'un programa específic i size — mida de l'objecte segons el punter, és a dir. normalment això sizeof(*attr). Al nucli 5.8 la crida al sistema bpf suporta 34 ordres diferents i determinació de union bpf_attr ocupa 200 línies. Però això no ens hem de deixar intimidar, ja que ens anirem familiaritzant amb les ordres i els paràmetres al llarg de diversos articles.

Comencem per l'equip BPF_PROG_LOAD, que crea programes BPF: pren un conjunt d'instruccions BPF i el carrega al nucli. En el moment de la càrrega, s'inicia el verificador, i després el compilador JIT i, després d'una execució correcta, es retorna a l'usuari el descriptor del fitxer de programa. Hem vist què li passa a l'apartat anterior sobre el cicle de vida dels objectes BPF.

Ara escriurem un programa personalitzat que carregarà un programa BPF senzill, però primer hem de decidir quin tipus de programa volem carregar; haurem de seleccionar Escriviu i en el marc d'aquest tipus, escriu un programa que superarà la prova del verificador. Tanmateix, per no complicar el procés, aquí teniu una solució ja feta: agafarem un programa com BPF_PROG_TYPE_XDP, que retornarà el valor XDP_PASS (omet tots els paquets). En l'assemblador BPF sembla molt senzill:

r0 = 2
exit

Després d'haver decidit que penjarem, us podem dir com ho farem:

#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();
}

Els esdeveniments interessants d'un programa comencen amb la definició d'una matriu insns - el nostre programa BPF en codi màquina. En aquest cas, cada instrucció del programa BPF s'empaqueta a l'estructura bpf_insn. Primer element insns compleix les instruccions r0 = 2, el segon - exit.

Retirada. El nucli defineix macros més convenients per escriure codis de màquina i utilitzar el fitxer de capçalera del nucli tools/include/linux/filter.h podríem escriure

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

Però com que escriure programes BPF en codi natiu només és necessari per escriure proves al nucli i articles sobre BPF, l'absència d'aquestes macros no complica realment la vida del desenvolupador.

Després de definir el programa BPF, passem a carregar-lo al nucli. El nostre conjunt minimalista de paràmetres attr inclou el tipus de programa, el conjunt i el nombre d'instruccions, la llicència necessària i el nom "woo", que fem servir per trobar el nostre programa al sistema després de descarregar-lo. El programa, tal com s'havia promès, es carrega al sistema mitjançant una trucada al sistema bpf.

Al final del programa acabem en un bucle infinit que simula la càrrega útil. Sense ell, el nucli matarà el programa quan es tanqui el descriptor de fitxers que ens ha retornat la trucada del sistema. bpf, i no ho veurem al sistema.

Bé, estem preparats per a la prova. Muntem i executem el programa sota straceper comprovar que tot funciona com cal:

$ 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(

Tot està bé, bpf(2) ens va tornar el mànec 3 i vam entrar en un bucle infinit amb pause(). Intentem trobar el nostre programa al sistema. Per fer-ho anirem a un altre terminal i utilitzarem la utilitat 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)

Veiem que hi ha un programa carregat al sistema woo l'identificador global del qual és 390 i actualment està en curs simple-prog hi ha un descriptor de fitxer obert que apunta al programa (i si simple-prog acabarà la feina, doncs woo desapareixerà). Com era d'esperar, el programa woo pren 16 bytes -dues instruccions- de codis binaris a l'arquitectura BPF, però en la seva forma nativa (x86_64) ja són 40 bytes. Vegem el nostre programa en la seva forma original:

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

sense sorpreses. Ara mirem el codi generat pel 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

no gaire eficaç per exit(2), però per ser justos, el nostre programa és massa senzill, i per a programes no trivials, el pròleg i l'epíleg afegits pel compilador JIT són, per descomptat, necessaris.

Mapes

Els programes BPF poden utilitzar àrees de memòria estructurada accessibles tant per a altres programes BPF com per a programes de l'espai d'usuari. Aquests objectes s'anomenen mapes i en aquesta secció mostrarem com manipular-los mitjançant una trucada al sistema bpf.

Diguem de seguida que les capacitats dels mapes no es limiten només a l'accés a la memòria compartida. Hi ha mapes especials que contenen, per exemple, punters a programes BPF o punters a interfícies de xarxa, mapes per treballar amb esdeveniments perf, etc. Aquí no en parlarem, per no confondre el lector. A part d'això, ignorem els problemes de sincronització, ja que això no és important per als nostres exemples. Es pot trobar una llista completa dels tipus de mapes disponibles a <linux/bpf.h>, i en aquest apartat prendrem com a exemple el primer tipus històricament, la taula hash BPF_MAP_TYPE_HASH.

Si creeu una taula hash en, per exemple, C++, diríeu unordered_map<int,long> woo, que en rus significa “Necessito una taula woo mida il·limitada, les claus són de tipus int, i els valors són el tipus long" Per crear una taula hash BPF, hem de fer gairebé el mateix, excepte que hem d'especificar la mida màxima de la taula i, en comptes d'especificar els tipus de claus i valors, hem d'especificar les seves mides en bytes. . Per crear mapes utilitzeu l'ordre BPF_MAP_CREATE trucada al sistema bpf. Vegem un programa més o menys mínim que crea un mapa. Després del programa anterior que carrega programes BPF, aquest us hauria de semblar senzill:

$ 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í definim un conjunt de paràmetres attr, en què diem "Necessito una taula hash amb claus i valors de mida sizeof(int), en què puc posar un màxim de quatre elements". En crear mapes BPF, podeu especificar altres paràmetres, per exemple, de la mateixa manera que a l'exemple amb el programa, hem especificat el nom de l'objecte com a "woo".

Compilem i executem el 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í teniu la trucada del sistema bpf(2) ens va retornar el número del mapa descriptor 3 i després el programa, com s'esperava, espera més instruccions a la trucada del sistema pause(2).

Ara enviem el nostre programa a un segon pla o obrim un altre terminal i mirem el nostre objecte mitjançant la utilitat bpftool (podem distingir el nostre mapa dels altres pel seu nom):

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

El número 114 és l'identificador global del nostre objecte. Qualsevol programa del sistema pot utilitzar aquest ID per obrir un mapa existent mitjançant l'ordre BPF_MAP_GET_FD_BY_ID trucada al sistema bpf.

Ara podem jugar amb la nostra taula hash. Vegem-ne el contingut:

$ sudo bpftool map dump id 114
Found 0 elements

Buit. Posem-hi un valor hash[1] = 1:

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

Tornem a mirar la taula:

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

Hura! Hem aconseguit afegir un element. Tingueu en compte que hem de treballar a nivell de bytes per fer-ho, ja que bptftool no sap de quin tipus són els valors de la taula hash. (Aquest coneixement es pot transferir a ella mitjançant BTF, però ara més sobre això.)

Com llegeix i afegeix elements exactament bpftool? Fem una ullada sota el 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

Primer vam obrir el mapa pel seu ID global mitjançant l'ordre BPF_MAP_GET_FD_BY_ID и bpf(2) ens ha retornat el descriptor 3. Seguint fent servir l'ordre BPF_MAP_GET_NEXT_KEY hem trobat la primera clau a la taula passant NULL com a punter a la clau "anterior". Si tenim la clau ho podem fer BPF_MAP_LOOKUP_ELEMque retorna un valor a un punter value. El següent pas és que intentem trobar el següent element passant un punter a la clau actual, però la nostra taula només conté un element i l'ordre BPF_MAP_GET_NEXT_KEY torna ENOENT.

D'acord, canviem el valor per la clau 1, diguem que la nostra lògica empresarial requereix registrar-se 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

Com era d'esperar, és molt senzill: l'ordre BPF_MAP_GET_FD_BY_ID obre el nostre mapa per ID i l'ordre BPF_MAP_UPDATE_ELEM sobreescriu l'element.

Així, després de crear una taula hash des d'un programa, podem llegir i escriure el seu contingut des d'un altre. Tingueu en compte que si poguéssim fer-ho des de la línia d'ordres, qualsevol altre programa del sistema ho pot fer. A més de les ordres descrites anteriorment, per treballar amb mapes des de l'espai d'usuari, El següent:

  • BPF_MAP_LOOKUP_ELEM: trobar valor per clau
  • BPF_MAP_UPDATE_ELEM: actualitzar/crear valor
  • BPF_MAP_DELETE_ELEM: elimina la clau
  • BPF_MAP_GET_NEXT_KEY: cerca la següent (o primera) clau
  • BPF_MAP_GET_NEXT_ID: permet recórrer tots els mapes existents, així és com funciona bpftool map
  • BPF_MAP_GET_FD_BY_ID: obre un mapa existent pel seu identificador global
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: actualitza atòmicament el valor d'un objecte i retorna l'antic
  • BPF_MAP_FREEZE: fa que el mapa sigui immutable des de l'espai d'usuari (aquesta operació no es pot desfer)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: operacions massives. Per exemple, BPF_MAP_LOOKUP_AND_DELETE_BATCH - aquesta és l'única manera fiable de llegir i restablir tots els valors del mapa

No totes aquestes ordres funcionen per a tots els tipus de mapes, però, en general, treballar amb altres tipus de mapes des de l'espai d'usuari sembla exactament el mateix que treballar amb taules hash.

Per tal d'ordre, acabem els nostres experiments de taula hash. Recordeu que vam crear una taula que pot contenir fins a quatre claus? Afegim uns quants elements més:

$ 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

Fins ara, tot bé:

$ 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

Intentem afegir-ne un més:

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

Com era d'esperar, no ho hem aconseguit. Vegem l'error amb més detall:

$ 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 +++

Tot està bé: com era d'esperar, l'equip BPF_MAP_UPDATE_ELEM intenta crear una nova clau, cinquena, però falla E2BIG.

Així, podem crear i carregar programes BPF, així com crear i gestionar mapes des de l'espai d'usuari. Ara és lògic mirar com podem utilitzar els mapes dels mateixos programes BPF. Podríem parlar d'això en el llenguatge de programes difícils de llegir en codis de macro de màquines, però de fet ha arribat el moment de mostrar com s'escriuen i mantenen els programes BPF, fent servir libbpf.

(Per als lectors que no estan satisfets amb la manca d'un exemple de baix nivell: analitzarem amb detall programes que utilitzen mapes i funcions d'ajuda creades amb libbpf i explicar-te què passa a nivell d'instrucció. Per als lectors que no estan satisfets molt, vam afegir exemple al lloc adequat de l'article.)

Escriptura de programes BPF amb libbpf

Escriure programes BPF amb codis de màquina només pot ser interessant la primera vegada i després s'instal·la la sacietat. En aquest moment cal centrar la vostra atenció llvm, que té un backend per generar codi per a l'arquitectura BPF, així com una biblioteca libbpf, que us permet escriure el costat d'usuari de les aplicacions BPF i carregar el codi dels programes BPF generats mitjançant llvm/clang.

De fet, com veurem en aquest i els següents articles, libbpf fa força feina sense ell (o eines similars - iproute2, libbcc, libbpf-go, etc.) és impossible viure. Una de les característiques assassines del projecte libbpf és BPF CO-RE (Compile Once, Run Everywhere): un projecte que permet escriure programes BPF que són portàtils d'un nucli a un altre, amb la possibilitat d'executar-se en diferents API (per exemple, quan l'estructura del nucli canvia de versió). a la versió). Per poder treballar amb CO-RE, el vostre nucli ha d'estar compilat amb suport BTF (descriurem com fer-ho a la secció Eines de desenvolupament. Podeu comprovar si el vostre nucli està construït amb BTF o no molt simplement, amb la presència del següent fitxer:

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

Aquest fitxer emmagatzema informació sobre tots els tipus de dades utilitzats al nucli i s'utilitza en tots els nostres exemples d'ús libbpf. Parlarem amb detall sobre CO-RE al proper article, però en aquest, només heu de crear un nucli amb CONFIG_DEBUG_INFO_BTF.

Biblioteca libbpf viu directament al directori tools/lib/bpf nucli i el seu desenvolupament es realitza a través de la llista de correu [email protected]. Tanmateix, es manté un repositori separat per a les necessitats de les aplicacions que viuen fora del nucli https://github.com/libbpf/libbpf en què la biblioteca del nucli es reflecteix per a l'accés de lectura més o menys tal com està.

En aquesta secció veurem com es pot crear un projecte que utilitzi libbpf, escrivim diversos programes de prova (més o menys sense sentit) i analitzem amb detall com funciona tot. Això ens permetrà explicar més fàcilment en les següents seccions exactament com interactuen els programes BPF amb mapes, ajudants del nucli, BTF, etc.

Normalment es fan servir projectes libbpf afegir un repositori GitHub com a submòdul git, farem el mateix:

$ 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.

Anar a libbpf molt simple:

$ 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

El nostre següent pla en aquesta secció és el següent: escriurem un programa BPF com BPF_PROG_TYPE_XDP, el mateix que a l'exemple anterior, però en C, el compilem utilitzant clang, i escriviu un programa d'ajuda que el carregarà al nucli. En els apartats següents ampliarem les capacitats tant del programa BPF com del programa d'assistent.

Exemple: crear una aplicació completa amb libbpf

Per començar, fem servir el fitxer /sys/kernel/btf/vmlinux, que es va esmentar més amunt, i crear el seu equivalent en forma de fitxer de capçalera:

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

Aquest fitxer emmagatzemarà totes les estructures de dades disponibles al nostre nucli, per exemple, així és com es defineix la capçalera IPv4 al nucli:

$ 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;
};

Ara escriurem el nostre 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";

Tot i que el nostre programa va resultar ser molt senzill, encara hem de parar atenció a molts detalls. En primer lloc, el primer fitxer de capçalera que incloem és vmlinux.h, que acabem de generar utilitzant bpftool btf dump - ara no necessitem instal·lar el paquet kernel-headers per esbrinar com són les estructures del nucli. El següent fitxer de capçalera ens arriba des de la biblioteca libbpf. Ara només ens falta per definir la macro SEC, que envia el caràcter a la secció adequada del fitxer d'objectes ELF. El nostre programa està inclòs a la secció xdp/simple, on abans de la barra definim el tipus de programa BPF: aquesta és la convenció que s'utilitza libbpf, segons el nom de la secció, substituirà el tipus correcte a l'inici bpf(2). El mateix programa BPF ho és C - molt senzill i consta d'una línia return XDP_PASS. Finalment, un apartat a part "license" conté el nom de la llicència.

Podem compilar el nostre programa utilitzant llvm/clang, versió >= 10.0.0, o millor encara, superior (vegeu la secció Eines de desenvolupament):

$ 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 les característiques interessants: indiquem l'arquitectura objectiu -target bpf i el camí cap a les capçaleres libbpf, que hem instal·lat recentment. A més, no us oblideu -O2, sense aquesta opció és possible que tingueu sorpreses en el futur. Mirem el nostre codi, hem aconseguit escriure el programa que volíem?

$ 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

Sí, va funcionar! Ara, tenim un fitxer binari amb el programa i volem crear una aplicació que el carregarà al nucli. Amb aquesta finalitat la biblioteca libbpf ens ofereix dues opcions: utilitzar una API de nivell inferior o una API de nivell superior. Anirem pel segon camí, ja que volem aprendre a escriure, carregar i connectar programes BPF amb el mínim esforç per al seu estudi posterior.

Primer, hem de generar l'"esquelet" del nostre programa a partir del seu binari utilitzant la mateixa utilitat bpftool — el ganivet suís del món BPF (que es pot prendre literalment, ja que Daniel Borkman, un dels creadors i mantenedors de BPF, és suís):

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

A l'arxiu xdp-simple.skel.h conté el codi binari del nostre programa i funcions per gestionar - carregar, adjuntar, esborrar el nostre objecte. En el nostre cas senzill, això sembla exagerat, però també funciona en el cas en què el fitxer objecte conté molts programes i mapes BPF i per carregar aquest ELF gegant només hem de generar l'esquelet i cridar una o dues funcions des de l'aplicació personalitzada que tenim. estem escrivint Anem endavant ara.

En sentit estricte, el nostre programa de càrrega és 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 definit a l'arxiu xdp-simple.skel.h i descriu el nostre fitxer objecte:

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

Aquí podem veure rastres d'una API de baix nivell: l'estructura struct bpf_program *simple и struct bpf_link *simple. La primera estructura descriu específicament el nostre programa, escrit a la secció xdp/simple, i el segon descriu com es connecta el programa a la font de l'esdeveniment.

Funció xdp_simple_bpf__open_and_load, obre un objecte ELF, l'analitza, crea totes les estructures i subestructures (a més del programa, ELF també conté altres seccions: dades, dades només de lectura, informació de depuració, llicència, etc.), i després el carrega al nucli mitjançant un sistema. anomenada bpf, que podem comprovar compilant i executant el 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

Vegem ara el nostre programa utilitzant bpftool. Trobem el 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)

i dump (utilitzem una forma abreujada de l'ordre bpftool prog dump xlated):

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

Alguna cosa nova! El programa va imprimir trossos del nostre fitxer font C. Això ho va fer la biblioteca libbpf, que va trobar la secció de depuració al binari, la va compilar en un objecte BTF, la va carregar al nucli utilitzant BPF_BTF_LOAD, i després va especificar el descriptor del fitxer resultant en carregar el programa amb l'ordre BPG_PROG_LOAD.

Ajudants del nucli

Els programes BPF poden executar funcions "externes": ajudants del nucli. Aquestes funcions d'ajuda permeten als programes BPF accedir a les estructures del nucli, gestionar mapes i també comunicar-se amb el "món real": crear esdeveniments de perf, controlar el maquinari (per exemple, redirigir paquets), etc.

Exemple: bpf_get_smp_processor_id

En el marc del paradigma "aprendre amb l'exemple", considerem una de les funcions auxiliars, bpf_get_smp_processor_id(), cert a l'arxiu kernel/bpf/helpers.c. Retorna el número del processador en què s'està executant el programa BPF que l'ha cridat. Però no ens interessa tant la seva semàntica com el fet que la seva implementació pren una línia:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Les definicions de la funció auxiliar de BPF són similars a les definicions de trucades del sistema Linux. Aquí, per exemple, es defineix una funció que no té arguments. (Una funció que pren, per exemple, tres arguments es defineix mitjançant la macro BPF_CALL_3. El nombre màxim d'arguments és de cinc.) Tanmateix, aquesta és només la primera part de la definició. La segona part és definir l'estructura del tipus struct bpf_func_proto, que conté una descripció de la funció d'ajuda que el verificador entén:

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

Registre de funcions d'ajuda

Perquè els programes BPF d'un tipus concret utilitzin aquesta funció, l'han de registrar, per exemple per al tipus BPF_PROG_TYPE_XDP una funció està definida al nucli xdp_func_proto, que determina a partir de l'ID de la funció auxiliar si XDP admet aquesta funció o no. La nostra funció és suports:

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;
    ...
    }
}

Els nous tipus de programes BPF estan "definits" al fitxer include/linux/bpf_types.h utilitzant una macro BPF_PROG_TYPE. Definit entre cometes perquè és una definició lògica, i en termes del llenguatge C la definició de tot un conjunt d'estructures concretes es produeix en altres llocs. En concret, a l'expedient kernel/bpf/verifier.c totes les definicions del fitxer bpf_types.h s'utilitzen per crear una sèrie d'estructures 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
};

És a dir, per a cada tipus de programa BPF, es defineix un punter a una estructura de dades del tipus struct bpf_verifier_ops, que s'inicia amb el valor _name ## _verifier_ops, és a dir, xdp_verifier_ops per xdp. Estructura xdp_verifier_ops determinat per a l'arxiu net/core/filter.c de la manera següent:

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í veiem la nostra funció familiar xdp_func_proto, que executarà el verificador cada vegada que trobi un repte algun tipus funcions dins d'un programa BPF, vegeu verifier.c.

Vegem com un hipotètic programa BPF utilitza la funció bpf_get_smp_processor_id. Per fer-ho, tornem a escriure el programa de la nostra secció anterior de la següent manera:

#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ímbol bpf_get_smp_processor_id determinat per в <bpf/bpf_helper_defs.h> biblioteca libbpf как

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

això és, bpf_get_smp_processor_id és un punter de funció el valor del qual és 8, on 8 és el valor BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, que es defineix per a nosaltres al fitxer vmlinux.h (dossier bpf_helper_defs.h al nucli es genera mitjançant un script, de manera que els números "màgics" estan bé). Aquesta funció no pren arguments i retorna un valor de tipus __u32. Quan l'executem al nostre programa, clang genera una instrucció BPF_CALL "el tipus adequat" Compilem el programa i mirem la secció 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

A la primera línia veiem instruccions call, paràmetre IMM que és igual a 8, i SRC_REG -zero. Segons l'acord ABI utilitzat pel verificador, es tracta d'una trucada a la funció d'ajuda número vuit. Un cop posat en marxa, la lògica és senzilla. Valor de retorn del registre r0 copiat a r1 i a les línies 2,3 es converteix en tipus u32 — S'esborren els 32 bits superiors. A les línies 4,5,6,7 tornem 2 (XDP_PASS) o 1 (XDP_DROP) depenent de si la funció auxiliar de la línia 0 ha retornat un valor zero o diferent de zero.

Posem-nos a prova: carregueu el programa i mireu la sortida 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

D'acord, el verificador ha trobat l'ajudant del nucli correcte.

Exemple: passar arguments i finalment executar el programa!

Totes les funcions auxiliars de nivell d'execució tenen un prototip

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

Els paràmetres de les funcions auxiliars es passen en registres r1-r5, i el valor es retorna al registre r0. No hi ha funcions que tinguin més de cinc arguments i no s'espera que s'afegeixin suport per a ells en el futur.

Fem una ullada al nou ajudant del nucli i a com BPF passa els paràmetres. Reescriurem xdp-simple.bpf.c de la següent manera (la resta de línies no han canviat):

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

El nostre programa imprimeix el número de la CPU on s'està executant. Compilem-lo i mirem el codi:

$ 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

A les línies 0-7 escrivim la cadena running on CPU%un, i després a la línia 8 executem la familiar bpf_get_smp_processor_id. A les línies 9-12 preparem els arguments auxiliars bpf_printk - registres r1, r2, r3. Per què n'hi ha tres i no dos? Perquè bpf_printkaquest és un embolcall de macro al voltant de l'ajudant real bpf_trace_printk, que ha de passar la mida de la cadena de format.

Ara afegim un parell de línies a xdp-simple.cperquè el nostre programa es connecti a la interfície lo i realment va començar!

$ 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í fem servir la funció bpf_set_link_xdp_fd, que connecta programes BPF tipus XDP a interfícies de xarxa. Hem codificat el número de la interfície lo, que sempre és 1. Executem la funció dues vegades per desconnectar primer el programa antic si estava adjunt. Tingueu en compte que ara no necessitem cap repte pause o un bucle infinit: el nostre programa carregador sortirà, però el programa BPF no es destruirà ja que està connectat a la font de l'esdeveniment. Després d'una descàrrega i connexió correctes, el programa s'iniciarà per a cada paquet de xarxa que arribi lo.

Descarreguem el programa i mirem la interfície 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

El programa que hem baixat té ID 669 i veiem el mateix ID a la interfície lo. Enviarem un parell de paquets a 127.0.0.1 (sol·licitud + resposta):

$ ping -c1 localhost

i ara mirem el contingut del fitxer virtual de depuració /sys/kernel/debug/tracing/trace_pipe, en quin bpf_printk escriu els seus missatges:

# 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

Es van detectar dos paquets lo i processat a CPU0: el nostre primer programa BPF sense sentit va funcionar!

Val la pena assenyalar-ho bpf_printk No és per res que escrigui al fitxer de depuració: aquest no és l'ajudant amb més èxit per utilitzar-lo en producció, però el nostre objectiu era mostrar alguna cosa senzilla.

Accés a mapes des dels programes BPF

Exemple: utilitzant un mapa del programa BPF

A les seccions anteriors vam aprendre a crear i utilitzar mapes des de l'espai d'usuari, i ara mirem la part del nucli. Comencem, com és habitual, amb un exemple. Reescriurem el nostre programa xdp-simple.bpf.c de la manera següent:

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

A l'inici del programa hem afegit una definició de mapa woo: Aquesta és una matriu de 8 elements que emmagatzema valors com u64 (en C definiríem una matriu com u64 woo[8]). En un programa "xdp/simple" obtenim el número de processador actual en una variable key i després utilitzant la funció d'ajuda bpf_map_lookup_element obtenim un punter a l'entrada corresponent a la matriu, que augmentem en un. Traduït al rus: calculem estadístiques sobre quina CPU ha processat els paquets entrants. Intentem executar el 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

Comprovem que està enganxada lo i envia alguns paquets:

$ 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

Ara mirem el contingut de la matriu:

$ 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 }
]

Gairebé tots els processos es van processar a la CPU7. Això no és important per a nosaltres, el més important és que el programa funcioni i entenem com accedir als mapes des dels programes BPF, fent servir хелперов bpf_mp_*.

Índex místic

Així, podem accedir al mapa des del programa BPF mitjançant trucades com

val = bpf_map_lookup_elem(&woo, &key);

on sembla la funció d'ajuda

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

però estem passant un punter &woo a una estructura sense nom struct { ... }...

Si mirem l'assemblador del programa, veiem que el valor &woo no està realment definit (línia 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
...

i està inclosa en les reubicacions:

$ 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

Però si mirem el programa ja carregat, veiem un punter al mapa correcte (línia 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]
...

Així, podem concloure que en el moment de llançar el nostre programa carregador, l'enllaç a &woo va ser substituït per alguna cosa amb una biblioteca libbpf. Primer veurem la sortida 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

Ho veiem libbpf va crear un mapa woo i després vam descarregar el nostre programa simple. Vegem més de prop com carreguem el programa:

  • anomenada xdp_simple_bpf__open_and_load del fitxer xdp-simple.skel.h
  • que provoca xdp_simple_bpf__load del fitxer xdp-simple.skel.h
  • que provoca bpf_object__load_skeleton del fitxer libbpf/src/libbpf.c
  • que provoca bpf_object__load_xattr d' libbpf/src/libbpf.c

L'última funció, entre altres coses, cridarà bpf_object__create_maps, que crea o obre mapes existents, convertint-los en descriptors de fitxers. (Aquí és on veiem BPF_MAP_CREATE a la sortida strace.) A continuació s'anomena la funció bpf_object__relocate i és ella qui ens interessa, ja que recordem el que vam veure woo a la taula de reubicació. Explorant-lo, finalment ens trobem a la funció bpf_program__relocate, quin s'ocupa dels trasllats de mapes:

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

Així que prenem les nostres instruccions

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

i substituïu el registre d'origen que hi ha per BPF_PSEUDO_MAP_FD, i el primer IMM al descriptor de fitxer del nostre mapa i, si és igual, per exemple, 0xdeadbeef, llavors, com a resultat, rebrem la instrucció

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

Així és com es transfereix la informació del mapa a un programa BPF carregat específic. En aquest cas, el mapa es pot crear utilitzant BPF_MAP_CREATE, i s'obre per identificació utilitzant BPF_MAP_GET_FD_BY_ID.

Total, quan s'utilitza libbpf l'algorisme és el següent:

  • durant la compilació, es creen registres a la taula de reubicació per als enllaços als mapes
  • libbpf obre el llibre d'objectes ELF, troba tots els mapes utilitzats i crea descriptors de fitxers per a ells
  • els descriptors de fitxers es carreguen al nucli com a part de la instrucció LD64

Com us podeu imaginar, hi ha més coses per venir i haurem de mirar-ne el nucli. Afortunadament, tenim una pista: hem escrit el significat BPF_PSEUDO_MAP_FD al registre de la font i podem enterrar-lo, que ens portarà al sant de tots els sants - kernel/bpf/verifier.c, on una funció amb un nom distintiu substitueix un descriptor de fitxer per l'adreça d'una estructura de tipus 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;

(es pot trobar el codi complet по ссылке). Així que podem ampliar el nostre algorisme:

  • mentre es carrega el programa, el verificador verifica l'ús correcte del mapa i escriu l'adreça de l'estructura corresponent struct bpf_map

Quan descarregueu el binari ELF utilitzant libbpf Hi ha moltes més coses, però en parlarem en altres articles.

Carregant programes i mapes sense libbpf

Com es va prometre, aquí teniu un exemple per als lectors que volen saber com crear i carregar un programa que utilitza mapes, sense ajuda libbpf. Això pot ser útil quan esteu treballant en un entorn per al qual no podeu crear dependències, desar tots els bits o escriure un programa com ara ply, que genera codi binari BPF sobre la marxa.

Per facilitar el seguiment de la lògica, reescriurem el nostre exemple amb aquests propòsits xdp-simple. El codi complet i lleugerament ampliat del programa tractat en aquest exemple es pot trobar en aquest essència.

La lògica de la nostra aplicació és la següent:

  • crear un mapa de tipus BPF_MAP_TYPE_ARRAY utilitzant l'ordre BPF_MAP_CREATE,
  • crear un programa que utilitzi aquest mapa,
  • connecteu el programa a la interfície lo,

que es tradueix en humà com

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 de la mateixa manera que vam fer al primer exemple sobre la trucada al sistema bpf - “kernel, si us plau, fes-me un mapa nou en forma de matriu de 8 elements com __u64 i torna'm el descriptor del fitxer":

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

El programa també és fàcil de carregar:

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

La part complicada prog_load és la definició del nostre programa BPF com un conjunt d'estructures struct bpf_insn insns[]. Però com que estem utilitzant un programa que tenim en C, podem fer trampes una mica:

$ 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, hem d'escriure 14 instruccions en forma d'estructures com struct bpf_insn (consell: agafeu l'abocador des de dalt, torneu a llegir la secció d'instruccions, obre linux/bpf.h и linux/bpf_common.h i intenta determinar struct bpf_insn insns[] pel seu compte):

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 exercici per a aquells que no ho van escriure ells mateixos: troba map_fd.

Hi ha una part més no revelada al nostre programa: xdp_attach. Malauradament, programes com XDP no es poden connectar mitjançant una trucada al sistema bpf. Les persones que van crear BPF i XDP eren de la comunitat Linux en línia, el que significa que utilitzaven la que els coneixia més (però no normal people) interfície per interactuar amb el nucli: endolls netlink, Vegeu també RFC3549. La forma més senzilla d'implementar xdp_attach està copiant el codi de libbpf, és a dir, del fitxer netlink.c, que és el que vam fer, escurçant-ho una mica:

Benvingut al món dels endolls netlink

Obriu un tipus de sòcol d'enllaç net 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;
}

Llegim des d'aquesta presa:

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

Finalment, aquí teniu la nostra funció que obre un sòcol i li envia un missatge especial que conté un descriptor de fitxer:

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

Per tant, tot està preparat per a la prova:

$ 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 veure si el nostre programa s'ha connectat 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

Enviem pings i mirem el 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

Hura, tot funciona. Tingueu en compte, per cert, que el nostre mapa es torna a mostrar en forma de bytes. Això es deu al fet que, a diferència libbpf no hem carregat informació de tipus (BTF). Però d'això en parlarem més la propera vegada.

Eines de desenvolupament

En aquesta secció, veurem el conjunt d'eines de desenvolupador BPF mínim.

En termes generals, no necessiteu res especial per desenvolupar programes BPF: BPF s'executa en qualsevol nucli de distribució decent i els programes es creen utilitzant clang, que es pot subministrar des del paquet. Tanmateix, a causa del fet que BPF està en desenvolupament, el nucli i les eines canvien constantment, si no voleu escriure programes BPF amb mètodes antics a partir del 2019, haureu de compilar

  • llvm/clang
  • pahole
  • el seu nucli
  • bpftool

(Com a referència, aquesta secció i tots els exemples de l'article es van executar a Debian 10.)

llvm/clang

BPF és amigable amb LLVM i, tot i que recentment es poden compilar programes per a BPF amb gcc, tot el desenvolupament actual es porta a terme per a LLVM. Per tant, primer de tot, construirem la versió 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
... много времени спустя
$

Ara podem comprovar si tot ha anat correctament:

$ ./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

(Instruccions de muntatge clang pres per mi de bpf_devel_QA.)

No instal·larem els programes que acabem de crear, sinó que els afegirem PATH, per exemple:

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

(Això es pot afegir a .bashrc o a un fitxer separat. Personalment, afegeixo coses com aquesta ~/bin/activate-llvm.sh i quan cal ho faig . activate-llvm.sh.)

Pahole i BTF

Utilitat pahole s'utilitza quan es construeix el nucli per crear informació de depuració en format BTF. No entrarem en detalls en aquest article sobre els detalls de la tecnologia BTF, a part del fet que és convenient i volem utilitzar-lo. Per tant, si aneu a construir el vostre nucli, construïu primer pahole (sense pahole no podreu construir el nucli amb l'opció 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

Nuclis per experimentar amb BPF

Quan exploro les possibilitats de BPF, vull muntar el meu propi nucli. Això, en general, no és necessari, ja que podreu compilar i carregar programes BPF al nucli de distribució, però, tenir el vostre propi nucli us permet utilitzar les últimes característiques de BPF, que apareixeran a la vostra distribució en el millor dels mesos. , o, com en el cas d'algunes eines de depuració, no s'empaquetaran en absolut en un futur previsible. A més, el seu propi nucli fa que se senti important experimentar amb el codi.

Per construir un nucli necessiteu, en primer lloc, el nucli en si, i en segon lloc, un fitxer de configuració del nucli. Per experimentar amb BPF podem utilitzar l'habitual vainilla nucli o un dels nuclis de desenvolupament. Històricament, el desenvolupament de BPF té lloc dins de la comunitat de xarxes Linux i, per tant, tots els canvis tard o d'hora passen per David Miller, el responsable de la xarxa Linux. Depenent de la seva naturalesa (edicions o funcions noves), els canvis de xarxa es troben en un dels dos nuclis: net o net-next. Els canvis per a BPF es distribueixen de la mateixa manera entre bpf и bpf-next, que després s'agrupen en net i net-next, respectivament. Per a més detalls, vegeu bpf_devel_QA и netdev-FAQ. Així que trieu un nucli en funció del vostre gust i de les necessitats d'estabilitat del sistema en què esteu provant (*-next els nuclis són els més inestables dels llistats).

Està fora de l'abast d'aquest article parlar de com gestionar els fitxers de configuració del nucli; se suposa que ja ho sabeu o com fer-ho, o disposat a aprendre pel seu compte. Tanmateix, les instruccions següents haurien de ser més o menys suficients per oferir-vos un sistema habilitat per BPF que funcioni.

Baixeu un dels nuclis anteriors:

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

Creeu una configuració mínima del nucli de treball:

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

Activa les opcions BPF al fitxer .config de la teva pròpia elecció (molt probablement CONFIG_BPF ja estarà habilitat ja que systemd l'utilitza). Aquí hi ha una llista d'opcions del nucli utilitzades per a aquest article:

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

A continuació, podem muntar i instal·lar fàcilment els mòduls i el nucli (per cert, podeu muntar el nucli amb el nou clangafegint CC=clang):

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

i reinicieu amb el nou nucli (jo faig servir per a això kexec del paquet 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

La utilitat més utilitzada a l'article serà la utilitat bpftool, subministrat com a part del nucli Linux. Està escrit i mantingut pels desenvolupadors de BPF per a desenvolupadors de BPF i es pot utilitzar per gestionar tot tipus d'objectes BPF: carregar programes, crear i editar mapes, explorar la vida de l'ecosistema BPF, etc. Es pot trobar documentació en forma de codis font per a pàgines de manual al nucli o, ja compilat, xarxa.

En el moment d'escriure aquest article bpftool ve preparat només per a RHEL, Fedora i Ubuntu (vegeu, per exemple, aquest fil, que explica la història inacabada del packaging bpftool a Debian). Però si ja heu construït el vostre nucli, creeu-lo bpftool tan fàcil com un pastís:

$ 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} - aquest és el vostre directori del nucli.) Després d'executar aquestes ordres bpftool es recullen en un directori ${linux}/tools/bpf/bpftool i es pot afegir al camí (en primer lloc a l'usuari root) o simplement copiar a /usr/local/sbin.

Recull bpftool el millor és utilitzar aquest últim clang, muntat com s'ha descrit anteriorment, i comproveu si està muntat correctament, utilitzant, per exemple, l'ordre

$ 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à quines funcions BPF estan habilitades al vostre nucli.

Per cert, l'ordre anterior es pot executar com

# bpftool f p k

Això es fa per analogia amb les utilitats del paquet iproute2, on podem, per exemple, dir ip a s eth0 en comptes de ip addr show dev eth0.

Conclusió

BPF us permet calçar una puça per mesurar eficaçment i canviar sobre la marxa la funcionalitat del nucli. El sistema va resultar ser molt reeixit, en les millors tradicions d'UNIX: un mecanisme senzill que permet (re)programar el nucli va permetre a un gran nombre de persones i organitzacions experimentar. I, tot i que els experiments, així com el desenvolupament de la pròpia infraestructura de BPF, estan lluny d'estar acabats, el sistema ja disposa d'un ABI estable que permet construir una lògica empresarial fiable i, sobretot, eficaç.

M'agradaria assenyalar que, al meu entendre, la tecnologia s'ha popularitzat molt perquè, d'una banda, pot jugar (l'arquitectura d'una màquina es pot entendre més o menys en un vespre), i d'altra banda, per resoldre problemes que no s'han pogut resoldre (bonicament) abans de la seva aparició. Aquests dos components junts obliguen a la gent a experimentar i somiar, la qual cosa porta a l'aparició de solucions cada cop més innovadores.

Aquest article, tot i que no és especialment breu, és només una introducció al món de BPF i no descriu característiques "avançades" i parts importants de l'arquitectura. El pla per endavant és una cosa així: el següent article serà una visió general dels tipus de programes BPF (hi ha 5.8 tipus de programes compatibles amb el nucli 30), i finalment veurem com escriure aplicacions BPF reals utilitzant programes de seguiment del nucli. com a exemple, llavors és hora de fer un curs més aprofundit sobre arquitectura BPF, seguit d'exemples d'aplicacions de seguretat i xarxes BPF.

Articles anteriors d'aquesta sèrie

  1. BPF per als més petits, part zero: BPF clàssic

Enllaços

  1. Guia de referència BPF i XDP — documentació sobre BPF de cilium, o més precisament de Daniel Borkman, un dels creadors i mantenedors de BPF. Aquesta és una de les primeres descripcions serioses, que es diferencia de les altres perquè en Daniel sap exactament de què està escrivint i no hi ha errors. En particular, aquest document descriu com treballar amb programes BPF dels tipus XDP i TC utilitzant la coneguda utilitat ip del paquet iproute2.

  2. Documentació/xarxes/filter.txt — arxiu original amb documentació per a BPF clàssic i després ampliat. Una bona lectura si voleu aprofundir en el llenguatge ensamblador i els detalls tècnics arquitectònics.

  3. Blog sobre BPF des de facebook. S'actualitza poques vegades, però encertadament, tal com hi escrivien Alexei Starovoitov (autor d'eBPF) i Andrii Nakryiko (mantenidor). libbpf).

  4. Secrets de bpftool. Un entretingut fil de Twitter de Quentin Monnet amb exemples i secrets de l'ús de bpftool.

  5. Submergir-se en BPF: una llista de material de lectura. Una llista gegant (i encara mantinguda) d'enllaços a la documentació de BPF de Quentin Monnet.

Font: www.habr.com

Afegeix comentari