BPF za najmlađe, prvi dio: produženi BPF

U početku je postojala tehnologija i zvala se BPF. Pogledali smo je prijašnji, starozavjetni članak iz ove serije. Godine 2013., zahvaljujući naporima Alexei Starovoitov i Daniel Borkman, njegova poboljšana verzija, optimizirana za moderne 64-bitne strojeve, razvijena je i uključena u Linux kernel. Ova nova tehnologija je kratko nazvana Internal BPF, zatim je preimenovana u Extended BPF, a sada, nakon nekoliko godina, svi je jednostavno zovu BPF.

Grubo govoreći, BPF vam omogućuje pokretanje proizvoljnog korisničkog koda u Linux kernel prostoru, a nova se arhitektura pokazala toliko uspješnom da će nam trebati još desetak članaka da opišemo sve njezine primjene. (Jedina stvar koju programeri nisu dobro napravili, kao što možete vidjeti u kodu izvedbe ispod, je stvaranje pristojnog logotipa.)

Ovaj članak opisuje strukturu BPF virtualnog stroja, kernel sučelja za rad s BPF-om, razvojne alate, kao i kratak, vrlo kratak pregled postojećih mogućnosti, tj. sve što će nam u budućnosti trebati za dublje proučavanje praktičnih primjena BPF-a.
BPF za najmlađe, prvi dio: produženi BPF

Sažetak članka

Uvod u BPF arhitekturu. Prvo ćemo baciti pogled na BPF arhitekturu iz ptičje perspektive i prikazati glavne komponente.

Registri i sustav naredbi BPF virtualnog stroja. Već imajući predodžbu o arhitekturi u cjelini, opisat ćemo strukturu BPF virtualnog stroja.

Životni ciklus BPF objekata, bpffs datotečni sustav. U ovom odjeljku pobliže ćemo pogledati životni ciklus BPF objekata - programa i mapa.

Upravljanje objektima pomoću bpf sistemskog poziva. Uz određeno razumijevanje sustava koji već postoji, konačno ćemo pogledati kako kreirati i manipulirati objektima iz korisničkog prostora pomoću posebnog sistemskog poziva − bpf(2).

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

Pomoćnici kernela. Ovdje ćemo naučiti kako BPF programi mogu pristupiti kernel helper funkcijama – alatu koji uz mape iz temelja proširuje mogućnosti novog BPF-a u odnosu na klasični.

Pristup kartama iz BPF programa. Do ove točke znat ćemo dovoljno da shvatimo kako točno možemo stvoriti programe koji koriste karte. I čak zavirimo na brzinu u veliki i moćni verifikator.

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

Zaključak. Na kraju članka, oni koji su dovde pročitali naći će poticajne riječi i kratak opis onoga što će se dogoditi u narednim člancima. Također ćemo navesti niz poveznica za samostalno učenje za one koji nemaju želju ili mogućnost čekati nastavak.

Uvod u BPF arhitekturu

Prije nego počnemo razmatrati BPF arhitekturu, posljednji put ćemo se osvrnuti na (oh). klasični BPF, koji je razvijen kao odgovor na pojavu RISC strojeva i riješio je problem učinkovitog filtriranja paketa. Arhitektura se pokazala toliko uspješnom da je, rođena u burnim devedesetima u Berkeley UNIX-u, portirana na većinu postojećih operativnih sustava, preživjela lude dvadesete i još uvijek nalazi nove primjene.

Novi BPF razvijen je kao odgovor na sveprisutnost 64-bitnih strojeva, usluga u oblaku i povećanu potrebu za alatima za stvaranje SDN-a (Ssoftver-dusavršen numrežavanje). Razvijen od strane mrežnih inženjera kernela kao poboljšanu zamjenu za klasični BPF, novi BPF je doslovno šest mjeseci kasnije pronašao primjenu u teškom zadatku praćenja Linux sustava, a sada, šest godina nakon njegove pojave, trebat će nam cijeli sljedeći članak samo da navesti različite vrste programa.

Smiješne slike

U svojoj jezgri, BPF je virtualni stroj sandbox koji vam omogućuje pokretanje "proizvoljnog" koda u prostoru kernela bez ugrožavanja sigurnosti. BPF programi se stvaraju u korisničkom prostoru, učitavaju u kernel i povezuju s nekim izvorom događaja. Događaj može biti, na primjer, isporuka paketa mrežnom sučelju, pokretanje neke kernel funkcije itd. U slučaju paketa, BPF program će imati pristup podacima i metapodacima paketa (za čitanje i, eventualno, pisanje, ovisno o vrsti programa); u slučaju pokretanja kernel funkcije, argumenti funkcija, uključujući pokazivače na memoriju jezgre itd.

Pogledajmo pobliže ovaj proces. Za početak, razgovarajmo o prvoj razlici od klasičnog BPF-a, programi za koje su napisani u asembleru. U novoj verziji, arhitektura je proširena tako da se programi mogu pisati na jezicima visoke razine, prvenstveno, naravno, u C. Za to je razvijen backend za llvm, koji vam omogućuje generiranje bajt koda za BPF arhitekturu.

BPF za najmlađe, prvi dio: produženi BPF

Arhitektura BPF-a dizajnirana je djelomično za učinkovit rad na modernim strojevima. Kako bi ovo funkcioniralo u praksi, BPF bajt kod, nakon što se učita u kernel, prevodi se u izvorni kod pomoću komponente koja se zove JIT kompajler (Jgornji In Time). Dalje, ako se sjećate, u klasičnom BPF-u program je učitavan u kernel i pripojen izvoru događaja atomski - u kontekstu jednog poziva sustava. U novoj arhitekturi to se događa u dvije faze - prvo se kod učitava u kernel pomoću sistemskog poziva bpf(2)a zatim, kasnije, putem drugih mehanizama koji variraju ovisno o vrsti programa, program se pripaja izvoru događaja.

Ovdje čitatelj može imati pitanje: je li to bilo moguće? Kako je zajamčena sigurnost izvršenja takvog koda? Sigurnost izvršenja jamči nam faza učitavanja BPF programa koja se zove verifier (na engleskom se ova faza zove verifier i ja ću nastaviti koristiti englesku riječ):

BPF za najmlađe, prvi dio: produženi BPF

Verifier je statički analizator koji osigurava da program ne ometa normalan rad kernela. To, usput, ne znači da program ne može ometati rad sustava - BPF programi, ovisno o vrsti, mogu čitati i prepisivati ​​dijelove memorije kernela, vraćati vrijednosti funkcija, rezati, dodavati, prepisivati pa čak i prosljeđivanje mrežnih paketa. Verifier jamči da izvođenje BPF programa neće srušiti kernel i da program koji, prema pravilima, ima pristup za pisanje, na primjer, podataka odlaznog paketa, neće moći prebrisati memoriju kernela izvan paketa. Verifikator ćemo pogledati malo detaljnije u odgovarajućem odjeljku, nakon što se upoznamo sa svim ostalim komponentama BPF-a.

Pa što smo do sada naučili? Korisnik piše program u C-u, učitava ga u kernel pomoću sistemskog poziva bpf(2), gdje ga provjerava verifikator i prevodi u izvorni bajt kod. Tada isti ili drugi korisnik povezuje program s izvorom događaja i on se počinje izvršavati. Odvajanje pokretanja i povezivanja potrebno je iz nekoliko razloga. Prvo, pokretanje verifikatora je relativno skupo i skidanjem istog programa nekoliko puta gubimo računalo. Drugo, točno kako je program povezan ovisi o njegovoj vrsti, a jedno "univerzalno" sučelje razvijeno prije godinu dana možda neće biti prikladno za nove vrste programa. (Iako sada kada arhitektura postaje zrelija, postoji ideja da se ovo sučelje unificira na razini libbpf.)

Pažljivi čitatelj može primijetiti da još nismo završili sa slikama. Dapače, sve navedeno ne objašnjava zašto BPF bitno mijenja sliku u odnosu na klasični BPF. Dvije inovacije koje značajno proširuju opseg primjenjivosti su mogućnost korištenja zajedničke memorije i kernel pomoćne funkcije. U BPF-u, zajednička memorija implementirana je pomoću takozvanih mapa - zajedničkih struktura podataka sa specifičnim API-jem. Vjerojatno su dobili ovo ime jer je prva vrsta karte koja se pojavila bila hash tablica. Zatim su se pojavili nizovi, lokalne (po CPU) hash tablice i lokalni nizovi, stabla pretraživanja, karte koje sadrže pokazivače na BPF programe i još mnogo toga. Ono što nam je sada zanimljivo je da BPF programi sada imaju mogućnost zadržavanja stanja između poziva i dijeljenja s drugim programima i korisničkim prostorom.

Kartama se pristupa iz korisničkih procesa pomoću sistemskog poziva bpf(2), i iz BPF programa koji se izvode u kernelu koristeći pomoćne funkcije. Štoviše, pomoćnici ne postoje samo za rad s kartama, već i za pristup drugim mogućnostima jezgre. Na primjer, BPF programi mogu koristiti pomoćne funkcije za prosljeđivanje paketa drugim sučeljima, generiranje perf događaja, pristup strukturama kernela i tako dalje.

BPF za najmlađe, prvi dio: produženi BPF

Ukratko, BPF pruža mogućnost učitavanja proizvoljnog, tj. testiranog verifikatorom korisničkog koda u prostor kernela. Ovaj kod može spremati stanje između poziva i razmjenjivati ​​podatke s korisničkim prostorom, a također ima pristup podsustavima jezgre koje dopušta ova vrsta programa.

Ovo je već slično mogućnostima koje pružaju moduli jezgre, u usporedbi s kojima BPF ima neke prednosti (naravno, možete usporediti samo slične aplikacije, na primjer, praćenje sustava - ne možete pisati proizvoljni upravljački program s BPF-om). Možete primijetiti niži ulazni prag (neki uslužni programi koji koriste BPF ne zahtijevaju od korisnika vještine programiranja kernela ili općenito vještine programiranja), sigurnost tijekom rada (podignite ruku u komentarima za one koji nisu pokvarili sustav prilikom pisanja ili testiranje modula), atomičnost - dolazi do zastoja prilikom ponovnog učitavanja modula, a BPF podsustav osigurava da nijedan događaj nije propušten (pravo rečeno, to ne vrijedi za sve vrste BPF programa).

Prisutnost takvih mogućnosti čini BPF univerzalnim alatom za proširenje kernela, što je potvrđeno u praksi: sve više i više novih vrsta programa dodaje se u BPF, sve više i više velikih tvrtki koristi BPF na borbenim poslužiteljima 24×7, sve više i više startupi grade svoje poslovanje na rješenjima na temelju kojih se temelje BPF. BPF se koristi posvuda: u zaštiti od DDoS napada, stvaranju SDN-a (na primjer, implementacija mreža za kubernetes), kao glavni alat za praćenje sustava i sakupljač statistike, u sustavima za otkrivanje upada i sandbox sustavima itd.

Završimo pregledni dio članka ovdje i pogledajmo virtualni stroj i BPF ekosustav detaljnije.

Digresija: komunalije

Da biste mogli pokrenuti primjere u sljedećim odjeljcima, možda će vam trebati nekoliko uslužnih programa, barem llvm/clang uz podršku bpf-a i bpftool, U odjeljku Razvojni alati Možete pročitati upute za sastavljanje uslužnih programa, kao i svoj kernel. Ovaj odjeljak je postavljen ispod kako ne bi narušio sklad naše prezentacije.

Registri virtualnog stroja BPF i sustav instrukcija

Arhitektura i sustav naredbi 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 naredbi odabrani imajući u vidu presjek, u matematičkom smislu, mogućnosti modernih strojeva. Osim toga, programima su nametnuta razna ograničenja, primjerice donedavno nije bilo moguće pisati petlje i potprograme, a broj instrukcija bio je ograničen na 4096 (sada privilegirani programi mogu učitati do milijun instrukcija).

BPF ima jedanaest korisnički dostupnih 64-bitnih registara r0-r10 i brojač programa. Registar r10 sadrži pokazivač okvira i samo je za čitanje. Programi imaju pristup 512-bajtnom stogu tijekom izvođenja i neograničenu količinu zajedničke memorije u obliku mapa.

BPF programima dopušteno je pokretanje određenog skupa kernel pomoćnika tipa programa i, u novije vrijeme, redovitih funkcija. Svaka pozvana funkcija može uzeti do pet argumenata koji se prosljeđuju u registrima r1-r5, a povratna vrijednost se prosljeđuje r0. Zajamčeno je da se nakon povratka s funkcije sadržaj registrira r6-r9 Neće se promijeniti.

Za učinkovito prevođenje programa, registri r0-r11 za sve podržane arhitekture jedinstveno su preslikani u stvarne registre, uzimajući u obzir ABI značajke trenutne arhitekture. Na primjer, za x86_64 registri r1-r5, koji se koristi za prosljeđivanje parametara funkcije, prikazani su na rdi, rsi, rdx, rcx, r8, koji se koriste za prosljeđivanje parametara funkcijama na x86_64. Na primjer, kôd s lijeve strane prevodi se u kôd 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

Registar r0 također se koristi za vraćanje rezultata izvršenja programa, te u 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, stog, pokazivač konteksta i zajedničku memoriju u obliku mapa. Nije da je sve ovo prijeko potrebno na putovanju, ali...

Nastavimo s opisom i razgovarajmo o sustavu naredbi za rad s ovim objektima. Svi (Gotovo sve) BPF instrukcije imaju fiksnu 64-bitnu veličinu. Ako pogledate jednu instrukciju na 64-bitnom Big Endian stroju vidjet ćete

BPF za najmlađe, prvi dio: produženi BPF

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

BPF za najmlađe, prvi dio: produženi BPF

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

Daljnji plan proučavanja sustava BPF instrukcija je sljedeći: umjesto pedantnog nabrajanja svih instrukcija i njihovih parametara, pogledat ćemo par primjera u ovom dijelu i iz njih će postati jasno kako instrukcije zapravo funkcioniraju i kako ručno rastaviti bilo koju binarnu datoteku za BPF. Da bismo konsolidirali materijal kasnije u članku, također ćemo se susresti s pojedinačnim uputama u odjeljcima o Verifikatoru, JIT kompajleru, prijevodu klasičnog BPF-a, kao i kod proučavanja mapa, pozivanja funkcija itd.

Kada govorimo o pojedinačnim uputama, mislit ćemo na osnovne datoteke bpf.h и bpf_common.h, koji definiraju numeričke kodove BPF instrukcija. Kada sami proučavate arhitekturu i/ili analizirate binarne datoteke, semantiku možete pronaći u sljedećim izvorima, poredanim po složenosti: Neslužbena eBPF specifikacija, BPF i XDP referentni vodič, set uputa, Dokumentacija/umrežavanje/filter.txt i, naravno, u izvornom kodu Linuxa - verifikator, JIT, BPF interpreter.

Primjer: rastavljanje BPF-a u glavi

Pogledajmo primjer u kojem kompiliramo program readelf-example.c i pogledajte rezultirajuću binarnu datoteku. Otkrit ćemo izvorni sadržaj readelf-example.c u nastavku, nakon što obnovimo 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 ................

Prvi stupac u izlazu readelf je uvlačenje i naš se program 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 naredbi su jednaki b7, 15, b7 и 95. Podsjetimo se da su tri najmanje značajna bita klasa instrukcije. U našem slučaju, četvrti bit svih instrukcija je prazan, tako da su klase instrukcija 7, 5, 7, 5, redom. 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 stupce 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. Dodjeljuje vrijednost odredišnom registru. Ako je bit postavljen s (izvor), tada se vrijednost preuzima iz izvornog registra, a ako, kao u našem slučaju, nije postavljen, tada se vrijednost preuzima iz polja Imm. Dakle, u prvoj i trećoj uputi izvodimo operaciju r0 = Imm. Nadalje, rad JMP klase 1 je BPF_JEQ (skok ako je jednako). U našem slučaju, budući da je bit S je nula, uspoređuje vrijednost izvornog registra s poljem Imm. Ako se vrijednosti podudaraju, tada dolazi do prijelaza PC + OffGdje PC, kao i obično, sadrži adresu sljedeće instrukcije. Konačno, JMP Class 9 Operation je BPF_EXIT. Ova instrukcija prekida program, vraćajući se u jezgru r0. Dodajmo novi stupac u našu tablicu:

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 prikladnijem obliku:

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

Ako se sjetimo što stoji u registru r1 programu se prosljeđuje pokazivač na kontekst iz kernela iu registru r0 vrijednost se vraća jezgri, tada možemo vidjeti da ako je pokazivač na kontekst nula, tada vraćamo 1, au suprotnom - 2. Provjerimo da li smo u pravu gledajući izvor:

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

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

Primjer iznimke: 16-bajtna instrukcija

Ranije smo spomenuli da neke upute zauzimaju više od 64 bita. To se, primjerice, odnosi na upute lddw (Kod = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — učitavanje dvostruke riječi 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 neposredne vrijednosti u registar u jednoj 64-bitnoj instrukciji neće raditi. U tu svrhu koriste se dvije susjedne instrukcije 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

Ponovno ćemo se naći s uputama lddw, kada govorimo o selidbama i radu s kartama.

Primjer: rastavljanje BPF-a pomoću standardnih alata

Dakle, naučili smo čitati BPF binarne kodove i spremni smo analizirati svaku instrukciju ako je potrebno. Međutim, vrijedi 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 datotečni sustav

(Prvi put sam saznao neke detalje opisane u ovom pododjeljku od post Aleksej Starovoitov u Blog BPF-a.)

BPF objekti - programi i mape - kreiraju se iz korisničkog prostora pomoću naredbi BPF_PROG_LOAD и BPF_MAP_CREATE sistemski poziv bpf(2), govorit ćemo o tome kako se to točno događa u sljedećem odjeljku. Ovo stvara strukture podataka jezgre i za svaku od njih refcount (broj referenci) je postavljen na jedan, a deskriptor datoteke koji pokazuje na objekt vraća se korisniku. Nakon što je ručka zatvorena refcount objekt se smanjuje za jedan, a kada dosegne nulu, objekt se uništava.

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

BPF za najmlađe, prvi dio: produženi 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žno sučelje za obradu dolaznih paketa ili ga povezati s nekim od njih tracepoint u jezgri. U ovom trenutku će se brojač referenci također povećati za jedan i moći ćemo zatvoriti deskriptor datoteke u programu za učitavanje.

Što se događa ako sada isključimo bootloader? Ovisi o vrsti generatora događaja (hooka). Sve mrežne kuke postojat će nakon što loader završi, to su takozvane globalne kuke. I, na primjer, programi praćenja bit će pušteni nakon završetka procesa koji ih je stvorio (i stoga se nazivaju lokalnim, od "lokalno prema procesu"). Tehnički, lokalne kuke uvijek imaju odgovarajući deskriptor datoteke u korisničkom prostoru i stoga se zatvaraju kada se proces zatvori, ali globalne kuke nemaju. Na sljedećoj slici, pomoću crvenih križića, pokušavam pokazati kako prekid programa učitavanja utječe na životni vijek objekata u slučaju lokalnih i globalnih zakačica.

BPF za najmlađe, prvi dio: produženi 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 napiše pravila i poveže BPF program s mrežnim sučeljem, nakon čega se bootloader može ubiti. S druge strane, zamislite debugging trace program koji ste napisali na koljenima u deset minuta – kada bude gotov, htjeli biste da u sustavu ne ostane smeća, a lokalne kuke će to osigurati.

S druge strane, zamislite da se želite spojiti na točku praćenja u kernelu i prikupljati statistiku tijekom mnogo godina. U ovom slučaju, trebali biste dovršiti korisnički dio i povremeno se vraćati na statistiku. Datotečni sustav bpf pruža ovu priliku. To je sustav pseudo datoteka samo u memoriji koji omogućuje stvaranje datoteka koje referenciraju BPF objekte i time povećavaju refcount objekti. Nakon toga, loader može izaći, a objekti koje je stvorio ostat će živi.

BPF za najmlađe, prvi dio: produženi BPF

Stvaranje datoteka u bpffs koje referenciraju BPF objekte naziva se "pinning" (kao u sljedećem izrazu: "proces može pin BPF program ili mapu"). Stvaranje datotečnih objekata za BPF objekte ima smisla ne samo zbog produljenja života lokalnih objekata, već i zbog upotrebljivosti globalnih objekata - vraćajući se na primjer s globalnim programom zaštite od DDoS-a, želimo imati mogućnost doći i pogledati statistiku s vremena na vrijeme.

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

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

Nazivi datotečnih sustava kreiraju se pomoću naredbe BPF_OBJ_PIN Poziv BPF sustava. Za ilustraciju, uzmimo program, prevedimo ga, učitajmo i prikvačimo bpffs. Naš program ne čini ništa korisno, mi samo predstavljamo kôd 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";

Prevedimo ovaj program i stvorimo lokalnu kopiju datotečnog sustava 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) (neki nevažni redovi uklonjeni 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 pomoću BPF_PROG_LOAD, primio je deskriptor datoteke od kernela 3 i pomoću naredbe BPF_OBJ_PIN prikvačio je ovaj deskriptor datoteke kao datoteku "bpf-mountpoint/test". Nakon ovoga program za podizanje sustava bpftool završio s radom, ali naš je program ostao u kernelu, iako ga nismo priključili ni na jedno mrežno sučelje:

$ 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 možemo normalno izbrisati unlink(2) i nakon toga odgovarajući program će biti izbrisan:

$ 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 odspojimo 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ćuju zamjenu programa u hodu, tj. osigurati atomičnost sekvence replace = detach old program, attach new program. U tom će slučaju sve aktivne instance stare verzije programa završiti s radom, a novi rukovatelji događajima bit će kreirani iz novog programa, a “atomičnost” ovdje znači da niti jedan događaj neće biti propušten.

Prilaganje programa izvorima događaja

U ovom članku nećemo posebno opisivati ​​povezivanje programa s izvorima događaja, budući da to ima smisla proučavati u kontekstu specifične vrste programa. Cm. primjer u nastavku, u kojem pokazujemo kako su povezani programi poput XDP-a.

Manipuliranje objektima pomoću bpf sistemskog poziva

BPF programi

Svi BPF objekti kreiraju se i njima se upravlja iz korisničkog prostora pomoću 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 naredbe, i utvrđivanje union bpf_attr zauzima 200 redaka. No, to nas ne bi trebalo zastrašiti, budući da ćemo se s naredbama i parametrima upoznati tijekom nekoliko članaka.

Počnimo s ekipom BPF_PROG_LOAD, koji stvara BPF programe - uzima skup BPF instrukcija i učitava ih u kernel. U trenutku učitavanja pokreće se verifikator, a potom JIT prevodilac i nakon uspješnog izvođenja korisniku se vraća deskriptor programske datoteke. Vidjeli smo što se dalje s njim događa u prethodnom odjeljku o životnom ciklusu BPF objekata.

Sada ćemo napisati prilagođeni program koji će učitati jednostavan BPF program, ali prvo moramo odlučiti kakvu vrstu programa želimo učitati - morat ćemo odabrati тип te u okviru ovog tipa napisati program koji će proći test verifikatora. No, kako ne bismo zakomplicirali proces, evo gotovog rješenja: uzet ćemo program poput BPF_PROG_TYPE_XDP, koji će vratiti vrijednost XDP_PASS (preskoči sve pakete). U BPF asembleru to izgleda vrlo jednostavno:

r0 = 2
exit

Nakon što smo se odlučili za da prenijet ćemo, 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 s definiranjem niza insns - naš BPF program u strojnom kodu. U ovom slučaju, svaka instrukcija BPF programa je upakirana u strukturu bpf_insn. Prvi element insns u skladu s uputama r0 = 2, drugi - exit.

Povlačenje. Kernel definira prikladnije makronaredbe za pisanje strojnih kodova i korištenje datoteke zaglavlja kernela tools/include/linux/filter.h mogli bismo napisati

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

No budući da je pisanje BPF programa u izvornom kodu potrebno samo za pisanje testova u kernelu i članaka o BPF-u, odsutnost ovih makronaredbi zapravo ne komplicira život programera.

Nakon definiranja BPF programa, prelazimo na njegovo učitavanje u kernel. Naš minimalistički skup parametara attr uključuje vrstu programa, skup i broj uputa, potrebnu licencu i naziv "woo", koji koristimo za pronalaženje našeg programa u sustavu nakon preuzimanja. Program se, kao što je obećano, učitava u sustav pomoću sistemskog poziva bpf.

Na kraju programa završavamo u beskonačnoj petlji koja simulira korisni teret. Bez toga, kernel će ugasiti program kada se zatvori deskriptor datoteke koji nam je sistemski poziv vratio bpf, a nećemo ga vidjeti u sustavu.

Pa, spremni smo za testiranje. Sastavimo i pokrenimo program pod straceda provjerite radi li sve 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 u redu, bpf(2) vratio nam je handle 3 i ušli smo u beskonačnu petlju s pause(). Pokušajmo pronaći naš program u sustavu. 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 postoji učitani program na sustavu woo čiji je globalni ID 390 i trenutno je u tijeku simple-prog postoji deskriptor otvorene datoteke koji upućuje na program (i ako simple-prog onda će završiti posao woo nestat će). Očekivano, program woo uzima 16 bajtova - dvije instrukcije - binarnih kodova u BPF arhitekturi, ali u izvornom obliku (x86_64) već ima 40 bajtova. Pogledajmo naš program u izvornom obliku:

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

bez iznenađenja. Sada pogledajmo kod koji je generirao JIT kompajler:

# 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 baš učinkovit za exit(2), ali pošteno govoreći, naš program je previše jednostavan, a za netrivijalne programe su, naravno, potrebni prolog i epilog koje je dodao JIT kompajler.

Karte

BPF programi mogu koristiti strukturirana memorijska područja koja su dostupna i drugim BPF programima i programima u korisničkom prostoru. Ti se objekti nazivaju mapama i u ovom odjeljku pokazat ćemo kako njima manipulirati pomoću sistemskog poziva bpf.

Recimo odmah da mogućnosti karata 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 s perf događajima itd. O njima ovdje nećemo govoriti, da ne zbunimo čitatelja. Osim ovoga, zanemarujemo probleme sinkronizacije, jer to nije važno za naše primjere. Potpuni popis dostupnih vrsta karata može se pronaći u <linux/bpf.h>, au ovom odjeljku ćemo uzeti kao primjer povijesno prvi tip, hash tablicu BPF_MAP_TYPE_HASH.

Ako kreirate hash tablicu u, recimo, C++, rekli biste unordered_map<int,long> woo, što na ruskom znači “Trebam stol woo neograničene veličine, čiji su ključevi vrste int, a vrijednosti su tip long" Kako bismo stvorili BPF hash tablicu, moramo učiniti gotovo istu stvar, osim što moramo navesti maksimalnu veličinu tablice, a umjesto da navedemo tipove ključeva i vrijednosti, moramo navesti njihove veličine u bajtovima . Za izradu karata koristite naredbu BPF_MAP_CREATE sistemski poziv bpf. Pogledajmo manje-više minimalan program koji stvara kartu. Nakon prethodnog programa koji učitava BPF programe, ovaj bi vam se trebao učiniti jednostavnim:

$ 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 “Trebam hash tablicu s ključevima i vrijednostima veličine sizeof(int), u koji mogu staviti najviše četiri elementa." Prilikom izrade BPF mapa, možete odrediti druge parametre, na primjer, na isti način kao u primjeru s programom, naveli smo naziv objekta kao "woo".

Prevedimo i pokrenimo 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(

Ovo je sistemski poziv bpf(2) vratio nam je broj karte deskriptora 3 a zatim program očekivano čeka daljnje upute u pozivu sustava pause(2).

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

$ 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. Svaki program u sustavu može koristiti ovaj ID za otvaranje postojeće karte pomoću naredbe BPF_MAP_GET_FD_BY_ID sistemski poziv bpf.

Sada se možemo igrati s našom hash tablicom. 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 opet tablicu:

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

hura! Uspjeli smo dodati jedan element. Imajte na umu da moramo raditi na razini bajta da bismo to učinili, jer bptftool ne zna koje su vrste vrijednosti u hash tablici. (Ovo znanje joj se može prenijeti pomoću BTF-a, ali više o tome sada.)

Kako toč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 kartu prema njenom globalnom ID-u pomoću naredbe BPF_MAP_GET_FD_BY_ID и bpf(2) vratio nam je deskriptor 3. Daljnjim korištenjem naredbe BPF_MAP_GET_NEXT_KEY mimoilazeći smo pronašli prvi ključ u tablici 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

Očekivano, vrlo je jednostavno: naredba BPF_MAP_GET_FD_BY_ID otvara našu kartu po ID-u i naredbi BPF_MAP_UPDATE_ELEM prepisuje element.

Dakle, nakon što kreiramo hash tablicu iz jednog programa, možemo čitati i pisati njen sadržaj iz drugog. Imajte na umu da ako smo mi to mogli učiniti iz naredbenog retka, onda to može učiniti bilo koji drugi program u sustavu. Osim gore opisanih naredbi, za rad s kartama iz korisničkog prostora, Sljedeći:

  • BPF_MAP_LOOKUP_ELEM: pronađite vrijednost po ključu
  • BPF_MAP_UPDATE_ELEM: ažuriraj/stvori vrijednost
  • BPF_MAP_DELETE_ELEM: izvadite ključ
  • BPF_MAP_GET_NEXT_KEY: pronađite sljedeći (ili prvi) ključ
  • BPF_MAP_GET_NEXT_ID: omogućuje vam prolazak kroz sve postojeće karte, tako to funkcionira bpftool map
  • BPF_MAP_GET_FD_BY_ID: otvori postojeću kartu prema njezinom globalnom ID-u
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomski ažurira vrijednost objekta i vraća staru
  • BPF_MAP_FREEZE: učini kartu nepromjenjivom iz korisničkog prostora (ova se operacija 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 s karte

Ne rade sve ove naredbe za sve vrste karata, ali općenito rad s drugim vrstama karata iz korisničkog prostora izgleda potpuno isto kao i rad s hash tablicama.

Reda radi, završimo naše eksperimente s hash tablicom. Sjećate se da smo napravili tablicu 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 pogreš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, tim BPF_MAP_UPDATE_ELEM pokušava stvoriti novi, peti, ključ, ali se ruši E2BIG.

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

(Za čitatelje koji su nezadovoljni nedostatkom primjera niske razine: detaljno ćemo analizirati programe koji koriste karte i pomoćne funkcije stvorene pomoću libbpf i reći vam što se događa na razini instrukcija. Za čitatelje koji su nezadovoljni jako puno, dodali smo primjer na odgovarajuće mjesto u članku.)

Pisanje BPF programa koristeći libbpf

Pisanje BPF programa pomoću strojnih kodova može biti zanimljivo samo prvi put, a onda nastupi sitost. U ovom trenutku morate obratiti pažnju na llvm, koji ima pozadinu za generiranje koda za BPF arhitekturu, kao i biblioteku libbpf, koji vam omogućuje pisanje korisničke strane BPF aplikacija i učitavanje koda BPF programa generiranih pomoću llvm/clang.

Zapravo, kao što ćemo vidjeti u ovom i sljedećim člancima, libbpf radi dosta posla bez njega (ili sličnih alata - iproute2, libbcc, libbpf-go, itd.) nemoguće je živjeti. Jedna od ubojitih karakteristika projekta libbpf je BPF CO-RE (Compile Once, Run Everywhere) - projekt koji vam omogućuje pisanje BPF programa koji su prenosivi s jednog kernela na drugi, s mogućnošću pokretanja na različitim API-jima (na primjer, kada se struktura kernela promijeni iz verzije do verzije). Kako biste mogli raditi s CO-RE, vaš kernel mora biti kompajliran s BTF podrškom (opisujemo kako to učiniti u odjeljku Razvojni alati. Možete provjeriti je li vaša jezgra izgrađena s BTF-om ili ne vrlo jednostavno - prisutnošću sljedeće datoteke:

$ 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. Detaljno ćemo govoriti o CO-RE-u u sljedećem članku, ali u ovom - samo izgradite sebi kernel CONFIG_DEBUG_INFO_BTF.

knjižnica libbpf živi točno u imeniku tools/lib/bpf kernel i njegov razvoj se provodi putem mailing liste [email protected]. Međutim, održava se zasebno spremište za potrebe aplikacija koje žive izvan kernela https://github.com/libbpf/libbpf u kojoj je biblioteka kernela zrcaljena za pristup čitanju više-manje kakva jest.

U ovom odjeljku ćemo pogledati kako možete stvoriti projekt koji koristi libbpf, napišimo nekoliko (više ili manje besmislenih) testnih programa i detaljno analizirajmo kako sve to radi. To će nam omogućiti da u sljedećim odjeljcima lakše objasnimo kako točno BPF programi stupaju u interakciju s mapama, pomoćnicima kernela, BTF-om itd.

Obično projekti koriste libbpf dodati GitHub repozitorij kao git podmodul, učinit ćemo 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 libbpf vrlo 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 odjeljku je sljedeći: napisat ćemo BPF program poput BPF_PROG_TYPE_XDP, isto kao u prethodnom primjeru, ali u C-u, kompajliramo ga pomoću clang, i napišite pomoćni program koji će ga učitati u kernel. U sljedećim odjeljcima proširit ćemo mogućnosti i programa BPF i programa pomoćnika.

Primjer: stvaranje potpune aplikacije pomoću libbpf

Za početak koristimo datoteku /sys/kernel/btf/vmlinux, koji je gore spomenut, i stvorite njegov ekvivalent u obliku datoteke 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 IPv4 zaglavlje definirano 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 u C-u:

$ 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 pozornost na mnoge detalje. Prvo, prva datoteka zaglavlja koju uključujemo je vmlinux.h, koji smo upravo generirali pomoću bpftool btf dump - sada ne trebamo instalirati kernel-headers paket da saznamo kako strukture kernela izgledaju. Sljedeća datoteka zaglavlja dolazi nam iz knjižnice libbpf. Sada nam treba samo za definiranje makronaredbe SEC, koji šalje znak u odgovarajući odjeljak ELF objektne datoteke. Naš program nalazi se u rubrici xdp/simple, gdje prije kose crte definiramo tip programa BPF - ovo je konvencija koja se koristi u libbpf, na temelju naziva odjeljka zamijenit će točnu vrstu pri pokretanju bpf(2). Sam program BPF-a je C - vrlo jednostavan i sastoji se od jedne linije return XDP_PASS. Na kraju, poseban dio "license" sadrži naziv licence.

Naš program možemo prevesti pomoću llvm/clang, verzija >= 10.0.0, ili još bolje, veća (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 značajkama: ukazujemo na ciljnu arhitekturu -target bpf i put do zaglavlja libbpf, koju smo nedavno instalirali. Također, ne zaboravite na -O2, bez ove opcije možda vas čekaju iznenađenja u budućnosti. Pogledajmo naš kod, jesmo li uspjeli napisati program koji smo htjeli?

$ 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 s programom i želimo stvoriti aplikaciju koja će je učitati u kernel. U tu svrhu knjižnica libbpf nudi nam dvije opcije - korištenje API-ja niže razine ili API-ja više razine. Ići ćemo drugim putem, jer želimo naučiti kako pisati, učitati i povezati BPF programe uz minimalan napor za njihovo kasnije proučavanje.

Prvo, moramo generirati "kostur" našeg programa iz njegove binarne datoteke pomoću istog uslužnog programa 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 spisu xdp-simple.skel.h sadrži binarni kod našeg programa i funkcije za upravljanje - učitavanje, prilaganje, 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);
}

Ovdje struct xdp_simple_bpf definiran u datoteci xdp-simple.skel.h i opisuje našu objektnu datoteku:

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 API-ja niske razine: 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 objekt, analizira ga, stvara sve strukture i podstrukture (osim programa, ELF sadrži i druge odjeljke - podatke, podatke samo za čitanje, informacije o ispravljanju pogrešaka, licencu itd.), a zatim ga učitava u kernel pomoću sustava poziv bpf, što možemo provjeriti prevođenjem 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 korištenje našeg programa bpftool. Pronađ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)

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 ispisao dijelove naše izvorne datoteke C. To je učinila knjižnica libbpf, koji je pronašao odjeljak za otklanjanje pogrešaka u binarnom obliku, preveo ga u BTF objekt, učitao u kernel pomoću BPF_BTF_LOAD, a zatim odredio rezultirajući deskriptor datoteke prilikom učitavanja programa s naredbom BPG_PROG_LOAD.

Pomoćnici kernela

BPF programi mogu pokretati "vanjske" funkcije - pomagače kernela. Ove pomoćne funkcije omogućuju BPF programima pristup strukturama jezgre, upravljanje mapama i također komunikaciju sa "stvarnim svijetom" - stvaranje perf događaja, kontrola hardvera (na primjer, preusmjeravanje paketa), itd.

Primjer: bpf_get_smp_processor_id

Unutar okvira paradigme “učenja primjerom”, razmotrimo jednu od pomoćnih funkcija, bpf_get_smp_processor_id(), određeni u spisu 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 ima jedan redak:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Definicije BPF pomoćne funkcije slične su definicijama poziva sustava Linux. Ovdje je npr. definirana funkcija koja nema argumenata. (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đene vrste mogli koristiti ovu funkciju, moraju je registrirati, na primjer za vrstu BPF_PROG_TYPE_XDP funkcija je definirana u kernelu xdp_func_proto, koji na temelju ID-a pomoćne funkcije određuje podržava li XDP ovu funkciju ili ne. Naša funkcija je podupire:

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 "definirani" u datoteci include/linux/bpf_types.h pomoću makronaredbe BPF_PROG_TYPE. Definirano pod navodnicima jer je to logična definicija, au terminima jezika C definicija čitavog niza konkretnih struktura pojavljuje se na drugim mjestima. Konkretno, u spisu kernel/bpf/verifier.c sve definicije iz datoteke bpf_types.h koriste se za stvaranje 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 podatkovnu strukturu tipa struct bpf_verifier_ops, koji se inicijalizira s vrijednošću _name ## _verifier_ops, tj. xdp_verifier_ops za xdp. Struktura xdp_verifier_ops određeno od strane u spisu 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 neka vrsta funkcije unutar BPF programa, vidi verifier.c.

Pogledajmo kako hipotetski 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 strane в <bpf/bpf_helper_defs.h> knjižnice libbpf kao

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 vrsta enum bpf_fun_id, koji nam je definiran u datoteci vmlinux.h (datoteka bpf_helper_defs.h u kernelu generira skripta, tako da su "magični" brojevi u redu). Ova funkcija ne uzima argumente i vraća vrijednost tipa __u32. Kada to pokrenemo u našem programu, clang generira instrukciju BPF_CALL "prava vrsta" Sastavimo program i pogledajmo odjeljak 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. Nakon što se pokrene, logika je jednostavna. Povratna vrijednost iz registra r0 kopirano u r1 a na linijama 2,3 pretvara se u tip u32 — brišu se gornja 32 bita. U redovima 4,5,6,7 vraćamo 2 (XDP_PASS) ili 1 (XDP_DROP) ovisno o tome je li pomoćna funkcija iz retka 0 vratila vrijednost nula ili ne.

Testirajmo se: učitajmo 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

U redu, verifikator je pronašao ispravan kernel-helper.

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

Sve pomoćne funkcije na razini izvođenja imaju prototip

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

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

Pogledajmo novi pomagač kernela i kako BPF prosljeđuje parametre. Prepišimo xdp-simple.bpf.c kako slijedi (ostali redovi nisu promijenjeni):

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. Sastavimo ga i pogledajmo 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 redovima 0-7 upisujemo niz running on CPU%un, a zatim na retku 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 su tri, a ne dva? Jer bpf_printkovo je makro omotač oko pravog pomagača bpf_trace_printk, koji treba proslijediti veličinu niza formata.

Dodajmo sada nekoliko redaka u xdp-simple.ctako da se naš program povezuje sa sučeljem lo i stvarno je 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 s mrežnim sučeljima. Čvrsto smo kodirali broj sučelja lo, što je uvijek 1. Funkciju pokrećemo dva puta da prvo odvojimo stari program ako je bio pripojen. Imajte na umu 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 jer je povezan s izvorom događaja. Nakon uspješnog preuzimanja i povezivanja, program će se pokrenuti za svaki mrežni paket koji stigne lo.

Skinimo program i pogledajmo sučelje 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 sučelju lo. Poslat ćemo par paketa na 127.0.0.1 (zahtjev + odgovor):

$ ping -c1 localhost

a sada pogledajmo sadržaj debug virtualne datoteke /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đen na CPU0 - naš prvi punopravni besmisleni BPF program je radio!

Vrijedi napomenuti da bpf_printk Nije uzalud to što piše u debug datoteku: ovo nije najuspješniji pomoćnik za upotrebu u proizvodnji, ali naš je cilj bio pokazati nešto jednostavno.

Pristup kartama iz BPF programa

Primjer: korištenje karte iz programa BPF

U prethodnim odjeljcima smo naučili kako kreirati i koristiti karte iz korisničkog prostora, a sada pogledajmo dio jezgre. Počnimo, kao i obično, s primjerom. Napišimo ponovno 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 takav niz definirali kao u64 woo[8]). U programu "xdp/simple" dobivamo trenutni broj procesora u varijablu key a zatim pomoću pomoćne funkcije bpf_map_lookup_element dobivamo 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

Provjerimo je li spojena 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, glavno je da program radi i da razumijemo kako pristupiti kartama iz BPF programa - koristeći хелперов bpf_mp_*.

Mistični indeks

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

val = bpf_map_lookup_elem(&woo, &key);

kako izgleda pomoćna funkcija

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

ali prolazimo pored pokazivača &woo na neimenovanu strukturu struct { ... }...

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

i sadržano je u premještanjima:

$ 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 kartu (linija 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]
...

Stoga možemo zaključiti da je u trenutku pokretanja našeg programa za učitavanje poveznica na &woo je zamijenjen nečim s knjižnicom 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

To vidimo libbpf stvorio kartu woo a zatim preuzeli naš program simple. Pogledajmo pobliže kako učitavamo program:

  • poziv xdp_simple_bpf__open_and_load iz datoteke xdp-simple.skel.h
  • koji uzrokuje xdp_simple_bpf__load iz datoteke xdp-simple.skel.h
  • koji uzrokuje bpf_object__load_skeleton iz datoteke libbpf/src/libbpf.c
  • koji uzrokuje bpf_object__load_xattr od libbpf/src/libbpf.c

Posljednja funkcija će, između ostalog, pozvati bpf_object__create_maps, koji stvara ili otvara postojeće karte, pretvarajući ih u deskriptore datoteka. (Ovdje vidimo BPF_MAP_CREATE u izlazu strace.) Zatim se poziva funkcija bpf_object__relocate a ona je ta koja nas zanima, jer se sjećamo što smo vidjeli woo u tablici preseljenja. Istražujući ga, na kraju se nađemo u funkciji bpf_program__relocate, koji bavi se premještanjem karata:

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

Stoga slijedimo 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, a prvi IMM u deskriptor datoteke naše karte i, ako je jednak, na primjer, 0xdeadbeef, tada ćemo kao rezultat dobiti uputu

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

Ovo je način na koji se informacije karte prenose u određeni učitani BPF program. U ovom slučaju, karta se može izraditi pomoću BPF_MAP_CREATE, a otvorio ID koristeći BPF_MAP_GET_FD_BY_ID.

Ukupno, kada se koristi libbpf algoritam je sljedeći:

  • tijekom kompilacije, zapisi se stvaraju u tablici premještanja za veze na karte
  • libbpf otvara knjigu ELF objekata, pronalazi sve korištene mape i stvara deskriptore datoteka za njih
  • deskriptori datoteka se učitavaju u kernel kao dio instrukcija 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 odvesti do svetinje svih svetih - kernel/bpf/verifier.c, gdje funkcija s karakterističnim nazivom 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;

(puni kod se može pronaći по ссылке). Dakle, možemo proširiti naš algoritam:

  • prilikom učitavanja programa verifikator provjerava ispravnost korištenja karte i upisuje adresu odgovarajuće strukture struct bpf_map

Prilikom preuzimanja ELF binarne datoteke pomoću libbpf Ima još puno toga, ali o tome ćemo raspravljati u drugim člancima.

Učitavanje programa i mapa bez libbpf-a

Kao što je obećano, ovdje je primjer za čitatelje koji žele znati kako stvoriti i učitati program koji koristi karte, bez pomoći libbpf. Ovo može biti korisno kada radite u okruženju za koje ne možete graditi ovisnosti, ili štedjeti svaki bit, ili pisati program kao ply, koji generira BPF binarni kod u hodu.

Da bismo lakše pratili logiku, prepisat ćemo naš primjer za ove potrebe xdp-simple. Cjeloviti i malo prošireni kod programa o kojem se govori u ovom primjeru može se pronaći ovdje suština.

Logika naše aplikacije je sljedeća:

  • izraditi kartu tipa BPF_MAP_TYPE_ARRAY pomoću naredbe BPF_MAP_CREATE,
  • stvoriti program koji koristi ovu kartu,
  • spojite program na sučelje lo,

što se prevodi na 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);
}

Ovdje map_create stvara mapu na isti način kao što smo učinili u prvom primjeru o pozivu sustava bpf - “kernel, molim te napravi mi novu mapu u obliku niza od 8 elemenata poput __u64 i vrati mi 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 se također 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));
}

Zeznuti dio prog_load je definicija našeg BPF programa kao niza struktura struct bpf_insn insns[]. Ali budući da 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 uputa u obliku struktura poput struct bpf_insn (savjet: uzmite deponij odozgo, ponovno pročitajte odjeljak s uputama, otvorite linux/bpf.h и linux/bpf_common.h i pokušati utvrditi 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 - nađite map_fd.

U našem programu ostao je još jedan neobjavljen dio - xdp_attach. Nažalost, programi poput XDP-a ne mogu se povezati pomoću sistemskog poziva bpf. Ljudi koji su stvorili BPF i XDP bili su iz mrežne Linux zajednice, što znači da su koristili onaj koji im je najpoznatiji (ali ne za normalan ljudi) sučelje za interakciju s jezgrom: netlink utičnice, vidi također RFC3549. Najjednostavniji način implementacije xdp_attach kopira kod iz libbpf, naime, iz datoteke netlink.c, što smo i učinili, malo skrativši:

Dobrodošli u svijet netlink utičnica

Otvorite vrstu 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, ovdje je naša funkcija koja otvara utičnicu i šalje joj 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 +++

Pogledajmo je li se naš program povezao s 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 kartu:

$ 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, sve radi. Imajte na umu, usput, da je naša karta ponovno prikazana u obliku bajtova. To je zbog činjenice da, za razliku od libbpf nismo učitali informacije o vrsti (BTF). Ali o tome ćemo više sljedeći put.

Razvojni alati

U ovom ćemo odjeljku pogledati minimalni alat za razvojne programere BPF-a.

Općenito govoreći, ne trebate ništa posebno za razvoj BPF programa - BPF radi na bilo kojoj pristojnoj distribucijskoj jezgri, a programi su izgrađeni korištenjem 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 iz 2019., tada ćete morati kompajlirati

  • llvm/clang
  • pahole
  • svoju jezgru
  • bpftool

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

llvm/zveket

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

$ 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 je li sve ispravno spojeno:

$ ./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 sastavljanje clang uzeto od mene iz bpf_devel_QA.)

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

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

(Ovo se može dodati .bashrc ili u zasebnu datoteku. Osobno dodajem ovakve stvari u ~/bin/activate-llvm.sh a kad je potrebno to i činim . activate-llvm.sh.)

Pahole i BTF

Korisnost pahole koristi se prilikom izgradnje kernela za stvaranje informacija o ispravljanju pogrešaka u BTF formatu. U ovom članku nećemo ulaziti u detalje o pojedinostima BTF tehnologije, osim činjenice da je praktična i da je želimo koristiti. Dakle, ako ćete graditi svoj kernel, prvo napravite pahole (bez pahole nećete moći izgraditi kernel s 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

Kerneli za eksperimentiranje s BPF-om

Kada istražujem mogućnosti BPF-a, želim sastaviti vlastitu jezgru. Ovo, općenito govoreći, nije potrebno, budući da ćete moći kompajlirati i učitati BPF programe na distribucijskom kernelu, međutim, posjedovanje vlastitog kernela omogućuje vam korištenje najnovijih BPF značajki, koje će se pojaviti u vašoj distribuciji u najboljem slučaju za nekoliko mjeseci , ili, kao u slučaju nekih alata za otklanjanje pogrešaka, neće uopće biti pakirani u doglednoj budućnosti. Također, njegova vlastita jezgra čini da je važno eksperimentirati s kodom.

Za izgradnju kernela potrebna vam je, prvo, sama jezgra, a zatim konfiguracijska datoteka kernela. Za eksperimentiranje s BPF-om možemo koristiti uobičajeni vanilija kernel ili jedan od razvojnih kernela. Povijesno gledano, razvoj BPF-a odvija se unutar Linux mrežne zajednice i stoga sve promjene prije ili kasnije prolaze kroz Davida Millera, Linux mrežnog održavatelja. Ovisno o njihovoj prirodi - izmjene ili nove značajke - mrežne promjene spadaju u jednu od dvije jezgre - net ili net-next. Promjene za BPF raspodjeljuju se na isti način između bpf и bpf-next, koji se zatim objedinjuju u net i net-next. Za više detalja pogledajte bpf_devel_QA и netdev-FAQ. Dakle, odaberite kernel na temelju vašeg ukusa i potreba stabilnosti sustava na kojem testirate (*-next kerneli su najnestabilniji od navedenih).

Izvan je opsega ovog članka govoriti o tome kako upravljati konfiguracijskim datotekama jezgre - pretpostavlja se da ili već znate kako to učiniti, ili spreman za učenje na svome. Međutim, sljedeće upute trebale bi biti više-manje dovoljne da vam daju radni sustav s omogućenim BPF-om.

Preuzmite jedan od gore navedenih kernela:

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

Izgradite minimalnu radnu konfiguraciju jezgre:

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

Omogući BPF opcije u datoteci .config po vlastitom izboru (najvjerojatnije CONFIG_BPF već će biti omogućen budući da ga koristi systemd). Ovdje je popis 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

Zatim možemo jednostavno sastaviti i instalirati module i kernel (usput, možete sastaviti kernel pomoću novosastavljenog clangdodavanjem CC=clang):

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

i ponovno pokrenite s novim kernelom (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 bit će uslužni program bpftool, koji se isporučuje kao dio Linux kernela. Napisali su ga i održavaju BPF programeri za BPF programere i može se koristiti za upravljanje svim vrstama BPF objekata - učitavanje programa, stvaranje i uređivanje karata, istraživanje života BPF ekosustava itd. Može se pronaći dokumentacija u obliku izvornih kodova za stranice priručnika u jezgri ili već sastavljeno, na mreži.

U vrijeme pisanja ovog teksta bpftool dolazi spreman samo za RHEL, Fedora i Ubuntu (pogledajte, na primjer, ova nit, koji priča nedovršenu priču o pakiranju bpftool u Debianu). Ali ako ste već izgradili svoj kernel, onda izgradite bpftool lako kao 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  ]

$

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

Skupljati bpftool najbolje je koristiti ovo drugo clang, sastavljen na gore opisani način, te provjerite je li pravilno sastavljen - pomoću npr. naredbe

$ 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 značajke omogućene u vašem kernelu.

Usput, prethodna naredba može se pokrenuti kao

# bpftool f p k

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

Zaključak

BPF vam omogućuje da potkujete buhu kako biste učinkovito izmjerili i promijenili funkcionalnost jezgre u hodu. Sustav se pokazao vrlo uspješnim, u najboljim tradicijama UNIX-a: jednostavan mehanizam koji vam omogućuje (re)programiranje kernela omogućio je velikom broju ljudi i organizacija da eksperimentiraju. I, iako su eksperimenti, kao i razvoj same BPF infrastrukture, daleko od završetka, sustav već ima stabilan ABI koji vam omogućuje izgradnju pouzdane i, što je najvažnije, učinkovite poslovne logike.

Želio bih napomenuti da je, po mom mišljenju, tehnologija postala toliko popularna jer, s jedne strane, može za reprodukciju (arhitektura stroja može se razumjeti više-manje u jednoj večeri), a s druge strane, riješiti probleme koji se nisu mogli (lijepo) riješiti prije njegove pojave. Ove dvije komponente zajedno tjeraju ljude na eksperimentiranje i sanjarenje, što dovodi do pojave sve više i više inovativnih rješenja.

Ovaj članak, iako nije osobito kratak, samo je uvod u svijet BPF-a i ne opisuje "napredne" značajke i važne dijelove arhitekture. Plan za dalje 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 napisati prave BPF aplikacije koristeći programe za praćenje kernela kao primjer, onda je vrijeme za dublji tečaj o BPF arhitekturi, nakon čega slijede primjeri BPF umrežavanja i sigurnosnih aplikacija.

Prethodni članci u ovoj seriji

  1. BPF za najmlađe, nulti dio: klasični BPF

Linkovi

  1. Referentni vodič za BPF i XDP — dokumentacija o BPF-u od ciliuma, točnije od Daniela Borkmana, jednog od kreatora i održavatelja BPF-a. Ovo je jedan od prvih ozbiljnijih opisa, koji se od ostalih razlikuje po tome što Danijel točno zna o čemu piše i tu nema grešaka. Konkretno, ovaj dokument opisuje kako raditi s BPF programima tipa XDP i TC pomoću dobro poznatog uslužnog programa ip iz paketa iproute2.

  2. Dokumentacija/umrežavanje/filter.txt — izvorna datoteka s dokumentacijom za klasični, a potom prošireni BPF. Dobro štivo ako želite proniknuti u asemblerski jezik i tehničke detalje arhitekture.

  3. Blog o BPF-u s Facebooka. Ažurira se rijetko, ali prikladno, kao što tamo pišu Alexei Starovoitov (autor eBPF-a) i Andrii Nakryiko - (održavatelj) libbpf).

  4. Tajne bpftoola. Zabavna twitter nit Quentina Monneta s primjerima i tajnama korištenja bpftoola.

  5. Zaronite u BPF: popis materijala za čitanje. Ogroman (i još uvijek održavan) popis poveznica na BPF dokumentaciju od Quentina Monneta.

Izvor: www.habr.com

Dodajte komentar