I begynnelsen var det en teknologi og den ble kalt BPF. Vi så på henne tidligere, Det gamle testamentets artikkel i denne serien. I 2013, gjennom innsatsen til Alexei Starovoitov og Daniel Borkman, ble en forbedret versjon av den, optimalisert for moderne 64-bits maskiner, utviklet og inkludert i Linux-kjernen. Denne nye teknologien ble kort kalt Internal BPF, deretter omdøpt til Extended BPF, og nå, etter flere år, kaller alle den bare BPF.
Grovt sett lar BPF deg kjøre vilkårlig brukerlevert kode i Linux-kjernen, og den nye arkitekturen viste seg å være så vellykket at vi vil trenge et dusin flere artikler for å beskrive alle applikasjonene. (Det eneste utviklerne ikke gjorde bra, som du kan se i ytelseskoden nedenfor, var å lage en anstendig logo.)
Denne artikkelen beskriver strukturen til den virtuelle BPF-maskinen, kjernegrensesnitt for arbeid med BPF, utviklingsverktøy, samt en kort, veldig kort oversikt over eksisterende muligheter, dvs. alt vi vil trenge i fremtiden for en dypere studie av de praktiske anvendelsene av BPF.
Administrere objekter ved å bruke bpf-systemkallet. Med en viss forståelse av systemet allerede på plass, vil vi til slutt se på hvordan man oppretter og manipulerer objekter fra brukerområdet ved å bruke et spesielt systemkall − bpf(2).
Пишем программы BPF с помощью libbpf. Selvfølgelig kan du skrive programmer ved hjelp av et systemanrop. Men det er vanskelig. For et mer realistisk scenario utviklet kjernefysiske programmerere et bibliotek libbpf. Vi lager et grunnleggende BPF-applikasjonsskjelett som vi vil bruke i påfølgende eksempler.
Kjernehjelpere. Her skal vi lære hvordan BPF-programmer kan få tilgang til kjernehjelpefunksjoner – et verktøy som sammen med kart fundamentalt utvider mulighetene til den nye BPF sammenlignet med den klassiske.
Tilgang til kart fra BPF-programmer. På dette tidspunktet vil vi vite nok til å forstå nøyaktig hvordan vi kan lage programmer som bruker kart. Og la oss til og med ta en rask titt inn i den store og mektige verifikatoren.
Utviklingsverktøy. Hjelpedel om hvordan du setter sammen de nødvendige verktøyene og kjernen for eksperimenter.
Konklusjon. På slutten av artikkelen vil de som leser så langt finne motiverende ord og en kort beskrivelse av hva som vil skje i de følgende artiklene. Vi vil også liste opp en rekke lenker for selvstudium for de som ikke har lyst eller evne til å vente på fortsettelsen.
Introduksjon til BPF-arkitektur
Før vi begynner å vurdere BPF-arkitekturen, vil vi referere en siste gang (oh) til klassisk BPF, som ble utviklet som et svar på bruken av RISC-maskiner og løste problemet med effektiv pakkefiltrering. Arkitekturen viste seg å være så vellykket at den, etter å ha blitt født på det flotte nittitallet i Berkeley UNIX, ble overført til de fleste eksisterende operativsystemer, overlevde inn i de sprø tjueårene og fortsatt finner nye applikasjoner.
Den nye BPF ble utviklet som et svar på allestedsnærværende 64-bits maskiner, skytjenester og det økte behovet for verktøy for å lage SDN (Softe-definert nnettverk). Utviklet av kjernenettverksingeniører som en forbedret erstatning for den klassiske BPF, fant den nye BPF bokstavelig talt seks måneder senere applikasjoner i den vanskelige oppgaven med å spore Linux-systemer, og nå, seks år etter at den dukket opp, trenger vi en hel neste artikkel bare for å liste opp ulike typer programmer.
Morsomme bilder
I kjernen er BPF en virtuell sandkassemaskin som lar deg kjøre "vilkårlig" kode i kjerneplass uten å gå på bekostning av sikkerheten. BPF-programmer opprettes i brukerområdet, lastes inn i kjernen og kobles til en hendelseskilde. En hendelse kan for eksempel være levering av en pakke til et nettverksgrensesnitt, lansering av en kjernefunksjon osv. Når det gjelder en pakke, vil BPF-programmet ha tilgang til dataene og metadataene til pakken (for lesing og muligens skriving, avhengig av type program); i tilfelle av å kjøre en kjernefunksjon, argumentene til funksjonen, inkludert pekere til kjerneminne, etc.
La oss se nærmere på denne prosessen. Til å begynne med, la oss snakke om den første forskjellen fra den klassiske BPF, programmer som ble skrevet i assembler. I den nye versjonen ble arkitekturen utvidet slik at programmer kunne skrives på høynivåspråk, primært selvfølgelig i C. Til dette ble det utviklet en backend for llvm, som lar deg generere bytekode for BPF-arkitekturen.
BPF-arkitekturen ble delvis designet for å kjøre effektivt på moderne maskiner. For å få dette til å fungere i praksis, blir BPF-bytekoden, når den er lastet inn i kjernen, oversatt til naturlig kode ved hjelp av en komponent kalt en JIT-kompilator (Just In Tjeg meg). Neste, hvis du husker, i klassisk BPF ble programmet lastet inn i kjernen og knyttet til hendelseskilden atomisk - i sammenheng med et enkelt systemkall. I den nye arkitekturen skjer dette i to trinn - først lastes koden inn i kjernen ved hjelp av et systemkall bpf(2)og så, senere, gjennom andre mekanismer som varierer avhengig av type program, kobles programmet til hendelseskilden.
Her kan leseren ha et spørsmål: var det mulig? Hvordan garanteres utførelsessikkerheten til en slik kode? Utførelsessikkerhet er garantert for oss ved lasting av BPF-programmer kalt verifier (på engelsk kalles dette stadiet verifier, og jeg vil fortsette å bruke det engelske ordet):
Verifier er en statisk analysator som sikrer at et program ikke forstyrrer den normale driften av kjernen. Dette betyr forresten ikke at programmet ikke kan forstyrre driften av systemet - BPF-programmer, avhengig av typen, kan lese og omskrive deler av kjerneminnet, returnere funksjonsverdier, trimme, legge til, omskrive og til og med videresende nettverkspakker. Verifier garanterer at å kjøre et BPF-program ikke vil krasje kjernen og at et program som i henhold til reglene har skrivetilgang, for eksempel dataene til en utgående pakke, ikke vil kunne overskrive kjerneminnet utenfor pakken. Vi vil se på verifikatoren litt mer detaljert i den tilsvarende delen, etter at vi har blitt kjent med alle de andre komponentene i BPF.
Så hva har vi lært så langt? Brukeren skriver et program i C, laster det inn i kjernen ved hjelp av et systemkall bpf(2), hvor den kontrolleres av en verifikator og oversettes til opprinnelig bytekode. Deretter kobler den samme eller en annen bruker programmet til hendelseskilden og det begynner å kjøre. Det er nødvendig å skille oppstart og tilkobling av flere grunner. For det første er det relativt dyrt å kjøre en verifikator, og ved å laste ned det samme programmet flere ganger kaster vi bort datatid. For det andre, nøyaktig hvordan et program er koblet til avhenger av typen, og et "universelt" grensesnitt utviklet for et år siden er kanskje ikke egnet for nye typer programmer. (Selv om nå som arkitekturen blir mer moden, er det en idé å forene dette grensesnittet på nivået libbpf.)
Den oppmerksomme leser legger kanskje merke til at vi ikke er ferdige med bildene enda. Alt det ovennevnte forklarer faktisk ikke hvorfor BPF fundamentalt endrer bildet sammenlignet med klassisk BPF. To nyvinninger som betydelig utvider anvendelsesområdet er muligheten til å bruke delt minne og kjernehjelpefunksjoner. I BPF implementeres delt minne ved hjelp av såkalte maps – delte datastrukturer med et spesifikt API. De har sannsynligvis fått dette navnet fordi den første typen kart som dukket opp var en hashtabell. Så dukket det opp matriser, lokale (per-CPU) hashtabeller og lokale matriser, søketrær, kart som inneholder pekere til BPF-programmer og mye mer. Det som er interessant for oss nå, er at BPF-programmer nå har muligheten til å fortsette tilstanden mellom samtaler og dele den med andre programmer og med brukerplass.
Kart er tilgjengelig fra brukerprosesser ved hjelp av et systemanrop bpf(2), og fra BPF-programmer som kjører i kjernen ved hjelp av hjelpefunksjoner. Dessuten eksisterer hjelpere ikke bare for å jobbe med kart, men også for å få tilgang til andre kjernefunksjoner. For eksempel kan BPF-programmer bruke hjelpefunksjoner til å videresende pakker til andre grensesnitt, generere perf-hendelser, få tilgang til kjernestrukturer og så videre.
Oppsummert gir BPF muligheten til å laste vilkårlig, dvs. verifikatortestet, brukerkode inn i kjernerommet. Denne koden kan lagre tilstand mellom samtaler og utveksle data med brukerplass, og har også tilgang til kjernedelsystemer tillatt av denne typen program.
Dette ligner allerede på egenskapene som tilbys av kjernemoduler, sammenlignet med hvilke BPF har noen fordeler (selvfølgelig kan du bare sammenligne lignende applikasjoner, for eksempel systemsporing - du kan ikke skrive en vilkårlig driver med BPF). Du kan merke deg en lavere inngangsterskel (noen verktøy som bruker BPF krever ikke at brukeren har kjerneprogrammeringsferdigheter, eller programmeringsferdigheter generelt), kjøretidssikkerhet (rekk opp hånden i kommentarene for de som ikke brøt systemet når de skrev eller testing av moduler), atomitet - det er nedetid ved omlasting av moduler, og BPF-delsystemet sikrer at ingen hendelser blir savnet (for å være rettferdig, dette er ikke sant for alle typer BPF-programmer).
Tilstedeværelsen av slike evner gjør BPF til et universelt verktøy for å utvide kjernen, noe som bekreftes i praksis: flere og flere nye typer programmer legges til BPF, flere og flere store selskaper bruker BPF på kampservere 24×7, mer og mer startups bygger sin virksomhet på løsninger basert på som er basert på BPF. BPF brukes overalt: i beskyttelse mot DDoS-angrep, opprettelse av SDN (for eksempel implementering av nettverk for kubernetes), som hovedsystemsporingsverktøy og statistikksamler, i inntrengningsdeteksjonssystemer og sandkassesystemer, etc.
La oss fullføre oversiktsdelen av artikkelen her og se på den virtuelle maskinen og BPF-økosystemet mer detaljert.
Digresjon: verktøy
For å kunne kjøre eksemplene i de følgende avsnittene, kan det hende du trenger en rekke verktøy, i det minste llvm/clang med bpf-støtte og bpftool. I seksjonen Utviklingsverktøy Du kan lese instruksjonene for å sette sammen verktøyene, så vel som kjernen din. Denne delen er plassert nedenfor for ikke å forstyrre harmonien i presentasjonen vår.
BPF virtuelle maskinregistre og instruksjonssystem
Arkitekturen og kommandosystemet til BPF ble utviklet under hensyntagen til det faktum at programmer vil bli skrevet på C-språket og, etter å ha lastet inn i kjernen, oversatt til innfødt kode. Derfor ble antallet registre og settet med kommandoer valgt med tanke på skjæringspunktet, i matematisk forstand, mellom egenskapene til moderne maskiner. I tillegg ble det pålagt ulike begrensninger for programmer, for eksempel var det inntil nylig ikke mulig å skrive løkker og subrutiner, og antallet instruksjoner var begrenset til 4096 (nå kan privilegerte programmer laste opp til en million instruksjoner).
BPF har elleve brukertilgjengelige 64-bits registre r0-r10 og en programteller. Registrere r10 inneholder en rammepeker og er skrivebeskyttet. Programmer har tilgang til en 512-byte stabel under kjøring og en ubegrenset mengde delt minne i form av kart.
BPF-programmer har lov til å kjøre et spesifikt sett med programtype kjernehjelpere og, mer nylig, vanlige funksjoner. Hver kalt funksjon kan ta opptil fem argumenter, sendt i registre r1-r5, og returverdien sendes til r0. Det er garantert at etter retur fra funksjonen, innholdet i registrene r6-r9 Vil ikke endre seg.
For effektiv programoversettelse, registrer r0-r11 for alle støttede arkitekturer er unikt kartlagt til reelle registre, tatt i betraktning ABI-funksjonene til gjeldende arkitektur. For eksempel for x86_64 registrerer r1-r5, som brukes til å sende funksjonsparametere, vises på rdi, rsi, rdx, rcx, r8, som brukes til å sende parametere til funksjoner på x86_64. For eksempel, koden til venstre oversettes til koden til høyre slik:
Registrere r0 også brukt til å returnere resultatet av programkjøring, og i registeret r1 programmet sendes en peker til konteksten - avhengig av type program kan dette for eksempel være en struktur struct xdp_md (for XDP) eller struktur struct __sk_buff (for forskjellige nettverksprogrammer) eller struktur struct pt_regs (for ulike typer sporingsprogrammer) osv.
Så vi hadde et sett med registre, kjernehjelpere, en stabel, en kontekstpeker og delt minne i form av kart. Ikke at alt dette er helt nødvendig på turen, men...
La oss fortsette beskrivelsen og snakke om kommandosystemet for å jobbe med disse objektene. Alle (Nesten alle) BPF-instruksjoner har en fast 64-bits størrelse. Hvis du ser på en instruksjon på en 64-bit Big Endian-maskin vil du se
Her Code - dette er kodingen av instruksjonen, Dst/Src er kodingene til henholdsvis mottakeren og kilden, Off - 16-bits signert innrykk, og Imm er et 32-bits fortegnet heltall som brukes i noen instruksjoner (ligner på cBPF-konstanten K). Koding Code har en av to typer:
Instruksjonsklassene 0, 1, 2, 3 definerer kommandoer for arbeid med minne. De kalles, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, henholdsvis. Klasser 4, 7 (BPF_ALU, BPF_ALU64) utgjør et sett med ALU-instruksjoner. Klasser 5, 6 (BPF_JMP, BPF_JMP32) inneholder hoppinstruksjoner.
Den videre planen for å studere BPF-instruksjonssystemet er som følger: i stedet for omhyggelig å liste opp alle instruksjonene og deres parametere, vil vi se på et par eksempler i denne delen, og fra dem vil det bli klart hvordan instruksjonene faktisk fungerer og hvordan demontere alle binære filer for BPF manuelt. For å konsolidere materialet senere i artikkelen vil vi også møte individuelle instruksjoner i seksjonene om Verifier, JIT-kompilator, oversettelse av klassisk BPF, samt når vi studerer kart, kaller funksjoner, etc.
La oss se på et eksempel der vi kompilerer et program readelf-example.c og se på den resulterende binære filen. Vi vil avsløre det originale innholdet readelf-example.c nedenfor, etter at vi har gjenopprettet logikken fra binære koder:
Kommandokoder er like b7, 15, b7 и 95. Husk at de minst signifikante tre bitene er instruksjonsklassen. I vårt tilfelle er den fjerde biten av alle instruksjoner tom, så instruksjonsklassene er henholdsvis 7, 5, 7, 5. Klasse 7 er BPF_ALU64, og 5 er BPF_JMP. For begge klasser er instruksjonsformatet det samme (se ovenfor), og vi kan omskrive programmet vårt slik (samtidig vil vi omskrive de resterende kolonnene i menneskelig form):
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
Operasjon b klasse ALU64 - Er BPF_MOV. Den tildeler en verdi til destinasjonsregisteret. Hvis biten er satt s (kilde), så hentes verdien fra kilderegisteret, og hvis den, som i vårt tilfelle, ikke er satt, så hentes verdien fra feltet Imm. Så i den første og tredje instruksjonen utfører vi operasjonen r0 = Imm. Videre er JMP klasse 1 operasjon BPF_JEQ (hopp hvis lik). I vårt tilfelle, siden litt S er null, sammenligner den verdien av kilderegisteret med feltet Imm. Hvis verdiene sammenfaller, skjer overgangen til PC + OffDer PC, som vanlig, inneholder adressen til neste instruksjon. Endelig er JMP Class 9 Operation BPF_EXIT. Denne instruksjonen avslutter programmet og går tilbake til kjernen r0. La oss legge til en ny kolonne i tabellen vår:
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
Vi kan omskrive dette i en mer praktisk form:
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exit
Hvis vi husker hva som står i registeret r1 programmet sendes en peker til konteksten fra kjernen, og i registeret r0 verdien returneres til kjernen, så kan vi se at hvis pekeren til konteksten er null, så returnerer vi 1, og ellers - 2. La oss sjekke at vi har rett ved å se på kilden:
Ja, det er et meningsløst program, men det oversettes til bare fire enkle instruksjoner.
Unntakseksempel: 16-byte instruksjon
Vi nevnte tidligere at noen instruksjoner tar opp mer enn 64 biter. Dette gjelder for eksempel instruksjoner lddw (Kode = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — last et dobbeltord fra feltene inn i registeret Imm. Faktum er det Imm har en størrelse på 32, og et dobbeltord er 64 biter, så å laste en 64-bits umiddelbar verdi inn i et register i en 64-bits instruksjon vil ikke fungere. For å gjøre dette brukes to tilstøtende instruksjoner for å lagre den andre delen av 64-bits verdien i feltet Imm... Eksempel:
Vi møtes igjen med instruksjoner lddw, når vi snakker om flytting og arbeid med kart.
Eksempel: demontering av BPF med standardverktøy
Så vi har lært å lese BPF-binære koder og er klare til å analysere alle instruksjoner om nødvendig. Imidlertid er det verdt å si at det i praksis er mer praktisk og raskere å demontere programmer ved hjelp av standardverktøy, for eksempel:
(Jeg lærte først noen av detaljene beskrevet i denne underseksjonen fra post Alexei Starovoitov inn BPF-bloggen.)
BPF-objekter - programmer og kart - lages fra brukerområdet ved hjelp av kommandoer BPF_PROG_LOAD и BPF_MAP_CREATE systemanrop bpf(2), skal vi snakke om nøyaktig hvordan dette skjer i neste avsnitt. Dette skaper kjernedatastrukturer og for hver av dem refcount (referansetelling) settes til én, og en filbeskrivelse som peker på objektet returneres til brukeren. Etter at håndtaket er lukket refcount objektet reduseres med én, og når det når null, blir objektet ødelagt.
Hvis programmet bruker kart, da refcount disse kartene økes med ett etter innlasting av programmet, dvs. deres filbeskrivelser kan lukkes fra brukerprosessen og fortsatt refcount blir ikke null:
Etter vellykket innlasting av et program, kobler vi det vanligvis til en slags hendelsesgenerator. For eksempel kan vi sette den på et nettverksgrensesnitt for å behandle innkommende pakker eller koble den til noen tracepoint i kjernen. På dette tidspunktet vil referansetelleren også øke med én og vi vil kunne lukke filbeskrivelsen i loader-programmet.
Hva skjer hvis vi nå slår av bootloaderen? Det avhenger av typen hendelsesgenerator (krok). Alle nettverkskroker vil eksistere etter at lasteren er fullført, disse er de såkalte globale krokene. Og for eksempel vil sporingsprogrammer bli utgitt etter at prosessen som opprettet dem avsluttes (og derfor kalles lokale, fra "lokal til prosess"). Teknisk sett har lokale hooks alltid en tilsvarende filbeskrivelse i brukerområdet og lukkes derfor når prosessen er lukket, men globale kroker gjør det ikke. I den følgende figuren, ved hjelp av røde kryss, prøver jeg å vise hvordan avslutningen av lasteprogrammet påvirker levetiden til objekter når det gjelder lokale og globale kroker.
Hvorfor er det et skille mellom lokale og globale kroker? Å kjøre noen typer nettverksprogrammer gir mening uten et brukerområde, for eksempel, forestill deg DDoS-beskyttelse - bootloaderen skriver reglene og kobler BPF-programmet til nettverksgrensesnittet, hvoretter bootloaderen kan gå og drepe seg selv. På den annen side, se for deg et program for feilsøking som du skrev på knærne på ti minutter – når det er ferdig, vil du gjerne at det ikke er noe søppel igjen i systemet, og lokale kroker vil sørge for det.
Tenk deg derimot at du vil koble til et sporingspunkt i kjernen og samle statistikk over mange år. I dette tilfellet ønsker du å fullføre brukerdelen og gå tilbake til statistikken fra tid til annen. Bpf-filsystemet gir denne muligheten. Det er et pseudo-filsystem som kun er i minnet som tillater opprettelse av filer som refererer til BPF-objekter og dermed øker refcount gjenstander. Etter dette kan lasteren gå ut, og gjenstandene den opprettet vil forbli i live.
Å lage filer i bpffs som refererer til BPF-objekter kalles "pinning" (som i følgende setning: "prosess kan feste et BPF-program eller kart"). Å lage filobjekter for BPF-objekter gir mening ikke bare for å forlenge levetiden til lokale objekter, men også for brukbarheten til globale objekter - tilbake til eksemplet med det globale DDoS-beskyttelsesprogrammet, ønsker vi å kunne komme og se på statistikk fra tid til annen.
BPF-filsystemet er vanligvis montert i /sys/fs/bpf, men den kan også monteres lokalt, for eksempel slik:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
Filsystemnavn opprettes ved hjelp av kommandoen BPF_OBJ_PIN BPF-systemanrop. For å illustrere, la oss ta et program, kompilere det, laste det opp og feste det til bpffs. Programmet vårt gjør ikke noe nyttig, vi presenterer bare koden slik at du kan gjenskape eksemplet:
La oss nå laste ned programmet vårt ved å bruke verktøyet bpftool og se på de medfølgende systemanropene bpf(2) (noen irrelevante linjer fjernet fra strace-utgang):
Her har vi lastet programmet vha BPF_PROG_LOAD, mottok en filbeskrivelse fra kjernen 3 og bruke kommandoen BPF_OBJ_PIN festet denne filbeskrivelsen som en fil "bpf-mountpoint/test". Etter dette bootloader-programmet bpftool arbeidet ferdig, men programmet vårt forble i kjernen, selv om vi ikke koblet det til noe nettverksgrensesnitt:
$ 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
Vi kan slette filobjektet normalt unlink(2) og etter det vil det tilsvarende programmet bli slettet:
$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory
Sletting av objekter
Når vi snakker om å slette objekter, er det nødvendig å klargjøre at etter at vi har koblet programmet fra kroken (hendelsesgeneratoren), vil ikke en eneste ny hendelse utløse lanseringen, men alle gjeldende forekomster av programmet vil bli fullført i normal rekkefølge .
Noen typer BPF-programmer lar deg erstatte programmet på farten, dvs. gi sekvensatomitet replace = detach old program, attach new program. I dette tilfellet vil alle aktive forekomster av den gamle versjonen av programmet fullføre arbeidet, og nye hendelsesbehandlere vil bli opprettet fra det nye programmet, og "atomicity" betyr her at ikke en eneste hendelse vil bli savnet.
Legge ved programmer til hendelseskilder
I denne artikkelen vil vi ikke separat beskrive å koble programmer til hendelseskilder, siden det er fornuftig å studere dette i sammenheng med en bestemt type program. Cm. eksempel nedenfor, der vi viser hvordan programmer som XDP er koblet sammen.
Manipulere objekter ved hjelp av bpf System Call
BPF-programmer
Alle BPF-objekter opprettes og administreres fra brukerområdet ved hjelp av et systemanrop bpf, som har følgende prototype:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
Her er laget cmd er en av verdiene for type enum bpf_cmd, attr — en peker til parametere for et spesifikt program og size — objektstørrelse i henhold til pekeren, dvs. vanligvis dette sizeof(*attr). I kjerne 5.8 kalles systemet bpf støtter 34 forskjellige kommandoer, og bestemmelse avunion bpf_attr opptar 200 linjer. Men vi bør ikke la oss skremme av dette, siden vi vil gjøre oss kjent med kommandoene og parameterne i løpet av flere artikler.
La oss starte med laget BPF_PROG_LOAD, som lager BPF-programmer - tar et sett med BPF-instruksjoner og laster det inn i kjernen. Ved lasting startes verifikatoren, og deretter returneres JIT-kompilatoren og, etter vellykket kjøring, programfilbeskrivelsen til brukeren. Vi så hva som skjer med ham videre i forrige avsnitt om livssyklusen til BPF-objekter.
Vi skal nå skrive et tilpasset program som vil laste et enkelt BPF-program, men først må vi bestemme hva slags program vi vil laste - vi må velge typen og innenfor rammen av denne typen, skriv et program som vil bestå verifikatoren. Men for ikke å komplisere prosessen, er her en ferdig løsning: vi tar et program som BPF_PROG_TYPE_XDP, som vil returnere verdien XDP_PASS (hopp over alle pakker). I BPF assembler ser det veldig enkelt ut:
r0 = 2
exit
Etter at vi har bestemt oss for at vi vil laste opp, vi kan fortelle deg hvordan vi skal gjøre det:
Interessante hendelser i et program begynner med definisjonen av en matrise insns - vårt BPF-program i maskinkode. I dette tilfellet er hver instruksjon i BPF-programmet pakket inn i strukturen bpf_insn. Første element insns samsvarer med instruksjonene r0 = 2, sekund - exit.
Retrett. Kjernen definerer mer praktiske makroer for å skrive maskinkoder og bruke kjerneoverskriftsfilen tools/include/linux/filter.h vi kunne skrive
Men siden det å skrive BPF-programmer i innfødt kode bare er nødvendig for å skrive tester i kjernen og artikler om BPF, kompliserer ikke fraværet av disse makroene utviklerens liv.
Etter å ha definert BPF-programmet, går vi videre til å laste det inn i kjernen. Vårt minimalistiske sett med parametere attr inkluderer programtype, sett og antall instruksjoner, nødvendig lisens og navn "woo", som vi bruker for å finne programmet vårt på systemet etter nedlasting. Programmet, som lovet, lastes inn i systemet ved hjelp av et systemanrop bpf.
På slutten av programmet havner vi i en uendelig sløyfe som simulerer nyttelasten. Uten det vil programmet bli drept av kjernen når filbeskrivelsen som systemanropet returnerte til oss lukkes bpf, og vi vil ikke se det i systemet.
Vel, vi er klare for testing. La oss sette sammen og kjøre programmet under stracefor å sjekke at alt fungerer som det skal:
Alt er bra, bpf(2) returnerte håndtak 3 til oss og vi gikk inn i en uendelig løkke med pause(). La oss prøve å finne programmet vårt i systemet. For å gjøre dette går vi til en annen terminal og bruker verktøyet bpftool:
Vi ser at det er et lastet program på systemet woo hvis globale ID er 390 og pågår nå simple-prog det er en åpen filbeskrivelse som peker på programmet (og hvis simple-prog vil fullføre jobben, da woo vil forsvinne). Som forventet, programmet woo tar 16 byte - to instruksjoner - med binære koder i BPF-arkitekturen, men i sin opprinnelige form (x86_64) er det allerede 40 byte. La oss se på programmet vårt i sin opprinnelige form:
ingen overraskelser. La oss nå se på koden generert av JIT-kompilatoren:
# 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
ikke særlig effektiv for exit(2), men i rettferdighet er programmet vårt for enkelt, og for ikke-trivielle programmer er prologen og epilogen lagt til av JIT-kompilatoren selvfølgelig nødvendig.
Kart
BPF-programmer kan bruke strukturerte minneområder som er tilgjengelige både for andre BPF-programmer og for programmer i brukerrommet. Disse objektene kalles kart, og i denne delen vil vi vise hvordan du kan manipulere dem ved hjelp av et systemanrop bpf.
La oss si med en gang at kartfunksjonene ikke er begrenset bare til tilgang til delt minne. Det finnes spesialkart som inneholder for eksempel pekere til BPF-programmer eller pekere til nettverksgrensesnitt, kart for arbeid med perf-hendelser osv. Vi vil ikke snakke om dem her, for ikke å forvirre leseren. Bortsett fra dette ignorerer vi synkroniseringsproblemer, siden dette ikke er viktig for våre eksempler. En fullstendig liste over tilgjengelige karttyper finner du i <linux/bpf.h>, og i denne delen tar vi som eksempel den historisk første typen, hash-tabellen BPF_MAP_TYPE_HASH.
Hvis du lager en hash-tabell i for eksempel C++, vil du si unordered_map<int,long> woo, som på russisk betyr «Jeg trenger et bord woo ubegrenset størrelse, hvis nøkler er av typen int, og verdiene er typen long" For å lage en BPF-hash-tabell må vi gjøre mye av det samme, bortsett fra at vi må spesifisere maksimal størrelse på tabellen, og i stedet for å spesifisere typene nøkler og verdier, må vi spesifisere størrelsene deres i byte . Bruk kommandoen for å lage kart BPF_MAP_CREATE systemanrop bpf. La oss se på et mer eller mindre minimalt program som lager et kart. Etter det forrige programmet som laster BPF-programmer, bør dette virke enkelt for deg:
Her definerer vi et sett med parametere attr, der vi sier "Jeg trenger en hashtabell med nøkler og størrelsesverdier sizeof(int), der jeg kan legge inn maksimalt fire elementer." Når du oppretter BPF-kart, kan du spesifisere andre parametere, for eksempel på samme måte som i eksempelet med programmet, spesifiserte vi navnet på objektet som "woo".
Her er systemanropet bpf(2) returnerte oss beskrivelseskartnummeret 3 og deretter venter programmet, som forventet, på ytterligere instruksjoner i systemanropet pause(2).
La oss nå sende programmet vårt til bakgrunnen eller åpne en annen terminal og se på objektet vårt ved å bruke verktøyet bpftool (vi kan skille kartet vårt fra andre ved navn):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...
Tallet 114 er den globale IDen til objektet vårt. Ethvert program på systemet kan bruke denne IDen til å åpne et eksisterende kart ved hjelp av kommandoen BPF_MAP_GET_FD_BY_ID systemanrop bpf.
Nå kan vi spille med hashbordet vårt. La oss se på innholdet:
$ sudo bpftool map dump id 114
Found 0 elements
Tømme. La oss sette en verdi i det hash[1] = 1:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0
La oss se på tabellen igjen:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 element
Hurra! Vi klarte å legge til ett element. Merk at vi må jobbe på bytenivå for å gjøre dette, siden bptftool vet ikke hvilken type verdiene i hashtabellen er. (Denne kunnskapen kan overføres til henne ved hjelp av BTF, men mer om det nå.)
Hvordan leser og legger bpftool til elementer? La oss ta en titt under panseret:
Først åpnet vi kartet med dets globale ID ved å bruke kommandoen BPF_MAP_GET_FD_BY_ID и bpf(2) returnerte deskriptor 3 til oss. Videre bruk av kommandoen BPF_MAP_GET_NEXT_KEY vi fant den første nøkkelen i tabellen ved å passere NULL som en peker til "forrige"-tasten. Hvis vi har nøkkelen, kan vi gjøre det BPF_MAP_LOOKUP_ELEMsom returnerer en verdi til en peker value. Det neste trinnet er at vi prøver å finne neste element ved å sende en peker til gjeldende nøkkel, men tabellen vår inneholder bare ett element og kommandoen BPF_MAP_GET_NEXT_KEY returnerer ENOENT.
Ok, la oss endre verdien med tast 1, la oss si at forretningslogikken vår krever registrering hash[1] = 2:
Som forventet er det veldig enkelt: kommandoen BPF_MAP_GET_FD_BY_ID åpner kartet vårt etter ID, og kommandoen BPF_MAP_UPDATE_ELEM overskriver elementet.
Så, etter å ha laget en hashtabell fra ett program, kan vi lese og skrive innholdet fra et annet. Merk at hvis vi var i stand til å gjøre dette fra kommandolinjen, så kan et hvilket som helst annet program på systemet gjøre det. I tillegg til kommandoene beskrevet ovenfor, for arbeid med kart fra brukerområdet, følgende:
BPF_MAP_LOOKUP_ELEM: finn verdi etter nøkkel
BPF_MAP_UPDATE_ELEM: oppdater/opprett verdi
BPF_MAP_DELETE_ELEM: fjern nøkkel
BPF_MAP_GET_NEXT_KEY: finn den neste (eller første) tasten
BPF_MAP_GET_NEXT_ID: lar deg gå gjennom alle eksisterende kart, det er slik det fungerer bpftool map
BPF_MAP_GET_FD_BY_ID: åpne et eksisterende kart ved hjelp av dens globale ID
BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomisk oppdater verdien til et objekt og returner det gamle
BPF_MAP_FREEZE: gjør kartet uforanderlig fra brukerområdet (denne operasjonen kan ikke angres)
BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: masseoperasjoner. For eksempel, BPF_MAP_LOOKUP_AND_DELETE_BATCH - dette er den eneste pålitelige måten å lese og tilbakestille alle verdier fra kartet
Ikke alle disse kommandoene fungerer for alle karttyper, men generelt ser det å jobbe med andre typer kart fra brukerområdet nøyaktig det samme ut som å jobbe med hashtabeller.
For ordens skyld, la oss fullføre hashtabelleksperimentene våre. Husker du at vi laget en tabell som kan inneholde opptil fire nøkler? La oss legge til noen flere elementer:
$ 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
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long
Som forventet lyktes vi ikke. La oss se på feilen mer detaljert:
$ 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 +++
Alt er bra: som forventet, laget BPF_MAP_UPDATE_ELEM prøver å lage en ny, femte nøkkel, men krasjer E2BIG.
Så vi kan lage og laste BPF-programmer, samt lage og administrere kart fra brukerområdet. Nå er det logisk å se på hvordan vi kan bruke kart fra selve BPF-programmene. Vi kunne snakket om dette på språket til vanskelige programmer i maskinmakrokoder, men faktisk er tiden inne for å vise hvordan BPF-programmer faktisk skrives og vedlikeholdes – vha. libbpf.
(For lesere som er misfornøyde med mangelen på et eksempel på lavt nivå: vi vil analysere i detalj programmer som bruker kart og hjelpefunksjoner opprettet ved hjelp av libbpf og fortelle deg hva som skjer på instruksjonsnivå. For lesere som er misfornøyde veldig mye, la vi til eksempel på riktig sted i artikkelen.)
Skrive BPF-programmer ved hjelp av libbpf
Å skrive BPF-programmer ved hjelp av maskinkoder kan være interessant bare første gang, og så setter mettheten inn. I dette øyeblikket må du rette oppmerksomheten mot llvm, som har en backend for å generere kode for BPF-arkitekturen, samt et bibliotek libbpf, som lar deg skrive brukersiden til BPF-applikasjoner og laste inn koden til BPF-programmer generert ved hjelp av llvm/clang.
Faktisk, som vi vil se i denne og påfølgende artikler, libbpf gjør ganske mye arbeid uten det (eller lignende verktøy - iproute2, libbcc, libbpf-goosv.) er det umulig å leve. En av de drepende funksjonene i prosjektet libbpf er BPF CO-RE (Compile Once, Run Everywhere) - et prosjekt som lar deg skrive BPF-programmer som er portable fra en kjerne til en annen, med muligheten til å kjøre på forskjellige APIer (for eksempel når kjernestrukturen endres fra versjon til versjon). For å kunne jobbe med CO-RE må kjernen din være kompilert med BTF-støtte (vi beskriver hvordan du gjør dette i avsnittet Utviklingsverktøy. Du kan sjekke om kjernen din er bygget med BTF eller ikke veldig enkelt - ved tilstedeværelsen av følgende fil:
Denne filen lagrer informasjon om alle datatyper som brukes i kjernen og brukes i alle våre eksempler på bruk libbpf. Vi vil snakke i detalj om CO-RE i neste artikkel, men i denne - bare bygg deg en kjerne med CONFIG_DEBUG_INFO_BTF.
Bibliotek libbpf bor rett i katalogen tools/lib/bpf kjernen og dens utvikling utføres gjennom e-postlisten [email protected]. Imidlertid opprettholdes et eget depot for behovene til applikasjoner som bor utenfor kjernen https://github.com/libbpf/libbpf der kjernebiblioteket er speilvendt for lesetilgang mer eller mindre som det er.
I denne delen skal vi se på hvordan du kan lage et prosjekt som bruker libbpf, la oss skrive flere (mer eller mindre meningsløse) testprogrammer og analysere i detalj hvordan det hele fungerer. Dette vil tillate oss å lettere forklare i de følgende avsnittene nøyaktig hvordan BPF-programmer samhandler med kart, kjernehjelpere, BTF, etc.
Vanligvis prosjekter ved hjelp av libbpf legg til et GitHub-depot som en git-undermodul, vi gjør det samme:
Vår neste plan i denne delen er som følger: vi vil skrive et BPF-program som BPF_PROG_TYPE_XDP, det samme som i forrige eksempel, men i C kompilerer vi det ved å bruke clang, og skriv et hjelpeprogram som vil laste det inn i kjernen. I de følgende avsnittene vil vi utvide mulighetene til både BPF-programmet og assistentprogrammet.
Eksempel: lage en fullverdig applikasjon ved hjelp av libbpf
Til å begynne med bruker vi filen /sys/kernel/btf/vmlinux, som ble nevnt ovenfor, og lag dets ekvivalent i form av en overskriftsfil:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Denne filen vil lagre alle datastrukturene som er tilgjengelige i kjernen vår, for eksempel er dette hvordan IPv4-overskriften er definert i kjernen:
Selv om programmet vårt viste seg å være veldig enkelt, må vi fortsatt ta hensyn til mange detaljer. For det første er den første overskriftsfilen vi inkluderer vmlinux.h, som vi nettopp genererte ved hjelp av bpftool btf dump - nå trenger vi ikke å installere kernel-headers-pakken for å finne ut hvordan kjernestrukturene ser ut. Følgende overskriftsfil kommer til oss fra biblioteket libbpf. Nå trenger vi det bare for å definere makroen SEC, som sender tegnet til den aktuelle delen av ELF-objektfilen. Programmet vårt finnes i seksjonen xdp/simple, hvor vi før skråstreken definerer programtypen BPF - dette er konvensjonen som brukes i libbpf, basert på seksjonsnavnet vil den erstatte den riktige typen ved oppstart bpf(2). Selve BPF-programmet er C - veldig enkelt og består av en linje return XDP_PASS. Til slutt et eget avsnitt "license" inneholder navnet på lisensen.
Vi kan kompilere programmet vårt ved å bruke llvm/clang, versjon >= 10.0.0, eller enda bedre, høyere (se avsnittet Utviklingsverktøy):
Blant de interessante funksjonene: vi angir målarkitekturen -target bpf og veien til overskriftene libbpf, som vi nylig installerte. Også, ikke glem det -O2, uten dette alternativet kan du få overraskelser i fremtiden. La oss se på koden vår, klarte vi å skrive programmet vi ønsket?
Ja, det fungerte! Nå har vi en binær fil med programmet, og vi ønsker å lage en applikasjon som vil laste den inn i kjernen. For dette formålet biblioteket libbpf tilbyr oss to alternativer – bruk en API på lavere nivå eller en API på høyere nivå. Vi vil gå den andre veien, siden vi ønsker å lære å skrive, laste og koble til BPF-programmer med minimal innsats for deres påfølgende studie.
Først må vi generere "skjelettet" til programmet vårt fra dets binære ved å bruke det samme verktøyet bpftool — den sveitsiske kniven til BPF-verdenen (som kan tas bokstavelig, siden Daniel Borkman, en av skaperne og vedlikeholderne av BPF, er sveitser):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
I fil xdp-simple.skel.h inneholder den binære koden til programmet vårt og funksjoner for å administrere - laste, legge ved, slette objektet vårt. I vårt enkle tilfelle ser dette ut som overkill, men det fungerer også i tilfellet der objektfilen inneholder mange BPF-programmer og kart og for å laste denne gigantiske ELF-en trenger vi bare å generere skjelettet og kalle en eller to funksjoner fra den tilpassede applikasjonen vi skriver La oss gå videre nå.
Strengt tatt er lasteprogrammet vårt trivielt:
#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);
}
Her struct xdp_simple_bpf definert i filen xdp-simple.skel.h og beskriver objektfilen vår:
Vi kan se spor av et lavt nivå API her: strukturen struct bpf_program *simple и struct bpf_link *simple. Den første strukturen beskriver spesifikt programmet vårt, skrevet i seksjonen xdp/simple, og den andre beskriver hvordan programmet kobles til hendelseskilden.
Funksjon xdp_simple_bpf__open_and_load, åpner et ELF-objekt, analyserer det, lager alle strukturer og understrukturer (i tillegg til programmet inneholder ELF også andre seksjoner - data, skrivebeskyttet data, feilsøkingsinformasjon, lisens, etc.), og laster det deretter inn i kjernen ved hjelp av et system anrop bpf, som vi kan sjekke ved å kompilere og kjøre programmet:
La oss nå se på programmet vårt ved hjelp av bpftool. La oss finne ID-en hennes:
# 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)
og dump (vi bruker en forkortet form av kommandoen bpftool prog dump xlated):
# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
0: (b7) r0 = 2
1: (95) exit
Noe nytt! Programmet skrev ut deler av C-kildefilen vår. Dette ble gjort av biblioteket libbpf, som fant feilsøkingsdelen i binærfilen, kompilerte den til et BTF-objekt, lastet den inn i kjernen ved å bruke BPF_BTF_LOAD, og spesifiserte deretter den resulterende filbeskrivelsen når du lastet programmet med kommandoen BPG_PROG_LOAD.
Kjernehjelpere
BPF-programmer kan kjøre "eksterne" funksjoner - kjernehjelpere. Disse hjelpefunksjonene lar BPF-programmer få tilgang til kjernestrukturer, administrere kart og også kommunisere med den "virkelige verden" - lage perf-hendelser, kontrollere maskinvare (for eksempel omdirigere pakker), etc.
Eksempel: bpf_get_smp_processor_id
Innenfor rammen av "læring ved eksempel"-paradigmet, la oss vurdere en av hjelpefunksjonene, bpf_get_smp_processor_id(), sikker i fil kernel/bpf/helpers.c. Den returnerer nummeret til prosessoren som BPF-programmet som kalte det kjører på. Men vi er ikke like interessert i dens semantikk som i det faktum at implementeringen tar en linje:
BPF-hjelpefunksjonsdefinisjonene ligner på Linux-systemanropsdefinisjonene. Her defineres for eksempel en funksjon som ikke har noen argumenter. (En funksjon som tar for eksempel tre argumenter er definert ved hjelp av makroen BPF_CALL_3. Maksimalt antall argumenter er fem.) Dette er imidlertid bare den første delen av definisjonen. Den andre delen er å definere typestrukturen struct bpf_func_proto, som inneholder en beskrivelse av hjelpefunksjonen som verifikatoren forstår:
For at BPF-programmer av en bestemt type skal bruke denne funksjonen, må de registrere den, for eksempel for typen BPF_PROG_TYPE_XDP en funksjon er definert i kjernen xdp_func_proto, som avgjør fra hjelpefunksjons-IDen om XDP støtter denne funksjonen eller ikke. Vår funksjon er støtter:
Nye BPF-programtyper er "definert" i filen include/linux/bpf_types.h ved hjelp av en makro BPF_PROG_TYPE. Definert i anførselstegn fordi det er en logisk definisjon, og i C-språklige termer forekommer definisjonen av et helt sett med betongkonstruksjoner andre steder. Spesielt i filen kernel/bpf/verifier.c alle definisjoner fra filen bpf_types.h brukes til å lage en rekke strukturer bpf_verifier_ops[]:
Det vil si at for hver type BPF-program er det definert en peker til en datastruktur av typen struct bpf_verifier_ops, som initialiseres med verdien _name ## _verifier_ops, dvs., xdp_verifier_ops for xdp... Struktur xdp_verifier_opsbestemt av i fil net/core/filter.c som følger:
Her ser vi vår kjente funksjon xdp_func_proto, som vil kjøre verifikatoren hver gang den støter på en utfordring noe slag funksjoner i et BPF-program, se verifier.c.
La oss se på hvordan et hypotetisk BPF-program bruker funksjonen bpf_get_smp_processor_id. For å gjøre dette, omskriver vi programmet fra forrige seksjon som følger:
det er, bpf_get_smp_processor_id er en funksjonspeker hvis verdi er 8, hvor 8 er verdien BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, som er definert for oss i filen vmlinux.h (fil bpf_helper_defs.h i kjernen genereres av et skript, så de "magiske" tallene er ok). Denne funksjonen tar ingen argumenter og returnerer en verdi av type __u32. Når vi kjører det i programmet vårt, clang genererer en instruksjon BPF_CALL "den rette typen" La oss kompilere programmet og se på delen xdp/simple:
I første linje ser vi instruksjoner call, parameter IMM som er lik 8, og SRC_REG - null. I følge ABI-avtalen som brukes av verifikatoren, er dette et kall til hjelperfunksjon nummer åtte. Når den først er lansert, er logikken enkel. Returverdi fra register r0 kopiert til r1 og på linje 2,3 er det konvertert til type u32 — de øverste 32 bitene slettes. På linjene 4,5,6,7 returnerer vi 2 (XDP_PASS) eller 1 (XDP_DROP) avhengig av om hjelpefunksjonen fra linje 0 returnerte en null eller ikke-null verdi.
La oss teste oss selv: last programmet og se på utdataene bpftool prog dump xlated:
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914
$ sudo bpftool p | grep simple
523: xdp name simple tag 44c38a10c657e1b0 gpl
pids xdp-simple(10915)
$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
0: (85) call bpf_get_smp_processor_id#114128
1: (bf) r1 = r0
2: (67) r1 <<= 32
3: (77) r1 >>= 32
4: (b7) r0 = 2
; }
5: (15) if r1 == 0x0 goto pc+1
6: (b7) r0 = 1
7: (95) exit
Ok, verifikatoren fant den riktige kjernehjelperen.
Eksempel: sende argumenter og til slutt kjøre programmet!
Alle hjelpefunksjoner på kjørenivå har en prototype
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
Parametre til hjelpefunksjoner sendes i registre r1-r5, og verdien returneres i registeret r0. Det er ingen funksjoner som tar mer enn fem argumenter, og støtte for dem forventes ikke å bli lagt til i fremtiden.
La oss ta en titt på den nye kjernehjelperen og hvordan BPF sender parametere. La oss skrive om xdp-simple.bpf.c som følger (resten av linjene er ikke endret):
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}
Programmet vårt skriver ut nummeret til CPUen som det kjører på. La oss kompilere den og se på koden:
På linje 0-7 skriver vi strengen running on CPU%un, og så på linje 8 kjører vi den kjente bpf_get_smp_processor_id. På linje 9-12 forbereder vi hjelperargumentene bpf_printk - registrerer r1, r2, r3. Hvorfor er det tre av dem og ikke to? Fordi bpf_printk - dette er en makro-innpakning rundt den virkelige hjelperen bpf_trace_printk, som må passere størrelsen på formatstrengen.
La oss nå legge til et par linjer til xdp-simple.cslik at programmet vårt kobles til grensesnittet lo og virkelig startet!
Her bruker vi funksjonen bpf_set_link_xdp_fd, som kobler XDP-type BPF-programmer til nettverksgrensesnitt. Vi har hardkodet grensesnittnummeret lo, som alltid er 1. Vi kjører funksjonen to ganger for først å koble fra det gamle programmet hvis det var vedlagt. Legg merke til at nå trenger vi ingen utfordring pause eller en uendelig sløyfe: lasterprogrammet vårt avsluttes, men BPF-programmet vil ikke bli drept siden det er koblet til hendelseskilden. Etter vellykket nedlasting og tilkobling, vil programmet startes for hver nettverkspakke som ankommer lo.
La oss laste ned programmet og se på grensesnittet 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
Programmet vi lastet ned har ID 669 og vi ser samme ID på grensesnittet lo. Vi sender et par pakker til 127.0.0.1 (forespørsel + svar):
$ ping -c1 localhost
og la oss nå se på innholdet i den virtuelle feilsøkingsfilen /sys/kernel/debug/tracing/trace_pipe, hvori bpf_printk skriver sine meldinger:
# 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
To pakker ble oppdaget lo og behandlet på CPU0 - vårt første fullverdige meningsløse BPF-program fungerte!
Det er verdt å merke seg det bpf_printk Det er ikke for ingenting at den skriver til feilsøkingsfilen: dette er ikke den mest vellykkede hjelperen for bruk i produksjon, men målet vårt var å vise noe enkelt.
Tilgang til kart fra BPF-programmer
Eksempel: bruk av kart fra BPF-programmet
I de forrige avsnittene lærte vi hvordan du lager og bruker kart fra brukerområdet, og la oss nå se på kjernedelen. La oss starte, som vanlig, med et eksempel. La oss omskrive programmet vårt xdp-simple.bpf.c som følger:
I begynnelsen av programmet la vi til en kartdefinisjon woo: Dette er en 8-elements matrise som lagrer verdier som u64 (i C vil vi definere en slik matrise som u64 woo[8]). I et program "xdp/simple" vi får det gjeldende prosessornummeret inn i en variabel key og deretter bruke hjelpefunksjonen bpf_map_lookup_element vi får en peker til den tilsvarende oppføringen i matrisen, som vi øker med én. Oversatt til russisk: vi beregner statistikk over hvilken CPU som behandlet innkommende pakker. La oss prøve å kjøre programmet:
La oss sjekke at hun er koblet til lo og send noen pakker:
$ 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
Nesten alle prosesser ble behandlet på CPU7. Dette er ikke viktig for oss, hovedsaken er at programmet fungerer og vi forstår hvordan man får tilgang til kart fra BPF-programmer - ved å bruke хелперов bpf_mp_*.
Mystisk indeks
Så vi kan få tilgang til kartet fra BPF-programmet ved å bruke samtaler som
$ 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
Men hvis vi ser på det allerede lastede programmet, ser vi en peker til riktig kart (linje 4):
Dermed kan vi konkludere med at på tidspunktet for lansering av vårt lasteprogram, var lenken til &woo ble erstattet av noe med et bibliotek libbpf. Først skal vi se på utgangen strace:
Det ser vi libbpf laget et kart woo og lastet ned programmet vårt simple. La oss se nærmere på hvordan vi laster programmet:
anrop xdp_simple_bpf__open_and_load fra fil xdp-simple.skel.h
som forårsaker xdp_simple_bpf__load fra fil xdp-simple.skel.h
som forårsaker bpf_object__load_skeleton fra fil libbpf/src/libbpf.c
som forårsaker bpf_object__load_xattr av libbpf/src/libbpf.c
Den siste funksjonen vil blant annet kalle bpf_object__create_maps, som oppretter eller åpner eksisterende kart, og gjør dem om til filbeskrivelser. (Det er her vi ser BPF_MAP_CREATE i utgangen strace.) Deretter kalles funksjonen bpf_object__relocate og det er hun som interesserer oss, siden vi husker det vi så woo i flyttetabellen. Når vi utforsker det, finner vi oss til slutt i funksjonen bpf_program__relocate, hvilken omhandler kartflyttinger:
case RELO_LD64:
insn[0].src_reg = BPF_PSEUDO_MAP_FD;
insn[0].imm = obj->maps[relo->map_idx].fd;
break;
og erstatte kilderegisteret i det med BPF_PSEUDO_MAP_FD, og den første IMM til filbeskrivelsen til kartet vårt, og hvis den er lik, for eksempel, 0xdeadbeef, så vil vi motta instruksjonen
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll
Slik overføres kartinformasjon til et spesifikt lastet BPF-program. I dette tilfellet kan kartet lages ved hjelp av BPF_MAP_CREATE, og åpnet med ID ved hjelp av BPF_MAP_GET_FD_BY_ID.
Totalt ved bruk libbpf Algoritmen er som følger:
under kompilering opprettes poster i flyttetabellen for lenker til kart
libbpf åpner ELF-objektboken, finner alle brukte kart og lager filbeskrivelser for dem
filbeskrivelser lastes inn i kjernen som en del av instruksjonen LD64
Som du kan forestille deg, er det mer i vente, og vi må se inn i kjernen. Heldigvis har vi peiling – vi har skrevet ned meningen BPF_PSEUDO_MAP_FD inn i kilderegisteret og vi kan begrave det, som vil føre oss til det hellige for alle hellige - kernel/bpf/verifier.c, der en funksjon med et særegent navn erstatter en filbeskrivelse med adressen til en type struktur struct bpf_map:
(full kode finner du по ссылке). Så vi kan utvide algoritmen vår:
mens du laster programmet, kontrollerer verifikatoren riktig bruk av kartet og skriver adressen til den tilsvarende strukturen struct bpf_map
Når du laster ned ELF binær ved hjelp av libbpf Det er mye mer som skjer, men vi vil diskutere det i andre artikler.
Laster programmer og kart uten libbpf
Som lovet, her er et eksempel for lesere som vil vite hvordan man lager og laster et program som bruker kart, uten hjelp libbpf. Dette kan være nyttig når du jobber i et miljø der du ikke kan bygge avhengigheter, eller lagre hver bit, eller skriver et program som ply, som genererer BPF-binær kode i farten.
For å gjøre det lettere å følge logikken, vil vi omskrive eksemplet vårt for disse formålene xdp-simple. Den fullstendige og litt utvidede koden til programmet som er omtalt i dette eksemplet, finner du i denne GIST.
Logikken i søknaden vår er som følger:
lage et typekart BPF_MAP_TYPE_ARRAY ved å bruke kommandoen BPF_MAP_CREATE,
lag et program som bruker dette kartet,
koble programmet til grensesnittet lo,
som oversettes til menneskelig som
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);
}
Her map_create lager et kart på samme måte som vi gjorde i det første eksemplet om systemkallingen bpf - "kjerne, lag meg et nytt kart i form av en rekke med 8 elementer som __u64 og gi meg tilbake filbeskrivelsen":
Den vanskelige delen prog_load er definisjonen av vårt BPF-program som en rekke strukturer struct bpf_insn insns[]. Men siden vi bruker et program som vi har i C, kan vi jukse litt:
Totalt må vi skrive 14 instruksjoner i form av strukturer som struct bpf_insn (råd: ta dumpen ovenfra, les instruksjonsdelen på nytt, åpne linux/bpf.h и linux/bpf_common.h og prøv å bestemme struct bpf_insn insns[] på egenhånd):
En øvelse for de som ikke skrev dette selv – finn map_fd.
Det er enda en ikke avslørt del igjen i programmet vårt - xdp_attach. Dessverre kan ikke programmer som XDP kobles til ved hjelp av et systemanrop bpf. Personene som opprettet BPF og XDP var fra det nettbaserte Linux-fellesskapet, noe som betyr at de brukte den mest kjente for dem (men ikke til normal people) grensesnitt for samhandling med kjernen: netlink-uttak, se også RFC3549. Den enkleste måten å implementere xdp_attach kopierer kode fra libbpf, nemlig fra filen netlink.c, som er hva vi gjorde, forkortet det litt:
Velkommen til en verden av netlink-sockets
Åpne en netlink-sockettype 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;
}
Vi leser fra denne kontakten:
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;
}
Til slutt, her er funksjonen vår som åpner en socket og sender en spesiell melding til den som inneholder en filbeskrivelse:
$ 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
Hurra, alt fungerer. Merk forresten at kartet vårt igjen vises i form av byte. Dette skyldes det faktum at i motsetning til libbpf vi lastet ikke inn typeinformasjon (BTF). Men vi snakker mer om dette neste gang.
Utviklingsverktøy
I denne delen skal vi se på minimum BPF-utviklerverktøysett.
Generelt sett trenger du ikke noe spesielt for å utvikle BPF-programmer - BPF kjører på en hvilken som helst anstendig distribusjonskjerne, og programmer er bygget med clang, som kan leveres fra pakken. Men på grunn av det faktum at BPF er under utvikling, er kjernen og verktøyene i stadig endring, hvis du ikke vil skrive BPF-programmer med gammeldagse metoder fra 2019, må du kompilere
llvm/clang
pahole
dens kjerne
bpftool
(Til referanse ble denne delen og alle eksemplene i artikkelen kjørt på Debian 10.)
llvm/clang
BPF er vennlig med LLVM, og selv om programmer for BPF nylig kan kompileres ved hjelp av gcc, utføres all gjeldende utvikling for LLVM. Derfor vil vi først og fremst bygge den nåværende versjonen clang fra 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
... много времени спустя
$
(Monteringsveiledning clang tatt av meg fra bpf_devel_QA.)
Vi vil ikke installere programmene vi nettopp har bygget, men bare legge dem til PATH, for eksempel:
export PATH="`pwd`/bin:$PATH"
(Dette kan legges til .bashrc eller til en egen fil. Personlig legger jeg til ting som dette ~/bin/activate-llvm.sh og når det er nødvendig gjør jeg det . activate-llvm.sh.)
Pahole og BTF
Nytte pahole brukes når du bygger kjernen for å lage feilsøkingsinformasjon i BTF-format. Vi vil ikke gå i detalj i denne artikkelen om detaljene i BTF-teknologi, annet enn det faktum at det er praktisk og vi ønsker å bruke det. Så hvis du skal bygge kjernen din, bygg først pahole (uten pahole du vil ikke kunne bygge kjernen med alternativet 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
Kjerner for å eksperimentere med BPF
Når jeg utforsker mulighetene til BPF, ønsker jeg å sette sammen min egen kjerne. Dette er generelt sett ikke nødvendig, siden du vil kunne kompilere og laste BPF-programmer på distribusjonskjernen, men å ha din egen kjerne lar deg bruke de nyeste BPF-funksjonene, som i beste fall vil vises i distribusjonen din om måneder , eller, som i tilfellet med noen feilsøkingsverktøy, vil ikke bli pakket i det hele tatt i overskuelig fremtid. Dens egen kjerne gjør det også viktig å eksperimentere med koden.
For å bygge en kjerne trenger du for det første selve kjernen, og for det andre en kjernekonfigurasjonsfil. For å eksperimentere med BPF kan vi bruke det vanlige vanilje kjerne eller en av utviklingskjernene. Historisk sett foregår BPF-utvikling innenfor Linux-nettverksfellesskapet, og derfor går alle endringer før eller siden gjennom David Miller, Linux-nettverksvedlikeholderen. Avhengig av deres natur - redigeringer eller nye funksjoner - faller nettverksendringer inn i en av to kjerner - net eller net-next. Endringer for BPF fordeles på samme måte mellom bpf и bpf-next, som deretter samles i henholdsvis netto og net-neste. For flere detaljer, se bpf_devel_QA и netdev-vanlige spørsmål. Så velg en kjerne basert på din smak og stabilitetsbehovene til systemet du tester på (*-next kjernene er de mest ustabile av de som er oppført).
Det er utenfor rammen av denne artikkelen å snakke om hvordan du administrerer kjernekonfigurasjonsfiler - det antas at du enten allerede vet hvordan du gjør dette, eller klar til å lære på egenhånd. Følgende instruksjoner bør imidlertid være mer eller mindre nok til å gi deg et fungerende BPF-aktivert system.
Last ned en av kjernene ovenfor:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next
Bygg en minimal fungerende kjernekonfigurasjon:
$ cp /boot/config-`uname -r` .config
$ make localmodconfig
Aktiver BPF-alternativer i filen .config etter eget valg (mest sannsynlig CONFIG_BPF vil allerede være aktivert siden systemd bruker det). Her er en liste over alternativer fra kjernen som brukes for denne artikkelen:
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
Da kan vi enkelt sette sammen og installere modulene og kjernen (du kan forresten sette sammen kjernen ved å bruke den nymonterte clangved å legge til CC=clang):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install
og start på nytt med den nye kjernen (jeg bruker for dette kexec fra pakken 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
Det mest brukte verktøyet i artikkelen vil være verktøyet bpftool, levert som en del av Linux-kjernen. Det er skrevet og vedlikeholdt av BPF-utviklere for BPF-utviklere og kan brukes til å administrere alle typer BPF-objekter - last inn programmer, opprette og redigere kart, utforske livet til BPF-økosystemet, etc. Dokumentasjon i form av kildekoder for man-sider finnes i kjernen eller allerede kompilert, nettverk.
I skrivende stund bpftool leveres kun ferdig for RHEL, Fedora og Ubuntu (se f.eks. denne tråden, som forteller den uferdige historien om emballasje bpftool i Debian). Men hvis du allerede har bygget kjernen din, så bygg bpftool like enkelt som en plett:
$ 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 ]
$
(her ${linux} - dette er kjernekatalogen din.) Etter å ha utført disse kommandoene bpftool vil bli samlet i en katalog ${linux}/tools/bpf/bpftool og den kan legges til banen (først av alt til brukeren root) eller bare kopier til /usr/local/sbin.
Samle inn bpftool det er best å bruke sistnevnte clang, satt sammen som beskrevet ovenfor, og sjekk om den er satt sammen riktig - ved hjelp av for eksempel kommandoen
$ 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
...
som vil vise hvilke BPF-funksjoner som er aktivert i kjernen din.
Forresten, den forrige kommandoen kan kjøres som
# bpftool f p k
Dette gjøres analogt med verktøyene fra pakken iproute2, hvor vi for eksempel kan si ip a s eth0 i stedet for ip addr show dev eth0.
Konklusjon
BPF lar deg sko en loppe for å effektivt måle og endre funksjonaliteten til kjernen. Systemet viste seg å være svært vellykket, i de beste tradisjonene til UNIX: en enkel mekanisme som lar deg (om)programmere kjernen tillot et stort antall mennesker og organisasjoner å eksperimentere. Og selv om eksperimentene, så vel som utviklingen av selve BPF-infrastrukturen, langt fra er ferdige, har systemet allerede en stabil ABI som lar deg bygge pålitelig, og viktigst av alt, effektiv forretningslogikk.
Jeg vil merke meg at teknologien etter min mening har blitt så populær fordi den på den ene siden kan spille (arkitekturen til en maskin kan forstås mer eller mindre på en kveld), og på den annen side å løse problemer som ikke kunne løses (vakkert) før den dukket opp. Disse to komponentene tvinger sammen mennesker til å eksperimentere og drømme, noe som fører til fremveksten av flere og flere innovative løsninger.
Denne artikkelen, selv om den ikke er spesielt kort, er bare en introduksjon til BPF-verdenen og beskriver ikke "avanserte" funksjoner og viktige deler av arkitekturen. Planen fremover er omtrent slik: den neste artikkelen vil være en oversikt over BPF-programtyper (det er 5.8 programtyper som støttes i 30-kjernen), så skal vi til slutt se på hvordan man skriver ekte BPF-applikasjoner ved hjelp av kjernesporingsprogrammer som et eksempel, så er det på tide med et mer dyptgående kurs om BPF-arkitektur, etterfulgt av eksempler på BPF-nettverk og sikkerhetsapplikasjoner.
BPF og XDP Referanseguide — dokumentasjon om BPF fra cilium, eller mer presist fra Daniel Borkman, en av skaperne og vedlikeholderne av BPF. Dette er en av de første seriøse beskrivelsene, som skiller seg fra de andre ved at Daniel vet nøyaktig hva han skriver om og det er ingen feil der. Dette dokumentet beskriver spesielt hvordan du arbeider med BPF-programmer av XDP- og TC-typene ved å bruke det velkjente verktøyet ip fra pakken iproute2.
Dokumentasjon/nettverk/filter.txt — original fil med dokumentasjon for klassisk og deretter utvidet BPF. God lesning hvis du vil fordype deg i monteringsspråk og tekniske arkitektoniske detaljer.
Blogg om BPF fra facebook. Den oppdateres sjelden, men passende, som Alexei Starovoitov (forfatter av eBPF) og Andrii Nakryiko - (vedlikeholdsholder) skriver der libbpf).
Hemmelighetene til bpftool. En underholdende twittertråd fra Quentin Monnet med eksempler og hemmeligheter ved bruk av bpftool.