BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Në fillim ishte teknologjia dhe quhej BPF. Ne e shqyrtuam atë në të kaluarën, artikullin e vjetër, të këtij cikli. Në vitin 2013, me ndihmën e Aleksej Starovoitov (Alexei Starovoitov) dhe Daniel Borkman (Daniel Borkman) u zhvillua dhe u përfshi në bërthamën e Linux një version i përmirësuar i saj, i optimizuar për makinat moderne 64-bit. Kjo teknologji e re për një kohë të shkurtër kishte emrin Internal BPF, pastaj u riformua në Extended BPF, dhe tani, pas disa viteve, të gjithë e quajnë thjesht BPF.

Në terma të përgjithshëm, BPF lejon ekzekutimin e kodit të rastësishëm, të ofruar nga përdoruesi, në hapësirën e bërthamës së Linux dhe kjo arkitekturë doli aq e suksesshme, saqë do na duhen edhe një duzinë artikuj për të përshkruar të gjitha aplikimet e saj. (E vetmja gjë që zhvilluesit nuk arritën, siç mund ta shihni në kpdv më poshtë, është krijimi i një logoje të denjë.)

Ky artikull përshkruan strukturen e mesin virtual BPF, ndërfaqet e bërthamës për punën me BPF, mjetet e zhvillimit, si dhe një përmbledhje të shkurtër, shumë të shkurtër, të mundësive ekzistuese, pra, gjithçka që do na nevojitet në vijim për një studim më të thellë të aplikimeve praktike të BPF.
BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Përmbledhje e shkurtër e artikullit

Hyrje në arkitekturën BPF. Fillimisht do të shohim arkitekturën BPF nga lart dhe do të identifikojmë komponentët kryesorë.

Regjistrat dhe sistemi i komandave të mesin virtual BPF. Duke pasur tashmë një kuptim të përgjithshëm të arkitekturës, do të përshkruajmë strukturën e mesin virtual BPF.

Cikli i jetës së objekteve BPF, sistemi i skedarëve bpffs. Në këtë pjesë do të hedhim një vështrim më të afërt në ciklin e jetës së objekteve BPF - programe dhe harta.

Menaxhimi i objekteve përmes thirrjes sistemore bpf. Duke pasur tani njëfarë kuptimi të sistemit, përfundimisht do të shohim se si të krijojmë dhe menaxhojmë objekte nga hapësira e përdoruesit përmes një thirrjeje të veçantë sistemore - bpf(2).

Krijimi i programeve BPF me ndihmën e libbpf. Sigurisht, është e mundur të shkruani programe përmes thirrjes sistemore. Por e vështirë. Për një skenar më real, programuesit e bërthamës zhvilluan bibliotekën libbpf. Ne do të krijojmë një skelet të thjeshtë të aplikacionit BPF, të cilin do ta përdorim në shembujt e ardhshëm.

NdihmĂ«sit e BĂ«rthamĂ«s. KĂ«tu do tĂ« mĂ«sojmĂ« si programet BPF mund tĂ« drejtohen nĂ« funksione ndihmĂ«se tĂ« kernelit — njĂ« mjet qĂ«, sĂ« bashku me harta, zgjeron ndjeshĂ«m mundĂ«sitĂ« e reja BPF nĂ« krahasim me BPF klasik.

Qasja në harta nga programet BPF. Në këtë pikë do të dimë mjaftueshëm për të kuptuar se si mund të krijojmë fragmente kodesh që përdorin harta. Edhe do të hedhim një vështrim të vogël në verifier-in e madh e të fuqishëm.

Mjetet e zhvillimit. Një seksion referencë mbi mënyrën se si të ndërtoni mjetet dhe kernelin e nevojshëm për eksperimente.

Konkluzion. Në fund të artikullit, ata që arrijnë deri aty do të gjejnë fjalë motivuese dhe një përshkrim të shkurtër të asaj që do të vijë në artikujt e ardhshëm. Ne gjithashtu do të listojmë një numër lidhjesh për studim të pavarur për ata që nuk kanë dëshirë ose mundësi të presin vazhdimin.

Hyrje në arkitekturën BPF

Para se të fillojmë të shqyrtojmë arkitekturën BPF, për herë të fundit do të referohemi në BPF klasik, i cili u zhvillua si një përgjigje ndaj shfaqjes së mjeteve RISC dhe zgjidhte problemin e filtrimit efikas të paketave. Arkitektura doli aq e suksesshme, sa që, duke u lindur në vitet e vështira në Berkeley UNIX, u portua në shumicën e sistemeve operative ekzistuese, arriti deri në vitet e çmendura njëzet dhe ende gjen aplikime të reja.

BPF e re u zhvillua si një përgjigje ndaj përhapjes së gjerë të mjeteve 64-bit, shërbimeve në re dhe nevojave në rritje për mjete për krijimin e SDN (Ssoftuerddefinuar nrrjetin). E zhvilluar nga inxhinierët e rrjetit si një zëvendësim i përmirësuar i BPF klasik, BPF i ri gjeti përdorim brenda gjashtë muajve në detyrën e vështirë të ndjekjes së sistemeve Linux, dhe tani, gjashtë vjet pas shfaqjes së saj, do të na nevojitet një artikull i tërë, i ardhshëm, vetëm për të enumeruar llojet e ndryshme të programeve.

VeSëLëYë KaRtInKi

Në thelb, BPF është një makinë virtuale-për të provuar që lejon ekzekutimin e kodit "të rastësishëm" në hapësirën e numrit pa rrezikuar sigurinë. Programet BPF krijohen në hapësirën e përdoruesit, ngarkohet në bërthamë dhe lidhen me një burim ngjarjesh. Një ngjarje mund të jetë, për shembull, dërgimi i një pakete në një ndërfaqe rrjeti, ekzekutimi i një funksioni të bërthamës, etj. Në rastin e paketave, programit BPF do t'i jenë të disponueshme të dhënat dhe metadata e paketës (për të lexuar dhe ndoshta për të shkruar, në varësi të llojit të programit), në rastin e ekzekutimit të një funksioni të bërthamës - argumentet e funksionit, duke përfshirë treguesit në memorjen e bërthamës, etj.

Le të shikojmë më për detaje këtë proces. Së pari, do të flasim për ndryshimin e parë nga BPF klasik, për të cilin programet shkruheshin në assembler. Në versionin e ri, arkitektura u përmirësua në një mënyrë që programet tani mund të shkruhen në gjuhë të nivelit të lartë, në radhë të parë, sigurisht, në C. Për këtë u zhvillua një backend për llvm, i cili lejon gjenerimin e kodit byte për arkitekturën BPF.

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Arkitektura BPF u zhvillua, në veçanti, për të nxjerrë përfitime efikase në makinat moderne. Për të siguruar që kjo të funksionojë në praktikë, kodi byte i BPF, pasi ngarkohet në bërthamë, përkthehet në kod natyror me ndihmën e një komponenti të quajtur JIT compiler (Just In Time). Më pas, nëse e mbani mend, në BPF klasik, programi ngarkohej në bërthamë dhe lidhej me burimin e ngjarjeve në mënyrë atomike - në kontekstin e një thirrjeje sistematike. Në arkitekturën e re, kjo ndodh në dy faza - së pari, kodi ngarkohet në bërthamë me ndihmën e një thirrjeje sistematike bpf(2), dhe më pas, më vonë, me ndihmën e mekanizmave të ndryshme, varësisht nga lloji i programit, programi lidhet (attaches) me burimin e ngjarjeve.

Këtu, ndoshta, lexuesit mund t'i lindin pyetje: a është kështu e mundur? Si garanton sigurinë e ekzekutimit të këtij lloji kodi? Siguria e ekzekutimit na garanton faza e ngarkimit të programeve BPF, e quajtur verifikues (në anglisht ky proces quhet verifier dhe unë do të vazhdoj të përdor këtë fjalë në anglisht):

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Verifier - është një analizues statik, i cili garanton që programi nuk do të dëmtojë funksionimin normal të bërthamës. Kjo, për shkak të faktit, nuk do të thotë që programi nuk mund të ndërhyjë në funksionimin e sistemit - programet BPF, në varësi të llojit, mund të lexojnë dhe shkruajnë pjesë të memories së bërthamës, të kthejnë vlera të funksioneve, të prishin, të plotësojnë, të rishkruajnë dhe madje të dërgojnë paketa rrjeti. Verifier garanton që bërthama nuk do të dështojë nga ekzekutimi i programit BPF dhe që programi, të cilit në përputhje me rregullat i janë dhënë të drejta për të shkruar, për shembull, të dhënat e paketës së daljes, nuk do të mund të rishkruajë memorjen e bërthamës jashtë paketës. Pak më në detaje, ne do ta shohim verifier në seksionin përkatës, pasi të njihemi me të gjithë pjesët e tjera të BPF.

Pra, çfarë kemi mësuar deri në këtë moment? Përdoruesi shkruan një program në gjuhën C, e ngarkon atë në bërthamë përmes thirrjes së sistemit bpf(2), ku ai kalon kontrollin te verifier dhe transpirohet në kod të natyrshëm. Pastaj, i njëjti ose një përdorues tjetër e lidh programin me burimin e ngjarjeve dhe ai fillon të ekzekutohet. Ndarja e ngarkimit dhe lidhjes është e nevojshme për disa arsye. Së pari, ekzekutimi i verifier është relativisht i shtrenjtë dhe, duke ngarkuar të njëjtin program disa herë, ne shpenzojmë kot kohë kompjuterike. Së dyti, se si saktësisht programi lidhet varet nga lloji i saj dhe një "ndërfaqe universale" e dizajnuar një vit më parë mund të mos përshtatet për lloje të reja programesh. (Megjithatë tani, kur arkitektura bëhet më e pjekur, ka një ide për të unifikuar këtë ndërfaqe në nivelin libbpf.)

NjĂ« lexues i kujdesshĂ«m mund tĂ« vĂ«rejĂ« se ne ende nuk kemi pĂ«rfunduar me imazhet. NĂ« tĂ« vĂ«rtetĂ«, gjithçka e thĂ«nĂ« mĂ« sipĂ«r nuk shpjegon se si BPF e ndryshon thellĂ«sisht pamjen krahasuar me BPF klasic. Dy novacione, tĂ« cilat zgjerojnĂ« ndjeshĂ«m kufijtĂ« e aplikueshmĂ«risĂ«, janĂ«: mundĂ«sia pĂ«r tĂ« pĂ«rdorur memorie tĂ« ndarĂ« dhe funksionet ndihmĂ«se tĂ« bĂ«rthamĂ«s (kernel helpers). NĂ« BPF, memoria e ndarĂ« realizohet pĂ«rmes ashtuquajturave maps — struktura tĂ« dhĂ«nash tĂ« ndara me njĂ« API tĂ« caktuar. Ky emĂ«r u dha ndoshta sepse lloji i parĂ« tĂ« map ishte njĂ« tabelĂ« hash. MĂ« pas erdhĂ«n array, tabela hash lokale (per-CPU) dhe array lokale, pemĂ« kĂ«rkimi, mapa qĂ« pĂ«rmbajnĂ« tregues pĂ«r programet BPF dhe shumĂ« mĂ« tepĂ«r. Ajo qĂ« na intereson tani Ă«shtĂ« fakti se programet BPF kanĂ« marrĂ« mundĂ«sinĂ« pĂ«r tĂ« ruajtur gjendjen midis thirrjeve dhe pĂ«r ta ndarĂ« atĂ« me programet e tjera dhe me hapĂ«sirĂ«n e pĂ«rdoruesit.

Aksesimi i maps bĂ«het nga proceset e pĂ«rdoruesve pĂ«rmes thirrjeve sistemike bpf(2), ndĂ«rsa nga programet BPF qĂ« funksionojnĂ« nĂ« bĂ«rthamĂ« — pĂ«rmes funksioneve ndihmĂ«se. MĂ« shumĂ«, ndihmĂ«sit ekzistojnĂ« jo vetĂ«m pĂ«r punĂ«n me maps, por edhe pĂ«r akses nĂ« mundĂ«si tĂ« tjera tĂ« bĂ«rthamĂ«s. PĂ«r shembull, programet BPF mund tĂ« pĂ«rdorin funksionet ndihmĂ«se pĂ«r tĂ« ridrejtuar paketa nĂ« interfazet e tjera, pĂ«r tĂ« gjeneruar ngjarje nga sistemet perf, akses nĂ« struktura tĂ« bĂ«rthamĂ«s etj.

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Pra, BPF ofron mundësinë për të ngarkuar kodin arbitrar, domethënë, atë që ka kaluar verifikimin në verifier, në hapësirën e bërthamës. Ky kod mund të ruajë gjendjen midis thirrjeve dhe të shkëmbejë të dhëna me hapësirën e përdoruesit, si dhe ka akses në nën-sistemet e bërthamës të lejuara për këtë lloj programi.

Kjo tashmë duket si mundësi që ofrohet nga modulat e kernelit, në krahasim me të cilat BPF ka disa avantazhe (sigurisht, krahasimi mund të bëhet vetëm për aplikacione të ngjashme, për shembull, gjurmimi i sistemit - nuk mund të shkruajmë një drejtues të rastësishëm me BPF). Mund të theksohet një prag më i ulët për hyrjen (disa mjete që përdorin BPF nuk kërkojnë që përdoruesi të ketë aftësi në programimin e kernelit, për të mos përmendur aftësitë në programim), siguria e kohës së ekzekutimit (ngrihni dorën në komentet ata që nuk e kanë dështuar sistemin gjatë shkruajtjes ose testimit të moduleve), atomikësia - gjatë riparimit të moduleve ka një kohë të ndërprerjes, ndërsa nën sistemi BPF garanton që asnjë ngjarje nuk do të humbasë (për drejtësinë, kjo nuk është e vërtetë për të gjithë llojet e programeve BPF).

Prania e kĂ«tyre mundĂ«sive e bĂ«n BPF njĂ« mjet universale pĂ«r zgjerimin e kernelit, diçka qĂ« konfirmohet nĂ« praktikĂ«: lloje tĂ« reja e tĂ« reja programesh po shtohen nĂ« BPF, gjithnjĂ« e mĂ« shumĂ« kompanitĂ« e mĂ«dha po e pĂ«rdorin BPF nĂ« serverĂ«t e tyre operativĂ« 24x7, gjithnjĂ« e mĂ« shumĂ« startup-e po e ndajnĂ« biznesin e tyre mbi zgjidhje qĂ« janĂ« tĂ« bazuara nĂ« BPF. BPF pĂ«rdoret kudo: nĂ« mbrojtjen nga sulmet DDoS, nĂ« krijimin e SDN (pĂ«r shembull, zbatimet e rrjeteve pĂ«r kubernetes), si mjeti kryesor pĂ«r gjurmimin e sistemeve dhe mbledhjen e statistikave, nĂ« sistemet e zb ĐŸĐ±ĐœĐ°Ń€ŃƒĐ¶Đ”ĐœĐžŃ dhe nĂ« sistemet e sandbox-eve etj.

Le të përfundojmë këtë pjesë përmbledhëse të artikullit dhe të shikojmë më në përmbajtje makinën virtuale dhe ekosistemin BPF.

Dalin: mjetet

Për të pasur mundësinë të ekzekutoni shembujt nga kapitujt e ardhshëm, mund t'ju nevojiten disa mjete, minimumi llvm/clang me mbështetje për bpf dhe bpftool. Në seksionin Mjetet e zhvillimit mund të lexoni udhëzimet për ndërtimin e mjeteve, si dhe të kernelit tuaj. Kjo pjesë është vendosur më poshtë për të mos prishur qartësinë e shkrimit tonë.

Regjistrat dhe sistemi i urdhrave të makinës virtuale BPF

Arkitektura dhe sistemi i komandave BPF janë zhvilluar me parasysh se programet do të shkruhen në gjuhën C dhe pas ngarkimit në bërthamë do të përkthehen në kod nativ. Prandaj, numri i regjistrave dhe shumëllojshmëria e komandave janë zgjedhur duke pasur parasysh përputhshmërinë, në kuptimin matematikor, e mundësive të makinave moderne. Përveç kësaj, programet kanë pasur kufizime të ndryshme, për shembull, deri para pak kohësh nuk kishte mundësi për të shkruar cikle dhe nënprograme, dhe numri i instrukcioneve ishte i kufizuar në 4096 (tani, programet me privilegje mund të ngarkojnë deri në një milion instrukcione).

NĂ« BPF ka njĂ«mbĂ«dhjetĂ« regjistra 64-bitĂ« tĂ« disponueshĂ«m pĂ«r pĂ«rdoruesin. r0—r10 dhe numĂ«ruesi i komandave (program counter). Regjistri r10 pĂ«rmban njĂ« tregues pĂ«r stek (frame pointer) dhe Ă«shtĂ« i disponueshĂ«m vetĂ«m pĂ«r lexim. Programet gjatĂ« ekzekutimit kanĂ« qasje nĂ« njĂ« stek prej 512 byte dhe njĂ« numĂ«r tĂ« pakufizuar tĂ« memories sĂ« ndarĂ« nĂ« formĂ«n e maps.

Programet BPF lejohet tĂ« ekzekutojnĂ« njĂ« grup funksionesh ndihmĂ«se (kernel helpers) tĂ« caktuara nĂ« varĂ«si tĂ« llojit tĂ« programit dhe, qĂ« nga koha e fundit, edhe funksione tĂ« zakonshme. Çdo funksion i thirrur mund tĂ« pranojĂ« deri nĂ« pesĂ« argumente, tĂ« kaluara nĂ« regjistrat r1—r5, ndĂ«rsa vlera e kthyer kalon nĂ« r0. Garantohet qĂ« pas kthimit nga funksioni pĂ«rmbajtja e regjistrave r6—r9 nuk do tĂ« ndryshojĂ«.

PĂ«r pĂ«rkthimin efektiv tĂ« programeve, regjistrat r0—r11 pĂ«r tĂ« gjitha arkitekturĂ«n e mbĂ«shtetur pĂ«rputhen njĂ«sh me regjistrat aktual tĂ« duke marrĂ« parasysh veçoritĂ« e ABI tĂ« arkitekturĂ«s aktuale. PĂ«r shembull, pĂ«r x86_64 regjistrat r1—r5, tĂ« cilĂ«t pĂ«rdoren pĂ«r kalimin e parametrave tĂ« funksioneve, pĂ«rputhen me rdi, rsi, rdx, rcx, r8, tĂ« cilĂ«t shĂ«rbejnĂ« pĂ«r kalimin e parametrave nĂ« funksione nĂ« x86_64. PĂ«r shembull, kodi nĂ« tĂ« majtĂ« pĂ«rkthehet nĂ« kodin nĂ« tĂ« djathtĂ« kĂ«shtu:

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

Regjistri r0 po ashtu pĂ«rdoret pĂ«r tĂ« kthyer rezultatin e ekzekutimit tĂ« programit, dhe nĂ« regjistrin r1 programi kalon njĂ« tregues nĂ« kontekst — varĂ«sisht nga lloji i programit, kjo mund tĂ« jetĂ«, pĂ«r shembull, struktura struct xdp_md (pĂ«r XDP) ose struktura struct __sk_buff (pĂ«r programe tĂ« ndryshme rrjetesh) ose struktura struct pt_regs (pĂ«r lloje tĂ« ndryshme tĂ« programeve tĂ« gjurmimit) etj.

Pra ndaj, ne kishim njĂ« grup regjistrash, ndihmĂ«s kernel-i, njĂ« stek, njĂ« tregues konteksti dhe memorie tĂ« ndarĂ« nĂ« formĂ«n e maps. Nuk Ă«shtĂ« se gjithçka kjo ishte absolutisht e nevojshme gjatĂ« udhĂ«timit, por


Le të vazhdojmë përshkrimin dhe të flasim për sistemin e komandave për punën me këto objekte. Të gjitha (gati të gjitha) instruksionet BPF kanë një madhësi fikse prej 64 bite. Nëse shikoni një instruktion në një makinë 64-bite Big Endian, do të shihni

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

KĂ«tu Kodi — kjo Ă«shtĂ« kodimi i instrukcionit, Dst/Src — kĂ«to janĂ« kodimet e marrĂ«sit dhe burimit, pĂ«rkatĂ«sisht, Çaktivizuar — njĂ« largim 16-bitĂ«sh nĂ«nshkrimi, dhe Imm — ky Ă«shtĂ« njĂ« numĂ«r i plotĂ« 32-bitĂ«sh nĂ«nshkrimi, i pĂ«rdorur nĂ« disa komanda (analog me konstantĂ«n K nga cBPF). Kodimi Kodi ka njĂ« nga dy lloje:

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Klaset e instrukcioneve 0, 1, 2, 3 përcaktuan komanda për punën me memorien. Ato quhen, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, përkatësisht. Klaset 4, 7 (BPF_ALU, BPF_ALU64) përbëjnë një grup instrukcionesh ALU. Klaset 5, 6 (BPF_JMP, BPF_JMP32) përmbledhin instrukcionet e kalimit.

Plani i mëtejshëm për studimin e sistemit të komandave BPF është ky: në vend që të listoni me saktësi të gjitha instrukcionet dhe parametrat e tyre, do të analizojmë disa shembuj në këtë seksion dhe do të bëhet e qartë se si janë ndërtuar në të vërtetë instrukcionet dhe si të ndërrohen manualisht çdo skedar binar për BPF. Për të konsoliduar materialin, më tej në artikull do të takojmë edhe instrukcione të veçanta në seksionet për Verifier, kompilatorin JIT, përkthimin e BPF klasik, si dhe në studimin e maps, thirrjet e funksioneve dhe punë të ngjashme.

Kur do të flasim për instruksionet e veçanta, do të referohemi në skedarët e kernel-it bpf.h dhe bpf_common.h, ku përcaktohen kodet numerike të instrukcioneve BPF. Gjatë studimit të pavarur të arkitekturës dhe/ose analizës së binarëve, semantikën e mund të gjeni në burime të mëposhtme, të renditura sipas kompleksitetit: Specifikimi i paautorizuar eBPF, Udhëzuesi referues për BPF dhe XDP, Seti i Instruksioneve, Dokumentacion/rrjetit/filter.txt dhe, sigurisht, në kodet burimore të Linux - verifier, JIT, interpretorin BPF.

Shembull: disasemblojmë BPF në mendje

Le të analizojmë një shembull, ku do të kompilojmë programin readelf-example.c dhe të shikojmë në binarin e rezultuar. Ne do të zbulojmë përmbajtjen origjinale readelf-example.c poshtë, pasi ta rikthejmë logjikën e saj nga kodet binare:

$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Heqja e hex e seksionit '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................

Kolona e parĂ« nĂ« daljen readelf — Ă«shtĂ« njĂ« hap dhe programi ynĂ«, nĂ« kĂ«tĂ« mĂ«nyrĂ«, pĂ«rbĂ«het nga katĂ«r komandat:

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

Kodet e komandave janĂ« tĂ« barabarta b7, 15, b7 dhe 95. Le tĂ« kujtojmĂ« se tre bitĂ«t mĂ« tĂ« ulĂ«t — janĂ« klasa e instruksionit. NĂ« rastin tonĂ«, biti i katĂ«rt pĂ«r tĂ« gjitha instruksionet Ă«shtĂ« i zbrazĂ«t, prandaj klasat e instruksionit janĂ« tĂ« barabarta, pĂ«rkatĂ«sisht, 7, 5, 7, 5. Klasa 7 — Ă«shtĂ« BPF_ALU64, ndĂ«rsa 5 — Ă«shtĂ« BPF_JMP. PĂ«r tĂ« dy klasat formati i instruksionit Ă«shtĂ« i njĂ«jtĂ« (shiko lart) dhe ne mund tĂ« ripersonifikojmĂ« programin tonĂ« kĂ«shtu (pĂ«rveç kĂ«saj do tĂ« ripersonifikojmĂ« kolonat e tjera nĂ« njĂ« format tĂ« kuptueshĂ«m):

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

Operacioni b klasĂ«s ALU64 — kjo Ă«shtĂ« BPF_MOV. Ajo i jep vlerĂ«n regjistrit-pranues. NĂ«se biti Ă«shtĂ« i vendosur s (source), atĂ«herĂ« vlera merret nga regjistri-burim, dhe nĂ«se, si nĂ« rastin tonĂ«, ai nuk Ă«shtĂ« vendosur, atĂ«herĂ« vlera merret nga fusha Imm. KĂ«shtu, nĂ« instruksionet e para dhe tĂ« treta ne realizojmĂ« operacionin r0 = Imm. MĂ« pas, operacioni i klasĂ«s JMP — Ă«shtĂ« BPF_JEQ (nisje nĂ«se janĂ« tĂ« barabarta). NĂ« rastin tonĂ«, pasi biti S Ă«shtĂ« i barabartĂ« me zero, ajo krahasohet vlera e regjistrit-burim me fushĂ«n Imm. NĂ«se vlerat pĂ«rputhen, atĂ«herĂ« kalimi ndodh nĂ« PC + Off, ku PC, siç Ă«shtĂ« zakon, pĂ«rmban adresĂ«n e instruksionit tĂ« ardhshĂ«m. MĂ« nĂ« fund, operacioni 9 i klasĂ«s JMP — Ă«shtĂ« BPF_EXIT. Ky instruksion pĂ«rfundon ekzekutimin e programit, duke e kthyer nĂ« bĂ«rthamĂ« r0. Le tĂ« shtojmĂ« njĂ« kolonĂ« tĂ« re nĂ« tabelĂ«n tonĂ«:

Op    S  Class   Dst Src Off  Imm    Disassm
MOV   0  ALU64   0   0   0    1      r0 = 1
JEQ   0  JMP     0   1   1    0      nëse (r1 == 0) shko te pc+1
MOV   0  ALU64   0   0   0    2      r0 = 2
EXIT  0  JMP     0   0   0    0      exit

Ne mund ta ripersonifikojmë këtë në një format më të përshtatshëm:

     r0 = 1
     nëse (r1 == 0) shko te END
     r0 = 2
END:
     exit

NĂ«se kujtojmĂ« se nĂ« regjistrin r1 programi dĂ«rgon njĂ« tregues nĂ« kontekst prej bĂ«rthamĂ«s, ndĂ«rsa nĂ« regjistrin r0 kthehet vlera nĂ« bĂ«rthamĂ«, atĂ«herĂ« ne mund tĂ« shohim se nĂ«se treguesi nĂ« kontekst Ă«shtĂ« i barabartĂ« me zero, ne kthejmĂ« 1, ndĂ«rsa nĂ« tĂ« kundĂ«rtĂ«n — 2. Le tĂ« verifikojmĂ« nĂ«se jemi tĂ« saktĂ«, duke parĂ« burimin:

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

Po, ky është një program pa kuptim, por megjithatë ai transkriptohet në vetëm katër instruksione të thjeshta.

Shembulli-përjashtim: instruksioni 16-bajtë

MĂ« parĂ« pĂ«rmendĂ«m se disa instruksione zĂ«nĂ« mĂ« shumĂ« se 64 bite. Kjo i referohet, pĂ«r shembull, instruksionit lddw (Kod = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — ngarko nĂ« regjistĂ«r fjalĂ« tĂ« dyfishta nga fusha Imm. E vĂ«rteta Ă«shtĂ« se Imm ka njĂ« madhĂ«si prej 32, ndĂ«rsa fjala dyfishe — 64 bit, prandaj ngarkimi nĂ« regjistĂ«r i njĂ« vlerĂ« direkte 64-bit nĂ« njĂ« udhĂ«zim 64-bit nuk Ă«shtĂ« i mundur. PĂ«r kĂ«tĂ«, pĂ«rdoren dy udhĂ«zime fqinje pĂ«r tĂ« ruajtur pjesĂ«n e dytĂ« tĂ« vlerĂ«s 64-bit nĂ« fushĂ«n Imm. Shembull:

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

Në programin binar ka vetëm dy udhëzime:

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

Ne do të takohemi sërish me udhëzimin lddw, kur të flasim për relokimet dhe punën me maps.

Shembull: disasemblojmë BPF me mjete standarde

Pra, ne kemi mësuar të lexojmë kodet binarë BPF dhe jemi gati të analizojmë çdo udhëzim nëse nevojitet. Megjithatë, është e rëndësishme të thuhet se në praktikë është më e lehtë dhe më e shpejtë të disasemblojmë programet me ndihmën e mjeteve standarde, për shembull:

$ 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

Cikli i jetës së objekteve BPF, sistemi i skedarëve bpffs

(Disa detaje, të përshkruara në këtë nëndeg, i kam mësuar për herë të parë nga të postit Alexei Starovoitov në BPF Blog.)

Objektet BPF — programet dhe mapat — krijohen nga hapĂ«sira e pĂ«rdoruesit me ndihmĂ«n e komandave BPF_PROG_LOAD dhe BPF_MAP_CREATE thirrjes sistematike bpf(2), ne do tĂ« flasim se si ndodh kjo nĂ« seksionin e ardhshĂ«m. GjatĂ« kĂ«saj krijohen struktura tĂ« dhĂ«nash nĂ« bĂ«rthamĂ« dhe pĂ«r secilĂ«n prej tyre refcount (numri i referencave) caktuar me vlerĂ«n njĂ«, dhe pĂ«rdoruesit i kthehet njĂ« deshifruese skedari, qĂ« tregon objekte. Pas mbylljes sĂ« deshifrusit refcount numri i referencave tĂ« objektit reduktohet me njĂ«, dhe kur arrin zero, objekti shkatĂ«rrohet.

Nëse programi përdor mapa, atëherë refcount këtë mapat rritet me një pas ngarkimit të programit, domethënë, deshifrusit të tyre mund të mbyllen nga procesi i përdoruesit dhe në të njëjtën kohë refcount nuk do të bëhet zero:

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Pas ngarkimit të suksesshëm të programit, ne zakonisht e bashkojmë atë me një gjenerator ngjarjesh. Për shembull, ne mund ta ulin atë në një ndërfaqe rrjeti për të trajtuar paketat e ardhshme ose t'i lidhim atë me një të tillë tracepoint në bërthamë. Në këtë moment numri i referencave gjithashtu do të rritet me një dhe ne do të jemi në gjendje të mbyllim deshifrusin në programin ngarkues.

ÇfarĂ« do tĂ« ndodhĂ« nĂ«se tani e pĂ«rfundojmĂ« punĂ«n e programit ngarkues? Kjo varet nga lloji i gjeneratorit tĂ« ngjarjeve (hook). TĂ« gjitha hook-Ă«t rrjet do tĂ« ekzistojnĂ« pas pĂ«rfundimit tĂ« ngarkuesit, kĂ«to quhen hook global. NdĂ«rsa, pĂ«r shembull, programet e gjurmimit do tĂ« çlirohen pas pĂ«rfundimit tĂ« procesit qĂ« i krijoi ato (dhe pĂ«r kĂ«tĂ« arsye quhen lokale, nga "local to the process"). Teknikisht, hook-et lokale gjithmonĂ« kanĂ« njĂ« descriptor tĂ« pĂ«rputhshĂ«m tĂ« skedarit nĂ« hapĂ«sirĂ«n e pĂ«rdoruesit dhe pĂ«r kĂ«tĂ« arsye mbyllen me mbylljen e procesit, ndĂ«rsa globalet — jo. NĂ« figurinĂ« e mĂ«poshtme, me ndihmĂ«n e kryqĂ«zave tĂ« kuqe, pĂ«rpiqem tĂ« tregoj se si pĂ«rfundimi i programit ngarkues ndikon nĂ« kohĂ«zgjatjen e objekteve nĂ« rastin e hook-ve lokale dhe globale.

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Pse ekziston ndarja nĂ« hook-e lokale dhe globale? Aktivizimi i disa llojeve tĂ« programeve rrjet ka kuptim edhe pa pĂ«rdoruesin, pĂ«r shembull, imagjinoni mbrojtjen nga DDoS — ngarkuesi shkruan rregullat dhe lidh programin BPF nĂ« ndĂ«rfaqen rrjet, pas kĂ«saj ngarkuesi mund tĂ« shkojĂ« dhe tĂ« vdesĂ«. Nga ana tjetĂ«r, imagjinoni njĂ« program debguese gjurmimi, tĂ« cilin e keni shkruar nĂ« kĂ«mbĂ« pĂ«r dhjetĂ« minuta — pas pĂ«rfundimit tĂ« saj do tĂ« dĂ«shironit qĂ« nĂ« sistem tĂ« mos kishte mbetje, dhe hook-et lokale e garanton kĂ«tĂ«.

Nga ana tjetër, imagjinoni se doni të lidheni me një point të gjurmimit në bërthamë dhe të grumbulloni statistika për shumë vite. Në këtë rast, do të dëshironit të përfundoni pjesën e përdoruesit dhe të ktheheni në statistika herë pas here. Kjo mundësi ofrohet nga sistemi i skedarëve bpf. Ky është një sistem skedarësh pseudo, që ekziston vetëm në kujtesë, i cili lejon krijimin e skedarëve që referojnë objekte BPF dhe, kështu, rritin refcount objektet. Pas kësaj, ngarkuesi mund të mbyllë punën, ndërsa objektet e krijuara nga ai do të mbeten të gjalla.

BPF për të vegjlit, pjesa e parë: BPF i zgjeruar

Krijimi i skedarĂ«ve nĂ« bpffs qĂ« referojnĂ« nĂ« objektet BPF quhet "kapje" ("pin", si nĂ« frazĂ«n next: "procesi mund tĂ« pin njĂ« program ose hartĂ« BPF"). Krijimi i objekteve skedarĂ«sh pĂ«r objektet BPF ka kuptim jo vetĂ«m pĂ«r tĂ« zgjatur jetĂ«n e objekteve lokale, por edhe pĂ«r lehtĂ«simin e pĂ«rdorimit tĂ« objekteve globale — duke iu rikthyer shembullit me programin global pĂ«r mbrojtjen nga DDoS, dĂ«shirojmĂ« tĂ« kemi mundĂ«sinĂ« tĂ« vijmĂ« nga koha nĂ« kohĂ« dhe tĂ« shohim statistikat.

Sistemi i skedarëve BPF zakonisht montohet në /sys/fs/bpf, por mund të montohet edhe lokalisht, për shembull, kështu:

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

Emrat në sistemin e skedarëve krijohen me ndihmën e komandës BPF_OBJ_PIN nga thirrja e sistemit BPF. Si ilustrim, le të marrim ndonjë program, ta kompilojmë, ta ngarkojmë dhe ta lidhim në bpffs. Programi ynë nuk bën asgjë të dobishme, ne e ofrojmë kodin e tij vetëm për të pasur mundësi të riprodhoni shembullin:

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

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

Do ta kompilojmë këtë program dhe do të krijojmë një kopje lokale të sistemit të skedarëve bpffs:

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

Tani do ta ngarkojmë programin tonë me ndihmën e utilitarit bpftool dhe do të shohim thirrjet përkatëse të sistemit bpf(2) (nga dalja e strace disa rreshta të parëndësishëm janë hequr):

$ 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

Këtu e ngarkuam programin me ndihmën e BPF_PROG_LOAD, morëm nga bërthama një descriptor skedari 3 dhe me anën e komandës BPF_OBJ_PIN e lidhëm këtë descriptor skedari si skedar "bpf-mountpoint\/test". Pas kësaj, programi ngarkues bpftool përfundoi punën, por programi ynë mbeti në bërthamë, megjithëse ne nuk e lidhëm atë me ndonjë ndërfaqe rrjeti:

$ 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

Mund të fshijmë objektin skedar me unlink(2) dhe pas kësaj programi përkatës do të fshihet:

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

Fshirja e objekteve

Duke folur për fshirjen e objekteve, duhet të saktësojmë se pasi që e kemi çmontuar programin nga hook (generatori i ngjarjeve), asnjë ngjarje e re nuk do ta sjellë atë në funksion, megjithatë, të gjitha instancat aktuale të programit do të përfundojnë në mënyrë të duhur.

Disa lloje të programeve BPF lejojnë zëvendësimin e programit në flakë, dmth. ofrojnë atomikën e sekuencës replace = detach old program, attach new program. Në këtë rast, të gjitha instancat aktive të versionit të vjeter të programit do të përfundojnë punën e tyre, ndërsa trajtuesit e rinj të ngjarjeve do të krijohen nga programi i ri, dhe "atomikën" nënkupton këtu se asnjë ngjarje nuk do të anashkalohet.

Shkëputja e programeve nga burimet e ngjarjeve

Në këtë artikull ne nuk do të përshkruajmë veçmas lidhjen e programeve me burimet e ngjarjeve, pasi ka kuptim të studiohet në kontekstin e një tipi të caktuar programi. Shihni. shembull më poshtë, ku ne tregojmë se si lidhen programet e tipit XDP.

Menaxhimi i objekteve përmes thirrjes sistemike bpf

Programet BPF

Të gjitha objektet BPF krijohen dhe menaxhohen nga hapësira e përdoruesit përmes thirrjes sistemike bpf, që ka prototipin e mëposhtëm:

#include <linux/bpf.h>

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

KĂ«tu komanda cmd Ă«shtĂ« njĂ« nga vlerat e llojit enum bpf_cmd, attr — tregues pĂ«r parametrat pĂ«r programin e veçantĂ« dhe size — madhĂ«sia e objektit sipas treguesit, dmth zakonisht Ă«shtĂ« sizeof(*attr). NĂ« bĂ«rthamĂ«n 5.8 thirrja sistemike bpf mbĂ«shtet 34 komanda tĂ« ndryshme, dhe definicion union bpf_attr zĂ« 200 rreshta. Por kjo nuk duhet tĂ« na frikĂ«sojĂ«, pasi do tĂ« njihemi me komandat dhe parametrat gjatĂ« disa artikujve.

Do tĂ« fillojmĂ« me komandĂ«n BPF_PROG_LOAD, e cila krijon programet BPF — merr njĂ« grup instruksionesh BPF dhe e ngarkon atĂ« nĂ« bĂ«rthamĂ«. NĂ« momentin e ngarkimit, aktivizohet verifikuesi, dhe pastaj kompaktori JIT dhe, pas ekzekutimit tĂ« suksesshĂ«m, pĂ«rdoruesit i kthehet njĂ« deshifruese e skedarit tĂ« programit. Kemi parĂ« se çfarĂ« ndodh mĂ« pas nĂ« seksionin e mĂ«parshĂ«m pĂ«r ciklin jetĂ«sor tĂ« objekteve BPF.

Tani do tĂ« shkruajmĂ« njĂ« program pĂ«rdoruesi, i cili do tĂ« ngarkojĂ« njĂ« program tĂ« thjeshtĂ« BPF, por fillimisht duhet tĂ« vendosim se çfarĂ« lloj programi duam tĂ« ngarkojmĂ« — do tĂ« duhet tĂ« zgjedhim llojin dhe brenda kĂ«tij tipi tĂ« shkruajmĂ« njĂ« program qĂ« do tĂ« kalojĂ« verifikimin e verifikuesit. SidoqoftĂ«, pĂ«r tĂ« mos e komplikuar procesin, ja njĂ« zgjidhje e gatshme: do tĂ« marrim njĂ« program tĂ« tipit BPF_PROG_TYPE_XDP, i cili do tĂ« kthejĂ« vlerĂ«n XDP_PASS (shkalo tĂ« gjitha paketat). NĂ« assembler BPF kjo duket shumĂ« e thjeshtĂ«:

r0 = 2
exit

Pasi kemi vendosur se çfarë çfarë do të ngarkojmë, mund të tregojmë se si do ta bëjmë këtë:

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

Ngjarjet interesante nĂ« program fillojnĂ« me pĂ«rcaktimin e vargut insns — programi ynĂ« BPF nĂ« kodin makinerik. NĂ« kĂ«tĂ« rast, çdo instruksion i programit BPF Ă«shtĂ« i paketuara nĂ« njĂ« strukturĂ« bpf_insn. Elementi i parĂ« insns pĂ«rputhet me instruksionin r0 = 2, tjetri — exit.

Një zhvendosje. Në bërthamë janë përcaktuar makro më të lehta për të shkruar kod makinerik, dhe, duke përdorur skedarin e titujve të bërthamës tools/include/linux/filter.h ne do të mund të shkruanim

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

Por që shkroi programe BPF në kodet mahnitëse është e nevojshme vetëm për të shkruar teste në bërthamë dhe artikuj mbi BPF, mungesa e këtyre makroëve në të vërtetë nuk e komplikon jetën e zhvilluesit.

Pasi të kemi përcaktuar programin BPF, ne kalojmë në ngarkimin e tij në bërthamë. Grupi ynë minimal i parametrit attr përfshin llojin e programit, grupin dhe numrin e instrukcioneve, licencën e detyrueshme, si dhe emrin "woo", që ne e përdorim për ta gjetur programin tonë në sistem pas ngarkimit. Programi, siç ishte premtuar, ngarkohet në sistem me ndihmën e thirrjes së sistemit bpf.

Në fund të programit, ne kalojmë në një cikël të pafund, i cili imiton ngarkesën e dobishme. Pa të, programi do të shkatërrohet nga bërthama kur të mbyllet deshkrimi i skedarit që na kthyen thirrja e sistemit bpf, dhe ne nuk do ta shohim atë në sistem.

Mirë, jemi gati për testim. Do të përgatisim dhe ekzekutojmë programin nën strace, për të verifikuar që gjithçka funksionon siç duhet:

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

Të gjithë janë në rend bpf(2) , na ktheu një deshkrim 3 dhe kaluam në ciklin e pafund me pause(). Le të provojmë të gjejmë programin tonë në sistem. Për këtë, do të shkojmë në një terminal tjetër dhe do të përdorim mjetin 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)

Ne shohim se nĂ« sistem ka njĂ« program tĂ« ngarkuar woo i cili ka njĂ« ID globale prej 390, dhe qĂ« aktualisht nĂ« procesin simple-prog ka njĂ« deshkrim skedari tĂ« hapur qĂ« tregon nĂ« program (dhe nĂ«se simple-prog do tĂ« pĂ«rfundojĂ«, atĂ«herĂ« woo do tĂ« zhduket). Siç pritej, programi woo zĂ« 16 byte – dy instrukcione – kodet binar nĂ« arkitekturĂ«n BPF, por nĂ« formĂ«n e saj natyrore (x86_64) – Ă«shtĂ« tashmĂ« 40 byte. Le tĂ« shohim programin tonĂ« nĂ« formĂ«n origjinale:

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

pa surpriza. Tani le të hedhim një sy në kodin e krijuar nga kompajleri JIT:

# bpftool prog dump jited id 390
bpf_prog_3b185187f1855c4c_woo:
   0:   nopl   0x0(%rax,%rax,1)
   5:   push   %rbp
   6:   mov    %rsp,%rbp
   9:   sub    $0x0,%rsp
  10:   push   %rbx
  11:   push   %r13
  13:   push   %r14
  15:   push   %r15
  17:   pushq  $0x0
  19:   mov    $0x2,%eax
  1e:   pop    %rbx
  1f:   pop    %r15
  21:   pop    %r14
  23:   pop    %r13
  25:   pop    %rbx
  26:   leaveq
  27:   retq

nuk është shumë efektiv për exit(2), por për të vërtetuar, programi ynë është tepër i thjeshtë, dhe për programet jo triviale, prologu dhe epilogu të shtuar nga kompajleri JIT, sigurisht, janë të nevojshme.

Maps

Programet BPF mund të përdorin zona të strukturuara të memories, të cilat janë të disponueshme si për programet e tjera BPF, ashtu edhe për programet nga hapësira e përdoruesit. Këto objekte quhen maps dhe në këtë seksion ne do të tregojmë se si t'i menaxhojmë ato përmes thirrjes së sistemit. bpf.

Menjëherë do të themi se mundësitë e maps nuk kufizohen vetëm në qasjen në memorien e përbashkët. Ekzistojnë mapa të destinuara për qëllime të veçanta, që përmbajnë, për shembull, tregues për programet BPF ose tregues për ndërfaqet rrjet. Mapa për punën me ngjarjet perf dhe të tilla. Këtu nuk do të flasim për to, në mënyrë që të mos ngatërrojmë lexuesin. Përveç kësaj, ne e injorojmë problemin e sinkronizimit, pasi kjo nuk është e rëndësishme për shembujt tanë. Lista e plotë e llojeve të disponueshme të mapave mund të gjendet në <linux/bpf.h>, dhe në këtë seksion ne do të marrim si shembull llojin historikisht të parë, tabelën e hash. BPF_MAP_TYPE_HASH.

Nëse po krijoni një tabelë hash, le të themi, në C++, do të thoni unordered_map woo, që në shqip do të thotë "më nevojitet një tabelë woo me madhësi të pakufizuar, ku çelësat kanë tipin int, dhe vlerat janë tip long'. Për të krijuar një tabelë hash BPF ne duhet të bëjmë përafërsisht të njëjtën gjë, me përjashtim të faktit se na duhet të specifikojmë madhësinë maksimale të tabelës, dhe në vend të tipeve të çelësit dhe vlerave, na nevojitet të specifikojmë madhësitë e tyre në byte. Për krijimin e mapave përdoret komanda BPF_MAP_CREATE thirrjes sistematike bpf. Le të shikojmë një program më pak më të komplikuar, që krijon një map. Pas programit të mëparshëm, që ngarkon programet BPF, ky duhet t'ju duket i thjeshtë:

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

KĂ«tu ne pĂ«rcaktojmĂ« njĂ« grup parametrash attr, ku themi "mĂ« nevojitet njĂ« tabelĂ« hash me çelĂ«sa dhe vlera qĂ« kanĂ« madhĂ«sinĂ« sizeof(int), tĂ« cilĂ«n mund tĂ« vendos deri nĂ« katĂ«r elemente”. Kur krijohen mapat BPF, mund tĂ« specifikohen edhe parametra tĂ« tjerĂ«, pĂ«r shembull, ashtu si nĂ« shembullin me programin, ne e kemi caktuar emrin e objektit si "woo".

Të kompilojmë dhe të ekzekutojmë programin:

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

Këtu ndodhet thirrja sistemore bpf(2) na ktheu natyrën e dosjes të hartës numri 3 dhe më pas programi, siç pritej, pret udhëzime të mëtejshme në thirrjen sistemore pause(2).

Tani do ta dërgojmë programin tonë në background ose do të hapim një terminal tjetër dhe do të shikojmë objektin tonë me ndihmën e utilitarit bpftool (mund ta ndijmë hartën tonë nga emri i saj):

$ sudo bpftool map
...
114: hash  emri woo  flamuj 0x0
        çelësi 4B  vlera 4B  max_entries 4  memlock 4096B
...

Numri 114 Ă«shtĂ« ID globale e objektit tonĂ«. Çdo program nĂ« sistem mund ta pĂ«rdorĂ« kĂ«tĂ« ID pĂ«r tĂ« hapur njĂ« hartĂ« ekzistuese me komandĂ«n BPF_MAP_GET_FD_BY_ID thirrjes sistematike bpf.

Tani mund të luajmë me tabelën tonë të heshme. Le të shikojmë përmbajtjen e saj:

$ sudo bpftool map dump id 114
Gjetur 0 elemente

Boß. Le tĂ« vendosim njĂ« vlerĂ« nĂ« tĂ« hash[1] = 1:

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

Të shohim tabelën përsëri:

$ sudo bpftool map dump id 114
çelësi: 01 00 00 00  vlera: 01 00 00 00
Gjetur 1 element

Hurra! Arritëm të shtojmë një element. Vini re se për këtë na duhet të punojmë në nivelin e byte-ve, sepse bptftool nuk di se çfarë lloji kanë vlerat në tabelën e heshme. (Mund t'i dërgojmë këtë njohuri duke përdorur BTF, por jo tani.)

Si i lexon dhe shton elementet bpftool? Le të hedhim një sy nën kapak:

$ 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
çelësi: 01 00 00 00  vlera: 01 00 00 00
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=0x55856ab65280, next_key=0x55856ab65280}, 120) = -1 ENOENT

Fillimisht hapëm hartën sipas ID-së së saj globale me komandën BPF_MAP_GET_FD_BY_ID dhe bpf(2) na ktheu natyrën e dosjes 3. Më pas, duke përdorur komandën BPF_MAP_GET_NEXT_KEY gjetëm çelësin e parë në tabelë, duke kaluar NULL si tregues në çelësin "e mëparshëm". Kur kemi një çelës, mund të bëjmë BPF_MAP_LOOKUP_ELEM, i cili kthen vlerën në treguesin vlera. Hapi i ardhshëm është të përpiqemi të gjejmë elementin tjetër, duke kaluar treguesin në çelësin aktual, por tabela jonë përmban vetëm një element dhe komanda BPF_MAP_GET_NEXT_KEY kthen ENOENT.

Mirë, le të ndryshojmë vlerën sipas çelësit 1, le të themi, logjika jonë e biznesit kërkon të shkruajmë 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

Siç e prisnim, është shumë e lehtë: komanda BPF_MAP_GET_FD_BY_ID hap hartën tonë sipas ID-së, ndërsa komanda BPF_MAP_UPDATE_ELEM rivendos elementin.

Prafrikisht, pas krijimit të një tabele hash nga një program, ne mund të lexojmë dhe shkruajmë përmbajtjen e saj nga një program tjetër. Vini re se nëse arritëm ta bëjmë këtë nga linja e komandës, atëherë mund ta bëjë çdo program tjetër në sistem. Përveç komandave të përmendura më lart, për të punuar me harta nga hapësira e përdoruesit janë të disponueshme këtë:

  • BPF_MAP_LOOKUP_ELEM: gjej vlerĂ«n sipas çelĂ«sit
  • BPF_MAP_UPDATE_ELEM: pĂ«rditĂ«so/ krijo vlerĂ«n
  • BPF_MAP_DELETE_ELEM: fshi çelĂ«sin
  • BPF_MAP_GET_NEXT_KEY: gjej çelĂ«sin e ardhshĂ«m (ose tĂ« parin)
  • BPF_MAP_GET_NEXT_ID: lejon tĂ« kalosh pĂ«rmes tĂ« gjitha hartave ekzistuese, kĂ«shtu funksionon bpftool map
  • BPF_MAP_GET_FD_BY_ID: hap harten ekzistuese sipas ID-sĂ« sĂ« saj globale
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: pĂ«rditĂ«son atome vlerĂ«n e objektit dhe kthen tĂ« kaluarĂ«n
  • BPF_MAP_FREEZE: bĂ«n harten tĂ« pandryshueshme nga hapĂ«sira e pĂ«rdoruesit (kjo operacion nuk mund tĂ« anulohet)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: operacione masive. PĂ«r shembull, BPF_MAP_LOOKUP_AND_DELETE_BATCH — kjo Ă«shtĂ« mĂ«nyra e vetme e sigurt pĂ«r tĂ« lexuar dhe zero tĂ« gjitha vlerat nga harta

Jo të gjitha këto komanda funksionojnë për të gjithë llojet e hartave, por përgjithësisht, puna me lloje të tjera hartash nga hapësira e përdoruesit duket ashtu si puna me tabelat hash.

Për renditje, le të mbyllim eksperimentet tona me tabelën hash. Mos harroni se krijuam një tabelë që mund të përmbajë deri në katër çelësa? Le të shtojmë disa elemente të tjera:

$ 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

Derisa gjithçka shkon mirë:

$ 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
Gjetëm 4 elemente

Le të përpiqemi të shtojmë një tjetër:

$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Gabim: përditësimi dështoi: lista e argumenteve është shumë e gjatë

Siç parashikohej, ne nuk ia dolëm. Le të shohim gabimin më në detaje:

$ 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 (lista e argumenteve është shumë e gjatë)
Gabim: përditësimi dështoi: lista e argumenteve është shumë e gjatë
+++ doli me 255 +++

Gjithçka është në rregull: ashtu siç pritej, komanda BPF_MAP_UPDATE_ELEM përpiqet të krijojë një çelës të ri, të pestë, por dështon me E2BIG.

Pra ndaj, ne dimĂ« tĂ« krijojmĂ« dhe ngarkojmĂ« programe BPF, si dhe tĂ« krijojmĂ« dhe menaxhojmĂ« harta nga hapĂ«sira e pĂ«rdoruesit. Tani Ă«shtĂ« logjike tĂ« shohim se si mund tĂ« pĂ«rdorim harta nga vetĂ« programet BPF. Ne do tĂ« mund tĂ« flasim pĂ«r kĂ«tĂ« nĂ« njĂ« gjuhĂ« tĂ« vĂ«shtirĂ« pĂ«r t'u lexuar nĂ« kodet makinĂ«-makro, por nĂ« tĂ« vĂ«rtetĂ« ka ardhur koha tĂ« tregojmĂ« se si shkruhen dhe mirĂ«mbahen programet BPF nĂ« tĂ« vĂ«rtetĂ« — me ndihmĂ«n e libbpf.

(Për lexuesit që nuk janë të kënaqur me mungesën e një shembulli të ulët: ne do të shqyrtojmë në detaje programet që përdorin harta dhe funksionet ndihmëse, të krijuara me ndihmën e libbpf dhe do të tregojmë se çfarë ndodh në nivelin e instruksioneve. Për lexuesit që nuk janë të kënaqur shumë, ne kemi shtuar shembull në vendin e duhur të artikullit.)

Krijimi i programeve BPF me ndihmën e libbpf

Të shkruani programe BPF me ndihmën e kodit makinë mund të jetë interesante vetëm fillimisht, dhe pastaj vjen një moment i ngopjes. Në këtë moment, duhet të përqendrohemi në llvm, i cili ka një backend për gjenerimin e kodit për arkitekturën BPF, si dhe në bibliotekën libbpf, e cila lejon të shkruajmë pjesën e përdoruesit të aplikacioneve BPF dhe të ngarkojmë kodet e programeve BPF, të gjeneruara me ndihmën e llvm/clang.

NĂ« tĂ« vĂ«rtetĂ«, siç do tĂ« shohim nĂ« kĂ«tĂ« dhe nĂ« artikujt e ardhshĂ«m, libbpf bĂ«n njĂ« punĂ« tĂ« madhe dhe pa tĂ« (ose mjete tĂ« ngjashme — iproute2, libbcc, libbpf-go, etj.) Ă«shtĂ« e pamundur tĂ« jetosh. NjĂ« nga karakteristikat e shkĂ«lqyera tĂ« projektit libbpf Ă«shtĂ« BPF CO-RE (Kompilo NjĂ«herĂ«, Kalo Kudo) — njĂ« projekt qĂ« lejon tĂ« shkruhen programe BPF, tĂ« transportueshme nga njĂ« bĂ«rthamĂ« nĂ« tjetrĂ«n, me mundĂ«sinĂ« e ekzekutimit nĂ« API tĂ« ndryshme (pĂ«r shembull, kur struktura e bĂ«rthamĂ«s ndryshon nga njĂ« version nĂ« tjetrin). PĂ«r tĂ« pasur mundĂ«sinĂ« tĂ« punoni me CO-RE, bĂ«rthama juaj duhet tĂ« jetĂ« e kompiluar me mbĂ«shtetje BTF (si ta bĂ«ni kĂ«tĂ« e shpjegojmĂ« nĂ« seksionin Mjetet e zhvillimit. TĂ« kontrolloni nĂ«se bĂ«rthama juaj Ă«shtĂ« e ndĂ«rtuar me BTF ose jo Ă«shtĂ« shumĂ« e thjeshtĂ« — me praninĂ« e skedarit tĂ« mĂ«poshtĂ«m:

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

Ky skedar pĂ«rmban informacion mbi tĂ« gjitha llojet e tĂ« dhĂ«nave tĂ« pĂ«rdorura nĂ« bĂ«rthamĂ« dhe pĂ«rdoret nĂ« tĂ« gjitha shembujt tanĂ« qĂ« pĂ«rdorin libbpf. Do tĂ« flasim nĂ« detaje pĂ«r CO-RE nĂ« artikullin e ardhshĂ«m, ndĂ«rsa nĂ« kĂ«tĂ« — thjesht ndĂ«rtoni njĂ« bĂ«rthamĂ« me CONFIG_DEBUG_INFO_BTF.

Biblioteka libbpf jeton direkt nĂ« direktoriumin tools/lib/bpf tĂ« bĂ«rthamĂ«s dhe zhvillimi i saj bĂ«het pĂ«rmes listĂ«s sĂ« shpĂ«rndarjes bpf@vger.kernel.org. MegjithatĂ«, pĂ«r nevojat e aplikacioneve qĂ« jetojnĂ« pĂ«rtej bĂ«rthamĂ«s, mbĂ«shtetet njĂ« depĂČzita e veçantĂ« https://github.com/libbpf/libbpf ku biblioteca bĂ«rthamore kyçet pĂ«r qasje nĂ« lexim mĂ« shumĂ«-mĂ« pak ashtu si Ă«shtĂ«.

Në këtë seksion do të shohim se si mund të krijojmë një projekt që përdor libbpf, do të shkruajmë disa (më shumë-më pak pa kuptim) programe testuese dhe do të shqyrtojmë me detaje se si funksionon gjithçka. Kjo do të na lejojë në seksionet e ardhshme të shpjegojmë më lehtë se si programet BPF ndërveprojnë me maps, ndihmësit e bërthamës, BTF, etj.

Zakonisht projektet që përdorin libbpf shtojnë depozitën e GitHub si një git submodule, le të bëjmë edhe ne:

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

Ndërtohet libbpf shumë thjesht:

$ 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

Plani ynë në këtë seksion është si më poshtë: do të shkruajmë një program BPF të tipit BPF_PROG_TYPE_XDP, të njëjtin që kemi pasur në shembullin e mëparshëm, por në C, do ta kompilohet me clang, dhe do të shkruajmë një program ndihmës që do ta ngarkojë atë në bërthamë. Në seksionet e ardhshme do të zgjerim funksionalitetin si të programit BPF ashtu edhe të programit ndihmës.

Shembulli: krijimi i një aplikacioni të plotë duke përdorur libbpf

Për fillim, ne do të përdorim skedarin /sys/kernel/btf/vmlinux, i cili u përmend më sipër, dhe do të krijojmë ekuivalentin e tij në formën e një skedari header:

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

Në këtë skedar do të ruhen të gjitha strukturat e të dhënave që ndodhen në bërthamën tonë, për shembull, ja si definohet kryefaqja IPv4 në bërthamë:

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

Tani do të shkruajmë programin tonë BPF në gjuhën C:

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

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

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

Edhe pse programi ynĂ« pĂ«rbĂ«het nga shumĂ« detaje tĂ« thjeshta, ne duhet tĂ« kushtojmĂ« vĂ«mendje tĂ« veçantĂ« shumĂ« aspekteve. SĂ« pari, skedari i parĂ« pĂ«rfshirĂ«s qĂ« ne po pĂ«rfshijmĂ« Ă«shtĂ« vmlinux.h, i cili sapo e kemi gjeneruar me ndihmĂ«n e bpftool btf dump — tani nuk kemi mĂ« nevojĂ« tĂ« instalojmĂ« paketĂ«n kernel-headers pĂ«r tĂ« kuptuar se si duken strukturat e kernelit. Skedari tjetĂ«r pĂ«rfshirĂ«s vjen nga biblioteka libbpf. Tani na nevojitet vetĂ«m pĂ«r tĂ« definuar makrosin SEC, i cili dĂ«rgon simbolin nĂ« seksionin pĂ«rkatĂ«s tĂ« skedarit ELF. Programi ynĂ« qĂ«ndron nĂ« seksionin xdp/simple, ku para shkronjĂ«s '/' ne pĂ«rcaktojmĂ« tipin e programit BPF — ky Ă«shtĂ« njĂ« rregull qĂ« pĂ«rdoret nĂ« libbpf, bazuar nĂ« emrin e seksionit, do tĂ« vendosĂ« tipin e duhur gjatĂ« ekzekutimit bpf(2). Programi BPF Ă«shtĂ« nĂ« C — shumĂ« i thjeshtĂ« dhe pĂ«rbĂ«het nga njĂ« rresht return XDP_PASS. NĂ« fund, njĂ« seksion i veçantĂ« "license" pĂ«rmban emrin e licencĂ«s.

Ne mund ta kompilohet programin tonĂ« me ndihmĂ«n e llvm/clang, versioni >= 10.0.0, dhe mĂ« mirĂ« — mĂ« shumĂ« (shihni seksionin Mjetet e zhvillimit):

$ 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

Nga karakteristikat interesante: ne specifikojmë arkitekturën e synuar -target bpf dhe rrugën e skedareve të përfshirjes libbpf, të cilat i kemi instaluar së fundmi. Gjithashtu, mos harroni për -O2, pa këtë opsion, mund të keni surpriza në të ardhmen. Le të shikojmë kodin tonë, a kemi arritur të shkruajmë programin që dëshironim?

$ 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 :
       0:       r0 = 2
       1:       exit

Po, arritĂ«m! Tani, kemi njĂ« skedar binar me programin dhe duam tĂ« krijojmĂ« njĂ« aplikacion qĂ« do ta ngarkojĂ« atĂ« nĂ« kernel. PĂ«r kĂ«tĂ«, biblioteka libbpf na ofron dy mundĂ«si — tĂ« pĂ«rdorim njĂ« API mĂ« tĂ« ulĂ«t ose njĂ« API mĂ« tĂ« lartĂ«. Ne do tĂ« zgjedhim rrugĂ«n e dytĂ«, pasi duam tĂ« mĂ«sojmĂ« tĂ« shkruajmĂ«, ngarkojmĂ« dhe lidhemi me programet BPF me pĂ«rpjekje minimale pĂ«r studimin e tyre tĂ« mĂ«tejshĂ«m.

PĂ«r tĂ« filluar, na nevojitet tĂ« gjenerojmĂ« "skele" tĂ« programit tonĂ« nga binari i saj me ndihmĂ«n e tĂ« njĂ«jtit utilitar bpftool — thjesht i thjeshtĂ« i thjesht i thjesht i thjesht BPF (cili mund tĂ« kuptohet dhe dosido, pasi Daniel Borkman Ă«shtĂ« njĂ« nga krijuesit dhe mirĂ«mbajtĂ«sit e BPF — Ă«shtĂ« zviceran):

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

NĂ« skedar xdp-simple.skel.h pĂ«rmban kodin binar tĂ« programit tonĂ« dhe funksionet pĂ«r menaxhimin — ngarkimin, lidhjen, fshirjen e objektit tonĂ«. NĂ« rastin tonĂ« tĂ« thjeshtĂ«, kjo duket si njĂ« overkill, por funksionon dhe kur skedari objekt pĂ«rmban shumĂ« programe BPF dhe hartĂ« dhe pĂ«r tĂ« ngarkuar kĂ«tĂ« ELF gjigand, mjafton tĂ« gjenerojmĂ« njĂ« skelet dhe tĂ« thĂ«rrasim njĂ« ose dy funksione nga aplikacioni i pĂ«rdoruesit, nĂ« shkrimin e tĂ« cilit tani po kalojmĂ«.

Në të vërtetë, programi ynë ngarkues është triviale:

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

Këtu struct xdp_simple_bpf përcaktohet në skedarin xdp-simple.skel.h dhe përshkruan skedarin tonë objekt:

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

Mund tĂ« vĂ«rejmĂ« kĂ«tu gjurmĂ«t e API-t tĂ« ulĂ«t: struktura struct bpf_program *simple dhe struct bpf_link *simple. Struktura e parĂ« pĂ«rshkruan konkretisht programin tonĂ«, tĂ« regjistruar nĂ« seksionin xdp/simple, ndĂ«rsa e dyta — pĂ«rshkruan se si programa lidhet me burimin e ngjarjeve.

Funksioni xdp_simple_bpf__open_and_load, hap skedarin ELF, e pĂ«rpunon atĂ«, krijon tĂ« gjitha struktura dhe nĂ«nstruktura (pĂ«rveç programit, nĂ« ELF ka dhe seksione tĂ« tjera — tĂ« dhĂ«nĂ«, tĂ« dhĂ«na tĂ« lexueshme, informacione rreth debugging, licenca etj.), dhe pastaj ngarkon nĂ« bĂ«rthamĂ« pĂ«rmes njĂ« thirrjeje sistemike bpf, çka mund ta verifikojmĂ« duke e kompiluar dhe ekzekutuar programin:

$ 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

Tani le të shohim programin tonë me ndihmën e bpftool. Do të gjejmë ID-në e tij:

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

dhe do ta dumpojmë (ne përdorim forma të shkurtuara të komandës bpftool prog dump xlated):

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

Diçka e re! Programi printoi copëza të skedarit tonë burimor në gjuhën C. Kjo u realizua nga biblioteka libbpf, e cila gjeti seksionin e debugging në binar, e kompiloi atë në një objekt BTF, e ngarkoi në bërthamë përmes BPF_BTF_LOAD, dhe pastaj tregoi file descriptor të marrë gjatë ngarkimit të programit me komandën BPG_PROG_LOAD.

Ndihmësit e Kernelit

Programet BPF mund tĂ« pĂ«rfshijnĂ« funksione "jashtme" — ndihmĂ«s tĂ« kernelit. KĂ«ta funksione ndihmes mundĂ«sojnĂ« qĂ« programet BPF tĂ« kenĂ« akses nĂ« strukturat e kernelit, tĂ« menaxhojnĂ« maps, si dhe tĂ« komunikojnĂ« me "botĂ«n reale" — tĂ« krijojnĂ« ngjarje perf, tĂ« menaxhojnĂ« pajisjet (p.sh., tĂ« ridrejtojnĂ« paketa) etj.

Shembull: bpf_get_smp_processor_id

Në kuadër të paradigmës "mësojmë nga shembujt", le të shqyrtojmë një nga funksionet ndihmëse, bpf_get_smp_processor_id(), të përcaktuar në skedarin kernel/bpf/helpers.c. Ajo kthen numrin e procesorit mbi të cilin ekzekutohet programi BPF që e thirri. Por ne nuk jemi aq të interesuar për semantikën e saj, sa për faktin që realizimi i saj zë një rresht:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Përcaktimet e funksioneve ndihmëse BPF ngjajnë me përcaktimet e thirrjeve sistemike Linux. Këtu, për shembull, përcaktohet një funksion pa argumente. (Një funksion që merr, le të themi, tre argumente, përcaktohet duke përdorur makron BPF_CALL_3. Numri maksimal i argumenteve është pesë.) Megjithatë, kjo është vetëm pjesa e parë e përcaktimit. Pjesa e dytë përfshin përcaktimin e strukturës së tipit struct bpf_func_proto, e cila përmban përshkrimin e funksionit ndihmëse, të kuptueshëm për verifier:

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

Regjistrimi i funksioneve ndihmëse

Në mënyrë që programet BPF të një lloji të caktuar të mund të përdorin këtë funksion, ata duhet ta regjistrojnë atë, për shembull, për tipin BPF_PROG_TYPE_XDP në kernel përcaktohet funksioni xdp_func_proto, i cili përmes ID-së së funksionit ndihmëse përcakton nëse XDP e mbështet këtë funksion apo jo. Funksioni ynë mbështet:

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

Llojet e reja të programeve BPF "përcaktohen" në skedarin include/linux/bpf_types.h nëpërmjet makros BPF_PROG_TYPE. Përcaktimet ndihen në cita, sepse kjo është një përcaktim logjik, dhe në terma të gjuhës C përcaktimi i një grupi konkret strukturash ndodhi në vende të tjera. Në veçanti, në skedarin kernel/bpf/verifier.c të gjitha përcaktimet nga skedari bpf_types.h përdoren për të krijuar një masiv strukturash 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
};

Pra, për çdo lloj programi BPF përcaktohet një tregues për strukturën e të dhënave të tipit struct bpf_verifier_ops, i cili inicializohet me vlerën _name ## _verifier_ops, dmth., xdp_verifier_ops për xdp. Struktura xdp_verifier_ops përcaktohet në skedarin net/core/filter.c si nga:

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

Këtu e shohim funksionin tonë të njohur xdp_func_proto, i cili do të aktivizojë verifier çdo herë që ai përballet me thirrjen ndonjë funksioni brenda programit BPF, shiko. verifier.c.

Të shohim se si një program hipotetik BPF e përdor funksionin bpf_get_smp_processor_id. Për këtë, do ta riparojmë programin nga seksioni ynë i mëparshëm si më poshtë:

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

Simboli bpf_get_smp_processor_id përcaktohet në <bpf/bpf_helper_defs.h> bibliotekës libbpf si

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

dmth., bpf_get_smp_processor_id — ky Ă«shtĂ« njĂ« tregues pĂ«r funksionin, vlera e tĂ« cilit Ă«shtĂ« 8, ku 8 Ă«shtĂ« vlera BPF_FUNC_get_smp_processor_id llojin e enum bpf_fun_id, e cila pĂ«rcaktohet pĂ«r ne nĂ« skedarin vmlinux.h (skedari bpf_helper_defs.h nĂ« kernel krijohet nga njĂ« skenar, prandaj "numrat magjike" janĂ« nĂ« rregull). Ky funksion nuk pranon argumente dhe kthen njĂ« vlerĂ« tĂ« tipit __u32. Kur e aktivizojmĂ« nĂ« programin tonĂ«, clang gjeneron njĂ« instrukcion BPF_CALL i "duhur". Le tĂ« kompilohet programi dhe tĂ« shohim seksionin 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

NĂ« rreshtin e parĂ« ne shohim instrukcionin call, parametri IMM i cili Ă«shtĂ« i barabartĂ« me 8, dhe SRC_REG — zero. Sipas marrĂ«veshjes ABI, e cila pĂ«rdoret nga verifier, kjo Ă«shtĂ« thirrja pĂ«r funksionin ndihmĂ«s me numĂ«r nĂ«ntĂ«. Pasi tĂ« aktivizohet, logjika Ă«shtĂ« e thjeshtĂ«. Vlera e kthyer nga regjistri r0 kopjohet nĂ« r1 dhe nĂ« rreshtat 2,3 konvertohet nĂ« tipin u32 — bitĂ«t e lartĂ« 32 zerohen. NĂ« rreshtat 4,5,6,7 kthen 2 (XDP_PASS) ose 1 (XDP_DROP) nĂ« varĂ«si tĂ« faktit nĂ«se funksioni ndihmĂ«s nga rreshti 0 ktheu njĂ« vlerĂ« zero ose jo-zero.

Le të kontrollojmë veten: ngarko programin dhe shiko rezultatin 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

Mirë, verifier zbuloi ndihmësin e saktë të kernelit.

Shembull: kalojmë argumentet dhe, në fund, e drejtojmë programin!

Të gjitha funksionet ndihmëse në nivelin e ekzekutimit kanë një prototip

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

Parametrat pĂ«r funksionet ndihmĂ«se kalohen nĂ« regjistra r1—r5, dhe vlera kthehet nĂ« regjistrin r0. Nuk ka funksione qĂ« pranojnĂ« mĂ« shumĂ« se pesĂ« argumente — dhe nuk Ă«shtĂ« planifikuar tĂ« shtohet mbĂ«shtetje pĂ«r to nĂ« tĂ« ardhmen.

Le të shohim ndihmësin e ri të kernelit dhe se si BPF kalon parametrat. Do të riprogramojmë xdp-simple.bpf.c në këtë mënyrë (rreshtat e tjerë nuk u ndryshuan):

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

Programi ynë printon numrin e CPU-s, në të cilin është drejtuar. Do ta kompilohet dhe do ta shohim kodin:

$ 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

NĂ« rreshtat 0-7 ne shkruajmĂ« nĂ« stak rrjedhĂ«n running on CPU%un, dhe mĂ« pas nĂ« rreshtin 8 drejtojmĂ« atĂ« qĂ« na Ă«shtĂ« njohur bpf_get_smp_processor_id. NĂ« rreshtat 9-12 ne pĂ«rgatisim argumentet pĂ«r ndihmĂ«sin bpf_printk — regjistrat r1, r2, r3. Pse janĂ« tre, e jo dy? Sepse bpf_printk — ështĂ« njĂ« makroshtesĂ« rreth ndihmĂ«sit tĂ« vĂ«rtetĂ« bpf_trace_printk, i cili kĂ«rkon tĂ« dĂ«rgojĂ« madhĂ«sinĂ« e vargjeve tĂ« formatit.

Tani le të shtojmë disa rreshta në xdp-simple.c, në mënyrë që programi ynë të lidhet me ndërfaqen lo dhe të ekzekutohet në të vërtetë!

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

Këtu përdorim funksionin bpf_set_link_xdp_fd, i cili lidh programet BPF të tipit XDP me ndërfaqet rrjetësore. Ne e kemi hardkoduar numrin e ndërfaqes lo, i cili gjithmonë është i barabartë me 1. Ne drejtojmë funksionin dy herë, për të shkëputur fillimisht programin e vjetër, nëse ai ka qenë i lidhur. Vini re se tani nuk na nevojitet thirrja pauzë ose cikli i pafund: programi ynë ngarkues do të përfundojë, por programu BPF nuk do të shkatërrohet, sepse është lidhur me burimin e ngjarjeve. Pas ngarkimit të suksesshëm dhe lidhjes, programi do të fillojë të ekzekutohet për çdo paketë rrjeti që mbërrin në lo.

Të ngarkojmë programin dhe të shikojmë ndërfaqen lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  emri simple  tag 4fca62e77ccb43d6  gpl
$ ip l show dev lo
1: lo:  mtu 65536 xdpgeneric qdisc noqueue gjendja UNKNOWN mode DEFAULT grupi default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

Programi që ngarkuam ka ID 669 dhe të njëjtin ID e shohim në ndërfaqe lo. Do të dërgojmë disa paketa në 127.0.0.1 (kërkesa + përgjigje):

$ ping -c1 localhost

dhe tani do të shikojmë përmbajtjen e skedarit virtual të debuggingut /sys/kernel/debug/tracing/trace_pipe, në të cilin bpf_printk shkruan mesazhet e tij:

# 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

Dy paketa u vĂ«rejtĂ«n nĂ« lo dhe u pĂ«rpunuan nĂ« CPU0 — programi ynĂ« i parĂ« plotĂ«sisht i padobishĂ«m BPF funksionoi!

Vlen të theksohet se bpf_printk nuk është pa arsye që shkruan në skedarin e debuggingut: kjo nuk është ndihmësja më e përshtatshme për t'u përdorur në prodhim, por qëllimi ynë ishte të tregonim diçka të thjeshtë.

Aksesi në hartat nga programet BPF

Shembulli: përdorim një hartë nga programi BPF

Në seksionet e mëparshme, ne mësuam të krijonim dhe të përdornim harta nga hapësira e përdoruesit, tani do të shikojmë pjesën bërthamore. Të fillojmë, si zakonisht, me një shembull. Ta rishkruajmë programin tonë xdp-simple.bpf.c si nga:

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

Në fillim të programit shtuam përcaktimin e hartës woo: është një array me 8 elemente, në të cilin ruhen vlerat e tipit u64 (në C do ta përcaktonim këtë array si u64 woo[8]). Në programin "xdp/simple" marrim numrin e procesorit aktual në një variabël key dhe pastaj me funksionin ndihmës bpf_map_lookup_element marrim një tregues në regjistrimin përkatës në array, të cilin e rrisim me një. Në përkthim në shqip: po numërojmë statistikën e asaj që, në cilin CPU janë përpunuar paketat e ardhshme. Le të provojmë të ekzekutojmë programin:

$ 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

Le të kontrollojmë se ajo është lidhur me lo dhe të dërgojmë disa paketa:

$ 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

Tani do të shohim përmbajtjen e array-t:

$ sudo bpftool map dump name woo
[
    { "key": 0, "value": 0 },
    { "key": 1, "value": 400 },
    { "key": 2, "value": 0 },
    { "key": 3, "value": 0 },
    { "key": 4, "value": 0 },
    { "key": 5, "value": 0 },
    { "key": 6, "value": 0 },
    { "key": 7, "value": 46400 }
]

Gati të gjitha proceset u përpunuan në CPU7. Kjo nuk është e rëndësishme për ne, gjëja kryesore është që programi punon dhe ne kuptuam si të qasjejmë në harta nga programet BPF - përmes ndihmësve bpf_mp_*.

Përfaqësuesi mistik

Pra, ne mund të qasemi nga programi BPF në hartë përmes thirrjeve të tilla:

val = bpf_map_lookup_elem(&woo, &key);

ku funksioni ndihmës duket si

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

por ne po kalojmë treguesin &woo në një strukturë të paemërtuar struct { ... }


Nëse shohim asamblin e programit, do të shohim se vlera &woo nuk është në të vërtetë e përcaktuar (rreshti 4):

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

xdp-simple.bpf.o:     formati i skedarit elf64-bpf

Shkëputja e seksionit xdp/simple:

0000000000000000 :
       0:       85 00 00 00 08 00 00 00 thirrja 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 thirrja 1
...

dhe është e pranishme në relokime:

$ llvm-readelf -r xdp-simple.bpf.o | head -4

Sektori i relokimit '.relxdp/simple' në offset 0xe18 përmban 1 hyrje:
    Offset             Info             Type               Vlera e Simbolit  Emri i Simbolit
0000000000000020  0000002700000001 R_BPF_64_64            0000000000000000 woo

Por nëse shohim programin tashmë të ngarkuar, do të shohim treguesin në hartën e duhur (rreshti 4):

$ sudo bpftool prog dump x emri simple
int simple(void *ctx):
   0: (85) thirrja bpf_get_smp_processor_id#114128
   1: (63) *(u32 *)(r10 -4) = r0
   2: (bf) r2 = r10
   3: (07) r2 += -4
   4: (18) r1 = hartë[id:64]
...

Kështu, mund të nxjerrim përfundimin se në momentin e ekzekutimit të programit tonë të ngarkuesit, referenca në &woo u zëvendësua nga biblioteka libbpf. Fillimisht do të shohim rezultatin 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

Ne shohim se libbpf kreu një hartë woo dhe pastaj ngarkoi programin tonë simple. Le të shohim më në detaje si e ngarkojmë programin:

  • thirrim xdp_simple_bpf__open_and_load nga skedari xdp-simple.skel.h
  • i cili thĂ«rret xdp_simple_bpf__load nga skedari xdp-simple.skel.h
  • i cili thĂ«rret bpf_object__load_skeleton nga skedari libbpf/src/libbpf.c
  • i cili thĂ«rret bpf_object__load_xattr nga libbpf/src/libbpf.c

Funksioni i fundit, përveç të gjitha gjërave të tjera, do të thërrasë bpf_object__create_maps, e cila krijon ose hap ekzistuesit maps, duke i kthyer ato në deshifrues skedarësh. (Kjo është ajo ku ne shohim BPF_MAP_CREATE në dalje strace.) Më pas thirret funksioni bpf_object__relocate dhe pikërisht ai na intereson, pasi ne e kujtojmë se çfarë kemi parë woo në tabelën e relokimeve. Duke e hulumtuar atë, përfundojmë në funksionin bpf_program__relocate, i cili merret me relokimet e mapave:

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

Pra, ne marrim instruktionen tonë

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

dhe e zëvendësojmë regjistrin burimor me BPF_PSEUDO_MAP_FD, dhe IMM e parë me deshifruesin e skedarëve të mapës tonë, dhe, nëse ai është, për shembull, 0xdeadbeef, atëherë në rezultat ne do të marrim instruktionen

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

Kështu kalon informacioni mbi mapat në programin specifik të ngarkuar BPF. Mapa mund të krijohet ose të hapet sipas ID-së përmes BPF_MAP_CREATE, ashtu siç është hapur përmes BPF_MAP_GET_FD_BY_ID.

Në përmbledhje, përdorimi i libbpf algoritmi është si më poshtë:

  • gjatĂ« kompilimit, pĂ«r referencat nĂ« mapa krijohen shĂ«nime nĂ« tabelĂ«n e relokimit
  • libbpf hap objekti ELF, gjen tĂ« gjitha mapat qĂ« pĂ«rdoren dhe krijon pĂ«r to deshifrues skedarĂ«sh
  • deshifruesit e skedarĂ«ve ngarkohen nĂ« bĂ«rthamĂ« si pjesĂ« e instruktionit LD64

Siç e kuptoni, kjo ende nuk Ă«shtĂ« gjithçka, dhe do tĂ« duhet tĂ« shohim nĂ« bĂ«rthamĂ«. FatmirĂ«sisht, kemi njĂ« indicie — kemi shkruar vlerĂ«n BPF_PSEUDO_MAP_FD nĂ« regjistrin burimor dhe mund ta harmizojmĂ« atĂ«, e cila do na çojĂ« nĂ« vendin e shenjtĂ« — kernel/bpf/verifier.c, ku funksioni me emrin karakteristik zĂ«vendĂ«son deshifruesin e skedarĂ«ve me adresĂ«n e struktures sĂ« tipit 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;

(Kodi i plotë mund të gjendet në lidhje). Pra, mund ta plotësojmë algoritmin tonë:

  • gjatĂ« ngarkimit tĂ« programit, verifier-i kontrollon saktĂ«sinĂ« e pĂ«rdorimit tĂ« mapĂ«s dhe shkruan adresĂ«n e strukturĂ«s pĂ«rkatĂ«se struct bpf_map

Kur ngarkohet binari ELF pĂ«rmes libbpf ndodhin shumĂ« ngjarje, por ne do t’i diskutojmĂ« ato nĂ« artikuj tĂ« tjerĂ«.

Ngarko programet dhe mapat pa libbpf

Siç ishte premtuar, ja një shembull për lexuesit që duan të dinë se si të krijojnë dhe ngarkojnë një program që përdor mapa, pa ndihmën libbpf. Kjo mund të jetë e dobishme kur punoni në një mjedis ku nuk mund të mbledhni varësitë, ose kurseni çdo bit, ose shkruani një program të tipit ply, i cili gjeneron kodin binar BPF në fluks.

Për ta bërë më të lehtë ndjekjen e logjikës, ne për këto qëllime do ta rishkruajmë shembulli tonë xdp-simple. Kodi i plotë dhe pak më i zgjeruar i programit që shqyrtohet në këtë shembull mund ta gjeni në këtë gist.

Logjika e aplikacionit tonë është si më poshtë:

  • krijoni njĂ« hartĂ« tĂ« tipit BPF_MAP_TYPE_ARRAY me anĂ« tĂ« komandĂ«s BPF_MAP_CREATE,
  • krijoni njĂ« program qĂ« pĂ«rdor kĂ«tĂ« hartĂ«,
  • lidheni programin me ndĂ«rfaqen lo,

e cila përkthehet në njerëzor si

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

KĂ«tu map_create krijton hartĂ«n nĂ« tĂ« njĂ«jtĂ«n mĂ«nyrĂ« siç e bĂ«mĂ« nĂ« shembullin e parĂ« pĂ«r thirrjen sistemike bpf — "bĂ«rthama, tĂ« lutem, mĂ« bĂ«j njĂ« hartĂ« tĂ« re nĂ« formĂ«n e njĂ« vargu me 8 elemente tĂ« tipit __u64 dhe mĂ« kthe njĂ« deshcriptor skedari":

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

Programi gjithashtu ngarkohet lehtësisht:

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

Pjesa e komplikuar prog_load — Ă«shtĂ« definimi i programit tonĂ« BPF si njĂ« varg strukturash struct bpf_insn insns[]. Por pasi pĂ«rdorim programin qĂ« kemi nĂ« C, mund tĂ« rrejshim pak:

$ 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

Pra, na nevojitet të shkruajmë 14 instrukcione në formën e strukturave të tipit struct bpf_insn (këshillë: merrni dump-in lart, rishikoni seksionin për udhëzime, hapni linux/bpf.h dhe linux/bpf_common.h dhe provoni të përcaktoni struct bpf_insn insns[] vetë):

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  */
    {
        .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,
    },

    /* : 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
    },
};

UshtĂ«rim pĂ«r ata qĂ« nuk e shkruan kĂ«tĂ« vetĂ« — gjeni map_fd.

NĂ« programin tonĂ« ka mbetur njĂ« pjesĂ« tjetĂ«r e zbuluar — xdp_attach. FatkeqĂ«sisht, programet e tipit XDP nuk mund tĂ« lidhen pĂ«rmes thirrjes sistemike bpf. NjerĂ«zit qĂ« krijuan BPF dhe XDP ishin nga komuniteti i rrjetit tĂ« Linux-it, prandaj ata pĂ«rdorĂ«n ndĂ«rfaqen mĂ« tĂ« njohur pĂ«r ta (por jo pĂ«r njerĂ«zit ) ndĂ«rveprimi me bĂ«rthamĂ«n: netlink sockets, shih gjithashtu RFC3549. MĂ«nyra mĂ« e thjeshtĂ« pĂ«r tĂ« realizuar xdp_attach Ă«shtĂ« kopjimi i kodit nga libbpf, konkretisht, nga skedari netlink.c, tĂ« cilin ne e bĂ«mĂ«, duke e shkurtuar pak:

Mirë se vini në botën e soket-eve netlink

Hapni soketin netlink të tipit 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("raporti i gabimeve në netlink nuk përkrahet");

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

Lexojmë nga një soket të tillë:

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, "pid i gabuar");
            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); shumë kode për tu kopjuar...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

Finalmente, ja funksioni ynë që hap soketin dhe dërgon një mesazh të veçantë që përmban një descriptor skedari:

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

Pra, gjithçka është gati për testim:

$ 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
+++ doli me 0 +++

Le të shohim nëse programi ynë është lidhur me 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

Do të dërgojmë ping të shohim map-in:

$ 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
Gjetur 8 elemente

Të hënën, gjithçka funksionon. Vini re, gjithashtu, se map-i ynë sërish shfaqet në formën e bajtave. Kjo ndodh për shkak se, në ndryshim nga libbpf ne nuk kemi ngarkuar informacionin mbi tipet (BTF). Por më shumë për këtë do të flasim herën tjetër.

Mjetet e zhvillimit

Në këtë seksion, do të shohim kitin minimal të veglave për zhvilluesit BPF.

NĂ« pĂ«rgjithĂ«si, pĂ«r zhvillimin e programeve BPF nuk nevojitet asgjĂ« e veçantĂ« — BPF funksionon nĂ« çdo bĂ«rthame tĂ« mirĂ« tĂ« distribucioneve, dhe programet kompilohet pĂ«rmes clang, qĂ« mund tĂ« vendoset nga paketa. MegjithatĂ«, pĂ«r shkak se BPF Ă«shtĂ« nĂ« proces zhvillimi, bĂ«rthama dhe mjetet vazhdojnĂ« tĂ« ndryshojnĂ«, nĂ«se nuk dĂ«shironi tĂ« shkruani programe BPF me metodat e vjetra nga vitet 2019, do t'ju duhet tĂ« ndĂ«rtoni

  • llvm/clang
  • pahole
  • berthamen tuaj
  • bpftool

(Për informacion: kjo pjesë dhe të gjithë shembujt në artikull janë ekzekutuar në Debian 10.)

llvm/clang

BPF punon me LLVM dhe, megjithëse në kohët e fundit programet për BPF mund të kompilohen gjithashtu me gcc, gjithë zhvillimi aktual po bëhet për LLVM. Prandaj, fillimisht ne do të ndërtojmë versionin aktual clang nga git:

$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86" 
                      -DLLVM_ENABLE_PROJECTS="clang" 
                      -DBUILD_SHARED_LIBS=OFF 
                      -DCMAKE_BUILD_TYPE=Release 
                      -DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... shumë kohë më vonë
$

Tani mund të kontrollojmë nëse gjithçka është ndërtuar siç duhet:

$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM version 11.0.0git
  Ndërtesë e optimizuar.
  Targeti default: x86_64-unknown-linux-gnu
  CPU i hostit: znver1

  Targetet e regjistruara:
    bpf    - BPF (endi i hostit)
    bpfeb  - BPF (endi i madh)
    bpfel  - BPF (endi i vogël)
    x86    - 32-bit X86: Pentium-Pro dhe më lart
    x86-64 - 64-bit X86: EM64T dhe AMD64

(Udhëzimi për ndërtim clang e kam marrë nga bpf_devel_QA.)

Ne nuk do të instalojmë programet e sapo ndërtuara, por përkundrazi do t'i shtojmë ato në PATH, për shembull:

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

(Këtë mund ta shtoni në .bashrc ose në një skedar të veçantë. Personalish, unë i shtoj këto gjëra në ~/bin/activate-llvm.sh dhe kur është e nevojshme bëj . activate-llvm.sh.)

Pahole dhe BTF

Utilitari pahole përdoret gjatë ndërtimit të bërthamës për të krijuar informacionin e zgjidhjes në formatin BTF. Ne nuk do të ndalemi në detajet e teknologjisë BTF në këtë artikull, përveç faktit që është e dobishme dhe ne dëshirojmë ta përdorim. Prandaj, nëse po planifikoni të ndërtoni bërthamën tuaj, ndërtoni së pari pahole (pa pahole nuk do të jeni në gjendje të ndërtoni bërthamën me opsionin 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

Bërthama për eksperimente me BPF

Kur të shqyrtojmë mundësitë e BPF-së, dëshirojmë të mbledhim një bërthamë të vetën. Kjo, në terma të përgjithshëm, nuk është e detyrueshme, pasi ju mund të mbledhni dhe ngarkoni programe BPF edhe me bërthamën e shpërndarjes, megjithatë, pasja e një bërthame të vetën lejon përdorimin e mundësive më të reja të BPF-së, të cilat do të jenë në shpërndarjen tuaj në rastin më të mirë pas disa muajsh ose, siç është rasti me disa mjete diagnostikuese, nuk do të paketohen aspak në të ardhmen e afërt. Gjithashtu, bërthama juaj lejon të ndiheni të rëndësishëm duke eksperimentuar me kodin.

Për të ndërtuar bërthamën, ju nevojitet, së pari, vetë bërthama dhe, së dyti, skedari i konfigurimit të bërthames. Për eksperimentet me BPF mund të përdorim bërthamën vanilje ose një nga bërthamën zhvillimore. Historikisht, zhvillimi i BPF ndodh brenda komunitetit rrjetor të Linux-it dhe prandaj të gjitha ndryshimet, herët a vonë, kalojnë përmes David Miller - mbanësit të pjesës rrjetore të Linux-it. Në varësi të natyrës së saj - ndreqje apo karakteristika të reja - ndryshimet rrjetore shkojnë në një nga dy bërthamat - net ose net-next. Ndryshimet për BPF shpërndahen po ashtu nën bpf dhe bpf-next, të cilat më pas dërgohen në net dhe net-next, përkatësisht. Më shumë informacion mund të gjeni në bpf_devel_QA dhe netdev-FAQ. Pra, zgjidhni bërthamën sipas shijes suaj dhe nevojave për stabilitetin e sistemit në të cilin po testoni (*-next bërthamat janë më të paqëndrueshmet nga ato që përmenden).

Ky artikull nuk përfshin tregimin se si të menaxhojmë skedarët e konfigurimit të bërthames - supozohet se ju tashmë e dini si ta bëni këtë ose jeni të gatshëm të mësoni vetë. Megjithatë, instrukcioni në vijim duhet të jetë, më shumë apo më pak, mjaftueshëm që ju të krijoni një sistem funksional me mbështetje për BPF.

Shkarkoni një nga bërthamat e përmendura më sipër:

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

Ndërtoni një konfigurim minimal funksional të bërthames:

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

Aktivizoni opsionet BPF në skedarin .config sipër të zgjedhjes suaj (më së shumti, vetë CONFIG_BPF do të jetë tashmë i aktivizuar, pasi përdoret nga systemd). Këtu është lista e opsioneve nga bërthama që është përdorur për këtë artikull:

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 nuk është vendosur
# CONFIG_BPFILTER nuk është vendosur
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

Më pas ne mund të mbledhim dhe instalojmë lehtësisht modulet dhe kernelin (përbyllësisht, mund të ndërtojmë kernelin vetëm me atë që sapo u ndërtua clang, duke shtuar CC=clang):

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

dhe të rinisim me kernelin e ri (unë e përdor këtë për kexec nga paketa kexec-tools):

v=5.8.0-rc6+ # nëse po rindërtoni kernelin aktual, mund të bëni v=`uname -r`
sudo kexec -l -t bzImage \/boot\/vmlinuz-$v --initrd=\/boot\/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

bpftool

Vegla e përdorur më shpesh në këtë artikull do të jetë vegla bpftool, e cila ofrohet si pjesë e kernelit Linux. Ajo është shkruar dhe mbështetur nga zhvilluesit e BPF për zhvilluesit e BPF dhe me ndihmën e saj mund të menaxhohen të gjitha llojet e objekteve BPF - të ngarkohen programe, të krijohen dhe të ndryshohen hartat, të hulumtohet jeta e ekosistemit të BPF, etj. Dokumentacioni në formën e burimeve për faqet man mund të gjendet në kernel ose, tashmë e kompiliuar, në internet.

Në momentin e shkruar bpftool ofrohet në formë të gatshme vetëm për RHEL, Fedora dhe Ubuntu (shihni, për shembull, këtë bisedë, e cila tregon historinë e papërfunduar të paketimit bpftool në Debian). Por nëse tashmë e keni ndërtuar kernelin tuaj, atëherë ta ndërtosh bpftool është tepër e lehtë:

$ cd ${linux}\/tools\/bpf\/bpftool
# ... shkruani rrugët te clang-i më i fundit, siç u përmend më sipër
$ make -s

Duke auto-detected karakteristikat e sistemit:
...                        libbfd: [ në  ]
...        disassembler-four-args: [ në  ]
...                          zlib: [ në  ]
...                        libcap: [ në  ]
...               clang-bpf-co-re: [ në  ]

Duke auto-detected karakteristikat e sistemit:
...                        libelf: [ në  ]
...                          zlib: [ në  ]
...                           bpf: [ në  ]

$

(kĂ«tu ${linux} — Ă«shtĂ« direktoria juaj me kernelin.) Pas pĂ«rfundimit tĂ« kĂ«tyre komandave bpftool do tĂ« ndĂ«rtosh nĂ« direktorinĂ« ${linux}\/tools\/bpf\/bpftool dhe mund ta shkruani nĂ« rrugĂ« (ndĂ«r tĂ« parat pĂ«r pĂ«rdoruesin root) ose ta kopjoni thjesht nĂ« /usr/local/sbin.

ËshtĂ« mĂ« mirĂ« tĂ« ndĂ«rtosh bpftool me ndihmĂ«n e fundit clang, e ndĂ«rtuar, siç Ă«shtĂ« pĂ«rmendur mĂ« sipĂ«r, dhe pĂ«r ta verifikuar nĂ«se Ă«shtĂ« ndĂ«rtuar saktĂ« - duke pĂ«rdorur, pĂ«r shembull, komandĂ«n

$ sudo bpftool feature probe kernel
Duke skanuar konfigurimin e sistemit...
bpf() syscall për përdoruesit e pa privilegjuar është aktivizuar
Kompilatori JIT është aktivizuar
Fortifikimi i kompilatorit JIT është i çaktivizuar
Eksportet e kallsyms të kompilatorit JIT janë aktivizuar për root
...

e cila do të tregojë se cilat karakteristika BPF janë aktivizuar në kernelin tuaj.

Për më shumë, komandën e mëparshme mund ta ekzekutoni si

# bpftool f p k

Kjo është bërë sipas analogjisë me veglat nga paketa iproute2, ku mund të themi, për shembull, ip a s eth0 në vend të ip addr show dev eth0.

Përfundimi

BPF lejon qĂ« tĂ« krijohet njĂ« ‘kafshë’ pĂ«r tĂ« matur dhe ndryshuar nĂ« fluks funksionalitetin e bĂ«rthamĂ«s nĂ« mĂ«nyrĂ« efektive. Sistemi ka dalĂ« shumĂ« i suksesshĂ«m, nĂ« traditat mĂ« tĂ« mira tĂ« UNIX: njĂ« mekanizĂ«m i thjeshtĂ« qĂ« lejon (riprogramimin e)bĂ«rthamĂ«s, i ka dhĂ«nĂ« mundĂ«si njĂ« numri tĂ« madh njerĂ«zish dhe organizatash tĂ« eksperimentojnĂ«. Dhe, ndonĂ«se eksperimentet, si dhe zhvillimi i infrastrukturĂ«s sĂ« BPF, ende nuk janĂ« pĂ«rfunduar, sistemi tashmĂ« ka njĂ« ABI tĂ« qĂ«ndrueshĂ«m qĂ« lejon ndĂ«rtimin e logjikĂ«s biznesore tĂ« besueshme dhe, mĂ« e rĂ«ndĂ«sishmja, efektive.

Dua tĂ« theksoj se, sipas mendimit tim, teknologjia ka patur kaq popullaritet sepse nga njĂ«ra anĂ«, mund tĂ« luajmĂ« (arkitektura e makinĂ«s mund tĂ« kuptohet mĂ« shumĂ« ose mĂ« pak brenda njĂ« nate), ndĂ«rsa nga ana tjetĂ«r — tĂ« zgjidhim probleme qĂ« nuk ishin tĂ« zgjidhshme (bukur) pĂ«rpara shfaqjes sĂ« saj. KĂ«to dy komponente sĂ« bashku i detyrojnĂ« njerĂ«zit tĂ« eksperimentojnĂ« dhe tĂ« Ă«ndĂ«rrojnĂ«, çka çon nĂ« krijimin e disa zgjidhjeve inovative tĂ« reja.

Ky artikull, edhe pse nuk Ă«shtĂ« shumĂ« i shkurtĂ«r, Ă«shtĂ« vetĂ«m njĂ« hyrje nĂ« botĂ«n e BPF dhe nuk pĂ«rshkruan mundĂ«sitĂ« "e avancuara" dhe pjesĂ«t e rĂ«ndĂ«sishme tĂ« arkitekturĂ«s. Plani i mĂ«tejshĂ«m Ă«shtĂ« pak a shumĂ« i tillĂ«: artikulli tjetĂ«r do tĂ« jetĂ« njĂ« pĂ«rmbledhje e llojeve tĂ« programeve BPF (nĂ« bĂ«rthamĂ«n 5.8 mbĂ«shteten 30 lloje programesh), pastaj do tĂ« shikojmĂ« se si tĂ« shkruajmĂ« aplikacione tĂ« vĂ«rteta nĂ« BPF me shembuj programesh pĂ«r gjurmimin e bĂ«rthamĂ«s, pastaj do tĂ« vijĂ« koha pĂ«r njĂ« kurs mĂ« tĂ« thellĂ« mbi arkitekturĂ«n e BPF, dhe mĂ« pas — pĂ«r shembuj tĂ« aplikacioneve rrjet dhe tĂ« sigurisĂ« tĂ« BPF.

Artikujt e mëparshëm të këtij cikli

  1. BPF për më të vegjlit, pjesa zero: classic BPF

Lidhjet

  1. BPF dhe XDP Reference Guide — dokumentacioni pĂ«r BPF nga cilium, mĂ« saktĂ« nga Daniel Borkman, njĂ« nga krijuesit dhe mbajtĂ«sit e BPF. Ky Ă«shtĂ« njĂ« nga pĂ«rshkrimet e para serioze, e cila dallohet nga tĂ« tjera sepse Daniel e di saktĂ«sisht pĂ«r çfarĂ« flet dhe nuk ka gabime atje. NĂ« veçanti, nĂ« kĂ«tĂ« dokument pĂ«rshkruhet si tĂ« punoni me programet BPF tĂ« llojeve XDP dhe TC duke pĂ«rdorur utilitarin e njohur ip nga paketa iproute2.

  2. Dokumentacion/rrjetit/filter.txt — skedari origjinal me dokumentacionin pĂ«r BPF klasik dhe mĂ« pas pĂ«r BPF tĂ« zgjeruar. ËshtĂ« e dobishme tĂ« lexoni, nĂ«se dĂ«shironi tĂ« fusni duar nĂ« assembler dhe detajet teknike tĂ« arkitekturĂ«s.

  3. Blogu pĂ«r BPF nga facebook. Rritet rrallĂ«, por me precizitet, pasi atje shkruajnĂ« Alexei Starovoitov (krijuesi i eBPF) dhe Andrii Nakryiko — (mbajtĂ«si libbpf).

  4. Sekretet e bpftool. Një treg i këndshëm në twitter nga Quentin Monnet me shembuj dhe sekrete përdorimi të bpftool.

  5. Shkoni në BPF: një listë materialesh leximi. Një listë gjigante (dhe akoma e mbështetur) e lidhjeve në dokumentacionin BPF nga Quentin Monnet.

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster