BPF för de minsta, del ett: extended BPF

I början fanns det en teknik som kallades BPF. Vi tittade pĂ„ den i den föregĂ„ende, 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.
BPF för de minsta, del ett: extended BPF

Sammanfattning av artikeln

Introduktion till BPF:s arkitektur. Först kommer vi att se pÄ BPF:s arkitektur frÄn en fÄgelperspektiv och definiera huvudkomponenterna.

Register och kommandosystem för BPF:s virtuella maskin. Med en översikt över arkitekturen kommer vi att beskriva konstruktionen av BPF:s virtuella maskin.

Livscykeln för BPF-objekt, filsystemet bpffs. I det hĂ€r avsnittet kommer vi att titta nĂ€rmare pĂ„ livscykeln för BPF-objekt — program och mappar.

Hantering av objekt genom systemanropet bpf. 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).

Skriva BPF-program med libbpf. 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.

Kernel Helpers. 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.

TillgÄng till maps frÄn BPF-program. 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.

Utvecklingsverktyg. En referensdel om hur man samlar de nödvÀndiga verktygen och kÀrnan för experiment.

Sammanfattning. 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 den klassiska BPF, 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 för de minsta, del ett: extended BPF

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

BPF för de minsta, del ett: extended BPF

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.

BPF för de minsta, del ett: extended BPF

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 Utvecklingsverktyg 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  0x0000000000001ee8

Registret 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 struct xdp_md (för XDP) eller strukturen struct __sk_buff (för olika nĂ€tverksprogram) eller strukturen struct pt_regs (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 (nÀstan 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

BPF för de minsta, del ett: extended BPF

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:

BPF för de minsta, del ett: extended BPF

Instruktionsklasser 0, 1, 2, 3 definierar kommandon för att arbeta med minnet. De kallas, 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 bpf.h och bpf_common.h, 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: Unofficial eBPF spec, BPF and XDP Reference Guide, Instruction Set, Documentation/networking/filter.txt 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 00000000

Kommandokoderna À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    0

Operation b klassen ALU64 — detta Ă€r BPF_MOV. 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 BPF_JEQ (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 BPF_EXIT. 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      exit

Vi kan skriva om detta pÄ ett mer bekvÀmt sÀtt:

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

Om 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 = BPF_LD | BPF_DW | BPF_IMM) — 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                      exit

Vi 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 exit

Livscykeln för BPF-objekt, filsystemet bpffs

(Vissa detaljer som beskrivs i detta avsnitt fick jag veta för första gÄngen frÄn inlÀgget Alexei Starovoitov pÄ BPF Blog.)

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:

BPF för de minsta, del ett: extended BPF

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.

BPF för de minsta, del ett: extended BPF

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.

BPF för de minsta, del ett: extended BPF

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-mountpoint

Namnen 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-mountpoint

Nu 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) = 0

HÀ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 4096B

Vi 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 directory

Borttagning 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 exempel 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 enum bpf_cmd, 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 definition 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 om livscykeln för BPF-objekt..

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 typ 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
exit

NÀ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 . Det första elementetmotsvarar 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) exit

utan ö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:   retq

inte 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 <linux/bpf.h>, 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 elements

Tomt. 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 0

LÄt oss granska tabellen igen:

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

Hurrra! 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 ENOENT

Fö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) = 0

Som 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 följande:

  • BPF_MAP_LOOKUP_ELEM: hitta vĂ€rde efter nyckel
  • BPF_MAP_UPDATE_ELEM: uppdatera/skapa vĂ€rde
  • BPF_MAP_DELETE_ELEM: ta bort nyckel
  • BPF_MAP_GET_NEXT_KEY: hitta nĂ€sta (eller första) nyckel
  • BPF_MAP_GET_NEXT_ID: gör det möjligt att iterera över alla befintliga kartor, sĂ„ hĂ€r fungerar bpftool map
  • BPF_MAP_GET_FD_BY_ID: öppna en befintlig karta via dess globala ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomĂ€rt uppdatera objektets vĂ€rde och returnera det gamla
  • BPF_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 0

Hittills 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 element

LÄ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 long

Som 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 exempel 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 Utvecklingsverktyg. 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/vmlinux

Denna 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. https://github.com/libbpf/libbpf 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.pc

VÄ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.h

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

$ 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

Bland 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:       exit

Ja, 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.h

I 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) = 4

LÄ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) exit

NÄ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(), bestÀmd 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 att:

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 include/linux/bpf_types.h 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 definieras 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. verifier.c.

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 definieras 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 exit

I 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) exit

Bra 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:       exit

I 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 — det Ă€r en makroomslagning 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 669

Programmet 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 localhost

och 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 CPU0

TvĂ„ 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-simple

LÄ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; done

LÄ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 hjĂ€lpare bpf_mp_*.

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 woo

Men 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) = 5

Vi 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_load frĂ„n en fil xdp-simple.skel.h
  • som anropar xdp_simple_bpf__load frĂ„n en fil xdp-simple.skel.h
  • som anropar bpf_object__load_skeleton frĂ„n en fil libbpf/src/libbpf.c
  • som anropar bpf_object__load_xattr frĂ„n libbpf/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 omplaceringen av maps:

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 ll

och 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 ll

SÄ 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
  • libbpf filhandtag 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: via lÀnkenVid 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 ply, 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 gist.

Logiken för vÄr applikation Àr följande:

  • skapa en mapp av typen BPF_MAP_TYPE_ARRAY med kommandot BPF_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 exit

SÄ 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 linux/bpf.h och linux/bpf_common.h 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: netlink sockets, se ocksÄ RFC3549. Det enklaste sÀttet att implementera xdp_attach Àr att kopiera koden frÄn libbpf, nÀmligen frÄn filen netlink.c, 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 160

Vi 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 element

Hurray, 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/clang
  • pahole
  • 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 bpf_devel_QA.)

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/pahole

KÀ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 vanilj 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 — net eller net-next. Ändringar för BPF delas pĂ„ samma sĂ€tt mellan bpf och bpf-next, som sedan skjuts ut till net och net-next, respektive. Mer information finns i bpf_devel_QA och netdev-FAQ. 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 Ă€r redo att lĂ€ra dig 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-next

Bygg en minimal fungerande konfiguration för kÀrnan:

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

Aktivera 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=y

DÀ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 install

och 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 -e

bpftool

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 i kĂ€rnan eller, redan kompilerad, pĂ„ nĂ€tet.

Vid tidpunkten för skrivandet av artikeln bpftool levereras i fÀrdig form endast för RHEL, Fedora och Ubuntu (se exempelvis denna trÄd, 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 som

Detta Àr gjort pÄ analogi med verktyg frÄn paketet

Förresten kan den tidigare kommandot köras som.

# bpftool f p k

Detta 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

  1. BPF för de minsta, del noll: klassisk BPF

LĂ€nkar

  1. BPF and XDP Reference Guide — 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 ip frĂ„n paketet iproute2.

  2. Documentation/networking/filter.txt — 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.

  3. Blogg om BPF frĂ„n Facebook. Uppdateras sĂ€llan, men trĂ€ffsĂ€kert, eftersom Alexei Starovoitov (skaparen av eBPF) och Andrii Nakryiko — (underhĂ„llare libbpf).

  4. Hemligheterna i bpftool. En fascinerande twitter-trÄd av Quentin Monnet med exempel och hemligheter för anvÀndning av bpftool.

  5. Dyk ner i BPF: en lista med lÀsmaterial. En gigantisk (och fortfarande underhÄllen) lista med lÀnkar till dokumentation om BPF av Quentin Monnet.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster