BPF för de minsta, del ett: utökad BPF

I början fanns det en teknik och den hette BPF. Vi tittade på henne tidigare, Gamla testamentets artikel i denna serie. Under 2013, genom ansträngningar av Alexei Starovoitov och Daniel Borkman, utvecklades en förbättrad version av den, optimerad för moderna 64-bitarsmaskiner, och inkluderades i Linux-kärnan. Den här nya tekniken kallades kort för Intern BPF, döptes sedan om till Extended BPF, och nu, efter flera år, kallar alla den helt enkelt BPF.

Grovt sett låter BPF dig köra godtycklig kod från användaren i Linux-kärnutrymmet, och den nya arkitekturen visade sig vara så framgångsrik att vi kommer att behöva ytterligare ett dussin artiklar för att beskriva alla dess applikationer. (Det enda som utvecklarna inte gjorde bra, som du kan se i prestandakoden nedan, var att skapa en anständig logotyp.)

Den här artikeln beskriver strukturen för den virtuella BPF-maskinen, kärngränssnitt för att arbeta med BPF, utvecklingsverktyg, samt en kort, mycket kort översikt över befintliga möjligheter, d.v.s. allt som vi kommer att behöva i framtiden för en djupare studie av de praktiska tillämpningarna av BPF.
BPF för de minsta, del ett: utökad BPF

Sammanfattning av artikeln

Introduktion till BPF-arkitektur. Först tar vi ett fågelperspektiv av BPF-arkitekturen och skisserar huvudkomponenterna.

Register och kommandosystem för den virtuella BPF-maskinen. Redan med en uppfattning om arkitekturen som helhet kommer vi att beskriva strukturen för den virtuella BPF-maskinen.

Livscykel för BPF-objekt, bpffs-filsystem. I det här avsnittet ska vi titta närmare på livscykeln för BPF-objekt - program och kartor.

Hantera objekt med bpf-systemanropet. Med viss förståelse för systemet redan på plats kommer vi äntligen att titta på hur man skapar och manipulerar objekt från användarutrymmet med hjälp av ett speciellt systemanrop − bpf(2).

Пишем программы BPF с помощью libbpf. Naturligtvis kan du skriva program med hjälp av ett systemanrop. Men det är svårt. För ett mer realistiskt scenario utvecklade kärnkraftsprogrammerare ett bibliotek libbpf. Vi kommer att skapa ett grundläggande BPF-applikationsskelett som vi kommer att använda i efterföljande exempel.

Kärnhjälpare. Här kommer vi att lära oss hur BPF-program kan komma åt kärnhjälparfunktioner - ett verktyg som tillsammans med kartor i grunden utökar kapaciteten hos den nya BPF jämfört med den klassiska.

Tillgång till kartor från BPF-program. Vid det här laget kommer vi att veta tillräckligt för att förstå exakt hur vi kan skapa program som använder kartor. Och låt oss till och med ta en snabb titt på den stora och mäktiga verifieraren.

Utvecklings verktyg. Hjälpavsnitt om hur man monterar de nödvändiga verktygen och kärnan för experiment.

Slutsats. I slutet av artikeln hittar de som läser så här långt motiverande ord och en kort beskrivning av vad som kommer att hända i följande artiklar. Vi kommer även att lista ett antal länkar för självstudier för den som inte har lust eller förmåga att vänta på fortsättningen.

Introduktion till BPF-arkitektur

Innan vi börjar överväga BPF-arkitekturen kommer vi att hänvisa en sista gång (oh) till klassisk BPF, som utvecklades som ett svar på tillkomsten av RISC-maskiner och löste problemet med effektiv paketfiltrering. Arkitekturen visade sig vara så framgångsrik att den, efter att ha fötts på det häftiga nittiotalet i Berkeley UNIX, portades till de flesta befintliga operativsystem, överlevde in i det galna tjugotalet och fortfarande hittar nya applikationer.

Den nya BPF utvecklades som ett svar på alla 64-bitars maskiner, molntjänster och det ökade behovet av verktyg för att skapa SDN (Softa-dförfinad nnätverk). Utvecklad av kärnnätverksingenjörer som en förbättrad ersättning för den klassiska BPF, hittade den nya BPF bokstavligen sex månader senare applikationer i den svåra uppgiften att spåra Linux-system, och nu, sex år efter dess utseende, kommer vi att behöva en hel nästa artikel bara för att lista de olika typerna av program.

Roliga bilder

I kärnan är BPF en virtuell sandlådemaskin som låter dig köra "godtycklig" kod i kärnutrymmet utan att kompromissa med säkerheten. BPF-program skapas i användarutrymmet, laddas in i kärnan och kopplas till någon händelsekälla. En händelse kan till exempel vara leverans av ett paket till ett nätverksgränssnitt, lansering av någon kärnfunktion osv. I fallet med ett paket kommer BPF-programmet att ha tillgång till paketets data och metadata (för att läsa och eventuellt skriva, beroende på typ av program); i fallet med att köra en kärnfunktion, argumenten för funktionen, inklusive pekare till kärnminne, etc.

Låt oss ta en närmare titt på denna process. Till att börja med, låt oss prata om den första skillnaden från den klassiska BPF, program för vilka skrivs i assembler. I den nya versionen utökades arkitekturen så att program kunde skrivas på högnivåspråk, i första hand givetvis i C. För detta utvecklades en backend för llvm som gör att man kan generera bytekod för BPF-arkitekturen.

BPF för de minsta, del ett: utökad BPF

BPF-arkitekturen designades delvis för att fungera effektivt på moderna maskiner. För att få detta att fungera i praktiken översätts BPF-bytekoden, när den väl har laddats in i kärnan, till inbyggd kod med hjälp av en komponent som kallas en JIT-kompilator (Jövre In Time). Därefter, om du kommer ihåg, i klassisk BPF laddades programmet in i kärnan och kopplades till händelsekällan atomärt - i samband med ett enda systemanrop. I den nya arkitekturen sker detta i två steg - först läses koden in i kärnan med hjälp av ett systemanrop bpf(2)och sedan, senare, genom andra mekanismer som varierar beroende på typ av program, kopplas programmet till händelsekällan.

Här kan läsaren ha en fråga: var det möjligt? Hur garanteras exekveringssäkerheten för sådan kod? Utförandesäkerhet garanteras för oss genom att ladda BPF-program som kallas verifier (på engelska kallas detta steg för verifier och jag kommer att fortsätta att använda det engelska ordet):

BPF för de minsta, del ett: utökad BPF

Verifier är en statisk analysator som säkerställer att ett program inte stör den normala driften av kärnan. Detta betyder förresten inte att programmet inte kan störa systemets funktion - BPF-program, beroende på typen, kan läsa och skriva om delar av kärnminnet, returnera värden för funktioner, trimma, lägga till, skriva om och även vidarebefordra nätverkspaket. Verifier garanterar att körning av ett BPF-program inte kommer att krascha kärnan och att ett program som enligt reglerna har skrivbehörighet, till exempel data från ett utgående paket, inte kommer att kunna skriva över kärnminnet utanför paketet. Vi kommer att titta på verifierare lite mer i detalj i motsvarande avsnitt, efter att vi har bekantat oss med alla andra komponenter i BPF.

Så vad har vi lärt oss hittills? Användaren skriver ett program i C, laddar det i kärnan med hjälp av ett systemanrop bpf(2), där den kontrolleras av en verifierare och översätts till inbyggd bytekod. Sedan ansluter samma eller annan användare programmet till händelsekällan och det börjar köras. Att separera start och anslutning är nödvändigt av flera skäl. För det första är det relativt dyrt att köra en verifierare och genom att ladda ner samma program flera gånger slösar vi bort datortid. För det andra, exakt hur ett program är anslutet beror på dess typ, och ett "universellt" gränssnitt som utvecklades för ett år sedan kanske inte är lämpligt för nya typer av program. (Även om nu när arkitekturen blir mer mogen, finns det en idé att förena detta gränssnitt på nivån libbpf.)

Den uppmärksamma läsaren kanske märker att vi inte är färdiga med bilderna än. Allt ovanstående förklarar faktiskt inte varför BPF fundamentalt förändrar bilden jämfört med klassisk BPF. Två innovationer som avsevärt utökar tillämpningsområdet är möjligheten att använda delat minne och kärnhjälparfunktioner. I BPF implementeras delat minne med hjälp av så kallade maps – delade datastrukturer med ett specifikt API. De fick förmodligen detta namn eftersom den första typen av karta som visades var en hashtabell. Sedan dök upp arrayer, lokala (per-CPU) hashtabeller och lokala arrayer, sökträd, kartor som innehåller pekare till BPF-program och mycket mer. Det som är intressant för oss nu är att BPF-program nu har förmågan att bestå tillstånd mellan samtal och dela det med andra program och med användarutrymme.

Kartor nås från användarprocesser med hjälp av ett systemanrop bpf(2), och från BPF-program som körs i kärnan med hjälp av hjälpfunktioner. Dessutom finns hjälpare inte bara för att arbeta med kartor, utan också för att komma åt andra kärnfunktioner. Till exempel kan BPF-program använda hjälpfunktioner för att vidarebefordra paket till andra gränssnitt, generera perf-händelser, komma åt kärnstrukturer och så vidare.

BPF för de minsta, del ett: utökad BPF

Sammanfattningsvis ger BPF möjligheten att ladda godtycklig, det vill säga verifiertestad, användarkod till kärnutrymmet. Denna kod kan spara tillstånd mellan samtal och utbyta data med användarutrymme, och har även tillgång till kärndelsystem som tillåts av denna typ av program.

Detta liknar redan de funktioner som kärnmodulerna tillhandahåller, jämfört med vilka BPF har vissa fördelar (naturligtvis kan du bara jämföra liknande applikationer, till exempel systemspårning - du kan inte skriva en godtycklig drivrutin med BPF). Du kan notera en lägre ingångströskel (vissa verktyg som använder BPF kräver inte att användaren har kärnprogrammeringskunskaper, eller programmeringskunskaper i allmänhet), körtidssäkerhet (räcker upp handen i kommentarerna för de som inte bröt systemet när du skrev eller testmoduler), atomicitet - det finns driftstopp när moduler laddas om, och BPF-delsystemet säkerställer att inga händelser missas (för att vara rättvis gäller detta inte för alla typer av BPF-program).

Närvaron av sådana kapaciteter gör BPF till ett universellt verktyg för att utöka kärnan, vilket bekräftas i praktiken: fler och fler nya typer av program läggs till BPF, fler och fler stora företag använder BPF på stridsservrar 24×7, mer och mer startups bygger sin verksamhet på lösningar som är baserade på BPF. BPF används överallt: för att skydda mot DDoS-attacker, skapa SDN (till exempel implementera nätverk för kubernetes), som det huvudsakliga systemspårningsverktyget och statistiksamlaren, i intrångsdetekteringssystem och sandlådesystem, etc.

Låt oss avsluta översiktsdelen av artikeln här och titta på den virtuella maskinen och BPF-ekosystemet mer detaljerat.

Utvikning: verktyg

För att kunna köra exemplen i följande avsnitt kan du behöva åtminstone ett antal verktyg llvm/clang med bpf-stöd och bpftool. I avsnittet Utvecklings verktyg Du kan läsa instruktionerna för montering av verktygen, såväl som din kärna. Detta avsnitt är placerat nedan för att inte störa harmonin i vår presentation.

BPF virtuella maskinregister och instruktionssystem

Arkitekturen och kommandosystemet för BPF utvecklades med hänsyn till det faktum att program kommer att skrivas på C-språket och, efter att ha laddats in i kärnan, översatta till inbyggd kod. Därför valdes antalet register och uppsättningen kommandon med tanke på skärningspunkten, i matematisk mening, mellan moderna maskiners kapacitet. Dessutom har olika restriktioner införts för program, till exempel var det tills nyligen inte möjligt att skriva loopar och subrutiner, och antalet instruktioner begränsades till 4096 (nu kan privilegierade program ladda upp till en miljon instruktioner).

BPF har elva användartillgängliga 64-bitarsregister r0-r10 och en programräknare. Registrera r10 innehåller en rampekare och är skrivskyddad. Program har tillgång till en 512-byte stack vid körning och en obegränsad mängd delat minne i form av kartor.

BPF-program tillåts köra en specifik uppsättning kärnanhjälpare av programtyp och, på senare tid, vanliga funktioner. Varje anropad funktion kan ta upp till fem argument som skickas i register r1-r5, och returvärdet skickas till r0. Det är garanterat att efter att ha återvänt från funktionen, innehållet i registren r6-r9 Kommer inte att förändras.

För effektiv programöversättning, register r0-r11 för alla arkitekturer som stöds är unikt mappade till verkliga register, med hänsyn till ABI-funktionerna i den aktuella arkitekturen. Till exempel för x86_64 register r1-r5, som används för att skicka funktionsparametrar, visas på rdi, rsi, rdx, rcx, r8, som används för att skicka parametrar till funktioner på x86_64. Till exempel översätts koden till vänster till koden till höger så här:

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

Registrera r0 används också för att returnera resultatet av programkörning och i registret r1 programmet skickas en pekare till sammanhanget - beroende på typ av program kan detta till exempel vara en struktur struct xdp_md (för XDP) eller struktur struct __sk_buff (för olika nätverksprogram) eller struktur struct pt_regs (för olika typer av spårningsprogram) osv.

Så vi hade en uppsättning register, kärnhjälpare, en stack, en kontextpekare och delat minne i form av kartor. Inte för att allt detta är absolut nödvändigt på resan, men...

Låt oss fortsätta beskrivningen och prata om kommandosystemet för att arbeta med dessa objekt. Allt (Nästan alla) BPF-instruktioner har en fast 64-bitars storlek. Om du tittar på en instruktion på en 64-bitars Big Endian-maskin kommer du att se

BPF för de minsta, del ett: utökad BPF

Här Code - detta är kodningen av instruktionen, Dst/Src är mottagarens och källans kodningar, Off - 16-bitars signerad indrag, och Imm är ett 32-bitars heltal med tecken som används i vissa instruktioner (liknande cBPF-konstanten K). Kodning Code har en av två typer:

BPF för de minsta, del ett: utökad BPF

Instruktionsklasserna 0, 1, 2, 3 definierar kommandon för att arbeta med minne. De kallas, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respektive. Klass 4, 7 (BPF_ALU, BPF_ALU64) utgör en uppsättning ALU-instruktioner. Klass 5, 6 (BPF_JMP, BPF_JMP32) innehåller hoppinstruktioner.

Den ytterligare planen för att studera BPF-instruktionssystemet är som följer: istället för att noggrant lista alla instruktioner och deras parametrar, kommer vi att titta på ett par exempel i detta avsnitt och från dem kommer det att bli tydligt hur instruktionerna faktiskt fungerar och hur man manuellt demontera alla binära filer för BPF. För att konsolidera materialet längre fram i artikeln kommer vi även att möta individuella instruktioner i avsnitten om Verifier, JIT-kompilator, översättning av klassisk BPF, samt när man studerar kartor, anropsfunktioner etc.

När vi talar om individuella instruktioner kommer vi att hänvisa till kärnfilerna bpf.h и bpf_common.h, som definierar de numeriska koderna för BPF-instruktioner. När du studerar arkitektur på egen hand och/eller analyserar binärer, kan du hitta semantik i följande källor, sorterade efter komplexitet: Inofficiell eBPF-specifikation, BPF och XDP Referensguide, Instruktionsuppsättning, Dokumentation/nätverk/filter.txt och, naturligtvis, i Linux-källkoden - verifier, JIT, BPF-tolk.

Exempel: att ta isär BPF i huvudet

Låt oss titta på ett exempel där vi kompilerar ett program readelf-example.c och titta på den resulterande binära filen. Vi kommer att avslöja det ursprungliga innehållet readelf-example.c nedan, efter att vi återställt dess logik från binära koder:

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

Första kolumnen i utdata readelf är en indragning och vårt program består alltså av fyra kommandon:

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

Kommandokoder är lika b7, 15, b7 и 95. Kom ihåg att de minst signifikanta tre bitarna är instruktionsklassen. I vårt fall är den fjärde biten av alla instruktioner tom, så instruktionsklasserna är 7, 5, 7, 5. Klass 7 är BPF_ALU64, och 5 är BPF_JMP. För båda klasserna är instruktionsformatet detsamma (se ovan) och vi kan skriva om vårt program så här (samtidigt som vi kommer att skriva om de återstående kolumnerna i mänsklig form):

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

Operation b klass ALU64 - Är BPF_MOV. Den tilldelar ett värde till destinationsregistret. Om biten är inställd s (källa), då tas värdet från källregistret, och om det, som i vårt fall, inte är inställt, tas värdet från fältet Imm. Så i de första och tredje instruktionerna utför vi operationen r0 = Imm. Vidare är JMP klass 1-drift BPF_JEQ (hoppa om lika). I vårt fall, sedan biten S är noll, jämför det värdet på källregistret med fältet Imm. Om värdena sammanfaller sker övergången till PC + Offvar PC, som vanligt, innehåller adressen till nästa instruktion. Slutligen är JMP Class 9 Operation BPF_EXIT. Denna instruktion avslutar programmet och återgår till kärnan r0. Låt oss lägga till en ny kolumn i vår tabell:

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

Vi kan skriva om detta i en mer bekväm form:

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

Om vi ​​kommer ihåg vad som står i registret r1 programmet skickas en pekare till sammanhanget från kärnan och i registret r0 värdet returneras till kärnan, då kan vi se att om pekaren till sammanhanget är noll, så returnerar vi 1, och annars - 2. Låt oss kontrollera att vi har rätt genom att titta på källan:

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

Ja, det är ett meningslöst program, men det översätts till bara fyra enkla instruktioner.

Undantagsexempel: 16-byte instruktion

Vi nämnde tidigare att vissa instruktioner tar upp mer än 64 bitar. Det gäller till exempel instruktioner lddw (Kod = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — ladda ett dubbelord från fälten till registret Imm. Faktum är det Imm har en storlek på 32 och ett dubbelord är 64 bitar, så att ladda ett 64-bitars omedelbart värde i ett register i en 64-bitars instruktion kommer inte att fungera. För att göra detta används två intilliggande instruktioner för att lagra den andra delen av 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                   ........

Det finns bara två instruktioner i ett binärt program:

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

Vi kommer att träffas igen med instruktioner lddw, när vi pratar om omplaceringar och att arbeta med kartor.

Exempel: demontering av BPF med standardverktyg

Så vi har lärt oss att läsa BPF-binära koder och är redo att analysera alla instruktioner om det behövs. Det är dock värt att säga att det i praktiken är bekvämare och snabbare att ta isär program med hjälp av standardverktyg, till exempel:

$ llvm-objdump -d x64.o

Disassembly of section .text:

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

Livscykel för BPF-objekt, bpffs-filsystem

(Jag lärde mig först några av detaljerna som beskrivs i detta underavsnitt från posta Alexei Starovoitov in BPF blogg.)

BPF-objekt - program och kartor - skapas från användarutrymmet med hjälp av kommandon BPF_PROG_LOAD и BPF_MAP_CREATE systemanrop bpf(2), vi kommer att prata om exakt hur detta händer i nästa avsnitt. Detta skapar kärndatastrukturer och för var och en av dem refcount (referensantal) sätts till ett, och en filbeskrivning som pekar på objektet returneras till användaren. Efter att handtaget är stängt refcount objektet reduceras med ett, och när det når noll förstörs objektet.

Om programmet använder kartor, då refcount dessa kartor ökas med en efter att programmet laddats, d.v.s. deras filbeskrivningar kan stängas från användarprocessen och fortfarande refcount blir inte noll:

BPF för de minsta, del ett: utökad BPF

Efter att ha lyckats ladda ett program, kopplar vi det vanligtvis till någon form av händelsegenerator. Till exempel kan vi sätta den på ett nätverksgränssnitt för att behandla inkommande paket eller koppla den till några tracepoint i kärnan. Vid denna tidpunkt kommer även referensräknaren att öka med ett och vi kommer att kunna stänga filbeskrivningen i loaderprogrammet.

Vad händer om vi nu stänger av bootloadern? Det beror på typen av händelsegenerator (hook). Alla nätverkskrokar kommer att finnas efter att lastaren är klar, dessa är de så kallade globala krokarna. Och till exempel kommer spårningsprogram att släppas efter att processen som skapade dem avslutas (och därför kallas lokal, från "lokal till processen"). Tekniskt sett har lokala krokar alltid en motsvarande filbeskrivning i användarutrymmet och stänger därför när processen är stängd, men globala krokar gör det inte. I följande figur försöker jag, med hjälp av röda kors, visa hur avslutningen av lastarprogrammet påverkar livslängden för objekt i fallet med lokala och globala krokar.

BPF för de minsta, del ett: utökad BPF

Varför finns det en skillnad mellan lokala och globala krokar? Att köra vissa typer av nätverksprogram är vettigt utan ett användarutrymme, till exempel, föreställ dig DDoS-skydd - starthanteraren skriver reglerna och kopplar BPF-programmet till nätverksgränssnittet, varefter starthanteraren kan gå och ta livet av sig. Å andra sidan, föreställ dig ett program för felsökningsspårning som du skrev på dina knän på tio minuter - när det är klart vill du att det inte ska finnas något skräp kvar i systemet, och lokala krokar kommer att säkerställa det.

Föreställ dig å andra sidan att du vill ansluta till en spårpunkt i kärnan och samla statistik över många år. I det här fallet skulle du vilja slutföra användardelen och återgå till statistiken då och då. Bpf-filsystemet ger denna möjlighet. Det är ett pseudofilsystem som endast finns i minnet som tillåter skapandet av filer som refererar till BPF-objekt och därmed ökar refcount föremål. Efter detta kan laddaren gå ur, och objekten som den skapade kommer att förbli levande.

BPF för de minsta, del ett: utökad BPF

Att skapa filer i bpffs som refererar till BPF-objekt kallas "pinning" (som i följande 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 för lokala objekt, utan också för användbarheten av globala objekt - om vi går tillbaka till exemplet med det globala DDoS-skyddsprogrammet vill vi kunna komma och titta på statistik då och då.

BPF-filsystemet är vanligtvis monterat 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

Filsystemnamn skapas med kommandot BPF_OBJ_PIN BPF-systemanrop. För att illustrera, låt oss ta ett program, kompilera det, ladda upp det och fästa det till bpffs. Vårt program gör inget användbart, vi presenterar bara koden 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";

Låt oss kompilera det här programmet och skapa en lokal kopia av filsystemet bpffs:

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

Låt oss nu ladda ner vårt program med hjälp av verktyget bpftool och titta på de medföljande systemanropen bpf(2) (några 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 har vi laddat programmet med hjälp av BPF_PROG_LOAD, fick en filbeskrivning från kärnan 3 och använda kommandot BPF_OBJ_PIN fäste den här filbeskrivningen som en fil "bpf-mountpoint/test". Efter detta bootloader-programmet bpftool slutade fungera, men vårt program fanns kvar i kärnan, även om vi inte kopplade det till något 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 normalt unlink(2) och efter det kommer motsvarande program att raderas:

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

Ta bort objekt

På tal om att ta bort objekt är det nödvändigt att klargöra att efter att vi har kopplat bort programmet från kroken (händelsegeneratorn), kommer inte en enda ny händelse att utlösa dess lansering, men alla aktuella instanser av programmet kommer att slutföras i normal ordning .

Vissa typer av BPF-program låter dig byta ut programmet i farten, d.v.s. tillhandahålla sekvensatomicitet replace = detach old program, attach new program. I det här fallet kommer alla aktiva instanser av den gamla versionen av programmet att avsluta sitt arbete, och nya händelsehanterare kommer att skapas från det nya programmet, och "atomicity" betyder här att inte en enda händelse kommer att missas.

Bifoga program till händelsekällor

I den här artikeln kommer vi inte separat att beskriva att koppla program till händelsekällor, eftersom det är vettigt att studera detta i samband med en specifik typ av program. Centimeter. exempel nedan, där vi visar hur program som XDP är anslutna.

Manipulera objekt med hjälp av bpf-systemanropet

BPF-program

Alla BPF-objekt skapas och hanteras från användarutrymmet med hjälp av ett systemanrop bpf, med följande prototyp:

#include <linux/bpf.h>

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

Här är laget cmd är ett av värdena för typ enum bpf_cmd, attr — en pekare till parametrar för ett specifikt program och size — objektstorlek enligt pekaren, d.v.s. vanligtvis detta sizeof(*attr). I kärnan 5.8 anropet systemet bpf stöder 34 olika kommandon, och bestämning av union bpf_attr upptar 200 rader. Men vi bör inte skrämmas av detta, eftersom vi kommer att bekanta oss med kommandon och parametrar under loppet av flera artiklar.

Låt oss börja med laget BPF_PROG_LOAD, som skapar BPF-program - tar en uppsättning BPF-instruktioner och laddar in den i kärnan. Vid laddningsögonblicket startas verifieraren och sedan returneras JIT-kompilatorn och, efter framgångsrik exekvering, programfilsbeskrivningen till användaren. Vi såg vad som händer med honom härnäst i föregående avsnitt om livscykeln för BPF-objekt.

Vi kommer nu att skriva ett anpassat program som kommer att ladda ett enkelt BPF-program, men först måste vi bestämma vilken typ av program vi vill ladda - vi måste välja тип och inom ramen för denna typ, skriv ett program som kommer att klara verifieringstestet. Men för att inte komplicera processen, här är en färdig lösning: vi kommer att ta ett program som BPF_PROG_TYPE_XDP, vilket returnerar värdet XDP_PASS (hoppa över alla paket). I BPF assembler ser det väldigt enkelt ut:

r0 = 2
exit

Efter att vi har bestämt oss att vi laddar upp, vi kan berätta hur vi ska göra:

#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();
}

Intressanta händelser i ett program börjar med definitionen av en array insns - vårt BPF-program i maskinkod. I det här fallet packas varje instruktion i BPF-programmet in i strukturen bpf_insn. Första elementet insns följer instruktionerna r0 = 2, andra - exit.

Reträtt. Kärnan definierar mer praktiska makron för att skriva maskinkoder och använda kärnhuvudfilen tools/include/linux/filter.h vi kunde skriva

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

Men eftersom att skriva BPF-program i inbyggd kod bara är nödvändigt för att skriva tester i kärnan och artiklar om BPF, så komplicerar inte frånvaron av dessa makron utvecklarens liv.

Efter att ha definierat BPF-programmet går vi vidare till att ladda det i kärnan. Vår minimalistiska uppsättning parametrar attr inkluderar programtyp, uppsättning och antal instruktioner, nödvändig licens och namn "woo", som vi använder för att hitta vårt program på systemet efter nedladdning. Programmet, som utlovat, laddas in i systemet med ett systemanrop bpf.

I slutet av programmet hamnar vi i en oändlig loop som simulerar nyttolasten. Utan det kommer programmet att dödas av kärnan när filbeskrivningen som systemanropet returnerade till oss stängs bpf, och vi kommer inte att se det i systemet.

Nåväl, vi är redo för testning. Låt oss montera och köra programmet under stracefö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_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

Allt är bra, bpf(2) lämnade tillbaka handtag 3 till oss och vi gick in i en oändlig slinga med pause(). Låt oss försöka hitta vårt program i systemet. För att göra detta 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 på systemet woo vars globala ID är 390 och pågår för närvarande simple-prog det finns en öppen filbeskrivning som pekar på programmet (och om simple-prog kommer att avsluta jobbet då woo kommer försvinna). Som väntat, programmet woo tar 16 byte - två instruktioner - av binära koder i BPF-arkitekturen, men i sin ursprungliga form (x86_64) är det redan 40 byte. Låt oss titta på vårt program i dess ursprungliga form:

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

inga överraskningar. Låt oss nu titta på koden som genereras 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 effektiv för exit(2), men i rättvisans namn är vårt program för enkelt, och för icke-triviala program behövs naturligtvis prologen och epilogen som lagts till av JIT-kompilatorn.

kartor

BPF-program kan använda strukturerade minnesområden som är tillgängliga både för andra BPF-program och för program i användarutrymmet. Dessa objekt kallas kartor och i det här avsnittet kommer vi att visa hur man manipulerar dem med ett systemanrop bpf.

Låt oss säga direkt att kartornas möjligheter inte bara är begränsade till tillgång till delat minne. Det finns specialkartor som innehåller till exempel pekare till BPF-program eller pekare till nätverksgränssnitt, kartor för att arbeta med perf-händelser osv. Vi kommer inte att prata om dem här, för att inte förvirra läsaren. Bortsett från detta ignorerar vi synkroniseringsproblem, eftersom detta inte är viktigt för våra exempel. En komplett lista över tillgängliga karttyper finns i <linux/bpf.h>, och i det här avsnittet tar vi som exempel den historiskt första typen, hashtabellen BPF_MAP_TYPE_HASH.

Om du skapar en hashtabell i, säg, C++, skulle du säga unordered_map<int,long> woo, som på ryska betyder "Jag behöver ett bord woo obegränsad storlek, vars nycklar är av typ int, och värdena är av typen long" För att skapa en BPF-hashtabell måste vi göra ungefär samma sak, förutom att vi måste ange den maximala storleken på tabellen, och istället för att specificera typerna av nycklar och värden, måste vi specificera deras storlekar i byte . Använd kommandot för att skapa kartor BPF_MAP_CREATE systemanrop bpf. Låt oss titta på ett mer eller mindre minimalt program som skapar en karta. Efter det föregående programmet som laddar BPF-program, bör detta verka enkelt för dig:

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

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

    for ( ;; )
        pause();
}

Här definierar vi en uppsättning parametrar attr, där vi säger "Jag behöver en hashtabell med nycklar och storleksvärden sizeof(int), där jag kan lägga maximalt fyra element." När du skapar BPF-kartor kan du ange andra parametrar, till exempel på samma sätt som i exemplet med programmet, vi angav namnet på objektet som "woo".

Låt oss kompilera och köra 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) returnerade oss beskrivaren kartnummer 3 och sedan väntar programmet, som förväntat, på ytterligare instruktioner i systemanropet pause(2).

Låt oss nu skicka vårt program till bakgrunden eller öppna en annan terminal och titta på vårt objekt med hjälp av verktyget bpftool (vi kan skilja vår karta från andra genom dess namn):

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

Siffran 114 är vårt objekts globala ID. Alla program i systemet kan använda detta ID för att öppna en befintlig karta med kommandot BPF_MAP_GET_FD_BY_ID systemanrop bpf.

Nu kan vi spela med vårt hashbord. Låt oss titta på dess innehåll:

$ sudo bpftool map dump id 114
Found 0 elements

Tömma. Låt oss lägga ett värde i det hash[1] = 1:

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

Låt oss titta på tabellen igen:

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

Hurra! Vi lyckades lägga till ett element. Observera att vi måste arbeta på bytenivå för att göra detta, eftersom bptftool vet inte vilken typ av värdena i hashtabellen. (Denna kunskap kan överföras till henne med BTF, men mer om det nu.)

Hur exakt läser och lägger bpftool till element? Låt oss ta en titt 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 kartan med dess globala ID med kommandot BPF_MAP_GET_FD_BY_ID и bpf(2) returnerade deskriptor 3 till oss. Använd kommandot vidare BPF_MAP_GET_NEXT_KEY vi hittade den första nyckeln i tabellen genom att passera NULL som en pekare till "föregående"-tangenten. Om vi ​​har nyckeln kan vi göra det BPF_MAP_LOOKUP_ELEMsom returnerar ett värde till en pekare value. Nästa steg är att vi försöker hitta nästa element genom att skicka en pekare till den aktuella nyckeln, men vår tabell innehåller bara ett element och kommandot BPF_MAP_GET_NEXT_KEY returnerar ENOENT.

Okej, låt oss ändra värdet med nyckel 1, låt oss säga att vår affärslogik kräver registrering hash[1] = 2:

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

Som väntat är det väldigt enkelt: kommandot BPF_MAP_GET_FD_BY_ID öppnar vår karta med ID och kommandot BPF_MAP_UPDATE_ELEM skriver över elementet.

Så efter att ha skapat en hashtabell 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, så kan vilket annat program på systemet som helst göra det. Förutom kommandona som beskrivs ovan, för att arbeta med kartor från användarutrymme, det följande:

  • BPF_MAP_LOOKUP_ELEM: hitta värde med nyckel
  • BPF_MAP_UPDATE_ELEM: uppdatera/skapa värde
  • BPF_MAP_DELETE_ELEM: ta bort nyckeln
  • BPF_MAP_GET_NEXT_KEY: hitta nästa (eller första) tangent
  • BPF_MAP_GET_NEXT_ID: låter dig gå igenom alla befintliga kartor, det är så det fungerar bpftool map
  • BPF_MAP_GET_FD_BY_ID: öppna en befintlig karta med dess globala ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomiskt uppdatera värdet på ett objekt och returnera det gamla
  • BPF_MAP_FREEZE: gör kartan 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 - detta är det enda pålitliga sättet att läsa och återställa alla värden från kartan

Alla dessa kommandon fungerar inte för alla karttyper, men i allmänhet ser det exakt likadant ut att arbeta med andra typer av kartor från användarutrymmet som att arbeta med hashtabeller.

Låt oss för ordningens skull avsluta våra hashtabellsexperiment. Kommer du 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

Än så länge är allt bra:

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

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 väntat lyckades vi inte. Låt oss titta på felet mer detaljerat:

$ 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 är bra: som förväntat, laget BPF_MAP_UPDATE_ELEM försöker skapa en ny, femte nyckel, men kraschar E2BIG.

Så vi kan skapa och ladda BPF-program, samt skapa och hantera kartor från användarutrymmet. Nu är det logiskt att titta på hur vi kan använda kartor från själva BPF-programmen. Vi skulle kunna prata om detta på språket med svårlästa program i maskinmakrokoder, men det är faktiskt dags att visa hur BPF-program faktiskt skrivs och underhålls - med hjälp av libbpf.

(För läsare som är missnöjda med avsaknaden av ett exempel på låg nivå: vi kommer att analysera i detalj program som använder kartor och hjälpfunktioner skapade med libbpf och berätta vad som händer på instruktionsnivån. För läsare som är missnöjda väldigt mycket, lade vi till exempel på lämplig plats i artikeln.)

Att skriva BPF-program med libbpf

Att skriva BPF-program med hjälp av maskinkoder kan vara intressant bara första gången, och sedan sätter mättnaden in. I detta ögonblick måste du vända din uppmärksamhet till llvm, som har en backend för att generera kod för BPF-arkitekturen, såväl som ett bibliotek libbpf, som låter dig skriva användarsidan av BPF-applikationer och ladda koden för BPF-program som genereras med llvm/clang.

I själva verket, som vi kommer att se i denna och efterföljande artiklar, libbpf gör ganska mycket arbete utan det (eller liknande verktyg - iproute2, libbcc, libbpf-go, etc.) det är omöjligt att leva. En av de mördande funktionerna i projektet libbpf är BPF CO-RE (Compile Once, Run Everywhere) - ett projekt som låter dig skriva BPF-program som är portabla från en kärna till en annan, med möjligheten att köras på olika API:er (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 BTF-stöd (vi beskriver hur du gör detta i avsnittet Utvecklings verktyg. Du kan kontrollera om din kärna är byggd med BTF eller inte mycket enkelt - genom närvaron av 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

Den här filen lagrar information om alla datatyper som används i kärnan och används i alla våra exempel libbpf. Vi kommer att prata i detalj om CO-RE i nästa artikel, men i den här - bygg bara en kärna själv med CONFIG_DEBUG_INFO_BTF.

Bibliotek libbpf bor precis i katalogen tools/lib/bpf kärnan och dess utveckling sker via e-postlistan [email protected]. Ett separat arkiv upprätthålls dock för behoven hos applikationer som lever utanför kärnan https://github.com/libbpf/libbpf där kärnbiblioteket speglas för läsåtkomst mer eller mindre som det är.

I det här avsnittet kommer vi att titta på hur du kan skapa ett projekt som använder libbpf, låt oss skriva flera (mer eller mindre meningslösa) testprogram och analysera i detalj hur det hela fungerar. Detta gör att vi lättare kan förklara i följande avsnitt exakt hur BPF-program interagerar med kartor, kärnhjälpare, BTF, etc.

Vanligtvis projekt med hjälp av libbpf lägg till ett GitHub-förråd som en git-undermodul, så gör vi detsamma:

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

Ska 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 nästa plan i det här avsnittet är följande: vi kommer att skriva ett BPF-program som BPF_PROG_TYPE_XDP, samma som i föregående exempel, men i C kompilerar vi det med hjälp av clang, och skriv ett hjälpprogram som laddar in det i kärnan. I de följande avsnitten kommer vi att utöka kapaciteten för både BPF-programmet och assistentprogrammet.

Exempel: skapa en fullfjädrad applikation med libbpf

Till att börja med använder vi filen /sys/kernel/btf/vmlinux, som nämndes ovan, och skapa dess motsvarighet i form av en rubrikfil:

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

Den här filen kommer att lagra alla datastrukturer som är tillgängliga i vår kärna, till exempel så här definieras IPv4-huvudet i kärnan:

$ 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 ska vi skriva vårt BPF-program i C:

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

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

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

Även om vårt program visade sig vara väldigt enkelt, måste vi fortfarande vara uppmärksamma på många detaljer. Först är den första rubrikfilen vi inkluderar vmlinux.h, som vi just genererade med hjälp av bpftool btf dump - nu behöver vi inte installera kernel-headers-paketet för att ta reda på hur kärnstrukturerna ser ut. Följande rubrikfil kommer till oss från biblioteket libbpf. Nu behöver vi bara det för att definiera makrot SEC, som skickar tecknet till lämplig sektion av ELF-objektfilen. Vårt program finns i avsnittet xdp/simple, där vi före snedstrecket definierar programtypen BPF - detta är konventionen som används i libbpf, baserat på avsnittets namn kommer den att ersätta den korrekta typen vid start bpf(2). Själva BPF-programmet är C - mycket enkel och består av en rad return XDP_PASS. Slutligen ett separat avsnitt "license" innehåller namnet på licensen.

Vi kan kompilera vårt program med llvm/clang, version >= 10.0.0, eller ännu bättre, högre (se avsnittet Utvecklings verktyg):

$ 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: vi anger målarkitekturen -target bpf och vägen till rubrikerna libbpf, som vi nyligen installerade. Glöm inte heller bort -O2, utan det här alternativet kan du få överraskningar i framtiden. Låt oss titta på vår kod, lyckades vi skriva det program vi ville ha?

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

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

Disassembly of section xdp/simple:

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

Ja, det funkade! Nu har vi en binär fil med programmet, och vi vill skapa en applikation som laddar den i kärnan. För detta ändamål biblioteket libbpf erbjuder oss två alternativ - använd ett API på lägre nivå eller ett API på högre nivå. Vi kommer att gå den andra vägen, eftersom vi vill lära oss att skriva, ladda och ansluta BPF-program med minimal ansträngning för deras efterföljande studier.

Först måste vi generera "skelettet" av vårt program från dess binära med samma verktyg bpftool — BPF-världens schweiziska kniv (som kan tas bokstavligt, eftersom Daniel Borkman, en av skaparna och underhållarna av BPF, är schweizisk):

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

I fil xdp-simple.skel.h innehåller den binära koden för vårt program och funktioner för att hantera - ladda, bifoga, ta bort vårt objekt. I vårt enkla fall ser detta ut som overkill, men det fungerar också i fallet där objektfilen innehåller många BPF-program och kartor och för att ladda denna gigantiska ELF behöver vi bara generera skelettet och anropa en eller två funktioner från den anpassade applikationen vi skriver. Låt oss gå vidare nu.

Strängt taget är vårt lastarprogram 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 se spår av ett lågnivå-API här: strukturen struct bpf_program *simple и struct bpf_link *simple. Den första strukturen beskriver specifikt vårt program, skrivet i avsnittet xdp/simple, och den andra beskriver hur programmet ansluter till händelsekällan.

Funktion xdp_simple_bpf__open_and_load, öppnar ett ELF-objekt, analyserar det, skapar alla strukturer och understrukturer (förutom programmet innehåller ELF även andra sektioner - data, skrivskyddad data, felsökningsinformation, licens, etc.) och laddar sedan in det i kärnan med hjälp av ett system ring upp bpf, som 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. Låt oss hitta hennes 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 dump (vi använder en förkortad form 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 C-källfil. Detta gjordes av biblioteket libbpf, som hittade felsökningssektionen i binären, kompilerade den till ett BTF-objekt, laddade in den i kärnan med BPF_BTF_LOAD, och angav sedan den resulterande filbeskrivningen när programmet laddades med kommandot BPG_PROG_LOAD.

Kärnhjälpare

BPF-program kan köra "externa" funktioner - kärnhjälpare. Dessa hjälpfunktioner tillåter BPF-program att komma åt kärnstrukturer, hantera kartor och även kommunicera med den "verkliga världen" - skapa perf-händelser, kontrollera hårdvara (till exempel omdirigera paket), etc.

Exempel: bpf_get_smp_processor_id

Inom ramen för paradigmet "lära genom exempel", låt oss överväga en av hjälpfunktionerna, bpf_get_smp_processor_id(), vissa i fil kernel/bpf/helpers.c. Det returnerar numret på processorn som BPF-programmet som anropade det körs på. Men vi är inte lika intresserade av dess semantik som av det faktum att dess implementering tar en linje:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF-hjälpfunktionsdefinitionerna liknar Linux-systemanropsdefinitionerna. Här definieras till exempel en funktion som inte har några argument. (En funktion som tar, säg, tre argument definieras med hjälp av makrot BPF_CALL_3. Det maximala antalet argument är fem.) Detta är dock bara den första delen av definitionen. Den andra delen är att definiera typstrukturen 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,
};

Registrera hjälpfunktioner

För att BPF-program av en viss typ ska använda denna funktion måste de registrera den, till exempel för typen BPF_PROG_TYPE_XDP en funktion är definierad i kärnan xdp_func_proto, som avgör från hjälpfunktionens ID om XDP stöder denna funktion eller inte. Vår funktion är stöder:

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 BPF-programtyper "definieras" i filen include/linux/bpf_types.h med hjälp av ett makro BPF_PROG_TYPE. Definierat inom citattecken eftersom det är en logisk definition, och i C-språkstermer förekommer definitionen av en hel uppsättning konkreta strukturer på andra ställen. I synnerhet i filen kernel/bpf/verifier.c alla definitioner från filen bpf_types.h används för att skapa en rad strukturer bpf_verifier_ops[]:

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

Det vill säga, för varje typ av BPF-program definieras en pekare till en datastruktur av typen struct bpf_verifier_ops, som initieras med värdet _name ## _verifier_ops, dvs. xdp_verifier_ops för xdp. Strukturera xdp_verifier_ops bestämd av i fil net/core/filter.c enligt följande:

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 välbekanta funktion xdp_func_proto, som kör verifieraren varje gång den stöter på en utmaning några funktioner i ett BPF-program, se verifier.c.

Låt oss titta på hur ett hypotetiskt BPF-program använder funktionen bpf_get_smp_processor_id. För att göra detta, skriver vi om programmet från vårt tidigare avsnitt enligt följande:

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

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

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

symbol bpf_get_smp_processor_id bestämd av в <bpf/bpf_helper_defs.h> bibliotek libbpf как

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

det är, bpf_get_smp_processor_id är en funktionspekare vars värde är 8, där 8 är värdet BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, som definieras för oss i filen vmlinux.h (fil bpf_helper_defs.h i kärnan genereras av ett skript, så de "magiska" siffrorna är ok). Den här funktionen tar inga argument och returnerar ett värde av typen __u32. När vi kör det i vårt program, clang genererar en instruktion BPF_CALL "rätt sort" Låt oss kompilera programmet och titta på avsnittet xdp/simple:

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

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

Disassembly of section xdp/simple:

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

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

På första raden ser vi instruktioner call, parameter IMM som är lika med 8, och SRC_REG - noll. Enligt ABI-avtalet som används av verifierare är detta ett samtal till hjälpfunktion nummer åtta. När den väl har lanserats är logiken enkel. Returnera värde från register r0 kopieras till r1 och på rad 2,3 omvandlas den till typ u32 — de övre 32 bitarna rensas. På raderna 4,5,6,7 returnerar vi 2 (XDP_PASS) eller 1 (XDP_DROP) beroende på om hjälpfunktionen från rad 0 returnerade ett noll- eller icke-nollvärde.

Låt oss testa oss själva: ladda programmet och titta på utgången bpftool prog dump xlated:

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

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

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

Ok, verifieraren hittade rätt kärnhjälpare.

Exempel: skicka argument och äntligen köra programmet!

Alla hjälpfunktioner på körnivå har en prototyp

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

Parametrar till hjälpfunktioner skickas i register r1-r5, och värdet returneras i registret r0. Det finns inga funktioner som tar mer än fem argument, och stöd för dem förväntas inte läggas till i framtiden.

Låt oss ta en titt på den nya kärnhjälparen och hur BPF skickar parametrar. Låt oss skriva om xdp-simple.bpf.c enligt följande (resten av raderna 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å den CPU som den körs på. Låt oss kompilera det och titta på koden:

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

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

På rad 0-7 skriver vi strängen running on CPU%un, och sedan på rad 8 kör vi den välbekanta bpf_get_smp_processor_id. På rad 9-12 förbereder vi hjälpargumenten bpf_printk - register r1, r2, r3. Varför är det tre av dem och inte två? Därför att bpf_printkdetta är ett makroomslag runt den verklige hjälparen bpf_trace_printk, som måste passera storleken på formatsträngen.

Låt oss nu lägga till ett par rader till xdp-simple.cså att vårt program ansluter till gränssnittet lo och började verkligen!

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

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

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

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

cleanup:
    xdp_simple_bpf__destroy(obj);
}

Här använder vi funktionen bpf_set_link_xdp_fd, som kopplar BPF-program av XDP-typ till nätverksgränssnitt. Vi hårdkodade gränssnittsnumret lo, som alltid är 1. Vi kör funktionen två gånger för att först koppla bort det gamla programmet om det var bifogat. Lägg märke till att nu behöver vi ingen utmaning pause eller en oändlig loop: vårt loader-program avslutas, men BPF-programmet kommer inte att dödas eftersom det är anslutet till händelsekällan. Efter lyckad nedladdning och anslutning kommer programmet att startas för varje nätverkspaket som anländer till lo.

Låt oss ladda ner 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: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

Programmet vi laddade ner har ID 669 och vi ser samma ID på gränssnittet lo. Vi skickar ett par paket till 127.0.0.1 (förfrågan + svar):

$ ping -c1 localhost

och låt oss nu titta på innehållet i den virtuella felsökningsfilen /sys/kernel/debug/tracing/trace_pipe, i vilken 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 upptäcktes lo och bearbetas på CPU0 - vårt första fullfjädrade meningslösa BPF-program fungerade!

Det är värt att notera bpf_printk Det är inte för inte som det skriver till felsökningsfilen: det här är inte den mest framgångsrika hjälparen för användning i produktionen, men vårt mål var att visa något enkelt.

Åtkomst till kartor från BPF-program

Exempel: att använda en karta från BPF-programmet

I de föregående avsnitten lärde vi oss hur man skapar och använder kartor från användarutrymmet, och låt oss nu 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 enligt följande:

#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 kartdefinition woo: Detta är en 8-elementarray som lagrar värden som u64 (i C skulle vi definiera en sådan array som u64 woo[8]). I ett program "xdp/simple" vi får det aktuella processornumret till en variabel key och sedan använda hjälpfunktionen bpf_map_lookup_element vi får en pekare till motsvarande post i arrayen, som vi ökar med en. Översatt till ryska: vi beräknar statistik om vilken CPU som behandlade 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 hon är ansluten till lo och skicka några paket:

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

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

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 bearbetades på CPU7. Detta är inte viktigt för oss, huvudsaken är att programmet fungerar och vi förstår hur man kommer åt kartor från BPF-program - med хелперов bpf_mp_*.

Mystiskt index

Så vi kan komma åt kartan från BPF-programmet med samtal som

val = bpf_map_lookup_elem(&woo, &key);

var hjälparfunktionen ser ut

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

men vi skickar en pekare &woo till en icke namngiven struktur struct { ... }.

Om vi ​​tittar på programsamlaren ser vi att värdet &woo är faktiskt inte definierad (rad 4):

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

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

Disassembly of section xdp/simple:

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

och ingår i omlokaliseringar:

$ 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 laddade programmet ser vi en pekare till rätt karta (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åledes kan vi dra slutsatsen att vid tidpunkten för lanseringen av vårt laddarprogram, länken till &woo ersattes av något med ett bibliotek libbpf. Först ska vi titta på utgången strace:

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

Vi ser det libbpf skapade en karta woo och sedan laddade ner vårt program simple. Låt oss ta en närmare titt på hur vi laddar programmet:

  • ring upp xdp_simple_bpf__open_and_load från fil xdp-simple.skel.h
  • som orsakar xdp_simple_bpf__load från fil xdp-simple.skel.h
  • som orsakar bpf_object__load_skeleton från fil libbpf/src/libbpf.c
  • som orsakar bpf_object__load_xattr av libbpf/src/libbpf.c

Den sista funktionen kommer bland annat att anropa bpf_object__create_maps, som skapar eller öppnar befintliga kartor och förvandlar dem till filbeskrivningar. (Det är här vi ser BPF_MAP_CREATE i utgången strace.) Därefter anropas funktionen bpf_object__relocate och det är hon som intresserar oss, eftersom vi minns vad vi såg woo i flytttabellen. När vi utforskar det, befinner vi oss så småningom i funktionen bpf_program__relocate, som sysslar med kartomflyttningar:

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åra instruktioner

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

och ersätt källregistret i det med BPF_PSEUDO_MAP_FD, och den första IMM till filbeskrivningen för vår karta och, om den är lika med t.ex. 0xdeadbeef, så kommer vi att få instruktionerna

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

Så här överförs kartinformation till ett specifikt laddat BPF-program. I det här fallet kan kartan skapas med hjälp av BPF_MAP_CREATE, och öppnas av ID med hjälp av BPF_MAP_GET_FD_BY_ID.

Totalt vid användning libbpf Algoritmen är som följer:

  • under sammanställningen skapas poster i flytttabellen för länkar till kartor
  • libbpf öppnar ELF-objektboken, hittar alla använda kartor och skapar filbeskrivningar för dem
  • filbeskrivningar laddas in i kärnan som en del av instruktionen LD64

Som du kan föreställa dig finns det mer att komma och vi måste titta in i kärnan. Som tur är har vi en aning – vi har skrivit ner innebörden BPF_PSEUDO_MAP_FD in i källregistret och vi kan begrava det, vilket kommer att leda oss till det heliga av alla helgon - kernel/bpf/verifier.c, där en funktion med ett distinkt namn ersätter en filbeskrivning med adressen till en typstruktur struct bpf_map:

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

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

(fullständig kod finns по ссылке). Så vi kan utöka vår algoritm:

  • medan verifieraren laddar programmet kontrollerar den korrekt användning av kartan och skriver adressen till motsvarande struktur struct bpf_map

När du laddar ner ELF binär med hjälp av libbpf Det händer mycket mer, men vi kommer att diskutera det i andra artiklar.

Laddar program och kartor 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 kartor, utan hjälp libbpf. Detta kan vara användbart när du arbetar i en miljö som du inte kan bygga beroenden för, eller sparar varje bit, eller skriver ett program som ply, som genererar BPF binär kod i farten.

För att göra det lättare att följa logiken kommer vi att skriva om vårt exempel för dessa ändamål xdp-simple. Den kompletta och något utökade koden för programmet som diskuteras i det här exemplet kan hittas i detta kontentan.

Logiken i vår ansökan är som följer:

  • skapa en typkarta BPF_MAP_TYPE_ARRAY med hjälp av kommandot BPF_MAP_CREATE,
  • skapa ett program som använder den här kartan,
  • ansluta programmet till gränssnittet lo,

som översätts till mänsklig 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 karta på samma sätt som vi gjorde i det första exemplet om systemanropet bpf - "kärna, snälla gör mig en ny karta i form av en array av 8 element som __u64 och ge mig tillbaka filbeskrivningen":

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 är också lätt att ladda:

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 knepiga delen prog_load är definitionen av vårt BPF-program som en rad strukturer struct bpf_insn insns[]. Men eftersom vi använder ett program som vi har i C, kan vi fuska lite:

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

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

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

Totalt behöver vi skriva 14 instruktioner i form av strukturer som struct bpf_insn (råd: ta soptippen från ovan, läs igenom instruktionerna igen, öppna linux/bpf.h и linux/bpf_common.h och försöka avgöra struct bpf_insn insns[] på egen hand):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

En övning för den som inte skrivit detta själv – hitta map_fd.

Det finns ytterligare en okänd del kvar i vårt program - xdp_attach. Tyvärr kan program som XDP inte anslutas med ett systemanrop bpf. Människorna som skapade BPF och XDP kom från Linux-communityt online, vilket betyder att de använde den som var mest bekant för dem (men inte för att vanligt people) gränssnitt för att interagera med kärnan: netlink-uttag, se även RFC3549. Det enklaste sättet att implementera xdp_attach kopierar kod från libbpf, nämligen från filen netlink.c, vilket är vad vi gjorde, förkortade det lite:

Välkommen till en värld av netlink-uttag

Öppna en typ av netlink-uttag 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 detta uttag:

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

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

        if (len == 0)
            break;

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

Slutligen, här är vår funktion som öppnar en socket och skickar ett speciellt meddelande till den som innehåller en filbeskrivning:

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

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

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

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

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

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

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

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

cleanup:
    close(sock);
    return ret;
}

Så 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 anslutit till lo:

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

Låt oss skicka pingar och titta på kartan:

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

Hurra, allt fungerar. Notera förresten att vår karta återigen visas i form av byte. Detta beror på det faktum att till skillnad från libbpf vi laddade inte typinformation (BTF). Men vi ska prata mer om detta nästa gång.

Utvecklings verktyg

I det här avsnittet kommer vi att titta på minimiverktyget för BPF-utvecklare.

Generellt sett behöver du inget speciellt för att utveckla BPF-program - BPF körs på vilken anständig distributionskärna som helst, och program byggs med clang, som kan levereras från förpackningen. Men på grund av det faktum att BPF är under utveckling förändras kärnan och verktygen hela tiden, om du inte vill skriva BPF-program med gammaldags metoder från 2019 måste du kompilera

  • llvm/clang
  • pahole
  • dess kärna
  • bpftool

(För referens, detta avsnitt och alla exempel i artikeln kördes på Debian 10.)

llvm/clang

BPF är vänligt mot LLVM och även om program för BPF nyligen kan kompileras med gcc, utförs all aktuell utveckling för LLVM. Därför kommer vi först och främst att bygga den nuvarande 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
... много времени спустя
$

Nu kan vi kontrollera om allt kom ihop korrekt:

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

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

(Monteringsanvisningar clang tagna av mig från bpf_devel_QA.)

Vi kommer inte att installera de program vi just byggt, utan istället bara lägga till dem PATH, till exempel:

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

(Detta kan läggas till .bashrc eller till en separat fil. Personligen lägger jag till sådana här saker ~/bin/activate-llvm.sh och när det behövs gör jag det . activate-llvm.sh.)

Pahole och BTF

Verktyg pahole används när man bygger kärnan för att skapa felsökningsinformation i BTF-format. Vi kommer inte att gå in i detalj i den här artikeln om detaljerna i BTF-teknik, annat än att det är bekvämt och vi vill använda det. Så om du ska bygga din kärna, bygg först pahole (utan pahole du kommer inte att kunna 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 att experimentera med BPF

När jag utforskar möjligheterna med BPF vill jag sätta ihop min egen kärna. Detta är generellt sett inte nödvändigt, eftersom du kommer att kunna kompilera och ladda BPF-program på distributionskärnan, men med din egen kärna kan du använda de senaste BPF-funktionerna, som i bästa fall kommer att dyka upp i din distribution om månader. , eller, som i fallet med vissa felsökningsverktyg, kommer inte att paketeras alls inom överskådlig framtid. Dessutom gör dess egen kärna att det känns viktigt att experimentera med koden.

För att bygga en kärna behöver du för det första själva kärnan och för det andra en kärnkonfigurationsfil. För att experimentera med BPF kan vi använda det vanliga vanilj kärna eller en av utvecklingskärnorna. Historiskt sett sker BPF-utveckling inom Linux-nätverksgemenskapen och därför går alla förändringar förr eller senare via David Miller, Linux-nätverksunderhållaren. Beroende på deras natur - redigeringar eller nya funktioner - delas nätverksändringar i en av två kärnor - net eller net-next. Förändringar för BPF fördelas på samma sätt mellan bpf и bpf-next, som sedan slås samman till netto respektive net-nästa. För mer information, se bpf_devel_QA и netdev-FAQ. Så välj en kärna baserat på din smak och stabilitetsbehoven för systemet du testar på (*-next kärnor är de mest instabila av de listade).

Det ligger utanför ramen för denna artikel att prata om hur man hanterar kärnkonfigurationsfiler - det antas att du antingen redan vet hur man gör detta, eller redo att lära sig på egen hand. Följande instruktioner bör dock vara mer eller mindre tillräckligt för att ge dig ett fungerande BPF-aktiverat system.

Ladda ner en av ovanstående kärnor:

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

Bygg en minimal fungerande kärnkonfiguration:

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

Aktivera BPF-alternativ i filen .config efter eget val (mest troligt CONFIG_BPF kommer redan att vara aktiverat eftersom systemd använder det). Här är en lista med alternativ från kärnan som används för den här artikeln:

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å kan vi enkelt sätta ihop och installera modulerna och kärnan (du kan förresten sätta ihop kärnan med den nymonterade clanggenom 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 för detta kexec från paketet kexec-tools):

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

bpftool

Det vanligaste verktyget i artikeln kommer att vara verktyget bpftool, levereras som en del av Linux-kärnan. Det är skrivet och underhållet av BPF-utvecklare för BPF-utvecklare och kan användas för att hantera alla typer av BPF-objekt - ladda program, skapa och redigera kartor, utforska livet i BPF-ekosystemet, etc. Dokumentation i form av källkoder för man-sidor finns i kärnan eller, redan sammanställd, nät.

När detta skrivs bpftool kommer endast färdigt för RHEL, Fedora och Ubuntu (se t.ex. denna tråd, som berättar den oavslutade historien om förpackningar bpftool i Debian). Men om du redan har byggt din kärna, bygg då bpftool lätt som en plätt:

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

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

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

$

(Här ${linux} - det här är din kärnkatalog.) Efter att du har kört dessa kommandon bpftool kommer att samlas i en katalog ${linux}/tools/bpf/bpftool och det kan läggas till sökvägen (först och främst till användaren root) eller bara kopiera till /usr/local/sbin.

Samla bpftool det är bäst att använda det senare clang, monterad enligt beskrivningen ovan, och kontrollera om den är korrekt monterad - med till exempel kommandot

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

som kommer att visa vilka BPF-funktioner som är aktiverade i din kärna.

Förresten, det föregående kommandot kan köras som

# bpftool f p k

Detta görs i analogi med verktygen från paketet iproute2, där vi till exempel kan säga ip a s eth0 istället för ip addr show dev eth0.

Slutsats

BPF låter dig sko en loppa för att effektivt mäta och ändra kärnans funktionalitet. Systemet visade sig vara mycket framgångsrikt, i de bästa UNIX-traditionerna: en enkel mekanism som låter dig (om)programmera kärnan tillät ett stort antal människor och organisationer att experimentera. Och även om experimenten, såväl som utvecklingen av själva BPF-infrastrukturen, är långt ifrån avslutade, har systemet redan en stabil ABI som låter dig bygga pålitlig och viktigast av allt, effektiv affärslogik.

Jag skulle vilja notera att tekniken enligt min mening har blivit så populär för att den å ena sidan kan spela (arkitekturen hos en maskin kan förstås mer eller mindre på en kväll), och å andra sidan att lösa problem som inte gick att lösa (vackert) innan dess uppkomst. Dessa två komponenter tvingar tillsammans människor att experimentera och drömma, vilket leder till uppkomsten av fler och fler innovativa lösningar.

Denna artikel, även om den inte är särskilt kort, är bara en introduktion till BPF-världen och beskriver inte "avancerade" funktioner och viktiga delar av arkitekturen. Planen framöver är ungefär så här: nästa artikel kommer att vara en översikt över BPF-programtyper (det finns 5.8 programtyper som stöds i 30-kärnan), sedan ska vi äntligen titta på hur man skriver riktiga BPF-applikationer med hjälp av kärnspårningsprogram som ett exempel, då är det dags för en mer djupgående kurs om BPF-arkitektur, följt av exempel på BPF-nätverk och säkerhetsapplikationer.

Tidigare artiklar i denna serie

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

Länkar

  1. BPF och XDP Referensguide — dokumentation om BPF från cilium, eller mer exakt från Daniel Borkman, en av skaparna och underhållarna av BPF. Det här är en av de första seriösa beskrivningarna, som skiljer sig från de andra genom att Daniel vet exakt vad han skriver om och det finns inga fel där. Detta dokument beskriver särskilt hur man arbetar med BPF-program av XDP- och TC-typerna med det välkända verktyget ip från paketet iproute2.

  2. Dokumentation/nätverk/filter.txt — originalfil med dokumentation för klassisk och sedan utökad BPF. Bra läsning om du vill fördjupa dig i monteringsspråk och tekniska arkitektoniska detaljer.

  3. Blogga om BPF från facebook. Den uppdateras sällan, men träffande, som Alexei Starovoitov (författare till eBPF) och Andrii Nakryiko - (underhållare) skriver där libbpf).

  4. Bpftools hemligheter. En underhållande twittertråd från Quentin Monnet med exempel och hemligheter för att använda bpftool.

  5. Dyk in i BPF: en lista med läsmaterial. En gigantisk (och fortfarande underhållen) lista med länkar till BPF-dokumentation från Quentin Monnet.

Källa: will.com

Lägg en kommentar