BPF por la etuloj, unua parto: plilongigita BPF

En la komenco ekzistis teknologio kaj ĝi estis nomita BPF. Ni rigardis ŝin antaŭa, Malnovtestamenta artikolo de ĉi tiu serio. En 2013, per la klopodoj de Alexei Starovoitov kaj Daniel Borkman, plibonigita versio de ĝi, optimumigita por modernaj 64-bitaj maŝinoj, estis evoluigita kaj inkludita en la Linukso-kerno. Ĉi tiu nova teknologio estis mallonge nomita Internal BPF, tiam renomita Extended BPF, kaj nun, post pluraj jaroj, ĉiuj simple nomas ĝin BPF.

Malglate parolante, BPF permesas ruli arbitran uzant-provizitan kodon en la Linukso-kernspaco, kaj la nova arkitekturo rezultis tiel sukcesa ke ni bezonos dekduon pliajn artikolojn por priskribi ĉiujn ĝiajn aplikojn. (La nura afero, kiun la programistoj ne faris bone, kiel vi povas vidi en la agado-kodo sube, estis krei decan emblemon.)

Ĉi tiu artikolo priskribas la strukturon de la virtuala maŝino de BPF, kerninterfacojn por labori kun BPF, evoluilojn, same kiel mallongan, tre mallongan superrigardon de ekzistantaj kapabloj, t.e. ĉion, kion ni bezonos en la estonteco por pli profunda studo de la praktikaj aplikoj de BPF.
BPF por la etuloj, unua parto: plilongigita BPF

Resumo de la artikolo

Enkonduko al BPF-arkitekturo. Unue, ni prenos birdan vidon de la BPF-arkitekturo kaj skizos la ĉefajn komponantojn.

Registroj kaj komandsistemo de la BPF virtuala maŝino. Jam havante ideon pri la arkitekturo kiel tuto, ni priskribos la strukturon de la virtuala maŝino de BPF.

Vivciklo de BPF-objektoj, bpffs-dosiersistemo. En ĉi tiu sekcio, ni rigardos pli detale la vivociklon de BPF-objektoj - programoj kaj mapoj.

Administri objektojn uzante la bpf sistemvokon. Kun iom da kompreno pri la sistemo jam en loko, ni finfine rigardos kiel krei kaj manipuli objektojn el uzantspaco uzante specialan sistemvokon − bpf(2).

Пишем программы BPF с помощью libbpf. Kompreneble, vi povas skribi programojn uzante sistemvokon. Sed estas malfacile. Por pli realisma scenaro, nukleaj programistoj evoluigis bibliotekon libbpf. Ni kreos bazan aplikaĵoskeleton de BPF, kiun ni uzos en postaj ekzemploj.

Kernelhelpantoj. Ĉi tie ni lernos kiel BPF-programoj povas aliri kernajn helpajn funkciojn - ilo kiu, kune kun mapoj, fundamente vastigas la kapablojn de la nova BPF kompare kun la klasika.

Aliro al mapoj de BPF-programoj. Je ĉi tiu punkto, ni scios sufiĉe por kompreni ĝuste kiel ni povas krei programojn kiuj uzas mapojn. Kaj ni eĉ rigardu la bonegan kaj potencan kontrolilon.

Disvolvaj iloj. Helpsekcio pri kiel kunmeti la postulatajn ilojn kaj kernon por eksperimentoj.

La konkludo. Fine de la artikolo, tiuj, kiuj ĉi tie legas, trovos instigajn vortojn kaj mallongan priskribon pri tio, kio okazos en la sekvaj artikoloj. Ni ankaŭ listigos kelkajn ligilojn por memstudado por tiuj, kiuj ne havas la deziron aŭ kapablon atendi la daŭrigon.

Enkonduko al BPF-Arkitekturo

Antaŭ ol ni komencu konsideri la BPF-arkitekturon, ni aludos la lastan fojon (ho). klasika BPF, kiu estis evoluigita kiel respondo al la apero de RISC-maŝinoj kaj solvis la problemon de efika pakaĵetfiltrado. La arkitekturo montriĝis tiel sukcesa ke, naskiĝinte en la impetaj naŭdekaj en Berkeley UNIX, ĝi estis adaptita al la plej multaj ekzistantaj operaciumoj, pluvivis ĝis la frenezaj dudekaj kaj daŭre trovas novajn aplikojn.

La nova BPF estis evoluigita kiel respondo al la ĉieeco de 64-bitaj maŝinoj, nubaj servoj kaj la pliigita bezono de iloj por krei SDN (Softaro-drafinita networking). Disvolvita de kernaj retaj inĝenieroj kiel plibonigita anstataŭaĵo por la klasika BPF, la nova BPF laŭvorte ses monatojn poste trovis aplikojn en la malfacila tasko spuri Linuksajn sistemojn, kaj nun, ses jarojn post ĝia apero, ni bezonos tutan sekvan artikolon nur por listigu la malsamajn specojn de programoj.

Amuzaj bildoj

En ĝia kerno, BPF estas sablokesto virtuala maŝino kiu permesas vin ruli "arbitran" kodon en kernspaco sen endanĝerigi sekurecon. BPF-programoj estas kreitaj en uzantspaco, ŝarĝitaj en la kernon, kaj konektitaj al iu okazaĵfonto. Okazaĵo povus esti, ekzemple, la livero de pako al reto-interfaco, la lanĉo de iu kernfunkcio, ktp. Kaze de pakaĵo, la BPF-programo havos aliron al la datumoj kaj metadatenoj de la pakaĵo (por legado kaj, eble, skribo, depende de la speco de programo); en la kazo de rulado de kerna funkcio, la argumentoj de la funkcio, inkluzive de montriloj al kernmemoro, ktp.

Ni rigardu pli detale ĉi tiun procezon. Komence, ni parolu pri la unua diferenco de la klasika BPF, por kiuj programoj estis skribitaj en asemblero. En la nova versio, la arkitekturo estis vastigita tiel ke programoj povus esti skribitaj en altnivelaj lingvoj, ĉefe, kompreneble, en C. Por tio, backend por llvm estis evoluigita, kiu ebligas al vi generi bajtkodon por la BPF-arkitekturo.

BPF por la etuloj, unua parto: plilongigita BPF

La BPF-arkitekturo estis dizajnita, delvis, por funkcii efike per modernaj maŝinoj. Por ke ĉi tio funkciu praktike, la BPF-bajtokodo, post kiam ŝarĝita en la kernon, estas tradukita en indiĝenan kodon uzante komponenton nomitan JIT-kompililo (JUst In Time). Poste, se vi memoras, en klasika BPF la programo estis ŝarĝita en la kernon kaj ligita al la okazaĵfonto atome - en la kunteksto de ununura sistemvoko. En la nova arkitekturo, tio okazas en du stadioj - unue, la kodo estas ŝarĝita en la kernon per sistema voko. bpf(2)kaj poste, poste, per aliaj mekanismoj, kiuj varias laŭ la tipo de programo, la programo aliĝas al la okazaĵfonto.

Ĉi tie la leganto eble havas demandon: ĉu ĝi eblas? Kiel estas la ekzekutsekureco de tia kodo garantiita? Sekureco de ekzekuto estas garantiita al ni per la etapo de ŝarĝo de BPF-programoj nomitaj kontrolilo (en la angla ĉi tiu etapo nomiĝas kontrolilo kaj mi daŭre uzos la anglan vorton):

BPF por la etuloj, unua parto: plilongigita BPF

Verifier estas senmova analizilo, kiu certigas, ke programo ne interrompas la normalan funkciadon de la kerno. Ĉi tio, cetere, ne signifas, ke la programo ne povas malhelpi la funkciadon de la sistemo - BPF-programoj, depende de la tipo, povas legi kaj reverki sekciojn de kernmemoro, redoni valorojn de funkcioj, tranĉi, aldoni, reverki. kaj eĉ plusendas retajn pakaĵojn. Verifier garantias, ke ruli BPF-programon ne kraŝos la kernon kaj ke programo kiu, laŭ la reguloj, havas skriban aliron, ekzemple, la datumojn de eksiĝinta pako, ne povos anstataŭigi la kernan memoron ekster la pako. Ni rigardos kontrolilon iom pli detale en la responda sekcio, post kiam ni konatiĝos kun ĉiuj aliaj komponantoj de BPF.

Kion do ni lernis ĝis nun? La uzanto skribas programon en C, ŝarĝas ĝin en la kernon uzante sisteman vokon bpf(2), kie ĝi estas kontrolita de kontrolilo kaj tradukita en indiĝenan bajtkodon. Tiam la sama aŭ alia uzanto konektas la programon al la okazaĵfonto kaj ĝi komencas ekzekuti. Apartigi lanĉon kaj konekton necesas pro pluraj kialoj. Unue, ruli kontrolilon estas relative multekosta kaj elŝutante la saman programon plurfoje ni malŝparas komputilan tempon. Due, ĝuste kiel programo estas konektita dependas de ĝia tipo, kaj unu "universala" interfaco evoluigita antaŭ jaro eble ne taŭgas por novaj specoj de programoj. (Kvankam nun ke la arkitekturo fariĝas pli matura, ekzistas ideo unuigi ĉi tiun interfacon je la nivelo libbpf.)

La atentema leganto eble rimarkos, ke ni ankoraŭ ne finis kun la bildoj. Efektive, ĉio ĉi-supra ne klarigas kial BPF esence ŝanĝas la bildon kompare kun klasika BPF. Du novigoj kiuj signife vastigas la amplekson de aplikebleco estas la kapablo uzi komunan memoron kaj kernhelpajn funkciojn. En BPF, komuna memoro estas efektivigita per tielnomitaj mapoj - komunaj datenstrukturoj kun specifa API. Ili verŝajne ricevis ĉi tiun nomon ĉar la unua speco de mapo aperinta estis hashtabelo. Tiam aperis tabeloj, lokaj (po-CPU) hashtabeloj kaj lokaj tabeloj, serĉarboj, mapoj enhavantaj montrilojn al BPF-programoj kaj multe pli. Kio estas interesa por ni nun estas ke BPF-programoj nun havas la kapablon persisti staton inter vokoj kaj dividi ĝin kun aliaj programoj kaj kun uzantspaco.

Mapoj estas aliritaj de uzantprocezoj uzante sistemvokon bpf(2), kaj de BPF-programoj kurantaj en la kerno uzante helpajn funkciojn. Krome, helpantoj ekzistas ne nur por labori kun mapoj, sed ankaŭ por aliri aliajn kernajn kapablojn. Ekzemple, BPF-programoj povas uzi helpajn funkciojn por plusendi pakaĵetojn al aliaj interfacoj, generi perf-okazaĵojn, aliri kernstrukturojn, ktp.

BPF por la etuloj, unua parto: plilongigita BPF

En resumo, BPF disponigas la kapablon ŝarĝi arbitran, t.e., kontrolil-testitan, uzantkodon en kernspacon. Ĉi tiu kodo povas ŝpari staton inter vokoj kaj interŝanĝi datumojn kun uzantspaco, kaj ankaŭ havas aliron al kernaj subsistemoj permesitaj de ĉi tiu speco de programo.

Ĉi tio jam similas al la kapabloj provizitaj de kernaj moduloj, kompare kun kiuj BPF havas iujn avantaĝojn (kompreneble, vi povas nur kompari similajn aplikojn, ekzemple, sisteman spuradon - vi ne povas skribi arbitran pelilon kun BPF). Vi povas noti pli malaltan enirsojlon (kelkaj utilecoj, kiuj uzas BPF, ne postulas, ke la uzanto havu kernajn programajn kapablojn, aŭ programkapablojn ĝenerale), rultempan sekurecon (levu vian manon en la komentoj por tiuj, kiuj ne rompis la sistemon skribante. aŭ testado de moduloj), atomeco - ekzistas malfunkcio dum reŝargado de moduloj, kaj la BPF-subsistemo certigas ke neniuj okazaĵoj estas sopiritaj (por esti juste, tio ne estas vera por ĉiuj specoj de BPF-programoj).

La ĉeesto de tiaj kapabloj igas BPF universala ilo por vastigi la kernon, kio estas konfirmita en la praktiko: pli kaj pli da novaj specoj de programoj estas aldonitaj al BPF, pli kaj pli da grandaj kompanioj uzas BPF sur batalserviloj 24×7, pli kaj pli. noventreprenoj konstruas sian komercon sur solvoj bazitaj sur kiuj baziĝas sur BPF. BPF estas uzata ĉie: en protektado kontraŭ DDoS-atakoj, kreado de SDN (ekzemple, efektivigado de retoj por kubernetoj), kiel la ĉefa sistema spura ilo kaj statistika kolektanto, en entrudiĝaj detektsistemoj kaj sandbox-sistemoj, ktp.

Ni finu la superrigardan parton de la artikolo ĉi tie kaj rigardu la virtualan maŝinon kaj la BPF-ekosistemon pli detale.

Digreso: utilecoj

Por povi ruli la ekzemplojn en la sekvaj sekcioj, vi eble bezonos kelkajn ilojn, almenaŭ llvm/clang kun bpf-subteno kaj bpftool. En la sekcio Disvolvaj Iloj Vi povas legi la instrukciojn por kunmeti la ilojn, same kiel vian kernon. Ĉi tiu sekcio estas metita sube por ne ĝeni la harmonion de nia prezento.

BPF Virtual Machine Registers kaj Instrukcia Sistemo

La arkitekturo kaj komandsistemo de BPF estis evoluigitaj konsiderante la fakton, ke programoj estos skribitaj en la C-lingvo kaj, post ŝarĝo en la kernon, tradukitaj en denaskan kodon. Tial, la nombro da registroj kaj la aro de komandoj estis elektitaj kun okulo al la intersekco, en la matematika signifo, de la kapabloj de modernaj maŝinoj. Krome, diversaj limigoj estis truditaj al programoj, ekzemple, ĝis antaŭ nelonge ne eblis skribi buklojn kaj subrutinojn, kaj la nombro da instrukcioj estis limigita al 4096 (nun privilegiitaj programoj povas ŝargi ĝis miliono da instrukcioj).

BPF havas dek unu uzant-alireblajn 64-bitajn registrojn r0-r10 kaj programkalkulilo. Registru r10 enhavas kadran montrilon kaj estas nurlegebla. Programoj havas aliron al 512-bajta stako ĉe rultempo kaj senliman kvanton de komuna memoro en formo de mapoj.

BPF-programoj rajtas ruli specifan aron de program-specaj kernaj helpantoj kaj, pli lastatempe, regulajn funkciojn. Ĉiu nomita funkcio povas preni ĝis kvin argumentoj, pasitaj en registroj r1-r5, kaj la revena valoro estas transdonita al r0. Estas garantiite, ke post reveno de la funkcio, la enhavo de la registroj r6-r9 Ne ŝanĝiĝos.

Por efika programtradukado, registriĝas r0-r11 ĉar ĉiuj subtenataj arkitekturoj estas unike mapitaj al realaj registroj, konsiderante la ABI-ecojn de la nuna arkitekturo. Ekzemple, por x86_64 registroj r1-r5, uzataj por pasi funkcio-parametrojn, estas montrataj rdi, rsi, rdx, rcx, r8, kiuj estas uzataj por transdoni parametrojn al funkcioj x86_64. Ekzemple, la kodo maldekstre tradukiĝas al la kodo dekstre jene:

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

Registriĝi r0 ankaŭ uzata por redoni la rezulton de programo ekzekuto, kaj en la registro r1 la programo estas pasigita montrilo al la kunteksto - depende de la speco de programo, tio povus esti, ekzemple, strukturo struct xdp_md (por XDP) aŭ strukturo struct __sk_buff (por malsamaj retprogramoj) aŭ strukturo struct pt_regs (por malsamaj specoj de spurprogramoj), ktp.

Do, ni havis aron da registroj, kernaj helpantoj, stako, kunteksta montrilo kaj komuna memoro en formo de mapoj. Ne ke ĉio ĉi estas absolute necesa dum la vojaĝo, sed...

Ni daŭrigu la priskribon kaj parolu pri la komanda sistemo por labori kun ĉi tiuj objektoj. Ĉiuj (Preskaŭ ĉio) BPF-instrukcioj havas fiksan 64-bitan grandecon. Se vi rigardas unu instrukcion pri 64-bita Big Endian-maŝino, vi vidos

BPF por la etuloj, unua parto: plilongigita BPF

estas Code - ĉi tio estas la kodigo de la instrukcio, Dst/Src estas la kodigoj de la ricevilo kaj fonto, respektive, Off - 16-bita subskribita indentaĵo, kaj Imm estas 32-bita signita entjero uzita en kelkaj instrukcioj (simila al la cBPF-konstanto K). Kodigado Code havas unu el du tipoj:

BPF por la etuloj, unua parto: plilongigita BPF

Instrukciaj klasoj 0, 1, 2, 3 difinas komandojn por labori kun memoro. Ili nomiĝas, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, respektive. Klasoj 4, 7 (BPF_ALU, BPF_ALU64) konsistigas aron de ALU-instrukcioj. Klasoj 5, 6 (BPF_JMP, BPF_JMP32) enhavas saltinstrukciojn.

La plua plano por studi la BPF-instruksistemon estas jena: anstataŭ zorge listigi ĉiujn instrukciojn kaj iliajn parametrojn, ni rigardos kelkajn ekzemplojn en ĉi tiu sekcio kaj el ili evidentiĝos kiel la instrukcioj efektive funkcias kaj kiel fari. permane malmunti ajnan binaran dosieron por BPF. Por plifirmigi la materialon poste en la artikolo, ni ankaŭ renkontos individuajn instrukciojn en la sekcioj pri Kontrolilo, JIT-kompililo, traduko de klasika BPF, kaj ankaŭ dum studado de mapoj, vokado de funkcioj ktp.

Kiam ni parolas pri individuaj instrukcioj, ni raportos al la kernaj dosieroj bpf.h и bpf_common.h, kiuj difinas la nombrajn kodojn de BPF-instrukciaĵo. Studante arkitekturon memstare kaj/aŭ analizante binarojn, vi povas trovi semantikon en la sekvaj fontoj, ordigitaj en ordo de komplekseco: Neoficiala eBPF-specifo, BPF kaj XDP Referenca Gvidilo, Instrukcio, Dokumentado/reto/filtrilo.txt kaj, kompreneble, en la Linukso fontkodo - kontrolilo, JIT, BPF interpretisto.

Ekzemplo: malmunti BPF en via kapo

Ni rigardu ekzemplon en kiu ni kompilas programon readelf-example.c kaj rigardu la rezultan binaron. Ni malkaŝos la originalan enhavon readelf-example.c sube, post kiam ni restarigas ĝian logikon de binaraj kodoj:

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

Unua kolumno en eligo readelf estas deŝovo kaj nia programo do konsistas el kvar komandoj:

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

Komandkodoj estas egalaj b7, 15, b7 и 95. Memoru, ke la malplej signifaj tri bitoj estas la instrukcia klaso. En nia kazo, la kvara bito de ĉiuj instrukcioj estas malplena, do la instrukciaj klasoj estas respektive 7, 5, 7, 5. Klaso 7 estas BPF_ALU64, kaj 5 estas BPF_JMP. Por ambaŭ klasoj, la instrukcioformato estas la sama (vidu supre) kaj ni povas reverki nian programon tiel (samtempe ni reverkos la ceterajn kolumnojn en homa formo):

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

Funkciado b klaso ALU64 Estas BPF_MOV. Ĝi asignas valoron al la celregistro. Se la bito estas fiksita s (fonto), tiam la valoro estas prenita el la fontregistro, kaj se, kiel en nia kazo, ĝi ne estas agordita, tiam la valoro estas prenita el la kampo Imm. Do en la unua kaj tria instrukcioj ni plenumas la operacion r0 = Imm. Plue, JMP-klaso 1 operacio estas BPF_JEQ (saltu se egale). En nia kazo, ekde la bito S estas nulo, ĝi komparas la valoron de la fontregistro kun la kampo Imm. Se la valoroj koincidas, tiam la transiro okazas PC + Offkie PC, kiel kutime, enhavas la adreson de la sekva instrukcio. Fine, JMP Klaso 9 Operacio estas BPF_EXIT. Ĉi tiu instrukcio finas la programon, revenante al la kerno r0. Ni aldonu novan kolumnon al nia tabelo:

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

Ni povas reverki ĉi tion en pli oportuna formo:

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

Se ni memoras kio estas en la registro r1 la programo estas pasita montrilo al la kunteksto de la kerno, kaj en la registro r0 la valoro estas resendita al la kerno, tiam ni povas vidi, ke se la montrilo al la kunteksto estas nulo, tiam ni redonas 1, kaj alie - 2. Ni kontrolu, ke ni pravas rigardante la fonton:

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

Jes, ĝi estas sensignifa programo, sed ĝi tradukiĝas en nur kvar simplajn instrukciojn.

Ekzemplo de escepto: 16-bajta instrukcio

Ni menciis pli frue, ke iuj instrukcioj okupas pli ol 64 bitojn. Ĉi tio validas, ekzemple, por instrukcioj lddw (Kodo = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — ŝarĝu duoblan vorton el la kampoj en la registron Imm. La fakto estas tio Imm havas grandecon de 32, kaj duobla vorto estas 64 bitoj, do ŝarĝi 64-bitan tujan valoron en registron en unu 64-bita instrukcio ne funkcios. Por fari tion, du apudaj instrukcioj estas uzataj por stoki la duan parton de la 64-bita valoro en la kampo Imm. Ekzemplo:

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

Estas nur du instrukcioj en binara programo:

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

Ni renkontiĝos denove kun instrukcioj lddw, kiam ni parolas pri translokiĝoj kaj pri laboro kun mapoj.

Ekzemplo: malmuntado de BPF uzante normajn ilojn

Do, ni lernis legi BPF-binarajn kodojn kaj pretas analizi ajnan instrukcion se necese. Tamen indas diri, ke praktike estas pli oportune kaj rapide malmunti programojn uzante normajn ilojn, ekzemple:

$ 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

Vivciklo de BPF-objektoj, bpffs dosiersistemo

(Mi unue lernis kelkajn el la detaloj priskribitaj en ĉi tiu subsekcio de fastado Aleksej Starovoitov en BPF Blogo.)

BPF-objektoj - programoj kaj mapoj - estas kreitaj de uzantspaco uzante komandojn BPF_PROG_LOAD и BPF_MAP_CREATE sistema voko bpf(2), ni parolos pri ĝuste kiel tio okazas en la sekva sekcio. Ĉi tio kreas kernajn datumstrukturojn kaj por ĉiu el ili refcount (referenca nombro) estas agordita al unu, kaj dosierpriskribilo montranta la objekton estas resendita al la uzanto. Post kiam la tenilo estas fermita refcount la objekto estas reduktita je unu, kaj kiam ĝi atingas nulon, la objekto estas detruita.

Se la programo uzas mapojn, tiam refcount ĉi tiuj mapoj estas pliigitaj je unu post ŝarĝo de la programo, t.e. iliaj dosierpriskribiloj povas esti fermitaj de la uzantprocezo kaj ankoraŭ refcount ne fariĝos nulo:

BPF por la etuloj, unua parto: plilongigita BPF

Post sukcese ŝargi programon, ni kutime kunligas ĝin al ia okazaĵgeneratoro. Ekzemple, ni povas meti ĝin sur retan interfacon por prilabori envenantajn pakaĵojn aŭ konekti ĝin al iuj tracepoint en la kerno. Je ĉi tiu punkto, la referenca nombrilo ankaŭ pliiĝos je unu kaj ni povos fermi la dosierpriskribilon en la ŝargila programo.

Kio okazas se ni nun malŝaltas la ekŝargilon? Ĝi dependas de la tipo de eventogeneratoro (hoko). Ĉiuj retaj hokoj ekzistos post kiam la ŝargilo finiĝos, ĉi tiuj estas la tiel nomataj tutmondaj hokoj. Kaj, ekzemple, spurprogramoj estos liberigitaj post kiam la procezo kiu kreis ilin finiĝas (kaj tial estas nomitaj lokaj, de "loka ĝis la procezo"). Teknike, lokaj hokoj ĉiam havas respondan dosierpriskribilon en uzantspaco kaj tial fermiĝas kiam la procezo estas fermita, sed tutmondaj hokoj ne faras. En la sekva figuro, uzante ruĝajn krucojn, mi provas montri kiel la fino de la ŝargila programo influas la vivdaŭron de objektoj en la kazo de lokaj kaj tutmondaj hokoj.

BPF por la etuloj, unua parto: plilongigita BPF

Kial estas distingo inter lokaj kaj tutmondaj hokoj? Ruli iujn retajn programojn havas sencon sen uzantspaco, ekzemple, imagu DDoS-protekton - la ekŝargilo skribas la regulojn kaj ligas la BPF-programon al la reto-interfaco, post kio la ekŝargilo povas iri kaj mortigi sin. Aliflanke, imagu sencimigan spurprogramon, kiun vi skribis surgenue post dek minutoj - kiam ĝi estos finita, vi ŝatus, ke ne restu rubo en la sistemo, kaj lokaj hokoj certigos tion.

Aliflanke, imagu, ke vi volas konektiĝi al spurpunkto en la kerno kaj kolekti statistikojn dum multaj jaroj. En ĉi tiu kazo, vi volus kompletigi la uzantan parton kaj reveni al la statistiko de tempo al tempo. La bpf dosiersistemo provizas ĉi tiun ŝancon. Ĝi estas nur en-memora pseŭdodosiersistemo kiu permesas la kreadon de dosieroj kiuj referencas BPF-objektojn kaj tiel pliigas. refcount objektoj. Post ĉi tio, la ŝargilo povas eliri, kaj la objektoj kiujn ĝi kreis restos vivantaj.

BPF por la etuloj, unua parto: plilongigita BPF

Krei dosierojn en bpffs kiuj referencas BPF-objektojn estas nomita "alpinglado" (kiel en la sekva frazo: "procezo povas alpingli BPF-programon aŭ mapon"). Krei dosierobjektojn por BPF-objektoj havas sencon ne nur por plilongigi la vivon de lokaj objektoj, sed ankaŭ por la uzebleco de tutmondaj objektoj - reirante al la ekzemplo kun la tutmonda DDoS-protekta programo, ni volas povi veni rigardi statistikojn. de tempo al tempo.

La BPF-dosiersistemo estas kutime muntita enen /sys/fs/bpf, sed ĝi ankaŭ povas esti muntita loke, ekzemple, jene:

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

Dosiersistemaj nomoj estas kreitaj per la komando BPF_OBJ_PIN BPF-sistemvoko. Por ilustri, ni prenu programon, kompilu ĝin, alŝutu ĝin kaj alpinglu ĝin bpffs. Nia programo faras nenion utilan, ni nur prezentas la kodon por ke vi povu reprodukti la ekzemplon:

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

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

Ni kompilu ĉi tiun programon kaj kreu lokan kopion de la dosiersistemo bpffs:

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

Nun ni elŝutu nian programon uzante la ilon bpftool kaj rigardu la akompanajn sistemvokojn bpf(2) (kelkaj sensignivaj linioj forigitaj de strace-produktaĵo):

$ 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

Ĉi tie ni ŝargis la programon uzante BPF_PROG_LOAD, ricevis dosierpriskribilon de la kerno 3 kaj uzante la komandon BPF_OBJ_PIN alpinglis ĉi tiun dosierpriskribilon kiel dosieron "bpf-mountpoint/test". Post tio la ekŝargila programo bpftool finis labori, sed nia programo restis en la kerno, kvankam ni ne alkroĉis ĝin al iu reto-interfaco:

$ 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

Ni povas forigi la dosierobjekton normale unlink(2) kaj post tio la responda programo estos forigita:

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

Forigo de objektoj

Parolante pri forigo de objektoj, necesas klarigi, ke post kiam ni malkonektis la programon de la hoko (okazaĵgeneratoro), eĉ ne unu nova evento ekfunkciigos ĝian lanĉon, tamen ĉiuj aktualaj okazoj de la programo finiĝos en la normala ordo. .

Iuj specoj de BPF-programoj permesas vin anstataŭigi la programon sur la flugo, t.e. havigi sekvencon atomecon replace = detach old program, attach new program. En ĉi tiu kazo, ĉiuj aktivaj okazoj de la malnova versio de la programo finos sian laboron, kaj novaj evento-traktiloj estos kreitaj de la nova programo, kaj "atomico" ĉi tie signifas, ke neniu evento estos maltrafita.

Aldonante programojn al okazaĵfontoj

En ĉi tiu artikolo, ni ne aparte priskribos konekti programojn al okazaĵfontoj, ĉar estas senco studi tion en la kunteksto de specifa speco de programo. Cm. ekzemplo sube, en kiu ni montras kiel programoj kiel XDP estas konektitaj.

Manipulado de Objektoj Uzante la bpf Sistemalvokon

BPF-programoj

Ĉiuj BPF-objektoj estas kreitaj kaj administritaj de uzantspaco uzante sistemvokon bpf, havante la sekvan prototipon:

#include <linux/bpf.h>

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

Jen la teamo cmd estas unu el la valoroj de tipo enum bpf_cmd, attr — montrilo al parametroj por specifa programo kaj size — objektograndeco laŭ la montrilo, t.e. kutime ĉi tio sizeof(*attr). En kerno 5.8 la sistemvoko bpf subtenas 34 malsamajn komandojn, kaj difino union bpf_attr okupas 200 liniojn. Sed ni ne devus esti timigitaj de tio, ĉar ni konatiĝos kun la komandoj kaj parametroj dum pluraj artikoloj.

Ni komencu kun la teamo BPF_PROG_LOAD, kiu kreas BPF-programojn - prenas aron da BPF-instrukcioj kaj ŝarĝas ĝin en la kernon. En la momento de ŝarĝo, la kontrolilo estas lanĉita, kaj tiam la JIT-kompililo kaj, post sukcesa ekzekuto, la programdosierpriskribilo estas resendita al la uzanto. Ni vidis kio okazas al li poste en la antaŭa sekcio pri la vivociklo de BPF-objektoj.

Ni nun skribos kutiman programon, kiu ŝargos simplan BPF-programon, sed unue ni devas decidi, kian programon ni volas ŝargi - ni devos elekti speco kaj en la kadro de ĉi tiu tipo, skribu programon, kiu trapasos la kontrolilon-teston. Tamen, por ne malfaciligi la procezon, jen preta solvo: ni prenos programon kiel BPF_PROG_TYPE_XDP, kiu redonos la valoron XDP_PASS (saltu ĉiujn pakaĵojn). En BPF-asemblero ĝi aspektas tre simpla:

r0 = 2
exit

Post kiam ni decidis ke ni alŝutos, ni povas diri al vi kiel ni faros ĝin:

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

Interesaj eventoj en programo komenciĝas per la difino de tabelo insns - nia BPF-programo en maŝinkodo. En ĉi tiu kazo, ĉiu instrukcio de la BPF-programo estas pakita en la strukturon bpf_insn. Unua elemento insns konformas al instrukcioj r0 = 2, la dua - exit.

Retiriĝo. La kerno difinas pli oportunajn makroojn por skribi maŝinkodojn, kaj uzi la kernan kapdosieron tools/include/linux/filter.h ni povus skribi

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

Sed ĉar skribi BPF-programojn en denaska kodo estas nur necesa por verki testojn en la kerno kaj artikolojn pri BPF, la foresto de ĉi tiuj makrooj ne vere malfaciligas la vivon de la programisto.

Post difini la BPF-programon, ni daŭrigas ŝarĝi ĝin en la kernon. Nia minimumisma aro de parametroj attr inkluzivas la programspecon, aron kaj nombron da instrukcioj, postulatan permesilon kaj nomon "woo", kiun ni uzas por trovi nian programon en la sistemo post elŝuto. La programo, kiel promesite, estas ŝarĝita en la sistemon per sistema voko bpf.

Ĉe la fino de la programo ni finiĝas en senfina buklo kiu simulas la utilan ŝarĝon. Sen ĝi, la programo estos senvivigita de la kerno kiam la dosierpriskribilo, kiun la sistema voko resendita al ni, estas fermita. bpf, kaj ni ne vidos ĝin en la sistemo.

Nu, ni estas pretaj por testado. Ni kunvenu kaj rulu la programon sub stracepor kontroli, ke ĉio funkcias kiel ĝi devus:

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

Ĉio estas bona, bpf(2) resendis tenilon 3 al ni kaj ni iris en senfinan buklon kun pause(). Ni provu trovi nian programon en la sistemo. Por fari tion ni iros al alia terminalo kaj uzos la ilon 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)

Ni vidas, ke estas ŝarĝita programo en la sistemo woo kies tutmonda ID estas 390 kaj estas nuntempe en progreso simple-prog estas malfermita dosierpriskribilo montranta al la programo (kaj se simple-prog do finos la laboron woo malaperos). Kiel atendite, la programo woo prenas 16 bajtojn - du instrukciojn - de binaraj kodoj en la BPF-arkitekturo, sed en ĝia denaska formo (x86_64) ĝi jam estas 40 bajtoj. Ni rigardu nian programon en ĝia originala formo:

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

neniuj surprizoj. Nun ni rigardu la kodon generitan de la JIT-kompililo:

# 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

ne tre efika por exit(2), sed juste, nia programo estas tro simpla, kaj por ne-trivialaj programoj la prologo kaj epilogo aldonitaj de la JIT-kompililo estas kompreneble necesaj.

mapoj

BPF-programoj povas uzi strukturitajn memorareojn kiuj estas alireblaj kaj al aliaj BPF-programoj kaj al programoj en uzantspaco. Ĉi tiuj objektoj nomiĝas mapoj kaj en ĉi tiu sekcio ni montros kiel manipuli ilin uzante sisteman vokon bpf.

Ni diru tuj, ke la kapabloj de mapoj ne estas limigitaj nur al aliro al komuna memoro. Ekzistas special-celaj mapoj enhavantaj, ekzemple, montrilojn al BPF-programoj aŭ montrilojn al retinterfacoj, mapoj por labori kun perf-okazaĵoj, ktp. Pri ili ĉi tie ni ne parolos, por ne konfuzi la leganton. Krom ĉi tio, ni ignoras problemojn pri sinkronigado, ĉar ĉi tio ne gravas por niaj ekzemploj. Kompleta listo de disponeblaj mapspecoj troveblas en <linux/bpf.h>, kaj en ĉi tiu sekcio ni prenos kiel ekzemplon la historie unuan tipon, la hashtabelo BPF_MAP_TYPE_HASH.

Se vi kreas hashtabelon en, ekzemple, C++, vi dirus unordered_map<int,long> woo, kiu en la rusa signifas “Mi bezonas tablon woo senlima grandeco, kies klavoj estas de tipo int, kaj la valoroj estas la tipo long" Por krei BPF-haŝtabelon, ni devas fari preskaŭ la samon, krom ke ni devas specifi la maksimuman grandecon de la tabelo, kaj anstataŭ specifi la specojn de ŝlosiloj kaj valoroj, ni devas specifi iliajn grandecojn en bajtoj. . Por krei mapojn uzu la komandon BPF_MAP_CREATE sistema voko bpf. Ni rigardu pli-malpli minimuman programon, kiu kreas mapon. Post la antaŭa programo, kiu ŝarĝas BPF-programojn, ĉi tiu devus ŝajni simpla al vi:

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

Ĉi tie ni difinas aron da parametroj attr, en kiu ni diras "Mi bezonas hashtabelon kun ŝlosiloj kaj grandvaloroj sizeof(int), en kiu mi povas meti maksimume kvar elementojn." Kreante BPF-mapojn, vi povas specifi aliajn parametrojn, ekzemple, sammaniere kiel en la ekzemplo kun la programo, ni specifis la nomon de la objekto kiel "woo".

Ni kompilu kaj rulu la programon:

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

Jen la sistemvoko bpf(2) resendis al ni la priskriban mapnumeron 3 kaj tiam la programo, kiel atendite, atendas pliajn instrukciojn en la sistemvoko pause(2).

Nun ni sendu nian programon al la fono aŭ malfermu alian terminalon kaj rigardu nian objekton uzante la ilon bpftool (ni povas distingi nian mapon de aliaj per ĝia nomo):

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

La numero 114 estas la tutmonda ID de nia objekto. Ajna programo en la sistemo povas uzi ĉi tiun ID por malfermi ekzistantan mapon per la komando BPF_MAP_GET_FD_BY_ID sistema voko bpf.

Nun ni povas ludi kun nia hashtabelo. Ni rigardu ĝian enhavon:

$ sudo bpftool map dump id 114
Found 0 elements

Malplena. Ni metu valoron en ĝi hash[1] = 1:

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

Ni rigardu la tablon denove:

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

Hura! Ni sukcesis aldoni unu elementon. Notu, ke ni devas labori je la bajta nivelo por fari tion, ĉar bptftool ne scias kian tipon estas la valoroj en la hashtabelo. (Ĉi tiu scio povas esti transdonita al ŝi uzante BTF, sed pli pri tio nun.)

Kiel precize bpftool legas kaj aldonas elementojn? Ni rigardu sub la kapuĉo:

$ 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

Unue ni malfermis la mapon per ĝia tutmonda ID uzante la komandon BPF_MAP_GET_FD_BY_ID и bpf(2) resendis al ni priskribilon 3. Plue uzante la komandon BPF_MAP_GET_NEXT_KEY ni trovis la unuan ŝlosilon en la tabelo preterpasante NULL kiel montrilo al la "antaŭa" klavo. Se ni havas la ŝlosilon, ni povas fari BPF_MAP_LOOKUP_ELEMkiu resendas valoron al montrilo value. La sekva paŝo estas, ke ni provas trovi la sekvan elementon pasante montrilon al la nuna ŝlosilo, sed nia tabelo enhavas nur unu elementon kaj la komandon. BPF_MAP_GET_NEXT_KEY revenas ENOENT.

Bone, ni ŝanĝu la valoron per ŝlosilo 1, ni diru, ke nia komerca logiko postulas registriĝon 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

Kiel atendite, ĝi estas tre simpla: la komando BPF_MAP_GET_FD_BY_ID malfermas nian mapon per ID, kaj la komando BPF_MAP_UPDATE_ELEM anstataŭigas la elementon.

Do, post kreado de hashtabelo de unu programo, ni povas legi kaj skribi ĝian enhavon de alia. Notu, ke se ni povis fari tion de la komandlinio, tiam iu ajn alia programo en la sistemo povas fari ĝin. Krom la komandoj priskribitaj supre, por labori kun mapoj de uzantspaco, la sekva:

  • BPF_MAP_LOOKUP_ELEM: trovi valoron per ŝlosilo
  • BPF_MAP_UPDATE_ELEM: ĝisdatigi/krei valoron
  • BPF_MAP_DELETE_ELEM: forigi ŝlosilon
  • BPF_MAP_GET_NEXT_KEY: trovi la sekvan (aŭ unuan) ŝlosilon
  • BPF_MAP_GET_NEXT_ID: permesas vin trarigardi ĉiujn ekzistantajn mapojn, tiel ĝi funkcias bpftool map
  • BPF_MAP_GET_FD_BY_ID: malfermu ekzistantan mapon per ĝia tutmonda ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atome ĝisdatigas la valoron de objekto kaj redonu la malnovan
  • BPF_MAP_FREEZE: igu la mapon neŝanĝebla de uzantspaco (ĉi tiu operacio ne povas esti malfarita)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: amasoperacioj. Ekzemple, BPF_MAP_LOOKUP_AND_DELETE_BATCH - ĉi tio estas la sola fidinda maniero legi kaj restarigi ĉiujn valorojn de la mapo

Ne ĉiuj ĉi tiuj komandoj funkcias por ĉiuj mapspecoj, sed ĝenerale labori kun aliaj specoj de mapoj de uzantspaco aspektas ekzakte same kiel labori kun haŝtabeloj.

Por ordo, ni finu niajn hashtabelajn eksperimentojn. Memoru, ke ni kreis tabelon, kiu povas enhavi ĝis kvar ŝlosilojn? Ni aldonu kelkajn pliajn elementojn:

$ 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

Ĝis nun bone:

$ 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

Ni provu aldoni unu pli:

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

Kiel atendite, ni ne sukcesis. Ni rigardu la eraron pli detale:

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

Ĉio estas en ordo: kiel atendite, la teamo BPF_MAP_UPDATE_ELEM provas krei novan, kvinan, ŝlosilon, sed kraŝas E2BIG.

Do, ni povas krei kaj ŝargi BPF-programojn, kaj ankaŭ krei kaj administri mapojn el uzantspaco. Nun estas logike rigardi kiel ni povas uzi mapojn de la BPF-programoj mem. Pri tio ni povus paroli en la lingvo de malfacile legeblaj programoj en maŝinaj makrokodoj, sed fakte venis la tempo montri kiel BPF-programoj estas efektive verkitaj kaj prizorgataj - uzante libbpf.

(Por legantoj, kiuj estas malkontenta pri la manko de malaltnivela ekzemplo: ni analizos detale programojn, kiuj uzas mapojn kaj helpajn funkciojn kreitajn per libbpf kaj diru al vi, kio okazas ĉe la instrua nivelo. Por legantoj, kiuj estas malkontentaj tre multe, ni aldonis ekzemplo en la taŭga loko en la artikolo.)

Skribante BPF-programojn uzante libbpf

Verki BPF-programojn uzante maŝinkodojn povas esti interesa nur la unuan fojon, kaj tiam sateco ekas. En ĉi tiu momento vi devas turni vian atenton al llvm, kiu havas backend por generado de kodo por la BPF-arkitekturo, same kiel biblioteko libbpf, kiu permesas vin skribi la uzantan flankon de BPF-aplikoj kaj ŝargi la kodon de BPF-programoj generitaj uzante llvm/clang.

Fakte, kiel ni vidos en ĉi tiu kaj postaj artikoloj, libbpf faras sufiĉe multe da laboro sen ĝi (aŭ similaj iloj - iproute2, libbcc, libbpf-go, ktp.) estas neeble vivi. Unu el la murdaj trajtoj de la projekto libbpf estas BPF CO-RE (Kompili Unufoje, Kuru Ĉie) - projekto kiu permesas vin skribi BPF-programojn porteblajn de unu kerno al alia, kun la kapablo funkcii per malsamaj APIoj (ekzemple, kiam la kernostrukturo ŝanĝiĝas de versio. al versio). Por povi labori kun CO-RE, via kerno devas esti kompilita kun BTF-subteno (ni priskribas kiel fari tion en la sekcio Disvolvaj Iloj. Vi povas kontroli ĉu via kerno estas konstruita per BTF aŭ ne tre simple - per la ĉeesto de la sekva dosiero:

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

Ĉi tiu dosiero konservas informojn pri ĉiuj datumtipoj uzataj en la kerno kaj estas uzata en ĉiuj niaj ekzemploj uzante libbpf. Pri CO-RE ni parolos detale en la sekva artikolo, sed en ĉi tiu - nur konstruu al vi kernon per CONFIG_DEBUG_INFO_BTF.

biblioteko libbpf loĝas ĝuste en la adresaro tools/lib/bpf kerno kaj ĝia evoluo estas efektivigita per la dissendolisto [email protected]. Tamen, aparta deponejo estas konservita por la bezonoj de aplikoj vivantaj ekster la kerno https://github.com/libbpf/libbpf en kiu la kernbiblioteko estas spegulita por legi aliro pli-malpli kiel estas.

En ĉi tiu sekcio ni rigardos kiel vi povas krei projekton kiu uzas libbpf, ni verku plurajn (pli-malpli sensencajn) testprogramojn kaj analizu detale kiel ĉio funkcias. Ĉi tio permesos al ni pli facile klarigi en la sekvaj sekcioj ĝuste kiel BPF-programoj interagas kun mapoj, kernaj helpantoj, BTF, ktp.

Tipe projektoj uzante libbpf aldonu GitHub-deponejon kiel git-submodulo, ni faros la samon:

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

Irante al libbpf tre simpla:

$ 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

Nia sekva plano en ĉi tiu sekcio estas kiel sekvas: ni skribos BPF-programon kiel BPF_PROG_TYPE_XDP, la sama kiel en la antaŭa ekzemplo, sed en C, ni kompilas ĝin uzante clang, kaj verku helpan programon, kiu ŝargos ĝin en la kernon. En la sekvaj sekcioj ni vastigos la kapablojn de kaj la BPF-programo kaj la helpprogramo.

Ekzemplo: kreante plenan aplikaĵon uzante libbpf

Por komenci, ni uzas la dosieron /sys/kernel/btf/vmlinux, kiu estis menciita supre, kaj kreu ĝian ekvivalenton en la formo de titoldosiero:

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

Ĉi tiu dosiero stokos ĉiujn datumstrukturojn disponeblajn en nia kerno, ekzemple, jen kiel la IPv4-kapo estas difinita en la kerno:

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

Nun ni skribos nian BPF-programon en C:

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

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

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

Kvankam nia programo montriĝis tre simpla, ni ankoraŭ devas atenti multajn detalojn. Unue, la unua titoldosiero, kiun ni inkluzivas estas vmlinux.h, kiun ni ĵus generis uzante bpftool btf dump - nun ni ne bezonas instali la pakaĵon kernel-headers por ekscii kiel aspektas la kernaj strukturoj. La sekva kapdosiero venas al ni el la biblioteko libbpf. Nun ni bezonas ĝin nur por difini la makroon SEC, kiu sendas la karakteron al la konvena sekcio de la ELF-objektdosiero. Nia programo estas enhavita en la sekcio xdp/simple, kie antaŭ la oblikvo ni difinas la programtipo BPF - ĉi tiu estas la konvencio uzata en libbpf, surbaze de la sekcionomo ĝi anstataŭigos la ĝustan tipon ĉe ekfunkciigo bpf(2). La BPF-programo mem estas C - tre simpla kaj konsistas el unu linio return XDP_PASS. Fine, aparta sekcio "license" enhavas la nomon de la permesilo.

Ni povas kompili nian programon per llvm/clang, versio >= 10.0.0, aŭ pli bone ankoraŭ, pli granda (vidu sekcion Disvolvaj Iloj):

$ 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

Inter la interesaj trajtoj: ni indikas la celan arkitekturon -target bpf kaj la vojo al la kaplinioj libbpf, kiun ni lastatempe instalis. Ankaŭ, ne forgesu -O2, sen ĉi tiu opcio vi eble estos en surprizoj en la estonteco. Ni rigardu nian kodon, ĉu ni sukcesis skribi la programon, kiun ni volis?

$ 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

Jes, ĝi funkciis! Nun ni havas binaran dosieron kun la programo, kaj ni volas krei aplikaĵon, kiu ŝarĝos ĝin en la kernon. Tiucele la biblioteko libbpf ofertas al ni du eblojn - uzu malsupran-nivelan API aŭ pli altan API. Ni iros la duan vojon, ĉar ni volas lerni kiel skribi, ŝargi kaj konekti BPF-programojn kun minimuma peno por ilia posta studo.

Unue, ni devas generi la "skeleton" de nia programo el ĝia binaro uzante la saman utilecon bpftool — la svisa tranĉilo de la mondo de BPF (kiu povas esti prenita laŭvorte, ĉar Daniel Borkman, unu el la kreintoj kaj prizorgantoj de BPF, estas sviso):

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

En dosiero xdp-simple.skel.h enhavas la binaran kodon de nia programo kaj funkciojn por administri - ŝargi, alfiksi, forigi nian objekton. En nia simpla kazo ĉi tio aspektas kiel troa, sed ĝi ankaŭ funkcias en la kazo kie la objektodosiero enhavas multajn BPF-programojn kaj mapojn kaj por ŝargi ĉi tiun gigantan ELF ni nur bezonas generi la skeleton kaj voki unu aŭ du funkciojn de la kutima aplikaĵo, kiun ni. skribas Ni nun pluiru.

Strikte parolante, nia ŝargi programo estas bagatela:

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

estas struct xdp_simple_bpf difinita en la dosiero xdp-simple.skel.h kaj priskribas nian objektodosieron:

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

Ni povas vidi spurojn de malaltnivela API ĉi tie: la strukturo struct bpf_program *simple и struct bpf_link *simple. La unua strukturo specife priskribas nian programon, skribitan en la sekcio xdp/simple, kaj la dua priskribas kiel la programo konektas al la okazaĵfonto.

funkcio xdp_simple_bpf__open_and_load, malfermas ELF-objekton, analizas ĝin, kreas ĉiujn strukturojn kaj substrukturojn (krom la programo, ELF enhavas ankaŭ aliajn sekciojn - datumojn, nurlegeblajn datumojn, sencimigan informon, permesilon, ktp.), kaj poste ŝarĝas ĝin en la kernon per sistemo. voki bpf, kiun ni povas kontroli per kompilo kaj rulado de la programo:

$ 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

Ni nun rigardu nian programon uzante bpftool. Ni trovu ŝian identigilon:

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

kaj dump (ni uzas mallongigitan formon de la komando bpftool prog dump xlated):

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

Io nova! La programo presis partojn de nia fontdosiero C. Tion faris la biblioteko libbpf, kiu trovis la sencimigan sekcion en la duuma, kompilis ĝin en BTF-objekton, ŝarĝis ĝin en la kernon uzante BPF_BTF_LOAD, kaj tiam specifis la rezultan dosierpriskribilon dum ŝarĝo de la programo per la komando BPG_PROG_LOAD.

Kernelhelpantoj

BPF-programoj povas ruli "eksterajn" funkciojn - kernelhelpantoj. Ĉi tiuj helpaj funkcioj permesas al BPF-programoj aliri kernstrukturojn, administri mapojn, kaj ankaŭ komuniki kun la "reala mondo" - krei perf-eventojn, kontroli aparataron (ekzemple, alidirekti pakaĵetojn), ktp.

Ekzemplo: bpf_get_smp_processor_id

En la kadro de la paradigmo "lernado per ekzemplo", ni konsideru unu el la helpaj funkcioj, bpf_get_smp_processor_id(), certa en dosiero kernel/bpf/helpers.c. Ĝi resendas la numeron de la procesoro sur kiu la BPF-programo kiu vokis ĝin funkcias. Sed ni ne tiom interesiĝas pri ĝia semantiko kiel pri la fakto, ke ĝia efektivigo prenas unu linion:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

La difinoj de la helpfunkcio de BPF estas similaj al la difinoj de Linuksaj sistemvokoj. Ĉi tie, ekzemple, funkcio estas difinita kiu ne havas argumentojn. (Funkcio kiu prenas, ekzemple, tri argumentojn estas difinita uzante la makroon BPF_CALL_3. La maksimuma nombro da argumentoj estas kvin.) Tamen tio estas nur la unua parto de la difino. La dua parto estas difini la tipstrukturon struct bpf_func_proto, kiu enhavas priskribon de la helpa funkcio kiun la kontrolisto komprenas:

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

Registrado de Helpaj Funkcioj

Por ke BPF-programoj de aparta tipo uzu ĉi tiun funkcion, ili devas registri ĝin, ekzemple por la tipo BPF_PROG_TYPE_XDP funkcio estas difinita en la kerno xdp_func_proto, kiu determinas el la helpa funkcio ID ĉu XDP subtenas ĉi tiun funkcion aŭ ne. Nia funkcio estas subtenoj:

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

Novaj BPF programtipoj estas "difinitaj" en la dosiero include/linux/bpf_types.h uzante makroon BPF_PROG_TYPE. Difinite inter citiloj ĉar ĝi estas logika difino, kaj en C-lingvaj terminoj la difino de tuta aro da konkretaj strukturoj okazas en aliaj lokoj. Precipe, en la dosiero kernel/bpf/verifier.c ĉiuj difinoj el dosiero bpf_types.h estas uzataj por krei aron de strukturoj 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
};

Tio estas, por ĉiu speco de BPF-programo, montrilo al datumstrukturo de la tipo estas difinita struct bpf_verifier_ops, kiu estas pravigita kun la valoro _name ## _verifier_ops, t.e., xdp_verifier_ops por xdp. Strukturo xdp_verifier_ops decidita en dosiero net/core/filter.c kiel sekvas:

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

Ĉi tie ni vidas nian konatan funkcion xdp_func_proto, kiu funkcios la kontrolilon ĉiufoje kiam ĝi renkontas defion iuj funkcioj ene de BPF-programo, vidu verifier.c.

Ni rigardu kiel hipoteza BPF-programo uzas la funkcion bpf_get_smp_processor_id. Por fari tion, ni reverkas la programon de nia antaŭa sekcio jene:

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

Simbolo bpf_get_smp_processor_id decidita в <bpf/bpf_helper_defs.h> bibliotekoj libbpf kiom

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

tio estas, bpf_get_smp_processor_id estas funkciomontrilo kies valoro estas 8, kie 8 estas la valoro BPF_FUNC_get_smp_processor_id tipo enum bpf_fun_id, kiu estas difinita por ni en la dosiero vmlinux.h (dosiero bpf_helper_defs.h en la kerno estas generita per skripto, do la "magiaj" nombroj estas en ordo). Ĉi tiu funkcio ne prenas argumentojn kaj redonas valoron de tipo __u32. Kiam ni rulas ĝin en nia programo, clang generas instrukcion BPF_CALL "la ĝusta speco" Ni kompilu la programon kaj rigardu la sekcion 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

En la unua linio ni vidas instrukciojn call, parametro IMM kiu estas egala al 8, kaj SRC_REG - nulo. Laŭ la ABI-interkonsento uzata de kontrolilo, ĉi tio estas alvoko al helpanta funkcio numero ok. Post kiam ĝi estas lanĉita, la logiko estas simpla. Revenvaloro de registro r0 kopiita al r1 kaj sur linioj 2,3 ĝi estas konvertita al tipo u32 — la supraj 32 bitoj estas purigitaj. Sur linioj 4,5,6,7 ni resendas 2 (XDP_PASS) aŭ 1 (XDP_DROP) depende de ĉu la helpa funkcio de linio 0 redonis nulan aŭ nenulan valoron.

Ni provu nin: ŝarĝu la programon kaj rigardu la eliron 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

Bone, kontrolilo trovis la ĝustan kernel-helpilon.

Ekzemplo: pasi argumentojn kaj fine ruli la programon!

Ĉiuj rulnivelaj helpaj funkcioj havas prototipon

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

Parametroj al helpaj funkcioj estas pasigitaj en registroj r1-r5, kaj la valoro estas redonita en la registro r0. Ne ekzistas funkcioj kiuj prenas pli ol kvin argumentojn, kaj subteno por ili ne estas atendita aldoniĝi estonte.

Ni rigardu la novan kernhelpilon kaj kiel BPF pasas parametrojn. Ni reverku xdp-simple.bpf.c jene (la resto de la linioj ne ŝanĝiĝis):

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

Nia programo presas la nombron de la CPU, sur kiu ĝi funkcias. Ni kompilu ĝin kaj rigardu la kodon:

$ 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

En linioj 0-7 ni skribas la ĉenon running on CPU%un, kaj tiam sur la linio 8 ni kuras la konatan bpf_get_smp_processor_id. Sur linioj 9-12 ni preparas la helpajn argumentojn bpf_printk - registroj r1, r2, r3. Kial estas tri el ili kaj ne du? Ĉar bpf_printkĉi tio estas makroa envolvaĵo ĉirkaŭ la vera helpanto bpf_trace_printk, kiu devas pasi la grandecon de la formata ĉeno.

Ni nun aldonu kelkajn liniojn al xdp-simple.cpor ke nia programo konektas al la interfaco lo kaj vere komencis!

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

Ĉi tie ni uzas la funkcion bpf_set_link_xdp_fd, kiu ligas XDP-specajn BPF-programojn al retinterfacoj. Ni malmolkodis la interfacan numeron lo, kiu ĉiam estas 1. Ni rulas la funkcion dufoje por unue dekroĉi la malnovan programon se ĝi estis alfiksita. Rimarku, ke nun ni ne bezonas defion pause aŭ senfina buklo: nia ŝargila programo eliros, sed la BPF-programo ne estos senvivigita ĉar ĝi estas konektita al la okazaĵfonto. Post sukcesa elŝuto kaj konekto, la programo estos lanĉita por ĉiu retpako alvenanta lo.

Ni elŝutu la programon kaj rigardu la interfacon 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

La programo, kiun ni elŝutis, havas ID 669 kaj ni vidas la saman ID sur la interfaco lo. Ni sendos kelkajn pakaĵojn al 127.0.0.1 (peto + respondo):

$ ping -c1 localhost

kaj nun ni rigardu la enhavon de la sencimiga virtuala dosiero /sys/kernel/debug/tracing/trace_pipe, en kiu bpf_printk skribas siajn mesaĝojn:

# 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

Du pakaĵoj estis ekviditaj lo kaj prilaborita sur CPU0 - nia unua plenrajta sensignifa BPF-programo funkciis!

Indas rimarki tion bpf_printk Ne vane ĝi skribas al la sencimdosiero: ĉi tiu ne estas la plej sukcesa helpanto por uzo en produktado, sed nia celo estis montri ion simplan.

Aliro al mapoj de BPF-programoj

Ekzemplo: uzante mapon de la programo BPF

En la antaŭaj sekcioj ni lernis kiel krei kaj uzi mapojn el uzantspaco, kaj nun ni rigardu la kernan parton. Ni komencu, kiel kutime, per ekzemplo. Ni reverku nian programon xdp-simple.bpf.c kiel sekvas:

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

Komence de la programo ni aldonis mapdifinon woo: Ĉi tio estas 8-elementa tabelo, kiu stokas valorojn kiel u64 (en C ni difinus tian tabelon kiel u64 woo[8]). En programo "xdp/simple" ni ricevas la nunan procesoran nombron en variablon key kaj poste uzante la helpan funkcion bpf_map_lookup_element ni ricevas montrilon al la responda eniro en la tabelo, kiun ni pliigas je unu. Tradukite en la rusan: ni kalkulas statistikojn pri kiu CPU prilaboris envenantajn pakaĵojn. Ni provu ruli la programon:

$ 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

Ni kontrolu, ke ŝi estas ligita lo kaj sendu kelkajn pakaĵojn:

$ 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

Nun ni rigardu la enhavon de la tabelo:

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

Preskaŭ ĉiuj procezoj estis prilaboritaj sur CPU7. Ĉi tio ne gravas por ni, la ĉefa afero estas, ke la programo funkcias kaj ni komprenas kiel aliri mapojn de BPF-programoj - uzante хелперов bpf_mp_*.

Mistika indekso

Do, ni povas aliri la mapon de la programo BPF uzante alvokojn kiel

val = bpf_map_lookup_elem(&woo, &key);

kie aspektas la helpa funkcio

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

sed ni preterpasas montrilon &woo al nenomita strukturo struct { ... }...

Se ni rigardas la programan asembleron, ni vidas ke la valoro &woo ne estas fakte difinita (linio 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
...

kaj estas enhavita en translokigoj:

$ 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

Sed se ni rigardas la jam ŝarĝitan programon, ni vidas montrilon al la ĝusta mapo (linio 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]
...

Tiel, ni povas konkludi, ke en la momento de lanĉo de nia ŝargila programo, la ligo al &woo estis anstataŭigita per io kun biblioteko libbpf. Unue ni rigardos la eliron 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

Ni vidas tion libbpf kreis mapon woo kaj poste elŝutis nian programon simple. Ni rigardu pli detale kiel ni ŝargas la programon:

  • voki xdp_simple_bpf__open_and_load el dosiero xdp-simple.skel.h
  • kiu kaŭzas xdp_simple_bpf__load el dosiero xdp-simple.skel.h
  • kiu kaŭzas bpf_object__load_skeleton el dosiero libbpf/src/libbpf.c
  • kiu kaŭzas bpf_object__load_xattr el libbpf/src/libbpf.c

La lasta funkcio, interalie, vokos bpf_object__create_maps, kiu kreas aŭ malfermas ekzistantajn mapojn, igante ilin dosierpriskribiloj. (Tien ni vidas BPF_MAP_CREATE en la eligo strace.) Poste la funkcio estas vokita bpf_object__relocate kaj estas ŝi, kiu interesas nin, ĉar ni memoras tion, kion ni vidis woo en la tabelo pri translokado. Esplorante ĝin, ni finfine trovas nin en la funkcio bpf_program__relocate, kiu traktas maptranslokiĝojn:

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

Do ni prenas niajn instrukciojn

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

kaj anstataŭigu la fontregistron en ĝi per BPF_PSEUDO_MAP_FD, kaj la unua IMM al la dosierpriskribilo de nia mapo kaj, se ĝi estas egala al, ekzemple, 0xdeadbeef, tiam kiel rezulto ni ricevos la instrukcion

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

Jen kiel mapaj informoj estas transdonitaj al specifa ŝarĝita BPF-programo. En ĉi tiu kazo, la mapo povas esti kreita uzante BPF_MAP_CREATE, kaj malfermita per ID uzante BPF_MAP_GET_FD_BY_ID.

Tuta, kiam vi uzas libbpf la algoritmo estas kiel sekvas:

  • dum kompilo, rekordoj estas kreitaj en la translokiga tabelo por ligiloj al mapoj
  • libbpf malfermas la ELF-objektlibron, trovas ĉiujn uzitajn mapojn kaj kreas dosierpriskribilojn por ili
  • dosierpriskribiloj estas ŝarĝitaj en la kernon kiel parto de la instrukcio LD64

Kiel vi povas imagi, estas pli por veni kaj ni devos rigardi en la kernon. Feliĉe, ni havas indikon - ni skribis la signifon BPF_PSEUDO_MAP_FD en la fontregistron kaj ni povas enterigi ĝin, kiu kondukos nin al la sanktejo de ĉiuj sanktuloj - kernel/bpf/verifier.c, kie funkcio kun karakteriza nomo anstataŭigas dosierpriskribilon kun la adreso de strukturo de tipo 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;

(plena kodo troveblas ligilo). Do ni povas vastigi nian algoritmon:

  • dum ŝarĝo de la programo, kontrolilo kontrolas la ĝustan uzon de la mapo kaj skribas la adreson de la responda strukturo struct bpf_map

Kiam vi elŝutas la ELF-binaron uzante libbpf Okazas multe pli, sed tion ni diskutos en aliaj artikoloj.

Ŝargado de programoj kaj mapoj sen libbpf

Kiel promesite, jen ekzemplo por legantoj, kiuj volas scii kiel krei kaj ŝargi programon, kiu uzas mapojn, sen helpo libbpf. Ĉi tio povas esti utila kiam vi laboras en medio por kiu vi ne povas konstrui dependecojn, aŭ ŝpari ĉiun pecon, aŭ skribi programon kiel ply, kiu generas BPF-binaran kodon sur la muŝo.

Por pli facile sekvi la logikon, ni reverkos nian ekzemplon por ĉi tiuj celoj xdp-simple. La kompleta kaj iomete vastigita kodo de la programo diskutita en ĉi tiu ekzemplo troveblas en ĉi tio kerno.

La logiko de nia aplikaĵo estas jena:

  • krei tipmapon BPF_MAP_TYPE_ARRAY uzante la komandon BPF_MAP_CREATE,
  • krei programon kiu uzas ĉi tiun mapon,
  • konekti la programon al la interfaco lo,

kiu tradukiĝas en homan kiel

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

estas map_create kreas mapon en la sama maniero kiel ni faris en la unua ekzemplo pri la sistemvoko bpf - "kerno, bonvolu fari al mi novan mapon en formo de tabelo de 8 elementoj kiel __u64 kaj redonu al mi la dosierpriskribilon":

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

La programo ankaŭ estas facile ŝarĝebla:

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

La malfacila parto prog_load estas la difino de nia BPF-programo kiel aro de strukturoj struct bpf_insn insns[]. Sed ĉar ni uzas programon, kiun ni havas en C, ni povas iomete trompi:

$ 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

Entute, ni devas skribi 14 instrukciojn en formo de strukturoj kiel struct bpf_insn (konsilo: prenu la rubejon de supre, relegu la sekcion pri instrukcioj, malfermu linux/bpf.h и linux/bpf_common.h kaj provu determini struct bpf_insn insns[] memstare):

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

Ekzerco por tiuj, kiuj ne skribis tion mem - trovi map_fd.

Restas ankoraŭ unu nekonita parto en nia programo - xdp_attach. Bedaŭrinde, programoj kiel XDP ne povas esti konektitaj per sistema voko bpf. La homoj kiuj kreis BPF kaj XDP estis de la interreta Linukso-komunumo, kio signifas, ke ili uzis la plej konatan al ili (sed ne por normala homoj) interfaco por interagado kun la kerno: retlink ingoj, Vidu ankaŭ RFC3549. La plej simpla maniero efektivigi xdp_attach kopias kodon de libbpf, nome, el la dosiero netlink.c, kion ni faris, iom mallongigante ĝin:

Bonvenon al la mondo de netlink-ingoj

Malfermu retlink-ingospecon 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;
}

Ni legas el ĉi tiu ingo:

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

Fine, jen nia funkcio, kiu malfermas ingon kaj sendas specialan mesaĝon al ĝi enhavantan dosierpriskribilon:

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

Do, ĉio estas preta por testado:

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

Ni vidu ĉu nia programo konektiĝis al 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

Ni sendu ping-ojn kaj rigardu la mapon:

$ 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

Hura, ĉio funkcias. Rimarku, cetere, ke nia mapo estas denove montrata en formo de bajtoj. Ĉi tio estas pro la fakto ke, male al libbpf ni ne ŝargis tipinformojn (BTF). Sed ni parolos pli pri tio venontfoje.

Disvolvaj Iloj

En ĉi tiu sekcio, ni rigardos la minimuman ilaron por programistoj de BPF.

Ĝenerale, vi ne bezonas ion specialan por evoluigi BPF-programojn - BPF funkcias per iu ajn deca distribua kerno, kaj programoj estas konstruitaj uzante clang, kiu povas esti provizita el la pakaĵo. Tamen, pro la fakto, ke BPF estas evoluinta, la kerno kaj iloj konstante ŝanĝiĝas, se vi ne volas skribi BPF-programojn uzante malnovmodajn metodojn de 2019, tiam vi devos kompili.

  • llvm/clang
  • pahole
  • ĝia kerno
  • bpftool

(Por referenco, ĉi tiu sekcio kaj ĉiuj ekzemploj en la artikolo estis rulitaj sur Debian 10.)

llvm/clang

BPF estas amika kun LLVM kaj, kvankam lastatempe programoj por BPF povas esti kompilitaj uzante gcc, ĉiu nuna evoluo estas farita por LLVM. Tial, antaŭ ĉio, ni konstruos la nunan version clang de 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
... много времени спустя
$

Nun ni povas kontroli ĉu ĉio kuniĝis ĝuste:

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

(Asembleaj instrukcioj clang prenita de mi de bpf_devel_QA.)

Ni ne instalos la programojn, kiujn ni ĵus konstruis, sed anstataŭe nur aldonos ilin PATHekzemple:

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

(Ĉi tio povas esti aldonita al .bashrc aŭ al aparta dosiero. Persone, mi aldonas tiajn aferojn ~/bin/activate-llvm.sh kaj kiam necese mi faras ĝin . activate-llvm.sh.)

Pahole kaj BTF

Utileco pahole uzata dum konstruado de la kerno por krei sencimigan informon en BTF-formato. Ni ne eniros en detalon en ĉi tiu artikolo pri la detaloj de BTF-teknologio, krom la fakto, ke ĝi estas oportuna kaj ni volas uzi ĝin. Do se vi konstruos vian kernon, konstruu unue pahole (sen pahole vi ne povos konstrui la kernon kun la opcio 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

Kernoj por eksperimenti kun BPF

Esplorante la eblecojn de BPF, mi volas kunveni mian propran kernon. Ĉi tio, ĝenerale, ne estas necesa, ĉar vi povos kompili kaj ŝargi BPF-programojn sur la distribua kerno, tamen, havi vian propran kernon ebligas al vi uzi la plej novajn funkciojn de BPF, kiuj aperos en via distribuo en la plej bona okazo post monatoj. , aŭ, kiel en la kazo de kelkaj sencimigaj iloj tute ne estos pakitaj en antaŭvidebla estonteco. Ankaŭ, ĝia propra kerno igas ĝin senti grava eksperimenti kun la kodo.

Por konstrui kernon vi bezonas, unue, la kernon mem, kaj due, kernan agordan dosieron. Por eksperimenti kun BPF ni povas uzi la kutiman vanilo kerno aŭ unu el la disvolvaj kernoj. Historie, BPF-evoluo okazas ene de la Linukso-retokomunumo kaj tial ĉiuj ŝanĝoj baldaŭ aŭ malfrue pasas per David Miller, la Linukso-retiga prizorganto. Depende de ilia naturo - redaktoj aŭ novaj funkcioj - retaj ŝanĝoj falas en unu el du kernoj - netnet-next. Ŝanĝoj por BPF estas distribuitaj en la sama maniero inter bpf и bpf-next, kiuj tiam estas kunigitaj en net kaj net-next, respektive. Por pliaj detaloj, vidu bpf_devel_QA и netdev-FAQ. Do elektu kernon bazitan sur via gusto kaj la stabilecbezonoj de la sistemo, sur kiu vi testas (*-next kernoj estas la plej malstabilaj el tiuj listigitaj).

Estas ekster la amplekso de ĉi tiu artikolo paroli pri kiel administri kernajn agordajn dosierojn - oni supozas, ke vi aŭ jam scias kiel fari tion, aŭ preta lerni memstare. Tamen, la sekvaj instrukcioj devus esti pli-malpli sufiĉaj por doni al vi funkciantan BPF-ebligitan sistemon.

Elŝutu unu el ĉi-supraj kernoj:

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

Konstruu minimuman funkciantan kernan agordon:

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

Ebligu BPF-opciojn en dosiero .config laŭ via elekto (plej verŝajne CONFIG_BPF jam estos ebligita ĉar systemd uzas ĝin). Jen listo de opcioj de la kerno uzata por ĉi tiu artikolo:

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

Tiam ni povas facile kunmeti kaj instali la modulojn kaj la kernon (cetere, vi povas kunveni la kernon uzante la nove kunmetitan clangper aldonado CC=clang):

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

kaj rekomencu per la nova kerno (mi uzas por ĉi tio kexec el la pako 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

La plej ofte uzata utileco en la artikolo estos la utileco bpftool, provizita kiel parto de la Linukso-kerno. Ĝi estas verkita kaj prizorgata de BPF-programistoj por BPF-programistoj kaj povas esti uzata por administri ĉiujn specojn de BPF-objektoj - ŝargi programojn, krei kaj redakti mapojn, esplori la vivon de la BPF-ekosistemo ktp. Dokumentaro en formo de fontkodoj por manpaĝoj troveblas en la kerno aŭ, jam kompilita, en la reto.

En la momento de ĉi tiu skribado bpftool venas preta nur por RHEL, Fedora kaj Ubuntu (vidu, ekzemple, ĉi tiu fadeno, kiu rakontas la nefinitan historion de pakado bpftool en Debian). Sed se vi jam konstruis vian kernon, tiam konstruu bpftool tiel facila kiel torto:

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

$

(Ĉi tie ${linux} - ĉi tiu estas via kerna dosierujo.) Post ekzekuto de ĉi tiuj komandoj bpftool estos kolektita en dosierujo ${linux}/tools/bpf/bpftool kaj ĝi povas esti aldonita al la vojo (antaŭ ĉio al la uzanto root) aŭ simple kopii al /usr/local/sbin.

Kolekti bpftool estas plej bone uzi ĉi-lastan clang, kunmetita kiel supre priskribite, kaj kontrolu ĉu ĝi estas kunmetita ĝuste - uzante, ekzemple, la komandon

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

kiu montros kiuj BPF-funkcioj estas ebligitaj en via kerno.

Cetere, la antaŭa komando povas ruliĝi kiel

# bpftool f p k

Ĉi tio estas farita per analogio kun la utilecoj de la pako iproute2, kie ni povas, ekzemple, diri ip a s eth0 anstataŭ ip addr show dev eth0.

konkludo

BPF permesas vin ŝui pulon por efike mezuri kaj sur-la-muŝe ŝanĝi la funkciecon de la kerno. La sistemo montriĝis tre sukcesa, laŭ la plej bonaj tradicioj de UNIX: simpla mekanismo, kiu permesas vin (re)programi la kernon, permesis al grandega nombro da homoj kaj organizoj eksperimenti. Kaj, kvankam la eksperimentoj, same kiel la disvolviĝo de la BPF-infrastrukturo mem, estas malproksimaj de finita, la sistemo jam havas stabilan ABI, kiu ebligas al vi konstrui fidindan, kaj plej grave, efikan komercan logikon.

Mi ŝatus rimarki, ke, laŭ mi, la teknologio fariĝis tiel populara ĉar, unuflanke, ĝi povas ludi (la arkitekturo de maŝino povas esti komprenata pli-malpli en unu vespero), kaj aliflanke, por solvi problemojn, kiuj ne povus esti solvitaj (bele) antaŭ ĝia apero. Ĉi tiuj du komponantoj kune devigas homojn eksperimenti kaj revi, kio kondukas al apero de pli kaj pli novigaj solvoj.

Ĉi tiu artikolo, kvankam ne precipe mallonga, estas nur enkonduko al la mondo de BPF kaj ne priskribas "altnivelajn" trajtojn kaj gravajn partojn de la arkitekturo. La plano antaŭen estas io kiel ĉi tio: la sekva artikolo estos superrigardo de BPF-programo-tipoj (estas 5.8 programtipoj subtenataj en la 30-kerno), tiam ni finfine rigardos kiel verki realajn BPF-aplikaĵojn uzante kernajn spurajn programojn. kiel ekzemplo, tiam estas tempo por pli profunda kurso pri BPF-arkitekturo, sekvita de ekzemploj de BPF-retoj kaj sekurecaj aplikoj.

Antaŭaj artikoloj en ĉi tiu serio

  1. BPF por la etuloj, parto nul: klasika BPF

Ligiloj

  1. BPF kaj XDP Referenca Gvidilo — dokumentado pri BPF el cilio, aŭ pli precize de Daniel Borkman, unu el la kreintoj kaj prizorgantoj de BPF. Ĉi tiu estas unu el la unuaj seriozaj priskriboj, kiu diferencas de la aliaj pro tio, ke Danielo scias precize pri kio li skribas kaj tie ne estas eraroj. Aparte, ĉi tiu dokumento priskribas kiel labori kun BPF-programoj de la tipoj XDP kaj TC uzante la konatan utilecon. ip el la pako iproute2.

  2. Dokumentado/reto/filtrilo.txt — originala dosiero kun dokumentado por klasika kaj poste plilongigita BPF. Bona legado se vi volas enprofundiĝi en asembla lingvo kaj teknikaj arkitekturaj detaloj.

  3. Blogo pri BPF de facebook. Ĝi estas malofte, sed trafe ĝisdatigita, kiel Alexei Starovoitov (aŭtoro de eBPF) kaj Andrii Nakryiko - (prizorganto) skribas tie libbpf).

  4. Sekretoj de bpftool. Distra tvitera fadeno de Quentin Monnet kun ekzemploj kaj sekretoj pri uzado de bpftool.

  5. Plonĝu en BPF: listo de legmaterialoj. Giganta (kaj ankoraŭ konservita) listo de ligiloj al BPF-dokumentaro de Quentin Monnet.

fonto: www.habr.com

Aldoni komenton