BPF pienimmille, osa yksi: pidennetty BPF

Alussa oli tekniikka ja sen nimi oli BPF. Katsoimme häntä edellinen, tämän sarjan Vanhan testamentin artikkeli. Vuonna 2013 Aleksei Starovoitovin ja Daniel Borkmanin ponnisteluilla siitä kehitettiin nykyaikaisille 64-bittisille koneille optimoitu parannettu versio, joka sisällytettiin Linux-ytimeen. Tätä uutta tekniikkaa kutsuttiin lyhyesti nimellä Internal BPF, sitten uudelleen nimeltä Extended BPF, ja nyt useiden vuosien jälkeen kaikki kutsuvat sitä yksinkertaisesti BPF:ksi.

Karkeasti sanottuna BPF mahdollistaa mielivaltaisen käyttäjän toimittaman koodin suorittamisen Linuxin ydintilassa, ja uusi arkkitehtuuri osoittautui niin onnistuneeksi, että tarvitsemme vielä tusinaa artikkelia kuvaamaan sen kaikkia sovelluksia. (Ainoa asia, jota kehittäjät eivät tehneet hyvin, kuten alla olevasta suorituskoodista näet, oli kunnollisen logon luominen.)

Tässä artikkelissa kuvataan BPF-virtuaalikoneen rakenne, ytimen käyttöliittymät BPF:n kanssa työskentelyyn, kehitystyökalut sekä lyhyt, hyvin lyhyt katsaus olemassa oleviin ominaisuuksiin, ts. kaikki mitä tarvitsemme tulevaisuudessa BPF:n käytännön sovellusten syvempään tutkimukseen.
BPF pienimmille, osa yksi: pidennetty BPF

Artikkelin tiivistelmä

Johdatus BPF-arkkitehtuuriin. Ensin tarkastellaan BPF-arkkitehtuuria lintuperspektiivistä ja hahmotellaan pääkomponentit.

BPF-virtuaalikoneen rekisterit ja komentojärjestelmä. Koska meillä on jo käsitys arkkitehtuurista kokonaisuutena, kuvailemme BPF-virtuaalikoneen rakennetta.

BPF-objektien elinkaari, bpffs-tiedostojärjestelmä. Tässä osiossa tarkastellaan lähemmin BPF-objektien - ohjelmien ja karttojen - elinkaarta.

Objektien hallinta bpf-järjestelmäkutsulla. Ymmärtäen jo jonkin verran järjestelmää, tarkastelemme lopuksi, kuinka luoda ja käsitellä objekteja käyttäjätilasta käyttämällä erityistä järjestelmäkutsua − bpf(2).

Пишем программы BPF с помощью libbpf. Voit tietysti kirjoittaa ohjelmia järjestelmäkutsulla. Mutta se on vaikeaa. Todellisempaa skenaariota varten ydinohjelmoijat kehittivät kirjaston libbpf. Luomme BPF-perussovellusrungon, jota käytämme seuraavissa esimerkeissä.

Ytimen avustajat. Täällä opimme kuinka BPF-ohjelmat voivat käyttää ytimen aputoimintoja - työkalua, joka karttojen ohella laajentaa perusteellisesti uuden BPF:n ominaisuuksia perinteiseen verrattuna.

Pääsy karttoihin BPF-ohjelmista. Tässä vaiheessa tiedämme tarpeeksi ymmärtääksemme tarkasti, kuinka voimme luoda karttoja käyttäviä ohjelmia. Ja katsotaanpa vaikka nopeasti mahtavaan ja mahtavaan todentajaan.

Kehitystyökalut. Ohjeosio tarvittavien apuohjelmien ja ytimen kokoamisesta kokeita varten.

Päätelmä. Artikkelin lopusta tähän asti lukeneet löytävät seuraavista artikkeleista motivoivia sanoja ja lyhyen kuvauksen siitä, mitä tapahtuu. Listaamme myös useita linkkejä itseopiskeluun niille, joilla ei ole halua tai kykyä odottaa jatkoa.

Johdatus BPF-arkkitehtuuriin

Ennen kuin alamme tarkastella BPF-arkkitehtuuria, viittaamme viimeisen kerran (oh). klassinen BPF, joka kehitettiin vastauksena RISC-koneiden tuloon ja ratkaisi tehokkaan pakettisuodatuksen ongelman. Arkkitehtuuri osoittautui niin menestyksekkääksi, että se syntyi räjähdysmäisellä XNUMX-luvulla Berkeley UNIXissa, ja se siirrettiin useimpiin olemassa oleviin käyttöjärjestelmiin, säilyi hulluna XNUMX-luvulla ja etsii edelleen uusia sovelluksia.

Uusi BPF kehitettiin vastauksena 64-bittisten koneiden, pilvipalvelujen yleisyyteen ja lisääntyneeseen tarpeeseen SDN:n luomiseen (Slaitteisto-dtarkennettu nverkottuminen). Ytimen verkkoinsinöörien kehittämä paranneltu klassisen BPF:n korvike, uusi BPF kirjaimellisesti kuusi kuukautta myöhemmin löysi sovelluksia vaikeassa tehtävässä jäljittää Linux-järjestelmiä, ja nyt, kuusi vuotta sen ilmestymisen jälkeen, tarvitsemme koko seuraavan artikkelin. luetella erityyppisiä ohjelmia.

Hauskoja kuvia

Pohjimmiltaan BPF on hiekkalaatikko-virtuaalikone, jonka avulla voit ajaa "mielivaltaista" koodia ydintilassa vaarantamatta turvallisuutta. BPF-ohjelmat luodaan käyttäjätilaan, ladataan ytimeen ja yhdistetään johonkin tapahtumalähteeseen. Tapahtuma voi olla esimerkiksi paketin toimittaminen verkkoliittymään, jonkin ydintoiminnon käynnistäminen jne. Paketin tapauksessa BPF-ohjelmalla on pääsy paketin tietoihin ja metatietoihin (lukemista ja mahdollisesti kirjoittamista varten, riippuen ohjelmatyypistä); jos kyseessä on ydintoiminto, sen argumentit funktio, mukaan lukien osoittimet ytimen muistiin jne.

Tarkastellaanpa tätä prosessia tarkemmin. Puhutaanpa aluksi ensimmäisestä erosta klassiseen BPF:ään, jonka ohjelmat kirjoitettiin assemblerissä. Uudessa versiossa arkkitehtuuria laajennettiin niin, että ohjelmia voitiin kirjoittaa korkean tason kielillä, ensisijaisesti tietysti C-kielellä. Tätä varten kehitettiin llvm:n taustaohjelma, jonka avulla voidaan luoda tavukoodia BPF-arkkitehtuurille.

BPF pienimmille, osa yksi: pidennetty BPF

BPF-arkkitehtuuri suunniteltiin osittain toimimaan tehokkaasti nykyaikaisissa koneissa. Jotta tämä toimisi käytännössä, BPF-tavukoodi, kun se on ladattu ytimeen, käännetään alkuperäiseksi koodiksi käyttämällä komponenttia nimeltä JIT-kääntäjä (Just In Time). Seuraavaksi, jos muistat, klassisessa BPF:ssä ohjelma ladattiin ytimeen ja liitettiin tapahtumalähteeseen atomisesti - yhden järjestelmäkutsun yhteydessä. Uudessa arkkitehtuurissa tämä tapahtuu kahdessa vaiheessa - ensin koodi ladataan ytimeen järjestelmäkutsulla bpf(2)ja sitten myöhemmin muilla mekanismeilla, jotka vaihtelevat ohjelman tyypistä riippuen, ohjelma kiinnittyy tapahtumalähteeseen.

Tässä lukijalla voi olla kysymys: oliko se mahdollista? Miten tällaisen koodin suoritusturvallisuus taataan? Toteutusturvallisuuden takaa meille BPF-ohjelmien latausvaihe nimeltä verifier (englanniksi tätä vaihetta kutsutaan verfieriksi ja käytän jatkossakin englanninkielistä sanaa):

BPF pienimmille, osa yksi: pidennetty BPF

Verifier on staattinen analysaattori, joka varmistaa, että ohjelma ei häiritse ytimen normaalia toimintaa. Tämä ei muuten tarkoita, että ohjelma ei voisi häiritä järjestelmän toimintaa - BPF-ohjelmat voivat tyypistä riippuen lukea ja kirjoittaa uudelleen ytimen muistin osia, palauttaa funktioiden arvoja, leikata, liittää, kirjoittaa uudelleen. ja jopa välittää verkkopaketteja. Verifier takaa, että BPF-ohjelman ajo ei kaada ydintä ja että ohjelma, jolla on sääntöjen mukaan kirjoitusoikeus, esimerkiksi lähtevän paketin tietoihin, ei pysty ylikirjoittamaan paketin ulkopuolella olevaa ytimen muistia. Tarkastelemme todentajaa hieman yksityiskohtaisemmin vastaavassa osiossa, kun olemme tutustuneet kaikkiin muihin BPF: n komponentteihin.

Joten mitä olemme oppineet tähän mennessä? Käyttäjä kirjoittaa ohjelman C-kielellä, lataa sen ytimeen järjestelmäkutsulla bpf(2), jossa todentaja tarkistaa sen ja käännetään alkuperäiseksi tavukoodiksi. Sitten sama tai toinen käyttäjä yhdistää ohjelman tapahtumalähteeseen ja se alkaa suorittaa. Käynnistyksen ja yhteyden erottaminen on tarpeen useista syistä. Ensinnäkin todentajan käyttäminen on suhteellisen kallista, ja lataamalla saman ohjelman useita kertoja tuhlaamme tietokoneen aikaa. Toiseksi, ohjelman tarkat liitännät riippuvat sen tyypistä, ja yksi vuosi sitten kehitetty "yleinen" käyttöliittymä ei välttämättä sovellu uudentyyppisille ohjelmille. (Vaikka nyt, kun arkkitehtuuri on tulossa kypsempään, on ajatus yhdistää tämä käyttöliittymä tasolle libbpf.)

Huomaavainen lukija saattaa huomata, ettemme ole vielä saaneet kuvia valmiiksi. Itse asiassa kaikki yllä oleva ei selitä miksi BPF muuttaa perusteellisesti kuvaa verrattuna klassiseen BPF:ään. Kaksi innovaatiota, jotka laajentavat merkittävästi sovellettavuutta, ovat kyky käyttää jaettua muistia ja ytimen aputoimintoja. BPF:ssä jaettu muisti on toteutettu käyttämällä ns. karttoja - jaettuja tietorakenteita tietyllä API:lla. He luultavasti saivat tämän nimen, koska ensimmäinen ilmestynyt karttatyyppi oli hash-taulukko. Sitten ilmestyi taulukoita, paikallisia (prosessorikohtaisia) hajautustaulukoita ja paikallisia taulukoita, hakupuita, karttoja, jotka sisältävät viitteitä BPF-ohjelmiin ja paljon muuta. Mielenkiintoista on nyt se, että BPF-ohjelmilla on nyt kyky säilyttää tila puhelujen välillä ja jakaa se muiden ohjelmien ja käyttäjätilan kanssa.

Mapsiin päästään käyttäjäprosesseista järjestelmäkutsulla bpf(2), ja BPF-ohjelmista, jotka ovat käynnissä ytimessä aputoimintojen avulla. Lisäksi apuohjelmia ei ole vain karttojen kanssa työskentelemiseen, vaan myös muihin ytimen ominaisuuksiin. Esimerkiksi BPF-ohjelmat voivat käyttää aputoimintoja pakettien välittämiseen muihin liitäntöihin, perf-tapahtumien luomiseen, ytimen rakenteiden käyttämiseen ja niin edelleen.

BPF pienimmille, osa yksi: pidennetty BPF

Yhteenvetona voidaan todeta, että BPF tarjoaa mahdollisuuden ladata mielivaltaista, eli todentajan testaamaa käyttäjäkoodia ydintilaan. Tämä koodi voi tallentaa tilan puhelujen välillä ja vaihtaa tietoja käyttäjätilan kanssa, ja sillä on myös pääsy tämän tyyppisten ohjelmien sallimiin ytimen alijärjestelmiin.

Tämä on jo samanlainen kuin ydinmoduulien tarjoamat ominaisuudet, joihin verrattuna BPF:llä on joitain etuja (tietysti voit verrata vain samanlaisia ​​​​sovelluksia, esimerkiksi järjestelmän seurantaa - et voi kirjoittaa mielivaltaista ohjainta BPF:llä). Voit huomata alhaisemman pääsykynnyksen (jotkut BPF:ää käyttävät apuohjelmat eivät vaadi käyttäjältä ytimen ohjelmointitaitoja tai ohjelmointitaitoja yleensä), ajonaikaisen turvallisuuden (nosoita kätesi kommenteissa niille, jotka eivät rikkoneet järjestelmää kirjoittaessaan tai testausmoduulit), atomiteetti - moduulien uudelleenlatauksessa on seisokkeja, ja BPF-alijärjestelmä varmistaa, että mitään tapahtumia ei jää väliin (rehellisyyden nimissä tämä ei pidä paikkaansa kaikentyyppisissä BPF-ohjelmissa).

Tällaisten ominaisuuksien olemassaolo tekee BPF:stä universaalin työkalun ytimen laajentamiseen, mikä on vahvistettu käytännössä: BPF:ään lisätään yhä enemmän uudentyyppisiä ohjelmia, yhä useammat suuret yritykset käyttävät BPF:ää taistelupalvelimissa 24 × 7, yhä enemmän startupit rakentavat liiketoimintaansa ratkaisuille, jotka perustuvat BPF:ään. BPF:ää käytetään kaikkialla: suojautumiseen DDoS-hyökkäyksiltä, ​​SDN:n luomiseen (esimerkiksi verkkojen toteuttamiseen kuberneteille), pääjärjestelmän seurantatyökaluna ja tilastojen kerääjänä, tunkeutumisen havaitsemisjärjestelmissä ja hiekkalaatikkojärjestelmissä jne.

Lopetetaan artikkelin yleiskatsaus tähän ja tarkastellaan virtuaalikonetta ja BPF-ekosysteemiä tarkemmin.

Poikkeama: apuohjelmat

Jotta voit suorittaa seuraavien osioiden esimerkit, saatat tarvita useita apuohjelmia, ainakin llvm/clang bpf-tuella ja bpftool. Kohdassa Kehitystyökalut Voit lukea apuohjelmien kokoamisohjeet sekä ytimen. Tämä osio on sijoitettu alle, jotta se ei häiritse esityksemme harmoniaa.

BPF-virtuaalikonerekisterit ja -ohjejärjestelmä

BPF:n arkkitehtuuri ja komentojärjestelmä kehitettiin ottaen huomioon, että ohjelmat kirjoitetaan C-kielellä ja käännetään ytimeen latauksen jälkeen natiivikoodiksi. Siksi rekisterien lukumäärä ja komentosarja valittiin ottaen huomioon nykyaikaisten koneiden kykyjen risteyskohta matemaattisessa mielessä. Lisäksi ohjelmille asetettiin erilaisia ​​rajoituksia, esimerkiksi vielä viime aikoihin asti silmukoita ja aliohjelmia ei ollut mahdollista kirjoittaa, ja käskyjen määrä rajoitettiin 4096:een (nyt etuoikeutetut ohjelmat voivat ladata jopa miljoona käskyä).

BPF:ssä on yksitoista 64-bittistä rekisteriä, jotka ovat käyttäjien käytettävissä r0-r10 ja ohjelmalaskuri. Rekisteröidy r10 sisältää kehysosoittimen ja on vain luku -tilassa. Ohjelmat voivat käyttää 512-tavun pinoa ajon aikana ja rajattomasti jaettua muistia karttojen muodossa.

BPF-ohjelmien sallitaan suorittaa tietty joukko ohjelmatyyppisiä ytimen apuohjelmia ja viime aikoina tavallisia toimintoja. Kukin kutsuttu funktio voi ottaa enintään viisi argumenttia, jotka välitetään rekistereissä r1-r5, ja palautusarvo välitetään r0. Toiminnosta palaamisen jälkeen taataan rekisterien sisältö r6-r9 Ei muutu.

Tehokas ohjelman käännös, rekisterit r0-r11 Kaikki tuetut arkkitehtuurit on kartoitettu yksilöllisesti todellisiin rekistereihin ottaen huomioon nykyisen arkkitehtuurin ABI-ominaisuudet. Esimerkiksi varten x86_64 rekisterit r1-r5, joita käytetään toimintoparametrien välittämiseen, näkyvät rdi, rsi, rdx, rcx, r8, joita käytetään parametrien välittämiseen funktioille x86_64. Esimerkiksi vasemmalla oleva koodi kääntää oikeanpuoleisen koodin seuraavasti:

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

Rekisteröidy r0 käytetään myös palauttamaan ohjelman suorituksen tulos ja rekisterissä r1 ohjelmalle välitetään osoitin kontekstiin - ohjelmatyypistä riippuen tämä voi olla esimerkiksi rakenne struct xdp_md (XDP:lle) tai rakenne struct __sk_buff (eri verkko-ohjelmille) tai rakenne struct pt_regs (erityyppisille jäljitysohjelmille) jne.

Meillä oli siis joukko rekistereitä, ytimen apuohjelmia, pino, kontekstiosoitin ja jaettu muisti karttojen muodossa. Ei sillä, että tämä kaikki olisi matkalla ehdottoman välttämätöntä, mutta...

Jatketaan kuvausta ja puhutaan komentojärjestelmästä näiden objektien kanssa työskentelemiseen. Kaikki (Lähes kaikki) BPF-käskyillä on kiinteä 64-bittinen koko. Jos katsot yhtä ohjetta 64-bittisessä Big Endian -koneessa, näet

BPF pienimmille, osa yksi: pidennetty BPF

Täällä Code - tämä on ohjeen koodaus, Dst/Src ovat vastaavasti vastaanottimen ja lähteen koodauksia, Off - 16-bittinen allekirjoitettu sisennys ja Imm on 32-bittinen etumerkillinen kokonaisluku, jota käytetään joissakin käskyissä (samanlainen kuin cBPF-vakio K). Koodaus Code on yksi kahdesta tyypistä:

BPF pienimmille, osa yksi: pidennetty BPF

Ohjeluokat 0, 1, 2, 3 määrittelevät komennot muistin käyttöä varten. Ne kutsutaan, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, vastaavasti. Luokat 4, 7 (BPF_ALU, BPF_ALU64) muodostavat joukon ALU-käskyjä. Luokat 5, 6 (BPF_JMP, BPF_JMP32) sisältävät hyppyohjeet.

BPF-käskyjärjestelmän opiskelun jatkosuunnitelma on seuraava: sen sijaan, että luettelemme huolellisesti kaikki ohjeet ja niiden parametrit, katsomme tässä osiossa paria esimerkkiä, joista selviää, miten ohjeet käytännössä toimivat ja miten pura manuaalisesti kaikki BPF:n binaaritiedostot. Aineiston konsolidoimiseksi myöhemmin artikkelissa tapaamme myös yksittäisiä ohjeita osioissa Verifier, JIT-kääntäjä, klassisen BPF:n käännös sekä tutkittaessa karttoja, kutsuttaessa toimintoja jne.

Kun puhumme yksittäisistä ohjeista, viittaamme ydintiedostoihin bpf.h и bpf_common.h, jotka määrittelevät BPF-käskyjen numerokoodit. Kun opiskelet arkkitehtuuria itsenäisesti ja/tai jäsentät binaaritiedostoja, löydät semantiikan seuraavista lähteistä, jotka on lajiteltu monimutkaisuuden mukaan: Epävirallinen eBPF-spesif, BPF- ja XDP-viiteopas, ohjesarja, Documentation/Networking/filter.txt ja tietysti Linux-lähdekoodissa - todentaja, JIT, BPF-tulkki.

Esimerkki: BPF:n purkaminen päässäsi

Katsotaanpa esimerkkiä, jossa käännämme ohjelman readelf-example.c ja katso tuloksena olevaa binaaria. Paljastamme alkuperäisen sisällön readelf-example.c alla, kun olemme palauttaneet sen logiikan binäärikoodeista:

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

Tulosteen ensimmäinen sarake readelf on sisennys, joten ohjelmamme koostuu neljästä komennosta:

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

Komentokoodit ovat samat b7, 15, b7 и 95. Muista, että vähiten merkitseviä kolme bittiä ovat käskyluokka. Meidän tapauksessamme kaikkien käskyjen neljäs bitti on tyhjä, joten käskyluokat ovat vastaavasti 7, 5, 7, 5. Luokka 7 on BPF_ALU64, ja 5 on BPF_JMP. Molempien luokkien ohjemuoto on sama (katso yllä) ja voimme kirjoittaa ohjelmamme uudelleen näin (samalla kirjoitamme loput sarakkeet ihmismuodossa):

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

Toiminta b luokka ALU64 - Onko BPF_MOV. Se antaa arvon kohderekisterille. Jos bitti on asetettu s (lähde), niin arvo otetaan lähderekisteristä, ja jos, kuten meidän tapauksessamme, sitä ei ole asetettu, niin arvo otetaan kentästä Imm. Joten ensimmäisessä ja kolmannessa ohjeessa suoritamme toimenpiteen r0 = Imm. Lisäksi JMP luokan 1 toiminta on BPF_JEQ (hyppää, jos yhtä suuri). Meidän tapauksessamme siitä lähtien S on nolla, se vertaa lähderekisterin arvoa kenttään Imm. Jos arvot ovat samat, siirtyminen tapahtuu PC + OffMissä PC, kuten tavallista, sisältää seuraavan käskyn osoitteen. Lopuksi, JMP Class 9 Operation on BPF_EXIT. Tämä käsky lopettaa ohjelman ja palaa ytimeen r0. Lisätään uusi sarake taulukkoomme:

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

Voimme kirjoittaa tämän uudelleen kätevämpään muotoon:

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

Jos muistamme mitä rekisterissä on r1 ohjelma välittää osoittimen kontekstiin ytimestä ja rekisteristä r0 arvo palautetaan ytimeen, niin voimme nähdä, että jos kontekstin osoitin on nolla, niin palautetaan 1 ja muuten - 2. Tarkistamme, että olemme oikeassa katsomalla lähdettä:

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

Kyllä, se on merkityksetön ohjelma, mutta se on vain neljä yksinkertaista ohjetta.

Poikkeusesimerkki: 16-tavuinen ohje

Mainitsimme aiemmin, että jotkut ohjeet vievät enemmän kuin 64 bittiä. Tämä koskee esimerkiksi ohjeita lddw (Koodi = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — lataa kaksoissana kentistä rekisteriin Imm. Tosiasia on se Imm on kooltaan 32 ja kaksoissana on 64 bittiä, joten 64-bittisen välittömän arvon lataaminen rekisteriin yhdessä 64-bittisessä käskyssä ei toimi. Tätä varten käytetään kahta vierekkäistä käskyä tallentamaan 64-bittisen arvon toinen osa kenttään Imm. Esimerkki:

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

Binääriohjelmassa on vain kaksi ohjetta:

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

Tapaamme uudelleen ohjeiden kera lddw, kun puhumme muutoksista ja karttojen kanssa työskentelystä.

Esimerkki: BPF:n purkaminen vakiotyökaluilla

Joten olemme oppineet lukemaan BPF-binäärikoodeja ja olemme valmiita jäsentämään kaikki ohjeet tarvittaessa. On kuitenkin syytä sanoa, että käytännössä on kätevämpää ja nopeampaa purkaa ohjelmia vakiotyökaluilla, esimerkiksi:

$ 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-objektien elinkaari, bpffs-tiedostojärjestelmä

(Opin ensin joitakin tässä alajaksossa kuvatuista yksityiskohdista lähettää Aleksei Starovoitov mukana BPF blogi.)

BPF-objektit - ohjelmat ja kartat - luodaan käyttäjätilasta komentojen avulla BPF_PROG_LOAD и BPF_MAP_CREATE järjestelmäpuhelu bpf(2), puhumme siitä, miten tämä tapahtuu seuraavassa osiossa. Tämä luo ytimen tietorakenteet ja jokaiselle niistä refcount (viitemäärä) asetetaan arvoon yksi, ja objektiin osoittava tiedostokuvaaja palautetaan käyttäjälle. Kun kahva on suljettu refcount kohde pienennetään yhdellä, ja kun se saavuttaa nollan, esine tuhoutuu.

Jos ohjelma käyttää karttoja, niin refcount näitä karttoja suurennetaan yhdellä ohjelman lataamisen jälkeen, ts. niiden tiedostokuvaajat voidaan sulkea käyttäjäprosessista ja silti refcount ei tule nollaksi:

BPF pienimmille, osa yksi: pidennetty BPF

Kun ohjelma on ladattu onnistuneesti, liitämme sen yleensä johonkin tapahtumageneraattoriin. Voimme esimerkiksi laittaa sen verkkoliittymään käsittelemään saapuvia paketteja tai yhdistämään sen joihinkin tracepoint ytimessä. Tässä vaiheessa myös referenssilaskuri kasvaa yhdellä ja voimme sulkea tiedostokuvaajan latausohjelmassa.

Mitä tapahtuu, jos nyt suljemme käynnistyslataimen? Se riippuu tapahtumageneraattorin (koukun) tyypistä. Kaikki verkkokoukut ovat olemassa, kun lataus on valmis, nämä ovat niin sanottuja globaaleja koukkuja. Ja esimerkiksi jäljitysohjelmat julkaistaan ​​sen jälkeen, kun ne luonut prosessi päättyy (ja siksi niitä kutsutaan paikallisiksi, "paikallisesta prosessiin"). Teknisesti paikallisilla koukuilla on aina vastaava tiedostokuvaaja käyttäjätilassa, ja siksi ne sulkeutuvat, kun prosessi suljetaan, mutta globaaleilla koukuilla ei ole. Seuraavassa kuvassa yritän punaisten ristien avulla näyttää kuinka latausohjelman lopettaminen vaikuttaa objektien elinikään paikallisten ja globaalien koukkujen tapauksessa.

BPF pienimmille, osa yksi: pidennetty BPF

Miksi paikallisten ja globaalien koukkujen välillä on ero? Tietyntyyppisten verkkoohjelmien ajaminen on järkevää ilman käyttäjätilaa, esimerkiksi kuvittele DDoS-suojaus - käynnistyslatain kirjoittaa säännöt ja yhdistää BPF-ohjelman verkkoliitäntään, minkä jälkeen käynnistyslatain voi mennä tappamaan itsensä. Toisaalta kuvittele vianetsintäjäljitysohjelma, jonka kirjoitit polvillesi kymmenessä minuutissa - kun se on valmis, haluat, että järjestelmään ei jää roskaa, ja paikalliset koukut varmistavat sen.

Toisaalta kuvittele, että haluat muodostaa yhteyden ytimen jäljityspisteeseen ja kerätä tilastoja useiden vuosien ajalta. Tässä tapauksessa kannattaa täydentää käyttäjäosio ja palata tilastoihin aika ajoin. Bpf-tiedostojärjestelmä tarjoaa tämän mahdollisuuden. Se on vain muistissa oleva pseudotiedostojärjestelmä, joka mahdollistaa tiedostojen luomisen, jotka viittaavat BPF-objekteihin ja siten lisäävät refcount esineitä. Tämän jälkeen lataaja voi poistua ja sen luomat objektit pysyvät hengissä.

BPF pienimmille, osa yksi: pidennetty BPF

BPF-objekteihin viittaavien tiedostojen luomista bpff-tiedostoissa kutsutaan "kiinnittämiseksi" (kuten seuraavassa lauseessa: "prosessi voi kiinnittää BPF-ohjelman tai -kartan"). Tiedostoobjektien luominen BPF-objekteille on järkevää paitsi paikallisten objektien käyttöiän pidentämisen, myös globaalien objektien käytettävyyden kannalta - palataksemme esimerkkiin globaalin DDoS-suojausohjelman kanssa, haluamme päästä katsomaan tilastoja. ajoittain.

BPF-tiedostojärjestelmä on yleensä asennettu /sys/fs/bpf, mutta se voidaan asentaa myös paikallisesti, esimerkiksi näin:

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

Tiedostojärjestelmän nimet luodaan komennolla BPF_OBJ_PIN BPF-järjestelmäkutsu. Otetaan havainnollistamiseksi ohjelma, käännetään se, ladataan ja kiinnitetään bpffs. Ohjelmamme ei tee mitään hyödyllistä, esitämme vain koodin, jotta voit toistaa esimerkin:

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

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

Käännetään tämä ohjelma ja luodaan paikallinen kopio tiedostojärjestelmästä bpffs:

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

Lataa nyt ohjelmamme apuohjelman avulla bpftool ja katso mukana tulevat järjestelmäkutsut bpf(2) (joitakin epäolennaisia ​​rivejä poistettu strace-tulostuksesta):

$ 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

Täällä olemme ladaneet ohjelman käyttämällä BPF_PROG_LOAD, sai tiedostokuvaajan ytimestä 3 ja käyttämällä komentoa BPF_OBJ_PIN kiinnitti tämän tiedostokuvaajan tiedostoksi "bpf-mountpoint/test". Tämän jälkeen käynnistyslatausohjelma bpftool valmis toimimaan, mutta ohjelmamme pysyi ytimessä, vaikka emme liittäneet sitä mihinkään verkkoliitäntään:

$ 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

Voimme poistaa tiedostoobjektin normaalisti unlink(2) ja sen jälkeen vastaava ohjelma poistetaan:

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

Objektien poistaminen

Objektien poistamisesta on syytä selventää, että kun olemme irrottanut ohjelman koukusta (tapahtumageneraattori), yksikään uusi tapahtuma ei laukaise sen käynnistystä, mutta kaikki nykyiset ohjelman esiintymät valmistuvat normaalissa järjestyksessä .

Tietyntyyppisten BPF-ohjelmien avulla voit vaihtaa ohjelman lennossa, ts. tarjoavat sekvenssin atomisuuden replace = detach old program, attach new program. Tässä tapauksessa kaikki ohjelman vanhan version aktiiviset esiintymät lopettavat työnsä ja uudesta ohjelmasta luodaan uudet tapahtumakäsittelijät, ja "atomicity" tarkoittaa tässä sitä, että yksikään tapahtuma ei jää väliin.

Ohjelman liittäminen tapahtumalähteisiin

Tässä artikkelissa emme kuvaile erikseen ohjelmien yhdistämistä tapahtumalähteisiin, koska tätä on järkevää tutkia tietyntyyppisen ohjelman yhteydessä. cm. esimerkki alla, jossa näytämme, kuinka XDP:n kaltaiset ohjelmat yhdistetään.

Objektien käsittely bpf-järjestelmäkutsulla

BPF-ohjelmat

Kaikki BPF-objektit luodaan ja hallitaan käyttäjätilasta järjestelmäkutsun avulla bpf, jolla on seuraava prototyyppi:

#include <linux/bpf.h>

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

Tässä on joukkue cmd on yksi tyypin arvoista enum bpf_cmd, attr — osoitin tietyn ohjelman parametreihin ja size — kohteen koko osoittimen mukaan, ts. yleensä tämä sizeof(*attr). Ytimessä 5.8 järjestelmäkutsu bpf tukee 34 eri komentoa ja määritelmä union bpf_attr vie 200 riviä. Mutta meidän ei pitäisi pelästyttää tätä, koska tulemme tutustumaan komentoihin ja parametreihin useiden artikkeleiden aikana.

Aloitetaan joukkueesta BPF_PROG_LOAD, joka luo BPF-ohjelmia - ottaa joukon BPF-käskyjä ja lataa ne ytimeen. Lataushetkellä todentaja käynnistetään ja sitten JIT-kääntäjä ja onnistuneen suorituksen jälkeen ohjelmatiedostokuvaaja palautetaan käyttäjälle. Näimme, mitä hänelle tapahtuu seuraavaksi edellisessä osiossa BPF-objektien elinkaaresta.

Kirjoitamme nyt mukautetun ohjelman, joka lataa yksinkertaisen BPF-ohjelman, mutta ensin meidän on päätettävä, millaisen ohjelman haluamme ladata - meidän on valittava tyyppi ja kirjoita tämän tyypin puitteissa ohjelma, joka läpäisee todentajatestin. Jotta prosessia ei kuitenkaan monimutkaistaisi, tässä on valmis ratkaisu: otamme ohjelman, kuten BPF_PROG_TYPE_XDP, joka palauttaa arvon XDP_PASS (ohita kaikki paketit). BPF-asentajassa se näyttää hyvin yksinkertaiselta:

r0 = 2
exit

Sen jälkeen kun olemme päättäneet että lataamme, voimme kertoa kuinka teemme sen:

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

Ohjelman mielenkiintoiset tapahtumat alkavat taulukon määrittelyllä insns - BPF-ohjelmamme konekoodissa. Tässä tapauksessa jokainen BPF-ohjelman käsky pakataan rakenteeseen bpf_insn. Ensimmäinen elementti insns noudattaa ohjeita r0 = 2, toinen - exit.

Vetäytyä. Ydin määrittelee kätevämpiä makroja konekoodien kirjoittamiseen ja ytimen otsikkotiedoston käyttämiseen tools/include/linux/filter.h voisimme kirjoittaa

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

Mutta koska BPF-ohjelmien kirjoittaminen alkuperäisessä koodissa on välttämätöntä vain testien kirjoittamiseen ytimeen ja BPF-artikkeleihin, näiden makrojen puuttuminen ei todellakaan vaikeuta kehittäjän elämää.

Kun olet määrittänyt BPF-ohjelman, siirrymme sen lataamiseen ytimeen. Minimalistinen parametrijoukkomme attr sisältää ohjelmatyypin, ohjeiden sarjan ja määrän, vaaditun lisenssin ja nimen "woo", jota käytämme ohjelman löytämiseen järjestelmästä lataamisen jälkeen. Ohjelma, kuten luvattiin, ladataan järjestelmään järjestelmäkutsulla bpf.

Ohjelman lopussa päädymme äärettömään silmukkaan, joka simuloi hyötykuormaa. Ilman sitä ydin tappaa ohjelman, kun järjestelmäkutsun meille palauttama tiedostokuvaaja suljetaan bpf, emmekä näe sitä järjestelmässä.

No, olemme valmiita testaukseen. Kootaan ja ajetaan alla oleva ohjelma stracetarkistaaksesi, että kaikki toimii niin kuin pitää:

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

Kaikki on hyvin, bpf(2) palautti kahvan 3 meille ja menimme äärettömään silmukkaan pause(). Yritetään löytää ohjelmamme järjestelmästä. Tätä varten siirrymme toiseen terminaaliin ja käytämme apuohjelmaa bpftool:

# bpftool prog | grep -A3 woo
390: xdp  name woo  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-31T24:66:44+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        pids simple-prog(10381)

Näemme, että järjestelmässä on ladattu ohjelma woo jonka globaali tunnus on 390 ja se on parhaillaan käynnissä simple-prog siellä on avoin tiedostokuvaaja, joka osoittaa ohjelmaan (ja jos simple-prog sitten lopettaa työn woo katoaa). Kuten odotettiin, ohjelma woo vie 16 tavua - kaksi käskyä - binäärikoodeja BPF-arkkitehtuurissa, mutta alkuperäisessä muodossaan (x86_64) se on jo 40 tavua. Katsotaanpa ohjelmaamme alkuperäisessä muodossaan:

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

ei yllätyksiä. Katsotaanpa nyt JIT-kääntäjän luomaa koodia:

# 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

ei kovin tehokas exit(2), mutta rehellisesti sanottuna, ohjelmamme on liian yksinkertainen, ja ei-triviaalisille ohjelmille tarvitaan tietysti JIT-kääntäjän lisäämä prologi ja epilogi.

Kartat

BPF-ohjelmat voivat käyttää strukturoituja muistialueita, jotka ovat sekä muiden BPF-ohjelmien että käyttäjätilassa olevien ohjelmien käytettävissä. Näitä objekteja kutsutaan kartoiksi, ja tässä osiossa näytämme, kuinka niitä voidaan käsitellä järjestelmäkutsulla bpf.

Sanotaan heti, että karttojen ominaisuudet eivät rajoitu vain pääsyyn jaettuun muistiin. On erikoiskarttoja, jotka sisältävät esimerkiksi osoittimia BPF-ohjelmiin tai osoittimia verkkoliitäntöihin, karttoja työskentelyyn perf-tapahtumien kanssa jne. Emme puhu niistä täällä, jotta emme hämmennä lukijaa. Tämän lisäksi jätämme huomiotta synkronointiongelmat, koska tämä ei ole tärkeää esimerkkiemme kannalta. Täydellinen luettelo käytettävissä olevista karttatyypeistä löytyy osoitteesta <linux/bpf.h>, ja tässä osiossa otamme esimerkkinä historiallisesti ensimmäisen tyypin, hash-taulukon BPF_MAP_TYPE_HASH.

Jos luot hash-taulukon esimerkiksi C++:ssa, sanoisit unordered_map<int,long> woo, joka venäjäksi tarkoittaa "Tarvitsen pöydän woo rajoittamaton koko, jonka avaimet ovat tyyppisiä int, ja arvot ovat tyyppiä long" BPF-hajautustaulukon luomiseksi meidän on tehtävä pitkälti sama asia, paitsi että meidän on määritettävä taulukon enimmäiskoko, ja avainten ja arvojen tyyppien määrittämisen sijaan meidän on määritettävä niiden koot tavuina . Luo karttoja käyttämällä komentoa BPF_MAP_CREATE järjestelmäpuhelu bpf. Katsotaanpa enemmän tai vähemmän minimaalista ohjelmaa, joka luo kartan. Edellisen BPF-ohjelmia lataavan ohjelman jälkeen tämän pitäisi näyttää sinulle yksinkertaiselta:

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

Tässä määrittelemme joukon parametreja attr, jossa sanomme "Tarvitsen hash-taulukon avaimilla ja kokoarvoilla sizeof(int), johon voin laittaa enintään neljä elementtiä." Kun luot BPF-karttoja, voit määrittää muita parametreja, esimerkiksi samalla tavalla kuin ohjelman esimerkissä, määritimme kohteen nimeksi "woo".

Käännetään ja ajetaan ohjelma:

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

Tässä on järjestelmäkutsu bpf(2) palautti meille kuvaajakartan numeron 3 ja sitten ohjelma odotetusti odottaa lisäohjeita järjestelmäkutsussa pause(2).

Lähetetään nyt ohjelmamme taustalle tai avataan toinen pääte ja katsotaan objektiamme apuohjelman avulla bpftool (Voimme erottaa karttamme muista sen nimen perusteella):

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

Numero 114 on objektimme yleinen tunnus. Mikä tahansa järjestelmän ohjelma voi käyttää tätä tunnusta avatakseen olemassa olevan kartan komennolla BPF_MAP_GET_FD_BY_ID järjestelmäpuhelu bpf.

Nyt voimme pelata hash-taulukollamme. Katsotaanpa sen sisältöä:

$ sudo bpftool map dump id 114
Found 0 elements

Tyhjä. Laitetaan sille arvo hash[1] = 1:

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

Katsotaanpa vielä taulukkoa:

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

Hurraa! Onnistuimme lisäämään yhden elementin. Huomaa, että meidän on työskenneltävä tavutasolla tehdäksemme tämän, koska bptftool ei tiedä minkä tyyppisiä arvot hash-taulukossa ovat. (Tämä tieto voidaan siirtää hänelle BTF:n avulla, mutta siitä lisää nyt.)

Kuinka tarkalleen bpftool lukee ja lisää elementtejä? Katsotaanpa konepellin alle:

$ 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

Ensin avasimme kartan sen globaalin tunnuksen mukaan komennolla BPF_MAP_GET_FD_BY_ID и bpf(2) palautti meille kuvaajan 3. Käytä edelleen komentoa BPF_MAP_GET_NEXT_KEY löysimme taulukon ensimmäisen avaimen ohittamalla NULL osoittimena "edelliselle" -näppäimelle. Jos meillä on avain, voimme tehdä sen BPF_MAP_LOOKUP_ELEMjoka palauttaa arvon osoittimelle value. Seuraava vaihe on, että yritämme löytää seuraavan elementin välittämällä osoittimen nykyiseen avaimeen, mutta taulukkomme sisältää vain yhden elementin ja komennon BPF_MAP_GET_NEXT_KEY palaa ENOENT.

Okei, muutetaan arvoa avaimella 1, oletetaan, että liiketoimintalogiikkamme edellyttää rekisteröitymistä 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

Kuten odotettiin, se on hyvin yksinkertainen: komento BPF_MAP_GET_FD_BY_ID avaa karttamme tunnuksella ja komennolla BPF_MAP_UPDATE_ELEM korvaa elementin.

Joten, kun olemme luoneet hajautustaulukon yhdestä ohjelmasta, voimme lukea ja kirjoittaa sen sisällön toisesta ohjelmasta. Huomaa, että jos pystyimme tekemään tämän komentoriviltä, ​​niin mikä tahansa muu järjestelmän ohjelma voi tehdä sen. Yllä kuvattujen komentojen lisäksi käyttäjätilan karttojen käsittelyyn seuraavaa:

  • BPF_MAP_LOOKUP_ELEM: etsi arvo avaimella
  • BPF_MAP_UPDATE_ELEM: päivitä/luo arvo
  • BPF_MAP_DELETE_ELEM: poista avain
  • BPF_MAP_GET_NEXT_KEY: etsi seuraava (tai ensimmäinen) avain
  • BPF_MAP_GET_NEXT_ID: voit käydä läpi kaikki olemassa olevat kartat, niin se toimii bpftool map
  • BPF_MAP_GET_FD_BY_ID: avaa olemassa oleva kartta sen yleisellä tunnuksella
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: päivittää atomisesti objektin arvoa ja palauttaa vanhan
  • BPF_MAP_FREEZE: tee kartasta muuttumaton käyttäjätilasta (tätä toimintoa ei voi kumota)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: massaoperaatiot. Esimerkiksi, BPF_MAP_LOOKUP_AND_DELETE_BATCH - Tämä on ainoa luotettava tapa lukea ja nollata kaikki arvot kartasta

Kaikki nämä komennot eivät toimi kaikille karttatyypeille, mutta yleensä muun tyyppisten karttojen kanssa työskentely käyttäjätilasta näyttää täsmälleen samalta kuin työskentely hash-taulukoiden kanssa.

Tehdään järjestyksen vuoksi tiivistetaulukkokokeilumme loppuun. Muistatko, että loimme taulukon, joka voi sisältää enintään neljä avainta? Lisätään vielä muutama elementti:

$ 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

Toistaiseksi hyvin:

$ 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

Yritetään lisätä vielä yksi:

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

Kuten odotettiin, emme onnistuneet. Katsotaanpa virhettä tarkemmin:

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

Kaikki on hyvin: kuten odotettiin, joukkue BPF_MAP_UPDATE_ELEM yrittää luoda uuden viidennen avaimen, mutta kaatuu E2BIG.

Voimme siis luoda ja ladata BPF-ohjelmia sekä luoda ja hallita karttoja käyttäjätilasta. Nyt on loogista tarkastella, kuinka voimme käyttää karttoja itse BPF-ohjelmista. Tästä voisi puhua vaikealukuisten ohjelmien kielellä konemakrokoodeissa, mutta itse asiassa on tullut aika näyttää, kuinka BPF-ohjelmia todellisuudessa kirjoitetaan ja ylläpidetään - käyttämällä libbpf.

(Lukijoille, jotka ovat tyytymättömiä matalan tason esimerkin puutteeseen: analysoimme yksityiskohtaisesti ohjelmia, jotka käyttävät karttoja ja aputoimintoja, jotka on luotu libbpf ja kertoa, mitä tapahtuu opetustasolla. Tyytymättömille lukijoille todella paljon, lisäsimme esimerkki artikkelin sopivaan kohtaan.)

BPF-ohjelmien kirjoittaminen libbpf:llä

BPF-ohjelmien kirjoittaminen konekoodeilla voi olla mielenkiintoista vasta ensimmäisellä kerralla, ja sitten tulee kylläisyys. Tällä hetkellä sinun on kiinnitettävä huomiosi llvm, jossa on taustaohjelma koodin luomiseen BPF-arkkitehtuurille sekä kirjasto libbpf, jonka avulla voit kirjoittaa BPF-sovellusten käyttäjäpuolelle ja ladata käyttämällä luotujen BPF-ohjelmien koodia llvm/clang.

Itse asiassa, kuten näemme tässä ja myöhemmissä artikkeleissa, libbpf tekee melko paljon työtä ilman sitä (tai vastaavia työkaluja - iproute2, libbcc, libbpf-gojne.) on mahdotonta elää. Yksi hankkeen tappavista piirteistä libbpf on BPF CO-RE (Compile Once, Run Everywhere) - projekti, jonka avulla voit kirjoittaa BPF-ohjelmia, jotka ovat siirrettävissä ytimestä toiseen ja jotka voivat toimia eri API:illa (esimerkiksi kun ytimen rakenne muuttuu versiosta). versioon). Jotta voisit työskennellä CO-RE:n kanssa, ytimen on oltava käännetty BTF-tuella (kuvaamme kuinka tämä tehdään osiossa Kehitystyökalut. Voit tarkistaa, onko ytimesi rakennettu BTF:llä vai ei kovin yksinkertaisesti - seuraavan tiedoston avulla:

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

Tämä tiedosto tallentaa tiedot kaikista ytimessä käytetyistä tietotyypeistä ja sitä käytetään kaikissa esimerkeissämme libbpf. Puhumme yksityiskohtaisesti CO-RE:stä seuraavassa artikkelissa, mutta tässä - rakenna itsellesi ydin CONFIG_DEBUG_INFO_BTF.

kirjasto libbpf asuu suoraan hakemistossa tools/lib/bpf ydin ja sen kehittäminen tapahtuu postituslistan kautta [email protected]. Ytimen ulkopuolella asuvien sovellusten tarpeita varten ylläpidetään kuitenkin erillistä arkistoa https://github.com/libbpf/libbpf jossa ytimen kirjasto peilataan lukuoikeuksia varten suunnilleen sellaisenaan.

Tässä osiossa tarkastellaan, kuinka voit luoda projektin, joka käyttää libbpf, kirjoitetaan useita (enemmän tai vähemmän merkityksettömiä) testiohjelmia ja analysoidaan yksityiskohtaisesti, kuinka se kaikki toimii. Tämän avulla voimme helpommin selittää seuraavissa osissa tarkalleen kuinka BPF-ohjelmat ovat vuorovaikutuksessa karttojen, ytimen apuohjelmien, BTF:n jne. kanssa.

Tyypillisesti projekteissa käytetään libbpf lisää GitHub-arkisto git-alimoduuliksi, teemme samoin:

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

Menossa libbpf hyvin yksinkertainen:

$ 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

Seuraava suunnitelmamme tässä osiossa on seuraava: kirjoitamme BPF-ohjelman kaltaisen BPF_PROG_TYPE_XDP, sama kuin edellisessä esimerkissä, mutta C:ssä käännämme sen käyttämällä clang, ja kirjoita apuohjelma, joka lataa sen ytimeen. Seuraavissa osioissa laajennamme sekä BPF-ohjelman että avustajaohjelman ominaisuuksia.

Esimerkki: täysimittaisen sovelluksen luominen libbpf:llä

Aluksi käytämme tiedostoa /sys/kernel/btf/vmlinux, joka mainittiin yllä, ja luo sen vastine otsikkotiedoston muodossa:

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

Tämä tiedosto tallentaa kaikki ytimessämme saatavilla olevat tietorakenteet, esimerkiksi näin IPv4-otsikko määritellään ytimessä:

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

Nyt kirjoitamme BPF-ohjelmamme C:llä:

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

Vaikka ohjelmamme osoittautui hyvin yksinkertaiseksi, meidän on silti kiinnitettävä huomiota moniin yksityiskohtiin. Ensinnäkin ensimmäinen sisällyttämämme otsikkotiedosto on vmlinux.h, jonka loimme juuri käyttämällä bpftool btf dump - Nyt meidän ei tarvitse asentaa kernel-headers-pakettia selvittääksemme, miltä ytimen rakenteet näyttävät. Seuraava otsikkotiedosto tulee meille kirjastosta libbpf. Nyt tarvitsemme sitä vain makron määrittelemiseen SEC, joka lähettää merkin ELF-objektitiedoston sopivaan osaan. Ohjelmamme löytyy osiosta xdp/simple, jossa ennen kauttaviivaa määritämme ohjelmatyypin BPF - tämä on käytäntö, jota käytetään libbpf, osion nimen perusteella se korvaa oikean tyypin käynnistyksen yhteydessä bpf(2). Itse BPF-ohjelma on C - erittäin yksinkertainen ja koostuu yhdestä rivistä return XDP_PASS. Lopuksi erillinen jakso "license" sisältää lisenssin nimen.

Voimme kääntää ohjelmamme käyttämällä komennolla llvm/clang, versio >= 10.0.0 tai vielä parempi, uudempi (katso kohta Kehitystyökalut):

$ 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

Mielenkiintoisten ominaisuuksien joukossa: osoitamme kohdearkkitehtuurin -target bpf ja polku otsikoihin libbpf, jonka asensimme äskettäin. Älä myöskään unohda -O2, ilman tätä vaihtoehtoa saatat joutua odottamaan yllätyksiä tulevaisuudessa. Katsotaanpa koodiamme, onnistuimmeko kirjoittamaan haluamamme ohjelman?

$ 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

Kyllä, se toimi! Nyt meillä on binääritiedosto ohjelman kanssa, ja haluamme luoda sovelluksen, joka lataa sen ytimeen. Tätä tarkoitusta varten kirjasto libbpf tarjoaa meille kaksi vaihtoehtoa - käytä alemman tason API:ta tai korkeamman tason APIa. Menemme toiseen suuntaan, koska haluamme oppia kirjoittamaan, lataamaan ja yhdistämään BPF-ohjelmia vähäisellä vaivalla myöhempää tutkimusta varten.

Ensinnäkin meidän on luotava ohjelmamme "luuranko" sen binääritiedostosta käyttämällä samaa apuohjelmaa bpftool — BPF-maailman sveitsiläinen veitsi (joka voidaan ottaa kirjaimellisesti, koska Daniel Borkman, yksi BPF:n luojista ja ylläpitäjistä, on sveitsiläinen):

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

Tiedostossa xdp-simple.skel.h sisältää ohjelmamme binäärikoodin ja toiminnot kohteen hallintaan - lataamiseen, liittämiseen, poistamiseen. Yksinkertaisessa tapauksessamme tämä näyttää ylilyönniltä, ​​mutta se toimii myös siinä tapauksessa, että objektitiedosto sisältää monia BPF-ohjelmia ja karttoja ja tämän jättimäisen ELF:n lataamiseksi meidän tarvitsee vain luoda luuranko ja kutsua yksi tai kaksi funktiota mukautetusta sovelluksesta. kirjoittavat Jatketaan nyt.

Tarkkaan ottaen latausohjelmamme on triviaali:

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

Täällä struct xdp_simple_bpf määritelty tiedostossa xdp-simple.skel.h ja kuvaa objektitiedostomme:

struct xdp_simple_bpf {
    struct bpf_object_skeleton *skeleton;
    struct bpf_object *obj;
    struct {
        struct bpf_program *simple;
    } progs;
    struct {
        struct bpf_link *simple;
    } links;
};

Näemme jäljet ​​matalan tason API:sta täällä: rakenne struct bpf_program *simple и struct bpf_link *simple. Ensimmäinen rakenne kuvaa erityisesti ohjelmaamme, joka on kirjoitettu osiossa xdp/simple, ja toinen kuvaa, kuinka ohjelma muodostaa yhteyden tapahtumalähteeseen.

Toiminto xdp_simple_bpf__open_and_load, avaa ELF-objektin, jäsentää sen, luo kaikki rakenteet ja alirakenteet (ohjelman lisäksi ELF sisältää myös muita osioita - data, vain luku -tiedot, virheenkorjaustiedot, lisenssi jne.) ja lataa sen sitten ytimeen järjestelmän avulla. puhelu bpf, jonka voimme tarkistaa kääntämällä ja suorittamalla ohjelman:

$ 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

Katsotaan nyt ohjelmaamme käyttämällä bpftool. Etsitään hänen henkilöllisyystodistuksensa:

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

ja dump (käytämme komennon lyhennettyä muotoa bpftool prog dump xlated):

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

Jotain uutta! Ohjelma tulosti osia C-lähdetiedostostamme. Tämän teki kirjasto libbpf, joka löysi debug-osion binaarista, käänsi sen BTF-objektiksi, latasi sen ytimeen käyttämällä BPF_BTF_LOAD, ja määritti sitten tuloksena olevan tiedostokuvaajan, kun ohjelma ladataan komennolla BPG_PROG_LOAD.

Ytimen avustajat

BPF-ohjelmat voivat suorittaa "ulkoisia" toimintoja - ytimen apuohjelmia. Näiden aputoimintojen avulla BPF-ohjelmat voivat käyttää ytimen rakenteita, hallita karttoja ja myös kommunikoida "todellisen maailman" kanssa - luoda täydellisiä tapahtumia, ohjata laitteistoa (esimerkiksi uudelleenohjauspaketteja) jne.

Esimerkki: bpf_get_smp_processor_id

Tarkastellaan "esimerkillä oppimisen" paradigman puitteissa yhtä aputoimintoa, bpf_get_smp_processor_id(), tietty tiedostossa kernel/bpf/helpers.c. Se palauttaa sen prosessorin numeron, jossa sitä kutsunut BPF-ohjelma on käynnissä. Mutta emme ole niin kiinnostuneita sen semantiikasta kuin siitä, että sen toteutus kestää yhden rivin:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF-apufunktion määritelmät ovat samanlaisia ​​kuin Linuxin järjestelmäkutsujen määritelmät. Tässä esimerkiksi määritellään funktio, jolla ei ole argumentteja. (Funktion, joka ottaa esimerkiksi kolme argumenttia, määritellään makron avulla BPF_CALL_3. Argumenttien enimmäismäärä on viisi.) Tämä on kuitenkin vain määritelmän ensimmäinen osa. Toinen osa on määrittää tyyppirakenne struct bpf_func_proto, joka sisältää kuvauksen aputoiminnosta, jonka todentaja ymmärtää:

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

Aputoimintojen rekisteröinti

Jotta tietyn tyyppiset BPF-ohjelmat voisivat käyttää tätä toimintoa, niiden on rekisteröitävä se esimerkiksi tyypille BPF_PROG_TYPE_XDP funktio on määritelty ytimessä xdp_func_proto, joka määrittää aputoiminnon tunnuksen perusteella, tukeeko XDP tätä toimintoa vai ei. Tehtävämme on tukee:

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

Uudet BPF-ohjelmatyypit "määritetään" tiedostoon include/linux/bpf_types.h käyttämällä makroa BPF_PROG_TYPE. Määritelty lainausmerkeissä, koska se on looginen määritelmä, ja C-kielen termeillä kokonaisen joukon konkreettisia rakenteita määritelmä esiintyy muualla. Erityisesti tiedostossa kernel/bpf/verifier.c kaikki määritelmät tiedostosta bpf_types.h käytetään luomaan joukko rakenteita 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
};

Toisin sanoen jokaiselle BPF-ohjelmatyypille määritetään osoitin tämän tyyppiseen tietorakenteeseen struct bpf_verifier_ops, joka alustetaan arvolla _name ## _verifier_ops, eli xdp_verifier_ops varten xdp. Rakenne xdp_verifier_ops määritetty tiedostossa net/core/filter.c seuraavasti:

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

Tässä näemme tutun toimintomme xdp_func_proto, joka suorittaa todentajan aina, kun se kohtaa haasteen jonkinlainen toimii BPF-ohjelman sisällä, katso verifier.c.

Katsotaanpa, kuinka hypoteettinen BPF-ohjelma käyttää funktiota bpf_get_smp_processor_id. Tätä varten kirjoitamme ohjelman uudelleen edellisestä osiostamme seuraavasti:

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

symboli bpf_get_smp_processor_id määritetty в <bpf/bpf_helper_defs.h> kirjasto libbpf как

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

se on bpf_get_smp_processor_id on funktioosoitin, jonka arvo on 8, jossa 8 on arvo BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, joka on määritelty meille tiedostossa vmlinux.h (tiedosto bpf_helper_defs.h ytimen generoi komentosarja, joten "maagiset" numerot ovat ok). Tämä funktio ei ota argumentteja ja palauttaa tyypin arvon __u32. Kun suoritamme sen ohjelmassamme, clang luo ohjeen BPF_CALL "oikea laji" Käännetään ohjelma ja katsotaan osiota 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

Ensimmäisellä rivillä näemme ohjeet call, parametri IMM joka on yhtä suuri kuin 8, ja SRC_REG - nolla. Todentajan käyttämän ABI-sopimuksen mukaan tämä on kutsu auttajatoimintoon numero kahdeksan. Kun se on käynnistetty, logiikka on yksinkertainen. Palautusarvo rekisteristä r0 kopioitiin r1 ja riveillä 2,3 se muunnetaan tyypiksi u32 — ylemmät 32 bittiä tyhjennetään. Riveillä 4,5,6,7 palautetaan 2 (XDP_PASS) tai 1 (XDP_DROP) riippuen siitä, palauttiko auttajafunktio riviltä 0 nollan vai nollasta poikkeavan arvon.

Testataan itseämme: lataa ohjelma ja katso tulos 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, todentaja löysi oikean ytimen apuohjelman.

Esimerkki: argumenttien välittäminen ja lopuksi ohjelman suorittaminen!

Kaikilla ajon tason aputoiminnoilla on prototyyppi

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

Aputoimintojen parametrit välitetään rekistereissä r1-r5, ja arvo palautetaan rekisteriin r0. Ei ole funktioita, jotka vaativat enemmän kuin viisi argumenttia, eikä niille ole odotettavissa tukea tulevaisuudessa.

Katsotaanpa uutta ytimen apuohjelmaa ja kuinka BPF välittää parametreja. Kirjoitetaan uudelleen xdp-simple.bpf.c seuraavasti (muut rivit eivät ole muuttuneet):

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

Ohjelmamme tulostaa sen suorittimen numeron, jossa se toimii. Käännetään se ja katsotaan koodia:

$ 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

Riveillä 0-7 kirjoitamme merkkijonon running on CPU%un, ja sitten linjalla 8 käytämme tuttua bpf_get_smp_processor_id. Riveillä 9-12 valmistelemme auttaja-argumentit bpf_printk -rekisterit r1, r2, r3. Miksi niitä on kolme eikä kahta? Koska bpf_printktämä on makrokääre todellisen auttajan ympärillä bpf_trace_printk, jonka on läpäistävä muotomerkkijonon koko.

Lisätään nyt pari riviä xdp-simple.cjotta ohjelmamme muodostaa yhteyden käyttöliittymään lo ja todella alkanut!

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

Tässä käytämme toimintoa bpf_set_link_xdp_fd, joka yhdistää XDP-tyyppiset BPF-ohjelmat verkkoliitäntöihin. Koodasimme käyttöliittymän numeron lo, joka on aina 1. Suoritamme toiminnon kahdesti irrottaaksemme ensin vanhan ohjelman, jos se oli liitetty. Huomaa, että nyt emme tarvitse haastetta pause tai ääretön silmukka: latausohjelmamme poistuu, mutta BPF-ohjelmaa ei lopeteta, koska se on kytketty tapahtumalähteeseen. Onnistuneen latauksen ja yhteyden jälkeen ohjelma käynnistetään jokaiselle saapuvalle verkkopaketille lo.

Ladataan ohjelma ja katsotaan käyttöliittymää 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

Lataamamme ohjelman tunnus on 669 ja näemme saman tunnuksen käyttöliittymässä lo. Lähetämme pari pakettia osoitteeseen 127.0.0.1 (pyyntö + vastaus):

$ ping -c1 localhost

ja nyt tarkastellaan debug-virtuaalitiedoston sisältöä /sys/kernel/debug/tracing/trace_pipe, jossa bpf_printk kirjoittaa viestinsä:

# 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

Kaksi pakettia löytyi lo ja käsitelty CPU0:lla - ensimmäinen täysimittainen merkityksetön BPF-ohjelmamme toimi!

On syytä huomata, että bpf_printk Ei turhaan se kirjoittaa debug-tiedostoon: tämä ei ole menestynein apuohjelma tuotannossa käytettäväksi, mutta tavoitteemme oli näyttää jotain yksinkertaista.

Karttojen käyttö BPF-ohjelmista

Esimerkki: BPF-ohjelman kartan käyttö

Edellisissä osioissa opimme luomaan ja käyttämään karttoja käyttäjätilasta, ja nyt tarkastellaan ydinosaa. Aloitetaan tavalliseen tapaan esimerkillä. Kirjoitetaan ohjelmamme uudelleen xdp-simple.bpf.c seuraavasti:

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

Ohjelman alussa lisäsimme karttamäärittelyn woo: Tämä on 8 elementin taulukko, joka tallentaa arvoja, kuten u64 (C:ssä määrittelisimme sellaisen taulukon kuin u64 woo[8]). Ohjelmassa "xdp/simple" saamme nykyisen prosessorin numeron muuttujaksi key ja sitten käyttämällä aputoimintoa bpf_map_lookup_element saamme osoittimen taulukon vastaavaan merkintään, jota lisäämme yhdellä. Käännetty venäjäksi: laskemme tilastot siitä, mikä CPU käsitteli saapuvia paketteja. Yritetään ajaa ohjelma:

$ 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

Tarkastetaan, että hän on koukussa lo ja lähetä paketteja:

$ 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

Katsotaan nyt taulukon sisältöä:

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

Lähes kaikki prosessit käsiteltiin CPU7:llä. Tämä ei ole meille tärkeää, pääasia, että ohjelma toimii ja ymmärrämme kuinka päästä karttoihin BPF-ohjelmista - käyttämällä хелперов bpf_mp_*.

Mystinen indeksi

Joten voimme käyttää karttaa BPF-ohjelmasta käyttämällä puheluita, kuten

val = bpf_map_lookup_elem(&woo, &key);

mistä aputoiminto näyttää

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

mutta ohitamme osoittimen &woo nimettömään rakenteeseen struct { ... }...

Jos katsomme ohjelman kokoajaa, näemme, että arvo &woo ei ole varsinaisesti määritelty (rivi 4):

llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
...

ja se sisältyy siirtoihin:

$ 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

Mutta jos katsomme jo ladattua ohjelmaa, näemme osoittimen oikeaan karttaan (rivi 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]
...

Näin ollen voimme päätellä, että latausohjelmamme käynnistämisen yhteydessä linkki osoitteeseen &woo korvattiin jollain kirjastolla libbpf. Ensin tarkastellaan tulosta 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

Näemme sen libbpf loi kartan woo ja ladannut sitten ohjelmamme simple. Katsotaanpa tarkemmin, kuinka lataamme ohjelman:

  • puhelu xdp_simple_bpf__open_and_load tiedostosta xdp-simple.skel.h
  • joka aiheuttaa xdp_simple_bpf__load tiedostosta xdp-simple.skel.h
  • joka aiheuttaa bpf_object__load_skeleton tiedostosta libbpf/src/libbpf.c
  • joka aiheuttaa bpf_object__load_xattr ja libbpf/src/libbpf.c

Viimeinen toiminto muun muassa kutsuu bpf_object__create_maps, joka luo tai avaa olemassa olevia karttoja ja muuttaa ne tiedostokuvaajiksi. (Tässä näemme BPF_MAP_CREATE ulostulossa strace.) Seuraavaksi kutsutaan funktiota bpf_object__relocate ja hän on se, joka kiinnostaa meitä, koska muistamme näkemämme woo siirtotaulukossa. Tutkimalla sitä löydämme lopulta itsemme funktiosta bpf_program__relocate, mikä käsittelee karttasiirtoja:

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

Noudatamme siis ohjeitamme

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

ja korvaa siinä oleva lähderekisteri arvolla BPF_PSEUDO_MAP_FD, ja ensimmäinen IMM karttamme tiedostokuvaajaan ja jos se on yhtä suuri kuin esim. 0xdeadbeef, niin sen seurauksena saamme ohjeet

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

Näin karttatiedot siirretään tiettyyn ladatuun BPF-ohjelmaan. Tässä tapauksessa kartta voidaan luoda käyttämällä BPF_MAP_CREATEja avataan tunnuksella käyttäen BPF_MAP_GET_FD_BY_ID.

Yhteensä käytössä libbpf algoritmi on seuraava:

  • kokoamisen aikana siirtotaulukkoon luodaan tietueita karttoihin johtavista linkeistä
  • libbpf avaa ELF-objektikirjan, etsii kaikki käytetyt kartat ja luo niille tiedostokuvaukset
  • tiedostokuvaajat ladataan ytimeen osana ohjetta LD64

Kuten voitte kuvitella, lisää on tulossa, ja meidän on tutkittava ydintä. Onneksi meillä on aavistustakaan – olemme kirjoittaneet merkityksen muistiin BPF_PSEUDO_MAP_FD lähderekisteriin ja voimme haudata sen, mikä johtaa meidät kaikkien pyhien pyhyyteen - kernel/bpf/verifier.c, jossa funktio, jolla on erottuva nimi, korvaa tiedostokuvaajan tyypin rakenteen osoitteella 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;

(koko koodi löytyy по ссылке). Joten voimme laajentaa algoritmiamme:

  • ohjelmaa ladattaessa todentaja tarkistaa kartan oikean käytön ja kirjoittaa vastaavan rakenteen osoitteen struct bpf_map

Kun lataat ELF-binaarin käyttämällä libbpf Paljon muutakin on meneillään, mutta keskustelemme siitä muissa artikkeleissa.

Ladataan ohjelmia ja karttoja ilman libbpf:tä

Kuten luvattiin, tässä on esimerkki lukijoille, jotka haluavat tietää kuinka luoda ja ladata karttoja käyttävä ohjelma ilman apua libbpf. Tästä voi olla hyötyä, kun työskentelet ympäristössä, jolle et voi rakentaa riippuvuuksia, tai tallennat jokaista bittiä tai kirjoitat ohjelmaa, kuten ply, joka luo BPF-binaarikoodin lennossa.

Jotta logiikkaa olisi helpompi seurata, kirjoitamme esimerkkimme uudelleen näitä tarkoituksia varten xdp-simple. Tässä esimerkissä käsitellyn ohjelman täydellinen ja hieman laajennettu koodi löytyy tästä ydin.

Sovelluksemme logiikka on seuraava:

  • luo tyyppikartta BPF_MAP_TYPE_ARRAY komentoa käyttämällä BPF_MAP_CREATE,
  • luo ohjelma, joka käyttää tätä karttaa,
  • liitä ohjelma käyttöliittymään lo,

joka käännetään ihmiseksi

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

Täällä map_create luo kartan samalla tavalla kuin teimme ensimmäisessä esimerkissä järjestelmäkutsusta bpf - "ydin, tee minulle uusi kartta 8 elementin joukon muodossa, kuten __u64 ja anna minulle takaisin tiedoston kuvaus":

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

Ohjelma on myös helppo ladata:

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

Hankala osa prog_load on BPF-ohjelmamme määritelmä rakenteiden joukkona struct bpf_insn insns[]. Mutta koska käytämme ohjelmaa, joka meillä on C:ssä, voimme huijata hieman:

$ 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

Yhteensä meidän on kirjoitettava 14 ohjetta rakenteiden muodossa, kuten struct bpf_insn (neuvoja: ota kaatopaikka ylhäältä, lue ohjeosio uudelleen, avaa linux/bpf.h и linux/bpf_common.h ja yritä määrittää struct bpf_insn insns[] omillaan):

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

Harjoitus niille, jotka eivät itse kirjoittaneet tätä - löydä map_fd.

Ohjelmassamme on vielä yksi julkistamaton osa - xdp_attach. Valitettavasti XDP:n kaltaisia ​​ohjelmia ei voida yhdistää järjestelmäkutsulla bpf. BPF:n ja XDP:n luoneet ihmiset olivat online-Linux-yhteisöstä, mikä tarkoittaa, että he käyttivät heille tutuinta (mutta ei normaali ihmiset) käyttöliittymä ytimen kanssa vuorovaikutukseen: netlink socketit, Katso myös RFC3549. Yksinkertaisin tapa toteuttaa xdp_attach kopioi koodia kohteesta libbpf, nimittäin tiedostosta netlink.c, minkä teimme lyhentäen sitä hieman:

Tervetuloa netlink-sockettien maailmaan

Avaa netlink-socket-tyyppi 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;
}

Luemme tästä pistorasiasta:

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

Lopuksi tässä on toimintomme, joka avaa socketin ja lähettää siihen erityisen viestin, joka sisältää tiedostokuvaajan:

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

Joten kaikki on valmis testattavaksi:

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

Katsotaan, onko ohjelmamme yhteydessä 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

Lähetetään pingit ja katsotaan karttaa:

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
$ sudo bpftool m dump name woo
key: 00 00 00 00  value: 90 01 00 00 00 00 00 00
key: 01 00 00 00  value: 00 00 00 00 00 00 00 00
key: 02 00 00 00  value: 00 00 00 00 00 00 00 00
key: 03 00 00 00  value: 00 00 00 00 00 00 00 00
key: 04 00 00 00  value: 00 00 00 00 00 00 00 00
key: 05 00 00 00  value: 00 00 00 00 00 00 00 00
key: 06 00 00 00  value: 40 b5 00 00 00 00 00 00
key: 07 00 00 00  value: 00 00 00 00 00 00 00 00
Found 8 elements

Hurraa, kaikki toimii. Huomaa muuten, että karttamme näytetään jälleen tavuina. Tämä johtuu siitä, että toisin kuin libbpf emme ladaneet tyyppitietoja (BTF). Mutta puhumme tästä lisää ensi kerralla.

Kehitystyökalut

Tässä osiossa tarkastellaan BPF-kehittäjätyökalujen vähimmäismäärää.

Yleisesti ottaen et tarvitse mitään erikoista BPF-ohjelmien kehittämiseen - BPF toimii missä tahansa kunnollisessa jakeluytimessä, ja ohjelmat on rakennettu käyttämällä clang, joka voidaan toimittaa pakkauksesta. Kuitenkin, koska BPF on kehitteillä, ydin ja työkalut muuttuvat jatkuvasti, jos et halua kirjoittaa BPF-ohjelmia vanhanaikaisilla menetelmillä vuodesta 2019 alkaen, sinun on käännettävä

  • llvm/clang
  • pahole
  • sen ydin
  • bpftool

(Viitettä varten tämä osio ja kaikki artikkelin esimerkit suoritettiin Debian 10:ssä.)

llvm/clang

BPF on ystävällinen LLVM:n kanssa, ja vaikka viime aikoina BPF:n ohjelmia voidaan kääntää gcc:n avulla, kaikki nykyinen kehitys tehdään LLVM:lle. Siksi ensin rakennamme nykyisen version clang gitistä:

$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86" 
                      -DLLVM_ENABLE_PROJECTS="clang" 
                      -DBUILD_SHARED_LIBS=OFF 
                      -DCMAKE_BUILD_TYPE=Release 
                      -DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$

Nyt voimme tarkistaa, menikö kaikki oikein:

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

(Kokoamisohjeet clang minulta ottamani bpf_devel_QA.)

Emme asenna juuri luomiamme ohjelmia, vaan lisäämme ne niihin PATH, esimerkiksi:

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

(Tämän voi lisätä .bashrc tai erilliseen tiedostoon. Henkilökohtaisesti lisään tällaisia ​​asioita ~/bin/activate-llvm.sh ja teen sen tarvittaessa . activate-llvm.sh.)

Pahole ja BTF

Apuohjelma pahole käytetään luotaessa ydintä virheenkorjaustietojen luomiseen BTF-muodossa. Emme mene tässä artikkelissa yksityiskohtiin BTF-tekniikan yksityiskohdista, paitsi siitä, että se on kätevä ja haluamme käyttää sitä. Joten jos aiot rakentaa ytimen, rakentaa ensin pahole (ilman pahole et voi rakentaa ydintä valinnalla 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

Ytimet BPF:n kokeiluun

Kun tutkin BPF:n mahdollisuuksia, haluan koota oman ydinni. Tämä ei yleisesti ottaen ole välttämätöntä, koska voit kääntää ja ladata BPF-ohjelmia jakeluytimeen, mutta omalla ytimellä voit käyttää uusimpia BPF-ominaisuuksia, jotka näkyvät jakelussasi parhaimmillaan kuukausien kuluttua. , tai, kuten joidenkin virheenkorjaustyökalujen tapauksessa, ei ole pakattu ollenkaan lähitulevaisuudessa. Lisäksi sen oman ytimen ansiosta koodin kokeileminen tuntuu tärkeältä.

Ytimen rakentamiseen tarvitaan ensinnäkin itse ydin ja toiseksi ytimen asetustiedosto. BPF:n kokeilemiseen voimme käyttää tavallista vanilja ydin tai jokin kehitysytimistä. Historiallisesti BPF-kehitys tapahtuu Linux-verkkoyhteisössä, ja siksi kaikki muutokset menevät ennemmin tai myöhemmin Linux-verkkojen ylläpitäjän David Millerin kautta. Niiden luonteesta riippuen - muokkauksista tai uusista ominaisuuksista - verkkomuutokset kuuluvat toiseen kahdesta ytimestä - net tai net-next. BPF:n muutokset jaetaan samalla tavalla välillä bpf и bpf-next, jotka sitten yhdistetään netto- ja net-seuraavaksi. Katso lisätietoja bpf_devel_QA и netdev-FAQ. Valitse siis ydin makusi ja testattavan järjestelmän vakaustarpeiden perusteella (*-next ytimet ovat epävakaimmat luetelluista).

Tämän artikkelin ulkopuolella on puhua ytimen asetustiedostojen hallinnasta - oletetaan, että tiedät jo, miten tämä tehdään, tai valmis oppimaan omillaan. Seuraavien ohjeiden pitäisi kuitenkin olla enemmän tai vähemmän riittäviä antamaan sinulle toimivan BPF-yhteensopivan järjestelmän.

Lataa jokin yllä olevista ytimistä:

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

Rakenna minimaalinen toimiva ytimen kokoonpano:

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

Ota BPF-asetukset käyttöön tiedostossa .config omasta valinnastasi (todennäköisimmin CONFIG_BPF on jo käytössä, koska systemd käyttää sitä). Tässä on luettelo tässä artikkelissa käytetyn ytimen vaihtoehdoista:

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

Sitten voimme helposti koota ja asentaa moduulit ja ytimen (muuten voit koota ytimen käyttämällä juuri koottua clanglisäämällä CC=clang):

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

ja käynnistä uudelleen uudella ytimellä (käytän tätä kexec paketista 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

Yleisimmin käytetty apuohjelma artikkelissa on apuohjelma bpftool, toimitetaan osana Linux-ydintä. Sen ovat kirjoittaneet ja ylläpitäneet BPF-kehittäjät BPF-kehittäjille, ja sitä voidaan käyttää kaikentyyppisten BPF-objektien hallintaan - ohjelmien lataamiseen, karttojen luomiseen ja muokkaamiseen, BPF-ekosysteemin elämän tutkimiseen jne. Man-sivujen lähdekoodien muodossa oleva dokumentaatio löytyy ytimessä tai jo koottu, verkko.

Tätä kirjoitettaessa bpftool tulee valmiina vain RHEL:lle, Fedoralle ja Ubuntulle (katso esim. tämä lanka, joka kertoo pakkaamisen keskeneräisen tarinan bpftool Debianissa). Mutta jos olet jo rakentanut ytimen, niin rakenna bpftool helppoa kuin mikä:

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

$

(tässä ${linux} - tämä on ytimen hakemisto.) Kun nämä komennot on suoritettu bpftool kerätään hakemistoon ${linux}/tools/bpf/bpftool ja se voidaan lisätä polkuun (ensinkin käyttäjälle root) tai vain kopioida /usr/local/sbin.

Kerätä bpftool on parasta käyttää jälkimmäistä clang, koottu yllä kuvatulla tavalla ja tarkista, onko se koottu oikein - esimerkiksi komennolla

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

joka näyttää, mitkä BPF-ominaisuudet ovat käytössä ytimessäsi.

Muuten, edellinen komento voidaan suorittaa muodossa

# bpftool f p k

Tämä tehdään analogisesti paketin apuohjelmien kanssa iproute2, jossa voimme esimerkiksi sanoa ip a s eth0 sen sijasta ip addr show dev eth0.

Johtopäätös

BPF:n avulla voit kenkiä kirppua mitataksesi tehokkaasti ja muuttaaksesi ytimen toimivuutta lennossa. Järjestelmä osoittautui erittäin onnistuneeksi UNIXin parhaiden perinteiden mukaisesti: yksinkertainen mekanismi, jonka avulla voit (uudelleen) ohjelmoida ytimen, antoi valtavan määrän ihmisiä ja organisaatioita kokeilla. Ja vaikka kokeilut sekä itse BPF-infrastruktuurin kehittäminen eivät ole läheskään valmis, järjestelmässä on jo vakaa ABI, jonka avulla voit rakentaa luotettavaa ja mikä tärkeintä, tehokasta liiketoimintalogiikkaa.

Haluaisin huomauttaa, että mielestäni teknologiasta on tullut niin suosittu, koska toisaalta se voi pelata (koneen arkkitehtuuri voidaan ymmärtää enemmän tai vähemmän yhdessä illassa), ja toisaalta ratkaista ongelmia, joita ei voitu ratkaista (kauniisti) ennen sen ilmestymistä. Nämä kaksi komponenttia yhdessä pakottavat ihmiset kokeilemaan ja haaveilemaan, mikä johtaa yhä innovatiivisempien ratkaisujen syntymiseen.

Tämä artikkeli, vaikkakaan ei erityisen lyhyt, on vain johdatus BPF:n maailmaan eikä kuvaa "kehittyneitä" ominaisuuksia ja tärkeitä arkkitehtuurin osia. Suunnitelma jatkossa on suunnilleen seuraava: seuraava artikkeli on yleiskatsaus BPF-ohjelmatyypeistä (5.8-ytimessä tuetaan 30 ohjelmatyyppiä), sitten tarkastellaan lopuksi oikeiden BPF-sovellusten kirjoittamista ytimen jäljitysohjelmilla. Esimerkiksi, on aika käydä syvemmälle BPF-arkkitehtuuria käsittelevä kurssi, jota seuraa esimerkkejä BPF-verkko- ja tietoturvasovelluksista.

Tämän sarjan aiemmat artikkelit

  1. BPF pienimmille, osa nolla: klassinen BPF

Linkit

  1. BPF- ja XDP-viiteopas - dokumentaatio BPF:stä ciliumilta tai tarkemmin sanottuna Daniel Borkmanilta, yhdeltä BPF:n luojista ja ylläpitäjistä. Tämä on yksi ensimmäisistä vakavista kuvauksista, joka eroaa muista siinä, että Daniel tietää tarkalleen mistä kirjoittaa, eikä siinä ole virheitä. Tässä asiakirjassa kuvataan erityisesti, kuinka XDP- ja TC-tyyppisten BPF-ohjelmien kanssa käytetään tunnettua apuohjelmaa. ip paketista iproute2.

  2. Documentation/Networking/filter.txt — alkuperäinen tiedosto ja dokumentaatio klassiselle ja sitten laajennetulle BPF:lle. Hyvää luettavaa, jos haluat syventyä assembly-kieleen ja teknisiin arkkitehtonisiin yksityiskohtiin.

  3. Blogi BPF:stä Facebookista. Sitä päivitetään harvoin, mutta osuvasti, kuten Aleksei Starovoitov (eBPF:n kirjoittaja) ja Andrii Nakryiko - (ylläpitäjä) kirjoittavat siellä libbpf).

  4. bpftoolin salaisuudet. Viihdyttävä twitter-ketju Quentin Monnet'lta, jossa on esimerkkejä ja salaisuuksia bpftoolin käytöstä.

  5. Sukella BPF:ään: luettavaa luettavaa. Jättiläinen (ja edelleen ylläpidetty) luettelo linkeistä Quentin Monnet'n BPF-dokumentaatioon.

Lähde: will.com

Lisää kommentti