BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Hapo mwanzo kulikuwa na teknolojia na iliitwa BPF. Tulimtazama uliopita, makala ya Agano la Kale ya mfululizo huu. Mnamo 2013, kupitia juhudi za Alexei Starovoitov na Daniel Borkman, toleo lake lililoboreshwa, lililoboreshwa kwa mashine za kisasa za 64-bit, lilitengenezwa na kujumuishwa kwenye kernel ya Linux. Teknolojia hii mpya iliitwa kwa ufupi BPF ya Ndani, kisha ikaitwa BPF Iliyoongezwa, na sasa, baada ya miaka kadhaa, kila mtu anaiita BPF tu.

Kwa kusema, BPF hukuruhusu kuendesha msimbo wa kiholela uliotolewa na mtumiaji katika nafasi ya kinu cha Linux, na usanifu mpya ulifanikiwa sana hivi kwamba tutahitaji nakala kadhaa zaidi kuelezea matumizi yake yote. (Kitu pekee ambacho watengenezaji hawakufanya vizuri, kama unavyoona kwenye nambari ya utendakazi hapa chini, ilikuwa kuunda nembo nzuri.)

Makala hii inaelezea muundo wa mashine ya BPF virtual, interfaces ya kernel kwa kufanya kazi na BPF, zana za maendeleo, pamoja na maelezo mafupi, mafupi sana ya uwezo uliopo, i.e. kila kitu ambacho tutahitaji katika siku zijazo kwa ajili ya utafiti wa kina wa matumizi ya vitendo ya BPF.
BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Muhtasari wa makala

Utangulizi wa usanifu wa BPF. Kwanza, tutachukua mtazamo wa jicho la ndege wa usanifu wa BPF na kuelezea vipengele vikuu.

Rejesta na mfumo wa amri wa mashine pepe ya BPF. Tayari tuna wazo la usanifu kwa ujumla, tutaelezea muundo wa mashine ya kawaida ya BPF.

Mzunguko wa maisha wa vitu vya BPF, mfumo wa faili wa bpffs. Katika sehemu hii, tutaangalia kwa karibu mzunguko wa maisha ya vitu vya BPF - programu na ramani.

Kusimamia vitu kwa kutumia simu ya mfumo wa bpf. Kwa ufahamu fulani wa mfumo tayari umewekwa, hatimaye tutaangalia jinsi ya kuunda na kuendesha vitu kutoka kwa nafasi ya mtumiaji kwa kutumia simu maalum ya mfumo - bpf(2).

ПишСм ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΡ‹ BPF с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ libbpf. Bila shaka, unaweza kuandika programu kwa kutumia simu ya mfumo. Lakini ni vigumu. Kwa hali halisi zaidi, watengenezaji programu za nyuklia walitengeneza maktaba libbpf. Tutaunda kiunzi msingi cha maombi ya BPF ambacho tutatumia katika mifano ifuatayo.

Wasaidizi wa Kernel. Hapa tutajifunza jinsi programu za BPF zinaweza kufikia vitendaji vya msaidizi wa kernel - zana ambayo, pamoja na ramani, kimsingi huongeza uwezo wa BPF mpya ikilinganishwa na ile ya zamani.

Upatikanaji wa ramani kutoka kwa programu za BPF. Kufikia hatua hii, tutajua vya kutosha kuelewa jinsi tunavyoweza kuunda programu zinazotumia ramani. Na hebu hata tuchunguze kwa haraka kithibitishaji kikuu na chenye nguvu.

Zana za maendeleo. Sehemu ya usaidizi kuhusu jinsi ya kuunganisha huduma zinazohitajika na kernel kwa majaribio.

Hitimisho. Mwishoni mwa makala hiyo, wale wanaosoma hadi hapa watapata maneno yenye kutia moyo na maelezo mafupi ya kile kitakachotokea katika makala zinazofuata. Pia tutaorodhesha idadi ya viungo vya kujisomea kwa wale ambao hawana hamu au uwezo wa kusubiri muendelezo.

Utangulizi wa Usanifu wa BPF

Kabla ya kuanza kuzingatia usanifu wa BPF, tutarejelea mara ya mwisho (oh) kwa BPF ya kawaida, ambayo ilitengenezwa kama jibu la ujio wa mashine za RISC na kutatua tatizo la kuchuja pakiti kwa ufanisi. Usanifu huo ulifanikiwa sana hivi kwamba, baada ya kuzaliwa katika miaka ya tisini huko Berkeley UNIX, iliwekwa kwenye mifumo mingi ya uendeshaji iliyopo, ilinusurika hadi miaka ya ishirini na bado inapata programu mpya.

BPF mpya ilitengenezwa kama jibu la ubiquity wa mashine 64-bit, huduma za wingu na hitaji kubwa la zana za kuunda SDN (Smara kwa mara-diliyosafishwa nkufanya kazi). Iliyoundwa na wahandisi wa mtandao wa kernel kama mbadala iliyoboreshwa ya BPF ya kawaida, BPF mpya miezi sita baadaye ilipata maombi katika kazi ngumu ya kufuatilia mifumo ya Linux, na sasa, miaka sita baada ya kuonekana kwake, tutahitaji makala yote ijayo orodhesha aina tofauti za programu.

Picha za kuchekesha

Katika msingi wake, BPF ni mashine pepe ya sandbox inayokuruhusu kuendesha msimbo "kiholela" katika nafasi ya kernel bila kuathiri usalama. Programu za BPF huundwa katika nafasi ya mtumiaji, kupakiwa kwenye kernel, na kuunganishwa kwenye chanzo fulani cha tukio. Tukio linaweza kuwa, kwa mfano, utoaji wa pakiti kwenye interface ya mtandao, uzinduzi wa kazi fulani ya kernel, nk. Kwa upande wa kifurushi, mpango wa BPF utakuwa na ufikiaji wa data na metadata ya kifurushi (kwa kusoma na, ikiwezekana, kuandika, kulingana na aina ya programu); katika kesi ya kuendesha kazi ya kernel, hoja za kazi, pamoja na viashiria kwa kumbukumbu ya kernel, nk.

Hebu tuangalie kwa karibu mchakato huu. Kuanza, hebu tuzungumze juu ya tofauti ya kwanza kutoka kwa BPF ya kawaida, mipango ambayo iliandikwa katika mkusanyiko. Katika toleo jipya, usanifu ulipanuliwa ili programu ziweze kuandikwa kwa lugha za juu, hasa, bila shaka, katika C. Kwa hili, backend kwa llvm ilitengenezwa, ambayo inakuwezesha kuzalisha bytecode kwa usanifu wa BPF.

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Usanifu wa BPF uliundwa, kwa sehemu, ili kukimbia kwa ufanisi kwenye mashine za kisasa. Ili kufanya kazi hii kwa vitendo, BPF bytecode, ikishapakiwa kwenye kernel, inatafsiriwa kuwa msimbo asilia kwa kutumia kijenzi kinachoitwa mkusanyaji wa JIT (Jsio In Tmimi). Ifuatayo, ikiwa unakumbuka, katika BPF ya kawaida programu ilipakiwa kwenye kernel na kushikamana na chanzo cha tukio kwa atomi - katika muktadha wa simu moja ya mfumo. Katika usanifu mpya, hii hufanyika katika hatua mbili - kwanza, nambari hupakiwa kwenye kernel kwa kutumia simu ya mfumo. bpf(2)na kisha, baadaye, kupitia mifumo mingine ambayo inatofautiana kulingana na aina ya programu, programu inaambatanisha na chanzo cha tukio.

Hapa msomaji anaweza kuwa na swali: iliwezekana? Je, usalama wa utekelezaji wa kanuni kama hizo unahakikishwaje? Usalama wa utekelezaji unahakikishwa kwetu na hatua ya kupakia programu za BPF zinazoitwa verifier (kwa Kiingereza hatua hii inaitwa verifier na nitaendelea kutumia neno la Kiingereza):

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Kithibitishaji ni kichanganuzi tuli ambacho huhakikisha kuwa programu haisumbui utendakazi wa kawaida wa kernel. Hii, kwa njia, haimaanishi kuwa programu haiwezi kuingiliana na uendeshaji wa mfumo - mipango ya BPF, kulingana na aina, inaweza kusoma na kuandika upya sehemu za kumbukumbu ya kernel, maadili ya kurudi kwa kazi, trim, append, kuandika upya. na hata pakiti za mbele za mtandao. Kithibitishaji kinahakikisha kwamba kuendesha programu ya BPF haitaharibu kernel na kwamba programu ambayo, kwa mujibu wa sheria, ina ufikiaji wa kuandika, kwa mfano, data ya pakiti inayotoka, haitaweza kufuta kumbukumbu ya kernel nje ya pakiti. Tutaangalia kithibitishaji kwa undani zaidi katika sehemu inayolingana, baada ya kufahamiana na vifaa vingine vyote vya BPF.

Kwa hivyo tumejifunza nini hadi sasa? Mtumiaji anaandika programu katika C, anapakia kwenye kernel kwa kutumia simu ya mfumo bpf(2), ambapo inakaguliwa na kithibitishaji na kutafsiriwa katika bytecode asili. Kisha mtumiaji sawa au mwingine huunganisha programu kwenye chanzo cha tukio na huanza kutekeleza. Kutenganisha boot na uunganisho ni muhimu kwa sababu kadhaa. Kwanza, kuendesha kithibitishaji ni ghali kiasi na kwa kupakua programu hiyo hiyo mara kadhaa tunapoteza wakati wa kompyuta. Pili, jinsi programu inavyounganishwa inategemea aina yake, na interface moja ya "ulimwengu" iliyotengenezwa mwaka mmoja uliopita inaweza kuwa haifai kwa aina mpya za programu. (Ingawa sasa usanifu unazidi kukomaa, kuna wazo la kuunganisha kiolesura hiki kwa kiwango. libbpf.)

Msomaji makini anaweza kugundua kuwa bado hatujamaliza na picha. Hakika, yote yaliyo hapo juu hayaelezi kwa nini BPF kimsingi inabadilisha picha ikilinganishwa na BPF ya kawaida. Ubunifu wawili ambao huongeza kwa kiasi kikubwa wigo wa utumiaji ni uwezo wa kutumia kumbukumbu iliyoshirikiwa na vitendaji vya msaidizi wa kernel. Katika BPF, kumbukumbu iliyoshirikiwa inatekelezwa kwa kutumia kinachojulikana ramani - miundo ya data iliyoshirikiwa na API maalum. Labda walipata jina hili kwa sababu aina ya kwanza ya ramani kuonekana ilikuwa jedwali la hashi. Kisha safu zilionekana, meza za ndani (per-CPU) za hashi na safu za mitaa, miti ya utafutaji, ramani zilizo na viashiria kwa programu za BPF na mengi zaidi. Kinachotuvutia sasa ni kwamba programu za BPF sasa zina uwezo wa kuendelea kusema kati ya simu na kuzishiriki na programu zingine na nafasi ya mtumiaji.

Ramani hufikiwa kutoka kwa michakato ya mtumiaji kwa kutumia simu ya mfumo bpf(2), na kutoka kwa programu za BPF zinazoendesha kwenye kernel kwa kutumia vitendaji vya msaidizi. Zaidi ya hayo, wasaidizi hawapo tu kufanya kazi na ramani, lakini pia kufikia uwezo mwingine wa kernel. Kwa mfano, programu za BPF zinaweza kutumia vitendaji vya msaidizi kusambaza pakiti kwa miingiliano mingine, kutoa matukio ya kawaida, kufikia miundo ya kernel, na kadhalika.

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Kwa muhtasari, BPF hutoa uwezo wa kupakia kiholela, yaani, iliyojaribiwa na kithibitishaji, msimbo wa mtumiaji kwenye nafasi ya kernel. Nambari hii inaweza kuhifadhi hali kati ya simu na kubadilishana data na nafasi ya mtumiaji, na pia ina ufikiaji wa mifumo ndogo ya kernel inayoruhusiwa na aina hii ya programu.

Hii tayari ni sawa na uwezo unaotolewa na moduli za kernel, ikilinganishwa na ambayo BPF ina faida fulani (bila shaka, unaweza tu kulinganisha maombi sawa, kwa mfano, ufuatiliaji wa mfumo - huwezi kuandika dereva wa kiholela na BPF). Unaweza kutambua kiwango cha chini cha kuingia (baadhi ya huduma zinazotumia BPF hazihitaji mtumiaji kuwa na ustadi wa kupanga kernel, au ustadi wa kupanga kwa ujumla), usalama wa wakati wa kukimbia (inua mkono wako kwenye maoni kwa wale ambao hawajavunja mfumo wakati wa kuandika. au moduli za kupima), atomiki - kuna muda wa kupungua wakati wa kupakia upya moduli, na mfumo mdogo wa BPF unahakikisha kuwa hakuna matukio yanayokosa (kuwa sawa, hii si kweli kwa aina zote za programu za BPF).

Uwepo wa uwezo kama huo hufanya BPF kuwa chombo cha ulimwengu wote cha kupanua kernel, ambayo imethibitishwa kwa vitendo: aina zaidi na zaidi za programu zinaongezwa kwa BPF, kampuni kubwa zaidi na zaidi hutumia BPF kwenye seva za kupambana 24 Γ— 7, zaidi na zaidi. wanaoanzisha biashara huunda biashara zao kwa masuluhisho kulingana na ambayo yanategemea BPF. BPF inatumika kila mahali: katika kulinda dhidi ya mashambulizi ya DDoS, kuunda SDN (kwa mfano, kutekeleza mitandao ya kubernetes), kama zana kuu ya ufuatiliaji wa mfumo na mkusanyaji wa takwimu, katika mifumo ya kugundua uvamizi na mifumo ya kisanduku cha mchanga, n.k.

Wacha tumalizie muhtasari wa sehemu ya kifungu hapa na tuangalie mashine pepe na mfumo ikolojia wa BPF kwa undani zaidi.

Kicheko: huduma

Ili uweze kuendesha mifano katika sehemu zifuatazo, unaweza kuhitaji idadi ya huduma, angalau llvm/clang kwa msaada wa bpf na bpftool. Katika sehemu hiyo Zana za Maendeleo Unaweza kusoma maagizo ya kukusanya huduma, pamoja na kernel yako. Sehemu hii imewekwa hapa chini ili isisumbue upatanifu wa wasilisho letu.

Rejesta za Mashine ya Mtandaoni ya BPF na Mfumo wa Maagizo

Mfumo wa usanifu na amri wa BPF ulitengenezwa kwa kuzingatia ukweli kwamba programu zitaandikwa kwa lugha ya C na, baada ya kupakia kwenye kernel, kutafsiriwa katika kanuni za asili. Kwa hiyo, idadi ya rejista na seti ya amri zilichaguliwa kwa jicho kwa makutano, kwa maana ya hisabati, ya uwezo wa mashine za kisasa. Kwa kuongeza, vikwazo mbalimbali viliwekwa kwenye programu, kwa mfano, hadi hivi karibuni haikuwezekana kuandika loops na subroutines, na idadi ya maelekezo ilikuwa mdogo hadi 4096 (sasa mipango ya upendeleo inaweza kupakia hadi maagizo milioni).

BPF ina rejista kumi na moja zinazoweza kufikiwa na mtumiaji za 64-bit r0-r10 na kihesabu programu. Sajili r10 ina kielekezi cha fremu na inasomwa tu. Programu zinaweza kufikia rafu ya baiti 512 wakati wa utekelezaji na idadi isiyo na kikomo ya kumbukumbu iliyoshirikiwa katika mfumo wa ramani.

Programu za BPF zinaruhusiwa kuendesha seti maalum ya wasaidizi wa kernel ya aina ya programu na, hivi karibuni, kazi za kawaida. Kila kipengele kinachoitwa kinaweza kuchukua hadi hoja tano, zilizopitishwa katika rejista r1-r5, na thamani ya kurudi inapitishwa r0. Imehakikishiwa kwamba baada ya kurudi kutoka kwa kazi, yaliyomo kwenye rejista r6-r9 Haitabadilika.

Kwa tafsiri bora ya programu, rejista r0-r11 kwa usanifu wote unaoungwa mkono umechorwa kipekee kwa rejista halisi, kwa kuzingatia vipengele vya ABI vya usanifu wa sasa. Kwa mfano, kwa x86_64 madaftari r1-r5, inayotumiwa kupitisha vigezo vya kazi, huonyeshwa rdi, rsi, rdx, rcx, r8, ambayo hutumiwa kupitisha vigezo vya kufanya kazi x86_64. Kwa mfano, msimbo upande wa kushoto hutafsiri kwa msimbo wa kulia kama hii:

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

Jiandikishe r0 pia hutumiwa kurudisha matokeo ya utekelezaji wa programu, na kwenye rejista r1 mpango hupitishwa pointer kwa muktadha - kulingana na aina ya programu, hii inaweza kuwa, kwa mfano, muundo struct xdp_md (kwa XDP) au muundo struct __sk_buff (kwa programu tofauti za mtandao) au muundo struct pt_regs (kwa aina tofauti za programu za kufuatilia), nk.

Kwa hivyo, tulikuwa na seti ya rejista, wasaidizi wa kernel, stack, pointer ya muktadha na kumbukumbu iliyoshirikiwa kwa namna ya ramani. Sio kwamba yote haya ni muhimu sana kwenye safari, lakini ...

Wacha tuendelee maelezo na tuzungumze juu ya mfumo wa amri wa kufanya kazi na vitu hivi. Wote (Karibu wote) Maagizo ya BPF yana saizi isiyobadilika ya 64-bit. Ukiangalia maagizo kwenye mashine ya 64-bit Big Endian utaona

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Hapa Code - huu ni usimbuaji wa maagizo, Dst/Src ni usimbaji wa mpokeaji na chanzo, mtawaliwa, Off - ujongezaji uliosainiwa wa 16-bit, na Imm ni nambari kamili iliyotiwa saini ya biti 32 inayotumika katika baadhi ya maagizo (sawa na cBPF isiyobadilika K). Usimbaji Code ina moja ya aina mbili:

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Madarasa ya maagizo 0, 1, 2, 3 hufafanua amri za kufanya kazi na kumbukumbu. Wao zinaitwa, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, kwa mtiririko huo. Madarasa ya 4, 7 (BPF_ALU, BPF_ALU64) kuunda seti ya maagizo ya ALU. Madarasa ya 5, 6 (BPF_JMP, BPF_JMP32) vyenye maagizo ya kuruka.

Mpango wa baadaye wa kusoma mfumo wa mafundisho ya BPF ni kama ifuatavyo: badala ya kuorodhesha kwa uangalifu maagizo yote na vigezo vyake, tutaangalia mifano michache katika sehemu hii na kutoka kwao itakuwa wazi jinsi maagizo yanavyofanya kazi na jinsi ya kufanya. tenga mwenyewe faili yoyote ya binary kwa BPF. Ili kuunganisha nyenzo baadaye katika kifungu, tutakutana pia na maagizo ya mtu binafsi katika sehemu kuhusu Kithibitishaji, mkusanyaji wa JIT, tafsiri ya BPF ya kawaida, na vile vile wakati wa kusoma ramani, kazi za kupiga simu, n.k.

Tunapozungumza juu ya maagizo ya mtu binafsi, tutarejelea faili za msingi bpf.h ΠΈ bpf_common.h, ambayo hufafanua nambari za nambari za maagizo ya BPF. Unaposoma usanifu peke yako na/au kuchanganua jozi, unaweza kupata semantiki katika vyanzo vifuatavyo, vilivyopangwa kwa mpangilio wa uchangamano: Vipimo visivyo rasmi vya eBPF, Mwongozo wa Marejeleo wa BPF na XDP, Seti ya Maagizo, Documentation/networking/filter.txt na, bila shaka, katika msimbo wa chanzo cha Linux - kithibitishaji, JIT, mkalimani wa BPF.

Mfano: kutenganisha BPF kichwani mwako

Wacha tuangalie mfano ambao tunakusanya programu readelf-example.c na angalia binary inayosababisha. Tutafichua yaliyomo asili readelf-example.c hapa chini, baada ya kurejesha mantiki yake kutoka kwa nambari za binary:

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

Safu wima ya kwanza katika pato readelf ni indentation na mpango wetu hivyo lina amri nne:

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

Nambari za amri ni sawa b7, 15, b7 ΠΈ 95. Kumbuka kwamba sehemu tatu muhimu zaidi ni darasa la maagizo. Kwa upande wetu, sehemu ya nne ya maagizo yote ni tupu, kwa hivyo madarasa ya maagizo ni 7, 5, 7, 5, mtawaliwa. Darasa la 7 ni BPF_ALU64, na 5 ni BPF_JMP. Kwa madarasa yote mawili, umbizo la maagizo ni sawa (tazama hapo juu) na tunaweza kuandika upya programu yetu kama hii (wakati huo huo tutaandika upya safu wima zilizobaki katika umbo la kibinadamu):

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

Operesheni b tabaka la ALU64 - Je, BPF_MOV. Inatoa thamani kwa rejista lengwa. Ikiwa kidogo imewekwa s (chanzo), basi thamani inachukuliwa kutoka kwa rejista ya chanzo, na ikiwa, kama ilivyo kwa upande wetu, haijawekwa, basi thamani inachukuliwa kutoka kwa shamba. Imm. Kwa hiyo katika maagizo ya kwanza na ya tatu tunafanya operesheni r0 = Imm. Zaidi ya hayo, operesheni ya darasa la 1 ya JMP ni BPF_JEQ (ruka ikiwa ni sawa). Kwa upande wetu, tangu kidogo S ni sifuri, inalinganisha thamani ya rejista ya chanzo na uwanja Imm. Ikiwa maadili yanaambatana, basi mabadiliko hufanyika PC + OffAmbapo PC, kama kawaida, ina anwani ya maagizo yanayofuata. Hatimaye, JMP Class 9 Operesheni ni BPF_EXIT. Maagizo haya yanasitisha programu, kurudi kwenye kernel r0. Wacha tuongeze safu mpya kwenye jedwali letu:

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

Tunaweza kuandika hii tena kwa njia inayofaa zaidi:

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

Ikiwa tunakumbuka kilicho kwenye rejista r1 programu inapitishwa pointer kwa muktadha kutoka kwa kernel, na kwenye rejista r0 thamani inarejeshwa kwenye kernel, basi tunaweza kuona kwamba ikiwa pointer kwa muktadha ni sifuri, basi tunarudi 1, na vinginevyo - 2. Hebu tuangalie kwamba tuko sawa kwa kuangalia chanzo:

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

Ndiyo, ni programu isiyo na maana, lakini inatafsiri katika maelekezo manne rahisi tu.

Mfano wa ubaguzi: maagizo ya 16-byte

Tulitaja hapo awali kwamba maagizo mengine huchukua zaidi ya bits 64. Hii inatumika, kwa mfano, kwa maagizo lddw (Kanuni = 0x18 = BPF_LD | BPF_DW | BPF_IMM) - pakia neno mara mbili kutoka kwa mashamba kwenye rejista Imm... Ukweli ni kwamba Imm ina ukubwa wa 32, na neno mbili ni bits 64, hivyo kupakia thamani ya haraka ya 64-bit kwenye rejista katika maelekezo moja ya 64-bit haitafanya kazi. Ili kufanya hivyo, maagizo mawili ya karibu hutumiwa kuhifadhi sehemu ya pili ya thamani ya 64-bit kwenye shamba. Imm. Mfano:

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

Kuna maagizo mawili tu katika programu ya binary:

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

Tutakutana tena na maagizo lddw, tunapozungumza juu ya kuhamishwa na kufanya kazi na ramani.

Mfano: kutenganisha BPF kwa kutumia zana za kawaida

Kwa hivyo, tumejifunza kusoma misimbo binary ya BPF na tuko tayari kuchanganua maagizo yoyote ikihitajika. Walakini, inafaa kusema kuwa katika mazoezi ni rahisi zaidi na haraka kutenganisha programu kwa kutumia zana za kawaida, kwa mfano:

$ 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

Mzunguko wa maisha wa vitu vya BPF, mfumo wa faili wa bpffs

(Kwanza nilijifunza baadhi ya maelezo yaliyoelezewa katika kifungu hiki kutoka chapisho Alexei Starovoitov katika BPF Blog.)

Vitu vya BPF - programu na ramani - huundwa kutoka kwa nafasi ya mtumiaji kwa kutumia amri BPF_PROG_LOAD ΠΈ BPF_MAP_CREATE simu ya mfumo bpf(2), tutazungumza kuhusu jinsi hii inavyotokea katika sehemu inayofuata. Hii inaunda miundo ya data ya kernel na kwa kila moja yao refcount (hesabu ya marejeleo) imewekwa kuwa moja, na kielezi cha faili kinachoelekeza kwenye kitu kinarejeshwa kwa mtumiaji. Baada ya kushughulikia kufungwa refcount kitu kinapunguzwa na moja, na inapofikia sifuri, kitu kinaharibiwa.

Ikiwa mpango unatumia ramani, basi refcount ramani hizi zinaongezeka kwa moja baada ya kupakia programu, i.e. maelezo yao ya faili yanaweza kufungwa kutoka kwa mchakato wa mtumiaji na bado refcount haitakuwa sifuri:

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Baada ya kupakia programu kwa mafanikio, kwa kawaida tunaiambatanisha na aina fulani ya jenereta ya tukio. Kwa mfano, tunaweza kuiweka kwenye kiolesura cha mtandao ili kuchakata pakiti zinazoingia au kuiunganisha kwa baadhi tracepoint katika msingi. Katika hatua hii, kihesabu cha kumbukumbu pia kitaongezeka kwa moja na tutaweza kufunga maelezo ya faili katika programu ya kipakiaji.

Je! ni nini kitatokea ikiwa sasa tutazima kiboreshaji cha boot? Inategemea aina ya jenereta ya tukio (ndoano). Kulabu zote za mtandao zitakuwepo baada ya kipakiaji kukamilika, hizi ndizo zinazoitwa ndoano za kimataifa. Na, kwa mfano, programu za kufuatilia zitatolewa baada ya mchakato uliowaumba kukomesha (na kwa hiyo huitwa ndani, kutoka "ndani hadi mchakato"). Kitaalam, ndoano za kawaida huwa na maelezo ya faili sambamba katika nafasi ya mtumiaji na kwa hiyo hufunga wakati mchakato umefungwa, lakini ndoano za kimataifa hazina. Katika takwimu ifuatayo, kwa kutumia misalaba nyekundu, ninajaribu kuonyesha jinsi kukomesha programu ya mzigo huathiri maisha ya vitu katika kesi ya ndoano za ndani na za kimataifa.

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Kwa nini kuna tofauti kati ya ndoano za ndani na za kimataifa? Kuendesha aina fulani za programu za mtandao kuna maana bila nafasi ya mtumiaji, kwa mfano, fikiria ulinzi wa DDoS - bootloader inaandika sheria na inaunganisha mpango wa BPF kwenye interface ya mtandao, baada ya hapo bootloader inaweza kwenda na kujiua. Kwa upande mwingine, fikiria mpango wa kufuatilia utatuzi ambao uliandika kwa magoti yako katika dakika kumi - wakati umekamilika, ungependa kusiwe na takataka kwenye mfumo, na ndoano za ndani zitahakikisha hilo.

Kwa upande mwingine, fikiria kuwa unataka kuunganishwa na kituo cha ufuatiliaji kwenye kernel na kukusanya takwimu kwa miaka mingi. Katika kesi hii, ungetaka kukamilisha sehemu ya mtumiaji na kurudi kwa takwimu mara kwa mara. Mfumo wa faili wa bpf hutoa fursa hii. Ni mfumo wa faili za uwongo za kumbukumbu pekee unaoruhusu uundaji wa faili zinazorejelea vitu vya BPF na hivyo kuongezeka. refcount vitu. Baada ya hayo, kipakiaji kinaweza kuondoka, na vitu vilivyounda vitabaki hai.

BPF kwa watoto wadogo, sehemu ya kwanza: BPF iliyopanuliwa

Kuunda faili katika bpffs ambazo hurejelea vitu vya BPF huitwa "kubana" (kama katika kifungu kifuatacho: "mchakato unaweza kubandika programu au ramani ya BPF"). Kuunda vitu vya faili kwa vitu vya BPF kuna maana sio tu kwa kupanua maisha ya vitu vya ndani, lakini pia kwa utumiaji wa vitu vya ulimwengu - tukirudi kwenye mfano na mpango wa ulinzi wa DDoS wa kimataifa, tunataka kuwa na uwezo wa kuja na kuangalia takwimu. mara kwa mara.

Mfumo wa faili wa BPF kawaida huwekwa ndani /sys/fs/bpf, lakini pia inaweza kuwekwa ndani, kwa mfano, kama hii:

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

Majina ya mfumo wa faili huundwa kwa kutumia amri BPF_OBJ_PIN Simu ya mfumo wa BPF. Kwa mfano, hebu tuchukue programu, tuikusanye, tuipakie, na tuibandike bpffs. Mpango wetu haufanyi chochote muhimu, tunawasilisha tu msimbo ili uweze kutoa mfano:

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

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

Hebu tukusanye programu hii na kuunda nakala ya ndani ya mfumo wa faili bpffs:

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

Sasa hebu tupakue programu yetu kwa kutumia matumizi bpftool na angalia simu za mfumo zinazoambatana bpf(2) (baadhi ya mistari isiyo na maana imeondolewa kutoka kwa pato la kamba):

$ 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

Hapa tumepakia programu kwa kutumia BPF_PROG_LOAD, imepokea maelezo ya faili kutoka kwa kernel 3 na kutumia amri BPF_OBJ_PIN alibandika kielezi cha faili hii kama faili "bpf-mountpoint/test". Baada ya hayo, programu ya bootloader bpftool kumaliza kufanya kazi, lakini programu yetu ilibaki kwenye kernel, ingawa hatukuiunganisha kwa kiolesura chochote cha mtandao:

$ 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

Tunaweza kufuta kipengee cha faili kawaida unlink(2) na baada ya hapo programu inayolingana itafutwa:

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

Kufuta vitu

Kuzungumza juu ya kufuta vitu, ni muhimu kufafanua kwamba baada ya kutenganisha programu kutoka kwa ndoano (jenereta ya tukio), hakuna tukio jipya litakalosababisha uzinduzi wake, hata hivyo, matukio yote ya sasa ya programu yatakamilika kwa utaratibu wa kawaida. .

Aina fulani za mipango ya BPF inakuwezesha kuchukua nafasi ya programu kwa kuruka, i.e. kutoa atomi ya mlolongo replace = detach old program, attach new program. Katika kesi hii, matukio yote amilifu ya toleo la zamani la programu itamaliza kazi yao, na vidhibiti vipya vya hafla vitaundwa kutoka kwa programu mpya, na "atomicity" hapa inamaanisha kuwa hakuna tukio moja litakalokosa.

Kuambatisha programu kwenye vyanzo vya matukio

Katika nakala hii, hatutaelezea kando mipango ya kuunganisha kwa vyanzo vya hafla, kwani ni mantiki kusoma hii katika muktadha wa aina fulani ya programu. Sentimita. mfano hapa chini, ambamo tunaonyesha jinsi programu kama XDP zimeunganishwa.

Kudhibiti Vitu Kwa Kutumia Simu ya Mfumo ya bpf

Programu za BPF

Vitu vyote vya BPF huundwa na kudhibitiwa kutoka kwa nafasi ya mtumiaji kwa kutumia simu ya mfumo bpf, yenye mfano ufuatao:

#include <linux/bpf.h>

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

Hii hapa timu cmd ni moja ya maadili ya aina enum bpf_cmd, attr - pointer kwa vigezo vya programu maalum na size - ukubwa wa kitu kulingana na pointer, i.e. kawaida hii sizeof(*attr). Katika kernel 5.8 simu ya mfumo bpf inasaidia amri 34 tofauti, na ufafanuzi union bpf_attr inachukua mistari 200. Lakini hatupaswi kutishwa na hili, kwa kuwa tutakuwa tukijitambulisha na amri na vigezo katika kipindi cha makala kadhaa.

Wacha tuanze na timu BPF_PROG_LOAD, ambayo huunda programu za BPF - inachukua seti ya maagizo ya BPF na kuipakia kwenye kernel. Wakati wa kupakia, kithibitishaji kinazinduliwa, na kisha mkusanyaji wa JIT na, baada ya kutekelezwa kwa mafanikio, maelezo ya faili ya programu hurejeshwa kwa mtumiaji. Tuliona kile kinachofuata kwake katika sehemu iliyotangulia kuhusu mzunguko wa maisha wa vitu vya BPF.

Sasa tutaandika programu maalum ambayo itapakia programu rahisi ya BPF, lakini kwanza tunahitaji kuamua ni aina gani ya programu tunayotaka kupakia - tutalazimika kuchagua. aina ya na ndani ya mfumo wa aina hii, andika programu ambayo itapita mtihani wa kithibitishaji. Walakini, ili sio kugumu mchakato, hapa kuna suluhisho lililotengenezwa tayari: tutachukua programu kama hiyo BPF_PROG_TYPE_XDP, ambayo itarudisha thamani XDP_PASS (ruka vifurushi vyote). Katika mkusanyiko wa BPF inaonekana rahisi sana:

r0 = 2
exit

Baada ya kuamua kwamba tutapakia, tunaweza kukuambia jinsi tutakavyofanya:

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

Matukio ya kuvutia katika programu huanza na ufafanuzi wa safu insns - Mpango wetu wa BPF katika msimbo wa mashine. Katika kesi hii, kila maagizo ya mpango wa BPF yamefungwa kwenye muundo bpf_insn. Kipengele cha kwanza insns inafuata maagizo r0 = 2, pili - exit.

Rudi nyuma. Kernel inafafanua macros rahisi zaidi kwa kuandika misimbo ya mashine, na kutumia faili ya kichwa cha kernel tools/include/linux/filter.h tungeweza kuandika

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

Lakini kwa kuwa kuandika programu za BPF kwa nambari asilia ni muhimu tu kwa uandishi wa majaribio kwenye kernel na vifungu kuhusu BPF, kutokuwepo kwa macros haya haifanyi maisha ya msanidi programu kuwa magumu.

Baada ya kufafanua mpango wa BPF, tunaendelea kupakia kwenye kernel. Seti yetu ndogo ya vigezo attr inajumuisha aina ya programu, seti na idadi ya maagizo, leseni inayohitajika, na jina "woo", ambayo tunatumia kupata programu yetu kwenye mfumo baada ya kupakua. Mpango, kama ilivyoahidiwa, hupakiwa kwenye mfumo kwa kutumia simu ya mfumo bpf.

Mwishoni mwa programu tunaishia kwenye kitanzi kisicho na mwisho ambacho huiga mzigo wa malipo. Bila hivyo, programu itauawa na kernel wakati maelezo ya faili ambayo simu ya mfumo ilirejeshwa kwetu imefungwa. bpf, na hatutaiona kwenye mfumo.

Naam, tuko tayari kwa majaribio. Wacha tukusanye na kuendesha programu chini stracekuangalia kuwa kila kitu kinafanya kazi kama inavyopaswa:

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

Kila kitu kiko sawa, bpf(2) akarudi kushughulikia 3 kwetu na sisi akaenda katika kitanzi usio na pause(). Wacha tujaribu kupata programu yetu kwenye mfumo. Ili kufanya hivyo tutaenda kwenye terminal nyingine na kutumia matumizi 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)

Tunaona kwamba kuna programu iliyopakiwa kwenye mfumo woo ambaye kitambulisho chake cha kimataifa ni 390 na kinaendelea kwa sasa simple-prog kuna maelezo ya faili wazi yanayoelekeza kwenye programu (na ikiwa simple-prog itamaliza kazi, basi woo itatoweka). Kama inavyotarajiwa, mpango woo inachukua byte 16 - maagizo mawili - ya nambari za binary katika usanifu wa BPF, lakini katika hali yake ya asili (x86_64) tayari ni 40 bytes. Wacha tuangalie programu yetu katika hali yake ya asili:

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

hakuna mshangao. Sasa hebu tuangalie msimbo uliotolewa na mkusanyaji wa JIT:

# 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

sio ufanisi sana kwa exit(2), lakini kwa haki, mpango wetu ni rahisi sana, na kwa programu zisizo za kawaida utangulizi na epilogue iliyoongezwa na mkusanyaji wa JIT, bila shaka, inahitajika.

Ramani

Programu za BPF zinaweza kutumia maeneo ya kumbukumbu yaliyopangwa ambayo yanaweza kufikiwa na programu zingine za BPF na kwa programu katika nafasi ya mtumiaji. Vitu hivi vinaitwa ramani na katika sehemu hii tutaonyesha jinsi ya kuvidhibiti kwa kutumia simu ya mfumo bpf.

Wacha tuseme mara moja kwamba uwezo wa ramani hauzuiliwi tu kufikia kumbukumbu iliyoshirikiwa. Kuna ramani za madhumuni maalum zilizo na, kwa mfano, vielelezo vya programu za BPF au vielelezo vya miingiliano ya mtandao, ramani za kufanya kazi na matukio ya kawaida, nk. Hatutazungumza juu yao hapa, ili tusiwachanganye msomaji. Kando na hili, tunapuuza masuala ya ulandanishi, kwani hii si muhimu kwa mifano yetu. Orodha kamili ya aina za ramani zinazopatikana zinaweza kupatikana ndani <linux/bpf.h>, na katika sehemu hii tutachukua kama mfano aina ya kwanza ya kihistoria, jedwali la hashi BPF_MAP_TYPE_HASH.

Ikiwa utaunda jedwali la hashi ndani, sema, C++, ungesema unordered_map<int,long> woo, ambayo kwa Kirusi inamaanisha "Ninahitaji meza woo saizi isiyo na kikomo, ambayo funguo zake ni za aina int, na maadili ni aina long" Ili kuunda jedwali la hashi la BPF, tunahitaji kufanya kitu sawa, isipokuwa kwamba tunapaswa kutaja ukubwa wa juu wa meza, na badala ya kutaja aina za funguo na maadili, tunahitaji kutaja ukubwa wao kwa byte. . Ili kuunda ramani tumia amri BPF_MAP_CREATE simu ya mfumo bpf. Hebu tuangalie programu ndogo zaidi au ndogo inayounda ramani. Baada ya programu iliyotangulia inayopakia programu za BPF, hii inapaswa kuonekana rahisi kwako:

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

Hapa tunafafanua seti ya vigezo attr, ambamo tunasema β€œNinahitaji jedwali la hashi lenye funguo na thamani za saizi sizeof(int), ambamo naweza kuweka upeo wa vipengele vinne." Wakati wa kuunda ramani za BPF, unaweza kutaja vigezo vingine, kwa mfano, kwa njia sawa na katika mfano na programu, tulitaja jina la kitu kama "woo".

Wacha tukusanye na kuendesha programu:

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

Hapa kuna simu ya mfumo bpf(2) aliturudishia nambari ya ramani ya maelezo 3 na kisha programu, kama inavyotarajiwa, inasubiri maagizo zaidi katika simu ya mfumo pause(2).

Sasa hebu tutume programu yetu nyuma au tufungue terminal nyingine na tuangalie kitu chetu kwa kutumia matumizi bpftool (tunaweza kutofautisha ramani yetu kutoka kwa wengine kwa jina lake):

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

Nambari 114 ni kitambulisho cha kimataifa cha kitu chetu. Mpango wowote kwenye mfumo unaweza kutumia kitambulisho hiki kufungua ramani iliyopo kwa kutumia amri BPF_MAP_GET_FD_BY_ID simu ya mfumo bpf.

Sasa tunaweza kucheza na meza yetu ya hashi. Wacha tuangalie yaliyomo:

$ sudo bpftool map dump id 114
Found 0 elements

Tupu. Hebu tuweke thamani ndani yake hash[1] = 1:

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

Wacha tuangalie meza tena:

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

Hooray! Tumeweza kuongeza kipengele kimoja. Kumbuka kwamba tunapaswa kufanya kazi katika ngazi ya byte kufanya hili, tangu bptftool hajui ni aina gani za maadili kwenye jedwali la hashi. (Maarifa haya yanaweza kuhamishiwa kwake kwa kutumia BTF, lakini zaidi kuhusu hilo sasa.)

Je, bpftool inasomaje na kuongeza vipengele? Wacha tuangalie chini ya kofia:

$ 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

Kwanza tulifungua ramani kwa kitambulisho chake cha kimataifa kwa kutumia amri BPF_MAP_GET_FD_BY_ID ΠΈ bpf(2) alirudisha kifafanuzi 3 kwetu. Zaidi kwa kutumia amri BPF_MAP_GET_NEXT_KEY tulipata ufunguo wa kwanza kwenye jedwali kwa kupita NULL kama kielekezi kwa kitufe cha "uliopita". Ikiwa tuna ufunguo tunaweza kufanya BPF_MAP_LOOKUP_ELEMambayo inarudisha thamani kwa pointer value. Hatua inayofuata ni tunajaribu kupata kipengee kinachofuata kwa kupitisha pointer kwa ufunguo wa sasa, lakini meza yetu ina kipengele kimoja tu na amri. BPF_MAP_GET_NEXT_KEY anarudi ENOENT.

Sawa, hebu tubadilishe thamani kwa ufunguo 1, tuseme mantiki ya biashara yetu inahitaji kusajiliwa 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

Kama inavyotarajiwa, ni rahisi sana: amri BPF_MAP_GET_FD_BY_ID inafungua ramani yetu kwa kitambulisho, na amri BPF_MAP_UPDATE_ELEM inafuta kipengele.

Kwa hiyo, baada ya kuunda meza ya hash kutoka kwa programu moja, tunaweza kusoma na kuandika yaliyomo kutoka kwa mwingine. Kumbuka kwamba ikiwa tuliweza kufanya hivyo kutoka kwa mstari wa amri, basi programu nyingine yoyote kwenye mfumo inaweza kuifanya. Mbali na maagizo yaliyoelezwa hapo juu, kwa kufanya kazi na ramani kutoka kwa nafasi ya mtumiaji, zifuatazo:

  • BPF_MAP_LOOKUP_ELEM: pata thamani kwa ufunguo
  • BPF_MAP_UPDATE_ELEM: sasisha/unda thamani
  • BPF_MAP_DELETE_ELEM: ondoa ufunguo
  • BPF_MAP_GET_NEXT_KEY: pata ufunguo unaofuata (au wa kwanza).
  • BPF_MAP_GET_NEXT_ID: hukuruhusu kupitia ramani zote zilizopo, ndivyo inavyofanya kazi bpftool map
  • BPF_MAP_GET_FD_BY_ID: fungua ramani iliyopo kwa kitambulisho chake cha kimataifa
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: sasisha thamani ya kitu kiotomatiki na urudishe kile cha zamani
  • BPF_MAP_FREEZE: fanya ramani isibadilike kutoka kwa nafasi ya mtumiaji (operesheni hii haiwezi kutenduliwa)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: shughuli za wingi. Kwa mfano, BPF_MAP_LOOKUP_AND_DELETE_BATCH - hii ndiyo njia pekee ya kuaminika ya kusoma na kuweka upya maadili yote kutoka kwenye ramani

Sio amri hizi zote zinazofanya kazi kwa aina zote za ramani, lakini kwa ujumla kufanya kazi na aina nyingine za ramani kutoka nafasi ya mtumiaji inaonekana sawa na kufanya kazi na jedwali la hashi.

Kwa ajili ya utaratibu, hebu tumalize majaribio yetu ya jedwali la hashi. Je! unakumbuka kwamba tuliunda jedwali ambalo linaweza kuwa na hadi funguo nne? Hebu tuongeze vipengele vichache zaidi:

$ 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

Kufikia sasa ni nzuri sana:

$ 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

Wacha tujaribu kuongeza moja zaidi:

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

Kama ilivyotarajiwa, hatukufanikiwa. Wacha tuangalie kosa kwa undani zaidi:

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

Kila kitu ni sawa: kama inavyotarajiwa, timu BPF_MAP_UPDATE_ELEM inajaribu kuunda ufunguo mpya, wa tano, lakini huacha kufanya kazi E2BIG.

Kwa hivyo, tunaweza kuunda na kupakia programu za BPF, na pia kuunda na kudhibiti ramani kutoka kwa nafasi ya mtumiaji. Sasa ni jambo la busara kuangalia jinsi tunavyoweza kutumia ramani kutoka kwa programu za BPF zenyewe. Tunaweza kuzungumza juu ya hili kwa lugha ya programu ngumu-kusoma katika nambari za jumla za mashine, lakini kwa kweli wakati umefika wa kuonyesha jinsi programu za BPF zimeandikwa na kudumishwa - kwa kutumia. libbpf.

(Kwa wasomaji ambao hawajaridhika na ukosefu wa mfano wa kiwango cha chini: tutachambua kwa undani programu zinazotumia ramani na vitendaji vya msaidizi vilivyoundwa kwa kutumia. libbpf na kukuambia kile kinachotokea katika kiwango cha maagizo. Kwa wasomaji ambao hawajaridhika sana, tuliongeza mfano mahali panapofaa katika kifungu hicho.)

Kuandika programu za BPF kwa kutumia libbpf

Kuandika programu za BPF kwa kutumia misimbo ya mashine kunaweza kuvutia mara ya kwanza tu, kisha kutosheka kunaanza. Kwa wakati huu unahitaji kugeuza mawazo yako llvm, ambayo ina sehemu ya nyuma ya kutoa msimbo wa usanifu wa BPF, pamoja na maktaba libbpf, ambayo hukuruhusu kuandika upande wa mtumiaji wa programu za BPF na kupakia msimbo wa programu za BPF zinazozalishwa kwa kutumia llvm/clang.

Kwa kweli, kama tutakavyoona katika nakala hii na inayofuata, libbpf hufanya kazi nyingi bila hiyo (au zana zinazofanana - iproute2, libbcc, libbpf-go, nk) haiwezekani kuishi. Moja ya sifa kuu za mradi huo libbpf ni BPF CO-RE (Compile Once, Run Everywhere) - mradi unaokuruhusu kuandika programu za BPF ambazo zinaweza kubebeka kutoka kernel moja hadi nyingine, na uwezo wa kukimbia kwenye API tofauti (kwa mfano, wakati muundo wa kernel unabadilika kutoka toleo. kwa toleo). Ili kuweza kufanya kazi na CO-RE, kernel yako lazima iundwe kwa usaidizi wa BTF (tunaelezea jinsi ya kufanya hivyo katika sehemu Zana za Maendeleo. Unaweza kuangalia ikiwa kernel yako imejengwa na BTF au sio kwa urahisi sana - kwa uwepo wa faili ifuatayo:

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

Faili hii huhifadhi taarifa kuhusu aina zote za data zinazotumika kwenye kernel na inatumika katika mifano yetu yote kwa kutumia libbpf. Tutazungumza kwa undani juu ya CO-RE katika makala inayofuata, lakini katika hii - jijengee kernel nayo CONFIG_DEBUG_INFO_BTF.

maktaba libbpf anaishi moja kwa moja kwenye saraka tools/lib/bpf kernel na ukuzaji wake unafanywa kupitia orodha ya barua [email protected]. Walakini, hazina tofauti hutunzwa kwa mahitaji ya programu zinazoishi nje ya kernel https://github.com/libbpf/libbpf ambayo maktaba ya kernel inaakisiwa kwa ufikiaji wa kusoma zaidi au chini kama ilivyo.

Katika sehemu hii tutaangalia jinsi unaweza kuunda mradi unaotumia libbpf, hebu tuandike mipango kadhaa ya majaribio (zaidi au isiyo na maana) na tuchambue kwa undani jinsi yote inavyofanya kazi. Hii itaturuhusu kueleza kwa urahisi zaidi katika sehemu zifuatazo jinsi programu za BPF zinavyoingiliana na ramani, visaidizi vya kernel, BTF, n.k.

Kawaida kwa kutumia miradi libbpf ongeza hazina ya GitHub kama moduli ndogo ya git, tutafanya vivyo hivyo:

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

Kwenda kwa libbpf rahisi sana:

$ 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

Mpango wetu unaofuata katika sehemu hii ni kama ifuatavyo: tutaandika mpango wa BPF kama BPF_PROG_TYPE_XDP, sawa na katika mfano uliopita, lakini katika C, tunaikusanya kwa kutumia clang, na uandike programu ya msaidizi ambayo itaipakia kwenye kernel. Katika sehemu zifuatazo tutapanua uwezo wa programu ya BPF na programu ya msaidizi.

Mfano: kuunda programu kamili kwa kutumia libbpf

Kuanza na, tunatumia faili /sys/kernel/btf/vmlinux, ambayo ilitajwa hapo juu, na kuunda sawa katika mfumo wa faili ya kichwa:

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

Faili hii itahifadhi miundo yote ya data inayopatikana kwenye kernel yetu, kwa mfano, hivi ndivyo kichwa cha IPv4 kinavyofafanuliwa kwenye kernel:

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

Sasa tutaandika programu yetu ya BPF katika 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";

Ingawa programu yetu iligeuka kuwa rahisi sana, bado tunahitaji kuzingatia maelezo mengi. Kwanza, faili ya kichwa cha kwanza tunachojumuisha ni vmlinux.h, ambayo tumetengeneza tu kutumia bpftool btf dump - sasa hatuitaji kusakinisha kifurushi cha vichwa vya kernel ili kujua miundo ya kernel inaonekanaje. Faili ya kichwa ifuatayo inakuja kwetu kutoka kwa maktaba libbpf. Sasa tunahitaji tu kufafanua jumla SEC, ambayo hutuma mhusika kwenye sehemu inayofaa ya faili ya kitu cha ELF. Programu yetu iko katika sehemu xdp/simple, ambapo kabla ya kufyeka tunafafanua aina ya programu BPF - hii ni mkataba unaotumiwa libbpf, kulingana na jina la sehemu itabadilisha aina sahihi wakati wa kuanza bpf(2). Mpango wa BPF yenyewe ni C - rahisi sana na lina mstari mmoja return XDP_PASS. Hatimaye, sehemu tofauti "license" ina jina la leseni.

Tunaweza kukusanya programu yetu kwa kutumia llvm/clang, toleo >= 10.0.0, au bora zaidi, kubwa zaidi (tazama sehemu Zana za Maendeleo):

$ 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

Miongoni mwa vipengele vya kuvutia: tunaonyesha usanifu wa lengo -target bpf na njia ya vichwa libbpf, ambayo tumeiweka hivi karibuni. Pia, usisahau kuhusu -O2, bila chaguo hili unaweza kuwa katika mshangao katika siku zijazo. Hebu tuangalie kanuni zetu, je tulifanikiwa kuandika programu tuliyotaka?

$ 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

Ndiyo, ilifanya kazi! Sasa, tunayo faili ya binary na programu, na tunataka kuunda programu ambayo itapakia kwenye kernel. Kwa kusudi hili maktaba libbpf inatupa chaguo mbili - tumia API ya kiwango cha chini au API ya kiwango cha juu. Tutaenda kwa njia ya pili, kwani tunataka kujifunza jinsi ya kuandika, kupakia na kuunganisha programu za BPF kwa juhudi ndogo kwa masomo yao ya baadaye.

Kwanza, tunahitaji kuzalisha "mifupa" ya programu yetu kutoka kwa binary yake kwa kutumia matumizi sawa bpftool - kisu cha Uswizi cha ulimwengu wa BPF (ambacho kinaweza kuchukuliwa kihalisi, kwani Daniel Borkman, mmoja wa waundaji na watunzaji wa BPF, ni Uswizi):

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

Katika faili xdp-simple.skel.h ina msimbo wa binary wa programu yetu na kazi za kusimamia - kupakia, kuambatisha, kufuta kitu chetu. Katika kesi yetu rahisi hii inaonekana kama kupindukia, lakini pia inafanya kazi katika kesi ambapo faili ya kitu ina programu na ramani nyingi za BPF na kupakia ELF hii kubwa tunahitaji tu kutengeneza mifupa na kupiga kazi moja au mbili kutoka kwa programu maalum tunayotumia. wanaandika Hebu tuendelee sasa.

Kwa kusema kweli, programu yetu ya kupakia ni ndogo:

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

Hapa struct xdp_simple_bpf iliyofafanuliwa kwenye faili xdp-simple.skel.h na inaelezea faili yetu ya kitu:

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

Tunaweza kuona athari za API ya kiwango cha chini hapa: muundo struct bpf_program *simple ΠΈ struct bpf_link *simple. Muundo wa kwanza unaelezea haswa programu yetu, iliyoandikwa katika sehemu hiyo xdp/simple, na ya pili inaeleza jinsi programu inavyounganishwa na chanzo cha tukio.

Kazi xdp_simple_bpf__open_and_load, hufungua kitu cha ELF, huichanganua, huunda miundo na miundo yote (mbali na programu, ELF pia ina sehemu zingine - data, data ya kusoma tu, habari ya utatuzi, leseni, nk), na kisha kuipakia kwenye kernel kwa kutumia mfumo. wito bpf, ambayo tunaweza kuangalia kwa kuandaa na kuendesha programu:

$ 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

Hebu sasa tuangalie programu yetu kutumia bpftool. Tutafute kitambulisho chake:

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

na kutupa (tunatumia fomu iliyofupishwa ya amri bpftool prog dump xlated):

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

Kitu kipya! Programu ilichapisha vipande vya faili yetu ya chanzo C. Hii ilifanywa na maktaba libbpf, ambayo ilipata sehemu ya utatuzi kwenye jozi, ikakusanya kwenye kitu cha BTF, ikapakia kwenye kernel kwa kutumia BPF_BTF_LOAD, na kisha kutaja maelezo ya faili inayosababisha wakati wa kupakia programu na amri BPG_PROG_LOAD.

Wasaidizi wa Kernel

Programu za BPF zinaweza kuendesha kazi za "nje" - wasaidizi wa kernel. Kazi hizi za wasaidizi huruhusu programu za BPF kufikia miundo ya kernel, kudhibiti ramani, na pia kuwasiliana na "ulimwengu halisi" - kuunda matukio ya kawaida, kudhibiti maunzi (kwa mfano, kuelekeza pakiti), nk.

Mfano: bpf_get_smp_processor_id

Ndani ya mfumo wa dhana ya "kujifunza kwa mfano", hebu tuchunguze mojawapo ya kazi za msaidizi, bpf_get_smp_processor_id(), fulani katika faili kernel/bpf/helpers.c. Hurejesha nambari ya processor ambayo programu ya BPF iliyoiita inaendesha. Lakini hatupendezwi sana na semantiki zake kama vile ukweli kwamba utekelezaji wake unachukua mstari mmoja:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Ufafanuzi wa utendakazi wa msaidizi wa BPF ni sawa na ufafanuzi wa simu za mfumo wa Linux. Hapa, kwa mfano, kazi inafafanuliwa ambayo haina hoja. (Kazi ambayo inachukua, sema, hoja tatu hufafanuliwa kwa kutumia macro BPF_CALL_3. Idadi ya juu ya hoja ni tano.) Hata hivyo, hii ni sehemu ya kwanza tu ya ufafanuzi. Sehemu ya pili ni kufafanua muundo wa aina struct bpf_func_proto, ambayo ina maelezo ya kazi ya msaidizi ambayo mthibitishaji anaelewa:

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

Kusajili Kazi za Msaidizi

Ili programu za BPF za aina fulani zitumie kazi hii, lazima zisajiliwe, kwa mfano kwa aina BPF_PROG_TYPE_XDP kazi imefafanuliwa kwenye kernel xdp_func_proto, ambayo huamua kutoka kwa kitambulisho cha kitendakazi cha msaidizi ikiwa XDP inaauni chaguo hili la kukokotoa au la. Kazi yetu ni huunga mkono:

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

Aina mpya za programu za BPF "zimefafanuliwa" kwenye faili include/linux/bpf_types.h kwa kutumia macro BPF_PROG_TYPE. Imefafanuliwa katika nukuu kwa sababu ni ufafanuzi wa kimantiki, na kwa maneno ya lugha C ufafanuzi wa seti nzima ya miundo thabiti hutokea katika maeneo mengine. Hasa, katika faili kernel/bpf/verifier.c ufafanuzi wote kutoka faili bpf_types.h hutumiwa kuunda safu ya miundo 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
};

Hiyo ni, kwa kila aina ya programu ya BPF, pointer kwa muundo wa data ya aina inaelezwa struct bpf_verifier_ops, ambayo imeanzishwa kwa thamani _name ## _verifier_ops, yaani, xdp_verifier_ops kwa xdp. Muundo xdp_verifier_ops kuamua na katika faili net/core/filter.c kama ifuatavyo:

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

Hapa tunaona kazi yetu inayojulikana xdp_func_proto, ambayo itaendesha kithibitishaji kila wakati inapokumbana na changamoto aina fulani inafanya kazi ndani ya programu ya BPF, ona verifier.c.

Wacha tuangalie jinsi mpango wa dhahania wa BPF hutumia kazi hiyo bpf_get_smp_processor_id. Ili kufanya hivyo, tunaandika upya programu kutoka kwa sehemu yetu ya awali kama ifuatavyo:

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

ishara bpf_get_smp_processor_id kuamua na Π² <bpf/bpf_helper_defs.h> maktaba libbpf kama

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

hiyo ni, bpf_get_smp_processor_id ni kiashirio cha chaguo la kukokotoa ambacho thamani yake ni 8, ambapo 8 ni thamani BPF_FUNC_get_smp_processor_id aina enum bpf_fun_id, ambayo imefafanuliwa kwa ajili yetu katika faili vmlinux.h (faili bpf_helper_defs.h kwenye kernel hutolewa na hati, kwa hivyo nambari za "uchawi" ni sawa). Chaguo hili la kukokotoa halichukui hoja na hurejesha thamani ya aina __u32. Tunapoiendesha katika programu yetu, clang inazalisha maagizo BPF_CALL "aina sahihi" Wacha tukusanye programu na tuangalie sehemu hiyo 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

Katika mstari wa kwanza tunaona maagizo call, kigezo IMM ambayo ni sawa na 8, na SRC_REG - sufuri. Kulingana na makubaliano ya ABI yanayotumiwa na kithibitishaji, huu ni wito kwa nambari ya nane ya kazi ya msaidizi. Mara tu inapozinduliwa, mantiki ni rahisi. Rejesha thamani kutoka kwa rejista r0 kunakiliwa kwa r1 na kwenye mistari 2,3 inabadilishwa kuwa aina u32 - bits 32 za juu zimefutwa. Kwenye mstari wa 4,5,6,7 tunarudi 2 (XDP_PASS) au 1 (XDP_DROP) kulingana na kama kitendakazi cha msaidizi kutoka kwa mstari wa 0 kilirudisha thamani ya sifuri au isiyo ya sifuri.

Wacha tujijaribu: pakia programu na uangalie matokeo 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

Sawa, kithibitishaji kimepata msaidizi sahihi wa kernel.

Mfano: kupitisha hoja na hatimaye kuendesha programu!

Vitendaji vyote vya usaidizi wa kiwango cha kukimbia vina mfano

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

Vigezo vya kazi za wasaidizi hupitishwa kwenye rejista r1-r5, na thamani inarudishwa kwenye rejista r0. Hakuna chaguo za kukokotoa ambazo huchukua zaidi ya hoja tano, na usaidizi kwao hautarajiwi kuongezwa katika siku zijazo.

Wacha tuangalie msaidizi mpya wa kernel na jinsi BPF inavyopitisha vigezo. Hebu tuandike upya xdp-simple.bpf.c kama ifuatavyo (mistari iliyobaki haijabadilika):

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

Programu yetu inachapisha nambari ya CPU ambayo inaendesha. Wacha tuikusanye na tuangalie nambari:

$ 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

Katika mistari 0-7 tunaandika kamba running on CPU%un, na kisha kwenye mstari wa 8 tunaendesha ile inayojulikana bpf_get_smp_processor_id. Kwenye mstari wa 9-12 tunatayarisha hoja za msaidizi bpf_printk - rejista r1, r2, r3. Kwa nini wako watatu na sio wawili? Kwa sababu bpf_printkhii ni kanga kubwa karibu na msaidizi wa kweli bpf_trace_printk, ambayo inahitaji kupitisha saizi ya safu ya umbizo.

Hebu sasa tuongeze mistari michache xdp-simple.cili programu yetu iunganishe kwenye kiolesura lo na kweli kuanza!

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

Hapa tunatumia kazi bpf_set_link_xdp_fd, ambayo inaunganisha programu za BPF za aina ya XDP na miingiliano ya mtandao. Tuliweka nambari ya kiolesura kwa bidii lo, ambayo daima ni 1. Tunaendesha kazi mara mbili ili kwanza kufuta programu ya zamani ikiwa ilikuwa imeshikamana. Ona kwamba sasa hatuhitaji changamoto pause au kitanzi kisicho na kikomo: programu yetu ya kipakiaji itaondoka, lakini programu ya BPF haitauawa kwa kuwa imeunganishwa kwenye chanzo cha tukio. Baada ya kupakua na kuunganishwa kwa mafanikio, programu itazinduliwa kwa kila pakiti ya mtandao inayofika lo.

Hebu kupakua programu na kuangalia interface 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

Programu tuliyopakua ina ID 669 na tunaona kitambulisho sawa kwenye kiolesura lo. Tutatuma vifurushi kadhaa kwa 127.0.0.1 (ombi + jibu):

$ ping -c1 localhost

na sasa hebu tuangalie yaliyomo kwenye faili ya urekebishaji ya utatuzi /sys/kernel/debug/tracing/trace_pipe, ambamo bpf_printk anaandika ujumbe wake:

# 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

Vifurushi viwili vilionekana lo na kuchakatwa kwenye CPU0 - programu yetu ya kwanza ya BPF isiyo na maana ilifanya kazi!

Ikumbukwe kwamba bpf_printk Sio bure kwamba inaandika kwa faili ya debug: hii sio msaidizi aliyefanikiwa zaidi kwa matumizi katika uzalishaji, lakini lengo letu lilikuwa kuonyesha kitu rahisi.

Kupata ramani kutoka kwa programu za BPF

Mfano: kutumia ramani kutoka kwa mpango wa BPF

Katika sehemu zilizopita tulijifunza jinsi ya kuunda na kutumia ramani kutoka kwa nafasi ya mtumiaji, na sasa hebu tuangalie sehemu ya kernel. Wacha tuanze, kama kawaida, na mfano. Wacha tuandike tena programu yetu xdp-simple.bpf.c kama ifuatavyo:

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

Mwanzoni mwa programu tuliongeza ufafanuzi wa ramani woo: Hii ni safu ya vipengele 8 ambayo huhifadhi thamani kama vile u64 (katika C tunaweza kufafanua safu kama vile u64 woo[8]) Katika programu "xdp/simple" tunapata nambari ya processor ya sasa kuwa kigezo key na kisha kutumia kazi ya msaidizi bpf_map_lookup_element tunapata pointer kwa kuingia sambamba katika safu, ambayo tunaongeza kwa moja. Ilitafsiriwa kwa Kirusi: tunakokotoa takwimu ambazo CPU ilichakata pakiti zinazoingia. Wacha tujaribu kuendesha programu:

$ 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

Wacha tuangalie ikiwa ameunganishwa lo na tuma vifurushi kadhaa:

$ 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

Sasa hebu tuangalie yaliyomo kwenye safu:

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

Takriban michakato yote ilichakatwa kwenye CPU7. Hii sio muhimu kwetu, jambo kuu ni kwamba programu inafanya kazi na tunaelewa jinsi ya kupata ramani kutoka kwa programu za BPF - kwa kutumia Ρ…Π΅Π»ΠΏΠ΅Ρ€ΠΎΠ² bpf_mp_*.

Kielezo cha fumbo

Kwa hivyo, tunaweza kupata ramani kutoka kwa mpango wa BPF kwa kutumia simu kama

val = bpf_map_lookup_elem(&woo, &key);

ambapo kazi ya msaidizi inaonekana kama

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

lakini tunapita pointer &woo kwa muundo usio na jina struct { ... }...

Ikiwa tunatazama mkusanyiko wa programu, tunaona kwamba thamani &woo haijafafanuliwa (mstari wa 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
...

na iko katika uhamishaji:

$ 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

Lakini tukiangalia programu iliyopakiwa tayari, tunaona pointer kwa ramani sahihi (mstari wa 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]
...

Kwa hivyo, tunaweza kuhitimisha kuwa wakati wa kuzindua programu yetu ya kupakia, kiunga cha &woo ilibadilishwa na kitu na maktaba libbpf. Kwanza tutaangalia pato 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

Tunaona hilo libbpf aliunda ramani woo na kisha kupakua programu yetu simple. Wacha tuangalie kwa undani jinsi tunapakia programu:

  • wito xdp_simple_bpf__open_and_load kutoka kwa faili xdp-simple.skel.h
  • ambayo husababisha xdp_simple_bpf__load kutoka kwa faili xdp-simple.skel.h
  • ambayo husababisha bpf_object__load_skeleton kutoka kwa faili libbpf/src/libbpf.c
  • ambayo husababisha bpf_object__load_xattr ya libbpf/src/libbpf.c

Kazi ya mwisho, kati ya mambo mengine, itaita bpf_object__create_maps, ambayo huunda au kufungua ramani zilizopo, kuzigeuza kuwa maelezo ya faili. (Hapa ndipo tunapoona BPF_MAP_CREATE katika pato strace.) Ifuatayo kitendakazi kinaitwa bpf_object__relocate na ndiye anayetuvutia, kwani tunakumbuka tuliyoyaona woo katika meza ya uhamishaji. Kuichunguza, hatimaye tunajikuta kwenye kazi bpf_program__relocate, ambayo inahusika na uhamishaji wa ramani:

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

Kwa hivyo tunachukua maagizo yetu

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

na ubadilishe rejista ya chanzo ndani yake na BPF_PSEUDO_MAP_FD, na IMM ya kwanza kwa maelezo ya faili ya ramani yetu na, ikiwa ni sawa na, kwa mfano, 0xdeadbeef, basi matokeo yake tutapokea maelekezo

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

Hivi ndivyo maelezo ya ramani yanavyohamishwa hadi kwa programu maalum ya BPF iliyopakiwa. Katika kesi hii, ramani inaweza kuundwa kwa kutumia BPF_MAP_CREATE, na kufunguliwa kwa kitambulisho kwa kutumia BPF_MAP_GET_FD_BY_ID.

Jumla, wakati wa kutumia libbpf algorithm ni kama ifuatavyo:

  • wakati wa ujumuishaji, rekodi huundwa kwenye jedwali la uhamishaji kwa viungo vya ramani
  • libbpf hufungua kitabu cha kitu cha ELF, hupata ramani zote zilizotumiwa na kuunda maelezo ya faili kwao
  • maelezo ya faili hupakiwa kwenye kernel kama sehemu ya maagizo LD64

Kama unavyoweza kufikiria, kuna mengi zaidi yajayo na tutalazimika kuangalia ndani ya msingi. Kwa bahati nzuri, tunayo kidokezo - tumeandika maana BPF_PSEUDO_MAP_FD kwenye rejista ya chanzo na tunaweza kuizika, ambayo itatuongoza kwa patakatifu pa watakatifu wote - kernel/bpf/verifier.c, ambapo chaguo la kukokotoa lenye jina la kipekee hubadilisha kifafanuzi cha faili na anwani ya muundo wa aina 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;

(nambari kamili inaweza kupatikana ΠΏΠΎ ссылкС) Kwa hivyo tunaweza kupanua algorithm yetu:

  • wakati wa kupakia programu, mthibitishaji huangalia matumizi sahihi ya ramani na anaandika anwani ya muundo unaofanana struct bpf_map

Wakati wa kupakua binary ya ELF kwa kutumia libbpf Kuna mengi zaidi yanayoendelea, lakini tutayajadili katika makala nyingine.

Inapakia programu na ramani bila libbpf

Kama ilivyoahidiwa, hapa kuna mfano kwa wasomaji ambao wanataka kujua jinsi ya kuunda na kupakia programu inayotumia ramani, bila msaada. libbpf. Hii inaweza kuwa muhimu wakati unafanya kazi katika mazingira ambayo huwezi kujenga utegemezi, au kuokoa kila kidogo, au kuandika programu kama ply, ambayo hutoa msimbo wa binary wa BPF kwenye kuruka.

Ili iwe rahisi kufuata mantiki, tutaandika upya mfano wetu kwa madhumuni haya xdp-simple. Nambari kamili na iliyopanuliwa kidogo ya programu iliyojadiliwa katika mfano huu inaweza kupatikana katika hili kiini.

Mantiki ya maombi yetu ni kama ifuatavyo:

  • tengeneza ramani ya aina BPF_MAP_TYPE_ARRAY kwa kutumia amri BPF_MAP_CREATE,
  • tengeneza programu inayotumia ramani hii,
  • unganisha programu kwenye interface lo,

ambayo hutafsiri kuwa binadamu kama

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

Hapa map_create huunda ramani kwa njia sawa na tulivyofanya katika mfano wa kwanza kuhusu simu ya mfumo bpf - "kernel, tafadhali nitengeneze ramani mpya katika mfumo wa safu ya vipengele 8 kama __u64 na unirudishe maelezo ya faili":

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

Programu pia ni rahisi kupakia:

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

Sehemu ya gumu prog_load ni ufafanuzi wa mpango wetu wa BPF kama safu ya miundo struct bpf_insn insns[]. Lakini kwa kuwa tunatumia programu ambayo tunayo katika C, tunaweza kudanganya kidogo:

$ 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

Kwa jumla, tunahitaji kuandika maagizo 14 kwa namna ya miundo kama struct bpf_insn (ushauri: chukua dampo kutoka juu, soma tena sehemu ya maagizo, fungua linux/bpf.h ΠΈ linux/bpf_common.h na jaribu kuamua struct bpf_insn insns[] peke yako):

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

Zoezi kwa wale ambao hawakuandika hii wenyewe - pata map_fd.

Kuna sehemu nyingine ambayo haijafichuliwa imesalia katika programu yetu - xdp_attach. Kwa bahati mbaya, programu kama XDP haziwezi kuunganishwa kwa kutumia simu ya mfumo bpf. Watu waliounda BPF na XDP walitoka kwenye jumuiya ya Linux mtandaoni, ambayo ina maana kwamba walitumia ile inayofahamika zaidi kwao (lakini si kawaida people) interface ya kuingiliana na kernel: soketi za netlink, Angalia pia 3549. Mchezaji hajali. Njia rahisi zaidi ya kutekeleza xdp_attach inanakili msimbo kutoka libbpf, yaani, kutoka kwa faili netlink.c, ambayo ndio tulifanya, tukifupisha kidogo:

Karibu katika ulimwengu wa soketi za netlink

Fungua aina ya soketi ya netlink 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;
}

Tunasoma kutoka kwa soketi hii:

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

Mwishowe, hapa kuna kazi yetu inayofungua tundu na kutuma ujumbe maalum kwake iliyo na maelezo ya faili:

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

Kwa hivyo, kila kitu kiko tayari kwa majaribio:

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

Wacha tuone ikiwa programu yetu imeunganishwa 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

Wacha tutume pings na tuangalie ramani:

$ 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

Hurray, kila kitu kinafanya kazi. Kumbuka, kwa njia, kwamba ramani yetu inaonyeshwa tena kwa namna ya ka. Hii ni kutokana na ukweli kwamba, tofauti libbpf hatukupakia maelezo ya aina (BTF). Lakini tutazungumza zaidi kuhusu hili wakati ujao.

Zana za Maendeleo

Katika sehemu hii, tutaangalia zana ya chini kabisa ya zana za msanidi wa BPF.

Kwa ujumla, hauitaji chochote maalum kuunda programu za BPF - BPF inaendesha kernel yoyote nzuri ya usambazaji, na programu hujengwa kwa kutumia. clang, ambayo inaweza kutolewa kutoka kwa kifurushi. Walakini, kwa sababu ya ukweli kwamba BPF iko chini ya maendeleo, kernel na zana zinabadilika kila wakati, ikiwa hutaki kuandika programu za BPF kwa kutumia njia za kizamani kutoka 2019, basi utalazimika kuunda.

  • llvm/clang
  • pahole
  • msingi wake
  • bpftool

(Kwa kumbukumbu, sehemu hii na mifano yote kwenye kifungu iliendeshwa kwenye Debian 10.)

llvm/clang

BPF ni rafiki na LLVM na, ingawa programu za hivi majuzi za BPF zinaweza kukusanywa kwa kutumia gcc, maendeleo yote ya sasa yanafanywa kwa LLVM. Kwa hiyo, kwanza kabisa, tutajenga toleo la sasa clang kutoka kwa 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
... ΠΌΠ½ΠΎΠ³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ спустя
$

Sasa tunaweza kuangalia ikiwa kila kitu kilikusanyika kwa usahihi:

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

(Maelekezo ya mkutano clang kuchukuliwa na mimi kutoka bpf_endeleza_QA.)

Hatutasakinisha programu ambazo tumeunda hivi punde, lakini badala yake tu kuziongeza PATH, kwa mfano:

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

(Hii inaweza kuongezwa kwa .bashrc au kwa faili tofauti. Binafsi, ninaongeza vitu kama hivi ~/bin/activate-llvm.sh na inapobidi naifanya . activate-llvm.sh.)

Pahole na BTF

Huduma pahole hutumika wakati wa kuunda kernel kuunda habari ya utatuzi katika umbizo la BTF. Hatutaingia kwa undani katika makala hii kuhusu maelezo ya teknolojia ya BTF, isipokuwa ukweli kwamba ni rahisi na tunataka kuitumia. Kwa hivyo ikiwa utaunda kernel yako, jenga kwanza pahole (bila pahole hautaweza kujenga kernel na chaguo 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

Kernels za kujaribu BPF

Wakati wa kuchunguza uwezekano wa BPF, ninataka kukusanya msingi wangu mwenyewe. Hii, kwa ujumla, sio lazima, kwani utaweza kukusanya na kupakia programu za BPF kwenye kernel ya usambazaji, hata hivyo, kuwa na kernel yako mwenyewe hukuruhusu kutumia vipengee vya hivi karibuni vya BPF, ambavyo vitaonekana katika usambazaji wako kwa miezi bora. , au, kama ilivyo kwa baadhi ya zana za utatuzi hazitafungwa hata kidogo katika siku zijazo. Pia, msingi wake hufanya kujisikia muhimu kufanya majaribio na kanuni.

Ili kuunda kernel unahitaji, kwanza, kernel yenyewe, na pili, faili ya usanidi wa kernel. Ili kujaribu BPF tunaweza kutumia kawaida vanilla kernel au moja ya punje za ukuzaji. Kihistoria, maendeleo ya BPF hufanyika ndani ya jumuiya ya mitandao ya Linux na kwa hivyo mabadiliko yote mapema au baadaye yatapitia David Miller, msimamizi wa mtandao wa Linux. Kulingana na asili yao - hariri au vipengele vipya - mabadiliko ya mtandao huanguka katika moja ya cores mbili - net au net-next. Mabadiliko ya BPF yanasambazwa kwa njia sawa kati ya bpf ΠΈ bpf-next, ambazo huwekwa kwenye wavu na wavu-ifuatayo, mtawalia. Kwa maelezo zaidi, tazama bpf_endeleza_QA ΠΈ netdev-Maswali Yanayoulizwa Mara kwa Mara. Kwa hivyo chagua kernel kulingana na ladha yako na mahitaji ya uthabiti wa mfumo unaojaribu (*-next kernels ndio zisizo thabiti zaidi kati ya hizo zilizoorodheshwa).

Ni zaidi ya upeo wa kifungu hiki kuzungumza juu ya jinsi ya kusimamia faili za usanidi wa kernel - inadhaniwa kuwa tayari unajua jinsi ya kufanya hivyo, au tayari kujifunza peke yake. Hata hivyo, maagizo yafuatayo yanapaswa kuwa zaidi au chini ya kutosha ili kukupa mfumo unaofanya kazi unaowezeshwa na BPF.

Pakua moja ya kokwa hapo juu:

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

Jenga usanidi mdogo wa kufanya kazi:

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

Washa chaguo za BPF kwenye faili .config kwa chaguo lako mwenyewe (uwezekano mkubwa CONFIG_BPF tayari itawezeshwa kwani systemd inaitumia). Hapa kuna orodha ya chaguzi kutoka kwa kernel inayotumiwa kwa nakala hii:

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

Basi tunaweza kukusanyika kwa urahisi na kusanikisha moduli na kernel (kwa njia, unaweza kukusanya kernel kwa kutumia iliyokusanywa mpya. clangkwa kuongeza CC=clang):

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

na uwashe tena na kernel mpya (mimi hutumia kwa hili kexec kutoka kwa kifurushi 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

Huduma ya kawaida kutumika katika makala itakuwa matumizi bpftool, inayotolewa kama sehemu ya kinu cha Linux. Imeandikwa na kudumishwa na wasanidi wa BPF kwa wasanidi wa BPF na inaweza kutumika kudhibiti aina zote za vitu vya BPF - kupakia programu, kuunda na kuhariri ramani, kuchunguza maisha ya mfumo ikolojia wa BPF, n.k. Nyaraka katika mfumo wa misimbo chanzo kwa kurasa za mtu zinaweza kupatikana katika msingi au, tayari imekusanywa, mtandaoni.

Wakati wa uandishi huu bpftool huja tayari kwa RHEL, Fedora na Ubuntu (tazama, kwa mfano, uzi huu, ambayo inasimulia hadithi ambayo haijakamilika ya ufungaji bpftool katika Debian). Lakini ikiwa tayari umeunda kernel yako, basi jenga bpftool rahisi kama mkate:

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

$

(Hapa ${linux} - hii ni saraka yako ya kernel.) Baada ya kutekeleza amri hizi bpftool itakusanywa katika saraka ${linux}/tools/bpf/bpftool na inaweza kuongezwa kwenye njia (kwanza kabisa kwa mtumiaji root) au nakili tu kwa /usr/local/sbin.

Kusanya bpftool ni bora kutumia mwisho clang, iliyokusanywa kama ilivyoelezwa hapo juu, na angalia ikiwa imekusanyika kwa usahihi - kwa kutumia, kwa mfano, amri

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

ambayo itaonyesha ni vipengele vipi vya BPF vimewezeshwa kwenye kernel yako.

Kwa njia, amri ya awali inaweza kuendeshwa kama

# bpftool f p k

Hii inafanywa kwa mlinganisho na huduma kutoka kwa kifurushi iproute2, ambapo tunaweza, kwa mfano, kusema ip a s eth0 badala ya ip addr show dev eth0.

Hitimisho

BPF hukuruhusu kuvaa kiroboto ili kupima kwa ufanisi na kubadilisha utendakazi wa msingi. Mfumo huo ulifanikiwa sana, katika mila bora ya UNIX: utaratibu rahisi ambao hukuruhusu (re) kupanga kernel iliruhusu idadi kubwa ya watu na mashirika kufanya majaribio. Na, ingawa majaribio, pamoja na maendeleo ya miundombinu ya BPF yenyewe, ni mbali na kumaliza, mfumo tayari una ABI imara ambayo inakuwezesha kujenga kuaminika, na muhimu zaidi, mantiki ya biashara yenye ufanisi.

Ningependa kutambua kwamba, kwa maoni yangu, teknolojia imekuwa maarufu sana kwa sababu, kwa upande mmoja, inaweza ΠΈΠ³Ρ€Π°Ρ‚ΡŒ (usanifu wa mashine inaweza kueleweka zaidi au chini kwa jioni moja), na kwa upande mwingine, kutatua matatizo ambayo hayakuweza kutatuliwa (kwa uzuri) kabla ya kuonekana kwake. Vipengele hivi viwili kwa pamoja huwalazimisha watu kufanya majaribio na kuota ndoto, jambo ambalo husababisha kuibuka kwa suluhu za kiubunifu zaidi na zaidi.

Nakala hii, ingawa sio fupi sana, ni utangulizi tu kwa ulimwengu wa BPF na haielezi sifa za "juu" na sehemu muhimu za usanifu. Mpango wa kwenda mbele ni kitu kama hiki: makala inayofuata itakuwa muhtasari wa aina za programu za BPF (kuna aina 5.8 za programu zinazotumika kwenye kernel 30), kisha hatimaye tutaangalia jinsi ya kuandika maombi halisi ya BPF kwa kutumia programu za kufuatilia kernel. kama mfano, basi ni wakati wa kozi ya kina zaidi juu ya usanifu wa BPF, ikifuatiwa na mifano ya mitandao ya BPF na maombi ya usalama.

Makala yaliyotangulia katika mfululizo huu

  1. BPF kwa watoto wadogo, sehemu ya sifuri: BPF ya kawaida

Viungo

  1. Mwongozo wa Marejeleo wa BPF na XDP β€” hati kuhusu BPF kutoka kwa cilium, au kwa usahihi zaidi kutoka kwa Daniel Borkman, mmoja wa waundaji na watunzaji wa BPF. Hii ni mojawapo ya maelezo mazito ya kwanza, ambayo yanatofautiana na mengine kwa kuwa Danieli anajua hasa anachoandika na hakuna makosa hapo. Hasa, hati hii inaelezea jinsi ya kufanya kazi na programu za BPF za aina za XDP na TC kwa kutumia huduma inayojulikana. ip kutoka kwa kifurushi iproute2.

  2. Documentation/networking/filter.txt - faili asili iliyo na hati za BPF ya kawaida na iliyopanuliwa. Usomaji mzuri ikiwa unataka kuzama katika lugha ya kusanyiko na maelezo ya kiufundi ya usanifu.

  3. Blogu kuhusu BPF kutoka facebook. Inasasishwa mara chache, lakini ipasavyo, kama Alexei Starovoitov (mwandishi wa eBPF) na Andrii Nakryiko - (mtunzaji) wanaandika hapo. libbpf).

  4. Siri za bpftool. Uzi wa twitter wa kuburudisha kutoka kwa Quentin Monnet wenye mifano na siri za kutumia bpftool.

  5. Ingia katika BPF: orodha ya nyenzo za kusoma. Orodha kubwa (na bado inadumishwa) ya viungo vya hati za BPF kutoka Quentin Monnet.

Chanzo: mapenzi.com

Kuongeza maoni