BPF vir die kleintjies, deel een: verlengde BPF

In die begin was daar 'n tegnologie en dit is BPF genoem. Ons het na haar gekyk vorige, Ou-Testamentiese artikel van hierdie reeks. In 2013, deur die pogings van Alexei Starovoitov en Daniel Borkman, is 'n verbeterde weergawe daarvan, geoptimaliseer vir moderne 64-bis-masjiene, ontwikkel en by die Linux-kern ingesluit. Hierdie nuwe tegnologie is kortliks Internal BPF genoem, toe herdoop na Extended BPF, en nou, na etlike jare, noem almal dit bloot BPF.

Rofweg gesproke laat BPF jou toe om arbitrêre kode wat deur gebruikers verskaf word in die Linux-kernruimte uit te voer, en die nuwe argitektuur het so suksesvol geblyk te wees dat ons nog 'n dosyn artikels sal benodig om al sy toepassings te beskryf. (Die enigste ding wat die ontwikkelaars nie goed gedoen het nie, soos u in die prestasiekode hieronder kan sien, was om 'n ordentlike logo te skep.)

Hierdie artikel beskryf die struktuur van die BPF virtuele masjien, kern-koppelvlakke om met BPF te werk, ontwikkelingsinstrumente, sowel as 'n kort, baie kort oorsig van bestaande vermoëns, d.w.s. alles wat ons in die toekoms sal benodig vir 'n dieper studie van die praktiese toepassings van BPF.
BPF vir die kleintjies, deel een: verlengde BPF

Opsomming van die artikel

Inleiding tot BPF-argitektuur. Eerstens sal ons 'n voëlvlugoorsig van die BPF-argitektuur neem en die hoofkomponente uiteensit.

Registers en opdragstelsel van die BPF virtuele masjien. Met 'n idee van die argitektuur as geheel, sal ons die struktuur van die BPF virtuele masjien beskryf.

Lewensiklus van BPF-voorwerpe, bpffs-lêerstelsel. In hierdie afdeling gaan ons die lewensiklus van BPF-voorwerpe - programme en kaarte - van nader bekyk.

Bestuur van voorwerpe deur die bpf-stelseloproep te gebruik. Met 'n mate van begrip van die stelsel wat reeds in plek is, sal ons uiteindelik kyk hoe om voorwerpe uit gebruikersruimte te skep en te manipuleer deur 'n spesiale stelseloproep te gebruik − bpf(2).

Пишем программы BPF с помощью libbpf. Natuurlik kan jy programme skryf deur 'n stelseloproep te gebruik. Maar dis moeilik. Vir 'n meer realistiese scenario het kernprogrammeerders 'n biblioteek ontwikkel libbpf. Ons sal 'n basiese BPF-toepassingskelet skep wat ons in volgende voorbeelde sal gebruik.

Kernhelpers. Hier sal ons leer hoe BPF-programme toegang tot kernhulpfunksies kan kry - 'n instrument wat saam met kaarte die vermoëns van die nuwe BPF fundamenteel uitbrei in vergelyking met die klassieke een.

Toegang tot kaarte vanaf BPF-programme. Teen hierdie punt sal ons genoeg weet om presies te verstaan ​​hoe ons programme kan skep wat kaarte gebruik. En kom ons kyk selfs vinnig na die groot en magtige verifieerder.

Ontwikkelingsinstrumente. Hulpafdeling oor hoe om die vereiste nutsprogramme en kern vir eksperimente saam te stel.

Gevolgtrekking. Aan die einde van die artikel sal diegene wat tot hier lees motiverende woorde en 'n kort beskrywing van wat gaan gebeur in die volgende artikels vind. Ons sal ook 'n aantal skakels vir selfstudie lys vir diegene wat nie die begeerte of vermoë het om te wag vir die voortsetting nie.

Inleiding tot BPF-argitektuur

Voordat ons begin om die BPF-argitektuur te oorweeg, sal ons nog 'n laaste keer (o) verwys klassieke BPF, wat ontwikkel is as 'n reaksie op die koms van RISC-masjiene en die probleem van doeltreffende pakkiefiltrering opgelos het. Die argitektuur het so suksesvol geblyk te wees dat dit, nadat dit in die skitterende negentigerjare in Berkeley UNIX gebore is, na die meeste bestaande bedryfstelsels oorgedra is, tot in die mal twintigerjare oorleef het en steeds nuwe toepassings vind.

Die nuwe BPF is ontwikkel as 'n reaksie op die alomteenwoordigheid van 64-bis masjiene, wolkdienste en die toenemende behoefte aan gereedskap vir die skep van SDN (Sgereeldware-deffined nnetwerkwerk). Ontwikkel deur kernnetwerkingenieurs as 'n verbeterde plaasvervanger vir die klassieke BPF, het die nuwe BPF letterlik ses maande later toepassings gevind in die moeilike taak om Linux-stelsels op te spoor, en nou, ses jaar na sy verskyning, sal ons 'n hele volgende artikel nodig hê net om lys die verskillende tipes programme.

Snaakse prentjies

In sy kern is BPF 'n sandbox virtuele masjien wat jou toelaat om "arbitrêre" kode in kernruimte te laat loop sonder om sekuriteit in te boet. BPF-programme word in gebruikersruimte geskep, in die kern gelaai en aan een of ander gebeurtenisbron gekoppel. 'n Gebeurtenis kan byvoorbeeld die aflewering van 'n pakkie aan 'n netwerkkoppelvlak wees, die bekendstelling van een of ander kernfunksie, ens. In die geval van 'n pakket, sal die BPF-program toegang hê tot die data en metadata van die pakket (vir lees en, moontlik, skryf, afhangende van die tipe program); in die geval van die uitvoer van 'n kernfunksie, die argumente van die funksie, insluitend wysers na kerngeheue, ens.

Kom ons kyk na hierdie proses van naderby. Om mee te begin, kom ons praat oor die eerste verskil van die klassieke BPF, waarvoor programme in assembler geskryf is. In die nuwe weergawe is die argitektuur uitgebrei sodat programme in hoëvlaktale geskryf kon word, primêr natuurlik in C. Hiervoor is 'n backend vir llvm ontwikkel, wat jou toelaat om greepkode vir die BPF-argitektuur te genereer.

BPF vir die kleintjies, deel een: verlengde BPF

Die BPF-argitektuur is gedeeltelik ontwerp om doeltreffend op moderne masjiene te werk. Om dit in die praktyk te laat werk, word die BPF-greepkode, sodra dit in die kern gelaai is, vertaal in inheemse kode met behulp van 'n komponent genaamd 'n JIT-samesteller (Joet In Time). Volgende, as jy onthou, in klassieke BPF is die program in die kern gelaai en atomies aan die gebeurtenisbron geheg - in die konteks van 'n enkele stelseloproep. In die nuwe argitektuur gebeur dit in twee fases - eerstens word die kode in die kern gelaai met 'n stelseloproep bpf(2)en dan, later, deur ander meganismes wat wissel na gelang van die tipe program, heg die program aan die gebeurtenisbron.

Hier kan die leser 'n vraag hê: was dit moontlik? Hoe word die uitvoeringsveiligheid van sulke kode gewaarborg? Uitvoeringsveiligheid word vir ons gewaarborg deur die stadium van laai van BPF-programme genaamd verifier (in Engels word hierdie stadium verifier genoem en ek sal voortgaan om die Engelse woord te gebruik):

BPF vir die kleintjies, deel een: verlengde BPF

Verifier is 'n statiese ontleder wat verseker dat 'n program nie die normale werking van die kern ontwrig nie. Dit beteken terloops nie dat die program nie met die werking van die stelsel kan inmeng nie - BPF-programme, afhangende van die tipe, kan dele van kerngeheue lees en herskryf, waardes van funksies terugstuur, snoei, byvoeg, herskryf en selfs netwerkpakkies aanstuur. Verifier waarborg dat die uitvoering van 'n BPF-program nie die kern sal laat crash nie en dat 'n program wat, volgens die reëls, skryftoegang het, byvoorbeeld die data van 'n uitgaande pakkie, nie die kerngeheue buite die pakkie sal kan oorskryf nie. Ons sal 'n bietjie meer detail na verifieerder kyk in die ooreenstemmende afdeling, nadat ons kennis gemaak het met al die ander komponente van BPF.

So, wat het ons tot dusver geleer? Die gebruiker skryf 'n program in C, laai dit in die kern met behulp van 'n stelseloproep bpf(2), waar dit deur 'n verifieerder nagegaan word en in inheemse greepkode vertaal word. Dan koppel dieselfde of 'n ander gebruiker die program aan die gebeurtenisbron en dit begin uitvoer. Die skeiding van selflaai en verbinding is om verskeie redes nodig. Eerstens is die bestuur van 'n verifieerder relatief duur en deur dieselfde program verskeie kere af te laai mors ons rekenaartyd. Tweedens, presies hoe 'n program gekoppel is, hang af van die tipe daarvan, en een "universele" koppelvlak wat 'n jaar gelede ontwikkel is, is dalk nie geskik vir nuwe soorte programme nie. (Alhoewel noudat die argitektuur meer volwasse word, is daar 'n idee om hierdie koppelvlak op die vlak te verenig libbpf.)

Die aandagtige leser sal dalk agterkom dat ons nog nie klaar is met die prente nie. Inderdaad, al die bogenoemde verduidelik nie hoekom BPF die prentjie fundamenteel verander in vergelyking met klassieke BPF nie. Twee innovasies wat die omvang van toepaslikheid aansienlik uitbrei, is die vermoë om gedeelde geheue en kernhelperfunksies te gebruik. In BPF word gedeelde geheue geïmplementeer met behulp van sogenaamde kaarte - gedeelde datastrukture met 'n spesifieke API. Hulle het waarskynlik hierdie naam gekry omdat die eerste tipe kaart wat verskyn het, 'n hash-tabel was. Toe verskyn skikkings, plaaslike (per-CPU) hash-tabelle en plaaslike skikkings, soekbome, kaarte wat wysers na BPF-programme bevat en nog baie meer. Wat nou vir ons interessant is, is dat BPF-programme nou die vermoë het om toestand tussen oproepe te behou en dit met ander programme en met gebruikersspasie te deel.

Kaarte word verkry vanaf gebruikerprosesse deur 'n stelseloproep te gebruik bpf(2), en van BPF-programme wat in die kern loop deur helperfunksies te gebruik. Boonop bestaan ​​helpers nie net om met kaarte te werk nie, maar ook om toegang tot ander kernvermoëns te verkry. BPF-programme kan byvoorbeeld helperfunksies gebruik om pakkies na ander koppelvlakke aan te stuur, perf-gebeurtenisse te genereer, toegang tot kernstrukture te verkry, ensovoorts.

BPF vir die kleintjies, deel een: verlengde BPF

Ter opsomming, BPF bied die vermoë om arbitrêre, dit wil sê verifieerder-getoetsde, gebruikerskode in kernspasie te laai. Hierdie kode kan toestand tussen oproepe stoor en data met gebruikersspasie uitruil, en het ook toegang tot kernsubstelsels wat deur hierdie tipe program toegelaat word.

Dit is reeds soortgelyk aan die vermoëns wat deur kernmodules verskaf word, in vergelyking met wat BPF 'n paar voordele het (natuurlik kan jy net soortgelyke toepassings vergelyk, byvoorbeeld stelselopsporing - jy kan nie 'n arbitrêre bestuurder met BPF skryf nie). U kan 'n laer toegangsdrempel opmerk (sommige nutsprogramme wat BPF gebruik vereis nie dat die gebruiker kernprogrammeringsvaardighede of programmeringsvaardighede in die algemeen moet hê nie), looptydveiligheid (steek u hand op in die opmerkings vir diegene wat nie die stelsel gebreek het tydens skryf nie of toetsmodules), atomisiteit - daar is stilstand wanneer modules herlaai word, en die BPF-substelsel verseker dat geen gebeurtenisse gemis word nie (om regverdig te wees, dit is nie waar vir alle soorte BPF-programme nie).

Die teenwoordigheid van sulke vermoëns maak BPF 'n universele hulpmiddel om die kern uit te brei, wat in die praktyk bevestig word: meer en meer nuwe soorte programme word by BPF gevoeg, meer en meer groot maatskappye gebruik BPF op gevegsbedieners 24×7, meer en meer startups bou hul besigheid op oplossings wat gebaseer is op BPF. BPF word oral gebruik: in die beskerming teen DDoS-aanvalle, die skep van SDN (byvoorbeeld, die implementering van netwerke vir kubernetes), as die hoofstelselopsporingsinstrument en statistiekversamelaar, in inbraakdetectiestelsels en sandboxstelsels, ens.

Kom ons voltooi die oorsiggedeelte van die artikel hier en kyk na die virtuele masjien en die BPF-ekosisteem in meer besonderhede.

Afwyking: nutsdienste

Om die voorbeelde in die volgende afdelings te kan uitvoer, het jy dalk 'n aantal nutsprogramme nodig llvm/clang met bpf ondersteuning en bpftool. In die afdeling Ontwikkelingsgereedskap Jy kan die instruksies lees vir die samestelling van die nutsprogramme, sowel as jou kern. Hierdie afdeling word hieronder geplaas om nie die harmonie van ons aanbieding te versteur nie.

BPF virtuele masjienregisters en instruksiestelsel

Die argitektuur en opdragstelsel van BPF is ontwikkel met inagneming van die feit dat programme in die C-taal geskryf sal word en, nadat dit in die kern gelaai is, in inheemse kode vertaal sal word. Daarom is die aantal registers en die stel opdragte gekies met die oog op die kruising, in die wiskundige sin, van die vermoëns van moderne masjiene. Boonop is verskeie beperkings op programme ingestel, byvoorbeeld, tot onlangs was dit nie moontlik om lusse en subroetines te skryf nie, en die aantal instruksies is beperk tot 4096 (nou kan bevoorregte programme tot 'n miljoen instruksies laai).

BPF het elf gebruikerstoeganklike 64-bis registers r0-r10 en 'n programteller. Registreer r10 bevat 'n raamwyser en is leesalleen. Programme het toegang tot 'n stapel van 512 grepe tydens looptyd en 'n onbeperkte hoeveelheid gedeelde geheue in die vorm van kaarte.

BPF-programme word toegelaat om 'n spesifieke stel programtipe kernhelpers en, meer onlangs, gereelde funksies uit te voer. Elke opgeroep funksie kan tot vyf argumente neem, deurgegee in registers r1-r5, en die terugkeerwaarde word oorgedra na r0. Dit is gewaarborg dat na terugkeer van die funksie, die inhoud van die registers r6-r9 Sal nie verander nie.

Vir doeltreffende programvertaling, registreer r0-r11 want alle ondersteunde argitekture is uniek gekarteer na werklike registers, met inagneming van die ABI-kenmerke van die huidige argitektuur. Byvoorbeeld, vir x86_64 registers r1-r5, wat gebruik word om funksieparameters deur te gee, word op vertoon rdi, rsi, rdx, rcx, r8, wat gebruik word om parameters deur te gee aan funksies op x86_64. Byvoorbeeld, die kode aan die linkerkant vertaal na die kode aan die regterkant soos volg:

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

Registreer r0 ook gebruik om die resultaat van programuitvoering terug te gee, en in die register r1 die program word 'n wyser na die konteks deurgegee - afhangend van die tipe program, kan dit byvoorbeeld 'n struktuur wees struct xdp_md (vir XDP) of struktuur struct __sk_buff (vir verskillende netwerkprogramme) of struktuur struct pt_regs (vir verskillende tipes opsporingsprogramme), ens.

So, ons het 'n stel registers, kernhelpers, 'n stapel, 'n kontekswyser en gedeelde geheue in die vorm van kaarte gehad. Nie dat dit alles absoluut nodig is op die reis nie, maar ...

Kom ons gaan voort met die beskrywing en praat oor die opdragstelsel om met hierdie voorwerpe te werk. Almal (Amper almal) BPF-instruksies het ’n vaste 64-bis-grootte. As jy kyk na een instruksie op 'n 64-bis Big Endian masjien sal jy sien

BPF vir die kleintjies, deel een: verlengde BPF

Hier Code - dit is die enkodering van die instruksie, Dst/Src is die enkoderings van die ontvanger en bron, onderskeidelik, Off - 16-bis getekende inkeping, en Imm is 'n 32-bis getekende heelgetal wat in sommige instruksies gebruik word (soortgelyk aan die cBPF konstante K). Enkodering Code het een van twee tipes:

BPF vir die kleintjies, deel een: verlengde BPF

Instruksieklasse 0, 1, 2, 3 definieer opdragte om met geheue te werk. Hulle is geroep, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, onderskeidelik. Klasse 4, 7 (BPF_ALU, BPF_ALU64) 'n stel ALU-instruksies uitmaak. Klasse 5, 6 (BPF_JMP, BPF_JMP32) bevat springinstruksies.

Die verdere plan vir die bestudering van die BPF-instruksiestelsel is soos volg: in plaas daarvan om al die instruksies en hul parameters noukeurig te lys, sal ons na 'n paar voorbeelde in hierdie afdeling kyk en daaruit sal dit duidelik word hoe die instruksies werklik werk en hoe om demonteer enige binêre lêer vir BPF handmatig. Om die materiaal later in die artikel te konsolideer, sal ons ook individuele instruksies ontmoet in die afdelings oor Verifier, JIT samesteller, vertaling van klassieke BPF, sowel as wanneer kaarte bestudeer word, funksies opgeroep word, ens.

Wanneer ons oor individuele instruksies praat, sal ons na die kernlêers verwys bpf.h и bpf_common.h, wat die numeriese kodes van BPF-instruksies definieer. Wanneer jy argitektuur op jou eie bestudeer en/of binaries ontleed, kan jy semantiek vind in die volgende bronne, gesorteer in volgorde van kompleksiteit: Nie-amptelike eBPF-spesifikasie, BPF en XDP Verwysingsgids, Instruksiestel, Dokumentasie/netwerk/filter.txt en, natuurlik, in die Linux-bronkode - verifier, JIT, BPF-tolk.

Voorbeeld: demonteer BPF in jou kop

Kom ons kyk na 'n voorbeeld waarin ons 'n program saamstel readelf-example.c en kyk na die gevolglike binêre. Ons sal die oorspronklike inhoud openbaar readelf-example.c hieronder, nadat ons sy logika van binêre kodes herstel het:

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

Eerste kolom in uitvoer readelf is 'n inkeping en ons program bestaan ​​dus uit vier opdragte:

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

Opdragkodes is gelyk b7, 15, b7 и 95. Onthou dat die minste betekenisvolle drie bisse die instruksieklas is. In ons geval is die vierde bietjie van alle instruksies leeg, dus is die instruksieklasse onderskeidelik 7, 5, 7, 5. Klas 7 is BPF_ALU64, en 5 is BPF_JMP. Vir beide klasse is die instruksieformaat dieselfde (sien hierbo) en ons kan ons program so herskryf (terselfdertyd sal ons die oorblywende kolomme in menslike vorm herskryf):

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

operasie b klas ALU64 - Is BPF_MOV. Dit ken 'n waarde aan die bestemmingsregister toe. As die bietjie ingestel is s (bron), dan word die waarde uit die bronregister geneem, en as, soos in ons geval, dit nie gestel is nie, dan word die waarde uit die veld geneem Imm. So in die eerste en derde instruksies voer ons die operasie uit r0 = Imm. Verder, JMP klas 1 operasie is BPF_JEQ (spring indien gelyk). In ons geval, sedert die bietjie S nul is, vergelyk dit die waarde van die bronregister met die veld Imm. As die waardes saamval, vind die oorgang plaas na PC + OffWaar PC, soos gewoonlik, bevat die adres van die volgende instruksie. Ten slotte, JMP Klas 9 Operasie is BPF_EXIT. Hierdie instruksie beëindig die program en keer terug na die kern r0. Kom ons voeg 'n nuwe kolom by ons tabel:

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

Ons kan dit in 'n geriefliker vorm herskryf:

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

As ons onthou wat in die register staan r1 die program word 'n wyser na die konteks vanaf die kern en in die register deurgegee r0 die waarde word teruggestuur na die kern, dan kan ons sien dat as die wyser na die konteks nul is, dan gee ons 1 terug, en andersins - 2. Kom ons kyk of ons reg is deur na die bron te kyk:

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

Ja, dit is 'n betekenislose program, maar dit vertaal in net vier eenvoudige instruksies.

Uitsonderingsvoorbeeld: 16-grepe instruksie

Ons het vroeër genoem dat sommige instruksies meer as 64 bisse in beslag neem. Dit geld byvoorbeeld vir instruksies lddw (Kode = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — laai 'n dubbele woord uit die velde in die register Imm... Die feit is dat Imm het 'n grootte van 32, en 'n dubbelwoord is 64 bisse, so die laai van 'n 64-bis onmiddellike waarde in 'n register in een 64-bis instruksie sal nie werk nie. Om dit te doen, word twee aangrensende instruksies gebruik om die tweede deel van die 64-bis waarde in die veld te stoor Imm. 'N Voorbeeld:

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

Daar is net twee instruksies in 'n binêre program:

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

Ons sal weer ontmoet met instruksies lddw, wanneer ons praat oor verskuiwings en werk met kaarte.

Voorbeeld: demontage van BPF met behulp van standaard gereedskap

So, ons het geleer om BPF-binêre kodes te lees en is gereed om enige instruksie te ontleed indien nodig. Dit is egter die moeite werd om te sê dat dit in die praktyk geriefliker en vinniger is om programme met behulp van standaardgereedskap uitmekaar te haal, byvoorbeeld:

$ 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

Lewensiklus van BPF-voorwerpe, bpffs-lêerstelsel

(Ek het eers van die besonderhede wat in hierdie onderafdeling beskryf word, geleer by vas Alexei Starovoitov in BPF Blog.)

BPF-voorwerpe - programme en kaarte - word vanuit gebruikersruimte geskep deur opdragte te gebruik BPF_PROG_LOAD и BPF_MAP_CREATE stelsel oproep bpf(2), sal ons in die volgende afdeling praat oor presies hoe dit gebeur. Dit skep kerndatastrukture en vir elkeen van hulle refcount (verwysingtelling) is op een gestel, en 'n lêerbeskrywer wat na die voorwerp wys, word aan die gebruiker teruggestuur. Nadat die handvatsel gesluit is refcount die voorwerp word met een verminder, en wanneer dit nul bereik, word die voorwerp vernietig.

As die program kaarte gebruik, dan refcount hierdie kaarte word met een vermeerder nadat die program gelaai is, d.w.s. hul lêerbeskrywings kan vanaf die gebruikerproses gesluit word en steeds refcount sal nie nul word nie:

BPF vir die kleintjies, deel een: verlengde BPF

Nadat ons 'n program suksesvol gelaai het, heg ons dit gewoonlik aan 'n soort gebeurtenisgenerator. Ons kan dit byvoorbeeld op 'n netwerkkoppelvlak plaas om inkomende pakkies te verwerk of aan sommige te koppel tracepoint in die kern. Op hierdie punt sal die verwysingsteller ook met een verhoog en ons sal die lêerbeskrywer in die laaiprogram kan sluit.

Wat gebeur as ons nou die selflaaiprogram afskakel? Dit hang af van die tipe gebeurtenisgenerator (haak). Alle netwerkhake sal bestaan ​​nadat die laaier voltooi is, dit is die sogenaamde globale hake. En, byvoorbeeld, sal spoorprogramme vrygestel word nadat die proses wat hulle geskep het, beëindig word (en word dus plaaslik genoem, van "plaaslik na die proses"). Tegnies het plaaslike hake altyd 'n ooreenstemmende lêerbeskrywing in gebruikersruimte en sluit dus wanneer die proses gesluit is, maar globale hake nie. In die volgende figuur, met behulp van rooi kruisies, probeer ek om te wys hoe die beëindiging van die laaiprogram die leeftyd van voorwerpe in die geval van plaaslike en globale hake beïnvloed.

BPF vir die kleintjies, deel een: verlengde BPF

Hoekom is daar 'n onderskeid tussen plaaslike en globale haakplekke? Om sommige soorte netwerkprogramme te laat loop maak sin sonder 'n gebruikersruimte, stel jou byvoorbeeld DDoS-beskerming voor - die selflaaiprogram skryf die reëls en koppel die BPF-program aan die netwerkkoppelvlak, waarna die selflaaiprogram homself kan doodmaak. Aan die ander kant, stel jou 'n ontfoutingspoorprogram voor wat jy binne tien minute op jou knieë geskryf het - wanneer dit klaar is, wil jy graag hê dat daar geen vullis in die stelsel oorbly nie, en plaaslike hake sal dit verseker.

Aan die ander kant, stel jou voor dat jy aan 'n spoorpunt in die kern wil koppel en statistieke oor baie jare wil versamel. In hierdie geval wil u die gebruikersdeel voltooi en van tyd tot tyd na die statistieke terugkeer. Die bpf-lêerstelsel bied hierdie geleentheid. Dit is 'n in-geheue-slegs pseudo-lêerstelsel wat die skepping van lêers moontlik maak wat verwys na BPF-voorwerpe en daardeur verhoog refcount voorwerpe. Hierna kan die laaier verlaat, en die voorwerpe wat dit geskep het, sal lewendig bly.

BPF vir die kleintjies, deel een: verlengde BPF

Die skep van lêers in bpffs wat verwys na BPF-voorwerpe word "penning" genoem (soos in die volgende frase: "proses kan 'n BPF-program of kaart vaspen"). Die skep van lêerobjekte vir BPF-objekte maak nie net sin vir die verlenging van die lewensduur van plaaslike voorwerpe nie, maar ook vir die bruikbaarheid van globale voorwerpe - as ons teruggaan na die voorbeeld met die globale DDoS-beskermingsprogram, wil ons na statistieke kan kom kyk. van tyd tot tyd.

Die BPF-lêerstelsel word gewoonlik in gemonteer /sys/fs/bpf, maar dit kan ook plaaslik gemonteer word, byvoorbeeld, soos volg:

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

Lêerstelselname word geskep met behulp van die opdrag BPF_OBJ_PIN BPF-stelseloproep. Om dit te illustreer, kom ons neem 'n program, stel dit saam, laai dit op en pen dit vas bpffs. Ons program doen niks nuttig nie, ons bied net die kode aan sodat jy die voorbeeld kan reproduseer:

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

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

Kom ons stel hierdie program saam en skep 'n plaaslike kopie van die lêerstelsel bpffs:

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

Kom ons laai nou ons program af met die hulpprogram bpftool en kyk na die gepaardgaande stelseloproepe bpf(2) ('n paar irrelevante reëls verwyder van strace-uitvoer):

$ 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

Hier het ons die program gelaai met behulp van BPF_PROG_LOAD, het 'n lêerbeskrywing van die kern ontvang 3 en gebruik die opdrag BPF_OBJ_PIN het hierdie lêerbeskrywing as 'n lêer vasgespeld "bpf-mountpoint/test". Hierna die selflaaiprogram bpftool klaar gewerk, maar ons program het in die kern gebly, alhoewel ons dit nie aan enige netwerkkoppelvlak geheg het nie:

$ 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

Ons kan die lêervoorwerp normaalweg uitvee unlink(2) en daarna sal die ooreenstemmende program uitgevee word:

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

Vee voorwerpe uit

As ons praat oor die verwydering van voorwerpe, is dit nodig om te verduidelik dat nadat ons die program van die haak (gebeurtenisgenerator ontkoppel het), nie 'n enkele nuwe gebeurtenis die bekendstelling daarvan sal veroorsaak nie, maar alle huidige gevalle van die program sal in die normale volgorde voltooi word .

Sommige tipes BPF-programme laat jou toe om die program dadelik te vervang, m.a.w. volgorde atomiteit verskaf replace = detach old program, attach new program. In hierdie geval sal alle aktiewe gevalle van die ou weergawe van die program hul werk voltooi, en nuwe gebeurtenishanteerders sal vanaf die nuwe program geskep word, en "atomicity" hier beteken dat nie 'n enkele gebeurtenis gemis sal word nie.

Heg programme aan gebeurtenisbronne

In hierdie artikel sal ons nie afsonderlik die koppeling van programme aan gebeurtenisbronne beskryf nie, aangesien dit sin maak om dit in die konteks van 'n spesifieke tipe program te bestudeer. Cm. Byvoorbeeld hieronder, waarin ons wys hoe programme soos XDP verbind is.

Manipuleer voorwerpe deur die bpf-stelseloproep te gebruik

BPF programme

Alle BPF-voorwerpe word geskep en bestuur vanuit gebruikersruimte deur 'n stelseloproep te gebruik bpf, met die volgende prototipe:

#include <linux/bpf.h>

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

Hier is die span cmd is een van die waardes van tipe enum bpf_cmd, attr — 'n wyser na parameters vir 'n spesifieke program en size — voorwerpgrootte volgens die wyser, d.w.s. gewoonlik dit sizeof(*attr). In kern 5.8 roep die stelsel bpf ondersteun 34 verskillende opdragte, en definisie union bpf_attr beslaan 200 reëls. Maar ons moet nie hierdeur geïntimideer word nie, aangesien ons onsself met die opdragte en parameters in die loop van verskeie artikels sal vergewis.

Kom ons begin by die span BPF_PROG_LOAD, wat BPF-programme skep - neem 'n stel BPF-instruksies en laai dit in die kern. Op die oomblik van laai word die verifieerder geloods, en dan word die JIT-samesteller en, na suksesvolle uitvoering, die programlêerbeskrywing aan die gebruiker teruggestuur. Ons het in die vorige afdeling gesien wat volgende met hom gebeur oor die lewensiklus van BPF-voorwerpe.

Ons sal nou 'n pasgemaakte program skryf wat 'n eenvoudige BPF-program sal laai, maar eers moet ons besluit watter soort program ons wil laai - ons sal moet kies tipe en skryf binne die raamwerk van hierdie tipe 'n program wat die verifieerdertoets sal slaag. Om egter nie die proses te bemoeilik nie, is hier 'n klaargemaakte oplossing: ons sal 'n program soos BPF_PROG_TYPE_XDP, wat die waarde sal teruggee XDP_PASS (slaan alle pakkette oor). In BPF assembler lyk dit baie eenvoudig:

r0 = 2
exit

Nadat ons besluit het op dat ons sal oplaai, ons kan jou vertel hoe ons dit sal doen:

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

Interessante gebeurtenisse in 'n program begin met die definisie van 'n skikking insns - ons BPF-program in masjienkode. In hierdie geval word elke instruksie van die BPF-program in die struktuur verpak bpf_insn. Eerste element insns voldoen aan instruksies r0 = 2, die tweede - exit.

Terugtrek. Die kern definieer geriefliker makro's vir die skryf van masjienkodes en die gebruik van die kernkoplêer tools/include/linux/filter.h ons kon skryf

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

Maar aangesien die skryf van BPF-programme in inheemse kode slegs nodig is vir die skryf van toetse in die kern en artikels oor BPF, kompliseer die afwesigheid van hierdie makro's nie die ontwikkelaar se lewe regtig nie.

Nadat ons die BPF-program gedefinieer het, gaan ons voort om dit in die kern te laai. Ons minimalistiese stel parameters attr sluit die programtipe, stel en aantal instruksies, vereiste lisensie en naam in "woo", wat ons gebruik om ons program op die stelsel te vind nadat dit afgelaai is. Die program, soos belowe, word in die stelsel gelaai deur 'n stelseloproep te gebruik bpf.

Aan die einde van die program beland ons in 'n oneindige lus wat die loonvrag simuleer. Daarsonder sal die program deur die kern doodgemaak word wanneer die lêerbeskrywing wat die stelseloproep aan ons teruggestuur het, gesluit is bpf, en ons sal dit nie in die stelsel sien nie.

Wel, ons is gereed vir toetsing. Kom ons stel die program saam en laat loop onder straceom seker te maak dat alles werk soos dit moet:

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

Alles is reg, bpf(2) het handvatsel 3 aan ons teruggegee en ons het in 'n oneindige lus gegaan met pause(). Kom ons probeer om ons program in die stelsel te vind. Om dit te doen, gaan ons na 'n ander terminaal en gebruik die hulpprogram 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)

Ons sien dat daar 'n gelaaide program op die stelsel is woo wie se globale ID 390 is en tans aan die gang is simple-prog daar is 'n oop lêerbeskrywing wat na die program wys (en as simple-prog sal dan die werk klaarmaak woo sal verdwyn). Soos verwag, die program woo neem 16 grepe - twee instruksies - van binêre kodes in die BPF-argitektuur, maar in sy oorspronklike vorm (x86_64) is dit reeds 40 grepe. Kom ons kyk na ons program in sy oorspronklike vorm:

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

geen verrassings nie. Kom ons kyk nou na die kode wat deur die JIT-samesteller gegenereer word:

# 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

nie baie effektief vir exit(2), maar in regverdigheid is ons program te eenvoudig, en vir nie-triviale programme is die proloog en epiloog wat deur die JIT-samesteller bygevoeg is, natuurlik nodig.

Kaarte

BPF-programme kan gestruktureerde geheue-areas gebruik wat toeganklik is vir beide ander BPF-programme en vir programme in gebruikersruimte. Hierdie voorwerpe word kaarte genoem en in hierdie afdeling sal ons wys hoe om hulle te manipuleer deur 'n stelseloproep te gebruik bpf.

Kom ons sê dadelik dat die vermoëns van kaarte nie net beperk is tot toegang tot gedeelde geheue nie. Daar is spesiale-doel kaarte wat, byvoorbeeld, wysers na BPF programme of wysers na netwerk koppelvlakke, kaarte vir werk met perf gebeurtenisse, ens. Ons sal nie hier oor hulle praat nie, om nie die leser te verwar nie. Afgesien hiervan ignoreer ons sinchronisasiekwessies, aangesien dit nie belangrik is vir ons voorbeelde nie. 'n Volledige lys van beskikbare kaarttipes kan gevind word in <linux/bpf.h>, en in hierdie afdeling sal ons as voorbeeld die histories eerste tipe, die hash-tabel, neem BPF_MAP_TYPE_HASH.

As jy 'n hash-tabel skep in byvoorbeeld C++, sou jy sê unordered_map<int,long> woo, wat in Russies beteken "Ek het 'n tafel nodig woo onbeperkte grootte, waarvan die sleutels van tipe is int, en die waardes is die tipe long" Om 'n BPF-hash-tabel te skep, moet ons baie dieselfde ding doen, behalwe dat ons die maksimum grootte van die tabel moet spesifiseer, en in plaas daarvan om die tipe sleutels en waardes te spesifiseer, moet ons hul groottes in grepe spesifiseer . Gebruik die opdrag om kaarte te skep BPF_MAP_CREATE stelsel oproep bpf. Kom ons kyk na 'n min of meer minimale program wat 'n kaart skep. Na die vorige program wat BPF-programme laai, behoort hierdie een vir jou eenvoudig te lyk:

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

Hier definieer ons 'n stel parameters attr, waarin ons sê "Ek benodig 'n hash-tabel met sleutels en groottewaardes sizeof(int), waarin ek 'n maksimum van vier elemente kan plaas." Wanneer u BPF-kaarte skep, kan u ander parameters spesifiseer, byvoorbeeld, op dieselfde manier as in die voorbeeld met die program, ons het die naam van die voorwerp gespesifiseer as "woo".

Kom ons stel die program saam en laat loop:

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

Hier is die stelseloproep bpf(2) het vir ons die beskrywingskaartnommer teruggegee 3 en dan wag die program, soos verwag, vir verdere instruksies in die stelseloproep pause(2).

Kom ons stuur nou ons program na die agtergrond of maak 'n ander terminaal oop en kyk na ons voorwerp met behulp van die nut bpftool (ons kan ons kaart van ander met sy naam onderskei):

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

Die nommer 114 is die globale ID van ons voorwerp. Enige program op die stelsel kan hierdie ID gebruik om 'n bestaande kaart oop te maak met die opdrag BPF_MAP_GET_FD_BY_ID stelsel oproep bpf.

Nou kan ons met ons hash-tabel speel. Kom ons kyk na die inhoud daarvan:

$ sudo bpftool map dump id 114
Found 0 elements

Leeg. Kom ons plaas 'n waarde daarin hash[1] = 1:

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

Kom ons kyk weer na die tabel:

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

Hoera! Ons het daarin geslaag om een ​​element by te voeg. Let daarop dat ons op die greepvlak moet werk om dit te doen, aangesien bptftool weet nie watter tipe waardes in die hash-tabel is nie. (Hierdie kennis kan met BTF aan haar oorgedra word, maar meer daaroor nou.)

Hoe presies lees en voeg bpftool elemente by? Kom ons kyk onder die enjinkap:

$ 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

Eers het ons die kaart oopgemaak deur sy globale ID met behulp van die opdrag BPF_MAP_GET_FD_BY_ID и bpf(2) het beskrywing 3 aan ons teruggestuur. Verder gebruik van die opdrag BPF_MAP_GET_NEXT_KEY ons het die eerste sleutel in die tabel gevind deur verby te gaan NULL as 'n wyser na die "vorige" sleutel. As ons die sleutel het, kan ons dit doen BPF_MAP_LOOKUP_ELEMwat 'n waarde na 'n wyser terugstuur value. Die volgende stap is ons probeer om die volgende element te vind deur 'n wyser na die huidige sleutel deur te gee, maar ons tabel bevat net een element en die opdrag BPF_MAP_GET_NEXT_KEY keer terug ENOENT.

Goed, kom ons verander die waarde deur sleutel 1, kom ons sê ons besigheidslogika vereis registrasie 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

Soos verwag, is dit baie eenvoudig: die opdrag BPF_MAP_GET_FD_BY_ID maak ons ​​kaart oop met ID, en die opdrag BPF_MAP_UPDATE_ELEM oorskryf die element.

Dus, nadat ons 'n hash-tabel van een program geskep het, kan ons die inhoud van 'n ander lees en skryf. Let daarop dat as ons dit vanaf die opdragreël kon doen, enige ander program op die stelsel dit kan doen. Benewens die opdragte wat hierbo beskryf word, om met kaarte vanuit gebruikersruimte te werk, die volgende:

  • BPF_MAP_LOOKUP_ELEM: vind waarde deur sleutel
  • BPF_MAP_UPDATE_ELEM: dateer / skep waarde
  • BPF_MAP_DELETE_ELEM: verwyder sleutel
  • BPF_MAP_GET_NEXT_KEY: vind die volgende (of eerste) sleutel
  • BPF_MAP_GET_NEXT_ID: laat jou toe om deur alle bestaande kaarte te gaan, dis hoe dit werk bpftool map
  • BPF_MAP_GET_FD_BY_ID: maak 'n bestaande kaart oop deur sy globale ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: werk die waarde van 'n voorwerp atoom op en gee die ou een terug
  • BPF_MAP_FREEZE: maak die kaart onveranderlik vanaf gebruikersruimte (hierdie bewerking kan nie ontdoen word nie)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: massa-operasies. Byvoorbeeld, BPF_MAP_LOOKUP_AND_DELETE_BATCH - dit is die enigste betroubare manier om alle waardes vanaf die kaart te lees en terug te stel

Nie al hierdie opdragte werk vir alle kaarttipes nie, maar in die algemeen lyk dit presies dieselfde as om met hash-tabelle met ander soorte kaarte uit gebruikersruimte te werk.

Ter wille van orde, kom ons voltooi ons hash tabel eksperimente. Onthou ons dat ons 'n tabel geskep het wat tot vier sleutels kan bevat? Kom ons voeg nog 'n paar elemente by:

$ 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

So ver so goed:

$ 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

Kom ons probeer om nog een by te voeg:

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

Soos verwag, het ons nie daarin geslaag nie. Kom ons kyk na die fout in meer besonderhede:

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

Alles is reg: soos verwag, die span BPF_MAP_UPDATE_ELEM probeer om 'n nuwe, vyfde sleutel te skep, maar val om E2BIG.

Ons kan dus BPF-programme skep en laai, sowel as kaarte vanaf gebruikersruimte skep en bestuur. Nou is dit logies om te kyk hoe ons kaarte van die BPF-programme self kan gebruik. Ons kan hieroor praat in die taal van moeilik leesbare programme in masjienmakrokodes, maar eintlik het die tyd aangebreek om te wys hoe BPF-programme eintlik geskryf en onderhou word - deur gebruik te maak van libbpf.

(Vir lesers wat ontevrede is met die gebrek aan 'n lae-vlak voorbeeld: ons sal programme in detail ontleed wat kaarte en helperfunksies gebruik wat geskep is met libbpf en jou vertel wat op die onderrigvlak gebeur. Vir lesers wat ontevrede is baie, het ons bygevoeg Byvoorbeeld op die toepaslike plek in die artikel.)

Skryf van BPF-programme met libbpf

Die skryf van BPF-programme met masjienkodes kan net die eerste keer interessant wees, en dan tree versadiging in. Op hierdie oomblik moet jy jou aandag daarop vestig llvm, wat 'n backend het vir die generering van kode vir die BPF-argitektuur, sowel as 'n biblioteek libbpf, wat jou toelaat om die gebruikerskant van BPF-toepassings te skryf en die kode van BPF-programme te laai wat gegenereer word met llvm/clang.

Trouens, soos ons in hierdie en daaropvolgende artikels sal sien, libbpf doen nogal baie werk daarsonder (of soortgelyke gereedskap - iproute2, libbcc, libbpf-go, ens.) is dit onmoontlik om te lewe. Een van die moordende kenmerke van die projek libbpf is BPF CO-RE (Compile Once, Run Everywhere) - 'n projek wat jou toelaat om BPF-programme te skryf wat van een kern na 'n ander draagbaar is, met die vermoë om op verskillende API's te loop (byvoorbeeld wanneer die kernstruktuur van weergawe verander na weergawe). Om met CO-RE te kan werk, moet jou kern saamgestel word met BTF ondersteuning (ons beskryf hoe om dit te doen in die afdeling Ontwikkelingsgereedskap. U kan kyk of u kern met BTF gebou is of nie baie eenvoudig nie - deur die teenwoordigheid van die volgende lêer:

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

Hierdie lêer stoor inligting oor alle datatipes wat in die kern gebruik word en word in al ons voorbeelde gebruik libbpf. Ons sal in die volgende artikel breedvoerig oor CO-RE praat, maar in hierdie een - bou net vir jou 'n kern mee CONFIG_DEBUG_INFO_BTF.

Biblioteek libbpf woon reg in die gids tools/lib/bpf kern en die ontwikkeling daarvan word deur die poslys uitgevoer [email protected]. 'n Afsonderlike bewaarplek word egter onderhou vir die behoeftes van toepassings wat buite die kern woon https://github.com/libbpf/libbpf waarin die kernbiblioteek weerspieël word vir leestoegang min of meer soos dit is.

In hierdie afdeling sal ons kyk hoe jy 'n projek kan skep wat gebruik libbpf, kom ons skryf verskeie (min of meer betekenislose) toetsprogramme en ontleed in detail hoe dit alles werk. Dit sal ons in staat stel om in die volgende afdelings makliker te verduidelik presies hoe BPF-programme met kaarte, kernhelpers, BTF, ens.

Tipies projekte met behulp van libbpf voeg 'n GitHub-bewaarplek as 'n git-submodule by, ons sal dieselfde doen:

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

Gaan na libbpf baie eenvoudig:

$ 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

Ons volgende plan in hierdie afdeling is soos volg: ons sal 'n BPF-program skryf soos BPF_PROG_TYPE_XDP, dieselfde as in die vorige voorbeeld, maar in C stel ons dit saam met behulp van clang, en skryf 'n hulpprogram wat dit in die kern sal laai. In die volgende afdelings sal ons die vermoëns van beide die BPF-program en die assistentprogram uitbrei.

Voorbeeld: skep 'n volwaardige toepassing met libbpf

Om mee te begin, gebruik ons ​​die lêer /sys/kernel/btf/vmlinux, wat hierbo genoem is, en skep die ekwivalent daarvan in die vorm van 'n koplêer:

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

Hierdie lêer sal al die datastrukture wat in ons kern beskikbaar is, stoor, byvoorbeeld, dit is hoe die IPv4-opskrif in die kern gedefinieer word:

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

Nou sal ons ons BPF-program in C skryf:

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

Alhoewel ons program baie eenvoudig blyk te wees, moet ons steeds aandag gee aan baie besonderhede. Eerstens is die eerste koplêer wat ons insluit vmlinux.h, wat ons pas gegenereer het met behulp van bpftool btf dump - nou hoef ons nie die kernel-headers pakket te installeer om uit te vind hoe die kern strukture lyk nie. Die volgende koplêer kom van die biblioteek af na ons toe libbpf. Nou het ons dit net nodig om die makro te definieer SEC, wat die karakter na die toepaslike afdeling van die ELF-objeklêer stuur. Ons program is vervat in die afdeling xdp/simple, waar voor die skuinsstreep ons die programtipe BPF definieer - dit is die konvensie wat in libbpf, gebaseer op die afdeling se naam sal dit die korrekte tipe vervang by opstart bpf(2). Die BPF-program self is C - baie eenvoudig en bestaan ​​uit een lyn return XDP_PASS. Ten slotte, 'n aparte afdeling "license" bevat die naam van die lisensie.

Ons kan ons program saamstel met behulp van llvm/clang, weergawe >= 10.0.0, of nog beter, groter (sien afdeling Ontwikkelingsgereedskap):

$ 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

Onder die interessante kenmerke: ons dui die teikenargitektuur aan -target bpf en die pad na die kopstukke libbpf, wat ons onlangs geïnstalleer het. Moet ook nie van vergeet nie -O2, sonder hierdie opsie kan jy dalk in die toekoms vir verrassings wees. Kom ons kyk na ons kode, het ons daarin geslaag om die program te skryf wat ons wou hê?

$ 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

Ja, dit het gewerk! Nou het ons 'n binêre lêer met die program, en ons wil 'n toepassing skep wat dit in die kern sal laai. Vir hierdie doel die biblioteek libbpf bied ons twee opsies - gebruik 'n laer-vlak API of 'n hoër-vlak API. Ons sal die tweede pad gaan, aangesien ons wil leer hoe om BPF-programme met minimale moeite te skryf, te laai en te koppel vir hul daaropvolgende studie.

Eerstens moet ons die "geraamte" van ons program uit sy binêre genereer deur dieselfde nut te gebruik bpftool — die Switserse mes van die BPF-wêreld (wat letterlik opgeneem kan word, aangesien Daniel Borkman, een van die skeppers en handhawers van BPF, Switsers is):

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

In lêer xdp-simple.skel.h bevat die binêre kode van ons program en funksies vir die bestuur - laai, heg, verwyder ons voorwerp. In ons eenvoudige geval lyk dit na oordrewe, maar dit werk ook in die geval waar die objeklêer baie BPF-programme en -kaarte bevat en om hierdie reuse-ELF te laai, hoef ons net die skelet te genereer en een of twee funksies van die pasgemaakte toepassing op te roep. skryf Kom ons gaan nou aan.

Streng gesproke is ons laaiprogram onbenullig:

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

Hier struct xdp_simple_bpf gedefinieer in die lêer xdp-simple.skel.h en beskryf ons objeklêer:

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

Ons kan spore van 'n lae-vlak API hier sien: die struktuur struct bpf_program *simple и struct bpf_link *simple. Die eerste struktuur beskryf spesifiek ons ​​program, geskryf in die afdeling xdp/simple, en die tweede beskryf hoe die program aan die gebeurtenisbron koppel.

Funksie xdp_simple_bpf__open_and_load, maak 'n ELF-voorwerp oop, ontleed dit, skep al die strukture en substrukture (behalwe die program, bevat ELF ook ander afdelings - data, leesalleen-data, ontfoutingsinligting, lisensie, ens.), en laai dit dan in die kern met behulp van 'n stelsel bel bpf, wat ons kan nagaan deur die program saam te stel en uit te voer:

$ 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

Kom ons kyk nou na ons program met behulp van bpftool. Kom ons vind haar ID:

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

en dump (ons gebruik 'n verkorte vorm van die opdrag bpftool prog dump xlated):

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

Iets nuuts! Die program het stukke van ons C-bronlêer gedruk. Dit is deur die biblioteek gedoen libbpf, wat die ontfoutafdeling in die binêre gevind het, dit saamgestel het in 'n BTF-voorwerp, dit in die kern gelaai met BPF_BTF_LOAD, en het dan die resulterende lêerbeskrywing gespesifiseer wanneer die program met die opdrag gelaai word BPG_PROG_LOAD.

Kernhelpers

BPF-programme kan "eksterne" funksies uitvoer - kernhelpers. Hierdie hulpfunksies laat BPF-programme toe om toegang tot kernstrukture te kry, kaarte te bestuur en ook met die "regte wêreld" te kommunikeer - skep perf-gebeurtenisse, beheer hardeware (byvoorbeeld herlei pakkies), ens.

Voorbeeld: bpf_get_smp_processor_id

Binne die raamwerk van die "leer deur voorbeeld"-paradigma, kom ons kyk na een van die helperfunksies, bpf_get_smp_processor_id(), seker in lêer kernel/bpf/helpers.c. Dit gee die nommer van die verwerker terug waarop die BPF-program wat dit genoem het, loop. Maar ons is nie so geïnteresseerd in die semantiek daarvan as in die feit dat die implementering daarvan een lyn neem nie:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Die BPF-helperfunksiedefinisies is soortgelyk aan die Linux-stelseloproepdefinisies. Hier word byvoorbeeld 'n funksie gedefinieer wat geen argumente het nie. ('n Funksie wat byvoorbeeld drie argumente neem, word gedefinieer deur die makro te gebruik BPF_CALL_3. Die maksimum aantal argumente is vyf.) Dit is egter slegs die eerste deel van die definisie. Die tweede deel is om die tipe struktuur te definieer struct bpf_func_proto, wat 'n beskrywing bevat van die helperfunksie wat verifieerder verstaan:

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

Registreer helperfunksies

Ten einde BPF-programme van 'n bepaalde tipe hierdie funksie te gebruik, moet hulle dit registreer, byvoorbeeld vir die tipe BPF_PROG_TYPE_XDP 'n funksie word in die kern gedefinieer xdp_func_proto, wat uit die helperfunksie-ID bepaal of XDP hierdie funksie ondersteun of nie. Ons funksie is ondersteun:

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

Nuwe BPF-programtipes word in die lêer "gedefinieer". include/linux/bpf_types.h met behulp van 'n makro BPF_PROG_TYPE. Gedefinieer in aanhalingstekens omdat dit 'n logiese definisie is, en in C-taalterme kom die definisie van 'n hele stel konkrete strukture op ander plekke voor. In die besonder, in die lêer kernel/bpf/verifier.c alle definisies van lêer bpf_types.h word gebruik om 'n verskeidenheid strukture te skep 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
};

Dit wil sê, vir elke tipe BPF-program word 'n wyser na 'n datastruktuur van die tipe gedefinieer struct bpf_verifier_ops, wat geïnisialiseer word met die waarde _name ## _verifier_ops, d.w.s. xdp_verifier_ops vir xdp... Struktuur xdp_verifier_ops vasbeslote in lêer net/core/filter.c soos volg:

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

Hier sien ons ons bekende funksie xdp_func_proto, wat die verifieerder sal laat loop elke keer as dit 'n uitdaging teëkom een of ander aard funksies binne 'n BPF-program, sien verifier.c.

Kom ons kyk hoe 'n hipotetiese BPF-program die funksie gebruik bpf_get_smp_processor_id. Om dit te doen, herskryf ons die program van ons vorige afdeling soos volg:

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

Simbool bpf_get_smp_processor_id vasbeslote в <bpf/bpf_helper_defs.h> biblioteke libbpf hoe

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

dit wil sê, bpf_get_smp_processor_id is 'n funksiewyser waarvan die waarde 8 is, waar 8 die waarde is BPF_FUNC_get_smp_processor_id tipe enum bpf_fun_id, wat vir ons in die lêer gedefinieer word vmlinux.h (lêer bpf_helper_defs.h in die kern gegenereer word deur 'n script, so die "magic" nommers is ok). Hierdie funksie neem geen argumente nie en gee 'n waarde van tipe terug __u32. Wanneer ons dit in ons program laat loop, clang genereer 'n instruksie BPF_CALL "die regte soort" Kom ons stel die program saam en kyk na die afdeling 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

In die eerste reël sien ons instruksies call, parameter IMM wat gelyk is aan 8, en SRC_REG - nul. Volgens die ABI-ooreenkoms wat deur verifieerder gebruik word, is dit 'n oproep tot helperfunksie nommer agt. Sodra dit van stapel gestuur is, is die logika eenvoudig. Gee waarde uit register terug r0 gekopieer na r1 en op reël 2,3 word dit na tipe omgeskakel u32 — die boonste 32 bisse word uitgevee. Op reëls 4,5,6,7 gee ons terug 2 (XDP_PASS) of 1 (XDP_DROP) afhangende van of die helperfunksie vanaf reël 0 'n nul- of nie-nulwaarde teruggegee het.

Kom ons toets onsself: laai die program en kyk na die uitset bpftool prog dump xlated:

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

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

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

Ok, verifieerder het die korrekte kernhelper gevind.

Voorbeeld: slaag argumente en hardloop uiteindelik die program!

Alle loopvlak-helperfunksies het 'n prototipe

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

Parameters vir helperfunksies word in registers deurgegee r1-r5, en die waarde word in die register teruggestuur r0. Daar is geen funksies wat meer as vyf argumente neem nie, en daar word nie verwag dat ondersteuning daarvoor in die toekoms bygevoeg sal word nie.

Kom ons kyk na die nuwe kernhelper en hoe BPF parameters deurgee. Kom ons herskryf xdp-simple.bpf.c soos volg (die res van die reëls het nie verander nie):

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

Ons program druk die nommer van die SVE waarop dit loop. Kom ons stel dit saam en kyk na die kode:

$ 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

In reël 0-7 skryf ons die string running on CPU%un, en dan op reël 8 hardloop ons die bekende een bpf_get_smp_processor_id. Op reël 9-12 berei ons die helper-argumente voor bpf_printk - registers r1, r2, r3. Hoekom is daar drie van hulle en nie twee nie? Omdat bpf_printkdit is 'n makro-omhulsel rondom die ware helper bpf_trace_printk, wat die grootte van die formaatstring moet slaag.

Kom ons voeg nou 'n paar reëls by xdp-simple.csodat ons program aan die koppelvlak koppel lo en regtig begin!

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

Hier gebruik ons ​​die funksie bpf_set_link_xdp_fd, wat XDP-tipe BPF-programme met netwerkkoppelvlakke verbind. Ons het die koppelvlaknommer hardkodeer lo, wat altyd 1 is. Ons hardloop die funksie twee keer om eers die ou program los te maak as dit aangeheg was. Let daarop dat ons nou nie 'n uitdaging nodig het nie pause of 'n oneindige lus: ons laaiprogram sal uitgaan, maar die BPF-program sal nie doodgemaak word nie aangesien dit aan die gebeurtenisbron gekoppel is. Na suksesvolle aflaai en verbinding, sal die program geloods word vir elke netwerkpakkie wat by aankom lo.

Kom ons laai die program af en kyk na die koppelvlak 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

Die program wat ons afgelaai het, het ID 669 en ons sien dieselfde ID op die koppelvlak lo. Ons sal 'n paar pakkies aan stuur 127.0.0.1 (versoek + antwoord):

$ ping -c1 localhost

en kom ons kyk nou na die inhoud van die virtuele ontfoutlêer /sys/kernel/debug/tracing/trace_pipe, waarin bpf_printk skryf sy boodskappe:

# 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

Twee pakkies is opgemerk lo en verwerk op CPU0 - ons eerste volwaardige betekenislose BPF-program het gewerk!

Dit is opmerklik dat bpf_printk Dit is nie verniet dat dit na die ontfoutlêer skryf nie: dit is nie die mees suksesvolle helper vir gebruik in produksie nie, maar ons doel was om iets eenvoudig te wys.

Toegang tot kaarte vanaf BPF-programme

Voorbeeld: gebruik 'n kaart van die BPF-program

In die vorige afdelings het ons geleer hoe om kaarte vanuit gebruikersruimte te skep en te gebruik, en kom ons kyk nou na die kerndeel. Kom ons begin, soos gewoonlik, met 'n voorbeeld. Kom ons herskryf ons program xdp-simple.bpf.c soos volg:

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

Aan die begin van die program het ons 'n kaartdefinisie bygevoeg woo: Dit is 'n 8-element skikking wat waardes stoor soos u64 (in C sal ons so 'n skikking definieer as u64 woo[8]). In 'n program "xdp/simple" ons kry die huidige verwerkernommer in 'n veranderlike key en gebruik dan die helperfunksie bpf_map_lookup_element ons kry 'n wyser na die ooreenstemmende inskrywing in die skikking, wat ons met een vermeerder. In Russies vertaal: ons bereken statistieke oor watter SVE inkomende pakkies verwerk het. Kom ons probeer om die program te laat loop:

$ 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

Kom ons kyk of sy aan gekoppel is lo en stuur 'n paar pakkies:

$ 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

Kom ons kyk nou na die inhoud van die skikking:

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

Byna alle prosesse is op CPU7 verwerk. Dit is nie vir ons belangrik nie, die belangrikste ding is dat die program werk en ons verstaan ​​hoe om toegang tot kaarte van BPF-programme te kry - met behulp van хелперов bpf_mp_*.

Mistieke indeks

Dus, ons kan toegang tot die kaart kry vanaf die BPF-program deur oproepe soos

val = bpf_map_lookup_elem(&woo, &key);

waar die helperfunksie lyk

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

maar ons gee 'n wyser deur &woo na 'n naamlose struktuur struct { ... }...

As ons kyk na die program assembler, sien ons dat die waarde &woo is nie eintlik gedefinieer nie (reël 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
...

en is vervat in verskuiwings:

$ 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

Maar as ons na die reeds gelaaide program kyk, sien ons 'n wyser na die korrekte kaart (reël 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]
...

Ons kan dus tot die gevolgtrekking kom dat ten tyde van die bekendstelling van ons laaiprogram, die skakel na &woo is vervang deur iets met 'n biblioteek libbpf. Eerstens kyk ons ​​na die uitset 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

Ons sien dit libbpf 'n kaart geskep woo en dan ons program afgelaai simple. Kom ons kyk van naderby na hoe ons die program laai:

  • bel xdp_simple_bpf__open_and_load uit lêer xdp-simple.skel.h
  • wat veroorsaak xdp_simple_bpf__load uit lêer xdp-simple.skel.h
  • wat veroorsaak bpf_object__load_skeleton uit lêer libbpf/src/libbpf.c
  • wat veroorsaak bpf_object__load_xattr van libbpf/src/libbpf.c

Die laaste funksie sal onder andere oproep bpf_object__create_maps, wat bestaande kaarte skep of oopmaak en dit in lêerbeskrywers verander. (Dit is waar ons sien BPF_MAP_CREATE in die uitset strace.) Vervolgens word die funksie genoem bpf_object__relocate en dit is sy wat ons interesseer, aangesien ons onthou wat ons gesien het woo in die hervestigingstabel. Deur dit te verken, vind ons onsself uiteindelik in die funksie bpf_program__relocate, watter handel oor kaartverskuiwings:

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

So ons neem ons instruksies

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

en vervang die bronregister daarin met BPF_PSEUDO_MAP_FD, en die eerste IMM na die lêerbeskrywer van ons kaart en, as dit gelyk is aan, byvoorbeeld, 0xdeadbeef, dan sal ons as gevolg daarvan die opdrag ontvang

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

Dit is hoe kaartinligting na 'n spesifieke gelaaide BPF-program oorgedra word. In hierdie geval kan die kaart geskep word met behulp van BPF_MAP_CREATE, en oopgemaak deur ID gebruik BPF_MAP_GET_FD_BY_ID.

Totaal, by gebruik libbpf die algoritme is soos volg:

  • tydens samestelling word rekords in die hervestigingstabel geskep vir skakels na kaarte
  • libbpf maak die ELF-objekboek oop, vind alle gebruikte kaarte en skep lêerbeskrywings daarvoor
  • lêerbeskrywers word in die kern gelaai as deel van die instruksie LD64

Soos jy jou kan voorstel, is daar meer om te kom en ons sal na die kern moet kyk. Gelukkig het ons 'n idee – ons het die betekenis neergeskryf BPF_PSEUDO_MAP_FD in die bronneregister en ons kan dit begrawe, wat ons sal lei na die heilige van alle heiliges - kernel/bpf/verifier.c, waar 'n funksie met 'n kenmerkende naam 'n lêerbeskrywer vervang met die adres van 'n tipe struktuur 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;

(volledige kode kan gevind word по ссылке). So ons kan ons algoritme uitbrei:

  • terwyl die program gelaai word, kontroleer die verifieerder die korrekte gebruik van die kaart en skryf die adres van die ooreenstemmende struktuur struct bpf_map

By die aflaai van die ELF binêre met behulp van libbpf Daar is baie meer aan die gang, maar ons sal dit in ander artikels bespreek.

Laai programme en kaarte sonder libbpf

Soos belowe, hier is 'n voorbeeld vir lesers wat wil weet hoe om 'n program te skep en te laai wat kaarte gebruik, sonder hulp libbpf. Dit kan nuttig wees wanneer jy in 'n omgewing werk waarvoor jy nie afhanklikhede kan bou nie, of elke stukkie kan stoor, of 'n program skryf soos ply, wat BPF binêre kode op die vlieg genereer.

Om dit makliker te maak om die logika te volg, sal ons ons voorbeeld vir hierdie doeleindes herskryf xdp-simple. Die volledige en effens uitgebreide kode van die program wat in hierdie voorbeeld bespreek word, kan hierin gevind word GIST.

Die logika van ons aansoek is soos volg:

  • skep 'n tipe kaart BPF_MAP_TYPE_ARRAY met behulp van die opdrag BPF_MAP_CREATE,
  • skep 'n program wat hierdie kaart gebruik,
  • koppel die program aan die koppelvlak lo,

wat vertaal in menslike as

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

Hier map_create skep 'n kaart op dieselfde manier as wat ons in die eerste voorbeeld oor die stelseloproep gedoen het bpf - "kern, maak asseblief vir my 'n nuwe kaart in die vorm van 'n verskeidenheid van 8 elemente soos __u64 en gee my die lêerbeskrywing terug":

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

Die program is ook maklik om te laai:

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

Die moeilike deel prog_load is die definisie van ons BPF-program as 'n verskeidenheid strukture struct bpf_insn insns[]. Maar aangesien ons 'n program gebruik wat ons in C het, kan ons 'n bietjie kul:

$ 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

In totaal moet ons 14 instruksies skryf in die vorm van strukture soos struct bpf_insn (raad: neem die storting van bo af, lees weer die instruksies-afdeling, maak oop linux/bpf.h и linux/bpf_common.h en probeer vasstel struct bpf_insn insns[] op jou eie):

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

'n Oefening vir diegene wat dit nie self geskryf het nie - vind map_fd.

Daar is nog een onbekende deel oor in ons program - xdp_attach. Ongelukkig kan programme soos XDP nie met 'n stelseloproep gekoppel word nie bpf. Die mense wat BPF en XDP geskep het, was van die aanlyn Linux-gemeenskap, wat beteken dat hulle die een gebruik het wat hulle die meeste bekend was (maar nie om normaal mense) koppelvlak vir interaksie met die kern: netlink-sokke, sien ook RFC3549. Die eenvoudigste manier om te implementeer xdp_attach kopieer kode vanaf libbpf, naamlik uit die lêer netlink.c, wat is wat ons gedoen het, en dit 'n bietjie verkort:

Welkom by die wêreld van netlink-sokke

Maak 'n tipe netlink-sok oop 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;
}

Ons lees uit hierdie aansluiting:

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

Ten slotte, hier is ons funksie wat 'n sok oopmaak en 'n spesiale boodskap daaraan stuur wat 'n lêerbeskrywing bevat:

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

So, alles is gereed vir toetsing:

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

Kom ons kyk of ons program gekoppel is aan 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

Kom ons stuur pings en kyk na die kaart:

$ 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

Hoera, alles werk. Let terloops daarop dat ons kaart weer in die vorm van grepe vertoon word. Dit is te wyte aan die feit dat, anders as libbpf ons het nie tipe inligting (BTF) gelaai nie. Maar ons sal volgende keer meer hieroor praat.

Ontwikkelingsgereedskap

In hierdie afdeling sal ons kyk na die minimum BPF-ontwikkelaarshulpmiddelstel.

Oor die algemeen het jy niks spesiaals nodig om BPF-programme te ontwikkel nie - BPF loop op enige ordentlike verspreidingskern, en programme word gebou met clang, wat uit die pakket verskaf kan word. As gevolg van die feit dat BPF onder ontwikkeling is, verander die kern en gereedskap voortdurend, as jy nie BPF-programme met outydse metodes vanaf 2019 wil skryf nie, dan sal jy moet saamstel

  • llvm/clang
  • pahole
  • sy kern
  • bpftool

(Vir verwysing, hierdie afdeling en alle voorbeelde in die artikel is op Debian 10 uitgevoer.)

llvm/clang

BPF is vriendelik met LLVM en hoewel programme vir BPF onlangs met behulp van gcc saamgestel kan word, word alle huidige ontwikkeling vir LLVM uitgevoer. Daarom sal ons eerstens die huidige weergawe bou clang van 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
... много времени спустя
$

Nou kan ons kyk of alles reg bymekaar gekom het:

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

(Monteer-instruksies clang deur my geneem uit bpf_devel_QA.)

Ons sal nie die programme wat ons sopas gebou het installeer nie, maar dit net byvoeg PATH, byvoorbeeld:

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

(Dit kan bygevoeg word .bashrc of na 'n aparte lêer. Persoonlik voeg ek sulke dinge by ~/bin/activate-llvm.sh en wanneer nodig doen ek dit . activate-llvm.sh.)

Pahole en BTF

Nuts pahole gebruik wanneer die kern gebou word om ontfoutingsinligting in BTF-formaat te skep. Ons gaan nie in hierdie artikel in detail oor die besonderhede van BTF-tegnologie nie, behalwe die feit dat dit gerieflik is en ons dit wil gebruik. So as jy jou kern gaan bou, bou eers pahole (sonder pahole jy sal nie die kern met die opsie kan bou nie 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

Pitte om met BPF te eksperimenteer

Wanneer ek die moontlikhede van BPF ondersoek, wil ek my eie kern saamstel. Dit is oor die algemeen nie nodig nie, aangesien jy BPF-programme op die verspreidingskern sal kan saamstel en laai, maar met jou eie kern kan jy die nuutste BPF-kenmerke gebruik, wat op sy beste in jou verspreiding sal verskyn , of, soos in die geval van sommige ontfoutingsinstrumente, sal glad nie in die afsienbare toekoms verpak word nie. Sy eie kern laat dit ook belangrik voel om met die kode te eksperimenteer.

Om 'n kern te bou, benodig jy eerstens die kern self, en tweedens 'n kernkonfigurasielêer. Om met BPF te eksperimenteer kan ons die gewone gebruik vanielje kern of een van die ontwikkelingskerne. Histories vind BPF-ontwikkeling binne die Linux-netwerkgemeenskap plaas en daarom gaan alle veranderinge vroeër of later deur David Miller, die Linux-netwerkonderhouer. Afhangende van hul aard - wysigings of nuwe kenmerke - val netwerkveranderings in een van twee kerne - net of net-next. Veranderinge vir BPF word op dieselfde manier tussen versprei bpf и bpf-next, wat dan onderskeidelik in netto en netto-volgende saamgevoeg word. Vir meer besonderhede, sien bpf_devel_QA и netdev-Gereelde Vrae. Kies dus 'n pit gebaseer op jou smaak en die stabiliteitsbehoeftes van die stelsel waarop jy toets (*-next pitte is die mees onstabiele van dié wat gelys is).

Dit is buite die bestek van hierdie artikel om te praat oor hoe om kernkonfigurasielêers te bestuur - daar word aanvaar dat jy óf reeds weet hoe om dit te doen, óf gereed om te leer op jou eie. Die volgende instruksies behoort egter min of meer genoeg te wees om vir jou 'n werkende BPF-geaktiveerde stelsel te gee.

Laai een van die bogenoemde pitte af:

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

Bou 'n minimale werkende kernkonfigurasie:

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

Aktiveer BPF-opsies in lêer .config van jou eie keuse (heel waarskynlik CONFIG_BPF sal reeds geaktiveer wees aangesien systemd dit gebruik). Hier is 'n lys opsies uit die kern wat vir hierdie artikel gebruik word:

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

Dan kan ons die modules en die kern maklik saamstel en installeer (terloops, jy kan die kern saamstel met die nuut saamgestelde clangdeur by te voeg CC=clang):

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

en herlaai met die nuwe kern (ek gebruik hiervoor kexec uit die pakkie 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

Die nut wat die meeste in die artikel gebruik word, is die nut bpftool, verskaf as deel van die Linux-kern. Dit is geskryf en onderhou deur BPF-ontwikkelaars vir BPF-ontwikkelaars en kan gebruik word om alle soorte BPF-voorwerpe te bestuur - laai programme, skep en redigeer kaarte, verken die lewe van die BPF-ekosisteem, ens. Dokumentasie in die vorm van bronkodes vir manbladsye kan gevind word in die kern of, reeds saamgestel, op die net.

Ten tyde van hierdie skrywe bpftool kom net gereed gemaak vir RHEL, Fedora en Ubuntu (sien bv. hierdie draad, wat die onvoltooide storie van verpakking vertel bpftool in Debian). Maar as jy reeds jou kern gebou het, bou dan bpftool so maklik soos 'n koek:

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

$

(Hier ${linux} - dit is jou kerngids.) Nadat u hierdie opdragte uitgevoer het bpftool sal in 'n gids versamel word ${linux}/tools/bpf/bpftool en dit kan by die pad gevoeg word (in die eerste plek vir die gebruiker root) of kopieer net na /usr/local/sbin.

Versamel bpftool dit is die beste om laasgenoemde te gebruik clang, saamgestel soos hierbo beskryf, en kyk of dit korrek saamgestel is - deur byvoorbeeld die opdrag te gebruik

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

wat sal wys watter BPF-kenmerke in jou kern geaktiveer is.

Terloops, die vorige opdrag kan uitgevoer word as

# bpftool f p k

Dit word gedoen in analogie met die nutsprogramme uit die pakket iproute2, waar ons byvoorbeeld kan sê ip a s eth0 in plaas van ip addr show dev eth0.

Gevolgtrekking

BPF laat jou toe om 'n vlooi te skoen om die funksionaliteit van die kern effektief te meet en te verander. Die stelsel blyk baie suksesvol te wees, in die beste tradisies van UNIX: 'n eenvoudige meganisme wat jou toelaat om die kern te (her)programmeer, het 'n groot aantal mense en organisasies toegelaat om te eksperimenteer. En hoewel die eksperimente, sowel as die ontwikkeling van die BPF-infrastruktuur self, nog lank nie klaar is nie, het die stelsel reeds 'n stabiele ABI wat jou toelaat om betroubare, en bowenal, effektiewe besigheidslogika te bou.

Ek wil daarop let dat, na my mening, die tegnologie so gewild geword het omdat dit aan die een kant kan играть (die argitektuur van 'n masjien kan min of meer in een aand verstaan ​​word), en andersyds om probleme op te los wat nie (pragtig) voor sy verskyning opgelos kon word nie. Hierdie twee komponente saam dwing mense om te eksperimenteer en te droom, wat lei tot die ontstaan ​​van meer en meer innoverende oplossings.

Hierdie artikel, hoewel nie besonder kort nie, is slegs 'n inleiding tot die wêreld van BPF en beskryf nie "gevorderde" kenmerke en belangrike dele van die argitektuur nie. Die plan vorentoe is iets soos hierdie: die volgende artikel sal 'n oorsig wees van BPF-programtipes (daar is 5.8 programtipes wat in die 30-kern ondersteun word), dan sal ons uiteindelik kyk hoe om regte BPF-toepassings te skryf deur kernopsporingsprogramme te gebruik as 'n voorbeeld, dan is dit tyd vir 'n meer in-diepte kursus oor BPF-argitektuur, gevolg deur voorbeelde van BPF-netwerk- en sekuriteitstoepassings.

Vorige artikels in hierdie reeks

  1. BPF vir die kleintjies, deel nul: klassieke BPF

Skakels

  1. BPF en XDP Verwysingsgids — dokumentasie oor BPF vanaf cilium, of meer presies van Daniel Borkman, een van die skeppers en instandhouers van BPF. Dit is een van die eerste ernstige beskrywings, wat van die ander verskil deurdat Daniël presies weet waaroor hy skryf en daar is geen foute daar nie. In die besonder beskryf hierdie dokument hoe om met BPF-programme van die XDP- en TC-tipes te werk deur die bekende nutsmiddel te gebruik ip uit die pakkie iproute2.

  2. Dokumentasie/netwerk/filter.txt — oorspronklike lêer met dokumentasie vir klassieke en dan uitgebreide BPF. 'n Goeie leesstuk as jy wil delf in samestellingstaal en tegniese argitektoniese besonderhede.

  3. Blog oor BPF vanaf Facebook. Dit word selde, maar gepas opgedateer, soos Alexei Starovoitov (skrywer van eBPF) en Andrii Nakryiko - (onderhouer) daar skryf libbpf).

  4. Geheime van bpftool. 'n Vermaaklike Twitter-draad van Quentin Monnet met voorbeelde en geheime van die gebruik van bpftool.

  5. Duik in BPF: 'n lys leesstof. 'n Reuse (en steeds in stand gehou) lys van skakels na BPF-dokumentasie van Quentin Monnet.

Bron: will.com

Voeg 'n opmerking