Pradžioje buvo technologija ir ji vadinosi BPF. Pažiūrėjome į ją , Senasis Testamentas, šios serijos straipsnis. 2013 m. Aleksejaus Starovoitovo ir Danielio Borkmano pastangomis jis buvo sukurtas ir įtrauktas į pagrindinę Linux Patobulinta versija, optimizuota šiuolaikiniams 64 bitų kompiuteriams. Ši nauja technologija trumpai buvo vadinama vidiniu BPF, vėliau pervadinta į išplėstinį BPF, o dabar, po kelerių metų, visi ją vadina tiesiog BPF.
Iš esmės BPF leidžia branduolio erdvėje vykdyti savavališką vartotojo pateiktą kodą. Linux Ir naujoji architektūra pasirodė esanti tokia sėkminga, kad mums reikėtų dar keliolikos straipsnių, kad aprašytume visas jos programas. (Vienintelis dalykas, kurio kūrėjai nesugebėjo padaryti, kaip matote toliau pateiktoje efektyvumo diagramoje, buvo sukurti tinkamą logotipą.)
Šiame straipsnyje aprašoma BPF virtualios mašinos struktūra, branduolio sąsajos darbui su BPF, kūrimo įrankiai, taip pat trumpai, labai trumpai apžvelgiamos esamos galimybės, t.y. viskas, ko mums prireiks ateityje gilesniam BPF praktinio pritaikymo tyrimui.
Straipsnio santrauka
Pirmiausia pažvelgsime į BPF architektūrą iš paukščio skrydžio ir apibūdinsime pagrindinius komponentus.
Jau turėdami idėją apie visą architektūrą, apibūdinsime BPF virtualios mašinos struktūrą.
Šioje dalyje atidžiau pažvelgsime į BPF objektų – programų ir žemėlapių – gyvavimo ciklą.
Turėdami tam tikrą supratimą apie sistemą, pagaliau pažvelgsime į tai, kaip sukurti ir valdyti objektus iš vartotojo erdvės naudojant specialų sistemos iškvietimą. bpf(2).
Žinoma, galite rašyti programas naudodami sistemos skambutį. Bet tai sunku. Realesniam scenarijui branduoliniai programuotojai sukūrė biblioteką libbpf. Sukursime pagrindinį BPF programos skeletą, kurį naudosime tolesniuose pavyzdžiuose.
Čia sužinosime, kaip BPF programos gali pasiekti branduolio pagalbinės funkcijos – įrankį, kuris kartu su žemėlapiais iš esmės praplečia naujojo BPF galimybes lyginant su klasikiniu.
Šiuo metu mes žinosime pakankamai, kad tiksliai suprastume, kaip galime kurti programas, kurios naudoja žemėlapius. Ir netgi greitai žvilgtelkime į puikų ir galingą tikrintuvą.
Pagalbos skyrius, kaip surinkti reikalingas programas ir branduolį eksperimentams.
Straipsnio pabaigoje tie, kurie perskaitė iki šiol, tolesniuose straipsniuose ras motyvuojančius žodžius ir trumpą aprašymą, kas nutiks. Taip pat išvardinsime nemažai savarankiško mokymosi nuorodų tiems, kurie neturi noro ar galimybių laukti tęsinio.
Įvadas į BPF architektūrą
Prieš pradėdami svarstyti BPF architektūrą, paskutinį kartą (oi) pakalbėsime apie tai , kuris buvo sukurtas kaip atsakas į RISC mašinų atsiradimą ir išsprendė efektyvaus paketų filtravimo problemą. Architektūra pasirodė tokia sėkminga, kad, gimusi veržliame devintajame dešimtmetyje Berkeley UNIX, ji buvo perkelta į daugumą esamų operacinių sistemų, išgyveno iki beprotiško dvidešimtojo dešimtmečio ir vis dar randa naujų programų.
Naujasis BPF buvo sukurtas kaip atsakas į 64 bitų mašinų, debesų paslaugų paplitimą ir padidėjusį SDN kūrimo įrankių poreikį (Sįranga -dpatikslintas ntinklai). Pagrindinių tinklų inžinierių sukurtas naujasis BPF, skirtas patobulintam klasikinio BPF pakaitalui, vos po šešių mėnesių buvo pritaikytas sudėtingai sekimo užduočiai. Linux sistemas, o dabar, praėjus šešeriems metams po jų atsiradimo, mums reikėtų visiškai naujo straipsnio, kad tik būtų išvardyti skirtingi programų tipai.
JUOKINGOS NUOTRAUKOS
Iš esmės BPF yra smėlio dėžės virtuali mašina, kuri leidžia paleisti „savavališką“ kodą branduolio erdvėje nepakenkiant saugumui. BPF programos sukuriamos vartotojo erdvėje, įkeliamos į branduolį ir prijungiamos prie kokio nors įvykio šaltinio. Įvykis gali būti, pavyzdžiui, paketo pristatymas į tinklo sąsają, kai kurios branduolio funkcijos paleidimas ir pan. Paketo atveju BPF programa turės prieigą prie paketo duomenų ir metaduomenų (skaitymui ir, galbūt, rašymui, priklausomai nuo programos tipo, kai vykdoma branduolio funkcija, argumentai). funkcija, įskaitant nuorodas į branduolio atmintį ir kt.
Pažvelkime į šį procesą atidžiau. Pirmiausia pakalbėkime apie pirmąjį skirtumą nuo klasikinio BPF, kurio programos buvo parašytos surinkime. Naujojoje versijoje architektūra buvo išplėsta taip, kad programas būtų galima rašyti aukšto lygio kalbomis, pirmiausia, žinoma, C. Tam buvo sukurtas llvm backend, kuris leidžia generuoti baitinį kodą BPF architektūrai.

BPF architektūra iš dalies buvo sukurta taip, kad efektyviai veiktų šiuolaikinėse mašinose. Kad tai veiktų praktiškai, BPF baito kodas, įkeltas į branduolį, yra išverstas į vietinį kodą naudojant komponentą, vadinamą JIT kompiliatoriumi (Just In Time). Toliau, jei prisimenate, klasikiniame BPF programa buvo įkelta į branduolį ir prijungta prie įvykio šaltinio atomiškai - vieno sistemos iškvietimo kontekste. Naujoje architektūroje tai vyksta dviem etapais – pirmiausia kodas įkeliamas į branduolį naudojant sistemos iškvietimą bpf(2)o vėliau, naudojant kitus mechanizmus, kurie skiriasi priklausomai nuo programos tipo, programa prisijungia prie įvykio šaltinio.
Čia skaitytojui gali kilti klausimas: ar tai buvo įmanoma? Kaip garantuojamas tokio kodo vykdymo saugumas? Vykdymo saugumą mums garantuoja BPF programų įkėlimo etapas, vadinamas verifikatoriumi (anglų kalba šis etapas vadinamas verifikatoriumi ir toliau vartosiu anglišką žodį):

Verifier yra statinis analizatorius, užtikrinantis, kad programa nesutrikdys normalaus branduolio veikimo. Tai, beje, nereiškia, kad programa negali trukdyti sistemos darbui – BPF programos, priklausomai nuo tipo, gali skaityti ir perrašyti branduolio atminties dalis, grąžinti funkcijų reikšmes, apkarpyti, pridėti, perrašyti. ir netgi persiųsti tinklo paketus. Verifier garantuoja, kad paleista BPF programa nesugadins branduolio ir programa, kuri pagal taisykles turi rašymo prieigą, pavyzdžiui, išeinančio paketo duomenis, negalės perrašyti branduolio atminties už paketo ribų. Mes apžvelgsime tikrintuvą šiek tiek išsamiau atitinkamame skyriuje, kai susipažinsime su visais kitais BPF komponentais.
Taigi, ko mes išmokome iki šiol? Vartotojas rašo programą C kalba, įkelia ją į branduolį, naudodamas sistemos iškvietimą bpf(2), kur jį patikrina tikrintuvas ir išverčia į vietinį baitų kodą. Tada tas pats ar kitas vartotojas prijungia programą prie įvykio šaltinio ir ji pradeda vykdyti. Atskirti įkrovą ir ryšį būtina dėl kelių priežasčių. Pirma, tikrinimo priemonės paleidimas yra gana brangus ir kelis kartus atsisiųsdami tą pačią programą eikvojame kompiuterio laiką. Antra, kaip tiksliai prijungiama programa, priklauso nuo jos tipo, o viena „universali“ sąsaja, sukurta prieš metus, gali netikti naujo tipo programoms. (Nors dabar, kai architektūra tampa vis brandesnė, yra idėja suvienodinti šią sąsają lygiu libbpf.)
Dėmesingas skaitytojas gali pastebėti, kad nuotraukos dar nebaigtos. Iš tiesų, visa tai, kas išdėstyta pirmiau, nepaaiškina, kodėl BPF iš esmės keičia vaizdą, palyginti su klasikiniu BPF. Dvi naujovės, kurios žymiai išplečia taikymo sritį, yra galimybė naudoti bendrinamą atmintį ir branduolio pagalbinės funkcijos. BPF bendroji atmintis įgyvendinama naudojant vadinamuosius žemėlapius – bendras duomenų struktūras su konkrečia API. Tikriausiai jie gavo šį pavadinimą, nes pirmasis pasirodęs žemėlapio tipas buvo maišos lentelė. Tada pasirodė masyvai, vietinės (per-CPU) maišos lentelės ir vietiniai masyvai, paieškos medžiai, žemėlapiai su nuorodomis į BPF programas ir daug daugiau. Mums dabar įdomu tai, kad BPF programos dabar turi galimybę išlaikyti būseną tarp skambučių ir dalytis ja su kitomis programomis bei vartotojo erdve.
Žemėlapiai pasiekiami iš vartotojo procesų naudojant sistemos skambutį bpf(2), ir iš BPF programų, veikiančių branduolyje, naudojant pagalbines funkcijas. Be to, pagalbininkai yra ne tik norint dirbti su žemėlapiais, bet ir pasiekti kitas branduolio galimybes. Pavyzdžiui, BPF programos gali naudoti pagalbines funkcijas, kad peradresuotų paketus į kitas sąsajas, generuotų tobulus įvykius, pasiektų branduolio struktūras ir pan.

Apibendrinant galima pasakyti, kad BPF suteikia galimybę į branduolio erdvę įkelti savavališką, t.y. tikrintojo patikrintą vartotojo kodą. Šis kodas gali išsaugoti būseną tarp skambučių ir keistis duomenimis su vartotojo erdve, taip pat turi prieigą prie branduolio posistemių, kurias leidžia tokio tipo programos.
Tai jau panašu į branduolio modulių teikiamas galimybes, su kuriomis palyginus BPF turi tam tikrų privalumų (žinoma, galima palyginti tik panašias programas, pavyzdžiui, sistemos sekimą – su BPF negalima rašyti savavališkos tvarkyklės). Galite atkreipti dėmesį į žemesnę įėjimo slenkstį (kai kurios komunalinės paslaugos, kurios naudoja BPF, nereikalauja, kad vartotojas turėtų branduolio programavimo įgūdžių ar apskritai programavimo įgūdžių), vykdymo saugą (pakelkite ranką į komentarus tiems, kurie rašydami nepažeidė sistemos arba testavimo moduliai), atomiškumas – perkraunant modulius būna prastovų, o BPF posistemis užtikrina, kad jokie įvykiai nepraleistų (teisybės dėlei tai galioja ne visų tipų BPF programoms).
Dėl tokių galimybių BPF tampa universaliu branduolio išplėtimo įrankiu, o tai patvirtina ir praktika: į BPF pridedama vis daugiau naujų tipų programų, vis daugiau didelių kompanijų naudoja BPF kovos serveriuose 24 × 7, vis daugiau ir daugiau. startuoliai kuria savo verslą remdamiesi sprendimais, kuriais remiasi BPF. BPF naudojamas visur: apsaugant nuo DDoS atakų, kuriant SDN (pavyzdžiui, diegiant tinklus kubernetes), kaip pagrindinis sistemos sekimo įrankis ir statistikos rinkėjas, įsibrovimų aptikimo sistemose ir smėlio dėžės sistemose ir kt.
Baigkime apžvalginę straipsnio dalį čia ir pažvelkime į virtualią mašiną ir BPF ekosistemą išsamiau.
Nukrypimas: komunalinės paslaugos
Kad galėtumėte paleisti pavyzdžius tolesniuose skyriuose, jums gali prireikti kelių paslaugų, bent llvm/clang su bpf palaikymu ir bpftool... Skyriuje Galite perskaityti instrukcijas, kaip surinkti komunalines paslaugas, taip pat savo branduolį. Šis skyrius pateikiamas žemiau, kad nesutrikdytų mūsų pristatymo harmonijos.
BPF virtualios mašinos registrai ir instrukcijų sistema
BPF architektūra ir komandų sistema buvo kuriama atsižvelgiant į tai, kad programos bus parašytos C kalba ir, įkėlus į branduolį, išverstos į gimtąjį kodą. Todėl registrų skaičius ir komandų rinkinys buvo pasirinktas atsižvelgiant į šiuolaikinių mašinų galimybių sankirtą matematine prasme. Be to, programoms buvo taikomi įvairūs apribojimai, pavyzdžiui, dar visai neseniai nebuvo galima rašyti kilpų ir paprogramių, o instrukcijų skaičius buvo ribojamas iki 4096 (dabar privilegijuotos programos gali įkelti iki milijono instrukcijų).
BPF turi vienuolika vartotojui prieinamų 64 bitų registrų r0-r10 ir programų skaitiklis. Registruotis r10 yra rėmelio žymeklis ir yra tik skaitomas. Programos turi prieigą prie 512 baitų krūvos vykdymo metu ir neribotą kiekį bendrinamos atminties žemėlapių pavidalu.
BPF programoms leidžiama vykdyti tam tikrą programos tipo branduolio pagalbinių elementų rinkinį, o pastaruoju metu – ir įprastas funkcijas. Kiekviena iškviesta funkcija gali užimti iki penkių argumentų, perduodamų registruose r1-r5, o grąžinama vertė perduodama r0. Garantuojama, kad grįžus iš funkcijos registrų turinys r6-r9 nepasikeis.
Efektyviam programų vertimui, registrai r0-r11 visos palaikomos architektūros yra unikaliai susietos su realiais registrais, atsižvelgiant į dabartinės architektūros ABI ypatybes. Pavyzdžiui, už x86_64 registrai r1-r5, naudojami funkcijų parametrams perduoti, rodomi rdi, rsi, rdx, rcx, r8, kurie naudojami parametrams perduoti funkcijoms x86_64. Pavyzdžiui, kodas kairėje verčiamas į kodą dešinėje taip:
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 0x0000000000001ee8Registruotis r0 taip pat naudojamas programos vykdymo rezultatui grąžinti ir registre r1 programa perduodama rodyklė į kontekstą – priklausomai nuo programos tipo, tai gali būti, pavyzdžiui, struktūra (XDP) arba struktūra (skirtingoms tinklo programoms) arba struktūra (skirtingų tipų sekimo programoms) ir kt.
Taigi, mes turėjome registrų rinkinį, branduolio pagalbininkus, krūvą, konteksto žymeklį ir bendrą atmintį žemėlapių pavidalu. Ne todėl, kad visa tai kelionėje būtina, bet...
Tęskime aprašymą ir pakalbėkime apie komandų sistemą dirbant su šiais objektais. Visi () BPF instrukcijos turi fiksuotą 64 bitų dydį. Jei pažvelgsite į vieną instrukciją 64 bitų „Big Endian“ įrenginyje, pamatysite
![]()
Čia Code - tai yra instrukcijos kodavimas, Dst/Src yra atitinkamai imtuvo ir šaltinio kodavimas, Off - 16 bitų pasirašyta įtrauka ir Imm yra 32 bitų ženklu pažymėtas sveikasis skaičius, naudojamas kai kuriose instrukcijose (panašus į cBPF konstantą K). Kodavimas Code turi vieną iš dviejų tipų:

Instrukcijų klasės 0, 1, 2, 3 apibrėžia komandas darbui su atmintimi. Jie , BPF_LD, BPF_LDX, BPF_ST, BPF_STX, atitinkamai. 4, 7 klasės (BPF_ALU, BPF_ALU64) sudaro ALU instrukcijų rinkinį. 5, 6 klasės (BPF_JMP, BPF_JMP32) yra šuolio instrukcijos.
Tolesnis BPF instrukcijų sistemos tyrimo planas yra toks: užuot kruopščiai surašę visas instrukcijas ir jų parametrus, šiame skyriuje pažvelgsime į porą pavyzdžių ir iš jų paaiškės, kaip instrukcijos iš tikrųjų veikia ir kaip tai padaryti. rankiniu būdu išardykite bet kokį BPF dvejetainį failą. Norėdami konsoliduoti medžiagą vėliau straipsnyje, taip pat susitiksime su atskiromis instrukcijomis skyriuose apie tikrintuvą, JIT kompiliatorių, klasikinio BPF vertimą, taip pat studijuodami žemėlapius, iškvietimo funkcijas ir kt.
Kalbėdami apie individualias instrukcijas, remsimės pagrindiniais failais и , kurie apibrėžia BPF instrukcijų skaitmeninius kodus. Studijuodami architektūrą savarankiškai ir (arba) analizuodami dvejetainius elementus, semantiką galite rasti šiuose šaltiniuose, surūšiuotus sudėtingumo tvarka: , , ir, žinoma, šaltinio koduose Linux — tikrintojas, JIT, BPF interpretatorius.
Pavyzdys: BPF išardymas galvoje
Pažiūrėkime į pavyzdį, kuriame mes sudarome programą readelf-example.c ir pažiūrėkite į gautą dvejetainį. Atskleisime originalų turinį readelf-example.c žemiau, atkūrę jo logiką iš dvejetainių kodų:
$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Hex dump of section '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................Pirmas išvesties stulpelis readelf yra įtrauka, todėl mūsų programą sudaro keturios komandos:
Code Dst Src Off Imm
b7 0 0 0000 01000000
15 0 1 0100 00000000
b7 0 0 0000 02000000
95 0 0 0000 00000000Komandų kodai yra vienodi b7, 15, b7 и 95. Prisiminkite, kad mažiausiai reikšmingi trys bitai yra instrukcijų klasė. Mūsų atveju visų instrukcijų ketvirtasis bitas yra tuščias, todėl komandų klasės yra atitinkamai 7, 5, 7, 5 BPF_ALU64, o 5 yra BPF_JMP. Abiejų klasių nurodymų formatas yra tas pats (žr. aukščiau) ir mes galime perrašyti savo programą taip (tuo pačiu metu likusius stulpelius perrašysime žmogaus pavidalu):
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 0Operacija b klasė ALU64 - yra . Ji priskiria reikšmę paskirties registrui. Jei bitas nustatytas s (šaltinis), tada reikšmė paimama iš šaltinio registro, o jei, kaip mūsų atveju, ji nenustatyta, tada reikšmė paimama iš lauko Imm. Taigi pirmoje ir trečioje instrukcijose atliekame operaciją r0 = Imm. Be to, JMP 1 klasės operacija yra (peršokti, jei lygus). Mūsų atveju, nuo bit S yra nulis, jis lygina šaltinio registro reikšmę su lauku Imm. Jei reikšmės sutampa, įvyksta perėjimas PC + OffKur PC, kaip įprasta, yra kitos instrukcijos adresas. Galiausiai, JMP Class 9 Operation yra . Ši instrukcija baigia programą ir grįžta į branduolį r0. Pridėkime prie lentelės naują stulpelį:
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 exitGalime tai perrašyti patogesne forma:
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exitJei prisiminsime, kas yra registre r1 programa perduodama rodyklė į kontekstą iš branduolio ir registre r0 reikšmė grąžinama branduoliui, tada matome, kad jei konteksto rodyklė yra nulis, tai grąžiname 1, o kitu atveju - 2. Pažiūrėkime į šaltinį, ar mes teisūs:
$ cat readelf-example.c
int foo(void *ctx)
{
return ctx ? 2 : 1;
}Taip, tai beprasmė programa, tačiau ji paverčiama tik keturiomis paprastomis instrukcijomis.
Išimties pavyzdys: 16 baitų instrukcija
Anksčiau minėjome, kad kai kurios instrukcijos užima daugiau nei 64 bitus. Tai taikoma, pavyzdžiui, instrukcijoms lddw (Kodas = 0x18 = | | ) — į registrą įkelkite dvigubą žodį iš laukų Imm. Tai yra faktas Imm turi 32 dydį, o dvigubas žodis yra 64 bitai, todėl 64 bitų tiesioginės reikšmės įkėlimas į registrą vienoje 64 bitų komandoje neveiks. Norėdami tai padaryti, naudojamos dvi gretimos instrukcijos, skirtos antrajai 64 bitų reikšmės daliai saugoti lauke Imm. Pavyzdys:
$ 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 ........Dvejetainėje programoje yra tik dvi instrukcijos:
Binary Disassm
18000000 ddccbbaa 00000000 44332211 r0 = Imm[0]|Imm[1]
95000000 00000000 exitMes vėl susitiksime su instrukcijomis lddw, kai kalbame apie perkėlimus ir darbą su žemėlapiais.
Pavyzdys: BPF išardymas naudojant standartinius įrankius
Taigi, mes išmokome skaityti BPF dvejetainius kodus ir, jei reikia, esame pasirengę išanalizuoti bet kokią instrukciją. Tačiau verta pasakyti, kad praktiškai patogiau ir greičiau išardyti programas naudojant standartinius įrankius, pavyzdžiui:
$ 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 exitBPF objektų gyvavimo ciklas, bpffs failų sistema
(Pirmiausia sužinojau kai kurias šiame poskyryje aprašytas detales iš Aleksejus Starovoitovas .)
BPF objektai – programos ir žemėlapiai – kuriami iš vartotojo erdvės naudojant komandas BPF_PROG_LOAD и BPF_MAP_CREATE sistemos skambutis bpf(2), apie tai, kaip tai vyksta, pakalbėsime kitame skyriuje. Taip sukuriamos branduolio duomenų struktūros ir kiekvienai iš jų refcount (nuorodų skaičius) nustatomas į vieną, o vartotojui grąžinamas failo deskriptorius, nurodantis objektą. Uždarius rankeną refcount objektas sumažinamas vienu, o jam pasiekus nulį, objektas sunaikinamas.
Jei programa naudoja žemėlapius, tada refcount šie žemėlapiai įkėlus programą padidinami vienu, t.y. jų failų aprašai gali būti uždaryti iš vartotojo proceso ir vis tiek refcount netaps nuliu:

Sėkmingai įkėlus programą, dažniausiai ją prijungiame prie kokio nors įvykių generatoriaus. Pavyzdžiui, galime įdėti jį į tinklo sąsają, kad galėtume apdoroti gaunamus paketus arba prijungti jį prie kai kurių tracepoint šerdyje. Šiuo metu nuorodų skaitiklis taip pat padidės vienu ir galėsime uždaryti failo aprašą kroviklio programoje.
Kas nutiks, jei dabar išjungsime įkrovos įkroviklį? Tai priklauso nuo įvykių generatoriaus (kabliuko) tipo. Visi tinklo kabliukai egzistuos baigus krautuvą, tai yra vadinamieji pasauliniai kabliukai. Ir, pavyzdžiui, sekimo programos bus išleistos pasibaigus jas sukūrusiam procesui (todėl vadinamos vietinėmis, iš „vietinės į procesą“). Techniškai vietiniai kabliukai visada turi atitinkamą failo deskriptorių vartotojo erdvėje ir todėl užsidaro, kai procesas uždaromas, tačiau globalūs kabliukai ne. Tolesniame paveikslėlyje, naudodamas raudonus kryžius, bandau parodyti, kaip kroviklio programos nutraukimas įtakoja objektų eksploatavimo laiką vietinių ir globalių kabliukų atveju.

Kodėl yra skirtumas tarp vietinių ir pasaulinių kabliukų? Kai kurių tipų tinklo programų paleidimas prasmingas be vartotojo erdvės, pavyzdžiui, įsivaizduokite DDoS apsaugą – įkrovos įkroviklis parašo taisykles ir prijungia BPF programą prie tinklo sąsajos, po kurios įkrovos įkroviklis gali eiti ir nusižudyti. Kita vertus, įsivaizduokite derinimo sekimo programą, kurią ant kelių surašėte per dešimt minučių – kai ji bus baigta, norėtumėte, kad sistemoje neliktų šiukšlių, o vietiniai kabliukai tai užtikrins.
Kita vertus, įsivaizduokite, kad norite prisijungti prie sekimo taško branduolyje ir rinkti statistiką per daugelį metų. Tokiu atveju norėtųsi užpildyti vartotojo dalį ir karts nuo karto grįžti prie statistikos. Šią galimybę suteikia bpf failų sistema. Tai tik atmintyje esanti pseudofailų sistema, leidžianti kurti failus, kurie nurodo BPF objektus ir taip padidina refcount objektų. Po to krautuvas gali išeiti, o jo sukurti objektai išliks gyvi.

Failų kūrimas bpffs, kuriuose nurodomi BPF objektai, vadinamas „prisegimu“ (kaip ir šioje frazėje: „procesas gali prisegti BPF programą arba žemėlapį“). Failų objektų kūrimas BPF objektams prasmingas ne tik dėl vietinių objektų eksploatavimo pratęsimo, bet ir dėl globalių objektų tinkamumo naudoti – grįžtant prie pavyzdžio su pasauline DDoS apsaugos programa, norime turėti galimybę ateiti ir pasižiūrėti statistiką. karts nuo karto.
Paprastai įmontuojama BPF failų sistema /sys/fs/bpf, bet jis taip pat gali būti montuojamas vietoje, pavyzdžiui, taip:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpointFailų sistemos pavadinimai sukuriami naudojant komandą BPF_OBJ_PIN BPF sistemos skambutis. Norėdami iliustruoti, paimkime programą, sukompiliuokite ją, įkelkime ir prisegkime bpffs. Mūsų programa nedaro nieko naudingo, mes tik pateikiame kodą, kad galėtumėte atkurti pavyzdį:
$ cat test.c
__attribute__((section("xdp"), used))
int test(void *ctx)
{
return 0;
}
char _license[] __attribute__((section("license"), used)) = "GPL";Sukompiliuokime šią programą ir sukurkime vietinę failų sistemos kopiją bpffs:
$ clang -target bpf -c test.c -o test.o
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpointDabar atsisiųskite mūsų programą naudodami įrankį bpftool ir pažiūrėkite į lydinčius sistemos skambučius bpf(2) (kai kurios nesusijusios eilutės pašalintos iš strace išvesties):
$ 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Čia mes įkėlėme programą naudodami BPF_PROG_LOAD, gavo failo aprašą iš branduolio 3 ir naudojant komandą BPF_OBJ_PIN prisegė šį failo aprašą kaip failą "bpf-mountpoint/test". Po to įkrovos programa bpftool baigė darbą, bet mūsų programa liko branduolyje, nors nepridėjome jos prie jokios tinklo sąsajos:
$ 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 4096BFailo objektą galime ištrinti įprastai unlink(2) ir po to atitinkama programa bus ištrinta:
$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directoryObjektų ištrynimas
Kalbant apie objektų ištrynimą, būtina paaiškinti, kad atjungus programą nuo kablio (įvykių generatoriaus), joks naujas įvykis nesukels jos paleidimo, tačiau visi esami programos egzemplioriai bus baigti įprasta tvarka. .
Kai kurių tipų BPF programos leidžia pakeisti programą skrydžio metu, t.y. suteikti sekos atomiškumą replace = detach old program, attach new program. Tokiu atveju visi aktyvūs senosios programos versijos egzemplioriai baigs savo darbą, o iš naujosios programos bus sukurti nauji įvykių tvarkytojai, o „atomiškumas“ čia reiškia, kad nebus praleistas nei vienas įvykis.
Programų prijungimas prie renginių šaltinių
Šiame straipsnyje atskirai neaprašysime programų prijungimo prie įvykių šaltinių, nes tikslinga tai tirti konkretaus tipo programos kontekste. cm. žemiau, kuriame parodome, kaip prijungiamos tokios programos kaip XDP.
Objektų manipuliavimas naudojant bpf sistemos iškvietimą
BPF programos
Visi BPF objektai sukuriami ir valdomi iš vartotojo erdvės naudojant sistemos iškvietimą bpf, turintis tokį prototipą:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);Štai komanda cmd yra viena iš tipo reikšmių , attr — konkrečios programos parametrų rodyklė ir size — objekto dydis pagal rodyklę, t.y. paprastai tai sizeof(*attr). 5.8 branduolyje sistemos skambutis bpf palaiko 34 skirtingas komandas ir union bpf_attr užima 200 eilučių. Tačiau mūsų neturėtų tai gąsdinti, nes su komandomis ir parametrais susipažinsime per kelis straipsnius.
Pradėkime nuo komandos BPF_PROG_LOAD, kuri kuria BPF programas – paima BPF instrukcijų rinkinį ir įkelia jį į branduolį. Įkėlimo momentu paleidžiamas tikrintuvas, o po to JIT kompiliatorius ir sėkmingai įvykdžius programos failo aprašas grąžinamas vartotojui. Kas jam nutiks toliau, matėme ankstesniame skyriuje .
Dabar parašysime pasirinktinę programą, kuri įkels paprastą BPF programą, bet pirmiausia turime nuspręsti, kokią programą norime įkelti – turėsime pasirinkti ir šio tipo rėmuose parašykite programą, kuri išlaikys tikrinimo testą. Tačiau, kad procesas neapsunkintų, čia yra paruoštas sprendimas: paimsime tokią programą kaip BPF_PROG_TYPE_XDP, kuris grąžins vertę XDP_PASS (praleisti visus paketus). BPF surinkime tai atrodo labai paprasta:
r0 = 2
exitPo to, kai nusprendėme kad mes įkelsime, galime pasakyti, kaip tai padarysime:
#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();
}Įdomūs įvykiai programoje prasideda nuo masyvo apibrėžimo insns - mūsų BPF programa mašininiu kodu. Tokiu atveju kiekviena BPF programos instrukcija yra supakuota į struktūrą . Pirmasis elementas insns atitinka instrukcijas r0 = 2, Antras - exit.
Atsitraukti. Branduolys apibrėžia patogesnes makrokomandas mašinų kodams rašyti ir branduolio antraštės failui naudoti tools/include/linux/filter.h galėtume parašyti
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
BPF_EXIT_INSN()
};Bet kadangi rašyti BPF programas vietiniu kodu reikia tik norint rašyti testus branduolyje ir straipsnius apie BPF, tai šių makrokomandų nebuvimas kūrėjo gyvenimo tikrai neapsunkina.
Apibrėžę BPF programą, pereiname prie jos įkėlimo į branduolį. Mūsų minimalistinis parametrų rinkinys attr apima programos tipą, instrukcijų rinkinį ir skaičių, reikalingą licenciją ir pavadinimą "woo", kurį naudojame norėdami rasti savo programą sistemoje po atsisiuntimo. Programa, kaip žadėta, įkeliama į sistemą naudojant sistemos iškvietimą bpf.
Programos pabaigoje patenkame į begalinę kilpą, kuri imituoja naudingą apkrovą. Be jo, programa bus nužudyta branduolio, kai bus uždarytas failo aprašas, kurį mums grąžino sistemos skambutis bpf, ir mes to nepamatysime sistemoje.
Na, mes pasiruošę bandymams. Surinkime ir paleisime programą pagal straceNorėdami patikrinti, ar viskas veikia taip, kaip turėtų:
$ 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(Viskas gerai, bpf(2) grąžino mums rankeną 3 ir mes su jais įėjome į begalinį ratą pause(). Pabandykime surasti savo programą sistemoje. Norėdami tai padaryti, eisime į kitą terminalą ir naudosime įrankį 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)Matome, kad sistemoje yra įkelta programa woo kurio pasaulinis ID yra 390 ir šiuo metu vyksta simple-prog yra atidarytas failo aprašas, nurodantis į programą (ir jei simple-prog tada baigs darbą woo išnyks). Kaip ir tikėtasi, programa woo BPF architektūroje užima 16 baitų – dvi instrukcijas – dvejetainių kodų, bet gimtojoje formoje (x86_64) tai jau 40 baitų. Pažvelkime į mūsų programą originalia forma:
# bpftool prog dump xlated id 390
0: (b7) r0 = 2
1: (95) exitjokių staigmenų. Dabar pažvelkime į JIT kompiliatoriaus sugeneruotą kodą:
# 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: retqnėra labai efektyvus exit(2), bet tiesą sakant, mūsų programa per paprasta, o nebanalioms programoms, žinoma, reikia JIT kompiliatoriaus pridėto prologo ir epilogo.
žemėlapiai
BPF programos gali naudoti struktūrines atminties sritis, kurios yra prieinamos ir kitoms BPF programoms, ir programoms vartotojo erdvėje. Šie objektai vadinami žemėlapiais ir šiame skyriuje parodysime, kaip jais manipuliuoti naudojant sistemos skambutį bpf.
Iš karto pasakykime, kad žemėlapių galimybės neapsiriboja tik prieiga prie bendros atminties. Yra specialios paskirties žemėlapių, kuriuose yra, pavyzdžiui, nuorodų į BPF programas arba nuorodas į tinklo sąsajas, žemėlapių, skirtų darbui su tobulais įvykiais ir kt. Apie juos čia nekalbėsime, kad nesupainiotume skaitytojo. Be to, mes ignoruojame sinchronizavimo problemas, nes tai nėra svarbu mūsų pavyzdžiams. Visą galimų žemėlapių tipų sąrašą galite rasti , o šioje dalyje kaip pavyzdį paimsime istoriškai pirmąjį tipą – maišos lentelę BPF_MAP_TYPE_HASH.
Jei sukurtumėte maišos lentelę, tarkime, C++, sakytumėte unordered_map<int,long> woo, kuris rusiškai reiškia „man reikia stalo woo neribotas dydis, kurio raktai yra tipo int, o vertės yra tipas long“ Norėdami sukurti BPF maišos lentelę, turime daryti beveik tą patį, išskyrus tai, kad turime nurodyti maksimalų lentelės dydį, o užuot nurodyę raktų ir reikšmių tipus, turime nurodyti jų dydžius baitais . Norėdami sukurti žemėlapius, naudokite komandą BPF_MAP_CREATE sistemos skambutis bpf. Pažiūrėkime į daugmaž minimalią programą, kuri sukuria žemėlapį. Po ankstesnės programos, kuri įkelia BPF programas, ši programa jums turėtų atrodyti paprasta:
$ 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();
}Čia apibrėžiame parametrų rinkinį attr, kuriame sakome „Man reikia maišos lentelės su raktais ir dydžio reikšmėmis sizeof(int), kuriame galiu įdėti daugiausia keturis elementus. Kurdami BPF žemėlapius galite nurodyti kitus parametrus, pvz., taip pat kaip ir pavyzdyje su programa, objekto pavadinimą nurodėme kaip "woo".
Sukompiliuokime ir paleiskime 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(Štai sistemos skambutis bpf(2) grąžino mums deskriptoriaus žemėlapio numerį 3 ir tada programa, kaip ir tikėtasi, laukia tolesnių nurodymų sistemos iškvietime pause(2).
Dabar išsiųkime savo programą į foną arba atidarykime kitą terminalą ir pažiūrėkime į savo objektą naudodami įrankį bpftool (savo žemėlapį nuo kitų galime atskirti pagal pavadinimą):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...Skaičius 114 yra pasaulinis mūsų objekto ID. Bet kuri sistemoje esanti programa gali naudoti šį ID, kad atidarytų esamą žemėlapį naudojant komandą BPF_MAP_GET_FD_BY_ID sistemos skambutis bpf.
Dabar galime žaisti su maišos lentele. Pažvelkime į jo turinį:
$ sudo bpftool map dump id 114
Found 0 elementsTuščias. Suteikime jai vertę hash[1] = 1:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0Dar kartą pažiūrėkime į lentelę:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 elementSveika! Mums pavyko pridėti vieną elementą. Atminkite, kad norėdami tai padaryti, turime dirbti baitų lygiu, nes bptftool nežino, kokio tipo reikšmės yra maišos lentelėje. (Šias žinias jai galima perduoti naudojant BTF, bet daugiau apie tai dabar.)
Kaip tiksliai bpftool skaito ir prideda elementus? Pažvelkime po gaubtu:
$ 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 ENOENTPirmiausia atidarėme žemėlapį pagal jo visuotinį ID naudodami komandą BPF_MAP_GET_FD_BY_ID и bpf(2) grąžino mums 3 aprašą Toliau naudojant komandą BPF_MAP_GET_NEXT_KEY pirmą raktą lentelėje radome praėję NULL kaip žymeklį į „ankstesnį“ klavišą. Jei turime raktą, galime tai padaryti BPF_MAP_LOOKUP_ELEMkuris grąžina žymeklio reikšmę value. Kitas žingsnis – mes bandome rasti kitą elementą pervesdami žymeklį į dabartinį raktą, tačiau mūsų lentelėje yra tik vienas elementas ir komanda BPF_MAP_GET_NEXT_KEY grįžta ENOENT.
Gerai, pakeiskime reikšmę 1 raktu, tarkime, kad mūsų verslo logika reikalauja registracijos 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) = 0Kaip ir tikėtasi, tai labai paprasta: komanda BPF_MAP_GET_FD_BY_ID atidaro mūsų žemėlapį pagal ID ir komandą BPF_MAP_UPDATE_ELEM perrašo elementą.
Taigi, sukūrę maišos lentelę iš vienos programos, jos turinį galime skaityti ir rašyti iš kitos. Atminkite, kad jei mes galėjome tai padaryti iš komandinės eilutės, tai gali padaryti bet kuri kita sistemos programa. Be aukščiau aprašytų komandų, norint dirbti su žemėlapiais iš vartotojo erdvės, :
BPF_MAP_LOOKUP_ELEM: raskite vertę pagal raktąBPF_MAP_UPDATE_ELEM: atnaujinti / sukurti vertęBPF_MAP_DELETE_ELEM: išimkite raktąBPF_MAP_GET_NEXT_KEY: suraskite kitą (arba pirmąjį) klavišąBPF_MAP_GET_NEXT_ID: leidžia peržiūrėti visus esamus žemėlapius, taip tai veikiabpftool mapBPF_MAP_GET_FD_BY_ID: atidarykite esamą žemėlapį pagal jo visuotinį IDBPF_MAP_LOOKUP_AND_DELETE_ELEM: atomiškai atnaujinkite objekto vertę ir grąžinkite senąjąBPF_MAP_FREEZE: padarykite žemėlapį nepakeičiamą vartotojo erdvėje (šios operacijos anuliuoti negalima)BPF_MAP_LOOKUP_BATCH,BPF_MAP_LOOKUP_AND_DELETE_BATCH,BPF_MAP_UPDATE_BATCH,BPF_MAP_DELETE_BATCH: masinės operacijos. Pavyzdžiui,BPF_MAP_LOOKUP_AND_DELETE_BATCH- tai vienintelis patikimas būdas nuskaityti ir iš naujo nustatyti visas reikšmes iš žemėlapio
Ne visos šios komandos veikia su visų tipų žemėlapiais, bet apskritai darbas su kitų tipų žemėlapiais iš vartotojo erdvės atrodo lygiai taip pat, kaip dirbant su maišos lentelėmis.
Tvarkos sumetimais užbaigkime maišos lentelės eksperimentus. Prisiminkite, kad sukūrėme lentelę, kurioje gali būti iki keturių raktų? Pridėkime dar kelis elementus:
$ 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 0Kol kas viskas gerai:
$ 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 elementsPabandykime pridėti dar vieną:
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too longKaip ir tikėjomės, mums nepasisekė. Pažvelkime į klaidą išsamiau:
$ 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 +++Viskas gerai: kaip ir tikėtasi, komanda BPF_MAP_UPDATE_ELEM bando sukurti naują, penktą, raktą, bet sugenda E2BIG.
Taigi, galime kurti ir įkelti BPF programas, taip pat kurti ir valdyti žemėlapius iš vartotojo erdvės. Dabar logiška pažvelgti į tai, kaip galime naudoti žemėlapius iš pačių BPF programų. Apie tai galėtume kalbėti sunkiai įskaitomų programų mašinų makrokode kalba, tačiau iš tikrųjų atėjo laikas parodyti, kaip iš tikrųjų rašomos ir prižiūrimos BPF programos – naudojant libbpf.
(Skaitytojams, kurie nepatenkinti žemo lygio pavyzdžio nebuvimu: išsamiai išanalizuosime programas, kuriose naudojami žemėlapiai ir pagalbinės funkcijos, sukurtos naudojant libbpf ir papasakoti, kas vyksta instrukcijų lygiu. Nepatenkintiems skaitytojams labai daug, pridėjome atitinkamoje straipsnio vietoje.)
BPF programų rašymas naudojant libbpf
BPF programų rašymas naudojant mašininius kodus gali būti įdomus tik pirmą kartą, o tada apima sotumas. Šiuo metu reikia atkreipti dėmesį į llvm, kuri turi BPF architektūros kodui generuoti skirtą užpakalinę programą, taip pat biblioteką libbpf, kuri leidžia rašyti BPF programų vartotojo pusę ir įkelti naudojant BPF programų kodą llvm/clang.
Tiesą sakant, kaip matysime šiame ir tolesniuose straipsniuose, libbpf gana daug dirba be jo (ar panašių įrankių - iproute2, libbcc, libbpf-goir pan.) gyventi neįmanoma. Vienas iš žudikiškų projekto bruožų libbpf yra BPF CO-RE (Compile Once, Run Everywhere) – projektas, leidžiantis rašyti BPF programas, kurios yra nešiojamos iš vieno branduolio į kitą, su galimybe paleisti skirtingose API (pavyzdžiui, kai branduolio struktūra pasikeičia nuo versijos). į versiją). Kad galėtumėte dirbti su CO-RE, jūsų branduolys turi būti sukompiliuotas su BTF palaikymu (kaip tai padaryti aprašome skyriuje . Galite patikrinti, ar jūsų branduolys sukurtas naudojant BTF, ar ne labai paprastai – naudodami šį failą:
$ ls -lh /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 2.6M Jul 29 15:30 /sys/kernel/btf/vmlinuxŠiame faile saugoma informacija apie visus duomenų tipus, naudojamus branduolyje, ir jis naudojamas visuose mūsų pavyzdžiuose naudojant libbpf. Išsamiai apie CO-RE kalbėsime kitame straipsnyje, o šiame – tiesiog susikurkite branduolį CONFIG_DEBUG_INFO_BTF.
biblioteka libbpf gyvena tiesiai kataloge tools/lib/bpf branduolys ir jo kūrimas vykdomas per adresų sąrašą bpf@vger.kernel.org. Tačiau už branduolio ribų esančių programų poreikiams palaikoma atskira saugykla kurioje branduolio biblioteka yra atspindima skaitymo prieigai daugiau ar mažiau tokia, kokia yra.
Šiame skyriuje apžvelgsime, kaip galite sukurti projektą, kuris naudoja libbpf, parašykime kelias (daugiau ar mažiau beprasmias) testavimo programas ir detaliai išanalizuokime, kaip visa tai veikia. Tai leis mums tolesniuose skyriuose lengviau paaiškinti, kaip BPF programos sąveikauja su žemėlapiais, branduolio pagalbininkais, BTF ir kt.
Paprastai projektuose naudojami libbpf Pridėkite „GitHub“ saugyklą kaip „git“ submodulį, padarysime tą patį:
$ 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.Ketina libbpf labai paprasta:
$ 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.pcKitas mūsų planas šioje dalyje yra toks: parašysime BPF programą kaip BPF_PROG_TYPE_XDP, tas pats kaip ir ankstesniame pavyzdyje, bet C versijoje mes jį sudarome naudodami clang, ir parašyti pagalbinę programą, kuri ją įkels į branduolį. Tolesniuose skyriuose išplėsime tiek BPF programos, tiek asistento programos galimybes.
Pavyzdys: visavertės programos kūrimas naudojant libbpf
Pirmiausia naudojame failą /sys/kernel/btf/vmlinux, kuris buvo paminėtas aukščiau, ir sukurkite jo atitikmenį antraštės failo pavidalu:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.hŠiame faile bus saugomos visos mūsų branduolyje esančios duomenų struktūros, pavyzdžiui, IPv4 antraštė apibrėžiama branduolyje:
$ 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;
};Dabar mes parašysime savo BPF programą 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";Nors mūsų programa pasirodė labai paprasta, vis tiek turime atkreipti dėmesį į daugybę smulkmenų. Pirma, pirmasis įtrauktas antraštės failas yra vmlinux.h, kurį ką tik sukūrėme naudodami bpftool btf dump - Dabar mums nereikia diegti branduolio antraštės paketo, kad sužinotume, kaip atrodo branduolio struktūros. Šis antraštės failas gaunamas iš bibliotekos libbpf. Dabar mums jo reikia tik makrokomandai apibrėžti SEC, kuris siunčia simbolį į atitinkamą ELF objekto failo skyrių. Mūsų programa yra skyriuje xdp/simple, kur prieš pasvirąjį brūkšnį apibrėžiame programos tipą BPF – tokia yra naudojama sutartinė libbpf, pagal sekcijos pavadinimą paleidžiant jis pakeis teisingą tipą bpf(2). Pati BPF programa yra C - labai paprasta ir susideda iš vienos eilutės return XDP_PASS. Galiausiai atskiras skyrius "license" yra licencijos pavadinimas.
Savo programą galime kompiliuoti naudodami llvm/clang, versija >= 10.0.0 arba dar geriau, naujesnė (žr. ):
$ 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.oTarp įdomių savybių: nurodome tikslinę architektūrą -target bpf ir kelias į antraštes libbpf, kurį neseniai įdiegėme. Be to, nepamirškite apie -O2, be šios parinkties ateityje galite sulaukti netikėtumų. Pažiūrėkime į savo kodą, ar mums pavyko parašyti norimą programą?
$ llvm-objdump --section=xdp/simple --no-show-raw-insn -D xdp-simple.bpf.o
xdp-simple.bpf.o: file format elf64-bpf
Disassembly of section xdp/simple:
0000000000000000 <simple>:
0: r0 = 2
1: exitTaip, pavyko! Dabar mes turime dvejetainį failą su programa ir norime sukurti programą, kuri ją įkeltų į branduolį. Tam tikslui biblioteka libbpf siūlo dvi parinktis – naudoti žemesnio lygio API arba aukštesnio lygio API. Mes eisime antruoju keliu, nes norime išmokti rašyti, įkelti ir prijungti BPF programas su minimaliomis pastangomis tolesniam jų tyrimui.
Pirmiausia turime sugeneruoti savo programos „skeletą“ iš dvejetainio, naudodami tą patį įrankį bpftool — šveicariškas BPF pasaulio peilis (kurį galima suprasti pažodžiui, nes Danielis Borkmanas, vienas iš BPF kūrėjų ir prižiūrėtojų, yra šveicaras):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.hByloje xdp-simple.skel.h yra dvejetainis mūsų programos kodas ir funkcijos, skirtos valdyti – įkelti, prijungti, ištrinti mūsų objektą. Mūsų paprastu atveju tai atrodo kaip perteklius, bet tai taip pat veikia tuo atveju, kai objekto faile yra daug BPF programų ir žemėlapių ir norint įkelti šį milžinišką ELF tereikia sugeneruoti skeletą ir iškviesti vieną ar dvi funkcijas iš pasirinktinės programos. rašo Dabar judėkime toliau.
Griežtai kalbant, mūsų krautuvo programa yra triviali:
#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);
}Čia struct xdp_simple_bpf apibrėžta faile xdp-simple.skel.h ir aprašo mūsų objekto failą:
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;
};Žemo lygio API pėdsakus galime pamatyti čia: struktūra struct bpf_program *simple и struct bpf_link *simple. Pirmoji struktūra konkrečiai apibūdina mūsų programą, parašyta skyriuje xdp/simple, o antrajame aprašoma, kaip programa prisijungia prie įvykio šaltinio.
Funkcija xdp_simple_bpf__open_and_load, atidaro ELF objektą, jį išanalizuoja, sukuria visas struktūras ir postruktūras (be programos, ELF turi ir kitų skyrių – duomenys, tik skaitomi duomenys, derinimo informacija, licencija ir t.t.), o po to naudojant sistemą įkelia jį į branduolį. skambinti bpf, kurią galime patikrinti sukompiliuodami ir paleisdami programą:
$ clang -O2 -I ./libbpf/src/root/usr/include/ xdp-simple.c -o xdp-simple ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_BTF_LOAD, 0x7ffdb8fd9670, 120) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0xdfd580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 8, 0), prog_flags=0, prog_name="simple", prog_ifindex=0, expected_attach_type=0x25 /* BPF_??? */, ...}, 120) = 4Dabar pažvelkime į mūsų naudojamą programą bpftool. Raskime jos 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)ir dump (naudojame sutrumpintą komandos formą bpftool prog dump xlated):
# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
0: (b7) r0 = 2
1: (95) exitKažkas naujo! Programa išspausdino mūsų C šaltinio failo dalis. Tai padarė biblioteka libbpf, kuris dvejetainėje surado derinimo skyrių, sukompiliavo jį į BTF objektą, įkėlė į branduolį naudojant BPF_BTF_LOAD, o tada įkeliant programą su komanda nurodė gautą failo deskriptorių BPG_PROG_LOAD.
Branduolio pagalbininkai
BPF programos gali vykdyti „išorines“ funkcijas – branduolio pagalbininkus. Šios pagalbinės funkcijos leidžia BPF programoms pasiekti branduolio struktūras, valdyti žemėlapius, taip pat bendrauti su „realiu pasauliu“ – kurti tobulus įvykius, valdyti aparatinę įrangą (pavyzdžiui, peradresavimo paketus) ir kt.
Pavyzdys: bpf_get_smp_processor_id
„Mokymosi pagal pavyzdį“ paradigmos rėmuose panagrinėkime vieną iš pagalbinių funkcijų, bpf_get_smp_processor_id(), faile kernel/bpf/helpers.c. Jis grąžina procesoriaus, kuriame veikia jį iškvietusi BPF programa, numerį. Tačiau mus domina ne tiek jo semantika, kiek tai, kad jo įgyvendinimas apima vieną eilutę:
BPF_CALL_0(bpf_get_smp_processor_id)
{
return smp_processor_id();
}BPF pagalbinės funkcijos apibrėžimai yra panašūs į sistemos iškvietimų apibrėžimus. LinuxPavyzdžiui, čia apibrėžiama funkcija, kuri neturi argumentų. (Funkcija, kuri, tarkime, priima tris argumentus, apibrėžiama naudojant makrokomandą.) BPF_CALL_3. Didžiausias argumentų skaičius yra penki.) Tačiau tai tik pirmoji apibrėžimo dalis. Antroji dalis skirta apibrėžti tipo struktūrą struct bpf_func_proto, kuriame yra pagalbinės funkcijos, kurią supranta tikrintojas, aprašymas:
const struct bpf_func_proto bpf_get_smp_processor_id_proto = {
.func = bpf_get_smp_processor_id,
.gpl_only = false,
.ret_type = RET_INTEGER,
};Pagalbinių funkcijų registravimas
Kad tam tikro tipo BPF programos naudotų šią funkciją, jos turi ją užregistruoti, pavyzdžiui, tipui BPF_PROG_TYPE_XDP funkcija yra apibrėžta branduolyje xdp_func_proto, kuris pagal pagalbinės funkcijos ID nustato, ar XDP palaiko šią funkciją, ar ne. Mūsų funkcija yra :
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;
...
}
}Nauji BPF programų tipai yra „apibrėžti“ faile naudojant makrokomandą BPF_PROG_TYPE. Apibrėžiama kabutėse, nes tai yra loginis apibrėžimas, o C kalbos terminais visos konkrečių konstrukcijų rinkinio apibrėžimas pasitaiko kitose vietose. Visų pirma, byloje kernel/bpf/verifier.c visi apibrėžimai iš failo bpf_types.h yra naudojami kuriant daugybę struktūrų 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
};Tai reiškia, kad kiekvienam BPF programos tipui yra apibrėžta rodyklė į tokio tipo duomenų struktūrą struct bpf_verifier_ops, kuris inicijuojamas su verte _name ## _verifier_opst.y., xdp_verifier_ops už xdp. Структура xdp_verifier_ops faile net/core/filter.c taip:
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,
};Čia matome mums pažįstamą funkciją xdp_func_proto, kuri paleidžia tikrintuvą kiekvieną kartą, kai susiduria su iššūkiu kažkoks funkcijas BPF programoje, žr .
Pažiūrėkime, kaip hipotetinė BPF programa naudoja šią funkciją bpf_get_smp_processor_id. Norėdami tai padaryti, perrašome programą iš ankstesnio skyriaus taip:
#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";Simbolis bpf_get_smp_processor_id в <bpf/bpf_helper_defs.h> bibliotekos libbpf kaip
static u32 (*bpf_get_smp_processor_id)(void) = (void *) 8;tai yra bpf_get_smp_processor_id yra funkcijos rodyklė, kurios reikšmė yra 8, kur 8 yra reikšmė BPF_FUNC_get_smp_processor_id tipo enum bpf_fun_id, kuris mums nurodytas faile vmlinux.h (failas bpf_helper_defs.h branduolyje yra sugeneruotas scenarijaus, todėl „stebuklingi“ skaičiai yra tinkami). Ši funkcija nepriima argumentų ir grąžina tipo reikšmę __u32. Kai paleidžiame jį savo programoje, clang generuoja nurodymą BPF_CALL "tinkama rūšis" Sukompiliuokime programą ir pažiūrėkime į skyrių 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 exitPirmoje eilutėje matome instrukcijas call, parametras IMM kuris lygus 8, ir SRC_REG - nulis. Pagal ABI sutartį, kurią naudoja tikrintojas, tai skambutis į pagalbinę funkciją numeris aštuntas. Kai jis paleidžiamas, logika yra paprasta. Grąžinama vertė iš registro r0 nukopijuota į r1 o 2,3 eilutėse jis konvertuojamas į tipą u32 — išvalomi viršutiniai 32 bitai. 4,5,6,7 eilutėse grąžiname 2 (XDP_PASS) arba 1 (XDP_DROP) priklausomai nuo to, ar pagalbinė funkcija iš 0 eilutės grąžino nulinę ar nulinę reikšmę.
Išbandykime save: įkelkime programą ir pažiūrėkime į išvestį 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) exitGerai, tikrintojas rado tinkamą branduolio pagalbininką.
Pavyzdys: perduoti argumentus ir pagaliau paleisti programą!
Visos vykdymo lygio pagalbinės funkcijos turi prototipą
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)Pagalbinių funkcijų parametrai perduodami registruose r1-r5, o reikšmė grąžinama registre r0. Nėra funkcijų, kurioms reikia daugiau nei penkių argumentų, ir ateityje joms palaikymo nesitikima.
Pažvelkime į naują branduolio pagalbininką ir kaip BPF perduoda parametrus. Perrašykime xdp-simple.bpf.c taip (likusios eilutės nepasikeitė):
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}Mūsų programa išspausdina procesoriaus, kuriame ji veikia, numerį. Sukompiliuokime jį ir pažiūrėkime į 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: exit0-7 eilutėse rašome eilutę running on CPU%un, o tada 8 eilutėje paleidžiame pažįstamą bpf_get_smp_processor_id. 9-12 eilutėse ruošiame pagalbinius argumentus bpf_printk - registrai r1, r2, r3. Kodėl jų yra trys, o ne du? Nes bpf_printk - aplink tikrąjį pagalbininką bpf_trace_printk, kuri turi atitikti formato eilutės dydį.
Dabar pridėkime keletą eilučių xdp-simple.ckad mūsų programa prisijungtų prie sąsajos lo ir tikrai prasidėjo!
$ 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);
}Čia mes naudojame funkciją bpf_set_link_xdp_fd, kuris sujungia XDP tipo BPF programas su tinklo sąsajomis. Mes užkodavome sąsajos numerį lo, kuris visada yra 1. Funkciją paleidžiame du kartus, kad pirmiausia atjungtume seną programą, jei ji buvo pridėta. Atkreipkite dėmesį, kad dabar mums nereikia iššūkio pause arba begalinis ciklas: mūsų įkėlimo programa išeis, bet BPF programa nebus užmušta, nes ji prijungta prie įvykio šaltinio. Po sėkmingo atsisiuntimo ir prisijungimo programa bus paleista kiekvienam atvykstančiam tinklo paketui lo.
Atsisiųskite programą ir pažiūrėkime į sąsają 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 669Programa, kurią atsisiuntėme, turi ID 669 ir tą patį ID matome sąsajoje lo. Atsiųsime porą paketų į 127.0.0.1 (užklausa + atsakymas):
$ ping -c1 localhosto dabar pažiūrėkime į derinimo virtualaus failo turinį /sys/kernel/debug/tracing/trace_pipe, kuriame bpf_printk rašo savo žinutes:
# 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 CPU0Buvo pastebėti du paketai lo ir apdorotas CPU0 – mūsų pirmoji pilnavertė beprasmė BPF programa veikė!
Verta pažymėti, kad bpf_printk Ne veltui jis rašo į derinimo failą: tai nėra pats sėkmingiausias pagalbininkas, naudojamas gamyboje, tačiau mūsų tikslas buvo parodyti kažką paprasto.
Prieiga prie žemėlapių iš BPF programų
Pavyzdys: naudojant žemėlapį iš BPF programos
Ankstesniuose skyriuose išmokome kurti ir naudoti žemėlapius iš vartotojo erdvės, o dabar pažvelkime į branduolio dalį. Pradėkime, kaip įprasta, nuo pavyzdžio. Perrašykime savo programą xdp-simple.bpf.c taip:
#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";Programos pradžioje pridėjome žemėlapio apibrėžimą woo: Tai yra 8 elementų masyvas, kuriame saugomos tokios vertės kaip u64 (C kalboje mes apibrėžtume tokį masyvą kaip u64 woo[8]). Programoje "xdp/simple" dabartinį procesoriaus numerį gauname į kintamąjį key ir tada naudodamiesi pagalbininko funkcija bpf_map_lookup_element gauname rodyklę į atitinkamą masyvo įrašą, kurį padidiname vienu. Išversta į rusų kalbą: skaičiuojame statistiką, kuris CPU apdorojo gaunamus paketus. Pabandykime paleisti 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-simplePatikrinkim, ar ji prisirišo lo ir išsiųsti kelis paketus:
$ 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; doneDabar pažvelkime į masyvo turinį:
$ 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 }
]Beveik visi procesai buvo apdoroti CPU7. Mums tai nėra svarbu, svarbiausia, kad programa veiktų ir mes suprantame, kaip pasiekti žemėlapius iš BPF programų - naudojant .
Mistinis indeksas
Taigi, mes galime pasiekti žemėlapį iš BPF programos naudodami tokius skambučius
val = bpf_map_lookup_elem(&woo, &key);kur atrodo pagalbininko funkcija
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)bet mes praleidžiame rodyklę &woo į neįvardytą struktūrą struct { ... }...
Jei pažvelgsime į programos surinkėją, pamatysime, kad reikšmė &woo iš tikrųjų nėra apibrėžtas (4 eilutė):
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
...ir yra įtrauktas į perkėlimus:
$ 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 wooBet jei pažvelgsime į jau įkeltą programą, pamatysime žymeklį į teisingą žemėlapį (4 eilutė):
$ 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]
...Taigi galime daryti išvadą, kad paleidžiant mūsų krautuvo programą, nuoroda į &woo buvo pakeistas kažkuo su biblioteka libbpf. Pirmiausia pažiūrėsime į išvestį 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) = 5Mes tai matome libbpf sukūrė žemėlapį woo ir tada atsisiuntėte mūsų programą simple. Pažiūrėkime atidžiau, kaip įkeliame programą:
- skambinti
xdp_simple_bpf__open_and_loadiš failoxdp-simple.skel.h - kurios sukelia
xdp_simple_bpf__loadiš failoxdp-simple.skel.h - kurios sukelia
bpf_object__load_skeletoniš failolibbpf/src/libbpf.c - kurios sukelia
bpf_object__load_xattrišlibbpf/src/libbpf.c
Paskutinė funkcija, be kita ko, bus iškviesta bpf_object__create_maps, kuri sukuria arba atidaro esamus žemėlapius, paversdama juos failų aprašais. (Štai kur mes matome BPF_MAP_CREATE išvestyje strace.) Toliau iškviečiama funkcija bpf_object__relocate ir ji mus domina, nes prisimename tai, ką matėme woo perkėlimo lentelėje. Ją tyrinėdami galiausiai atsiduriame funkcijoje bpf_program__relocate, kuris :
case RELO_LD64:
insn[0].src_reg = BPF_PSEUDO_MAP_FD;
insn[0].imm = obj->maps[relo->map_idx].fd;
break;Taigi mes laikomės savo nurodymų
18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 llir pakeiskite jame esantį šaltinio registrą į BPF_PSEUDO_MAP_FD, o pirmasis IMM į mūsų žemėlapio failo aprašą ir, jei jis lygus, pvz. 0xdeadbeef, tada mes gausime nurodymą
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 llTaip žemėlapio informacija perkeliama į konkrečią įkeltą BPF programą. Tokiu atveju žemėlapį galima sukurti naudojant BPF_MAP_CREATEir atidarytas naudojant ID BPF_MAP_GET_FD_BY_ID.
Iš viso, kai naudojamas libbpf algoritmas yra toks:
- kompiliavimo metu perkėlimo lentelėje sukuriami įrašai nuorodoms į žemėlapius
libbpfatidaro ELF objektų knygą, suranda visus naudotus žemėlapius ir sukuria jiems failų aprašus- failų aprašai įkeliami į branduolį kaip instrukcijos dalis
LD64
Kaip galite įsivaizduoti, laukia dar daugiau, ir mes turėsime pažvelgti į esmę. Laimei, turime supratimą – užsirašėme prasmę BPF_PSEUDO_MAP_FD į šaltinių registrą ir mes galime jį palaidoti, o tai nuves mus į visų šventųjų šventą kernel/bpf/verifier.c, kur funkcija su skiriamuoju pavadinimu pakeičia failo deskriptorių tipo struktūros adresu 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;(visą kodą galima rasti ). Taigi galime išplėsti savo algoritmą:
- įkeldamas programą tikrintojas patikrina, ar teisingai naudojamas žemėlapis ir parašo atitinkamos struktūros adresą
struct bpf_map
Atsisiunčiant ELF dvejetainį failą naudojant libbpf Vyksta daug daugiau, bet mes tai aptarsime kituose straipsniuose.
Įkeliamos programos ir žemėlapiai be libbpf
Kaip buvo žadėta, čia yra pavyzdys skaitytojams, kurie nori žinoti, kaip sukurti ir įkelti programą, kuri naudoja žemėlapius be pagalbos libbpf. Tai gali būti naudinga, kai dirbate aplinkoje, kurioje negalite sukurti priklausomybių, išsaugokite kiekvieną bitą arba rašote tokią programą kaip , kuris generuoja BPF dvejetainį kodą.
Kad būtų lengviau vadovautis logika, šiems tikslams perrašysime savo pavyzdį xdp-simple. Visą ir šiek tiek išplėstą šiame pavyzdyje aptariamos programos kodą rasite čia .
Mūsų programos logika yra tokia:
- sukurti tipo žemėlapį
BPF_MAP_TYPE_ARRAYnaudojant komandąBPF_MAP_CREATE, - sukurti programą, kuri naudoja šį žemėlapį,
- prijunkite programą prie sąsajos
lo,
kuris verčiamas į žmogų kaip
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);
}Čia map_create sukuria žemėlapį taip pat, kaip ir pirmame pavyzdyje apie sistemos iškvietimą bpf - „branduolys, prašau padaryti man naują žemėlapį 8 elementų masyvo pavidalu, pvz., __u64 ir grąžinkite man failo aprašą":
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ą taip pat lengva įkelti:
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));
}Sudėtinga dalis prog_load yra mūsų BPF programos kaip struktūrų masyvo apibrėžimas struct bpf_insn insns[]. Bet kadangi mes naudojame programą, kurią turime C, galime šiek tiek apgauti:
$ 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 exitIš viso turime parašyti 14 instrukcijų tokių struktūrų pavidalu struct bpf_insn (patarimas: paimkite sąvartyną iš viršaus, dar kartą perskaitykite instrukcijų skyrių, atidarykite и ir pabandykite nustatyti struct bpf_insn insns[] savarankiškai):
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
},
};Pratimas tiems, kurie patys to neparašė – raskite map_fd.
Mūsų programoje liko dar viena neatskleista dalis - xdp_attach. Deja, tokių programų kaip XDP negalima prijungti naudojant sistemos skambutį bpfBPF ir XDP sukūrė žmonės iš internetinės bendruomenės. Linux, o tai reiškia, kad jie naudojo tą, kuris jiems buvo labiausiai pažįstamas (bet ne normalus žmonės) sąsaja, skirta sąveikai su branduoliu: , taip pat žr . Paprasčiausias įgyvendinimo būdas xdp_attach kopijuoja kodą iš libbpf, būtent iš failo , ką mes padarėme, šiek tiek sutrumpinę:
Sveiki atvykę į „netlink“ lizdų pasaulį
Atidarykite „netlink“ lizdo tipą 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;
}Iš šio lizdo skaitome:
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;
}Galiausiai, čia yra mūsų funkcija, kuri atidaro lizdą ir siunčia į jį specialų pranešimą, kuriame yra failo aprašas:
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;
}Taigi, viskas paruošta bandymui:
$ 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 +++Pažiūrėkime, ar mūsų programa prijungta prie 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 160Siųskime pingus ir pažiūrėkime į žemėlapį:
$ 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 elementsHurray, viskas veikia. Beje, atkreipkite dėmesį, kad mūsų žemėlapis vėl rodomas baitų pavidalu. Taip yra dėl to, kad, skirtingai nei libbpf neįkėlėme tipo informacijos (BTF). Bet apie tai daugiau pakalbėsime kitą kartą.
Kūrimo įrankiai
Šiame skyriuje apžvelgsime minimalų BPF kūrėjo įrankių rinkinį.
Paprastai kalbant, norint kurti BPF programas nereikia nieko ypatingo – BPF veikia bet kuriame tinkamo platinimo branduolyje, o programos kuriamos naudojant clang, kuris gali būti tiekiamas iš pakuotės. Tačiau dėl to, kad BPF yra kuriamas, branduolys ir įrankiai nuolat keičiasi, jei nenorite nuo 2019 m. rašyti BPF programas senamadiškais metodais, tuomet turėsite kompiliuoti
llvm/clangpahole- jo šerdis
bpftool
(Nuoroda: šis skyrius ir visi straipsnio pavyzdžiai buvo paleisti naudojant Debian 10.)
llvm/clang
BPF yra draugiškas LLVM ir, nors pastaruoju metu BPF programas galima sudaryti naudojant gcc, visa dabartinė plėtra vykdoma LLVM. Todėl pirmiausia sukursime dabartinę versiją clang iš git:
$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86"
-DLLVM_ENABLE_PROJECTS="clang"
-DBUILD_SHARED_LIBS=OFF
-DCMAKE_BUILD_TYPE=Release
-DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$Dabar galime patikrinti, ar viskas susidėjo teisingai:
$ ./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(Surinkimo instrukcijos clang paėmiau iš .)
Mes neįdiegsime ką tik sukurtų programų, o tiesiog pridėsime jas PATH, pavyzdžiui:
export PATH="`pwd`/bin:$PATH"(Tai galima pridėti prie .bashrc arba į atskirą failą. Asmeniškai aš pridedu tokius dalykus ~/bin/activate-llvm.sh ir kai reikia, tai darau . activate-llvm.sh.)
Pahole ir BTF
Naudingumas pahole naudojamas kuriant branduolį, siekiant sukurti derinimo informaciją BTF formatu. Šiame straipsnyje mes nekalbėsime apie BTF technologijos detales, išskyrus tai, kad ji yra patogi ir norime ją naudoti. Taigi, jei ketinate kurti savo branduolį, pirmiausia sukurkite pahole (be pahole negalėsite sukurti branduolio su parinktimi 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/paholeBranduoliai, skirti eksperimentuoti su BPF
Tyrinėdamas BPF galimybes, noriu surinkti savo branduolį. Paprastai tai nėra būtina, nes galėsite kompiliuoti ir įkelti BPF programas į platinimo branduolį, tačiau, turėdami savo branduolį, galėsite naudotis naujausiomis BPF funkcijomis, kurios geriausiu atveju jūsų platinime pasirodys po kelių mėnesių. , arba, kaip ir kai kurių derinimo įrankių atveju, artimiausioje ateityje nebus supakuoti. Be to, dėl savo branduolio svarbu eksperimentuoti su kodu.
Norint sukurti branduolį, pirmiausia reikia paties branduolio ir, antra, branduolio konfigūracijos failo. Norėdami eksperimentuoti su BPF, galime naudoti įprastą branduolys arba vienas iš kūrėjo branduolių. Istoriškai BPF kūrimas vyko tinklo bendruomenėje. Linux ir todėl visi pakeitimai anksčiau ar vėliau patenka per tinklo prižiūrėtoją Davidą Millerį LinuxPriklausomai nuo jų pobūdžio – pataisymai ar naujos funkcijos – tinklo pakeitimai patenka į vieną iš dviejų branduolių: arba . BPF pakeitimai paskirstomi taip pat и , kurios atitinkamai sujungiamos į „net“ ir „net-next“. Norėdami gauti daugiau informacijos, žr и . Taigi rinkitės branduolį pagal savo skonį ir sistemos, kurią bandote, stabilumo poreikius (*-next branduoliai yra nestabiliausi iš išvardytų).
Šiame straipsnyje kalbama apie tai, kaip valdyti branduolio konfigūracijos failus – manoma, kad jūs arba jau žinote, kaip tai padaryti, arba savarankiškai. Tačiau toliau pateiktų instrukcijų turėtų pakakti, kad sistema veiktų su BPF.
Atsisiųskite vieną iš aukščiau pateiktų branduolių:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-nextSukurkite minimalią veikiančią branduolio konfigūraciją:
$ cp /boot/config-`uname -r` .config
$ make localmodconfigĮgalinti BPF parinktis faile .config jūsų pasirinkimas (greičiausiai CONFIG_BPF jau bus įjungta, nes systemd ją naudoja). Čia yra šiame straipsnyje naudojamų branduolio parinkčių sąrašas:
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=yTada mes galime lengvai surinkti ir įdiegti modulius ir branduolį (beje, branduolį galite surinkti naudodami naujai surinktą clangpridedant CC=clang):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make installir paleiskite iš naujo su nauju branduoliu (aš naudoju tam kexec iš pakuotės 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 -ebpftool
Straipsnyje dažniausiai naudojamas įrankis bus naudingumas bpftool, tiekiama kaip branduolio dalis LinuxJį rašo ir prižiūri BPF kūrėjai BPF kūrėjams ir jis gali būti naudojamas visų tipų BPF objektams valdyti – programoms įkelti, žemėlapiams kurti ir modifikuoti, BPF ekosistemai tyrinėti ir kt. Dokumentaciją žinyno puslapių šaltinio kodo formatu galima rasti čia. arba jau sudarytas, .
Šio rašymo metu bpftool yra paruoštas tik RHEL, Fedora ir Ubuntu (žr., pavyzdžiui, , kuriame pasakojama nebaigta pakavimo istorija bpftool в Debian). Bet jei jau surinkote savo branduolį, tuomet surinkite bpftool taip paprasta, kaip kriaušes lukštenti:
$ 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 ]
$(Čia ${linux} – tai jūsų branduolio katalogas.) Įvykdę šias komandas bpftool bus surinkti į katalogą ${linux}/tools/bpf/bpftool ir jį galima pridėti prie kelio (pirmiausia vartotojui root) arba tiesiog nukopijuokite į /usr/local/sbin.
surinkti bpftool geriausia naudoti pastarąjį clang, surinktas taip, kaip aprašyta aukščiau, ir patikrinkite, ar jis surinktas teisingai – naudodami, pavyzdžiui, komandą
$ 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
...kuri parodys, kurios BPF funkcijos įjungtos jūsų branduolyje.
Beje, ankstesnę komandą galima paleisti kaip
# bpftool f p kTai atliekama pagal analogiją su komunalinėmis paslaugomis iš paketo iproute2, kur galime, pavyzdžiui, pasakyti ip a s eth0 vietoj ip addr show dev eth0.
išvada
BPF leidžia apauti blusą, kad būtų galima efektyviai išmatuoti ir skrydžio metu pakeisti šerdies funkcionalumą. Sistema pasirodė labai sėkminga pagal geriausias UNIX tradicijas: paprastas mechanizmas, leidžiantis (per)programuoti branduolį, leido eksperimentuoti daugybei žmonių ir organizacijų. Ir nors eksperimentai, kaip ir pačios BPF infrastruktūros kūrimas, toli gražu nebaigti, sistema jau turi stabilų ABI, leidžiantį sukurti patikimą ir, svarbiausia, efektyvią verslo logiką.
Norėčiau pastebėti, kad, mano nuomone, ši technologija tapo tokia populiari, nes, viena vertus, ji gali žaisti (mašinos architektūrą galima perprasti daugmaž per vieną vakarą), o kita vertus, išspręsti problemas, kurių nepavyko (gražiai) išspręsti iki jos atsiradimo. Šie du komponentai kartu verčia žmones eksperimentuoti ir svajoti, o tai lemia vis naujoviškesnių sprendimų atsiradimą.
Šis straipsnis, nors ir ne itin trumpas, yra tik įvadas į BPF pasaulį ir neaprašo „pažangių“ funkcijų bei svarbių architektūros dalių. Ateities planas yra maždaug toks: kitame straipsnyje bus apžvelgta BPF programų tipai (5.8 branduolyje palaikoma 30 programų tipų), tada pagaliau pažiūrėsime, kaip parašyti tikras BPF programas naudojant branduolio sekimo programas. Pavyzdžiui, atėjo laikas nuodugnesniam BPF architektūros kursui, o vėliau BPF tinklų ir saugos programų pavyzdžiams.
Ankstesni šios serijos straipsniai
Nuorodos
— dokumentai apie BPF iš cilium, o tiksliau Danielio Borkmano, vieno iš BPF kūrėjų ir prižiūrėtojų. Tai vienas pirmųjų rimtų apibūdinimų, kuris nuo kitų skiriasi tuo, kad Danielius puikiai žino apie ką rašo ir klaidų ten nėra. Visų pirma, šiame dokumente aprašoma, kaip dirbti su XDP ir TC tipų BPF programomis naudojant gerai žinomą įrankį.
ipiš pakuotėsiproute2.- originalus failas su dokumentais, skirtas klasikiniam, o vėliau išplėstiniam BPF. Geras skaitymas, jei norite įsigilinti į montažinę kalbą ir technines architektūros detales.
. Jis atnaujinamas retai, bet taikliai, kaip rašo Aleksejus Starovoitovas (eBPF autorius) ir Andrii Nakryiko - (prižiūrėtojas)
libbpf).. Linksma Quentin Monnet Twitter gija su bpftool naudojimo pavyzdžiais ir paslaptimis.
. Milžiniškas (ir vis dar palaikomas) nuorodų į BPF dokumentus iš Quentin Monnet sąrašas.
Šaltinis: www.habr.com
