BPF kõige väiksematele, esimene osa: pikendatud BPF

Alguses oli tehnoloogia ja selle nimi oli BPF. Vaatasime teda eelmine, Selle sarja Vana Testamendi artikkel. 2013. aastal töötati Aleksei Starovoitovi ja Daniel Borkmani jõupingutustega välja selle täiustatud versioon, mis on optimeeritud tänapäevaste 64-bitiste masinate jaoks ja lisati see Linuxi kernelisse. Seda uut tehnoloogiat nimetati lühidalt Internal BPF-iks, seejärel nimetati see ümber Extended BPF-iks ja nüüd, mitme aasta pärast, kutsuvad kõik seda lihtsalt BPF-iks.

Jämedalt öeldes võimaldab BPF Linuxi tuumaruumis käivitada suvalise kasutaja pakutava koodi ja uus arhitektuur osutus nii edukaks, et vajame veel tosinat artiklit kõigi selle rakenduste kirjeldamiseks. (Ainus asi, mida arendajatel hästi ei läinud, nagu näete allolevast jõudluskoodist, oli korraliku logo loomine.)

Selles artiklis kirjeldatakse BPF-i virtuaalmasina ülesehitust, kerneli liideseid BPF-iga töötamiseks, arendustööriistu, aga ka lühikest väga lühikest ülevaadet olemasolevatest võimalustest, st. kõike, mida me tulevikus vajame BPF-i praktiliste rakenduste sügavamaks uurimiseks.
BPF kõige väiksematele, esimene osa: pikendatud BPF

Artikli kokkuvõte

Sissejuhatus BPF-i arhitektuuri. Esiteks vaatame BPF-i arhitektuuri linnulennult ja kirjeldame põhikomponente.

BPF-i virtuaalmasina registrid ja käsusüsteem. Omades juba ettekujutust arhitektuurist kui tervikust, kirjeldame BPF-i virtuaalmasina struktuuri.

BPF-objektide elutsükkel, bpffs-failisüsteem. Selles osas vaatleme lähemalt BPF objektide – programmide ja kaartide – elutsüklit.

Objektide haldamine bpf süsteemikutsega. Kuna süsteem on juba paigas, vaatame lõpuks, kuidas luua ja manipuleerida objekte kasutajaruumist, kasutades spetsiaalset süsteemikutset. bpf(2).

Пишем программы BPF с помощью libbpf. Loomulikult saate programme kirjutada süsteemikõne abil. Aga see on raske. Realistlikuma stsenaariumi jaoks töötasid tuumaprogrammeerijad välja raamatukogu libbpf. Loome põhilise BPF-i rakenduse skeleti, mida kasutame järgmistes näidetes.

Kerneli abistajad. Siit saame teada, kuidas BPF-i programmid pääsevad juurde kerneli abifunktsioonidele – tööriistale, mis koos kaartidega laiendab uue BPF-i võimalusi võrreldes klassikalisega.

Juurdepääs kaartidele BPF programmidest. Selleks hetkeks teame piisavalt, et mõista täpselt, kuidas saame luua kaarte kasutavaid programme. Ja heitkem kasvõi kiire pilk suurepärasesse ja võimsasse kontrollijasse.

Arendustööriistad. Abijaotis selle kohta, kuidas katseteks vajalikke utiliite ja kerneli kokku panna.

Järeldus. Artikli lõpust leiavad need, kes siiamaani lugesid, motiveerivaid sõnu ja lühikirjelduse toimunust järgmistest artiklitest. Toome kirja ka hulga linke iseõppimiseks neile, kel pole soovi ega jaksu jätku oodata.

Sissejuhatus BPF-i arhitektuuri

Enne kui hakkame BPF-i arhitektuuri käsitlema, viitame sellele klassikaline BPF, mis töötati välja vastusena RISC-masinate tulekule ja lahendas tõhusa pakettide filtreerimise probleemi. Arhitektuur osutus nii edukaks, et olles sündinud üheksakümnendatel Berkeley UNIX-is, teisaldati see enamikesse olemasolevatesse operatsioonisüsteemidesse, püsis hullumeelsetes kahekümnendates ja leiab endiselt uusi rakendusi.

Uus BPF töötati välja vastusena 64-bitiste masinate, pilveteenuste üldlevinud levikule ja suurenenud vajadusele SDN-i loomise tööriistade järele (Spõhivara-drafineeritud nvõrgustamine). Kerneli võrguinseneride poolt klassikalise BPF-i täiustatud asendusena välja töötatud uus BPF leidis sõna otseses mõttes kuus kuud hiljem rakendusi Linuxi süsteemide jälgimise keerulises ülesandes ja nüüd, kuus aastat pärast selle ilmumist, vajame tervet järgmist artiklit. loetlege eri tüüpi programme.

Naljakad pildid

BPF on oma olemuselt liivakasti virtuaalmasin, mis võimaldab käivitada "suvalise" koodi tuumaruumis ilma turvalisust ohustamata. BPF-programmid luuakse kasutajaruumis, laaditakse kernelisse ja ühendatakse mõne sündmuse allikaga. Sündmus võib olla näiteks paketi edastamine võrguliidesele, mõne kerneli funktsiooni käivitamine vms. Paketi puhul on BPF-programmil juurdepääs paketi andmetele ja metaandmetele (olenevalt programmi tüübist lugemiseks ja võimalusel ka kirjutamiseks), kerneli funktsiooni käitamise korral funktsioon, sealhulgas viited kerneli mälule jne.

Vaatame seda protsessi lähemalt. Alustuseks räägime esimesest erinevusest klassikalisest BPF-ist, mille programmid on kirjutatud assembleris. Uues versioonis laiendati arhitektuuri nii, et programme sai kirjutada kõrgetasemelistes keeltes, eeskätt muidugi C-s. Selleks töötati välja llvm-i taustaprogramm, mis võimaldab genereerida BPF-i arhitektuuri jaoks baitkoodi.

BPF kõige väiksematele, esimene osa: pikendatud BPF

BPF-i arhitektuur loodi osaliselt nii, et see töötaks tõhusalt kaasaegsetes masinates. Selle praktikas toimimiseks tõlgitakse BPF-i baitkood pärast kernelisse laadimist natiivseks koodiks, kasutades komponenti, mida nimetatakse JIT-kompilaatoriks (Just In Time). Järgmisena, kui mäletate, laaditi klassikalises BPF-is programm kernelisse ja ühendati sündmuse allikaga aatomiliselt - ühe süsteemikutse kontekstis. Uues arhitektuuris toimub see kahes etapis – esiteks laaditakse kood süsteemikutse abil kernelisse bpf(2)ja seejärel, hiljem, muude mehhanismide kaudu, mis sõltuvad programmi tüübist, seostub programm sündmuse allikaga.

Siin võib lugejal tekkida küsimus: kas see oli võimalik? Kuidas on tagatud sellise koodi täitmise ohutus? Täitmise ohutuse tagab meile BPF-i programmide laadimise etapp nimega verifier (inglise keeles nimetatakse seda etappi verifier ja ma jätkan ingliskeelse sõna kasutamist):

BPF kõige väiksematele, esimene osa: pikendatud BPF

Verifier on staatiline analüsaator, mis tagab, et programm ei häiri tuuma normaalset tööd. See, muide, ei tähenda, et programm ei saaks süsteemi tööd segada - BPF-programmid saavad olenevalt tüübist lugeda ja ümber kirjutada kerneli mälu sektsioone, tagastada funktsioonide väärtusi, kärpida, lisada, ümber kirjutada. ja isegi võrgupakette edasi saata. Verifier garanteerib, et BPF-programmi käivitamisel kernel kokku ei jookse ja programm, millel on reeglite järgi kirjutamisõigus, näiteks väljamineva paketi andmetele, ei saa paketivälist tuumamälu üle kirjutada. Vaatleme kontrollijat veidi üksikasjalikumalt vastavas jaotises pärast seda, kui oleme tutvunud kõigi teiste BPF-i komponentidega.

Mida me siis seni õppinud oleme? Kasutaja kirjutab programmi C keeles, laadib selle süsteemikutsega kernelisse bpf(2), kus seda kontrollib kontrollija ja tõlgitakse natiivseks baitkoodiks. Seejärel ühendab sama või mõni teine ​​kasutaja programmi sündmuse allikaga ja see hakkab täitma. Alglaadimise ja ühenduse eraldamine on vajalik mitmel põhjusel. Esiteks on kontrollija käivitamine suhteliselt kallis ja sama programmi mitu korda alla laadides raiskame arvutiaega. Teiseks sõltub see, kuidas programm täpselt ühendatakse, selle tüübist ja üks aasta tagasi välja töötatud “universaalne” liides ei pruugi uut tüüpi programmide jaoks sobida. (Kuigi nüüd, mil arhitektuur on muutumas küpsemaks, on mõte see liides tasemel ühtlustada libbpf.)

Tähelepanelik lugeja võib märgata, et me pole piltidega veel valmis. Tõepoolest, kõik eelnev ei selgita, miks BPF muudab pilti võrreldes klassikalise BPF-iga põhjalikult. Kaks uuendust, mis oluliselt laiendavad rakendusala, on ühismälu ja kerneli abifunktsioonide kasutamise võimalus. BPF-is realiseeritakse ühismälu nn kaartide abil - jagatud andmestruktuurid, millel on kindel API. Tõenäoliselt said nad selle nime seetõttu, et esimene kaart, mis ilmus, oli räsitabel. Seejärel ilmusid massiivid, kohalikud (CPU-põhised) räsitabelid ja kohalikud massiivid, otsingupuud, kaardid, mis sisaldavad viiteid BPF-programmidele ja palju muud. Meie jaoks on praegu huvitav see, et BPF-programmidel on nüüd võimalus kõnede vahel olekut säilitada ja seda teiste programmide ja kasutajaruumiga jagada.

Mapsile pääseb ligi kasutajaprotsessidest süsteemikõne abil bpf(2), ja BPF-i programmidest, mis töötavad kernelis abifunktsioone kasutades. Lisaks on abilised olemas mitte ainult kaartidega töötamiseks, vaid ka muudele kerneli võimalustele juurdepääsuks. Näiteks saavad BPF-programmid kasutada abifunktsioone, et edastada pakette teistele liidestele, genereerida täiuslikke sündmusi, pääseda juurde kerneli struktuuridele ja nii edasi.

BPF kõige väiksematele, esimene osa: pikendatud BPF

Kokkuvõttes annab BPF võimaluse laadida suvalist, st kontrollija poolt testitud kasutajakoodi kerneli ruumi. See kood võib salvestada olekuid kõnede vahel ja vahetada andmeid kasutajaruumiga ning sellel on juurdepääs ka seda tüüpi programmide poolt lubatud tuuma alamsüsteemidele.

See on juba sarnane kerneli moodulite pakutavate võimalustega, millega võrreldes on BPF-il mõned eelised (muidugi saate võrrelda ainult sarnaseid rakendusi, näiteks süsteemi jälgimist - BPF-iga ei saa kirjutada suvalist draiverit). Võite märkida madalamat sisenemisläve (mõned BPF-i kasutavad utiliidid ei nõua kasutajalt kerneli programmeerimisoskusi ega programmeerimisoskusi üldiselt), käitusaegset ohutust (tõstke kommentaarides käsi neile, kes kirjutades süsteemi ei rikkunud või moodulite testimine), aatomilisus – moodulite uuesti laadimisel esineb seisakuid ja BPF-i alamsüsteem tagab, et ükski sündmus ei jääks vahele (aus, et see ei kehti igat tüüpi BPF-programmide puhul).

Selliste võimaluste olemasolu teeb BPF-ist universaalse tööriista kerneli laiendamiseks, mis on ka praktikas kinnitust leidnud: BPF-ile lisatakse üha rohkem uut tüüpi programme, üha rohkem suurettevõtteid kasutab BPF-i võitlusserverites 24 × 7, üha enam. idufirmad ehitavad oma äri üles lahendustele, mis põhinevad BPF-il. BPF-i kasutatakse kõikjal: DDoS-i rünnakute eest kaitsmisel, SDN-i loomisel (näiteks kubernetes võrkude juurutamisel), peamise süsteemijälgimise tööriista ja statistika kogujana, sissetungituvastussüsteemides ja liivakastisüsteemides jne.

Lõpetame siin artikli ülevaateosa ja vaatame virtuaalmasinat ja BPF-i ökosüsteemi lähemalt.

Kõrvalepõike: kommunaalkulud

Järgmistes jaotistes olevate näidete käitamiseks võib vaja minna mitmeid utiliite, vähemalt llvm/clang bpf toega ja bpftool. Jaos Arendustööriistad Saate lugeda utiliitide koostamise juhiseid ja ka oma tuuma. See jaotis on paigutatud allpool, et mitte häirida meie esitluse harmooniat.

BPF virtuaalmasina registrid ja juhiste süsteem

BPF-i arhitektuur ja käsusüsteem töötati välja, võttes arvesse asjaolu, et programmid kirjutatakse C-keeles ja tõlgitakse pärast kernelisse laadimist omakoodi. Seetõttu valiti registrite arvu ja käskude komplekti silmas pidades tänapäevaste masinate võimaluste ristumiskohta matemaatilises mõttes. Lisaks kehtestati programmidele erinevaid piiranguid, näiteks kuni viimase ajani ei saanud kirjutada silmuseid ja alamprogramme ning käskude arv oli piiratud 4096-ga (nüüd saavad privilegeeritud programmid laadida kuni miljon käsku).

BPF-il on üksteist kasutajale ligipääsetavat 64-bitist registrit r0-r10 ja programmiloendur. Registreeri r10 sisaldab kaadrikursorit ja on kirjutuskaitstud. Programmidel on käitusajal juurdepääs 512-baidisele pinule ja piiramatul hulgal ühismälu kaartide kujul.

BPF-programmidel on lubatud käivitada teatud komplekt programmitüüpi kerneli abistajaid ja viimasel ajal ka tavalisi funktsioone. Iga kutsutud funktsioon võib võtta kuni viis argumenti, mis edastatakse registrites r1-r5ja tagastatav väärtus edastatakse r0. Tagatud on, et pärast funktsioonist naasmist registrite sisu r6-r9 Ei muutu.

Programmide tõhusaks tõlkimiseks registrid r0-r11 kõigi toetatud arhitektuuride jaoks on unikaalselt vastendatud reaalsetele registritele, võttes arvesse praeguse arhitektuuri ABI-funktsioone. Näiteks selleks x86_64 registrid r1-r5, mida kasutatakse funktsiooni parameetrite edastamiseks, kuvatakse rdi, rsi, rdx, rcx, r8, mida kasutatakse parameetrite edastamiseks funktsioonidele x86_64. Näiteks vasakpoolne kood tõlgitakse parempoolseks koodiks järgmiselt:

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

Registreeri r0 kasutatakse ka programmi täitmise tulemuse tagastamiseks ja registris r1 programmile antakse konteksti osutaja – olenevalt programmi tüübist võib selleks olla näiteks struktuur struct xdp_md (XDP jaoks) või struktuur struct __sk_buff (erinevate võrguprogrammide jaoks) või struktuur struct pt_regs (erinevat tüüpi jälgimisprogrammide jaoks) jne.

Niisiis, meil oli komplekt registreid, kerneli abistajaid, virn, konteksti osuti ja jagatud mälu kaartide kujul. Mitte, et seda kõike reisil tingimata vaja oleks, aga...

Jätkame kirjeldust ja räägime nende objektidega töötamise käsusüsteemist. Kõik (Peaaegu kõik) BPF-i käskudel on fikseeritud 64-bitine suurus. Kui vaatate ühte juhist 64-bitises Big Endiani masinas, näete

BPF kõige väiksematele, esimene osa: pikendatud BPF

see on Code - see on juhise kodeering, Dst/Src on vastavalt vastuvõtja ja allika kodeeringud, Off - 16-bitise märgiga taane ja Imm on 32-bitine märgiga täisarv, mida kasutatakse mõnes käsus (sarnaselt cBPF konstandiga K). Kodeerimine Code sellel on üks kahest tüübist:

BPF kõige väiksematele, esimene osa: pikendatud BPF

Käsuklassid 0, 1, 2, 3 määratlevad käsud mäluga töötamiseks. Nad nimetatakse, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, vastavalt. Klassid 4, 7 (BPF_ALU, BPF_ALU64) moodustavad ALU juhiste komplekti. Klassid 5, 6 (BPF_JMP, BPF_JMP32) sisaldavad hüppejuhiseid.

Tulevikuplaan BPF-i juhistesüsteemiga tutvumiseks on järgmine: selle asemel, et kõiki juhiseid ja nende parameetreid pedantselt üles loetleda, vaatame selles osas paari näidet ja nendest selgub, kuidas juhised tegelikult töötavad ja kuidas seda teha. võtke käsitsi lahti kõik BPF-i binaarfailid. Materjali hilisemaks artiklis konsolideerimiseks kohtume ka individuaalsete juhistega jaotistes Verifier, JIT-kompilaator, klassikalise BPF-i tõlkimine, samuti kaartide uurimisel, funktsioonide kutsumisel jne.

Kui räägime individuaalsetest juhistest, viitame põhifailidele bpf.h и bpf_common.h, mis määratlevad BPF-i käskude numbrilised koodid. Arhitektuuri iseseisvalt uurides ja/või binaarfaile sõeludes leiate keerukuse järjekorras järjestatud semantika järgmistest allikatest: Mitteametlik eBPF spetsifikatsioon, BPF ja XDP teatmik, juhiste komplekt, Dokumentatsioon/võrk/filter.txt ja loomulikult Linuxi lähtekoodis - kontrollija, JIT, BPF-i tõlk.

Näide: BPF-i lahtivõtmine peas

Vaatame näidet, milles koostame programmi readelf-example.c ja vaadake saadud kahendfaili. Avaldame algse sisu readelf-example.c allpool, pärast seda, kui oleme taastanud selle loogika binaarkoodidest:

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

Väljundi esimene veerg readelf on taane ja meie programm koosneb seega neljast käsust:

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

Käsukoodid on võrdsed b7, 15, b7 и 95. Tuletame meelde, et kõige vähem olulised kolm bitti on käsuklass. Meie puhul on kõigi käskude neljas bitt tühi, seega on käsuklassid vastavalt 7, 5, 7, 5. Klass 7 on BPF_ALU64, ja 5 on BPF_JMP. Mõlema klassi käsuvorming on sama (vt ülalt) ja saame oma programmi ümber kirjutada nii (samal ajal kirjutame ülejäänud veerud ümber inimese kujul):

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

Operatsioon b klass ALU64 - Kas BPF_MOV. See määrab sihtregistrile väärtuse. Kui bitt on seatud s (allikas), siis võetakse väärtus lähteregistrist ja kui nagu meie puhul seda ei määrata, siis võetakse väärtus väljalt Imm. Nii et esimeses ja kolmandas juhises teostame toimingu r0 = Imm. Lisaks on JMP 1. klassi operatsioon BPF_JEQ (hüppa, kui võrdne). Meie puhul alates natuke S on null, võrdleb see lähteregistri väärtust väljaga Imm. Kui väärtused langevad kokku, toimub üleminek PC + OffKus PC, nagu tavaliselt, sisaldab järgmise juhise aadressi. Lõpuks on JMP Class 9 Operation BPF_EXIT. See käsk lõpetab programmi ja naaseb kerneli r0. Lisame oma tabelisse uue veeru:

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

Saame selle mugavamal kujul ümber kirjutada:

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

Kui mäletame, mis registris on r1 programm edastatakse kernelist ja registris kontekstile viitav osuti r0 väärtus tagastatakse kernelile, siis näeme, et kui konteksti osuti on null, siis tagastame 1 ja muul juhul - 2. Kontrollime allikat vaadates, et meil on õigus:

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

Jah, see on mõttetu programm, kuid see tähendab vaid nelja lihtsat juhist.

Erandi näide: 16-baidine käsk

Mainisime varem, et mõned juhised võtavad rohkem kui 64 bitti. See kehtib näiteks juhiste kohta lddw (Kood ​​= 0x18 = BPF_LD | BPF_DW | BPF_IMM) — laadib registrisse väljadelt topeltsõna Imm. Asjaolu on selles Imm on suurus 32 ja topeltsõna on 64 bitti, nii et 64-bitise vahetu väärtuse laadimine registrisse ühe 64-bitise käsuga ei toimi. Selleks kasutatakse kahte kõrvuti asetsevat käsku, et salvestada väljale 64-bitise väärtuse teine ​​osa Imm... Näide:

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

Binaarprogrammis on ainult kaks juhist:

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

Kohtume uuesti koos juhistega lddw, kui räägime ümberpaigutamisest ja tööst kaartidega.

Näide: BPF-i lahtivõtmine standardsete tööriistade abil

Niisiis, oleme õppinud lugema BPF-i binaarkoode ja oleme valmis vajadusel kõiki juhiseid sõeluma. Siiski tasub öelda, et praktikas on mugavam ja kiirem programme lahti võtta standardsete tööriistade abil, näiteks:

$ 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

BPF-objektide elutsükkel, bpffs-failisüsteem

(Mõned selles alapeatükis kirjeldatud üksikasjad õppisin kõigepealt postitusest Aleksei Starovoitov sisse BPF ajaveeb.)

BPF-objekte – programme ja kaarte – luuakse kasutajaruumist käskude abil BPF_PROG_LOAD и BPF_MAP_CREATE süsteemikõne bpf(2), sellest, kuidas see täpselt juhtub, räägime järgmises jaotises. See loob tuuma andmestruktuure ja igaühe jaoks neist refcount (viitearv) on seatud ühele ja kasutajale tagastatakse objektile osutav failideskriptor. Pärast käepideme sulgemist refcount objekti vähendatakse ühe võrra ja kui see jõuab nullini, objekt hävitatakse.

Kui programm kasutab kaarte, siis refcount neid kaarte suurendatakse pärast programmi laadimist ühe võrra, st. nende failideskriptorid saab kasutaja protsessist sulgeda ja ikkagi refcount ei muutu nulliks:

BPF kõige väiksematele, esimene osa: pikendatud BPF

Pärast programmi edukat laadimist ühendame selle tavaliselt mingisuguse sündmuste generaatori külge. Näiteks saame selle panna võrguliidesele, et töödelda sissetulevaid pakette või mõnega ühendada tracepoint tuumas. Siinkohal suureneb ka võrdlusloendur ühe võrra ja saame laaduriprogrammis failideskriptori sulgeda.

Mis juhtub, kui me nüüd alglaaduri sulgeme? See sõltub sündmuste generaatori (konksu) tüübist. Kõik võrgukonksud eksisteerivad pärast laaduri valmimist, need on niinimetatud globaalsed konksud. Ja näiteks jälgimisprogrammid vabastatakse pärast seda, kui need loonud protsess lõpeb (ja seetõttu nimetatakse neid kohalikuks, „kohalikust protsessiks”). Tehniliselt on kohalikel konksidel alati vastav failideskriptor kasutajaruumis ja seetõttu sulguvad need protsessi sulgemisel, globaalsetel konksidel aga mitte. Järgmisel joonisel püüan punaste ristide abil näidata, kuidas laaduriprogrammi lõpetamine lokaalsete ja globaalsete konksude puhul objektide eluiga mõjutab.

BPF kõige väiksematele, esimene osa: pikendatud BPF

Miks tehakse vahet kohalikel ja globaalsetel konksudel? Teatud tüüpi võrguprogrammide käivitamine on mõttekas ilma kasutajaruumita, näiteks kujutage ette DDoS-kaitset - alglaadur kirjutab reeglid ja ühendab BPF-programmi võrguliidesega, misjärel saab alglaadur minna ja ennast tappa. Teisest küljest kujutage ette silumisjälgimise programmi, mille kirjutasite kümne minutiga põlvili – kui see on lõppenud, siis soovite, et süsteemi ei jääks prügi ja kohalikud konksud tagavad selle.

Teisest küljest kujutage ette, et soovite luua ühenduse kerneli jälituspunktiga ja koguda statistikat paljude aastate jooksul. Sel juhul sooviksite kasutaja osa lõpule viia ja aeg-ajalt statistika juurde tagasi pöörduda. Selle võimaluse pakub bpf-failisüsteem. See on ainult mälus olev pseudofailisüsteem, mis võimaldab luua faile, mis viitavad BPF-objektidele ja suurendavad seeläbi refcount objektid. Pärast seda saab laadur väljuda ja selle loodud objektid jäävad ellu.

BPF kõige väiksematele, esimene osa: pikendatud BPF

BPF-objektidele viitavate bpff-failide loomist nimetatakse "kinnitamiseks" (nagu järgmises fraasis: "protsess võib kinnitada BPF-i programmi või kaardi"). Failiobjektide loomine BPF-objektidele on mõttekas mitte ainult lokaalsete objektide eluea pikendamise, vaid ka globaalsete objektide kasutatavuse seisukohalt – globaalse DDoS-kaitseprogrammi näite juurde tagasi tulles tahame, et saaksime tulla statistikat vaatama. aeg-ajalt.

BPF-failisüsteem on tavaliselt sisse ühendatud /sys/fs/bpf, kuid seda saab paigaldada ka kohapeal, näiteks järgmiselt:

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

Failisüsteemide nimed luuakse käsu abil BPF_OBJ_PIN BPF-süsteemi kõne. Illustreerimiseks võtame programmi, kompileerime selle, laadime üles ja kinnitame selle külge bpffs. Meie programm ei tee midagi kasulikku, me esitame ainult koodi, et saaksite näidet reprodutseerida:

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

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

Kompileerime selle programmi ja loome failisüsteemist kohaliku koopia bpffs:

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

Nüüd laadime oma programmi utiliidi abil alla bpftool ja vaadake kaasnevaid süsteemikutseid bpf(2) (mõned ebaolulised read eemaldati strace väljundist):

$ 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

Siin laadisime programmi kasutades BPF_PROG_LOAD, sai tuumalt failideskriptori 3 ja kasutades käsku BPF_OBJ_PIN kinnitas selle failikirjelduse failina "bpf-mountpoint/test". Pärast seda alglaaduri programm bpftool lõpetas töö, kuid meie programm jäi kernelisse, kuigi me ei ühendanud seda ühegi võrguliidese külge:

$ 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

Saame failiobjekti tavapäraselt kustutada unlink(2) ja pärast seda vastav programm kustutatakse:

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

Objektide kustutamine

Objektide kustutamisest rääkides on vaja selgitada, et pärast seda, kui oleme programmi konksust (sündmuste generaatorist) lahti ühendanud, ei käivita ükski uus sündmus selle käivitamist, kuid kõik programmi praegused eksemplarid valmivad tavapärases järjekorras .

Teatud tüüpi BPF programmid võimaldavad programmi käigu pealt välja vahetada, s.t. tagavad järjestuse aatomilisuse replace = detach old program, attach new program. Sel juhul lõpetavad oma töö kõik programmi vana versiooni aktiivsed eksemplarid ning uuest programmist luuakse uued sündmuste käitlejad ning “atomicity” tähendab siin seda, et ükski sündmus ei jää vahele.

Programmide lisamine sündmuste allikatele

Selles artiklis me eraldi ei kirjelda programmide ühendamist sündmuste allikatega, kuna seda on mõttekas uurida konkreetset tüüpi programmi kontekstis. cm. näide allpool, kus näitame, kuidas sellised programmid nagu XDP on ühendatud.

Objektidega manipuleerimine bpf süsteemikutse abil

BPF programmid

Kõik BPF-objektid luuakse ja hallatakse kasutajaruumist süsteemikõne abil bpf, millel on järgmine prototüüp:

#include <linux/bpf.h>

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

Siin on meeskond cmd on üks tüübi väärtustest enum bpf_cmd, attr — osutaja konkreetse programmi parameetritele ja size — objekti suurus vastavalt osutile, s.o. tavaliselt see sizeof(*attr). Kernelis 5.8 süsteemikutse bpf toetab 34 erinevat käsku ja määramine union bpf_attr võtab enda alla 200 rida. Kuid me ei tohiks end sellest hirmutada, sest me tutvume käskude ja parameetritega mitme artikli jooksul.

Alustame meeskonnaga BPF_PROG_LOAD, mis loob BPF-programme – võtab BPF-i juhiste komplekti ja laadib selle kernelisse. Laadimise hetkel käivitatakse kontrollija ning seejärel tagastatakse kasutajale JIT-kompilaator ja pärast edukat täitmist programmifaili deskriptor. Mis temaga edasi saab, nägime eelmises osas BPF-objektide elutsükli kohta.

Nüüd kirjutame kohandatud programmi, mis laadib lihtsa BPF-programmi, kuid kõigepealt peame otsustama, millist programmi tahame laadida - peame valima Tüüp ja selle tüübi raames kirjutada programm, mis läbib verifitseerija testi. Kuid selleks, et protsessi mitte keerulisemaks muuta, on siin valmis lahendus: võtame programmi nagu BPF_PROG_TYPE_XDP, mis tagastab väärtuse XDP_PASS (jätke kõik paketid vahele). BPF-i monteerijas näeb see välja väga lihtne:

r0 = 2
exit

Pärast seda, kui oleme otsustanud et me laadime üles, saame teile öelda, kuidas me seda teeme:

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

Huvitavad sündmused programmis algavad massiivi määratlusega insns - meie BPF-programm masinkoodis. Sel juhul pakitakse iga BPF-programmi käsk struktuuri bpf_insn. Esimene element insns vastab juhistele r0 = 2, teine ​​- exit.

Taganeda. Kernel määratleb mugavamad makrod masinkoodide kirjutamiseks ja tuuma päisefaili kasutamiseks tools/include/linux/filter.h võiksime kirjutada

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

Kuid kuna BPF-programmide kirjutamine natiivses koodis on vajalik ainult tuumas testide ja BPF-i käsitlevate artiklite kirjutamiseks, ei muuda nende makrode puudumine arendaja elu tegelikult keeruliseks.

Pärast BPF-programmi määratlemist jätkame selle kernelisse laadimisega. Meie minimalistlik parameetrite komplekt attr sisaldab programmi tüüpi, komplekti ja juhiste arvu, vajalikku litsentsi ja nime "woo", mida kasutame oma programmi leidmiseks süsteemist pärast allalaadimist. Programm, nagu lubatud, laaditakse süsteemi süsteemikõne abil bpf.

Programmi lõpus jõuame lõpmatusse tsüklisse, mis simuleerib kasulikku koormust. Ilma selleta tapab kernel programmi, kui failideskriptor, mille süsteemikutse meile tagastas, suletakse bpf, ja me ei näe seda süsteemis.

Noh, oleme testimiseks valmis. Paneme programmi kokku ja käivitame selle all straceet kontrollida, kas kõik töötab nii nagu peab:

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

Kõik on korras, bpf(2) tagastas meile käepideme 3 ja me läksime lõpmatusse ahelasse pause(). Proovime oma programmi süsteemist üles leida. Selleks läheme teise terminali ja kasutame utiliiti 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)

Näeme, et süsteemis on laaditud programm woo mille globaalne ID on 390 ja see on praegu pooleli simple-prog seal on avatud faili deskriptor, mis osutab programmile (ja kui simple-prog lõpetan töö siis woo kaob). Nagu oodatud, programm woo võtab BPF-i arhitektuuris 16 baiti - kaks käsku - binaarkoode, kuid natiivsel kujul (x86_64) on see juba 40 baiti. Vaatame oma programmi algsel kujul:

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

ei mingeid üllatusi. Vaatame nüüd JIT-i kompilaatori loodud koodi:

# 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

jaoks pole eriti tõhus exit(2), kuid ausalt öeldes on meie programm liiga lihtne ja mittetriviaalsete programmide jaoks on loomulikult vaja JIT-i kompilaatori lisatud proloogi ja epiloogi.

kaardid

BPF-programmid saavad kasutada struktureeritud mälualasid, mis on kättesaadavad nii teistele BPF-programmidele kui ka kasutajaruumis olevatele programmidele. Neid objekte nimetatakse kaartideks ja selles jaotises näitame, kuidas nendega süsteemikõne abil manipuleerida bpf.

Ütleme kohe ära, et kaartide võimalused ei piirdu ainult juurdepääsuga ühismälule. On eriotstarbelisi kaarte, mis sisaldavad näiteks viiteid BPF-i programmidele või viiteid võrguliidestele, kaarte perf-sündmustega töötamiseks jne. Nendest me siinkohal ei räägi, et lugejat mitte segadusse ajada. Peale selle ignoreerime sünkroonimisprobleeme, kuna see pole meie näidete puhul oluline. Saadaolevate kaarditüüpide täieliku loendi leiate aadressilt <linux/bpf.h>, ja selles osas võtame näitena ajalooliselt esimese tüübi, räsitabeli BPF_MAP_TYPE_HASH.

Kui loote räsitabeli näiteks C++ keeles, siis ütleksite unordered_map<int,long> woo, mis vene keeles tähendab “Mul on vaja lauda woo piiramatu suurus, mille võtmed on tüüpi intja väärtused on tüüp long" BPF-i räsitabeli loomiseks peame tegema peaaegu sama, välja arvatud see, et peame määrama tabeli maksimaalse suuruse ning võtmete ja väärtuste tüüpide määramise asemel peame määrama nende suurused baitides . Kaartide loomiseks kasutage käsku BPF_MAP_CREATE süsteemikõne bpf. Vaatame enam-vähem minimaalset programmi, mis loob kaardi. Pärast eelmist programmi, mis laadib BPF-i programme, peaks see teile tunduma lihtne:

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

Siin määratleme parameetrite komplekti attr, milles ütleme: „Mul on vaja räsitabelit võtmete ja suuruse väärtustega sizeof(int), kuhu saan panna maksimaalselt neli elementi." BPF kaartide loomisel saab määrata muid parameetreid, näiteks samamoodi nagu programmiga näites, määrasime objekti nimeks "woo".

Kompileerime ja käivitame programmi:

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

Siin on süsteemikõne bpf(2) tagastas meile deskriptorkaardi numbri 3 ja seejärel ootab programm ootuspäraselt süsteemikutses edasisi juhiseid pause(2).

Nüüd saadame oma programmi taustale või avame teise terminali ja vaatame utiliidi abil oma objekti bpftool (saame oma kaarti teistest eristada nime järgi):

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

Number 114 on meie objekti globaalne ID. Iga süsteemi programm saab seda ID-d kasutada olemasoleva kaardi avamiseks käsuga BPF_MAP_GET_FD_BY_ID süsteemikõne bpf.

Nüüd saame mängida oma räsilauaga. Vaatame selle sisu:

$ sudo bpftool map dump id 114
Found 0 elements

Tühi. Paneme sellele väärtuse hash[1] = 1:

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

Vaatame uuesti tabelit:

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

Hurraa! Meil õnnestus lisada üks element. Pange tähele, et selleks peame töötama baitide tasemel, kuna bptftool ei tea, mis tüüpi väärtused räsitabelis on. (Neid teadmisi saab talle BTF-i abil üle anda, kuid sellest nüüd lähemalt.)

Kuidas täpselt bpftool elemente loeb ja lisab? Heidame pilgu kapoti alla:

$ 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

Kõigepealt avasime kaardi selle globaalse ID järgi, kasutades käsku BPF_MAP_GET_FD_BY_ID и bpf(2) tagastas meile deskriptori 3. Kasutades edasi käsku BPF_MAP_GET_NEXT_KEY leidsime möödaminnes tabelist esimese võtme NULL kui kursor "eelmise" klahvile. Kui meil on võti, saame hakkama BPF_MAP_LOOKUP_ELEMmis tagastab osutile väärtuse value. Järgmise sammuna proovime leida järgmist elementi, suunates kursori aktiivsele võtmele, kuid meie tabel sisaldab ainult ühte elementi ja käsku BPF_MAP_GET_NEXT_KEY naaseb ENOENT.

Olgu, muudame väärtust võtmega 1, oletame, et meie äriloogika nõuab registreerimist 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

Nagu oodatud, on see väga lihtne: käsk BPF_MAP_GET_FD_BY_ID avab meie kaardi ID ja käsu järgi BPF_MAP_UPDATE_ELEM kirjutab elemendi üle.

Seega saame pärast ühest programmist räsitabeli loomist lugeda ja kirjutada selle sisu teisest programmist. Pange tähele, et kui meil õnnestus seda teha käsurealt, saab seda teha iga teine ​​süsteemis olev programm. Lisaks ülalkirjeldatud käskudele kasutajaruumist kaartidega töötamiseks Järgmised:

  • BPF_MAP_LOOKUP_ELEM: leidke väärtus võtme järgi
  • BPF_MAP_UPDATE_ELEM: värskenda/loo väärtust
  • BPF_MAP_DELETE_ELEM: eemalda võti
  • BPF_MAP_GET_NEXT_KEY: leidke järgmine (või esimene) võti
  • BPF_MAP_GET_NEXT_ID: võimaldab teil läbida kõik olemasolevad kaardid, nii see toimib bpftool map
  • BPF_MAP_GET_FD_BY_ID: olemasoleva kaardi avamine selle globaalse ID järgi
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: värskendab aatomiliselt objekti väärtust ja tagastab vana
  • BPF_MAP_FREEZE: muuta kaart kasutajaruumist muutumatuks (seda toimingut ei saa tagasi võtta)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: massioperatsioonid. Näiteks, BPF_MAP_LOOKUP_AND_DELETE_BATCH - see on ainus usaldusväärne viis kõigi kaardil olevate väärtuste lugemiseks ja lähtestamiseks

Kõik need käsud ei tööta kõigi kaarditüüpide puhul, kuid üldiselt näeb teist tüüpi kaartidega töötamine kasutajaruumist välja täpselt sama, mis räsitabelitega töötamine.

Korra huvides lõpetame oma räsitabeli katsetused. Pea meeles, et lõime tabeli, mis võib sisaldada kuni nelja võtit? Lisame veel mõned elemendid:

$ 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

Siiamaani on kõik korras:

$ 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

Proovime lisada veel ühe:

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

Nagu oodatud, meil ei õnnestunud. Vaatame viga üksikasjalikumalt:

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

Kõik on hästi: nagu oodatud, meeskond BPF_MAP_UPDATE_ELEM üritab luua uut, viiendat võtit, kuid jookseb kokku E2BIG.

Seega saame luua ja laadida BPF-programme, samuti luua ja hallata kaarte kasutajaruumist. Nüüd on loogiline vaadata, kuidas saame kasutada BPF-i programmide endi kaarte. Sellest võiks rääkida raskesti loetavate programmide keeles masinmakrokoodides, kuid tegelikult on kätte jõudnud aeg näidata, kuidas BPF-programme tegelikult kirjutatakse ja hooldatakse – kasutades libbpf.

(Lugejatele, kes pole rahul madala taseme näite puudumisega: analüüsime üksikasjalikult programme, mis kasutavad kaarte ja abifunktsioone, mis on loodud kasutades libbpf ja räägin teile, mis juhiste tasemel toimub. Lugejatele, kes pole rahul väga, lisasime näide artikli sobivas kohas.)

BPF programmide kirjutamine libbpf abil

BPF-programmide kirjutamine masinkoodide abil võib olla huvitav alles esimesel korral ja siis tekib küllastustunne. Sel hetkel peate oma tähelepanu pöörama llvm, millel on taustaprogramm BPF-arhitektuuri koodi genereerimiseks ja ka teek libbpf, mis võimaldab kirjutada BPF-rakenduste kasutajapoolt ja laadida kasutades loodud BPF-programmide koodi llvm/clang.

Tegelikult, nagu näeme selles ja järgmistes artiklites, libbpf teeb üsna palju tööd ilma selleta (või sarnaste tööriistadeta - iproute2, libbcc, libbpf-gojne) on võimatu elada. Üks projekti tapvatest omadustest libbpf on BPF CO-RE (Compile Once, Run Everywhere) – projekt, mis võimaldab kirjutada ühest tuumast teise teisaldatavaid BPF-programme, millel on võimalus töötada erinevatel API-del (näiteks kui kerneli struktuur muutub versioonist versioonini). CO-RE-ga töötamiseks peab teie kernel olema kompileeritud BTF-i toega (seda kirjeldame jaotises Arendustööriistad. Saate kontrollida, kas teie kernel on ehitatud BTF-iga või mitte, väga lihtsalt - järgmise faili olemasolul:

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

See fail salvestab teavet kõigi tuumas kasutatavate andmetüüpide kohta ja seda kasutatakse kõigis meie näidetes libbpf. Räägime CO-RE-st üksikasjalikult järgmises artiklis, kuid selles - lihtsalt looge endale kernel CONFIG_DEBUG_INFO_BTF.

raamatukogu libbpf elab otse kataloogis tools/lib/bpf kernel ja selle arendamine toimub meililisti kaudu [email protected]. Väljaspool kernelit elavate rakenduste vajaduste jaoks peetakse aga eraldi hoidlat https://github.com/libbpf/libbpf milles kerneliteek on lugemisjuurdepääsu jaoks peegeldatud enam-vähem sellisel kujul, nagu see on.

Selles jaotises vaatleme, kuidas saate luua projekti, mis kasutab libbpf, kirjutame mitu (enam-vähem mõttetut) testprogrammi ja analüüsime üksikasjalikult, kuidas see kõik töötab. See võimaldab meil järgmistes jaotistes hõlpsamini selgitada, kuidas BPF-programmid suhtlevad kaartide, kerneli abistajate, BTF-i jne.

Tavaliselt kasutavad projektid libbpf lisage GitHubi hoidla giti alammoodulina, teeme sama:

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

Lähen libbpf väga lihtne:

$ 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

Meie järgmine plaan selles jaotises on järgmine: kirjutame BPF-i programmi nagu BPF_PROG_TYPE_XDP, sama mis eelmises näites, kuid C-s koostame selle kasutades clangja kirjutage abiprogramm, mis laadib selle kernelisse. Järgmistes osades laiendame nii BPF-programmi kui ka abiprogrammi võimalusi.

Näide: täisväärtusliku rakenduse loomine libbpf abil

Alustuseks kasutame faili /sys/kernel/btf/vmlinux, mida oli eespool mainitud, ja looge selle ekvivalent päisefaili kujul:

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

See fail salvestab kõik meie kernelis saadaolevad andmestruktuurid, näiteks IPv4 päis on tuumas määratletud järgmiselt:

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

Nüüd kirjutame oma BPF programmi C keeles:

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

Kuigi meie programm osutus väga lihtsaks, peame siiski paljudele detailidele tähelepanu pöörama. Esiteks, esimene kaasatud päisefail on vmlinux.h, mille me just kasutades lõime bpftool btf dump - nüüd ei pea me installima kerneli päiste paketti, et teada saada, millised tuumastruktuurid välja näevad. Järgmine päisefail tuleb meile raamatukogust libbpf. Nüüd vajame seda ainult makro määratlemiseks SEC, mis saadab märgi ELF-i objektifaili vastavasse jaotisesse. Meie programm sisaldub jaotises xdp/simple, kus enne kaldkriipsu defineerime programmitüübi BPF – see on konventsioon, mida kasutatakse libbpf, asendab see jaotise nime põhjal käivitamisel õige tüübi bpf(2). BPF programm ise on C - väga lihtne ja koosneb ühest reast return XDP_PASS. Lõpuks eraldi jaotis "license" sisaldab litsentsi nime.

Saame oma programmi kompileerida kasutades llvm/clang, versioon >= 10.0.0 või veel parem, suurem (vt jaotist Arendustööriistad):

$ 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

Huvitavate funktsioonide hulgas: näitame sihtarhitektuuri -target bpf ja tee päistesse libbpf, mille me hiljuti installisime. Samuti ärge unustage -O2, ilma selle võimaluseta võite tulevikus oodata üllatusi. Vaatame oma koodi, kas meil õnnestus kirjutada soovitud programm?

$ 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

Jah, see töötas! Nüüd on meil programmiga binaarfail ja me tahame luua rakenduse, mis laadib selle kernelisse. Selleks on raamatukogu libbpf pakub meile kahte võimalust – kasutada madalama taseme API-d või kõrgema taseme API-d. Me läheme teist teed, kuna tahame õppida, kuidas kirjutada, laadida ja ühendada BPF-programme nende edasiseks uurimiseks minimaalse pingutusega.

Esiteks peame sama utiliidi abil genereerima oma programmi "skeleti" selle kahendfailist bpftool — BPF-maailma Šveitsi nuga (mida võib võtta sõna-sõnalt, kuna Daniel Borkman, üks BPF-i loojatest ja hooldajatest, on šveitslane):

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

Failis xdp-simple.skel.h sisaldab meie programmi binaarkoodi ja funktsioone meie objekti haldamiseks - laadimiseks, kinnitamiseks, kustutamiseks. Meie lihtsal juhul näeb see välja nagu liialdus, kuid see toimib ka juhul, kui objektifail sisaldab palju BPF-i programme ja kaarte ning selle hiiglasliku ELF-i laadimiseks peame lihtsalt looma skeleti ja kutsuma kohandatud rakendusest välja ühe või kaks funktsiooni. kirjutavad Liigume nüüd edasi.

Rangelt võttes on meie laadimisprogramm triviaalne:

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

see on struct xdp_simple_bpf failis määratletud xdp-simple.skel.h ja kirjeldab meie objektifaili:

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

Näeme madala taseme API jälgi siin: struktuur struct bpf_program *simple и struct bpf_link *simple. Esimene struktuur kirjeldab konkreetselt meie programmi, mis on kirjutatud jaotises xdp/simpleja teine ​​kirjeldab, kuidas programm ühendub sündmuse allikaga.

Funktsioon xdp_simple_bpf__open_and_load, avab ELF-i objekti, parsib selle, loob kõik struktuurid ja alamstruktuurid (peale programmi sisaldab ELF ka muid jaotisi - andmed, kirjutuskaitstud andmed, silumisinfo, litsents jne) ning laadib selle siis süsteemi abil kernelisse. helistama bpf, mida saame kontrollida programmi kompileerimise ja käivitamisega:

$ 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

Vaatame nüüd oma programmi kasutades bpftool. Leiame tema isikutunnistuse:

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

ja dump (kasutame käsu lühendatud vormi bpftool prog dump xlated):

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

Midagi uut! Programm printis meie C lähtefaili tükke. Seda tegi raamatukogu libbpf, mis leidis binaarfailist silumise jaotise, kompileeris selle BTF-objektiks, laadis selle kernelisse kasutades BPF_BTF_LOAD, ja seejärel määras programmi laadimisel käsuga saadud faili deskriptori BPG_PROG_LOAD.

Kerneli abistajad

BPF-programmid võivad käivitada "väliseid" funktsioone - tuuma abistajaid. Need abifunktsioonid võimaldavad BPF-i programmidel pääseda juurde kerneli struktuuridele, hallata kaarte ja suhelda ka “pärismaailmaga” – luua täiuslikke sündmusi, juhtida riistvara (näiteks ümbersuunamise pakette) jne.

Näide: bpf_get_smp_processor_id

„Eeskuju järgi õppimise“ paradigma raames vaatleme ühte abifunktsiooni, bpf_get_smp_processor_id(), kindel failis kernel/bpf/helpers.c. See tagastab selle protsessori numbri, millel seda kutsunud BPF-programm töötab. Kuid meid ei huvita nii selle semantika kui see, et selle rakendamine võtab ühe rea:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF-i abifunktsioonide määratlused on sarnased Linuxi süsteemikutse määratlustega. Siin on näiteks defineeritud funktsioon, millel pole argumente. (Funktsioon, mis võtab näiteks kolm argumenti, määratakse makro abil BPF_CALL_3. Argumentide maksimaalne arv on viis.) See on aga ainult definitsiooni esimene osa. Teine osa on tüübistruktuuri määratlemine struct bpf_func_proto, mis sisaldab kontrollijale mõistetava abifunktsiooni kirjeldust:

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

Abifunktsioonide registreerimine

Selleks, et teatud tüüpi BPF-programmid saaksid seda funktsiooni kasutada, peavad nad selle registreerima, näiteks tüübi jaoks BPF_PROG_TYPE_XDP funktsioon on defineeritud tuumas xdp_func_proto, mis määrab abifunktsiooni ID põhjal, kas XDP toetab seda funktsiooni või mitte. Meie funktsioon on toetab:

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

Uued BPF-i programmitüübid on failis "defineeritud". include/linux/bpf_types.h kasutades makrot BPF_PROG_TYPE. Määratletakse jutumärkides, kuna see on loogiline definitsioon, ja C-keele terminites esineb terve hulga betoonkonstruktsioonide määratlus teistes kohtades. Eelkõige failis kernel/bpf/verifier.c kõik definitsioonid failist bpf_types.h kasutatakse mitmesuguste struktuuride loomiseks 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
};

See tähendab, et iga BPF-programmi tüübi jaoks on määratletud osuti seda tüüpi andmestruktuurile struct bpf_verifier_ops, mis lähtestatakse väärtusega _name ## _verifier_opsst, xdp_verifier_ops eest xdp... Struktuur xdp_verifier_ops määratud failis net/core/filter.c järgmiselt:

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

Siin näeme meie tuttavat funktsiooni xdp_func_proto, mis käivitab kontrollija iga kord, kui see väljakutse kohtab mingi funktsioonid BPF programmi sees, vt verifier.c.

Vaatame, kuidas hüpoteetiline BPF-programm seda funktsiooni kasutab bpf_get_smp_processor_id. Selleks kirjutame programmi eelmisest jaotisest ümber järgmiselt:

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

Sümbol bpf_get_smp_processor_id määratud в <bpf/bpf_helper_defs.h> raamatukogud libbpf kui

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

see on, bpf_get_smp_processor_id on funktsiooni osuti, mille väärtus on 8, kus 8 on väärtus BPF_FUNC_get_smp_processor_id tüüp enum bpf_fun_id, mis on meie jaoks failis määratletud vmlinux.h (fail bpf_helper_defs.h tuumas genereerib skript, nii et "maagilised" numbrid on ok). See funktsioon ei võta argumente ja tagastab tüübi väärtuse __u32. Kui me seda oma programmis käivitame, clang genereerib juhise BPF_CALL "õige tüüp" Paneme programmi kokku ja vaatame sektsiooni 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

Esimesel real näeme juhiseid call, parameeter IMM mis on võrdne 8-ga ja SRC_REG - null. Kontrollija poolt kasutatava ABI lepingu kohaselt on see kõne abifunktsioonile number kaheksa. Kui see on käivitatud, on loogika lihtne. Tagastusväärtus registrist r0 kopeeritud r1 ja ridadel 2,3 teisendatakse see tüübiks u32 — ülemised 32 bitti kustutatakse. Ridadel 4,5,6,7 tagastame 2 (XDP_PASS) või 1 (XDP_DROP) olenevalt sellest, kas rea 0 abifunktsioon tagastas nulli või nullist erineva väärtuse.

Testime ennast: laadime programmi ja vaatame väljundit bpftool prog dump xlated:

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

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

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

Ok, kontrollija leidis õige kerneli abistaja.

Näide: argumentide edastamine ja lõpuks programmi käivitamine!

Kõigil käitamistaseme abifunktsioonidel on prototüüp

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

Abifunktsioonide parameetrid edastatakse registrites r1-r5ja väärtus tagastatakse registris r0. Funktsioone, mis võtavad rohkem kui viis argumenti, ei ole ja nendele toetust tulevikus ei lisandu.

Vaatame uut kerneli abistajat ja seda, kuidas BPF parameetreid edastab. Kirjutame ümber xdp-simple.bpf.c järgmiselt (ülejäänud read ei ole muutunud):

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

Meie programm prindib selle CPU numbri, millel see töötab. Kompileerime selle ja vaatame koodi:

$ 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

Ridadele 0-7 kirjutame stringi running on CPU%un, ja siis 8. real käivitame tuttava bpf_get_smp_processor_id. Ridadel 9-12 valmistame ette abiargumendid bpf_printk - registrid r1, r2, r3. Miks neid on kolm ja mitte kaks? Sest bpf_printksee on makro ümbris tõelise abilise ümber bpf_trace_printk, mis peab läbima vormingustringi suuruse.

Lisame nüüd paar rida xdp-simple.cet meie programm ühenduks liidesega lo ja tõesti algas!

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

Siin kasutame funktsiooni bpf_set_link_xdp_fd, mis ühendab XDP-tüüpi BPF programmid võrguliidestega. Kodeerisime liidese numbri lo, mis on alati 1. Käivitame funktsiooni kaks korda, et esmalt eemaldada vana programm, kui see oli lisatud. Pange tähele, et nüüd pole meil väljakutset vaja pause või lõpmatu tsükkel: meie laadimisprogramm väljub, kuid BPF-programmi ei suretata, kuna see on ühendatud sündmuse allikaga. Pärast edukat allalaadimist ja ühendamist käivitatakse programm iga saabuva võrgupaketi jaoks lo.

Laadime programmi alla ja vaatame liidest 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

Allalaaditud programmil on ID 669 ja sama ID-d näeme ka liidesel lo. Saadame paar pakki aadressile 127.0.0.1 (päring + vastus):

$ ping -c1 localhost

ja nüüd vaatame silumisfaili virtuaalse faili sisu /sys/kernel/debug/tracing/trace_pipe, milles bpf_printk kirjutab oma sõnumeid:

# 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

Kahte pakki märgati lo ja töödeldud CPU0-s – meie esimene täieõiguslik mõttetu BPF-programm töötas!

Väärib märkimist, et bpf_printk Pole asjata, et see kirjutab silumisfaili: see pole kõige edukam abimees tootmises kasutamiseks, kuid meie eesmärk oli näidata midagi lihtsat.

Juurdepääs kaartidele BPF programmidest

Näide: BPF-programmi kaardi kasutamine

Eelmistes osades õppisime kasutajaruumist kaarte looma ja kasutama ning nüüd vaatame kerneli osa. Alustame, nagu tavaliselt, näitega. Kirjutame oma programmi ümber xdp-simple.bpf.c järgmiselt:

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

Programmi alguses lisasime kaardi definitsiooni woo: see on 8-elemendiline massiiv, mis salvestab selliseid väärtusi nagu u64 (C-s defineeriksime sellise massiivi kui u64 woo[8]). Programmis "xdp/simple" saame praeguse protsessori numbri muutujaks key ja seejärel abifunktsiooni kasutamine bpf_map_lookup_element saame osuti massiivi vastavale kirjele, mida suurendame ühe võrra. Vene keelde tõlgitud: arvutame statistika selle kohta, milline CPU töötles sissetulevaid pakette. Proovime programmi käivitada:

$ 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

Kontrollime, kas ta on sellega seotud lo ja saatke mõned paketid:

$ 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

Vaatame nüüd massiivi sisu:

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

Peaaegu kõiki protsesse töödeldi CPU7-s. See pole meie jaoks oluline, peaasi, et programm töötaks ja me mõistame, kuidas BPF programmidest kaartidele juurde pääseda - kasutades хелперов bpf_mp_*.

Müstiline indeks

Seega pääseme kaardile juurde BPF-programmist, kasutades selliseid kõnesid

val = bpf_map_lookup_elem(&woo, &key);

kus abifunktsioon välja näeb

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

kuid me liigume kursorist mööda &woo nimeta struktuurile struct { ... }...

Kui vaatame programmi koostajat, näeme, et väärtus &woo pole tegelikult määratletud (rida 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
...

ja see sisaldub ümberpaigutamistes:

$ 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

Aga kui vaatame juba laaditud programmi, näeme kursorit õigele kaardile (rida 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]
...

Seega võime järeldada, et meie laadimisprogrammi käivitamise ajal oli link aadressile &woo asendati millegi raamatukoguga libbpf. Kõigepealt vaatame väljundit 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

Me näeme seda libbpf lõi kaardi woo ja seejärel laadisime alla meie programmi simple. Vaatame lähemalt, kuidas programmi laadime:

  • helistama xdp_simple_bpf__open_and_load failist xdp-simple.skel.h
  • mis põhjustab xdp_simple_bpf__load failist xdp-simple.skel.h
  • mis põhjustab bpf_object__load_skeleton failist libbpf/src/libbpf.c
  • mis põhjustab bpf_object__load_xattr kohta libbpf/src/libbpf.c

Muu hulgas helistab viimane funktsioon bpf_object__create_maps, mis loob või avab olemasolevaid kaarte, muutes need failideskriptoriteks. (See on koht, kus me näeme BPF_MAP_CREATE väljundis strace.) Järgmisena kutsutakse funktsioon bpf_object__relocate ja just tema huvitab meid, kuna mäletame, mida nägime woo ümberpaigutamise tabelis. Seda uurides leiame end lõpuks funktsioonist bpf_program__relocate, mis tegeleb kaartide ümberpaigutamisega:

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

Seega võtame oma juhised vastu

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

ja asendada selles olev lähteregister väärtusega BPF_PSEUDO_MAP_FD, ja esimene IMM meie kaardi failideskriptorile ja kui see võrdub näiteks 0xdeadbeef, siis selle tulemusena saame juhise

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

Nii edastatakse kaarditeave konkreetsesse laaditud BPF-i programmi. Sel juhul saab kaardi luua kasutades BPF_MAP_CREATEja avatakse ID abil, kasutades BPF_MAP_GET_FD_BY_ID.

Kasutamisel kokku libbpf algoritm on järgmine:

  • koostamise käigus luuakse ümberpaigutamise tabelis kirjed kaartide linkide jaoks
  • libbpf avab ELF-i objektiraamatu, otsib üles kõik kasutatud kaardid ja koostab neile failideskriptorid
  • failideskriptorid laaditakse kernelisse juhise osana LD64

Nagu võite ette kujutada, on veel tulemas ja me peame uurima tuuma. Õnneks on meil aimu – oleme tähenduse kirja pannud BPF_PSEUDO_MAP_FD allikaregistrisse ja me saame selle maha matta, mis viib meid kõigi pühakute pühakusse - kernel/bpf/verifier.c, kus eristuva nimega funktsioon asendab failideskriptori tüübistruktuuri aadressiga 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;

(täielik kood on leitav по ссылке). Nii saame oma algoritmi laiendada:

  • programmi laadimise ajal kontrollib kontrollija kaardi õiget kasutamist ja kirjutab vastava struktuuri aadressi struct bpf_map

ELF-i binaarfaili allalaadimisel kasutades libbpf Toimub palju muud, kuid me arutame seda teistes artiklites.

Programmide ja kaartide laadimine ilma libbpf-ita

Nagu lubatud, on siin näide lugejatele, kes soovivad teada, kuidas luua ja laadida ilma abita kaarte kasutavat programmi libbpf. See võib olla kasulik, kui töötate keskkonnas, mille jaoks te ei saa luua sõltuvusi või salvestate iga bitti või kirjutate selliseid programme nagu ply, mis genereerib käigupealt BPF-i binaarkoodi.

Loogika järgimise hõlbustamiseks kirjutame oma näite nendel eesmärkidel ümber xdp-simple. Selles näites käsitletud programmi täielik ja veidi laiendatud kood on leitav siit sisuliselt.

Meie rakenduse loogika on järgmine:

  • luua tüübikaart BPF_MAP_TYPE_ARRAY kasutades käsku BPF_MAP_CREATE,
  • luua programm, mis kasutab seda kaarti,
  • ühendage programm liidesega lo,

mis tõlgitakse inimeseks kui

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

see on map_create loob kaardi samamoodi nagu tegime esimeses näites süsteemikõne kohta bpf - "tuum, palun tehke mulle uus kaart 8 elemendi massiivi kujul nagu __u64 ja andke mulle tagasi faili kirjeldus":

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

Programmi on ka lihtne laadida:

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

Keeruline osa prog_load on meie BPF-programmi määratlus struktuuride massiivina struct bpf_insn insns[]. Kuid kuna me kasutame programmi, mis meil on C-s, saame natuke petta:

$ 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

Kokku peame kirjutama 14 juhist selliste struktuuride kujul nagu struct bpf_insn (nõuanne: võtke prügimägi ülalt, lugege uuesti juhiste jaotist, avage linux/bpf.h и linux/bpf_common.h ja proovige kindlaks teha struct bpf_insn insns[] üksinda):

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

Harjutus neile, kes seda ise ei kirjutanud – leidke map_fd.

Meie programmis on veel üks avalikustamata osa - xdp_attach. Kahjuks ei saa selliseid programme nagu XDP süsteemikõne abil ühendada bpf. Inimesed, kes lõid BPF-i ja XDP-d, olid võrgu Linuxi kogukonnast, mis tähendab, et nad kasutasid neile kõige tuttavamat (kuid mitte normaalne inimesed) liides tuumaga suhtlemiseks: netlinki pesad, Vaata ka RFC3549. Lihtsaim viis rakendamiseks xdp_attach kopeerib koodi kohast libbpf, nimelt failist netlink.c, mida me tegimegi, lühendades seda veidi:

Tere tulemast netlinki pistikupesade maailma

Avage võrgulingi pesatüüp 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;
}

Sellest pistikupesast loeme:

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

Lõpuks on siin meie funktsioon, mis avab pesa ja saadab sellele spetsiaalse sõnumi, mis sisaldab failikirjeldust:

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

Niisiis, kõik on testimiseks valmis:

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

Vaatame, kas meie programm on ühendatud 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

Saadame pingid ja vaatame kaarti:

$ 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

Hurraa, kõik töötab. Pange tähele, muide, et meie kaarti kuvatakse taas baitide kujul. See on tingitud asjaolust, et erinevalt libbpf me ei laadinud tüübiteavet (BTF). Aga sellest räägime lähemalt järgmine kord.

Arendustööriistad

Selles jaotises vaatleme minimaalset BPF-i arendaja tööriistakomplekti.

Üldiselt pole BPF-programmide arendamiseks vaja midagi erilist – BPF töötab mis tahes korralikul levitustuumal ja programmid on ehitatud kasutades clang, mida saab tarnida pakendist. Kuna aga BPF on arendamisel, siis kernel ja tööriistad muutuvad pidevalt, kui te ei soovi aastast 2019 BPF-i programme vanamoodsate meetoditega kirjutada, siis peate kompileerima

  • llvm/clang
  • pahole
  • selle tuum
  • bpftool

(Viide, seda jaotist ja kõiki artiklis toodud näiteid kasutati Debian 10-s.)

llvm/klang

BPF on LLVM-iga sõbralik ja kuigi viimasel ajal saab BPF-i programme kompileerida gcc abil, tehakse kogu praegune arendus LLVM-i jaoks. Seetõttu ehitame kõigepealt praeguse versiooni clang gitist:

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

Nüüd saame kontrollida, kas kõik on õigesti kokku pandud:

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

(Kokkupanemise juhised clang minu poolt võetud bpf_devel_QA.)

Me ei installi äsja loodud programme, vaid lisame need lihtsalt PATH, näiteks:

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

(Selle saab lisada .bashrc või eraldi faili. Mina isiklikult lisan selliseid asju juurde ~/bin/activate-llvm.sh ja vajadusel teen seda . activate-llvm.sh.)

Pahole ja BTF

Utiliit pahole kasutatakse kerneli ehitamisel, et luua silumisinfot BTF-vormingus. Me ei käsitle selles artiklis BTF-tehnoloogia üksikasju, välja arvatud asjaolu, et see on mugav ja me tahame seda kasutada. Nii et kui kavatsete tuuma ehitada, ehitage kõigepealt pahole (ilma pahole valikuga ei saa te kernelit ehitada 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

Kernelid BPF-iga katsetamiseks

BPF-i võimalusi uurides tahan kokku panna oma tuuma. Üldiselt pole see vajalik, kuna saate kompileerida ja laadida BPF-i programme jaotustuumale, kuid oma kerneli olemasolu võimaldab teil kasutada uusimaid BPF-i funktsioone, mis ilmuvad teie distributsioonis parimal juhul kuude pärast. , või, nagu mõne silumistööriista puhul, ei pakendata lähitulevikus üldse. Samuti muudab selle enda tuum koodiga katsetamise oluliseks.

Kerneli ehitamiseks on vaja esiteks kernelit ennast ja teiseks kerneli konfiguratsioonifaili. BPF-iga katsetamiseks võime kasutada tavalist vanill kernel või mõni arendustuumadest. Ajalooliselt toimub BPF-i arendus Linuxi võrgukogukonnas ja seetõttu toimuvad kõik muudatused varem või hiljem Linuxi võrguhalduri David Milleri kaudu. Olenevalt nende olemusest – muudatused või uued funktsioonid – jagunevad võrgumuudatused ühte kahest tuumast – net või net-next. BPF-i muudatused jaotatakse samamoodi bpf и bpf-next, mis seejärel ühendatakse vastavalt net ja neto-next. Täpsemalt vt bpf_devel_QA и netdev-KKK. Seega vali tuum vastavalt oma maitsele ja testitava süsteemi stabiilsusvajadustele (*-next tuumad on loetletud kõige ebastabiilsemad).

See artikkel ei hõlma kerneli konfiguratsioonifailide haldamisest rääkimist – eeldatakse, et teate, kuidas seda juba teha või valmis õppima omapäi. Kuid järgmistest juhistest peaks enam-vähem piisama, et anda teile töötav BPF-i toega süsteem.

Laadige alla üks ülaltoodud tuumadest:

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

Ehitage minimaalne töötav kerneli konfiguratsioon:

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

Luba failis BPF-i valikud .config teie enda valikul (tõenäoliselt CONFIG_BPF on juba lubatud, kuna systemd seda kasutab). Siin on selle artikli jaoks kasutatud kerneli valikute loend:

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

Seejärel saame moodulid ja kerneli hõlpsalt kokku panna ja installida (muide, saate tuuma kokku panna äsja kokkupandud clanglisades CC=clang):

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

ja taaskäivitage uue kerneliga (ma kasutan selleks kexec pakendist 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

Artiklis kõige sagedamini kasutatav utiliit on utiliit bpftool, tarnitakse Linuxi tuuma osana. Seda on kirjutanud ja hooldanud BPF-i arendajad BPF-i arendajatele ning seda saab kasutada igat tüüpi BPF-objektide haldamiseks – programmide laadimiseks, kaartide loomiseks ja redigeerimiseks, BPF-i ökosüsteemi elu uurimiseks jne. Leiate dokumentatsiooni man-lehtede lähtekoodide kujul tuumas või juba koostatud, võrgus.

Selle kirjutamise ajal bpftool on valmis ainult RHEL-i, Fedora ja Ubuntu jaoks (vt näiteks see lõim, mis räägib pakendamise lõpetamata loo bpftool Debianis). Aga kui sa oled oma kerneli juba ehitanud, siis ehita bpftool sama lihtne kui pirukas:

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

$

(siin ${linux} - see on teie kerneli kataloog.) Pärast nende käskude täitmist bpftool kogutakse kataloogi ${linux}/tools/bpf/bpftool ja selle saab lisada teele (eelkõige kasutajale root) või lihtsalt kopeerida /usr/local/sbin.

Koguge bpftool kõige parem on kasutada viimast clang, mis on kokku pandud ülalkirjeldatud viisil, ja kontrollige, kas see on õigesti kokku pandud – kasutades näiteks käsku

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

mis näitab, millised BPF-i funktsioonid on teie kernelis lubatud.

Muide, eelmist käsku saab käivitada kui

# bpftool f p k

Seda tehakse analoogselt paketis sisalduvate kommunaalteenustega iproute2, kus saame näiteks öelda ip a s eth0 asemel ip addr show dev eth0.

Järeldus

BPF võimaldab teil kirbu jalata, et tõhusalt mõõta ja käigupealt muuta südamiku funktsionaalsust. Süsteem osutus UNIXi parimate traditsioonide kohaselt väga edukaks: lihtne mehhanism, mis võimaldab tuuma (ümber)programmeerida, võimaldas katsetada tohutul hulgal inimestel ja organisatsioonidel. Ja kuigi katsed, nagu ka BPF-i taristu enda arendamine, pole veel kaugeltki lõppenud, on süsteemil juba stabiilne ABI, mis võimaldab teil luua usaldusväärse ja mis kõige tähtsam - tõhusa äriloogika.

Tahaksin märkida, et minu arvates on see tehnoloogia muutunud nii populaarseks, sest ühelt poolt saab see seda teha mängima (masina arhitektuurist saab enam-vähem ühe õhtuga aru), teisalt aga lahendada probleeme, mida enne selle ilmumist (ilusasti) lahendada ei saanud. Need kaks komponenti koos sunnivad inimesi eksperimenteerima ja unistama, mis toob kaasa üha uuenduslikumate lahenduste tekkimise.

See artikkel, kuigi mitte eriti lühike, on vaid sissejuhatus BPF-i maailma ega kirjelda "täiustatud" funktsioone ega arhitektuuri olulisi osi. Edasine plaan on umbes selline: järgmises artiklis antakse ülevaade BPF-i programmitüüpidest (5.8 tuumas on toetatud 30 programmitüüpi), seejärel vaatame lõpuks, kuidas kirjutada päris BPF-i rakendusi, kasutades kerneli jälgimisprogramme. Näiteks on aeg läbida BPF-i arhitektuuri põhjalikum kursus, millele järgneb BPF-i võrgu- ja turberakenduste näited.

Selle sarja eelmised artiklid

  1. BPF kõige väiksematele, osa null: klassikaline BPF

Lingid

  1. BPF ja XDP teatmik — dokumentatsioon BPF-i kohta ciliumilt või täpsemalt Daniel Borkmanilt, ühelt BPF-i loojalt ja hooldajalt. See on üks esimesi tõsisemaid kirjeldusi, mis erineb teistest selle poolest, et Daniel teab täpselt, millest kirjutab ja seal pole vigu. Eelkõige kirjeldatakse selles dokumendis, kuidas töötada XDP ja TC tüüpi BPF-programmidega, kasutades tuntud utiliiti. ip pakendist iproute2.

  2. Dokumentatsioon/võrk/filter.txt — originaalfail koos dokumentatsiooniga klassikalise ja seejärel laiendatud BPF jaoks. Hea lugemine, kui soovite süveneda montaažikeelde ja tehnilistesse arhitektuurilistesse detailidesse.

  3. Blogi BPF-ist Facebookist. Uuendatakse harva, kuid tabavalt, nagu kirjutavad seal Aleksei Starovoitov (eBPF autor) ja Andrii Nakryiko - (hooldaja) libbpf).

  4. Bpftooli saladused. Quentin Monnet' meelelahutuslik twitterilõng koos näidete ja saladustega bpftool'i kasutamisest.

  5. Sukelduge BPF-i: lugemismaterjalide loend. Hiiglaslik (ja endiselt hooldatav) loetelu linkidest BPF-i dokumentatsioonile Quentin Monnet'lt.

Allikas: www.habr.com

Lisa kommentaar