I begyndelsen var der en teknologi, og den hed BPF. Vi kiggede på hende Tidligere, Gamle Testamentes artikel i denne serie. I 2013, gennem indsatsen fra Alexei Starovoitov og Daniel Borkman, blev en forbedret version af den, optimeret til moderne 64-bit maskiner, udviklet og inkluderet i Linux-kernen. Denne nye teknologi blev kort kaldt Intern BPF, derefter omdøbt til Extended BPF, og nu, efter flere år, kalder alle den simpelthen BPF.
Groft sagt giver BPF dig mulighed for at køre vilkårlig brugerleveret kode i Linux-kernerummet, og den nye arkitektur viste sig at være så vellykket, at vi får brug for et dusin flere artikler til at beskrive alle dets applikationer. (Det eneste, udviklerne ikke gjorde godt, som du kan se i ydeevnekoden nedenfor, var at skabe et anstændigt logo.)
Denne artikel beskriver strukturen af den virtuelle BPF-maskine, kernegrænseflader til at arbejde med BPF, udviklingsværktøjer, samt en kort, meget kort oversigt over eksisterende muligheder, dvs. alt, hvad vi får brug for i fremtiden til en dybere undersøgelse af de praktiske anvendelser af BPF.
Håndtering af objekter ved hjælp af bpf-systemkaldet. Med en vis forståelse af systemet, der allerede er på plads, vil vi endelig se på, hvordan man opretter og manipulerer objekter fra brugerrummet ved hjælp af et særligt systemkald − bpf(2).
Пишем программы BPF с помощью libbpf. Selvfølgelig kan du skrive programmer ved hjælp af et systemkald. Men det er svært. For et mere realistisk scenarie udviklede nukleare programmører et bibliotek libbpf. Vi opretter et grundlæggende BPF-applikationsskelet, som vi vil bruge i efterfølgende eksempler.
Kernelhjælpere. Her vil vi lære, hvordan BPF-programmer kan få adgang til kernehjælpefunktioner - et værktøj, der sammen med kort fundamentalt udvider mulighederne for den nye BPF sammenlignet med den klassiske.
Adgang til kort fra BPF-programmer. På dette tidspunkt ved vi nok til at forstå præcis, hvordan vi kan oprette programmer, der bruger kort. Og lad os endda tage et hurtigt kig ind i den store og mægtige verifikator.
Udviklingsværktøjer. Hjælpeafsnit om, hvordan man samler de nødvendige hjælpeprogrammer og kerne til eksperimenter.
Konklusion. Sidst i artiklen finder de, der læser så langt, motiverende ord og en kort beskrivelse af, hvad der vil ske, i de følgende artikler. Vi vil også liste en række links til selvstudie for dem, der ikke har lyst eller evne til at vente på fortsættelsen.
Introduktion til BPF-arkitektur
Før vi begynder at overveje BPF-arkitekturen, vil vi henvise en sidste gang (åh) til klassisk BPF, som blev udviklet som et svar på fremkomsten af RISC-maskiner og løste problemet med effektiv pakkefiltrering. Arkitekturen viste sig at være så vellykket, at den, efter at være født i de flotte halvfemser i Berkeley UNIX, blev overført til de fleste eksisterende operativsystemer, overlevede ind i de skøre tyvere og stadig finder nye applikationer.
Den nye BPF blev udviklet som et svar på allestedsnærværelsen af 64-bit maskiner, cloud-tjenester og det øgede behov for værktøjer til at skabe SDN (Softe-defineret nnetværk). Udviklet af kernenetværksingeniører som en forbedret erstatning for den klassiske BPF, fandt den nye BPF bogstaveligt talt seks måneder senere applikationer i den vanskelige opgave at spore Linux-systemer, og nu, seks år efter dens fremkomst, har vi brug for en hel næste artikel bare for at liste de forskellige typer programmer.
Sjove billeder
I sin kerne er BPF en virtuel sandbox-maskine, der giver dig mulighed for at køre "vilkårlig" kode i kernerummet uden at gå på kompromis med sikkerheden. BPF-programmer oprettes i brugerrummet, indlæses i kernen og forbindes til en eventkilde. En hændelse kan for eksempel være levering af en pakke til en netværksgrænseflade, lancering af en eller anden kernefunktion osv. I tilfælde af en pakke, vil BPF-programmet have adgang til pakkens data og metadata (til læsning og eventuelt skrivning, afhængigt af programtypen); i tilfælde af at køre en kernefunktion, argumenterne for funktionen, herunder pointere til kernehukommelsen osv.
Lad os se nærmere på denne proces. Til at begynde med, lad os tale om den første forskel fra den klassiske BPF, som programmer blev skrevet i assembler. I den nye version blev arkitekturen udvidet, så programmer kunne skrives på højniveausprog, primært naturligvis i C. Til dette blev der udviklet en backend til llvm, som giver mulighed for at generere bytekode til BPF-arkitekturen.
BPF-arkitekturen blev til dels designet til at køre effektivt på moderne maskiner. For at få dette til at fungere i praksis, bliver BPF-bytekoden, når den først er indlæst i kernen, oversat til native kode ved hjælp af en komponent kaldet en JIT-kompiler (Jøvre In Tjeg mig). Dernæst, hvis du husker det, i klassisk BPF blev programmet indlæst i kernen og knyttet til begivenhedskilden atomisk - i sammenhæng med et enkelt systemkald. I den nye arkitektur sker dette i to trin - først indlæses koden i kernen ved hjælp af et systemkald bpf(2)og så, senere, via andre mekanismer, der varierer afhængigt af programtypen, knytter programmet til begivenhedskilden.
Her kan læseren have et spørgsmål: var det muligt? Hvordan er eksekveringssikkerheden af en sådan kode garanteret? Udførelsessikkerhed er garanteret for os ved indlæsning af BPF-programmer kaldet verifier (på engelsk kaldes denne fase verifier, og jeg vil fortsætte med at bruge det engelske ord):
Verifier er en statisk analysator, der sikrer, at et program ikke forstyrrer den normale drift af kernen. Dette betyder i øvrigt ikke, at programmet ikke kan forstyrre driften af systemet - BPF-programmer, afhængigt af typen, kan læse og omskrive sektioner af kernehukommelsen, returnere værdier af funktioner, trimme, tilføje, omskrive og endda videresende netværkspakker. Verifier garanterer, at kørsel af et BPF-program ikke vil crashe kernen, og at et program, der ifølge reglerne har skriveadgang, for eksempel data fra en udgående pakke, ikke vil være i stand til at overskrive kernehukommelsen uden for pakken. Vi vil se på verifikator lidt mere detaljeret i det tilsvarende afsnit, efter at vi har stiftet bekendtskab med alle de andre komponenter i BPF.
Så hvad har vi lært indtil videre? Brugeren skriver et program i C, indlæser det i kernen ved hjælp af et systemkald bpf(2), hvor det kontrolleres af en verifikator og oversættes til native bytecode. Derefter forbinder den samme eller en anden bruger programmet til begivenhedskilden, og det begynder at køre. Det er nødvendigt at adskille boot og forbindelse af flere årsager. For det første er det relativt dyrt at køre en verifikator, og ved at downloade det samme program flere gange spilder vi computertid. For det andet afhænger præcis hvordan et program er forbundet af dets type, og en "universel" grænseflade udviklet for et år siden er muligvis ikke egnet til nye typer programmer. (Selvom nu hvor arkitekturen bliver mere moden, er der en idé om at forene denne grænseflade på niveau libbpf.)
Den opmærksomme læser bemærker måske, at vi ikke er færdige med billederne endnu. Alt ovenstående forklarer faktisk ikke, hvorfor BPF fundamentalt ændrer billedet sammenlignet med klassisk BPF. To innovationer, der markant udvider anvendelsesområdet, er muligheden for at bruge delt hukommelse og kernehjælpefunktioner. I BPF implementeres delt hukommelse ved hjælp af såkaldte maps – delte datastrukturer med en specifik API. De har sandsynligvis fået dette navn, fordi den første type kort, der dukkede op, var en hash-tabel. Derefter dukkede arrays op, lokale (per-CPU) hash-tabeller og lokale arrays, søgetræer, kort indeholdende pointere til BPF-programmer og meget mere. Det, der er interessant for os nu, er, at BPF-programmer nu har evnen til at fortsætte tilstanden mellem opkald og dele den med andre programmer og med brugerplads.
Kort tilgås fra brugerprocesser ved hjælp af et systemkald bpf(2), og fra BPF-programmer, der kører i kernen ved hjælp af hjælpefunktioner. Desuden eksisterer hjælpere ikke kun for at arbejde med kort, men også for at få adgang til andre kernefunktioner. For eksempel kan BPF-programmer bruge hjælpefunktioner til at videresende pakker til andre grænseflader, generere perf-begivenheder, få adgang til kernestrukturer og så videre.
Sammenfattende giver BPF muligheden for at indlæse vilkårlig, dvs. verifikator-testet, brugerkode i kernerummet. Denne kode kan gemme tilstand mellem opkald og udveksle data med brugerplads og har også adgang til kerneundersystemer, der er tilladt af denne type program.
Dette ligner allerede de muligheder, som kernemoduler giver, sammenlignet med hvilke BPF har nogle fordele (selvfølgelig kan du kun sammenligne lignende applikationer, for eksempel systemsporing - du kan ikke skrive en vilkårlig driver med BPF). Du kan notere en lavere indgangstærskel (nogle værktøjer, der bruger BPF, kræver ikke, at brugeren har kerneprogrammeringsfærdigheder eller programmeringsfærdigheder generelt), runtime-sikkerhed (ræk hånden op i kommentarerne for dem, der ikke har brudt systemet, når de skriver eller testmoduler), atomicitet - der er nedetid ved genindlæsning af moduler, og BPF-undersystemet sikrer, at ingen hændelser går glip af (for at være retfærdig er dette ikke sandt for alle typer BPF-programmer).
Tilstedeværelsen af sådanne kapaciteter gør BPF til et universelt værktøj til at udvide kernen, hvilket bekræftes i praksis: flere og flere nye typer programmer føjes til BPF, flere og flere store virksomheder bruger BPF på kampservere 24×7, flere og flere startups bygger deres forretning på løsninger baseret på, som er baseret på BPF. BPF bruges overalt: til beskyttelse mod DDoS-angreb, oprettelse af SDN (for eksempel implementering af netværk til kubernetes), som det primære systemsporingsværktøj og statistikindsamler, i indtrængningsdetektionssystemer og sandkassesystemer osv.
Lad os afslutte oversigtsdelen af artiklen her og se på den virtuelle maskine og BPF-økosystemet mere detaljeret.
Digression: hjælpeprogrammer
For at kunne køre eksemplerne i de følgende sektioner kan du i det mindste have brug for en række hjælpeprogrammer llvm/clang med bpf support og bpftool. I afsnit Udviklingsværktøjer Du kan læse instruktionerne til montering af hjælpeprogrammerne såvel som din kerne. Dette afsnit er placeret nedenfor for ikke at forstyrre harmonien i vores præsentation.
BPF Virtual Machine registre og instruktionssystem
BPF's arkitektur og kommandosystem blev udviklet under hensyntagen til det faktum, at programmer vil blive skrevet på C-sproget og efter indlæsning i kernen oversat til indfødt kode. Derfor blev antallet af registre og rækken af kommandoer valgt med henblik på skæringspunktet, i matematisk forstand, mellem moderne maskiners muligheder. Derudover blev der pålagt forskellige begrænsninger på programmer, for eksempel var det indtil for nylig ikke muligt at skrive loops og subrutiner, og antallet af instruktioner var begrænset til 4096 (nu kan privilegerede programmer indlæse op til en million instruktioner).
BPF har elleve brugertilgængelige 64-bit registre r0—r10 og en programtæller. Tilmeld r10 indeholder en rammemarkør og er skrivebeskyttet. Programmer har adgang til en 512-byte stack ved kørsel og en ubegrænset mængde delt hukommelse i form af kort.
BPF-programmer har tilladelse til at køre et specifikt sæt af program-type kernehjælpere og for nylig regulære funktioner. Hver kaldet funktion kan tage op til fem argumenter, der sendes i registre r1—r5, og returværdien sendes til r0. Det er garanteret, at efter hjemkomst fra funktionen, indholdet af registrene r6—r9 Vil ikke ændre sig.
For effektiv programoversættelse, registre r0—r11 for alle understøttede arkitekturer er unikt kortlagt til rigtige registre under hensyntagen til ABI-funktionerne i den aktuelle arkitektur. For eksempel for x86_64 registre r1—r5, der bruges til at videregive funktionsparametre, vises på rdi, rsi, rdx, rcx, r8, som bruges til at videregive parametre til funktioner på x86_64. For eksempel oversættes koden til venstre til koden til højre sådan her:
Регистр r0 bruges også til at returnere resultatet af programafviklingen og i registret r1 programmet sendes en pegepind til konteksten - afhængigt af programtypen kan dette for eksempel være en struktur struct xdp_md (til XDP) eller struktur struct __sk_buff (til forskellige netværksprogrammer) eller struktur struct pt_regs (for forskellige typer sporingsprogrammer) osv.
Så vi havde et sæt registre, kernehjælpere, en stak, en kontekstmarkør og delt hukommelse i form af kort. Ikke at alt dette er absolut nødvendigt på turen, men...
Lad os fortsætte beskrivelsen og tale om kommandosystemet til at arbejde med disse objekter. Alle (Næsten alle) BPF-instruktioner har en fast 64-bit størrelse. Hvis du ser på en instruktion på en 64-bit Big Endian-maskine, vil du se
Her Code - dette er kodningen af instruktionen, Dst/Src er kodningerne af henholdsvis modtageren og kilden, Off - 16-bit signeret indrykning, og Imm er et 32-bit fortegnet heltal, der bruges i nogle instruktioner (svarende til cBPF-konstanten K). Indkodning Code har en af to typer:
Instruktionsklasserne 0, 1, 2, 3 definerer kommandoer til at arbejde med hukommelse. De hedder, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, henholdsvis. Klasse 4, 7 (BPF_ALU, BPF_ALU64) udgør et sæt ALU-instruktioner. Klasse 5, 6 (BPF_JMP, BPF_JMP32) indeholder springinstruktioner.
Den videre plan for at studere BPF-instruktionssystemet er som følger: i stedet for omhyggeligt at liste alle instruktionerne og deres parametre, vil vi se på et par eksempler i dette afsnit, og ud fra dem vil det blive klart, hvordan instruktionerne faktisk fungerer, og hvordan man adskille enhver binær fil til BPF manuelt. For at konsolidere materialet senere i artiklen, vil vi også mødes med individuelle instruktioner i afsnittene om Verifier, JIT compiler, oversættelse af klassisk BPF, samt når man studerer kort, kalder funktioner mv.
Når vi taler om individuelle instruktioner, vil vi henvise til kernefilerne bpf.h и bpf_common.h, som definerer de numeriske koder for BPF-instruktioner. Når du studerer arkitektur på egen hånd og/eller parser binære filer, kan du finde semantik i følgende kilder, sorteret efter kompleksitet: Uofficiel eBPF spec, BPF og XDP referencevejledning, instruktionssæt, Dokumentation/netværk/filter.txt og selvfølgelig i Linux-kildekoden - verifier, JIT, BPF-fortolker.
Eksempel: adskille BPF i dit hoved
Lad os se på et eksempel, hvor vi kompilerer et program readelf-example.c og se på den resulterende binære. Vi vil afsløre det originale indhold readelf-example.c nedenfor, efter at vi har gendannet dens logik fra binære koder:
Kommandokoder er ens b7, 15, b7 и 95. Husk, at de mindst signifikante tre bits er instruktionsklassen. I vores tilfælde er den fjerde bit af alle instruktioner tom, så instruktionsklasserne er henholdsvis 7, 5, 7, 5. Klasse 7 er BPF_ALU64, og 5 er BPF_JMP. For begge klasser er instruktionsformatet det samme (se ovenfor), og vi kan omskrive vores program sådan her (samtidigt vil vi omskrive de resterende kolonner 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
Operation b klasse ALU64 - Er BPF_MOV. Den tildeler en værdi til destinationsregistret. Hvis bit er sat s (kilde), så tages værdien fra kilderegisteret, og hvis den, som i vores tilfælde, ikke er sat, så tages værdien fra feltet Imm. Så i den første og tredje vejledning udfører vi operationen r0 = Imm. Ydermere er JMP klasse 1 operation BPF_JEQ (hop hvis lige). I vores tilfælde, siden lidt S er nul, sammenligner den værdien af kilderegisteret med feltet Imm. Hvis værdierne falder sammen, sker overgangen til PC + OffHvor PC, som sædvanlig, indeholder adressen på den næste instruktion. Endelig er JMP Class 9 Operation BPF_EXIT. Denne instruktion afslutter programmet og vender tilbage til kernen r0. Lad os tilføje en ny kolonne til vores tabel:
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 mere bekvem form:
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exit
Hvis vi husker, hvad der står i registret r1 programmet sendes en pointer til konteksten fra kernen og i registret r0 værdien returneres til kernen, så kan vi se, at hvis pointeren til konteksten er nul, så returnerer vi 1, og ellers - 2. Lad os tjekke, at vi har ret ved at se på kilden:
Ja, det er et meningsløst program, men det oversættes til kun fire enkle instruktioner.
Undtagelseseksempel: 16-byte instruktion
Vi nævnte tidligere, at nogle instruktioner fylder mere end 64 bit. Det gælder for eksempel instruktioner lddw (Kode = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — indlæs et dobbeltord fra felterne i registret Imm. Faktum er det Imm har en størrelse på 32, og et dobbeltord er 64 bit, så indlæsning af en 64-bit øjeblikkelig værdi i et register i en 64-bit instruktion vil ikke fungere. For at gøre dette bruges to tilstødende instruktioner til at gemme den anden del af 64-bit værdien i feltet Imm. Et eksempel:
Vi mødes igen med instruktioner lddw, når vi taler om flytninger og arbejde med kort.
Eksempel: adskillelse af BPF med standardværktøj
Så vi har lært at læse BPF binære koder og er klar til at parse enhver instruktion, hvis det er nødvendigt. Det er dog værd at sige, at det i praksis er mere bekvemt og hurtigere at adskille programmer ved hjælp af standardværktøjer, for eksempel:
(Jeg lærte først nogle af detaljerne beskrevet i dette underafsnit fra faste Alexei Starovoitov ind BPF blog.)
BPF-objekter - programmer og kort - oprettes fra brugerens plads ved hjælp af kommandoer BPF_PROG_LOAD и BPF_MAP_CREATE systemopkald bpf(2), vil vi tale om præcis, hvordan dette sker i næste afsnit. Dette skaber kernedatastrukturer og for hver af dem refcount (referenceantal) indstilles til én, og en filbeskrivelse, der peger på objektet, returneres til brugeren. Efter at håndtaget er lukket refcount objektet reduceres med én, og når det når nul, ødelægges objektet.
Hvis programmet bruger kort, så refcount disse kort øges med et efter indlæsning af programmet, dvs. deres filbeskrivelser kan lukkes fra brugerprocessen og stadig refcount bliver ikke nul:
Efter succesfuld indlæsning af et program, vedhæfter vi det normalt til en slags begivenhedsgenerator. For eksempel kan vi sætte det på en netværksgrænseflade for at behandle indgående pakker eller forbinde det til nogle tracepoint i kernen. På dette tidspunkt vil referencetælleren også stige med én, og vi vil være i stand til at lukke filbeskrivelsen i loader-programmet.
Hvad sker der, hvis vi nu lukker bootloaderen ned? Det afhænger af typen af hændelsesgenerator (krog). Alle netværkskroge vil eksistere efter at loaderen er færdig, disse er de såkaldte globale kroge. Og for eksempel vil sporingsprogrammer blive frigivet efter den proces, der skabte dem, afsluttes (og derfor kaldes lokale, fra "lokalt til processen"). Teknisk set har lokale hooks altid en tilsvarende filbeskrivelse i brugerrummet og lukker derfor, når processen er lukket, men det gør globale hooks ikke. I den følgende figur forsøger jeg ved hjælp af røde krydser at vise, hvordan afslutningen af loaderprogrammet påvirker objekternes levetid i tilfælde af lokale og globale kroge.
Hvorfor er der en sondring mellem lokale og globale hooks? At køre nogle typer netværksprogrammer giver mening uden et brugerområde, forestil dig for eksempel DDoS-beskyttelse - bootloaderen skriver reglerne og forbinder BPF-programmet til netværksgrænsefladen, hvorefter bootloaderen kan gå hen og slå sig selv ihjel. På den anden side, forestil dig et debugging-sporingsprogram, som du skrev på dine knæ på ti minutter - når det er færdigt, vil du gerne have, at der ikke er noget affald tilbage i systemet, og det vil lokale hooks sørge for.
På den anden side, forestil dig, at du vil oprette forbindelse til et sporingspunkt i kernen og indsamle statistik over mange år. I dette tilfælde vil du gerne afslutte brugerdelen og vende tilbage til statistikken fra tid til anden. Bpf-filsystemet giver denne mulighed. Det er et pseudo-filsystem, der kun er i hukommelsen, og som tillader oprettelse af filer, der refererer til BPF-objekter og derved øger refcount genstande. Herefter kan læsseren afslutte, og de genstande, den har skabt, forbliver i live.
Oprettelse af filer i bpffs, der refererer til BPF-objekter, kaldes "pinning" (som i følgende sætning: "processen kan fastgøre et BPF-program eller -kort"). Oprettelse af filobjekter til BPF-objekter giver mening ikke kun for at forlænge levetiden af lokale objekter, men også for brugbarheden af globale objekter - går vi tilbage til eksemplet med det globale DDoS-beskyttelsesprogram, vil vi gerne kunne komme og se på statistik. fra tid til anden.
BPF-filsystemet er normalt monteret i /sys/fs/bpf, men den kan også monteres lokalt, for eksempel sådan:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
Filsystemnavne oprettes ved hjælp af kommandoen BPF_OBJ_PIN BPF-systemkald. For at illustrere det, lad os tage et program, kompilere det, uploade det og fastgøre det til bpffs. Vores program gør ikke noget nyttigt, vi præsenterer kun koden, så du kan gengive eksemplet:
Lad os nu downloade vores program ved hjælp af værktøjet bpftool og se på de medfølgende systemkald bpf(2) (nogle irrelevante linjer fjernet fra strace-output):
Her har vi indlæst programmet vha BPF_PROG_LOAD, modtog en filbeskrivelse fra kernen 3 og bruge kommandoen BPF_OBJ_PIN fastgjort denne filbeskrivelse som en fil "bpf-mountpoint/test". Herefter bootloader-programmet bpftool færdig med at arbejde, men vores program forblev i kernen, selvom vi ikke vedhæftede det til nogen netværksgrænseflade:
$ 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 derefter vil det tilsvarende program blive slettet:
$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory
Sletning af objekter
Når vi taler om sletning af objekter, er det nødvendigt at præcisere, at efter at vi har afbrudt programmet fra krogen (hændelsesgeneratoren), vil ikke en eneste ny begivenhed udløse dets lancering, dog vil alle aktuelle forekomster af programmet blive afsluttet i normal rækkefølge .
Nogle typer af BPF-programmer giver dig mulighed for at erstatte programmet på farten, dvs. give sekvensatomicitet replace = detach old program, attach new program. I dette tilfælde vil alle aktive forekomster af den gamle version af programmet afslutte deres arbejde, og nye hændelseshandlere vil blive oprettet fra det nye program, og "atomicity" betyder her, at ikke en eneste hændelse vil blive savnet.
Vedhæftning af programmer til begivenhedskilder
I denne artikel vil vi ikke særskilt beskrive tilslutning af programmer til begivenhedskilder, da det giver mening at studere dette i sammenhæng med en bestemt type program. Cm. eksempel nedenfor, hvor vi viser, hvordan programmer som XDP er forbundet.
Manipulering af objekter ved hjælp af bpf-systemopkaldet
BPF programmer
Alle BPF-objekter oprettes og administreres fra brugerområdet ved hjælp af et systemkald bpf, der har følgende prototype:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
Her er holdet cmd er en af værdierne for type enum bpf_cmd, attr — en pegepind til parametre for et specifikt program og size — objektstørrelse ifølge markøren, dvs. normalt dette sizeof(*attr). I kerne 5.8 kaldes systemet bpf understøtter 34 forskellige kommandoer, og bestemmelse afunion bpf_attr fylder 200 linjer. Men vi skal ikke lade os skræmme af dette, da vi vil sætte os ind i kommandoerne og parametrene i løbet af adskillige artikler.
Lad os starte med holdet BPF_PROG_LOAD, som opretter BPF-programmer - tager et sæt BPF-instruktioner og indlæser det i kernen. På tidspunktet for indlæsning startes verifikatoren, og derefter returneres JIT-kompileren og, efter vellykket udførelse, programfilbeskrivelsen til brugeren. Vi så, hvad der sker med ham næste gang i det forrige afsnit om BPF-objekters livscyklus.
Vi vil nu skrive et brugerdefineret program, der vil indlæse et simpelt BPF-program, men først skal vi beslutte, hvilken slags program vi vil indlæse - vi skal vælge typen og inden for rammerne af denne type, skriv et program, der vil bestå verifikatortesten. Men for ikke at komplicere processen, er her en færdig løsning: vi tager et program som BPF_PROG_TYPE_XDP, som returnerer værdien XDP_PASS (spring alle pakker over). I BPF assembler ser det meget enkelt ud:
r0 = 2
exit
Efter vi har besluttet os for at vi uploader, vi kan fortælle dig, hvordan vi gør det:
Interessante begivenheder i et program begynder med definitionen af et array insns - vores BPF-program i maskinkode. I dette tilfælde er hver instruktion i BPF-programmet pakket ind i strukturen bpf_insn. Første element insns overholder instruktionerne r0 = 2, Sekundet - exit.
Trække sig tilbage. Kernen definerer mere bekvemme makroer til at skrive maskinkoder og bruge kernehovedfilen tools/include/linux/filter.h vi kunne skrive
Men da det kun er nødvendigt at skrive BPF-programmer i native kode for at skrive test i kernen og artikler om BPF, komplicerer fraværet af disse makroer ikke udviklerens liv.
Efter at have defineret BPF-programmet, går vi videre til at indlæse det i kernen. Vores minimalistiske sæt af parametre attr inkluderer programtype, sæt og antal instruktioner, påkrævet licens og navn "woo", som vi bruger til at finde vores program på systemet efter download. Programmet, som lovet, indlæses i systemet ved hjælp af et systemkald bpf.
I slutningen af programmet ender vi i en uendelig løkke, der simulerer nyttelasten. Uden det vil programmet blive dræbt af kernen, når filbeskrivelsen, som systemkaldet returnerede til os, lukkes bpf, og vi vil ikke se det i systemet.
Nå, vi er klar til at teste. Lad os samle og køre programmet under stracefor at kontrollere, at alt fungerer, som det skal:
Alt er fint, bpf(2) returnerede håndtag 3 til os, og vi gik ind i en uendelig løkke med pause(). Lad os prøve at finde vores program i systemet. For at gøre dette vil vi gå til en anden terminal og bruge værktøjet bpftool:
Vi ser, at der er et indlæst program på systemet woo hvis globale ID er 390 og er i gang i øjeblikket simple-prog der er en åben filbeskrivelse, der peger på programmet (og hvis simple-prog vil afslutte jobbet, så woo vil forsvinde). Som forventet, programmet woo tager 16 bytes - to instruktioner - af binære koder i BPF-arkitekturen, men i sin oprindelige form (x86_64) er det allerede 40 bytes. Lad os se på vores program i sin oprindelige form:
ingen overraskelser. Lad os nu se på koden genereret af JIT-kompileren:
# 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 til exit(2), men retfærdigvis er vores program for simpelt, og for ikke-trivielle programmer er prologen og epilogen tilføjet af JIT-kompileren selvfølgelig nødvendig.
Maps
BPF-programmer kan bruge strukturerede hukommelsesområder, der er tilgængelige både for andre BPF-programmer og for programmer i brugerrummet. Disse objekter kaldes kort, og i dette afsnit vil vi vise, hvordan man manipulerer dem ved hjælp af et systemkald bpf.
Lad os sige med det samme, at mulighederne for kort ikke kun er begrænset til adgang til delt hukommelse. Der findes specielle kort, der for eksempel indeholder pointere til BPF-programmer eller pointere til netværksgrænseflader, kort til at arbejde med perf-begivenheder mv. Vi vil ikke tale om dem her for ikke at forvirre læseren. Bortset fra dette ignorerer vi synkroniseringsproblemer, da dette ikke er vigtigt for vores eksempler. En komplet liste over tilgængelige korttyper kan findes i <linux/bpf.h>, og i dette afsnit vil vi som eksempel tage den historisk første type, hash-tabellen BPF_MAP_TYPE_HASH.
Hvis du opretter en hash-tabel i f.eks. C++, ville du sige unordered_map<int,long> woo, som på russisk betyder "Jeg har brug for et bord woo ubegrænset størrelse, hvis nøgler er af typen int, og værdierne er typen long" For at oprette en BPF-hash-tabel skal vi gøre stort set det samme, bortset fra at vi skal angive den maksimale størrelse af tabellen, og i stedet for at specificere typer af nøgler og værdier, skal vi angive deres størrelser i bytes . Brug kommandoen til at oprette kort BPF_MAP_CREATE systemopkald bpf. Lad os se på et mere eller mindre minimalt program, der skaber et kort. Efter det forrige program, der indlæser BPF-programmer, skulle dette virke simpelt for dig:
Her definerer vi et sæt parametre attr, hvori vi siger "Jeg har brug for en hash-tabel med nøgler og størrelsesværdier sizeof(int), hvori jeg maksimalt kan sætte fire elementer." Når du opretter BPF-kort, kan du angive andre parametre, for eksempel på samme måde som i eksemplet med programmet, vi specificerede navnet på objektet som "woo".
Her er systemkaldet bpf(2) returnerede os beskrivelseskortnummeret 3 og så venter programmet som forventet på yderligere instruktioner i systemkaldet pause(2).
Lad os nu sende vores program til baggrunden eller åbne en anden terminal og se på vores objekt ved hjælp af hjælpeprogrammet bpftool (vi kan skelne vores kort fra andre ved dets navn):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...
Tallet 114 er det globale ID for vores objekt. Ethvert program på systemet kan bruge dette ID til at åbne et eksisterende kort ved hjælp af kommandoen BPF_MAP_GET_FD_BY_ID systemopkald bpf.
Nu kan vi lege med vores hash-tabel. Lad os se på indholdet:
$ sudo bpftool map dump id 114
Found 0 elements
Tom. Lad os sætte en værdi i det hash[1] = 1:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0
Lad os se på tabellen igen:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 element
Hurra! Det lykkedes os at tilføje et element. Bemærk, at vi skal arbejde på byte-niveau for at gøre dette, siden bptftool ved ikke, hvilken type værdierne i hash-tabellen er. (Denne viden kan overføres til hende ved hjælp af BTF, men mere om det nu.)
Hvordan præcis læser og tilføjer bpftool elementer? Lad os tage et kig under motorhjelmen:
Først åbnede vi kortet ved dets globale ID ved hjælp af kommandoen BPF_MAP_GET_FD_BY_ID и bpf(2) returnerede deskriptor 3 til os. Yderligere brug af kommandoen BPF_MAP_GET_NEXT_KEY vi fandt den første nøgle i tabellen ved at passere NULL som en pegepind til "forrige"-tasten. Hvis vi har nøglen, kan vi gøre det BPF_MAP_LOOKUP_ELEMsom returnerer en værdi til en pointer value. Det næste trin er, at vi forsøger at finde det næste element ved at sende en markør til den aktuelle nøgle, men vores tabel indeholder kun et element og kommandoen BPF_MAP_GET_NEXT_KEY vender tilbage ENOENT.
Okay, lad os ændre værdien med tast 1, lad os sige, at vores forretningslogik kræver registrering hash[1] = 2:
Som forventet er det meget enkelt: kommandoen BPF_MAP_GET_FD_BY_ID åbner vores kort efter ID, og kommandoen BPF_MAP_UPDATE_ELEM overskriver elementet.
Så efter at have oprettet en hash-tabel fra et program, kan vi læse og skrive dets indhold fra et andet. Bemærk, at hvis vi var i stand til at gøre dette fra kommandolinjen, så kan ethvert andet program på systemet gøre det. Ud over de ovenfor beskrevne kommandoer, til arbejde med kort fra brugerrummet, Følgende:
BPF_MAP_LOOKUP_ELEM: find værdi ved nøgle
BPF_MAP_UPDATE_ELEM: opdater/skab værdi
BPF_MAP_DELETE_ELEM: fjern nøglen
BPF_MAP_GET_NEXT_KEY: find den næste (eller første) tast
BPF_MAP_GET_NEXT_ID: giver dig mulighed for at gennemgå alle eksisterende kort, det er sådan det virker bpftool map
BPF_MAP_GET_FD_BY_ID: Åbn et eksisterende kort ved dets globale ID
BPF_MAP_LOOKUP_AND_DELETE_ELEM: Opdater atomisk værdien af et objekt og returner det gamle
BPF_MAP_FREEZE: gør kortet uforanderligt fra brugerområdet (denne handling kan ikke fortrydes)
BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: masseoperationer. For eksempel, BPF_MAP_LOOKUP_AND_DELETE_BATCH - dette er den eneste pålidelige måde at læse og nulstille alle værdier fra kortet
Ikke alle disse kommandoer virker for alle korttyper, men generelt ser arbejdet med andre typer kort fra brugerrummet nøjagtigt ud som at arbejde med hash-tabeller.
Lad os for ordens skyld afslutte vores hash-tabeleksperimenter. Kan du huske, at vi har lavet en tabel, der kan indeholde op til fire nøgler? Lad os tilføje nogle 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 lykkedes det ikke. Lad os se på fejlen mere detaljeret:
$ 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 fint: som forventet, holdet BPF_MAP_UPDATE_ELEM forsøger at oprette en ny, femte nøgle, men går ned E2BIG.
Så vi kan oprette og indlæse BPF-programmer samt oprette og administrere kort fra brugerrummet. Nu er det logisk at se på, hvordan vi kan bruge kort fra selve BPF-programmerne. Det kunne vi tale om på sproget med svært læselige programmer i maskinmakrokoder, men faktisk er tiden kommet til at vise, hvordan BPF-programmer faktisk skrives og vedligeholdes – vha. libbpf.
(For læsere, der er utilfredse med manglen på et eksempel på lavt niveau: vi vil analysere i detaljer programmer, der bruger kort og hjælpefunktioner oprettet vha. libbpf og fortælle dig, hvad der sker på instruktionsniveau. For læsere, der er utilfredse rigtig meget, tilføjede vi eksempel på det rigtige sted i artiklen.)
Skrivning af BPF-programmer ved hjælp af libbpf
At skrive BPF-programmer ved hjælp af maskinkoder kan kun være interessant første gang, og derefter sætter mætheden ind. I dette øjeblik skal du vende din opmærksomhed mod llvm, som har en backend til generering af kode til BPF-arkitekturen, samt et bibliotek libbpf, som giver dig mulighed for at skrive brugersiden af BPF-applikationer og indlæse koden for BPF-programmer, der er genereret vha. llvm/clang.
Faktisk, som vi vil se i denne og efterfølgende artikler, libbpf gør ret meget arbejde uden det (eller lignende værktøjer - iproute2, libbcc, libbpf-goosv.) er det umuligt at leve. En af de dræbende træk ved projektet libbpf er BPF CO-RE (Compile Once, Run Everywhere) - et projekt, der giver dig mulighed for at skrive BPF-programmer, der er bærbare fra en kerne til en anden, med mulighed for at køre på forskellige API'er (f.eks. når kernestrukturen ændres fra version til version). For at kunne arbejde med CO-RE skal din kerne være kompileret med BTF-understøttelse (vi beskriver hvordan du gør dette i afsnittet Udviklingsværktøjer. Du kan kontrollere, om din kerne er bygget med BTF eller ikke meget enkelt - ved tilstedeværelsen af følgende fil:
Denne fil gemmer information om alle datatyper, der bruges i kernen og bruges i alle vores eksempler på brug libbpf. Vi vil tale detaljeret om CO-RE i den næste artikel, men i denne – byg dig bare en kerne med CONFIG_DEBUG_INFO_BTF.
bibliotek libbpf bor lige i mappen tools/lib/bpf kernel og dens udvikling udføres via mailinglisten [email protected]. Der opretholdes dog et separat lager til behovene hos applikationer, der lever uden for kernen https://github.com/libbpf/libbpf hvori kernebiblioteket spejles for læseadgang mere eller mindre som det er.
I dette afsnit vil vi se på, hvordan du kan oprette et projekt, der bruger libbpf, lad os skrive flere (mere eller mindre meningsløse) testprogrammer og analysere i detaljer, hvordan det hele fungerer. Dette vil give os mulighed for lettere at forklare i de følgende afsnit præcis, hvordan BPF-programmer interagerer med kort, kernehjælpere, BTF osv.
Typisk projekter vha libbpf tilføje et GitHub-lager som et git-undermodul, så gør vi det samme:
Vores næste plan i dette afsnit er som følger: vi vil skrive et BPF-program som BPF_PROG_TYPE_XDP, det samme som i det foregående eksempel, men i C kompilerer vi det ved hjælp af clang, og skriv et hjælpeprogram, der vil indlæse det i kernen. I de følgende afsnit vil vi udvide mulighederne for både BPF-programmet og assistentprogrammet.
Eksempel: oprettelse af en fuldgyldig applikation ved hjælp af libbpf
Til at begynde med bruger vi filen /sys/kernel/btf/vmlinux, som blev nævnt ovenfor, og opret dets ækvivalent i form af en header-fil:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Denne fil vil gemme alle de datastrukturer, der er tilgængelige i vores kerne, for eksempel er det sådan, IPv4-headeren er defineret i kernen:
Selvom vores program viste sig at være meget enkelt, skal vi stadig være opmærksomme på mange detaljer. For det første er den første header-fil, vi inkluderer vmlinux.h, som vi lige har genereret ved hjælp af bpftool btf dump - nu behøver vi ikke installere kernel-headers-pakken for at finde ud af, hvordan kernestrukturerne ser ud. Følgende header-fil kommer til os fra biblioteket libbpf. Nu mangler vi det kun til at definere makroen SEC, som sender tegnet til den relevante sektion af ELF-objektfilen. Vores program er indeholdt i afsnittet xdp/simple, hvor vi før skråstreg definerer programtypen BPF - det er den konvention, der bruges i libbpf, baseret på sektionsnavnet vil den erstatte den korrekte type ved opstart bpf(2). Selve BPF-programmet er C - meget enkel og består af én linje return XDP_PASS. Til sidst et særskilt afsnit "license" indeholder navnet på licensen.
Vi kan kompilere vores program ved hjælp af llvm/clang, version >= 10.0.0, eller endnu bedre, større (se afsnittet Udviklingsværktøjer):
Blandt de interessante funktioner: vi angiver målarkitekturen -target bpf og stien til overskrifterne libbpf, som vi for nylig installerede. Glem heller ikke -O2, uden denne mulighed kan du få overraskelser i fremtiden. Lad os se på vores kode, lykkedes det os at skrive det program, vi ønskede?
Ja, det virkede! Nu har vi en binær fil med programmet, og vi vil oprette et program, der indlæser det i kernen. Til dette formål biblioteket libbpf tilbyder os to muligheder - brug en API på lavere niveau eller en API på højere niveau. Vi vil gå den anden vej, da vi ønsker at lære at skrive, indlæse og forbinde BPF-programmer med minimal indsats til deres efterfølgende undersøgelse.
Først skal vi generere "skelettet" af vores program fra dets binære ved hjælp af det samme værktøj bpftool — den schweiziske kniv i BPF-verdenen (som kan tages bogstaveligt, da Daniel Borkman, en af skaberne og vedligeholderne af BPF, er schweizisk):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
I fil xdp-simple.skel.h indeholder den binære kode for vores program og funktioner til styring - indlæsning, vedhæftning, sletning af vores objekt. I vores simple tilfælde ligner dette overkill, men det virker også i det tilfælde, hvor objektfilen indeholder mange BPF-programmer og kort, og for at indlæse denne gigantiske ELF skal vi blot generere skelettet og kalde en eller to funktioner fra den brugerdefinerede applikation, vi skriver Lad os komme videre nu.
Strengt taget er vores loader-program 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 defineret i filen xdp-simple.skel.h og beskriver vores objektfil:
Vi kan se spor af en lav-niveau API her: strukturen struct bpf_program *simple и struct bpf_link *simple. Den første struktur beskriver specifikt vores program, skrevet i afsnittet xdp/simple, og den anden beskriver, hvordan programmet forbinder til begivenhedskilden.
Funktion xdp_simple_bpf__open_and_load, åbner et ELF-objekt, analyserer det, opretter alle strukturer og understrukturer (udover programmet indeholder ELF også andre sektioner - data, skrivebeskyttede data, fejlfindingsoplysninger, licens osv.), og indlæser det derefter i kernen ved hjælp af et system opkald bpf, som vi kan kontrollere ved at kompilere og køre programmet:
Lad os nu se på vores program ved hjælp af bpftool. Lad os finde hendes ID:
# 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 bruger en forkortet form af 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
Noget nyt! Programmet udskrev bidder af vores C-kildefil. Dette blev udført af biblioteket libbpf, som fandt fejlretningssektionen i binæren, kompilerede den til et BTF-objekt, indlæste den i kernen vha. BPF_BTF_LOAD, og specificerede derefter den resulterende filbeskrivelse, når programmet indlæses med kommandoen BPG_PROG_LOAD.
Kernelhjælpere
BPF-programmer kan køre "eksterne" funktioner - kernehjælpere. Disse hjælpefunktioner gør det muligt for BPF-programmer at få adgang til kernestrukturer, administrere kort og også kommunikere med den "virkelige verden" - oprette perf-begivenheder, styre hardware (for eksempel omdirigere pakker) osv.
Eksempel: bpf_get_smp_processor_id
Inden for rammerne af "læring ved eksempel"-paradigmet, lad os overveje en af hjælperfunktionerne, bpf_get_smp_processor_id(), bestemte i fil kernel/bpf/helpers.c. Det returnerer nummeret på den processor, som det BPF-program, der kaldte det, kører på. Men vi er ikke så interesserede i dens semantik som i det faktum, at dens implementering tager én linje:
BPF-hjælpefunktionsdefinitionerne ligner Linux-systemopkaldsdefinitionerne. Her defineres for eksempel en funktion, der ikke har nogen argumenter. (En funktion, der f.eks. tager tre argumenter, er defineret ved hjælp af makroen BPF_CALL_3. Det maksimale antal argumenter er fem.) Dette er dog kun den første del af definitionen. Den anden del er at definere typestrukturen struct bpf_func_proto, som indeholder en beskrivelse af hjælpefunktionen, som verifikator forstår:
For at BPF-programmer af en bestemt type kan bruge denne funktion, skal de registrere den, for eksempel for typen BPF_PROG_TYPE_XDP en funktion er defineret i kernen xdp_func_proto, som bestemmer ud fra hjælpefunktions-id'et, om XDP understøtter denne funktion eller ej. Vores funktion er bakker op:
Nye BPF-programtyper "defineres" i filen include/linux/bpf_types.h ved hjælp af en makro BPF_PROG_TYPE. Defineret i anførselstegn, fordi det er en logisk definition, og i C-sprogede termer forekommer definitionen af et helt sæt af konkrete strukturer andre steder. Især i filen kernel/bpf/verifier.c alle definitioner fra filen bpf_types.h bruges til at skabe en række strukturer bpf_verifier_ops[]:
Det vil sige, at for hver type BPF-program er der defineret en pegepind til en datastruktur af typen struct bpf_verifier_ops, som initialiseres med værdien _name ## _verifier_ops, dvs. xdp_verifier_ops for xdp. Struktur xdp_verifier_opsbestemt af i fil net/core/filter.c som følger:
Her ser vi vores velkendte funktion xdp_func_proto, som vil køre verifikatoren, hver gang den støder på en udfordring nogle funktioner i et BPF-program, se verifier.c.
Lad os se på, hvordan et hypotetisk BPF-program bruger funktionen bpf_get_smp_processor_id. For at gøre dette omskriver vi programmet fra vores forrige afsnit som følger:
det er, bpf_get_smp_processor_id er en funktionsmarkør, hvis værdi er 8, hvor 8 er værdien BPF_FUNC_get_smp_processor_id typen enum bpf_fun_id, som er defineret for os i filen vmlinux.h (fil bpf_helper_defs.h i kernen genereres af et script, så de "magiske" tal er ok). Denne funktion tager ingen argumenter og returnerer en værdi af typen __u32. Når vi kører det i vores program, clang genererer en instruktion BPF_CALL "den rigtige slags" Lad os kompilere programmet og se på afsnittet xdp/simple:
I første linje ser vi instruktioner call, parameter IMM som er lig med 8, og SRC_REG - nul. Ifølge den ABI-aftale, som verifikator bruger, er dette et opkald til hjælperfunktion nummer otte. Når først det er lanceret, er logikken enkel. Returværdi fra register r0 kopieret til r1 og på linje 2,3 er det konverteret til type u32 — de øverste 32 bit slettes. På linje 4,5,6,7 returnerer vi 2 (XDP_PASS) eller 1 (XDP_DROP) afhængig af om hjælpefunktionen fra linje 0 returnerede en nul- eller ikke-nul værdi.
Lad os teste os selv: Indlæs programmet og se på outputtet 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, verifikator fandt den korrekte kernehjælper.
Eksempel: sende argumenter og til sidst køre programmet!
Alle hjælpefunktioner på køreniveau har en prototype
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
Parametre til hjælpefunktioner sendes i registre r1—r5, og værdien returneres i registret r0. Der er ingen funktioner, der tager mere end fem argumenter, og understøttelse af dem forventes ikke at blive tilføjet i fremtiden.
Lad os tage et kig på den nye kernehjælper, og hvordan BPF sender parametre. Lad os omskrive xdp-simple.bpf.c som følger (resten af linjerne er ikke ændret):
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}
Vores program udskriver nummeret på den CPU, den kører på. Lad os kompilere det og se på koden:
I linje 0-7 skriver vi strengen running on CPU%un, og så på linje 8 kører vi den velkendte bpf_get_smp_processor_id. På linje 9-12 forbereder vi hjælperargumenterne bpf_printk - registre r1, r2, r3. Hvorfor er der tre af dem og ikke to? Fordi bpf_printk — dette er en makro-indpakning omkring den rigtige hjælper bpf_trace_printk, som skal passere størrelsen på formatstrengen.
Lad os nu tilføje et par linjer til xdp-simple.cså vores program forbinder til grænsefladen lo og virkelig startet!
Her bruger vi funktionen bpf_set_link_xdp_fd, som forbinder XDP-type BPF-programmer til netværksgrænseflader. Vi har hårdkodet grænsefladenummeret lo, som altid er 1. Vi kører funktionen to gange for først at frakoble det gamle program, hvis det var tilknyttet. Bemærk, at nu har vi ikke brug for en udfordring pause eller en uendelig løkke: vores loader-program afsluttes, men BPF-programmet vil ikke blive dræbt, da det er forbundet til begivenhedskilden. Efter vellykket download og forbindelse, vil programmet blive lanceret for hver netværkspakke, der ankommer til lo.
Lad os downloade programmet og se på grænsefladen 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 downloadede har ID 669 og vi ser det samme ID på interfacet lo. Vi sender et par pakker til 127.0.0.1 (anmodning + svar):
$ ping -c1 localhost
og lad os nu se på indholdet af den virtuelle fejlfindingsfil /sys/kernel/debug/tracing/trace_pipe, hvori bpf_printk skriver sine beskeder:
# 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 blev set på lo og behandlet på CPU0 - vores første fuldgyldige meningsløse BPF-program virkede!
Det er værd at bemærke, at bpf_printk Det er ikke for ingenting, at det skriver til fejlfindingsfilen: dette er ikke den mest succesfulde hjælper til brug i produktionen, men vores mål var at vise noget simpelt.
Adgang til kort fra BPF-programmer
Eksempel: brug af et kort fra BPF-programmet
I de foregående afsnit lærte vi, hvordan man opretter og bruger kort fra brugerrummet, og lad os nu se på kernedelen. Lad os som sædvanlig starte med et eksempel. Lad os omskrive vores program xdp-simple.bpf.c som følger:
I starten af programmet tilføjede vi en kortdefinition woo: Dette er et 8-element array, der gemmer værdier som u64 (i C ville vi definere et sådant array som u64 woo[8]). I et program "xdp/simple" vi får det aktuelle processornummer ind i en variabel key og derefter bruge hjælpefunktionen bpf_map_lookup_element vi får en pointer til den tilsvarende indgang i arrayet, som vi øger med én. Oversat til russisk: vi beregner statistik over, hvilken CPU der behandlede indgående pakker. Lad os prøve at køre programmet:
Lad os tjekke, at hun er tilsluttet lo og send nogle 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
Næsten alle processer blev behandlet på CPU7. Dette er ikke vigtigt for os, det vigtigste er, at programmet fungerer, og vi forstår, hvordan man får adgang til kort fra BPF-programmer - vha. хелперов bpf_mp_*.
Mystisk indeks
Så vi kan få adgang til kortet fra BPF-programmet ved hjælp af opkald 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 indlæste program, ser vi en pegepind til det korrekte kort (linje 4):
Således kan vi konkludere, at på tidspunktet for lanceringen af vores loader-program, linket til &woo blev erstattet af noget med et bibliotek libbpf. Først ser vi på outputtet strace:
Det ser vi libbpf oprettet et kort woo og downloadede derefter vores program simple. Lad os se nærmere på, hvordan vi indlæser programmet:
opkald xdp_simple_bpf__open_and_load fra fil xdp-simple.skel.h
som forårsager xdp_simple_bpf__load fra fil xdp-simple.skel.h
som forårsager bpf_object__load_skeleton fra fil libbpf/src/libbpf.c
som forårsager bpf_object__load_xattr af libbpf/src/libbpf.c
Den sidste funktion vil blandt andet kalde bpf_object__create_maps, som opretter eller åbner eksisterende kort og gør dem til filbeskrivelser. (Det er her vi ser BPF_MAP_CREATE i outputtet strace.) Dernæst kaldes funktionen bpf_object__relocate og det er hende, der interesserer os, siden vi husker, hvad vi så woo i flyttetabellen. Udforsker vi det, finder vi til sidst os selv i funktionen bpf_program__relocate, hvilken omhandler kortflytninger:
case RELO_LD64:
insn[0].src_reg = BPF_PSEUDO_MAP_FD;
insn[0].imm = obj->maps[relo->map_idx].fd;
break;
og erstatte kilderegistret i det med BPF_PSEUDO_MAP_FD, og den første IMM til filbeskrivelsen af vores kort, og hvis den er lig med f.eks. 0xdeadbeef, så vil vi modtage instruktionen
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll
Sådan overføres kortinformation til et specifikt indlæst BPF-program. I dette tilfælde kan kortet oprettes vha BPF_MAP_CREATE, og åbnes med ID vha BPF_MAP_GET_FD_BY_ID.
I alt, ved brug libbpf Algoritmen er som følger:
under kompileringen oprettes poster i flytningstabellen for links til kort
libbpf åbner ELF-objektbogen, finder alle brugte kort og opretter filbeskrivelser til dem
filbeskrivelser indlæses i kernen som en del af instruktionen LD64
Som du kan forestille dig, er der mere på vej, og vi bliver nødt til at se ind i kernen. Heldigvis har vi en anelse – vi har skrevet meningen ned BPF_PSEUDO_MAP_FD ind i kilderegistret, og vi kan begrave det, hvilket vil føre os til alle helliges hellige - kernel/bpf/verifier.c, hvor en funktion med et karakteristisk navn erstatter en filbeskrivelse med adressen på en typestruktur struct bpf_map:
(Fuld kode kan findes по ссылке). Så vi kan udvide vores algoritme:
under indlæsning af programmet, kontrollerer verifikator den korrekte brug af kortet og skriver adressen på den tilsvarende struktur struct bpf_map
Når du downloader ELF binær vha libbpf Der sker meget mere, men det vil vi diskutere i andre artikler.
Indlæser programmer og kort uden libbpf
Som lovet er her et eksempel til læsere, der vil vide, hvordan man opretter og indlæser et program, der bruger kort, uden hjælp libbpf. Dette kan være nyttigt, når du arbejder i et miljø, som du ikke kan opbygge afhængigheder til, eller gemme hver bit eller skrive et program som f.eks. ply, som genererer BPF binær kode i farten.
For at gøre det lettere at følge logikken, vil vi omskrive vores eksempel til disse formål xdp-simple. Den komplette og lidt udvidede kode for programmet, der er beskrevet i dette eksempel, kan findes i dette GIST.
Logikken i vores ansøgning er som følger:
oprette et typekort BPF_MAP_TYPE_ARRAY ved hjælp af kommandoen BPF_MAP_CREATE,
oprette et program, der bruger dette kort,
tilslut programmet til grænsefladen lo,
som oversættes til menneske 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 opretter et kort på samme måde, som vi gjorde i det første eksempel om systemkaldet bpf - "kernel, lav venligst et nyt kort til mig i form af en række af 8 elementer som __u64 og giv mig filbeskrivelsen tilbage":
Den vanskelige del prog_load er definitionen af vores BPF-program som en række strukturer struct bpf_insn insns[]. Men da vi bruger et program, som vi har i C, kan vi snyde lidt:
I alt skal vi skrive 14 instruktioner i form af strukturer som f.eks struct bpf_insn (råd: tag lossepladsen fra oven, læs vejledningssektionen igen, åbn linux/bpf.h и linux/bpf_common.h og prøv at bestemme struct bpf_insn insns[] på egen hånd):
En øvelse til dem der ikke selv har skrevet dette – find map_fd.
Der er endnu en uoplyst del tilbage i vores program - xdp_attach. Desværre kan programmer som XDP ikke tilsluttes ved hjælp af et systemkald bpf. De mennesker, der skabte BPF og XDP, var fra online Linux-fællesskabet, hvilket betyder, at de brugte den, de mest kendte (men ikke til at normal people) interface til at interagere med kernen: netlink-stik, se også RFC3549. Den nemmeste måde at implementere xdp_attach kopierer kode fra libbpf, nemlig fra filen netlink.c, hvilket er, hvad vi gjorde, og forkortede det lidt:
Velkommen til en verden af netlink sockets
Åbn en netlink socket type 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 læser fra denne fatning:
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;
}
Endelig er her vores funktion, der åbner en socket og sender en speciel besked til den, der indeholder en filbeskrivelse:
Lad os se, om vores program har tilsluttet sig 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
Hurra, alt virker. Bemærk i øvrigt, at vores kort igen vises i form af bytes. Dette skyldes, at i modsætning til libbpf vi indlæste ikke typeoplysninger (BTF). Men det taler vi mere om næste gang.
Udviklingsværktøjer
I dette afsnit vil vi se på minimum BPF-udviklerværktøjssættet.
Generelt behøver du ikke noget særligt for at udvikle BPF-programmer - BPF kører på enhver anstændig distributionskerne, og programmer er bygget vha. clang, som kan leveres fra pakken. Men på grund af det faktum, at BPF er under udvikling, ændrer kernen og værktøjer sig konstant, hvis du ikke vil skrive BPF-programmer ved hjælp af gammeldags metoder fra 2019, så bliver du nødt til at kompilere
llvm/clang
pahole
dens kerne
bpftool
(Til reference blev dette afsnit og alle eksempler i artiklen kørt på Debian 10.)
llvm/clang
BPF er venlig med LLVM, og selvom programmer for BPF for nylig kan kompileres ved hjælp af gcc, udføres al nuværende udvikling for LLVM. Derfor vil vi først og fremmest bygge den nuværende version 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
... много времени спустя
$
(Samlingsvejledning clang taget af mig fra bpf_devel_QA.)
Vi vil ikke installere de programmer, vi lige har bygget, men i stedet føje dem til PATH, for eksempel:
export PATH="`pwd`/bin:$PATH"
(Dette kan tilføjes .bashrc eller til en separat fil. Personligt tilføjer jeg ting som dette ~/bin/activate-llvm.sh og når det er nødvendigt gør jeg det . activate-llvm.sh.)
Pahole og BTF
Hjælpeprogram pahole bruges ved opbygning af kernen til at skabe fejlfindingsinformation i BTF-format. Vi vil ikke gå i detaljer i denne artikel om detaljerne i BTF-teknologi, bortset fra det faktum, at det er praktisk, og vi ønsker at bruge det. Så hvis du skal bygge din kerne, så byg først pahole (uden pahole du vil ikke være i stand til at bygge kernen med muligheden 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
Kerner til at eksperimentere med BPF
Når jeg udforsker mulighederne for BPF, vil jeg samle min egen kerne. Dette er generelt set ikke nødvendigt, da du vil være i stand til at kompilere og indlæse BPF-programmer på distributionskernen, men at have din egen kerne giver dig mulighed for at bruge de nyeste BPF-funktioner, som i bedste fald vil dukke op i din distribution om måneder , eller, som i tilfældet med nogle fejlfindingsværktøjer, vil slet ikke blive pakket i en overskuelig fremtid. Dens egen kerne gør det også vigtigt at eksperimentere med koden.
For at bygge en kerne skal du for det første selve kernen og for det andet en kernekonfigurationsfil. For at eksperimentere med BPF kan vi bruge det sædvanlige vanilje kerne eller en af udviklingskernerne. Historisk set finder BPF-udvikling sted inden for Linux-netværksfællesskabet, og derfor går alle ændringer før eller siden gennem David Miller, Linux-netværksvedligeholderen. Afhængigt af deres karakter - redigeringer eller nye funktioner - falder netværksændringer ind i en af to kerner - net eller net-next. Ændringer for BPF fordeles på samme måde mellem bpf и bpf-next, som derefter samles i henholdsvis net og net-next. For flere detaljer, se bpf_devel_QA и netdev-Ofte stillede spørgsmål. Så vælg en kerne baseret på din smag og stabilitetsbehovet for det system, du tester på (*-next kerner er de mest ustabile af de angivne).
Det er uden for rammerne af denne artikel at tale om, hvordan man administrerer kernekonfigurationsfiler - det antages, at du enten allerede ved, hvordan du gør dette, eller klar til at lære på egen hånd. De følgende instruktioner skulle dog være mere eller mindre nok til at give dig et fungerende BPF-aktiveret system.
Download en af ovenstående kerner:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next
Byg en minimal fungerende kernekonfiguration:
$ cp /boot/config-`uname -r` .config
$ make localmodconfig
Aktiver BPF-indstillinger i filen .config efter eget valg (sandsynligvis CONFIG_BPF vil allerede være aktiveret, da systemd bruger det). Her er en liste over muligheder fra kernen brugt til denne artikel:
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
Så kan vi nemt samle og installere modulerne og kernen (du kan i øvrigt samle kernen ved at bruge den nysamlede clangved at tilføje CC=clang):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install
og genstart med den nye kerne (jeg bruger til 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 brugte værktøj i artiklen vil være værktøjet bpftool, leveret som en del af Linux-kernen. Det er skrevet og vedligeholdt af BPF-udviklere til BPF-udviklere og kan bruges til at styre alle typer BPF-objekter - indlæse programmer, oprette og redigere kort, udforske livet i BPF-økosystemet osv. Dokumentation i form af kildekoder til man-sider kan findes i kernen eller allerede kompileret, netværk.
I skrivende stund bpftool leveres kun færdiglavet til RHEL, Fedora og Ubuntu (se f.eks. denne tråd, som fortæller den ufærdige historie om emballage bpftool i Debian). Men hvis du allerede har bygget din kerne, så byg bpftool lige så let som en plet:
$ 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 din kernemappe.) Efter at have udført disse kommandoer bpftool vil blive samlet i en mappe ${linux}/tools/bpf/bpftool og det kan føjes til stien (først og fremmest til brugeren root) eller bare kopier til /usr/local/sbin.
Indsamle bpftool det er bedst at bruge sidstnævnte clang, samlet som beskrevet ovenfor, og tjek om det er samlet korrekt - ved hjælp af f.eks. 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-funktioner der er aktiveret i din kerne.
Forresten kan den forrige kommando køres som
# bpftool f p k
Dette gøres analogt med hjælpeprogrammerne fra pakken iproute2, hvor vi f.eks. kan sige ip a s eth0 i stedet for ip addr show dev eth0.
Konklusion
BPF giver dig mulighed for at sko en loppe for effektivt at måle og ændre kernens funktionalitet undervejs. Systemet viste sig at være meget vellykket, i de bedste UNIX-traditioner: en simpel mekanisme, der giver dig mulighed for at (om)programmere kernen, tillod et stort antal mennesker og organisationer at eksperimentere. Og selvom eksperimenterne, såvel som udviklingen af selve BPF-infrastrukturen, langt fra er færdige, har systemet allerede en stabil ABI, der giver dig mulighed for at opbygge pålidelig og vigtigst af alt, effektiv forretningslogik.
Jeg vil gerne bemærke, at teknologien efter min mening er blevet så populær, fordi den på den ene side kan spille (arkitekturen af en maskine kan forstås mere eller mindre på en aften), og på den anden side at løse problemer, der ikke kunne løses (smukt) før dens fremkomst. Disse to komponenter tvinger tilsammen folk til at eksperimentere og drømme, hvilket fører til fremkomsten af flere og flere innovative løsninger.
Denne artikel, selvom den ikke er særlig kort, er kun en introduktion til BPF's verden og beskriver ikke "avancerede" funktioner og vigtige dele af arkitekturen. Planen fremadrettet er sådan her: den næste artikel vil være en oversigt over BPF-programtyper (der er 5.8 programtyper understøttet i 30-kernen), så skal vi endelig se på, hvordan man skriver rigtige BPF-applikationer ved hjælp af kernesporingsprogrammer som et eksempel, så er det tid til et mere dybdegående kursus om BPF-arkitektur, efterfulgt af eksempler på BPF-netværk og sikkerhedsapplikationer.
BPF og XDP referencevejledning — dokumentation om BPF fra cilium, eller mere præcist fra Daniel Borkman, en af skaberne og vedligeholderne af BPF. Dette er en af de første seriøse beskrivelser, som adskiller sig fra de andre ved, at Daniel ved præcis, hvad han skriver om, og der er ingen fejl der. Dette dokument beskriver især, hvordan man arbejder med BPF-programmer af XDP- og TC-typerne ved hjælp af det velkendte hjælpeprogram ip fra pakken iproute2.
Dokumentation/netværk/filter.txt — original fil med dokumentation for klassisk og derefter udvidet BPF. God læsning, hvis du vil fordybe dig i samlesprog og tekniske arkitektoniske detaljer.
Blog om BPF fra facebook. Den opdateres sjældent, men passende, som Alexei Starovoitov (forfatter af eBPF) og Andrii Nakryiko - (vedligeholder) skriver der libbpf).
Hemmeligheder af bpftool. En underholdende twitter-tråd fra Quentin Monnet med eksempler og hemmeligheder ved brug af bpftool.