BPF for de minste, del én: utvidet BPF

I begynnelsen var det en teknologi og den ble kalt BPF. Vi så på henne tidligere, Det gamle testamentets artikkel i denne serien. I 2013, gjennom innsatsen til Alexei Starovoitov og Daniel Borkman, ble en forbedret versjon av den, optimalisert for moderne 64-bits maskiner, utviklet og inkludert i Linux-kjernen. Denne nye teknologien ble kort kalt Internal BPF, deretter omdøpt til Extended BPF, og nå, etter flere år, kaller alle den bare BPF.

Grovt sett lar BPF deg kjøre vilkårlig brukerlevert kode i Linux-kjernen, og den nye arkitekturen viste seg å være så vellykket at vi vil trenge et dusin flere artikler for å beskrive alle applikasjonene. (Det eneste utviklerne ikke gjorde bra, som du kan se i ytelseskoden nedenfor, var å lage en anstendig logo.)

Denne artikkelen beskriver strukturen til den virtuelle BPF-maskinen, kjernegrensesnitt for arbeid med BPF, utviklingsverktøy, samt en kort, veldig kort oversikt over eksisterende muligheter, dvs. alt vi vil trenge i fremtiden for en dypere studie av de praktiske anvendelsene av BPF.
BPF for de minste, del én: utvidet BPF

Sammendrag av artikkelen

Introduksjon til BPF-arkitektur. Først tar vi et fugleperspektiv av BPF-arkitekturen og skisserer hovedkomponentene.

Registre og kommandosystem for den virtuelle BPF-maskinen. Allerede med en ide om arkitekturen som helhet, vil vi beskrive strukturen til den virtuelle BPF-maskinen.

Livssyklus for BPF-objekter, bpffs-filsystem. I denne delen skal vi se nærmere på livssyklusen til BPF-objekter – programmer og kart.

Administrere objekter ved å bruke bpf-systemkallet. Med en viss forståelse av systemet allerede på plass, vil vi til slutt se på hvordan man oppretter og manipulerer objekter fra brukerområdet ved å bruke et spesielt systemkall − bpf(2).

Пишем программы BPF с помощью libbpf. Selvfølgelig kan du skrive programmer ved hjelp av et systemanrop. Men det er vanskelig. For et mer realistisk scenario utviklet kjernefysiske programmerere et bibliotek libbpf. Vi lager et grunnleggende BPF-applikasjonsskjelett som vi vil bruke i påfølgende eksempler.

Kjernehjelpere. Her skal vi lære hvordan BPF-programmer kan få tilgang til kjernehjelpefunksjoner – et verktøy som sammen med kart fundamentalt utvider mulighetene til den nye BPF sammenlignet med den klassiske.

Tilgang til kart fra BPF-programmer. På dette tidspunktet vil vi vite nok til å forstå nøyaktig hvordan vi kan lage programmer som bruker kart. Og la oss til og med ta en rask titt inn i den store og mektige verifikatoren.

Utviklingsverktøy. Hjelpedel om hvordan du setter sammen de nødvendige verktøyene og kjernen for eksperimenter.

Konklusjon. På slutten av artikkelen vil de som leser så langt finne motiverende ord og en kort beskrivelse av hva som vil skje i de følgende artiklene. Vi vil også liste opp en rekke lenker for selvstudium for de som ikke har lyst eller evne til å vente på fortsettelsen.

Introduksjon til BPF-arkitektur

Før vi begynner å vurdere BPF-arkitekturen, vil vi referere en siste gang (oh) til klassisk BPF, som ble utviklet som et svar på bruken av RISC-maskiner og løste problemet med effektiv pakkefiltrering. Arkitekturen viste seg å være så vellykket at den, etter å ha blitt født på det flotte nittitallet i Berkeley UNIX, ble overført til de fleste eksisterende operativsystemer, overlevde inn i de sprø tjueårene og fortsatt finner nye applikasjoner.

Den nye BPF ble utviklet som et svar på allestedsnærværende 64-bits maskiner, skytjenester og det økte behovet for verktøy for å lage SDN (Softe-definert nnettverk). Utviklet av kjernenettverksingeniører som en forbedret erstatning for den klassiske BPF, fant den nye BPF bokstavelig talt seks måneder senere applikasjoner i den vanskelige oppgaven med å spore Linux-systemer, og nå, seks år etter at den dukket opp, trenger vi en hel neste artikkel bare for å liste opp ulike typer programmer.

Morsomme bilder

I kjernen er BPF en virtuell sandkassemaskin som lar deg kjøre "vilkårlig" kode i kjerneplass uten å gå på bekostning av sikkerheten. BPF-programmer opprettes i brukerområdet, lastes inn i kjernen og kobles til en hendelseskilde. En hendelse kan for eksempel være levering av en pakke til et nettverksgrensesnitt, lansering av en kjernefunksjon osv. Når det gjelder en pakke, vil BPF-programmet ha tilgang til dataene og metadataene til pakken (for lesing og muligens skriving, avhengig av type program); i tilfelle av å kjøre en kjernefunksjon, argumentene til funksjonen, inkludert pekere til kjerneminne, etc.

La oss se nærmere på denne prosessen. Til å begynne med, la oss snakke om den første forskjellen fra den klassiske BPF, programmer som ble skrevet i assembler. I den nye versjonen ble arkitekturen utvidet slik at programmer kunne skrives på høynivåspråk, primært selvfølgelig i C. Til dette ble det utviklet en backend for llvm, som lar deg generere bytekode for BPF-arkitekturen.

BPF for de minste, del én: utvidet BPF

BPF-arkitekturen ble delvis designet for å kjøre effektivt på moderne maskiner. For å få dette til å fungere i praksis, blir BPF-bytekoden, når den er lastet inn i kjernen, oversatt til naturlig kode ved hjelp av en komponent kalt en JIT-kompilator (Just In Tjeg meg). Neste, hvis du husker, i klassisk BPF ble programmet lastet inn i kjernen og knyttet til hendelseskilden atomisk - i sammenheng med et enkelt systemkall. I den nye arkitekturen skjer dette i to trinn - først lastes koden inn i kjernen ved hjelp av et systemkall bpf(2)og så, senere, gjennom andre mekanismer som varierer avhengig av type program, kobles programmet til hendelseskilden.

Her kan leseren ha et spørsmål: var det mulig? Hvordan garanteres utførelsessikkerheten til en slik kode? Utførelsessikkerhet er garantert for oss ved lasting av BPF-programmer kalt verifier (på engelsk kalles dette stadiet verifier, og jeg vil fortsette å bruke det engelske ordet):

BPF for de minste, del én: utvidet BPF

Verifier er en statisk analysator som sikrer at et program ikke forstyrrer den normale driften av kjernen. Dette betyr forresten ikke at programmet ikke kan forstyrre driften av systemet - BPF-programmer, avhengig av typen, kan lese og omskrive deler av kjerneminnet, returnere funksjonsverdier, trimme, legge til, omskrive og til og med videresende nettverkspakker. Verifier garanterer at å kjøre et BPF-program ikke vil krasje kjernen og at et program som i henhold til reglene har skrivetilgang, for eksempel dataene til en utgående pakke, ikke vil kunne overskrive kjerneminnet utenfor pakken. Vi vil se på verifikatoren litt mer detaljert i den tilsvarende delen, etter at vi har blitt kjent med alle de andre komponentene i BPF.

Så hva har vi lært så langt? Brukeren skriver et program i C, laster det inn i kjernen ved hjelp av et systemkall bpf(2), hvor den kontrolleres av en verifikator og oversettes til opprinnelig bytekode. Deretter kobler den samme eller en annen bruker programmet til hendelseskilden og det begynner å kjøre. Det er nødvendig å skille oppstart og tilkobling av flere grunner. For det første er det relativt dyrt å kjøre en verifikator, og ved å laste ned det samme programmet flere ganger kaster vi bort datatid. For det andre, nøyaktig hvordan et program er koblet til avhenger av typen, og et "universelt" grensesnitt utviklet for et år siden er kanskje ikke egnet for nye typer programmer. (Selv om nå som arkitekturen blir mer moden, er det en idé å forene dette grensesnittet på nivået libbpf.)

Den oppmerksomme leser legger kanskje merke til at vi ikke er ferdige med bildene enda. Alt det ovennevnte forklarer faktisk ikke hvorfor BPF fundamentalt endrer bildet sammenlignet med klassisk BPF. To nyvinninger som betydelig utvider anvendelsesområdet er muligheten til å bruke delt minne og kjernehjelpefunksjoner. I BPF implementeres delt minne ved hjelp av såkalte maps – delte datastrukturer med et spesifikt API. De har sannsynligvis fått dette navnet fordi den første typen kart som dukket opp var en hashtabell. Så dukket det opp matriser, lokale (per-CPU) hashtabeller og lokale matriser, søketrær, kart som inneholder pekere til BPF-programmer og mye mer. Det som er interessant for oss nå, er at BPF-programmer nå har muligheten til å fortsette tilstanden mellom samtaler og dele den med andre programmer og med brukerplass.

Kart er tilgjengelig fra brukerprosesser ved hjelp av et systemanrop bpf(2), og fra BPF-programmer som kjører i kjernen ved hjelp av hjelpefunksjoner. Dessuten eksisterer hjelpere ikke bare for å jobbe med kart, men også for å få tilgang til andre kjernefunksjoner. For eksempel kan BPF-programmer bruke hjelpefunksjoner til å videresende pakker til andre grensesnitt, generere perf-hendelser, få tilgang til kjernestrukturer og så videre.

BPF for de minste, del én: utvidet BPF

Oppsummert gir BPF muligheten til å laste vilkårlig, dvs. verifikatortestet, brukerkode inn i kjernerommet. Denne koden kan lagre tilstand mellom samtaler og utveksle data med brukerplass, og har også tilgang til kjernedelsystemer tillatt av denne typen program.

Dette ligner allerede på egenskapene som tilbys av kjernemoduler, sammenlignet med hvilke BPF har noen fordeler (selvfølgelig kan du bare sammenligne lignende applikasjoner, for eksempel systemsporing - du kan ikke skrive en vilkårlig driver med BPF). Du kan merke deg en lavere inngangsterskel (noen verktøy som bruker BPF krever ikke at brukeren har kjerneprogrammeringsferdigheter, eller programmeringsferdigheter generelt), kjøretidssikkerhet (rekk opp hånden i kommentarene for de som ikke brøt systemet når de skrev eller testing av moduler), atomitet - det er nedetid ved omlasting av moduler, og BPF-delsystemet sikrer at ingen hendelser blir savnet (for å være rettferdig, dette er ikke sant for alle typer BPF-programmer).

Tilstedeværelsen av slike evner gjør BPF til et universelt verktøy for å utvide kjernen, noe som bekreftes i praksis: flere og flere nye typer programmer legges til BPF, flere og flere store selskaper bruker BPF på kampservere 24×7, mer og mer startups bygger sin virksomhet på løsninger basert på som er basert på BPF. BPF brukes overalt: i beskyttelse mot DDoS-angrep, opprettelse av SDN (for eksempel implementering av nettverk for kubernetes), som hovedsystemsporingsverktøy og statistikksamler, i inntrengningsdeteksjonssystemer og sandkassesystemer, etc.

La oss fullføre oversiktsdelen av artikkelen her og se på den virtuelle maskinen og BPF-økosystemet mer detaljert.

Digresjon: verktøy

For å kunne kjøre eksemplene i de følgende avsnittene, kan det hende du trenger en rekke verktøy, i det minste llvm/clang med bpf-støtte og bpftool. I seksjonen Utviklingsverktøy Du kan lese instruksjonene for å sette sammen verktøyene, så vel som kjernen din. Denne delen er plassert nedenfor for ikke å forstyrre harmonien i presentasjonen vår.

BPF virtuelle maskinregistre og instruksjonssystem

Arkitekturen og kommandosystemet til BPF ble utviklet under hensyntagen til det faktum at programmer vil bli skrevet på C-språket og, etter å ha lastet inn i kjernen, oversatt til innfødt kode. Derfor ble antallet registre og settet med kommandoer valgt med tanke på skjæringspunktet, i matematisk forstand, mellom egenskapene til moderne maskiner. I tillegg ble det pålagt ulike begrensninger for programmer, for eksempel var det inntil nylig ikke mulig å skrive løkker og subrutiner, og antallet instruksjoner var begrenset til 4096 (nå kan privilegerte programmer laste opp til en million instruksjoner).

BPF har elleve brukertilgjengelige 64-bits registre r0-r10 og en programteller. Registrere r10 inneholder en rammepeker og er skrivebeskyttet. Programmer har tilgang til en 512-byte stabel under kjøring og en ubegrenset mengde delt minne i form av kart.

BPF-programmer har lov til å kjøre et spesifikt sett med programtype kjernehjelpere og, mer nylig, vanlige funksjoner. Hver kalt funksjon kan ta opptil fem argumenter, sendt i registre r1-r5, og returverdien sendes til r0. Det er garantert at etter retur fra funksjonen, innholdet i registrene r6-r9 Vil ikke endre seg.

For effektiv programoversettelse, registrer r0-r11 for alle støttede arkitekturer er unikt kartlagt til reelle registre, tatt i betraktning ABI-funksjonene til gjeldende arkitektur. For eksempel for x86_64 registrerer r1-r5, som brukes til å sende funksjonsparametere, vises på rdi, rsi, rdx, rcx, r8, som brukes til å sende parametere til funksjoner på x86_64. For eksempel, koden til venstre oversettes til koden til høyre slik:

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

Registrere r0 også brukt til å returnere resultatet av programkjøring, og i registeret r1 programmet sendes en peker til konteksten - avhengig av type program kan dette for eksempel være en struktur struct xdp_md (for XDP) eller struktur struct __sk_buff (for forskjellige nettverksprogrammer) eller struktur struct pt_regs (for ulike typer sporingsprogrammer) osv.

Så vi hadde et sett med registre, kjernehjelpere, en stabel, en kontekstpeker og delt minne i form av kart. Ikke at alt dette er helt nødvendig på turen, men...

La oss fortsette beskrivelsen og snakke om kommandosystemet for å jobbe med disse objektene. Alle (Nesten alle) BPF-instruksjoner har en fast 64-bits størrelse. Hvis du ser på en instruksjon på en 64-bit Big Endian-maskin vil du se

BPF for de minste, del én: utvidet BPF

Her Code - dette er kodingen av instruksjonen, Dst/Src er kodingene til henholdsvis mottakeren og kilden, Off - 16-bits signert innrykk, og Imm er et 32-bits fortegnet heltall som brukes i noen instruksjoner (ligner på cBPF-konstanten K). Koding Code har en av to typer:

BPF for de minste, del én: utvidet BPF

Instruksjonsklassene 0, 1, 2, 3 definerer kommandoer for arbeid med minne. De kalles, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, henholdsvis. Klasser 4, 7 (BPF_ALU, BPF_ALU64) utgjør et sett med ALU-instruksjoner. Klasser 5, 6 (BPF_JMP, BPF_JMP32) inneholder hoppinstruksjoner.

Den videre planen for å studere BPF-instruksjonssystemet er som følger: i stedet for omhyggelig å liste opp alle instruksjonene og deres parametere, vil vi se på et par eksempler i denne delen, og fra dem vil det bli klart hvordan instruksjonene faktisk fungerer og hvordan demontere alle binære filer for BPF manuelt. For å konsolidere materialet senere i artikkelen vil vi også møte individuelle instruksjoner i seksjonene om Verifier, JIT-kompilator, oversettelse av klassisk BPF, samt når vi studerer kart, kaller funksjoner, etc.

Når vi snakker om individuelle instruksjoner, vil vi referere til kjernefilene bpf.h и bpf_common.h, som definerer de numeriske kodene til BPF-instruksjoner. Når du studerer arkitektur på egen hånd og/eller analyserer binærfiler, kan du finne semantikk i følgende kilder, sortert etter kompleksitet: Uoffisiell eBPF-spesifikasjon, BPF og XDP Referanseguide, instruksjonssett, Dokumentasjon/nettverk/filter.txt og, selvfølgelig, i Linux-kildekoden - verifier, JIT, BPF-tolk.

Eksempel: demontering av BPF i hodet

La oss se på et eksempel der vi kompilerer et program readelf-example.c og se på den resulterende binære filen. Vi vil avsløre det originale innholdet readelf-example.c nedenfor, etter at vi har gjenopprettet logikken fra binære koder:

$ 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 utdata readelf er en innrykk og programmet vårt består derfor av 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 like b7, 15, b7 и 95. Husk at de minst signifikante tre bitene er instruksjonsklassen. I vårt tilfelle er den fjerde biten av alle instruksjoner tom, så instruksjonsklassene er henholdsvis 7, 5, 7, 5. Klasse 7 er BPF_ALU64, og 5 er BPF_JMP. For begge klasser er instruksjonsformatet det samme (se ovenfor), og vi kan omskrive programmet vårt slik (samtidig vil vi omskrive de resterende kolonnene i menneskelig form):

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

Operasjon b klasse ALU64 - Er BPF_MOV. Den tildeler en verdi til destinasjonsregisteret. Hvis biten er satt s (kilde), så hentes verdien fra kilderegisteret, og hvis den, som i vårt tilfelle, ikke er satt, så hentes verdien fra feltet Imm. Så i den første og tredje instruksjonen utfører vi operasjonen r0 = Imm. Videre er JMP klasse 1 operasjon BPF_JEQ (hopp hvis lik). I vårt tilfelle, siden litt S er null, sammenligner den verdien av kilderegisteret med feltet Imm. Hvis verdiene sammenfaller, skjer overgangen til PC + OffDer PC, som vanlig, inneholder adressen til neste instruksjon. Endelig er JMP Class 9 Operation BPF_EXIT. Denne instruksjonen avslutter programmet og går tilbake til kjernen r0. La oss legge til en ny kolonne i tabellen vår:

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

Vi kan omskrive dette i en mer praktisk form:

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

Hvis vi husker hva som står i registeret r1 programmet sendes en peker til konteksten fra kjernen, og i registeret r0 verdien returneres til kjernen, så kan vi se at hvis pekeren til konteksten er null, så returnerer vi 1, og ellers - 2. La oss sjekke at vi har rett ved å se på kilden:

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

Ja, det er et meningsløst program, men det oversettes til bare fire enkle instruksjoner.

Unntakseksempel: 16-byte instruksjon

Vi nevnte tidligere at noen instruksjoner tar opp mer enn 64 biter. Dette gjelder for eksempel instruksjoner lddw (Kode = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — last et dobbeltord fra feltene inn i registeret Imm. Faktum er det Imm har en størrelse på 32, og et dobbeltord er 64 biter, så å laste en 64-bits umiddelbar verdi inn i et register i en 64-bits instruksjon vil ikke fungere. For å gjøre dette brukes to tilstøtende instruksjoner for å lagre den andre delen av 64-bits verdien i feltet Imm... Eksempel:

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

Det er bare to instruksjoner i et binært program:

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

Vi møtes igjen med instruksjoner lddw, når vi snakker om flytting og arbeid med kart.

Eksempel: demontering av BPF med standardverktøy

Så vi har lært å lese BPF-binære koder og er klare til å analysere alle instruksjoner om nødvendig. Imidlertid er det verdt å si at det i praksis er mer praktisk og raskere å demontere programmer ved hjelp av standardverktøy, for eksempel:

$ 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

Livssyklus til BPF-objekter, bpffs-filsystem

(Jeg lærte først noen av detaljene beskrevet i denne underseksjonen fra post Alexei Starovoitov inn BPF-bloggen.)

BPF-objekter - programmer og kart - lages fra brukerområdet ved hjelp av kommandoer BPF_PROG_LOAD и BPF_MAP_CREATE systemanrop bpf(2), skal vi snakke om nøyaktig hvordan dette skjer i neste avsnitt. Dette skaper kjernedatastrukturer og for hver av dem refcount (referansetelling) settes til én, og en filbeskrivelse som peker på objektet returneres til brukeren. Etter at håndtaket er lukket refcount objektet reduseres med én, og når det når null, blir objektet ødelagt.

Hvis programmet bruker kart, da refcount disse kartene økes med ett etter innlasting av programmet, dvs. deres filbeskrivelser kan lukkes fra brukerprosessen og fortsatt refcount blir ikke null:

BPF for de minste, del én: utvidet BPF

Etter vellykket innlasting av et program, kobler vi det vanligvis til en slags hendelsesgenerator. For eksempel kan vi sette den på et nettverksgrensesnitt for å behandle innkommende pakker eller koble den til noen tracepoint i kjernen. På dette tidspunktet vil referansetelleren også øke med én og vi vil kunne lukke filbeskrivelsen i loader-programmet.

Hva skjer hvis vi nå slår av bootloaderen? Det avhenger av typen hendelsesgenerator (krok). Alle nettverkskroker vil eksistere etter at lasteren er fullført, disse er de såkalte globale krokene. Og for eksempel vil sporingsprogrammer bli utgitt etter at prosessen som opprettet dem avsluttes (og derfor kalles lokale, fra "lokal til prosess"). Teknisk sett har lokale hooks alltid en tilsvarende filbeskrivelse i brukerområdet og lukkes derfor når prosessen er lukket, men globale kroker gjør det ikke. I den følgende figuren, ved hjelp av røde kryss, prøver jeg å vise hvordan avslutningen av lasteprogrammet påvirker levetiden til objekter når det gjelder lokale og globale kroker.

BPF for de minste, del én: utvidet BPF

Hvorfor er det et skille mellom lokale og globale kroker? Å kjøre noen typer nettverksprogrammer gir mening uten et brukerområde, for eksempel, forestill deg DDoS-beskyttelse - bootloaderen skriver reglene og kobler BPF-programmet til nettverksgrensesnittet, hvoretter bootloaderen kan gå og drepe seg selv. På den annen side, se for deg et program for feilsøking som du skrev på knærne på ti minutter – når det er ferdig, vil du gjerne at det ikke er noe søppel igjen i systemet, og lokale kroker vil sørge for det.

Tenk deg derimot at du vil koble til et sporingspunkt i kjernen og samle statistikk over mange år. I dette tilfellet ønsker du å fullføre brukerdelen og gå tilbake til statistikken fra tid til annen. Bpf-filsystemet gir denne muligheten. Det er et pseudo-filsystem som kun er i minnet som tillater opprettelse av filer som refererer til BPF-objekter og dermed øker refcount gjenstander. Etter dette kan lasteren gå ut, og gjenstandene den opprettet vil forbli i live.

BPF for de minste, del én: utvidet BPF

Å lage filer i bpffs som refererer til BPF-objekter kalles "pinning" (som i følgende setning: "prosess kan feste et BPF-program eller kart"). Å lage filobjekter for BPF-objekter gir mening ikke bare for å forlenge levetiden til lokale objekter, men også for brukbarheten til globale objekter - tilbake til eksemplet med det globale DDoS-beskyttelsesprogrammet, ønsker vi å kunne komme og se på statistikk fra tid til annen.

BPF-filsystemet er vanligvis montert i /sys/fs/bpf, men den kan også monteres lokalt, for eksempel slik:

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

Filsystemnavn opprettes ved hjelp av kommandoen BPF_OBJ_PIN BPF-systemanrop. For å illustrere, la oss ta et program, kompilere det, laste det opp og feste det til bpffs. Programmet vårt gjør ikke noe nyttig, vi presenterer bare koden slik at du kan gjenskape eksemplet:

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

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

La oss kompilere dette programmet og lage en lokal kopi av filsystemet bpffs:

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

La oss nå laste ned programmet vårt ved å bruke verktøyet bpftool og se på de medfølgende systemanropene bpf(2) (noen irrelevante linjer fjernet fra strace-utgang):

$ 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 lastet programmet vha BPF_PROG_LOAD, mottok en filbeskrivelse fra kjernen 3 og bruke kommandoen BPF_OBJ_PIN festet denne filbeskrivelsen som en fil "bpf-mountpoint/test". Etter dette bootloader-programmet bpftool arbeidet ferdig, men programmet vårt forble i kjernen, selv om vi ikke koblet det til noe nettverksgrensesnitt:

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

Vi kan slette filobjektet normalt unlink(2) og etter det vil det tilsvarende programmet bli slettet:

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

Sletting av objekter

Når vi snakker om å slette objekter, er det nødvendig å klargjøre at etter at vi har koblet programmet fra kroken (hendelsesgeneratoren), vil ikke en eneste ny hendelse utløse lanseringen, men alle gjeldende forekomster av programmet vil bli fullført i normal rekkefølge .

Noen typer BPF-programmer lar deg erstatte programmet på farten, dvs. gi sekvensatomitet replace = detach old program, attach new program. I dette tilfellet vil alle aktive forekomster av den gamle versjonen av programmet fullføre arbeidet, og nye hendelsesbehandlere vil bli opprettet fra det nye programmet, og "atomicity" betyr her at ikke en eneste hendelse vil bli savnet.

Legge ved programmer til hendelseskilder

I denne artikkelen vil vi ikke separat beskrive å koble programmer til hendelseskilder, siden det er fornuftig å studere dette i sammenheng med en bestemt type program. Cm. eksempel nedenfor, der vi viser hvordan programmer som XDP er koblet sammen.

Manipulere objekter ved hjelp av bpf System Call

BPF-programmer

Alle BPF-objekter opprettes og administreres fra brukerområdet ved hjelp av et systemanrop bpf, som har følgende prototype:

#include <linux/bpf.h>

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

Her er laget cmd er en av verdiene for type enum bpf_cmd, attr — en peker til parametere for et spesifikt program og size — objektstørrelse i henhold til pekeren, dvs. vanligvis dette sizeof(*attr). I kjerne 5.8 kalles systemet bpf støtter 34 forskjellige kommandoer, og bestemmelse av union bpf_attr opptar 200 linjer. Men vi bør ikke la oss skremme av dette, siden vi vil gjøre oss kjent med kommandoene og parameterne i løpet av flere artikler.

La oss starte med laget BPF_PROG_LOAD, som lager BPF-programmer - tar et sett med BPF-instruksjoner og laster det inn i kjernen. Ved lasting startes verifikatoren, og deretter returneres JIT-kompilatoren og, etter vellykket kjøring, programfilbeskrivelsen til brukeren. Vi så hva som skjer med ham videre i forrige avsnitt om livssyklusen til BPF-objekter.

Vi skal nå skrive et tilpasset program som vil laste et enkelt BPF-program, men først må vi bestemme hva slags program vi vil laste - vi må velge typen og innenfor rammen av denne typen, skriv et program som vil bestå verifikatoren. Men for ikke å komplisere prosessen, er her en ferdig løsning: vi tar et program som BPF_PROG_TYPE_XDP, som vil returnere verdien XDP_PASS (hopp over alle pakker). I BPF assembler ser det veldig enkelt ut:

r0 = 2
exit

Etter at vi har bestemt oss for at vi vil laste opp, vi kan fortelle deg hvordan vi skal gjøre det:

#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 hendelser i et program begynner med definisjonen av en matrise insns - vårt BPF-program i maskinkode. I dette tilfellet er hver instruksjon i BPF-programmet pakket inn i strukturen bpf_insn. Første element insns samsvarer med instruksjonene r0 = 2, sekund - exit.

Retrett. Kjernen definerer mer praktiske makroer for å skrive maskinkoder og bruke kjerneoverskriftsfilen tools/include/linux/filter.h vi kunne skrive

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

Men siden det å skrive BPF-programmer i innfødt kode bare er nødvendig for å skrive tester i kjernen og artikler om BPF, kompliserer ikke fraværet av disse makroene utviklerens liv.

Etter å ha definert BPF-programmet, går vi videre til å laste det inn i kjernen. Vårt minimalistiske sett med parametere attr inkluderer programtype, sett og antall instruksjoner, nødvendig lisens og navn "woo", som vi bruker for å finne programmet vårt på systemet etter nedlasting. Programmet, som lovet, lastes inn i systemet ved hjelp av et systemanrop bpf.

På slutten av programmet havner vi i en uendelig sløyfe som simulerer nyttelasten. Uten det vil programmet bli drept av kjernen når filbeskrivelsen som systemanropet returnerte til oss lukkes bpf, og vi vil ikke se det i systemet.

Vel, vi er klare for testing. La oss sette sammen og kjøre programmet under stracefor å sjekke at alt fungerer som det skal:

$ 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 bra, bpf(2) returnerte håndtak 3 til oss og vi gikk inn i en uendelig løkke med pause(). La oss prøve å finne programmet vårt i systemet. For å gjøre dette går vi til en annen terminal og bruker verktøyet bpftool:

# 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 det er et lastet program på systemet woo hvis globale ID er 390 og pågår nå simple-prog det er en åpen filbeskrivelse som peker på programmet (og hvis simple-prog vil fullføre jobben, da woo vil forsvinne). Som forventet, programmet woo tar 16 byte - to instruksjoner - med binære koder i BPF-arkitekturen, men i sin opprinnelige form (x86_64) er det allerede 40 byte. La oss se på programmet vårt i sin opprinnelige form:

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

ingen overraskelser. La oss nå se på koden generert av JIT-kompilatoren:

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

ikke særlig effektiv for exit(2), men i rettferdighet er programmet vårt for enkelt, og for ikke-trivielle programmer er prologen og epilogen lagt til av JIT-kompilatoren selvfølgelig nødvendig.

Kart

BPF-programmer kan bruke strukturerte minneområder som er tilgjengelige både for andre BPF-programmer og for programmer i brukerrommet. Disse objektene kalles kart, og i denne delen vil vi vise hvordan du kan manipulere dem ved hjelp av et systemanrop bpf.

La oss si med en gang at kartfunksjonene ikke er begrenset bare til tilgang til delt minne. Det finnes spesialkart som inneholder for eksempel pekere til BPF-programmer eller pekere til nettverksgrensesnitt, kart for arbeid med perf-hendelser osv. Vi vil ikke snakke om dem her, for ikke å forvirre leseren. Bortsett fra dette ignorerer vi synkroniseringsproblemer, siden dette ikke er viktig for våre eksempler. En fullstendig liste over tilgjengelige karttyper finner du i <linux/bpf.h>, og i denne delen tar vi som eksempel den historisk første typen, hash-tabellen BPF_MAP_TYPE_HASH.

Hvis du lager en hash-tabell i for eksempel C++, vil du si unordered_map<int,long> woo, som på russisk betyr «Jeg trenger et bord woo ubegrenset størrelse, hvis nøkler er av typen int, og verdiene er typen long" For å lage en BPF-hash-tabell må vi gjøre mye av det samme, bortsett fra at vi må spesifisere maksimal størrelse på tabellen, og i stedet for å spesifisere typene nøkler og verdier, må vi spesifisere størrelsene deres i byte . Bruk kommandoen for å lage kart BPF_MAP_CREATE systemanrop bpf. La oss se på et mer eller mindre minimalt program som lager et kart. Etter det forrige programmet som laster BPF-programmer, bør dette virke enkelt for deg:

$ 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 sett med parametere attr, der vi sier "Jeg trenger en hashtabell med nøkler og størrelsesverdier sizeof(int), der jeg kan legge inn maksimalt fire elementer." Når du oppretter BPF-kart, kan du spesifisere andre parametere, for eksempel på samme måte som i eksempelet med programmet, spesifiserte vi navnet på objektet som "woo".

La oss kompilere og kjø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 systemanropet bpf(2) returnerte oss beskrivelseskartnummeret 3 og deretter venter programmet, som forventet, på ytterligere instruksjoner i systemanropet pause(2).

La oss nå sende programmet vårt til bakgrunnen eller åpne en annen terminal og se på objektet vårt ved å bruke verktøyet bpftool (vi kan skille kartet vårt fra andre ved navn):

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

Tallet 114 er den globale IDen til objektet vårt. Ethvert program på systemet kan bruke denne IDen til å åpne et eksisterende kart ved hjelp av kommandoen BPF_MAP_GET_FD_BY_ID systemanrop bpf.

Nå kan vi spille med hashbordet vårt. La oss se på innholdet:

$ sudo bpftool map dump id 114
Found 0 elements

Tømme. La oss sette en verdi i det hash[1] = 1:

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

La oss se på tabellen igjen:

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

Hurra! Vi klarte å legge til ett element. Merk at vi må jobbe på bytenivå for å gjøre dette, siden bptftool vet ikke hvilken type verdiene i hashtabellen er. (Denne kunnskapen kan overføres til henne ved hjelp av BTF, men mer om det nå.)

Hvordan leser og legger bpftool til elementer? La oss ta en titt under panseret:

$ 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 åpnet vi kartet med dets globale ID ved å bruke kommandoen BPF_MAP_GET_FD_BY_ID и bpf(2) returnerte deskriptor 3 til oss. Videre bruk av kommandoen BPF_MAP_GET_NEXT_KEY vi fant den første nøkkelen i tabellen ved å passere NULL som en peker til "forrige"-tasten. Hvis vi har nøkkelen, kan vi gjøre det BPF_MAP_LOOKUP_ELEMsom returnerer en verdi til en peker value. Det neste trinnet er at vi prøver å finne neste element ved å sende en peker til gjeldende nøkkel, men tabellen vår inneholder bare ett element og kommandoen BPF_MAP_GET_NEXT_KEY returnerer ENOENT.

Ok, la oss endre verdien med tast 1, la oss si at forretningslogikken vår krever registrering hash[1] = 2:

$ 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 veldig enkelt: kommandoen BPF_MAP_GET_FD_BY_ID åpner kartet vårt etter ID, og ​​kommandoen BPF_MAP_UPDATE_ELEM overskriver elementet.

Så, etter å ha laget en hashtabell fra ett program, kan vi lese og skrive innholdet fra et annet. Merk at hvis vi var i stand til å gjøre dette fra kommandolinjen, så kan et hvilket som helst annet program på systemet gjøre det. I tillegg til kommandoene beskrevet ovenfor, for arbeid med kart fra brukerområdet, følgende:

  • BPF_MAP_LOOKUP_ELEM: finn verdi etter nøkkel
  • BPF_MAP_UPDATE_ELEM: oppdater/opprett verdi
  • BPF_MAP_DELETE_ELEM: fjern nøkkel
  • BPF_MAP_GET_NEXT_KEY: finn den neste (eller første) tasten
  • BPF_MAP_GET_NEXT_ID: lar deg gå gjennom alle eksisterende kart, det er slik det fungerer bpftool map
  • BPF_MAP_GET_FD_BY_ID: åpne et eksisterende kart ved hjelp av dens globale ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomisk oppdater verdien til et objekt og returner det gamle
  • BPF_MAP_FREEZE: gjør kartet uforanderlig fra brukerområdet (denne operasjonen kan ikke angres)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: masseoperasjoner. For eksempel, BPF_MAP_LOOKUP_AND_DELETE_BATCH - dette er den eneste pålitelige måten å lese og tilbakestille alle verdier fra kartet

Ikke alle disse kommandoene fungerer for alle karttyper, men generelt ser det å jobbe med andre typer kart fra brukerområdet nøyaktig det samme ut som å jobbe med hashtabeller.

For ordens skyld, la oss fullføre hashtabelleksperimentene våre. Husker du at vi laget en tabell som kan inneholde opptil fire nøkler? La oss legge til noen flere elementer:

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

Så langt så bra:

$ 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

La oss prøve å legge til en til:

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

Som forventet lyktes vi ikke. La oss se på feilen mer detaljert:

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

Alt er bra: som forventet, laget BPF_MAP_UPDATE_ELEM prøver å lage en ny, femte nøkkel, men krasjer E2BIG.

Så vi kan lage og laste BPF-programmer, samt lage og administrere kart fra brukerområdet. Nå er det logisk å se på hvordan vi kan bruke kart fra selve BPF-programmene. Vi kunne snakket om dette på språket til vanskelige programmer i maskinmakrokoder, men faktisk er tiden inne for å vise hvordan BPF-programmer faktisk skrives og vedlikeholdes – vha. libbpf.

(For lesere som er misfornøyde med mangelen på et eksempel på lavt nivå: vi vil analysere i detalj programmer som bruker kart og hjelpefunksjoner opprettet ved hjelp av libbpf og fortelle deg hva som skjer på instruksjonsnivå. For lesere som er misfornøyde veldig mye, la vi til eksempel på riktig sted i artikkelen.)

Skrive BPF-programmer ved hjelp av libbpf

Å skrive BPF-programmer ved hjelp av maskinkoder kan være interessant bare første gang, og så setter mettheten inn. I dette øyeblikket må du rette oppmerksomheten mot llvm, som har en backend for å generere kode for BPF-arkitekturen, samt et bibliotek libbpf, som lar deg skrive brukersiden til BPF-applikasjoner og laste inn koden til BPF-programmer generert ved hjelp av llvm/clang.

Faktisk, som vi vil se i denne og påfølgende artikler, libbpf gjør ganske mye arbeid uten det (eller lignende verktøy - iproute2, libbcc, libbpf-goosv.) er det umulig å leve. En av de drepende funksjonene i prosjektet libbpf er BPF CO-RE (Compile Once, Run Everywhere) - et prosjekt som lar deg skrive BPF-programmer som er portable fra en kjerne til en annen, med muligheten til å kjøre på forskjellige APIer (for eksempel når kjernestrukturen endres fra versjon til versjon). For å kunne jobbe med CO-RE må kjernen din være kompilert med BTF-støtte (vi beskriver hvordan du gjør dette i avsnittet Utviklingsverktøy. Du kan sjekke om kjernen din er bygget med BTF eller ikke veldig enkelt - ved tilstedeværelsen av følgende fil:

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

Denne filen lagrer informasjon om alle datatyper som brukes i kjernen og brukes i alle våre eksempler på bruk libbpf. Vi vil snakke i detalj om CO-RE i neste artikkel, men i denne - bare bygg deg en kjerne med CONFIG_DEBUG_INFO_BTF.

Bibliotek libbpf bor rett i katalogen tools/lib/bpf kjernen og dens utvikling utføres gjennom e-postlisten [email protected]. Imidlertid opprettholdes et eget depot for behovene til applikasjoner som bor utenfor kjernen https://github.com/libbpf/libbpf der kjernebiblioteket er speilvendt for lesetilgang mer eller mindre som det er.

I denne delen skal vi se på hvordan du kan lage et prosjekt som bruker libbpf, la oss skrive flere (mer eller mindre meningsløse) testprogrammer og analysere i detalj hvordan det hele fungerer. Dette vil tillate oss å lettere forklare i de følgende avsnittene nøyaktig hvordan BPF-programmer samhandler med kart, kjernehjelpere, BTF, etc.

Vanligvis prosjekter ved hjelp av libbpf legg til et GitHub-depot som en git-undermodul, vi gjør det samme:

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

Skal libbpf veldig enkelt:

$ 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

Vår neste plan i denne delen er som følger: vi vil skrive et BPF-program som BPF_PROG_TYPE_XDP, det samme som i forrige eksempel, men i C kompilerer vi det ved å bruke clang, og skriv et hjelpeprogram som vil laste det inn i kjernen. I de følgende avsnittene vil vi utvide mulighetene til både BPF-programmet og assistentprogrammet.

Eksempel: lage en fullverdig applikasjon ved hjelp av libbpf

Til å begynne med bruker vi filen /sys/kernel/btf/vmlinux, som ble nevnt ovenfor, og lag dets ekvivalent i form av en overskriftsfil:

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

Denne filen vil lagre alle datastrukturene som er tilgjengelige i kjernen vår, for eksempel er dette hvordan IPv4-overskriften er definert i kjernen:

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

Nå skal vi skrive BPF-programmet vårt 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";

Selv om programmet vårt viste seg å være veldig enkelt, må vi fortsatt ta hensyn til mange detaljer. For det første er den første overskriftsfilen vi inkluderer vmlinux.h, som vi nettopp genererte ved hjelp av bpftool btf dump - nå trenger vi ikke å installere kernel-headers-pakken for å finne ut hvordan kjernestrukturene ser ut. Følgende overskriftsfil kommer til oss fra biblioteket libbpf. Nå trenger vi det bare for å definere makroen SEC, som sender tegnet til den aktuelle delen av ELF-objektfilen. Programmet vårt finnes i seksjonen xdp/simple, hvor vi før skråstreken definerer programtypen BPF - dette er konvensjonen som brukes i libbpf, basert på seksjonsnavnet vil den erstatte den riktige typen ved oppstart bpf(2). Selve BPF-programmet er C - veldig enkelt og består av en linje return XDP_PASS. Til slutt et eget avsnitt "license" inneholder navnet på lisensen.

Vi kan kompilere programmet vårt ved å bruke llvm/clang, versjon >= 10.0.0, eller enda bedre, høyere (se avsnittet Utviklingsverktøy):

$ 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

Blant de interessante funksjonene: vi angir målarkitekturen -target bpf og veien til overskriftene libbpf, som vi nylig installerte. Også, ikke glem det -O2, uten dette alternativet kan du få overraskelser i fremtiden. La oss se på koden vår, klarte vi å skrive programmet vi ønsket?

$ 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 fungerte! Nå har vi en binær fil med programmet, og vi ønsker å lage en applikasjon som vil laste den inn i kjernen. For dette formålet biblioteket libbpf tilbyr oss to alternativer – bruk en API på lavere nivå eller en API på høyere nivå. Vi vil gå den andre veien, siden vi ønsker å lære å skrive, laste og koble til BPF-programmer med minimal innsats for deres påfølgende studie.

Først må vi generere "skjelettet" til programmet vårt fra dets binære ved å bruke det samme verktøyet bpftool — den sveitsiske kniven til BPF-verdenen (som kan tas bokstavelig, siden Daniel Borkman, en av skaperne og vedlikeholderne av BPF, er sveitser):

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

I fil xdp-simple.skel.h inneholder den binære koden til programmet vårt og funksjoner for å administrere - laste, legge ved, slette objektet vårt. I vårt enkle tilfelle ser dette ut som overkill, men det fungerer også i tilfellet der objektfilen inneholder mange BPF-programmer og kart og for å laste denne gigantiske ELF-en trenger vi bare å generere skjelettet og kalle en eller to funksjoner fra den tilpassede applikasjonen vi skriver La oss gå videre nå.

Strengt tatt er lasteprogrammet vårt trivielt:

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

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

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

    pause();

    xdp_simple_bpf__destroy(obj);
}

Her struct xdp_simple_bpf definert i filen xdp-simple.skel.h og beskriver objektfilen vår:

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 av et lavt nivå API her: strukturen struct bpf_program *simple и struct bpf_link *simple. Den første strukturen beskriver spesifikt programmet vårt, skrevet i seksjonen xdp/simple, og den andre beskriver hvordan programmet kobles til hendelseskilden.

Funksjon xdp_simple_bpf__open_and_load, åpner et ELF-objekt, analyserer det, lager alle strukturer og understrukturer (i tillegg til programmet inneholder ELF også andre seksjoner - data, skrivebeskyttet data, feilsøkingsinformasjon, lisens, etc.), og laster det deretter inn i kjernen ved hjelp av et system anrop bpf, som vi kan sjekke ved å kompilere og kjøre programmet:

$ 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

La oss nå se på programmet vårt ved hjelp av bpftool. La oss finne ID-en hennes:

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

og dump (vi bruker en forkortet form av kommandoen bpftool prog dump xlated):

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

Noe nytt! Programmet skrev ut deler av C-kildefilen vår. Dette ble gjort av biblioteket libbpf, som fant feilsøkingsdelen i binærfilen, kompilerte den til et BTF-objekt, lastet den inn i kjernen ved å bruke BPF_BTF_LOAD, og spesifiserte deretter den resulterende filbeskrivelsen når du lastet programmet med kommandoen BPG_PROG_LOAD.

Kjernehjelpere

BPF-programmer kan kjøre "eksterne" funksjoner - kjernehjelpere. Disse hjelpefunksjonene lar BPF-programmer få tilgang til kjernestrukturer, administrere kart og også kommunisere med den "virkelige verden" - lage perf-hendelser, kontrollere maskinvare (for eksempel omdirigere pakker), etc.

Eksempel: bpf_get_smp_processor_id

Innenfor rammen av "læring ved eksempel"-paradigmet, la oss vurdere en av hjelpefunksjonene, bpf_get_smp_processor_id(), sikker i fil kernel/bpf/helpers.c. Den returnerer nummeret til prosessoren som BPF-programmet som kalte det kjører på. Men vi er ikke like interessert i dens semantikk som i det faktum at implementeringen tar en linje:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF-hjelpefunksjonsdefinisjonene ligner på Linux-systemanropsdefinisjonene. Her defineres for eksempel en funksjon som ikke har noen argumenter. (En funksjon som tar for eksempel tre argumenter er definert ved hjelp av makroen BPF_CALL_3. Maksimalt antall argumenter er fem.) Dette er imidlertid bare den første delen av definisjonen. Den andre delen er å definere typestrukturen struct bpf_func_proto, som inneholder en beskrivelse av hjelpefunksjonen som verifikatoren forstår:

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

Registrere hjelpefunksjoner

For at BPF-programmer av en bestemt type skal bruke denne funksjonen, må de registrere den, for eksempel for typen BPF_PROG_TYPE_XDP en funksjon er definert i kjernen xdp_func_proto, som avgjør fra hjelpefunksjons-IDen om XDP støtter denne funksjonen eller ikke. Vår funksjon er støtter:

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 er "definert" i filen include/linux/bpf_types.h ved hjelp av en makro BPF_PROG_TYPE. Definert i anførselstegn fordi det er en logisk definisjon, og i C-språklige termer forekommer definisjonen av et helt sett med betongkonstruksjoner andre steder. Spesielt i filen kernel/bpf/verifier.c alle definisjoner fra filen bpf_types.h brukes til å lage en rekke strukturer bpf_verifier_ops[]:

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 si at for hver type BPF-program er det definert en peker til en datastruktur av typen struct bpf_verifier_ops, som initialiseres med verdien _name ## _verifier_ops, dvs., xdp_verifier_ops for xdp... Struktur xdp_verifier_ops bestemt av 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 vår kjente funksjon xdp_func_proto, som vil kjøre verifikatoren hver gang den støter på en utfordring noe slag funksjoner i et BPF-program, se verifier.c.

La oss se på hvordan et hypotetisk BPF-program bruker funksjonen bpf_get_smp_processor_id. For å gjøre dette, omskriver vi programmet fra forrige seksjon som følger:

#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 av в <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 funksjonspeker hvis verdi er 8, hvor 8 er verdien BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, som er definert for oss i filen vmlinux.h (fil bpf_helper_defs.h i kjernen genereres av et skript, så de "magiske" tallene er ok). Denne funksjonen tar ingen argumenter og returnerer en verdi av type __u32. Når vi kjører det i programmet vårt, clang genererer en instruksjon BPF_CALL "den rette typen" La oss kompilere programmet og se på delen xdp/simple:

$ 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 instruksjoner call, parameter IMM som er lik 8, og SRC_REG - null. I følge ABI-avtalen som brukes av verifikatoren, er dette et kall til hjelperfunksjon nummer åtte. Når den først er lansert, er logikken enkel. Returverdi fra register r0 kopiert til r1 og på linje 2,3 er det konvertert til type u32 — de øverste 32 bitene slettes. På linjene 4,5,6,7 returnerer vi 2 (XDP_PASS) eller 1 (XDP_DROP) avhengig av om hjelpefunksjonen fra linje 0 returnerte en null eller ikke-null verdi.

La oss teste oss selv: last programmet og se på utdataene bpftool prog dump xlated:

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

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

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

Ok, verifikatoren fant den riktige kjernehjelperen.

Eksempel: sende argumenter og til slutt kjøre programmet!

Alle hjelpefunksjoner på kjørenivå har en prototype

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

Parametre til hjelpefunksjoner sendes i registre r1-r5, og verdien returneres i registeret r0. Det er ingen funksjoner som tar mer enn fem argumenter, og støtte for dem forventes ikke å bli lagt til i fremtiden.

La oss ta en titt på den nye kjernehjelperen og hvordan BPF sender parametere. La oss skrive om xdp-simple.bpf.c som følger (resten av linjene er ikke endret):

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

Programmet vårt skriver ut nummeret til CPUen som det kjører på. La oss kompilere den og se på koden:

$ 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

På linje 0-7 skriver vi strengen running on CPU%un, og så på linje 8 kjører vi den kjente bpf_get_smp_processor_id. På linje 9-12 forbereder vi hjelperargumentene bpf_printk - registrerer r1, r2, r3. Hvorfor er det tre av dem og ikke to? Fordi bpf_printkdette er en makro-innpakning rundt den virkelige hjelperen bpf_trace_printk, som må passere størrelsen på formatstrengen.

La oss nå legge til et par linjer til xdp-simple.cslik at programmet vårt kobles til grensesnittet lo og virkelig startet!

$ 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 bruker vi funksjonen bpf_set_link_xdp_fd, som kobler XDP-type BPF-programmer til nettverksgrensesnitt. Vi har hardkodet grensesnittnummeret lo, som alltid er 1. Vi kjører funksjonen to ganger for først å koble fra det gamle programmet hvis det var vedlagt. Legg merke til at nå trenger vi ingen utfordring pause eller en uendelig sløyfe: lasterprogrammet vårt avsluttes, men BPF-programmet vil ikke bli drept siden det er koblet til hendelseskilden. Etter vellykket nedlasting og tilkobling, vil programmet startes for hver nettverkspakke som ankommer lo.

La oss laste ned programmet og se på grensesnittet lo:

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

Programmet vi lastet ned har ID 669 og vi ser samme ID på grensesnittet lo. Vi sender et par pakker til 127.0.0.1 (forespørsel + svar):

$ ping -c1 localhost

og la oss nå se på innholdet i den virtuelle feilsøkingsfilen /sys/kernel/debug/tracing/trace_pipe, hvori bpf_printk skriver sine meldinger:

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

To pakker ble oppdaget lo og behandlet på CPU0 - vårt første fullverdige meningsløse BPF-program fungerte!

Det er verdt å merke seg det bpf_printk Det er ikke for ingenting at den skriver til feilsøkingsfilen: dette er ikke den mest vellykkede hjelperen for bruk i produksjon, men målet vårt var å vise noe enkelt.

Tilgang til kart fra BPF-programmer

Eksempel: bruk av kart fra BPF-programmet

I de forrige avsnittene lærte vi hvordan du lager og bruker kart fra brukerområdet, og la oss nå se på kjernedelen. La oss starte, som vanlig, med et eksempel. La oss omskrive programmet vårt xdp-simple.bpf.c som følger:

#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 begynnelsen av programmet la vi til en kartdefinisjon woo: Dette er en 8-elements matrise som lagrer verdier som u64 (i C vil vi definere en slik matrise som u64 woo[8]). I et program "xdp/simple" vi får det gjeldende prosessornummeret inn i en variabel key og deretter bruke hjelpefunksjonen bpf_map_lookup_element vi får en peker til den tilsvarende oppføringen i matrisen, som vi øker med én. Oversatt til russisk: vi beregner statistikk over hvilken CPU som behandlet innkommende pakker. La oss prøve å kjøre programmet:

$ 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

La oss sjekke at hun er koblet til lo og send noen pakker:

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

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

La oss nå se på innholdet i matrisen:

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

Nesten alle prosesser ble behandlet på CPU7. Dette er ikke viktig for oss, hovedsaken er at programmet fungerer og vi forstår hvordan man får tilgang til kart fra BPF-programmer - ved å bruke хелперов bpf_mp_*.

Mystisk indeks

Så vi kan få tilgang til kartet fra BPF-programmet ved å bruke samtaler som

val = bpf_map_lookup_elem(&woo, &key);

hvor hjelpefunksjonen ser ut

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

men vi sender en pekepinn &woo til en ikke navngitt struktur struct { ... }...

Hvis vi ser på programsamleren, ser vi at verdien &woo er faktisk ikke definert (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 inneholdt i flyttinger:

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

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

Men hvis vi ser på det allerede lastede programmet, ser vi en peker til riktig kart (linje 4):

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

Dermed kan vi konkludere med at på tidspunktet for lansering av vårt lasteprogram, var lenken til &woo ble erstattet av noe med et bibliotek libbpf. Først skal vi se på utgangen strace:

$ 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 laget et kart woo og lastet ned programmet vårt simple. La oss se nærmere på hvordan vi laster programmet:

  • anrop xdp_simple_bpf__open_and_load fra fil xdp-simple.skel.h
  • som forårsaker xdp_simple_bpf__load fra fil xdp-simple.skel.h
  • som forårsaker bpf_object__load_skeleton fra fil libbpf/src/libbpf.c
  • som forårsaker bpf_object__load_xattr av libbpf/src/libbpf.c

Den siste funksjonen vil blant annet kalle bpf_object__create_maps, som oppretter eller åpner eksisterende kart, og gjør dem om til filbeskrivelser. (Det er her vi ser BPF_MAP_CREATE i utgangen strace.) Deretter kalles funksjonen bpf_object__relocate og det er hun som interesserer oss, siden vi husker det vi så woo i flyttetabellen. Når vi utforsker det, finner vi oss til slutt i funksjonen bpf_program__relocate, hvilken omhandler kartflyttinger:

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

Så vi tar våre instruksjoner

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

og erstatte kilderegisteret i det med BPF_PSEUDO_MAP_FD, og den første IMM til filbeskrivelsen til kartet vårt, og hvis den er lik, for eksempel, 0xdeadbeef, så vil vi motta instruksjonen

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

Slik overføres kartinformasjon til et spesifikt lastet BPF-program. I dette tilfellet kan kartet lages ved hjelp av BPF_MAP_CREATE, og åpnet med ID ved hjelp av BPF_MAP_GET_FD_BY_ID.

Totalt ved bruk libbpf Algoritmen er som følger:

  • under kompilering opprettes poster i flyttetabellen for lenker til kart
  • libbpf åpner ELF-objektboken, finner alle brukte kart og lager filbeskrivelser for dem
  • filbeskrivelser lastes inn i kjernen som en del av instruksjonen LD64

Som du kan forestille deg, er det mer i vente, og vi må se inn i kjernen. Heldigvis har vi peiling – vi har skrevet ned meningen BPF_PSEUDO_MAP_FD inn i kilderegisteret og vi kan begrave det, som vil føre oss til det hellige for alle hellige - kernel/bpf/verifier.c, der en funksjon med et særegent navn erstatter en filbeskrivelse med adressen til en type struktur struct bpf_map:

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;

(full kode finner du по ссылке). Så vi kan utvide algoritmen vår:

  • mens du laster programmet, kontrollerer verifikatoren riktig bruk av kartet og skriver adressen til den tilsvarende strukturen struct bpf_map

Når du laster ned ELF binær ved hjelp av libbpf Det er mye mer som skjer, men vi vil diskutere det i andre artikler.

Laster programmer og kart uten libbpf

Som lovet, her er et eksempel for lesere som vil vite hvordan man lager og laster et program som bruker kart, uten hjelp libbpf. Dette kan være nyttig når du jobber i et miljø der du ikke kan bygge avhengigheter, eller lagre hver bit, eller skriver et program som ply, som genererer BPF-binær kode i farten.

For å gjøre det lettere å følge logikken, vil vi omskrive eksemplet vårt for disse formålene xdp-simple. Den fullstendige og litt utvidede koden til programmet som er omtalt i dette eksemplet, finner du i denne GIST.

Logikken i søknaden vår er som følger:

  • lage et typekart BPF_MAP_TYPE_ARRAY ved å bruke kommandoen BPF_MAP_CREATE,
  • lag et program som bruker dette kartet,
  • koble programmet til grensesnittet lo,

som oversettes til menneskelig som

int main(void)
{
    int map_fd, prog_fd;

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

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

    xdp_attach(1, prog_fd);
}

Her map_create lager et kart på samme måte som vi gjorde i det første eksemplet om systemkallingen bpf - "kjerne, lag meg et nytt kart i form av en rekke med 8 elementer som __u64 og gi meg tilbake filbeskrivelsen":

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å enkelt å laste:

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 delen prog_load er definisjonen av vårt BPF-program som en rekke strukturer struct bpf_insn insns[]. Men siden vi bruker et program som vi har i C, kan vi jukse litt:

$ 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

Totalt må vi skrive 14 instruksjoner i form av strukturer som struct bpf_insn (råd: ta dumpen ovenfra, les instruksjonsdelen på nytt, åpne linux/bpf.h и linux/bpf_common.h og prøv å bestemme struct bpf_insn insns[] på egenhånd):

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 for de som ikke skrev dette selv – finn map_fd.

Det er enda en ikke avslørt del igjen i programmet vårt - xdp_attach. Dessverre kan ikke programmer som XDP kobles til ved hjelp av et systemanrop bpf. Personene som opprettet BPF og XDP var fra det nettbaserte Linux-fellesskapet, noe som betyr at de brukte den mest kjente for dem (men ikke til normal people) grensesnitt for samhandling med kjernen: netlink-uttak, se også RFC3549. Den enkleste måten å implementere xdp_attach kopierer kode fra libbpf, nemlig fra filen netlink.c, som er hva vi gjorde, forkortet det litt:

Velkommen til en verden av netlink-sockets

Åpne en netlink-sockettype NETLINK_ROUTE:

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

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

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

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

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

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

    *nl_pid = sa.nl_pid;
    return sock;
}

Vi leser fra denne kontakten:

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

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

        if (len == 0)
            break;

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

Til slutt, her er funksjonen vår som åpner en socket og sender en spesiell melding til den som inneholder en filbeskrivelse:

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 klart for testing:

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

La oss se om programmet vårt har koblet til 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

La oss sende ping og se på kartet:

$ 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 fungerer. Merk forresten at kartet vårt igjen vises i form av byte. Dette skyldes det faktum at i motsetning til libbpf vi lastet ikke inn typeinformasjon (BTF). Men vi snakker mer om dette neste gang.

Utviklingsverktøy

I denne delen skal vi se på minimum BPF-utviklerverktøysett.

Generelt sett trenger du ikke noe spesielt for å utvikle BPF-programmer - BPF kjører på en hvilken som helst anstendig distribusjonskjerne, og programmer er bygget med clang, som kan leveres fra pakken. Men på grunn av det faktum at BPF er under utvikling, er kjernen og verktøyene i stadig endring, hvis du ikke vil skrive BPF-programmer med gammeldagse metoder fra 2019, må du kompilere

  • llvm/clang
  • pahole
  • dens kjerne
  • bpftool

(Til referanse ble denne delen og alle eksemplene i artikkelen kjørt på Debian 10.)

llvm/clang

BPF er vennlig med LLVM, og selv om programmer for BPF nylig kan kompileres ved hjelp av gcc, utføres all gjeldende utvikling for LLVM. Derfor vil vi først og fremst bygge den nåværende versjonen clang fra git:

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

Nå kan vi sjekke om alt kom riktig sammen:

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

(Monteringsveiledning clang tatt av meg fra bpf_devel_QA.)

Vi vil ikke installere programmene vi nettopp har bygget, men bare legge dem til PATH, for eksempel:

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

(Dette kan legges til .bashrc eller til en egen fil. Personlig legger jeg til ting som dette ~/bin/activate-llvm.sh og når det er nødvendig gjør jeg det . activate-llvm.sh.)

Pahole og BTF

Nytte pahole brukes når du bygger kjernen for å lage feilsøkingsinformasjon i BTF-format. Vi vil ikke gå i detalj i denne artikkelen om detaljene i BTF-teknologi, annet enn det faktum at det er praktisk og vi ønsker å bruke det. Så hvis du skal bygge kjernen din, bygg først pahole (uten pahole du vil ikke kunne bygge kjernen med alternativet CONFIG_DEBUG_INFO_BTF:

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

Kjerner for å eksperimentere med BPF

Når jeg utforsker mulighetene til BPF, ønsker jeg å sette sammen min egen kjerne. Dette er generelt sett ikke nødvendig, siden du vil kunne kompilere og laste BPF-programmer på distribusjonskjernen, men å ha din egen kjerne lar deg bruke de nyeste BPF-funksjonene, som i beste fall vil vises i distribusjonen din om måneder , eller, som i tilfellet med noen feilsøkingsverktøy, vil ikke bli pakket i det hele tatt i overskuelig fremtid. Dens egen kjerne gjør det også viktig å eksperimentere med koden.

For å bygge en kjerne trenger du for det første selve kjernen, og for det andre en kjernekonfigurasjonsfil. For å eksperimentere med BPF kan vi bruke det vanlige vanilje kjerne eller en av utviklingskjernene. Historisk sett foregår BPF-utvikling innenfor Linux-nettverksfellesskapet, og derfor går alle endringer før eller siden gjennom David Miller, Linux-nettverksvedlikeholderen. Avhengig av deres natur - redigeringer eller nye funksjoner - faller nettverksendringer inn i en av to kjerner - net eller net-next. Endringer for BPF fordeles på samme måte mellom bpf и bpf-next, som deretter samles i henholdsvis netto og net-neste. For flere detaljer, se bpf_devel_QA и netdev-vanlige spørsmål. Så velg en kjerne basert på din smak og stabilitetsbehovene til systemet du tester på (*-next kjernene er de mest ustabile av de som er oppført).

Det er utenfor rammen av denne artikkelen å snakke om hvordan du administrerer kjernekonfigurasjonsfiler - det antas at du enten allerede vet hvordan du gjør dette, eller klar til å lære på egenhånd. Følgende instruksjoner bør imidlertid være mer eller mindre nok til å gi deg et fungerende BPF-aktivert system.

Last ned en av kjernene ovenfor:

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

Bygg en minimal fungerende kjernekonfigurasjon:

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

Aktiver BPF-alternativer i filen .config etter eget valg (mest sannsynlig CONFIG_BPF vil allerede være aktivert siden systemd bruker det). Her er en liste over alternativer fra kjernen som brukes for denne artikkelen:

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

Da kan vi enkelt sette sammen og installere modulene og kjernen (du kan forresten sette sammen kjernen ved å bruke den nymonterte clangved å legge til CC=clang):

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

og start på nytt med den nye kjernen (jeg bruker for dette kexec fra pakken kexec-tools):

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

bpftool

Det mest brukte verktøyet i artikkelen vil være verktøyet bpftool, levert som en del av Linux-kjernen. Det er skrevet og vedlikeholdt av BPF-utviklere for BPF-utviklere og kan brukes til å administrere alle typer BPF-objekter - last inn programmer, opprette og redigere kart, utforske livet til BPF-økosystemet, etc. Dokumentasjon i form av kildekoder for man-sider finnes i kjernen eller allerede kompilert, nettverk.

I skrivende stund bpftool leveres kun ferdig for RHEL, Fedora og Ubuntu (se f.eks. denne tråden, som forteller den uferdige historien om emballasje bpftool i Debian). Men hvis du allerede har bygget kjernen din, så bygg bpftool like enkelt som en plett:

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

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

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

$

(her ${linux} - dette er kjernekatalogen din.) Etter å ha utført disse kommandoene bpftool vil bli samlet i en katalog ${linux}/tools/bpf/bpftool og den kan legges til banen (først av alt til brukeren root) eller bare kopier til /usr/local/sbin.

Samle inn bpftool det er best å bruke sistnevnte clang, satt sammen som beskrevet ovenfor, og sjekk om den er satt sammen riktig - ved hjelp av for eksempel kommandoen

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

som vil vise hvilke BPF-funksjoner som er aktivert i kjernen din.

Forresten, den forrige kommandoen kan kjøres som

# bpftool f p k

Dette gjøres analogt med verktøyene fra pakken iproute2, hvor vi for eksempel kan si ip a s eth0 i stedet for ip addr show dev eth0.

Konklusjon

BPF lar deg sko en loppe for å effektivt måle og endre funksjonaliteten til kjernen. Systemet viste seg å være svært vellykket, i de beste tradisjonene til UNIX: en enkel mekanisme som lar deg (om)programmere kjernen tillot et stort antall mennesker og organisasjoner å eksperimentere. Og selv om eksperimentene, så vel som utviklingen av selve BPF-infrastrukturen, langt fra er ferdige, har systemet allerede en stabil ABI som lar deg bygge pålitelig, og viktigst av alt, effektiv forretningslogikk.

Jeg vil merke meg at teknologien etter min mening har blitt så populær fordi den på den ene siden kan spille (arkitekturen til en maskin kan forstås mer eller mindre på en kveld), og på den annen side å løse problemer som ikke kunne løses (vakkert) før den dukket opp. Disse to komponentene tvinger sammen mennesker til å eksperimentere og drømme, noe som fører til fremveksten av flere og flere innovative løsninger.

Denne artikkelen, selv om den ikke er spesielt kort, er bare en introduksjon til BPF-verdenen og beskriver ikke "avanserte" funksjoner og viktige deler av arkitekturen. Planen fremover er omtrent slik: den neste artikkelen vil være en oversikt over BPF-programtyper (det er 5.8 programtyper som støttes i 30-kjernen), så skal vi til slutt se på hvordan man skriver ekte BPF-applikasjoner ved hjelp av kjernesporingsprogrammer som et eksempel, så er det på tide med et mer dyptgående kurs om BPF-arkitektur, etterfulgt av eksempler på BPF-nettverk og sikkerhetsapplikasjoner.

Tidligere artikler i denne serien

  1. BPF for de minste, del null: klassisk BPF

Lenker

  1. BPF og XDP Referanseguide — dokumentasjon om BPF fra cilium, eller mer presist fra Daniel Borkman, en av skaperne og vedlikeholderne av BPF. Dette er en av de første seriøse beskrivelsene, som skiller seg fra de andre ved at Daniel vet nøyaktig hva han skriver om og det er ingen feil der. Dette dokumentet beskriver spesielt hvordan du arbeider med BPF-programmer av XDP- og TC-typene ved å bruke det velkjente verktøyet ip fra pakken iproute2.

  2. Dokumentasjon/nettverk/filter.txt — original fil med dokumentasjon for klassisk og deretter utvidet BPF. God lesning hvis du vil fordype deg i monteringsspråk og tekniske arkitektoniske detaljer.

  3. Blogg om BPF fra facebook. Den oppdateres sjelden, men passende, som Alexei Starovoitov (forfatter av eBPF) og Andrii Nakryiko - (vedlikeholdsholder) skriver der libbpf).

  4. Hemmelighetene til bpftool. En underholdende twittertråd fra Quentin Monnet med eksempler og hemmeligheter ved bruk av bpftool.

  5. Dykk inn i BPF: en liste over lesestoff. En gigantisk (og fortsatt vedlikeholdt) liste over lenker til BPF-dokumentasjon fra Quentin Monnet.

Kilde: www.habr.com

Legg til en kommentar