BPF për fillestarët, pjesa e parë: extended BPF

Në fillim ishte teknologjia dhe ajo u quajt BPF. Ne e shqyrtuam atë në artikullit të mëparshëm, artikulli i vjetër i këtij cikli. Në vitin 2013, falë përpjekjeve të Alexei Starovoitov dhe Daniel Borkman, u zhvillua një version i përmirësuar dhe u përfshi në bërthamën Linux, e optimizuar për makinat moderne 64-bit. Kjo teknologji e re për një kohë të shkurtër u quajt Internal BPF, pastaj u ribërtërit në Extended BPF, dhe tani, pas disa vitesh, të gjithë e quajnë thjesht BPF.

Në terma të thjeshtë, BPF lejon ekzekutimin e kodit të rastësishëm, të ofruar nga përdoruesi, në hapësirën e bërthamës Linux dhe arkitektura e re rezultoi të jetë aq e suksesshme, sa që do të na duhen edhe dhjetra artikuj për të përshkruar të gjitha aplikimet e saj. (E vetmja gjë me të cilën zhvilluesit nuk u përballuan, siç mund ta shihni në bpm e mëposhtme, është krijimi i një logo të denjë.)

Në këtë artikull përshkruhet struktura e makinerisë virtuale BPF, ndërfaqet e bërthamës për të punuar me BPF, mjetet e zhvillimit, si dhe një përmbledhje e shkurtër, shumë e shkurtër, e mundësive ekzistuese, pra gjithçka që na nevojitet më vonë për një studim më të thellë të aplikimeve praktike të BPF.
BPF për fillestarët, pjesa e parë: extended BPF

Përmbledhje e shkurtër e artikullit

Hyrje në arkitekturën BPF. Fillimisht, ne do të shohim arkitekturën BPF nga një lartësi fluturimi dhe do të shënjojmë komponentët kryesorë.

Regjistrat dhe sistemi i komandave të makinerisë virtuale BPF. Duke pasur tashmë një ide për arkitekturën në përgjithësi, ne do të përshkruajmë strukturën e makinerisë virtuale BPF.

Cikli i jetĂ«s sĂ« objekteve BPF, file sistemi bpffs. NĂ« kĂ«tĂ« seksion, ne do tĂ« shohim mĂ« afĂ«r ciklin e jetĂ«s sĂ« objekteve BPF — programeve dhe hartave.

Menaxhimi i objekteve pĂ«rmes thirrjes sĂ« sistemit bpf. Duke pasur tashmĂ« njĂ« ide pĂ«r sistemin, ne 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Ă« tĂ« sistemit — bpf(2).

Shkruajmë programe BPF me libbpf.. Sigurisht, është e mundur të shkruhen programe përmes thirrjes së sistemit. Por është e komplikuar. Për një skenar më realistik, programuesit e bërthamës zhvilluan një bibliotekë libbpf. Ne do të krijojmë skeleti më të thjeshtë të aplikacionit BPF, të cilin do ta përdorim në shembujt e ardhshëm.

PĂ«rfituese tĂ« kernelit. KĂ«tu do tĂ« mĂ«sojmĂ« se si programet BPF mund tĂ« drejtojnĂ« funksionet ndihmĂ«se tĂ« bĂ«rthamĂ«s — njĂ« mjet qĂ«, sĂ« bashku me harta, zgjeron nĂ« mĂ«nyrĂ« parĂ«sore kapacitetet e BPF-sĂ« sĂ« re krahasuar me atĂ« klasike.

Qëndrimi në harta nga programet BPF. Në këtë pikë, ne do të dimë mjaftueshëm për të kuptuar se si pikërisht mund të krijojmë programe që përdorin harta. Dhe madje do të hedhim një sy në verifikuesin e madh dhe të fuqishëm.

Mjetet zhvillimi. Një seksion ndihmës mbi atë se si të ndërtojmë utilitetet e nevojshme dhe bërthamën për eksperimente.

Përfundim. 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ë jetë në artikujt e ardhshëm. Ne gjithashtu do të përmendim disa lidhje për studim të pavarur për ata që nuk kanë dëshirë apo mundësi të presin vazhdimin.

Hyjnizimi në arkitekturën BPF.

Para se të fillojmë të shqyrtojmë arkitekturën BPF, një herë të fundit do të referohemi në BPF-e klasike, e cila u krijua si përgjigje ndaj shfaqjes së makinave RISC dhe zgjidhte problemin e filtrimit efikas të paketave. Arkitektura rezultoi kaq e suksesshme, saqë, duke lindur në vitet e errëta të nëntëdhjetës në Berkeley UNIX, ajo u portua në shumicën e sistemeve operativë ekzistuese, duke mbijetuar deri në vitet e çmendura njëzet dhe ende gjen përdorime të reja.

BPF-i ri u zhvillua si një përgjigje ndaj përhapjes së gjërë të makinave 64-bit, shërbimeve cloud dhe nevojave të rritura për mjete për ndërtimin e SDN (Ssoftuer-ddefinuar nrrjetin). E krijuar nga inxhinierët e rrjeteve si një zëvendësim të përmirësuar të BPF-së klasike, BPF-i ri përfundimisht gjeti aplikime brenda gjashtë muajve për hetimin e sistemeve Linux, dhe tani, pas gjashtë vjetësh nga shfaqja e saj, do të kërkojmë një artikull të plotë vetëm për të renditur llojet e ndryshme të programeve.

VëSëLë KëRtInKë

NĂ« thelb, BPF Ă«shtĂ« njĂ« makinĂ« virtuale qĂ« vepron si njĂ« sandĂ«, e lejon ekzekutimin e kodit "tĂ« rastĂ«sishĂ«m" nĂ« hapĂ«sirĂ«n e kernelit pa rrezikuar sigurinĂ«. Programet BPF krijohen nĂ« hapĂ«sirĂ«n e pĂ«rdoruesit, ngarkohen nĂ« kernel dhe lidhen me njĂ« burim ngjarjesh. NjĂ« ngjarje mund tĂ« jetĂ«, pĂ«r shembull, dorĂ«zimi i njĂ« pakete nĂ« ndĂ«rfaqen rrjetĂ«, ekzekutimi i njĂ« funksioni tĂ« kernelit, etj. NĂ« rastin e paketĂ«s, programit BPF do t'i jenĂ« tĂ« disponueshme tĂ« dhĂ«nat dhe metadata e paketĂ«s (pĂ«r lexim dhe, ndoshta, pĂ«r shkrim, nĂ« varĂ«si tĂ« tipit tĂ« programit), ndĂ«rsa nĂ« rastin e ekzekutimit tĂ« funksionit tĂ« kernelit — argumentet e funksionit, duke pĂ«rfshirĂ« treguesit e memories sĂ« kernelit, etj.

Le të hedhim një vështrim më të detajuar në këtë proces. Fillimisht, le të flasim për dallimin e parë nga BPF tradicional, për të cilin programet shkruheshin në assembler. Në versionin e ri, arkitektura u përmirësua në mënyrë që programet të mund të shkruheshin në gjuhë të nivelit të lartë, kryesisht, natyrisht, në C. Për këtë, u zhvillua një backend për llvm, që mundëson gjenerimin e kodit të byte për arkitekturën BPF.

BPF për fillestarët, pjesa e parë: extended BPF

Arkitektura BPF u zhvillua, veçanĂ«risht, pĂ«r tĂ« funksionuar nĂ« mĂ«nyrĂ« efikase nĂ« makinat moderne. PĂ«r tĂ« siguruar qĂ« kjo tĂ« funksiononte nĂ« praktikĂ«, kodi i byte BPF, pas ngarkimit nĂ« kernel, pĂ«rkthehet nĂ« kod natyror me ndihmĂ«n e njĂ« komponenti tĂ« quajtur JIT compiler (Just In Time). MĂ« tej, nĂ«se e mbani mend, nĂ« BPF-nĂ« tradicionale, programi ngarkohej nĂ« kernel dhe lidhej me burimin e ngjarjeve nĂ« mĂ«nyrĂ« atomike — nĂ« kuadĂ«r tĂ« njĂ« thirrjeje sistemike. NĂ« arkitekturĂ«n e re, kjo ndodh nĂ« dy etapa — sĂ« pari, kodi ngarkohet nĂ« kernel pĂ«rmes njĂ« thirrjeje sistemike bpf(2), dhe pastaj, mĂ« vonĂ«, pĂ«rmes mekanizmave tĂ« tjerĂ«, tĂ« ndryshĂ«m nĂ« varĂ«si tĂ« tipit tĂ« programit, programi lidhet (attaches) me burimin e ngjarjeve.

Këtu mund të lindë një pyetje për lexuesin: a ka qenë kështu? Si i garanton siguria ekzekutimin e tillë të kodit? Siguria e ekzekutimit garanton nga faza e ngarkimit të programeve BPF të quajtur verifikues (në anglisht këtë fazë e quajnë verifier dhe unë do ta përdor më tej fjalën në anglisht):

BPF për fillestarët, pjesa e parë: extended BPF

Verifier Ă«shtĂ« njĂ« analizator statik qĂ« garanton se programi nuk do tĂ« ndĂ«rhyjĂ« nĂ« funksionimin normal tĂ« bĂ«rthamĂ«s. Kjo, megjithatĂ«, nuk do tĂ« thotĂ« qĂ« programi nuk mund tĂ« ndĂ«rhyjĂ« nĂ« funksionimin e sistemit — programet BPF, varĂ«sisht nga lloji, mund tĂ« lexojnĂ« dhe shkruajnĂ« pjesĂ« tĂ« memories sĂ« bĂ«rthamĂ«s, tĂ« kthejnĂ« vlera funksionesh, tĂ« prishin, plotĂ«sojnĂ«, 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Ă« njĂ« program, i cili sipas rregullave ka leje shkruese, si pĂ«r shembull tĂ« dhĂ«nat e paketĂ«s sĂ« dalsim, nuk do tĂ« jetĂ« nĂ« gjendje tĂ« rishkruajĂ« memorjen e bĂ«rthamĂ«s jashtĂ« paketĂ«s. Pak mĂ« nĂ« detaje, ne do tĂ« shikojmĂ« verifier nĂ« seksionin pĂ«rkatĂ«s, pasi tĂ« njihemi me tĂ« gjithĂ« komponentĂ«t e tjerĂ« 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 një thirjeje sistemike bpf(2), ku ai kalon verifikimin në verifier dhe përkthimin në kodin nativ të bajtëve. Më pas, të njëjtin ose një përdorues tjetër lidh programin me burimin e ngjarjeve dhe ai fillon të ekzekutohet. Ndara e ngarkimit dhe lidhjes është e nevojshme për disa arsye. Së pari, ekzekutimi i verifier është relativisht i kushtueshëm dhe, duke ngarkuar të njëjtin program disa herë, ne humbasim kohë kompjuterike pa kuptim. Së dyti, mënyra se si lidhet programi varet nga lloji i tij dhe një "ndërfaqe universale" e dizajnuar një vit më parë mund të mos jetë e përshtatshme për lloje të reja programesh. (Megjithatë, tani, kur arkitektura po bëhet më e pjekur, ka një ide për të unifikuar këtë ndërfaqe në nivelin libbpf.)

NjĂ« lexues i vĂ«mendshĂ«m mund tĂ« vĂ«rejĂ« se ne nuk kemi pĂ«rfunduar akoma me imazhet. NĂ« tĂ« vĂ«rtetĂ«, gjithçka e thĂ«nĂ« mĂ« sipĂ«r nuk shpjegon se si BPF ndryshon nĂ« mĂ«nyrĂ« thelbĂ«sore pamjen pĂ«rballĂ« BPF klasike. Dy novacione, tĂ« cilat zgjasin ndjeshĂ«m kufijtĂ« e aplikueshmĂ«risĂ« — janĂ« mundĂ«sia pĂ«r tĂ« pĂ«rdorur kujtesĂ« tĂ« ndarĂ« dhe funksione ndihmĂ«se tĂ« bĂ«rthamĂ«s (kernel helpers). NĂ« BPF, kujtesa e ndarĂ« realizohet me anĂ« tĂ« ashtuquajturave maps — struktura tĂ« dhĂ«nash tĂ« ndara me njĂ« API tĂ« caktuar. Ky emĂ«r e merr ndoshta sepse tipi i parĂ« i map qĂ« u shfaq ishte njĂ« tabelĂ« hashi. MĂ« pas u shfaqĂ«n array, tabela hashi lokale (per-CPU) dhe array lokale, pemĂ« kĂ«rkimi, mapa qĂ« pĂ«rmbajnĂ« tregues pĂ«r programet BPF dhe shumĂ« tĂ« tjera. ÇfarĂ« na intereson tani Ă«shtĂ« fakti qĂ« programet BPF morĂ«n mundĂ«sinĂ« tĂ« ruajnĂ« gjendjen midis thirrjeve dhe ta ndajnĂ« atĂ« me programe tĂ« tjera dhe me hapĂ«sirĂ«n e pĂ«rdoruesit.

Qasja nĂ« maps bĂ«het nga proceset e pĂ«rdoruesve me anĂ« tĂ« njĂ« thirrjeje sistemike bpf(2), ndĂ«rsa nga programet BPF qĂ« punojnĂ« nĂ« bĂ«rthamĂ« — me anĂ« tĂ« funksioneve ndihmĂ«se. MĂ« shumĂ« se kaq, ndihmĂ«sit ekzistojnĂ« jo vetĂ«m pĂ«r punĂ«n me mapa, por gjithashtu pĂ«r qasjen nĂ« mundĂ«si tĂ« tjera tĂ« bĂ«rthamĂ«s. PĂ«r shembull, programet BPF mund tĂ« pĂ«rdorin funksione ndihmĂ«se pĂ«r tĂ« ridirekruar paketat nĂ« interface tĂ« tjera, pĂ«r tĂ« gjeneruar ngjarje nĂ« nĂ«n-sistemin perf, pĂ«r tĂ« aksesuar struktura tĂ« bĂ«rthamĂ«s dhe e tĂ« tjera.

BPF për fillestarët, pjesa e parë: extended BPF

Përfundimisht, BPF ofron mundësinë të ngarkojë kod të rastësishëm, dmth, kodin që ka kaluar kontrollin në verifier, në hapësirën e bërthamës. Ky kod mund të ruajë gjendjen midis thirrjeve dhe të ndajë të dhëna me hapësirën e përdoruesit, si dhe ka qasje në nën-sistemat e bërthamës të lejuara për këtë tip programesh.

Kjo tashmë ngjan si mundësi që ofrohen nga modulat e bërthamës, përballë të cilave BPF ka disa avantazhe (sigurisht, mund të krahasohet vetëm aplikacione të ngjashme, për shembull, gjurmimi i sistemit - në BPF nuk mund të shkruhet ndonjë drejtues të rastësishëm). Mund të vërejmë një prag më të ulët hyrjeje (disa utilitare që përdorin BPF nuk kërkojnë që përdoruesi të ketë aftësi programimi të bërthamës, dhe madje, as aftësi programimi në përgjithësi), sigurinë e kohës ekzekutimi (ngrihuni dorën në komente ata që nuk e kanë thyer sistemin kur kanë shkruar ose testuar module), atomaritetin - gjatë rinisjes së moduleve ka kohë ndërprerjeje, ndërsa nën-sistemi BPF garanton që asnjë ngjarje nuk do të kalojë (për t'i bërë të drejta, kjo nuk është e vërtetë për të gjitha llojet e programeve BPF).

Prania e tĂ« tilla mundĂ«sive e bĂ«n BPF njĂ« mjet universale pĂ«r zgjerimin e bĂ«rthamĂ«s, gjĂ« qĂ« vĂ«rtetohet nĂ« praktikĂ«: çdo ditĂ« po shtohen lloje tĂ« reja programesh nĂ« BPF, gjithnjĂ« e mĂ« shumĂ« kompanitĂ« e mĂ«dha e pĂ«rdorin BPF nĂ« serverĂ«t e tyre nĂ« mĂ«nyrĂ« 24×7, gjithnjĂ« e mĂ« shumĂ« startup-e po ndĂ«rtojnĂ« biznesin e tyre mbi zgjidhje tĂ« bazuara nĂ« BPF. BPF pĂ«rdoret kudo: nĂ« mbrojtjen nga sulmet DDoS, nĂ« krijimin e SDN (pĂ«r shembull, implementimin e rrjeteve pĂ«r kubernetes), si mjeti kryesor pĂ«r gjurmimin e sistemeve dhe mbledhjen e statistikave, nĂ« sistemet e zbulimit tĂ« depĂ«rtimit dhe nĂ« sistemet e rĂ«ra etj.

Le të përfundojmë këtë pjesë përmbledhëse të artikullit dhe të shikojmë më në detaje mbi makinat virtuale dhe ekosistemin BPF.

Shkëputje: utilitarët

Për të pasur mundësinë të ekzekutoni shembujt nga kapitujt e ardhshëm, mund t'ju nevojitet një sërë utilitarësh, minimalisht llvm/clang me mbështetje për bpf dhe bpftool. Në seksionin Mjetet e zhvillimit mund të lexoni udhëzime për ndërtimin e utilitarëve, si dhe të bërthamës tuaj. Ky seksion është vendosur poshtë, për të mos prishur rregullsinë e përshkrimit tonë.

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

Arkitektura dhe sistemi i komandave BPF u zhvilluan duke pasur parasysh që programet do të ishin shkruar në gjuhën C dhe, pas ngarkimit në bërthatë, do të përktheheshin në kod nativ. Prandaj, numri i regjistrave dhe shumica e komandave u zgjodh duke marrë parasysh përputhjen, në kuptimin matematikor, me mundësitë e makinave moderne. Përveç kësaj, programet kishin disa kufizime, për shembull, deri para pak kohësh nuk kishte mundësi për të shkruar cikle dhe nënprograme, dhe numri i instruksioneve ishte i kufizuar në 4096 (tani programet me privilegje mund të ngarkojnë deri në një milion instruksione).

NĂ« BPF ka njĂ«mbĂ«dhjetĂ« regjistra 64-bitĂ« qĂ« janĂ« tĂ« disponueshĂ«m pĂ«r pĂ«rdoruesit. r0—r10 dhe njĂ« numĂ«rues komandash (program counter). Regjistri r10 mban njĂ« tregues pĂ«r shpejtĂ«sinĂ« (frame pointer) dhe Ă«shtĂ« i disponueshĂ«m vetĂ«m pĂ«r lexim. Programet gjatĂ« ekzekutimit i qasen njĂ« steku prej 512 byte dhe njĂ« numri tĂ« pakufizuar tĂ« memories sĂ« ndarĂ« nĂ« formĂ« maps.

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

PĂ«r pĂ«rkthimin efikas tĂ« programeve, regjistrat r0—r11 pĂ«r tĂ« gjitha arkitekturĂ«n e mbĂ«shtetur hartohen nĂ« mĂ«nyrĂ« tĂ« qartĂ« nĂ« regjistra tĂ« vĂ«rtetĂ« 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, hartohen nĂ« rdi, rsi, rdx, rcx, r8, tĂ« cilat pĂ«rdoren pĂ«r kalimin e parametrave nĂ« funksionet nĂ« x86_64. PĂ«r shembull, kodi nĂ« majĂ« 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 pĂ«rdoret gjithashtu pĂ«r kthimin e rezultatit tĂ« ekzekutimit tĂ« programit, ndĂ«rsa nĂ« regjistrin r1 programi kalon njĂ« tregues pĂ«r kontekstin — nĂ« varĂ«si tĂ« llojit tĂ« 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 rrjetĂ«she) ose struktura struct pt_regs (pĂ«r lloje tĂ« ndryshme programesh tracing) etj.

Pra të gjithë, kishim një grup regjistrash, ndihmës të kernelit, një stack, një tregues konteksti dhe memorie të ndarë në formën e maps. Nuk ishte se e gjitha kjo ishte absolutisht e nevojshme gjatë udhëtimit, por...

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

BPF për fillestarët, pjesa e parë: extended BPF

KĂ«tu Code — kjo Ă«shtĂ« kodifikimi i instruksionit, Dst/Src — kĂ«to janĂ« kodifikimet e marrĂ«sit dhe burimit, pĂ«rkatĂ«sisht, Off — njĂ« zhvendosje e shenjuar 16-bit, dhe Imm — kjo Ă«shtĂ« njĂ« numĂ«r i tĂ«rĂ« 32-bit i shenuar, e pĂ«rdorur nĂ« disa komanda (analog me konstantĂ«n K nga cBPF). Kodifikimi Code ka njĂ« nga dy lloje:

BPF për fillestarët, pjesa e parë: extended BPF

Klasat e instruksioneve 0, 1, 2, 3 përcaktojnë komanda për të punuar me memorien. Ato quhen, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, përkatësisht. Klasat 4, 7 (BPF_ALU, BPF_ALU64) përbëjnë një grup instruksionesh ALU. Klasat 5, 6 (BPF_JMP, BPF_JMP32) përmbajnë instruksione kalimi.

Plani tjetër për të studiuar sistemin e komandeve BPF është ky: në vend që të përmendim me hollësi të gjitha instrukcionet dhe parametrat e tyre, ne do të shqyrtojmë disa shembuj në këtë seksion dhe do të bëhet e qartë se si janë organizuar instruksionet në të vërtetë dhe si të disasemblonim një skedar binar për BPF. Për të forcuar materialin, më tej në artikull, do të takojmë instruksione të veçanta në seksionet për Verifier, kompiluesin JIT, transkriptimin e BPF klasik, si dhe gjatë studimit të maps, thirrjes së funksioneve etj.

Kur tĂ« flasim pĂ«r instruksione tĂ« veçanta, ne do t'i referohemi skedarĂ«ve tĂ« kernelit bpf.h dhe bpf_common.h, nĂ« tĂ« cilat pĂ«rcaktohen kodet numerike tĂ« instruksioneve BPF. GjatĂ« studimeve tĂ« pavarura tĂ« arkitekturĂ«s dhe/ose analizĂ«s sĂ« binarĂ«ve, semantikĂ«n e mund tĂ« gjeni nĂ« burimet e mĂ«poshtme, tĂ« renditura sipas vĂ«shtirĂ«sisĂ«: Specifikimi i paautorizuar eBPF, UdhĂ«zuesi Referencial i BPF dhe XDP, Seti i Instruksioneve, Dokumentacioni/networking/filter.txt dhe, natyrisht, nĂ« kodin burimor tĂ« Linux — verifier, JIT, interpreter BPF.

Shembulli: disasemblojmë BPF në mendje

Le të shqyrtojmë një shembull, ku do të kompilirojmë programin readelf-example.c dhe të shohim binarin e fituar. Do të zbulojmë përmbajtjen origjinale readelf-example.c më poshtë, pasi të rikthejmë logjikën e tij nga kodet binare:

$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Hex dump of section '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................

Kolona e parĂ« nĂ« dalje readelf — Ă«shtĂ« hapsira dhe programi ynĂ«, kĂ«shtu qĂ« pĂ«rbĂ«het nga katĂ«r komanda:

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. Kujtojmë se tri bitët më të ulët janë klasa e instrukcionit. Në rastin tonë, bita e katërt është bosh për të gjitha instrukcionet, kështu që klasat e instrukcioneve janë të barabarta, përkatësisht, 7, 5, 7, 5. Klasa 7 është BPF_ALU64, ndërsa 5 është BPF_JMP. Për të dyja klasat formati i instrukcionit është i njëjtë (shih më lart) dhe ne mund ta shkruajmë programin tonë kështu (po ashtu, do të shkruajmë edhe kolonat e tjera në një format më të qartë):

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

OperaciĂłn b klasĂ«s ALU64 — Ă«shtĂ« BPF_MOV. Ajo i cakton njĂ« vlerĂ« regjistrimit-rresht tĂ« pranuesit. NĂ«se Ă«shtĂ« vendosur bita s (source), 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Ă« instrukcionet e para dhe tĂ« treta ne kryejmĂ« operacionin r0 = Imm. MĂ« pas, operacioni 1 i klasĂ«s JMP Ă«shtĂ« BPF_JEQ (kalim nĂ«se janĂ« tĂ« barabarta). NĂ« rastin tonĂ«, pasi bita S Ă«shtĂ« zero, ajo krahason vlerĂ«n e regjistrit-burim me fushĂ«n Imm. NĂ«se vlerat pĂ«rputhen, atĂ«herĂ« kalimi ndodh nĂ« PC + Offindex PC, si zakonisht, pĂ«rmban adresĂ«n e instrukcionit tjetĂ«r. SĂ« fundmi, operacioni 9 i klasĂ«s JMP Ă«shtĂ« BPF_EXIT. Ky instrukcion pĂ«rfundon ekzekutimin e programit, duke kthyer nĂ« bĂ«rthamĂ« r0. Do 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) kaloni në pc+1
MOV   0  ALU64   0   0   0    2      r0 = 2
EXIT  0  JMP     0   0   0    0      dil

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

     r0 = 1
     nëse (r1 == 0) kaloni në FUND
     r0 = 2
FUND:
     dil

Nëse ne kujtojmë se në regjistrin r1 programi kalon një tregues në kontekst nga bërthama, dhe në regjistrin r0 kthehet një vlerë në bërthamë, atëherë ne mund të shohim se nëse treguesi në kontekst është zero, ne kthejmë 1, ndryshe 2. Le të kontrollojmë 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 përkthehet vetëm në katër instrukcione të thjeshta.

Shembulli përjashtim: instrukcioni 16-bajtësh

MĂ« parĂ« pĂ«rmendĂ«m se disa instrukcione zĂ«nĂ« mĂ« shumĂ« se 64 bita. Kjo vlen pĂ«r shembuj pĂ«r instrukcionin lddw (Code = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — ngarko nĂ« regjistĂ«r njĂ« fjalĂ« tĂ« dyfishtĂ« nga fushat Imm. E vĂ«rteta Ă«shtĂ« se Imm ka ka njĂ« madhĂ«si prej 32, ndĂ«rsa fjala e dyfishtĂ« — 64 bita, kĂ«shtu qĂ« ngarkimi i njĂ« vlerĂ« tĂ« drejtpĂ«rdrejtĂ« 64-bit nĂ« njĂ« regjistĂ«r me njĂ« udhĂ«zim 64-bitsh nuk Ă«shtĂ« i mundur. PĂ«r kĂ«tĂ«, dy udhĂ«zime ngjitur pĂ«rdoren pĂ«r tĂ« ruajtur pjesĂ«n e dytĂ« tĂ« vlerĂ«s 64-bit nĂ« fushĂ« 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ë takojmë përsëri me udhëzimin lddw, kur të flasim për rilokimet dhe punën me maps.

Shembull: disasemblojmë BPF me mjete standarde

Pra, ne kemi mësuar të lexojmë kodet binare BPF dhe jemi gati të analizojmë çdo udhëzim nëse është e nevojshme. Megjithatë, duhet thënë se në praktikë është më e lehtë dhe më e shpejtë të disasemblojmë programet duke përdorur mjete 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 që përshkruhen në këtë nënkapitull, unë i mësova për herë të parë nga të postimit Alexei Starovoitov në BPF Blog.)

Objektet BPF — programet dhe mapat — krijohen nga hapĂ«sira e pĂ«rdoruesit pĂ«rmes komandave BPF_PROG_LOAD dhe BPF_MAP_CREATE thirrjes sĂ« sistemit bpf(2), ne do tĂ« flasim se si ndodh kjo nĂ« kapitullin e ardhshĂ«m. KĂ«tu krijohen struktura tĂ« dhĂ«nash tĂ« bĂ«rthamĂ«s dhe pĂ«r secilĂ«n prej tyre refcount (numri iReferencave) vendoset tĂ« jetĂ« njĂ«, dhe pĂ«rdoruesit i kthehet njĂ« deshifrues skedari qĂ« tregon nĂ« objekt. Pasi tĂ« mbyllet deshifruesi refcount numri iReferencave tĂ«

kësaj mape rritet me një pas ngarkimit të programit, dmth. deshifruesit e skedarëve mund të mbyllen nga procesi i përdoruesit dhe për këtë arsye refcount nuk bëhet zero: refcount Pasi të ngarkohet me sukses programi ne zakonisht e lidhim atë me ndonjë gjenerator ngjarjesh. Për shembull, ne mund ta vendosim atë në ndërfaqen rrjetë për përpunimin e paketimeve që vijnë ose ta lidhim atë me ndonjë

BPF për fillestarët, pjesa e parë: extended BPF

tracepoint në bërthamë. Në këtë moment, numri iReferencave gjithashtu do të rritet me një dhe ne do të mund të mbyllim deshifruesin e skedarit në programin ngarkues. në thellësi. Në këtë moment, numri i lidhjeve gjithashtu do të rritet me një dhe mund të mbyllim descriptorin e skedarit në programin ngarkues.

ÇfarĂ« do tĂ« ndodhĂ« nĂ«se ne tani pĂ«rfundojmĂ« punĂ«n e ngarkuesit? Kjo varet nga lloji i gjeneratorit tĂ« ngjarjeve (hook). TĂ« gjitha hook-et rrjetĂ« do tĂ« ekzistojnĂ« pas pĂ«rfundimit tĂ« ngarkuesit, kĂ«to quhen hook-et globale. NdĂ«rsa, pĂ«r shembull, programet e gjurmimeve 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Ă« deshifrim tĂ« dosjes pĂ«rkatĂ«se nĂ« hapĂ«sirĂ«n e pĂ«rdoruesit dhe pĂ«r kĂ«tĂ« arsye mbyllen me mbylljen e procesit, ndĂ«rsa ato globale jo. NĂ« figurĂ«n e ardhshme, pĂ«rmes crosses tĂ« kuq, do tĂ« pĂ«rpiqem tĂ« tregoj si pĂ«rfundimi i programit ngarkues ndikon nĂ« kohĂ«zgjatjen e objekteve nĂ« rastin e hook-eve lokale dhe globale.

BPF për fillestarët, pjesa e parë: extended BPF

Pse egziston ndarja nĂ« hook-e lokale dhe globale? Shkarkimi i disa llojeve tĂ« programit rrjetĂ« ka kuptim dhe pa hapĂ«sirĂ« pĂ«r pĂ«rdoruesit, pĂ«r shembull, imagjinoni mbrojtjen nga DDoS — ngarkuesi shkruan rregullat dhe lidh njĂ« program BPF me ndĂ«rfaqen rrjetĂ«, pas sĂ« cilĂ«s ngarkuesi mund tĂ« shkojĂ« dhe tĂ« vetĂ«vritet. Nga ana tjetĂ«r, mendoni pĂ«r njĂ« program gjurmimi qĂ« keni shkruar shpejt pĂ«r dhjetĂ« minuta — pas pĂ«rfundimit tĂ« tij, do tĂ« dĂ«shironit qĂ« nĂ« sistem tĂ« mos kishte mbetje dhe hook-et lokale e garantohen kĂ«tĂ«.

Nga ana tjetër, imagjinoni që dëshironi të lidheni me një pikë gjurmimi në bërthamë dhe të mbledhni statistikat për shumë vite. Në këtë rast, do të dëshironit të përfundoni pjesën e përdoruesit dhe të ktheheni herë pas here te statistikat. Këtë mundësi ofron sistemi i dosjeve bpf. Ky është një sistem dosjesh psevdosh, që ekziston vetëm në memorie, i cili lejon krijimin e dosjeve që referojnë objekte BPF dhe, kështu, rritin refcount objektet. Pasi që ngarkuesi mund të përfundojë punën, objektet që krijoi do të mbeten të gjalla.

BPF për fillestarët, pjesa e parë: extended BPF

Krijimi i dosjeve nĂ« bpffs, qĂ« referojnĂ« objekte BPF quhet „ngjitje” („pin”, si nĂ« frazĂ«n tjetĂ«r: „process can pin a BPF program or map”). Krijimi i objekteve dosje 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 u kthyer te shembulli me programin global pĂ«r mbrojtjen nga DDoS, ne duam tĂ« kemi mundĂ«si herĂ« pas here tĂ« vijmĂ« dhe tĂ« shikojmĂ« statistikat.

Sistemi datotekave BPF zakonisht montohet në /sys/fs/bpf, por gjithashtu mund të montohet lokalisht, për shembull, kështu:

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

Emrat në sistemin e skedave krijohen duke përdorur komandën BPF_OBJ_PIN të thirrjes së sistemit BPF. Si ilustim, le të marrim një program, ta kompilojmë, ta ngarkojmë dhe ta lidhim në bpffs. Programi ynë nuk bën asgjë të dobishme, ne vetëm po sjellim kodin e tij për t'ju lejuar të riprodhoni shembullin:

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

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

Tani do ta kompilojmë këtë program dhe të krijojmë një kopje lokale të sistemit të skedave bpffs:

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

Tani le të ngarkojmë programin tonë duke përdorur utilitarin bpftool dhe të shohim thirrjet e sistemeve përkatëse bpf(2) (nga dalja e strace janë hequr disa rreshta të panhulumtuar):

$ 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 duke përdorur BPF_PROG_LOAD, morëm një descriptor të skedave nga bërthama 3 dhe duke përdorur komandën BPF_OBJ_PIN e lidhi këtë descriptor skedash si një skedë "bpf-mountpoint\/test". Pas kësaj, programi ngarkues bpftool përfundoi punën, por programi ynë mbeti në bërthamë, ndonëse nuk e lidhëm atë me asnjë 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

Ne mund ta fshijmë objektin e skedave me anë të 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, është e nevojshme të saktësohet se pasi që e kemi shkëputur programin nga hook (generaatorin e ngjarjeve), asnjë ngjarje e re nuk do të nxisë fillimin e saj, megjithatë, të gjitha ekzistencat aktuale të programit do të përfundojnë në mënyrë të rregullt.

Disa lloje programesh BPF lejojnë ndërrimin e programit në flakë, dmth. ofrojnë atomikën e sekuencës replace = detach old program, attach new program. Në këtë mënyrë, të gjitha ekzistencat aktive të versions së vjetër të programit do të përfundojnë punën e tyre, ndërsa përpunuesit e rinj të ngjarjeve do të krijohen nga programi i ri, dhe "atomikën" në këtë kontekst do të thotë se asnjë ngjarje nuk do të humbasë.

Kjo programesh për lidhjen me burimet e ngjarjeve

Në këtë artikull nuk do të përshkruajmë veçmas lidhjen e programeve me burimet e ngjarjeve, pasi ka kuptim ta studiojmë në kontekstin e një lloji specifik programi. Shihni është më poshtë, ku tregojmë si lidhen programet e tipit XDP.

Menaxhimi i objekteve përmes thirrjes sistemore bpf

Programet BPF

Të gjitha objektet BPF krijohen dhe menaxhohen nga hapësira e përdoruesit përmes thirrjes sistemore 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 tipit enum bpf_cmd, attr – tregues ndaj parametrave pĂ«r programin specifik dhe size – madhĂ«sia e objektit pĂ«rmes treguesit, dmth zakonisht Ă«shtĂ« sizeof(*attr). NĂ« kernelin 5.8, thirrja sistemore bpf mbĂ«shtet 34 komanda tĂ« ndryshme, dhe pĂ«rcaktimi union bpf_attr merr 200 rreshta. Por kjo nuk duhet tĂ« na friksojĂ«, pasi do tĂ« njihemi me komandat dhe parametrat gjatĂ« disa artikujve.

TĂ« fillojmĂ« me komandĂ«n BPF_PROG_LOAD, e cila krijon programet BPF — merr njĂ« grup instruksionesh BPF dhe e ngarkon ato nĂ« kernel. NĂ« momentin e ngarkimit, aktivizohet verifikuesi, pastaj kompjileri JIT dhe, pas njĂ« pĂ«rfundimi tĂ« suksesshĂ«m, pĂ«rdoruesit i kthehet descriptori i skedarit tĂ« programit. E pamĂ« se çfarĂ« ndodhi mĂ« tej nĂ« seksionin e mĂ«parshĂ«m rreth ciklit tĂ« jetĂ«s sĂ« objekteve BPF..

Tani do tĂ« shkruajmĂ« njĂ« program pĂ«rdoruesi qĂ« do tĂ« ngarkohet njĂ« program tĂ« thjeshtĂ« BPF, por sĂ« pari na nevojitet tĂ« vendosim se çfarĂ« programi dĂ«shirojmĂ« tĂ« ngarkojmĂ« — do tĂ« duhet tĂ« zgjedhim tipin dhe brenda kĂ«tij tipi tĂ« shkruajmĂ« njĂ« program qĂ« do tĂ« kalojĂ« verifikimin. SidoqoftĂ«, pĂ«r tĂ« mos e komplikuar procesin, kĂ«tu Ă«shtĂ« njĂ« zgjidhje e gatshme: do tĂ« marrim programin e tipit BPF_PROG_TYPE_XDP, i cili do tĂ« kthejĂ« vlerĂ«n XDP_PASS (kaloni tĂ« gjitha paketat). NĂ« assemblerin BPF kjo duket shumĂ« e thjeshtĂ«:

r0 = 2
exit

Pas përcaktimit të asaj që nuk shkon, dhe gjithashtu ka një ide për atë që ndodh në shërbimet përreth, për të kuptuar, ne do të ngarkojmë, mund të tregojmë se si do ta bëjmë:

#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 njĂ« vargu insns – programi ynĂ« BPF nĂ« kodin makinerik. NĂ« kĂ«tĂ« rast, çdo instruksion i programit BPF paketizohet nĂ« njĂ« strukturĂ« bpf_insn.Elementi i parĂ« insns pĂ«rkon me instruksionin r0 = 2, tjetri — exit.

Përjashtim. Në bërthama janë përcaktuar më shumë makro të përshtatshme për të shkruar kode makine, dhe, duke përdorur dosjen kryesore të bërthamës tools/include/linux/filter.h ne do të mund të shkruajmë

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

Por shkakun se shkruajtja e programeve BPF në kodet makinë nevojitet vetëm për të shkruar teste në bërthamë dhe artikuj për BPF, mungesa e këtyre makros nuk e komplikon jetën e zhvilluesit në të vërtetë.

Pas përcaktimit të programit BPF, ne kalojmë në ngarkimin e tij në bërthamë. Grupi ynë minimalistik i parametrave attr përfshin llojin e programit, grupin dhe numrin e instruksioneve, licencën e detyrueshme, si dhe emrin "woo", i cili përdoret për të gjetur programin tonë në sistem pas ngarkimit. Programi, siç është premtuar, ngarkohet në sistem përmes 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ë zhduket nga bërthama kur mbyllet deshifruesi i skedarit që na ktheu thirrja e sistemit bpf, dhe ne nuk do ta shohim atë në sistem.

Mirë, jemi gati për testim. Do ta ndërrymë dhe ekzekutojmë programin në strace, për të kontrolluar nëse 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_VERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

Gjithçka është në rregull, bpf(2) na ktheu deshifruesin 3 dhe ne kaluam në një cikël të pafund me pause(). Le të përpiqemi të gjejmë programin tonë në sistem. Për këtë do të shkojmë në një terminal tjetër dhe do të përdorim utilitarin 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 ekziston një program i ngarkuar woo i cili ka një ID global prej 390, dhe që në procesin simple-prog ka një deshifrues të hapur të skedarit që tregon për programin (dhe nëse simple-prog përfundon, atëherë woo do të zhduket). Siç pritej, programi woo zë 16 byte - dy instruksione - kodet binare në arkitekturën BPF, por në formatin natyror (x86_64) - është tashmë 40 byte. Le të shohim programin tonë në formatin origjinal:

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

pa surpriza. Tani le të shohim kodin e krijuar nga JIT kompaktori:

# 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ë efikas për exit(2), por për të drejtën, programi ynë është shumë i thjeshtë, dhe për programet jo triviale, prologu dhe epilogu të shtuar nga JIT kompaktori, sigurisht, janë të nevojshme.

Maps

Programet BPF mund të përdorin zona strukturore të memories, të aksesueshme si nga programet e tjera BPF, ashtu edhe nga programet në hapësirën e përdoruesit. Këto objekte quhen maps dhe në këtë pjesë ne do të tregojmë si t'i menaxhojmë ato me anë të thirrjeve sistemike. bpf.

Të themi qartë se mundësitë e maps nuk kufizohen vetëm në aksesin në memorjen e përbashkët. Ekzistojnë mapa me destinacion të veçantë, që përmbajnë, për shembull, tregues për programet BPF ose tregues për ndërfaqet rrjet, mape për të punuar me ngjarje perf dhe kështu me radhë. Këtu ne nuk do të flasim për to, për të mos ngatërruar lexuesin. Përveç kësaj, ne e injorojmë problemin e sinkronizimit, pasi kjo nuk është e rëndësishme për shembujt tanë. Një listë e plotë e tipeve të mapeve që janë në dispozicion mund të gjendet në <linux/bpf.h>, dhe në këtë pjesë do të marrim si shembull tipin historikisht të parë, tabelën me hash. BPF_MAP_TYPE_HASH.

Nëse po krijoni një tabelë me hash, le të themi në C++, do të thoni unordered_map woo, që në shqip do të thotë "më nevojitet një tabelë woo të pakufizuar, ku çelësat kanë tipin int, ndërsa vlerat - tipin long". Për të krijuar një tabelë BPF me hash duhet të bëjmë më shumë ose pak të njëjtën gjë, me ndryshimin se na nevojitet të tregojmë madhësinë maksimale të tabelës dhe në vend të tipeve të çelësave dhe vlerave, na duhen madhësitë e tyre në byte. Për krijimin e mapeve përdoret komanda BPF_MAP_CREATE thirrjes së sistemit bpf. Le të shohim një program mëse minimal, i cili 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Ă« set parametrash attr, ku thĂ«rrasim "mĂ« nevojitet njĂ« tabelĂ« me hash me çelĂ«sa dhe vlera tĂ« madhĂ«sisĂ« sizeof(int), nĂ« tĂ« cilĂ«n mund tĂ« vendos maksimum katĂ«r elemente”. Kur krijoni mape BPF mund tĂ« pĂ«rcaktoni edhe parametra tĂ« tjerĂ«, pĂ«r shembull, ashtu siç nĂ« shembullin me programin, kemi pĂ«rcaktuar emrin e objektit si "woo".

Le të kompilojmë dhe 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 është thirrja sistematike bpf(2) na ktheu një deskriptori të hartës numër 3 dhe më pas programi, siç pritej, pret udhëzime të mëtejshme në thirrjen sistematike pause(2).

Tani do të dërgojmë programin tonë në background ose do të hapim një terminal tjetër dhe do të shohim objektin tonë me ndihmën e utilitarit bpftool (mund të dallojmë hartën tonë nga të tjera sipas emrit të 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 tĂ« pĂ«rdorĂ« kĂ«tĂ« ID pĂ«r tĂ« hapur njĂ« hartĂ« ekzistuese me komandĂ«n BPF_MAP_GET_FD_BY_ID thirrjes sĂ« sistemit bpf.

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

$ sudo bpftool map dump id 114
Gjetur 0 elementë

E pustë. 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

Le të shohim tabelën edhe një herë:

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

Hurrah! Mundëm të shtonim një element. Vini re se për këtë na duhej të punonim në nivelin e byte-ve, pasi bptftool nuk e di se çfarë lloj kanë vlerat në tabelën e hash. (Kjo njohuri mund t'i jepet, duke përdorur BTF, por jo tani.)

Si i lexon dhe shton elementet bpftool? Le të hedhim një sy poshtë kapakut:

$ 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 një deskriptor 3. Më pas, me komandën BPF_MAP_GET_NEXT_KEY gjetëm çelësin e parë në tabelë, duke kaluar NULL si një tregues ndaj çelësit "të mëparshëm". Nëse kemi një çelës mund të bëjmë BPF_MAP_LOOKUP_ELEM, i cili kthen vlerën në treguesin vlera. Hapi tjetër është që përpiqemi të gjejmë elementin tjetër, duke kaluar treguesin ndaj çelësit aktual, por tabela jonë përmban vetëm një element dhe komanda BPF_MAP_GET_NEXT_KEY false, nëse elementi që do të zëvendësohet është ENOENT.

Mirë, le të ndryshojmë vlerën për çelësin 1, le të themi se 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ç pritej, është shumë e lehtë: komandë BPF_MAP_GET_FD_BY_ID hap hartën tonë sipas ID-së, dhe komandë BPF_MAP_UPDATE_ELEM shkruan përsëri elementin.

Në përfundim, pas krijimit të një hesh-tabele 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ënim këtë nga rreshti i komandave, atëherë çdo program tjetër në sistem mund ta bëjë po ashtu. Përveç komandave të përmendura më sipër, për të punuar me harta nga hapësira e përdoruesit janë të disponueshme këto:

  • BPF_MAP_LOOKUP_ELEM: tĂ« gjejmĂ« vlerĂ«n sipas çelĂ«sit
  • BPF_MAP_UPDATE_ELEM: tĂ« pĂ«rditĂ«sojmĂ«/krijojmĂ« vlerĂ«n
  • BPF_MAP_DELETE_ELEM: tĂ« fshijmĂ« çelĂ«sin
  • BPF_MAP_GET_NEXT_KEY: tĂ« gjejmĂ« çelĂ«sin e ardhshĂ«m (ose tĂ« parin)
  • BPF_MAP_GET_NEXT_ID: lejon kalimin nĂ«pĂ«r tĂ« gjitha harta ekzistuese, kĂ«shtu funksionon bpftool map
  • BPF_MAP_GET_FD_BY_ID: tĂ« hapim njĂ« hartĂ« ekzistuese sipas ID-sĂ« sĂ« saj globale
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: tĂ« pĂ«rditĂ«sojmĂ« atomikisht vlerĂ«n e objektit dhe tĂ« kthejmĂ« tĂ« vjetrĂ«n
  • BPF_MAP_FREEZE: tĂ« bĂ«jmĂ« hartĂ«n 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 — ky Ă«shtĂ« mĂ«nyra e vetme e besueshme pĂ«r tĂ« lexuar dhe resetuar tĂ« gjitha vlerat nga harta

Jo të gjitha këto komanda funksionojnë për të gjitha tipet e hartave, por në përgjithësi puna me lloje të tjera të hartave nga hapësira e përdoruesit duket kështu si puna me hesh-tabelat.

Për renditje, le të përfundojmë eksperimentet tona me hesh-tabelën. Mos harroni se krijuam një tabelë, në të cilën mund të ketë 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 çdo gjë po 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
Gjetur 4 elemente

Le të përpiqemi të dytojmë edhe 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ç pritej, ne nuk arritëm. Le të shohim më afër gabimin:

$ 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: siç pritej, komanda BPF_MAP_UPDATE_ELEM përpiquni të krijoni një çelës të ri, të pestë, por dështon me E2BIG.

Pra ndaj, ne dimĂ« si tĂ« krijojmĂ« dhe ngarkojmĂ« programe BPF, si dhe si tĂ« krijojmĂ« dhe menaxhojmĂ« hartat nga hapĂ«sira e pĂ«rdoruesit. Tani Ă«shtĂ« logjikisht koha tĂ« shohim si mund tĂ« pĂ«rdorim hartat nga vetĂ« programet BPF. Mund tĂ« flasim pĂ«r kĂ«tĂ« nĂ« njĂ« gjuhĂ« tĂ« vĂ«shtirĂ« pĂ«r t'u lexuar, nĂ« makro-kodet e makinave, por nĂ« tĂ« vĂ«rtetĂ« erdhi koha tĂ« tregojmĂ« se si shkruhen dhe mbahen programet BPF nĂ« tĂ« vĂ«rtetĂ« — duke pĂ«rdorur libbpf.

(Për lexuesit që janë të pakënaqur me mungesën e një shembulli të nivelit të ulët: ne do të shqyrtojmë detajisht programet që përdorin hartat dhe funksionet ndihmëse të krijuara duke përdorur libbpf dhe do të tregojmë se çfarë ndodh në nivelin e instrukcioneve. Për lexuesit që janë të pakënaqur me shumë fort, ne kemi shtuar është në vendin e duhur të artikullit.)

Shkruajmë programe BPF me libbpf.

Të shkruash programe BPF me kod makina mund të jetë interesante vetëm për një kohë të shkurtër, dhe pastaj vjen një pikë e ngopjes. Në këtë moment duhet të drejtojmë vëmendjen tonë në llvm, e cila ka një backend për gjenerimin e kodit për arkitekturën BPF, si dhe në bibliotekën libbpf, e cila lejon të shkruani pjesën e përdoruesit të aplikacioneve BPF dhe ngarkoni kodin e programeve BPF të gjeneruara duke përdorur llvm/clang.

NĂ« tĂ« vĂ«rtetĂ«, si do tĂ« shohim nĂ« kĂ«tĂ« dhe artikujt e ardhshĂ«m, libbpf bĂ«n njĂ« punĂ« tĂ« madhe dhe pa tĂ« (ose mjete tĂ« ngjashme — iproute2, libbcc, libbpf-go, etj.) nuk Ă«shtĂ« e mundur tĂ« jetosh. NjĂ« nga funksionet killer tĂ« projektit libbpf Ă«shtĂ« BPF CO-RE (Compile Once, Run Everywhere) — njĂ« projekt qĂ« lejon tĂ« shkruhen programe BPF qĂ« janĂ« tĂ« transferueshme nga njĂ« bĂ«rthamĂ« nĂ« tjetrĂ«n, me mundĂ«sinĂ« e ekzekutimit nĂ« API tĂ« ndryshme (p.sh. kur struktura e bĂ«rthamĂ«s ndryshon nga versioni nĂ« version). PĂ«r tĂ« qenĂ« nĂ« gjendje tĂ« punoni me CO-RE, bĂ«rthama juaj duhet tĂ« jetĂ« kompiliuar me mbĂ«shtetje pĂ«r BTF (si ta bĂ«ni kĂ«tĂ« e shpjegojmĂ« nĂ« seksionin Mjetet e zhvillimit. TĂ« kontrolloni nĂ«se bĂ«rthama juaj Ă«shtĂ« kompiliuar me BTF ose jo, Ă«shtĂ« shumĂ« e lehtĂ« — Ă«shtĂ« shumica e pranishme 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 ruan informacionin mbi tĂ« gjitha llojet e tĂ« dhĂ«nave qĂ« pĂ«rdoren nĂ« bĂ«rthamĂ« dhe pĂ«rdoret nĂ« tĂ« gjithĂ« shembujt tanĂ« qĂ« pĂ«rdorin libbpf. Ne do tĂ« flasim nĂ« detaje pĂ«r CO-RE nĂ« artikullin e ardhshĂ«m, ndĂ«rsa nĂ« kĂ«tĂ« — thjesht ndihmoni veten tĂ« ndĂ«rtoni njĂ« bĂ«rthamĂ« me CONFIG_DEBUG_INFO_BTF.

Biblioteka libbpf jeton direkt në drejtorinë tools/lib/bpf të bërthamës dhe zhvillimi i saj bëhet përmes listës së postimeve bpf@vger.kernel.org. Megjithatë, për nevojat e aplikacioneve që jetojnë jashtë bërthamës, mbështetet një depo e veçantë https://github.com/libbpf/libbpf ku biblioteka e bërthamës pasqyrohet për qasje leximi më shumë-më pak ashtu siç është.

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

Zakoni projekti që përdorin libbpf shtojnë një depo nga GitHub si një git submodule, le të bëjmë edhe ne këtë:

$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Inicializuar depo të zbrazët Git në /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Duke klonuar në '/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
Duke marrë objekte: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, done.
Zgjidhja e deltas: 100% (2176/2176), done.

Kompilohet libbpf shumë lehtë:

$ 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ë i mëtejshëm në këtë pjesë është si vjen: ne do të shkruajmë një program BPF të tipit BPF_PROG_TYPE_XDP, të njëjtin që kishim në shembullin e mëparshëm, por në C, do ta kompilojmë me clang, dhe do të shkruajmë një program ndihmës që do ta ngarkojë në bërthamë. Në pjesët e ardhshme ne do të zgjedhim mundësitë e si programit BPF, ashtu edhe programit ndihmës.

Shembulli: të krijojmë një aplikacion të plotë me libbpf

Fillimisht ne do të përdorim skedarin /sys/kernel/btf/vmlinux, për të cilin u përmend më parë, dhe do të krijojmë ekuivalentin e tij si një skedar titulli:

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

Ky skedar do të ruajë të gjitha strukturat e të dhënave që janë në bërthamën tonë, për shembull, kështu përcaktohet në bërthamë titulli IPv4:

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

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Ă« Ă«shtĂ« shumĂ« i thjeshtĂ«, gjithsesi duhet tĂ« kushtojmĂ« vĂ«mendje shumĂ« detajeve. SĂ« pari, skedari i parĂ« i titullit qĂ« pĂ«rfshijmĂ« Ă«shtĂ« vmlinux.h, i cili sapo e kemi gjeneruar me anĂ« tĂ« bpftool btf dump — tani nuk kemi nevojĂ« tĂ« instalojmĂ« paketĂ«n kernel-headers pĂ«r tĂ« ditur si duken strukturat e kernelit. Skedari tjetĂ«r i titullit vjen nga biblioteka libbpf. Aktualisht na nevojitet vetĂ«m pĂ«r t'u definuar makrosi SEC, i cili dĂ«rgon simbolin nĂ« seksionin pĂ«rkatĂ«s tĂ« skedarit objekt ELF. Programi ynĂ« ndodhet nĂ« seksionin xdp/simple, ku para shkronjĂ«s ndiqet lloji i programit BPF—kjo Ă«shtĂ« njĂ« marrĂ«veshje qĂ« pĂ«rdoret nĂ« libbpf, sipas emrit tĂ« seksionit do tĂ« shĂ«nohet lloji i duhur gjatĂ« ekzekutimit bpf(2). Programi BPF nĂ« C Ă«shtĂ« 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.

Mund tĂ« kompilijmĂ« programin tonĂ« me anĂ« tĂ« llvm/clang, versioni >= 10.0.0, mĂ« mirĂ« akoma—mĂ« tĂ« lartĂ« (shiko kapitullin 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 veçoritë interesante: ne specifikojmë arkitekturën e synuar -target bpf dhe rrugën drejt titujve libbpf, të cilat sapo i kemi instaluar. Gjithashtu, mos harroni për -O2, pa këtë opsion ju mund të përballeni me surpriza më vonë. Le të shikojmë kodin tonë, a e kemi shkruar programin që doja?

$ 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, ia arritĂ«m! Tani, kemi njĂ« skedar binar me programin, dhe duam tĂ« krijojmĂ« njĂ« aplikacion qĂ« do ta ngarkojĂ« 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 dĂ«shirojmĂ« tĂ« mĂ«sojmĂ« si tĂ« shkruajmĂ«, ngarkojmĂ« dhe lidhemi me programet BPF me pĂ«rpjekje minimale pĂ«r studim tĂ« mĂ«tejshĂ«m.

SĂ« pari, na nevojitet tĂ« gjenerojmĂ« "skeletin" e programit tonĂ« nga binari i tij me anĂ« tĂ« po atij mjeti bpftool — mjera mĂ« e mirĂ« e shumĂ«llojshmĂ«risĂ« BPF (gjĂ« qĂ« mund tĂ« kuptohet edhe nĂ« kuptimin literal, pasi Daniel Borkman Ă«shtĂ« njĂ« nga krijuesit dhe mbajtĂ«sit e BPF — shqiptar):

$ 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, heqjen e objektit tonĂ«. NĂ« rastin tonĂ« tĂ« thjeshtĂ«, kjo duket si mbi-mundim, por funksionon dhe kur skedari objekt pĂ«rmban shumĂ« programe BPF dhe mapa, dhe pĂ«r ngarkimin e kĂ«tij ELF gjigand, na mjafton tĂ« gjenerojmĂ« njĂ« skelet dhe tĂ« thĂ«rrasim njĂ« ose dy funksione nga aplikacioni pĂ«rdorues, pĂ«r tĂ« cilin do tĂ« kalojmĂ« tani.

Në të vërtetë, programi ynë ngarkues është i thjeshtë:

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

Këtu mund të vëmë re shenjat e API-së së ulët: struktura struct bpf_program *simple dhe struct bpf_link *simple. Struktura e parë përshkruan saktësisht programin tonë, i shkruar në seksionin xdp/simple, ndërsa e dyta përshkruan se si programi lidhet me burimin e ngjarjeve.

Funksioni xdp_simple_bpf__open_and_load, hap skedarin ELF, e parse, krijon tĂ« gjitha strukturat dhe nĂ«n-strukturat (pĂ«rveç programit, nĂ« ELF ndodhen edhe seksione tĂ« tjera — data, data e lexueshme, informacion pĂ«r paraqitje, licencĂ« etj.), dhe mĂ« pas ngarkohet nĂ« bĂ«rthamĂ« pĂ«rmes thirrjes sĂ« sistemit bpf, tĂ« cilin mund ta verifikojmĂ« duke e kompaktuar 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ë shikojmë programin tonë përmes bpftool. 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 ta dumpojmë (ne përdorim formën e shkurtuar 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 copa të skedarit tonë burimor në gjuhën C. Kjo u realizua nga biblioteka libbpf, e cila gjeti seksionin e ndihmës në binar, e kompiloi atë në objekt BTF, e ngarkoi në bërthamë përmes BPF_BTF_LOAD, dhe pastaj tregoi deshifruesin e skedarit të fituar gjatë ngarkimit të programit me komandën BPG_PROG_LOAD.

Ndihmësit e Bërthamës

Programet BPF mund tĂ« ekzekutojnĂ« funksione 'tĂ« jashtme' — ndihmĂ«s tĂ« kernelit. KĂ«to funksione ndihmĂ«s lejojnĂ« programet BPF tĂ« aksesojnĂ« struktura tĂ« kernelit, menaxhojnĂ« maps, si dhe tĂ« komunikojnĂ« me 'botĂ«n reale' — tĂ« krijojnĂ« ngjarje perf, tĂ« menaxhojnĂ« pajisjet (p.sh., tĂ« shpĂ«rndajnĂ« paketa) etj.

Shembuj: bpf_get_smp_processor_id

Në kuadër të paradigms 'mësojmë nga shembujt', le të shqyrtojmë një nga funksionet ndihmëse, bpf_get_smp_processor_id(), e cila është në skedarin kernel/bpf/helpers.c. Ajo kthen numrin e procesorit në të cilin ekzekutohet programi BPF që e thirri atë. Por ajo që na intereson më shumë është se implementimi i saj zë një rresht:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Kufizimet e funksioneve ndihmëse BPF janë të ngjashme me ata të thirrjeve sistemike në Linux. Këtu, për shembull, përcaktohet një funksion që nuk ka 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 tipit struct bpf_func_proto, e cila përmban përshkrimin e funksionit ndihmës, të kuptueshëm për verifierin:

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

Për që programet BPF të një lloji të caktuar të mund të përdorin këtë funksion, ato duhet ta regjistrojnë atë, për shembull, për llojin BPF_PROG_TYPE_XDP në kernel përcaktohet funksioni xdp_func_proto, i cili përmes ID-së së funksionit ndihmës përcakton nëse XDP 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;
    ...
    }
}

Lloje të reja programesh BPF 'përcaktohen' në skedarin include/linux/bpf_types.h nëpërmjet makros BPF_PROG_TYPE. Përcaktimi është marrë në cita, pasi kjo është një përcaktim logjik, dhe në terma të gjuhës C, përcaktimi i një grupi të tërë strukturash specifike ndodh në vende të tjera. Konkretisht, në skedarin kernel/bpf/verifier.c të gjitha përcaktimet nga skedari bpf_types.h përdoren për të krijuar një array 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 
#undef BPF_PROG_TYPE
};

Pra të thoni, për çdo lloj programa BPF përcaktohet një tregues për një strukturë të dhënash 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 në mënyrë të tillë:

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ë aktivizohet nga verifier çdo herë që ai takohet me një thirrje ndonjë funksioni brenda programit BPF, shihni. verifier.c.

Le të shohim se si një program hipotetik BPF përdor funksionin bpf_get_smp_processor_id. Për këtë do të rishkruajmë 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 tĂ« tipit enum bpf_fun_id, e cila pĂ«rcaktohet pĂ«r ne nĂ« skedarin vmlinux.h (skedari bpf_helper_defs.h nĂ« bĂ«rthamĂ« gjenerohet me njĂ« skript, kĂ«shtu qĂ« numrat "magjikĂ«" janĂ« nĂ« rregull). Ky funksion nuk pranon argumente dhe kthen njĂ« vlerĂ« tĂ« tipit __u32. Kur e ekzekutojmĂ« nĂ« programin tonĂ«, clang gjeneron njĂ« instruktion BPF_CALL "tĂ« saktĂ«." Le tĂ« kompilohemi programin 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:       formati i skedarit elf64-bpf

Dekompilimi i seksionit xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 thirr 8
       1:       bf 01 00 00 00 00 00 00 r1 = r0
       2:       67 01 00 00 20 00 00 00 r1 <<= 32
       3:       77 01 00 00 20 00 00 00 r1 >>= 32
       4:       b7 00 00 00 02 00 00 00 r0 = 2
       5:       15 01 01 00 00 00 00 00 nëse r1 == 0 shko +1 <LBB0_2>
       6:       b7 00 00 00 01 00 00 00 r0 = 1

0000000000000038 <LBB0_2>:
       7:       95 00 00 00 00 00 00 00 dil

NĂ« rreshtin e parĂ« ne shohim instruktionin thirr, parametri IMM i cili Ă«shtĂ« 8, dhe SRC_REG — zero. Sipas marrĂ«veshjes ABI, qĂ« pĂ«rdoret nga verifier, ky Ă«shtĂ« thirrja pĂ«r funksionin ndihmĂ«s me numrin tetĂ«. Pas ekzekutimit tĂ« saj logjika Ă«shtĂ« e thjeshtĂ«. Vlera e kthyer nga regjistri r0 kopjohet nĂ« r1 dhe nĂ« rreshtat 2,3 konvertohet nĂ« tipin u32 — bitet e sipĂ«rme 32 janĂ« zero. NĂ« rreshtat 4,5,6,7 ne kthejmĂ« 2 (XDP_PASS) ose 1 (XDP_DROP) nĂ« varĂ«si tĂ« asaj qĂ« ktheu funksioni ndihmĂ«s nga rreshti 0, gjithsesi 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 gjeti ndihmësin e duhur të kernelit.

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

Të gjitha funksionet ndihmëse gjatë ekzekutimit kanë prototip

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

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

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

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ë ekzekutuar. Le të kompilohet dhe të 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Ă« stack stringun running on CPU%un, dhe mĂ« pas nĂ« rreshtin 8 ekzekutojmĂ« atĂ« qĂ« e dimĂ« 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Ă« makro-lidhĂ«se rreth ndihmĂ«sit tĂ« vĂ«rtetĂ« bpf_trace_printk, qĂ« kĂ«rkon qĂ« tĂ« kalojmĂ« madhĂ«sinĂ« e stringut formatues.

Le të shtojmë tani 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 ne përdorim funksionin bpf_set_link_xdp_fd, i cili lidhi programet BPF të tipit XDP me ndërfaqet rrjetësore. Ne e kemi koduar me dorë numrin e ndërfaqes lo, i cili gjithmonë është i barabartë me 1. Ne e aktivizojmë funksionin dy herë, për të shkëputur fillimisht programin e vjetër, nëse kishte qenë i lidhur. Vini re, tani nuk na nevojitet thirrja pause ose cikli infinit: programi ynë ngarkues do të mbyllet, por programi BPF nuk do të shkatërrohet, pasi është i lidhur me burimin e ngjarjeve. Pas ngarkimit të suksesshëm dhe lidhjes, programi do të ekzekutohet për çdo paketë rrjeti që vjen në lo.

Do të ngarkojmë programin dhe do të shikojmë në ndërfaqe lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  emri simple  etiketa 4fca62e77ccb43d6  gpl
$ ip l shiko dev lo
1: lo:  mtu 65536 xdpgeneric qdisc noqueue gjendja UNKNOWN mode DEFAULT grup 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ërkesë + përgjigje):

$ ping -c1 localhost

dhe tani do të shikojmë përmbajtjen e skedarit virtual të debugging-ut /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 vunĂ« re nĂ« lo dhe u pĂ«rpunuan nĂ« CPU0 — programi ynĂ« i parĂ« i plotĂ« BPF punoi!

Vlen të theksohet se bpf_printk nuk e bën këtë kot në skedarin e debugging-ut: ky nuk është ndihmësi më i mirë për përdorim në production, por qëllimi ynë ishte të tregojmë diçka të thjeshtë.

Qasja në maps nga programet BPF

Shembuj: përdorim map nga programi BPF

Në seksionet e mëparshme, ne mësojmë të krijojmë dhe përdorim mapa nga hapësira e përdoruesit, tani do të shikojmë pjesën në bërthamë. Do të fillojmë, si zakonisht, me një shembull. Do ta rishkruajmë programin tonë xdp-simple.bpf.c në mënyrë të tillë:

#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 kemi shtuar përkufizimin e mapës woo: ky është një array me 8 elemente, ku ruhen vlerat e tipit u64 (në C do ta përkufizonim këtë array si u64 woo[8]). Në programin "xdp/simple" ne marrim numrin e procesorit aktual në variablën key dhe pastaj me ndihmën e funksionit ndihmës bpf_map_lookup_element marrim një pointer në regjistrin përkatës në array, të cilin e rrisim me një. Në përkthimin e shqip: ne po numërojmë statistikën e cilit CPU iu përpunuan paketat e hyrjes. Le të përpiqemi 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ë që ajo është lidhur me lo dhe 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 po shikojmë përmbajtjen e masivit:

$ 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 na pĂ«rket, e rĂ«ndĂ«sishme Ă«shtĂ« qĂ« programi punon dhe ne kuptuam si tĂ« qasel nĂ« hartat nga programet BPF — pĂ«rmes ndihmĂ«sve bpf_mp_*.

Të dyshimtë tregues

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

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ë një tregues &woo në strukturën pa emër struct { ... }


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

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

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

Zhvillimi i seksionit xdp/simple:

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

dhe përmban në relokimet:

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

Seksioni 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 shikojmë në programin që është ngarkuar, do të shohim një tregues në hartën e duhur (rreshti 4):

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

Kështu, ne mund të arrijmë në përfundimin se në momentin e ekzekutimit të programit tonë ngarkues, referenca në &woo u zëvendësua nga biblioteka libbpf. Fillimisht do të shikojmë në daljen 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 krijoi një hartë woo dhe pastaj ngarkoi programin tonë simple. Le të shikojmë më me kujdes se si e ngarkojmë programin:

  • thĂ«rrasim 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, do të thërrasë bpf_object__create_maps, e cila krijon ose hap ekzistuese maps, duke i kthyer ato në dëshmitë e skedarëve. (Këtu është vendi ku ne shohim BPF_MAP_CREATE në daljen strace.) Më pas thirret funksioni bpf_object__relocate dhe ky është interesanti për ne, sepse kujtojmë se kemi parë woo në tabelën e relokimeve. Duke e shqyrtuar, ne përfundojmë tek funksioni bpf_program__relocate, i cili merret me relokimet e mapave:

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

Prandaj, ne marrim instruksionin 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-në e parë me dëshmitë e skedarëve të mapit tonë, dhe, sikur të ishte p.sh., 0xdeadbeef, atëherë rezultati do të ishte instruksioni

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

Kështu informata për mapat kalon në programin konkret të ngarkuar BPF. Ndërkohë, mapa mund të jetë krijuar me BPF_MAP_CREATE, ose të hapet me ID përmes BPF_MAP_GET_FD_BY_ID.

Përfundimisht, me përdorimin e libbpf algoritmi është si më poshtë:

  • gjatĂ« kompilimit pĂ«r referencat nĂ« mapa krijohen regjistrime nĂ« tabelĂ«n e relokimit
  • libbpf hap objekti ELF, gjen tĂ« gjitha mapat e pĂ«rdorura dhe krijon pĂ«r to dĂ«shmitĂ« e skedarĂ«ve
  • dĂ«shmitĂ« e skedarĂ«ve ngarkohen nĂ« bĂ«rthamĂ« si pjesĂ« e instruksionit LD64

Ashtu siç e kuptoni, kjo nuk Ă«shtĂ« e gjitha, dhe do tĂ« duhet tĂ« hedhim njĂ« sy nĂ« bĂ«rthamĂ«. FatmirĂ«sisht, kemi njĂ« gjurmĂ« — kemi shkruar vlerĂ«n BPF_PSEUDO_MAP_FD nĂ« regjistrin burimor dhe mund ta shqyrtojmĂ«, gjĂ« qĂ« do tĂ« na çojĂ« nĂ« vendin mĂ« tĂ« shenjtĂ« tĂ« tĂ« gjithĂ«ve — kernel/bpf/verifier.c, ku funksioni me emrin karakteristik zĂ«vendĂ«son dĂ«shminĂ« e skedarit me adresĂ«n e strukturĂ«s 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 të plotësojmë algoritmin tonë:

  • gjatĂ« ngarkimit tĂ« programit, verifikuesi 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 të tjera, por ne do t'i diskutojmë ato në kuadër të artikujve të tjerë.

Ngarko programet dhe mapat pa libbpf

Ashtu siç ishte e premtuar, ja një shembull për lexuesit që dëshirojnë të dinë 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ë ambient ku nuk mund të grumbulloni varësi, ose kurseni çdo bit, ose shkruani një program si ply, i cili gjeneron kodin binar BPF në fluturim.

Për t'u ndjekur më lehtë logjika, ne për këto qëllime do ta riparojmë shembullin tonë xdp-simple. Kodi i plotë dhe pak më i zgjeruar i programit, i shqyrtuar në këtë shembull, mund të gjendet në këtë gist.

Logjika e aplikacionit tonë është si vijon:

  • tĂ« krijojmĂ« njĂ« hartĂ« tĂ« tipit BPF_MAP_TYPE_ARRAY me komandĂ«n BPF_MAP_CREATE,
  • tĂ« krijojmĂ« njĂ« program qĂ« pĂ«rdor kĂ«tĂ« hartĂ«,
  • tĂ« lidhim programin me ndĂ«rfaqen lo,

çka përkthehet në gjuhën njerëzore 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 krijon njĂ« hartĂ« po ashtu siç e kemi bĂ«rĂ« nĂ« shembullin e parĂ« mbi thirrjen e sistemit bpf — "nĂ« bĂ«rthamĂ«, tĂ« lutem, mĂ« krijo njĂ« hartĂ« si njĂ« array me 8 elemente tĂ« tipit __u64 dhe mĂ« kthe njĂ« deshkrim tĂ« dosjes":

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ë array strukturash struct bpf_insn insns[]. Por pasi përdorim programin që kemi në C, mund të jemi pak të zgjuar:

$ 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 instruksione si struktura 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 thirr 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,
    },
    { }, /* vendi i lirë */

    /* 85 00 00 00 01 00 00 00 thirr 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 nëse r0 == 0 shko +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 dilni */
    {
        .code = BPF_JMP | BPF_EXIT
    },
};

NjĂ« ushtrim pĂ«r ata qĂ« nuk e shkruan vetĂ« kĂ«tĂ« – gjeni map_fd.

NĂ« programin tonĂ« ka mbetur njĂ« pjesĂ« tjetĂ«r e papĂ«rfunduar – xdp_attach. FatkeqĂ«sisht, programet e tipit XDP nuk mund tĂ« lidhen pĂ«rmes thirrjeve tĂ« sistemit bpf. NjerĂ«zit qĂ« krijuan BPF dhe XDP ishin nga komuniteti rrjetor i Linux, prandaj ata pĂ«rdorĂ«n ndĂ«rfaqen mĂ« tĂ« njohur pĂ«r ta (por jo pĂ«r njerezit e zakonshĂ«m ) pĂ«r tĂ« ndĂ«rvepruar me kernelin: netlink sockets, shih gjithashtu RFC3549. MĂ«nyra mĂ« e lehtĂ« pĂ«r ta zbatuar xdp_attach Ă«shtĂ« kopjimi i kodit nga libbpf, konkretisht, nga skedari netlink.c, tĂ« cilin ne e kemi ndjekur, paksa duke e shkurtruar atĂ«:

Mirë se vini në botën e netlink sockets

Hapni netlink socket tipin NETLINK_ROUTE:

int netlink_open(__u32 *nl_pid)
{
    struct sockaddr_nl sa;
    socklen_t addrlen;
    int one = 1, ret;
    int sock;

    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;

    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (sock < 0)
        err(1, "socket");

    if (setsockopt(sock, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)) < 0)
        warnx("netlink error reporting not supported");

    if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
        err(1, "bind");

    addrlen = sizeof(sa);
    if (getsockname(sock, (struct sockaddr *)&sa, &addrlen) < 0)
        err(1, "getsockname");

    *nl_pid = sa.nl_pid;
    return sock;
}

Lexojmë nga ky soket:

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, "wrong pid");
            if (nh->nlmsg_seq != seq)
                errx(1, "INVSEQ");
            if (nh->nlmsg_flags & NLM_F_MULTI)
                multipart = true;
            switch (nh->nlmsg_type) {
                case NLMSG_ERROR:
                    errm = (struct nlmsgerr *)NLMSG_DATA(nh);
                    if (!errm->error)
                        continue;
                    ret = errm->error;
                    // libbpf_nla_dump_errormsg(nh); too many code to copy...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

Së fundi, ja funksioni ynë që hap soketin dhe dërgon në të një mesazh të veçantë, që përmban një deshifrues 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;
}

Kështu, 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
+++ exited with 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'i dërgojmë ping dhe do të shohim në hartë:

$ 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

Ura, gjithçka funksionon. Vini re, gjithashtu, se harta jonë sërish paraqitet në formë bajtësh. Kjo ndodh sepse, ndryshe nga libbpf ne nuk kemi ngarkuar informacionin mbi llojet (BTF). Por për këtë do të flasim më në detaje herën tjetër.

Mjetet e zhvillimit

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

NĂ« tĂ« vĂ«rtetĂ«, pĂ«r tĂ« zhvilluar programe BPF nuk nevojitet asgjĂ« e veçantĂ« — BPF funksionon nĂ« çdo kernel tĂ« njohur tĂ« distirbutit, dhe programet kompilohet pĂ«rmes clang, i cili mund tĂ« vendoset nga paketa. MegjithatĂ«, pĂ«r shkak se BPF Ă«shtĂ« nĂ« proces zhvillimi, blloku dhe mjetet ndryshojnĂ« vazhdimisht; nĂ«se nuk dĂ«shiron tĂ« shkruash programe BPF nĂ« mĂ«nyrat e vjetra nga vitet 2019, do tĂ« duhet ta ndĂ«rtosh

  • llvm/clang
  • pahole
  • nĂ« bllokun tĂ«nd
  • bpftool

(Për referencë: kjo seksion dhe të gjithë shembujt në artikull u ekzekutuan në Debian 10.)

llvm/clang

BPF punon mirë me LLVM dhe, megjithëse kohët e fundit programet për BPF mund të kompilohen edhe me gcc, e gjithë zhvillimi i tanishëm po bëhet për LLVM. Prandaj, fillimisht ne do të ndërtoshim 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ërtimi i optimizuar.
  Objektivi i paracaktuar: x86_64-unknown-linux-gnu
  CPU host: znver1

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

(Udhëzimi për ndërtimin clang i marrë nga bpf_devel_QA.)

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

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

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

Pahole dhe BTF

Mjeti pahole përdoret gjatë ndërtimit të bllokut për të krijuar informacionin e ndihmës në formatin BTF. Ne nuk do të shkojmë në detaje të teknologjisë BTF në këtë artikull, përveç faktit që është e përshtatshme dhe ne duam ta përdorim atë. Prandaj, nëse po planifikoni të ndërtosh bllokun tënd, ndihmoni që të ndërtosh së pari pahole (pa pahole nuk do të jesh në gjendje të ndërtosh bllokun 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

Blloqe për eksperimente me BPF

Në shqyrtimin e mundësive të BPF, dëshiron të ndërtohësh bërthamën tënde. Kjo nuk është e domosdoshme, sepse mund të ndërtohesh dhe ngarkosh programe BPF edhe me bërthamën e shpërndarjes, megjithatë, të pasosh bërthamën tënde të lejon të përdorësh mundësitë më të reja të BPF, të cilat do të bëhen të disponueshme në shpërndarjen tënde, në më të mirën e rastit, pas disa muajsh ose, si në rastin e disa mjeteve për debuggim, nuk do të paketohen fare në një të ardhme të dukshme. Gjithashtu, bërthama e jotja të lejon të ndihesh e rëndësishme duke eksperimetuar me kodin.

PĂ«r tĂ« ndĂ«rtuar bĂ«rthamĂ«n, nevojitet, sĂ« pari, vetĂ« bĂ«rthama dhe, sĂ« dyti, skedari i konfigurimit tĂ« bĂ«rthamĂ«s. PĂ«r eksperimente me BPF, mund tĂ« pĂ«rdorim bĂ«rthamĂ«n e zakonshme ose njĂ« nga bĂ«rthamat zhvillimore. Historikisht, zhvillimi i BPF ndodh brenda komunitetit rrjetor tĂ« Linux dhe kĂ«shtu qĂ« tĂ« gjitha ndryshimet kalojnĂ«, herĂ«t ose vonĂ«, pĂ«rmes David Miller — mbajtĂ«sit tĂ« pjesĂ«s rrjetore tĂ« Linux. NĂ« varĂ«si tĂ« natyrĂ«s sĂ« saj — rregullimi ose karakteristika tĂ« reja — ndryshimet rrjetore pĂ«rfshihen nĂ« njĂ«rĂ«n nga dy bĂ«rthamat — net ose net-next. Ndryshimet pĂ«r BPF po ashtu shpĂ«rndahen mes bpf dhe bpf-next, tĂ« cilat mĂ« pas transferohen nĂ« net dhe net-next, pĂ«rkatĂ«sisht. Shihni mĂ« shumĂ« nĂ« bpf_devel_QA dhe netdev-FAQ. Prandaj, zgjidhni bĂ«rthamĂ«n nĂ« pĂ«rputhje me preferencat dhe nevojat tuaja pĂ«r stabilitetin e sistemit ku po testoni (*-next bĂ«rthamat janĂ« mĂ« tĂ« paqĂ«ndrueshmet nga ato qĂ« janĂ« pĂ«rmendur).

Ky artikull nuk pĂ«rfshin histori mbi si tĂ« menaxhoni skedarĂ«t e konfigurimit tĂ« bĂ«rthamĂ«s — supozohet qĂ« ju tashmĂ« e dini kĂ«tĂ« ose jini tĂ« gatshĂ«m tĂ« mĂ«soni vetĂ«. MegjithatĂ«, udhĂ«zimet e mĂ«poshtme duhet tĂ« jenĂ«, mĂ« shumĂ« ose mĂ« pak, tĂ« mjaftueshme qĂ« ju tĂ« keni 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ërto një konfigurim minimal të punueshmërisë së bërthamës:

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

Aktivizoni opsionet BPF në skedarin .config sipër zgjedhjes suaj (shumë mund të jetë se vetë CONFIG_BPF do të jetë tashmë aktivizuar, pasi përdoresh 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 is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y

Pastaj mund të mbledhim dhe instalojmë lehtësisht modul dhe kernel (me rastin, mund ta mbledhim kernel-in duke përdorur atë që sapo u mbledh) clang, duke shtuar CC=clang):

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

dhe rinisni me kernel-in e ri (unë e përdor për këtë kexec nga paketa kexec-tools):

v=5.8.0-rc6+ # nëse po rimbledhni kernel-in 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

Utliliti mĂ« i pĂ«rdorur nĂ« kĂ«tĂ« artikull do tĂ« jetĂ« utiliti bpftool, i ofruar si pjesĂ« e kernel-it Linux. Ai Ă«shtĂ« shkruar dhe mbĂ«shtetur nga zhvilluesit e BPF pĂ«r zhvilluesit e BPF dhe me tĂ« mund tĂ« menaxhoni tĂ« gjitha llojet e objekteve BPF — ngarkoni programe, krijoni dhe ndryshoni mapa, shqyrtoni aktivitetin e ekosistemit BPF, etj. Dokumentacionin nĂ« formĂ«n e burimeve pĂ«r man pages mund ta gjeni nĂ« kernel ose, tashmĂ« tĂ« kompiluar, nĂ« internet.

Në momentin që po shkruhet ky artikull bpftool ofrohet në formë të gatshme vetëm për RHEL, Fedora dhe Ubuntu (shihni, për shembull, këta thread, në të cilin flitet për historinë e papërfunduar të paketimit bpftool në Debian). Por nëse tashmë keni mbledhur kernel-in tuaj, atëherë ta mbledhni bpftool është shumë e lehtë:

$ cd ${linux}/tools/bpf/bpftool
# ... shkruani rrugët e clang të fundit, siç është treguar më sipër
$ make -s

Auto-detecting system features:
...                        libbfd: [ on  ]
...        disassembler-four-args: [ on  ]
...                          zlib: [ on  ]
...                        libcap: [ on  ]
...               clang-bpf-co-re: [ on  ]

Auto-detecting system features:
...                        libelf: [ on  ]
...                          zlib: [ on  ]
...                           bpf: [ on  ]

$

(kĂ«tu ${linux} — Ă«shtĂ« direktoria juaj me kernel-in). Pas ekzekutimit tĂ« kĂ«tyre komandave bpftool do tĂ« mbledhet nĂ« direktorinĂ« ${linux}/tools/bpf/bpftool dhe mund ta shtoni nĂ« rrugĂ« (pĂ«rpara se gjithash user root) ose thjesht ta kopjoni nĂ« /usr/local/sbin.

MĂ« mirĂ« Ă«shtĂ« ta mbledhni me versionin mĂ« tĂ« fundit bpftool , e mbledhur, siç Ă«shtĂ« treguar mĂ« sipĂ«r, dhe ta kontrolloni nĂ«se Ă«shtĂ« mbledhur saktĂ« — duke pĂ«rdorur, pĂ«r shembull, komandĂ«n clang$ sudo bpftool feature probe kernel Scanning system configuration... bpf() syscall for unprivileged users is enabled JIT compiler is enabled JIT compiler hardening is disabled JIT compiler kallsyms exports are enabled for root ...

e cila do të tregojë cilat karakteristika BPF janë të aktivizuara në kernel-in tuaj.

Për më tepër, komandën paraprake mund ta ekzekutoni si

Kjo është bërë në mënyrë analogjike me utilitetet nga paketa

# bpftool f p k

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

Përfundim

BPF lejon që një bletë të masë efektivisht dhe të ndryshojë funksionalitetin e kernelit në fluturim. Sistema rezultoi shumë e suksesshme, në traditat më të mira të UNIX: një mekanizëm i thjeshtë, që lejon (rindërtimin) e kernelit, u dha mundësinë një numri të madh njerëzish dhe organizatash të eksperimentojnë. Dhe, ndonëse eksperimentet, ashtu si zhvillimi i infrastrukturës BPF, ende nuk kanë përfunduar, sistemi tashmë ka një ABI të qëndrueshme që lejon ndërtimin e një logjike biznesi të besueshme e, më e rëndësishmja, efektive.

Dua tĂ« theksoj se, sipas mendimit tim, teknologjia u bĂ« kaq e njohur sepse, nga njĂ«ra anĂ«, mund tĂ« luajĂ« (arkitektura e makinĂ«s mund tĂ« kuptohet mĂ« shumĂ« ose mĂ« pak brenda njĂ« mbrĂ«mjeje), dhe nga ana tjetĂ«r — tĂ« zgjidhĂ« probleme, qĂ« nuk mund tĂ« zgjidhen (bukur) para shfaqjes sĂ« saj. KĂ«to dy komponente sĂ« bashku e nxisin njerĂ«zit tĂ« eksperimentojnĂ« dhe tĂ« Ă«ndĂ«rrojnĂ«, gjĂ« qĂ« çon nĂ« shfaqjen e zgjidhjeve inovative tĂ« reja e mĂ« 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Ă«, pĂ«rafĂ«rsisht, kĂ«shtu: artikulli i ardhshĂ«m do tĂ« jetĂ« njĂ« pĂ«rmbledhje e llojeve tĂ« programeve BPF (nĂ« kernelin 5.8 pĂ«rkrah 30 lloje programesh), atĂ«herĂ« pĂ«rfundimisht do tĂ« shohim se si tĂ« shkruajmĂ« aplikacione reale nĂ« BPF pĂ«rmes programeve pĂ«r gjurmimin e kernelit, pastaj do tĂ« vijĂ« koha pĂ«r njĂ« kurs mĂ« tĂ« thelluar mbi arkitekturĂ«n BPF, e pastaj — pĂ«r shembuj tĂ« aplikacioneve BPF nĂ« rrjet dhe siguri.

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

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

Linket

  1. UdhĂ«zuesi ReferencĂ« BPF dhe XDP — dokumentacioni pĂ«r BPF nga cilium, mĂ« saktĂ«sisht nga Daniel Borkman, njĂ«ri nga krijuesit dhe mirĂ«mbajtĂ«sit e BPF. Kjo Ă«shtĂ« njĂ« nga pĂ«rshkrimet e para serioze, qĂ« dallohet nga tĂ« tjerat pĂ«r faktin se Daniel e di saktĂ«sisht pĂ«r çfarĂ« po shkruan dhe nuk ka gabime aty. NĂ« veçanti, nĂ« kĂ«tĂ« dokument shpjegohet si tĂ« punoni me programet BPF tĂ« llojeve XDP dhe TC duke pĂ«rdorur utilitarin e njohur ip nga paketa iproute2.

  2. Dokumentacioni/networking/filter.txt — dokumenti origjinal me dokumentacionin pĂ«r BPF klasik, dhe pastaj pĂ«r BPF tĂ« zgjeruar. E dobishme pĂ«r t'u lexuar, nĂ«se dĂ«shironi tĂ« hidhni njĂ« vĂ«shtrim nĂ« asamblin dhe detajet teknike tĂ« arkitekturĂ«s.

  3. Blogu pĂ«r BPF nga facebook. Rreth aktualizohet rrallĂ«, por me precizitet, pasi aty shkruajnĂ« Alexei Starovoitov (autori i eBPF) dhe Andrii Nakryiko — (mirĂ«mbajtĂ«s libbpf).

  4. Sekretet e bpftool. Një thread i interesant në Twitter nga Quentin Monnet me shembuj dhe sekrete për përdorimin e bpftool.

  5. Hidhni një vështrim në BPF: një listë materialesh letrare. Një listë gjigante (dhe akoma e mbështetur) e lidhjeve në dokumentacionin mbi BPF nga Quentin Monnet.

Burimi: habr.com

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