BPF til de mindste, del et: udvidet BPF

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.
BPF til de mindste, del et: udvidet BPF

Resumé af artiklen

Introduktion til BPF-arkitektur. Først tager vi et fugleperspektiv af BPF-arkitekturen og skitserer hovedkomponenterne.

Registre og kommandosystem for den virtuelle BPF-maskine. Allerede med en idé om arkitekturen som helhed, vil vi beskrive strukturen af ​​den virtuelle BPF-maskine.

Livscyklus for BPF-objekter, bpffs-filsystem. I dette afsnit vil vi se nærmere på BPF-objekters livscyklus – programmer og kort.

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 til de mindste, del et: udvidet BPF

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):

BPF til de mindste, del et: udvidet BPF

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.

BPF til de mindste, del et: udvidet BPF

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 r0r10 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 r1r5, og returværdien sendes til r0. Det er garanteret, at efter hjemkomst fra funktionen, indholdet af registrene r6r9 Vil ikke ændre sig.

For effektiv programoversættelse, registre r0r11 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 r1r5, 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:

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

Регистр 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

BPF til de mindste, del et: udvidet BPF

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:

BPF til de mindste, del et: udvidet BPF

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:

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

Første kolonne i output readelf er en indrykning, og vores program består således af fire kommandoer:

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

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:

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

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:

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

Der er kun to instruktioner i et binært program:

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

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:

$ llvm-objdump -d x64.o

Disassembly of section .text:

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

Livscyklus for BPF-objekter, bpffs-filsystem

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

BPF til de mindste, del et: udvidet BPF

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.

BPF til de mindste, del et: udvidet BPF

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.

BPF til de mindste, del et: udvidet BPF

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:

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

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

Lad os kompilere dette program og lave en lokal kopi af filsystemet bpffs:

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

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):

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

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 af union 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:

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

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

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

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

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

    for ( ;; )
        pause();
}

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

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

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:

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

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

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:

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

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:

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

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:

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

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

    for ( ;; )
        pause();
}

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

Lad os kompilere og køre programmet:

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

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:

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

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:

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

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

Så langt så godt:

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

Lad os prøve at tilføje en mere:

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

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

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:

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

Går til libbpf meget simpelt:

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

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:

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

Nu vil vi skrive vores BPF-program i C:

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

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

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

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):

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

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

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?

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

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

Disassembly of section xdp/simple:

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

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:

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

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:

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

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

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_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

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:

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

Registrering af hjælpefunktioner

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:

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

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[]:

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

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_ops bestemt af i fil net/core/filter.c som følger:

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

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:

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

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

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

Symbol bpf_get_smp_processor_id bestemt af в <bpf/bpf_helper_defs.h> bibliotek libbpf som

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

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:

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

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

Disassembly of section xdp/simple:

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

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

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 r1r5, 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:

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

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

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!

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

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

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

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

cleanup:
    xdp_simple_bpf__destroy(obj);
}

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:

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

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

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

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

    *val += 1;

    return XDP_PASS;
}

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

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:

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

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

Lad os nu se på indholdet af arrayet:

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

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

val = bpf_map_lookup_elem(&woo, &key);

hvor hjælperfunktionen ser ud

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

men vi sender en pegepind &woo til en unavngiven struktur struct { ... }...

Hvis vi ser på programsamleren, ser vi, at værdien &woo er faktisk ikke defineret (linje 4):

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

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

Disassembly of section xdp/simple:

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

og er indeholdt i flytninger:

$ 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):

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

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:

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

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;

Så vi tager vores instruktioner

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

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:

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

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

(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":

static int map_create()
{
    union bpf_attr attr;

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

Programmet er også nemt at indlæse:

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

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

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:

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

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

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

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):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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:

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

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

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

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

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

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

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

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

cleanup:
    close(sock);
    return ret;
}

Så alt er klar til test:

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

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

Lad os sende pings og se på kortet:

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

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
... много времени спустя
$

Nu kan vi kontrollere, om alt kom sammen korrekt:

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

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

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

Tidligere artikler i denne serie

  1. BPF til de mindste, del nul: klassisk BPF

Links

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

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

  3. Blog om BPF fra facebook. Den opdateres sjældent, men passende, som Alexei Starovoitov (forfatter af eBPF) og Andrii Nakryiko - (vedligeholder) skriver der libbpf).

  4. Hemmeligheder af bpftool. En underholdende twitter-tråd fra Quentin Monnet med eksempler og hemmeligheder ved brug af bpftool.

  5. Dyk ned i BPF: en liste over læsestof. En kæmpe (og stadig vedligeholdt) liste over links til BPF-dokumentation fra Quentin Monnet.

Kilde: www.habr.com

Tilføj en kommentar