BPF za najmlajše, prvi del: razširjeni BPF

Na začetku je obstajala tehnologija in se je imenovala BPF. Pogledali smo jo prejšnji, članek iz te serije o Stari zavezi. Leta 2013 je bila s prizadevanji Alekseja Starovoitova in Daniela Borkmana razvita izboljšana različica, optimizirana za sodobne 64-bitne stroje, ki je bila vključena v jedro Linuxa. To novo tehnologijo so na kratko poimenovali Internal BPF, nato preimenovali v Extended BPF, zdaj, po nekaj letih, pa jo vsi preprosto imenujejo BPF.

Grobo rečeno, BPF vam omogoča izvajanje poljubne kode, ki jo zagotovi uporabnik, v prostoru jedra Linuxa in nova arhitektura se je izkazala za tako uspešno, da bomo potrebovali še ducat člankov za opis vseh njenih aplikacij. (Edina stvar, ki razvijalcem ni uspela, kot lahko vidite v spodnji kodi uspešnosti, je bilo ustvarjanje spodobnega logotipa.)

Ta članek opisuje strukturo virtualnega stroja BPF, vmesnike jedra za delo z BPF, razvojna orodja ter kratek, zelo kratek pregled obstoječih zmogljivosti, tj. vse, kar bomo v prihodnosti potrebovali za globlji študij praktičnih aplikacij BPF.
BPF za najmlajše, prvi del: razširjeni BPF

Povzetek članka

Uvod v arhitekturo BPF. Najprej si bomo iz ptičje perspektive ogledali arhitekturo BPF in orisali glavne komponente.

Registri in ukazni sistem virtualnega stroja BPF. Ko že imamo idejo o arhitekturi kot celoti, bomo opisali strukturo virtualnega stroja BPF.

Življenjski cikel objektov BPF, datotečni sistem bpffs. V tem razdelku si bomo podrobneje ogledali življenjski cikel objektov BPF - programov in zemljevidov.

Upravljanje objektov s sistemskim klicem bpf. Z nekaj razumevanja sistema, ki je že vzpostavljen, si bomo končno ogledali, kako ustvariti in manipulirati objekte iz uporabniškega prostora s posebnim sistemskim klicem − bpf(2).

Пишем программы BPF с помощью libbpf. Seveda lahko programe pišete s sistemskim klicem. Ampak to je težko. Za bolj realističen scenarij so jedrski programerji razvili knjižnico libbpf. Ustvarili bomo osnovno okostje aplikacije BPF, ki ga bomo uporabili v naslednjih primerih.

Pomočniki jedra. Tukaj bomo izvedeli, kako lahko programi BPF dostopajo do funkcij pomočnika jedra – orodja, ki skupaj z zemljevidi temeljito razširi zmogljivosti novega BPF v primerjavi s klasičnim.

Dostop do zemljevidov iz programov BPF. Do te točke bomo vedeli dovolj, da natančno razumemo, kako lahko ustvarimo programe, ki uporabljajo zemljevide. Pa še na hitro pokukajmo v veliki in mogočni overitelj.

Razvojna orodja. Oddelek za pomoč o tem, kako sestaviti potrebne pripomočke in jedro za poskuse.

Zaključek. Na koncu članka bodo tisti, ki so prebrali do tukaj, našli spodbudne besede in kratek opis dogajanja v naslednjih člankih. Navedli bomo tudi številne povezave za samostojno učenje za tiste, ki nimajo želje ali možnosti čakati na nadaljevanje.

Uvod v arhitekturo BPF

Preden začnemo obravnavati arhitekturo BPF, se bomo še zadnjič (oh) sklicevali na klasični BPF, ki je bil razvit kot odgovor na pojav strojev RISC in je rešil problem učinkovitega filtriranja paketov. Arhitektura se je izkazala za tako uspešno, da je bila rojena v drznih devetdesetih v Berkeley Unixu prenesena na večino obstoječih operacijskih sistemov, preživela v norih dvajsetih in še vedno išče nove aplikacije.

Novi BPF je bil razvit kot odgovor na vseprisotnost 64-bitnih strojev, storitev v oblaku in povečano potrebo po orodjih za ustvarjanje SDN (Sprogramska oprema-ddodelano nomrežna dela). Novi BPF, ki so ga razvili omrežni inženirji jedra kot izboljšano zamenjavo za klasični BPF, je dobesedno šest mesecev pozneje našel aplikacije pri težki nalogi sledenja sistemov Linux, zdaj, šest let po njegovem pojavu, pa bomo potrebovali cel naslednji članek samo za naštejte različne vrste programov.

Smešne slike

V svojem jedru je BPF navidezni stroj peskovnika, ki vam omogoča zagon "poljubne" kode v prostoru jedra brez ogrožanja varnosti. Programi BPF so ustvarjeni v uporabniškem prostoru, naloženi v jedro in povezani z nekim virom dogodkov. Dogodek je lahko na primer dostava paketa omrežnemu vmesniku, zagon neke funkcije jedra itd. V primeru paketa bo imel program BPF dostop do podatkov in metapodatkov paketa (za branje in po možnosti pisanje, odvisno od vrste programa); v primeru izvajanja funkcije jedra argumenti funkcijo, vključno s kazalci na pomnilnik jedra itd.

Oglejmo si ta proces pobližje. Za začetek se pogovorimo o prvi razliki od klasičnega BPF, programi za katere so bili napisani v asemblerju. V novi različici je bila arhitektura razširjena, tako da je bilo mogoče programe pisati v jezikih visoke ravni, predvsem seveda v C. Za to je bilo razvito zaledje za llvm, ki vam omogoča ustvarjanje bajtne kode za arhitekturo BPF.

BPF za najmlajše, prvi del: razširjeni BPF

Arhitektura BPF je bila delno zasnovana za učinkovito delovanje na sodobnih strojih. Da bi to delovalo v praksi, se bajtna koda BPF, ko je naložena v jedro, prevede v izvorno kodo z uporabo komponente, imenovane prevajalnik JIT (Jzgornji In Tjaz mene). Nato, če se spomnite, je bil v klasičnem BPF program naložen v jedro in pritrjen na izvor dogodka atomsko - v kontekstu enega samega sistemskega klica. V novi arhitekturi se to zgodi v dveh stopnjah – najprej se koda naloži v jedro s sistemskim klicem bpf(2)nato pa se pozneje prek drugih mehanizmov, ki se razlikujejo glede na vrsto programa, program poveže z izvorom dogodka.

Tu se lahko bralec vpraša: ali je bilo mogoče? Kako je zagotovljena varnost izvajanja takšne kode? Varnost izvajanja nam zagotavlja stopnja nalaganja BPF programov, imenovana verifier (v angleščini se ta stopnja imenuje verifier in bom še naprej uporabljal angleško besedo):

BPF za najmlajše, prvi del: razširjeni BPF

Verifier je statični analizator, ki zagotavlja, da program ne moti normalnega delovanja jedra. To, mimogrede, ne pomeni, da program ne more motiti delovanja sistema - programi BPF lahko, odvisno od vrste, berejo in prepisujejo odseke pomnilnika jedra, vračajo vrednosti funkcij, obrezujejo, dodajajo, prepisujejo in celo posreduje omrežne pakete. Verifier zagotavlja, da izvajanje programa BPF ne bo zrušilo jedra in da program, ki ima v skladu s pravili dostop za pisanje, na primer podatkov izhodnega paketa, ne bo mogel prepisati pomnilnika jedra zunaj paketa. Verifikator si bomo nekoliko podrobneje ogledali v ustreznem razdelku, potem ko se seznanimo z vsemi drugimi komponentami BPF.

Kaj smo se torej do zdaj naučili? Uporabnik napiše program v C, ga naloži v jedro s sistemskim klicem bpf(2), kjer ga preveri preveritelj in prevede v izvorno bajtno kodo. Nato isti ali drug uporabnik poveže program z izvorom dogodka in ta se začne izvajati. Ločevanje zagona in povezave je potrebno iz več razlogov. Prvič, poganjanje preverjalnika je relativno drago in z večkratnim nalaganjem istega programa izgubljamo računalniški čas. Drugič, natančno, kako je program povezan, je odvisno od njegove vrste in en "univerzalni" vmesnik, razvit pred letom dni, morda ni primeren za nove vrste programov. (Čeprav zdaj, ko postaja arhitektura zrelejša, obstaja ideja, da bi ta vmesnik poenotili na ravni libbpf.)

Pozoren bralec lahko opazi, da s slikami še nismo končali. Vse našteto namreč ne pojasni, zakaj BPF bistveno spremeni sliko v primerjavi s klasičnim BPF. Dve novosti, ki znatno razširita obseg uporabnosti, sta možnost uporabe skupnega pomnilnika in pomožne funkcije jedra. V BPF je skupni pomnilnik implementiran s tako imenovanimi zemljevidi - skupnimi podatkovnimi strukturami s posebnim API-jem. To ime so verjetno dobili, ker je bila prva vrsta zemljevida zgoščevalna tabela. Nato so se pojavile matrike, lokalne (na CPU) zgoščene tabele in lokalne matrike, iskalna drevesa, zemljevidi, ki vsebujejo kazalce na programe BPF in še veliko več. Za nas je zdaj zanimivo to, da imajo programi BPF zdaj zmožnost ohraniti stanje med klici in ga deliti z drugimi programi in z uporabniškim prostorom.

Do zemljevidov se dostopa iz uporabniških procesov s sistemskim klicem bpf(2)in iz programov BPF, ki se izvajajo v jedru, s pomožnimi funkcijami. Poleg tega pomočniki ne obstajajo le za delo z zemljevidi, ampak tudi za dostop do drugih zmogljivosti jedra. Programi BPF lahko na primer uporabljajo pomožne funkcije za posredovanje paketov drugim vmesnikom, ustvarjanje dogodkov perf, dostop do struktur jedra itd.

BPF za najmlajše, prvi del: razširjeni BPF

Če povzamemo, BPF nudi možnost nalaganja poljubne uporabniške kode, tj. s preverjanjem preizkušene, v prostor jedra. Ta koda lahko shranjuje stanje med klici in izmenjuje podatke z uporabniškim prostorom ter ima tudi dostop do podsistemov jedra, ki jih dovoljuje ta vrsta programa.

To je že podobno zmožnostim, ki jih ponujajo moduli jedra, v primerjavi s katerimi ima BPF nekaj prednosti (seveda lahko primerjate samo podobne aplikacije, na primer sledenje sistemu - z BPF ne morete napisati poljubnega gonilnika). Opazite lahko nižji vstopni prag (nekateri pripomočki, ki uporabljajo BPF, od uporabnika ne zahtevajo veščin programiranja jedra ali veščin programiranja na splošno), varnost med izvajanjem (dvignite roko v komentarjih za tiste, ki med pisanjem niso zlomili sistema ali testiranje modulov), atomičnost - pri ponovnem nalaganju modulov prihaja do izpadov, podsistem BPF pa zagotavlja, da ni spregledan noben dogodek (če smo pošteni, to ne velja za vse vrste programov BPF).

Prisotnost takšnih zmožnosti naredi BPF univerzalno orodje za razširitev jedra, kar potrjuje praksa: vse več novih vrst programov se dodaja BPF, vse več velikih podjetij uporablja BPF na bojnih strežnikih 24 × 7, vse več startupi gradijo svoje poslovanje na rešitvah, na katerih temeljijo BPF. BPF se uporablja povsod: pri zaščiti pred napadi DDoS, ustvarjanju SDN (na primer implementacija omrežij za kubernetes), kot glavno orodje za sledenje sistemu in zbiralec statističnih podatkov, v sistemih za zaznavanje vdorov in sistemih peskovnika itd.

Tukaj zaključimo pregledni del članka in si podrobneje oglejmo virtualni stroj in ekosistem BPF.

Digresija: pripomočki

Da bi lahko izvajali primere v naslednjih razdelkih, boste morda potrebovali številne pripomočke, vsaj llvm/clang s podporo bpf in bpftool. V razdelku Razvojna orodja Preberete lahko navodila za sestavljanje pripomočkov, kot tudi vaše jedro. Ta del je postavljen spodaj, da ne moti harmonije naše predstavitve.

BPF Virtual Machine Registri in sistem navodil

Arhitektura in ukazni sistem BPF sta bila razvita ob upoštevanju dejstva, da bodo programi napisani v jeziku C in po nalaganju v jedro prevedeni v izvorno kodo. Zato je bilo število registrov in nabor ukazov izbrano s pogledom na presečišče, v matematičnem smislu, zmogljivosti sodobnih strojev. Poleg tega so bile za programe uvedene različne omejitve, na primer do nedavnega ni bilo mogoče pisati zank in podprogramov, število navodil pa je bilo omejeno na 4096 (zdaj lahko privilegirani programi naložijo do milijon navodil).

BPF ima enajst uporabnikom dostopnih 64-bitnih registrov r0 -r10 in programski števec. Registrirajte se r10 vsebuje kazalec okvirja in je samo za branje. Programi imajo med izvajanjem dostop do 512-bajtnega sklada in neomejeno količino skupnega pomnilnika v obliki zemljevidov.

Programom BPF je dovoljeno zagnati določen niz pomočnikov za jedro tipa programa in v zadnjem času običajne funkcije. Vsaka klicana funkcija lahko sprejme do pet argumentov, posredovanih v registrih r1 -r5in vrnjena vrednost je posredovana r0. Zagotovljeno je, da se po vrnitvi z funkcije vsebina registrira r6 -r9 Ne bo spremenilo.

Za učinkovito prevajanje programa registri r0 -r11 za vse podprte arhitekture so edinstveno preslikani v realne registre, ob upoštevanju funkcij ABI trenutne arhitekture. Na primer za x86_64 registri r1 -r5, ki se uporabljajo za posredovanje funkcijskih parametrov, so prikazani na rdi, rsi, rdx, rcx, r8, ki se uporabljajo za posredovanje parametrov funkcijam na x86_64. Na primer, koda na levi se prevede v kodo na desni takole:

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

Registrirajte se r0 uporablja se tudi za vrnitev rezultata izvajanja programa in v registru r1 programu se posreduje kazalec na kontekst - odvisno od vrste programa je to lahko na primer struktura struct xdp_md (za XDP) ali strukturo struct __sk_buff (za različne omrežne programe) ali strukturo struct pt_regs (za različne vrste programov za sledenje) itd.

Tako smo imeli nabor registrov, pomočnike jedra, sklad, kazalec konteksta in skupni pomnilnik v obliki zemljevidov. Saj ne, da je vse to nujno potrebno na potovanju, ampak...

Nadaljujmo z opisom in se pogovorimo o ukaznem sistemu za delo s temi predmeti. vse (Skoraj vsi) Navodila BPF imajo fiksno 64-bitno velikost. Če pogledate eno navodilo na 64-bitnem stroju Big Endian, boste videli

BPF za najmlajše, prvi del: razširjeni BPF

Tukaj Code - to je kodiranje navodil, Dst/Src so kodiranja sprejemnika oziroma vira, Off - 16-bitni zamik s predznakom in Imm je 32-bitno celo število s predznakom, ki se uporablja v nekaterih navodilih (podobno konstanti cBPF K). Kodiranje Code ima eno od dveh vrst:

BPF za najmlajše, prvi del: razširjeni BPF

Razredi ukazov 0, 1, 2, 3 določajo ukaze za delo s pomnilnikom. Oni kličejo, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, oz. Razredi 4, 7 (BPF_ALU, BPF_ALU64) sestavljajo nabor navodil ALU. 5., 6. razred (BPF_JMP, BPF_JMP32) vsebujejo navodila za skok.

Nadaljnji načrt proučevanja sistema ukazov BPF je naslednji: namesto natančnega naštevanja vseh navodil in njihovih parametrov si bomo v tem razdelku ogledali nekaj primerov in iz njih bo razvidno, kako navodila dejansko delujejo in kako ročno razstavite katero koli binarno datoteko za BPF. Za utrjevanje gradiva v nadaljevanju članka se bomo srečali tudi s posameznimi navodili v razdelkih o Verifierju, JIT prevajalniku, prevodu klasičnega BPF, pa tudi pri preučevanju zemljevidov, klicnih funkcij itd.

Ko govorimo o posameznih navodilih, se bomo sklicevali na jedrne datoteke bpf.h и bpf_common.h, ki določajo številčne kode navodil BPF. Ko sami preučujete arhitekturo in/ali razčlenjujete binarne datoteke, lahko najdete semantiko v naslednjih virih, razvrščenih po zahtevnosti: Neuradna specifikacija eBPF, Referenčni vodnik za BPF in XDP, komplet navodil, Documentation/networking/filter.txt in seveda v izvorni kodi Linuxa - preverjalnik, JIT, BPF tolmač.

Primer: razstavljanje BPF v glavi

Poglejmo si primer, v katerem prevajamo program readelf-example.c in si oglejte nastalo dvojiško datoteko. Razkrili bomo izvirno vsebino readelf-example.c spodaj, potem ko obnovimo njegovo logiko iz binarnih kod:

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

Prvi stolpec v izpisu readelf je zamik, zato je naš program sestavljen iz štirih ukazov:

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

Kode ukazov so enake b7, 15, b7 и 95. Spomnimo se, da so najmanj pomembni trije biti razred navodil. V našem primeru je četrti bit vseh ukazov prazen, zato so razredi ukazov 7, 5, 7, 5. Razred 7 je BPF_ALU64, in 5 je BPF_JMP. Za oba razreda je format navodil enak (glej zgoraj) in naš program lahko prepišemo takole (hkrati bomo preostale stolpce prepisali v človeški obliki):

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

Operacija b Razred ALU64 - Je BPF_MOV. Ciljnemu registru dodeli vrednost. Če je bit nastavljen s (vir), potem se vrednost vzame iz izvornega registra, in če, kot v našem primeru, ni nastavljen, se vrednost vzame iz polja Imm. Torej v prvem in tretjem navodilu izvedemo operacijo r0 = Imm. Poleg tega je operacija JMP razreda 1 BPF_JEQ (skoči, če je enako). V našem primeru že od bit S je nič, primerja vrednost izvornega registra s poljem Imm. Če vrednosti sovpadajo, pride do prehoda PC + OffČe PC, kot običajno, vsebuje naslov naslednjega navodila. Končno, JMP Class 9 Operation je BPF_EXIT. To navodilo prekine program in se vrne v jedro r0. V našo tabelo dodamo nov stolpec:

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

To lahko prepišemo v bolj priročni obliki:

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

Če se spomnimo, kaj je v registru r1 programu se posreduje kazalec na kontekst iz jedra in v register r0 je jedru vrnjena vrednost, potem lahko vidimo, da če je kazalec na kontekst nič, vrnemo 1, sicer pa 2. Preverimo, ali imamo prav, tako da pogledamo izvor:

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

Da, to je nesmiseln program, vendar se prevede v samo štiri preprosta navodila.

Primer izjeme: 16-bajtno navodilo

Prej smo omenili, da nekatera navodila zavzamejo več kot 64 bitov. To velja na primer za navodila lddw (Koda = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — naloži dvojno besedo iz polj v register Imm. Dejstvo je, da Imm ima velikost 32, dvojna beseda pa je 64 bitov, tako da nalaganje 64-bitne takojšnje vrednosti v register v enem 64-bitnem ukazu ne bo delovalo. Za to se uporabita dve sosednji navodili za shranjevanje drugega dela 64-bitne vrednosti v polje Imm... Primer:

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

V binarnem programu sta samo dve navodili:

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

Z navodili se spet srečamo lddw, ko govorimo o selitvah in delu z zemljevidi.

Primer: razstavljanje BPF s standardnimi orodji

Tako smo se naučili brati binarne kode BPF in smo pripravljeni razčleniti katero koli navodilo, če je potrebno. Vendar je vredno povedati, da je v praksi bolj priročno in hitreje razstaviti programe s standardnimi orodji, na primer:

$ 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

Življenjski cikel objektov BPF, datotečni sistem bpffs

(Nekatere podrobnosti, opisane v tem pododdelku, sem najprej izvedel od postenje Aleksej Starovoitov in Blog BPF.)

Objekti BPF - programi in zemljevidi - so ustvarjeni iz uporabniškega prostora z uporabo ukazov BPF_PROG_LOAD и BPF_MAP_CREATE sistemski klic bpf(2), o tem, kako točno se to zgodi, bomo govorili v naslednjem razdelku. To ustvari podatkovne strukture jedra in za vsako od njih refcount (število sklicev) je nastavljeno na ena, deskriptor datoteke, ki kaže na predmet, pa je vrnjen uporabniku. Ko je ročaj zaprt refcount predmet se zmanjša za ena, in ko doseže nič, se predmet uniči.

Če program uporablja zemljevide, potem refcount ti zemljevidi se po nalaganju programa povečajo za eno, tj. njihove deskriptorje datotek je mogoče zapreti iz uporabniškega procesa in še vedno refcount ne bo postalo nič:

BPF za najmlajše, prvi del: razširjeni BPF

Po uspešnem nalaganju programa ga običajno priklopimo na nekakšen generator dogodkov. Na primer, lahko ga postavimo na omrežni vmesnik za obdelavo dohodnih paketov ali ga povežemo z nekaterimi tracepoint v jedru. Na tej točki se bo tudi referenčni števec povečal za eno in lahko bomo zaprli deskriptor datoteke v nalagalnem programu.

Kaj se zgodi, če zdaj zaustavimo zagonski nalagalnik? Odvisno je od vrste generatorja dogodkov (kavelj). Vsi omrežni kavlji bodo obstajali po zaključku nalagalnika, to so tako imenovani globalni kavlji. In na primer, programi za sledenje bodo sproščeni po zaključku procesa, ki jih je ustvaril (in se zato imenujejo lokalni, od "lokalno do procesa"). Tehnično gledano imajo lokalne kljuke vedno ustrezen deskriptor datoteke v uporabniškem prostoru in se zato zaprejo, ko je proces zaprt, globalne kljuke pa ne. Na naslednji sliki z rdečimi križci poskušam pokazati, kako prekinitev nalagalnega programa vpliva na življenjsko dobo objektov v primeru lokalnih in globalnih kavljev.

BPF za najmlajše, prvi del: razširjeni BPF

Zakaj obstaja razlika med lokalnimi in globalnimi trnki? Zagon nekaterih vrst omrežnih programov je smiseln brez uporabniškega prostora, na primer, predstavljajte si zaščito DDoS - zagonski nalagalnik napiše pravila in poveže program BPF z omrežnim vmesnikom, nakar se lahko zagonski nalagalnik ubije. Po drugi strani pa si predstavljajte program za sledenje odpravljanju napak, ki ste ga napisali na kolenih v desetih minutah – ko je končan, bi želeli, da v sistemu ne bi bilo smeti, za to pa bodo poskrbeli lokalni kavlji.

Po drugi strani pa si predstavljajte, da se želite povezati s točko sledenja v jedru in več let zbirati statistične podatke. V tem primeru bi želeli dokončati uporabniški del in se občasno vrniti k statistiki. Datotečni sistem bpf ponuja to možnost. Je psevdodatotečni sistem samo v pomnilniku, ki omogoča ustvarjanje datotek, ki se sklicujejo na objekte BPF in s tem povečajo refcount predmetov. Po tem lahko nakladalnik zapusti in predmeti, ki jih je ustvaril, bodo ostali živi.

BPF za najmlajše, prvi del: razširjeni BPF

Ustvarjanje datotek v bpffs, ki se sklicujejo na objekte BPF, se imenuje "pripenjanje" (kot v naslednji frazi: "proces lahko pripne program ali zemljevid BPF"). Ustvarjanje datotečnih objektov za objekte BPF ni smiselno samo zaradi podaljšanja življenjske dobe lokalnih objektov, temveč tudi zaradi uporabnosti globalnih objektov – če se vrnemo k primeru z globalnim programom za zaščito pred napadi DDoS, želimo imeti možnost priti in pogledati statistiko od časa do časa.

Datotečni sistem BPF je običajno nameščen v /sys/fs/bpf, vendar ga je mogoče namestiti tudi lokalno, na primer takole:

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

Imena datotečnih sistemov se ustvarijo z ukazom BPF_OBJ_PIN Sistemski klic BPF. Za ponazoritev vzemimo program, ga prevedemo, naložimo in pripnemo bpffs. Naš program ne naredi nič uporabnega, samo predstavljamo kodo, da lahko ponovite primer:

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

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

Prevedimo ta program in ustvarimo lokalno kopijo datotečnega sistema bpffs:

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

Zdaj pa prenesimo naš program s pomočjo pripomočka bpftool in si oglejte spremljajoče sistemske klice bpf(2) (nekatere nepomembne vrstice so bile odstranjene iz izpisa strace):

$ 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

Tukaj smo naložili program z uporabo BPF_PROG_LOAD, prejel deskriptor datoteke od jedra 3 in z uporabo ukaza BPF_OBJ_PIN je ta deskriptor datoteke pripel kot datoteko "bpf-mountpoint/test". Po tem bootloader program bpftool končal z delom, vendar je naš program ostal v jedru, čeprav ga nismo priključili na noben omrežni vmesnik:

$ 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

Objekt datoteke lahko normalno izbrišemo unlink(2) in po tem bo ustrezen program izbrisan:

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

Brisanje predmetov

Ko govorimo o brisanju predmetov, je treba pojasniti, da potem, ko smo program odklopili od kljuke (generatorja dogodkov), noben nov dogodek ne bo sprožil njegovega zagona, vendar bodo vsi trenutni primerki programa dokončani v običajnem vrstnem redu. .

Nekatere vrste programov BPF omogočajo sprotno zamenjavo programa, tj. zagotoviti atomičnost zaporedja replace = detach old program, attach new program. V tem primeru bodo vsi aktivni primerki stare različice programa končali svoje delo in iz novega programa bodo ustvarjeni novi obdelovalci dogodkov, »atomičnost« pa tukaj pomeni, da ne bo izpuščen niti en dogodek.

Pripenjanje programov k virom dogodkov

V tem članku ne bomo posebej opisovali povezovanja programov z viri dogodkov, saj je to smiselno preučevati v kontekstu določenega tipa programa. Cm. Primer spodaj, v katerem prikazujemo, kako so povezani programi, kot je XDP.

Manipulacija objektov s sistemskim klicem bpf

BPF programi

Vsi objekti BPF so ustvarjeni in upravljani iz uporabniškega prostora s sistemskim klicem bpf, ki ima naslednji prototip:

#include <linux/bpf.h>

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

Tukaj je ekipa cmd je ena od vrednosti tipa enum bpf_cmd, attr — kazalec na parametre za določen program in size — velikost predmeta glede na kazalec, tj. ponavadi to sizeof(*attr). V jedru 5.8 sistemski klic bpf podpira 34 različnih ukazov in določitev union bpf_attr zavzema 200 vrstic. Vendar nas to ne sme prestrašiti, saj se bomo z ukazi in parametri seznanili v več člankih.

Začnimo z ekipo BPF_PROG_LOAD, ki ustvarja programe BPF - vzame nabor navodil BPF in ga naloži v jedro. V trenutku nalaganja se zažene preveritelj, nato pa se uporabniku vrne prevajalnik JIT in po uspešni izvedbi deskriptor programske datoteke. Videli smo, kaj se z njim zgodi v prejšnjem razdelku o življenjskem ciklu objektov BPF.

Zdaj bomo napisali program po meri, ki bo naložil preprost program BPF, vendar se moramo najprej odločiti, kakšen program želimo naložiti - morali bomo izbrati Tip in v okviru tega tipa napisati program, ki bo prestal preizkus preverjanja. Da pa ne bi zapletli postopka, je tukaj že pripravljena rešitev: vzeli bomo program, kot je BPF_PROG_TYPE_XDP, ki bo vrnil vrednost XDP_PASS (preskoči vse pakete). V zbirniku BPF je videti zelo preprosto:

r0 = 2
exit

Potem ko smo se odločili za da bomo naložili, lahko vam povemo, kako bomo to storili:

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

Zanimivi dogodki v programu se začnejo z definicijo polja insns - naš program BPF v strojni kodi. V tem primeru je vsako navodilo programa BPF zapakirano v strukturo bpf_insn. Prvi element insns upošteva navodila r0 = 2, drugič - exit.

Umik. Jedro definira bolj priročne makre za pisanje strojnih kod in uporabo datoteke glave jedra tools/include/linux/filter.h lahko bi pisali

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

A ker je pisanje programov BPF v izvorni kodi potrebno samo za pisanje testov v jedru in člankov o BPF, odsotnost teh makrov ne oteži življenja razvijalca.

Po definiranju programa BPF preidemo na nalaganje v jedro. Naš minimalističen nabor parametrov attr vključuje vrsto programa, niz in število navodil, zahtevano licenco in ime "woo", s katerim po prenosu poiščemo naš program v sistemu. Program se, kot obljubljeno, naloži v sistem s sistemskim klicem bpf.

Na koncu programa končamo v neskončni zanki, ki simulira koristni tovor. Brez tega bo jedro program uničilo, ko se zapre deskriptor datoteke, ki nam ga je vrnil sistemski klic bpf, in tega ne bomo videli v sistemu.

Pa smo pripravljeni na testiranje. Sestavimo in zaženimo program pod straceda preverite, ali vse deluje, kot bi moralo:

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

Vse je vredu, bpf(2) nam je vrnil ročaj 3 in šli smo v neskončno zanko z pause(). Poskusimo najti naš program v sistemu. Za to bomo šli na drug terminal in uporabili pripomoček 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)

Vidimo, da je v sistemu naložen program woo katerega globalni ID je 390 in je trenutno v teku simple-prog obstaja deskriptor odprte datoteke, ki kaže na program (in če simple-prog potem bo dokončal delo woo bo izginil). Po pričakovanjih program woo sprejme 16 bajtov - dva ukaza - binarnih kod v arhitekturi BPF, vendar je v izvorni obliki (x86_64) že 40 bajtov. Oglejmo si naš program v izvirni obliki:

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

brez presenečenj. Zdaj pa poglejmo kodo, ki jo ustvari prevajalnik JIT:

# 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

ni zelo učinkovito za exit(2), a pošteno povedano, naš program je preveč preprost in za netrivialne programe sta seveda potrebna prolog in epilog, ki ju doda prevajalnik JIT.

Zemljevidi

Programi BPF lahko uporabljajo strukturirana pomnilniška področja, ki so dostopna drugim programom BPF in programom v uporabniškem prostoru. Ti objekti se imenujejo zemljevidi in v tem razdelku bomo pokazali, kako z njimi upravljati s sistemskim klicem bpf.

Takoj povejmo, da zmožnosti zemljevidov niso omejene le na dostop do skupnega pomnilnika. Obstajajo zemljevidi za posebne namene, ki vsebujejo na primer kazalce na programe BPF ali kazalce na omrežne vmesnike, zemljevide za delo s perf dogodki itd. O njih tukaj ne bomo govorili, da bralca ne zmedemo. Poleg tega ignoriramo težave s sinhronizacijo, saj to za naše primere ni pomembno. Celoten seznam razpoložljivih vrst zemljevidov najdete v <linux/bpf.h>, v tem razdelku pa bomo kot primer vzeli zgodovinsko prvo vrsto, razpršilno tabelo BPF_MAP_TYPE_HASH.

Če ustvarite zgoščevalno tabelo v, recimo, C++, bi rekli unordered_map<int,long> woo, kar v ruščini pomeni »Potrebujem mizo woo neomejene velikosti, katerih ključi so vrste int, vrednosti pa so vrsta long" Če želimo ustvariti zgoščevalno tabelo BPF, moramo narediti približno enako, le da moramo podati največjo velikost tabele in namesto da podamo vrste ključev in vrednosti, moramo podati njihove velikosti v bajtih . Za ustvarjanje zemljevidov uporabite ukaz BPF_MAP_CREATE sistemski klic bpf. Poglejmo bolj ali manj minimalen program, ki izdela zemljevid. Po prejšnjem programu, ki nalaga programe BPF, se vam mora tale zdeti preprost:

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

Tukaj definiramo nabor parametrov attr, v katerem rečemo »Potrebujem razpršilno tabelo s ključi in vrednostmi velikosti sizeof(int), v katerega lahko dam največ štiri elemente.« Ko ustvarjate zemljevide BPF, lahko določite druge parametre, na primer, na enak način kot v primeru s programom, določili smo ime predmeta kot "woo".

Prevedimo in zaženimo program:

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

Tukaj je sistemski klic bpf(2) nam je vrnil številko karte deskriptorja 3 nato pa program po pričakovanjih čaka na nadaljnja navodila v sistemskem klicu pause(2).

Zdaj pa pošljimo naš program v ozadje ali odprimo drug terminal in si oglejmo naš objekt s pomočjo pripomočka bpftool (naš zemljevid ločimo od drugih po imenu):

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

Številka 114 je globalni ID našega objekta. Vsak program v sistemu lahko uporabi ta ID za odpiranje obstoječega zemljevida z ukazom BPF_MAP_GET_FD_BY_ID sistemski klic bpf.

Zdaj se lahko igramo z našo razpršilno tabelo. Poglejmo si njegovo vsebino:

$ sudo bpftool map dump id 114
Found 0 elements

Prazno. Vnesimo vrednost vanj hash[1] = 1:

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

Poglejmo še enkrat tabelo:

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

Hura! Uspelo nam je dodati en element. Upoštevajte, da moramo za to delati na ravni bajtov, saj bptftool ne ve, katere vrste so vrednosti v zgoščeni tabeli. (To znanje je mogoče prenesti nanjo z uporabo BTF, a več o tem zdaj.)

Kako natančno bpftool bere in dodaja elemente? Poglejmo pod pokrov:

$ 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

Najprej smo odprli zemljevid z njegovim globalnim ID-jem z ukazom BPF_MAP_GET_FD_BY_ID и bpf(2) nam je vrnil deskriptor 3. Nadaljnja uporaba ukaza BPF_MAP_GET_NEXT_KEY s prehajanjem smo našli prvi ključ v tabeli NULL kot kazalec na "prejšnji" ključ. Če imamo ključ, zmoremo BPF_MAP_LOOKUP_ELEMki vrne vrednost kazalcu value. Naslednji korak je, da poskušamo najti naslednji element s posredovanjem kazalca na trenutni ključ, vendar naša tabela vsebuje samo en element in ukaz BPF_MAP_GET_NEXT_KEY vrne ENOENT.

V redu, spremenimo vrednost s ključem 1, recimo, da naša poslovna logika zahteva registracijo 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

Kot je bilo pričakovano, je zelo preprosto: ukaz BPF_MAP_GET_FD_BY_ID odpre naš zemljevid z ID-jem in ukazom BPF_MAP_UPDATE_ELEM prepiše element.

Torej, ko ustvarimo razpršilno tabelo iz enega programa, lahko beremo in pišemo njeno vsebino iz drugega. Upoštevajte, da če smo to lahko storili iz ukazne vrstice, lahko to stori kateri koli drug program v sistemu. Poleg zgoraj opisanih ukazov za delo z zemljevidi iz uporabniškega prostora, Naslednji:

  • BPF_MAP_LOOKUP_ELEM: poiščite vrednost po ključu
  • BPF_MAP_UPDATE_ELEM: posodobi/ustvari vrednost
  • BPF_MAP_DELETE_ELEM: odstranite ključ
  • BPF_MAP_GET_NEXT_KEY: poiščite naslednji (ali prvi) ključ
  • BPF_MAP_GET_NEXT_ID: omogoča pregledovanje vseh obstoječih zemljevidov, tako deluje bpftool map
  • BPF_MAP_GET_FD_BY_ID: odpre obstoječi zemljevid z njegovim globalnim ID-jem
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomsko posodobi vrednost predmeta in vrne staro
  • BPF_MAP_FREEZE: naredi zemljevid nespremenljiv iz uporabniškega prostora (te operacije ni mogoče razveljaviti)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: množične operacije. na primer BPF_MAP_LOOKUP_AND_DELETE_BATCH - to je edini zanesljiv način za branje in ponastavitev vseh vrednosti iz zemljevida

Vsi ti ukazi ne delujejo za vse vrste zemljevidov, vendar je na splošno delo z drugimi vrstami zemljevidov iz uporabniškega prostora videti popolnoma enako kot delo z zgoščenimi tabelami.

Zaradi reda zaključimo naše poskuse zgoščevalnih tabel. Se spomnite, da smo ustvarili tabelo, ki lahko vsebuje do štiri ključe? Dodajmo še nekaj elementov:

$ 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

Zaenkrat dobro:

$ 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

Poskusimo dodati še eno:

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

Po pričakovanjih nam ni uspelo. Oglejmo si napako podrobneje:

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

Vse v redu: po pričakovanjih, ekipa BPF_MAP_UPDATE_ELEM poskuša ustvariti nov, peti ključ, vendar se zruši E2BIG.

Tako lahko ustvarjamo in nalagamo programe BPF ter ustvarjamo in upravljamo zemljevide iz uporabniškega prostora. Zdaj je logično pogledati, kako lahko uporabimo zemljevide iz samih programov BPF. O tem bi lahko govorili v jeziku težko berljivih programov v strojnih makro kodah, a dejansko je prišel čas, da pokažemo, kako so programi BPF dejansko napisani in vzdrževani - z uporabo libbpf.

(Za bralce, ki niso zadovoljni s pomanjkanjem primera nizke ravni: podrobno bomo analizirali programe, ki uporabljajo zemljevide in pomožne funkcije, ustvarjene z libbpf in vam povem, kaj se zgodi na ravni navodil. Za bralce, ki so nezadovoljni zelo, smo dodali Primer na ustreznem mestu v članku.)

Pisanje programov BPF z uporabo libbpf

Pisanje programov BPF z uporabo strojnih kod je lahko zanimivo le prvič, potem pa nastopi sitost. V tem trenutku se morate osredotočiti na llvm, ki ima zaledje za generiranje kode za arhitekturo BPF, pa tudi knjižnico libbpf, ki vam omogoča pisanje uporabniške strani aplikacij BPF in nalaganje kode programov BPF, ustvarjenih z llvm/clang.

Pravzaprav, kot bomo videli v tem in naslednjih člankih, libbpf opravi precej dela brez njega (ali podobnih orodij - iproute2, libbcc, libbpf-go, itd.) je nemogoče živeti. Ena od ubijalskih lastnosti projekta libbpf je BPF CO-RE (Compile Once, Run Everywhere) – projekt, ki omogoča pisanje programov BPF, ki so prenosljivi iz enega jedra v drugo, z možnostjo izvajanja na različnih API-jih (na primer, ko se struktura jedra spremeni glede na različico na različico). Da bi lahko delovali s CO-RE, mora biti vaše jedro prevedeno s podporo za BTF (kako to storite, opisujemo v razdelku Razvojna orodja. Preprosto lahko preverite, ali je vaše jedro zgrajeno z BTF ali ne - s prisotnostjo naslednje datoteke:

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

Ta datoteka shranjuje informacije o vseh vrstah podatkov, uporabljenih v jedru, in se uporablja v vseh naših primerih uporabe libbpf. Podrobneje o CO-RE bomo govorili v naslednjem članku, v tem pa si samo zgradite jedro CONFIG_DEBUG_INFO_BTF.

Knjižnica libbpf živi prav v imeniku tools/lib/bpf jedro in njegov razvoj poteka prek poštnega seznama [email protected]. Vendar se vzdržuje ločeno skladišče za potrebe aplikacij, ki živijo zunaj jedra https://github.com/libbpf/libbpf v katerem je knjižnica jedra zrcaljena za bralni dostop bolj ali manj takšna, kot je.

V tem razdelku si bomo ogledali, kako lahko ustvarite projekt, ki uporablja libbpf, napišimo več (bolj ali manj nesmiselnih) testnih programov in podrobno analizirajmo, kako vse skupaj deluje. To nam bo omogočilo, da v naslednjih razdelkih lažje razložimo, kako točno programi BPF komunicirajo z zemljevidi, pomočniki jedra, BTF itd.

Običajno projekti uporabljajo libbpf dodamo repozitorij GitHub kot podmodul git, bomo storili enako:

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

Grem na libbpf zelo preprosto:

$ 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

Naš naslednji načrt v tem razdelku je naslednji: napisali bomo program BPF, kot je BPF_PROG_TYPE_XDP, enako kot v prejšnjem primeru, vendar ga v C prevedemo z uporabo clang, in napišite pomožni program, ki ga bo naložil v jedro. V naslednjih razdelkih bomo razširili zmožnosti programa BPF in programa pomočnika.

Primer: ustvarjanje polne aplikacije z uporabo libbpf

Za začetek uporabimo datoteko /sys/kernel/btf/vmlinux, ki je bil omenjen zgoraj, in ustvarite njegov ekvivalent v obliki datoteke glave:

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

Ta datoteka bo shranila vse podatkovne strukture, ki so na voljo v našem jedru, na primer, tako je definirana glava IPv4 v jedru:

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

Zdaj bomo naš program BPF napisali v 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";

Čeprav se je izkazalo, da je naš program zelo preprost, moramo vseeno biti pozorni na številne podrobnosti. Prvič, prva datoteka glave, ki jo vključimo, je vmlinux.h, ki smo ga pravkar ustvarili z uporabo bpftool btf dump - zdaj nam ni treba namestiti paketa kernel-headers, da bi ugotovili, kako izgledajo strukture jedra. Naslednja datoteka glave prihaja iz knjižnice libbpf. Zdaj ga potrebujemo samo za definiranje makra SEC, ki pošlje znak v ustrezen razdelek objektne datoteke ELF. Naš program je v rubriki xdp/simple, kjer pred poševnico definiramo vrsto programa BPF - to je konvencija, ki se uporablja v libbpf, bo glede na ime razdelka ob zagonu zamenjal pravilno vrsto bpf(2). Sam program BPF je C - zelo preprosto in je sestavljeno iz ene vrstice return XDP_PASS. Nazadnje, ločen razdelek "license" vsebuje ime licence.

Naš program lahko prevedemo z uporabo llvm/clang, različica >= 10.0.0 ali še bolje, večja (glejte razdelek Razvojna orodja):

$ 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

Med zanimivostmi: navedemo ciljno arhitekturo -target bpf in pot do glav libbpf, ki smo ga pred kratkim namestili. Prav tako ne pozabite na -O2, brez te možnosti vas lahko v prihodnosti čakajo presenečenja. Poglejmo našo kodo, ali nam je uspelo napisati želeni program?

$ 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, uspelo je! Zdaj imamo binarno datoteko s programom in želimo ustvariti aplikacijo, ki jo bo naložila v jedro. V ta namen knjižnica libbpf nam ponuja dve možnosti - uporabo API-ja nižje ravni ali API-ja višje ravni. Šli bomo po drugi poti, saj se želimo naučiti pisati, nalagati in povezovati programe BPF z minimalnim naporom za njihovo kasnejšo študijo.

Najprej moramo ustvariti "okostje" našega programa iz njegove binarne datoteke z istim pripomočkom bpftool — švicarski nož sveta BPF (kar lahko razumemo dobesedno, saj je Daniel Borkman, eden od ustvarjalcev in vzdrževalcev BPF, Švicar):

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

V datoteki xdp-simple.skel.h vsebuje binarno kodo našega programa in funkcije za upravljanje - nalaganje, pripenjanje, brisanje našega objekta. V našem preprostem primeru je to videti pretirano, vendar deluje tudi v primeru, ko objektna datoteka vsebuje veliko programov in zemljevidov BPF in za nalaganje tega velikanskega ELF moramo samo ustvariti okostje in poklicati eno ali dve funkciji iz aplikacije po meri, ki jo pišejo Gremo zdaj naprej.

Strogo gledano je naš nalagalni program trivialen:

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

Tukaj struct xdp_simple_bpf določeno v datoteki xdp-simple.skel.h in opisuje našo predmetno datoteko:

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

Tukaj lahko vidimo sledi API-ja nizke ravni: struktura struct bpf_program *simple и struct bpf_link *simple. Prva struktura posebej opisuje naš program, zapisan v razdelku xdp/simple, drugi pa opisuje, kako se program poveže z izvorom dogodka.

Funkcija xdp_simple_bpf__open_and_load, odpre objekt ELF, ga razčleni, ustvari vse strukture in podstrukture (poleg programa vsebuje ELF tudi druge razdelke - podatke, podatke samo za branje, informacije o odpravljanju napak, licenco itd.) in ga nato naloži v jedro s pomočjo sistema klic bpf, kar lahko preverimo tako, da prevedemo in zaženemo program:

$ 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

Poglejmo zdaj naš program z uporabo bpftool. Poiščimo njen 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)

in dump (uporabljamo skrajšano obliko ukaza bpftool prog dump xlated):

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

Nekaj ​​novega! Program je natisnil dele naše izvorne datoteke C. To je naredila knjižnica libbpf, ki je našel razdelek za odpravljanje napak v binarni datoteki, ga prevedel v objekt BTF in ga naložil v jedro z uporabo BPF_BTF_LOAD, in nato ob nalaganju programa z ukazom določil nastali deskriptor datoteke BPG_PROG_LOAD.

Pomočniki jedra

Programi BPF lahko izvajajo "zunanje" funkcije - pomočnike jedra. Te pomožne funkcije omogočajo programom BPF dostop do struktur jedra, upravljanje zemljevidov in tudi komunikacijo z "resničnim svetom" - ustvarjanje perf dogodkov, nadzor strojne opreme (na primer preusmeritev paketov) itd.

Primer: bpf_get_smp_processor_id

V okviru paradigme »učenje z zgledom« razmislimo o eni od funkcij pomočnika, bpf_get_smp_processor_id(), določene v datoteki kernel/bpf/helpers.c. Vrne številko procesorja, na katerem se izvaja program BPF, ki ga je poklical. Vendar nas ne zanima toliko njegova semantika kot dejstvo, da njegova implementacija poteka v eni vrstici:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Definicije funkcij pomočnika BPF so podobne definicijam sistemskih klicev Linux. Tukaj je na primer definirana funkcija, ki nima argumentov. (Funkcija, ki sprejme, recimo, tri argumente, je definirana z uporabo makra BPF_CALL_3. Največje število argumentov je pet.) Vendar je to le prvi del definicije. Drugi del je definiranje strukture tipa struct bpf_func_proto, ki vsebuje opis pomožne funkcije, ki jo preveritelj razume:

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

Registracija pomožnih funkcij

Da lahko programi BPF določene vrste uporabljajo to funkcijo, jo morajo registrirati, na primer za vrsto BPF_PROG_TYPE_XDP funkcija je definirana v jedru xdp_func_proto, ki iz ID-ja pomožne funkcije določi, ali XDP podpira to funkcijo ali ne. Naša funkcija je podpira:

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

Nove vrste programov BPF so "definirane" v datoteki include/linux/bpf_types.h z uporabo makra BPF_PROG_TYPE. Definirano v narekovajih, ker je to logična definicija, v izrazih jezika C pa se definicija celotnega niza konkretnih struktur pojavlja na drugih mestih. Zlasti v datoteki kernel/bpf/verifier.c vse definicije iz datoteke bpf_types.h se uporabljajo za ustvarjanje niza struktur 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
};

To pomeni, da je za vsako vrsto programa BPF definiran kazalec na podatkovno strukturo tipa struct bpf_verifier_ops, ki je inicializiran z vrednostjo _name ## _verifier_ops, tj. xdp_verifier_ops za xdp. Struktura xdp_verifier_ops določi s v datoteki net/core/filter.c kot sledi:

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

Tukaj vidimo našo znano funkcijo xdp_func_proto, ki bo zagnal preverjalnik vsakič, ko bo naletel na izziv neke vrste funkcije znotraj programa BPF, glejte verifier.c.

Poglejmo, kako hipotetični program BPF uporablja funkcijo bpf_get_smp_processor_id. Da bi to naredili, prepišemo program iz našega prejšnjega razdelka, kot sledi:

#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";

Simbol bpf_get_smp_processor_id določi s в <bpf/bpf_helper_defs.h> knjižnice libbpf kot

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

to je, bpf_get_smp_processor_id je funkcijski kazalec, katerega vrednost je 8, kjer je 8 vrednost BPF_FUNC_get_smp_processor_id tip enum bpf_fun_id, ki nam je definiran v datoteki vmlinux.h (mapa bpf_helper_defs.h v jedru generira skript, tako da so "magične" številke v redu). Ta funkcija ne sprejema argumentov in vrne vrednost vrste __u32. Ko ga izvajamo v našem programu, clang ustvari navodilo BPF_CALL "prava vrsta" Sestavimo program in poglejmo razdelek 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

V prvi vrstici vidimo navodila call, parameter IMM kar je enako 8, in SRC_REG - nič. Glede na dogovor ABI, ki ga uporablja preveritelj, je to klic pomožne funkcije številka osem. Ko je enkrat zagnan, je logika preprosta. Povratna vrednost iz registra r0 kopirano v r1 in v vrsticah 2,3 se pretvori v vrsto u32 — zgornjih 32 bitov je počiščenih. V vrsticah 4,5,6,7 vrnemo 2 (XDP_PASS) ali 1 (XDP_DROP), odvisno od tega, ali je pomožna funkcija iz vrstice 0 vrnila vrednost nič ali nič.

Preizkusimo se: naložimo program in poglejmo izhod 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

V redu, preverjalnik je našel pravilnega pomočnika za jedro.

Primer: posredovanje argumentov in končno izvajanje programa!

Vse pomožne funkcije na ravni izvajanja imajo prototip

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

Parametri pomožnim funkcijam se posredujejo v registrih r1 -r5, vrednost pa se vrne v register r0. Ni funkcij, ki sprejmejo več kot pet argumentov, in podpora zanje naj ne bi bila dodana v prihodnosti.

Oglejmo si novega pomočnika za jedro in kako BPF posreduje parametre. Prepišimo xdp-simple.bpf.c kot sledi (ostale vrstice niso spremenjene):

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

Naš program natisne številko procesorja, na katerem deluje. Prevedimo ga in poglejmo kodo:

$ 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

V vrstice 0-7 zapišemo niz running on CPU%un, nato pa v vrstici 8 zaženemo znanega bpf_get_smp_processor_id. V vrsticah 9-12 pripravimo pomožne argumente bpf_printk - registri r1, r2, r3. Zakaj so trije in ne dva? Ker bpf_printkto je makro ovoj okoli pravega pomočnika bpf_trace_printk, ki mora posredovati velikost formatnega niza.

Zdaj pa dodajmo nekaj vrstic xdp-simple.ctako da se naš program poveže z vmesnikom lo in res se je začelo!

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

Tukaj uporabljamo funkcijo bpf_set_link_xdp_fd, ki povezuje programe BPF tipa XDP z omrežnimi vmesniki. Številko vmesnika smo trdo kodirali lo, ki je vedno 1. Funkcijo zaženemo dvakrat, da najprej odklopimo stari program, če je bil priložen. Upoštevajte, da zdaj ne potrebujemo izziva pause ali neskončna zanka: naš nalagalni program bo zapustil, vendar program BPF ne bo uničen, ker je povezan z izvorom dogodka. Po uspešnem prenosu in vzpostavitvi povezave se bo program zagnal za vsak prispeli omrežni paket lo.

Prenesimo program in si oglejmo vmesnik 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

Program, ki smo ga prenesli, ima ID 669 in isti ID vidimo na vmesniku lo. Poslali bomo nekaj paketov na 127.0.0.1 (povpraševanje + odgovor):

$ ping -c1 localhost

zdaj pa si poglejmo vsebino virtualne datoteke za odpravljanje napak /sys/kernel/debug/tracing/trace_pipe, v katerem bpf_printk piše svoja sporočila:

# 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

Opažena sta bila dva paketa lo in obdelan na CPU0 - naš prvi polnopravni nesmiselni program BPF je deloval!

To velja omeniti bpf_printk Ni zaman, da piše v datoteko za odpravljanje napak: to ni najuspešnejši pomočnik za uporabo v proizvodnji, vendar je bil naš cilj pokazati nekaj preprostega.

Dostop do zemljevidov iz programov BPF

Primer: uporaba zemljevida iz programa BPF

V prejšnjih razdelkih smo se naučili ustvarjati in uporabljati zemljevide iz uporabniškega prostora, zdaj pa si poglejmo del jedra. Začnimo, kot običajno, s primerom. Prepišimo naš program xdp-simple.bpf.c kot sledi:

#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";

Na začetku programa smo dodali definicijo zemljevida woo: To je niz z 8 elementi, ki shranjuje vrednosti, kot so u64 (v C bi takšno matriko definirali kot u64 woo[8]). V programu "xdp/simple" dobimo trenutno številko procesorja v spremenljivko key in nato s funkcijo pomočnika bpf_map_lookup_element dobimo kazalec na ustrezen vnos v nizu, ki ga povečamo za ena. Prevedeno v ruščino: izračunamo statistiko o tem, kateri procesor je obdelal dohodne pakete. Poskusimo zagnati program:

$ 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

Preverimo, ali je navezana na lo in pošlji nekaj paketov:

$ 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

Zdaj pa poglejmo vsebino matrike:

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

Skoraj vsi procesi so bili obdelani na CPU7. To za nas ni pomembno, glavno je, da program deluje in razumemo, kako dostopati do zemljevidov iz programov BPF - z uporabo хелперов bpf_mp_*.

Mistično kazalo

Torej lahko dostopamo do zemljevida iz programa BPF s klici, kot je

val = bpf_map_lookup_elem(&woo, &key);

kako izgleda pomočna funkcija

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

vendar gremo mimo kazalca &woo na neimenovano strukturo struct { ... }...

Če pogledamo programski sestavljalnik, vidimo, da je vrednost &woo dejansko ni definiran (vrstica 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
...

in je vsebovano v premestitvah:

$ 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

Če pa pogledamo že naložen program, vidimo kazalec na pravilen zemljevid (4. vrstica):

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

Tako lahko sklepamo, da je v času zagona našega nakladalnega programa povezava do &woo je nadomestilo nekaj s knjižnico libbpf. Najprej si bomo ogledali izhod 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

To vidimo libbpf ustvarili zemljevid woo in nato prenesli naš program simple. Oglejmo si podrobneje, kako naložimo program:

  • klic xdp_simple_bpf__open_and_load iz datoteke xdp-simple.skel.h
  • ki povzroča xdp_simple_bpf__load iz datoteke xdp-simple.skel.h
  • ki povzroča bpf_object__load_skeleton iz datoteke libbpf/src/libbpf.c
  • ki povzroča bpf_object__load_xattr z dne libbpf/src/libbpf.c

Zadnja funkcija bo med drugim poklicala bpf_object__create_maps, ki ustvari ali odpre obstoječe zemljevide in jih spremeni v deskriptorje datotek. (Tukaj vidimo BPF_MAP_CREATE v izhodu strace.) Nato se pokliče funkcija bpf_object__relocate in prav ona nas zanima, saj se spomnimo, kaj smo videli woo v premestitveni tabeli. Ko ga raziskujemo, se na koncu znajdemo v funkciji bpf_program__relocate, ki se ukvarja s prestavitvami zemljevidov:

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

Zato upoštevamo naša navodila

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

in zamenjajte izvorni register v njem z BPF_PSEUDO_MAP_FD, in prvi IMM v deskriptor datoteke našega zemljevida in, če je enak npr. 0xdeadbeef, potem bomo kot rezultat prejeli navodilo

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

Tako se informacije zemljevida prenesejo v določen naložen program BPF. V tem primeru lahko zemljevid ustvarite z uporabo BPF_MAP_CREATE, in odprl ID z uporabo BPF_MAP_GET_FD_BY_ID.

Skupaj, pri uporabi libbpf algoritem je naslednji:

  • med prevajanjem se v premestitveni tabeli ustvarijo zapisi za povezave do zemljevidov
  • libbpf odpre knjigo predmetov ELF, poišče vse uporabljene zemljevide in zanje ustvari deskriptorje datotek
  • deskriptorji datotek so naloženi v jedro kot del navodil LD64

Kot si lahko predstavljate, prihaja še več in pogledati bomo morali v bistvo. Na srečo imamo namig – zapisali smo pomen BPF_PSEUDO_MAP_FD v izvorni register in ga lahko zakopljemo, kar nas bo pripeljalo do svetinje vseh svetnikov - kernel/bpf/verifier.c, kjer funkcija z značilnim imenom zamenja deskriptor datoteke z naslovom strukture tipa 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;

(celotno kodo najdete по ссылке). Tako lahko razširimo naš algoritem:

  • med nalaganjem programa preverjalnik preveri pravilnost uporabe zemljevida in zapiše naslov pripadajoče strukture struct bpf_map

Pri prenosu binarne datoteke ELF z uporabo libbpf Dogaja se še marsikaj, a o tem bomo razpravljali v drugih člankih.

Nalaganje programov in zemljevidov brez libbpf

Kot obljubljeno, tukaj je primer za bralce, ki želijo vedeti, kako ustvariti in naložiti program, ki uporablja zemljevide, brez pomoči libbpf. To je lahko uporabno, ko delate v okolju, za katerega ne morete zgraditi odvisnosti, ali prihraniti vsakega bita ali napisati program, kot je ply, ki sproti ustvarja binarno kodo BPF.

Da bi lažje sledili logiki, bomo naš primer za te namene prepisali xdp-simple. Celotno in nekoliko razširjeno kodo programa, obravnavanega v tem primeru, lahko najdete tukaj bistvo.

Logika naše aplikacije je naslednja:

  • ustvarite tipski zemljevid BPF_MAP_TYPE_ARRAY z uporabo ukaza BPF_MAP_CREATE,
  • ustvarite program, ki uporablja ta zemljevid,
  • povežite program z vmesnikom lo,

kar se v človeško prevede kot

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

Tukaj map_create ustvari zemljevid na enak način kot v prvem primeru o sistemskem klicu bpf - “jedro, prosim naredi mi nov zemljevid v obliki niza 8 elementov, kot je __u64 in mi vrni deskriptor datoteke":

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

Program je tudi enostaven za nalaganje:

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

Zapleten del prog_load je definicija našega programa BPF kot niza struktur struct bpf_insn insns[]. Ker pa uporabljamo program, ki ga imamo v C, lahko malo goljufamo:

$ 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

Skupaj moramo napisati 14 navodil v obliki struktur, kot je struct bpf_insn (nasvet: vzemite smetišče od zgoraj, ponovno preberite razdelek z navodili, odprite linux/bpf.h и linux/bpf_common.h in poskusite ugotoviti struct bpf_insn insns[] po svoje):

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

Vaja za tiste, ki tega niste napisali sami - poiščite map_fd.

V našem programu je ostal še en nerazkrit del - xdp_attach. Na žalost programov, kot je XDP, ni mogoče povezati s sistemskim klicem bpf. Ljudje, ki so ustvarili BPF in XDP, so bili iz spletne skupnosti Linux, kar pomeni, da so uporabili tistega, ki jim je najbolj znan (vendar ne za normalno ljudje) vmesnik za interakcijo z jedrom: netlink vtičnice, Poglej tudi RFC3549. Najenostavnejši način izvedbe xdp_attach kopira kodo iz libbpf, in sicer iz datoteke netlink.c, kar smo tudi storili in ga nekoliko skrajšali:

Dobrodošli v svetu netlink vtičnic

Odprite vrsto vtičnice netlink 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;
}

Iz te vtičnice beremo:

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

Končno je tukaj naša funkcija, ki odpre vtičnico in ji pošlje posebno sporočilo, ki vsebuje deskriptor datoteke:

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

Torej, vse je pripravljeno za testiranje:

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

Poglejmo, ali se je naš program povezal z 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

Pošljimo pinge in poglejmo zemljevid:

$ 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

Hura, vse deluje. Mimogrede, upoštevajte, da je naš zemljevid spet prikazan v obliki bajtov. To je posledica dejstva, da za razliko od libbpf nismo naložili informacij o vrsti (BTF). A o tem bomo več govorili naslednjič.

Razvojna orodja

V tem razdelku si bomo ogledali minimalni komplet orodij za razvijalce BPF.

Na splošno za razvoj programov BPF ne potrebujete nič posebnega - BPF deluje na katerem koli spodobnem distribucijskem jedru, programi pa so zgrajeni z uporabo clang, ki se lahko dobavi iz paketa. Ker pa je BPF v razvoju, se jedro in orodja nenehno spreminjajo, če ne želite pisati programov BPF z uporabo staromodnih metod iz leta 2019, boste morali prevesti

  • llvm/clang
  • pahole
  • njeno jedro
  • bpftool

(Za referenco, ta razdelek in vsi primeri v članku so bili izvedeni v Debianu 10.)

llvm/clang

BPF je prijazen do LLVM in čeprav je nedavno mogoče programe za BPF prevesti z uporabo gcc, se ves trenutni razvoj izvaja za LLVM. Zato bomo najprej zgradili trenutno različico clang iz 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
... много времени спустя
$

Sedaj lahko preverimo, ali je vse skupaj pravilno:

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

(Navodila za montažo clang vzeto iz bpf_devel_QA.)

Programov, ki smo jih pravkar zgradili, ne bomo namestili, temveč jih bomo samo dodali PATH, na primer:

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

(To lahko dodate k .bashrc ali v ločeno datoteko. Osebno dodajam takšne stvari k ~/bin/activate-llvm.sh in ko je treba to naredim . activate-llvm.sh.)

Pahole in BTF

Uporabnost pahole ki se uporablja pri gradnji jedra za ustvarjanje informacij za odpravljanje napak v formatu BTF. V tem članku se ne bomo spuščali v podrobnosti o tehnologiji BTF, razen dejstva, da je priročna in jo želimo uporabljati. Torej, če nameravate zgraditi svoje jedro, najprej zgradite pahole (brez pahole ne boste mogli zgraditi jedra z možnostjo 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

Jedra za eksperimentiranje z BPF

Ko raziskujem možnosti BPF, želim sestaviti lastno jedro. To na splošno ni potrebno, saj boste lahko prevajali in nalagali programe BPF v distribucijskem jedru, vendar pa vam lastno jedro omogoča uporabo najnovejših funkcij BPF, ki se bodo v vaši distribuciji pojavile v najboljšem primeru čez mesece ali, kot v primeru nekaterih orodij za odpravljanje napak, v bližnji prihodnosti sploh ne bo zapakiran. Poleg tega je zaradi lastnega jedra pomembno eksperimentirati s kodo.

Če želite zgraditi jedro, potrebujete, prvič, samo jedro in drugič, konfiguracijsko datoteko jedra. Za eksperimentiranje z BPF lahko uporabimo običajno vanilija jedro ali eno od razvojnih jeder. Zgodovinsko gledano razvoj BPF poteka znotraj omrežne skupnosti Linux in zato gredo vse spremembe prej ali slej prek Davida Millerja, vzdrževalca omrežij Linux. Glede na njihovo naravo - urejanja ali nove funkcije - spremembe omrežja spadajo v eno od dveh jeder - net ali net-next. Spremembe za BPF so razdeljene na enak način med bpf и bpf-next, ki se nato združijo v net oziroma net-next. Za več podrobnosti glejte bpf_devel_QA и netdev-FAQ. Zato izberite jedro glede na svoj okus in potrebe po stabilnosti sistema, v katerem preizkušate (*-next jedra so najbolj nestabilna od naštetih).

Razprava o tem, kako upravljati konfiguracijske datoteke jedra, ne spada v okvir tega članka - predpostavlja se, da to že veste ali pripravljen na učenje na svojem. Vendar pa bi morala naslednja navodila bolj ali manj zadostovati, da vam zagotovijo delujoč sistem, ki podpira BPF.

Prenesite eno od zgornjih jeder:

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

Zgradite minimalno delujočo konfiguracijo jedra:

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

Omogoči možnosti BPF v datoteki .config po lastni izbiri (najverjetneje CONFIG_BPF bo že omogočen, ker ga sistem uporablja). Tukaj je seznam možnosti iz jedra, uporabljenega za ta članek:

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

Nato lahko enostavno sestavimo in namestimo module in jedro (mimogrede, jedro lahko sestavite z novo sestavljenim clangz dodajanjem CC=clang):

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

in znova zaženite z novim jedrom (za to uporabljam kexec iz paketa 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

Najpogosteje uporabljen pripomoček v članku bo pripomoček bpftool, dobavljen kot del jedra Linuxa. Napisali in vzdržujejo ga razvijalci BPF za razvijalce BPF in se lahko uporablja za upravljanje vseh vrst objektov BPF – nalaganje programov, ustvarjanje in urejanje zemljevidov, raziskovanje življenja ekosistema BPF itd. Najdete lahko dokumentacijo v obliki izvornih kod za strani z navodili v jedru ali že sestavljeno, na spletu.

V času tega pisanja bpftool je že pripravljen samo za RHEL, Fedora in Ubuntu (glejte, na primer, ta nit, ki pripoveduje nedokončano zgodbo embalaže bpftool v Debianu). Toda če ste že zgradili svoje jedro, ga zgradite bpftool enostavno kot pita:

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

$

(tukaj ${linux} - to je imenik vašega jedra.) Po izvedbi teh ukazov bpftool bodo zbrani v imeniku ${linux}/tools/bpf/bpftool in ga je mogoče dodati na pot (najprej uporabniku root) ali samo kopirajte na /usr/local/sbin.

Zberite bpftool najbolje je uporabiti slednjega clang, sestavljeno, kot je opisano zgoraj, in preverite, ali je pravilno sestavljeno - na primer z ukazom

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

ki bo pokazal, katere funkcije BPF so omogočene v vašem jedru.

Mimogrede, prejšnji ukaz lahko zaženete kot

# bpftool f p k

To se naredi po analogiji s pripomočki iz paketa iproute2, kjer lahko npr ip a s eth0 namesto ip addr show dev eth0.

Zaključek

BPF vam omogoča, da podkujete bolho za učinkovito merjenje in sprotno spreminjanje funkcionalnosti jedra. Sistem se je izkazal za zelo uspešnega, v najboljših tradicijah UNIX-a: preprost mehanizem, ki omogoča (ponovno) programiranje jedra, je omogočil eksperimentiranje ogromnemu številu ljudi in organizacij. In čeprav poskusi, kot tudi razvoj same infrastrukture BPF, še zdaleč niso končani, sistem že ima stabilen ABI, ki vam omogoča, da zgradite zanesljivo in, kar je najpomembneje, učinkovito poslovno logiko.

Rad bi opozoril, da je po mojem mnenju tehnologija postala tako priljubljena, ker po eni strani lahko igraj (arhitekturo stroja je mogoče razumeti več ali manj v enem večeru), po drugi strani pa rešiti probleme, ki jih pred njegovim pojavom ni bilo (lepo) rešiti. Ti dve komponenti skupaj silita ljudi k eksperimentiranju in sanjarjenju, kar vodi v nastanek vedno bolj inovativnih rešitev.

Ta članek, čeprav ni posebej kratek, je le uvod v svet BPF in ne opisuje "naprednih" funkcij in pomembnih delov arhitekture. Načrt za naprej je približno tak: naslednji članek bo pregled vrst programov BPF (v jedru 5.8 je podprtih 30 vrst programov), nato pa si bomo končno ogledali, kako napisati prave aplikacije BPF z uporabo programov za sledenje jedru kot primer, potem je čas za bolj poglobljen tečaj o arhitekturi BPF, ki mu sledijo primeri omrežnih in varnostnih aplikacij BPF.

Prejšnji članki v tej seriji

  1. BPF za najmlajše, ničelni del: klasični BPF

Povezave

  1. Referenčni vodnik za BPF in XDP — dokumentacija o BPF od cilium, ali natančneje od Daniela Borkmana, enega od ustvarjalcev in vzdrževalcev BPF. To je eden prvih resnih opisov, ki se od drugih razlikuje po tem, da Daniel točno ve, o čem piše, in tam ni nobenih napak. Zlasti ta dokument opisuje, kako delati s programi BPF tipa XDP in TC z uporabo znanega pripomočka ip iz paketa iproute2.

  2. Documentation/networking/filter.txt — originalna datoteka z dokumentacijo za klasični in nato razširjeni BPF. Dobro branje, če se želite poglobiti v zbirni jezik in tehnične arhitekturne podrobnosti.

  3. Blog o BPF iz Facebooka. Redko se posodablja, a primerno, kot tam pišeta Alexei Starovoitov (avtor eBPF) in Andrii Nakryiko - (vzdrževalec). libbpf).

  4. Skrivnosti bpftoola. Zabavna nit Twitterja Quentina Monneta s primeri in skrivnostmi uporabe bpftool.

  5. Potopite se v BPF: seznam bralnega gradiva. Ogromen (in še vedno vzdrževan) seznam povezav do dokumentacije BPF Quentina Monneta.

Vir: www.habr.com

Dodaj komentar