BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Í upphafi var tækni og hún var kölluð BPF. Við horfðum á hana fyrri, Grein Gamla testamentisins í þessari röð. Árið 2013, með viðleitni Alexei Starovoitov og Daniel Borkman, var endurbætt útgáfa af því, fínstillt fyrir nútíma 64-bita vélar, þróuð og innifalin í Linux kjarnanum. Þessi nýja tækni var stuttlega kölluð Innri BPF, síðan endurnefnd Extended BPF, og nú, eftir nokkur ár, kalla allir hana einfaldlega BPF.

Í grófum dráttum gerir BPF þér kleift að keyra handahófskenndan kóða frá notanda í Linux kjarnarýminu og nýi arkitektúrinn reyndist svo vel heppnaður að við þurfum tugi greina til viðbótar til að lýsa öllum forritum hans. (Það eina sem verktaki gekk ekki vel, eins og þú sérð í frammistöðukóðanum hér að neðan, var að búa til ágætis lógó.)

Þessi grein lýsir uppbyggingu BPF sýndarvélarinnar, kjarnaviðmótum til að vinna með BPF, þróunarverkfærum, sem og stuttu, mjög stuttu yfirliti yfir núverandi getu, þ.e. allt sem við munum þurfa í framtíðinni fyrir dýpri rannsókn á hagnýtri notkun BPF.
BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Samantekt á greininni

Kynning á BPF arkitektúr. Í fyrsta lagi munum við skoða BPF arkitektúrinn og útlista helstu þættina.

Skrár og stjórnkerfi BPF sýndarvélarinnar. Þegar við höfum hugmynd um arkitektúrinn í heild sinni munum við lýsa uppbyggingu BPF sýndarvélarinnar.

Lífsferill BPF hluta, bpffs skráarkerfi. Í þessum hluta munum við skoða nánar lífsferil BPF-hluta - forrita og korta.

Stjórna hlutum með bpf kerfiskallinu. Með nokkurn skilning á kerfinu sem þegar er til staðar, munum við að lokum skoða hvernig á að búa til og meðhöndla hluti úr notendarými með sérstöku kerfiskalli − bpf(2).

Пишем программы BPF с помощью libbpf. Auðvitað geturðu skrifað forrit með kerfiskalli. En það er erfitt. Fyrir raunsærri atburðarás þróuðu kjarnorkuforritarar bókasafn libbpf. Við munum búa til grunn BPF umsókn beinagrind sem við munum nota í síðari dæmum.

Kjarnahjálparar. Hér munum við læra hvernig BPF forrit hafa aðgang að kjarnahjálparaðgerðum - tól sem ásamt kortum stækkar í grundvallaratriðum getu nýja BPF miðað við það klassíska.

Aðgangur að kortum frá BPF forritum. Á þessum tímapunkti munum við vita nóg til að skilja nákvæmlega hvernig við getum búið til forrit sem nota kort. Og við skulum jafnvel kíkja fljótt inn í hinn mikla og volduga sannprófanda.

Þróunartæki. Hjálparhluti um hvernig á að setja saman nauðsynleg tól og kjarna fyrir tilraunir.

Niðurstöðu. Í lok greinarinnar munu þeir sem lesa svona langt finna hvetjandi orð og stutta lýsingu á því sem mun gerast í eftirfarandi greinum. Einnig munum við telja upp fjölda tengla fyrir sjálfsnám fyrir þá sem ekki hafa löngun eða getu til að bíða eftir framhaldinu.

Kynning á BPF arkitektúr

Áður en við byrjum að íhuga BPF arkitektúrinn munum við vísa í síðasta sinn (ó) til klassískt BPF, sem var þróað sem svar við tilkomu RISC véla og leysti vandamálið við skilvirka pakkasíun. Arkitektúrinn reyndist svo vel heppnaður að hann var fæddur á hinum glæsilega tíunda áratugnum í Berkeley UNIX og var fluttur yfir í flest núverandi stýrikerfi, lifði af á brjálaðan tuttugasta áratuginn og er enn að finna ný forrit.

Nýja BPF var þróað sem svar við alls staðar nálægð 64-bita véla, skýjaþjónustu og aukinni þörf fyrir verkfæri til að búa til SDN (Soft-definn nnetvinnu). Nýi BPF, hannaður af verkfræðingum kjarnanets sem endurbættur staðgengill fyrir klassíska BPF, fann bókstaflega sex mánuðum síðar forrit í því erfiða verkefni að rekja Linux kerfi, og núna, sex árum eftir að það birtist, þurfum við heila næstu grein bara til að skrá mismunandi gerðir af forritum.

Skemmtilegar myndir

Í kjarna sínum er BPF sýndarvél í sandkassa sem gerir þér kleift að keyra „handahófskennda“ kóða í kjarnarými án þess að skerða öryggi. BPF forrit eru búin til í notendarými, hlaðið inn í kjarnann og tengt við einhvern viðburðargjafa. Atburður gæti til dæmis verið afhending pakka í netviðmót, ræsing á einhverri kjarnaaðgerð o.s.frv. Ef um er að ræða pakka mun BPF forritið hafa aðgang að gögnum og lýsigögnum pakkans (til að lesa og hugsanlega skrifa, allt eftir tegund forrits); ef um er að ræða að keyra kjarnafall, rök aðgerðina, þar á meðal ábendingar á kjarnaminni o.s.frv.

Við skulum skoða þetta ferli nánar. Til að byrja með skulum við tala um fyrsta muninn frá klassískum BPF, forrit sem voru skrifuð í assembler. Í nýju útgáfunni var arkitektúrinn stækkaður þannig að hægt væri að skrifa forrit á háttsettum tungumálum, fyrst og fremst, að sjálfsögðu, í C. Til þess var þróaður bakendi fyrir llvm, sem gerir þér kleift að búa til bætikóða fyrir BPF arkitektúrinn.

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

BPF arkitektúrinn var hannaður að hluta til til að keyra á skilvirkan hátt á nútíma vélum. Til að þetta virki í reynd er BPF bætikóðinn, þegar hann er hlaðinn inn í kjarnann, þýddur yfir í innfæddan kóða með því að nota íhlut sem kallast JIT þýðandi (Just In Time). Næst, ef þú manst, í klassískum BPF var forritinu hlaðið inn í kjarnann og tengt við atburðaruppsprettu frumeinda - í samhengi við eitt kerfiskall. Í nýja arkitektúrnum gerist þetta í tveimur áföngum - í fyrsta lagi er kóðanum hlaðið inn í kjarnann með því að nota kerfiskall bpf(2)og síðan, síðar, með öðrum aðferðum sem eru mismunandi eftir tegund forrits, tengist forritið við viðburðaruppsprettu.

Hér gæti lesandinn haft spurningu: var það mögulegt? Hvernig er framkvæmdaröryggi slíks kóða tryggt? Framkvæmdaöryggi er tryggt okkur með því að hlaða BPF forritum sem kallast verifier (á ensku er þetta stig kallað verifier og ég mun halda áfram að nota enska orðið):

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Verifier er kyrrstöðugreiningartæki sem tryggir að forrit truflar ekki eðlilega starfsemi kjarnans. Þetta þýðir, við the vegur, ekki að forritið geti ekki truflað rekstur kerfisins - BPF forrit, allt eftir gerð, geta lesið og endurskrifað hluta af kjarnaminni, skilað gildum aðgerða, klippt, bætt við, endurskrifað. og jafnvel áframsenda netpakka. Verifier ábyrgist að keyrsla á BPF forriti hrynji ekki kjarnann og að forrit sem samkvæmt reglunum hefur skrifaðgang, td gögn sends pakka, geti ekki skrifað yfir kjarnaminnið utan pakkans. Við munum skoða sannprófanda aðeins nánar í samsvarandi hluta, eftir að við höfum kynnst öllum öðrum hlutum BPF.

Svo hvað höfum við lært hingað til? Notandinn skrifar forrit í C, hleður því inn í kjarnann með því að nota kerfiskall bpf(2), þar sem það er athugað af sannprófanda og þýtt í innfæddan bækikóða. Þá tengir sami eða annar notandi forritið við atburðargjafann og það byrjar að keyra. Aðskilja ræsingu og tengingu er nauðsynleg af ýmsum ástæðum. Í fyrsta lagi er tiltölulega dýrt að keyra sannprófara og með því að hlaða niður sama forritinu nokkrum sinnum sóum við tölvutíma. Í öðru lagi fer nákvæmlega hvernig forrit er tengt eftir gerð þess og eitt „alhliða“ viðmót sem þróað var fyrir ári síðan hentar kannski ekki fyrir nýjar gerðir forrita. (Þó nú þegar arkitektúrinn er að verða þroskaðri, þá er hugmynd að sameina þetta viðmót á stigi libbpf.)

Eftirtektarsamur lesandi tekur kannski eftir því að við erum ekki búin með myndirnar ennþá. Reyndar útskýrir allt ofangreint ekki hvers vegna BPF breytir myndinni í grundvallaratriðum miðað við klassíska BPF. Tvær nýjungar sem víkka verulega út gildissviðið eru hæfileikinn til að nota samnýtt minni og kjarnahjálparaðgerðir. Í BPF er sameiginlegt minni útfært með því að nota svokölluð kort - sameiginleg gagnastrúktúr með tilteknu API. Þeir hafa líklega fengið þetta nafn vegna þess að fyrsta gerð korta sem birtist var kjötkássatafla. Síðan birtust fylki, staðbundnar (á hverja örgjörva) kjötkássatöflur og staðbundnar fylki, leitartré, kort sem innihalda vísa til BPF forrita og margt fleira. Það sem er áhugavert fyrir okkur núna er að BPF forrit hafa nú getu til að halda stöðu milli símtala og deila því með öðrum forritum og með notendarými.

Hægt er að nálgast kort frá notendaferlum með því að nota kerfiskall bpf(2), og frá BPF forritum sem keyra í kjarnanum með því að nota hjálparaðgerðir. Þar að auki eru aðstoðarmenn ekki aðeins til til að vinna með kort, heldur einnig til að fá aðgang að öðrum kjarnamöguleikum. Til dæmis geta BPF forrit notað hjálparaðgerðir til að senda pakka til annarra viðmóta, búa til perf atburði, fá aðgang að kjarnabyggingum og svo framvegis.

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Í stuttu máli, BPF veitir möguleika á að hlaða handahófskenndum, þ.e.a.s. sannprófandaprófuðum, notendakóða inn í kjarnarými. Þessi kóði getur vistað ástand milli símtala og skipt gögnum með notendarými, og hefur einnig aðgang að kjarna undirkerfum sem þessi tegund af forritum leyfir.

Þetta er nú þegar svipað og möguleikinn sem kjarnaeiningin býður upp á, samanborið við það sem BPF hefur nokkra kosti (auðvitað er aðeins hægt að bera saman svipuð forrit, til dæmis kerfisrakningu - þú getur ekki skrifað handahófskenndan ökumann með BPF). Þú getur tekið eftir lægri aðgangsþröskuldi (sum tólum sem nota BPF krefjast þess að notandinn hafi ekki kjarnaforritunarkunnáttu, eða forritunarkunnáttu almennt), keyrsluöryggi (réttu upp hendinni í athugasemdunum fyrir þá sem brutu ekki kerfið við ritun eða prófunareiningar), atomicity - það er niður í miðbæ þegar einingar eru endurhlaðnar og BPF undirkerfið tryggir að engir atburðir séu sleppt (til að vera sanngjarnt, þetta á ekki við um allar tegundir BPF forrita).

Tilvist slíkra getu gerir BPF að alhliða tæki til að stækka kjarnann, sem er staðfest í reynd: fleiri og fleiri nýjar gerðir af forritum bætast við BPF, fleiri og fleiri stór fyrirtæki nota BPF á bardagaþjónum 24×7, fleiri og fleiri sprotafyrirtæki byggja viðskipti sín á lausnum sem byggjast á BPF. BPF er notað alls staðar: til að vernda gegn DDoS árásum, búa til SDN (til dæmis, innleiða net fyrir kubernetes), sem aðal kerfisrakningartól og tölfræðisafnari, í innbrotsskynjunarkerfum og sandkassakerfum osfrv.

Ljúkum yfirlitshluta greinarinnar hér og skoðum sýndarvélina og BPF vistkerfið nánar.

Útrás: veitur

Til þess að geta keyrt dæmin í eftirfarandi köflum gætirðu þurft fjölda tóla, að minnsta kosti llvm/clang með bpf stuðningi og bpftool. Í kafla Þróunarverkfæri Þú getur lesið leiðbeiningarnar um að setja saman tólin, sem og kjarnann þinn. Þessi hluti er settur hér að neðan til að raska ekki samræmi í framsetningu okkar.

BPF sýndarvélaskrár og kennslukerfi

Arkitektúr og stjórnkerfi BPF var þróað með hliðsjón af þeirri staðreynd að forrit verða skrifuð á C tungumálinu og, eftir að hafa verið hlaðið inn í kjarnann, þýdd í innfæddan kóða. Því var fjöldi skráa og skipanasamstæður valdir með auga fyrir skurðpunktum, í stærðfræðilegum skilningi, á getu nútíma véla. Auk þess voru ýmsar takmarkanir settar á forrit, til dæmis var þar til nýlega ekki hægt að skrifa lykkjur og undiráætlanir og fjöldi leiðbeininga var takmarkaður við 4096 (nú geta forréttindaforrit hlaðið allt að milljón leiðbeiningum).

BPF er með ellefu notendaaðgengilegar 64-bita skrár r0-r10 og dagskrárteljari. Skráðu þig r10 inniheldur rammabendil og er skrifvarinn. Forrit hafa aðgang að 512 bæta stafla á keyrslutíma og ótakmarkað magn af sameiginlegu minni í formi korta.

BPF forritum er leyft að keyra tiltekið sett af forritagerð kjarnahjálpar og, í seinni tíð, reglulegar aðgerðir. Hver kölluð fall getur tekið allt að fimm frumbreytur, sendar í skrám r1-r5, og skilagildið er sent til r0. Það er tryggt að eftir að koma aftur úr aðgerðinni, innihald skránna r6-r9 Mun ekki breytast.

Fyrir skilvirka forritsþýðingu, skráir r0-r11 þar sem allir studdir byggingarlistar eru einstaklega kortlagðir á raunverulegar skrár, að teknu tilliti til ABI eiginleika núverandi byggingarlistar. Til dæmis, fyrir x86_64 skrár r1-r5, sem notuð eru til að senda breytur virka, birtast á rdi, rsi, rdx, rcx, r8, sem eru notuð til að senda færibreytur til aðgerðir á x86_64. Til dæmis þýðir kóðinn til vinstri yfir í kóðann hægra megin svona:

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

Skráðu þig r0 einnig notað til að skila niðurstöðu af framkvæmd forritsins, og í skránni r1 forritinu er vísað til samhengisins - allt eftir tegund forrits gæti þetta til dæmis verið uppbygging struct xdp_md (fyrir XDP) eða uppbyggingu struct __sk_buff (fyrir mismunandi netforrit) eða uppbyggingu struct pt_regs (fyrir mismunandi gerðir af rekjaforritum) o.s.frv.

Þannig að við áttum safn skrár, kjarnahjálpar, stafla, samhengisbendil og sameiginlegt minni í formi korta. Ekki það að allt þetta sé algjörlega nauðsynlegt í ferðinni, en...

Höldum áfram lýsingunni og tölum um stjórnkerfið til að vinna með þessa hluti. Allt (Næstum allt) BPF leiðbeiningar hafa fasta 64 bita stærð. Ef þú skoðar eina leiðbeiningar á 64-bita Big Endian vél muntu sjá

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Hér Code - þetta er kóðun leiðbeiningarinnar, Dst/Src eru kóðun móttakarans og upprunans, í sömu röð, Off - 16 bita undirrituð inndráttur, og Imm er 32 bita táknuð heiltala sem notuð er í sumum leiðbeiningum (svipað og cBPF fastan K). Kóðun Code hefur eina af tveimur gerðum:

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Kennsluflokkar 0, 1, 2, 3 skilgreina skipanir til að vinna með minni. Þeir eru kölluð, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, í sömu röð. 4., 7. flokkur (BPF_ALU, BPF_ALU64) mynda sett af ALU leiðbeiningum. 5., 6. flokkur (BPF_JMP, BPF_JMP32) innihalda stökkleiðbeiningar.

Frekari áætlun um að rannsaka BPF leiðbeiningakerfið er sem hér segir: í stað þess að skrá allar leiðbeiningarnar og færibreytur þeirra nákvæmlega, munum við skoða nokkur dæmi í þessum hluta og út frá þeim verður ljóst hvernig leiðbeiningarnar virka í raun og veru og hvernig á að taka í sundur hvaða tvíundarskrá sem er fyrir BPF handvirkt. Til að sameina efnið síðar í greininni munum við einnig hitta einstakar leiðbeiningar í köflum um Verifier, JIT þýðanda, þýðingu á klassískum BPF, sem og við að rannsaka kort, kalla aðgerðir o.s.frv.

Þegar við tölum um einstakar leiðbeiningar munum við vísa til kjarnaskránna bpf.h и bpf_common.h, sem skilgreina tölukóða BPF leiðbeininga. Þegar þú ert að læra arkitektúr á eigin spýtur og/eða þáttun í tvítölum geturðu fundið merkingarfræði í eftirfarandi heimildum, raðað eftir flækjustig: Óopinber eBPF forskrift, BPF og XDP tilvísunarhandbók, leiðbeiningasett, Documentation/networking/filter.txt og, auðvitað, í Linux frumkóðanum - sannprófandi, JIT, BPF túlkur.

Dæmi: að taka BPF í sundur í hausnum á þér

Við skulum skoða dæmi þar sem við setjum saman forrit readelf-example.c og líttu á tvöfaldann sem myndast. Við munum sýna upprunalega innihaldið readelf-example.c hér að neðan, eftir að við endurheimtum rökfræði þess úr tvöföldum kóða:

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

Fyrsti dálkur í úttak readelf er inndráttur og forritið okkar samanstendur þannig af fjórum skipunum:

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

Skipunarkóðar eru jafnir b7, 15, b7 и 95. Mundu að vægustu þrír bitarnir eru kennsluflokkurinn. Í okkar tilviki er fjórði bitinn af öllum leiðbeiningum tómur, þannig að kennsluflokkarnir eru 7, 5, 7, 5, í sömu röð. Class 7 er BPF_ALU64, og 5 er BPF_JMP. Fyrir báða flokka er kennslusniðið það sama (sjá hér að ofan) og við getum endurskrifað forritið okkar svona (á sama tíma munum við endurskrifa þá dálka sem eftir eru í mannsmynd):

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

Operation b bekknum ALU64 - Er BPF_MOV. Það úthlutar gildi til áfangastaðaskrárinnar. Ef bitinn er stilltur s (heimild), þá er gildið tekið úr heimildaskránni og ef það er ekki stillt, eins og í okkar tilviki, þá er gildið tekið úr reitnum Imm. Svo í fyrstu og þriðju leiðbeiningunum framkvæmum við aðgerðina r0 = Imm. Ennfremur er JMP flokkur 1 aðgerð BPF_JEQ (hoppa ef jafnt). Í okkar tilviki, þar sem bit S er núll ber það saman gildi upprunaskrárinnar við reitinn Imm. Ef gildin falla saman, þá eiga sér stað umskipti til PC + Offhvar PC, eins og venjulega, inniheldur heimilisfang næstu leiðbeiningar. Að lokum, JMP Class 9 Operation er BPF_EXIT. Þessi leiðbeining lýkur forritinu og fer aftur í kjarnann r0. Við skulum bæta nýjum dálki við töfluna okkar:

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

Við getum endurskrifað þetta á þægilegra formi:

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

Ef við munum hvað er í skránni r1 forritinu er vísað til samhengis frá kjarnanum og í skránni r0 gildið er skilað til kjarnans, þá getum við séð að ef bendillinn á samhengið er núll, þá skilum við 1, og annars - 2. Við skulum athuga hvort við höfum rétt fyrir okkur með því að skoða upprunann:

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

Já, það er tilgangslaust forrit, en það þýðir aðeins fjórar einfaldar leiðbeiningar.

Undantekningadæmi: 16-bæta kennsla

Við nefndum áðan að sumar leiðbeiningar taka meira en 64 bita. Þetta á til dæmis við um leiðbeiningar lddw (Kóði = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — hlaða tvöföldu orði úr reitunum í skrána Imm. Staðreyndin er sú Imm hefur stærðina 32 og tvöfalt orð er 64 bita, þannig að hleðsla á 64 bita strax gildi í skrá í einni 64 bita leiðbeiningu mun ekki virka. Til að gera þetta eru tvær samliggjandi leiðbeiningar notaðar til að geyma seinni hluta 64-bita gildisins í reitnum Imm. Dæmi:

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

Það eru aðeins tvær leiðbeiningar í tvíundarforriti:

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

Við munum hittast aftur með leiðbeiningum lddw, þegar rætt er um flutninga og vinnu með kort.

Dæmi: að taka BPF í sundur með venjulegum verkfærum

Þannig að við höfum lært að lesa BPF tvöfalda kóða og erum tilbúin til að flokka allar leiðbeiningar ef þörf krefur. Hins vegar er það þess virði að segja að í reynd er þægilegra og fljótlegra að taka í sundur forrit með venjulegum verkfærum, til dæmis:

$ 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

Lífsferill BPF hluta, bpffs skráarkerfi

(Ég lærði fyrst sum smáatriðin sem lýst er í þessum undirkafla frá færslu Alexei Starovoitov inn BPF blogg.)

BPF hlutir - forrit og kort - eru búnir til úr notendarými með skipunum BPF_PROG_LOAD и BPF_MAP_CREATE kerfiskall bpf(2), við munum tala um nákvæmlega hvernig þetta gerist í næsta kafla. Þetta skapar kjarnagagnaskipulag og fyrir hvert þeirra refcount (tilvísunarfjöldi) er stilltur á einn og skráarlýsing sem bendir á hlutinn er skilað til notanda. Eftir að handfangið er lokað refcount hluturinn minnkar um einn og þegar hann nær núlli eyðist hluturinn.

Ef forritið notar kort, þá refcount þessum kortum er fjölgað um eitt eftir að forritið er hlaðið, þ.e. Hægt er að loka skráarlýsingum þeirra úr notendaferlinu og enn refcount verður ekki núll:

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Eftir að forrit hefur verið hlaðið með góðum árangri festum við það venjulega við einhvers konar viðburðarrafall. Til dæmis getum við sett það á netviðmót til að vinna úr komandi pakka eða tengt það við suma tracepoint í kjarnanum. Á þessum tímapunkti mun viðmiðunarteljarinn einnig hækka um einn og við munum geta lokað skráarlýsingunni í hleðsluforritinu.

Hvað gerist ef við slökkva á ræsiforritinu? Það fer eftir tegund viðburðarrafalls (krók). Allir netkrókar verða til eftir að hleðslutæki lýkur, þetta eru hinir svokölluðu alþjóðlegu krókar. Og, til dæmis, verða rekjaforrit gefin út eftir að ferlinu sem skapaði þau lýkur (og eru því kölluð staðbundin, frá "staðbundin til ferlisins"). Tæknilega séð hafa staðbundnir krókar alltaf samsvarandi skráarlýsingu í notendarými og lokast því þegar ferlinu er lokað, en alþjóðlegir krókar gera það ekki. Í eftirfarandi mynd, með því að nota rauða krossa, reyni ég að sýna hvernig lokun hleðsluforritsins hefur áhrif á líftíma hluta þegar um er að ræða staðbundna og alþjóðlega króka.

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Hvers vegna er greinarmunur á staðbundnum og alþjóðlegum krókum? Það er skynsamlegt að keyra sumar tegundir netforrita án notendarýmis, til dæmis, ímyndaðu þér DDoS vernd - ræsiforritið skrifar reglurnar og tengir BPF forritið við netviðmótið, eftir það getur ræsiforritið farið og drepið sig. Á hinn bóginn, ímyndaðu þér kembiforrit sem þú skrifaðir á hnén á þér eftir tíu mínútur - þegar því er lokið, vilt þú að það sé ekkert sorp eftir í kerfinu og staðbundnir krókar munu tryggja það.

Á hinn bóginn, ímyndaðu þér að þú viljir tengjast rakningarpunkti í kjarnanum og safna tölfræði yfir mörg ár. Í þessu tilviki myndirðu vilja klára notendahlutann og fara aftur í tölfræðina af og til. Bpf skráarkerfið veitir þetta tækifæri. Þetta er gerviskráakerfi sem er aðeins í minni sem gerir kleift að búa til skrár sem vísa til BPF hlutum og auka þar með refcount hlutir. Eftir þetta getur hleðslutækið farið út og hlutirnir sem hann bjó til halda lífi.

BPF fyrir litlu börnin, hluti eitt: framlengdur BPF

Að búa til skrár í bpffs sem vísa til BPF-hluta er kallað "pinning" (eins og í eftirfarandi setningu: "ferlið getur fest BPF forrit eða kort"). Að búa til skráarhluti fyrir BPF hluti er skynsamlegt, ekki aðeins til að lengja líf staðbundinna hluta, heldur einnig fyrir notagildi hnattrænna hluta - aftur til dæmisins með alþjóðlegu DDoS verndarforritinu, viljum við geta komið og skoðað tölfræði af og til.

BPF skráarkerfið er venjulega fest í /sys/fs/bpf, en það er líka hægt að setja það upp á staðnum, til dæmis, svona:

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

Skráarkerfisnöfn eru búin til með skipuninni BPF_OBJ_PIN BPF kerfiskall. Til að útskýra það skulum við taka forrit, setja það saman, hlaða því upp og festa það við bpffs. Forritið okkar gerir ekkert gagnlegt, við erum aðeins að kynna kóðann svo þú getir endurskapað dæmið:

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

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

Við skulum setja þetta forrit saman og búa til staðbundið afrit af skráarkerfinu bpffs:

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

Nú skulum við hlaða niður forritinu okkar með því að nota tólið bpftool og skoðaðu meðfylgjandi kerfissímtöl bpf(2) (nokkrar óviðkomandi línur fjarlægðar úr úttaksútgáfu):

$ 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

Hér höfum við hlaðið forritinu með því að nota BPF_PROG_LOAD, fékk skráarlýsingu frá kjarnanum 3 og nota skipunina BPF_OBJ_PIN festi þessa skráarlýsingu sem skrá "bpf-mountpoint/test". Eftir þetta ræsiforritið bpftool kláraði að virka, en forritið okkar var áfram í kjarnanum, þó að við tengdum það ekki við neitt netviðmót:

$ 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

Við getum eytt skráarhlutnum venjulega unlink(2) og eftir það verður samsvarandi forriti eytt:

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

Eyðir hlutum

Talandi um að eyða hlutum, þá er nauðsynlegt að skýra að eftir að við höfum aftengt forritið frá króknum (atburðaframleiðandinn), mun ekki einn nýr atburður koma af stað ræsingu þess, hins vegar verður öllum núverandi tilvikum forritsins lokið í venjulegri röð .

Sumar tegundir BPF forrita gera þér kleift að skipta um forritið á flugi, þ.e. veita röð atómvirkni replace = detach old program, attach new program. Í þessu tilviki munu öll virk tilvik af gömlu útgáfunni af forritinu ljúka verki sínu og nýir atburðastjórnunaraðilar verða búnir til úr nýja forritinu og „atomicity“ þýðir hér að ekki verður saknað af einum atburði.

Að tengja forrit við viðburðaheimildir

Í þessari grein munum við ekki lýsa sérstaklega tengingu forrita við viðburðaheimildir, þar sem skynsamlegt er að rannsaka þetta í samhengi við tiltekna tegund forrits. Cm. Dæmi hér að neðan, þar sem við sýnum hvernig forrit eins og XDP eru tengd.

Meðhöndla hluti með bpf kerfiskallinu

BPF forrit

Allir BPF hlutir eru búnir til og stjórnað úr notendarými með því að nota kerfiskall bpf, með eftirfarandi frumgerð:

#include <linux/bpf.h>

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

Hér er liðið cmd er eitt af gildum tegundar enum bpf_cmd, attr — bendi á færibreytur fyrir tiltekið forrit og size — hlutastærð samkvæmt bendilinn, þ.e. venjulega þetta sizeof(*attr). Í kjarna 5.8 kerfiskallið bpf styður 34 mismunandi skipanir, og ákvörðun um union bpf_attr tekur 200 línur. En við ættum ekki að vera hræddur við þetta, þar sem við munum kynna okkur skipanir og færibreytur í gegnum nokkrar greinar.

Byrjum á liðinu BPF_PROG_LOAD, sem býr til BPF forrit - tekur sett af BPF leiðbeiningum og hleður því inn í kjarnann. Við hleðslu er sannprófunartækið ræst og síðan er JIT þýðandinn og, eftir árangursríka framkvæmd, lýsingu forritsskrárinnar skilað til notanda. Við sáum hvað verður um hann næst í fyrri hlutanum um lífsferil BPF-hluta.

Við munum nú skrifa sérsniðið forrit sem mun hlaða einfalt BPF forrit, en fyrst þurfum við að ákveða hvers konar forrit við viljum hlaða - við verðum að velja Tegund og innan ramma þessarar tegundar, skrifaðu forrit sem mun standast sannprófunarprófið. Hins vegar, til að flækja ekki ferlið, er hér tilbúin lausn: við munum taka forrit eins og BPF_PROG_TYPE_XDP, sem mun skila gildinu XDP_PASS (slepptu öllum pakkningum). Í BPF assembler lítur það mjög einfalt út:

r0 = 2
exit

Eftir að við höfum ákveðið við munum hlaða upp, við getum sagt þér hvernig við munum gera það:

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

Áhugaverðir atburðir í dagskrá byrja með skilgreiningu á fylki insns - BPF forritið okkar í vélkóða. Í þessu tilviki er hverri kennslu BPF forritsins pakkað inn í uppbygginguna bpf_insn. Fyrsti þáttur insns uppfyllir leiðbeiningar r0 = 2, sekúndan - exit.

hörfa. Kjarninn skilgreinir þægilegri fjölvi til að skrifa vélkóða og nota kjarnahausaskrána tools/include/linux/filter.h við gætum skrifað

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

En þar sem að skrifa BPF forrit í innfæddum kóða er aðeins nauðsynlegt til að skrifa próf í kjarnanum og greinar um BPF, flækir fjarvera þessara fjölva í raun ekki líf þróunaraðilans.

Eftir að hafa skilgreint BPF forritið förum við áfram að hlaða því inn í kjarnann. Minimalískt sett af breytum okkar attr inniheldur tegund forrits, sett og fjölda leiðbeininga, áskilið leyfi og nafn "woo", sem við notum til að finna forritið okkar á kerfinu eftir niðurhal. Forritið, eins og lofað var, er hlaðið inn í kerfið með því að nota kerfiskall bpf.

Í lok forritsins endum við í óendanlega lykkju sem líkir eftir farmálaginu. Án þess verður forritið drepið af kjarnanum þegar skráarlýsingunni sem kerfiskallið skilaði til okkar er lokað bpf, og við munum ekki sjá það í kerfinu.

Jæja, við erum tilbúin til prófunar. Við skulum setja saman og keyra forritið undir stracetil að athuga hvort allt virki eins og það á að gera:

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

Allt er í lagi, bpf(2) skilaði handfangi 3 til okkar og við fórum í óendanlega lykkju með pause(). Við skulum reyna að finna forritið okkar í kerfinu. Til að gera þetta munum við fara í aðra flugstöð og nota tólið 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)

Við sjáum að það er hlaðið forrit í kerfinu woo þar sem alþjóðlegt auðkenni er 390 og er í vinnslu simple-prog það er opinn skráarlýsing sem bendir á forritið (og ef simple-prog mun þá ljúka verkinu woo mun hverfa). Eins og við var að búast, dagskráin woo tekur 16 bæti - tvær leiðbeiningar - af tvöföldum kóða í BPF arkitektúrnum, en í upprunalegu formi (x86_64) er það nú þegar 40 bæti. Við skulum skoða forritið okkar í upprunalegri mynd:

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

kemur ekkert á óvart. Nú skulum við skoða kóðann sem myndaður er af JIT þýðandanum:

# 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

ekki mjög árangursríkt fyrir exit(2), en í sanngirni er forritið okkar of einfalt, og fyrir forrit sem ekki eru léttvæg, þarf að sjálfsögðu frummálið og eftirmálið sem JIT þýðandinn hefur bætt við.

Maps

BPF forrit geta notað skipulögð minnissvæði sem eru aðgengileg bæði öðrum BPF forritum og forritum í notendarými. Þessir hlutir eru kallaðir kort og í þessum hluta munum við sýna hvernig á að vinna með þá með því að nota kerfiskall bpf.

Segjum strax að möguleikar korta takmarkast ekki aðeins við aðgang að sameiginlegu minni. Það eru sérstök kort sem innihalda til dæmis ábendingar í BPF forrit eða ábendingar um netviðmót, kort til að vinna með perf atburði o.s.frv. Við munum ekki tala um þau hér til að rugla ekki lesandann. Fyrir utan þetta, hunsum við samstillingarvandamál, þar sem þetta er ekki mikilvægt fyrir okkar dæmi. Heildarlista yfir tiltækar kortagerðir má finna í <linux/bpf.h>, og í þessum hluta munum við taka sem dæmi sögulega fyrstu gerð, kjötkássatöfluna BPF_MAP_TYPE_HASH.

Ef þú býrð til kjötkássatöflu í td C++ myndirðu segja unordered_map<int,long> woo, sem á rússnesku þýðir „Ég þarf borð woo ótakmarkað stærð, þar sem lyklar eru af gerðinni int, og gildin eru gerð long" Til þess að búa til BPF kjötkássatöflu, þurfum við að gera það sama, nema að við verðum að tilgreina hámarksstærð töflunnar, og í stað þess að tilgreina tegundir lykla og gilda, þurfum við að tilgreina stærð þeirra í bætum . Notaðu skipunina til að búa til kort BPF_MAP_CREATE kerfiskall bpf. Skoðum meira og minna lágmarksforrit sem býr til kort. Eftir fyrra forritið sem hleður BPF forritum ætti þetta að virðast einfalt fyrir þig:

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

Hér skilgreinum við sett af breytum attr, þar sem við segjum „Ég þarf kjötkássatöflu með lyklum og stærðargildum sizeof(int), þar sem ég get sett að hámarki fjóra þætti." Þegar þú býrð til BPF kort geturðu tilgreint aðrar breytur, til dæmis á sama hátt og í dæminu með forritinu, við tilgreindum nafn hlutarins sem "woo".

Við skulum setja saman og keyra forritið:

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

Hér er kerfiskallið bpf(2) skilaði okkur lýsingarkortanúmerinu 3 og þá bíður forritið, eins og búist var við, eftir frekari leiðbeiningum í kerfiskallinu pause(2).

Nú skulum við senda forritið okkar í bakgrunninn eða opna aðra flugstöð og skoða hlutinn okkar með því að nota tólið bpftool (við getum greint kortið okkar frá öðrum með nafni þess):

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

Talan 114 er alþjóðlegt auðkenni hlutarins okkar. Hvaða forrit sem er í kerfinu getur notað þetta auðkenni til að opna núverandi kort með skipuninni BPF_MAP_GET_FD_BY_ID kerfiskall bpf.

Nú getum við spilað með kjötkássaborðinu okkar. Við skulum skoða innihald þess:

$ sudo bpftool map dump id 114
Found 0 elements

Tómt. Við skulum setja gildi í það hash[1] = 1:

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

Við skulum líta á töfluna aftur:

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

Húrra! Okkur tókst að bæta við einum þætti. Athugaðu að við verðum að vinna á bætistigi til að gera þetta, þar sem bptftool veit ekki hvaða tegund gildin í kjötkássatöflunni eru. (Þessa þekkingu er hægt að flytja til hennar með BTF, en meira um það núna.)

Hvernig nákvæmlega les bpftool og bætir við þáttum? Við skulum kíkja undir hettuna:

$ 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

Fyrst opnuðum við kortið með alþjóðlegu auðkenni þess með því að nota skipunina BPF_MAP_GET_FD_BY_ID и bpf(2) skilaði okkur lýsingu 3. Notaðu skipunina frekar BPF_MAP_GET_NEXT_KEY við fundum fyrsta lykilinn í töflunni með því að fara framhjá NULL sem bendi á „fyrri“ takkann. Ef við höfum lykilinn getum við gert það BPF_MAP_LOOKUP_ELEMsem skilar gildi til bendils value. Næsta skref er að við reynum að finna næsta þátt með því að senda bendi í núverandi lykil, en taflan okkar inniheldur aðeins einn þátt og skipunina BPF_MAP_GET_NEXT_KEY skilar ENOENT.

Allt í lagi, við skulum breyta gildinu með lykli 1, segjum að viðskiptarökfræði okkar krefjist skráningar 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

Eins og við var að búast er það mjög einfalt: skipunin BPF_MAP_GET_FD_BY_ID opnar kortið okkar með auðkenni og skipunina BPF_MAP_UPDATE_ELEM skrifar yfir þáttinn.

Svo, eftir að hafa búið til kjötkássatöflu úr einu forriti, getum við lesið og skrifað innihald hennar úr öðru. Athugaðu að ef við gætum gert þetta frá skipanalínunni, þá getur hvert annað forrit í kerfinu gert það. Til viðbótar við skipanirnar sem lýst er hér að ofan, til að vinna með kort úr notendarými, Eftirfarandi:

  • BPF_MAP_LOOKUP_ELEM: finna gildi eftir lykli
  • BPF_MAP_UPDATE_ELEM: uppfæra/búa til gildi
  • BPF_MAP_DELETE_ELEM: fjarlægðu lykilinn
  • BPF_MAP_GET_NEXT_KEY: finndu næsta (eða fyrsta) takkann
  • BPF_MAP_GET_NEXT_ID: gerir þér kleift að fara í gegnum öll núverandi kort, það er hvernig það virkar bpftool map
  • BPF_MAP_GET_FD_BY_ID: opna núverandi kort með alþjóðlegu auðkenni þess
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: frumeindauppfærðu gildi hlutar og skilaðu þeim gamla
  • BPF_MAP_FREEZE: Gerðu kortið óbreytanlegt frá notendasvæði (ekki er hægt að afturkalla þessa aðgerð)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: fjöldaaðgerðir. Til dæmis, BPF_MAP_LOOKUP_AND_DELETE_BATCH - þetta er eina áreiðanlega leiðin til að lesa og endurstilla öll gildi af kortinu

Ekki virka allar þessar skipanir fyrir allar kortagerðir, en almennt lítur vinna með aðrar gerðir af kortum úr notendarými nákvæmlega eins út og að vinna með kjötkássatöflum.

Fyrir regluna þá skulum við klára kjötkássatöflutilraunirnar okkar. Mundu að við bjuggum til töflu sem getur innihaldið allt að fjóra lykla? Við skulum bæta við nokkrum þáttum í viðbót:

$ 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

Svo langt svo gott:

$ 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

Við skulum reyna að bæta einu við:

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

Eins og við var að búast tókst okkur það ekki. Við skulum skoða villuna nánar:

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

Allt er í lagi: eins og við var að búast, liðið BPF_MAP_UPDATE_ELEM reynir að búa til nýjan, fimmta lykil, en hrynur E2BIG.

Þannig að við getum búið til og hlaðið BPF forritum, sem og búið til og stjórnað kortum úr notendarými. Nú er rökrétt að skoða hvernig við getum notað kort úr BPF forritunum sjálfum. Við gætum talað um þetta á tungumáli erfitt að lesa forrit í makrókóðum véla, en í raun er kominn tími til að sýna hvernig BPF forrit eru í raun skrifuð og viðhaldið - með libbpf.

(Fyrir lesendur sem eru óánægðir með skort á lágu stigi dæmi: við munum greina ítarlega forrit sem nota kort og hjálparaðgerðir sem eru búnar til með libbpf og segja þér hvað gerist á kennslustigi. Fyrir lesendur sem eru óánægðir mjög mikið, bættum við við Dæmi á viðeigandi stað í greininni.)

Að skrifa BPF forrit með libbpf

Að skrifa BPF forrit með því að nota vélakóða getur aðeins verið áhugavert í fyrsta skiptið og þá setur mettunin inn. Á þessari stundu þarftu að beina athyglinni að llvm, sem hefur bakenda til að búa til kóða fyrir BPF arkitektúrinn, sem og bókasafn libbpf, sem gerir þér kleift að skrifa notendahlið BPF forrita og hlaða kóða BPF forrita sem eru búin til með llvm/clang.

Reyndar, eins og við munum sjá í þessari og síðari greinum, libbpf gerir töluvert mikla vinnu án þess (eða svipuð verkfæri - iproute2, libbcc, libbpf-goo.s.frv.) það er ómögulegt að lifa. Einn af drápseiginleikum verkefnisins libbpf er BPF CO-RE (Compile Once, Run Everywhere) - verkefni sem gerir þér kleift að skrifa BPF forrit sem eru færanleg frá einum kjarna til annars, með getu til að keyra á mismunandi API (til dæmis þegar kjarnabyggingin breytist úr útgáfu til útgáfu). Til þess að geta unnið með CO-RE verður kjarninn þinn að vera settur saman með BTF stuðningi (við lýsum því hvernig á að gera þetta í kaflanum Þróunarverkfæri. Þú getur athugað hvort kjarninn þinn sé byggður með BTF eða ekki mjög einfaldlega - með því að eftirfarandi skrá er til staðar:

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

Þessi skrá geymir upplýsingar um allar gagnategundir sem notaðar eru í kjarnanum og er notuð í öllum dæmum okkar libbpf. Við munum tala ítarlega um CO-RE í næstu grein, en í þessari - smíðaðu þér bara kjarna með CONFIG_DEBUG_INFO_BTF.

Bókasafnið libbpf býr rétt í möppunni tools/lib/bpf kjarna og þróun hans fer fram í gegnum póstlistann [email protected]. Hins vegar er sérstakri geymsla viðhaldið fyrir þarfir forrita sem búa utan kjarnans https://github.com/libbpf/libbpf þar sem kjarnasafnið er speglað fyrir lesaðgang nokkurn veginn eins og það er.

Í þessum hluta munum við skoða hvernig þú getur búið til verkefni sem notar libbpf, við skulum skrifa nokkur (meira eða minna tilgangslaus) prófunarforrit og greina ítarlega hvernig þetta virkar allt saman. Þetta gerir okkur auðveldara að útskýra í eftirfarandi köflum nákvæmlega hvernig BPF forrit hafa samskipti við kort, kjarnahjálpar, BTF osfrv.

Venjulega verkefni sem nota libbpf bæta við GitHub geymslu sem git undireiningu, við gerum það 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.

Fara að libbpf mjög einfalt:

$ 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

Næsta áætlun okkar í þessum hluta er sem hér segir: við munum skrifa BPF forrit eins og BPF_PROG_TYPE_XDP, það sama og í fyrra dæmi, en í C setjum við það saman með því að nota clang, og skrifaðu hjálparforrit sem mun hlaða því inn í kjarnann. Í eftirfarandi köflum munum við auka getu bæði BPF forritsins og aðstoðarforritsins.

Dæmi: búa til fullbúið forrit með libbpf

Til að byrja með notum við skrána /sys/kernel/btf/vmlinux, sem minnst var á hér að ofan, og búa til jafngildi þess í formi hausskrár:

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

Þessi skrá mun geyma öll gagnaskipulag sem er tiltækt í kjarnanum okkar, til dæmis er þetta hvernig IPv4 hausinn er skilgreindur í kjarnanum:

$ 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ú munum við skrifa BPF forritið okkar í 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";

Þó að forritið okkar hafi reynst mjög einfalt þurfum við samt að huga að mörgum smáatriðum. Í fyrsta lagi er fyrsta hausskráin sem við tökum með vmlinux.h, sem við bjuggum til með því að nota bpftool btf dump - nú þurfum við ekki að setja upp kernel-headers pakkann til að komast að því hvernig kjarnabyggingin lítur út. Eftirfarandi hausskrá kemur til okkar frá bókasafninu libbpf. Nú þurfum við það aðeins til að skilgreina makróið SEC, sem sendir persónuna í viðeigandi hluta ELF-hlutaskrárinnar. Dagskrá okkar er að finna í kaflanum xdp/simple, þar sem á undan skástrikinu skilgreinum við forritagerðina BPF - þetta er venjan sem notuð er í libbpf, byggt á hlutaheitinu mun það koma í staðinn fyrir rétta gerð við ræsingu bpf(2). BPF forritið sjálft er C - mjög einfalt og samanstendur af einni línu return XDP_PASS. Að lokum sérstakur kafli "license" inniheldur nafn leyfisins.

Við getum sett saman forritið okkar með því að nota llvm/clang, útgáfu >= 10.0.0, eða betra enn, stærri (sjá kafla Þróunarverkfæri):

$ clang --version
clang version 11.0.0 (https://github.com/llvm/llvm-project.git afc287e0abec710398465ee1f86237513f2b5091)
...

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o

Meðal áhugaverðra eiginleika: við tilgreinum markarkitektúrinn -target bpf og leiðin að hausunum libbpf, sem við settum nýlega upp. Einnig, ekki gleyma um -O2, án þessa valkosts gætirðu komið þér á óvart í framtíðinni. Við skulum skoða kóðann okkar, tókst okkur að skrifa forritið sem við vildum?

$ 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

Já, það virkaði! Nú höfum við tvöfalda skrá með forritinu og við viljum búa til forrit sem hleður því inn í kjarnann. Í þessu skyni er bókasafnið libbpf býður okkur upp á tvo valkosti - notaðu lægra stigi API eða hærra stigi API. Við munum fara aðra leiðina, þar sem við viljum læra hvernig á að skrifa, hlaða og tengja BPF forrit með lágmarks fyrirhöfn fyrir síðari nám þeirra.

Í fyrsta lagi þurfum við að búa til „beinagrind“ forritsins okkar úr tvöfaldri þess með því að nota sama tólið bpftool — svissneskur hnífur BPF heimsins (sem hægt er að taka bókstaflega, þar sem Daniel Borkman, einn af höfundum og viðhaldsaðilum BPF, er Svisslendingur):

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

Í skrá xdp-simple.skel.h inniheldur tvöfalda kóða forritsins okkar og aðgerðir til að stjórna - hlaða, hengja, eyða hlutnum okkar. Í okkar einfalda tilviki lítur þetta út fyrir að vera of mikið, en það virkar líka í því tilviki þar sem hlutskráin inniheldur mörg BPF forrit og kort og til að hlaða þessu risastóra ELF þurfum við bara að búa til beinagrindina og kalla eina eða tvær aðgerðir úr sérsniðna forritinu sem við eru að skrifa Höldum áfram núna.

Strangt til tekið er hleðsluforritið okkar léttvægt:

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

Hér struct xdp_simple_bpf skilgreind í skránni xdp-simple.skel.h og lýsir hlutskránni okkar:

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

Við getum séð ummerki um lágstig API hér: uppbyggingin struct bpf_program *simple и struct bpf_link *simple. Fyrsta uppbyggingin lýsir forritinu okkar sérstaklega, skrifað í kaflanum xdp/simple, og önnur lýsir því hvernig forritið tengist viðburðargjafanum.

Virka xdp_simple_bpf__open_and_load, opnar ELF hlut, flokkar hann, býr til öll mannvirki og undirbyggingu (fyrir utan forritið inniheldur ELF einnig aðra hluta - gögn, skrifvarið gögn, villuleitarupplýsingar, leyfi osfrv.) og hleður þeim svo inn í kjarnann með því að nota kerfi hringja bpf, sem við getum athugað með því að setja saman og keyra forritið:

$ 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

Við skulum nú líta á forritið okkar með því að nota bpftool. Við skulum finna auðkenni hennar:

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

og dump (við notum stytta mynd af skipuninni bpftool prog dump xlated):

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

Eitthvað nýtt! Forritið prentaði bita af C frumskránni okkar. Þetta var gert af bókasafninu libbpf, sem fann villuleitarhlutann í tvöfaldanum, setti hann saman í BTF hlut, hlóð honum inn í kjarnann með því að nota BPF_BTF_LOAD, og tilgreindi síðan skráarlýsinguna sem myndast þegar forritið er hlaðið með skipuninni BPG_PROG_LOAD.

Kjarnahjálparar

BPF forrit geta keyrt „ytri“ aðgerðir - kjarnahjálpar. Þessar hjálparaðgerðir gera BPF forritum kleift að fá aðgang að kjarnabyggingum, stjórna kortum og einnig eiga samskipti við „raunverulega heiminn“ - búa til perf atburði, stjórna vélbúnaði (til dæmis endurbeina pökkum) o.s.frv.

Dæmi: bpf_get_smp_processor_id

Innan ramma hugmyndafræðinnar „að læra með fordæmi“ skulum við íhuga eina af hjálparaðgerðunum, bpf_get_smp_processor_id(), viss í skrá kernel/bpf/helpers.c. Það skilar númeri örgjörvans sem BPF forritið sem kallaði það er í gangi á. En við höfum ekki eins áhuga á merkingarfræði þess og þeirri staðreynd að útfærsla þess tekur eina línu:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Skilgreiningar BPF hjálparaðgerða eru svipaðar skilgreiningum Linux kerfiskalla. Hér er til dæmis fall skilgreint sem hefur engin rök. (Funkið sem tekur til dæmis þrjár röksemdir er skilgreint með fjölvi BPF_CALL_3. Hámarksfjöldi röksemda er fimm.) Þetta er þó aðeins fyrsti hluti skilgreiningarinnar. Seinni hlutinn er að skilgreina gerð byggingarinnar struct bpf_func_proto, sem inniheldur lýsingu á hjálparaðgerðinni sem sannprófandi skilur:

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

Skráning hjálparaðgerða

Til þess að BPF forrit af tiltekinni gerð geti notað þessa aðgerð verða þau að skrá hana, til dæmis fyrir tegundina BPF_PROG_TYPE_XDP fall er skilgreint í kjarnanum xdp_func_proto, sem ákvarðar út frá auðkenni hjálparaðgerðarinnar hvort XDP styður þessa aðgerð eða ekki. Hlutverk okkar er styður:

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

Nýjar BPF forritagerðir eru "skilgreindar" í skránni include/linux/bpf_types.h með því að nota macro BPF_PROG_TYPE. Skilgreint í gæsalöppum vegna þess að það er rökrétt skilgreining, og í C tungumálaskilmálum kemur skilgreining á heilu mengi steinsteyptra mannvirkja fyrir á öðrum stöðum. Sérstaklega í skránni kernel/bpf/verifier.c allar skilgreiningar úr skrá bpf_types.h eru notuð til að búa til fjölda mannvirkja 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
};

Það er að segja að fyrir hverja tegund af BPF forriti er bendi á gagnaskipulag af gerðinni skilgreindur struct bpf_verifier_ops, sem er frumstillt með gildinu _name ## _verifier_ops, þ.e.a.s. xdp_verifier_ops í xdp... Uppbygging xdp_verifier_ops ákvörðuð af í skrá net/core/filter.c sem hér segir:

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

Hér sjáum við kunnuglega virkni okkar xdp_func_proto, sem mun keyra sannprófandann í hvert sinn sem hann lendir í áskorun einhvers konar virka inni í BPF forriti, sjá verifier.c.

Við skulum skoða hvernig ímyndað BPF forrit notar aðgerðina bpf_get_smp_processor_id. Til að gera þetta endurskrifum við forritið frá fyrri hluta okkar sem hér segir:

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

Táknið bpf_get_smp_processor_id ákvörðuð af в <bpf/bpf_helper_defs.h> bókasöfn libbpf sem

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

það er, bpf_get_smp_processor_id er fallbendi sem hefur gildið 8, þar sem 8 er gildið BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, sem er skilgreint fyrir okkur í skránni vmlinux.h (skrá bpf_helper_defs.h í kjarnanum er búið til af handriti, þannig að "töfra" tölurnar eru í lagi). Þessi aðgerð tekur engin rök og skilar gildi af gerðinni __u32. Þegar við keyrum það í forritinu okkar, clang býr til leiðbeiningar BPF_CALL "rétta tegundin" Við skulum setja saman forritið og skoða kaflann 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

Í fyrstu línu sjáum við leiðbeiningar call, færibreyta IMM sem er jafnt og 8, og SRC_REG - núll. Samkvæmt ABI samningnum sem sannprófandi notar er þetta ákall til hjálparaðgerðar númer átta. Þegar það er hleypt af stokkunum er rökfræðin einföld. Skila gildi úr skrá r0 afritað til r1 og á línum 2,3 er því breytt í gerð u32 — efri 32 bitarnir eru hreinsaðir. Á línum 4,5,6,7 skilum við 2 (XDP_PASS) eða 1 (XDP_DROP) eftir því hvort hjálparfallið úr línu 0 skilaði núlli eða ekki núlli.

Við skulum prófa okkur sjálf: hlaða forritinu og skoða úttakið 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

Allt í lagi, sannprófandi fann rétta kjarnahjálparann.

Dæmi: fara framhjá rökum og loksins keyra forritið!

Allar hjálparaðgerðir á keyrslustigi eru með frumgerð

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

Færibreytur til hjálparaðgerða eru sendar í skrám r1-r5, og gildið er skilað í skránni r0. Það eru engar aðgerðir sem taka meira en fimm rök og ekki er búist við að stuðningur við þær verði bætt við í framtíðinni.

Við skulum skoða nýja kjarnahjálparann ​​og hvernig BPF sendir breytur. Við skulum endurskrifa xdp-simple.bpf.c sem hér segir (afgangurinn af línunum hefur ekki breyst):

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

Forritið okkar prentar númer örgjörvans sem það er í gangi á. Við skulum setja það saman og skoða kóðann:

$ 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

Í línum 0-7 skrifum við strenginn running on CPU%un, og svo á línu 8 keyrum við kunnuglega bpf_get_smp_processor_id. Á línum 9-12 undirbúum við hjálparrök bpf_printk - skráir r1, r2, r3. Af hverju eru þeir þrír en ekki tveir? Vegna þess að bpf_printkþetta er macro umbúðir í kringum hinn raunverulega aðstoðarmann bpf_trace_printk, sem þarf að standast stærð sniðstrengsins.

Við skulum nú bæta nokkrum línum við xdp-simple.cþannig að forritið okkar tengist viðmótinu lo og byrjaði virkilega!

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

Hér notum við aðgerðina bpf_set_link_xdp_fd, sem tengir XDP-gerð BPF forrit við netviðmót. Við harðkóða viðmótsnúmerið lo, sem er alltaf 1. Við keyrum aðgerðina tvisvar til að aftengja fyrst gamla forritið ef það var tengt. Taktu eftir að nú þurfum við ekki áskorun pause eða óendanlega lykkju: hleðsluforritið okkar mun hætta, en BPF forritið verður ekki drepið þar sem það er tengt við viðburðargjafann. Eftir vel heppnaða niðurhal og tengingu verður forritið ræst fyrir hvern netpakka sem kemur á lo.

Við skulum hlaða niður forritinu og skoða viðmótið 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

Forritið sem við sóttum hefur ID 669 og við sjáum sama auðkenni á viðmótinu lo. Við sendum nokkra pakka til 127.0.0.1 (beiðni + svar):

$ ping -c1 localhost

og nú skulum við skoða innihald villuleitar sýndarskrárinnar /sys/kernel/debug/tracing/trace_pipe, þar sem bpf_printk skrifar skilaboðin sín:

# 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

Tveir pakkar sáust á lo og unnið á CPU0 - fyrsta fullgilda tilgangslausa BPF forritið okkar virkaði!

Vert er að taka það fram bpf_printk Það er ekki fyrir neitt sem það skrifar í villuleitarskrána: þetta er ekki farsælasta hjálparinn til notkunar í framleiðslu, en markmið okkar var að sýna eitthvað einfalt.

Aðgangur að kortum frá BPF forritum

Dæmi: að nota kort úr BPF forritinu

Í fyrri köflum lærðum við hvernig á að búa til og nota kort úr notendarými og nú skulum við skoða kjarnahlutann. Byrjum eins og venjulega á dæmi. Endurskrifum prógrammið okkar xdp-simple.bpf.c sem hér segir:

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

Í upphafi forritsins bættum við við kortaskilgreiningu woo: Þetta er 8-þátta fylki sem geymir gildi eins og u64 (í C myndum við skilgreina slíkt fylki sem u64 woo[8]). Í forriti "xdp/simple" við fáum núverandi örgjörvanúmer í breytu key og nota síðan hjálparaðgerðina bpf_map_lookup_element við fáum bendi á samsvarandi færslu í fylkinu, sem við aukum um einn. Þýtt yfir á rússnesku: við reiknum út tölfræði um hvaða örgjörvi afgreiddi komandi pakka. Við skulum reyna að keyra forritið:

$ 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

Við skulum athuga hvort hún sé tengd við lo og sendu nokkra pakka:

$ 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

Nú skulum við líta á innihald fylkisins:

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

Næstum öll ferli voru unnin á CPU7. Þetta skiptir okkur ekki máli, aðalatriðið er að forritið virki og við skiljum hvernig á að nálgast kort úr BPF forritum - með хелперов bpf_mp_*.

Dulræn vísitala

Þannig að við getum nálgast kortið frá BPF forritinu með því að nota símtöl eins og

val = bpf_map_lookup_elem(&woo, &key);

þar sem hjálparaðgerðin lítur út

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

en við sendum vísbendingu &woo að ónefndu mannvirki struct { ... }...

Ef við skoðum forritasamsetninguna sjáum við að gildið &woo er í raun ekki skilgreint (lína 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
...

og er innifalið í flutningum:

$ 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

En ef við skoðum forritið sem þegar er hlaðið, sjáum við bendi í rétta kortið (lína 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]
...

Þannig getum við ályktað að á þeim tíma sem hleðsluforritið okkar er opnað hafi hlekkurinn á &woo var skipt út fyrir eitthvað með bókasafni libbpf. Fyrst munum við skoða úttakið 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

Við sjáum það libbpf búið til kort woo og sótti svo forritið okkar simple. Við skulum skoða nánar hvernig við hleðum forritinu:

  • hringja xdp_simple_bpf__open_and_load úr skrá xdp-simple.skel.h
  • sem veldur xdp_simple_bpf__load úr skrá xdp-simple.skel.h
  • sem veldur bpf_object__load_skeleton úr skrá libbpf/src/libbpf.c
  • sem veldur bpf_object__load_xattr á libbpf/src/libbpf.c

Síðasta fallið mun meðal annars kalla bpf_object__create_maps, sem býr til eða opnar núverandi kort og breytir þeim í skráarlýsingar. (Þetta er þar sem við sjáum BPF_MAP_CREATE í úttakinu strace.) Næst er fallið kallað bpf_object__relocate og það er hún sem hefur áhuga á okkur, þar sem við munum eftir því sem við sáum woo í flutningstöflunni. Þegar við skoðum það, finnum við okkur að lokum í aðgerðinni bpf_program__relocate, sem fjallar um kortaflutninga:

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

Svo við tökum fyrirmæli okkar

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

og skipta út heimildaskránni í henni fyrir BPF_PSEUDO_MAP_FD, og fyrsta IMM í skráarlýsingu kortsins okkar og, ef það er jafnt og td, 0xdeadbeef, þá fáum við fræðsluna í kjölfarið

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

Þannig eru kortaupplýsingar fluttar yfir í ákveðið hlaðið BPF forrit. Í þessu tilviki er hægt að búa til kortið með því að nota BPF_MAP_CREATE, og opnað með auðkenni með því að nota BPF_MAP_GET_FD_BY_ID.

Samtals, við notkun libbpf reikniritið er sem hér segir:

  • við samantekt eru færslur búnar til í flutningstöflunni fyrir tengla á kort
  • libbpf opnar ELF hlutabókina, finnur öll notuð kort og býr til skráarlýsingar fyrir þau
  • skráarlýsingar eru hlaðnar inn í kjarnann sem hluti af leiðbeiningunum LD64

Eins og þú getur ímyndað þér er meira að koma og við verðum að skoða kjarnann. Sem betur fer höfum við vísbendingu - við höfum skrifað niður merkinguna BPF_PSEUDO_MAP_FD inn í heimildaskrána og við getum grafið hana, sem mun leiða okkur til hins heilaga allra heilagra - kernel/bpf/verifier.c, þar sem fall með áberandi nafni kemur í stað skráarlýsingar fyrir heimilisfang tegundargerðar 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;

(allur kóða er að finna по ссылке). Svo við getum aukið reikniritið okkar:

  • meðan á hleðslu forritsins stendur, athugar sannprófandi rétta notkun kortsins og skrifar heimilisfang samsvarandi skipulags struct bpf_map

Þegar þú hleður niður ELF tvöfaldur með libbpf Það er margt fleira að gerast, en við munum ræða það í öðrum greinum.

Hleður forritum og kortum án libbpf

Eins og lofað var, hér er dæmi fyrir lesendur sem vilja vita hvernig á að búa til og hlaða forriti sem notar kort, án hjálpar libbpf. Þetta getur verið gagnlegt þegar þú ert að vinna í umhverfi sem þú getur ekki byggt upp ósjálfstæði fyrir, eða vistað hvern bita eða skrifar forrit eins og ply, sem býr til BPF tvöfaldur kóða á flugu.

Til að gera það auðveldara að fylgja rökfræðinni munum við endurskrifa dæmið okkar í þessum tilgangi xdp-simple. Fullkominn og örlítið stækkaður kóða forritsins sem fjallað er um í þessu dæmi má finna í þessu Gist.

Rökfræði umsóknar okkar er sem hér segir:

  • búa til tegundakort BPF_MAP_TYPE_ARRAY með því að nota skipunina BPF_MAP_CREATE,
  • búa til forrit sem notar þetta kort,
  • tengja forritið við viðmótið lo,

sem þýðist í mannlegt sem

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

Hér map_create býr til kort á sama hátt og við gerðum í fyrsta dæminu um kerfiskallið bpf - "kjarna, vinsamlegast búðu til nýtt kort fyrir mig í formi fylkis af 8 þáttum eins og __u64 og gefðu mér til baka skráarlýsinguna":

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

Einnig er auðvelt að hlaða forritinu:

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

Erfiði hlutinn prog_load er skilgreiningin á BPF áætluninni okkar sem fjölda mannvirkja struct bpf_insn insns[]. En þar sem við erum að nota forrit sem við erum með í C, þá getum við svindlað aðeins:

$ 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

Alls þurfum við að skrifa 14 leiðbeiningar í formi mannvirkja eins og struct bpf_insn (ráð: taktu sorphauginn að ofan, lestu aftur leiðbeiningarhlutann, opnaðu linux/bpf.h и linux/bpf_common.h og reyndu að ákveða struct bpf_insn insns[] á eigin spýtur):

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

Æfing fyrir þá sem skrifuðu þetta ekki sjálfir - finndu map_fd.

Það er enn einn ótilgreindur hluti eftir í dagskránni okkar - xdp_attach. Því miður er ekki hægt að tengja forrit eins og XDP með kerfissímtali bpf. Fólkið sem bjó til BPF og XDP var frá Linux samfélaginu á netinu, sem þýðir að þeir notuðu það sem þeir þekkja best (en ekki til að eðlilegt people) tengi fyrir samskipti við kjarnann: netlink innstungur, sjá einnig RFC3549. Einfaldasta leiðin til að framkvæma xdp_attach er að afrita kóða frá libbpf, nefnilega úr skránni netlink.c, sem er það sem við gerðum, styttum það aðeins:

Velkomin í heim netlink innstungna

Opnaðu netlink fals gerð 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;
}

Við lesum úr þessari innstungu:

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

Að lokum, hér er aðgerðin okkar sem opnar fals og sendir sérstök skilaboð til hennar sem inniheldur skráarlýsingu:

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

Svo, allt er tilbúið til prófunar:

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

Við skulum sjá hvort forritið okkar hefur tengst 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

Sendum ping og skoðum kortið:

$ 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

Húrra, allt virkar. Athugaðu, við the vegur, að kortið okkar birtist aftur í formi bæta. Þetta er vegna þess að ólíkt libbpf við hlóðum ekki upplýsingar um tegund (BTF). En við tölum meira um þetta næst.

Þróunarverkfæri

Í þessum hluta munum við skoða lágmarks BPF verkfærasett fyrir þróunaraðila.

Almennt séð þarftu ekkert sérstakt til að þróa BPF forrit - BPF keyrir á hvaða almennilegu dreifingarkjarna sem er og forrit eru smíðuð með clang, sem hægt er að fá úr pakkanum. Hins vegar, vegna þess að BPF er í þróun, eru kjarninn og verkfærin að breytast stöðugt, ef þú vilt ekki skrifa BPF forrit með gamaldags aðferðum frá 2019, þá verður þú að setja saman

  • llvm/clang
  • pahole
  • kjarni þess
  • bpftool

(Til tilvísunar var þessi hluti og öll dæmi í greininni keyrð á Debian 10.)

llvm/clang

BPF er vingjarnlegur við LLVM og þó nýlega sé hægt að setja saman forrit fyrir BPF með gcc, þá fer öll núverandi þróun fram fyrir LLVM. Þess vegna munum við fyrst og fremst byggja núverandi útgáfu clang frá 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
... много времени спустя
$

Nú getum við athugað hvort allt hafi komið rétt saman:

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

(Samsetningarleiðbeiningar clang tekin af mér frá bpf_devel_QA.)

Við munum ekki setja upp forritin sem við bjuggum til, heldur bara bæta þeim við PATH, til dæmis:

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

(Hægt að bæta þessu við .bashrc eða í sérstaka skrá. Persónulega bæti ég svona hlutum við ~/bin/activate-llvm.sh og þegar nauðsyn krefur geri ég það . activate-llvm.sh.)

Pahole og BTF

Gagnsemi pahole notað þegar kjarnann er byggður til að búa til villuleitarupplýsingar á BTF sniði. Við munum ekki fara í smáatriði í þessari grein um smáatriði BTF tækni, annað en þá staðreynd að það er þægilegt og við viljum nota það. Svo ef þú ætlar að byggja kjarnann þinn skaltu byggja fyrst pahole (án pahole þú munt ekki geta byggt kjarnann með möguleikanum 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

Kjarni til að gera tilraunir með BPF

Þegar ég kanna möguleika BPF vil ég setja saman minn eigin kjarna. Þetta er almennt talað ekki nauðsynlegt, þar sem þú munt geta sett saman og hlaðið BPF forritum á dreifingarkjarnann, þó að hafa þinn eigin kjarna gerir þér kleift að nota nýjustu BPF eiginleikana, sem munu birtast í dreifingunni þinni í besta falli , eða, eins og í tilfelli sumra kembiforrita, verður alls ekki pakkað í fyrirsjáanlegri framtíð. Einnig gerir eigin kjarni þess mikilvægt að gera tilraunir með kóðann.

Til þess að byggja upp kjarna þarftu í fyrsta lagi kjarnann sjálfan og í öðru lagi kjarnastillingarskrá. Til að gera tilraunir með BPF getum við notað venjulega vanillu kjarna eða einn af þróunarkjarnanum. Sögulega séð fer BPF þróun fram innan Linux netsamfélagsins og því fara allar breytingar fyrr eða síðar í gegnum David Miller, Linux netviðhaldara. Það fer eftir eðli þeirra - breytingar eða nýir eiginleikar - netbreytingar falla í annan af tveimur kjarna - net eða net-next. Breytingar fyrir BPF dreifast á sama hátt á milli bpf и bpf-next, sem síðan eru sameinuð í net og net-næst, í sömu röð. Fyrir frekari upplýsingar, sjá bpf_devel_QA и netdev-algengar spurningar. Svo veldu kjarna út frá smekk þínum og stöðugleikaþörfum kerfisins sem þú ert að prófa á (*-next kjarna eru óstöðugastir af þeim sem eru taldir upp).

Það er utan gildissviðs þessarar greinar að tala um hvernig á að stjórna kjarnastillingarskrám - það er gert ráð fyrir að þú annað hvort veist nú þegar hvernig á að gera þetta, eða tilbúinn til að læra á eigin spýtur. Hins vegar ættu eftirfarandi leiðbeiningar að vera meira og minna nóg til að gefa þér virkt BPF-virkt kerfi.

Sæktu einn af ofangreindum kjarna:

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

Búðu til lágmarks virka kjarnastillingu:

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

Virkjaðu BPF valkosti í skrá .config að eigin vali (líklegast CONFIG_BPF verður nú þegar virkt þar sem systemd notar það). Hér er listi yfir valkosti úr kjarnanum sem notaður er fyrir þessa grein:

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

Þá getum við auðveldlega sett saman og sett upp einingarnar og kjarnann (við the vegur, þú getur sett saman kjarnann með því að nota nýsamsettan clangmeð því að bæta við CC=clang):

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

og endurræstu með nýja kjarnanum (ég nota fyrir þetta kexec úr pakkanum 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

Algengasta tólið í greininni er tólið bpftool, afhent sem hluti af Linux kjarnanum. Það er skrifað og viðhaldið af BPF forriturum fyrir BPF forritara og hægt að nota það til að stjórna öllum gerðum BPF hlutum - hlaða forritum, búa til og breyta kortum, kanna líf BPF vistkerfisins o.s.frv. Hægt er að finna skjöl í formi frumkóða fyrir man síður í kjarnanum eða, þegar tekið saman, á netinu.

Þegar þetta er skrifað bpftool kemur aðeins tilbúið fyrir RHEL, Fedora og Ubuntu (sjá td. þessum þræði, sem segir ólokið sögu umbúða bpftool í Debian). En ef þú hefur þegar smíðað kjarnann þinn, byggðu þá bpftool eins auðvelt eins og pönnukökur:

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

$

(hér ${linux} - þetta er kjarnaskráin þín.) Eftir að hafa keyrt þessar skipanir bpftool verður safnað í möppu ${linux}/tools/bpf/bpftool og það er hægt að bæta því við slóðina (fyrst af öllu fyrir notandann root) eða afritaðu bara til /usr/local/sbin.

Safna bpftool það er best að nota það síðarnefnda clang, sett saman eins og lýst er hér að ofan, og athugaðu hvort það sé rétt sett saman - með því að nota til dæmis skipunina

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

sem mun sýna hvaða BPF eiginleikar eru virkjaðir í kjarnanum þínum.

Við the vegur, fyrri skipun er hægt að keyra sem

# bpftool f p k

Þetta er gert á hliðstæðan hátt við tólin úr pakkanum iproute2, þar sem við getum til dæmis sagt ip a s eth0 í staðinn fyrir ip addr show dev eth0.

Ályktun

BPF gerir þér kleift að skór flóa til að mæla og breyta virkni kjarnans á áhrifaríkan hátt. Kerfið reyndist mjög vel, í bestu hefðum UNIX: einfalt kerfi sem gerir þér kleift að (endur)forrita kjarnann leyfði miklum fjölda fólks og stofnana að gera tilraunir. Og þó að tilraunirnar, sem og þróun BPF innviðanna sjálfs, sé langt frá því að vera lokið, hefur kerfið nú þegar stöðugt ABI sem gerir þér kleift að byggja upp áreiðanlega og síðast en ekki síst skilvirka viðskiptarökfræði.

Ég vil taka það fram að að mínu mati er tæknin orðin svo vinsæl vegna þess að hún getur það annars vegar spila (arkitektúr vélar er hægt að skilja meira og minna á einu kvöldi), og hins vegar að leysa vandamál sem ekki var hægt að leysa (fallega) áður en hún birtist. Þessir tveir þættir saman neyða fólk til að gera tilraunir og dreyma, sem leiðir til þess að fleiri og nýstárlegri lausnir koma fram.

Þessi grein, þó hún sé ekki sérstaklega stutt, er aðeins kynning á heimi BPF og lýsir ekki „háþróuðum“ eiginleikum og mikilvægum hlutum arkitektúrsins. Áætlunin framundan er eitthvað á þessa leið: næsta grein mun vera yfirlit yfir BPF forritagerðir (það eru 5.8 forritagerðir studdar í 30 kjarnanum), síðan munum við að lokum skoða hvernig á að skrifa alvöru BPF forrit með því að nota kjarnarakningarforrit sem dæmi, þá er kominn tími á ítarlegra námskeið um BPF arkitektúr, fylgt eftir með dæmum um BPF netkerfi og öryggisforrit.

Fyrri greinar í þessum flokki

  1. BPF fyrir litlu börnin, hluti núll: klassískt BPF

Tenglar

  1. BPF og XDP tilvísunarhandbók — skjöl um BPF frá cilium, eða nánar tiltekið frá Daniel Borkman, einum af höfundum og viðhaldsaðilum BPF. Þetta er ein af fyrstu alvarlegu lýsingunum, sem er frábrugðin hinum að því leyti að Daníel veit nákvæmlega hvað hann er að skrifa um og þar eru engin mistök. Sérstaklega lýsir þetta skjal hvernig á að vinna með BPF forrit af XDP og TC gerðum með því að nota hið vel þekkta tól ip úr pakkanum iproute2.

  2. Documentation/networking/filter.txt — upprunalega skrá með skjölum fyrir klassíska og síðan framlengda BPF. Góð lesning ef þú vilt kafa ofan í samsetningarmál og tæknilegar byggingarupplýsingar.

  3. Blogg um BPF frá facebook. Það er sjaldan uppfært, en á viðeigandi hátt, eins og Alexei Starovoitov (höfundur eBPF) og Andrii Nakryiko - (umsjónarmaður) skrifa þar libbpf).

  4. Leyndarmál bpftool. Skemmtilegur twitterþráður frá Quentin Monnet með dæmum og leyndarmálum um notkun bpftool.

  5. Kafa í BPF: listi yfir lesefni. Risastór (og enn viðhaldinn) listi yfir tengla á BPF skjöl frá Quentin Monnet.

Heimild: www.habr.com

Bæta við athugasemd