I början fanns det en teknik som kallades BPF. Vi tittade pÄ den i , den gammaltestamentliga artikeln i denna cykel. à r 2013 utvecklades en förbÀttrad version av den av Alexei Starovoitov och Daniel Borkman och inkluderades i Linux-kÀrnan, optimerad för moderna 64-bitars maskiner. Denna nya teknik hade en kort tid namnet Internal BPF, innan den bytte namn till Extended BPF, och nu, efter nÄgra Är, kallar alla den helt enkelt BPF.
Grovt sett lÄter BPF anvÀndare köra valfri kod i Linux-kÀrnans rum, och den nya arkitekturen visade sig vara sÄ framgÄngsrik att vi kommer att behöva ett dussin artiklar för att beskriva alla dess tillÀmpningar. (Det enda som utvecklarna inte lyckades med, som du kan se pÄ bilden nedan, Àr att skapa en anstÀndig logotyp.)
Den hÀr artikeln beskriver konstruktionen av BPF:s virtuella maskin, kÀrnans grÀnssnitt för att arbeta med BPF, utvecklingsverktyg samt en kort, mycket kort, översikt över de befintliga möjligheterna, det vill sÀga allt som vi kommer att behöva för en djupare förstÄelse av praktiska tillÀmpningar av BPF.
Sammanfattning av artikeln
Först kommer vi att se pÄ BPF:s arkitektur frÄn en fÄgelperspektiv och definiera huvudkomponenterna.
Med en översikt över arkitekturen kommer vi att beskriva konstruktionen av BPF:s virtuella maskin.
I det hĂ€r avsnittet kommer vi att titta nĂ€rmare pĂ„ livscykeln för BPF-objekt â program och mappar.
Med en viss förstĂ„else för systemet kommer vi Ă€ntligen att titta pĂ„ hur man skapar och hanterar objekt frĂ„n anvĂ€ndarrummet med hjĂ€lp av en speciell systemanrop â bpf(2).
Att skriva program med hjÀlp av systemanrop Àr givetvis möjligt. Men det Àr svÄrt. För ett mer realistiskt scenario har kÀrnutvecklare utvecklat biblioteket libbpf. Vi kommer att skapa ett enkelt skelett för BPF-applikationen som vi kommer att anvÀnda i de kommande exemplen.
HÀr kommer vi att lÀra oss hur BPF-program kan anvÀnda kÀrnans hjÀlpfunktioner - ett verktyg som, tillsammans med maps, vÀsentligt utökar det nya BPF:s kapabiliteter jÀmfört med den klassiska varianten.
Vid det hÀr laget kommer vi att förstÄ tillrÀckligt för att veta hur man skapar program som anvÀnder maps. Vi kommer Àven att fÄ en liten inblick i den stora och mÀktiga verifieraren.
En referensdel om hur man samlar de nödvÀndiga verktygen och kÀrnan för experiment.
I slutet av artikeln hittar de som har lÀst dit motiverande ord och en kort beskrivning av vad som kommer i kommande artiklar. Vi kommer ocksÄ att lista ett antal lÀnkar för sjÀlvstudier för dem som inte har lust eller möjlighet att vÀnta pÄ fortsÀttningen.
Introduktion till BPF-arkitekturen
Innan vi börjar titta pÄ BPF-arkitekturen kommer vi en sista gÄng (eller kanske inte) att referera till , som utvecklades som ett svar pÄ uppkomsten av RISC-maskiner och löste problemet med effektiv paketfiltrering. Arkitekturen blev sÄ framgÄngsrik att den, född under de turbulenta nittiotalen i Berkeley UNIX, portades till de flesta existerande operativsystem och har överlevt in i de galna tjugotalen och fortfarande finner nya tillÀmpningar.
Den nya BPF utvecklades som ett svar pÄ den utbredda anvÀndningen av 64-bitarsmaskiner, molntjÀnster och ökade krav pÄ verktyg för att skapa SDN (Software-defined networking). Utvecklad av nÀtverksingenjörer som en förbÀttrad ersÀttning av den klassiska BPF, fann den nya BPF redan efter sex mÄnader tillÀmpningar i den svÄra konsten att spÄra Linux-system, och idag, sex Är efter sin ankomst, kommer vi att behöva en hel, kommande, artikel bara för att lista olika typer av program.
VeSöLyaE KaRtiNki
I grunden Ă€r BPF en sandboxad virtuell maskin som möjliggör körning av "vilken kod som helst" i kĂ€rnans utrymme utan att Ă€ventyra sĂ€kerheten. BPF-program skapas i anvĂ€ndarutrymmet, laddas in i kĂ€rnan och kopplas till en viss hĂ€ndelsekĂ€lla. En hĂ€ndelse kan vara, till exempel, leveransen av ett paket till ett nĂ€tverksgrĂ€nssnitt, anrop av en viss kĂ€rnfunktion, osv. I fallet med paket kommer BPF-programmet att ha tillgĂ„ng till paketets data och metadata (för lĂ€sning och kanske för skrivning, beroende pĂ„ typen av program), och i fallet med anrop av kĂ€rnfunktionen â till funktionens argument, inklusive pekare till kĂ€rnans minne, osv.
LÄt oss titta pÄ denna process mer ingÄende. Till att börja med, lÄt oss diskutera den första skillnaden frÄn klassisk BPF, vars program skrevs pÄ assembler. I den nya versionen har arkitekturen utökats sÄ att program kan skrivas pÄ högre nivÄers sprÄk, frÀmst C. För detta har en backend för llvm utvecklats, som möjliggör generering av bytekod för BPF-arkitekturen.

BPF-arkitekturen utvecklades, delvis, för att effektivt köras pĂ„ moderna maskiner. För att detta ska fungera i praktiken översĂ€tts BPF-bytekod, efter att den laddats in i kĂ€rnan, till nativ kod med hjĂ€lp av en komponent som kallas JIT-kompilator (Just In Time). Som ni kanske minns laddades programmet i klassisk BPF in i kĂ€rnan och kopplades till hĂ€ndelsekĂ€llan atomĂ€rt â i kontexten av ett systemanrop. I den nya arkitekturen sker detta i tvĂ„ steg â först laddas koden in i kĂ€rnan med hjĂ€lp av ett systemanrop bpf(2), och dĂ€refter, senare, kopplas programmet (attaches) till hĂ€ndelsekĂ€llan med andra mekanismer, olika beroende pĂ„ typ av program.
HĂ€r kan lĂ€saren stĂ€lla sig frĂ„gan: Ăr det verkligen möjligt? Hur sĂ€kerstĂ€lls sĂ€kerheten vid körning av sĂ„dan kod? SĂ€kerheten garanteras av steget att ladda BPF-program som kallas verifierare (pĂ„ engelska kallas detta steg verifier och jag kommer att anvĂ€nda det engelska ordet vidare):

Verifier Ă€r en statisk analysator som garanterar att programmet inte stör den normala driften av kĂ€rnan. Detta innebĂ€r dock inte att programmet inte kan pĂ„verka systemets funktion â BPF-program kan, beroende pĂ„ typ, lĂ€sa och skriva till omrĂ„den i kĂ€rnans minne, Ă€ndra returvĂ€rden för funktioner, trimma, komplettera, skriva över och till och med vidarebefordra nĂ€tverkspaket. Verifieraren sĂ€kerstĂ€ller att kĂ€rnan inte kraschar vid körning av ett BPF-program och att ett program som enligt reglerna har skrivĂ„tkomst, sĂ„som data frĂ„n utgĂ„ende paket, inte kan skriva över kĂ€rnans minne utanför paketet. Vi kommer att titta lite mer detaljerat pĂ„ verifieraren i den relevanta sektionen efter att vi har bekantat oss med alla andra komponenter i BPF.
SĂ„, vad har vi lĂ€rt oss vid det hĂ€r laget? AnvĂ€ndaren skriver ett program i C, laddar upp det till kĂ€rnan via ett systemanrop bpf(2), dĂ€r det kontrolleras av verifieraren och översĂ€tts till nativ bytekod. DĂ€refter ansluter samma eller en annan anvĂ€ndare programmet till en hĂ€ndelsekĂ€lla och det börjar köras. Ă
tskillnad av inladdning och anslutning behövs av flera skĂ€l. För det första, att köra verifieraren Ă€r relativt dyrt och genom att ladda samma program flera gĂ„nger slösar vi datorns tid. För det andra beror det pĂ„ hur programmet ansluts pĂ„ dess typ och en "universell" grĂ€nssnitt som utvecklades för ett Ă„r sedan kanske inte Ă€r lĂ€mplig för nya typer av program. (Ăven om det nu, nĂ€r arkitekturen blir mer mogen, finns en idĂ© att förena detta grĂ€nssnitt pĂ„ nivĂ„n libbpf.)
Den uppmĂ€rksamma lĂ€saren kan mĂ€rka att vi fortfarande inte har avslutat diskussionen om bilder. Och visst, allt som sagts ovan förklarar inte hur BPF principiellt förĂ€ndrar bilden jĂ€mfört med klassisk BPF. TvĂ„ nyheter som vĂ€sentligt utökar tillĂ€mpningarna Ă€r möjligheten att anvĂ€nda delad minne och kĂ€rnfunktioner (kernel helpers). I BPF implementeras delad minne genom sĂ„ kallade maps â delade datatyper med en specifik API. De fick troligen namnet eftersom den första typen av map som introducerades var en hash-tabell. DĂ€refter kom arrayer, lokala (per-CPU) hash-tabeller och lokala arrayer, söktrĂ€d, maps som innehĂ„ller pekare till BPF-program och mycket mer. Det som nu intresserar oss Ă€r att BPF-program har möjlighet att bevara tillstĂ„nd mellan anrop och dela det med andra program samt med anvĂ€ndarutrymmet.
Ă
tkomst till maps sker frĂ„n anvĂ€ndarprocesser genom systemanrop bpf(2), och frĂ„n BPF-program som körs i kĂ€rnan â via kĂ€rnfunktioner. Dessutom finns det inte bara helpers för att arbeta med maps, utan ocksĂ„ för att fĂ„ tillgĂ„ng till andra möjligheter i kĂ€rnan. Till exempel kan BPF-program anvĂ€nda kĂ€rnfunktioner för att omdirigera paket till andra grĂ€nssnitt, generera hĂ€ndelser i perf-subsystemet, fĂ„ Ă„tkomst till kĂ€rnstrukturer och sĂ„ vidare.

Sammanfattningsvis ger BPF möjlighet att ladda godtycklig, dvs. verifierad kod frÄn anvÀndaren till kÀrnutrymmet. Denna kod kan bevara tillstÄnd mellan anrop och utbyta data med anvÀndarutrymmet, samt har tillgÄng till de subsystem i kÀrnan som tillÄts för denna typ av program.
Det liknar redan de möjligheter som kĂ€rnmoduler erbjuder, jĂ€mfört med vilka BPF har vissa fördelar (naturligtvis kan man bara jĂ€mföra liknande applikationer, till exempel systemtracing â man kan inte skriva en godtycklig drivrutin för BPF). Man kan notera den lĂ€gre intrĂ€deströskeln (vissa verktyg som anvĂ€nder BPF förutsĂ€tter inte att anvĂ€ndaren har kĂ€rnprogrammeringskunskaper, eller ens programmeringskunskaper överhuvudtaget), sĂ€kerheten vid körning (rĂ€ck upp en hand i kommentarerna om du inte har kraschat systemet vid skrivande eller testande av moduler), atomĂ€ritet â nĂ€r moduler laddas om finns det stillestĂ„ndstid, medan BPF-systemet garanterar att inga hĂ€ndelser gĂ„r förlorade (för rĂ€ttvisans skull, detta gĂ€ller inte alla typer av BPF-program).
Att ha sĂ„dana möjligheter gör BPF till ett universellt verktyg för att utöka kĂ€rnan, vilket bekrĂ€ftas genom praktiken: allt fler typer av program lĂ€ggs till BPF, fler stora företag anvĂ€nder BPF pĂ„ produktionsservrar 24Ă7, fler startups bygger sina verksamheter kring lösningar baserade pĂ„ BPF. BPF anvĂ€nds överallt: för skydd mot DDoS-attacker, skapande av SDN (till exempel genomförande av nĂ€tverk för kubernetes), som huvudverktyg för systemtracing och statistiksamling, i intrĂ„ngsdetekteringssystem och i sandbox-system, etc.
LÄt oss avsluta den översiktliga delen av artikeln och titta pÄ den virtuella maskinen och BPF-ekosystemet mer detaljerat.
Avvikelse: verktyg
För att kunna köra exemplen frÄn de kommande sektionerna kan du behöva ett antal verktyg, minst llvm/clang med stöd för bpf och bpftool. I avsnittet kan du lÀsa instruktionerna för att bygga verktygen samt din egen kÀrna. Denna sektion placeras lÀngre ner för att inte störa flödet i vÄr presentation.
Register och kommandosystem för BPF:s virtuella maskin
Arkitektur och kommandosystem BPF utvecklades med tanke pÄ att program skulle skrivas pÄ C-sprÄket och efter att ha laddats in i kÀrnan översÀttas till maskinkod. DÀrför valdes antalet register och mÀngden kommandon med beaktande av samspelet, i matematisk mening, mellan moderna maskiners kapabiliteter. Dessutom stÀlldes olika typer av begrÀnsningar pÄ programmen; till exempel, fram till nyligen fanns det ingen möjlighet att skriva loopar och underprogram, och antalet instruktioner var begrÀnsat till 4096 (nu kan privilegierade program ladda upp till en miljon instruktioner).
I BPF finns det elva tillgĂ€ngliga 64-bitarsregister för anvĂ€ndaren r0âr10 och en programrĂ€knare (program counter). Registret r10 innehĂ„ller en pekare till stacken (frame pointer) och Ă€r endast lĂ€sbart. Program fĂ„r under körning tillgĂ„ng till en stack pĂ„ 512 byte och en obegrĂ€nsad mĂ€ngd gemensamt minne i form av maps.
Program i BPF fĂ„r köra en uppsĂ€ttning hjĂ€lpfunktioner (kernel helpers) beroende pĂ„ programtyp och, nyligen, Ă€ven vanliga funktioner. Varje anropad funktion kan ta upp till fem argument, som överförs i registren r1âr5, och det returnerade vĂ€rdet överförs i r0. Det garanteras att efter Ă„tergĂ„ng frĂ„n funktionen kommer innehĂ„llet i registren r6âr9 inte att Ă€ndras.
För effektiv översĂ€ttning av program mappas registren r0âr11 för alla stödda arkitekturer entydigt till verkliga register med beaktande av ABI-specifikationerna för nuvarande arkitektur. Till exempel, för x86_64 registren r1âr5, som anvĂ€nds för att överföra funktionsparametrar, mappas till rdi, rsi, rdx, rcx, r8, som anvĂ€nds för att överföra parametrar till funktioner pĂ„ x86_64. Till exempel översĂ€tts koden till vĂ€nster till koden till höger pĂ„ följande sĂ€tt:
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 0x0000000000001ee8Registret r0 anvĂ€nds ocksĂ„ för att returnera resultatet av programkörningen, och i registret r1 skickas en pekare till kontexten â beroende pĂ„ programtyp kan detta vara, till exempel, en struktur (för XDP) eller strukturen (för olika nĂ€tverksprogram) eller strukturen (för olika typer av spĂ„rningsprogram) etc.
SĂ„ vi hade en uppsĂ€ttning register, kernel helpers, en stack, en kontextpekare och delat minne i form av maps. Inte för att allt detta var absolut nödvĂ€ndigt pĂ„ resan, menâŠ
LÄt oss fortsÀtta beskrivningen och prata om kommandosystemet för att arbeta med dessa objekt. Alla () BPF-instruktioner har en fast storlek pÄ 64 bitar. Om du tittar pÄ en instruktion pÄ en 64-bitars Big Endian-maskin, sÄ kommer du att se
![]()
HĂ€r Code â detta Ă€r instruktionens kodning, Dst/Src â det Ă€r kodningarna för mottagaren och kĂ€llan, respektive, Av â 16-bitars teckenförskjutning, och Imm â det Ă€r ett 32-bitars heltal med tecken som anvĂ€nds i vissa kommandon (motsvarighet till konstant K frĂ„n cBPF). Kodningen Code har en av tvĂ„ typer:

Instruktionsklasser 0, 1, 2, 3 definierar kommandon för att arbeta med minnet. De , BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respektive. Klasserna 4, 7 (BPF_ALU, BPF_ALU64) utgör en uppsÀttning ALU-instruktioner. Klasserna 5, 6 (BPF_JMP, BPF_JMP32) omfattar instruktioner för villkorssprÄng.
Den fortsatta planen för att studera BPF-kommandosystemet Àr att istÀllet för att noggrant lista alla instruktioner och deras parametrar, kommer vi att gÄ igenom ett par exempel i denna sektion, vilket kommer att klargöra hur instruktioner verkligen fungerar och hur man manuellt disassemblerar vilken binÀrfil som helst för BPF. För att befÀsta materialet kommer vi lÀngre fram i artikeln att stöta pÄ individuella instruktioner i sektionerna om Verifier, JIT-kompilatorn, översÀttning av klassisk BPF, samt nÀr vi studerar maps, anrop av funktioner osv.
NĂ€r vi pratar om individuella instruktioner, kommer vi att hĂ€nvisa till kĂ€rnfilerna och , dĂ€r de numeriska koderna för BPF-instruktioner definieras. Vid sjĂ€lvstĂ€ndig studie av arkitekturen och/eller analys av binĂ€rer kan du hitta semantiken i följande, sorterade efter svĂ„righetsgrad, kĂ€llor: , , och naturligtvis i kĂ€llkoden för Linux â verifier, JIT, BPF-tolk.
Exempel: disassemblera BPF i sinnet
LÄt oss gÄ igenom ett exempel dÀr vi kompilerar programmet readelf-example.c och ser pÄ den resulterande binÀren. Vi kommer att avslöja det ursprungliga innehÄllet readelf-example.c nedan, efter att vi har ÄterstÀllt dess logik frÄn de binÀra koderna:
$ 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 ................Den första kolumnen i utskriften readelf â detta Ă€r indentering och vĂ„rt program bestĂ„r sĂ„ledes av fyra kommandon:
Kod Dst Src Off Imm
b7 0 0 0000 01000000
15 0 1 0100 00000000
b7 0 0 0000 02000000
95 0 0 0000 00000000Kommandokoderna Àr lika b7, 15, b7 och 95. Vi minns att de tre lÀgsta bitarna Àr instruktionens klass. I vÄrt fall Àr den fjÀrde biten tom för alla instruktioner, dÀrför Àr klasserna lika, det vill sÀga 7, 5, 7, 5. Klass 7 Àr BPF_ALU64, och 5 Àr BPF_JMP. För bÄda klasserna Àr formatet för instruktionen detsamma (se ovan) och vi kan skriva om vÄrt program sÄ hÀr (och vi skriver Àven om de andra kolumnerna pÄ ett mer begripligt sÀtt):
Op S Klass 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 0Operation b klassen ALU64 â detta Ă€r . Den tilldelar vĂ€rdet till mottagarregistret. Om biten s (kĂ€lla) Ă€r instĂ€lld, tas vĂ€rdet frĂ„n kĂ€llregistret, och om den, som i vĂ„rt fall, inte Ă€r instĂ€lld, tas vĂ€rdet frĂ„n fĂ€ltet Imm. SĂ„, i den första och tredje instruktionen utför vi operationen r0 = Imm. DĂ€refter Ă€r operationen av klass JMP â det Ă€r (hoppa om lika). I vĂ„rt fall, eftersom biten S Ă€r lika med noll, jĂ€mför den vĂ€rdet i kĂ€llregistret med fĂ€ltet Imm. Om vĂ€rdena överensstĂ€mmer sker hoppet till PC + Off, dĂ€r PC, som vanligtvis innehĂ„ller adressen till nĂ€sta instruktion. Slutligen Ă€r operationen av klass JMP 9 â det Ă€r . Denna instruktion avslutar programmets körning och returnerar till kĂ€rnan r0. LĂ„t oss lĂ€gga till en ny kolumn i vĂ„r tabell:
Op S Klass 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 exitVi kan skriva om detta pÄ ett mer bekvÀmt sÀtt:
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exitOm vi minns att registret r1 till programmet överför en pekare till kontexten frÄn kÀrnan, och registret r0 returnerar vÀrdet till kÀrnan, sÄ kan vi se att om pekaren till kontexten Àr lika med noll, sÄ returnerar vi 1, annars returnerar vi 2. LÄt oss kontrollera att vi har rÀtt genom att titta pÄ kÀllkoden:
$ cat readelf-example.c
int foo(void *ctx)
{
return ctx ? 2 : 1;
}Ja, detta Àr ett meningslöst program, men det kompileras till bara fyra enkla instruktioner.
Exempel pÄ undantag: 16-byte instruktion
Tidigare nĂ€mnde vi att vissa instruktioner tar mer Ă€n 64 bitar. Detta gĂ€ller till exempel instruktionen lddw (Kod = 0x18 = | | ) â att lĂ€sa ett dubbelt ord i registret frĂ„n fĂ€lten Imm. Saken Ă€r den att Imm har en storlek pĂ„ 32, och ett dubbelt ord Ă€r 64 bitar, sĂ„ att ladda en 64-biters direktvĂ€rde i ett register i en enda 64-bitars instruktion Ă€r inte möjligt. För detta anvĂ€nds tvĂ„ intilliggande instruktioner för att lagra den andra delen av det 64-bitarsvĂ€rdet i fĂ€ltet Imm. Exempel:
$ 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 ........I det binÀra programmet finns det endast tvÄ instruktioner:
BinÀr Disassm
18000000 ddccbbaa 00000000 44332211 r0 = Imm[0]|Imm[1]
95000000 00000000 exitVi kommer att stöta pÄ instruktionen lddw, nÀr vi pratar om omplaceringar och arbete med maps.
Exempel: disassemblera BPF med standardverktyg
SÄ vi har lÀrt oss att lÀsa BPF-binÀra koder och Àr redo att analysera vilken instruktion som helst, om det behövs. Men det Àr vÀrt att nÀmna att det Àr mer praktiskt och snabbare att disassemblera program med hjÀlp av standardverktyg, till exempel:
$ llvm-objdump -d x64.o
Disassembly of section .text:
0000000000000000 :
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 exitLivscykeln för BPF-objekt, filsystemet bpffs
(Vissa detaljer som beskrivs i detta avsnitt fick jag veta för första gÄngen frÄn Alexei Starovoitov pÄ .)
BPF-objekt â program och maps â skapas frĂ„n anvĂ€ndarutrymmet med hjĂ€lp av kommandon BPF_PROG_LOAD och BPF_MAP_CREATE systemanropet bpf(2), ska vi prata om hur detta exakt gĂ„r till i nĂ€sta avsnitt. Under detta skapas datakonstruktioner i kĂ€rnan och för var och en av dem refcount (referensrĂ€knare) sĂ€tts till ett, och anvĂ€ndaren fĂ„r tillbaka en filbeskrivare som pekar pĂ„ objektet. NĂ€r filbeskrivaren stĂ€ngs refcount minskas objektets rĂ€knare med ett, och nĂ€r den nĂ„r noll förstörs objektet.
Om programmet anvÀnder maps, sÄ refcount ökar referensen för dessa maps med ett efter att programmet laddats, det vill sÀga att deras filbeskrivare kan stÀngas frÄn anvÀndarprocessen och samtidigt refcount inte blir noll:

Efter att programmet har laddats framgÄngsrikt kopplar vi vanligtvis det till nÄgon hÀndelsegenerator. Till exempel kan vi koppla det till ett nÀtverksgrÀnssnitt för att bearbeta inkommande paket eller koppla det till nÄgot tracepoint i kÀrnan. Vid denna tidpunkt ökar ocksÄ referensrÀknaren med ett och vi kan stÀnga filbeskrivaren i laddningsprogrammet.
Vad hÀnder om vi nu avslutar bootloaderns arbete? Det beror pÄ typen av hÀndelsegenerator (hook). Alla nÀtverks-hooks kommer att finnas kvar efter att bootloadern avslutats, de sÄ kallade globala hooks. Program för spÄrning, Ä andra sidan, kommer att frigöras efter att processen som skapade dem avslutats (och dÀrför kallas de lokala, av "local to the process"). Tekniskt sett har lokala hooks alltid en motsvarande filbeskrivare i anvÀndarutrymmet och stÀngs dÀrför med stÀngning av processen, medan globala hooks inte gör det. PÄ nÀsta bild försöker jag med röda kryss visa hur avslutningen av bootloaderprogrammet pÄverkar livslÀngden pÄ objekt i fall av lokala och globala hooks.

Varför finns det en uppdelning mellan lokala och globala hooks? Att köra vissa typer av nĂ€tverksprogram Ă€r meningsfullt Ă€ven utan anvĂ€ndarutrymme, till exempel, förestĂ€ll dig skydd mot DDoS â bootloadern skriver regler och kopplar ett BPF-program till nĂ€tverksgrĂ€nssnittet, varefter bootloadern kan avsluta sig sjĂ€lv. Ă andra sidan, förestĂ€ll dig ett avlusningsprogram för spĂ„rning som du skrivit snabbt pĂ„ tio minuter â efter att det avslutats skulle du vilja att systemet inte lĂ€mnar skrĂ€p, och lokala hooks garanterar detta.
Ă
andra sidan, förestÀll dig att du vill ansluta till tracepoint i kÀrnan och samla statistik under mÄnga Är. I det hÀr fallet skulle du vilja avsluta anvÀndardelen och Äterkomma till statistiken dÄ och dÄ. Denna möjlighet erbjuds av filsystemet bpf. Det Àr ett pseudo-filsystem som endast existerar i minnet, vilket gör att du kan skapa filer som refererar till BPF-objekt och dÀrmed förlÀnger refcount objektens livslÀngd. Efter det kan bootloadern avsluta sitt arbete, medan de skapade objekten förblir kvar.

Att skapa filer i bpffs som refererar till BPF-objekt kallas "pinna" ("pin", som i nĂ€sta fras: "process can pin a BPF program or map"). Att skapa filobjekt för BPF-objekt Ă€r meningsfullt inte bara för att förlĂ€nga livslĂ€ngden pĂ„ lokala objekt, utan Ă€ven för att underlĂ€tta anvĂ€ndningen av globala objekt â för att Ă„tergĂ„ till exemplet med det globala DDoS-skyddsprogrammet, vi vill ha möjlighet att dĂ„ och dĂ„ besöka och titta pĂ„ statistiken.
BPF-filsystemen monteras vanligtvis i /sys/fs/bpf, men den kan ocksÄ monteras lokalt, till exempel sÄ hÀr:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpointNamnen i filsystmet skapas med kommandot BPF_OBJ_PIN systemanropet BPF. Som exempel lÄt oss ta ett program, kompilera det, ladda det och fÀsta det i bpffs. VÄrt program gör ingenting anvÀndbart, vi ger bara dess kod sÄ att du kan Äterskapa exemplet:
$ cat test.c
__attribute__((section("xdp"), used))
int test(void *ctx)
{
return 0;
}
char _license[] __attribute__((section("license"), used)) = "GPL";Vi kommer att kompilera detta program och skapa en lokal kopia av filsystmet bpffs:
$ clang -target bpf -c test.c -o test.o
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpointNu laddar vi vÄrt program med hjÀlp av verktyget bpftool och tittar pÄ de relaterade systemanropen bpf(2) (vissa irrelevanta rader har tagits bort frÄn strace-utdata):
$ 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) = 0HÀr laddade vi programmet med hjÀlp av BPF_PROG_LOAD, fick en filbeskrivare frÄn kÀrnan 3 och med hjÀlp av kommandot BPF_OBJ_PIN fÀste vi denna filbeskrivare som en fil "bpf-mountpoint\/test". Efter detta avslutade laddningsprogrammet bpftool sitt arbete, men vÄrt program kvarstod i kÀrnan, Àven om vi inte kopplade det till nÄgon nÀtverksgrÀnssnitt:
$ 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 4096BVi kan ta bort filobjektet med en vanlig unlink(2) och efter det kommer motsvarande program att tas bort:
$ sudo rm .\/bpf-mountpoint\/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directoryBorttagning av objekt
NÀr vi talar om att ta bort objekt mÄste vi klargöra att efter att vi har kopplat bort programmet frÄn hooken (hÀndelsegeneratorn) kommer inga nya hÀndelser att leda till att det startas, men alla aktuella instanser av programmet kommer att avslutas korrekt.
Vissa typer av BPF-program tillÄter att programmet byts ut i farten, dvs. ger atomÀr funktionalitet i sekvensen replace = koppla bort gammalt program, koppla pÄ nytt program. HÀr betyder atomaritet att inga hÀndelser kommer att missas.
Anslutning av program till hÀndelsekÀllor
I den hÀr artikeln kommer vi inte att beskriva anslutningen av program till hÀndelsekÀllor separat, eftersom det Àr mer meningsfullt att studera det i samband med en specifik typ av program. Se nedan, dÀr vi visar hur program av typen XDP ansluts.
Hantera objekt med systemanropet bpf
BPF-program
Alla BPF-objekt skapas och hanteras frÄn anvÀndarrummet med hjÀlp av systemanropet bpf, som har följande prototyp:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);HĂ€r Ă€r kommandot cmd â det Ă€r ett av vĂ€rdena av typen , attr â en pekare till parametrarna för det specifika programmet och size â storleken pĂ„ objektet pekaren pekar pĂ„, det vill sĂ€ga vanligtvis sizeof(*attr). I kĂ€rnan 5.8 stöder systemanropet bpf 34 olika kommandon, och union bpf_attr tar upp 200 rader. Men vi ska inte lĂ„ta detta skrĂ€mma oss, eftersom vi kommer att bekanta oss med kommandon och parametrar under flera artiklar.
Vi börjar med kommandot BPF_PROG_LOAD, som skapar BPF-program â tar en uppsĂ€ttning BPF-instruktioner och laddar in den i kĂ€rnan. Vid laddning aktiveras verifieraren, och sedan JIT-kompilatorn och, efter att den har utfört framgĂ„ngsrikt, returneras en filbeskrivare för programmet till anvĂ€ndaren. Vi sĂ„g vad som hĂ€nde med det i avsnittet tidigare .
Nu ska vi skriva ett anvĂ€ndarprogram som laddar ett enkelt BPF-program, men först mĂ„ste vi bestĂ€mma vilket program vi vill ladda â vi mĂ„ste vĂ€lja och inom denna typ skriva ett program som klarar verifierarens kontroller. För att inte komplicera processen, hĂ€r Ă€r en fĂ€rdig lösning: vi tar ett program av typen BPF_PROG_TYPE_XDP, som kommer att returnera vĂ€rdet XDP_PASS (skippa alla paket). I BPF assembly ser det vĂ€ldigt enkelt ut:
r0 = 2
exitNÀr vi har bestÀmt oss för vad vi ska ladda kan vi berÀtta hur vi ska göra det: vad Intressanta hÀndelser i programmet börjar med definitionen av en array
#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();
}insns â vĂ„rt BPF-program i maskinkod. Varje instruktion i BPF-programmet packas i en struktur bpf_insn motsvarar instruktionen â vĂ„rt BPF-program i maskinkod. Varje instruktion i BPF-programmet packas i en struktur r0 = 2 En anmĂ€rkning., den andra â exit.
I kÀrnan finns mer anvÀndarvÀnliga makron för att skriva maskinkoder, och genom att anvÀnda kÀrnans headerfil tools/include/linux/filter.h skulle vi kunna skriva struct bpf_insn insns[] = { BPF_MOV64_IMM(BPF_REG_0, XDP_PASS), BPF_EXIT_INSN() };
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
BPF_EXIT_INSN()
};Men eftersom skrivning av BPF-program i maskinkoder endast Àr nödvÀndig för att skriva tester i kÀrnan och artiklar om BPF, gör avsaknaden av dessa makron faktiskt inte livet svÄrare för utvecklaren.
Efter att BPF-programmet har definierats gÄr vi vidare till att ladda det i kÀrnan. VÄrt minimalistiska sett av parametrar attr innehÄller programtyp, uppsÀttning och mÀngd instruktioner, obligatorisk licens samt namn "woo", som vi anvÀnder för att hitta vÄrt program i systemet efter att det har laddats. Programmet, som lovat, laddas in i systemet med hjÀlp av ett systemanrop bpf.
I slutet av programmet hamnar vi i en oÀndlig loop som imiterar nyttolasten. Utan den kommer programmet att förstöras av kÀrnan vid stÀngning av filbeskrivaren som systemanropet returnerade till oss, bpf, och vi kommer inte att se det i systemet.
NÄvÀl, vi Àr redo för testning. LÄt oss kompilera och köra programmet under för att kontrollera paketöverföringen Ät bÄda hÄll, för att kontrollera att allt fungerar som det ska:
$ 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_VERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(Allt ser bra ut, bpf(2) vi fick tillbaka deskriptor 3 och vi gick in i en oÀndlig loop med pause(). LÄt oss försöka hitta vÄrt program i systemet. För att göra det gÄr vi till en annan terminal och anvÀnder verktyget 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 att det finns ett laddat program i systemet woo vars globala ID Ă€r 390, och att det för nĂ€rvarande finns en öppen filbeskrivare i processen simple-prog som pekar pĂ„ programmet (och om simple-prog det avslutas, kommer det woo att försvinna). Som förvĂ€ntat tar programmet woo upp 16 byte â tvĂ„ instruktioner â av binĂ€rkod i BPF-arkitekturen, men i sitt ursprungliga format (x86_64) Ă€r det redan 40 byte. LĂ„t oss titta pĂ„ vĂ„rt program i sitt ursprungliga skick:
# bpftool prog dump xlated id 390
0: (b7) r0 = 2
1: (95) exitutan överraskningar. Nu lÄt oss titta pÄ koden som skapades av JIT-kompilatorn:
# 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: retqinte sÀrskilt effektivt för exit(2), men för rÀttvisans skull, vÄrt program Àr alltför enkelt, och för icke-triviala program Àr prolog och epilog som JIT-kompilatorn lÀgger till sjÀlvklart nödvÀndiga.
Maps
BPF-program kan anvÀnda strukturerade minnesomrÄden som Àr tillgÀngliga bÄde för andra BPF-program och för program frÄn anvÀndarutrymmet. Dessa objekt kallas map och i detta avsnitt visar vi hur man hanterar dem med hjÀlp av systemanrop. bpf.
LÄt oss snabbt sÀga att map-funktioner inte bara begrÀnsas till Ätkomst till delat minne. Det finns speciella mappar som innehÄller exempelvis pekare till BPF-program eller pekare till nÀtverksgrÀnssnitt, mappar för att arbeta med perf-hÀndelser osv. HÀr kommer vi inte att prata om dem för att inte förvirra lÀsaren. Dessutom ignorerar vi synkroniseringsproblem eftersom det inte Àr viktigt för vÄra exempel. En fullstÀndig lista över tillgÀngliga typer av mappar finns i , och i detta avsnitt tar vi som exempel den historiskt första typen, hash-tabellen. BPF_MAP_TYPE_HASH.
Om du skapar en hash-tabell, lÄt oss sÀga i C++, skulle du sÀga unordered_map woo, vilket pÄ svenska betyder "jag behöver en tabell woo utan begrÀnsning i storlek, dÀr nycklar har typ int, och vÀrden Àr av typ long'. För att skapa en BPF-hash-tabell mÄste vi göra ungefÀr samma sak, med den justeringen att vi mÄste ange den maximala storleken för tabellen, och istÀllet för typerna av nycklar och vÀrden mÄste vi ange deras storlekar i byte. För att skapa mappar anvÀnds kommandot BPF_MAP_CREATE systemanropet bpf. LÄt oss titta pÄ ett mer eller mindre minimalt program som skapar en map. Efter det föregÄende programmet som laddar BPF-programmet bör detta verka enkelt:
$ cat simple-map.c
#define _GNU_SOURCE
#include
#include
#include
#include
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();
}HÀr definierar vi en uppsÀttning parametrar attr, dÀr vi sÀger "jag behöver en hash-tabell med nycklar och vÀrden av storleken sizeof(int), dÀr jag kan lÀgga högst fyra element." Vid skapandet av BPF-mappar kan man ange andra parametrar, exempelvis, precis som i exemplet med programmet, angav vi objektets namn som "woo".
Vi kompilerar och kör 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(HÀr Àr systemanropet bpf(2) gav oss descriptorn för mappen nummer 3 och programmet vÀntar, som förvÀntat, pÄ vidare instruktioner i systemanropet pause(2).
Nu skickar vi vÄrt program till bakgrunden eller öppnar en annan terminal för att titta pÄ vÄrt objekt med hjÀlp av verktyget bpftool (vi kan sÀrskilja vÄr mapp frÄn andra genom dess namn):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...Numret 114 Àr vÄrt objekts globala ID. Alla program i systemet kan anvÀnda detta ID för att öppna den redan existerande mappen med kommandot BPF_MAP_GET_FD_BY_ID systemanropet bpf.
Nu kan vi leka med vÄr hash-tabell. LÄt oss titta pÄ dess innehÄll:
$ sudo bpftool map dump id 114
Found 0 elementsTomt. LÄt oss lÀgga till ett vÀrde hash[1] = 1:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0LÄt oss granska tabellen igen:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 elementHurrra! Vi lyckades lÀgga till ett element. Observera att vi mÄste arbeta pÄ byte-nivÄ, eftersom bpftool inte vet vilken typ vÀrdena i hash-tabellen har. (Det gÄr att ge den denna information via BTF, men inte nu.)
Hur lÀser och lÀgger bpftool till element? LÄt oss kika under huven:
$ 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 ENOENTFörst öppnade vi mappen med dess globala ID via kommandot BPF_MAP_GET_FD_BY_ID och bpf(2) gav oss descriptorn 3. DÀrefter anvÀnde vi kommandot BPF_MAP_GET_NEXT_KEY vi hittade den första nyckeln i tabellen genom att skicka NULL en pekare till "föregÄende" nyckel. NÀr vi har en nyckel kan vi göra BPF_MAP_LOOKUP_ELEM, vilket returnerar vÀrdet i pekaren value. NÀsta steg Àr att vi försöker hitta nÀsta element genom att skicka pekaren till den aktuella nyckeln, men vÄr tabell innehÄller bara ett element, och kommandot BPF_MAP_GET_NEXT_KEY ÄtervÀnder ENOENT.
Okej, lÄt oss Àndra vÀrdet för nyckel 1, sÀg att vÄr affÀrslogik krÀver att skriva 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) = 0Som förvÀntat Àr det mycket enkelt: kommandot BPF_MAP_GET_FD_BY_ID öppnar vÄr mapp via ID, och kommandot BPF_MAP_UPDATE_ELEM överskriver elementet.
SÄ, efter att ha skapat en hash-tabell frÄn ett program kan vi lÀsa och skriva dess innehÄll frÄn ett annat. Observera att om vi kunde göra detta frÄn kommandoraden, kan vilket annat program som helst i systemet ocksÄ göra det. Förutom kommandona som beskrivits ovan Àr följande kommandon tillgÀngliga för att arbeta med kartor frÄn anvÀndarutrymmet :
BPF_MAP_LOOKUP_ELEM: hitta vĂ€rde efter nyckelBPF_MAP_UPDATE_ELEM: uppdatera/skapa vĂ€rdeBPF_MAP_DELETE_ELEM: ta bort nyckelBPF_MAP_GET_NEXT_KEY: hitta nĂ€sta (eller första) nyckelBPF_MAP_GET_NEXT_ID: gör det möjligt att iterera över alla befintliga kartor, sĂ„ hĂ€r fungerarbpftool mapBPF_MAP_GET_FD_BY_ID: öppna en befintlig karta via dess globala IDBPF_MAP_LOOKUP_AND_DELETE_ELEM: atomĂ€rt uppdatera objektets vĂ€rde och returnera det gamlaBPF_MAP_FREEZE: gör en karta oförĂ€nderlig frĂ„n anvĂ€ndarutrymmet (denna operation kan inte Ă„ngras)BPF_MAP_LOOKUP_BATCH,BPF_MAP_LOOKUP_AND_DELETE_BATCH,BPF_MAP_UPDATE_BATCH,BPF_MAP_DELETE_BATCH: massoperationer. Till exempel,BPF_MAP_LOOKUP_AND_DELETE_BATCHâ det Ă€r det enda pĂ„litliga sĂ€ttet att lĂ€sa och nollstĂ€lla alla vĂ€rden i en karta
Inte alla dessa kommandon fungerar för alla typer av kartor, men generellt sett ser arbetet med andra typer av kartor frÄn anvÀndarutrymmet precis likadant ut som arbetet med hash-tabeller.
För ordningens skull, lÄt oss avsluta vÄra experiment med hash-tabellen. Kom ihÄg att vi skapade en tabell som kan innehÄlla upp till fyra nycklar? LÄt oss lÀgga till nÄgra fler element:
$ 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 0Hittills 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
Hittade 4 elementLÄt oss försöka lÀgga till en till:
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too longSom förvÀntat lyckades vi inte. LÄt oss titta nÀrmare pÄ felet:
$ 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 +++Allt i ordning: som förvÀntat försöker kommandot BPF_MAP_UPDATE_ELEM skapa en ny, femte nyckel, men misslyckas med E2BIG.
SĂ„, vi kan skapa och ladda upp BPF-program samt skapa och hantera maps frĂ„n anvĂ€ndarutrymmet. Nu Ă€r det logiskt att titta pĂ„ hur vi kan anvĂ€nda maps frĂ„n sjĂ€lva BPF-programmen. Vi skulle kunna berĂ€tta om detta pĂ„ ett sĂ€tt som Ă€r svĂ„rt att lĂ€sa med maskinkod-makron, men det Ă€r dags att visa hur man faktiskt skriver och underhĂ„ller BPF-program â med hjĂ€lp av libbpf.
(För lÀsare som Àr missnöjda med avsaknaden av en lÄgnivÄexempel: vi kommer att gÄ igenom program som anvÀnder maps och hjÀlpfunktioner skapade med hjÀlp av libbpf och berÀtta vad som hÀnder pÄ instruktionernas nivÄ. För lÀsare som Àr missnöjda med mycket, vi har lagt till i den relevanta delen av artikeln.)
Skriva BPF-program med libbpf
Att skriva BPF-program med maskinkod kan verka intressant till en början, men sen kommer mÀttnaden. I det ögonblicket bör man rikta sin uppmÀrksamhet mot llvm, som har en backend för kodgenerering för BPF-arkitekturen, samt pÄ biblioteket libbpf, vilket lÄter en skriva den anvÀndardel av BPF-applikationer och ladda upp BPF-programkoden som genererats med hjÀlp av llvm/clang.
I sjĂ€lva verket, som vi kommer att se i denna och kommande artiklar, libbpf gör ganska mycket arbete och utan det (eller liknande verktyg â iproute2, libbcc, libbpf-go, osv.) gĂ„r det inte att leva. En av killer-funktionerna i projektet libbpf Ă€r BPF CO-RE (Compile Once, Run Everywhere) â ett projekt som möjliggör att skriva BPF-program som Ă€r portabla frĂ„n en kĂ€rna till en annan, med möjlighet till körning pĂ„ olika API (till exempel nĂ€r kĂ€rnans struktur Ă€ndras frĂ„n version till version). För att kunna arbeta med CO-RE mĂ„ste din kĂ€rna vara kompilerad med stöd för BTF (hur man gör detta beskrivs i avsnittet . Man kan enkelt kontrollera om ens kĂ€rna Ă€r byggd med BTF eller inte â genom att kolla pĂ„ följande fil:
$ ls -lh /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 2.6M Jul 29 15:30 /sys/kernel/btf/vmlinuxDenna fil innehĂ„ller information om alla datatyper som anvĂ€nds i kĂ€rnan och anvĂ€nds i alla vĂ„ra exempel som anvĂ€nder libbpf. Vi kommer att gĂ„ in pĂ„ CO-RE mer detaljerat i nĂ€sta artikel, men i denna â bygg bara din kĂ€rna med CONFIG_DEBUG_INFO_BTF.
Bibliotek libbpf som finns direkt i katalogen tools/lib/bpf i kÀrnan och dess utveckling sker via mailinglistan bpf@vger.kernel.org. Men för behov av applikationer som lever utanför kÀrnan stöds ett separat lager. dÀr kÀrnan biblioteket speglas för lÀsÄtkomst mer eller mindre som det Àr.
I det hÀr avsnittet kommer vi att titta pÄ hur man kan skapa ett projekt som anvÀnder libbpf, vi kommer att skriva nÄgra (mer eller mindre meningslösa) testprogram och vi kommer att gÄ igenom hur allt fungerar. Detta kommer att hjÀlpa oss att i nÀsta avsnitt enklare förklara hur BPF-program interagerar med maps, kernel helpers, BTF osv.
Vanligtvis lÀgger projekt som anvÀnder libbpf ett GitHub-repo som ett git-submodul, lÄt oss göra det ocksÄ:
$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Initierad tom Git-repo i /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Klonar in i '/tmp/libbpf-example/libbpf'...
remote: Enumererar objekt: 200, klart.
remote: RĂ€kning av objekt: 100% (200/200), klart.
remote: Komprimerar objekt: 100% (103/103), klart.
remote: Totalt 3354 (delta 101), ÄteranvÀnd 118 (delta 79), packÄteranvÀnd 3154
Mottar objekt: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, klart.
Löser deltan: 100% (2176/2176), klart.Byggs libbpf vÀldigt 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.pcVÄr vidare plan i detta avsnitt Àr följande: vi kommer att skriva ett BPF-program av typen BPF_PROG_TYPE_XDP, samma som i det föregÄende exemplet, men i C, kompilera det med clang, och skriva ett hjÀlpprogram som kommer att ladda det i kÀrnan. I följande avsnitt kommer vi att utöka funktionaliteten bÄde i BPF-programmet och hjÀlpprogrammet.
Exempel: skapa en komplett applikation med libbpf
Till att börja med kommer vi att anvÀnda filen /sys/kernel/btf/vmlinux, som omnÀmndes ovan, och skapa dess motsvarande version som en headerfil:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.hI den hÀr filen kommer alla datatyper som finns i vÄr kÀrna att lagras, till exempel definieras IPv4-headern i kÀrnan sÄ hÀr:
$ 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 kommer vi att skriva vÄrt BPF-program i C:
$ cat xdp-simple.bpf.c
#include "vmlinux.h"
#include
SEC("xdp/simple")
int simple(void *ctx)
{
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";Ăven om vĂ„rt program blev vĂ€ldigt enkelt, mĂ„ste vi Ă€ndĂ„ uppmĂ€rksamma mĂ„nga detaljer. För det första Ă€r den första headerfilen vi inkluderar vmlinux.h, som vi just har genererat med hjĂ€lp av bpftool btf dump â nu behöver vi inte installera paketet kernel-headers för att ta reda pĂ„ hur kĂ€rnans strukturer ser ut. NĂ€sta headerfil kommer frĂ„n biblioteket libbpf. Just nu behöver vi det bara för att definiera makrot SEC, som skickar symbolen till rĂ€tt sektion i ELF-objektfilen. VĂ„rt program ligger i sektionen xdp/simple, dĂ€r vi före snedstrecket definierar typen av BPF-program â detta Ă€r en konvention som anvĂ€nds i libbpf, baserat pĂ„ sektionens namn kommer det att stĂ€lla in rĂ€tt typ vid körning bpf(2). SjĂ€lva BPF-programmet i C â Ă€r mycket enkelt och bestĂ„r av en rad return XDP_PASS. Slutligen innehĂ„ller den separata sektionen "license" namnet pĂ„ licensen.
Vi kan kompilera vÄrt program med hjÀlp av llvm/clang, version >= 10.0.0, helst Ànnu högre (se avsnittet ):
$ 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.oBland de intressanta funktionerna specificerar vi mÄarchitekturen -target bpf och vÀgen till headerfilerna libbpf, som vi nyligen har installerat. Kom ocksÄ ihÄg -O2, utan det hÀr alternativet kan du förvÀnta dig överraskningar senare. LÄt oss titta pÄ vÄr kod, lyckades vi skriva det program vi ville?
$ 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 :
0: r0 = 2
1: exitJa, det lyckades! Nu har vi en binĂ€rfil med programmet, och vi vill skapa en applikation som kommer att ladda den in i kĂ€rnan. För detta erbjuder biblioteket libbpf oss olika alternativ â antingen anvĂ€nda ett lĂ€gre nivĂ„-API eller ett högre nivĂ„-API. Vi vĂ€ljer den andra vĂ€gen, eftersom vi vill lĂ€ra oss att skriva, ladda och ansluta BPF-program med minimal anstrĂ€ngning för vidare studier.
För att börja med mĂ„ste vi generera en "skelett" av vĂ„rt program frĂ„n dess binĂ€rfil med hjĂ€lp av samma verktyg bpftool â den schweiziska armĂ©n BPF (vilket kan förstĂ„s bokstavligt, eftersom Daniel Borkman Ă€r en av skaparna och underhĂ„llarna av BPF â schweizare):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.hI filen xdp-simple.skel.h innehĂ„ller binĂ€rkoden för vĂ„rt program samt funktioner för att hantera â ladda, ansluta, radera vĂ„rt objekt. I vĂ„rt enkla fall verkar detta överdrivet, men det fungerar Ă€ven nĂ€r objektfilen innehĂ„ller mĂ„nga BPF-program och kartor; för att ladda denna gigantiska ELF rĂ€cker det med att generera skelettet och kalla pĂ„ en till tvĂ„ funktioner frĂ„n anvĂ€ndarapplikationen, som vi nu kommer att skriva.
Faktiskt Àr vÄrt laddningsprogram trivialt:
#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);
}HÀr struct xdp_simple_bpf definieras i filen xdp-simple.skel.h och beskriver vÄr 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 hÀr mÀrka spÄr av lÄgnivÄ-API:et: strukturen struct bpf_program *simple och struct bpf_link *simple. Den första strukturen beskriver specifikt vÄrt program, som Àr skrivet i sektionen xdp/simple, och den andra beskriver hur programmet kopplas till hÀndelsekÀllan.
Funktionen xdp_simple_bpf__open_and_load, öppnar ELF-objektet, parsar det, skapar alla strukturer och understrukturer (förutom programmet finns andra sektioner i ELF â data, read-only data, debuginformation, licens osv.), och laddar det sedan till kĂ€rnan via systemanropet bpf, vilket vi kan kontrollera genom att kompilera och köra 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) = 4LÄt oss nu titta pÄ vÄrt program med hjÀlp av bpftool. Vi hittar dess 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)och dumpa den (vi anvÀnder den förkortade formen av kommandot bpftool prog dump xlated):
# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
0: (b7) r0 = 2
1: (95) exitNÄgot nytt! Programmet skrev ut delar av vÄr kÀllkod pÄ C-sprÄket. Detta gjordes av biblioteket libbpf, som hittade debugsektionen i binÀren, kompilerade den till ett BTF-objekt, laddade det till kÀrnan med hjÀlp av BPF_BTF_LOAD, och angav sedan den erhÄllna filbeskrivaren vid laddningen av programmet genom kommandot BPG_PROG_LOAD.
Kernel Helpers
BPF-program kan köra "externa" funktioner â kernel helpers. Dessa hjĂ€lpfunktioner tillĂ„ter BPF-program att fĂ„ Ă„tkomst till kĂ€rnstrukturer, hantera maps och kommunicera med "den verkliga vĂ€rlden" â skapa perf events, hantera hĂ„rdvara (t.ex. omdirigera paket) osv.
Exempel: bpf_get_smp_processor_id
Inom ramen för paradigmet "lÀrande genom exempel", lÄt oss titta pÄ en av hjÀlp-funktionerna, bpf_get_smp_processor_id(), i filen kernel/bpf/helpers.c. Den returnerar numret pÄ den processor som kör det anropande BPF-programmet. Men vi Àr inte intresserade av dess semantik, utan av att dess implementering tar en rad:
BPF_CALL_0(bpf_get_smp_processor_id)
{
return smp_processor_id();
}Definitionerna av BPF-hjÀlpfunktioner liknar definitionerna av systemanrop i Linux. HÀr definieras exempelvis en funktion utan argument. (En funktion som tar tre argument definieras med makrot BPF_CALL_3. Det maximala antalet argument Àr fem.) Men detta Àr bara den första delen av definitionen. Den andra delen Àr definieringen av typen struct bpf_func_proto, som innehÄller en beskrivning av hjÀlpfunktionen som verifieraren förstÄ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 av hjÀlpfunktioner
För att program av viss typ ska kunna anvÀnda denna funktion mÄste de registrera den, exempelvis för typen BPF_PROG_TYPE_XDP definieras i kÀrnan funktionen xdp_func_proto, som baserat pÄ ID för hjÀlpfunktionen avgör om XDP stöder denna funktion eller inte. VÄr funktion :
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;
...
}
}Nya typer av BPF-program "definieras" i filen med makrot BPF_PROG_TYPE. Definieras Äberopas inom citat, eftersom det Àr en logisk definition, och i termer av C-sprÄket sker definitionen av en uppsÀttning specifika strukturer pÄ andra stÀllen. I synnerhet i filen kernel/bpf/verifier.c anvÀnds alla definitioner frÄn filen bpf_types.h för att skapa en array av 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
#undef BPF_PROG_TYPE
};Det vill sÀga, för varje typ av BPF-program definieras en pekare till datatypens struktur. struct bpf_verifier_ops, som initialiseras till vÀrdet _name ## _verifier_ops, det vill sÀga, xdp_verifier_ops för xdp. Strukturen xdp_verifier_ops i filen net/core/filter.c pÄ följande sÀtt:
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,
};HÀr ser vi vÄr bekanta funktion xdp_func_proto, som kommer att köras av verifier varje gÄng den stöter pÄ ett anrop nÄgon funktion inom BPF-programmet, se. .
LÄt oss titta pÄ hur ett hypotetiskt BPF-program anvÀnder funktionen bpf_get_smp_processor_id. För detta syfte omskriver vi programmet frÄn vÄrt föregÄende avsnitt pÄ följande sÀtt:
#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";Symbolen bpf_get_smp_processor_id pÄ <bpf/bpf_helper_defs.h> bibliotek libbpf som
static u32 (*bpf_get_smp_processor_id)(void) = (void *) 8;det vill sĂ€ga, bpf_get_smp_processor_id â det Ă€r en pekare till funktionen, vars vĂ€rde Ă€r 8, dĂ€r 8 Ă€r vĂ€rdet BPF_FUNC_get_smp_processor_id av typen enum bpf_fun_id, vilket definieras för oss i filen vmlinux.h (filen bpf_helper_defs.h i kĂ€rnan genereras med ett skript, dĂ€rför Ă€r "magiska" nummer ok). Den hĂ€r funktionen tar inga argument och returnerar ett vĂ€rde av typen __u32. NĂ€r vi kör den i vĂ„rt program, clang genererar den en instruktion BPF_CALL av "rĂ€tt typ". LĂ„t oss kompilera programmet och titta pĂ„ sektionen 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: filformat elf64-bpf
Dissassembly av sektionen 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 exitI den första raden ser vi instruktionen call, parametern IMM som Ă€r lika med 8, och SRC_REG Ă€r noll. Enligt ABI-överenskommelsen som verifier anvĂ€nds, Ă€r detta anropet till hjĂ€lpfunktionen med nummer Ă„tta. NĂ€r den körs Ă€r logiken enkel. Det returnerade vĂ€rdet frĂ„n registret r0 kopieras till r1 och pĂ„ raderna 2,3 omvandlas det till typen u32 â de övre 32 bitarna nollstĂ€lls. PĂ„ raderna 4,5,6,7 returnerar vi 2 (XDP_PASS) eller 1 (XDP_DROP) beroende pĂ„ om hjĂ€lpfunktionen pĂ„ rad 0 returnerade ett nollvĂ€rde eller ett icke-nollvĂ€rde.
LÄt oss kontrollera oss sjÀlva: ladda upp programmet och titta pÄ utsikten 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
4: (b7) r0 = 2
; }
5: (15) if r1 == 0x0 goto pc+1
6: (b7) r0 = 1
7: (95) exitBra jobbat, verifier har hittat rÀtt kernel-helper.
Exempel: vi skickar argument och till sist kör programmet!
Alla hjÀlpfunktioner pÄ exekveringsnivÄ har en prototyp
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)Parametrarna skickas till hjĂ€lpfunktionerna via register r1âr5, och vĂ€rdet returneras i registret r0. Det finns inga funktioner som tar emot mer Ă€n fem argument â och stöd för att lĂ€gga till dem förvĂ€ntas inte i framtiden.
LÄt oss titta pÄ den nya kernel-helpern och hur BPF skickar parametrar. Vi skriver om xdp-simple.bpf.c pÄ följande sÀtt (övriga rader har inte Àndrats):
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}VÄrt program skriver ut numret pÄ CPU:n det körs pÄ. Vi ska kompilera det och titta pÄ koden:
$ llvm-objdump -D --section=xdp/simple --no-show-raw-insn xdp-simple.bpf.o
0000000000000000 :
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: exitI raderna 0-7 skriver vi pĂ„ stacken strĂ€ngen running on CPU%un, och sedan pĂ„ rad 8 kör vi den bekanta bpf_get_smp_processor_id. I raderna 9-12 förbereder vi argumenten till hjĂ€lpfunktionen bpf_printk â register r1, r2, r3. Varför Ă€r de tre, och inte tvĂ„? För att bpf_printk â kring den verkliga hjĂ€lpfunktionen bpf_trace_printk, som krĂ€ver att storleken pĂ„ formatstrĂ€ngen skickas.
LÄt oss nu lÀgga till nÄgra rader till xdp-simple.c, sÄ att vÄrt program kopplar upp sig mot grÀnssnittet lo och körs pÄ riktigt!
$ cat xdp-simple.c
#include
#include
#include
#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);
}HÀr anvÀnder vi funktionen bpf_set_link_xdp_fd, som kopplar BPF-program av typen XDP till nÀtverksgrÀnssnitt. Vi har hÄrdkodad grÀnssnittets nummer. lo, som alltid Àr lika med 1. Vi kör funktionen tvÄ gÄnger för att först koppla bort det gamla programmet om det var anslutet. Observera att vi nu inte behöver ett samtal pause eller en oÀndlig loop: vÄrt laddningsprogram kommer att avslutas, men BPF-programmet kommer inte att förstöras eftersom det Àr anslutet till hÀndelsekÀllan. Efter en framgÄngsrik laddning och anslutning kommer programmet att köras för varje nÀtverkspaket som kommer till lo.
LÄt oss ladda programmet och titta pÄ grÀnssnittet lo:
$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp name simple tag 4fca62e77ccb43d6 gpl
$ ip l show dev lo
1: lo: 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 669Programmet vi laddade har ID 669 och samma ID ser vi pÄ grÀnssnittet lo. Vi kommer att skicka ett par paket till 127.0.0.1 (request + reply):
$ ping -c1 localhostoch nu lÄt oss titta pÄ innehÄllet i den virtuella felsökningsfilen /sys/kernel/debug/tracing/trace_pipe, dit bpf_printk skriver sina meddelanden:
# 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 CPU0TvĂ„ paket har registrerats pĂ„ lo och behandlades pĂ„ CPU0 â vĂ„rt första fullstĂ€ndiga meningsfria BPF-program har kört klart!
Det Àr vÀrt att notera att bpf_printk det inte Àr för ingenting som den skriver till felsökningsfilen: det Àr inte den bÀsta hjÀlparen att anvÀnda i produktion, men vÄrt mÄl var att visa nÄgot enkelt.
TillgÄng till mappar frÄn BPF-program
Exempel: vi anvÀnder en karta frÄn ett BPF-program
I de tidigare avsnitten lÀrde vi oss att skapa och anvÀnda kartor frÄn anvÀndarrummet, och nu ska vi titta pÄ kÀrndelen. LÄt oss börja, som vanligt, med ett exempel. LÄt oss skriva om vÄrt program xdp-simple.bpf.c pÄ följande sÀtt:
#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 början av programmet lade vi till en definitionskarta woo: det Ă€r en array av 8 element dĂ€r vĂ€rden av typen u64 (i C skulle vi definiera en sĂ„dan array som u64 woo[8]). I programmet "xdp/simple" fĂ„r vi nummer pĂ„ den aktuella processorn i variabeln key och sedan med hjĂ€lp av hjĂ€lpfunktionen bpf_map_lookup_element fĂ„r vi en pekare till motsvarande post i arrayen, som vi ökar med ett. Ăversatt till svenska: vi rĂ€knar statistiken för vilken CPU som har behandlat inkommande paket. LĂ„t oss försöka köra 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-simpleLÄt oss kontrollera att det har kopplat sig till lo och skicka lite paket:
$ ip l show dev lo
1: lo: 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; doneLÄt oss nu titta pÄ innehÄllet i arrayen:
$ 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Ă€stan alla processer har hanterats av CPU7. Det spelar ingen roll för oss, det viktiga Ă€r att programmet fungerar och att vi har förstĂ„tt hur man fĂ„r Ă„tkomst till mapparna frĂ„n BPF-program â via .
Mystisk pekare
SÄ vi kan fÄ Ätkomst frÄn BPF-programmet till mappen genom att anvÀnda anrop av typen
val = bpf_map_lookup_elem(&woo, &key);dÀr hjÀlpfunktionen ser ut som
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)men vi skickar en pekare &woo till en anonym struktur struct { ... }âŠ
Om vi tittar pÄ assembly av programmet sÄ ser vi att vÀrdet &woo faktiskt inte Àr definierat (rad 4):
llvm-objdump -D --section xdp/simple xdp-simple.bpf.o
xdp-simple.bpf.o: filformat elf64-bpf
Disassemblering av sektion xdp/simple:
0000000000000000 :
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
...och finns i omflyttningarna:
$ 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 wooMen om vi tittar pÄ det redan inlÀsta programmet ser vi en pekare till den rÀtta mappen (rad 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Ä vi kan dra slutsatsen att vid starten av vÄrt laddningsprogram har referensen &woo har blivit ersatt av nÄgot av biblioteket libbpf. Först ska vi titta pÄ utskriften för att kontrollera paketöverföringen Ät bÄda hÄll:
$ 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) = 5Vi ser att libbpf har skapat en karta woo och sedan laddade vÄrt program simple. LÄt oss titta nÀrmare pÄ hur vi laddar programmet:
- vi anropar
xdp_simple_bpf__open_and_loadfrÄn en filxdp-simple.skel.h - som anropar
xdp_simple_bpf__loadfrÄn en filxdp-simple.skel.h - som anropar
bpf_object__load_skeletonfrÄn en fillibbpf/src/libbpf.c - som anropar
bpf_object__load_xattrfrÄnlibbpf/src/libbpf.c
Den sista funktionen kommer, förutom allt annat, att anropa bpf_object__create_maps, som skapar eller öppnar befintliga maps och omvandlar dem till filhandtag. (Detta Àr dÀr vi ser BPF_MAP_CREATE i utskriften för att kontrollera paketöverföringen Ät bÄda hÄll.) Vidare anropas funktionen bpf_object__relocate , och det Àr just den som intresserar oss, eftersom vi minns att vi sÄg woo i omplacerings-tabellen. NÀr vi undersöker den hamnar vi slutligen i funktionen bpf_program__relocate, som ansvarar för :
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Är instruktion
18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 lloch ersÀtter kÀllregister med BPF_PSEUDO_MAP_FD, och den första IMM med filhandtaget för vÄr map, och om det exempelvis Àr 0xdeadbeef, sÄ fÄr vi som resultat instruktionen
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 llSÄ hÀr överförs information om maps till det specifika BPF-programmet som laddas. Observera att en map kan skapas med BPF_MAP_CREATE, eller öppnas med ID med hjÀlp av BPF_MAP_GET_FD_BY_ID.
SÄ sammanfattningsvis, algoritmen ser ut som följande: libbpf Under kompileringen skapas poster i omplacerings-tabellen för referenser till maps
- öppnar ELF-objektet, hittar alla anvÀnda maps och skapar filhandtag för dem
libbpffilhandtag laddas in i kÀrnan som en del av instruktionen- LD64
FörstĂ„r du att detta inte Ă€r allt, och vi mĂ„ste titta in i kĂ€rnan. Lyckligtvis har vi en ledtrĂ„d â vi har skrivit vĂ€rdet
i kĂ€llregistret och kan söka efter det, vilket för oss till heliga av heliga â BPF_PSEUDO_MAP_FD , dĂ€r funktionen med det karaktĂ€ristiska namnet ersĂ€tter filhandtaget med adressen till strukturen av typen kernel/bpf/verifier.cstruct 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;:
(fullstÀndiga koden hittar du). SÄ vi kan komplettera vÄr algoritm: Vid laddningen kontrollerar verifieraren korrekt anvÀndning av map och registrerar adressen till den motsvarande strukturen
- Vid laddning av ELF-binÀrfil med
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;
finns det mÄnga fler hÀndelser, men vi kommer att diskutera detta i andra artiklar. libbpf Laddar program och maps utan libbpf
Som utlovat, hÀr Àr ett exempel för lÀsare som vill veta hur man skapar och laddar ett program som anvÀnder maps utan hjÀlp.
Som som lovat, hÀr Àr ett exempel för lÀsare som vill veta hur man skapar och laddar upp ett program som anvÀnder mappar, utan hjÀlp. libbpf. Det kan vara anvÀndbart nÀr du arbetar i en miljö dÀr du inte kan installera beroenden, sparar varje bit, eller skriver ett program av typen , som genererar BPF-binarier pÄ sprÄng.
För att göra det enklare att följa logiken, kommer vi att skriva om vÄrt exempel xdp-simple. Den fullstÀndiga och nÄgot utökade koden för programmet som diskuteras i detta exempel kan du hitta i denna .
Logiken för vÄr applikation Àr följande:
- skapa en mapp av typen
BPF_MAP_TYPE_ARRAYmed kommandotBPF_MAP_CREATE, - skapa ett program som anvÀnder denna mapp,
- koppla programmet till grÀnssnittet
lo,
vilket översÀtts till mÀnskligt 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);
}HĂ€r map_create skapar en mapp precis som vi gjorde i det första exemplet om systemanropet bpf â "kĂ€rna, var snĂ€ll och gör en ny mapp som en array av 8 element av typen __u64 och returnera en filhandtag till mig":
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 laddas ocksÄ enkelt:
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 svĂ„ra delen prog_load â Ă€r definitionen av vĂ„rt BPF-program som en array av strukturer struct bpf_insn insns[]. Men eftersom vi anvĂ€nder ett program som vi har pĂ„ C kan vi fuska lite:
$ llvm-objdump -D --section xdp/simple xdp-simple.bpf.o
0000000000000000 :
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
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 :
13: bf 10 00 00 00 00 00 00 r0 = r1
14: 95 00 00 00 00 00 00 00 exitSÄ totalt sett behöver vi skriva 14 instruktioner i form av strukturer av typen struct bpf_insn (tips: ta en dump frÄn toppen, lÀs igenom avsnittet om instruktioner, öppna och och försök att bestÀmma struct bpf_insn insns[] sjÀlv):
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 */
{
.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,
},
/* : 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 övning för dem som inte skrev detta sjÀlva - hitta map_fd.
Det finns en annan okÀnd del i vÄrt program - xdp_attach. TyvÀrr kan man inte koppla program av typen XDP via systemanrop bpf. MÀnniskorna som skapade BPF och XDP kom frÄn Linux-nÀtverksgemenskapen, vilket innebar att de anvÀnde det mest bekanta för dem (men inte för normala mÀnniskor) grÀnssnittet för att interagera med kÀrnan: , se ocksÄ . Det enklaste sÀttet att implementera xdp_attach Àr att kopiera koden frÄn libbpf, nÀmligen frÄn filen , vilket vi har gjort, och lite förkortat den:
VÀlkommen till vÀrlden av netlink sockets
Vi öppnar en netlink socket av typen 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 frÄn en sÄdan socket:
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 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); för mycket kod att kopiera...
goto done;
case NLMSG_DONE:
return 0;
default:
break;
}
}
}
ret = 0;
done:
return ret;
}Till sist, hÀr Àr vÄr funktion som öppnar en socket och skickar ett speciellt meddelande till den som innehÄller en filbeskrivare:
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 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;
}Allt Àr klart för testning:
$ 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 +++LÄt oss se om vÄrt program har kopplat ihop sig med lo:
$ ip l show dev lo
1: lo: 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 160Vi skickar pingar och kollar pÄ mappen:
$ 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
Hittade 8 elementHurray, allt fungerar. Observera att vÄr mapp Äterigen visas i form av bytes. Detta hÀnder eftersom vi inte laddade information om typer (BTF). Men mer om det tar vi nÀsta gÄng. libbpf I det hÀr avsnittet kommer vi att titta pÄ den minimala uppsÀttningen verktyg för BPF-utvecklare.
Utvecklingsverktyg
I allmĂ€nhet behövs inget speciellt för att utveckla BPF-program â BPF fungerar pĂ„ vilken anstĂ€ndig distribution som helst och programmen kompileras med hjĂ€lp av
Generellt sett krĂ€vs det inget speciellt för att utveckla BPF-program â BPF fungerar pĂ„ vilken anstĂ€ndig distributionskerna som helst, och programmen byggs med hjĂ€lp av. clang, som kan installeras frĂ„n paketet. Men eftersom BPF Ă€r under utveckling Ă€ndras kĂ€rnan och verktygen stĂ€ndigt. Om du inte vill skriva BPF-program med gammaldags metoder frĂ„n 2019, mĂ„ste du bygga
llvm/clangpahole- din egen kÀrna
bpftool
(För referens: denna sektion och alla exempel i artikeln kördes pÄ Debian 10.)
llvm/clang
BPF fungerar bra med LLVM och Àven om program för BPF nyligen kan kompileras ocksÄ med gcc, pÄgÄr all nuvarande utveckling för LLVM. SÄ först kommer vi att bygga den aktuella versionen clang frÄn 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
... mÄnga timmar senare
$Nu kan vi kontrollera om allt har byggts korrekt:
$ ./bin/llc --version
LLVM (http://llvm.org/):
LLVM version 11.0.0git
Optimerad byggnad.
StandardmÄl: x86_64-unknown-linux-gnu
VĂ€rd CPU: znver1
Registrerade mÄl:
bpf - BPF (host endian)
bpfeb - BPF (big endian)
bpfel - BPF (little endian)
x86 - 32-bit X86: Pentium-Pro och uppÄt
x86-64 - 64-bit X86: EM64T och AMD64(Bygginstruktionen clang Àr tagen av mig frÄn .)
Vi kommer inte att installera nyss byggda program, utan istÀllet bara lÀgga till dem i PATH, till exempel:
export PATH="`pwd`/bin:$PATH"(Detta kan lÀggas till i .bashrc eller i en separat fil. Personligen lÀgger jag sÄdana saker i ~/bin/activate-llvm.sh och nÀr det behövs gör jag . activate-llvm.sh.)
Pahole och BTF
Verktyg pahole anvÀnds vid byggandet av kÀrnan för att skapa debug-information i BTF-format. Vi kommer inte att gÄ in pÄ detaljer kring BTF-teknologin i denna artikel, förutom att det Àr praktiskt och vi vill anvÀnda det. SÄ om du tÀnker bygga din egen kÀrna, bygg först pahole (utan pahole du kan inte bygga kÀrnan 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/paholeKÀrnor för experiment med BPF
NÀr man utforskar möjligheterna med BPF vill man gÀrna samla sitt eget kÀrna. Detta Àr i och för sig inte nödvÀndigt, eftersom du kan samla och ladda BPF-program Àven pÄ en distributionskÀrna. Men att ha sitt eget kÀrna tillÄter dig att anvÀnda de senaste BPF-funktionerna, vilka i bÀsta fall kommer att finnas i din distribution om nÄgra mÄnader, eller i vÀrsta fall, som i fallet med vissa debuggningsverktyg, inte kommer att paketeras i överskÄdlig framtid. Dessutom ger ditt eget kÀrna en kÀnsla av att kunna experimentera med koden.
För att bygga ett kĂ€rna behöver du, för det första, sjĂ€lva kĂ€rnan, och för det andra, en konfigurationsfil för kĂ€rnan. För experiment med BPF kan vi anvĂ€nda en vanlig kĂ€rna eller en av utvecklarkĂ€rnorna. Historiskt sett sker utvecklingen av BPF inom den nĂ€tverksgemenskapen i Linux, och dĂ€rför gĂ„r alla Ă€ndringar ŃĐ°ĐœĐŸ eller ĐżĐŸĐ·ĐŽĐœĐŸ ŃĐ”ŃДз David Miller â underhĂ„llare för Linux nĂ€tverksdel. Beroende pĂ„ deras karaktĂ€r â reparationar eller nya funktioner â tilldelas nĂ€tverksĂ€ndringar en av tvĂ„ kĂ€rnor â eller . Ăndringar för BPF delas pĂ„ samma sĂ€tt mellan och , som sedan skjuts ut till net och net-next, respektive. Mer information finns i och . SĂ„ vĂ€lj kĂ€rna baserat pĂ„ din smak och behov för systemstabilitet pĂ„ vilket du testar (*-next kĂ€rnorna Ă€r de mest instabila av de som nĂ€mnts).
Denna artikel omfattar inte hur man hanterar konfigurationsfiler för kĂ€rnor â det antas att du antingen redan kan göra det, eller sjĂ€lv. Men följande instruktioner bör vara tillrĂ€ckliga för att du ska fĂ„ ett fungerande system med BPF-stöd.
Ladda ner en av de ovan nÀmnda kÀrnorna:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-nextBygg en minimal fungerande konfiguration för kÀrnan:
$ cp /boot/config-`uname -r` .config
$ make localmodconfigAktivera BPF-alternativen i filen .config efter eget val (antagligen kommer sjÀlva CONFIG_BPF att redan vara aktiverad, eftersom systemd anvÀnder det). HÀr Àr en lista över alternativen frÄn kÀrnan som anvÀndes för denna 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=yDÀrefter kan vi enkelt sammanstÀlla och installera moduler och kÀrnan (för övrigt kan kÀrnan sammanstÀllas med just sammanstÀlld clang, genom att lÀgga till CC=clang):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make installoch starta om med den nya kÀrnan (jag anvÀnder kexec frÄn paketet kexec-tools):
v=5.8.0-rc6+ # om du omkompilerar den aktuella kÀrnan kan du anvÀnda v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -ebpftool
Den mest anvĂ€nda verktyget i artikeln kommer att vara verktyget bpftool, som levereras med Linux-kĂ€rnan. Det Ă€r skrivet och underhĂ„llet av BPF-utvecklarna för BPF-utvecklare och kan anvĂ€ndas för att hantera alla typer av BPF-objekt â ladda program, skapa och Ă€ndra maps, utforska livscykeln för BPF-ekosystemet, osv. Dokumentationen i form av kĂ€llkod till man-sidor kan hittas eller, redan kompilerad, .
Vid tidpunkten för skrivandet av artikeln bpftool levereras i fÀrdig form endast för RHEL, Fedora och Ubuntu (se exempelvis , dÀr den ofÀrdiga historien om paketering diskuteras bpftool i Debian). Men om du redan har sammanstÀllt din kÀrna Àr det enklare Àn nÄgonsin: bpftool $ cd ${linux}/tools/bpf/bpftool # ... ange vÀgar till senaste clang, som beskrivits ovan $ make -sAuto-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 ]$
(hÀr${linux} Àr din katalog med kÀrnan.) Efter att dessa kommandon har körts kommer det att sammanstÀllas i katalogen bpftool ${linux}/tools/bpf/bpftool och det kan lÀggas till i sökvÀgen (först och frÀmst för anvÀndaren ) eller helt enkelt kopieras till rootDet Àr bÀst att sammanstÀlla med den senaste /usr/local/sbin.
, sammanstÀlld, som beskrivits ovan, och för att kontrollera om den har sammanstÀllts korrekt kan du anvÀnda till exempel kommandot bpftool $ 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 ... clangsom visar vilka BPF-funktioner som Àr aktiverade i din kÀrna.
För övrigt kan det föregÄende kommandot köras somDetta Àr gjort pÄ analogi med verktyg frÄn paketet
Förresten kan den tidigare kommandot köras som.
# bpftool f p kDetta görs pÄ analogi med verktyg frÄn paketet. iproute2, dÀr vi kan till exempel sÀga ip a s eth0 istÀllet för ip addr show dev eth0.
Slutsats
BPF möjliggör för en fÀlla att effektivt mÀta och pÄ sprÄng förÀndra kÀrnans funktionalitet. Systemet har visat sig vara mycket framgÄngsrikt, i bÀsta UNIX-tradition: en enkel mekanism som tillÄter att (om)programmera kÀrnan, har gjort det möjligt för ett stort antal mÀnniskor och organisationer att experimentera. Och Àven om experimenten, liksom utvecklingen av sjÀlva BPF-infrastrukturen, fortfarande pÄgÄr, har systemet redan en stabil ABI som möjliggör bygget av pÄlitlig, och framför allt, effektiv affÀrslogik.
Jag vill pĂ„peka att, enligt min Ă„sikt, teknologin blivit sĂ„ populĂ€r för att den Ă„ ena sidan gör det möjligt att leka (maskinens arkitektur kan förstĂ„s mer eller mindre pĂ„ en kvĂ€ll), och Ă„ andra sidan â lösa uppgifter som inte kunde lösas (vackert) innan den introducerades. Dessa tvĂ„ komponenter tillsammans tvingar mĂ€nniskor att experimentera och drömma, vilket leder till nya och innovativa lösningar.
Denna artikel, Ă€ven om den inte Ă€r sĂ€rskilt kort, Ă€r bara en introduktion till BPF-vĂ€rlden och beskriver inte de "avancerade" möjligheterna och viktiga delar av arkitekturen. Den fortsatta planen Ă€r ungefĂ€r följande: nĂ€sta artikel kommer att vara en översikt av BPF-programtyper (i kĂ€rnan 5.8 stöds 30 programtyper), sedan kommer vi Ă€ntligen att se pĂ„ hur man skriver riktiga applikationer pĂ„ BPF genom exempel pĂ„ kĂ€rnspĂ„rningsprogram, sedan kommer det bli dags för en mer ingĂ„ende kurs om BPF-arkitekturen, och dĂ€refter â för exempel pĂ„ nĂ€tverks- och sĂ€kerhetsapplikationer för BPF.
Tidigare artiklar i denna cykel
LĂ€nkar
â dokumentation om BPF frĂ„n cilium, nĂ€rmare bestĂ€mt frĂ„n Daniel Borkman, en av skaparna och underhĂ„llarna av BPF. Detta Ă€r en av de första seriösa beskrivningarna som skiljer sig frĂ„n de andra i det avseendet att Daniel vet exakt vad han pratar om och det finns inga slarvfel. I denna dokumentation beskrivs hur man arbetar med BPF-program av typerna XDP och TC med hjĂ€lp av det vĂ€lkĂ€nda verktyget
ipfrĂ„n paketetiproute2.â den ursprungliga filen med dokumentation om klassisk, och senare utvidgad BPF. Det Ă€r anvĂ€ndbart att lĂ€sa om du vill grĂ€va i assembler och tekniska detaljer i arkitekturen.
. Uppdateras sĂ€llan, men trĂ€ffsĂ€kert, eftersom Alexei Starovoitov (skaparen av eBPF) och Andrii Nakryiko â (underhĂ„llare
libbpf).. En fascinerande twitter-trÄd av Quentin Monnet med exempel och hemligheter för anvÀndning av bpftool.
. En gigantisk (och fortfarande underhÄllen) lista med lÀnkar till dokumentation om BPF av Quentin Monnet.
KĂ€lla: habr.com
