BPF pre najmenších, časť prvá: rozšírený BPF

Na začiatku bola technológia a volala sa BPF. Pozreli sme sa na ňu predchádzajúca, starozákonný článok tejto série. V roku 2013 bola vďaka úsiliu Alexeja Starovoitova a Daniela Borkmana vyvinutá jeho vylepšená verzia, optimalizovaná pre moderné 64-bitové stroje, a zahrnutá do linuxového jadra. Táto nová technológia sa krátko nazývala Internal BPF, potom sa premenovala na Extended BPF a teraz, po niekoľkých rokoch, ju každý nazýva jednoducho BPF.

Zhruba povedané, BPF vám umožňuje spúšťať ľubovoľný kód dodaný používateľom v priestore jadra Linuxu a nová architektúra sa ukázala byť taká úspešná, že na popis všetkých jej aplikácií budeme potrebovať ešte tucet článkov. (Jediná vec, ktorá sa vývojárom nepodarila, ako môžete vidieť v kóde výkonu nižšie, bolo vytvorenie slušného loga.)

Tento článok popisuje štruktúru virtuálneho stroja BPF, rozhrania jadra pre prácu s BPF, vývojové nástroje, ako aj stručný, veľmi stručný prehľad existujúcich schopností, t.j. všetko, čo budeme v budúcnosti potrebovať na hlbšie štúdium praktických aplikácií BPF.
BPF pre najmenších, časť prvá: rozšírený BPF

Zhrnutie článku

Úvod do architektúry BPF. Najprv sa pozrieme na architektúru BPF z vtáčej perspektívy a načrtneme hlavné komponenty.

Registre a príkazový systém virtuálneho stroja BPF. Už keď máme predstavu o architektúre ako celku, popíšeme štruktúru virtuálneho stroja BPF.

Životný cyklus objektov BPF, súborový systém bpffs. V tejto časti sa bližšie pozrieme na životný cyklus BPF objektov – programov a máp.

Správa objektov pomocou systémového volania bpf. S určitým porozumením systému, ktorý už existuje, sa konečne pozrieme na to, ako vytvárať a manipulovať s objektmi z užívateľského priestoru pomocou špeciálneho systémového volania − bpf(2).

Пишем программы BPF с помощью libbpf. Samozrejme, môžete písať programy pomocou systémového volania. Ale je to ťažké. Pre realistickejší scenár vyvinuli jadroví programátori knižnicu libbpf. Vytvoríme základnú kostru aplikácie BPF, ktorú použijeme v nasledujúcich príkladoch.

Pomocníci jadra. Tu sa dozvieme, ako môžu programy BPF pristupovať k pomocným funkciám jadra – nástroju, ktorý spolu s mapami zásadne rozširuje možnosti nového BPF oproti klasickému.

Prístup k mapám z programov BPF. V tomto bode budeme vedieť dosť na to, aby sme presne pochopili, ako môžeme vytvárať programy, ktoré používajú mapy. A poďme dokonca rýchlo nahliadnuť do veľkého a mocného overovateľa.

Vývojové nástroje. Časť pomocníka o tom, ako zostaviť potrebné nástroje a jadro pre experimenty.

Záver. Na konci článku tí, ktorí sa dočítali až sem, nájdu v nasledujúcich článkoch motivujúce slová a stručný popis toho, čo sa bude diať. Uvedieme aj množstvo odkazov na samoštúdium pre tých, ktorí nemajú chuť či schopnosti čakať na pokračovanie.

Úvod do architektúry BPF

Predtým, ako začneme uvažovať o architektúre BPF, odkážeme ešte poslednýkrát (och). klasický BPF, ktorý bol vyvinutý ako reakcia na nástup RISC strojov a vyriešil problém efektívneho filtrovania paketov. Architektúra sa ukázala byť taká úspešná, že keďže sa zrodila v deväťdesiatych rokoch v Berkeley UNIX, bola portovaná na väčšinu existujúcich operačných systémov, prežila do šialených dvadsiatych rokov a stále nachádza nové aplikácie.

Nový BPF bol vyvinutý ako odpoveď na všadeprítomnosť 64-bitových strojov, cloudových služieb a zvýšenú potrebu nástrojov na vytváranie SDN (Software-dupresnený nvytváranie sietí). Nový BPF, vyvinutý sieťovými inžiniermi jadra ako vylepšená náhrada klasického BPF, doslova o šesť mesiacov neskôr našiel uplatnenie v náročnej úlohe sledovania linuxových systémov a teraz, šesť rokov po jeho objavení, budeme potrebovať celý ďalší článok. zoznam rôznych typov programov.

Smiešne obrázky

Vo svojom jadre je BPF sandboxový virtuálny stroj, ktorý vám umožňuje spúšťať „ľubovoľný“ kód v priestore jadra bez ohrozenia bezpečnosti. BPF programy sú vytvorené v užívateľskom priestore, načítané do jadra a pripojené k nejakému zdroju udalostí. Udalosťou môže byť napríklad doručenie paketu na sieťové rozhranie, spustenie nejakej funkcie jadra atď. V prípade balíka bude mať program BPF prístup k údajom a metadátam balíka (na čítanie a prípadne zápis, v závislosti od typu programu); v prípade spustenia funkcie jadra budú argumenty funkciu vrátane ukazovateľov na pamäť jadra atď.

Pozrime sa na tento proces bližšie. Na začiatok si povedzme o prvom rozdiele od klasického BPF, pre ktoré boli programy napísané v assembleri. V novej verzii bola architektúra rozšírená tak, aby bolo možné písať programy vo vyšších jazykoch, primárne, samozrejme, v C. Na tento účel bol vyvinutý backend pre llvm, ktorý umožňuje generovať bajtový kód pre architektúru BPF.

BPF pre najmenších, časť prvá: rozšírený BPF

Architektúra BPF bola čiastočne navrhnutá tak, aby fungovala efektívne na moderných strojoch. Aby to fungovalo v praxi, bajtový kód BPF sa po načítaní do jadra preloží do natívneho kódu pomocou komponentu nazývaného kompilátor JIT (Jvrchný In Time). Ďalej, ak si pamätáte, v klasickom BPF bol program načítaný do jadra a pripojený k zdroju udalosti atomicky - v kontexte jedného systémového volania. V novej architektúre sa to deje v dvoch fázach – najprv sa kód načíta do jadra pomocou systémového volania bpf(2)a potom, neskôr, prostredníctvom iných mechanizmov, ktoré sa líšia v závislosti od typu programu, sa program pripojí k zdroju udalosti.

Tu môže mať čitateľ otázku: bolo to možné? Ako je zaručená bezpečnosť vykonávania takéhoto kódu? Bezpečnosť vykonávania nám zaručuje fáza načítania BPF programov s názvom verifikátor (v angličtine sa táto fáza nazýva verifikátor a naďalej budem používať anglické slovo):

BPF pre najmenších, časť prvá: rozšírený BPF

Verifier je statický analyzátor, ktorý zabezpečuje, že program nenaruší normálnu činnosť jadra. To mimochodom neznamená, že program nemôže zasahovať do prevádzky systému - programy BPF v závislosti od typu môžu čítať a prepisovať časti pamäte jadra, vracať hodnoty funkcií, orezávať, pripájať, prepisovať a dokonca posielať sieťové pakety. Verifier zaručuje, že spustenie programu BPF nespôsobí pád jadra a že program, ktorý má podľa pravidiel prístup k zápisu, napríklad k dátam odchádzajúceho paketu, nebude môcť prepísať pamäť jadra mimo paketu. Po zoznámení sa so všetkými ostatnými zložkami BPF sa na overovateľa pozrieme trochu podrobnejšie v príslušnej časti.

Čo sme sa teda doteraz naučili? Používateľ napíše program v C, načíta ho do jadra pomocou systémového volania bpf(2), kde je skontrolovaný overovateľom a preložený do natívneho bajtkódu. Potom ten istý alebo iný používateľ pripojí program k zdroju udalosti a začne sa vykonávať. Oddelenie bootovania a pripojenia je potrebné z niekoľkých dôvodov. Po prvé, spustenie overovača je pomerne drahé a viacnásobným stiahnutím toho istého programu strácame počítačový čas. Po druhé, presne to, ako je program pripojený, závisí od jeho typu a jedno „univerzálne“ rozhranie vyvinuté pred rokom nemusí byť vhodné pre nové typy programov. (Aj keď teraz, keď sa architektúra stáva vyspelejšou, existuje myšlienka zjednotiť toto rozhranie na úrovni libbpf.)

Pozorný čitateľ si môže všimnúť, že s obrázkami ešte nekončíme. Všetko vyššie uvedené skutočne nevysvetľuje, prečo BPF zásadne mení obraz v porovnaní s klasickým BPF. Dve novinky, ktoré výrazne rozširujú rozsah použiteľnosti, sú možnosť využívať zdieľanú pamäť a pomocné funkcie jadra. V BPF je zdieľaná pamäť implementovaná pomocou takzvaných máp – zdieľaných dátových štruktúr so špecifickým API. Tento názov dostali pravdepodobne preto, že prvý typ mapy, ktorý sa objavil, bola hašovacia tabuľka. Potom sa objavili polia, lokálne (per-CPU) hašovacie tabuľky a lokálne polia, vyhľadávacie stromy, mapy obsahujúce ukazovatele na programy BPF a mnoho ďalšieho. Teraz je pre nás zaujímavé, že programy BPF majú teraz schopnosť udržiavať stav medzi hovormi a zdieľať ho s inými programami a s užívateľským priestorom.

K mapám sa pristupuje z používateľských procesov pomocou systémového volania bpf(2)a z programov BPF spustených v jadre pomocou pomocných funkcií. Okrem toho existujú pomocníci nielen na prácu s mapami, ale aj na prístup k ďalším schopnostiam jadra. Napríklad programy BPF môžu používať pomocné funkcie na posielanie paketov iným rozhraniam, generovanie udalostí perf, prístup k štruktúram jadra atď.

BPF pre najmenších, časť prvá: rozšírený BPF

Stručne povedané, BPF poskytuje možnosť načítať ľubovoľný, t. j. overovateľom testovaný používateľský kód do priestoru jadra. Tento kód môže ukladať stav medzi volaniami a vymieňať si údaje s užívateľským priestorom a má tiež prístup k subsystémom jadra, ktoré tento typ programu umožňuje.

Toto je už podobné schopnostiam, ktoré poskytujú moduly jadra, v porovnaní s ktorými má BPF určité výhody (samozrejme, porovnávať môžete iba podobné aplikácie, napríklad sledovanie systému - s BPF nemôžete napísať ľubovoľný ovládač). Môžete si všimnúť nižšiu vstupnú hranicu (niektoré nástroje, ktoré používajú BPF, nevyžadujú od používateľa znalosti programovania v jadre alebo programovacie zručnosti vo všeobecnosti), bezpečnosť pri behu (zdvihnite ruku v komentároch pre tých, ktorí pri písaní neporušili systém alebo testovanie modulov), atomicita – pri opätovnom načítaní modulov dochádza k prestojom a subsystém BPF zaisťuje, že sa nezmeškajú žiadne udalosti (aby som bol spravodlivý, neplatí to pre všetky typy programov BPF).

Prítomnosť takýchto schopností robí z BPF univerzálny nástroj na rozšírenie jadra, čo potvrdzuje aj prax: do BPF sa pridáva stále viac nových typov programov, stále viac veľkých spoločností používa BPF na bojových serveroch 24×7, stále viac a viac startupy budujú svoje podnikanie na riešeniach, na ktorých sú založené BPF. BPF sa používa všade: pri ochrane pred útokmi DDoS, vytváraní SDN (napríklad implementácia sietí pre kubernetes), ako hlavný nástroj na sledovanie systému a zberač štatistík, v systémoch detekcie narušenia a systémoch sandbox atď.

Dokončime tu prehľadovú časť článku a pozrime sa na virtuálny stroj a ekosystém BPF podrobnejšie.

Odbočka: komunálne služby

Aby ste mohli spustiť príklady v nasledujúcich častiach, možno budete potrebovať aspoň niekoľko pomocných programov llvm/clang s podporou bpf a bpftool, V časti Vývojové nástroje Môžete si prečítať pokyny na zostavenie nástrojov, ako aj vášho jadra. Táto časť je umiestnená nižšie, aby nenarúšala harmóniu našej prezentácie.

Registre virtuálnych strojov a systém výučby BPF

Architektúra a príkazový systém BPF boli vyvinuté s ohľadom na skutočnosť, že programy budú napísané v jazyku C a po načítaní do jadra preložené do natívneho kódu. Preto bol počet registrov a súbor príkazov zvolený s ohľadom na prienik, v matematickom zmysle, schopností moderných strojov. Okrem toho boli na programy uvalené rôzne obmedzenia, napríklad donedávna nebolo možné zapisovať slučky a podprogramy a počet inštrukcií bol obmedzený na 4096 (teraz môžu privilegované programy načítať až milión inštrukcií).

BPF má jedenásť užívateľsky prístupných 64-bitových registrov r0-r10 a počítadlo programov. Registrovať r10 obsahuje ukazovateľ rámca a je len na čítanie. Programy majú za behu prístup k 512-bajtovému zásobníku a neobmedzenému množstvu zdieľanej pamäte vo forme máp.

Programy BPF môžu spúšťať špecifickú sadu pomocníkov jadra programového typu a v poslednom čase aj bežné funkcie. Každá volaná funkcia môže mať až päť argumentov odovzdaných v registroch r1-r5a návratová hodnota sa odovzdá do r0. Je zaručené, že po návrate z funkcie sa obsah registrov r6-r9 nezmení sa.

Pre efektívny preklad programu registre r0-r11 pre všetky podporované architektúry sú jedinečne mapované na skutočné registre, berúc do úvahy vlastnosti ABI súčasnej architektúry. Napríklad pre x86_64 registrov r1-r5, ktoré sa používajú na odovzdávanie parametrov funkcií, sú zobrazené na rdi, rsi, rdx, rcx, r8, ktoré sa používajú na odovzdávanie parametrov funkciám x86_64. Napríklad kód vľavo sa preloží do kódu vpravo takto:

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

Registrovať r0 používa sa aj na vrátenie výsledku vykonávania programu a v registri r1 programu sa odovzdá ukazovateľ na kontext - v závislosti od typu programu to môže byť napríklad štruktúra struct xdp_md (pre XDP) alebo štruktúru struct __sk_buff (pre rôzne sieťové programy) alebo štruktúru struct pt_regs (pre rôzne typy programov sledovania) atď.

Mali sme teda sadu registrov, pomocníkov jadra, zásobník, kontextový ukazovateľ a zdieľanú pamäť vo forme máp. Nie že by toto všetko bolo na výlete absolútne nevyhnutné, ale...

Pokračujme v popise a hovorme o príkazovom systéme pre prácu s týmito objektmi. Všetky (Takmer všetky) BPF inštrukcie majú pevnú 64-bitovú veľkosť. Ak sa pozriete na jednu inštrukciu na 64-bitovom stroji Big Endian, uvidíte

BPF pre najmenších, časť prvá: rozšírený BPF

Tu Code - toto je kódovanie inštrukcie, Dst/Src sú kódovania prijímača a zdroja, resp. Off - 16-bitové podpísané odsadenie a Imm je 32-bitové celé číslo so znamienkom používané v niektorých inštrukciách (podobne ako cBPF konštanta K). Kódovanie Code má jeden z dvoch typov:

BPF pre najmenších, časť prvá: rozšírený BPF

Triedy inštrukcií 0, 1, 2, 3 definujú príkazy pre prácu s pamäťou. Oni sa volajú, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, resp. Triedy 4, 7 (BPF_ALU, BPF_ALU64) tvoria súbor inštrukcií ALU. Triedy 5, 6 (BPF_JMP, BPF_JMP32) obsahujú pokyny na skok.

Ďalší plán na štúdium inštrukčného systému BPF je nasledovný: namiesto starostlivého uvádzania všetkých inštrukcií a ich parametrov sa v tejto časti pozrieme na niekoľko príkladov a z nich bude jasné, ako inštrukcie v skutočnosti fungujú a ako manuálne rozobrať akýkoľvek binárny súbor pre BPF. Pre konsolidáciu materiálu ďalej v článku sa s jednotlivými návodmi stretneme aj v častiach o Verifier, JIT kompilátore, preklade klasických BPF, ako aj pri štúdiu máp, volaní funkcií a pod.

Keď hovoríme o jednotlivých pokynoch, budeme odkazovať na základné súbory bpf.h и bpf_common.h, ktoré definujú číselné kódy inštrukcií BPF. Pri štúdiu architektúry na vlastnej koži a/alebo analýze binárnych súborov môžete nájsť sémantiku v nasledujúcich zdrojoch zoradených podľa zložitosti: Neoficiálna špecifikácia eBPF, Referenčná príručka BPF a XDP, súbor inštrukcií, Documentation/networking/filter.txt a samozrejme v zdrojovom kóde Linuxu - overovač, JIT, BPF interpreter.

Príklad: rozoberanie BPF v hlave

Pozrime sa na príklad, v ktorom zostavujeme program readelf-example.c a pozrite sa na výslednú dvojhviezdu. Prezradíme pôvodný obsah readelf-example.c nižšie, keď obnovíme jeho logiku z binárnych kódov:

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

Prvý stĺpec na výstupe readelf je odsadenie a náš program teda pozostáva zo štyroch príkazov:

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

Príkazové kódy sú rovnaké b7, 15, b7 и 95. Pripomeňme, že najmenej významné tri bity sú trieda inštrukcie. V našom prípade je štvrtý bit všetkých inštrukcií prázdny, takže triedy inštrukcií sú 7, 5, 7, 5. Trieda 7 je BPF_ALU64a 5 je BPF_JMP. Pre obe triedy je formát inštrukcie rovnaký (pozri vyššie) a náš program môžeme prepísať takto (zároveň prepíšeme zvyšné stĺpce do ľudskej podoby):

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

Operácie b trieda ALU64 - je BPF_MOV. Priradí hodnotu cieľovému registru. Ak je bit nastavený s (zdroj), potom sa hodnota prevezme zo zdrojového registra a ak, ako v našom prípade, nie je nastavená, potom sa hodnota prevezme z poľa Imm. Takže v prvom a treťom návode vykonáme operáciu r0 = Imm. Ďalej je to prevádzka JMP triedy 1 BPF_JEQ (skok, ak je rovnaký). V našom prípade od bit S je nula, porovnáva hodnotu zdrojového registra s poľom Imm. Ak sa hodnoty zhodujú, dôjde k prechodu na PC + OffKde PC, ako obvykle, obsahuje adresu nasledujúceho pokynu. Nakoniec je to prevádzka JMP triedy 9 BPF_EXIT. Táto inštrukcia ukončí program a vráti sa do jadra r0. Pridajme do našej tabuľky nový stĺpec:

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

Môžeme to prepísať do vhodnejšej formy:

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

Ak si pamätáme, čo je v registri r1 programu je odovzdaný ukazovateľ na kontext z jadra a do registra r0 hodnota sa vráti do jadra, potom môžeme vidieť, že ak je ukazovateľ na kontext nula, vrátime 1 a v opačnom prípade - 2. Skontrolujeme, či máme pravdu pohľadom na zdroj:

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

Áno, je to nezmyselný program, ale prekladá sa len do štyroch jednoduchých pokynov.

Príklad výnimky: 16-bajtová inštrukcia

Už sme spomenuli, že niektoré inštrukcie zaberajú viac ako 64 bitov. Týka sa to napríklad návodu lddw (Kód = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — načítať dvojité slovo z polí do registra Imm, Faktom je, že Imm má veľkosť 32 a dvojité slovo má 64 bitov, takže načítanie 64-bitovej okamžitej hodnoty do registra v jednej 64-bitovej inštrukcii nebude fungovať. Na tento účel sa použijú dve susediace inštrukcie na uloženie druhej časti 64-bitovej hodnoty do poľa Imm... Príklad:

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

V binárnom programe sú len dve inštrukcie:

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

Opäť sa stretneme s pokynmi lddw, keď hovoríme o presunoch a práci s mapami.

Príklad: demontáž BPF pomocou štandardných nástrojov

Takže sme sa naučili čítať binárne kódy BPF a v prípade potreby sme pripravení analyzovať akúkoľvek inštrukciu. Je však potrebné povedať, že v praxi je pohodlnejšie a rýchlejšie rozoberať programy pomocou štandardných nástrojov, napríklad:

$ 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

Životný cyklus objektov BPF, súborový systém bpffs

(Niektoré podrobnosti opísané v tejto podsekcii som sa prvýkrát dozvedel z pôst Alexej Starovoitov v Blog BPF.)

BPF objekty – programy a mapy – sa vytvárajú z užívateľského priestoru pomocou príkazov BPF_PROG_LOAD и BPF_MAP_CREATE systémové volanie bpf(2), presne o tom, ako sa to stane, si povieme v ďalšej časti. Tým sa vytvárajú dátové štruktúry jadra a pre každú z nich refcount (počet referencií) sa nastaví na jednu a používateľovi sa vráti deskriptor súboru, ktorý ukazuje na objekt. Po zatvorení rukoväte refcount objekt sa zníži o jednotku a keď dosiahne nulu, objekt sa zničí.

Ak program používa mapy, potom refcount tieto mapy sa po načítaní programu zväčšia o jednu, t.j. ich deskriptory súborov môžu byť zatvorené z používateľského procesu a stále refcount sa nestane nulou:

BPF pre najmenších, časť prvá: rozšírený BPF

Po úspešnom načítaní programu ho zvyčajne pripojíme k nejakému generátoru udalostí. Môžeme ho napríklad umiestniť na sieťové rozhranie na spracovanie prichádzajúcich paketov alebo ho k niektorým pripojiť tracepoint v jadre. V tomto momente sa o jednotku zvýši aj počítadlo referencií a budeme môcť zavrieť deskriptor súboru v programe loader.

Čo sa stane, ak teraz bootloader vypneme? Závisí to od typu generátora udalostí (háku). Všetky sieťové háky budú existovať po dokončení zavádzača, ide o takzvané globálne háky. A napríklad programy sledovania budú uvoľnené po ukončení procesu, ktorý ich vytvoril (a preto sa nazývajú lokálne, od „miestneho k procesu“). Technicky, lokálne háky majú vždy zodpovedajúci deskriptor súboru v užívateľskom priestore, a preto sa zatvárajú, keď je proces zatvorený, ale globálne háky nie. Na nasledujúcom obrázku sa pomocou červených krížikov snažím ukázať, ako ukončenie programu loader ovplyvňuje životnosť objektov v prípade lokálnych a globálnych hákov.

BPF pre najmenších, časť prvá: rozšírený BPF

Prečo existuje rozdiel medzi lokálnymi a globálnymi háčikmi? Spúšťanie niektorých typov sieťových programov má zmysel bez užívateľského priestoru, predstavte si napríklad DDoS ochranu – bootloader napíše pravidlá a pripojí program BPF k sieťovému rozhraniu, po čom sa bootloader môže ísť zabiť. Na druhej strane si predstavte ladiaci sledovací program, ktorý ste za desať minút napísali na kolene – po dokončení by ste chceli, aby v systéme nezostali žiadne smeti a to zabezpečia lokálne háčiky.

Na druhej strane si predstavte, že sa chcete pripojiť k sledovaciemu bodu v jadre a zbierať štatistiky počas mnohých rokov. V tomto prípade by ste chceli dokončiť používateľskú časť a z času na čas sa vrátiť k štatistikám. Túto možnosť poskytuje súborový systém bpf. Je to systém pseudosúborov iba v pamäti, ktorý umožňuje vytváranie súborov, ktoré odkazujú na objekty BPF, a tým zvyšujú refcount predmety. Potom môže nakladač vystúpiť a objekty, ktoré vytvoril, zostanú živé.

BPF pre najmenších, časť prvá: rozšírený BPF

Vytváranie súborov v bpffs, ktoré odkazujú na objekty BPF, sa nazýva „pripnutie“ (ako v nasledujúcej fráze: „proces môže pripnúť program alebo mapu BPF“). Vytváranie súborových objektov pre BPF objekty má zmysel nielen pre predĺženie životnosti lokálnych objektov, ale aj pre použiteľnosť globálnych objektov – ak sa vrátime k príkladu s globálnym programom DDoS ochrany, chceme mať možnosť prísť sa pozrieť na štatistiky z času na čas.

Súborový systém BPF je zvyčajne pripojený /sys/fs/bpf, ale dá sa namontovať aj lokálne, napríklad takto:

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

Názvy súborových systémov sa vytvárajú pomocou príkazu BPF_OBJ_PIN Systémové volanie BPF. Pre ilustráciu si vezmeme program, skompilujeme ho, nahráme a pripneme bpffs. Náš program nerobí nič užitočné, iba prezentujeme kód, aby ste si mohli príklad zopakovať:

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

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

Poďme skompilovať tento program a vytvoriť lokálnu kópiu súborového systému bpffs:

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

Teraz si stiahnite náš program pomocou pomôcky bpftool a pozrite sa na sprievodné systémové volania bpf(2) (niektoré irelevantné riadky odstránené z výstupu sledovania):

$ 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

Tu sme načítali program pomocou BPF_PROG_LOAD, dostal deskriptor súboru z jadra 3 a pomocou príkazu BPF_OBJ_PIN pripol tento deskriptor súboru ako súbor "bpf-mountpoint/test". Potom program bootloader bpftool dokončil prácu, ale náš program zostal v jadre, hoci sme ho nepripojili k žiadnemu sieťovému rozhraniu:

$ sudo bpftool prog | tail -3
783: xdp  name test  tag 5c8ba0cf164cb46c  gpl
        loaded_at 2020-05-05T13:27:08+0000  uid 0
        xlated 24B  jited 41B  memlock 4096B

Objekt súboru môžeme normálne vymazať unlink(2) a potom sa príslušný program vymaže:

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

Odstraňovanie objektov

Keď už hovoríme o odstraňovaní objektov, je potrebné objasniť, že po odpojení programu od háku (generátora udalostí) ani jedna nová udalosť nespustí jeho spustenie, ale všetky aktuálne inštancie programu budú dokončené v normálnom poradí. .

Niektoré typy programov BPF umožňujú nahradiť program za chodu, t.j. poskytujú sekvenčnú atomicitu replace = detach old program, attach new program. V tomto prípade všetky aktívne inštancie starej verzie programu dokončia svoju prácu a z nového programu sa vytvoria nové obslužné programy udalostí, pričom „atómickosť“ tu znamená, že nezmeškáte ani jednu udalosť.

Pripojenie programov k zdrojom udalostí

V tomto článku nebudeme samostatne popisovať pripojenie programov k zdrojom udalostí, pretože má zmysel študovať to v kontexte konkrétneho typu programu. Cm. príklad nižšie, v ktorom ukážeme, ako sú programy ako XDP prepojené.

Manipulácia s objektmi pomocou systémového volania bpf

programy BPF

Všetky objekty BPF sa vytvárajú a riadia z užívateľského priestoru pomocou systémového volania bpfs nasledujúcim prototypom:

#include <linux/bpf.h>

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

Tu je tím cmd je jednou z hodnôt typu enum bpf_cmd, attr — ukazovateľ na parametre pre konkrétny program a size — veľkosť objektu podľa ukazovateľa, t.j. zvyčajne toto sizeof(*attr). V jadre 5.8 systémové volanie bpf podporuje 34 rôznych príkazov a určenie union bpf_attr zaberá 200 riadkov. Nemali by sme sa toho však zľaknúť, keďže s príkazmi a parametrami sa zoznámime v priebehu niekoľkých článkov.

Začnime tým BPF_PROG_LOAD, ktorý vytvára BPF programy – vezme sadu BPF inštrukcií a nahrá ju do jadra. V momente načítania sa spustí overovač a následne sa používateľovi vráti kompilátor JIT a po úspešnom vykonaní deskriptor súboru programu. Čo sa s ním stane ďalej, sme videli v predchádzajúcej časti o životnom cykle BPF objektov.

Teraz napíšeme vlastný program, ktorý načíta jednoduchý program BPF, ale najprv sa musíme rozhodnúť, aký program chceme načítať - budeme musieť vybrať тип a v rámci tohto typu napíšte program, ktorý prejde overovacím testom. Aby sme však proces nekomplikovali, tu je hotové riešenie: vezmeme si program ako BPF_PROG_TYPE_XDP, ktorý vráti hodnotu XDP_PASS (preskočiť všetky balíčky). V assembleri BPF to vyzerá veľmi jednoducho:

r0 = 2
exit

Potom, čo sme sa rozhodli že nahráme, môžeme vám povedať, ako to urobíme:

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

Zaujímavé udalosti v programe začínajú definíciou poľa insns - náš program BPF v strojovom kóde. V tomto prípade je každá inštrukcia programu BPF zabalená do štruktúry bpf_insn. Prvý prvok insns vyhovuje pokynom r0 = 2, druhy - exit.

Ustúpiť. Jadro definuje pohodlnejšie makrá na písanie strojových kódov a používanie hlavičkového súboru jadra tools/include/linux/filter.h mohli by sme písať

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

Ale keďže písanie BPF programov v natívnom kóde je potrebné len na písanie testov v jadre a článkov o BPF, absencia týchto makier naozaj nekomplikuje vývojárovi život.

Po zadefinovaní programu BPF prejdeme k jeho načítaniu do jadra. Naša minimalistická sada parametrov attr obsahuje typ programu, sadu a počet inštrukcií, požadovanú licenciu a názov "woo", pomocou ktorého po stiahnutí nájdeme náš program v systéme. Program, ako bolo sľúbené, sa načíta do systému pomocou systémového volania bpf.

Na konci programu skončíme v nekonečnej slučke, ktorá simuluje užitočné zaťaženie. Bez nej bude program zabitý jadrom, keď sa zatvorí deskriptor súboru, ktorý nám vrátilo systémové volanie bpf, a v systéme ho neuvidíme.

No, sme pripravení na testovanie. Poďme zostaviť a spustiť program pod straceaby ste skontrolovali, či všetko funguje ako má:

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

Všetko je v poriadku, bpf(2) vrátil nám rukoväť 3 a dostali sme sa do nekonečnej slučky s pause(). Skúsme nájsť náš program v systéme. Za týmto účelom prejdeme na iný terminál a použijeme nástroj 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)

Vidíme, že v systéme je načítaný program woo ktorého globálne ID je 390 a momentálne prebieha simple-prog existuje otvorený deskriptor súboru, ktorý ukazuje na program (a ak simple-prog potom dokončí prácu woo zmizne). Ako sa dalo očakávať, program woo zaberá 16 bajtov - dve inštrukcie - binárnych kódov v architektúre BPF, ale v natívnej forme (x86_64) je to už 40 bajtov. Pozrime sa na náš program v jeho pôvodnej podobe:

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

žiadne prekvapenia. Teraz sa pozrime na kód generovaný kompilátorom 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

nie veľmi efektívne pre exit(2), ale spravodlivo povedané, náš program je príliš jednoduchý a pre netriviálne programy je samozrejme potrebný prológ a epilóg pridaný kompilátorom JIT.

mapy

Programy BPF môžu využívať oblasti štruktúrovanej pamäte, ktoré sú prístupné ako pre iné programy BPF, tak aj pre programy v užívateľskom priestore. Tieto objekty sa nazývajú mapy av tejto časti si ukážeme, ako s nimi manipulovať pomocou systémového volania bpf.

Hneď si povedzme, že možnosti máp sa neobmedzujú len na prístup k zdieľanej pamäti. Existujú špeciálne mapy obsahujúce napríklad ukazovatele na programy BPF alebo ukazovatele na sieťové rozhrania, mapy pre prácu s udalosťami perf atď. Nebudeme tu o nich hovoriť, aby sme čitateľa nezmiatli. Okrem toho ignorujeme problémy so synchronizáciou, pretože to nie je dôležité pre naše príklady. Kompletný zoznam dostupných typov máp nájdete v <linux/bpf.h>, a v tejto časti si vezmeme ako príklad historicky prvý typ, hašovaciu tabuľku BPF_MAP_TYPE_HASH.

Ak vytvoríte hašovaciu tabuľku napríklad v C++, povedali by ste unordered_map<int,long> woo, čo v ruštine znamená „Potrebujem stôl woo neobmedzená veľkosť, ktorých kľúče sú typu inta hodnoty sú typ long" Aby sme vytvorili BPF hašovaciu tabuľku, musíme urobiť takmer to isté, okrem toho, že musíme určiť maximálnu veľkosť tabuľky a namiesto špecifikovania typov kľúčov a hodnôt musíme špecifikovať ich veľkosti v bajtoch. . Na vytvorenie máp použite príkaz BPF_MAP_CREATE systémové volanie bpf. Pozrime sa na viac-menej minimálny program, ktorý vytvára mapu. Po predchádzajúcom programe, ktorý načítava programy BPF, by sa vám tento mal zdať jednoduchý:

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

Tu definujeme sadu parametrov attr, v ktorom hovoríme „Potrebujem hašovaciu tabuľku s kľúčmi a hodnotami veľkosti sizeof(int), do ktorého môžem dať maximálne štyri prvky.“ Pri vytváraní BPF máp môžete zadať ďalšie parametre, napríklad rovnakým spôsobom ako v príklade s programom sme zadali názov objektu ako "woo".

Poďme skompilovať a spustiť program:

$ clang -g -O2 simple-map.c -o simple-map
$ sudo strace ./simple-map
execve("./simple-map", ["./simple-map"], 0x7ffd40a27070 /* 14 vars */) = 0
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=4, max_entries=4, map_name="woo", ...}, 72) = 3
pause(

Tu je systémové volanie bpf(2) nám vrátilo číslo deskriptorovej mapy 3 a potom program podľa očakávania čaká na ďalšie pokyny v systémovom volaní pause(2).

Teraz pošlime náš program na pozadie alebo otvorme iný terminál a pozrime sa na náš objekt pomocou utility bpftool (našu mapu môžeme odlíšiť od ostatných podľa názvu):

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

Číslo 114 je globálne ID nášho objektu. Akýkoľvek program v systéme môže použiť toto ID na otvorenie existujúcej mapy pomocou príkazu BPF_MAP_GET_FD_BY_ID systémové volanie bpf.

Teraz sa môžeme hrať s našou hash tabuľkou. Pozrime sa na jeho obsah:

$ sudo bpftool map dump id 114
Found 0 elements

Prázdny. Dajme tomu hodnotu hash[1] = 1:

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

Pozrime sa ešte raz na tabuľku:

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

Hurá! Podarilo sa nám pridať jeden prvok. Všimnite si, že na to musíme pracovať na úrovni bajtov, pretože bptftool nevie, aký typ sú hodnoty v hašovacej tabuľke. (Tieto znalosti jej možno preniesť pomocou BTF, ale o tom teraz.)

Ako presne bpftool číta a pridáva prvky? Poďme sa pozrieť pod kapotu:

$ 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

Najprv sme pomocou príkazu otvorili mapu podľa jej globálneho ID BPF_MAP_GET_FD_BY_ID и bpf(2) nám vrátil deskriptor 3. Ďalej pomocou príkazu BPF_MAP_GET_NEXT_KEY odovzdaním sme našli prvý kľúč v tabuľke NULL ako ukazovateľ na "predchádzajúci" kľúč. Ak máme kľúč, môžeme to urobiť BPF_MAP_LOOKUP_ELEMktorý vráti hodnotu do ukazovateľa value. Ďalším krokom je, že sa pokúsime nájsť ďalší prvok odovzdaním ukazovateľa na aktuálny kľúč, ale naša tabuľka obsahuje iba jeden prvok a príkaz BPF_MAP_GET_NEXT_KEY sa vracia ENOENT.

Dobre, zmeňme hodnotu kľúčom 1, povedzme, že naša obchodná logika vyžaduje registráciu 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

Ako sa dalo očakávať, je to veľmi jednoduché: príkaz BPF_MAP_GET_FD_BY_ID otvorí našu mapu podľa ID a príkazu BPF_MAP_UPDATE_ELEM prepíše prvok.

Takže po vytvorení hašovacej tabuľky z jedného programu môžeme čítať a zapisovať jej obsah z iného. Všimnite si, že ak sme to dokázali urobiť z príkazového riadku, potom to dokáže akýkoľvek iný program v systéme. Okrem vyššie opísaných príkazov pre prácu s mapami z užívateľského priestoru, Nasledujúci:

  • BPF_MAP_LOOKUP_ELEM: nájsť hodnotu podľa kľúča
  • BPF_MAP_UPDATE_ELEM: aktualizovať/vytvoriť hodnotu
  • BPF_MAP_DELETE_ELEM: vytiahnite kľúč
  • BPF_MAP_GET_NEXT_KEY: nájsť ďalší (alebo prvý) kľúč
  • BPF_MAP_GET_NEXT_ID: umožňuje vám prejsť všetkými existujúcimi mapami, tak to funguje bpftool map
  • BPF_MAP_GET_FD_BY_ID: otvorte existujúcu mapu podľa jej globálneho ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomicky aktualizovať hodnotu objektu a vrátiť starú
  • BPF_MAP_FREEZE: urobte mapu nemennou z používateľského priestoru (túto operáciu nie je možné vrátiť späť)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: hromadné operácie. Napríklad, BPF_MAP_LOOKUP_AND_DELETE_BATCH - toto je jediný spoľahlivý spôsob čítania a resetovania všetkých hodnôt z mapy

Nie všetky tieto príkazy fungujú pre všetky typy máp, ale vo všeobecnosti práca s inými typmi máp z užívateľského priestoru vyzerá úplne rovnako ako práca s hašovacími tabuľkami.

Pre poriadok ukončíme experimenty s hashovacími tabuľkami. Pamätáte si, že sme vytvorili tabuľku, ktorá môže obsahovať až štyri kľúče? Pridajme ešte niekoľko prvkov:

$ 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

Zatiaľ je všetko dobré:

$ 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

Skúsme pridať ešte jednu:

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

Podľa očakávania sa nám to nepodarilo. Pozrime sa na chybu podrobnejšie:

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

Všetko je v poriadku: podľa očakávania tím BPF_MAP_UPDATE_ELEM pokúsi sa vytvoriť nový, piaty kľúč, ale zlyhá E2BIG.

Môžeme teda vytvárať a načítavať programy BPF, ako aj vytvárať a spravovať mapy z používateľského priestoru. Teraz je logické pozrieť sa na to, ako môžeme použiť mapy zo samotných programov BPF. Mohli by sme o tom hovoriť v jazyku ťažko čitateľných programov v kódoch strojových makier, ale v skutočnosti prišiel čas ukázať, ako sa programy BPF skutočne píšu a udržiavajú - pomocou libbpf.

(Pre čitateľov, ktorí nie sú spokojní s nedostatkom príkladu nízkej úrovne: podrobne rozoberieme programy, ktoré využívajú mapy a pomocné funkcie vytvorené pomocou libbpf a povie vám, čo sa stane na úrovni výučby. Pre nespokojných čitateľov veľmi veľa, dodali sme príklad na príslušnom mieste v článku.)

Písanie BPF programov pomocou libbpf

Písanie programov BPF pomocou strojových kódov môže byť zaujímavé len prvýkrát a potom príde sýtosť. V tejto chvíli musíte obrátiť svoju pozornosť llvm, ktorý má backend na generovanie kódu pre architektúru BPF, ako aj knižnicu libbpf, ktorý vám umožňuje písať používateľskú stránku aplikácií BPF a načítať kód programov BPF generovaných pomocou llvm/clang.

V skutočnosti, ako uvidíme v tomto a nasledujúcich článkoch, libbpf robí dosť veľa práce bez neho (alebo podobných nástrojov - iproute2, libbcc, libbpf-go, atď.) nedá sa žiť. Jedna zo zabijáckych čŕt projektu libbpf je BPF CO-RE (Compile Once, Run Everywhere) – projekt, ktorý vám umožňuje písať programy BPF, ktoré sú prenosné z jedného jadra do druhého, s možnosťou bežať na rôznych API (napríklad keď sa štruktúra jadra zmení od verzie na verziu). Aby ste mohli pracovať s CO-RE, vaše jadro musí byť skompilované s podporou BTF (popisujeme, ako to urobiť v časti Vývojové nástroje. Či je vaše jadro zostavené s BTF alebo nie, môžete skontrolovať veľmi jednoducho – prítomnosťou nasledujúceho súboru:

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

Tento súbor uchováva informácie o všetkých typoch údajov používaných v jadre a používa sa vo všetkých našich príkladoch použitia libbpf. O CO-RE sa budeme podrobne rozprávať v nasledujúcom článku, ale v tomto - stačí si zostaviť jadro CONFIG_DEBUG_INFO_BTF.

knižnica libbpf býva priamo v adresári tools/lib/bpf jadro a jeho vývoj prebieha prostredníctvom mailing listu [email protected]. Pre potreby aplikácií žijúcich mimo jadra je však udržiavané samostatné úložisko https://github.com/libbpf/libbpf v ktorom je knižnica jadra zrkadlená pre prístup na čítanie viac-menej tak, ako je.

V tejto časti sa pozrieme na to, ako môžete vytvoriť projekt, ktorý používa libbpf, napíšme si niekoľko (viac-menej nezmyselných) testovacích programov a podrobne rozoberme, ako to celé funguje. To nám umožní v nasledujúcich častiach jednoduchšie vysvetliť, ako presne programy BPF interagujú s mapami, pomocníkmi jadra, BTF atď.

Typicky projekty využívajúce libbpf pridajte úložisko GitHub ako submodul git, urobíme to isté:

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

Chystáte sa libbpf veľmi jednoduché:

$ cd libbpf/src
$ mkdir build
$ OBJDIR=build DESTDIR=root make -s install
$ find root
root
root/usr
root/usr/include
root/usr/include/bpf
root/usr/include/bpf/bpf_tracing.h
root/usr/include/bpf/xsk.h
root/usr/include/bpf/libbpf_common.h
root/usr/include/bpf/bpf_endian.h
root/usr/include/bpf/bpf_helpers.h
root/usr/include/bpf/btf.h
root/usr/include/bpf/bpf_helper_defs.h
root/usr/include/bpf/bpf.h
root/usr/include/bpf/libbpf_util.h
root/usr/include/bpf/libbpf.h
root/usr/include/bpf/bpf_core_read.h
root/usr/lib64
root/usr/lib64/libbpf.so.0.1.0
root/usr/lib64/libbpf.so.0
root/usr/lib64/libbpf.a
root/usr/lib64/libbpf.so
root/usr/lib64/pkgconfig
root/usr/lib64/pkgconfig/libbpf.pc

Náš ďalší plán v tejto sekcii je nasledovný: napíšeme program BPF ako BPF_PROG_TYPE_XDP, rovnako ako v predchádzajúcom príklade, ale v C ho skompilujeme pomocou clanga napíšte pomocný program, ktorý ho nahrá do jadra. V nasledujúcich častiach rozšírime možnosti programu BPF aj programu asistenta.

Príklad: vytvorenie plnohodnotnej aplikácie pomocou libbpf

Na začiatok použijeme súbor /sys/kernel/btf/vmlinux, ktorý bol spomenutý vyššie, a vytvorte jeho ekvivalent vo forme hlavičkového súboru:

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

Tento súbor bude uchovávať všetky dátové štruktúry dostupné v našom jadre, napríklad takto je v jadre definovaná hlavička IPv4:

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

Teraz napíšeme náš program BPF v 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";

Aj keď sa náš program ukázal ako veľmi jednoduchý, stále musíme venovať pozornosť mnohým detailom. Po prvé, prvý súbor hlavičky, ktorý zahrnieme, je vmlinux.h, ktorý sme práve vygenerovali pomocou bpftool btf dump - teraz už nemusíme inštalovať balík kernel-headers, aby sme zistili, ako vyzerajú štruktúry jadra. Nasledujúci hlavičkový súbor k nám prichádza z knižnice libbpf. Teraz ho potrebujeme len na definovanie makra SEC, ktorý odošle znak do príslušnej sekcie súboru objektov ELF. Náš program je obsiahnutý v sekcii xdp/simple, kde pred lomkou definujeme typ programu BPF - to je konvencia používaná v libbpf, na základe názvu sekcie nahradí správny typ pri spustení bpf(2). Samotný program BPF je C - veľmi jednoduchý a pozostáva z jedného riadku return XDP_PASS. Na záver samostatná časť "license" obsahuje názov licencie.

Náš program môžeme skompilovať pomocou llvm/clang, verzia >= 10.0.0 alebo ešte lepšie vyššia (pozri časť Vývojové nástroje):

$ 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

Medzi zaujímavé funkcie: uvádzame cieľovú architektúru -target bpf a cesta k hlavičkám libbpf, ktorý sme nedávno nainštalovali. Tiež nezabudnite na -O2, bez tejto možnosti vás v budúcnosti môžu čakať prekvapenia. Pozrime sa na náš kód, podarilo sa nám napísať program, ktorý sme chceli?

$ 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

Áno, podarilo sa! Teraz máme binárny súbor s programom a chceme vytvoriť aplikáciu, ktorá ho načíta do jadra. Na tento účel knižnica libbpf nám ponúka dve možnosti – použiť API nižšej úrovne alebo API vyššej úrovne. Pôjdeme druhou cestou, keďže sa chceme naučiť písať, načítavať a spájať BPF programy s minimálnou námahou na ich následné štúdium.

Najprv musíme vygenerovať „kostru“ nášho programu z jeho binárneho súboru pomocou rovnakého nástroja bpftool — švajčiarsky nôž sveta BPF (čo možno brať doslovne, keďže Daniel Borkman, jeden z tvorcov a správcov BPF, je Švajčiar):

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

V súbore xdp-simple.skel.h obsahuje binárny kód nášho programu a funkcie pre správu - načítanie, pripájanie, mazanie nášho objektu. V našom jednoduchom prípade to vyzerá ako prehnané, ale funguje to aj v prípade, že objektový súbor obsahuje veľa BPF programov a máp a na načítanie tohto obrovského ELFu nám stačí vygenerovať kostru a zavolať jednu alebo dve funkcie z vlastnej aplikácie, ktorú píšu Poďme teraz ďalej.

Presne povedané, náš program nakladača je triviálny:

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

Tu struct xdp_simple_bpf definované v súbore xdp-simple.skel.h a popisuje náš objektový súbor:

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

Tu môžeme vidieť stopy nízkoúrovňového API: štruktúra struct bpf_program *simple и struct bpf_link *simple. Prvá štruktúra konkrétne popisuje náš program, napísaný v sekcii xdp/simplea druhý popisuje, ako sa program pripája k zdroju udalosti.

Funkcia xdp_simple_bpf__open_and_load, otvorí objekt ELF, analyzuje ho, vytvorí všetky štruktúry a podštruktúry (okrem programu ELF obsahuje aj ďalšie sekcie - dáta, dáta len na čítanie, ladiace informácie, licenciu atď.) a následne ho pomocou systému načíta do jadra hovor bpf, čo môžeme skontrolovať kompiláciou a spustením 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

Pozrime sa teraz na používanie nášho programu bpftool. Poďme nájsť jej ID:

# bpftool p | grep -A4 simple
463: xdp  name simple  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-01T01:59:49+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        btf_id 185
        pids xdp-simple(16498)

a výpis (používame skrátenú formu príkazu bpftool prog dump xlated):

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

Niečo nové! Program vytlačil časti nášho zdrojového súboru C. Urobila to knižnica libbpf, ktorý našiel sekciu ladenia v binárnom súbore, skompiloval ho do objektu BTF, načítal ho do jadra pomocou BPF_BTF_LOADa potom zadali výsledný deskriptor súboru pri načítaní programu pomocou príkazu BPG_PROG_LOAD.

Pomocníci jadra

Programy BPF môžu spúšťať „externé“ funkcie – pomocníkov jadra. Tieto pomocné funkcie umožňujú programom BPF pristupovať k štruktúram jadra, spravovať mapy a tiež komunikovať so „skutočným svetom“ – vytvárať udalosti výkonu, ovládať hardvér (napríklad presmerovanie paketov) atď.

Príklad: bpf_get_smp_processor_id

V rámci paradigmy „učenia sa príkladom“ uvažujme o jednej z pomocných funkcií, bpf_get_smp_processor_id(), určitý v súbore kernel/bpf/helpers.c. Vráti číslo procesora, na ktorom beží program BPF, ktorý ho volal. Nás však nezaujíma ani tak jeho sémantika, ako to, že jeho implementácia má jednu líniu:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Definície pomocných funkcií BPF sú podobné definíciám systémových volaní Linuxu. Tu je napríklad definovaná funkcia, ktorá nemá žiadne argumenty. (Funkcia, ktorá má povedzme tri argumenty, je definovaná pomocou makra BPF_CALL_3. Maximálny počet argumentov je päť.) Toto je však len prvá časť definície. Druhou časťou je definovanie typovej štruktúry struct bpf_func_proto, ktorý obsahuje popis pomocnej funkcie, ktorej overovateľ rozumie:

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

Registrácia pomocných funkcií

Aby programy BPF konkrétneho typu mohli využívať túto funkciu, musia ju zaregistrovať, napríklad pre typ BPF_PROG_TYPE_XDP funkcia je definovaná v jadre xdp_func_proto, ktorý z ID pomocnej funkcie určí, či XDP túto funkciu podporuje alebo nie. Naša funkcia je podporuje:

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

V súbore sú „definované“ nové typy programov BPF include/linux/bpf_types.h pomocou makra BPF_PROG_TYPE. Definované v úvodzovkách, pretože ide o logickú definíciu a v pojmoch jazyka C sa definícia celého súboru betónových štruktúr vyskytuje na iných miestach. Najmä v spise kernel/bpf/verifier.c všetky definície zo súboru bpf_types.h sa používajú na vytvorenie radu štruktúr bpf_verifier_ops[]:

static const struct bpf_verifier_ops *const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) 
    [_id] = & _name ## _verifier_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
};

To znamená, že pre každý typ programu BPF je definovaný ukazovateľ na dátovú štruktúru daného typu struct bpf_verifier_ops, ktorý je inicializovaný hodnotou _name ## _verifier_ops, t.j. xdp_verifier_ops pre xdp. Štruktúra xdp_verifier_ops určený v súbore net/core/filter.c takto:

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

Tu vidíme našu známu funkciu xdp_func_proto, ktorý spustí overovač vždy, keď narazí na výzvu nejaký druh funkcie vnútri programu BPF, pozri verifier.c.

Pozrime sa, ako hypotetický program BPF používa funkciu bpf_get_smp_processor_id. Aby sme to dosiahli, prepíšeme program z našej predchádzajúcej časti takto:

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

symbol bpf_get_smp_processor_id určený в <bpf/bpf_helper_defs.h> knižnica libbpf ako

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

teda bpf_get_smp_processor_id je ukazovateľ funkcie, ktorého hodnota je 8, kde 8 je hodnota BPF_FUNC_get_smp_processor_id typ enum bpf_fun_id, ktorý je pre nás definovaný v súbore vmlinux.h (súbor bpf_helper_defs.h v jadre je generovaný skriptom, takže „magické“ čísla sú v poriadku). Táto funkcia neberie žiadne argumenty a vracia hodnotu typu __u32. Keď to spustíme v našom programe, clang vygeneruje pokyn BPF_CALL "správny druh" Poďme skompilovať program a pozrieť sa na sekciu 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

V prvom riadku vidíme pokyny call, parameter IMM ktorá sa rovná 8 a SRC_REG - nula. Podľa dohody ABI používanej overovateľom ide o volanie pomocnej funkcie číslo osem. Po spustení je logika jednoduchá. Návratová hodnota z registra r0 skopírované do r1 a na riadkoch 2,3 sa prevedie na typ u32 — horných 32 bitov sa vymaže. Na riadkoch 4,5,6,7 vrátime 2 (XDP_PASS) alebo 1 (XDP_DROP) podľa toho, či pomocná funkcia z riadku 0 vrátila nulovú alebo nenulovú hodnotu.

Poďme sa otestovať: načítajte program a pozrite sa na výstup bpftool prog dump xlated:

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

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

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

Ok, overovateľ našiel správneho pomocníka jadra.

Príklad: odovzdanie argumentov a nakoniec spustenie programu!

Všetky pomocné funkcie na úrovni spustenia majú prototyp

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

Parametre pomocným funkciám sa odovzdávajú v registroch r1-r5a hodnota sa vráti do registra r0. Neexistujú žiadne funkcie, ktoré vyžadujú viac ako päť argumentov, a neočakáva sa, že ich podpora bude v budúcnosti pridaná.

Pozrime sa na nového pomocníka jadra a na to, ako BPF odovzdáva parametre. Poďme prepísať xdp-simple.bpf.c takto (zvyšné riadky sa nezmenili):

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

Náš program vypíše číslo CPU, na ktorom beží. Poďme to skompilovať a pozrieť sa na kód:

$ 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

Do riadkov 0-7 napíšeme reťazec running on CPU%un, a potom na linke 8 spustíme ten známy bpf_get_smp_processor_id. Na riadkoch 9-12 pripravíme pomocné argumenty bpf_printk - registre r1, r2, r3. Prečo sú traja a nie dvaja? Pretože bpf_printktoto je makro wrapper okolo skutočného pomocníka bpf_trace_printk, ktorý musí prejsť veľkosťou formátovacieho reťazca.

Pridajme teraz pár riadkov xdp-simple.caby sa náš program pripojil k rozhraniu lo a naozaj to začalo!

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

Tu používame funkciu bpf_set_link_xdp_fd, ktorý pripája programy BPF typu XDP k sieťovým rozhraniam. Napevno sme zakódovali číslo rozhrania lo, čo je vždy 1. Funkciu spustíme dvakrát, aby sme najprv odpojili starý program, ak bol pripojený. Všimnite si, že teraz nepotrebujeme žiadnu výzvu pause alebo nekonečná slučka: náš zavádzací program sa ukončí, ale program BPF nebude ukončený, pretože je pripojený k zdroju udalosti. Po úspešnom stiahnutí a pripojení sa program spustí pre každý prichádzajúci sieťový paket lo.

Stiahneme si program a pozrieme sa na rozhranie lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  name simple  tag 4fca62e77ccb43d6  gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

Program, ktorý sme si stiahli, má ID 669 a na rozhraní vidíme rovnaké ID lo. Pošleme pár balíkov na 127.0.0.1 (žiadosť + odpoveď):

$ ping -c1 localhost

a teraz sa pozrime na obsah virtuálneho súboru ladenia /sys/kernel/debug/tracing/trace_pipe, v ktorom bpf_printk píše svoje správy:

# 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

Boli spozorované dva balíky lo a spracované na CPU0 - náš prvý plnohodnotný nezmyselný BPF program fungoval!

Stojí za zmienku, že bpf_printk Nie nadarmo sa zapisuje do ladiaceho súboru: nie je to najúspešnejší pomocník na použitie vo výrobe, ale naším cieľom bolo ukázať niečo jednoduché.

Prístup k mapám z programov BPF

Príklad: pomocou mapy z programu BPF

V predchádzajúcich častiach sme sa naučili vytvárať a používať mapy z užívateľského priestoru a teraz sa pozrime na časť s jadrom. Začnime, ako obvykle, príkladom. Poďme prepísať náš program xdp-simple.bpf.c takto:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 8);
    __type(key, u32);
    __type(value, u64);
} woo SEC(".maps");

SEC("xdp/simple")
int simple(void *ctx)
{
    u32 key = bpf_get_smp_processor_id();
    u32 *val;

    val = bpf_map_lookup_elem(&woo, &key);
    if (!val)
        return XDP_ABORTED;

    *val += 1;

    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

Na začiatku programu sme pridali definíciu mapy woo: Toto je 8-prvkové pole, ktoré ukladá hodnoty ako u64 (v C by sme definovali také pole ako u64 woo[8]). V programe "xdp/simple" dostaneme aktuálne číslo procesora do premennej key a potom pomocou funkcie pomocníka bpf_map_lookup_element dostaneme ukazovateľ na príslušný záznam v poli, ktorý zväčšíme o jednu. Preložené do ruštiny: vypočítavame štatistiky o tom, ktorý procesor spracovával prichádzajúce pakety. Skúsme spustiť program:

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

Skontrolujme, či je napojená lo a poslať nejaké balíčky:

$ 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

Teraz sa pozrime na obsah poľa:

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

Takmer všetky procesy boli spracované na CPU7. Toto pre nás nie je dôležité, hlavné je, že program funguje a my rozumieme, ako pristupovať k mapám z programov BPF - pomocou хелперов bpf_mp_*.

Mystický index

Takže môžeme pristupovať k mape z programu BPF pomocou volaní ako

val = bpf_map_lookup_elem(&woo, &key);

kde pomocná funkcia vyzerá

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

ale míňame ukazovateľ &woo do nepomenovanej štruktúry struct { ... }...

Ak sa pozrieme na assembler programu, vidíme, že hodnota &woo nie je v skutočnosti definovaný (riadok 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
...

a je obsiahnutá v premiestneniach:

$ 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

Ak sa však pozrieme na už načítaný program, vidíme ukazovateľ na správnu mapu (riadok 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]
...

Môžeme teda dospieť k záveru, že v čase spustenia nášho programu nakladača bol odkaz na &woo bola nahradená niečím s knižnicou libbpf. Najprv sa pozrieme na výstup strace:

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=8, max_entries=8, map_name="woo", ...}, 120) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="simple", ...}, 120) = 5

To vidíme libbpf vytvoril mapu woo a potom si stiahol náš program simple. Pozrime sa bližšie na to, ako načítame program:

  • hovor xdp_simple_bpf__open_and_load zo súboru xdp-simple.skel.h
  • ktorá spôsobuje xdp_simple_bpf__load zo súboru xdp-simple.skel.h
  • ktorá spôsobuje bpf_object__load_skeleton zo súboru libbpf/src/libbpf.c
  • ktorá spôsobuje bpf_object__load_xattr z libbpf/src/libbpf.c

Posledná funkcia okrem iného zavolá bpf_object__create_maps, ktorý vytvára alebo otvára existujúce mapy a mení ich na deskriptory súborov. (Toto vidíme BPF_MAP_CREATE vo výstupe strace.) Ďalej sa volá funkcia bpf_object__relocate a práve ona nás zaujíma, keďže si pamätáme, čo sme videli woo v tabuľke premiestňovania. Pri jej skúmaní sa nakoniec ocitneme vo funkcii bpf_program__relocate, ktorý sa zaoberá presunmi máp:

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

Takže sa riadime našimi pokynmi

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

a nahraďte v ňom zdrojový register BPF_PSEUDO_MAP_FD, a prvý IMM do deskriptora súboru našej mapy a ak sa rovná napr. 0xdeadbeef, potom ako výsledok dostaneme pokyn

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

Takto sa mapové informácie prenesú do konkrétneho načítaného programu BPF. V tomto prípade je možné mapu vytvoriť pomocou BPF_MAP_CREATEa otvorí sa pomocou ID BPF_MAP_GET_FD_BY_ID.

Celkom pri použití libbpf algoritmus je nasledovný:

  • pri kompilácii sa v relokačnej tabuľke vytvoria záznamy pre odkazy na mapy
  • libbpf otvorí knihu objektov ELF, nájde všetky použité mapy a vytvorí pre ne deskriptory súborov
  • deskriptory súborov sú načítané do jadra ako súčasť inštrukcie LD64

Ako si viete predstaviť, je toho ešte viac a budeme sa musieť pozrieť do jadra. Našťastie máme tušenie – význam sme si zapísali BPF_PSEUDO_MAP_FD do zdrojového registra a môžeme ho pochovať, čo nás privedie k svätyni všetkých svätých - kernel/bpf/verifier.c, kde funkcia s príznačným názvom nahrádza deskriptor súboru adresou štruktúry typu 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;

(úplný kód nájdete по ссылке). Takže môžeme rozšíriť náš algoritmus:

  • pri načítavaní programu overovateľ skontroluje správne použitie mapy a zapíše adresu príslušnej štruktúry struct bpf_map

Pri sťahovaní binárneho ELF pomocou libbpf Deje sa toho oveľa viac, ale o tom budeme diskutovať v iných článkoch.

Načítavanie programov a máp bez libbpf

Ako sme sľúbili, tu je príklad pre čitateľov, ktorí chcú vedieť, ako vytvoriť a načítať program, ktorý používa mapy, bez pomoci libbpf. To môže byť užitočné, keď pracujete v prostredí, pre ktoré nemôžete vytvárať závislosti, ukladať každý bit alebo píšete program ako ply, ktorý generuje BPF binárny kód za behu.

Aby sme uľahčili dodržiavanie logiky, prepíšeme náš príklad na tieto účely xdp-simple. Kompletný a mierne rozšírený kód programu diskutovaný v tomto príklade nájdete v tomto podstata.

Logika našej aplikácie je nasledovná:

  • vytvorte typovú mapu BPF_MAP_TYPE_ARRAY pomocou príkazu BPF_MAP_CREATE,
  • vytvoriť program, ktorý používa túto mapu,
  • pripojte program k rozhraniu lo,

čo sa prekladá do ľudského ako

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

Tu map_create vytvorí mapu rovnakým spôsobom, ako sme to urobili v prvom príklade systémového volania bpf - „kernel, prosím, urobte mi novú mapu vo forme poľa 8 prvkov ako __u64 a vráťte mi deskriptor súboru":

static int map_create()
{
    union bpf_attr attr;

    memset(&attr, 0, sizeof(attr));
    attr.map_type = BPF_MAP_TYPE_ARRAY,
    attr.key_size = sizeof(__u32),
    attr.value_size = sizeof(__u64),
    attr.max_entries = 8,
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}

Program sa tiež ľahko načíta:

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

Zložitá časť prog_load je definícia nášho programu BPF ako súboru štruktúr struct bpf_insn insns[]. Ale keďže používame program, ktorý máme v C, môžeme trochu podvádzať:

$ 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

Celkovo potrebujeme napísať 14 inštrukcií vo forme štruktúr ako struct bpf_insn (rada: zoberte skládku zhora, znovu si prečítajte časť s pokynmi, otvorte linux/bpf.h и linux/bpf_common.h a pokúsiť sa určiť struct bpf_insn insns[] sám za seba):

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

Cvičenie pre tých, ktorí to sami nenapísali - nájdite map_fd.

V našom programe zostáva ešte jedna nezverejnená časť - xdp_attach. Bohužiaľ, programy ako XDP nie je možné pripojiť pomocou systémového volania bpf. Ľudia, ktorí vytvorili BPF a XDP, boli z online linuxovej komunity, čo znamená, že použili tú, ktorá je im najznámejšia (ale nie normálne people) rozhranie na interakciu s jadrom: netlink zásuvky, pozri tiež RFC3549. Najjednoduchší spôsob implementácie xdp_attach kopíruje kód z libbpf, a to zo spisu netlink.c, čo sme urobili, trochu sme to skrátili:

Vitajte vo svete netlink zásuviek

Otvorte typ zásuvky sieťového prepojenia 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;
}

Čítame z tejto zásuvky:

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

Nakoniec je tu naša funkcia, ktorá otvorí soket a odošle doň špeciálnu správu obsahujúcu deskriptor súboru:

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

Takže všetko je pripravené na testovanie:

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

Pozrime sa, či sa náš program pripojil k lo:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 160

Pošlime pingy a pozrime sa na mapu:

$ 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

Hurá, všetko funguje. Všimnite si, mimochodom, že naša mapa sa opäť zobrazuje vo forme bajtov. Je to spôsobené tým, že na rozdiel libbpf nenačítali sme informácie o type (BTF). Ale o tom si povieme viac nabudúce.

Vývojové nástroje

V tejto časti sa pozrieme na minimálnu sadu nástrojov pre vývojárov BPF.

Všeobecne povedané, na vývoj programov BPF nepotrebujete nič špeciálne – BPF beží na akomkoľvek slušnom distribučnom jadre a programy sú zostavené pomocou clang, ktoré je možné dodať z balenia. Avšak vzhľadom na skutočnosť, že BPF je vo vývoji, jadro a nástroje sa neustále menia, ak nechcete písať programy BPF pomocou staromódnych metód od roku 2019, budete musieť kompilovať

  • llvm/clang
  • pahole
  • jeho jadro
  • bpftool

(Pre informáciu, táto časť a všetky príklady v článku boli spustené na Debiane 10.)

llvm/clang

BPF je priateľský s LLVM a hoci programy pre BPF možno v poslednom čase skompilovať pomocou gcc, všetok súčasný vývoj sa vykonáva pre LLVM. Preto v prvom rade zostavíme aktuálnu verziu clang z 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
... много времени спустя
$

Teraz môžeme skontrolovať, či sa všetko spojilo správne:

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

(Návod na montáž clang mnou prevzaté z bpf_devel_QA.)

Nebudeme inštalovať programy, ktoré sme práve vytvorili, ale namiesto toho ich len pridáme PATH, napríklad:

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

(Toto je možné doplniť .bashrc alebo do samostatného súboru. Osobne pridávam takéto veci ~/bin/activate-llvm.sh a keď je to potrebné, urobím to . activate-llvm.sh.)

Pahole a BTF

Užitočnosť pahole používa sa pri zostavovaní jadra na vytváranie ladiacich informácií vo formáte BTF. O detailoch technológie BTF sa v tomto článku nebudeme rozpisovať, okrem toho, že je pohodlná a chceme ju využívať. Takže ak sa chystáte zostaviť svoje jadro, najprv zostavte pahole (bez pahole nebudete môcť zostaviť jadro s možnosťou 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

Jadrá na experimentovanie s BPF

Pri skúmaní možností BPF si chcem zostaviť vlastné jadro. Vo všeobecnosti to nie je potrebné, pretože budete môcť kompilovať a načítať programy BPF na distribučnom jadre, avšak vlastné jadro vám umožňuje využívať najnovšie funkcie BPF, ktoré sa vo vašej distribúcii objavia prinajlepšom o mesiace. , alebo ako v prípade niektorých nástrojov na ladenie nebudú v dohľadnej dobe vôbec pribalené. Vďaka svojmu vlastnému jadru je tiež dôležité experimentovať s kódom.

Na zostavenie jadra potrebujete po prvé jadro samotné a po druhé konfiguračný súbor jadra. Na experimentovanie s BPF môžeme použiť obvyklé vanilka jadro alebo jedno z vývojových jadier. Historicky sa vývoj BPF odohráva v rámci linuxovej sieťovej komunity, a preto všetky zmeny skôr či neskôr prejdú cez Davida Millera, správcu siete Linuxu. V závislosti od ich povahy - úpravy alebo nové funkcie - zmeny siete spadajú do jedného z dvoch jadier - net alebo net-next. Zmeny pre BPF sú medzi sebou rozdelené rovnakým spôsobom bpf и bpf-next, ktoré sú potom združené do net a net-next. Ďalšie podrobnosti nájdete v časti bpf_devel_QA и netdev-FAQ. Vyberte si teda jadro podľa svojho vkusu a potrieb stability systému, na ktorom testujete (*-next jadrá sú najnestabilnejšie z uvedených).

Je nad rámec tohto článku hovoriť o tom, ako spravovať konfiguračné súbory jadra - predpokladá sa, že buď už viete, ako to urobiť, alebo pripravený učiť sa sám za seba. Nasledujúce pokyny by však mali byť viac-menej dostatočné na to, aby vám poskytli funkčný systém s podporou BPF.

Stiahnite si jedno z vyššie uvedených jadier:

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

Vytvorte minimálnu fungujúcu konfiguráciu jadra:

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

Povoliť možnosti BPF v súbore .config podľa vlastného výberu (s najväčšou pravdepodobnosťou CONFIG_BPF bude už povolený, pretože ho systemd používa). Tu je zoznam možností z jadra použitého pre tento článok:

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

Potom môžeme jednoducho zostaviť a nainštalovať moduly a jadro (mimochodom, jadro môžete zostaviť pomocou novo zostaveného clangpridaním CC=clang):

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

a reštartujte s novým jadrom (na to používam kexec z balíka kexec-tools):

v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

bpftool

Najčastejšie používanou pomôckou v článku bude pomôcka bpftool, dodávaný ako súčasť linuxového jadra. Je napísaný a udržiavaný vývojármi BPF pre vývojárov BPF a dá sa použiť na správu všetkých typov objektov BPF – načítanie programov, vytváranie a úpravu máp, skúmanie života ekosystému BPF atď. Dokumentáciu vo forme zdrojových kódov pre manuálové stránky nájdete v jadre alebo už zostavené, sieť.

V čase tohto písania bpftool je pripravený iba pre RHEL, Fedora a Ubuntu (pozri napr. toto vlákno, ktorá rozpráva nedokončený príbeh balenia bpftool v Debiane). Ale ak ste už svoje jadro postavili, potom vytvorte bpftool jednoduché ako koláč:

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

$

(tu ${linux} - toto je adresár vášho jadra.) Po vykonaní týchto príkazov bpftool budú zhromaždené v adresári ${linux}/tools/bpf/bpftool a môže sa pridať do cesty (predovšetkým k používateľovi root) alebo jednoducho skopírujte do /usr/local/sbin.

Zbierať bpftool najlepšie je použiť to druhé clang, zložený podľa vyššie uvedeného popisu a skontrolujte, či je zložený správne – napríklad pomocou príkazu

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

ktorý ukáže, ktoré funkcie BPF sú vo vašom jadre povolené.

Mimochodom, predchádzajúci príkaz možno spustiť ako

# bpftool f p k

Robí sa to analogicky s nástrojmi z balíka iproute2, kde môžeme napr ip a s eth0 namiesto ip addr show dev eth0.

Záver

BPF vám umožňuje obuť blchu, aby ste efektívne zmerali a za behu zmenili funkčnosť jadra. Ukázalo sa, že systém je veľmi úspešný v najlepších tradíciách UNIX: jednoduchý mechanizmus, ktorý vám umožňuje (re)programovať jadro, umožnil experimentovať obrovskému počtu ľudí a organizácií. A hoci experimenty, ako aj samotný vývoj infraštruktúry BPF nie sú ani zďaleka ukončené, systém už má stabilné ABI, ktoré vám umožňuje vybudovať spoľahlivú a hlavne efektívnu obchodnú logiku.

Chcel by som poznamenať, že podľa môjho názoru sa technológia stala tak populárnou, pretože na jednej strane môže hrať (architektúru stroja sa dá pochopiť viac-menej za jeden večer), a na druhej strane riešiť problémy, ktoré sa nedali (krásne) vyriešiť pred jeho objavením. Tieto dve zložky spolu nútia ľudí experimentovať a snívať, čo vedie k vzniku stále viac inovatívnych riešení.

Tento článok, aj keď nie je príliš krátky, je len úvodom do sveta BPF a nepopisuje „pokročilé“ funkcie a dôležité časti architektúry. Plán do budúcna je asi takýto: v ďalšom článku bude prehľad typov programov BPF (v jadre 5.8 je podporovaných 30 typov programov), potom sa konečne pozrieme na to, ako písať skutočné BPF aplikácie pomocou programov na sledovanie jadra ako príklad, potom je čas na hlbší kurz architektúry BPF, po ktorom nasledujú príklady sieťových a bezpečnostných aplikácií BPF.

Predchádzajúce články z tejto série

  1. BPF pre najmenších, časť nula: klasický BPF

Odkazy

  1. Referenčná príručka BPF a XDP — dokumentácia o BPF od cilium, presnejšie od Daniela Borkmana, jedného z tvorcov a správcov BPF. Toto je jeden z prvých serióznejších popisov, ktorý sa od ostatných líši tým, že Daniel presne vie, o čom píše a nie sú tam žiadne chyby. Tento dokument popisuje najmä prácu s programami BPF typu XDP a TC pomocou známej pomôcky ip z balíka iproute2.

  2. Documentation/networking/filter.txt — pôvodný súbor s dokumentáciou pre klasický a potom rozšírený BPF. Dobré čítanie, ak sa chcete ponoriť do jazyka montáže a technických architektonických detailov.

  3. Blog o BPF z facebooku. Aktualizuje sa zriedka, ale výstižne, ako tam píšu Alexej Starovoitov (autor eBPF) a Andrii Nakryiko - (správca) libbpf).

  4. Tajomstvo bpftool. Zábavné twitterové vlákno od Quentina Monneta s príkladmi a tajomstvami používania bpftool.

  5. Ponorte sa do BPF: zoznam materiálu na čítanie. Obrovský (a stále udržiavaný) zoznam odkazov na dokumentáciu BPF od Quentina Monneta.

Zdroj: hab.com

Pridať komentár