BPF a kicsiknek, első rész: kiterjesztett BPF

Kezdetben volt egy technológia, és ezt BPF-nek hívták. Ránéztünk előző, a sorozat ószövetségi cikke. 2013-ban Alekszej Starovoitov és Daniel Borkman erőfeszítései révén ennek továbbfejlesztett, modern 64 bites gépekre optimalizált változatát fejlesztették ki, amely bekerült a Linux kernelbe. Ezt az új technológiát röviden Internal BPF-nek, majd Extended BPF-nek nevezték el, és mostanra, több év után, mindenki egyszerűen BPF-nek hívja.

Nagyjából a BPF lehetővé teszi tetszőleges, felhasználó által biztosított kód futtatását a Linux kerneltérben, és az új architektúra olyan sikeresnek bizonyult, hogy további tucatnyi cikkre lesz szükségünk az összes alkalmazás leírásához. (Az egyetlen dolog, amit a fejlesztők nem csináltak jól, ahogy az alábbi teljesítménykódban is látható, az egy tisztességes logó létrehozása.)

Ez a cikk ismerteti a BPF virtuális gép felépítését, a BPF-fel való munkavégzéshez szükséges kernel felületeket, a fejlesztőeszközöket, valamint egy rövid, nagyon rövid áttekintést ad a meglévő képességekről, pl. mindent, amire a jövőben szükségünk lesz a BPF gyakorlati alkalmazásainak mélyebb tanulmányozásához.
BPF a kicsiknek, első rész: kiterjesztett BPF

A cikk összefoglalója

Bevezetés a BPF architektúrába. Először is madártávlatból tekintjük meg a BPF architektúrát, és felvázoljuk a fő összetevőket.

A BPF virtuális gép regiszterei és parancsrendszere. Már az architektúra egészének elképzelésével leírjuk a BPF virtuális gép szerkezetét.

BPF objektumok életciklusa, bpffs fájlrendszer. Ebben a részben közelebbről megvizsgáljuk a BPF objektumok - programok és térképek - életciklusát.

Objektumok kezelése bpf rendszerhívással. A rendszer megértése után végre megvizsgáljuk, hogyan lehet objektumokat létrehozni és kezelni a felhasználói térből egy speciális rendszerhívás segítségével. bpf(2).

Пишем программы BPF с помощью libbpf. Természetesen rendszerhívással is írhatunk programokat. De nehéz. A valósághűbb forgatókönyv érdekében a nukleáris programozók könyvtárat fejlesztettek ki libbpf. Létrehozunk egy alapvető BPF alkalmazásvázat, amelyet a következő példákban fogunk használni.

Kernel segítők. Itt megtudjuk, hogyan férhetnek hozzá a BPF programok a kernel segítő funkcióihoz – egy olyan eszközhöz, amely a térképekkel együtt alapvetően bővíti az új BPF képességeit a klasszikushoz képest.

Hozzáférés a térképekhez a BPF programokból. Ekkor már eleget fogunk tudni ahhoz, hogy pontosan megértsük, hogyan hozhatunk létre térképeket használó programokat. És vessünk egy gyors pillantást a nagyszerű és hatalmas hitelesítőbe.

Fejlesztési eszközök. Súgó rész a szükséges segédprogramok és kernel összeállításáról a kísérletekhez.

Következtetés. A cikk végén azok, akik idáig elolvasták, a következő cikkekben találnak motiváló szavakat és egy rövid leírást a történésekről. Számos linket is felsorolunk az önálló tanuláshoz azok számára, akiknek nincs kedvük vagy nem tudnak kivárni a folytatást.

Bevezetés a BPF architektúrába

Mielőtt elkezdenénk foglalkozni a BPF architektúrával, még egyszer utalunk (ó) erre klasszikus BPF, amelyet a RISC gépek megjelenésére válaszul fejlesztettek ki, és megoldotta a hatékony csomagszűrés problémáját. Az architektúra olyan sikeresnek bizonyult, hogy a Berkeley UNIX lendületes kilencvenes éveiben született, a legtöbb létező operációs rendszerre portolták, túlélte az őrült húszas éveket, és még mindig talál új alkalmazásokat.

Az új BPF-et a 64 bites gépek, a felhőszolgáltatások és az SDN létrehozásához szükséges eszközök megnövekedett igénye miatt fejlesztették ki.Sgyakoridkifinomult nhálózatépítés). A kernelhálózati mérnökök által a klasszikus BPF továbbfejlesztett helyettesítőjeként kifejlesztett új BPF szó szerint hat hónappal később talált alkalmazásokat a Linux rendszerek nyomon követésének nehéz feladatában, és most, hat évvel megjelenése után szükségünk lesz egy egész következő cikkre, csak hogy sorolja fel a különböző típusú programokat.

Vicces képek

Lényegében a BPF egy homokozó virtuális gép, amely lehetővé teszi „tetszőleges” kód futtatását a kerneltérben a biztonság veszélyeztetése nélkül. A BPF programokat a felhasználói térben hozzák létre, betöltik a kernelbe, és kapcsolódnak valamilyen eseményforráshoz. Esemény lehet például egy csomag hálózati interfészre szállítása, valamilyen kernelfunkció elindítása stb. Csomag esetén a BPF program hozzáfér a csomag adataihoz és metaadataihoz (olvasáshoz, esetleg íráshoz, programtípustól függően), kernelfüggvény futtatása esetén a csomag argumentumai a függvény, beleértve a kernelmemóriára mutató mutatókat stb.

Nézzük meg közelebbről ezt a folyamatot. Először is beszéljünk az első különbségről a klasszikus BPF-hez képest, amelyhez a programokat assemblerben írták. Az új verzióban az architektúra kibővült, hogy a programok magas szintű nyelveken írhatók legyenek, elsősorban természetesen C-ben. Erre fejlesztették ki az llvm-hez egy backendet, amely lehetővé teszi a BPF architektúra bájtkódjának generálását.

BPF a kicsiknek, első rész: kiterjesztett BPF

A BPF architektúrát részben úgy tervezték, hogy hatékonyan működjön modern gépeken. Ahhoz, hogy ez a gyakorlatban is működjön, a kernelbe betöltött BPF bájtkódot natív kódba fordítják le egy JIT fordítónak nevezett komponens segítségével (Just In Time). Következő, ha emlékszel, a klasszikus BPF-ben a program betöltődött a kernelbe, és atomosan csatolva lett az eseményforráshoz - egyetlen rendszerhívás keretében. Az új architektúrában ez két lépésben történik - először a kód betöltődik a kernelbe egy rendszerhívás segítségével bpf(2)majd később más, a program típusától függően változó mechanizmusokon keresztül a program csatlakozik az eseményforráshoz.

Itt felmerülhet az olvasóban a kérdés: lehetséges volt? Hogyan garantálható egy ilyen kód végrehajtásának biztonsága? A végrehajtás biztonságát a BPF programok betöltési szakasza, a verifier garantálja számunkra (angolul ezt a szakaszt verfiernek hívják, és továbbra is az angol szót fogom használni):

BPF a kicsiknek, első rész: kiterjesztett BPF

A Verifier egy statikus elemző, amely biztosítja, hogy egy program ne zavarja meg a kernel normál működését. Ez egyébként nem jelenti azt, hogy a program nem zavarhatja meg a rendszer működését - a BPF programok típustól függően olvashatják és átírhatják a kernel memória szakaszait, visszaadhatják a függvények értékeit, vághatják, hozzáfűzhetik, átírhatják és még hálózati csomagokat is továbbíthat. A Verifier garantálja, hogy egy BPF program futtatása nem fogja összeomolni a rendszermagot, és hogy a szabályok szerint írási hozzáféréssel rendelkező program, például egy kimenő csomag adatai, nem tudja felülírni a csomagon kívüli kernelmemóriát. Az ellenőrzőt egy kicsit részletesebben megvizsgáljuk a megfelelő részben, miután megismerkedtünk a BPF összes többi összetevőjével.

Szóval mit tanultunk eddig? A felhasználó C nyelven ír egy programot, amit rendszerhívással betölt a kernelbe bpf(2), ahol egy hitelesítő ellenőrzi, és natív bájtkódra fordítja. Ezután ugyanaz vagy egy másik felhasználó csatlakoztatja a programot az eseményforráshoz, és elkezdődik a végrehajtás. A rendszerindítás és a kapcsolat szétválasztása több okból is szükséges. Először is, az ellenőrző futtatása viszonylag drága, és ugyanazt a programot többször letöltve számítógépes időt veszítünk. Másodszor, az, hogy egy program pontosan hogyan csatlakozik, a típusától függ, és előfordulhat, hogy egy egy évvel ezelőtt kifejlesztett „univerzális” interfész nem alkalmas új típusú programok számára. (Bár most, hogy az architektúra egyre kiforrottabb, van egy ötlet ennek a felületnek a szinten történő egységesítésére libbpf.)

A figyelmes olvasó észreveheti, hogy még nem végeztünk a képekkel. Valójában a fentiek mindegyike nem magyarázza meg, hogy a BPF miért változtatja meg alapvetően a képet a klasszikus BPF-hez képest. Két újítás, amely jelentősen kibővíti az alkalmazási területet, az osztott memória és a kernel segédfunkciók használatának lehetősége. A BPF-ben az osztott memóriát úgynevezett térképek – meghatározott API-val rendelkező megosztott adatstruktúrák – segítségével valósítják meg. Valószínűleg azért kapták ezt a nevet, mert az elsőként megjelenő térképtípus egy hash-tábla volt. Aztán megjelentek a tömbök, a helyi (CPU-nkénti) hash-táblák és helyi tömbök, keresési fák, térképek, amelyek mutatják a BPF-programokat és még sok más. Számunkra most az az érdekes, hogy a BPF-programok képesek arra, hogy a hívások között fennmaradjanak az állapotok, és megosszák azt más programokkal és a felhasználói területtel.

A Maps a felhasználói folyamatokból rendszerhívással érhető el bpf(2), valamint a kernelben futó BPF-programokból helper függvények segítségével. Sőt, a segítők nem csak a térképekkel való munkához léteznek, hanem a kernel egyéb képességeinek eléréséhez is. Például a BPF programok segítő függvényeket használhatnak csomagok továbbítására más interfészekre, perf események generálására, kernelstruktúrák elérésére stb.

BPF a kicsiknek, első rész: kiterjesztett BPF

Összefoglalva, a BPF lehetőséget biztosít tetszőleges, azaz ellenőrző által tesztelt felhasználói kód betöltésére a kerneltérbe. Ez a kód mentheti az állapotot a hívások között, és adatokat cserélhet a felhasználói területtel, valamint hozzáféréssel rendelkezik az ilyen típusú programok által engedélyezett kernel-alrendszerekhez.

Ez már hasonló a kernel modulok által biztosított képességekhez, amelyekhez képest a BPF-nek van néhány előnye (természetesen csak hasonló alkalmazásokat lehet összehasonlítani, pl. a rendszerkövetést - BPF-fel nem lehet tetszőleges illesztőprogramot írni). Megjegyzendő az alacsonyabb belépési küszöb (egyes BPF-et használó segédprogramok nem követelik meg a felhasználótól kernelprogramozási ismereteket, vagy általában programozási ismereteket), futásidejű biztonság (emelje fel a kezét a megjegyzésekben, aki írás közben nem törte meg a rendszert vagy modulok tesztelése), atomitás - a modulok újratöltésekor leállás van, és a BPF alrendszer gondoskodik arról, hogy egyetlen esemény se maradjon el (az igazság kedvéért ez nem igaz minden típusú BPF programra).

Az ilyen képességek jelenléte a BPF-et univerzális eszközzé teszi a kernel bővítésére, ami a gyakorlatban is beigazolódik: egyre több új típusú program kerül a BPF-be, egyre több nagyvállalat használja a BPF-et 24 × 7 harci szervereken, egyre több a startupok olyan megoldásokra építik fel üzletüket, amelyekre a BPF-re épülnek. A BPF-et mindenhol használják: a DDoS támadások elleni védelemben, SDN létrehozásában (például hálózatok megvalósítása kubernetes számára), fő rendszerkövető eszközként és statisztikai adatgyűjtőként, behatolásjelző rendszerekben és sandbox rendszerekben stb.

Itt fejezzük be a cikk áttekintő részét, és nézzük meg részletesebben a virtuális gépet és a BPF ökoszisztémát.

Kitérő: közművek

A következő szakaszokban található példák futtatásához szükség lehet néhány segédprogramra, legalább llvm/clang bpf támogatással és bpftool. A szakaszban Fejlesztési eszközök Elolvashatja a segédprogramok összeállítására vonatkozó utasításokat, valamint a kernelt. Ez a rész alább került elhelyezésre, hogy ne zavarja előadásunk harmóniáját.

BPF virtuális gép nyilvántartások és utasítási rendszer

A BPF architektúráját és parancsrendszerét annak figyelembevételével fejlesztették ki, hogy a programokat C nyelven írják, és a kernelbe való betöltés után natív kódra fordítják. Ezért a regiszterek számát és a parancskészletet úgy választottuk meg, hogy a matematikai értelemben vett metszéspontja legyen a modern gépek képességeinek. Emellett különféle megszorításokat is szabtak a programokra, például egészen a közelmúltig nem lehetett ciklusokat és szubrutinokat írni, az utasítások számát pedig 4096-ra korlátozták (ma már akár millió utasítást is betölthetnek a kiemelt programok).

A BPF tizenegy, felhasználó által elérhető 64 bites regiszterrel rendelkezik r0-r10 és egy programszámláló. Regisztráció r10 keretmutatót tartalmaz, és csak olvasható. A programok futás közben 512 bájtos veremhez és korlátlan mennyiségű megosztott memóriához férhetnek hozzá térképek formájában.

A BPF-programok meghatározott programtípusú kernelsegéd-készletet, illetve újabban szokványos függvényeket futtathatnak. Minden meghívott függvény legfeljebb öt argumentumot vehet fel, amelyeket regiszterekben adnak át r1-r5, és a visszatérési érték átadásra kerül r0. A funkcióból való visszatérés után garantált a regiszterek tartalma r6-r9 Nem fog változni.

A hatékony programfordítás érdekében regiszterek r0-r11 Az összes támogatott architektúra egyedileg valós regiszterekre van leképezve, figyelembe véve a jelenlegi architektúra ABI-szolgáltatásait. Például azért x86_64 regisztereket r1-r5A funkcióparaméterek átadására használt ikonok jelennek meg rdi, rsi, rdx, rcx, r8, amelyek a paraméterek átadására szolgálnak a funkcióknak x86_64. Például a bal oldali kód a jobb oldali kódra fordítódik, így:

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

Regisztráció r0 szintén a programvégrehajtás eredményének visszaadására szolgál, és a regiszterben r1 a program egy mutatót ad át a kontextusra - a program típusától függően ez lehet például egy szerkezet struct xdp_md (XDP-hez) vagy struktúra struct __sk_buff (különböző hálózati programok esetén) vagy szerkezete struct pt_regs (különböző típusú nyomkövető programokhoz) stb.

Tehát volt egy sor regiszterünk, kernel segítőnk, veremünk, kontextusmutatónk és megosztott memóriánk térképek formájában. Nem mintha mindez feltétlenül szükséges lenne az utazás során, de...

Folytassuk a leírást, és beszéljünk az ezekkel az objektumokkal való munkavégzés parancsrendszeréről. Minden (Szinte minden) A BPF utasítások fix 64 bites méretűek. Ha megnéz egy utasítást egy 64 bites Big Endian gépen, látni fogja

BPF a kicsiknek, első rész: kiterjesztett BPF

Itt Code - ez az utasítás kódolása, Dst/Src ezek a vevő és a forrás kódolásai, Off - 16 bites előjeles behúzás, és Imm egy 32 bites előjeles egész szám, amelyet néhány utasításban használnak (hasonlóan a K cBPF konstanshoz). Kódolás Code két típus egyike van:

BPF a kicsiknek, első rész: kiterjesztett BPF

A 0, 1, 2, 3 utasításosztályok parancsokat határoznak meg a memóriával való munkához. Ők hívják, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, ill. 4., 7. osztály (BPF_ALU, BPF_ALU64) ALU utasítások halmazát alkotják. 5., 6. osztály (BPF_JMP, BPF_JMP32) ugrási utasításokat tartalmaz.

A BPF utasításrendszer tanulmányozásának további terve a következő: ahelyett, hogy aprólékosan felsorolnánk az összes utasítást és paramétereiket, megnézünk néhány példát ebben a részben, és ezekből kiderül, hogyan működnek az utasítások, és hogyan manuálisan szétszedni a BPF bináris fájljait. A cikk későbbi anyagának konszolidálása érdekében egyéni utasításokkal is találkozunk a Verifier-ről, a JIT-fordítóról, a klasszikus BPF fordításáról, valamint a térképek tanulmányozásáról, függvények hívásáról stb.

Amikor egyéni utasításokról beszélünk, akkor az alapvető fájlokra hivatkozunk bpf.h и bpf_common.h, amelyek a BPF utasítások numerikus kódjait határozzák meg. Az építészet önálló tanulmányozásakor és/vagy bináris fájlok elemzésekor a szemantikát a következő forrásokban találhatja meg, összetettség szerint rendezve: Nem hivatalos eBPF specifikáció, BPF és XDP használati útmutató, utasításkészlet, Dokumentáció/hálózat/filter.txt és természetesen a Linux forráskódban - ellenőrző, JIT, BPF interpreter.

Példa: a BPF szétszerelése a fejében

Nézzünk egy példát, amelyben egy programot fordítunk readelf-example.c és nézd meg a kapott binárist. Eláruljuk az eredeti tartalmat readelf-example.c alább, miután visszaállítottuk a logikáját bináris kódokból:

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

Az első oszlop a kimenetben readelf egy behúzás, így programunk négy parancsból áll:

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

A parancskódok egyenlőek b7, 15, b7 и 95. Emlékezzünk vissza, hogy a legkisebb jelentőségű három bit az utasításosztály. Esetünkben az összes utasítás negyedik bitje üres, így az utasítás osztályok rendre 7, 5, 7, 5. A 7. BPF_ALU64, és az 5 BPF_JMP. Mindkét osztálynál azonos az utasítás formátuma (lásd fent), és így is átírhatjuk a programunkat (egyúttal a fennmaradó oszlopokat is emberi alakra írjuk át):

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

Működés b класса ALU64 - Van BPF_MOV. Értéket rendel a célregiszterhez. Ha a bit be van állítva s (forrás), akkor az értéket a forrás regiszterből veszi, és ha, mint esetünkben, nincs beállítva, akkor az értéket a mezőből veszi Imm. Tehát az első és a harmadik utasításban végrehajtjuk a műveletet r0 = Imm. Továbbá, a JMP 1. osztályú művelet BPF_JEQ (ugrás, ha egyenlő). A mi esetünkben, mivel a bit S nulla, összehasonlítja a forrásregiszter értékét a mezővel Imm. Ha az értékek egybeesnek, akkor az átmenet megtörténik PC + OffAhol PCszokás szerint a következő utasítás címét tartalmazza. Végül a JMP Class 9 Operation az BPF_EXIT. Ez az utasítás leállítja a programot, és visszatér a kernelhez r0. Adjunk hozzá egy új oszlopot a táblázatunkhoz:

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

Ezt átírhatjuk kényelmesebb formában:

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

Ha emlékszünk mi van a nyilvántartásban r1 a program egy mutatót ad át a kontextusra a kernelből és a regiszterből r0 az érték visszakerül a kernelbe, akkor láthatjuk, hogy ha a kontextusra mutató mutató nulla, akkor 1-et adunk vissza, egyébként pedig - 2-t. Ellenőrizzük, hogy igazunk van-e a forrásból:

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

Igen, ez egy értelmetlen program, de mindössze négy egyszerű utasításból áll.

Kivétel példa: 16 bájtos utasítás

Korábban említettük, hogy egyes utasítások több mint 64 bitet foglalnak el. Ez vonatkozik például az utasításokra lddw (Kód = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — dupla szó betöltése a mezőkből a regiszterbe Imm. Az a tény Imm mérete 32, a dupla szó pedig 64 bites, így egy 64 bites azonnali érték regiszterbe való betöltése egy 64 bites utasításban nem fog működni. Ehhez két szomszédos utasítást használnak a 64 bites érték második részének a mezőben való tárolására Imm... Példa:

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

Egy bináris programban csak két utasítás van:

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

Utasításokkal újra találkozunk lddw, amikor az áthelyezésekről és a térképekkel való munkáról beszélünk.

Példa: a BPF szétszerelése szabványos szerszámokkal

Tehát megtanultuk olvasni a BPF bináris kódokat, és készek vagyunk minden utasítás elemzésére, ha szükséges. Érdemes azonban elmondani, hogy a gyakorlatban kényelmesebb és gyorsabb a programok szétszerelése szabványos eszközökkel, például:

$ llvm-objdump -d x64.o

Disassembly of section .text:

0000000000000000 <foo>:
 0: 18 00 00 00 dd cc bb aa 00 00 00 00 44 33 22 11 r0 = 1234605617868164317 ll
 2: 95 00 00 00 00 00 00 00 exit

BPF objektumok életciklusa, bpffs fájlrendszer

(Az ebben az alfejezetben leírt részleteket először innen tanultam meg böjtölés Alekszej Starovoitov be BPF Blog.)

A BPF objektumok - programok és térképek - a felhasználói térből, parancsok segítségével jönnek létre BPF_PROG_LOAD и BPF_MAP_CREATE rendszerhívás bpf(2), arról a következő részben fogunk beszélni, hogy ez pontosan hogyan történik. Ez létrehozza a kernel adatstruktúrákat és mindegyikhez refcount (referenciaszám) értéke egy, és az objektumra mutató fájlleíró visszakerül a felhasználónak. A fogantyú zárása után refcount az objektumot eggyel csökkentjük, és amikor eléri a nullát, az objektum megsemmisül.

Ha a program térképeket használ, akkor refcount ezeket a térképeket a program betöltése után eggyel növeljük, azaz. fájlleíróik bezárhatók a felhasználói folyamatból és továbbra is refcount nem lesz nulla:

BPF a kicsiknek, első rész: kiterjesztett BPF

Egy program sikeres betöltése után általában valamilyen eseménygenerátorhoz csatoljuk. Például ráhelyezhetjük egy hálózati interfészre, hogy feldolgozzuk a bejövő csomagokat, vagy csatlakoztassuk néhányhoz tracepoint a magban. Ekkor a referenciaszámláló is eggyel nő, és a betöltő programban bezárhatjuk a fájlleírót.

Mi történik, ha most leállítjuk a rendszerbetöltőt? Ez az eseménygenerátor (hook) típusától függ. A betöltő befejezése után minden hálózati hook létezni fog, ezek az úgynevezett globális hook. És például a nyomkövetési programok azután kerülnek kiadásra, hogy az őket létrehozó folyamat leáll (ezért helyinek nevezik őket, „helyiről a folyamatra”). Technikailag a helyi hookok mindig rendelkeznek egy megfelelő fájlleíróval a felhasználói térben, ezért bezárulnak a folyamat lezárásakor, de a globális hookok nem. A következő ábrán piros keresztekkel próbálom bemutatni, hogy a betöltő program leállása hogyan befolyásolja az objektumok élettartamát lokális és globális hook esetén.

BPF a kicsiknek, első rész: kiterjesztett BPF

Miért van különbség a helyi és a globális horgok között? Bizonyos típusú hálózati programok futtatásának van értelme felhasználói terület nélkül is, például képzeljük el a DDoS védelmet - a rendszerbetöltő megírja a szabályokat, és csatlakoztatja a BPF programot a hálózati interfészhez, ami után a rendszerbetöltő mehet és megölheti magát. Másrészt képzeljünk el egy hibakereső nyomkövető programot, amit tíz perc alatt térdre írtunk – ha elkészül, azt szeretné, ha nem maradna szemét a rendszerben, és ezt a helyi horgok biztosítják.

Másrészt képzelje el, hogy szeretne csatlakozni egy nyomkövetési ponthoz a kernelben, és sok éven keresztül gyűjteni statisztikákat. Ebben az esetben érdemes kitölteni a felhasználói részt, és időnként visszatérni a statisztikákhoz. A bpf fájlrendszer biztosítja ezt a lehetőséget. Ez egy csak a memóriában lévő pszeudofájlrendszer, amely lehetővé teszi olyan fájlok létrehozását, amelyek BPF objektumokra hivatkoznak, és ezáltal növelik refcount tárgyakat. Ezt követően a betöltő kiléphet, és az általa létrehozott objektumok életben maradnak.

BPF a kicsiknek, első rész: kiterjesztett BPF

A BPF-objektumokra hivatkozó fájlok bpff-ben történő létrehozását "rögzítésnek" nevezik (mint a következő kifejezésben: "a folyamat rögzíthet BPF-programot vagy leképezést"). A BPF-objektumok fájlobjektumainak létrehozása nem csak a lokális objektumok élettartamának meghosszabbítása, hanem a globális objektumok használhatósága szempontjából is értelmes – visszatérve a globális DDoS védelmi programmal kapcsolatos példához, szeretnénk, ha eljöhetnénk megnézni a statisztikákat. időről időre.

A BPF fájlrendszer általában be van építve /sys/fs/bpf, de helyben is felszerelhető, például így:

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

A fájlrendszernevek a paranccsal jönnek létre BPF_OBJ_PIN BPF rendszerhívás. Szemléltetésképpen vegyünk egy programot, fordítsuk le, töltsük fel és rögzítsük bpffs. Programunk nem csinál semmi hasznosat, csak a kódot mutatjuk be, hogy reprodukálhassa a példát:

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

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

Fordítsuk le ezt a programot, és készítsük el a fájlrendszer helyi másolatát bpffs:

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

Most töltsük le programunkat a segédprogram segítségével bpftool és nézd meg a kísérő rendszerhívásokat bpf(2) (néhány irreleváns sor eltávolítva a strace kimenetből):

$ 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

Itt töltöttük be a programot a segítségével BPF_PROG_LOAD, kapott egy fájlleírót a kerneltől 3 és a parancs használatával BPF_OBJ_PIN rögzítette ezt a fájlleírót fájlként "bpf-mountpoint/test". Ezt követően a rendszerbetöltő program bpftool befejezte a munkát, de a programunk a kernelben maradt, bár nem csatoltuk semmilyen hálózati interfészhez:

$ 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

A fájlobjektumot a szokásos módon törölhetjük unlink(2) és ezt követően a megfelelő program törlődik:

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

Objektumok törlése

Az objektumok törlésével kapcsolatban tisztázni kell, hogy miután leválasztottuk a programot a hook-ról (eseménygenerátor), egyetlen új esemény sem indítja el az indítást, azonban a program minden aktuális példánya a normál sorrendben elkészül. .

A BPF programok bizonyos típusai lehetővé teszik a program menet közbeni cseréjét, pl. szekvencia atomitást biztosítanak replace = detach old program, attach new program. Ebben az esetben a program régi verziójának minden aktív példánya befejezi a munkáját, és az új programból új eseménykezelők jönnek létre, és az „atomicity” itt azt jelenti, hogy egyetlen esemény sem marad el.

Programok csatolása rendezvényforrásokhoz

Ebben a cikkben nem írjuk le külön a programok eseményforrásokhoz való kapcsolását, mivel ezt célszerű egy adott programtípussal összefüggésben tanulmányozni. Cm. példa alább, amelyben bemutatjuk, hogyan csatlakoznak az olyan programok, mint az XDP.

Objektumok manipulálása a bpf rendszerhívás használatával

BPF programok

Az összes BPF objektumot a felhasználói területről rendszerhívással hozza létre és kezeli bpf, amely a következő prototípussal rendelkezik:

#include <linux/bpf.h>

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

Itt a csapat cmd típus értékei közé tartozik enum bpf_cmd, attr — egy mutató egy adott program paramétereire és size — tárgyméret a mutató szerint, azaz. általában ezt sizeof(*attr). Az 5.8-as kernelben a rendszerhívás bpf 34 különböző parancsot támogat, és определение union bpf_attr 200 sort foglal el. Ettől azonban nem kell megijednünk, hiszen több cikk során ismerkedünk meg a parancsokkal és paraméterekkel.

Kezdjük a csapattal BPF_PROG_LOAD, amely BPF programokat hoz létre - vesz egy BPF utasításkészletet és betölti a kernelbe. A betöltés pillanatában elindul a hitelesítő, majd a JIT fordító és sikeres végrehajtás után a programfájl leíró visszakerül a felhasználóhoz. Az előző részben láthattuk, mi történik vele ezután a BPF objektumok életciklusáról.

Most írunk egy egyedi programot, amely egy egyszerű BPF programot tölt be, de először el kell döntenünk, hogy milyen programot szeretnénk betölteni - ki kell választanunk típus és ennek a típusnak a keretein belül írjunk egy programot, ami átmegy az ellenőrző teszten. Azonban, hogy ne bonyolítsuk a folyamatot, itt van egy kész megoldás: veszünk egy olyan programot, mint pl. BPF_PROG_TYPE_XDP, amely visszaadja az értéket XDP_PASS (az összes csomag kihagyása). A BPF assemblerben nagyon egyszerűnek tűnik:

r0 = 2
exit

Miután eldöntöttük hogy feltöltjük, elmondhatjuk, hogyan tesszük:

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

A program érdekes eseményei egy tömb meghatározásával kezdődnek insns - BPF programunk gépi kódban. Ebben az esetben a BPF program minden egyes utasítása be van csomagolva a struktúrába bpf_insn. Első elem insns utasításoknak megfelel r0 = 2, a második - exit.

Visszavonulás. A kernel kényelmesebb makrókat határoz meg a gépi kódok írásához és a kernel fejlécfájl használatához tools/include/linux/filter.h írhatnánk

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

De mivel a BPF-programokat natív kódban írni csak a kernelben lévő tesztek és a BPF-ről szóló cikkek írásához szükséges, ezeknek a makróknak a hiánya nem igazán bonyolítja a fejlesztő életét.

A BPF program meghatározása után áttérünk a kernelbe való betöltésére. Minimalista paraméterkészletünk attr tartalmazza a program típusát, az utasítások készletét és számát, a szükséges licencet és nevet "woo", amellyel a letöltés után megtaláljuk programunkat a rendszeren. A program, ahogy ígértük, rendszerhívással töltődik be a rendszerbe bpf.

A program végén egy végtelen ciklusba kerülünk, amely a hasznos terhet szimulálja. Enélkül a kernel megöli a programot, amikor a rendszerhívás által visszaadott fájlleíró bezárul bpf, és nem fogjuk látni a rendszerben.

Nos, készen állunk a tesztelésre. Szereljük össze és futtassuk le a programot straceannak ellenőrzésére, hogy minden megfelelően működik-e:

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

Minden rendben, bpf(2) visszaadta nekünk a 3-as fogantyút, és végtelen körbe mentünk vele pause(). Próbáljuk meg megtalálni a programunkat a rendszerben. Ehhez átmegyünk egy másik terminálra, és használjuk a segédprogramot 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)

Látjuk, hogy van egy betöltött program a rendszerben woo amelynek globális azonosítója 390, és jelenleg folyamatban van simple-prog van egy nyitott fájlleíró, amely a programra mutat (és ha simple-prog akkor befejezi a munkát woo el fog tűnni). Ahogy az várható volt, a program woo 16 bájt - két utasítás - bináris kódot vesz igénybe a BPF architektúrában, de natív formában (x86_64) már 40 bájt. Nézzük meg programunkat eredeti formájában:

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

semmi meglepetés. Most nézzük meg a JIT fordító által generált kódot:

# 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

számára nem túl hatékony exit(2), de az igazat megvallva, a programunk túl egyszerű, és a nem triviális programokhoz természetesen szükség van a JIT fordító által hozzáadott prológra és epilógusra.

Térképek

A BPF programok olyan strukturált memóriaterületeket használhatnak, amelyek mind más BPF-programok, mind a felhasználói térben lévő programok számára elérhetők. Ezeket az objektumokat térképeknek nevezzük, és ebben a részben bemutatjuk, hogyan lehet őket rendszerhívással kezelni bpf.

Tegyük fel rögtön, hogy a térképek képességei nem korlátozódnak csak a megosztott memóriához való hozzáférésre. Vannak speciális célú térképek, amelyek tartalmaznak például mutatókat BPF programokra vagy mutatókat a hálózati interfészekre, térképeket a tökéletes eseményekkel való munkavégzéshez stb. Itt nem beszélünk róluk, nehogy összezavarjuk az olvasót. Ezen kívül figyelmen kívül hagyjuk a szinkronizálási problémákat, mivel ez nem fontos a példáink szempontjából. Az elérhető térképtípusok teljes listája itt található <linux/bpf.h>, és ebben a részben a történetileg első típust, a hash táblát vesszük példaként BPF_MAP_TYPE_HASH.

Ha létrehoz egy hash táblát mondjuk C++ nyelven, akkor azt mondaná unordered_map<int,long> woo, ami oroszul azt jelenti: „Kell egy asztal woo korlátlan méretű, melynek kulcsai típusúak int, és az értékek a típus long" Egy BPF hash tábla létrehozásához nagyjából ugyanezt kell tennünk, csak meg kell adnunk a tábla maximális méretét, és ahelyett, hogy megadnánk a kulcsok és értékek típusát, a méretüket bájtban kell megadnunk. . Térképek létrehozásához használja a parancsot BPF_MAP_CREATE rendszerhívás bpf. Nézzünk meg egy többé-kevésbé minimális programot, ami térképet készít. Az előző, BPF-programokat betöltő program után ez egyszerűnek tűnik Önnek:

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

Itt definiálunk egy paraméterkészletet attr, amelyben azt mondjuk: „Szükségem van egy hash-táblázatra kulcsokkal és méretértékekkel sizeof(int), amibe maximum négy elemet tudok beilleszteni." A BPF térképek készítésekor megadhatunk más paramétereket is, például ugyanúgy, mint a programnál a példában, az objektum nevét adtuk meg "woo".

Fordítsuk le és futtassuk a programot:

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

Itt a rendszerhívás bpf(2) visszaküldte nekünk a leíró térkép számát 3 majd a program a várakozásoknak megfelelően megvárja a további utasításokat a rendszerhívásban pause(2).

Most küldjük a programunkat a háttérbe, vagy nyissunk egy másik terminált, és a segédprogram segítségével nézzük meg az objektumunkat bpftool (térképünket a neve alapján tudjuk megkülönböztetni másoktól):

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

A 114-es szám az objektumunk globális azonosítója. A rendszer bármely programja használhatja ezt az azonosítót egy meglévő térkép megnyitásához a parancs segítségével BPF_MAP_GET_FD_BY_ID rendszerhívás bpf.

Most már játszhatunk a hash asztalunkkal. Nézzük a tartalmát:

$ sudo bpftool map dump id 114
Found 0 elements

Üres. Adjunk hozzá értéket hash[1] = 1:

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

Nézzük még egyszer a táblázatot:

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

Hurrá! Sikerült hozzáadnunk egy elemet. Jegyezzük meg, hogy ehhez bájt szinten kell dolgoznunk, mivel bptftool nem tudja, milyen típusúak a hash táblázatban szereplő értékek. (Ezt a tudást át lehet adni neki a BTF segítségével, de erről most bővebben.)

Pontosan hogyan olvassa be és ad hozzá elemeket a bpftool? Vessünk egy pillantást a motorháztető alá:

$ 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

Először a paranccsal megnyitottuk a térképet a globális azonosítója alapján BPF_MAP_GET_FD_BY_ID и bpf(2) visszaadta nekünk a 3. leírót. Tovább a parancs használatával BPF_MAP_GET_NEXT_KEY passzolással megtaláltuk a táblázatban az első kulcsot NULL mutatóként az "előző" kulcsra. Ha megvan a kulcs, megtehetjük BPF_MAP_LOOKUP_ELEMamely értéket ad vissza egy mutatónak value. A következő lépésben megpróbáljuk megtalálni a következő elemet úgy, hogy egy mutatót adunk az aktuális kulcshoz, de a táblázatunk csak egy elemet tartalmaz, és a parancsot BPF_MAP_GET_NEXT_KEY visszatér ENOENT.

Rendben, változtassuk meg az értéket az 1-es kulccsal, tegyük fel, hogy az üzleti logikánk regisztrációt igényel 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

Ahogy az várható volt, nagyon egyszerű: a parancs BPF_MAP_GET_FD_BY_ID megnyitja a térképünket az azonosító és a parancs alapján BPF_MAP_UPDATE_ELEM felülírja az elemet.

Tehát miután az egyik programból létrehoztunk egy hash táblát, egy másik programból olvashatjuk és írhatjuk annak tartalmát. Vegye figyelembe, hogy ha ezt meg tudtuk tenni a parancssorból, akkor a rendszer bármely más programja megteheti. A fent leírt parancsokon túlmenően a felhasználói térből származó térképekkel való munkavégzéshez következő:

  • BPF_MAP_LOOKUP_ELEM: érték keresése kulcs alapján
  • BPF_MAP_UPDATE_ELEM: frissítés/érték létrehozása
  • BPF_MAP_DELETE_ELEM: távolítsa el a kulcsot
  • BPF_MAP_GET_NEXT_KEY: keresse meg a következő (vagy első) gombot
  • BPF_MAP_GET_NEXT_ID: lehetővé teszi az összes létező térkép áttekintését, ez így működik bpftool map
  • BPF_MAP_GET_FD_BY_ID: egy meglévő térkép megnyitása annak globális azonosítójával
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomosan frissíti egy objektum értékét, és visszaadja a régit
  • BPF_MAP_FREEZE: a térképet megváltoztathatatlanná tenni a felhasználói térből (ez a művelet nem vonható vissza)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: tömeges műveletek. Például, BPF_MAP_LOOKUP_AND_DELETE_BATCH - ez az egyetlen megbízható módja az összes érték kiolvasásának és visszaállításának a térképről

Ezeknek a parancsoknak nem mindegyike működik minden térképtípusnál, de általában a felhasználói térből más típusú térképekkel való munka pontosan ugyanúgy néz ki, mint a hash táblákkal.

A rend kedvéért fejezzük be hash táblázatos kísérleteinket. Emlékszel, hogy létrehoztunk egy táblázatot, amely legfeljebb négy kulcsot tartalmazhat? Adjunk hozzá még néhány elemet:

$ 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

Eddig jó:

$ 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

Próbáljunk még egyet hozzáadni:

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

Ahogy az várható volt, nem sikerült. Nézzük meg részletesebben a hibát:

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

Minden rendben: ahogy az várható volt, a csapat BPF_MAP_UPDATE_ELEM megpróbál új, ötödik kulcsot létrehozni, de összeomlik E2BIG.

Így tudunk BPF programokat létrehozni és betölteni, valamint térképeket készíteni és kezelni felhasználói térből. Most logikus, hogy megvizsgáljuk, hogyan használhatjuk fel magukból a BPF-programokból származó térképeket. Beszélhetnénk erről a gépi makrókódokban nehezen olvasható programok nyelvén, de valójában eljött az idő, hogy megmutassuk, hogyan is készülnek és karbantartják a BPF programokat – a libbpf.

(Azoknak az olvasóknak, akik elégedetlenek az alacsony szintű példa hiányával: részletesen elemezzük azokat a programokat, amelyek térképeket és segédfunkciókat használnak. libbpf és elmondja, mi történik az utasítások szintjén. Az elégedetlen olvasóknak nagyon, tettük hozzá példa a cikk megfelelő helyén.)

BPF programok írása libbpf segítségével

BPF-programok gépi kódokkal történő írása csak első alkalommal lehet érdekes, aztán a jóllakottság megtörténik. Ebben a pillanatban rá kell fordítania a figyelmét llvm, amely rendelkezik egy háttérrendszerrel a BPF architektúra kódjának előállításához, valamint egy könyvtárral libbpf, amely lehetővé teszi a BPF alkalmazások felhasználói oldalának megírását és a segítségével generált BPF programok kódjának betöltését llvm/clang.

Valójában, amint látni fogjuk ebben és a következő cikkekben, libbpf elég sok munkát végez anélkül (vagy hasonló eszközök) iproute2, libbcc, libbpf-gostb.) nem lehet élni. A projekt egyik gyilkos jellemzője libbpf a BPF CO-RE (Compile Once, Run Everywhere) – egy projekt, amely lehetővé teszi olyan BPF programok írását, amelyek egyik kernelről a másikra hordozhatók, és különböző API-kon futhatnak (például ha a kernel szerkezete megváltozik a verziótól). verzióhoz). Ahhoz, hogy a CO-RE-vel tudjon dolgozni, a kernelnek BTF-támogatással kell lefordítania (ennek módját a részben leírjuk Fejlesztési eszközök. A következő fájl jelenlétével ellenőrizheti, hogy a kernel BTF-fel van-e építve, vagy nem nagyon egyszerűen:

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

Ez a fájl információkat tárol a kernelben használt összes adattípusról, és minden példánkban felhasználjuk libbpf. A következő cikkben részletesen fogunk beszélni a CO-RE-ről, de ebben - csak építs magadnak egy kernelt ezzel CONFIG_DEBUG_INFO_BTF.

könyvtár libbpf közvetlenül a címtárban lakik tools/lib/bpf kernel és fejlesztése a levelezőlistán keresztül történik [email protected]. A kernelen kívül élő alkalmazások igényeihez azonban külön tárolót tartanak fenn https://github.com/libbpf/libbpf amelyben a kernelkönyvtár többé-kevésbé úgy van tükrözve, ahogy van.

Ebben a részben megvizsgáljuk, hogyan hozhat létre olyan projektet, amely a libbpf, írjunk több (többé-kevésbé értelmetlen) tesztprogramot, és elemezzük részletesen, hogyan működik mindez. Ez lehetővé teszi számunkra, hogy a következő szakaszokban könnyebben elmagyarázzuk, hogy a BPF-programok hogyan működnek együtt a térképekkel, kernelsegédekkel, BTF-fel stb.

Jellemzően projektek segítségével libbpf adjunk hozzá egy GitHub-tárat git-almodulként, akkor ugyanezt tesszük:

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

Fog libbpf nagyon egyszerű:

$ 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

A következő tervünk ebben a részben a következő: írunk egy BPF programot, mint pl BPF_PROG_TYPE_XDP, ugyanaz, mint az előző példában, de C-ben a segítségével fordítjuk le clang, és írjon egy segédprogramot, amely betölti a kernelbe. A következő részekben mind a BPF program, mind az asszisztens program lehetőségeit bővítjük.

Példa: teljes értékű alkalmazás létrehozása a libbpf segítségével

Először a fájlt használjuk /sys/kernel/btf/vmlinux, amelyet fent említettünk, és hozzon létre megfelelőjét fejlécfájl formájában:

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

Ez a fájl tárolja a kernelünkben elérhető összes adatstruktúrát, például így van definiálva az IPv4 fejléc a kernelben:

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

Most C-ben írjuk meg a BPF programunkat:

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

Bár programunk nagyon egyszerűnek bizonyult, sok részletre még mindig oda kell figyelnünk. Először is, az első beépített fejlécfájl vmlinux.h, amelyet az imént generáltunk bpftool btf dump - most már nem kell telepítenünk a kernel-headers csomagot, hogy megtudjuk, hogyan néznek ki a kernelstruktúrák. A következő fejléc fájl érkezik hozzánk a könyvtárból libbpf. Most már csak a makró meghatározásához van szükségünk rá SEC, amely elküldi a karaktert az ELF objektumfájl megfelelő szakaszába. Programunkat a rovat tartalmazza xdp/simple, ahol a perjel előtt definiáljuk a BPF programtípust - ez a konvenció, amiben használatos libbpf, a szakasz neve alapján indításkor a megfelelő típust helyettesíti bpf(2). Maga a BPF program az C - nagyon egyszerű és egy sorból áll return XDP_PASS. Végül egy külön rész "license" tartalmazza az engedély nevét.

A programunkat lefordíthatjuk az llvm/clang paranccsal, verzió >= 10.0.0, vagy még jobb, nagyobb (lásd a részt Fejlesztési eszközök):

$ 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

Az érdekességek között: feltüntetjük a cél architektúrát -target bpf és a fejlécekhez vezető útvonalat libbpf, amelyet nemrég telepítettünk. Továbbá ne feledkezzünk meg róla -O2, e lehetőség nélkül a jövőben meglepetések érhetik. Nézzük meg a kódunkat, sikerült megírni a kívánt programot?

$ 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

Igen, sikerült! Most van egy bináris fájlunk a programmal, és szeretnénk létrehozni egy alkalmazást, amely betölti a kernelbe. Erre a célra a könyvtár libbpf két lehetőséget kínál – alacsonyabb szintű API vagy magasabb szintű API használata. A második utat választjuk, mivel szeretnénk megtanulni, hogyan írjunk, töltsünk be és kapcsoljunk össze BPF programokat minimális erőfeszítéssel a későbbi tanulmányozáshoz.

Először is létre kell hoznunk programunk „csontvázát” a binárisból, ugyanazzal a segédprogrammal bpftool — a BPF világ svájci bicskája (ami szó szerint is érthető, hiszen Daniel Borkman, a BPF egyik alkotója és fenntartója svájci):

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

Fájlban xdp-simple.skel.h tartalmazza programunk bináris kódját és az objektumunk kezelésére - betöltésére, csatolására, törlésére szolgáló funkciókat. A mi egyszerű esetünkben ez túlzásnak tűnik, de működik abban az esetben is, ha az objektumfájl sok BPF-programot és térképet tartalmaz, és ennek az óriási ELF-nek a betöltéséhez csak le kell generálnunk a csontvázat, és meghívnunk egy vagy két függvényt az egyéni alkalmazásból. írnak, menjünk tovább.

Szigorúan véve a betöltő programunk triviális:

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

Itt struct xdp_simple_bpf fájlban meghatározott xdp-simple.skel.h és leírja az objektumfájlunkat:

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

Itt egy alacsony szintű API nyomait láthatjuk: a szerkezetet struct bpf_program *simple и struct bpf_link *simple. Az első szerkezet kifejezetten leírja programunkat, a részben írva xdp/simple, a második pedig leírja, hogyan csatlakozik a program az eseményforráshoz.

Funkció xdp_simple_bpf__open_and_load, megnyit egy ELF objektumot, elemzi, létrehozza az összes struktúrát és alstruktúrát (a programon kívül az ELF egyéb részeket is tartalmaz - adatok, csak olvasható adatok, hibakeresési információk, licenc stb.), majd egy rendszer segítségével betölti a kernelbe. hívás bpf, amit a program lefordításával és futtatásával ellenőrizhetünk:

$ 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

Most nézzük meg a programunkat bpftool. Keressük az azonosítóját:

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

és dump (a parancs rövidített formáját használjuk bpftool prog dump xlated):

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

Valami újat! A program kinyomtatta a C forrásfájlunk darabjait, ezt a könyvtár tette libbpf, amely megtalálta a debug szakaszt a binárisban, lefordította egy BTF objektummá, betöltötte a kernelbe a segítségével BPF_BTF_LOAD, majd a paranccsal a program betöltésekor megadta az eredményül kapott fájlleírót BPG_PROG_LOAD.

Kernel segítők

A BPF programok „külső” függvényeket – kernel segítőket – futtathatnak. Ezek a segítő funkciók lehetővé teszik a BPF-programok számára, hogy hozzáférjenek a kernelstruktúrákhoz, kezeljék a térképeket, és kommunikáljanak a „valós világgal” – tökéletes eseményeket hozhatnak létre, hardvert vezérelhetnek (például átirányítási csomagokat) stb.

Példa: bpf_get_smp_processor_id

A „példatanulás” paradigma keretein belül vegyük figyelembe az egyik segítő funkciót, bpf_get_smp_processor_id(), egy bizonyos fájlban kernel/bpf/helpers.c. Annak a processzornak a számát adja vissza, amelyen az azt hívó BPF program fut. De minket nem annyira a szemantikája érdekel, mint az, hogy megvalósítása egy vonalat foglal magában:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

A BPF helper függvény definíciói hasonlóak a Linux rendszerhívás definícióihoz. Itt például egy olyan függvény van definiálva, amelynek nincsenek argumentumai. (Azt a függvényt, amely mondjuk három argumentumot vesz fel, a makró segítségével határozzuk meg BPF_CALL_3. Az argumentumok maximális száma öt.) Ez azonban csak a definíció első része. A második rész a típusstruktúra meghatározása struct bpf_func_proto, amely az ellenőrző által értelmezett segítő funkció leírását tartalmazza:

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

Segítő funkciók regisztrálása

Ahhoz, hogy egy adott típusú BPF-programok használhassák ezt a funkciót, regisztrálniuk kell azt, például a típushoz BPF_PROG_TYPE_XDP egy függvény van definiálva a kernelben xdp_func_proto, amely a helper függvény azonosítójából meghatározza, hogy az XDP támogatja-e ezt a funkciót vagy sem. A mi funkciónk az támogatja:

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

Az új BPF programtípusok „definiálva” vannak a fájlban include/linux/bpf_types.h makró segítségével BPF_PROG_TYPE. Azért van idézőjelben definiálva, mert ez egy logikai definíció, a C nyelvben pedig konkrét szerkezetek egész halmazának meghatározása más helyeken is előfordul. Különösen a fájlban kernel/bpf/verifier.c minden definíció fájlból bpf_types.h struktúrák tömbjének létrehozására szolgálnak 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
};

Ez azt jelenti, hogy minden BPF programtípushoz meg van határozva egy mutató az adott típusú adatszerkezetre struct bpf_verifier_ops, amelyet az értékkel inicializálunk _name ## _verifier_ops, azaz xdp_verifier_ops a xdp. Szerkezet xdp_verifier_ops meghatározta fájlban net/core/filter.c az alábbiak szerint:

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

Itt látjuk ismerős funkciónkat xdp_func_proto, amely minden alkalommal futtatja az ellenőrzőt, amikor kihívással találkozik néhány funkciók egy BPF programon belül, lásd verifier.c.

Nézzük meg, hogyan használja egy hipotetikus BPF program a függvényt bpf_get_smp_processor_id. Ehhez átírjuk a programot előző részünkből az alábbiak szerint:

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

Szimbólum bpf_get_smp_processor_id meghatározta в <bpf/bpf_helper_defs.h> könyvtár libbpf mint

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

vagyis bpf_get_smp_processor_id egy függvénymutató, amelynek értéke 8, ahol 8 az érték BPF_FUNC_get_smp_processor_id típus enum bpf_fun_id, amely számunkra a fájlban van definiálva vmlinux.h (fájl bpf_helper_defs.h a kernelben egy szkript generálja, tehát a „varázslatos” számok rendben vannak). Ez a függvény nem vesz fel argumentumokat, és egy típusú értéket ad vissza __u32. Amikor futtatjuk a programunkban, clang utasítást generál BPF_CALL "a megfelelő fajta" Állítsuk össze a programot, és nézzük meg a részt 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

Az első sorban utasításokat látunk call, paraméter IMM ami egyenlő 8-cal, és SRC_REG - nulla. A hitelesítő által használt ABI-megállapodás szerint ez egy hívás a nyolcas számú helper funkcióhoz. Miután elindította, a logika egyszerű. Visszatérési érték a regiszterből r0 -be másolva r1 a 2,3 sorokon pedig típussá alakítjuk u32 — a felső 32 bit törlődik. A 4,5,6,7, 2, XNUMX, XNUMX sorban XNUMX-t adunk vissza (XDP_PASS) vagy 1 (XDP_DROP) attól függően, hogy a 0. sor helper függvénye nulla vagy nullától eltérő értéket adott vissza.

Teszteljük magunkat: töltsük be a programot és nézzük meg a kimenetet 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

Rendben, az ellenőrző megtalálta a megfelelő kernel-segítőt.

Példa: argumentumok átadása és végül a program futtatása!

Minden futási szintű segédfunkciónak van prototípusa

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

A segédfunkciók paraméterei regiszterekben kerülnek átadásra r1-r5, és az érték visszakerül a regiszterben r0. Nincsenek olyan függvények, amelyek ötnél több argumentumot igényelnek, és ezek támogatása a jövőben várhatóan nem lesz hozzáadva.

Vessünk egy pillantást az új kernel segítőre, és arra, hogy a BPF hogyan adja át a paramétereket. Írjuk át xdp-simple.bpf.c a következőképpen (a többi sor nem változott):

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

Programunk kiírja annak a CPU-nak a számát, amelyen fut. Fordítsuk össze, és nézzük meg a kódot:

$ 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

A 0-7 sorba írjuk a karakterláncot running on CPU%un, majd a 8-as vonalon az ismerőst futtatjuk bpf_get_smp_processor_id. A 9-12. sorban elkészítjük a segítő érveket bpf_printk - nyilvántartások r1, r2, r3. Miért van belőlük három és nem kettő? Mert bpf_printkez egy makróburkoló az igazi segítő körül bpf_trace_printk, amelynek át kell adnia a formátum karakterláncának méretét.

Most adjunk hozzá néhány sort xdp-simple.chogy programunk csatlakozzon az interfészhez lo és tényleg elkezdődött!

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

Itt a függvényt használjuk bpf_set_link_xdp_fd, amely az XDP-típusú BPF programokat hálózati interfészekhez köti. Az interfész számát keményen kódoltuk lo, ami mindig 1. A függvényt kétszer futtatjuk, hogy először leválasztjuk a régi programot, ha csatolva volt. Vegyük észre, hogy most nincs szükségünk kihívásra pause vagy egy végtelen ciklus: a betöltő programunk kilép, de a BPF program nem ölődik meg, mivel az eseményforráshoz kapcsolódik. Sikeres letöltés és csatlakozás után a program minden címre érkező hálózati csomagra elindul lo.

Töltsük le a programot és nézzük meg a felületet 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

A letöltött program 669-es azonosítóval rendelkezik, és ugyanazt az azonosítót látjuk a felületen lo. Küldünk pár csomagot a címre 127.0.0.1 (kérés + válasz):

$ ping -c1 localhost

és most nézzük meg a debug virtuális fájl tartalmát /sys/kernel/debug/tracing/trace_pipe, amiben bpf_printk írja az üzeneteit:

# 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

Két csomagot vettek észre lo és CPU0-n dolgozzuk fel - az első teljes értékű értelmetlen BPF programunk működött!

Érdemes megjegyezni, hogy bpf_printk Nem hiába ír a debug fájlba: nem ez a legsikeresebb termelési segédeszköz, de az volt a célunk, hogy valami egyszerűt mutassunk.

Térképek elérése BPF programokból

Példa: a BPF program térképének használata

Az előző részekben megtanultuk, hogyan lehet térképeket létrehozni és használni felhasználói térből, most pedig nézzük a kernel részét. Kezdjük szokás szerint egy példával. Írjuk át a programunkat xdp-simple.bpf.c az alábbiak szerint:

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

A program elején térképdefiníciót adtunk hozzá woo: Ez egy 8 elemből álló tömb, amely olyan értékeket tárol, mint pl u64 (C-ben egy olyan tömböt határoznánk meg, mint u64 woo[8]). Egy programban "xdp/simple" változóba kapjuk az aktuális processzorszámot key majd a segítő funkció használatával bpf_map_lookup_element mutatót kapunk a tömb megfelelő bejegyzésére, amit eggyel növelünk. Oroszra lefordítva: statisztikát számítunk ki, hogy melyik CPU dolgozta fel a bejövő csomagokat. Próbáljuk meg futtatni a programot:

$ 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

Ellenőrizzük, hogy be van-e kapcsolva lo és küldj néhány csomagot:

$ 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

Most nézzük a tömb tartalmát:

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

Szinte az összes folyamatot a CPU7-en dolgozták fel. Ez nem fontos számunkra, a lényeg, hogy a program működjön, és megértsük, hogyan lehet térképeket elérni a BPF programokból - a хелперов bpf_mp_*.

Misztikus index

Tehát a térképet a BPF programból érhetjük el olyan hívásokkal, mint pl

val = bpf_map_lookup_elem(&woo, &key);

ahol a segítő funkció néz ki

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

de áthaladunk egy mutató mellett &woo egy névtelen szerkezetre struct { ... }...

Ha megnézzük a program összeállítót, azt látjuk, hogy az érték &woo valójában nincs meghatározva (4. sor):

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

és az áthelyezések tartalmazzák:

$ 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

De ha megnézzük a már betöltött programot, akkor egy mutatót látunk a megfelelő térképre (4. sor):

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

Így arra a következtetésre juthatunk, hogy betöltő programunk indításakor a link a &woo valami könyvtárral helyettesítették libbpf. Először nézzük meg a kimenetet 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

Ezt látjuk libbpf térképet készített woo majd letöltötte a programunkat simple. Nézzük meg közelebbről, hogyan töltjük be a programot:

  • hívás xdp_simple_bpf__open_and_load fájlból xdp-simple.skel.h
  • ami okozza xdp_simple_bpf__load fájlból xdp-simple.skel.h
  • ami okozza bpf_object__load_skeleton fájlból libbpf/src/libbpf.c
  • ami okozza bpf_object__load_xattr A libbpf/src/libbpf.c

Többek között az utolsó függvény hívja meg bpf_object__create_maps, amely meglévő térképeket hoz létre vagy nyit meg, fájlleírókká alakítva azokat. (Itt látjuk BPF_MAP_CREATE a kimenetben strace.) Ezután a függvény meghívása bpf_object__relocate és ő az, aki érdekel minket, hiszen emlékszünk arra, amit láttunk woo az áthelyezési táblázatban. Feltárva végül a funkcióban találjuk magunkat bpf_program__relocate, melyik térképes költöztetésekkel foglalkozik:

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

Tehát megfogadjuk az utasításainkat

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

és cserélje ki a benne lévő forrásregisztert erre BPF_PSEUDO_MAP_FD, és az első IMM a térképünk fájlleírójához, és ha egyenlő pl. 0xdeadbeef, akkor ennek eredményeként megkapjuk az utasítást

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

Így kerül átvitelre a térképinformáció egy adott betöltött BPF programba. Ebben az esetben a térképet a segítségével lehet létrehozni BPF_MAP_CREATE, és azonosítóval nyitható meg BPF_MAP_GET_FD_BY_ID.

Használat közben összesen libbpf az algoritmus a következő:

  • az összeállítás során rekordok jönnek létre az áthelyezési táblában a térképekre mutató hivatkozásokhoz
  • libbpf megnyitja az ELF objektumkönyvet, megkeresi az összes használt térképet és fájlleírókat hoz létre hozzájuk
  • a fájlleírók az utasítás részeként betöltődnek a kernelbe LD64

Ahogy el tudod képzelni, még több van hátra, és meg kell vizsgálnunk a lényeget. Szerencsére van egy nyomunk – felírtuk a jelentését BPF_PSEUDO_MAP_FD be a forrásnyilvántartásba, és eltemethetjük, ami elvezet minket minden szentek szentjébe - kernel/bpf/verifier.c, ahol egy megkülönböztető nevű függvény lecseréli a fájlleírót egy típusú szerkezet címére 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;

(a teljes kód megtalálható по ссылке). Tehát kibővíthetjük az algoritmusunkat:

  • A program betöltésekor a hitelesítő ellenőrzi a térkép helyes használatát és kiírja a megfelelő szerkezet címét struct bpf_map

Az ELF bináris letöltése során a libbpf Még sok minden történik, de erről más cikkekben fogunk beszélni.

Programok és térképek betöltése libbpf nélkül

Ahogy ígértük, itt egy példa azoknak az olvasóknak, akik szeretnék tudni, hogyan hozhatnak létre és tölthetnek be egy térképeket használó programot segítség nélkül libbpf. Ez akkor lehet hasznos, ha olyan környezetben dolgozik, amelyhez nem tud függőséget létrehozni, vagy minden bitet menteni, vagy olyan programot ír, mint pl. ply, amely menet közben generál BPF bináris kódot.

A logika könnyebb követhetősége érdekében átírjuk a példánkat erre a célra xdp-simple. A példában tárgyalt program teljes és kissé kibővített kódja ebben található lényeg.

Alkalmazásunk logikája a következő:

  • típustérképet készíteni BPF_MAP_TYPE_ARRAY parancs segítségével BPF_MAP_CREATE,
  • hozzon létre egy programot, amely ezt a térképet használja,
  • csatlakoztassa a programot az interfészhez lo,

ami lefordítva emberi mint

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

Itt map_create ugyanúgy létrehoz egy térképet, mint az első példában a rendszerhívásról bpf - „kernel, kérlek készíts nekem egy új térképet egy 8 elemből álló tömb formájában, mint pl __u64 és adja vissza a fájlleírót":

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

A program betöltése is egyszerű:

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

A trükkös rész prog_load a BPF programunk struktúrák tömbjeként való meghatározása struct bpf_insn insns[]. De mivel olyan programot használunk, ami C-ben van, csalhatunk egy kicsit:

$ 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

Összesen 14 utasítást kell írnunk hasonló szerkezetek formájában struct bpf_insn (tanács: vegye ki a szemétlerakót felülről, olvassa el újra az utasításokat, nyissa meg linux/bpf.h и linux/bpf_common.h és próbálja meghatározni struct bpf_insn insns[] egyedül):

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

Gyakorlat azoknak, akik ezt nem maguk írták – találd meg map_fd.

Még egy fel nem tárt rész maradt a programunkból - xdp_attach. Sajnos az XDP-hez hasonló programok nem csatlakoztathatók rendszerhívással bpf. A BPF-et és az XDP-t létrehozó emberek az online Linux közösségből származtak, ami azt jelenti, hogy a számukra legismertebbet használták (de nem Normál emberek) interfész a kernellel való interakcióhoz: netlink socketeket, Lásd még RFC3549. A megvalósítás legegyszerűbb módja xdp_attach kódot másol innen libbpf, mégpedig a fájlból netlink.c, amit mi csináltunk, kicsit lerövidítve:

Üdvözöljük a netlink socketek világában

Nyisson meg egy netlink socket típust 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;
}

Ebből a foglalatból ezt olvassuk:

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

Végül itt van a funkciónk, amely megnyit egy socketet, és egy speciális üzenetet küld neki, amely egy fájlleírót tartalmaz:

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

Tehát minden készen áll a tesztelésre:

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

Nézzük meg, hogy a programunk csatlakozott-e 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

Küldjünk pingeket és nézzük meg a térképet:

$ 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

Hurrá, minden működik. Megjegyezzük egyébként, hogy a térképünk ismét bájtok formájában jelenik meg. Ez annak a ténynek köszönhető, hogy ellentétben libbpf nem töltöttünk be típusinformációt (BTF). De erről majd legközelebb.

Fejlesztési eszközök

Ebben a részben a minimális BPF fejlesztői eszközkészletet tekintjük át.

Általánosságban elmondható, hogy nincs szükség semmi különlegesre a BPF programok fejlesztéséhez – a BPF bármilyen tisztességes terjesztési kernelen fut, és a programok a clang, amely a csomagból szállítható. A BPF fejlesztése miatt azonban a kernel és az eszközök folyamatosan változnak, ha nem akarsz 2019-től régimódi módszerekkel BPF programokat írni, akkor le kell fordítanod

  • llvm/clang
  • pahole
  • a magja
  • bpftool

(Referenciaképpen ez a szakasz és a cikkben szereplő összes példa Debian 10-en futott.)

llvm/cseng

A BPF barátságos az LLVM-mel, és bár a közelmúltban a BPF-hez programokat le lehet fordítani gcc segítségével, minden jelenlegi fejlesztés LLVM-re történik. Ezért mindenekelőtt a jelenlegi verziót készítjük el clang gitből:

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

Most ellenőrizhetjük, hogy minden rendben van-e:

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

(Szerelési útmutató clang tőlem vettem bpf_devel_QA.)

Nem telepítjük a most készített programokat, hanem csak hozzáadjuk őket PATH, például:

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

(Ezt hozzá lehet adni .bashrc vagy egy külön fájlba. Én személy szerint hozzáteszem az ehhez hasonló dolgokat ~/bin/activate-llvm.sh és ha kell, megteszem . activate-llvm.sh.)

Pahole és BTF

Hasznosság pahole a kernel felépítésekor használják a hibakeresési információk BTF formátumú létrehozására. Ebben a cikkben nem részletezzük a BTF technológia részleteit, azon kívül, hogy kényelmes és használni akarjuk. Tehát ha meg akarja építeni a kernelt, először építse meg pahole (nélkül pahole opcióval nem fogod tudni felépíteni a kernelt 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

Kernelek a BPF-fel való kísérletezéshez

A BPF lehetőségeinek feltárásakor a saját magomat szeretném összeállítani. Általánosságban elmondható, hogy ez nem szükséges, mivel a terjesztési rendszermagra képes lesz a BPF programokat lefordítani és betölteni, azonban a saját kernel birtokában a legújabb BPF funkciókat használhatja, amelyek legjobb esetben hónapok múlva jelennek meg a disztribúciójában. , vagy, mint egyes hibakereső eszközök esetében, belátható időn belül egyáltalán nem lesz csomagolva. Ezenkívül a saját magja fontosnak érzi a kóddal való kísérletezést.

A kernel felépítéséhez először magára a kernelre, másodsorban egy kernel konfigurációs fájlra van szükség. A BPF-fel való kísérletezéshez használhatjuk a szokásosat vanília kernel vagy valamelyik fejlesztői kernel. Történelmileg a BPF fejlesztés a Linux hálózati közösségen belül zajlik, ezért minden változás előbb-utóbb David Miller, a Linux hálózat karbantartója révén megy végbe. Természetüktől függően – szerkesztések vagy új funkciók – a hálózati változtatások két mag egyikébe esnek – net vagy net-next. A BPF-re vonatkozó változtatások azonos módon kerülnek elosztásra bpf и bpf-next, amelyeket azután nettó és net-next-be vonnak össze. További részletekért lásd bpf_devel_QA и netdev-GYIK. Tehát válasszon kernelt ízlése és a tesztelt rendszer stabilitási igényei alapján (*-next kernelek a leginstabilabbak a felsoroltak közül).

A kernel konfigurációs fájlok kezeléséről nem beszélünk e cikk keretein belül – feltételezzük, hogy vagy már tudja, hogyan kell ezt megtenni, vagy készen áll a tanulásra egymaga. A következő utasítások azonban többé-kevésbé elegendőek ahhoz, hogy működőképes BPF-kompatibilis rendszert kapjon.

Töltse le a fenti kernelek egyikét:

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

Minimális működő kernel konfiguráció létrehozása:

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

BPF opciók engedélyezése a fájlban .config saját választása szerint (valószínűleg CONFIG_BPF már engedélyezve lesz, mivel a systemd használja). Íme a cikkhez használt kernel opcióinak listája:

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

Ezután egyszerűen összeállíthatjuk és telepíthetjük a modulokat és a kernelt (egyébként a kernelt az újonnan összeállított clanghozzáadásával CC=clang):

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

és indítsa újra az új kernellel (én erre használom kexec a csomagból 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

A cikkben leggyakrabban használt segédprogram a segédprogram lesz bpftool, amelyet a Linux kernel részeként szállítanak. A BPF fejlesztői írták és tartják karban a BPF fejlesztők számára, és minden típusú BPF objektum kezelésére használható - programok betöltésére, térképek létrehozására és szerkesztésére, a BPF ökoszisztéma életének felfedezésére stb. A kézikönyvoldalak forráskódjai formájában található dokumentáció megtalálható a magban vagy már összeállított, hálózat.

Jelen írás idején bpftool csak RHEL-hez, Fedorához és Ubuntuhoz készül (lásd pl. ezt a szálat, amely a csomagolás befejezetlen történetét meséli el bpftool Debianban). De ha már megépítetted a kernelt, akkor építsd bpftool olyan egyszerű, mint a pite:

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

$

(itt ${linux} - ez a kernelkönyvtárad.) A parancsok végrehajtása után bpftool könyvtárba gyűjtjük ${linux}/tools/bpf/bpftool és hozzáadható az elérési úthoz (elsősorban a felhasználóhoz root), vagy csak másolja ide /usr/local/sbin.

Gyűjt bpftool a legjobb az utóbbit használni clang, a fent leírtak szerint összeszerelve, és ellenőrizze, hogy megfelelően van-e összeszerelve - például a parancs segítségével

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

amely megmutatja, hogy mely BPF-szolgáltatások vannak engedélyezve a kernelben.

Egyébként az előző parancs futtatható mint

# bpftool f p k

Ez a csomagban található segédprogramokkal analóg módon történik iproute2, ahol például azt mondhatjuk ip a s eth0 helyett ip addr show dev eth0.

Következtetés

A BPF lehetővé teszi a bolhák felcipelését, hogy hatékonyan mérje és menet közben módosítsa a mag funkcionalitását. A rendszer a UNIX legjobb hagyományai szerint nagyon sikeresnek bizonyult: egy egyszerű mechanizmus, amely lehetővé teszi a kernel (újra)programozását, hatalmas számú ember és szervezet kísérletezését tette lehetővé. És bár a kísérletek, valamint maga a BPF infrastruktúra fejlesztése még korántsem fejeződött be, a rendszer már rendelkezik egy stabil ABI-val, amely lehetővé teszi megbízható és legfőképpen hatékony üzleti logika felépítését.

Szeretném megjegyezni, hogy véleményem szerint a technológia azért lett ilyen népszerű, mert egyrészt képes rá játszik (a gép architektúráját nagyjából egy este alatt meg lehet érteni), másrészt megoldani a megjelenése előtt (szépen) nem megoldható problémákat. Ez a két összetevő együtt kísérletezésre és álmodozásra készteti az embereket, ami egyre innovatívabb megoldások megjelenéséhez vezet.

Ez a cikk, bár nem különösebben rövid, csak bevezető a BPF világába, és nem írja le a „fejlett” funkciókat és az architektúra fontos részeit. A jövőre vonatkozó terv a következő: a következő cikk a BPF programtípusok áttekintése lesz (az 5.8-as kernelben 30 programtípus támogatott), majd végül megnézzük, hogyan lehet valódi BPF alkalmazásokat írni kernelkövető programokkal. Például itt az ideje egy alaposabb kurzusnak a BPF architektúráról, majd a BPF hálózati és biztonsági alkalmazások példáival.

A sorozat korábbi cikkei

  1. BPF kicsiknek, nulladik rész: klasszikus BPF

Linkek

  1. BPF és XDP referencia útmutató — dokumentáció a BPF-ről a ciliumtól, pontosabban Daniel Borkmantől, a BPF egyik alkotójától és fenntartójától. Ez az egyik első komolyabb leírás, ami annyiban különbözik a többitől, hogy Daniel pontosan tudja, miről ír, és nincsenek benne hibák. Ez a dokumentum különösen azt írja le, hogyan kell dolgozni XDP és TC típusú BPF programokkal a jól ismert segédprogram segítségével. ip a csomagból iproute2.

  2. Dokumentáció/hálózat/filter.txt — eredeti fájl a klasszikus, majd a kiterjesztett BPF dokumentációjával. Jó olvasmány, ha szeretne elmélyülni az assembly nyelvben és a műszaki építészeti részletekben.

  3. Blog a BPF-ről a Facebookról. Ritkán, de találóan frissül, ahogy Alexei Starovoitov (az eBPF szerzője) és Andrii Nakryiko - (karbantartó) írja ott libbpf).

  4. A bpftool titkai. Szórakoztató twitter-szál Quentin Monnet-tól a bpftool használatának példáival és titkaival.

  5. Merüljön el a BPF-ben: olvasnivalók listája. Óriási (és továbbra is karbantartott) lista a Quentin Monnet BPF-dokumentációjához mutató hivatkozásokról.

Forrás: will.com

Hozzászólás