BPF za najmlađe, prvi dio: prošireni BPF

U početku je postojala tehnologija i zvala se BPF. Pogledali smo je prethodni, starozavjetni članak ove serije. 2013. godine, trudom Alekseja Starovoitova i Daniela Borkmana, razvijena je njegova poboljšana verzija, optimizovana za moderne 64-bitne mašine, i uključena u jezgro Linuxa. Ova nova tehnologija je nakratko nazvana Interni BPF, zatim preimenovana u Extended BPF, a sada, nakon nekoliko godina, svi je jednostavno zovu BPF.

Grubo govoreći, BPF vam omogućava da pokrenete proizvoljni korisnički kod u Linux kernel prostoru, a nova arhitektura se pokazala toliko uspješnom da će nam trebati još desetak članaka da opišemo sve njegove aplikacije. (Jedina stvar koju programeri nisu dobro uradili, kao što možete vidjeti u kodu performansi ispod, je kreiranje pristojnog logotipa.)

Ovaj članak opisuje strukturu BPF virtuelne mašine, kernel interfejse za rad sa BPF-om, razvojne alate, kao i kratak, vrlo kratak pregled postojećih mogućnosti, tj. sve što će nam trebati u budućnosti za dublje proučavanje praktične primjene BPF-a.
BPF za najmlađe, prvi dio: prošireni BPF

Sažetak članka

Uvod u BPF arhitekturu. Prvo ćemo pogledati BPF arhitekturu iz ptičje perspektive i ocrtati glavne komponente.

Registri i komandni sistem BPF virtuelne mašine. Već imamo ideju o arhitekturi u cjelini, opisati ćemo strukturu BPF virtuelne mašine.

Životni ciklus BPF objekata, bpffs sistem datoteka. U ovom dijelu ćemo detaljnije pogledati životni ciklus BPF objekata - programa i mapa.

Upravljanje objektima korištenjem bpf sistemskog poziva. Uz određeno razumijevanje sistema koji je već uspostavljen, konačno ćemo pogledati kako kreirati i manipulirati objektima iz korisničkog prostora koristeći poseban sistemski poziv − bpf(2).

Пишем программы BPF с помощью libbpf. Naravno, možete pisati programe koristeći sistemski poziv. Ali teško je. Za realističniji scenario, nuklearni programeri su razvili biblioteku libbpf. Napravit ćemo osnovni kostur BPF aplikacije koji ćemo koristiti u sljedećim primjerima.

Kernel Helpers. Ovdje ćemo naučiti kako BPF programi mogu pristupiti pomoćnim funkcijama kernela - alatu koji, zajedno sa mapama, fundamentalno proširuje mogućnosti novog BPF-a u odnosu na klasični.

Pristup kartama iz BPF programa. Do ovog trenutka ćemo znati dovoljno da shvatimo kako tačno možemo kreirati programe koji koriste mape. A hajde da čak i na brzinu zavirimo u veliki i moćni verifikator.

Razvojni alati. Odjeljak pomoći o tome kako sastaviti potrebne uslužne programe i kernel za eksperimente.

Zaključak. Na kraju članka, oni koji su čitali do sada naći će motivirajuće riječi i kratak opis onoga što će se dogoditi u sljedećim člancima. Navešćemo i niz linkova za samostalno učenje za one koji nemaju želju ili mogućnost da čekaju nastavak.

Uvod u BPF arhitekturu

Pre nego što počnemo da razmatramo BPF arhitekturu, osvrnućemo se poslednji put (oh). klasični BPF, koji je razvijen kao odgovor na pojavu RISC mašina i rešio je problem efikasnog filtriranja paketa. Arhitektura se pokazala toliko uspješnom da je, nakon što je rođena u poletnim devedesetim u Berkeley UNIX-u, portovana na većinu postojećih operativnih sistema, preživjela lude dvadesete i još uvijek pronalazi nove aplikacije.

Novi BPF je razvijen kao odgovor na sveprisutnost 64-bitnih mašina, usluga u oblaku i povećanu potrebu za alatima za kreiranje SDN (Sroba-defined networking). Razvijen od strane kernel mrežnih inženjera kao poboljšanu zamenu za klasični BPF, novi BPF bukvalno šest meseci kasnije našao je aplikacije u teškom zadatku praćenja Linux sistema, a sada, šest godina nakon njegovog pojavljivanja, trebaće nam čitav sledeći članak samo da bismo navesti različite vrste programa.

Smiješne slike

U svojoj srži, BPF je sandbox virtuelna mašina koja vam omogućava da pokrenete "proizvoljni" kod u prostoru kernela bez ugrožavanja sigurnosti. BPF programi se kreiraju u korisničkom prostoru, učitavaju u kernel i povezuju se na neki izvor događaja. Događaj može biti, na primjer, isporuka paketa mrežnom sučelju, pokretanje neke funkcije kernela, itd. U slučaju paketa, BPF program će imati pristup podacima i metapodacima paketa (za čitanje i, eventualno, pisanje, u zavisnosti od tipa programa); u slučaju pokretanja funkcije kernela, argumenti funkcija, uključujući pokazivače na memoriju kernela, itd.

Pogledajmo detaljnije ovaj proces. Za početak, hajde da razgovaramo o prvoj razlici od klasičnog BPF-a, programi za koji su napisani na asembleru. U novoj verziji arhitektura je proširena tako da se programi mogu pisati na jezicima visokog nivoa, prvenstveno, naravno, na C. Za to je razvijen backend za llvm koji vam omogućava da generišete bajt kod za BPF arhitekturu.

BPF za najmlađe, prvi dio: prošireni BPF

BPF arhitektura je delimično dizajnirana da efikasno radi na modernim mašinama. Da bi ovo funkcioniralo u praksi, BPF bajt kod, jednom učitan u kernel, se prevodi u izvorni kod pomoću komponente koja se zove JIT kompajler (Just In Time). Dalje, ako se sjećate, u klasičnom BPF-u program je učitan u kernel i atomski pridružen izvoru događaja - u kontekstu jednog sistemskog poziva. U novoj arhitekturi to se događa u dvije faze - prvo, kod se učitava u kernel pomoću sistemskog poziva bpf(2)a zatim, kasnije, kroz druge mehanizme koji se razlikuju u zavisnosti od tipa programa, program se povezuje sa izvorom događaja.

Ovdje čitalac može imati pitanje: da li je to bilo moguće? Kako se garantuje sigurnost izvršavanja takvog koda? Sigurnost izvršenja nam garantuje faza učitavanja BPF programa koja se zove verifier (na engleskom se ova faza zove verifier i nastavit ću koristiti englesku riječ):

BPF za najmlađe, prvi dio: prošireni BPF

Verifier je statički analizator koji osigurava da program ne poremeti normalan rad kernela. To, inače, ne znači da program ne može ometati rad sistema - BPF programi, ovisno o vrsti, mogu čitati i prepisivati ​​dijelove memorije kernela, vraćati vrijednosti funkcija, urezivati, dodavati, prepisivati pa čak i prosljeđivanje mrežnih paketa. Verifier garantuje da pokretanje BPF programa neće srušiti kernel i da program koji, prema pravilima, ima pristup pisanju, na primjer, podacima odlaznog paketa, neće moći prepisati memoriju kernela izvan paketa. Verifikator ćemo pogledati malo detaljnije u odgovarajućem odeljku, nakon što se upoznamo sa svim ostalim komponentama BPF-a.

Dakle, šta smo do sada naučili? Korisnik piše program u C, učitava ga u kernel koristeći sistemski poziv bpf(2), gdje ga provjerava verifikator i prevodi u izvorni bajt kod. Tada isti ili drugi korisnik povezuje program sa izvorom događaja i on počinje da se izvršava. Razdvajanje pokretanja i povezivanja potrebno je iz nekoliko razloga. Prvo, pokretanje verifikatora je relativno skupo i preuzimanjem istog programa nekoliko puta gubimo vrijeme na računalu. Drugo, tačno kako je program povezan zavisi od njegovog tipa, a jedan „univerzalni“ interfejs razvijen pre godinu dana možda neće biti pogodan za nove tipove programa. (Iako sada kada arhitektura postaje zrelija, postoji ideja da se ovaj interfejs objedini na nivou libbpf.)

Pažljivi čitalac može primijetiti da još nismo završili sa slikama. Zaista, sve navedeno ne objašnjava zašto BPF suštinski mijenja sliku u odnosu na klasični BPF. Dvije inovacije koje značajno proširuju opseg primjenjivosti su mogućnost korištenja dijeljene memorije i pomoćnih funkcija kernela. U BPF-u, zajednička memorija je implementirana korištenjem takozvanih mapa - zajedničkih struktura podataka sa određenim API-jem. Vjerovatno su dobili ovo ime jer je prva vrsta mape koja se pojavila bila hash tablica. Zatim su se pojavili nizovi, lokalne (po CPU) hash tablice i lokalni nizovi, stabla pretraživanja, mape koje sadrže pokazivače na BPF programe i još mnogo toga. Ono što nam je sada interesantno je da BPF programi sada imaju mogućnost da perzistiraju stanje između poziva i dijele ga s drugim programima i sa korisničkim prostorom.

Mapama se pristupa iz korisničkih procesa koristeći sistemski poziv bpf(2), i iz BPF programa koji rade u kernelu koristeći pomoćne funkcije. Štaviše, pomoćnici postoje ne samo za rad sa mapama, već i za pristup drugim mogućnostima kernela. Na primjer, BPF programi mogu koristiti pomoćne funkcije za prosljeđivanje paketa na druga sučelja, generiranje perf događaja, pristup strukturama kernela i tako dalje.

BPF za najmlađe, prvi dio: prošireni BPF

Ukratko, BPF pruža mogućnost učitavanja proizvoljnog, tj. verifikatorskog testiranog korisničkog koda u prostor kernela. Ovaj kod može sačuvati stanje između poziva i razmjenjivati ​​podatke s korisničkim prostorom, a također ima pristup podsistemima kernela koje dozvoljava ova vrsta programa.

Ovo je već slično mogućnostima koje pružaju moduli kernela, u poređenju sa kojima BPF ima neke prednosti (naravno, možete upoređivati ​​samo slične aplikacije, na primjer, praćenje sistema - ne možete napisati proizvoljan drajver sa BPF-om). Možete primijetiti niži ulazni prag (neki uslužni programi koji koriste BPF ne zahtijevaju od korisnika da ima vještine programiranja kernela, ili općenito vještine programiranja), sigurnost u toku rada (podignite ruku u komentarima za one koji nisu razbili sistem prilikom pisanja ili testiranje modula), atomičnost - postoji zastoj prilikom ponovnog učitavanja modula, a BPF podsistem osigurava da se nijedan događaj ne propusti (da budemo pošteni, ovo nije tačno za sve tipove BPF programa).

Prisutnost takvih mogućnosti čini BPF univerzalnim alatom za proširenje kernela, što se potvrđuje u praksi: sve više i više novih tipova programa se dodaje u BPF, sve više velikih kompanija koristi BPF na borbenim serverima 24×7, sve više i više startupi grade svoje poslovanje na rješenjima na osnovu kojih su zasnovani na BPF-u. BPF se koristi svuda: u zaštiti od DDoS napada, kreiranju SDN-a (na primjer, implementaciji mreža za kubernetes), kao glavni alat za praćenje sistema i sakupljač statistike, u sistemima za otkrivanje upada i sandbox sistemima, itd.

Hajde da završimo pregledni deo članka ovde i pogledamo virtuelnu mašinu i BPF ekosistem detaljnije.

Digresija: komunalne usluge

Da biste mogli pokrenuti primjere u sljedećim odjeljcima, možda će vam trebati određeni broj uslužnih programa, barem llvm/clang uz bpf podršku i bpftool. U odjeljku Razvojni alati Možete pročitati uputstva za sastavljanje uslužnih programa, kao i vaš kernel. Ovaj odjeljak je smješten ispod kako ne bi narušio harmoniju naše prezentacije.

BPF registri virtuelnih mašina i sistem instrukcija

Arhitektura i komandni sistem BPF-a razvijeni su uzimajući u obzir činjenicu da će programi biti napisani u jeziku C i, nakon učitavanja u kernel, prevedeni u izvorni kod. Stoga su broj registara i skup komandi odabrani s obzirom na ukrštanje, u matematičkom smislu, mogućnosti modernih mašina. Osim toga, programima su nametnuta razna ograničenja, na primjer, donedavno nije bilo moguće pisati petlje i potprograme, a broj instrukcija je bio ograničen na 4096 (sada privilegirani programi mogu učitati do milion instrukcija).

BPF ima jedanaest korisničkih 64-bitnih registara r0-r10 i programski brojač. Registrirajte se r10 sadrži pokazivač okvira i samo je za čitanje. Programi imaju pristup 512-bajtnom stogu tokom rada i neograničenoj količini dijeljene memorije u obliku mapa.

BPF programima je dozvoljeno da pokreću određeni skup pomoćnika kernela programskog tipa i, u novije vrijeme, regularne funkcije. Svaka pozvana funkcija može uzeti do pet argumenata, proslijeđenih u registrima r1-r5, a povratna vrijednost se prosljeđuje r0. Garantovano je da će se nakon povratka iz funkcije sadržaj registara r6-r9 Neće se promijeniti.

Za efikasno prevođenje programa, registri r0-r11 za sve podržane arhitekture su jedinstveno mapirane u stvarne registre, uzimajući u obzir karakteristike ABI trenutne arhitekture. Na primjer, za x86_64 registri r1-r5, koji se koriste za prosljeđivanje parametara funkcije, prikazuju se na rdi, rsi, rdx, rcx, r8, koji se koriste za prosljeđivanje parametara funkcijama x86_64. Na primjer, kod s lijeve strane se prevodi u kod s desne strane ovako:

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

Registrujte se r0 koristi se i za vraćanje rezultata izvršavanja programa iu registru r1 programu se prosljeđuje pokazivač na kontekst - ovisno o vrsti programa, to može biti, na primjer, struktura struct xdp_md (za XDP) ili strukturu struct __sk_buff (za različite mrežne programe) ili strukturu struct pt_regs (za različite vrste programa za praćenje) itd.

Dakle, imali smo skup registara, pomoćnike kernela, stek, pokazivač konteksta i zajedničku memoriju u obliku mapa. Nije da je sve ovo apsolutno neophodno na putovanju, ali...

Nastavimo opis i pričamo o sistemu komandi za rad sa ovim objektima. sve (Gotovo sve) BPF instrukcije imaju fiksnu 64-bitnu veličinu. Ako pogledate jednu instrukciju na 64-bitnoj Big Endian mašini, videćete

BPF za najmlađe, prvi dio: prošireni BPF

to je Code - ovo je kodiranje instrukcije, Dst/Src su kodiranja prijemnika i izvora, respektivno, Off - 16-bitno uvlačenje sa predznakom, i Imm je 32-bitni predpisani cijeli broj koji se koristi u nekim instrukcijama (slično cBPF konstanti K). Kodiranje Code ima jednu od dvije vrste:

BPF za najmlađe, prvi dio: prošireni BPF

Klase instrukcija 0, 1, 2, 3 definiraju komande za rad s memorijom. Oni su pozvani, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, odnosno. Odeljenja 4, 7 (BPF_ALU, BPF_ALU64) čine skup ALU instrukcija. Odeljenja 5, 6 (BPF_JMP, BPF_JMP32) sadrže upute za skok.

Dalji plan za proučavanje BPF sistema instrukcija je sljedeći: umjesto pedantno nabrajanja svih instrukcija i njihovih parametara, u ovom dijelu ćemo pogledati nekoliko primjera i iz njih će postati jasno kako instrukcije zapravo funkcioniraju i kako se ručno rastavite bilo koju binarnu datoteku za BPF. Da bismo konsolidirali materijal kasnije u članku, susrećemo se i sa pojedinačnim uputstvima u odjeljcima o Verifieru, JIT kompajleru, prijevodu klasičnog BPF-a, kao i prilikom proučavanja mapa, pozivanja funkcija itd.

Kada govorimo o pojedinačnim uputstvima, osvrnut ćemo se na osnovne datoteke bpf.h и bpf_common.h, koji definiraju numeričke kodove BPF instrukcija. Kada samostalno proučavate arhitekturu i/ili analizirate binarne datoteke, semantiku možete pronaći u sljedećim izvorima, sortiranim po složenosti: Nezvanična eBPF specifikacija, BPF i XDP Referentni vodič, set instrukcija, Documentation/networking/filter.txt i, naravno, u Linux izvornom kodu - verifikator, JIT, BPF interpreter.

Primjer: rastavljanje BPF-a u vašoj glavi

Pogledajmo primjer u kojem kompajliramo program readelf-example.c i pogledajte rezultujuću binarnost. Otkrit ćemo originalni sadržaj readelf-example.c ispod, nakon što vratimo njegovu logiku iz binarnih kodova:

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

Prva kolona u izlazu readelf je uvlačenje i naš program se stoga sastoji od četiri naredbe:

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

Kodovi komandi su jednaki b7, 15, b7 и 95. Podsjetimo da su najmanje značajna tri bita klasa instrukcija. U našem slučaju, četvrti bit svih instrukcija je prazan, tako da su klase instrukcija 7, 5, 7, 5. Klasa 7 je BPF_ALU64, a 5 je BPF_JMP. Za obje klase format instrukcija je isti (vidi gore) i možemo prepisati naš program ovako (istovremeno ćemo prepisati preostale kolone u ljudskom obliku):

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. On dodjeljuje vrijednost odredišnom registru. Ako je bit postavljen s (izvor), tada se vrijednost uzima iz izvornog registra, a ako, kao u našem slučaju, nije postavljena, onda se vrijednost uzima iz polja Imm. Dakle, u prvom i trećem uputstvu izvodimo operaciju r0 = Imm. Nadalje, operacija JMP klase 1 je BPF_JEQ (skok ako je jednak). U našem slučaju, od bit S je nula, uspoređuje vrijednost izvornog registra sa poljem Imm. Ako se vrijednosti poklapaju, tada dolazi do prijelaza na PC + Offgde PC, kao i obično, sadrži adresu sljedeće instrukcije. Konačno, operacija JMP klase 9 je BPF_EXIT. Ova instrukcija prekida program, vraćajući se na kernel r0. Dodajmo novu kolonu našoj tabeli:

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

Ovo možemo prepisati u prikladnijoj formi:

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

Ako se sjetimo šta je u registru r1 programu se prosljeđuje pokazivač na kontekst iz kernela i u registar r0 vrijednost se vraća kernelu, tada možemo vidjeti da ako je pokazivač na kontekst nula, onda vraćamo 1, a inače - 2. Provjerimo da smo u pravu gledajući izvor:

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

Da, to je besmislen program, ali se prevodi u samo četiri jednostavne upute.

Primjer izuzetaka: 16-bajtna instrukcija

Ranije smo spomenuli da neke instrukcije zauzimaju više od 64 bita. Ovo se, na primjer, odnosi na upute lddw (Šifra = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — učitajte dvostruku riječ iz polja u registar Imm. Činjenica je to Imm ima veličinu 32, a dvostruka riječ je 64 bita, tako da učitavanje 64-bitne trenutne vrijednosti u registar u jednoj 64-bitnoj instrukciji neće raditi. Da biste to učinili, dvije susjedne instrukcije se koriste za pohranjivanje drugog dijela 64-bitne vrijednosti u polje Imm... Primjer:

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

Postoje samo dvije instrukcije u binarnom programu:

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

Sastat ćemo se ponovo sa uputama lddw, kada govorimo o selidbama i radu sa mapama.

Primjer: rastavljanje BPF-a pomoću standardnih alata

Dakle, naučili smo čitati BPF binarne kodove i spremni smo da raščlanimo bilo koju instrukciju ako je potrebno. Međutim, vrijedno je reći da je u praksi praktičnije i brže rastaviti programe pomoću standardnih alata, na primjer:

$ 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

Životni ciklus BPF objekata, bpffs sistem datoteka

(Prvo sam saznao neke detalje opisane u ovom pododjeljku od posta Aleksej Starovoitov u BPF Blog.)

BPF objekti - programi i mape - kreiraju se iz korisničkog prostora pomoću komandi BPF_PROG_LOAD и BPF_MAP_CREATE sistemski poziv bpf(2), govorićemo o tome kako se tačno to dešava u sledećem odeljku. Ovo stvara strukture podataka kernela i za svaku od njih refcount (broj referenci) je postavljen na jedan, a deskriptor datoteke koji ukazuje na objekt se vraća korisniku. Nakon što je ručka zatvorena refcount objekat se smanjuje za jedan, a kada dostigne nulu, objekat se uništava.

Ako program koristi mape, onda refcount ove karte se povećavaju za jedan nakon učitavanja programa, tj. njihovi deskriptori datoteka mogu biti zatvoreni iz korisničkog procesa i dalje refcount neće postati nula:

BPF za najmlađe, prvi dio: prošireni BPF

Nakon uspješnog učitavanja programa, obično ga priključimo na neku vrstu generatora događaja. Na primjer, možemo ga staviti na mrežni interfejs za obradu dolaznih paketa ili ga povezati s nekim tracepoint u jezgru. U ovom trenutku, referentni brojač će se takođe povećati za jedan i moći ćemo da zatvorimo deskriptor datoteke u programu za učitavanje.

Šta će se dogoditi ako sada isključimo bootloader? Zavisi od tipa generatora događaja (hook). Sve mrežne zakačice će postojati nakon što učitavač završi, to su takozvane globalne kuke. I, na primjer, programi praćenja bit će pušteni nakon što proces koji ih je kreirao završi (i stoga se nazivaju lokalnim, od „lokalnog do procesa“). Tehnički, lokalne zakačice uvijek imaju odgovarajući deskriptor datoteke u korisničkom prostoru i stoga se zatvaraju kada se proces zatvori, ali globalne zakačice nemaju. Na sledećoj slici, koristeći crvene krstove, pokušavam da pokažem kako završetak programa učitavanja utiče na životni vek objekata u slučaju lokalnih i globalnih zakačivaca.

BPF za najmlađe, prvi dio: prošireni BPF

Zašto postoji razlika između lokalnih i globalnih udica? Pokretanje nekih vrsta mrežnih programa ima smisla bez korisničkog prostora, na primjer, zamislite DDoS zaštitu - bootloader piše pravila i povezuje BPF program sa mrežnim sučeljem, nakon čega pokretač može otići i ubiti se. S druge strane, zamislite program za praćenje grešaka koji ste napisali na koljenima za deset minuta - kada se završi, željeli biste da u sistemu ne ostane smeće, a lokalne kuke će to osigurati.

S druge strane, zamislite da se želite povezati na točku praćenja u kernelu i prikupiti statistiku tokom mnogo godina. U ovom slučaju, željeli biste završiti korisnički dio i s vremena na vrijeme se vraćati na statistiku. Bpf sistem datoteka pruža ovu mogućnost. To je sistem pseudo-datoteka samo u memoriji koji omogućava kreiranje datoteka koje upućuju na BPF objekte i na taj način povećavaju refcount objekata. Nakon toga, loader može izaći, a objekti koje je kreirao ostat će živi.

BPF za najmlađe, prvi dio: prošireni BPF

Kreiranje datoteka u bpffs-ima koje upućuju na BPF objekte naziva se "pinning" (kao u sljedećoj frazi: "proces može zakačiti BPF program ili mapu"). Kreiranje fajl objekata za BPF objekte ima smisla ne samo za produžavanje života lokalnih objekata, već i za upotrebljivost globalnih objekata - vraćajući se na primjer s globalnim DDoS zaštitnim programom, želimo da možemo doći i pogledati statistiku s vremena na vrijeme.

BPF sistem datoteka se obično montira /sys/fs/bpf, ali se može montirati i lokalno, na primjer, ovako:

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

Imena sistema datoteka kreiraju se pomoću naredbe BPF_OBJ_PIN BPF sistemski poziv. Za ilustraciju, uzmimo program, kompajliramo ga, otpremimo i zakačimo bpffs. Naš program ne radi ništa korisno, mi samo predstavljamo kod kako biste mogli reproducirati primjer:

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

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

Hajde da kompajliramo ovaj program i napravimo lokalnu kopiju sistema datoteka bpffs:

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

Sada preuzmimo naš program pomoću uslužnog programa bpftool i pogledajte prateće sistemske pozive bpf(2) (neke irelevantne linije su uklonjene iz strace izlaza):

$ 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

Ovdje smo učitali program koristeći BPF_PROG_LOAD, primio je deskriptor datoteke iz kernela 3 i koristeći komandu BPF_OBJ_PIN zakačio ovaj deskriptor datoteke kao datoteku "bpf-mountpoint/test". Nakon toga program za pokretanje bpftool završio sa radom, ali naš program je ostao u kernelu, iako ga nismo priključili ni na jedan mrežni interfejs:

$ 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

Objekat datoteke možemo obrisati normalno unlink(2) a nakon toga će odgovarajući program biti obrisan:

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

Brisanje objekata

Govoreći o brisanju objekata, potrebno je pojasniti da nakon što smo isključili program sa kuke (generatora događaja), niti jedan novi događaj neće pokrenuti njegovo pokretanje, međutim, sve trenutne instance programa će se završiti normalnim redoslijedom .

Neke vrste BPF programa omogućavaju vam da zamenite program u hodu, tj. obezbjeđuju atomičnost sekvence replace = detach old program, attach new program. U ovom slučaju, sve aktivne instance stare verzije programa će završiti svoj rad, a novi rukovaoci događaja će biti kreirani iz novog programa, a „atomičnost“ ovde znači da nijedan događaj neće biti propušten.

Priključivanje programa izvorima događaja

U ovom članku nećemo posebno opisivati ​​povezivanje programa sa izvorima događaja, jer ima smisla to proučavati u kontekstu određene vrste programa. Cm. primer ispod, u kojem pokazujemo kako su povezani programi poput XDP-a.

Manipulisanje objektima pomoću bpf sistemskog poziva

BPF programi

Svi BPF objekti se kreiraju i upravljaju iz korisničkog prostora korištenjem sistemskog poziva bpf, koji ima sljedeći prototip:

#include <linux/bpf.h>

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

Evo tima cmd je jedna od vrijednosti tipa enum bpf_cmd, attr — pokazivač na parametre za određeni program i size — veličina objekta prema pokazivaču, tj. obično ovo sizeof(*attr). U kernelu 5.8 sistemski poziv bpf podržava 34 različite komande, i određivanje union bpf_attr zauzima 200 redova. Ali ovo nas ne treba plašiti, jer ćemo se upoznati sa komandama i parametrima tokom nekoliko članaka.

Počnimo sa timom BPF_PROG_LOAD, koji kreira BPF programe - uzima skup BPF instrukcija i učitava ga u kernel. U trenutku učitavanja pokreće se verifikator, a zatim se korisniku vraća JIT kompajler i nakon uspješnog izvršenja deskriptor programske datoteke. Videli smo šta mu se dalje dešava u prethodnom delu o životnom ciklusu BPF objekata.

Sada ćemo napisati prilagođeni program koji će učitati jednostavan BPF program, ali prvo moramo odlučiti koju vrstu programa želimo učitati - morat ćemo odabrati Tip i u okviru ovog tipa napišite program koji će proći test verifikatora. Međutim, da ne bismo komplicirali proces, evo gotovog rješenja: uzet ćemo program poput BPF_PROG_TYPE_XDP, koji će vratiti vrijednost XDP_PASS (preskočiti sve pakete). U BPF asembleru to izgleda vrlo jednostavno:

r0 = 2
exit

Nakon što smo se odlučili za da mi ćemo otpremiti, možemo vam reći kako ćemo to učiniti:

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

Zanimljivi događaji u programu počinju definicijom niza insns - naš BPF program u mašinskom kodu. U ovom slučaju, svaka instrukcija BPF programa je upakovana u strukturu bpf_insn. Prvi element insns u skladu sa uputstvima r0 = 2, drugi - exit.

Povlačenje. Kernel definiše pogodnije makroe za pisanje mašinskih kodova i korišćenje zaglavlja kernela tools/include/linux/filter.h mogli bismo pisati

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

Ali pošto je pisanje BPF programa u izvornom kodu neophodno samo za pisanje testova u kernelu i članaka o BPF-u, odsustvo ovih makroa ne komplikuje život programera.

Nakon definiranja BPF programa, prelazimo na njegovo učitavanje u kernel. Naš minimalistički skup parametara attr uključuje tip programa, set i broj instrukcija, potrebnu licencu i naziv "woo", koji koristimo da pronađemo naš program na sistemu nakon preuzimanja. Program se, kao što je obećano, učitava u sistem pomoću sistemskog poziva bpf.

Na kraju programa završavamo u beskonačnoj petlji koja simulira korisno opterećenje. Bez toga, program će biti ubijen od strane kernela kada se zatvori deskriptor datoteke koji nam je vratio sistemski poziv bpf, i nećemo ga vidjeti u sistemu.

Pa, spremni smo za testiranje. Hajde da sastavimo i pokrenemo program ispod straceda provjerite da li sve radi kako treba:

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

Sve je uredu, bpf(2) vratio nam je ručku 3 i ušli smo u beskonačnu petlju sa pause(). Pokušajmo pronaći naš program u sistemu. Da bismo to učinili, otići ćemo na drugi terminal i koristiti uslužni program 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 na sistemu učitan program woo čiji je globalni ID 390 i trenutno je u toku simple-prog postoji otvoreni deskriptor datoteke koji ukazuje na program (i if simple-prog onda će završiti posao woo će nestati). Očekivano, program woo uzima 16 bajtova - dvije instrukcije - binarnih kodova u BPF arhitekturi, ali u svom izvornom obliku (x86_64) to je već 40 bajtova. Pogledajmo naš program u originalnom obliku:

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

nema iznenađenja. Sada pogledajmo kod generiran od strane JIT kompajlera:

# 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

nije veoma efikasan za exit(2), ali pošteno rečeno, naš program je previše jednostavan, a za netrivijalne programe su, naravno, potrebni prolog i epilog koje dodaje JIT kompajler.

Mape

BPF programi mogu koristiti strukturirana memorijska područja koja su dostupna i drugim BPF programima i programima u korisničkom prostoru. Ovi objekti se nazivaju mape i u ovom odeljku ćemo pokazati kako se njima manipuliše korišćenjem sistemskog poziva bpf.

Recimo odmah da mogućnosti mapa nisu ograničene samo na pristup zajedničkoj memoriji. Postoje mape posebne namjene koje sadrže, na primjer, pokazivače na BPF programe ili pokazivače na mrežna sučelja, mape za rad sa perf događajima, itd. Ovdje nećemo govoriti o njima, kako ne bismo zbunili čitaoca. Osim toga, zanemarimo probleme sinhronizacije, jer to nije važno za naše primjere. Kompletnu listu dostupnih tipova mapa možete pronaći u <linux/bpf.h>, a u ovom odeljku ćemo uzeti kao primer istorijski prvi tip, hash tabelu BPF_MAP_TYPE_HASH.

Ako kreirate hash tabelu u, recimo, C++, rekli biste unordered_map<int,long> woo, što na ruskom znači „Treba mi sto woo neograničene veličine, čiji su ključevi tipa int, a vrijednosti su tip long" Da bismo kreirali BPF hash tabelu, moramo da uradimo skoro istu stvar, osim što moramo da navedemo maksimalnu veličinu tabele i umesto da specificiramo tipove ključeva i vrednosti, moramo da navedemo njihove veličine u bajtovima . Za kreiranje mapa koristite naredbu BPF_MAP_CREATE sistemski poziv bpf. Pogledajmo manje-više minimalan program koji kreira mapu. Nakon prethodnog programa koji učitava BPF programe, ovaj bi vam trebao izgledati jednostavno:

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

Ovdje definiramo skup parametara attr, u kojem kažemo „Treba mi hash tablica s ključevima i vrijednostima veličine sizeof(int), u koji mogu staviti najviše četiri elementa." Prilikom kreiranja BPF mapa, možete odrediti druge parametre, na primjer, na isti način kao u primjeru s programom, naveli smo naziv objekta kao "woo".

Hajde da kompajliramo i pokrenemo 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(

Evo sistemskog poziva bpf(2) vratio nam je broj karte deskriptora 3 a zatim program, kao što se i očekivalo, čeka dalja uputstva u sistemskom pozivu pause(2).

Sada pošaljimo naš program u pozadinu ili otvorimo drugi terminal i pogledamo naš objekt pomoću uslužnog programa bpftool (našu kartu možemo razlikovati od drugih po imenu):

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

Broj 114 je globalni ID našeg objekta. Bilo koji program na sistemu može koristiti ovaj ID da otvori postojeću mapu koristeći naredbu BPF_MAP_GET_FD_BY_ID sistemski poziv bpf.

Sada se možemo igrati sa našom hash stolom. Pogledajmo njegov sadržaj:

$ sudo bpftool map dump id 114
Found 0 elements

Prazan. Stavimo vrijednost u to hash[1] = 1:

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

Pogledajmo ponovo tabelu:

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

Ura! Uspjeli smo dodati jedan element. Imajte na umu da moramo raditi na nivou bajtova da bismo to učinili, jer bptftool ne zna koji su tip vrijednosti u hash tabeli. (Ovo znanje joj se može prenijeti pomoću BTF-a, ali više o tome sada.)

Kako tačno bpftool čita i dodaje elemente? Pogledajmo ispod haube:

$ 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

Prvo smo otvorili mapu po njenom globalnom ID-u koristeći naredbu BPF_MAP_GET_FD_BY_ID и bpf(2) vratio nam je deskriptor 3. Dalje koristeći naredbu BPF_MAP_GET_NEXT_KEY pronašli smo prvi ključ u tabeli prolazom NULL kao pokazivač na "prethodni" ključ. Ako imamo ključ, možemo BPF_MAP_LOOKUP_ELEMkoji vraća vrijednost pokazivaču value. Sljedeći korak je da pokušamo pronaći sljedeći element prosljeđivanjem pokazivača na trenutni ključ, ali naša tablica sadrži samo jedan element i naredbu BPF_MAP_GET_NEXT_KEY vraća ENOENT.

U redu, promijenimo vrijednost ključem 1, recimo da naša poslovna logika zahtijeva registraciju 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

Kao što se i očekivalo, vrlo je jednostavno: komanda BPF_MAP_GET_FD_BY_ID otvara našu mapu po ID-u i naredbi BPF_MAP_UPDATE_ELEM prepisuje element.

Dakle, nakon kreiranja hash tablice iz jednog programa, možemo čitati i pisati njen sadržaj iz drugog. Imajte na umu da ako smo to mogli učiniti iz komandne linije, onda to može učiniti bilo koji drugi program na sistemu. Pored gore opisanih komandi, za rad sa mapama iz korisničkog prostora, slijedeći:

  • BPF_MAP_LOOKUP_ELEM: pronađite vrijednost po ključu
  • BPF_MAP_UPDATE_ELEM: ažuriranje/kreiranje vrijednosti
  • BPF_MAP_DELETE_ELEM: uklonite ključ
  • BPF_MAP_GET_NEXT_KEY: pronađite sljedeći (ili prvi) ključ
  • BPF_MAP_GET_NEXT_ID: omogućava vam da prođete kroz sve postojeće mape, tako funkcionira bpftool map
  • BPF_MAP_GET_FD_BY_ID: otvori postojeću mapu po njenom globalnom ID-u
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomski ažuriranje vrijednosti objekta i vraćanje starog
  • BPF_MAP_FREEZE: učinite mapu nepromjenjivom iz korisničkog prostora (ova operacija se ne može poništiti)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: masovne operacije. Na primjer, BPF_MAP_LOOKUP_AND_DELETE_BATCH - ovo je jedini pouzdan način za čitanje i resetiranje svih vrijednosti sa karte

Ne rade sve ove naredbe za sve tipove mapa, ali općenito rad s drugim tipovima mapa iz korisničkog prostora izgleda potpuno isto kao rad s hash tablicama.

Radi reda, hajde da završimo naše eksperimente s hash tablicama. Sjećate li se da smo kreirali tabelu koja može sadržavati do četiri ključa? Dodajmo još nekoliko elemenata:

$ 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

Zasada je 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

Pokušajmo dodati još jedno:

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

Očekivano, nismo uspjeli. Pogledajmo grešku detaljnije:

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

Sve je u redu: očekivano, ekipa BPF_MAP_UPDATE_ELEM pokušava kreirati novi, peti, ključ, ali pada E2BIG.

Dakle, možemo kreirati i učitavati BPF programe, kao i kreirati i upravljati mapama iz korisničkog prostora. Sada je logično pogledati kako možemo koristiti mape iz samih BPF programa. O tome bismo mogli govoriti jezikom teško čitljivih programa u mašinskim makro kodovima, ali je u stvari došlo vrijeme da pokažemo kako se BPF programi zapravo pišu i održavaju - koristeći libbpf.

(Za čitatelje koji su nezadovoljni nedostatkom primjera niske razine: detaljno ćemo analizirati programe koji koriste mape i pomoćne funkcije kreirane pomoću libbpf i reći vam šta se dešava na nivou instrukcija. Za čitaoce koji su nezadovoljni puno, dodali smo primer na odgovarajućem mestu u članku.)

Pisanje BPF programa koristeći libbpf

Pisanje BPF programa pomoću mašinskih kodova može biti zanimljivo samo prvi put, a onda nastupa sitost. U ovom trenutku morate skrenuti pažnju na llvm, koji ima pozadinu za generisanje koda za BPF arhitekturu, kao i biblioteku libbpf, koji vam omogućava da napišete korisničku stranu BPF aplikacija i učitate kod BPF programa generiran pomoću llvm/clang.

Zapravo, kao što ćemo vidjeti u ovom i narednim člancima, libbpf radi dosta posla bez njega (ili sličnih alata - iproute2, libbcc, libbpf-goitd.) nemoguće je živjeti. Jedna od ubitačnih karakteristika projekta libbpf je BPF CO-RE (Compile Once, Run Everywhere) - projekat koji vam omogućava da pišete BPF programe koji su prenosivi s jednog kernela na drugo, sa mogućnošću pokretanja na različitim API-jima (na primjer, kada se struktura kernela promijeni iz verzije na verziju). Da biste mogli raditi sa CO-RE, vaše kernel mora biti kompajliran sa BTF podrškom (opisujemo kako to učiniti u odjeljku Razvojni alati. Možete provjeriti da li je vaš kernel izgrađen sa BTF-om ili ne vrlo jednostavno - prisustvom sljedećeg fajla:

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

Ova datoteka pohranjuje informacije o svim tipovima podataka koji se koriste u kernelu i koristi se u svim našim primjerima korištenja libbpf. O CO-RE ćemo detaljno govoriti u sljedećem članku, ali u ovom - samo napravite sebi kernel sa CONFIG_DEBUG_INFO_BTF.

biblioteka libbpf živi direktno u imeniku tools/lib/bpf kernel i njegov razvoj se odvija preko mailing liste [email protected]. Međutim, odvojeno spremište se održava za potrebe aplikacija koje žive izvan kernela https://github.com/libbpf/libbpf u kojoj je biblioteka kernela preslikana za pristup čitanju manje-više onakva kakva jeste.

U ovom odeljku ćemo pogledati kako možete kreirati projekat koji koristi libbpf, hajde da napišemo nekoliko (manje-više besmislenih) testnih programa i detaljno analiziramo kako sve to funkcionira. Ovo će nam omogućiti da u sljedećim odjeljcima lakše objasnimo kako tačno BPF programi komuniciraju sa mapama, pomoćnicima kernela, BTF-om itd.

Obično projekti koriste libbpf dodajte GitHub spremište kao git podmodul, mi ćemo učiniti isto:

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

Idem u libbpf veoma jednostavno:

$ 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š sljedeći plan u ovom dijelu je sljedeći: napisat ćemo BPF program poput BPF_PROG_TYPE_XDP, isto kao u prethodnom primjeru, ali u C-u ga kompajliramo koristeći clang, i napišite pomoćni program koji će ga učitati u kernel. U narednim odeljcima ćemo proširiti mogućnosti i BPF programa i programa asistenta.

Primjer: kreiranje punopravne aplikacije koristeći libbpf

Za početak koristimo datoteku /sys/kernel/btf/vmlinux, koji je gore spomenut, i kreirajte njegov ekvivalent u obliku zaglavlja:

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

Ova datoteka će pohraniti sve strukture podataka dostupne u našem kernelu, na primjer, ovako je definirano IPv4 zaglavlje u kernelu:

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

Sada ćemo napisati naš BPF program na 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";

Iako se naš program pokazao vrlo jednostavnim, ipak moramo obratiti pažnju na mnoge detalje. Prvo, prva datoteka zaglavlja koju uključujemo je vmlinux.h, koji smo upravo generirali koristeći bpftool btf dump - sada ne trebamo instalirati paket kernel-headers da bismo saznali kako izgledaju strukture kernela. Sljedeći fajl zaglavlja dolazi nam iz biblioteke libbpf. Sada nam treba samo da definišemo makro SEC, koji šalje znak u odgovarajući odjeljak ELF objektne datoteke. Naš program je sadržan u odjeljku xdp/simple, gdje prije kose crte definiramo tip programa BPF - to je konvencija koja se koristi libbpf, na osnovu naziva sekcije će zamijeniti ispravan tip pri pokretanju bpf(2). Sam program BPF-a jeste C - vrlo jednostavno i sastoji se od jednog reda return XDP_PASS. Konačno, poseban odjeljak "license" sadrži naziv licence.

Možemo kompajlirati naš program koristeći llvm/clang, verziju >= 10.0.0, ili još bolje, veću (pogledajte odjeljak Razvojni alati):

$ 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

Među zanimljivim karakteristikama: ukazujemo na ciljnu arhitekturu -target bpf i put do zaglavlja libbpf, koji smo nedavno instalirali. Takođe, ne zaboravite na -O2, bez ove opcije možda vas očekuju iznenađenja u budućnosti. Pogledajmo naš kod, da li smo uspjeli napisati program koji smo željeli?

$ 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

Da, uspjelo je! Sada imamo binarnu datoteku sa programom i želimo da kreiramo aplikaciju koja će je učitati u kernel. U tu svrhu biblioteka libbpf nudi nam dvije opcije - koristiti API nižeg nivoa ili API višeg nivoa. Ići ćemo drugim putem, jer želimo naučiti kako pisati, učitavati i povezati BPF programe uz minimalan napor za njihovo naknadno proučavanje.

Prvo, trebamo generirati “kostur” našeg programa iz njegovog binarnog programa koristeći isti uslužni program bpftool — švicarski nož BPF svijeta (što se može shvatiti doslovno, budući da je Daniel Borkman, jedan od kreatora i održavatelja BPF-a, Švicarac):

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

U fajlu xdp-simple.skel.h sadrži binarni kod našeg programa i funkcije za upravljanje - učitavanje, pričvršćivanje, brisanje našeg objekta. U našem jednostavnom slučaju ovo izgleda kao pretjerano, ali također funkcionira u slučaju kada objektna datoteka sadrži mnogo BPF programa i mapa i da bismo učitali ovaj divovski ELF samo trebamo generirati kostur i pozvati jednu ili dvije funkcije iz prilagođene aplikacije koju pišu. Idemo dalje.

Strogo govoreći, naš program za učitavanje je trivijalan:

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

to je struct xdp_simple_bpf definisano u datoteci xdp-simple.skel.h i opisuje naš objektni fajl:

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

Ovdje možemo vidjeti tragove niskog nivoa API-ja: struktura struct bpf_program *simple и struct bpf_link *simple. Prva struktura posebno opisuje naš program, napisan u odjeljku xdp/simple, a drugi opisuje kako se program povezuje s izvorom događaja.

funkcija xdp_simple_bpf__open_and_load, otvara ELF objekat, analizira ga, kreira sve strukture i podstrukture (osim programa, ELF sadrži i druge sekcije - podatke, podatke samo za čitanje, informacije za otklanjanje grešaka, licencu, itd.), a zatim ga učitava u kernel koristeći sistem poziv bpf, što možemo provjeriti kompajliranjem i pokretanjem programa:

$ 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

Pogledajmo sada kako naš program koristi bpftool. Pronađimo njenu ličnu kartu:

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

i dump (koristimo skraćeni oblik naredbe bpftool prog dump xlated):

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

Nešto novo! Program je štampao delove našeg izvornog fajla C. To je uradila biblioteka libbpf, koji je pronašao odjeljak za otklanjanje grešaka u binarnom sistemu, preveo ga u BTF objekt, učitao ga u kernel koristeći BPF_BTF_LOAD, a zatim odredio rezultujući deskriptor datoteke prilikom učitavanja programa pomoću naredbe BPG_PROG_LOAD.

Kernel Helpers

BPF programi mogu pokrenuti “spoljne” funkcije - pomoćnike kernela. Ove pomoćne funkcije omogućavaju BPF programima da pristupe strukturama kernela, upravljaju mapama, a također komuniciraju sa “stvarnim svijetom” - kreiraju perf događaje, kontrolišu hardver (na primjer, preusmjeravaju pakete) itd.

Primjer: bpf_get_smp_processor_id

U okviru paradigme „učenja na primjeru“, razmotrimo jednu od pomoćnih funkcija, bpf_get_smp_processor_id(), određeni u fajlu kernel/bpf/helpers.c. Vraća broj procesora na kojem se izvodi BPF program koji ga je pozvao. Ali nas ne zanima toliko njegova semantika koliko činjenica da njegova implementacija zauzima jednu liniju:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Definicije BPF pomoćnih funkcija slične su definicijama Linux sistemskog poziva. Ovdje je, na primjer, definirana funkcija koja nema argumente. (Funkcija koja uzima, recimo, tri argumenta definirana je pomoću makronaredbe BPF_CALL_3. Maksimalan broj argumenata je pet.) Međutim, ovo je samo prvi dio definicije. Drugi dio je definiranje strukture tipa struct bpf_func_proto, koji sadrži opis pomoćne funkcije koju verifikator razumije:

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

Registriranje pomoćnih funkcija

Da bi BPF programi određenog tipa koristili ovu funkciju, moraju je registrovati, na primjer za tip BPF_PROG_TYPE_XDP funkcija je definirana u kernelu xdp_func_proto, koji iz ID-a pomoćne funkcije određuje da li XDP podržava ovu funkciju ili ne. Naša funkcija je podržava:

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

Novi tipovi BPF programa su "definisani" u datoteci include/linux/bpf_types.h koristeći makro BPF_PROG_TYPE. Definisano pod navodnicima jer je to logična definicija, au terminima jezika C definicija čitavog skupa konkretnih struktura se javlja na drugim mjestima. Konkretno, u fajlu kernel/bpf/verifier.c sve definicije iz fajla bpf_types.h koriste se za kreiranje niza struktura 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 jest, za svaki tip BPF programa definiran je pokazivač na strukturu podataka tog tipa struct bpf_verifier_ops, koji je inicijaliziran vrijednošću _name ## _verifier_ops, tj. xdp_verifier_ops do xdp... Struktura xdp_verifier_ops određeno od u fajlu net/core/filter.c kako slijedi:

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

Ovdje vidimo našu poznatu funkciju xdp_func_proto, koji će pokrenuti verifikator svaki put kada naiđe na izazov neke funkcije unutar BPF programa, vidi verifier.c.

Pogledajmo kako hipotetički BPF program koristi funkciju bpf_get_smp_processor_id. Da bismo to učinili, prepisujemo program iz našeg prethodnog odjeljka na sljedeći način:

#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 određeno od в <bpf/bpf_helper_defs.h> biblioteke libbpf kako

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

to je, bpf_get_smp_processor_id je pokazivač funkcije čija je vrijednost 8, gdje je 8 vrijednost BPF_FUNC_get_smp_processor_id tip enum bpf_fun_id, koji je za nas definiran u datoteci vmlinux.h (fajl bpf_helper_defs.h u kernelu se generiše skriptom, tako da su “magični” brojevi ok). Ova funkcija ne uzima argumente i vraća vrijednost tipa __u32. Kada ga pokrenemo u našem programu, clang generiše instrukciju BPF_CALL "prava vrsta" Hajde da kompajliramo program i pogledamo sekciju 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

U prvom redu vidimo upute call, parametar IMM što je jednako 8, i SRC_REG - nula. Prema ABI sporazumu koji koristi verifikator, ovo je poziv pomoćnoj funkciji broj osam. Jednom kada se pokrene, logika je jednostavna. Povratna vrijednost iz registra r0 kopirano na r1 a u redovima 2,3 se pretvara u tip u32 — gornja 32 bita su obrisana. U redovima 4,5,6,7 vraćamo 2 (XDP_PASS) ili 1 (XDP_DROP) ovisno o tome da li je pomoćna funkcija iz reda 0 vratila nultu ili nenultu vrijednost.

Testirajmo se: učitaj program i pogledajmo izlaz 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, verifikator je pronašao ispravan kernel-helper.

Primjer: prosljeđivanje argumenata i konačno pokretanje programa!

Sve pomoćne funkcije na razini pokretanja imaju prototip

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

Parametri pomoćnim funkcijama se prosljeđuju u registrima r1-r5, a vrijednost se vraća u registar r0. Ne postoje funkcije koje uzimaju više od pet argumenata, a podrška za njih se ne očekuje u budućnosti.

Pogledajmo novi pomoćnik kernela i kako BPF prosljeđuje parametre. Hajde da prepišemo xdp-simple.bpf.c kako slijedi (ostatak redova nije promijenjen):

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

Naš program ispisuje broj CPU-a na kojem radi. Hajde da ga kompajliramo i pogledamo kod:

$ 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

U redove 0-7 upisujemo string running on CPU%un, a zatim na liniji 8 pokrećemo poznati bpf_get_smp_processor_id. U redovima 9-12 pripremamo pomoćne argumente bpf_printk - registri r1, r2, r3. Zašto ih ima tri, a ne dva? Jer bpf_printkovo je omot za makro oko pravog pomagača bpf_trace_printk, koji treba proći veličinu niza formata.

Hajde sada da dodamo nekoliko redova xdp-simple.ctako da se naš program povezuje sa interfejsom lo i stvarno poč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);
}

Ovdje koristimo funkciju bpf_set_link_xdp_fd, koji povezuje BPF programe tipa XDP sa mrežnim interfejsima. Tvrdo smo kodirali broj interfejsa lo, što je uvijek 1. Pokrećemo funkciju dvaput da bismo prvo odvojili stari program ako je bio priključen. Primijetite da nam sada ne treba izazov pause ili beskonačna petlja: naš program za učitavanje će izaći, ali BPF program neće biti ubijen pošto je povezan sa izvorom događaja. Nakon uspješnog preuzimanja i povezivanja, program će biti pokrenut za svaki mrežni paket koji stigne lo.

Hajde da preuzmemo program i pogledamo interfejs 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 koji smo preuzeli ima ID 669 i vidimo isti ID na interfejsu lo. Poslat ćemo par paketa na 127.0.0.1 (zahtjev + odgovor):

$ ping -c1 localhost

a sada pogledajmo sadržaj virtuelne datoteke za otklanjanje grešaka /sys/kernel/debug/tracing/trace_pipe, u kojem bpf_printk piše svoje poruke:

# 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

Uočena su dva paketa lo i obrađeno na CPU0 - naš prvi punopravni besmisleni BPF program je radio!

Vrijedi to napomenuti bpf_printk Nije uzalud što upisuje u datoteku za otklanjanje grešaka: ovo nije najuspješniji pomoćnik za korištenje u proizvodnji, ali naš cilj je bio pokazati nešto jednostavno.

Pristup kartama iz BPF programa

Primjer: korištenje karte iz BPF programa

U prethodnim odjeljcima naučili smo kako kreirati i koristiti mape iz korisničkog prostora, a sada pogledajmo dio kernela. Počnimo, kao i obično, s primjerom. Hajde da prepišemo naš program xdp-simple.bpf.c kako slijedi:

#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 početku programa dodali smo definiciju karte woo: Ovo je niz od 8 elemenata koji pohranjuje vrijednosti poput u64 (u C-u bismo definirali takav niz kao u64 woo[8]). U programu "xdp/simple" dobijamo trenutni broj procesora u varijablu key a zatim koristeći pomoćnu funkciju bpf_map_lookup_element dobijamo pokazivač na odgovarajući unos u nizu, koji povećavamo za jedan. Prevedeno na ruski: izračunavamo statistiku o tome koji je CPU obradio dolazne pakete. Pokušajmo pokrenuti 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

Hajde da proverimo da li je zakačena lo i pošaljite neke pakete:

$ 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

Sada pogledajmo sadržaj niza:

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

Gotovo svi procesi su obrađeni na CPU7. Ovo nam nije važno, glavna stvar je da program radi i da razumijemo kako pristupiti kartama iz BPF programa - koristeći хелперов bpf_mp_*.

Mistični indeks

Dakle, možemo pristupiti mapi iz BPF programa koristeći pozive poput

val = bpf_map_lookup_elem(&woo, &key);

gdje izgleda pomoćna funkcija

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

ali mi prenosimo pokazivač &woo na neimenovanu strukturu struct { ... }...

Ako pogledamo asembler programa, vidimo da je vrijednost &woo nije zapravo definiran (red 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
...

a sadržan je u selidbama:

$ 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

Ali ako pogledamo već učitani program, vidimo pokazivač na ispravnu mapu (red 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]
...

Dakle, možemo zaključiti da je u trenutku pokretanja našeg programa za učitavanje, veza na &woo je zamijenjen nečim sa bibliotekom libbpf. Prvo ćemo pogledati izlaz 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

Vidimo to libbpf kreirao mapu woo a zatim preuzeli naš program simple. Pogledajmo bliže kako učitavamo program:

  • poziv xdp_simple_bpf__open_and_load iz fajla xdp-simple.skel.h
  • koji uzrokuje xdp_simple_bpf__load iz fajla xdp-simple.skel.h
  • koji uzrokuje bpf_object__load_skeleton iz fajla libbpf/src/libbpf.c
  • koji uzrokuje bpf_object__load_xattr из libbpf/src/libbpf.c

Posljednja funkcija će, između ostalog, pozvati bpf_object__create_maps, koji kreira ili otvara postojeće mape, pretvarajući ih u deskriptore datoteka. (Ovdje vidimo BPF_MAP_CREATE u izlazu strace.) Zatim se poziva funkcija bpf_object__relocate i ona je ta koja nas zanima, pošto se sećamo onoga što smo videli woo u tabeli preseljenja. Istražujući ga, na kraju se nalazimo u funkciji bpf_program__relocate, koji bavi se premeštanjem mapa:

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

Dakle, primamo naše upute

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

i zamijenite izvorni registar u njemu sa BPF_PSEUDO_MAP_FD, i prvi IMM u deskriptor datoteke naše mape i, ako je jednak, npr. 0xdeadbeef, tada ćemo kao rezultat dobiti instrukciju

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

Ovako se informacije o karti prenose u određeni učitani BPF program. U ovom slučaju, mapa se može kreirati pomoću BPF_MAP_CREATE, a otvara se pomoću ID-a BPF_MAP_GET_FD_BY_ID.

Ukupno, prilikom upotrebe libbpf algoritam je sljedeći:

  • tokom kompilacije, kreiraju se zapisi u tabeli preseljenja za veze ka kartama
  • libbpf otvara knjigu ELF objekata, pronalazi sve korištene mape i kreira deskriptore datoteka za njih
  • deskriptori datoteka se učitavaju u kernel kao dio instrukcije LD64

Kao što možete zamisliti, ima još toga i morat ćemo pogledati u srž. Srećom, imamo trag - zapisali smo značenje BPF_PSEUDO_MAP_FD u izvorni registar i možemo ga zakopati, što će nas dovesti do svetinje svih svetaca - kernel/bpf/verifier.c, gdje funkcija s razlikovnim imenom zamjenjuje deskriptor datoteke adresom 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;

(pun kod se može naći link). Tako da možemo proširiti naš algoritam:

  • dok učitava program, verifikator provjerava ispravnu upotrebu mape i upisuje adresu odgovarajuće strukture struct bpf_map

Prilikom preuzimanja binarnog ELF-a koristeći libbpf Događa se još mnogo toga, ali o tome ćemo raspravljati u drugim člancima.

Učitavanje programa i mapa bez libbpf-a

Kao što je obećano, evo primjera za čitatelje koji žele znati kako napraviti i učitati program koji koristi mape, bez pomoći libbpf. Ovo može biti korisno kada radite u okruženju za koje ne možete izgraditi ovisnosti, ili čuvate svaki dio, ili pišete program kao što je ply, koji generiše BPF binarni kod u hodu.

Da bismo lakše pratili logiku, prepisat ćemo naš primjer u ove svrhe xdp-simple. Kompletan i malo proširen kod programa o kojem se govori u ovom primjeru može se naći u ovome suština.

Logika naše aplikacije je sljedeća:

  • kreirajte kartu tipa BPF_MAP_TYPE_ARRAY koristeći naredbu BPF_MAP_CREATE,
  • kreirati program koji koristi ovu mapu,
  • povežite program sa interfejsom lo,

što se prevodi u ljudski kao

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

to je map_create kreira mapu na isti način kao što smo to učinili u prvom primjeru o sistemskom pozivu bpf - “kernel, molim te napravi mi novu mapu u obliku niza od 8 elemenata kao __u64 i vrati mi deskriptor fajla":

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 se takođe lako učitava:

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

Zlobni dio prog_load je definicija našeg BPF programa kao niza struktura struct bpf_insn insns[]. Ali pošto koristimo program koji imamo u C-u, možemo malo varati:

$ 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

Ukupno trebamo napisati 14 instrukcija u obliku struktura poput struct bpf_insn (savjet: uzmite smetlište odozgo, ponovo pročitajte odjeljak s uputama, otvorite linux/bpf.h и linux/bpf_common.h i pokušajte da odredite struct bpf_insn insns[] samostalno):

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

Vježba za one koji ovo nisu sami napisali - pronađite map_fd.

U našem programu je ostao još jedan neotkriveni dio - xdp_attach. Nažalost, programi kao što je XDP ne mogu se povezati pomoću sistemskog poziva bpf. Ljudi koji su kreirali BPF i XDP bili su iz online Linux zajednice, što znači da su koristili onu koja im je najpoznatija (ali ne i normalno ljudi) interfejs za interakciju sa kernelom: netlink utičnice, vidi takođe RFC3549. Najjednostavniji način implementacije xdp_attach kopira kod iz libbpf, naime, iz datoteke netlink.c, što smo i uradili, malo skrateći:

Dobrodošli u svijet netlink soketa

Otvorite tip netlink utičnice 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;
}

Čitamo iz ove utičnice:

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

Konačno, evo naše funkcije koja otvara socket i šalje mu posebnu poruku koja sadrži 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;
}

Dakle, sve je spremno 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 +++

Da vidimo da li se naš program povezao sa 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šaljimo pingove i pogledajmo mapu:

$ 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

Ura, sve radi. Uzgred, imajte na umu da se naša mapa ponovo prikazuje u obliku bajtova. To je zbog činjenice da, za razliku od libbpf nismo učitali informacije o tipu (BTF). Ali o tome ćemo više razgovarati sljedeći put.

Razvojni alati

U ovom odeljku ćemo pogledati minimalni alat BPF programera.

Uopšteno govoreći, ne treba vam ništa posebno da biste razvili BPF programe - BPF radi na bilo kojem pristojnom distribucijskom kernelu, a programi su napravljeni pomoću clang, koji se može isporučiti iz paketa. Međutim, zbog činjenice da je BPF u razvoju, kernel i alati se stalno mijenjaju, ako ne želite pisati BPF programe koristeći staromodne metode od 2019. godine, morat ćete kompajlirati

  • llvm/clang
  • pahole
  • njeno jezgro
  • bpftool

(Za referencu, ovaj odjeljak i svi primjeri u članku pokrenuti su na Debianu 10.)

llvm/clang

BPF je prijateljski sa LLVM-om i, iako se nedavno programi za BPF mogu kompajlirati pomoću gcc-a, sav trenutni razvoj se odvija za LLVM. Stoga ćemo prije svega izgraditi trenutnu verziju clang iz git-a:

$ 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
... много времени спустя
$

Sada možemo provjeriti da li je sve bilo kako treba:

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

(Upute za montažu clang preuzeto od mene bpf_devel_QA.)

Nećemo instalirati programe koje smo upravo napravili, već ćemo ih samo dodati u njih PATH, na primjer:

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

(Ovo se može dodati .bashrc ili u posebnu datoteku. Ja lično dodajem ovakve stvari ~/bin/activate-llvm.sh i kada je potrebno to radim . activate-llvm.sh.)

Pahole i BTF

Utility pahole koristi se prilikom izgradnje kernela za kreiranje informacija za otklanjanje grešaka u BTF formatu. U ovom članku nećemo ulaziti u detalje o detaljima BTF tehnologije, osim činjenice da je zgodna i da je želimo koristiti. Dakle, ako ćete izgraditi svoje kernel, prvo napravite pahole (bez pahole nećete moći da napravite kernel sa opcijom 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

Jezgra za eksperimentisanje sa BPF-om

Kada istražujem mogućnosti BPF-a, želim da sastavim svoje jezgro. Ovo, generalno govoreći, nije neophodno, jer ćete moći da kompajlirate i učitavate BPF programe na distributivnom kernelu, međutim, posedovanje sopstvenog kernela vam omogućava da koristite najnovije BPF karakteristike, koje će se pojaviti u vašoj distribuciji u najboljem slučaju za nekoliko meseci , ili, kao u slučaju nekih alata za otklanjanje grešaka, uopšte neće biti upakovani u doglednoj budućnosti. Takođe, njegovo sopstveno jezgro čini da se oseća važnim eksperimentisanje sa kodom.

Da biste napravili kernel potreban vam je, prvo, sam kernel, a drugo, konfiguraciona datoteka kernela. Za eksperimentiranje sa BPF-om možemo koristiti uobičajeno vanilija kernela ili jednog od razvojnih jezgara. Istorijski gledano, razvoj BPF-a se odvija unutar Linux mrežne zajednice i stoga sve promjene prije ili kasnije idu preko Davida Millera, Linux mrežnog održavatelja. Ovisno o njihovoj prirodi - izmjene ili nove funkcije - mrežne promjene spadaju u jedno od dva jezgra - net ili net-next. Promjene za BPF se distribuiraju na isti način između bpf и bpf-next, koji se zatim objedinjuju u net i net-next, respektivno. Za više detalja, pogledajte bpf_devel_QA и netdev-FAQ. Zato odaberite kernel na osnovu vašeg ukusa i potreba za stabilnošću sistema na kojem testirate (*-next kerneli su najnestabilniji od navedenih).

Iznad okvira ovog članka je govoriti o tome kako upravljati konfiguracijskim datotekama kernela - pretpostavlja se da ili već znate kako to učiniti, ili spreman za učenje na svoju ruku. Međutim, sljedeće upute trebale bi biti manje-više dovoljne da vam daju radni sistem koji podržava BPF.

Preuzmite jedan od gore navedenih kernela:

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

Napravite minimalnu radnu konfiguraciju kernela:

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

Omogućite BPF opcije u datoteci .config po sopstvenom izboru (najverovatnije CONFIG_BPF će već biti omogućen jer ga systemd koristi). Evo liste opcija iz kernela korištenih za ovaj članak:

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

Tada možemo jednostavno sastaviti i instalirati module i kernel (usput, kernel možete sastaviti koristeći novosastavljenu clangdodavanjem CC=clang):

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

i restartujte sa novim kernelom (ja koristim za ovo 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

Najčešće korišteni uslužni program u članku će biti uslužni program bpftool, koji se isporučuje kao dio Linux kernela. Napisan je i održavan od strane BPF programera za BPF programere i može se koristiti za upravljanje svim vrstama BPF objekata - učitavanje programa, kreiranje i uređivanje mapa, istraživanje života BPF ekosistema, itd. Dokumentaciju u obliku izvornih kodova za man stranice možete pronaći u jezgru ili, već sastavljeno, na mreži.

U vrijeme pisanja ovog teksta bpftool dolazi gotov samo za RHEL, Fedoru i Ubuntu (vidi, na primjer, ovu temu, koji priča nedovršenu priču o pakovanju bpftool u Debianu). Ali ako ste već izgradili kernel, onda ga napravite bpftool lako peasy:

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

$

(ovdje ${linux} - ovo je vaš kernel direktorij.) Nakon izvršenja ovih naredbi bpftool biće prikupljeni u imeniku ${linux}/tools/bpf/bpftool i može se dodati na putanju (prije svega korisniku root) ili samo kopirajte na /usr/local/sbin.

Skupiti bpftool najbolje je koristiti ovo drugo clang, sastavljen kako je gore opisano, i provjerite da li je ispravno sastavljen - koristeći, na primjer, naredbu

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

koji će pokazati koje su BPF funkcije omogućene u vašem kernelu.

Usput, prethodna naredba se može pokrenuti kao

# bpftool f p k

Ovo se radi po analogiji sa uslužnim programima iz paketa iproute2, gdje možemo, na primjer, reći ip a s eth0 umjesto ip addr show dev eth0.

zaključak

BPF vam omogućava da potujete buvu kako biste efikasno izmjerili i u hodu promijenili funkcionalnost jezgra. Sistem se pokazao vrlo uspješnim, u najboljoj tradiciji UNIX-a: jednostavan mehanizam koji vam omogućava da (re)programirate kernel omogućio je velikom broju ljudi i organizacija da eksperimentišu. I, iako su eksperimenti, kao i razvoj same BPF infrastrukture, daleko od završetka, sistem već ima stabilan ABI koji vam omogućava da izgradite pouzdanu, i što je najvažnije, efikasnu poslovnu logiku.

Želio bih napomenuti da je, po mom mišljenju, tehnologija postala toliko popularna jer, s jedne strane, može igraj (arhitektura mašine se može razumeti manje-više u jednoj večeri), a sa druge strane da se rešavaju problemi koji se nisu mogli (lepo) rešiti pre njene pojave. Ove dvije komponente zajedno tjeraju ljude na eksperimentiranje i sanjarenje, što dovodi do pojave sve inovativnijih rješenja.

Ovaj članak, iako nije posebno kratak, samo je uvod u svijet BPF-a i ne opisuje „napredne“ karakteristike i važne dijelove arhitekture. Plan za budućnost je otprilike ovakav: sljedeći članak će biti pregled tipova BPF programa (postoji 5.8 tipova programa podržanih u kernelu 30), zatim ćemo konačno pogledati kako pisati prave BPF aplikacije koristeći programe za praćenje kernela kao primjer, onda je vrijeme za detaljniji kurs o BPF arhitekturi, praćen primjerima BPF umrežavanja i sigurnosnih aplikacija.

Prethodni članci u ovoj seriji

  1. BPF za mališane, nulti dio: klasični BPF

Linkovi

  1. Referentni vodič za BPF i XDP — dokumentacija o BPF-u od cilija, tačnije od Daniela Borkmana, jednog od kreatora i održavatelja BPF-a. Ovo je jedan od prvih ozbiljnijih opisa, koji se razlikuje od ostalih po tome što Danijel tačno zna o čemu piše i tu nema grešaka. Ovaj dokument posebno opisuje kako raditi sa BPF programima tipa XDP i TC koristeći dobro poznati uslužni program ip iz paketa iproute2.

  2. Documentation/networking/filter.txt — originalni fajl sa dokumentacijom za klasični, a zatim prošireni BPF. Dobro štivo ako želite da se udubite u asemblerski jezik i tehničke arhitektonske detalje.

  3. Blog o BPF-u sa facebook-a. Ažurira se rijetko, ali prikladno, kako tamo pišu Aleksej Starovoitov (autor eBPF-a) i Andrii Nakryiko - (održavalac) libbpf).

  4. Tajne bpftool-a. Zabavna twitter tema od Quentina Monnea sa primjerima i tajnama korištenja bpftool-a.

  5. Zaronite u BPF: spisak materijala za čitanje. Ogromna (i još uvijek održavana) lista veza do BPF dokumentacije od Quentina Monnea.

izvor: www.habr.com

Dodajte komentar