BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

Alguses oli tehnoloogia, mida nimetati BPF-iks. Me vaatasime selle peale kontrolli. Sel ajal on C++ analĂŒsaatorisse lisatud kokku 66 ĂŒldotstarbelist diagnoosi., vana testamendi artiklis sellest tsĂŒklist. 2013. aastal Alexei Starovoitovi ja Daniel Borkmani pingutuste abil töötati vĂ€lja ja lisati Linuxi kernelisse selle tĂ€iustatud versioon, mis oli optimeeritud kaasaegsetele 64-bitistele masinatele. See uus tehnoloogia kandis lĂŒhikest aega nime Internal BPF, seejĂ€rel muudeti see Extended BPF-iks ja nĂŒĂŒd, pĂ€rast mitmeid aastaid, nimetavad kĂ”ik seda lihtsalt BPF-iks.

Üldiselt vĂ”imaldab BPF kĂ€ivitada kasutaja poolt antud arbitraarset koodi Linuxi kernelis ja uus arhitektuur osutus nii edukaks, et meil on vaja veel umbes tosina artikli jagu, et kirjeldada kĂ”iki selle rakendusi. (Ainus, millega arendajad hakkama ei saanud, nagu nĂ€ete alloleval plakatil, on korraliku logo loomine.)

Selles artiklis kirjeldatakse BPF-i virtuaalse masina struktuuri, kerneliga töötamise liideseid, arendustööriistu ning ka lĂŒhikest, vĂ€ga lĂŒhikest ĂŒlevaadet olemasolevatest vĂ”imalustest, st kĂ”ike seda, mida vajame edasise sĂŒgava uurimise jaoks BPF-i praktiliste rakenduste osas.
BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

Artikli kokkuvÔte

Tutvustus BPF-i arhitektuuriga. Alguses vaatame BPF-i arhitektuuri linnulennult ja mÀÀratleme peamised koostisosad.

BPF-i virtuaalse masina registrid ja kĂ€susĂŒsteem. Olles juba omandanud arusaama arhitektuurist, kirjeldame BPF-i virtuaalse masina ehitust.

BPF-i objektide elutsĂŒkkel, faili sĂŒsteem bpffs. Selles osas vaatame lĂ€hemalt BPF-i objektide — programmide ja kaartide — elutsĂŒklit.

Objektide haldamine sĂŒsteemi kĂ”ne bpf abil. Olles saanud juba teatud arusaama sĂŒsteemist, vaatame lĂ”puks, kuidas luua ja hallata objekte kasutajaruumi kaudu erilise sĂŒsteemi kĂ”ne abil — bpf(2).

Kirjutame BPF-i programme libbpf-kasutades. Programmi kirjutamine sĂŒsteemi kĂ”ne abil on muidugi vĂ”imalik. Kuid keeruline. Rohkem realistliku stsenaariumi jaoks on tuumaprogrammeerijad töötanud vĂ€lja raamatukogu libbpf. Loome kĂ”ige lihtsama BPF-i rakenduse karkassi, mida kasutame edaspidi nĂ€idetes.

Kernel Helpers. Siin saame teada, kuidas BPF programmid saavad pöörduda tuumasĂŒsteemide abifunktsioonide poole – tööriista, mis koos kaartidega oluliselt laiendab uue BPF vĂ”imalusi vĂ”rreldes klassikalisega.

JuurdepÀÀs kaartidele BPF programmide kaudu. Selleks ajaks teame piisavalt, et mÔista, kuidas luua programme, mis kasutavad kaarte. Ja isegi piilume suure ja vÔimsa verifitseerija sisse.

Arendustöötlusvahendid. Tehnilised juhised selle kohta, kuidas koguda vajalikud utiliidid ja tuum eksperimenteerimiseks.

JĂ€reldus. Artikli lĂ”pus leiavad need, kes siiani jĂ”udsid, julgustavaid sĂ”nu ja lĂŒhikese ĂŒlevaate sellest, millest jĂ€rgmistes artiklites juttu tuleb. Samuti loetleme mitmeid lingid iseseisvaks uurimiseks neile, kellel ei ole soovi vĂ”i vĂ”imalust oodata jĂ€rgmist osa.

BPF arhitektuuri sissejuhatus

Enne kui hakkame vaatama BPF arhitektuuri, viitame viimast korda (kas tĂ”esti?) klassikalisele BPF, mis töötati vĂ€lja vastusena RISC masinate tekkele ja lahendas pakettide tĂ”hususe filtreerimise probleemi. Arhitektuur osutus nii Ă”nnestunuks, et sĂŒndis 90ndate alguses Berkeley UNIX-is, see portiti enamikule olemasolevatest operatsioonisĂŒsteemidest ja on ellu jÀÀnud kahekĂŒmnendate hullustes, leidnud uusi rakendusi.

Uus BPF töötati vĂ€lja vastusena 64-bitiste masinate, pilveteenuste ja hĂŒppeliselt suurenenud nĂ”udmistele SDN (Starkvara-dmÀÀra nvĂ”rgustik). Uute BPF loomine tuumainĆŸenere on tĂ€iustatud asendus klassikalisele BPF-le, ja juba poole aasta pĂ€rast leidis uus BPF rakendusi keerulises Linuxi sĂŒsteemide jĂ€lgimise praktikas, ja nĂŒĂŒd, kuus aastat pĂ€rast selle ilmumist, vajame me jĂ€rgmiseks artikliks ainult selleks, et loetleda erinevad programmide tĂŒĂŒbid.

VeSÔLeVd PiltId

BPF on virtuaalmasin, mis vĂ”imaldab kĂ€ivitada "tavalist" koodi kerneli ruumis ilma turvalisust ohustamata. BPF-i programmid luuakse kasutaja ruumis, laaditakse kerneli sisse ja ĂŒhendatakse mingisuguse sĂŒndmuse allikaga. SĂŒndmuseks vĂ”ib olla nĂ€iteks paketi edastamine vĂ”rguliidesesse, kerneli funktsiooni kĂ€ivitamine jne. Paketi puhul on BPF-programmile saadaval paketi andmed ja metaandmed (lugemiseks ning vĂ”ib-olla ka kirjutamiseks, sĂ”ltuvalt programmi tĂŒĂŒbist), kerneli funktsiooni kĂ€ivitamisel — funktsiooni argumendid, sealhulgas mĂ€lupunktid kerneli mĂ€lus jne.

Vaatame seda protsessi lÀhemalt. Esiteks rÀÀgime esimesest erinevusest klassikalisest BPF-ist, mille programmid olid kirjutatud assembleris. Uues versioonis muudeti arhitektuuri nii, et programme saab kirjutada kÔrgetasemelistel keeltele, eelkÔige C-le. Selle jaoks loodi llvm-i backend, mis vÔimaldab genereerida BPF-i arhitektuurile sobivat baitkoodi.

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

BPF-i arhitektuuri arendati peamiselt selleks, et see saaks tĂ”husalt töötada tĂ€napĂ€evastes seadmetes. Selleks, et see praktikas töötaks, tĂ”lgitakse BPF-i baitkood pĂ€rast kerneli laadimist masinkoodiks komponente nimega JIT compiler abil (Just In Time). Lisaks, kui te mĂ€letate, laaditi klassikalises BPF-is programm kerneli ja ĂŒhendati sĂŒndmuse allikaga atomaarsetena — ĂŒhe sĂŒsteemikĂ”ne kontekstis. Uues arhitektuuris toimub see kahel etapil — alguses laaditakse kood kerneli sĂŒsteemikĂ”ne abil bpf(2), seejĂ€rel, hiljem, erinevate mehhanismide abil, sĂ”ltuvalt programmi tĂŒĂŒbist, programmil on vĂ”imalus (attaches) sĂŒndmuse allikaga ĂŒhenduda.

Siin vĂ”ib lugejal tekkida kĂŒsimus: kas tĂ”esti oli see vĂ”imalik? Kuidas tagatakse sellise koodi kĂ€itamise turvalisus? Turvalisus tagatakse BPF-programmide laadimise etappimisega, mida nimetatakse valideerijaks (inglise keeles nimetus on verifier ja ma kasutan edaspidi ingliskeelset sĂ”na):

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

Verifier on staatiline analĂŒĂŒsija, mis tagab, et programm ei sega tuuma normaalset toimimist. See aga ei tĂ€henda, et programm ei saaks sĂŒsteemi sekkuda — BPF programmid, sĂ”ltuvalt tĂŒĂŒbist, saavad lugeda ja kirjutada tuuma mĂ€lu osi, muuta funktsioonide tagastatavaid vÀÀrtusi, kĂ€rpida, tĂ€iendada, kirjutada ja isegi ĂŒmber suunata vĂ”rgu pakette. Verifier tagab, et BPF programmi kĂ€ivitamine ei pĂ”hjusta tuuma kokku kukkumist ja et programm, mis vĂ”ib reeglitesse tuginedes kirjutada andmeid, nĂ€iteks vĂ€ljamineva paketi, ei suuda kirjutada tuuma mĂ€lu vĂ€ljaspool paketti. Natuke pĂ”hjalikumalt vaatame verifierit vastavas osas, pĂ€rast seda, kui tutvume kĂ”igi teiste BPF komponentidega.

Nii et, mida me siiani Ă”ppisime? Kasutaja kirjutab programmi C keeles, laadib selle tuuma sĂŒsteemikĂ”ne abil, bpf(2), kus see lĂ€bib verifi katse ja tĂ”lgitakse masinkoodiks. SeejĂ€rel ĂŒhendab sama vĂ”i teine kasutaja programmi sĂŒndmuste allikaga ja see hakkab töötama. Laadimise ja ĂŒhendamise eristamine on vajalik mitmel pĂ”hjusel. Esiteks, verifi kĂ€ivitamine on suhteliselt kallis ja laadides sama programmi mitu korda, raiskame me arvuti aega asjata. Teiseks, kuidas programm tĂ€pselt ĂŒhendatakse, sĂ”ltub selle tĂŒĂŒbist ja ĂŒks "ĂŒldine" liides, mis loodi aasta tagasi, ei pruugi sobida uute programmide tĂŒĂŒpide jaoks. (Kuigi nĂŒĂŒd, kui arhitektuur muutub kĂŒpsemaks, on idee see liides ĂŒhtlustada tasemel) libbpf.)

Hooliv lugeja vĂ”ib mĂ€rkida, et me pole veel piltide osas lĂ”petanud. TĂ”epoolest, eespool öeldu ei selgita, kuidas BPF pĂ”himĂ”tteliselt muudab pilti vĂ”rreldes klassikalise BPF-iga. Kaks uuendust, mis oluliselt laiendavad rakendamisvĂ”imalusi, on jagatud mĂ€lu kasutamise vĂ”imalus ja tuuma abifunktsioonid (kernel helpers). BPF-is on jagatud mĂ€lu rakendatud nii-öelda maps'i abil - jagatud andmestruktuurid kindla API-ga. Sellise nime nad said tĂ”enĂ€oliselt seetĂ”ttu, et esimene ilmunud maps'i tĂŒĂŒp oli rĂ€sitabel. Edasi on tulnud massiivid, lokaalsed (per-CPU) rĂ€sitabelid ja lokaalsed massiivid, otsingupuud, maps'id, mis sisaldavad viiteid BPF-programmidele, ja palju muud. Meid huvitab praegu fakt, et BPF-programmid said vĂ”imaluse salvestada seisundi kutsungite vahel ja jagada seda teiste programmide ning kasutajaruumi vahel.

Maps'esse pÀÀseb kasutajaprotsessidest sĂŒsteemi kĂ”ne abil bpf(2), ning BPF-programmid, mis töötavad tuumas, pÀÀsevad sellele ligi abifunktsioonide kaudu. Veelgi enam, abifunktsioonid ei eksisteeri mitte ainult maps'iga töötamiseks, vaid ka muude tuuma vĂ”imaluste kasutamiseks. NĂ€iteks saavad BPF-programmid kasutada abifunktsioone pakettide ĂŒmbersuunamiseks teistele liidesele, perf-alamsĂŒsteemi sĂŒndmuste genereerimiseks, tuuma struktuuridele ja nii edasi pÀÀsemiseks.

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

KokkuvĂ”ttes annab BPF vĂ”imaluse laadida tuuma ruumi suvalist, st verifikaatoril lĂ€bi kĂ€inud kasutaja koodi. See kood vĂ”ib salvestada seisundi kutsungite vahel ja vahetada andmeid kasutajaruumi ning tal on juurdepÀÀs selle tĂŒĂŒbi programmidele lubatud tuuma alamsĂŒsteemidele.

See on juba sarnane vĂ”imalustele, mida pakuvad tuumamoodulid, mille suhtes BPF-l on mĂ”ned eelised (loomulikult saab sarnaseid rakendusi vĂ”rrelda ainult sarnastega, nĂ€iteks sĂŒsteemi jĂ€lgimisega — BPF-le ei saa kirjutada suvalist draiverit). VĂ”ib mĂ€rkida madalamat sisenemislĂ€ve (osad BPF-i kasutavad utiliitide ei eelda, et kasutajal oleks tuumaprogrammeerimise oskused, ega ka ĂŒldse programmeerimisoskused), kĂ€itamisohutust (tĂ”stke kĂ€si kommentaarides need, kellel pole olnud sĂŒsteemi riket mooduleid kirjutades vĂ”i testides), aatomilisust — moodulite laadimise ajal on seisaku aeg, kuid BPF-i alamsĂŒsteem tagab, et mitte ĂŒkski sĂŒndmus ei jÀÀ tĂ€helepanuta (aususe nimel, see ei kehti mitte kĂ”ikide BPF-i programmiliikide kohta).

Need vĂ”imalused muudavad BPF-i universaalseks tööriistaks tuuma laiendamiseks, mida tĂ”endab praktika: jĂ€rjest rohkem ja rohkem uut tĂŒĂŒpi programme lisandub BPF-ile, ĂŒha rohkem suurettevĂ”tteid kasutab BPF-i tootmisserverites 24×7, ĂŒha rohkem idufirmasid ehitab oma Ă€ri lahendustele, mille aluseks on BPF. BPF-i kasutatakse kĂ”ikjal: DDoS-rĂŒnnakute kaitseks, SDN-i loomisel (nĂ€iteks Kubernetesest vĂ”rkude rakendamine), peamise sĂŒsteemi jĂ€lgimise tööriistana ja statistika kogumisena, sissetungi tuvastamise sĂŒsteemides ja liivakasti sĂŒsteemides jms.

LĂ”petame selle ĂŒlevaateosa artiklist ja vaatame virtuaalmasinat ning BPF-i ökosĂŒsteemi pĂ”hjalikumalt.

NĂŒanss: utiliidid

Et saaksite jÀrgmistes osades nÀidatud nÀiteid kÀivitada, vÔib teil olla vaja teatud arvu utiliite, vÀhemalt llvm/clang BPF-i toe ja bpftool. Osas Arendustooted jaotatud juhised nende utiliitide ning oma tuuma koostamiseks. See osa on paigutatud alla, et mitte segada meie arutelu selgust.

BPF virtuaalmasina registrid ja kĂ€skude sĂŒsteem

BPF arhitektuur ja kĂ€skude sĂŒsteem on loodud silmas pidades, et programmid kirjutatakse C keeles ja tĂ”lgitakse pĂ€rast laadimist tuumasse natiivkoodi. SeetĂ”ttu valiti registrite ja kĂ€skude arv, arvestades tĂ€napĂ€evaste masinate vĂ”imaluste ristumisi matemaatilises mĂ”ttes. Lisaks kehtestati programmidele mitmesuguseid piiranguid; nĂ€iteks ei olnud kuni hiljuti vĂ”imalik kirjutada silmuseid ja alamprogramme, ning kĂ€skude arv oli piiratud 4096-ga (praegu saavad privileeritud programmid laadida kuni miljon kĂ€sku).

BPF-s on kasutajale ligipÀÀsetavad ĂŒheksa 64-bitist registreid r0—r10 ja kĂ€skude loendur (program counter). Register r10 hoiab raami nĂ€idikut (frame pointer) ja on ainult lugemiseks saadaval. Programmidele on tĂ€itmise ajal ligipÀÀsetav 512-bytesit tagasihoidlik salvestus ja piiramatu jagatud mĂ€lu kaardina (maps).

BPF programmidele on lubatud kĂ€ivitada kindel funktsioonide kogum sĂ”ltuvalt programmi tĂŒĂŒbist (kernel helpers) ja hiljuti ka tavalisi funktsioone. Iga kutsutav funktsioon vĂ”ib vastu vĂ”tta kuni viis argumendit, mis edastatakse registrites r1—r5, ning tagastatav vÀÀrtus edastatakse registrisse r0. Tagatakse, et pĂ€rast funktsioonist vĂ€ljumist registrite sisu r6—r9 ei muutu.

Kuna programmide tĂ”husaks tĂ”lkimiseks on kĂ”ik toetatud arhitektuuride registrid r0—r11 kaardistatud selgelt tegelikele registeritele, arvestades hetke arhitektuuri ABI eripĂ€rasid. NĂ€iteks kasutatakse x86_64 registreid r1—r5, mis edastavad funktsioonide parameetreid, registreerimiseks rdi, rsi, rdx, rcx, r8, mis on kasutusel parameetrite edastamiseks funktsioonidesse x86_64. NĂ€iteks tĂ”lgitakse vasakul olev kood paremal poole nii:

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

Register r0 kasutatakse ka programmi tĂ€itmise tulemuse tagastamiseks, ja registris r1 edastatakse programmile konteksti nĂ€idik — sĂ”ltuvalt programmi tĂŒĂŒbist vĂ”ib see olla nĂ€iteks struktuur struct xdp_md (XDP jaoks) vĂ”i struktuur struct __sk_buff (erinevate vĂ”rguprogrammide jaoks) vĂ”i struktuur struct pt_regs (erinevate jĂ€lgimisprogrammide jaoks) jne.

Nii, meil oli registeride komplekt, kernel helpers, stakk, konteksti nÀitaja ja jagatud mÀlu kujul maps. Mitte et kÔik see oleks kindlasti vajalik reisil, aga


JĂ€tkame kirjeldust ja rÀÀgime kĂ€skude sĂŒsteemist nende objektide töötlemiseks. KĂ”ik (peaaegu kĂ”ik) BPF-kĂ€sud on fikseeritud 64-bitise suurusega. Kui vaatate ĂŒht kĂ€sku 64-bitises Big Endian masinas, nĂ€ete

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

Siin Kood — see on kĂ€su kodeering, Dst/Src — need on vastavalt siht- ja allika kodeeringud, KĂ”rval — 16-bitine signeeritud nihkevÀÀrtus, ja Imm — see on 32-bitine signeeritud tĂ€isarv, mida kasutatakse mĂ”nedes kĂ€skudes (sarnane konstant K-st cBPF-is). Kodeering Kood on ĂŒhes kahest vormist:

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

KĂ€skude klassid 0, 1, 2, 3 mÀÀratlevad kĂ€sud mĂ€lu töötlemiseks. Nad kĂŒnditakse, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, vastavalt. Klassid 4, 7 (BPF_ALU, BPF_ALU64) koosnevad ALU-kĂ€skudest. Klassid 5, 6 (BPF_JMP, BPF_JMP32) sisaldavad hĂŒppe kĂ€ske.

Edasi on BPF-kĂ€skude Ă”ppimise plaan jĂ€rgmine: selle asemel, et hoolikalt loetleda kĂ”iki kĂ€ske ja nende parameetreid, vaatame siin paar nĂ€idet ja selgub, kuidas kĂ€skud tegelikult on ĂŒles ehitatud ja kuidas kĂ€sitsi disassembeldada igasuguseid binaarfailide BPF jaoks. Teema kinnistamiseks kohtame artikli edasistes osades individuaalseid kĂ€ske, mis puudutavad Verifierit, JIT kompilaatorit, klassikalise BPF tĂ”lkimist ning ka mapside ja funktsioonide kutsumist jne.

Kui rÀÀgime individuaalsetest kĂ€skudest, viitame me kernelifailidele bpf.h ja bpf_common.h, kus on mÀÀratletud BPF kĂ€su numbrilised koodid. Kui uurite arhitektuuri ja/vĂ”i vaatate ĂŒle binaare, vĂ”ite leida semantika jĂ€rgmistest allikatest, mis on jĂ€rjestatud keerukuse jĂ€rgi: Ebofficial eBPF spetsiifikatsioon, BPF ja XDP viidatud juhend, kĂ€skude komplekt, Dokumentatsioon/networking/filter.txt ja muidugi Linuxi lĂ€htekoodis — verifier, JIT, BPF tĂ”lkija.

NÀide: disassembeldame BPF mÔttes

Vaadake seda nÀidet, kus kompileerime programmi readelf-example.c ja vaatame, mis binaar on saadud. Avame originaalse sisu readelf-example.c allpool, pÀrast seda, kui oleme taastanud selle loogika binaarkoodidest:

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

Esimene veerg vĂ€ljundis readelf — see on sisend ja meie programm koosneb seega neljast kĂ€sklusest:

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

KĂ€skluskoodid on vĂ”rdsed b7, 15, b7 ja 95. Meenutame, et kolm madalaimat bitti tĂ€histavad kĂ€su klassi. Meie puhul on neljas bitt kĂ”ikide kĂ€skluste jaoks tĂŒhi, seega on kĂ€su klassid vĂ”rdsed, vastavalt 7, 5, 7, 5. Klass 7 on BPF_ALU64, aga 5 on BPF_JMP. MĂ”lema klassi korral on kĂ€su formaat sama (vt eelnevat) ja saame meie programmi ĂŒmber kirjutada nii (samuti kirjutame ĂŒlejÀÀnud veerud inimlikus vormis):

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

Tehe b klassist ALU64 — see on BPF_MOV. See mÀÀrab vÀÀrtuse sihtregistrisse. Kui on seatud bitt s (source), siis vÀÀrtus vĂ”etakse allikaregistrist, kuid kui, nagu meie puhul, see ei ole seatud, siis vÀÀrtus vĂ”etakse vĂ€ljast Imm. Seega teeme esimese ja kolmanda kĂ€su korral operatsiooni r0 = Imm. JĂ€rgmine kĂ€sk 1 klassi JMP — see on BPF_JEQ (hĂŒpe, kui vĂ”rdsed). Meie puhul, kuna bitt S on null, vĂ”rdleb see allikaregistri vÀÀrtust vĂ€ljadega Imm. Kui vÀÀrtused kattuvad, toimub ĂŒleminek PC + Off, kus PC, nagu tavaliselt, sisaldab see jĂ€rgmise kĂ€su aadressi. LĂ”puks, kĂ€sk 9 klassi JMP — see on BPF_EXIT. See kĂ€sk lĂ”petab programmi toimimise, tagastades kernelile r0. Lisame meie tabelisse uue veeru:

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

Saame selle ĂŒmber kirjutada mugavamas vormis:

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

Kui me meenutame, et registris r1 edastatakse programmi kaudu konteksti viidatud tuumale, ning registris r0 tagastatakse vÀÀrtus tuumale, siis vÔime nÀha, et kui konteksti viidatud on null, siis tagastame 1, vastasel juhul 2. Kontrollime, et oleme Ôiged, vaadates lÀhtekoodi:

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

Jah, see on mÔttetu programm, kuid see kompileeritakse vaid neljaks lihtsaks kÀsuks.

NÀide-erand: 16-baidine kÀsk

Oleme varem maininud, et mĂ”ned kĂ€sud vĂ”tavad rohkem kui 64 bitti. See kehtib nĂ€iteks kĂ€sku lddw (Code = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — laadige registrisse kahe sĂ”naga andmed vĂ€ljadest Imm. Asi on selles, et Imm on 32 byte size, and a double word — 64 bits, so we cannot load a 64-bit immediate value into a register in one 64-bit instruction. For this, two adjacent instructions are used to store the second part of the 64-bit value in the field Imm. NĂ€idis:

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

The binary program contains only two instructions:

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

We will meet again with the instruction lddw, when we talk about relocations and working with maps.

Example: disassembling BPF using standard tools

So, we have learned to read BPF binary codes and are ready to disassemble any instruction if needed. However, it is worth noting that in practice, it is more convenient and faster to disassemble programs using standard tools, for example:

$ llvm-objdump -d x64.o

Disassembly of section .text:

0000000000000000 :
 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

The life cycle of BPF objects, the bpffs filesystem

(Some details presented in this subsection were first learned from the post by Alexei Starovoitov in BPF Blog.)

BPF objects — programs and maps — are created from user space using the commands BPF_PROG_LOAD ja BPF_MAP_CREATE system call bpf(2), we will discuss how this happens in the next section. This creates kernel data structures and for each of them refcount (reference count) is set to one, and the user is returned a file descriptor pointing to the object. After closing the descriptor refcount the object’s reference count reduces by one, and when it reaches zero, the object is destroyed.

If the program uses maps, then refcount the reference count of these maps increases by one after the program is loaded, i.e., their file descriptors can be closed from the user process without causing the count refcount to drop to zero:

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

After successfully loading the program, we usually attach it to some event generator. For example, we can attach it to a network interface to process incoming packets or connect it to some tracepoint in the kernel. At this moment, the reference count will also increase by one, and we will be able to close the file descriptor in the loading program.

Mis juhtub, kui me nĂŒĂŒd laadijaga töö lĂ”petame? see sĂ”ltub sĂŒndmuste generaatori (hook) tĂŒĂŒbist. KĂ”ik vĂ”rguhooked jÀÀvad alles pĂ€rast laadija lĂ”petamist, need on nn globaalhookid. NĂ€iteks jĂ€lgimisprogrammid vabanevad pĂ€rast nende loonud protsessi lĂ”puleviimist (seega nimetatakse neid kohalikeks, mis tuleneb terminist 'local to the process'). Tehniliselt on kohalikud hookid alati seotud vastava failidĆŸa vaba ruumiga ja seega sulguvad protsessi lĂ”petamisega, samas kui globaalhookid ei sulgu. JĂ€rgmises joonises pĂŒĂŒan punaste ristikestega nĂ€idata, kuidas laadija programmi lĂ”petamine mĂ”jutab objektide eluiga kohalike ja globaalsete hookide korral.

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

Miks on olemas kohalike ja globaalsete hookide eraldamine? MĂ”nede tĂŒĂŒpi vĂ”rguprogrammide kĂ€itamine on mĂ”ttekas ka ilma userspace'ita, nĂ€iteks kujutage ette DDoS-kaitset - laadija mÀÀrab reeglid ja ĂŒhendab BPF programmi vĂ”rguliidesega, pĂ€rast mida laadija vĂ”ib ise lĂ”petada. Teisest kĂŒljest, kujutage ette jĂ€lgimisprogrammi, mille te kĂŒmne minutiga ise kokku keerasite - pĂ€rast selle lĂ”petamist sooviksite, et sĂŒsteemis ei jÀÀks prĂŒgi, ja kohalikud hookid tagavad selle.

Teiselt poolt, kujutage ette, et soovite ĂŒhenduda jĂ€lgimispunktiga (tracepoint) tuumas ja koguda statistikat aastaid. Sel juhul sooviksite lĂ”petada kasutaja osa ja aeg-ajalt statistika juurde tagasi tulla. Selle vĂ”imaluse pakub bpf failisĂŒsteem. See on pseudo-failisĂŒsteem, mis eksisteerib ainult mĂ€lus ja vĂ”imaldab luua faile, mis viitavad BPF objektidele, ja seelĂ€bi pikendada refcount objekte. PĂ€rast seda vĂ”ib laadija lĂ”petada oma töö ja loodud objektid jÀÀvad elama.

BPF kÔige vÀiksematele, esimene osa: laiendatud BPF

BPF objektide pindamine ("pin", nagu jÀrgmises lauses: "process can pin a BPF program or map") BPF objektide jaoks failide loomine on mÔttekas mitte ainult kohalike objektide elu pikendamiseks, vaid ka globaalsete objektide mugavamaks kasutamiseks - naastes DDoS-i kaitse globaalprogrammi nÀite juurde, tahame meil oleks aeg-ajalt vÔimalik tagasi tulla ja statistika peale vaadata.

BPF faili sĂŒsteem monteeritakse tavaliselt /sys/fs/bpf, kuid selle saab monteerida ka kohapeal, nĂ€iteks nii:

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

FailisĂŒsteemis nimed luuakse kĂ€suga BPF_OBJ_PIN BPF sĂŒsteemikĂ”ne abil. NĂ€itena vĂ”tame mĂ”ne programmi, kompileerime, laadime ja kinnitame selle bpffs. Meie programm ei tee midagi kasulikku, toome selle koodi vaid selleks, et saaksite nĂ€idist korrata:

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

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

Kompileerime selle programmi ja loome kohaliku koopia faili sĂŒsteemist bpffs:

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

NĂŒĂŒd laadime meie programmi utiliidi abil bpftool ja vaatame kaasnevaid sĂŒsteemikĂ”nesid bpf(2) (strace vĂ€ljundist on mĂ”ned asjakohatud read eemaldatud):

$ 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

Siin laadisime programmi BPF_PROG_LOAD, saime kernelilt faili deskripti 3 ja kĂ€suga BPF_OBJ_PIN kinnitasime selle faili deskripti failina "bpf-mountpoint\/test". PĂ€rast seda lĂ”petas laadimisprogramm töö, kuid meie programm jĂ€i karneli, kuigi me ei kinnitanud seda ĂŒhelgi vĂ”rguliidesele: bpftool $ 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

Saame failiobjekti tavalise

unlink(2) ja pÀrast seda vastav programm kustutatakse: $ sudo rm .\/bpf-mountpoint\/test $ sudo bpftool prog show id 783 Error: get by id (783): No such file or directory

Objektide kustutamine

RÀÀkides objektide kustutamisest, tuleb tĂ€psustada, et pĂ€rast programmi eraldamist konksust (sĂŒndmuste generaatorist) ei pĂ”hjusta ĂŒkski uus sĂŒndmus selle kĂ€ivitamist, kuid kĂ”ik jooksvaid programmi eksemplarid lĂ”petatakse korrektselt.

MĂ”ned BPF programmide tĂŒĂŒbid vĂ”imaldavad programmi töö ajal vahetada, st pakuvad jĂ€rjepidevust

replace = detach old program, attach new program . Sellega lĂ”petavad kĂ”ik vanade programmide aktiivsed eksemplarid oma töö ja uued sĂŒndmuste töötlejad luuakse juba uuest programmist, ning "jĂ€rjepidevus" tĂ€hendab siin, et ĂŒkski sĂŒndmus ei jÀÀ vahele.Sellega lĂ”petavad kĂ”ik aktiivsed vanade programmide eksemplarid oma töö ning uued sĂŒndmuste töötlejad luuakse juba uue programmi pĂ”hjal, kusjuures "aatomiilisus" tĂ€hendab seda, et ĂŒhtegi sĂŒndmust ei jÀÀ vahele.

Programmid sĂŒndmustele liitumine

Selles artiklis ei hakka me eraldi kirjeldama programmide ĂŒhendamist sĂŒndmustele, kuna seda on mĂ”ttekas uurida konkreetse programmiliigi kontekstis. Vaata. nĂ€ide allpool, kus nĂ€itame, kuidas liituvad XDP tĂŒĂŒpi programmid.

Objektide haldamine BPF sĂŒsteemikĂ”ne abil

BPF programmid

KĂ”ik BPF objektid luuakse ja hallatakse kasutaja ruumist sĂŒsteemikĂ”ne abil bpf, millel on jĂ€rgmine prototĂŒĂŒp:

#include <linux/bpf.h>

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

Siin kĂ€sk cmd on ĂŒks vÀÀrtusi enum bpf_cmd, attr — nĂ€itaja konkreetse programmi parameetritele ja size — objekti suurus nĂ€itaja jĂ€rgi, st tavaliselt see on sizeof(*attr). Kerneli versioonis 5.8 toetab sĂŒsteemikĂ”ne bpf 34 erinevat kĂ€sku, ja defineerimine union bpf_attr koosneb 200 reast. Kuid see ei peaks meid hirmutama, kuna tutvume kĂ€skude ja parameetritega mitme artikli jooksul.

Alustame kĂ€sust BPF_PROG_LOAD, mis loob BPF programme — vĂ”tab BPF kĂ€sukogumi ja laadib selle ykerneli. Laadimise hetkel aktiveeritakse valideerija ning seejĂ€rel JIT kompilaator ja pĂ€rast edukat tĂ€itmist tagastatakse kasutajale programmifaili deskriptor. Olime varem nĂ€inud, mida sealt edasi toimub BPF objektide elutsĂŒkli kohta..

Praegu kirjutame kasutaja programmi, mis laadib lihtsa BPF programmi, kuid kĂ”igepealt peame otsustama, millise programmi tahame laadida — peame valima tĂŒĂŒp ja selle tĂŒĂŒbi raames kirjutama programmi, mis lĂ€bib valideerijat. Kuid et protsessi keerulisemaks mitte muuta, siin on valmis lahendus: vĂ”tame programmi tĂŒĂŒpi BPF_PROG_TYPE_XDP, mis tagastab vÀÀrtuse XDP_PASS (kĂ”ik paketid jĂ€tta vahele). BPF assambleeris see vĂ€lja nĂ€eb vĂ€ga lihtne:

r0 = 2
exit

PÀrast seda, kui oleme otsustanud, mida mida me laadime, saame rÀÀkida, kuidas me seda teeme:

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

Huvilised sĂŒndmused programmis algavad massiivi mÀÀratlemisest insns — meie BPF programm masinakoode sÀÀstev. Samuti sisaldab iga BPF kĂ€su juhist struktuuris bpf_insn. Esimene element insns vastab kĂ€skudele r0 = 2, teine — exit.

KÀik. Kernelis on mÀÀratletud mugavamad makrod masinakoode kirjutamiseks ja kasutades kernelipeade faili tools/include/linux/filter.h saaksime kirjutada

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

Kuna BPF programmide kirjutamine masinkoodides on vajalik ainult testide ja BPF artiklite kirjutamiseks, siis nende makrode puudumine ei tee arendaja elu tegelikult keerulisemaks.

PĂ€rast BPF programmi mÀÀratlemist liigume selle laadimise juurde kernelisse. Meie minimalistlik parameetrite komplekt attr sisaldab programmi tĂŒĂŒpi, kĂ€skude arvu ja komplekti, kohustuslikku litsentsi ning nime "woo", mida me kasutame, et leida meie programm sĂŒsteemis pĂ€rast selle laadimist. Programm laaditakse sĂŒsteemi nagu lubatud sĂŒsteemikĂ”ne abil bpf.

Programmi lĂ”pus satume lĂ”pmatusse tsĂŒklisse, mis simuleerib kasulikku koormust. Ilma selleta hĂ€vitab programm kernel, kui faili deskriptor, mille sĂŒsteemikĂ”ne meile andis, suletakse bpf, ja me ei nĂ€e seda sĂŒsteemis.

Noh, oleme testimiseks valmis. Kogume ja kÀivitame programmi strace, et kontrollida, kas kÔik töötab nagu peab:

$ 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_VERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

KĂ”ik on korras, bpf(2) meil on deskriptor 3 ja me suundume lĂ”pmatusse tsĂŒklisse koos pause(). Proovime nĂŒĂŒd leida meie programmi sĂŒsteemis. Selleks lĂ€heme teise terminali ja kasutame utiliiti 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Ă€eme, et sĂŒsteemis on laaditud programm woo mille globaalne ID on 390, ja et praegu protsessis simple-prog on avatud faili deskriptor, mis viitab programmile (ja kui simple-prog see lĂ”petab töö, siis woo kaob). Nagu oodata oli, programm woo vĂ”tab 16 baiti — kaks kĂ€sku — BPF arhitektuuri binaarkoodis, kuid originaali (x86_64) formaat on juba 40 baiti. Vaadake meie programmi originaalses vormis:

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

ilma ĂŒllatusteta. NĂŒĂŒd vaatame koodi, mille on loonud JIT kompilaator:

# 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 ole kuigi tÔhus exit(2), kuid Ôiguse nimel, meie programm on liiga lihtne, ja mitte triviaalsete programmide puhul on JIT kompilaatori poolt lisatud proloog ja epiloog kindlasti vajalikud.

Maps

BPF-programmid saavad kasutada struktuuritud mĂ€lu alasid, mis on kergesti kĂ€ttesaadavad nii teistele BPF-programmidele kui ka kasutajaruumi programmidele. Need objekte nimetatakse "maps" ja kĂ€esolevas osas nĂ€itame, kuidas neid hallata sĂŒsteemikutsumise abil. bpf.

Ütleme kohe, et maps'i vĂ”imalused ei piirdu ainult juurdepÀÀsuga jagatud mĂ€lule. On olemas spetsiaalsed kaardid, mis sisaldavad nĂ€iteks viiteid BPF-programmidele vĂ”i viiteid vĂ”rguliidestele, kaardid perf-events'i jaoks ja nii edasi. Siin me neist rÀÀkima ei hakka, et lugejat segadusse ei ajada. Lisaks ignoreerime sĂŒnkroniseerimisprobleeme, kuna need ei ole meie nĂ€idete jaoks olulised. TĂ€ieliku nimekirja saadaval olevatest kaarditĂŒĂŒpidest leiate. <linux/bpf.h>, ja kĂ€esolevas osas vĂ”tame me nĂ€itena ajalooliselt esimese tĂŒĂŒbi, hash-tabeli. BPF_MAP_TYPE_HASH.

Kui loote hash-tabeli, ĂŒtleme, et C++-s ĂŒtlete: unordered_map woo, mis eesti keeles tĂ€hendab "mul on vaja tabelit, woo mis on piiranguteta suurusega, mille vĂ”tmed on tĂŒĂŒpi int, ja vÀÀrtused on tĂŒĂŒpi long." BPF-hash-tabeli loomiseks peame me tegema enam-vĂ€hem sama, arvestades, et peame mÀÀrama tabeli maksimaalse suuruse ja vĂ”tmete ning vÀÀrtuste tĂŒĂŒbi asemel peame me tĂ€psustama nende suurused baitides. Kaartide loomiseks kasutatakse kĂ€sku BPF_MAP_CREATE system call bpf. Vaatame ĂŒle enam-vĂ€hem minimaalse programmi, mis loob kaardi. Eelmise BPF-programmi laadimise jĂ€rel peaks see teile tunduma lihtne:

$ cat simple-map.c
#define _GNU_SOURCE
#include 
#include 
#include 
#include 

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

Siin mÀÀratleme parametrites kogumi, attr, kus ĂŒtlen "mul on vaja hash-tabelit, mille vĂ”tmed ja vÀÀrtused on suurusega sizeof(int), kuhu ma saan paigutada maksimaalselt neli elementi." BPF-kaartide loomisel vĂ”ib mÀÀrata ka teisi parameetreid, nĂ€iteks, nagu programmide nĂ€ites, mÀÀrasime objekti nimeks "woo".

Kompileerime ja kÀivitame programmi:

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

Siin on sĂŒsteemikutsung bpf(2) tagastas meile kaardiga seotud deskriptorinumbri 3 ja edasi programm, nagu oodatud, ootab sĂŒsteemikutses edasisi juhiseid pause(2).

NĂŒĂŒd saadame meie programmi taustale vĂ”i avame teise terminali ning vaatame meie objekti seejĂ€rel utiliidi abil bpftool (me saame eristada meie kaarti teistest selle nime jĂ€rgi):

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

Number 114 on meie objekti globaalne ID. Iga programm sĂŒsteemis vĂ”ib kasutada seda ID-d, et avada juba olemasolev kaart kĂ€suga BPF_MAP_GET_FD_BY_ID system call bpf.

NĂŒĂŒd saame hakata katsetama meie hash-tabeliga. Vaatame selle sisu:

$ sudo bpftool map dump id 114
Leitud 0 elementi

TĂŒhi. Paneme nĂŒĂŒd sinna vÀÀrtuse hash[1] = 1:

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

Vaatame tabelit veel kord:

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

Hurraa! Meil Ă”nnestus lisada ĂŒks element. Pane tĂ€hele, et selleks peame töötama baitide tasemel, kuna bptftool ei tea, milline tĂŒĂŒp on vÀÀrtustel hash-tabelis. (Seda teavet saab anda, kasutades BTF-d, kuid mitte nĂŒĂŒd.)

Kuidas bpftool tÀpselt elemente loeb ja lisab? Uurime, mis peidab end kapoti all:

$ 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

Esmalt avasime kaardi selle globaalsete ID-de pĂ”hjal kĂ€suga BPF_MAP_GET_FD_BY_ID ja bpf(2) tagastas meile deskriptor 3. Edasi kĂ€suga BPF_MAP_GET_NEXT_KEY leidis me tabelis esimese vĂ”tme, edastades NULL nagu "eelneva" vĂ”tme nĂ€itaja. Kui meil on vĂ”tme olemas, saame teha BPF_MAP_LOOKUP_ELEM, mis tagastab vÀÀrtuse nĂ€itaja value. JĂ€rgmine samm — proovime leida jĂ€rgmise elemendi, edastades nĂ€itaja praegusele vĂ”tmele, kuid meie tabelis on ainult ĂŒks element ja kĂ€sk BPF_MAP_GET_NEXT_KEY toob tagasi ENOENT.

HÀsti, muudetakse vÀÀrtust vÔtme 1 kaudu, oletame, et meie Àri loogika nÔuab selle mÀÀramist 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

Kuna oodatud, on see vĂ€ga lihtne: kĂ€sk BPF_MAP_GET_FD_BY_ID ava meie kaardi ID kaudu, ja kĂ€sk BPF_MAP_UPDATE_ELEM ĂŒlekirjutab elemendi.

KokkuvĂ”ttes, pĂ€rast rĂ€sitabeli loomist ĂŒhest programmist saame lugeda ja kirjutada selle sisu teisest. Pange tĂ€hele, et kui me suutsime seda teha kĂ€surealt, siis suudab seda ka iga teine programm sĂŒsteemis. Üheks koos kĂ€itumiseks on kasutusel jĂ€rgmised:

  • BPF_MAP_LOOKUP_ELEM: leida vÀÀrtus vĂ”tme jĂ€rgi
  • BPF_MAP_UPDATE_ELEM: vĂ€rskendada/luua vÀÀrtus
  • BPF_MAP_DELETE_ELEM: kustutada vĂ”ti
  • BPF_MAP_GET_NEXT_KEY: leida jĂ€rgmine (vĂ”i esimene) vĂ”ti
  • BPF_MAP_GET_NEXT_ID: vĂ”imaldab lĂ€bida kĂ”ik olemasolevad mapid, nii töötab bpftool map
  • BPF_MAP_GET_FD_BY_ID: avada olemasolev map selle globaalsete ID-de jĂ€rgi
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomaarne objekti vÀÀrtuse vĂ€rskendamine ja vana tagastamine
  • BPF_MAP_FREEZE: muuta map muutumatuks userspace'ist (seda toimingut ei saa tĂŒhistada)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: massilised toimingud. NĂ€iteks, BPF_MAP_LOOKUP_AND_DELETE_BATCH — see on ainus usaldusvÀÀrne viis lugeda ja nullida kĂ”ik vÀÀrtused mapist

Mitte kĂ”ik need kĂ€sud ei tööta kĂ”igi mapitĂŒĂŒpide puhul, kuid ĂŒldiselt on töö teiste mapide tĂŒĂŒpidega userspace'ist sama, nagu rĂ€sitabelitega.

Korraks lÔpetame meie katsetused rÀsitabeliga. Pidage meeles, et me lÔime tabeli, kuhu vÔib mahtuda kuni neli vÔtit? Lisame veel mÔned elemendid:

$ 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

Praegu on kÔik hÀsti:

$ 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
Leitud 4 elementi

Proovime lisada veel ĂŒhe:

$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: vÀrskendamine ebaÔnnestus: Argumendi loend on liiga pikk

Nagu oodatud, ei Ônnestunud meil. Vaatame viga lÀhemalt:

$ 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 (Argumendi loend on liiga pikk)
Error: vÀrskendamine ebaÔnnestus: Argumendi loend on liiga pikk
+++ lÔpetas 255 +++

KĂ”ik on korras: nagu oodatud, kĂ€sk BPF_MAP_UPDATE_ELEM ĂŒritab luua uut, viiendat, vĂ”tit, kuid ebaĂ”nnestub E2BIG.

Nii, me oskame luua ja laadida BPF programme ning hallata kaarte kasutajaruumi kaudu. NĂŒĂŒd on loogiline vaadata, kuidas saame kaarte kasutada BPF programmides endis. Me vĂ”iksime rÀÀkida sellest raskesti loetavate masinkoodide makrode keeles, kuid tegelikult on kĂ€tte jĂ”udnud aeg nĂ€idata, kuidas BPF programme kirjutatakse ja hooldatakse — abi lĂ€bi libbpf.

(Lugejatele, kes on rahulolematu madala taseme nĂ€idete puudumise ĂŒle: me arutame ĂŒksikasjalikult programme, mis kasutavad kaarte ja abifunktsioone, mis on loodud lĂ€bi libbpf ja rÀÀgime, mis toimub kĂ€su tasemel. Lugejatele, kes on rahulolematu vĂ€ga tugevalt, oleme lisanud nĂ€ide seoses artikli sobivasse kohta.)

Kirjutame BPF-i programme libbpf-kasutades

BPF programmide kirjutamine masinkoodide kaudu vĂ”ib olla huvitav ainult esialgu, seejĂ€rel muutub see tĂŒĂŒtuks. Sel hetkel tuleks oma pilk pöörata llvm, mis sisaldab tagakĂŒlge BPF arhitektuurile koodi genereerimiseks, ja ka raamatukogu libbpf, mis vĂ”imaldab kirjutada BPF rakenduste kasutajapoolset osa ja laadida BPF programmide koodi, mis on genereeritud lĂ€bi llvm/clang.

Tegelikult, nagu nĂ€eme selles ja jĂ€rgnevates artiklites, libbpf teeb ĂŒsna palju tööd ja ilma selleta (vĂ”i sarnaste tööriistadeta — iproute2, libbcc, libbpf-go, jne.) ei saa elada. Üks projekti killer-funktsioone libbpf on BPF CO-RE (Compile Once, Run Everywhere) — projekt, mis vĂ”imaldab kirjutada BPF programe, mis on ĂŒlekandmiseks erinevate tuumade vahel ja töötamine erinevate API-dega (nĂ€iteks, kui tuuma struktuur muutub erinevate versioonide vahel). Selleks, et CO-RE-ga töötada, peab teie tuum olema BTF toega kompileeritud (kuidas seda teha, rÀÀgime jaotises Arendustooted. Kontrollida, kas teie tuum on BTF toega kompileeritud, on vĂ€ga lihtne — olemasoleva faili jĂ€rgi:

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

See fail sisaldab teavet kĂ”igi andmetĂŒĂŒpide kohta, mida tuumas kasutatakse, ja seda kasutatakse kĂ”igis meie nĂ€idetes, mis kasutavad libbpf. RÀÀgime CO-RE-st pĂ”hjalikult jĂ€rgnevates artiklites, aga sel korral — lihtsalt koostage oma tuum CONFIG_DEBUG_INFO_BTF.

Raamatukogu libbpf elab otse kataloogis tools/lib/bpf tuuma ja selle arendamist juhitakse postiloendi kaudu bpf@vger.kernel.org. Kuid rakenduste jaoks, mis elavad kestast vÀljaspool, toetatakse eraldi hoidlat https://github.com/libbpf/libbpf , kus kernelirakenduse teek peegeldub lugemiseks enam-vÀhem nii nagu on.

Selles osas vaatame, kuidas luua projekt, mis kasutab libbpf, kirjutame mÔned (enam-vÀhem mÔttetud) testprogrammid ja vaatame, kuidas see kÔik töötab. See vÔimaldab meil jÀrgnevates osades lihtsamalt selgitada, kuidas just BPF programmid interakteeruvad maps, kernel helpers, BTF jne.

Tavaliselt lisavad projektid, mis kasutavad libbpf , GitHubi hoidla git submodulina, teeme seda ka meie:

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

Kogumine libbpf on vÀga lihtne:

$ 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

Meie edasine plaan selles osas on jĂ€rgmine: kirjutame BPF tĂŒĂŒpi programmi BPF_PROG_TYPE_XDP, sama, mis eelnevas nĂ€ites, aga C keeles, kompileerime selle kasutades clang, ja kirjutame abiprogrammi, mis laadib selle kernelisse. JĂ€rgnevates osades laiendame nii BPF programmi kui ka abiprogrammi funktsioone.

NÀide: loome tÀisfunktsionaalse rakenduse libbpf abil

Alustuseks kasutame ĂŒlalpool mainitud faili /sys/kernel/btf/vmlinux, ja loome selle ekvivalent pea failina:

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

Selles failis hoitakse kÔiki andmestruktuure, mis meie kernelis eksisteerivad, nÀiteks mÀÀratletakse siin IPv4 pÀis nii:

$ grep -A 12 'struct iphdr {' vmlinux.h
struct iphdr {
    __u8 ihl: 4;
    __u8 version: 4;
    __u8 tos;
    __be16 tot_len;
    __be16 id;
    __be16 frag_off;
    __u8 ttl;
    __u8 protocol;
    __sum16 check;
    __be32 saddr;
    __be32 daddr;
};

NĂŒĂŒd kirjutame oma BPF programmi C keeles:

$ cat xdp-simple.bpf.c
#include "vmlinux.h"
#include 

SEC("xdp/simple")
int simple(void *ctx)
{
        return XDP_PASS;
}

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

Kuigi meie programm on vĂ€ga lihtne, peame siiski tĂ€helepanu pöörama mitmetele detailidele. Esiteks on esimene pĂ€isfail, mida me kaasame, vmlinux.h, mille me just genereerisime abiga bpftool btf dump — nĂŒĂŒd ei pea me enam installima kernel-headers paketti, et teada saada, kuidas tuumstruktuurid vĂ€lja nĂ€evad. JĂ€rgmine pĂ€isfail tuleb meile raamatukogust libbpf. Praegu on see meile vajalik ainult selleks, et makro SEC, mis saadab sĂŒmboli ELF objektifaili vastavasse sektsiooni, mÀÀratletaks. Meie programm on sektsioonis xdp/simple, kus enne kaldkriipsu mÀÀrame BPF programmi tĂŒĂŒbi — see on kokkulepe, mida kasutatakse libbpf, mis pĂ”hineb sektsiooni nimele, sisestab Ă”ige tĂŒĂŒbi kĂ€ivitamisel bpf(2). BPF programm ise on C — vĂ€ga lihtne ja koosneb ĂŒhisest reast return XDP_PASS. LĂ”puks sisaldab eraldi sektsioon "license" litsentsi nime.

Saame meie programmi kompileerida llvm/clang abil, versiooniga >= 10.0.0, veel parem — uuema (vt jaotist Arendustooted):

$ 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

Huvitavad omadused: me mÀÀrame sihtarhitektuuri -target bpf ja teed pĂ€isfailideni libbpf, mille me hiljuti installisime. Samuti Ă€rge unustage -O2, ilma selle valikuta vĂ”ivad teid oodata edaspidi ĂŒllatused. Vaatame meie koodi, kas suutsime kirjutada programmi, mida soovisime?

$ llvm-objdump --section=xdp/simple --no-show-raw-insn -D xdp-simple.bpf.o

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

Sektsiooni xdp/simple dekoodeerimine:

0000000000000000 :
       0:       r0 = 2
       1:       exit

Jah, see Ă”nnestus! NĂŒĂŒd on meil binaarfail programmiga ja tahame luua rakenduse, mis laadib selle tuuma. Selleks pakub raamatukogu meile kahte vĂ”imalust — kasutada madalama taseme API-d vĂ”i kĂ”rgema taseme API-d. Me valime teise tee, kuna tahame Ă”ppida, kuidas kirjutada, laadida ja ĂŒhendada BPF programme minimaalse vaevaga nende edasise uurimise jaoks. libbpf Esiteks peame genereerima meie programmi "skelett" sellest binaarfailist sama utiliidi abil

Alustuseks peame meie programmi „skeleti“ genereerima selle binaarfailist, kasutades sama utiliiti. bpftool — ĆĄveitsi armee noa BPF (mida saab mĂ”ista ka otseselt, sest Daniel Borkman on ĂŒks BPF loojatest ja hoidlaseisunditest — ĆĄveitslane):

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

Failis xdp-simple.skel.h Sisaldab meie programmi binaarkoodi ja funktsioone haldamiseks — laadimiseks, ĂŒhendamiseks, meie objekti eemaldamiseks. Meie lihtsal juhul nĂ€eb see vĂ€lja ĂŒlemÀÀrasena, kuid see töötab ka olukordades, kus objektifail sisaldab palju BPF programme ja kaarte, ning selle tohutu ELF laadimiseks piisab, kui genereerida skelett ja kutsuda ĂŒhe vĂ”i kahe funktsiooni kasutajarakendusest, mille kirjutamise juurde me nĂŒĂŒd jĂ”uame.

Tegelikult on meie laadimisprogramm triviaalne:

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

Siin struct xdp_simple_bpf mÀÀratakse failis xdp-simple.skel.h ja kirjeldab meie objektifaili:

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

Siit vĂ”ib mĂ€rgata madalamal tasemel API jĂ€lgi: struktuur struct bpf_program *simple ja struct bpf_link *simple. Esimene struktuur kirjeldab konkreetselt meie programmi, mis on salvestatud sektsiooni xdp/simple, ja teine kirjeldab seda, kuidas programm on ĂŒhendatud sĂŒndmuste allikaga.

Funktsioon xdp_simple_bpf__open_and_load, avab ELF objekti, parsib selle, loob kĂ”ik struktuurid ja alastruktuurid (kuna ELF-is on programme ja teisi sektsioone — andmed, ainult lugemiseks mĂ”eldud andmed, silumisinfo, litsents jne), ja laadib siis sĂŒdamikku sĂŒsteemikĂ”ne kaudu bpf, mille vĂ”ime kontrollida, kompileerides ja kĂ€ivitades programmi:

$ 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

Vaatame nĂŒĂŒd oma programmi lĂ€bi bpftool. Leiame selle ID:

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

ja teeme dumpimise (kasutame kĂ€skluse lĂŒhendatud vormi bpftool prog dump xlated):

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

Midagi uut! Programm printis meie allika faili osad C keeles. Seda tegi teek libbpf, mis leidis binaaris silumissektsiooni, kompileeris selle BTF objektiks, laadis selle sĂŒdamikku kĂ€sku BPF_BTF_LOAD, ja seejĂ€rel kasutas saadud failide descriptorit programmi laadimiseks kĂ€su BPG_PROG_LOAD.

Kernel Helpers

BPF-i programmid vĂ”ivad kĂ€ivitada 'vĂ€list' funktsioone — kernel helpers. Need abifunktsioonid vĂ”imaldavad BPF-i programmidel pÀÀseda ligi kernelistruktuuridele, hallata maps'e ning suhelda 'reaalse maailmaga' — luua perf events, hallata riistvara (nt suunata pakette) jne.

NĂ€ide: bpf_get_smp_processor_id

Konteksti 'Ă”pime nĂ€idete kaudu' raames vaatame ĂŒhte abifunktsiooni, bpf_get_smp_processor_id(), konkreetne failis kernel/bpf/helpers.c. See tagastab protsessori numbri, millel kĂ€ivitub seda kutsuv BPF-i programm. Kuid meid huvitab rohkem mitte selle semantika, vaid see, et selle rakendamine vĂ”tab ĂŒhe rea:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF-i abifunktsioonide mÀÀratlemine sarnaneb Linuxi sĂŒsteemikĂ”nede mÀÀratlemisega. Siin mÀÀratletakse nĂ€iteks argumentideta funktsioon. (Funktsioon, mis vĂ”tab nĂ€iteks kolm argumenti, mÀÀratletakse makroga BPF_CALL_3. Maksimaalne argumentide arv on viis.) Kuid see on ainult mÀÀratlemise esmane osa. Teine osa seisneb tĂŒĂŒbi mÀÀratlemises struct bpf_func_proto, mis sisaldab abifunktsiooni kirjeldust, mis on verifikaatorile arusaadav:

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

Abifunktsioonide registreerimine

Selleks, et teatud tĂŒĂŒpi BPF-i programmid saaksid seda funktsiooni kasutada, peavad nad selle registreerima, nĂ€iteks tĂŒĂŒbi jaoks BPF_PROG_TYPE_XDP kernelis mÀÀratletakse funktsioon xdp_func_proto, mis funktsiooni ID kaudu mÀÀrab, kas XDP toetab seda funktsiooni vĂ”i mitte. Meie funktsiooni toetab:

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

Uued BPF-i programmide tĂŒĂŒbid 'mÀÀratakse' failis include/linux/bpf_types.h makro abil BPF_PROG_TYPE. 'MÀÀratlemine' on jutumĂ€rkides, kuna see on ratsionaalne mÀÀratlemine, kuid C keele kontekstis mÀÀratakse konkreetsete struktuuride kogum teistes kohtades. Eriti failis kernel/bpf/verifier.c kĂ”ik mÀÀratlused failist bpf_types.h kasutatakse struktuuride massiivi loomiseks 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 
#undef BPF_PROG_TYPE
};

See, for each type of BPF program, a pointer to a data type structure is defined struct bpf_verifier_ops, which is initialized with the value _name ## _verifier_ops, i.e., xdp_verifier_ops jaoks xdp. The structure xdp_verifier_ops is defined failis net/core/filter.c jÀrgmiselt:

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

Here we see our familiar function xdp_func_proto, which will be invoked by the verifier every time it encounters a call of some function inside the BPF program, see verifier.c.

Let's look at how a hypothetical BPF program uses the function bpf_get_smp_processor_id. To do this, we will rewrite the program from our previous section as follows:

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

SEC("xdp/simple")
int simple(void *ctx)
{
    if (bpf_get_smp_processor_id() != 0)
        return XDP_DROP;
    return XDP_PASS;
}

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

Symbol bpf_get_smp_processor_id is defined ja <bpf/bpf_helper_defs.h> teeki libbpf kuidas

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

that is, bpf_get_smp_processor_id — this is a pointer to a function, the value of which is 8, where 8 is the value BPF_FUNC_get_smp_processor_id tĂŒĂŒpi enum bpf_fun_id, which is defined for us in the file vmlinux.h (the file bpf_helper_defs.h is generated by a script in the kernel, so ‘magic’ numbers are ok). This function takes no arguments and returns a value of type __u32. When we run it in our program, clang it generates an instruction BPF_CALL of the ‘correct form’. Let's compile the program and look at the section 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 :
       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
       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 
       6:       b7 00 00 00 01 00 00 00 r0 = 1

0000000000000038 :
       7:       95 00 00 00 00 00 00 00 exit

In the very first line, we see the instruction call, parameeter IMM which equals 8, and SRC_REG — zero. According to the ABI convention used by the verifier, this is indeed a call to the helper function number eight. After it is executed, the logic is straightforward. The returned value from the register r0 is copied into r1 and on lines 2,3 converted to the type u32 — the upper 32 bits are zeroed. In lines 4,5,6,7 we return 2 (XDP_PASS) or 1 (XDP_DROP) depending on whether the helper function from line 0 returned zero or non-zero.

Let's check ourselves: load the program and look at the output 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
   4: (b7) r0 = 2
; }
   5: (15) if r1 == 0x0 goto pc+1
   6: (b7) r0 = 1
   7: (95) exit

HÀsti, verifikaator leidis Ôige kernel-helperi.

NÀide: edastame argumendid ja lÔpuks kÀivitame programmi!

KĂ”ik jooksutamise abifunktsioonid omavad prototĂŒĂŒpe

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

Abifunktsioonide parameetrid edastatakse registrites r1—r5, ja tagastatav vÀÀrtus on registris r0. Funktsioone, mis vĂ”tavad rohkem kui viis argumenti, ei ole ja nende toetamine ei ole tulevikus plaanis.

Vaadakem uut kernel helperit ja seda, kuidas BPF edastab parameetreid. Kirjutame ĂŒmber xdp-simple.bpf.c jĂ€rgmiste sĂ”nadega (muud read ei muutunud):

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

Meie programm prindib CPU numbri, millel see töötab. Kompileerime selle ja vaatame koodi:

$ llvm-objdump -D --section=xdp/simple --no-show-raw-insn xdp-simple.bpf.o

0000000000000000 :
       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

Ridade 0-7 salvestame kuhja rea running on CPU%un, ja siis real 8 kĂ€ivitame tuttava bpf_get_smp_processor_id. Ridades 9-12 valmistame ette abifunktsiooni bpf_printk — registrid r1, r2, r3. Miks neid on kolm, mitte kaks? Sest bpf_printk — see on makro, mis ĂŒmbritseb tĂ”elist abifunktsiooni bpf_trace_printk, millele tuleb edastada formaadi stringi suurus.

NĂŒĂŒd lisame paar rida xdp-simple.c, et meie programm ĂŒhenduks liidesega lo ja töötaks tĂ”eliselt!

$ cat xdp-simple.c
#include 
#include 
#include 
#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);
}

Siin kasutame funktsiooni bpf_set_link_xdp_fd, mis ĂŒhendab BPF XDP tĂŒĂŒpi programmid vĂ”rguliidestega. Oleme kĂ”va koodiga mÀÀranud liidese numbri lo, mis on alati 1. KĂ€ivitame funktsiooni kaks korda, et esmalt katkestada vana programm, kui see on ĂŒhendatud. Pange tĂ€hele, et nĂŒĂŒd ei ole meil enam vaja kutsuda pause vĂ”i lĂ”pmatust tsĂŒklit: meie laadimisprogramm lĂ”petab töö, kuid BPF-programm ei hĂ€vine, kuna see on ĂŒhendatud sĂŒndmuste allikaga. PĂ€rast eduka laadimise ja ĂŒhendamise korral kĂ€ivitatakse programm iga vĂ”rkpaketi jaoks, mis jĂ”uab lo.

Laadime programmi ja vaatame liidest lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp name simple tag 4fca62e77ccb43d6 gpl
$ ip l show dev lo
1: lo:  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

Laaditud programm on ID-ga 669 ja sama ID on nÀha liideses lo. Saatame paar paketti aadressile 127.0.0.1 (pÀring + vastus):

$ ping -c1 localhost

ja nĂŒĂŒd vaatame silumine virtuaalsesse faili /sys/kernel/debug/tracing/trace_pipe, kuhu bpf_printk kirjutab oma sĂ”numid:

# 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

Kaks paketti tuvastati lo ja töödeldi CPU0-l — meie esimene tĂ€ielik mĂ”tetu BPF-programm töötas!

Tuleb mÀrkida, et bpf_printk ei kirjuta asjata silumise faili: see ei ole parim abivahend tootmiskeskkonnas kasutamiseks, kuid meie eesmÀrk oli nÀidata midagi lihtsat.

JuurdepÀÀs BPF-programmidest maps

NĂ€ide: kasutame mappi BPF-programmist

Eelmistes lĂ”ikudes Ă”ppisime looma ja kasutama mappe kasutaja ruumis, nĂŒĂŒd vaatame tuuma osa. Alustame, nagu tavaliselt, nĂ€itest. Kirjutame oma programmi ĂŒmber xdp-simple.bpf.c jĂ€rgmiselt:

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

Programmi alguses lisasime mapi defineerimise woo: see on 8 elemendi massiiv, kuhu salvestatakse andmed, mille tĂŒĂŒp on u64 (C-s defineeriksime sellise massiivi kui u64 woo[8]). Programmis "xdp/simple" saame kĂ€imasoleva protsessori numbri muutuja key ja seejĂ€rel helper-funktsiooni abil bpf_map_lookup_element saame vastava kirje massiivist ja suurendame seda ĂŒhe vĂ”rra. Venekeelsena: loeme statistikat selle kohta, millisel CPU-l on töödeldud sissetulevad paketid. Proovime programmi kĂ€ivitada:

$ 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

Kontrollime, et see on ĂŒhendatud lo ja saadame natuke pakette:

$ ip l show dev lo
1: lo:  mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 108

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done

NĂŒĂŒd vaatame massiivi sisu:

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

Peaaegu kĂ”ik protsessid töötati CPU7 peal. Meie jaoks pole see oluline, peamine on, et programm töötab ja me mĂ”istsime, kuidas pÀÀseda BPF-i kaartidele — kasutades abiainete bpf_mp_*.

MĂŒstiline nĂ€itaja

Nii saame me BPF-programmist kaardile juurdepÀÀsu, kasutades kÔnesid nagu

val = bpf_map_lookup_elem(&woo, &key);

kus abifunktsioon nÀeb vÀlja nagu

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

aga me edastame nÀitaja &woo nimetule struktuurile struct { ... }


Kui vaatame programmi assemblerit, siis nÀeme, et vÀÀrtus &woo ei ole tegelikult mÀÀratletud (rida 4):

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

xdp-simple.bpf.o:       faili formaat elf64-bpf

XRP lihtsate sektsioonide vÀljavÔte:

0000000000000000 :
       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 sisaldub relegatsioonides:

$ 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

Aga kui vaatame juba laaditud programmi, siis nÀeme viit Ôigesse kaardile (rida 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]
...

Nii saame jĂ€reldada, et meie ĂŒhenduslikus programmi kĂ€ivitamise hetkel viidatakse &woo oli asendatud midagi raamatukoguga libbpf. Esmalt vaatame vĂ€ljundit 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

Me nÀeme, et libbpf loomine kaardina woo ja seejÀrel laadis meie programmi simple. Vaatame lÀhemalt, kuidas me programmi laadime:

  • kutsume xdp_simple_bpf__open_and_load failist xdp-simple.skel.h
  • mis kutsub esile xdp_simple_bpf__load failist xdp-simple.skel.h
  • mis kutsub esile bpf_object__load_skeleton failist libbpf/src/libbpf.c
  • mis kutsub esile bpf_object__load_xattr API-s libbpf/src/libbpf.c

Viimane funktsioon kutsub muuhulgas esile bpf_object__create_maps, mis loob vĂ”i avab olemasolevaid kaarte, muutes need failikirjeldajateks. (Siin me nĂ€eme BPF_MAP_CREATE vĂ€ljastuses strace.) Edasi kutsutakse funktsioon bpf_object__relocate , ja just see meid huvitab, kuna me mĂ€letame, et nĂ€gime woo ĂŒlejÀÀnud tabelis. Seda uurides jĂ”uame lĂ”puks funktsiooni bpf_program__relocate, mis tegeleb kaartide relokatsioonidega:

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

Nii et me vÔtame meie kÀsu

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

ja asendame selles allika registri BPF_PSEUDO_MAP_FD, ja esimese IMM failikirjeldajaga meie kaardist. Ja kui see on nÀiteks 0xdeadbeef, siis tulemuseks saame kÀsu

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

Nii edastatakse teave kaartide kohta konkreetsele laaditud BPF programmile. Samal ajal vÔib kaart olla loodud BPF_MAP_CREATE, vÔi avatud ID-ga, kasutades BPF_MAP_GET_FD_BY_ID.

Seega, kasutades libbpf on algoritm jÀrgmine:

  • kompileerimise ajal luuakse viidete jaoks kaartidele relokatsiooni tabelis kirjed
  • libbpf ava ELF objekt, leidke kĂ”ik kasutatud kaardid ja looge neile failikirjeldajad
  • failikirjeldajad laetakse tuuma osana kĂ€skudest LD64

Nagu aru saate, see pole veel kĂ”ik ja meil tuleb vaadata tuuma. Õnneks on meil vihje — oleme mÀÀranud vÀÀrtuse BPF_PSEUDO_MAP_FD allika registrisse ja saame seda otsida, mis viib meid kĂ”igi pĂŒhamate pĂŒhade — kernel/bpf/verifier.c, kus funktsioon, millel on iseloomulik nimi, asendab failikirjeldaja struktuuri aadressiga 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;

(tÀielik kood on saadaval linki pidi). Nii et saame tÀiendada meie algoritmi:

  • programmi laadimise ajal kontrollib verifier kaardi Ă”iget kasutamist ja mÀÀrab vastava struktuuri aadressi struct bpf_map

Eesti ELF binaari laadimisel libbpf juhtuda veel palju sĂŒndmusi, kuid me arutame seda teiste artiklite raames.

Laadime programme ja kaarte ilma libbpf-ita

Nagu lubatud, siin on nÀide lugejatele, kes soovivad teada, kuidas luua ja laadida programmi, mis kasutab kaarte, ilma abita. libbpf. See vÔib olla kasulik, kui töötate keskkonnas, kus te ei saa sÔltuvusi kokku koguda, vÔi sÀÀstate iga bitti vÔi kirjutate sarnast programmi ply, mis genereerib BPF binaarkoodi lennult.

Kergemaks jĂ€rgimiseks kirjutame meie nĂ€ite ĂŒmber xdp-simple. TĂ€ieliku ja veidi laiendatud programmi koodi, mida selles nĂ€ites kĂ€sitleme, leiate siit gist.

Meie rakenduse loogika on jÀrgmine:

  • luua kaarditĂŒĂŒp BPF_MAP_TYPE_ARRAY kĂ€skluse abil BPF_MAP_CREATE,
  • luua programm, mis kasutab seda kaarti,
  • ĂŒhendada programm liidesega lo,

mis tÔlgib inimkeelde kui

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

Siin map_create loob kaardi tĂ€pselt nii, nagu me seda tegime esimeses nĂ€ites sĂŒsteemikĂ”nest bpf — "tuum, palun tee mulle uus kaart massiivi kujul, kus on 8 elementi tĂŒĂŒpi __u64 ja naita mulle failide deskriptor":

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

Programm laaditakse samuti lihtsalt:

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

Raske osa prog_load — see on meie BPF programmi mÀÀratlemine struktuuride massiivina struct bpf_insn insns[]. Kuid kuna me kasutame programmi, mis meil on C-s, saame natuke pettuse kasutada:

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

0000000000000000 :
       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 
       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 :
      13:       bf 10 00 00 00 00 00 00 r0 = r1
      14:       95 00 00 00 00 00 00 00 exit

KokkuvĂ”ttes peame kirjutama 14 kĂ€sku struktuuride tĂŒĂŒpidena struct bpf_insn (nĂ”uanne: vĂ”tke ĂŒlevaade, lugege uuesti juhendit, avage linux/bpf.h ja linux/bpf_common.h ja proovige tuvastada struct bpf_insn insns[] iseselt):

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

Harjutus neile, kes seda ise ei kirjutanud — leidke map_fd.

Meie programmis on veel ĂŒks avaldamata osa — xdp_attach. Kahjuks ei saa XDP tĂŒĂŒpi programme sĂŒsteemi kutse kaudu ĂŒhenduda bpf. Inimesed, kes lĂ”id BPF-i ja XDP-d, olid Linuxi vĂ”rgu kogukonnast, seega kasutasid nad kĂ”ige tuttavamat neile (aga mitte tavalistele inimestele) suhtlemisliidest tuumaga: netlink sockets, vt ka RFC3549. Lihtsaim viis rakendamiseks xdp_attach — on koodi kopeerimine libbpf, nimelt failist netlink.c, mida me just tegime, seda veidi lĂŒhendades:

Tere tulemast netlink socketite maailma

Avame netlink socketi tĂŒĂŒbi 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;
}

Loeme sellisest soketist:

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 nlmsg_pid != nl_pid)
                errx(1, "vale 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); liiga palju koodi, mida kopeerida...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

LÔpuks, siin on meie funktsioon, mis avab soketi ja saadab sellesse spetsiaalse sÔnumi, mis sisaldab failihalduri descriptorit:

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

Nii, kÔik on testimiseks valmis:

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

Vaadakem, kas meie programm on ĂŒhendatud lo:

$ ip l show dev lo
1: lo:  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

Saadame ping-sÔnumeid ja vaatame kaarti:

$ 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
Leidsime 8 elementi

HĂ€sti, kĂ”ik töötab. MĂ€rkige, et meie kaart kuvatakse jĂ€lle baitidena. See tuleneb sellest, et erinevalt libbpf ei ole me tĂŒĂŒbinfot (BTF) laadinud. Kuid sellest rÀÀgime jĂ€rgmine kord.

Arendustooted

Selles osas vaatame pÔhitehniliste tööriistade komplekti BPF arendajatele.

Üldiselt ei ole BPF programmide arendamiseks midagi erilist vaja — BPF töötab igas korralikus jaotuse tĂ”ukes ja programmid koostatakse clang, mille saab paketi kaudu installida. Ent kuna BPF on arenduses, muutuvad tuum ja tööriistad pidevalt. Kui te ei soovi kirjutada BPF programme eelmise sajandi meetoditega, peate selle koguma.

  • llvm/clang
  • pahole
  • oma tuum
  • bpftool

(Teabe jaoks: see ja kÔik nÀidised artiklis jooksutati Debian 10-l.)

llvm/clang

BPF töötab koos LLVM-iga ja kuigi hiljutistel aegadel saab BPF programme kompileerida ka gcc abil, toimub kogu praegune arendus LLVM-i jaoks. Seega kogume kÔigepealt praeguse versiooni clang git-ist:

$ 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
... palju aega hiljem
$

NĂŒĂŒd saame kontrollida, kas kĂ”ik on Ă”igesti kokku pandud:

$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM versioon 11.0.0git
  Optimeeritud versioon.
  Vaikimisi sihtmÀrk: x86_64-unknown-linux-gnu
  Host CPU: znver1

  Registreeritud sihtmÀrkide
    bpf    - BPF (host endian)
    bpfeb  - BPF (big endian)
    bpfel  - BPF (little endian)
    x86    - 32-bit X86: Pentium-Pro ja kÔrgem
    x86-64 - 64-bit X86: EM64T ja AMD64

(Kogumise juhised clang on mulle saadud bpf_devel_QA.)

Me ei paigalda alles kogutud programme, vaid lisame need lihtsalt PATH, nÀiteks:

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

(Selle saab lisada .bashrc vÔi eraldi faili. Isiklikult lisan sellised asjad ~/.bin/activate-llvm.sh ja kui on vaja, teen . activate-llvm.sh.)

Pahole ja BTF

Utiliit pahole kasutatakse tuuma kogumise ajal BTF formaadis silumisinfote loobmiseks. Me ei kĂ€sitle selle artikli raames BTF tehnoloogia ĂŒksikasju, vĂ€lja arvatud see, et see on mugav ja me tahame seda kasutada. Seega, kui kavatsete koguda oma tuuma, koguge esmalt pahole (ilma pahole te ei saa tuuma koguda, kui valik 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

Tuuma BPF-iga eksperimenteerimiseks

BPF vĂ”imaluste uurimisel tasub kokku panna oma kerneli. Üldiselt ei ole see kohustuslik, kuna saate kokku panna ja laadida BPF programme ka jaotuse kernelil, kuid oma kernel vĂ”imaldab kasutada kĂ”ige uuemaid BPF vĂ”imalusi, mis jĂ”uavad teie jaotusse parimal juhul alles pĂ€rast mitut kuud vĂ”i, nagu mĂ”nedate silumisriistade puhul, ei panda neid ĂŒldse lĂ€hitulevikus kokku. Samuti vĂ”imaldab oma kernel tunda end olulisena, katsetades koodi.

Kernel’i ehitamiseks vajate kĂ”igepealt ise kernelit ja teiseks kernelikonfiguratsiooni faili. BPF eksperimentide jaoks saame kasutada tavapĂ€rast puhast kernelit vĂ”i ĂŒht arenduskernelit. Ajalooliselt toimub BPF arendamine Linuxi vĂ”rgukogukonna raames ja seega lĂ€hevad kĂ”ik muudatused varem vĂ”i hiljem lĂ€bi David Milleri (David Miller) — Linuxi vĂ”rguosa hooldaja. sĂ”ltuvalt oma iseloomust — parandused vĂ”i uued funktsioonid — jĂ”uavad vĂ”rgu muudatused ĂŒhte kahest kernelist — net vĂ”i net-next. BPF muudatused jaotatakse samuti bpf ja bpf-next, mis hiljem jĂ”uavad vastavalt net-i ja net-next-i. Lisainfot vt bpf_devel_QA ja netdev-FAQ. Valige seega kernel oma maitse ja sĂŒsteemi stabiilsuse vajaduste pĂ”hjal, millel testite (*-next kerneli on kĂ”ige ebastabiilsemad kĂ”ikidest loetletud).

KĂ€esoleva artikli raames ei ole juttu kernelikonfiguratsiooni failide haldamisest — eeldatakse, et oskate seda juba teha vĂ”i olete valmis ise Ă”ppima. Siiski, jĂ€rgmised juhised peaksid olema enam-vĂ€hem piisavad, et teil oleks toimiv sĂŒsteem BPF toe saavutamiseks.

Laadige alla ĂŒks ĂŒlalmainitud kernelitest:

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

KĂŒpsetage minimaalne töötav kernelikonfiguratsioon:

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

Aktiveerige BPF valikud failis .config oma valiku jÀrgi (tÔenÀoliselt on CONFIG_BPF juba aktiveeritud, kuna seda kasutab systemd). Siin on valikuvÔimaluste loetelu kernelist, mida selle artikli jaoks kasutati:

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

JÀrgmiseks saame kergesti koguda ja installida mooduleid ja kerneli (muide, kerneli saab koguda just vÀrskelt koostatud) clang, lisades CC=clang):

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

ja taaskÀivitades uue kerneliga (kasutan selleks kexec paketist kexec-tools):

v=5.8.0-rc6+ # kui uuendate olemasolevat kernelit, siis saate teha v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

bpftool

Artiklis kĂ”ige sagedamini kasutatav utiliit on utiliit bpftool, mis on tarnitud Linuxi kerneli koosseisus. Selle on kirjutanud ja hooldanud BPF arendajad BPF arendajatele ning selle abil saab hallata kĂ”iki BPF objekte — laadida programme, luua ja muuta kaarte, uurida BPF ökosĂŒsteemi elu jne. Dokumentatsiooni man pages'i allikaid saab leida kernelis vĂ”i juba kompileerituna, internetis.

Artikli kirjutamise hetkeks bpftool tarnitakse valmis kujul ainult RHELile, Federale ja Ubuntule (vt nĂ€iteks, see teema, kus rÀÀgitakse lĂ”petamata loost pakendamisest bpftool Debianis). Kuid kui olete juba oma kernelit kogunud, siis on selle kogumine imelihtne: bpftool $ cd ${linux}/tools/bpf/bpftool # ... kirjuta viimase clang'i teed nagu eespool kirjeldatud $ make -sSĂŒsteemi omaduste automaatne tuvastamine: ... libbfd: [ on ] ... disassembler-four-args: [ on ] ... zlib: [ on ] ... libcap: [ on ] ... clang-bpf-co-re: [ on ]SĂŒsteemi omaduste automaatne tuvastamine: ... libelf: [ on ] ... zlib: [ on ] ... bpf: [ on ]$

(siin

${linux} — see on teie kerneli kataloog.) PĂ€rast nende kĂ€skude tĂ€itmist kolitakse katalooge bpftool ${linux}/tools/bpf/bpftool ja seda saab kirja panna teele (esimesele kasutajale ) vĂ”i lihtsalt kopeerida rootKogumine /usr/local/sbin.

on kĂ”ige parem lĂ€bi viia viimasel bpftool , nagu eespool kirjeldatud, ning kontrollida, kas see on Ă”igesti koostatud — nĂ€iteks jĂ€rgmise kĂ€su abil clang$ sudo bpftool feature probe kernel SĂŒsteemi konfiguratsiooni skaneerimine... bpf() syscall mittenĂ”utud kasutajatele on lubatud JIT kompilaator on lubatud JIT kompilaatorite kĂ”vade seadistuste muutmine on keelatud JIT kompilaatori kallasymlsi ekspordid on juurte jaoks lubatud ...

mis nÀitab, millised BPF omadused on teil kernelis lubatud.

Muide, eelmist kÀsku saab kÀivitada nagu

See on sarnane utiliitide tööriistade pakettidest

# bpftool f p k

See on tehtud sarnaselt paketi utiliitidele. iproute2, kus me saame nÀiteks öelda ip a s eth0 asemel ip addr show dev eth0.

KokkuvÔte

BPF vĂ”imaldab tĂ”husalt hiire suurust mÔÔta ja sĂŒsteemi tuuma funktsionaalsust reaalajas muuta. SĂŒsteem on osutunud vĂ€ga edukaks, jĂ€rgides parimaid UNIX-i traditsioone: lihtne mehhanism, mis vĂ”imaldab tuuma (uuesti)programmeerida, on vĂ”imaldanud tohutul hulgal inimesi ja organisatsioone eksperimenteerida. Ja kuigi eksperimentide, nagu ka BPF infrastruktuuri arendamise, lĂ”pp on veel kaugel, on sĂŒsteemil juba stabiilne ABI, mis vĂ”imaldab luua usaldusvÀÀrset ja mis kĂ”ige tĂ€htsam, efektiivset Ă€riloogikat.

Tahaksin mĂ€rkida, et minu arvates on tehnoloogia saamas nii populaarseks, kuna selle kaudu saab mĂ€ngida (masina arhitektuuri vĂ”ib enam-vĂ€hem mĂ”ista ĂŒhe Ă”htu jooksul), ning samas lahendada ĂŒlesandeid, mis enne selle ilmumist ei olnud (ilusasti) lahendatavad. Need kaks komponenti koos panevad inimesi eksperimenteerima ja unistama, mis viib uute ja uute uuenduste tekkimiseni.

See artikkel, kuigi ei osutunud eriti lĂŒhikeseks, on vaid sissejuhatus BPF maailma ning ei kĂ€sitle „edasiarendatud“ vĂ”imalusi ja olulisi arhitektuuri osi. Edasi-plaan on ligikaudu jĂ€rgmine: jĂ€rgmine artikkel kĂ€sitleb BPF programmide tĂŒĂŒpide ĂŒlevaadet (kernel 5.8 toetab 30 programmi tĂŒĂŒpi), seejĂ€rel vaatame lĂ”puks, kuidas kirjutada tĂ”elisi rakendusi BPF, kasutades tuuma jĂ€lgimise programme, seejĂ€rel saab aeg sĂŒvitsi minekuks BPF arhitektuuri juurde ja seejĂ€rel tulevad nĂ€ited vĂ”rgurakendustest ja BPF turbarakendustest.

Selles tsĂŒklis eelnevad artiklid

  1. BPF kÔige vÀiksematele, osa null: klassikaline BPF

Lingid

  1. BPF ja XDP Viidatud Juhend — BPF dokumentatsioon ciliumilt, tĂ€psemalt Daniel Borkmanilt, ĂŒhelt BPF looja ja hooldaja poolt. See on ĂŒks esimesi tĂ”siseid kirjeldusi, mis eristub ĂŒlejÀÀnutest, kuna Daniel teab tĂ€pselt, millest ta kirjutab ja vigu seal ei leidu. EelkĂ”ige kĂ€sitletakse selles dokumendis, kuidas töötada BPF programmidega, mille tĂŒĂŒbid on XDP ja TC tuntud utiliidi abil ip paketist iproute2.

  2. Dokumentatsioon/networking/filter.txt — originaaldokument klassikalise, ja seejĂ€rel laiendatud BPF kohta. Kasulik lugeda, kui soovite sĂŒveneda assemblerisse ja arhitektuuri tehnilistesse detailidesse.

  3. BPF blogi facebookist. Uuendatakse harva, kuid tĂ€pselt, kuna kirjutavad Alexei Starovoitov (eBPF autor) ja Andrii Nakryiko — (hooldaja libbpf).

  4. bpftooli saladused. Huvitav Twitteri teema Quentin Monnetilt nÀidete ja bpftooli kasutamise saladustega.

  5. Sukeldu BPF-i: lugemisvĂ”imaluste loend. Üks suurimaid (ja jĂ€tkuvalt toetatavaid) linkide loetelusid BPF-i dokumentatsioonile Quentin Monnetilt.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster