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

Në fillim kishte një teknologji dhe quhej BPF. Ne e shikuam atë e mëparshme, artikull i kësaj serie të Dhiatës së Vjetër. Në vitin 2013, me përpjekjet e Alexei Starovoitov dhe Daniel Borkman, një version i përmirësuar i tij, i optimizuar për makinat moderne 64-bit, u zhvillua dhe u përfshi në kernelin Linux. Kjo teknologji e re u quajt shkurtimisht Internal BPF, më pas u riemërua Extended BPF dhe tani, pas disa vitesh, të gjithë e quajnë thjesht BPF.

Përafërsisht, BPF ju lejon të ekzekutoni kodin arbitrar të furnizuar nga përdoruesi në hapësirën e kernelit Linux dhe arkitektura e re doli të ishte aq e suksesshme sa do të na duhen një duzinë artikujsh të tjerë për të përshkruar të gjitha aplikacionet e saj. (E vetmja gjë që zhvilluesit nuk e bënë mirë, siç mund ta shihni në kodin e performancës më poshtë, ishte krijimi i një logoje të mirë.)

Ky artikull përshkruan strukturën e makinës virtuale BPF, ndërfaqet e kernelit për të punuar me BPF, mjetet e zhvillimit, si dhe një përmbledhje të shkurtër, shumë të shkurtër të aftësive ekzistuese, d.m.th. gjithçka që do të na nevojitet në të ardhmen për një studim më të thellë të zbatimeve praktike të BPF.
BPF për të vegjlit, pjesa e parë: BPF e zgjeruar

Përmbledhje e artikullit

Hyrje në arkitekturën BPF. Së pari, ne do të bëjmë një pamje nga sytë e shpendëve të arkitekturës BPF dhe do të përshkruajmë komponentët kryesorë.

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

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

Menaxhimi i objekteve duke përdorur thirrjen e sistemit bpf. Me njëfarë kuptimi të sistemit tashmë të vendosur, më në fund do të shikojmë se si të krijojmë dhe manipulojmë objekte nga hapësira e përdoruesit duke përdorur një thirrje të veçantë të sistemit - bpf(2).

Пишем программы BPF с помощью libbpf. Sigurisht, ju mund të shkruani programe duke përdorur një thirrje sistemi. Por është e vështirë. Për një skenar më realist, programuesit bërthamorë zhvilluan një bibliotekë libbpf. Ne do të krijojmë një skelet bazë aplikimi BPF që do ta përdorim në shembujt vijues.

Ndihmuesit e kernelit. Këtu do të mësojmë se si programet BPF mund të aksesojnë funksionet ndihmëse të kernelit - një mjet që, së bashku me hartat, zgjeron rrënjësisht aftësitë e BPF-së së re në krahasim me atë klasik.

Qasja në harta nga programet BPF. Deri në këtë pikë, ne do të dimë mjaftueshëm për të kuptuar saktësisht se si mund të krijojmë programe që përdorin harta. Dhe le t'i hedhim një vështrim të shpejtë verifikuesit të madh dhe të fuqishëm.

Mjetet e zhvillimit. Seksioni i ndihmës se si të mblidhen shërbimet e nevojshme dhe bërthama për eksperimente.

Përfundimi. Në fund të artikullit, ata që lexojnë deri këtu do të gjejnë fjalë motivuese dhe një përshkrim të shkurtër të asaj që do të ndodhë në artikujt vijues. Gjithashtu do të listojmë një sërë lidhjesh për vetëstudim për ata që nuk kanë dëshirën apo aftësinë për të pritur vazhdimin.

Hyrje në Arkitekturën BPF

Përpara se të fillojmë të shqyrtojmë arkitekturën BPF, do t'i referohemi një herë të fundit (oh). BPF klasike, i cili u zhvillua si një përgjigje ndaj ardhjes së makinave RISC dhe zgjidhi problemin e filtrimit efikas të paketave. Arkitektura doli të ishte aq e suksesshme sa, pasi kishte lindur në vitet e nëntëdhjeta të shpejta në Berkeley UNIX, ajo u transferua në shumicën e sistemeve operative ekzistuese, mbijetoi deri në të njëzetat e çmendura dhe ende po gjen aplikacione të reja.

BPF i ri u zhvillua si përgjigje ndaj përhapjes së makinave 64-bit, shërbimeve cloud dhe nevojës së shtuar për mjete për krijimin e SDN (Stë gjitha-definuar npërpunim). Zhvilluar nga inxhinierët e rrjetit kernel si një zëvendësim i përmirësuar për BPF-në klasike, BPF-ja e re fjalë për fjalë gjashtë muaj më vonë gjeti aplikacione në detyrën e vështirë të gjurmimit të sistemeve Linux, dhe tani, gjashtë vjet pas shfaqjes së tij, do të na duhet një artikull i tërë tjetër vetëm për listoni llojet e ndryshme të programeve.

Foto qesharake

Në thelbin e tij, BPF është një makinë virtuale sandbox që ju lejon të ekzekutoni kodin "arbitrar" në hapësirën e kernelit pa kompromentuar sigurinë. Programet BPF krijohen në hapësirën e përdoruesit, ngarkohen në kernel dhe lidhen me disa burime ngjarjesh. Një ngjarje mund të jetë, për shembull, shpërndarja e një pakete në një ndërfaqe rrjeti, nisja e disa funksioneve të kernelit, etj. Në rastin e një pakete, programi BPF do të ketë akses në të dhënat dhe metadatat e paketës (për lexim dhe, mundësisht, shkrim, në varësi të llojit të programit); në rastin e ekzekutimit të një funksioni kernel, argumentet e funksionin, duke përfshirë treguesit në kujtesën e kernelit, etj.

Le të hedhim një vështrim më të afërt në këtë proces. Për të filluar, le të flasim për ndryshimin e parë nga BPF klasik, programet për të cilat janë shkruar në asembler. Në versionin e ri, arkitektura u zgjerua 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, i cili ju lejon të gjeneroni bytecode për arkitekturën BPF.

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

Arkitektura BPF u projektua, pjesërisht, për të funksionuar në mënyrë efikase në makinat moderne. Për ta bërë këtë të funksionojë në praktikë, bytekodi BPF, pasi të ngarkohet në kernel, përkthehet në kodin vendas duke përdorur një komponent të quajtur përpilues JIT (Just In Tkohë). Tjetra, nëse ju kujtohet, në BPF klasik, programi ngarkohej në kernel dhe iu bashkëngjit burimit të ngjarjes në mënyrë atomike - në kontekstin e një thirrjeje të vetme sistemi. Në arkitekturën e re, kjo ndodh në dy faza - së pari, kodi ngarkohet në kernel duke përdorur një thirrje sistemi bpf(2)dhe më pas, më vonë, përmes mekanizmave të tjerë që ndryshojnë në varësi të llojit të programit, programi i bashkëngjitet burimit të ngjarjes.

Këtu lexuesi mund të ketë një pyetje: a ishte e mundur? Si garantohet siguria e ekzekutimit të një kodi të tillë? Siguria e ekzekutimit na garantohet nga faza e ngarkimit të programeve BPF të quajtur verifikues (në anglisht kjo fazë quhet verifikues dhe unë do të vazhdoj të përdor fjalën angleze):

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

Verifikuesi është një analizues statik që siguron që një program të mos prishë funksionimin normal të kernelit. Kjo, nga rruga, nuk do të thotë që programi nuk mund të ndërhyjë në funksionimin e sistemit - programet BPF, në varësi të llojit, mund të lexojnë dhe rishkruajnë seksione të kujtesës së kernelit, të kthejnë vlerat e funksioneve, të shkurtojnë, shtojnë, rishkruajnë dhe madje edhe përcjellja e paketave të rrjetit. Verifikuesi garanton që ekzekutimi i një programi BPF nuk do të prishë kernelin dhe se një program që, sipas rregullave, ka akses shkrimi, për shembull, të dhënat e një pakete në dalje, nuk do të jetë në gjendje të mbishkruajë memorien e kernelit jashtë paketës. Verifikuesin do ta shikojmë pak më në detaje në seksionin përkatës, pasi të njihemi me të gjithë përbërësit e tjerë të BPF.

Pra, çfarë kemi mësuar deri tani? Përdoruesi shkruan një program në C, e ngarkon atë në kernel duke përdorur një thirrje sistemi bpf(2), ku kontrollohet nga një verifikues dhe përkthehet në bytekod vendas. Pastaj i njëjti ose një përdorues tjetër lidh programin me burimin e ngjarjes dhe ai fillon të ekzekutohet. Ndarja e boot dhe lidhja është e nevojshme për disa arsye. Së pari, ekzekutimi i një verifikuesi është relativisht i shtrenjtë dhe duke shkarkuar të njëjtin program disa herë humbim kohën e kompjuterit. Së dyti, saktësisht se si lidhet një program varet nga lloji i tij dhe një ndërfaqe "universale" e zhvilluar një vit më parë mund të mos jetë e përshtatshme për lloje të reja programesh. (Edhe pse tani që arkitektura po bëhet më e pjekur, ekziston një ide për të unifikuar këtë ndërfaqe në nivel libbpf.)

Lexuesi i vëmendshëm mund të vërejë se nuk kemi mbaruar ende me fotot. Në të vërtetë, të gjitha sa më sipër nuk shpjegojnë pse BPF ndryshon rrënjësisht pamjen në krahasim me BPF klasik. Dy risi që zgjerojnë ndjeshëm fushën e zbatueshmërisë janë aftësia për të përdorur memorien e përbashkët dhe funksionet ndihmëse të kernelit. Në BPF, memoria e përbashkët zbatohet duke përdorur të ashtuquajturat harta - struktura të përbashkëta të të dhënave me një API specifike. Ata ndoshta e morën këtë emër sepse lloji i parë i hartës që u shfaq ishte një tabelë hash. Pastaj u shfaqën vargje, tabela hash lokale (për CPU) dhe grupe lokale, pemë kërkimi, harta që përmbajnë tregues për programet BPF dhe shumë më tepër. Ajo që është interesante për ne tani është se programet BPF tani kanë aftësinë për të vazhduar gjendjen midis thirrjeve dhe për ta ndarë atë me programe të tjera dhe me hapësirën e përdoruesit.

Hartat aksesohen nga proceset e përdoruesit duke përdorur një thirrje sistemi bpf(2), dhe nga programet BPF që ekzekutohen në kernel duke përdorur funksione ndihmëse. Për më tepër, ndihmësit ekzistojnë jo vetëm për të punuar me harta, por edhe për të hyrë në aftësi të tjera të kernelit. Për shembull, programet BPF mund të përdorin funksione ndihmëse për të përcjellë paketat në ndërfaqe të tjera, për të gjeneruar ngjarje perf, për të aksesuar strukturat e kernelit, e kështu me radhë.

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

Në përmbledhje, BPF ofron mundësinë për të ngarkuar kodin e përdoruesit arbitrar, d.m.th., të testuar nga verifikuesi në hapësirën e kernelit. Ky kod mund të ruajë gjendjen midis thirrjeve dhe të shkëmbejë të dhëna me hapësirën e përdoruesit, dhe gjithashtu ka akses në nënsistemet e kernelit të lejuara nga ky lloj programi.

Kjo tashmë është e ngjashme me aftësitë e ofruara nga modulet e kernelit, në krahasim me të cilat BPF ka disa avantazhe (natyrisht, mund të krahasoni vetëm aplikacione të ngjashme, për shembull, gjurmimin e sistemit - nuk mund të shkruani një drejtues arbitrar me BPF). Ju mund të vini re një prag më të ulët të hyrjes (disa shërbime që përdorin BPF nuk kërkojnë që përdoruesi të ketë aftësi programimi kernel, ose aftësi programimi në përgjithësi), siguri në kohën e ekzekutimit (ngre dorën në komente për ata që nuk e thyen sistemin kur shkruan ose modulet e testimit), atomiciteti - ka kohë joproduktive gjatë ngarkimit të moduleve dhe nënsistemi BPF siguron që asnjë ngjarje të mos mungojë (të jemi të drejtë, kjo nuk është e vërtetë për të gjitha llojet e programeve BPF).

Prania e aftësive të tilla e bën BPF një mjet universal për zgjerimin e kernelit, gjë që konfirmohet në praktikë: gjithnjë e më shumë lloje të reja programesh shtohen në BPF, gjithnjë e më shumë kompani të mëdha përdorin BPF në serverët luftarakë 24×7, gjithnjë e më shumë. startup-et e ndërtojnë biznesin e tyre mbi zgjidhje të bazuara në të cilat bazohen në BPF. BPF përdoret kudo: në mbrojtjen kundër sulmeve DDoS, krijimin e SDN (për shembull, zbatimin e rrjeteve për kubernetes), si mjeti kryesor i gjurmimit të sistemit dhe mbledhësi i statistikave, në sistemet e zbulimit të ndërhyrjeve dhe sistemet e sandbox, etj.

Le të përfundojmë pjesën e përmbledhjes së artikullit këtu dhe të shikojmë makinën virtuale dhe ekosistemin BPF në më shumë detaje.

Digresioni: shërbimet komunale

Për të qenë në gjendje të ekzekutoni shembujt në seksionet e mëposhtme, mund t'ju nevojiten një numër shërbimesh, të paktën llvm/clang me mbështetjen e bpf dhe bpftool. Në pjesën Mjetet e Zhvillimit Ju mund të lexoni udhëzimet për montimin e shërbimeve, si dhe kernelin tuaj. Ky seksion është vendosur më poshtë për të mos prishur harmoninë e prezantimit tonë.

Regjistrat dhe Sistemi i Instruksioneve të Makinave Virtuale BPF

Arkitektura dhe sistemi i komandës së BPF u zhvilluan duke marrë parasysh faktin se programet do të shkruhen në gjuhën C dhe, pas ngarkimit në kernel, do të përkthehen në kodin vendas. Prandaj, numri i regjistrave dhe grupi i komandave u zgjodhën duke pasur parasysh kryqëzimin, në kuptimin matematikor, të aftësive të makinave moderne. Për më tepër, programet u vendosën kufizime të ndryshme, për shembull, deri vonë nuk ishte e mundur të shkruante sythe dhe nënprograme, dhe numri i udhëzimeve ishte i kufizuar në 4096 (tani programet e privilegjuara mund të ngarkojnë deri në një milion udhëzime).

BPF ka njëmbëdhjetë regjistra 64-bit të aksesueshëm nga përdoruesi r0-r10 dhe një numërues programi. Regjistrohu r10 përmban një tregues kornizë dhe është vetëm për lexim. Programet kanë akses në një pirg 512-byte në kohën e ekzekutimit dhe një sasi të pakufizuar memorie të përbashkët në formën e hartave.

Programet BPF lejohen të ekzekutojnë një grup specifik të ndihmësve të kernelit të tipit programor dhe, së fundmi, funksione të rregullta. Çdo funksion i thirrur mund të marrë deri në pesë argumente, të kaluara në regjistra r1-r5, dhe vlera e kthimit i kalohet r0. Garantohet që pas kthimit nga funksioni, përmbajtja e regjistrave r6-r9 Nuk do të ndryshojë.

Për përkthim efikas të programit, regjistrat r0-r11 për të gjitha arkitekturat e mbështetura janë hartuar në mënyrë unike me regjistra realë, duke marrë parasysh veçoritë ABI të arkitekturës aktuale. Për shembull, për x86_64 regjistrat r1-r5, që përdoret për të kaluar parametrat e funksionit, shfaqen në rdi, rsi, rdx, rcx, r8, të cilat përdoren për të kaluar parametrat tek funksionet e ndezura x86_64. Për shembull, kodi në të majtë përkthehet në kodin në të djathtë si kjo:

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

Regjistrohuni r0 përdoret gjithashtu për të kthyer rezultatin e ekzekutimit të programit dhe në regjistër r1 programit i kalohet një tregues në kontekst - në varësi të llojit të programit, kjo mund të jetë, për shembull, një strukturë struct xdp_md (për XDP) ose strukturë struct __sk_buff (për programe të ndryshme të rrjetit) ose strukturë struct pt_regs (për lloje të ndryshme programesh gjurmuese), etj.

Pra, ne kishim një grup regjistrash, ndihmës të kernelit, një pirg, një tregues konteksti dhe memorie të përbashkët në formën e hartave. Jo se e gjithë kjo është absolutisht e nevojshme në udhëtim, por...

Le të vazhdojmë përshkrimin dhe të flasim për sistemin e komandës për të punuar me këto objekte. te gjitha (Pothuajse te gjitha) Udhëzimet BPF kanë një madhësi fikse 64-bit. Nëse shikoni një udhëzim në një makinë Big Endian 64-bit, do të shihni

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

Këtu Code - ky është kodimi i udhëzimit, Dst/Src janë përkatësisht kodimet e marrësit dhe burimit, Off - Vërshimi i nënshkruar 16-bit, dhe Imm është një numër i plotë i nënshkruar 32-bit i përdorur në disa instruksione (i ngjashëm me konstantën cBPF K). Kodimi Code ka një nga dy llojet:

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

Klasat e udhëzimeve 0, 1, 2, 3 përcaktojnë komandat për të punuar me kujtesën. Ata quhen, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respektivisht. Klasat 4, 7 (BPF_ALU, BPF_ALU64) përbëjnë një grup udhëzimesh ALU. Klasat 5, 6 (BPF_JMP, BPF_JMP32) përmbajnë udhëzime kërcimi.

Plani i mëtejshëm për studimin e sistemit të udhëzimeve BPF është si më poshtë: në vend që të rendisim me përpikëri të gjitha udhëzimet dhe parametrat e tyre, ne do të shikojmë disa shembuj në këtë seksion dhe prej tyre do të bëhet e qartë se si funksionojnë në të vërtetë udhëzimet dhe si të çmontoni manualisht çdo skedar binar për BPF. Për të konsoliduar materialin më vonë në artikull, do të takojmë gjithashtu udhëzime individuale në seksionet në lidhje me Verifierin, përpiluesin JIT, përkthimin e BPF klasike, si dhe kur studiojmë hartat, funksionet e thirrjes, etj.

Kur flasim për udhëzime individuale, do t'i referohemi skedarëve bazë bpf.h и bpf_common.h, të cilat përcaktojnë kodet numerike të instruksioneve BPF. Kur studioni arkitekturën vetë dhe/ose analizoni binarët, mund të gjeni semantikë në burimet e mëposhtme, të renditura sipas kompleksitetit: Specifikim jozyrtar eBPF, Udhëzuesi i referencës BPF dhe XDP, grup instruksionesh, Documentation/networking/filter.txt dhe, natyrisht, në kodin burimor Linux - verifikues, JIT, përkthyes BPF.

Shembull: çmontimi i BPF në kokën tuaj

Le të shohim një shembull në të cilin ne përpilojmë një program readelf-example.c dhe shikoni binarin që rezulton. Ne do të zbulojmë përmbajtjen origjinale readelf-example.c më poshtë, pasi të rivendosim 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ë një indentacion dhe kështu programi ynë 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 komandës janë të barabarta b7, 15, b7 и 95. Kujtoni se tre bitët më pak të rëndësishëm janë klasa e instruksionit. Në rastin tonë, biti i katërt i të gjitha udhëzimeve është bosh, kështu që klasat e udhëzimeve janë përkatësisht 7, 5, 7, 5. Klasa 7 është BPF_ALU64, dhe 5 është BPF_JMP. Për të dyja klasat, formati i udhëzimeve është i njëjtë (shih më lart) dhe ne mund ta rishkruajmë programin tonë kështu (në të njëjtën kohë do të rishkruajmë kolonat e mbetura në formë njerëzore):

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

Operacion b klasë ALU64 - A BPF_MOV. Ai i cakton një vlerë regjistrit të destinacionit. Nëse biti është vendosur s (burimi), atëherë vlera merret nga regjistri burimor dhe nëse, si në rastin tonë, nuk vendoset, atëherë vlera merret nga fusha. Imm. Pra në instruksionin e parë dhe të tretë kryejmë operacionin r0 = Imm. Më tej, operacioni i klasës 1 JMP është BPF_JEQ (kërcim nëse është i barabartë). Në rastin tonë, që nga pak S është zero, ai krahason vlerën e regjistrit burimor me fushën Imm. Nëse vlerat përkojnë, atëherë ndodh kalimi në PC + OffKu PC, si zakonisht, përmban adresën e udhëzimit të ardhshëm. Së fundi, JMP Klasa 9 Operacioni është BPF_EXIT. Ky udhëzim përfundon programin, duke u kthyer në kernel r0. Le të shtojmë një kolonë të re në tabelën tonë:

Op    S  Class   Dst Src Off  Imm    Disassm
MOV   0  ALU64   0   0   0    1      r0 = 1
JEQ   0  JMP     0   1   1    0      if (r1 == 0) goto pc+1
MOV   0  ALU64   0   0   0    2      r0 = 2
EXIT  0  JMP     0   0   0    0      exit

Ne mund ta rishkruajmë këtë në një formë më të përshtatshme:

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

Nëse kujtojmë se çfarë është në regjistër r1 programit i kalohet një tregues në kontekst nga kerneli dhe në regjistër r0 vlera kthehet në kernel, atëherë mund të shohim se nëse treguesi në kontekst është zero, atëherë kthejmë 1, dhe përndryshe - 2. Le të kontrollojmë nëse kemi të drejtë duke parë burimin:

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

Po, është një program pa kuptim, por përkthehet në vetëm katër udhëzime të thjeshta.

Shembull përjashtimi: udhëzim 16 bajt

Ne përmendëm më herët se disa udhëzime marrin më shumë se 64 bit. Kjo vlen, për shembull, për udhëzimet lddw (Kodi = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — ngarkoni një fjalë të dyfishtë nga fushat në regjistër Imm. Fakti është se Imm ka një madhësi prej 32, dhe një fjalë e dyfishtë është 64 bit, kështu që ngarkimi i një vlere të menjëhershme 64-bit në një regjistër në një instruksion 64-bit nuk do të funksionojë. Për ta bërë këtë, përdoren dy udhëzime ngjitur 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                   ........

Ekzistojnë vetëm dy udhëzime në një program binar:

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

Ne do të takohemi përsëri me udhëzime lddw, kur flasim për zhvendosje dhe punë me harta.

Shembull: çmontimi i BPF duke përdorur 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. Sidoqoftë, vlen të thuhet se në praktikë është më i përshtatshëm dhe më i shpejtë për të çmontuar programet duke përdorur mjete standarde, për shembull:

$ llvm-objdump -d x64.o

Disassembly of section .text:

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

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

(Së pari mësova disa nga detajet e përshkruara në këtë nënseksion nga postimi Alexei Starovoitov në Blog BPF.)

Objektet BPF - programet dhe hartat - krijohen nga hapësira e përdoruesit duke përdorur komanda BPF_PROG_LOAD и BPF_MAP_CREATE thirrje sistemi bpf(2), do të flasim saktësisht se si ndodh kjo në pjesën tjetër. Kjo krijon struktura të dhënash të kernelit dhe për secilën prej tyre refcount (numri i referencës) vendoset në një dhe një përshkrues skedari që tregon objektin i kthehet përdoruesit. Pasi të mbyllet doreza refcount objekti zvogëlohet me një dhe kur arrin zero, objekti shkatërrohet.

Nëse programi përdor harta, atëherë refcount këto harta rriten me një pas ngarkimit të programit, d.m.th. përshkruesit e tyre të skedarëve mund të mbyllen nga procesi i përdoruesit dhe ende refcount nuk do të bëhet zero:

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

Pas ngarkimit me sukses të një programi, ne zakonisht e bashkojmë atë me një lloj gjeneruesi të ngjarjeve. Për shembull, ne mund ta vendosim atë në një ndërfaqe rrjeti për të përpunuar paketat hyrëse ose për ta lidhur atë me disa tracepoint në thelb. Në këtë pikë, numëruesi i referencës gjithashtu do të rritet me një dhe ne do të mund të mbyllim përshkruesin e skedarit në programin e ngarkuesit.

Çfarë ndodh nëse tani mbyllim ngarkuesin? Varet nga lloji i gjeneratorit të ngjarjeve (grepa). Të gjitha lidhjet e rrjetit do të ekzistojnë pasi të përfundojë ngarkuesi, këto janë të ashtuquajturat grepa globale. Dhe, për shembull, programet e gjurmimit do të lëshohen pasi procesi që i ka krijuar ato të përfundojë (dhe për këtë arsye quhen lokale, nga "lokal në proces"). Teknikisht, grepa lokale kanë gjithmonë një përshkrues skedari përkatës në hapësirën e përdoruesit dhe për këtë arsye mbyllen kur procesi mbyllet, por grepa globale jo. Në figurën e mëposhtme, duke përdorur kryqe të kuqe, përpiqem të tregoj se si përfundimi i programit të ngarkuesit ndikon në jetëgjatësinë e objekteve në rastin e grepave lokale dhe globale.

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

Pse ka një dallim midis grepave lokale dhe globale? Drejtimi i disa llojeve të programeve të rrjetit ka kuptim pa hapësirën e përdoruesit, për shembull, imagjinoni mbrojtjen DDoS - ngarkuesi shkruan rregullat dhe lidh programin BPF me ndërfaqen e rrjetit, pas së cilës ngarkuesi mund të shkojë dhe të vrasë veten. Nga ana tjetër, imagjinoni një program gjurmimi korrigjues që keni shkruar në gjunjë në dhjetë minuta - kur të ketë mbaruar, do të dëshironit që të mos ketë mbeturina në sistem dhe grepat lokale do ta sigurojnë këtë.

Nga ana tjetër, imagjinoni që dëshironi të lidheni me një pikë gjurmë në kernel dhe të mbledhni statistika për shumë vite. Në këtë rast, do të dëshironit të plotësoni pjesën e përdoruesit dhe t'i ktheheni statistikave herë pas here. Sistemi i skedarëve bpf e ofron këtë mundësi. Është një sistem pseudo-skedari vetëm në memorie që lejon krijimin e skedarëve që referojnë objektet BPF dhe në këtë mënyrë rriten refcount objektet. Pas kësaj, ngarkuesi mund të dalë dhe objektet që ai krijoi do të mbeten të gjalla.

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

Krijimi i skedarëve në bpffs që referojnë objektet BPF quhet "gozhdim" (si në frazën e mëposhtme: "procesi mund të pin një program ose një hartë BPF"). Krijimi i objekteve të skedarëve për objektet BPF ka kuptim jo vetëm për zgjatjen e jetës së objekteve lokale, por edhe për përdorshmërinë e objekteve globale - duke u kthyer te shembulli me programin global të mbrojtjes DDoS, ne duam të jemi në gjendje të vijmë dhe të shikojmë statistikat nga koha ne kohe.

Sistemi i skedarëve BPF zakonisht montohet brenda /sys/fs/bpf, por mund të montohet edhe në nivel lokal, për shembull, si kjo:

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

Emrat e sistemit të skedarëve krijohen duke përdorur komandën BPF_OBJ_PIN Thirrja e sistemit BPF. Për ta ilustruar, le të marrim një program, ta kompilojmë, ta ngarkojmë dhe ta lidhim bpffs. Programi ynë nuk bën asgjë të dobishme, ne po paraqesim vetëm kodin që të mund të riprodhoni shembullin:

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

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

Le ta përpilojmë këtë program dhe të krijojmë një kopje lokale të sistemit të skedarëve bpffs:

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

Tani le të shkarkojmë programin tonë duke përdorur programin bpftool dhe shikoni thirrjet shoqëruese të sistemit bpf(2) (disa linja të parëndësishme janë hequr nga dalja e shiritit):

$ 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 kemi ngarkuar programin duke përdorur BPF_PROG_LOAD, mori një përshkrues skedari nga kerneli 3 dhe duke përdorur komandën BPF_OBJ_PIN e gozhdoi këtë përshkrues skedari si skedar "bpf-mountpoint/test". Pas kësaj, programi i ngarkuesit bpftool mbaroi së punuari, por programi ynë mbeti në kernel, megjithëse nuk e lidhëm atë me ndonjë ndërfaqe rrjeti:

$ sudo bpftool prog | tail -3
783: xdp  name test  tag 5c8ba0cf164cb46c  gpl
        loaded_at 2020-05-05T13:27:08+0000  uid 0
        xlated 24B  jited 41B  memlock 4096B

Ne mund ta fshijmë objektin e skedarit normalisht 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ë sqarohet se pasi të kemi shkëputur programin nga grepa (gjeneruesi i ngjarjeve), asnjë ngjarje e re e vetme nuk do të shkaktojë nisjen e tij, megjithatë, të gjitha rastet aktuale të programit do të përfundojnë në rendin normal. .

Disa lloje të programeve BPF ju lejojnë të zëvendësoni programin në fluturim, d.m.th. sigurojnë atomicitetin e sekuencës replace = detach old program, attach new program. Në këtë rast, të gjitha rastet aktive të versionit të vjetër të programit do të përfundojnë punën e tyre, dhe mbajtës të rinj të ngjarjeve do të krijohen nga programi i ri, dhe "atomiciteti" këtu do të thotë që asnjë ngjarje e vetme nuk do të humbasë.

Bashkëngjitja e programeve me burimet e ngjarjeve

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

Manipulimi i objekteve duke përdorur thirrjen e sistemit bpf

Programet BPF

Të gjitha objektet BPF krijohen dhe menaxhohen nga hapësira e përdoruesit duke përdorur një thirrje sistemi 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 është ekipi cmd është një nga vlerat e tipit enum bpf_cmd, attr — një tregues për parametrat për një program specifik dhe size — madhësia e objektit sipas treguesit, d.m.th. zakonisht kjo sizeof(*attr). Në kernel 5.8 thirrja e sistemit bpf mbështet 34 komanda të ndryshme, dhe përcaktim union bpf_attr zë 200 rreshta. Por ne nuk duhet të frikësohemi nga kjo, pasi do të familjarizohemi me komandat dhe parametrat gjatë rrjedhës së disa artikujve.

Le të fillojmë me ekipin BPF_PROG_LOAD, i cili krijon programe BPF - merr një grup udhëzimesh BPF dhe e ngarkon atë në kernel. Në momentin e ngarkimit, lëshohet verifikuesi dhe më pas përpiluesi JIT dhe, pas ekzekutimit të suksesshëm, përshkruesi i skedarit të programit i kthehet përdoruesit. Ne pamë se çfarë ndodh me të më pas në seksionin e mëparshëm rreth ciklit jetësor të objekteve BPF.

Tani do të shkruajmë një program të personalizuar që do të ngarkojë një program të thjeshtë BPF, por së pari duhet të vendosim se çfarë lloj programi duam të ngarkojmë - do të duhet të zgjedhim një tip dhe në kuadër të këtij lloji, shkruani një program që do të kalojë testin e verifikuesit. Sidoqoftë, për të mos e komplikuar procesin, këtu është një zgjidhje e gatshme: do të marrim një program si BPF_PROG_TYPE_XDP, e cila do të kthejë vlerën XDP_PASS (kapërceni të gjitha paketat). Në montuesin BPF duket shumë e thjeshtë:

r0 = 2
exit

Pasi kemi vendosur ne do të ngarkojmë, mund t'ju 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ë një program fillojnë me përcaktimin e një grupi insns - programi ynë BPF në kodin e makinës. Në këtë rast, çdo instruksion i programit BPF paketohet në strukturë bpf_insn. Elementi i parë insns përputhet me udhëzimet r0 = 2, i dyti - exit.

Tërheqje. Kerneli përcakton makro më të përshtatshme për shkrimin e kodeve të makinës dhe përdorimin e skedarit të kokës së kernelit tools/include/linux/filter.h mund të shkruanim

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

Por meqenëse shkrimi i programeve BPF në kodin vendas është i nevojshëm vetëm për të shkruar teste në kernel dhe artikuj rreth BPF, mungesa e këtyre makrove nuk e ndërlikon vërtet jetën e zhvilluesit.

Pas përcaktimit të programit BPF, kalojmë në ngarkimin e tij në kernel. Grupi ynë minimalist i parametrave attr përfshin llojin e programit, grupin dhe numrin e udhëzimeve, licencën e kërkuar dhe emrin "woo", të cilin e përdorim për të gjetur programin tonë në sistem pas shkarkimit. Programi, siç ishte premtuar, ngarkohet në sistem duke përdorur një thirrje sistemi bpf.

Në fund të programit ne përfundojmë në një lak të pafund që simulon ngarkesën. Pa të, programi do të vritet nga kerneli kur përshkruesi i skedarit që thirrja e sistemit na u kthye të mbyllet. bpf, dhe nuk do ta shohim në sistem.

Epo, ne jemi gati për testim. Le të mbledhim dhe ekzekutojmë programin nën stracepë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_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

Cdo gje eshte ne rregull, bpf(2) na u kthye doreza 3 dhe ne hymë në një lak të pafund me pause(). Le të përpiqemi të gjejmë programin tonë në sistem. Për ta bërë këtë, ne do të shkojmë në një terminal tjetër dhe do të përdorim programin 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 që ka një program të ngarkuar në sistem woo ID globale e të cilit është 390 dhe aktualisht është në proces simple-prog ekziston një përshkrues i skedarit të hapur që tregon programin (dhe nëse simple-prog atëherë do të përfundojë punën woo do të zhduket). Siç pritej, programi woo merr 16 bajt - dy instruksione - të kodeve binare në arkitekturën BPF, por në formën e saj amtare (x86_64) është tashmë 40 bajt. Le të shohim programin tonë në formën e tij origjinale:

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

asnjë surprizë. Tani le të shohim kodin e krijuar nga përpiluesi JIT:

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

jo shumë efektive për exit(2), por me të drejtë, programi ynë është shumë i thjeshtë, dhe për programet jo të parëndësishme, sigurisht që nevojiten prologu dhe epilogu i shtuar nga përpiluesi JIT.

Maps

Programet BPF mund të përdorin zona të strukturuara të memories që janë të aksesueshme si për programet e tjera BPF ashtu edhe për programet në hapësirën e përdoruesit. Këto objekte quhen harta dhe në këtë seksion do të tregojmë se si t'i manipulojmë ato duke përdorur një thirrje sistemi bpf.

Le të themi menjëherë se aftësitë e hartave nuk kufizohen vetëm në aksesin në kujtesën e përbashkët. Ka harta për qëllime të veçanta që përmbajnë, për shembull, tregues për programet BPF ose tregues për ndërfaqet e rrjetit, harta për të punuar me ngjarjet perf, etj. Këtu nuk do të flasim për to, për të mos e ngatërruar lexuesin. Përveç kësaj, ne i shpërfillim çështjet e sinkronizimit, pasi kjo nuk është e rëndësishme për shembujt tanë. Një listë e plotë e llojeve të disponueshme të hartave mund të gjendet në <linux/bpf.h>, dhe në këtë pjesë do të marrim si shembull llojin e parë historikisht, tabelën hash BPF_MAP_TYPE_HASH.

Nëse krijoni një tabelë hash në, të themi, C++, do të thoni unordered_map<int,long> woo, që në rusisht do të thotë "Kam nevojë për një tryezë woo madhësi të pakufizuar, çelësat e të cilit janë të tipit int, dhe vlerat janë lloji long" Për të krijuar një tabelë hash BPF, duhet të bëjmë pothuajse të njëjtën gjë, përveç se duhet të specifikojmë madhësinë maksimale të tabelës, dhe në vend që të specifikojmë llojet e çelësave dhe vlerave, duhet të specifikojmë madhësitë e tyre në bajt. . Për të krijuar harta përdorni komandën BPF_MAP_CREATE thirrje sistemi bpf. Le të shohim një program pak a shumë minimal që krijon një hartë. 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 <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

int main(void)
{
    union bpf_attr attr = {
        .map_type = BPF_MAP_TYPE_HASH,
        .key_size = sizeof(int),
        .value_size = sizeof(int),
        .max_entries = 4,
    };
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

Këtu ne përcaktojmë një grup parametrash attr, në të cilën themi “Kam nevojë për një tabelë hash me çelësa dhe vlera të madhësisë sizeof(int), në të cilin mund të vendos maksimumi katër elementë." Kur krijoni harta BPF, mund të specifikoni parametra të tjerë, për shembull, në të njëjtën mënyrë si në shembullin me programin, ne specifikuam emrin e objektit si "woo".

Le të përpilojmë 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 e sistemit bpf(2) na ktheu numrin e hartës përshkruese 3 dhe më pas programi, siç pritej, pret për udhëzime të mëtejshme në thirrjen e sistemit pause(2).

Tani le ta dërgojmë programin tonë në sfond ose të hapim një terminal tjetër dhe të shikojmë objektin tonë duke përdorur programin bpftool (ne mund ta dallojmë hartën tonë nga të tjerët me emrin e saj):

$ sudo bpftool map
...
114: hash  name woo  flags 0x0
        key 4B  value 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 duke përdorur komandën BPF_MAP_GET_FD_BY_ID thirrje sistemi bpf.

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

$ sudo bpftool map dump id 114
Found 0 elements

Bosh. 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 përsëri tabelën:

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

Hora! Ne arritëm të shtonim një element. Vini re se ne duhet të punojmë në nivelin e bajtit për ta bërë këtë, pasi bptftool nuk e di se çfarë lloji janë vlerat në tabelën hash. (Kjo njohuri mund t'i transferohet asaj duke përdorur BTF, por më shumë për këtë tani.)

Si saktësisht lexon dhe shton elemente bpftool? Le të hedhim një vështrim nën kapuç:

$ sudo strace -e bpf bpftool map dump id 114
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=NULL, next_key=0x55856ab65280}, 120) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=3, key=0x55856ab65280, value=0x55856ab652a0}, 120) = 0
key: 01 00 00 00  value: 01 00 00 00
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=0x55856ab65280, next_key=0x55856ab65280}, 120) = -1 ENOENT

Fillimisht hapëm hartën me ID-në e saj globale duke përdorur komandën BPF_MAP_GET_FD_BY_ID и bpf(2) na e ktheu përshkruesin 3. Më tej duke përdorur komandën BPF_MAP_GET_NEXT_KEY çelësin e parë në tabelë e gjetëm duke kaluar NULL si tregues për tastin "e mëparshme". Nëse kemi çelësin, mund ta bëjmë BPF_MAP_LOOKUP_ELEMi cili i kthen një vlerë një treguesi value. Hapi tjetër është që ne përpiqemi të gjejmë elementin tjetër duke kaluar një tregues te çelësi aktual, por tabela jonë përmban vetëm një element dhe komandën BPF_MAP_GET_NEXT_KEY kthehet ENOENT.

Mirë, le të ndryshojmë vlerën me çelësin 1, le të themi se logjika jonë e biznesit kërkon regjistrimin 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 thjeshtë: komanda BPF_MAP_GET_FD_BY_ID hap hartën tonë me ID, dhe komandën BPF_MAP_UPDATE_ELEM mbishkruan elementin.

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

  • BPF_MAP_LOOKUP_ELEM: gjeni vlerë për çelës
  • BPF_MAP_UPDATE_ELEM: përditëso/krijo vlerë
  • BPF_MAP_DELETE_ELEM: hiqni çelësin
  • BPF_MAP_GET_NEXT_KEY: gjeni çelësin tjetër (ose të parën).
  • BPF_MAP_GET_NEXT_ID: ju lejon të kaloni nëpër të gjitha hartat ekzistuese, kështu funksionon bpftool map
  • BPF_MAP_GET_FD_BY_ID: hapni një hartë ekzistuese me ID-në e saj globale
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: përditësoni në mënyrë atomike vlerën e një objekti dhe ktheni atë të vjetër
  • BPF_MAP_FREEZE: bëje hartën të pandryshueshme nga hapësira e përdoruesit (ky operacion nuk mund të zhbëhet)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: operacionet masive. Për shembull, BPF_MAP_LOOKUP_AND_DELETE_BATCH - kjo është mënyra e vetme e besueshme për të lexuar dhe rivendosur të gjitha vlerat nga harta

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

Për hir të rendit, le të përfundojmë eksperimentet tona të tabelës hash. Mos harroni se kemi krijuar një tabelë që mund të përmbajë deri në katër çelësa? Le të shtojmë disa elementë të tjerë:

$ 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

Deri këtu 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
Found 4 elements

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

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

Siç pritej, nuk ia dolëm. Le ta shohim gabimin në mënyrë më të detajuar:

$ sudo strace -e bpf bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=80, info=0x7ffe6c626da0}}, 120) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x56049ded5260, value=0x56049ded5280, flags=BPF_ANY}, 120) = -1 E2BIG (Argument list too long)
Error: update failed: Argument list too long
+++ exited with 255 +++

Gjithçka është në rregull: siç pritej, ekipi BPF_MAP_UPDATE_ELEM përpiqet të krijojë një çelës të ri, të pestë, por rrëzohet E2BIG.

Pra, ne mund të krijojmë dhe ngarkojmë programe BPF, si dhe të krijojmë dhe menaxhojmë harta nga hapësira e përdoruesit. Tani është logjike të shohim se si mund të përdorim hartat nga vetë programet BPF. Ne mund të flasim për këtë në gjuhën e programeve të vështirë për t'u lexuar në kodet makro të makinerive, por në fakt ka ardhur koha për të treguar se si programet BPF janë shkruar dhe mirëmbahen 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ë analizojmë në detaje programet që përdorin harta dhe funksione ndihmëse të krijuara duke përdorur libbpf dhe t'ju tregojë se çfarë ndodh në nivelin e udhëzimit. Për lexuesit që janë të pakënaqur shumë, shtuam ne shembull në vendin e duhur në artikull.)

Shkrimi i programeve BPF duke përdorur libbpf

Shkrimi i programeve BPF duke përdorur kodet e makinës mund të jetë interesant vetëm herën e parë, dhe më pas fillon ngopja. Në këtë moment duhet të përqendroni vëmendjen tuaj llvm, i cili ka një backend për gjenerimin e kodit për arkitekturën BPF, si dhe një bibliotekë libbpf, i cili ju lejon të shkruani anën e përdoruesit të aplikacioneve BPF dhe të ngarkoni kodin e programeve BPF të krijuara duke përdorur llvm/clang.

Në fakt, siç do të shohim në këtë dhe artikujt pasues, libbpf bën shumë punë pa të (ose mjete të ngjashme - iproute2, libbcc, libbpf-go, etj.) është e pamundur të jetosh. Një nga tiparet vrasëse të projektit libbpf është BPF CO-RE (Compile Once, Run Everywhere) - një projekt që ju lejon të shkruani programe BPF që janë të lëvizshme nga një kernel në tjetrin, me aftësinë për të ekzekutuar në API të ndryshme (për shembull, kur struktura e kernelit ndryshon nga versioni në version). Për të qenë në gjendje të punoni me CO-RE, kerneli juaj duhet të përpilohet me mbështetje BTF (ne përshkruajmë se si ta bëjmë këtë në seksion Mjetet e Zhvillimit. Ju mund të kontrolloni nëse kerneli juaj është ndërtuar me BTF apo jo shumë thjesht - me praninë e skedarit të mëposhtëm:

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

Ky skedar ruan informacione për të gjitha llojet e të dhënave të përdorura në kernel dhe përdoret në të gjithë shembujt tanë duke përdorur libbpf. Ne do të flasim në detaje për CO-RE në artikullin tjetër, por në këtë - thjesht ndërtoni vetes një kernel me CONFIG_DEBUG_INFO_BTF.

Bibliotekë libbpf jeton pikërisht në drejtori tools/lib/bpf kerneli dhe zhvillimi i tij kryhet përmes listës së postimeve [email protected]. Megjithatë, një depo e veçantë mbahet për nevojat e aplikacioneve që jetojnë jashtë kernelit https://github.com/libbpf/libbpf në të cilën biblioteka e kernelit pasqyrohet për qasje në lexim pak a shumë siç është.

Në këtë seksion do të shikojmë se si mund të krijoni një projekt që përdor libbpf, le të shkruajmë disa programe testimi (pak a shumë të pakuptimta) dhe të analizojmë në detaje se si funksionon gjithçka. Kjo do të na lejojë të shpjegojmë më lehtë në seksionet e mëposhtme saktësisht se si programet BPF ndërveprojnë me hartat, ndihmësit e kernelit, BTF, etj.

Në mënyrë tipike projektet duke përdorur libbpf shtoni një depo GitHub si një nënmodul git, ne do të bëjmë të njëjtën gjë:

$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Initialized empty Git repository in /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Cloning into '/tmp/libbpf-example/libbpf'...
remote: Enumerating objects: 200, done.
remote: Counting objects: 100% (200/200), done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 3354 (delta 101), reused 118 (delta 79), pack-reused 3154
Receiving objects: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, done.
Resolving deltas: 100% (2176/2176), done.

Duke shkuar në libbpf shumë e thjeshtë:

$ cd libbpf/src
$ mkdir build
$ OBJDIR=build DESTDIR=root make -s install
$ find root
root
root/usr
root/usr/include
root/usr/include/bpf
root/usr/include/bpf/bpf_tracing.h
root/usr/include/bpf/xsk.h
root/usr/include/bpf/libbpf_common.h
root/usr/include/bpf/bpf_endian.h
root/usr/include/bpf/bpf_helpers.h
root/usr/include/bpf/btf.h
root/usr/include/bpf/bpf_helper_defs.h
root/usr/include/bpf/bpf.h
root/usr/include/bpf/libbpf_util.h
root/usr/include/bpf/libbpf.h
root/usr/include/bpf/bpf_core_read.h
root/usr/lib64
root/usr/lib64/libbpf.so.0.1.0
root/usr/lib64/libbpf.so.0
root/usr/lib64/libbpf.a
root/usr/lib64/libbpf.so
root/usr/lib64/pkgconfig
root/usr/lib64/pkgconfig/libbpf.pc

Plani ynë i ardhshëm në këtë seksion është si vijon: ne do të shkruajmë një program BPF si BPF_PROG_TYPE_XDP, njësoj si në shembullin e mëparshëm, por në C, ne e përpilojmë atë duke përdorur clang, dhe shkruani një program ndihmës që do ta ngarkojë atë në kernel. Në seksionet e mëposhtme do të zgjerojmë aftësitë e programit BPF dhe programit të asistentit.

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

Për të filluar, ne përdorim skedarin /sys/kernel/btf/vmlinux, i cili u përmend më lart, dhe krijoni ekuivalentin e tij në formën e një skedari kokë:

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

Ky skedar do të ruajë të gjitha strukturat e të dhënave të disponueshme në kernelin tonë, për shembull, kjo është mënyra se si përkufizohet titulli IPv4 në kernel:

$ 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ë C:

$ cat xdp-simple.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

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

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

Edhe pse programi ynë doli të ishte shumë i thjeshtë, ne ende duhet t'i kushtojmë vëmendje shumë detajeve. Së pari, skedari i parë i kokës që përfshijmë është vmlinux.h, të cilin sapo e krijuam duke përdorur bpftool btf dump - tani nuk kemi nevojë të instalojmë paketën kernel-headers për të zbuluar se si duken strukturat e kernelit. Skedari i mëposhtëm i kokës na vjen nga biblioteka libbpf. Tani na duhet vetëm për të përcaktuar makro SEC, i cili dërgon karakterin në seksionin përkatës të skedarit të objektit ELF. Programi ynë është i përfshirë në seksion xdp/simple, ku para vijës së pjerrët përcaktojmë llojin e programit BPF - kjo është konventa e përdorur në libbpf, bazuar në emrin e seksionit do të zëvendësojë llojin e saktë në fillim bpf(2). Vetë programi BPF është C - shumë e thjeshtë dhe përbëhet nga një rresht return XDP_PASS. Së fundi, një seksion i veçantë "license" përmban emrin e licencës.

Ne mund ta përpilojmë programin tonë duke përdorur llvm/clang, version >= 10.0.0, ose më mirë akoma, më i madh (shih seksionin Mjetet e Zhvillimit):

$ clang --version
clang version 11.0.0 (https://github.com/llvm/llvm-project.git afc287e0abec710398465ee1f86237513f2b5091)
...

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o

Ndër veçoritë interesante: ne tregojmë arkitekturën e synuar -target bpf dhe shtegu drejt kokave libbpf, të cilin e kemi instaluar së fundmi. Gjithashtu, mos harroni për -O2, pa këtë opsion mund të keni surpriza në të ardhmen. Le të shohim kodin tonë, a arritëm të shkruanim programin që dëshironim?

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

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

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       r0 = 2
       1:       exit

Po, funksionoi! Tani, ne kemi një skedar binar me programin dhe duam të krijojmë një aplikacion që do ta ngarkojë atë në kernel. Për këtë qëllim biblioteka libbpf na ofron dy opsione - përdorni një API të nivelit më të ulët ose një API të nivelit më të lartë. Ne do të shkojmë në rrugën e dytë, pasi duam të mësojmë se si të shkruajmë, ngarkojmë dhe lidhim programet BPF me përpjekje minimale për studimin e tyre të mëvonshëm.

Së pari, ne duhet të gjenerojmë "skeletin" e programit tonë nga binari i tij duke përdorur të njëjtin mjet bpftool - thika zvicerane e botës BPF (e cila mund të merret fjalë për fjalë, pasi Daniel Borkman, një nga krijuesit dhe mirëmbajtësit e BPF, është zviceran):

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

Në dosje xdp-simple.skel.h përmban kodin binar të programit tonë dhe funksionet për menaxhimin - ngarkimin, bashkëngjitjen, fshirjen e objektit tonë. Në rastin tonë të thjeshtë kjo duket si e tepërt, por funksionon edhe në rastin kur skedari i objektit përmban shumë programe dhe harta BPF dhe për të ngarkuar këtë ELF gjigant, ne vetëm duhet të gjenerojmë skeletin dhe të thërrasim një ose dy funksione nga aplikacioni i personalizuar që ne. po shkruajnë Le të vazhdojmë tani.

Në mënyrë rigoroze, programi ynë ngarkues është i parëndësishëm:

#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 të përcaktuara në dosje xdp-simple.skel.h dhe përshkruan skedarin tonë të objektit:

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ë shohim gjurmë të një API të nivelit të ulët: strukturën struct bpf_program *simple и struct bpf_link *simple. Struktura e parë përshkruan në mënyrë specifike programin tonë, të shkruar në seksion xdp/simple, dhe e dyta përshkruan se si programi lidhet me burimin e ngjarjes.

Funksion xdp_simple_bpf__open_and_load, hap një objekt ELF, e analizon atë, krijon të gjitha strukturat dhe nënstrukturat (përveç programit, ELF përmban edhe seksione të tjera - të dhëna, të dhëna vetëm për lexim, informacion korrigjimi, licencë, etj.), dhe më pas e ngarkon atë në kernel duke përdorur një sistem telefononi bpf, të cilin mund ta kontrollojmë duke përpiluar 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

Le të shohim tani programin tonë duke përdorur bpftool. Le të gjejmë ID-në e saj:

# 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 dump (ne përdorim një formë të 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ë burim C. Kjo u bë nga biblioteka libbpf, e cila gjeti seksionin e korrigjimit në binar, e përpiloi atë në një objekt BTF, e ngarkoi atë në kernel duke përdorur BPF_BTF_LOAD, dhe më pas specifikoi përshkruesin e skedarit që rezulton kur ngarkoni programin me komandën BPG_PROG_LOAD.

Ndihmuesit e kernelit

Programet BPF mund të ekzekutojnë funksione "të jashtme" - ndihmësit e kernelit. Këto funksione ndihmëse i lejojnë programet BPF të kenë qasje në strukturat e kernelit, të menaxhojnë hartat dhe gjithashtu të komunikojnë me "botën reale" - të krijojnë ngjarje perf, të kontrollojnë pajisjen (për shembull, ridrejtimin e paketave), etj.

Shembull: bpf_get_smp_processor_id

Në kuadrin e paradigmës "të mësuarit me shembull", le të shqyrtojmë një nga funksionet ndihmëse, bpf_get_smp_processor_id(), të caktuara në dosje kernel/bpf/helpers.c. Ai kthen numrin e procesorit në të cilin funksionon programi BPF që e ka thirrur. Por ne nuk jemi aq të interesuar për semantikën e tij sa për faktin që zbatimi i tij merr një rresht:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Përkufizimet e funksionit ndihmës BPF janë të ngjashme me përkufizimet e thirrjeve të sistemit Linux. Këtu, për shembull, përcaktohet një funksion që nuk ka argumente. (Një funksion që merr, të themi, tre argumente përcaktohet duke përdorur makro BPF_CALL_3. Numri maksimal i argumenteve është pesë.) Megjithatë, kjo është vetëm pjesa e parë e përkufizimit. Pjesa e dytë është përcaktimi i strukturës së tipit struct bpf_func_proto, i cili përmban një përshkrim të funksionit ndihmës që verifikuesi kupton:

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

Regjistrimi i funksioneve ndihmëse

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

static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
    switch (func_id) {
    ...
    case BPF_FUNC_get_smp_processor_id:
        return &bpf_get_smp_processor_id_proto;
    ...
    }
}

Llojet e reja të programit BPF "përcaktohen" në skedar include/linux/bpf_types.h duke përdorur një makro BPF_PROG_TYPE. Përkufizohet në thonjëza sepse është një përkufizim logjik, dhe në termat e gjuhës C përkufizimi i një grupi të tërë strukturash konkrete ndodh në vende të tjera. Në veçanti, në dosje kernel/bpf/verifier.c të gjitha përkufizimet nga skedari bpf_types.h përdoren për të krijuar një sërë strukturash bpf_verifier_ops[]:

static const struct bpf_verifier_ops *const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) 
    [_id] = & _name ## _verifier_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
};

Kjo do të thotë, për çdo lloj programi BPF, përcaktohet një tregues për një strukturë të dhënash të llojit struct bpf_verifier_ops, e cila inicializohet me vlerën _name ## _verifier_ops, d.m.th. xdp_verifier_ops për xdp. Struktura xdp_verifier_ops percaktuar nga në dosje net/core/filter.c si më poshtë:

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 shohim funksionin tonë të njohur xdp_func_proto, i cili do të ekzekutojë verifikuesin sa herë që has një sfidë një lloj funksionet brenda një programi BPF, shih verifier.c.

Le të shohim se si një program hipotetik BPF e përdor funksionin bpf_get_smp_processor_id. Për ta bërë këtë, ne e 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";

Simbol bpf_get_smp_processor_id percaktuar nga в <bpf/bpf_helper_defs.h> bibliotekë libbpf si

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

kjo eshte, bpf_get_smp_processor_id është një tregues funksioni vlera e të cilit është 8, ku 8 është vlera BPF_FUNC_get_smp_processor_id lloj enum bpf_fun_id, e cila është e përcaktuar për ne në dosje vmlinux.h (skedarë bpf_helper_defs.h në kernel gjenerohet nga një skript, kështu që numrat "magjikë" janë në rregull). Ky funksion nuk merr argumente dhe kthen një vlerë të llojit __u32. Kur e ekzekutojmë në programin tonë, clang gjeneron një udhëzim BPF_CALL "Lloji i duhur" Le të përpilojmë 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:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       bf 01 00 00 00 00 00 00 r1 = r0
       2:       67 01 00 00 20 00 00 00 r1 <<= 32
       3:       77 01 00 00 20 00 00 00 r1 >>= 32
       4:       b7 00 00 00 02 00 00 00 r0 = 2
       5:       15 01 01 00 00 00 00 00 if r1 == 0 goto +1 <LBB0_2>
       6:       b7 00 00 00 01 00 00 00 r0 = 1

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

Në rreshtin e parë shohim udhëzime call, parametri IMM që është e barabartë me 8, dhe SRC_REG - zero. Sipas marrëveshjes ABI të përdorur nga verifikuesi, kjo është një thirrje për funksionin ndihmës numër tetë. Pasi është nisur, logjika është e thjeshtë. Ktheni vlerën nga regjistri r0 kopjuar në r1 dhe në rreshtat 2,3 shndërrohet në tip u32 - 32 bitet e sipërme janë pastruar. Në rreshtat 4,5,6,7 kthejmë 2 (XDP_PASS) ose 1 (XDP_DROP) në varësi të faktit nëse funksioni ndihmës nga rreshti 0 ktheu një vlerë zero ose jo zero.

Le të testojmë veten: ngarkoni programin dhe shikoni daljen bpftool prog dump xlated:

$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914

$ sudo bpftool p | grep simple
523: xdp  name simple  tag 44c38a10c657e1b0  gpl
        pids xdp-simple(10915)

$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
   0: (85) call bpf_get_smp_processor_id#114128
   1: (bf) r1 = r0
   2: (67) r1 <<= 32
   3: (77) r1 >>= 32
   4: (b7) r0 = 2
; }
   5: (15) if r1 == 0x0 goto pc+1
   6: (b7) r0 = 1
   7: (95) exit

Në rregull, verifikuesi gjeti ndihmësin e duhur të kernelit.

Shembull: kalimi i argumenteve dhe në fund ekzekutimi i programit!

Të gjitha funksionet ndihmëse të nivelit të ekzekutimit kanë një prototip

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

Parametrat për funksionet ndihmëse kalohen në regjistra r1-r5, dhe vlera kthehet në regjistër r0. Nuk ka funksione që marrin më shumë se pesë argumente dhe mbështetja për to nuk pritet të shtohet në të ardhmen.

Le të hedhim një vështrim në ndihmësin e ri të kernelit dhe se si BPF kalon parametrat. Le të rishkruajmë xdp-simple.bpf.c si më poshtë (pjesa tjetër e rreshtave nuk kanë 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ë cilën po funksionon. Le ta përpilojmë atë dhe të shohim kodin:

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

0000000000000000 <simple>:
       0:       r1 = 10
       1:       *(u16 *)(r10 - 8) = r1
       2:       r1 = 8441246879787806319 ll
       4:       *(u64 *)(r10 - 16) = r1
       5:       r1 = 2334956330918245746 ll
       7:       *(u64 *)(r10 - 24) = r1
       8:       call 8
       9:       r1 = r10
      10:       r1 += -24
      11:       r2 = 18
      12:       r3 = r0
      13:       call 6
      14:       r0 = 2
      15:       exit

Në rreshtat 0-7 shkruajmë vargun running on CPU%un, dhe më pas në rreshtin 8 ekzekutojmë atë të njohur bpf_get_smp_processor_id. Në rreshtat 9-12 ne përgatisim argumentet ndihmëse bpf_printk - regjistrat r1, r2, r3. Pse janë tre prej tyre dhe jo dy? Sepse bpf_printkky është një mbështjellës makro rreth ndihmësit të vërtetë bpf_trace_printk, e cila duhet të kalojë madhësinë e vargut të formatit.

Tani le t'i shtojmë disa rreshta xdp-simple.cnë mënyrë që programi ynë të lidhet me ndërfaqen lo dhe me të vërtetë filloi!

$ cat xdp-simple.c
#include <linux/if_link.h>
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    __u32 flags = XDP_FLAGS_SKB_MODE;
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    bpf_set_link_xdp_fd(1, -1, flags);
    bpf_set_link_xdp_fd(1, bpf_program__fd(obj->progs.simple), flags);

cleanup:
    xdp_simple_bpf__destroy(obj);
}

Këtu përdorim funksionin bpf_set_link_xdp_fd, i cili lidh programet BPF të tipit XDP me ndërfaqet e rrjetit. Ne kemi koduar numrin e ndërfaqes lo, që është gjithmonë 1. Ne e ekzekutojmë funksionin dy herë për të shkëputur fillimisht programin e vjetër nëse ai ishte i bashkangjitur. Vini re se tani nuk kemi nevojë për një sfidë pause ose një lak i pafund: programi ynë ngarkues do të dalë, por programi BPF nuk do të mbyllet pasi është i lidhur me burimin e ngjarjes. Pas shkarkimit dhe lidhjes së suksesshme, programi do të hapet për secilën paketë të rrjetit që arrin lo.

Le të shkarkojmë programin dhe të shikojmë ndërfaqen lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  name simple  tag 4fca62e77ccb43d6  gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

Programi që kemi shkarkuar ka ID 669 dhe shohim të njëjtën ID në ndërfaqe lo. Do të dërgojmë disa pako tek 127.0.0.1 (kërkesë + përgjigje):

$ ping -c1 localhost

dhe tani le të shohim përmbajtjen e skedarit virtual të korrigjimit /sys/kernel/debug/tracing/trace_pipe, në të cilën 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 pako u vunë re lo dhe i përpunuar në CPU0 - programi ynë i parë i plotë dhe i pakuptimtë BPF funksionoi!

Vlen të theksohet se bpf_printk Nuk është më kot që ai shkruan në skedarin e korrigjimit: ky nuk është ndihmësi më i suksesshëm për përdorim në prodhim, por qëllimi ynë ishte të tregonim diçka të thjeshtë.

Qasja në harta nga programet BPF

Shembull: duke përdorur një hartë nga programi BPF

Në seksionet e mëparshme mësuam se si të krijojmë dhe përdorim harta nga hapësira e përdoruesit, dhe tani le të shohim pjesën e kernelit. Le të fillojmë, si zakonisht, me një shembull. Le të rishkruajmë programin tonë xdp-simple.bpf.c si më poshtë:

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

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 8);
    __type(key, u32);
    __type(value, u64);
} woo SEC(".maps");

SEC("xdp/simple")
int simple(void *ctx)
{
    u32 key = bpf_get_smp_processor_id();
    u32 *val;

    val = bpf_map_lookup_elem(&woo, &key);
    if (!val)
        return XDP_ABORTED;

    *val += 1;

    return XDP_PASS;
}

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

Në fillim të programit shtuam një përkufizim harte woo: Ky është një grup me 8 elementë që ruan vlera si u64 (në C do të përcaktonim një grup të tillë si u64 woo[8]). Në një program "xdp/simple" ne marrim numrin aktual të procesorit në një ndryshore key dhe më pas duke përdorur funksionin ndihmës bpf_map_lookup_element marrim një tregues për hyrjen përkatëse në grup, të cilin e rrisim me një. Përkthyer në Rusisht: ne llogarisim statistikat mbi të cilat CPU përpunoi paketat hyrëse. 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ë e lidhur me lo dhe dërgoni disa pako:

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

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

Tani le të shohim përmbajtjen e grupit:

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

Pothuajse të gjitha proceset u përpunuan në CPU7. Kjo nuk është e rëndësishme për ne, gjëja kryesore është që programi funksionon dhe ne kuptojmë se si të aksesojmë hartat nga programet BPF - duke përdorur хелперов bpf_mp_*.

Indeksi mistik

Pra, ne mund të aksesojmë hartën nga programi BPF duke përdorur thirrje si

val = bpf_map_lookup_elem(&woo, &key);

ku duket funksioni ndihmës

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

por ne po kalojmë një tregues &woo në një strukturë pa emër struct { ... }...

Nëse shikojmë asemblerin e programit, shohim se vlera &woo në fakt nuk është përcaktuar (rreshti 4):

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

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

Disassembly of section xdp/simple:

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

dhe përfshihet në zhvendosjet:

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

Relocation section '.relxdp/simple' at offset 0xe18 contains 1 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name
0000000000000020  0000002700000001 R_BPF_64_64            0000000000000000 woo

Por nëse shikojmë programin e ngarkuar tashmë, shohim një tregues në hartën e saktë (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, mund të konkludojmë se në kohën e nisjes së programit tonë ngarkues, lidhja me &woo u zëvendësua nga diçka me një bibliotekë libbpf. Së pari do të shikojmë rezultatin strace:

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=8, max_entries=8, map_name="woo", ...}, 120) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="simple", ...}, 120) = 5

Ne e shohim atë libbpf krijoi një hartë woo dhe më pas shkarkoi programin tonë simple. Le të hedhim një vështrim më të afërt se si e ngarkojmë programin:

  • telefononi xdp_simple_bpf__open_and_load nga skedari xdp-simple.skel.h
  • që shkakton xdp_simple_bpf__load nga skedari xdp-simple.skel.h
  • që shkakton bpf_object__load_skeleton nga skedari libbpf/src/libbpf.c
  • që shkakton bpf_object__load_xattr nga libbpf/src/libbpf.c

Funksioni i fundit, ndër të tjera, do të thërrasë bpf_object__create_maps, i cili krijon ose hap hartat ekzistuese, duke i kthyer ato në përshkrues skedarësh. (Kjo është ajo ku ne shohim BPF_MAP_CREATE në dalje strace.) Më pas thirret funksioni bpf_object__relocate dhe është ajo që na intereson, pasi kujtojmë atë që pamë woo në tabelën e zhvendosjes. Duke e eksploruar atë, ne përfundimisht e gjejmë veten në funksion bpf_program__relocate, e cila merret me zhvendosjet e hartave:

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

Pra, ne marrim udhëzimet tona

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

dhe zëvendësoni regjistrin burimor në të me BPF_PSEUDO_MAP_FD, dhe IMM-ja e parë në përshkruesin e skedarit të hartës sonë dhe, nëse është e barabartë me, për shembull, 0xdeadbeef, atëherë si rezultat do të marrim udhëzimin

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

Kjo është mënyra se si informacioni i hartës transferohet në një program specifik të ngarkuar BPF. Në këtë rast, harta mund të krijohet duke përdorur BPF_MAP_CREATE, dhe u hap me ID duke përdorur BPF_MAP_GET_FD_BY_ID.

Totali, kur përdoret libbpf algoritmi është si më poshtë:

  • gjatë përpilimit krijohen të dhëna në tabelën e zhvendosjes për lidhjet me hartat
  • libbpf hap librin e objekteve ELF, gjen të gjitha hartat e përdorura dhe krijon përshkruesit e skedarëve për to
  • përshkruesit e skedarëve ngarkohen në kernel si pjesë e udhëzimit LD64

Siç mund ta imagjinoni, ka më shumë për të ardhur dhe ne do të duhet të shqyrtojmë thelbin. Për fat të mirë, ne kemi një të dhënë - ne kemi shkruar kuptimin BPF_PSEUDO_MAP_FD në regjistrin e burimit dhe ne mund ta varrosim, gjë që do të na çojë në të shenjtën e të gjithë shenjtorëve - kernel/bpf/verifier.c, ku një funksion me një emër të veçantë zëvendëson një përshkrues skedari me adresën e një strukture të llojit 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;

(mund të gjendet kodi i plotë по ссылке). Kështu që ne mund të zgjerojmë algoritmin tonë:

  • gjatë ngarkimit të programit, verifikuesi kontrollon përdorimin e saktë të hartës dhe shkruan adresën e strukturës përkatëse struct bpf_map

Kur shkarkoni binarin ELF duke përdorur libbpf Ka shumë më tepër që po ndodhin, por ne do ta diskutojmë këtë në artikuj të tjerë.

Ngarkimi i programeve dhe hartave pa libbpf

Siç u premtua, këtu është një shembull për lexuesit që duan të dinë se si të krijojnë dhe ngarkojnë një program që përdor harta, pa ndihmë libbpf. Kjo mund të jetë e dobishme kur jeni duke punuar në një mjedis për të cilin nuk mund të krijoni varësi, ose kur ruani çdo pjesë, ose kur shkruani një program si p.sh. ply, i cili gjeneron kodin binar BPF në fluturim.

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

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

  • krijoni një hartë të tipit BPF_MAP_TYPE_ARRAY duke përdorur komandën BPF_MAP_CREATE,
  • krijoni një program që përdor këtë hartë,
  • lidhni programin me ndërfaqen lo,

që përkthehet në njeriun 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ë në të njëjtën mënyrë siç bëmë në shembullin e parë për thirrjen e sistemit bpf - “kernel, ju lutem më bëni një hartë të re në formën e një grupi prej 8 elementësh si __u64 dhe më kthe përshkruesin e skedarit":

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 është gjithashtu i lehtë për t'u ngarkuar:

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 ndërlikuar prog_load është përkufizimi i programit tonë BPF si një grup strukturash struct bpf_insn insns[]. Por meqenëse po përdorim një program që kemi në C, mund të mashtrojmë pak:

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

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
       7:       b7 01 00 00 00 00 00 00 r1 = 0
       8:       15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2>
       9:       61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0)
      10:       07 01 00 00 01 00 00 00 r1 += 1
      11:       63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1
      12:       b7 01 00 00 02 00 00 00 r1 = 2

0000000000000068 <LBB0_2>:
      13:       bf 10 00 00 00 00 00 00 r0 = r1
      14:       95 00 00 00 00 00 00 00 exit

Në total, ne duhet të shkruajmë 14 udhëzime në formën e strukturave si struct bpf_insn (këshilla: merrni deponinë nga lart, rilexoni seksionin e udhëzimeve, hapeni linux/bpf.h и linux/bpf_common.h dhe përpiquni të përcaktoni struct bpf_insn insns[] vetë):

struct bpf_insn insns[] = {
    /* 85 00 00 00 08 00 00 00 call 8 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 8,
    },

    /* 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 */
    {
        .code = BPF_MEM | BPF_STX,
        .off = -4,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_10,
    },

    /* bf a2 00 00 00 00 00 00 r2 = r10 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_10,
        .dst_reg = BPF_REG_2,
    },

    /* 07 02 00 00 fc ff ff ff r2 += -4 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_2,
        .imm = -4,
    },

    /* 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll */
    {
        .code = BPF_LD | BPF_DW | BPF_IMM,
        .src_reg = BPF_PSEUDO_MAP_FD,
        .dst_reg = BPF_REG_1,
        .imm = map_fd,
    },
    { }, /* placeholder */

    /* 85 00 00 00 01 00 00 00 call 1 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 1,
    },

    /* b7 01 00 00 00 00 00 00 r1 = 0 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 0,
    },

    /* 15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2> */
    {
        .code = BPF_JMP | BPF_JEQ | BPF_K,
        .off = 4,
        .src_reg = BPF_REG_0,
        .imm = 0,
    },

    /* 61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0) */
    {
        .code = BPF_MEM | BPF_LDX,
        .off = 0,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_1,
    },

    /* 07 01 00 00 01 00 00 00 r1 += 1 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 1,
    },

    /* 63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1 */
    {
        .code = BPF_MEM | BPF_STX,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* b7 01 00 00 02 00 00 00 r1 = 2 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 2,
    },

    /* <LBB0_2>: bf 10 00 00 00 00 00 00 r0 = r1 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* 95 00 00 00 00 00 00 00 exit */
    {
        .code = BPF_JMP | BPF_EXIT
    },
};

Një ushtrim për ata që nuk e kanë shkruar vetë këtë - gjeni map_fd.

Në programin tonë ka mbetur edhe një pjesë e pazbuluar - xdp_attach. Fatkeqësisht, programe si XDP nuk mund të lidhen duke përdorur një thirrje sistemi bpf. Njerëzit që krijuan BPF dhe XDP ishin nga komuniteti online Linux, që do të thotë se ata përdorën atë më të njohurin për ta (por jo normale njerëzit) ndërfaqe për ndërveprim me kernelin: prizat netlink, Shiko gjithashtu RFC3549. Mënyra më e thjeshtë për të zbatuar xdp_attach po kopjon kodin nga libbpf, domethënë, nga skedari netlink.c, që është ajo që bëmë, duke e shkurtuar pak:

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

Hapni një lloj foleje netlink 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;
}

Ne lexojmë nga kjo fole:

static int bpf_netlink_recv(int sock, __u32 nl_pid, int seq)
{
    bool multipart = true;
    struct nlmsgerr *errm;
    struct nlmsghdr *nh;
    char buf[4096];
    int len, ret;

    while (multipart) {
        multipart = false;
        len = recv(sock, buf, sizeof(buf), 0);
        if (len < 0)
            err(1, "recv");

        if (len == 0)
            break;

        for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);
                nh = NLMSG_NEXT(nh, len)) {
            if (nh->nlmsg_pid != nl_pid)
                errx(1, "wrong pid");
            if (nh->nlmsg_seq != seq)
                errx(1, "INVSEQ");
            if (nh->nlmsg_flags & NLM_F_MULTI)
                multipart = true;
            switch (nh->nlmsg_type) {
                case NLMSG_ERROR:
                    errm = (struct nlmsgerr *)NLMSG_DATA(nh);
                    if (!errm->error)
                        continue;
                    ret = errm->error;
                    // libbpf_nla_dump_errormsg(nh); too many code to copy...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

Më në fund, këtu është funksioni ynë që hap një fole dhe i dërgon një mesazh të veçantë që përmban një përshkrues 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 < 0)
        return sock;

    memset(&req, 0, sizeof(req));
    req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
    req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    req.nh.nlmsg_type = RTM_SETLINK;
    req.nh.nlmsg_pid = 0;
    req.nh.nlmsg_seq = ++seq;
    req.ifinfo.ifi_family = AF_UNSPEC;
    req.ifinfo.ifi_index = ifindex;

    /* started nested attribute for XDP */
    nla = (struct nlattr *)(((char *)&req)
            + NLMSG_ALIGN(req.nh.nlmsg_len));
    nla->nla_type = NLA_F_NESTED | IFLA_XDP;
    nla->nla_len = NLA_HDRLEN;

    /* add XDP fd */
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FD;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(int);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &prog_fd, sizeof(prog_fd));
    nla->nla_len += nla_xdp->nla_len;

    /* if user passed in any flags, add those too */
    __u32 flags = XDP_FLAGS_SKB_MODE;
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FLAGS;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(flags);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &flags, sizeof(flags));
    nla->nla_len += nla_xdp->nla_len;

    req.nh.nlmsg_len += NLA_ALIGN(nla->nla_len);

    if (send(sock, &req, req.nh.nlmsg_len, 0) < 0)
        err(1, "send");
    ret = bpf_netlink_recv(sock, nl_pid, seq);

cleanup:
    close(sock);
    return ret;
}

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

$ cc nolibbpf.c -o nolibbpf
$ sudo strace -e bpf ./nolibbpf
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, map_name="woo", ...}, 72) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=15, prog_name="woo", ...}, 72) = 4
+++ exited with 0 +++

Le të shohim nëse programi ynë është lidhur me lo:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 160

Le të dërgojmë ping dhe të shikojmë hartën:

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

Hurra, gjithçka funksionon. Vini re, nga rruga, se harta jonë shfaqet përsëri në formën e bajteve. Kjo për faktin se, ndryshe nga libbpf ne nuk ngarkuam informacionin e llojit (BTF). Por ne do të flasim më shumë për këtë herën tjetër.

Mjetet e Zhvillimit

Në këtë seksion, ne do të shikojmë paketën minimale të veglave të zhvilluesve BPF.

Në përgjithësi, nuk keni nevojë për ndonjë gjë të veçantë për të zhvilluar programe BPF - BPF funksionon në çdo kernel të mirë të shpërndarjes dhe programet ndërtohen duke përdorur clang, e cila mund të sigurohet nga paketa. Sidoqoftë, për shkak të faktit se BPF është në zhvillim e sipër, kerneli dhe mjetet po ndryshojnë vazhdimisht, nëse nuk dëshironi të shkruani programe BPF duke përdorur metoda të modës së vjetër nga viti 2019, atëherë do të duhet të përpiloni

  • llvm/clang
  • pahole
  • thelbi i saj
  • bpftool

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

llvm/cang

BPF është miqësore me LLVM dhe, megjithëse kohët e fundit programet për BPF mund të përpilohen duke përdorur gcc, i gjithë zhvillimi aktual kryhet për LLVM. Prandaj, para së gjithash, ne do të ndërtojmë versionin aktual clang nga git:

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

Tani mund të kontrollojmë nëse gjithçka u bashkua siç duhet:

$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM version 11.0.0git
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: znver1

  Registered Targets:
    bpf    - BPF (host endian)
    bpfeb  - BPF (big endian)
    bpfel  - BPF (little endian)
    x86    - 32-bit X86: Pentium-Pro and above
    x86-64 - 64-bit X86: EM64T and AMD64

(Udhëzimet e montimit clang marrë nga unë nga bpf_devel_QA.)

Ne nuk do t'i instalojmë programet që sapo kemi ndërtuar, por thjesht do t'i shtojmë ato PATH, për shembull:

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

(Kjo mund t'i shtohet .bashrc ose në një skedar të veçantë. Personalisht, unë i shtoj gjëra të tilla ~/bin/activate-llvm.sh dhe kur është e nevojshme e bëj . activate-llvm.sh.)

Pahole dhe BTF

Shërbim pahole përdoret gjatë ndërtimit të kernelit për të krijuar informacion korrigjimi në formatin BTF. Ne nuk do të hyjmë në detaje në këtë artikull në lidhje me detajet e teknologjisë BTF, përveç faktit që ajo është e përshtatshme dhe ne duam ta përdorim atë. Pra, nëse do të ndërtoni kernelin tuaj, ndërtoni së pari pahole (pa pahole ju nuk do të jeni në gjendje të ndërtoni kernelin 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

Kernelet për eksperimentimin me BPF

Kur eksploroj mundësitë e BPF, unë dua të mbledh bërthamën time. Kjo, në përgjithësi, nuk është e nevojshme, pasi do të jeni në gjendje të përpiloni dhe ngarkoni programe BPF në kernelin e shpërndarjes, megjithatë, të kesh kernelin tënd të lejon të përdorësh veçoritë më të fundit të BPF, të cilat do të shfaqen në shpërndarjen tuaj në disa muaj. , ose, si në rastin e disa mjeteve të korrigjimit nuk do të paketohen fare në të ardhmen e parashikueshme. Gjithashtu, thelbi i tij e bën të ndihet i rëndësishëm për të eksperimentuar me kodin.

Për të ndërtuar një kernel ju nevojitet, së pari, vetë kerneli, dhe së dyti, një skedar konfigurimi i kernelit. Për të eksperimentuar me BPF mund të përdorim të zakonshmen vanilje kernel ose një nga bërthamat e zhvillimit. Historikisht, zhvillimi i BPF ndodh brenda komunitetit të rrjeteve Linux dhe për këtë arsye të gjitha ndryshimet herët a vonë kalojnë përmes David Miller, mirëmbajtësi i rrjeteve Linux. Në varësi të natyrës së tyre - modifikimet ose veçoritë e reja - ndryshimet e rrjetit bien në një nga dy bërthamat - net ose net-next. Ndryshimet për BPF shpërndahen në të njëjtën mënyrë ndërmjet bpf и bpf-next, të cilat më pas grumbullohen në net dhe net-next, respektivisht. Për më shumë detaje, shihni bpf_devel_QA и netdev-FAQ. Kështu që zgjidhni një kernel bazuar në shijen tuaj dhe nevojat e stabilitetit të sistemit ku po testoni (*-next bërthamat janë më të paqëndrueshmet nga ato të listuara).

Është përtej qëllimit të këtij artikulli të flasim për mënyrën e menaxhimit të skedarëve të konfigurimit të kernelit - supozohet se ju ose tashmë dini se si ta bëni këtë, ose gati për të mësuar më vete. Megjithatë, udhëzimet e mëposhtme duhet të jenë pak a shumë të mjaftueshme për t'ju dhënë një sistem funksional të aktivizuar me BPF.

Shkarkoni një nga kernelët e mësipërm:

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

Ndërtoni një konfigurim minimal të kernelit të punës:

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

Aktivizo opsionet BPF në skedar .config sipas zgjedhjes suaj (me shumë mundësi CONFIG_BPF do të aktivizohet tashmë pasi systemd e përdor atë). Këtu është një listë opsionesh nga kerneli i 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

Atëherë ne mund t'i montojmë dhe instalojmë lehtësisht modulet dhe kernelin (nga rruga, mund ta montoni kernelin duke përdorur atë të sapomontuar clangduke shtuar CC=clang):

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

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

v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

bpftool

Shërbimi më i përdorur në artikull do të jetë mjeti bpftool, i ofruar si pjesë e kernelit Linux. Është shkruar dhe mirëmbajtur nga zhvilluesit BPF për zhvilluesit BPF dhe mund të përdoret për të menaxhuar të gjitha llojet e objekteve BPF - ngarkoni programet, krijoni dhe modifikoni hartat, eksploroni jetën e ekosistemit BPF, etj. Dokumentacioni në formën e kodeve burimore për faqet e njeriut mund të gjendet në thelb ose, tashmë të përpiluar, në rrjetë.

Në kohën e këtij shkrimi bpftool vjen i gatshëm vetëm për RHEL, Fedora dhe Ubuntu (shih, për shembull, këtë fill, i cili tregon historinë e papërfunduar të paketimit bpftool në Debian). Por nëse e keni ndërtuar tashmë kernelin tuaj, atëherë ndërtoni bpftool e lehtë si byrek:

$ cd ${linux}/tools/bpf/bpftool
# ... пропишите пути к последнему clang, как рассказано выше
$ make -s

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

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

$

(këtu ${linux} - kjo është drejtoria juaj e kernelit.) Pas ekzekutimit të këtyre komandave bpftool do të mblidhen në një drejtori ${linux}/tools/bpf/bpftool dhe mund t'i shtohet shtegut (para së gjithash te përdoruesi root) ose thjesht kopjoni në /usr/local/sbin.

Mblidhni bpftool është më mirë të përdorni këtë të fundit clang, i montuar siç përshkruhet më sipër dhe kontrolloni nëse është montuar saktë - duke përdorur, për shembull, komandën

$ 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ë se cilat veçori BPF janë aktivizuar në kernelin tuaj.

Nga rruga, komanda e mëparshme mund të ekzekutohet si

# bpftool f p k

Kjo bëhet në analogji me shërbimet nga paketa iproute2, ku mund të themi p.sh ip a s eth0 në vend të ip addr show dev eth0.

Përfundim

BPF ju lejon të vishni një plesht me këpucë për të matur në mënyrë efektive dhe për të ndryshuar funksionalitetin e bërthamës gjatë fluturimit. Sistemi doli të ishte shumë i suksesshëm, në traditat më të mira të UNIX: një mekanizëm i thjeshtë që ju lejon të (ri)programoni kernelin, lejoi një numër të madh njerëzish dhe organizatash të eksperimentojnë. Dhe, megjithëse eksperimentet, si dhe zhvillimi i vetë infrastrukturës BPF, janë larg përfundimit, sistemi tashmë ka një ABI të qëndrueshme që ju lejon të ndërtoni logjikë biznesi të besueshme dhe më e rëndësishmja, efektive.

Dua të vërej se, për mendimin tim, teknologjia është bërë kaq e njohur sepse, nga njëra anë, mundet играть (arkitektura e një makine mund të kuptohet pak a shumë në një mbrëmje), dhe nga ana tjetër, për të zgjidhur probleme që nuk mund të zgjidheshin (bukur) përpara shfaqjes së saj. Këto dy komponentë së bashku i detyrojnë njerëzit të eksperimentojnë dhe të ëndërrojnë, gjë që çon në shfaqjen e zgjidhjeve gjithnjë e më inovative.

Ky artikull, megjithëse jo veçanërisht i shkurtër, është vetëm një hyrje në botën e BPF dhe nuk përshkruan veçori "të avancuara" dhe pjesë të rëndësishme të arkitekturës. Plani në vazhdim është diçka e tillë: artikulli tjetër do të jetë një përmbledhje e llojeve të programit BPF (ka 5.8 lloje programesh të mbështetura në kernelin 30), më në fund do të shikojmë se si të shkruajmë aplikacione reale BPF duke përdorur programet e gjurmimit të kernelit si shembull, atëherë është koha për një kurs më të thelluar mbi arkitekturën BPF, i ndjekur nga shembuj të rrjeteve BPF dhe aplikacioneve të sigurisë.

Artikujt e mëparshëm në këtë seri

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

Lidhjet

  1. Udhëzuesi i referencës BPF dhe XDP — dokumentacion mbi BPF nga cilium, ose më saktë nga Daniel Borkman, një nga krijuesit dhe mirëmbajtësit e BPF. Ky është një nga përshkrimet e para serioze, i cili ndryshon nga të tjerët në atë se Danieli e di saktësisht se për çfarë po shkruan dhe nuk ka asnjë gabim. Në veçanti, ky dokument përshkruan se si të punohet me programet BPF të llojeve XDP dhe TC duke përdorur programin e mirënjohur ip nga paketa iproute2.

  2. Documentation/networking/filter.txt — skedar origjinal me dokumentacion për BPF klasike dhe më pas të zgjeruar. Një lexim i mirë nëse doni të gërmoni në gjuhën e montimit dhe detajet teknike arkitekturore.

  3. Blog për BPF nga facebook. Përditësohet rrallë, por me vend, siç shkruajnë Alexei Starovoitov (autor i eBPF) dhe Andrii Nakryiko - (mirëmbajtësi) libbpf).

  4. Sekretet e bpftool. Një temë argëtuese në Twitter nga Quentin Monnet me shembuj dhe sekrete të përdorimit të bpftool.

  5. Zhyt në BPF: një listë materialesh leximi. Një listë gjigante (dhe ende e ruajtur) e lidhjeve me dokumentacionin BPF nga Quentin Monnet.

Burimi: www.habr.com

Shto një koment