BPF para sa maliliit, part one: extended BPF

Sa simula ay may teknolohiya at ito ay tinatawag na BPF. Napatingin kami sa kanya nauna, artikulo sa Lumang Tipan ng seryeng ito. Noong 2013, sa pamamagitan ng pagsisikap nina Alexei Starovoitov at Daniel Borkman, isang pinahusay na bersyon nito, na na-optimize para sa mga modernong 64-bit na makina, ay binuo at isinama sa Linux kernel. Ang bagong teknolohiyang ito ay panandaliang tinawag na Internal BPF, pagkatapos ay pinalitan ng pangalan na Extended BPF, at ngayon, pagkaraan ng ilang taon, tinawag na lang itong BPF ng lahat.

Sa halos pagsasalita, pinapayagan ka ng BPF na magpatakbo ng di-makatwirang code na ibinigay ng gumagamit sa espasyo ng kernel ng Linux, at ang bagong arkitektura ay naging matagumpay na kakailanganin namin ng isang dosenang higit pang mga artikulo upang ilarawan ang lahat ng mga aplikasyon nito. (Ang tanging bagay na hindi nagawa ng mga developer, tulad ng makikita mo sa performance code sa ibaba, ay ang paglikha ng isang disenteng logo.)

Inilalarawan ng artikulong ito ang istraktura ng virtual machine ng BPF, mga interface ng kernel para sa pagtatrabaho sa BPF, mga tool sa pag-unlad, pati na rin ang isang maikli, napakaikling pangkalahatang-ideya ng mga umiiral na kakayahan, i.e. lahat ng kakailanganin natin sa hinaharap para sa mas malalim na pag-aaral ng mga praktikal na aplikasyon ng BPF.
BPF para sa maliliit, part one: extended BPF

Buod ng artikulo

Panimula sa arkitektura ng BPF. Una, titingnan natin ang arkitektura ng BPF at balangkasin ang mga pangunahing bahagi.

Mga rehistro at command system ng BPF virtual machine. Mayroon nang ideya ng arkitektura sa kabuuan, ilalarawan namin ang istraktura ng virtual machine ng BPF.

Life cycle ng BPF objects, bpffs file system. Sa seksyong ito, titingnan natin ang siklo ng buhay ng mga bagay sa BPF - mga programa at mapa.

Pamamahala ng mga bagay gamit ang bpf system call. Sa ilang pag-unawa sa system na mayroon na, titingnan natin sa wakas kung paano lumikha at magmanipula ng mga bagay mula sa espasyo ng gumagamit gamit ang isang espesyal na tawag sa system βˆ’ bpf(2).

ПишСм ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΡ‹ BPF с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ libbpf. Siyempre, maaari kang magsulat ng mga programa gamit ang isang tawag sa system. Pero mahirap. Para sa isang mas makatotohanang senaryo, ang mga nuclear programmer ay bumuo ng isang aklatan libbpf. Gagawa kami ng basic BPF application skeleton na gagamitin namin sa mga susunod na halimbawa.

Mga Katulong sa Kernel. Dito natin malalaman kung paano maa-access ng mga programa ng BPF ang mga function ng kernel helper - isang tool na, kasama ng mga mapa, sa panimula ay nagpapalawak ng mga kakayahan ng bagong BPF kumpara sa classic.

Access sa mga mapa mula sa mga programa ng BPF. Sa puntong ito, sapat na ang ating malalaman upang maunawaan nang eksakto kung paano tayo makakalikha ng mga program na gumagamit ng mga mapa. At tingnan pa natin ang mahusay at makapangyarihang taga-verify.

Mga tool sa pag-unlad. Seksyon ng tulong kung paano i-assemble ang mga kinakailangang utility at kernel para sa mga eksperimento.

Konklusyon. Sa dulo ng artikulo, ang mga nagbabasa nito hanggang dito ay makakahanap ng nakapagpapasiglang mga salita at isang maikling paglalarawan kung ano ang mangyayari sa susunod na mga artikulo. Maglilista din kami ng ilang link para sa sariling pag-aaral para sa mga walang pagnanais o kakayahang maghintay para sa pagpapatuloy.

Panimula sa Arkitektura ng BPF

Bago natin simulan ang pagsasaalang-alang sa arkitektura ng BPF, sasangguni tayo sa huling pagkakataon (oh). klasikong BPF, na binuo bilang tugon sa pagdating ng RISC machine at nilutas ang problema ng mahusay na packet filtering. Ang arkitektura ay naging napaka-matagumpay na, nang isinilang sa napakagandang nineties sa Berkeley UNIX, ito ay nai-port sa karamihan ng mga umiiral na operating system, nakaligtas sa nakatutuwang twenties at naghahanap pa rin ng mga bagong application.

Ang bagong BPF ay binuo bilang tugon sa ubiquity ng 64-bit machine, cloud services at ang tumaas na pangangailangan para sa mga tool para sa paglikha ng SDN (Sofware-defined neworking). Binuo ng mga inhinyero ng kernel network bilang pinahusay na kapalit para sa klasikong BPF, ang bagong BPF ay literal na makalipas ang anim na buwan ay nakahanap ng mga aplikasyon sa mahirap na gawain ng pagsubaybay sa mga sistema ng Linux, at ngayon, anim na taon pagkatapos ng paglitaw nito, kakailanganin natin ang isang buong susunod na artikulo para lamang ilista ang iba't ibang uri ng mga programa.

Nakakatawang mga larawan

Sa kaibuturan nito, ang BPF ay isang sandbox virtual machine na nagbibigay-daan sa iyong patakbuhin ang "arbitrary" na code sa kernel space nang hindi nakompromiso ang seguridad. Ang mga programa ng BPF ay nilikha sa espasyo ng gumagamit, na-load sa kernel, at nakakonekta sa ilang pinagmulan ng kaganapan. Ang isang kaganapan ay maaaring, halimbawa, ang paghahatid ng isang packet sa isang interface ng network, ang paglulunsad ng ilang function ng kernel, atbp. Sa kaso ng isang package, ang BPF program ay magkakaroon ng access sa data at metadata ng package (para sa pagbabasa at, posibleng, pagsusulat, depende sa uri ng programa); sa kaso ng pagpapatakbo ng kernel function, ang mga argumento ng ang function, kabilang ang mga pointer sa kernel memory, atbp.

Tingnan natin ang prosesong ito nang mas malapitan. Upang magsimula, pag-usapan natin ang unang pagkakaiba mula sa klasikong BPF, mga programa kung saan isinulat sa assembler. Sa bagong bersyon, ang arkitektura ay pinalawak upang ang mga programa ay maisulat sa mataas na antas ng mga wika, lalo na, siyempre, sa C. Para dito, isang backend para sa llvm ay binuo, na nagpapahintulot sa iyo na bumuo ng bytecode para sa arkitektura ng BPF.

BPF para sa maliliit, part one: extended BPF

Ang arkitektura ng BPF ay idinisenyo, sa bahagi, upang tumakbo nang mahusay sa mga modernong makina. Upang magawa ito sa pagsasanay, ang BPF bytecode, sa sandaling na-load sa kernel, ay isinalin sa katutubong code gamit ang isang bahagi na tinatawag na JIT compiler (Just In Time). Susunod, kung naaalala mo, sa klasikong BPF ang programa ay na-load sa kernel at naka-attach sa pinagmulan ng kaganapan nang atomically - sa konteksto ng isang solong tawag sa system. Sa bagong arkitektura, ito ay nangyayari sa dalawang yugto - una, ang code ay na-load sa kernel gamit ang isang tawag sa system bpf(2)at pagkatapos, sa ibang pagkakataon, sa pamamagitan ng iba pang mga mekanismo na nag-iiba-iba depende sa uri ng programa, nakakabit ang program sa pinagmulan ng kaganapan.

Dito maaaring may tanong ang mambabasa: posible ba? Paano ginagarantiyahan ang kaligtasan ng pagpapatupad ng naturang code? Ang kaligtasan ng pagpapatupad ay ginagarantiyahan sa amin sa pamamagitan ng yugto ng paglo-load ng mga programa ng BPF na tinatawag na verifier (sa Ingles ang yugtong ito ay tinatawag na verifier at patuloy kong gagamitin ang salitang Ingles):

BPF para sa maliliit, part one: extended BPF

Ang verifier ay isang static na analyzer na nagsisiguro na ang isang program ay hindi nakakaabala sa normal na operasyon ng kernel. Ito, sa pamamagitan ng paraan, ay hindi nangangahulugan na ang programa ay hindi makagambala sa pagpapatakbo ng system - Ang mga programa ng BPF, depende sa uri, ay maaaring basahin at muling isulat ang mga seksyon ng memorya ng kernel, ibalik ang mga halaga ng mga pag-andar, gupitin, idagdag, isulat muli at kahit na ipasa ang mga packet ng network. Tinitiyak ng Verifier na ang pagpapatakbo ng isang BPF program ay hindi mag-crash sa kernel at ang isang program na, ayon sa mga patakaran, ay may access sa pagsulat, halimbawa, ang data ng isang papalabas na packet, ay hindi magagawang i-overwrite ang kernel memory sa labas ng packet. Titingnan namin ang verifier sa kaunting detalye sa kaukulang seksyon, pagkatapos naming makilala ang lahat ng iba pang bahagi ng BPF.

Kaya ano ang natutunan natin sa ngayon? Ang gumagamit ay nagsusulat ng isang programa sa C, nilo-load ito sa kernel gamit ang isang tawag sa system bpf(2), kung saan ito ay sinusuri ng isang verifier at isinalin sa katutubong bytecode. Pagkatapos ay ikinonekta ng pareho o ibang user ang program sa pinagmulan ng kaganapan at magsisimula itong isagawa. Ang paghihiwalay ng boot at koneksyon ay kinakailangan para sa ilang kadahilanan. Una, ang pagpapatakbo ng verifier ay medyo mahal at sa pamamagitan ng pag-download ng parehong program nang ilang beses ay nag-aaksaya kami ng oras sa computer. Pangalawa, eksakto kung paano konektado ang isang programa ay depende sa uri nito, at ang isang "unibersal" na interface na binuo noong isang taon ay maaaring hindi angkop para sa mga bagong uri ng mga programa. (Bagaman ngayon na ang arkitektura ay nagiging mas mature, may ideya na pag-isahin ang interface na ito sa antas libbpf.)

Maaaring mapansin ng matulungin na mambabasa na hindi pa tayo tapos sa mga larawan. Sa katunayan, hindi ipinapaliwanag ng lahat ng nasa itaas kung bakit binago ng BPF ang larawan kumpara sa klasikong BPF. Dalawang inobasyon na makabuluhang nagpapalawak sa saklaw ng pagiging angkop ay ang kakayahang gumamit ng shared memory at kernel helper function. Sa BPF, ipinapatupad ang nakabahaging memorya gamit ang tinatawag na mga mapa - mga nakabahaging istruktura ng data na may partikular na API. Malamang na nakuha nila ang pangalang ito dahil ang unang uri ng mapa na lumitaw ay isang hash table. Pagkatapos ay lumitaw ang mga array, lokal (per-CPU) hash table at lokal na array, search tree, mga mapa na naglalaman ng mga pointer sa BPF program at marami pang iba. Ang kawili-wili sa amin ngayon ay ang mga programa ng BPF ay mayroon na ngayong kakayahan na magpatuloy sa estado sa pagitan ng mga tawag at ibahagi ito sa ibang mga programa at sa espasyo ng gumagamit.

Maa-access ang mga mapa mula sa mga proseso ng user gamit ang isang system call bpf(2), at mula sa mga programang BPF na tumatakbo sa kernel gamit ang mga function ng helper. Bukod dito, ang mga katulong ay umiiral hindi lamang upang gumana sa mga mapa, kundi pati na rin upang ma-access ang iba pang mga kakayahan sa kernel. Halimbawa, ang mga programa ng BPF ay maaaring gumamit ng mga function ng helper upang ipasa ang mga packet sa iba pang mga interface, bumuo ng mga kaganapan sa perf, ma-access ang mga istruktura ng kernel, at iba pa.

BPF para sa maliliit, part one: extended BPF

Sa buod, ang BPF ay nagbibigay ng kakayahang mag-load ng arbitrary, ibig sabihin, verifier-tested, user code sa kernel space. Maaaring i-save ng code na ito ang estado sa pagitan ng mga tawag at palitan ng data sa espasyo ng user, at mayroon ding access sa mga kernel subsystem na pinapayagan ng ganitong uri ng program.

Ito ay katulad na sa mga kakayahan na ibinigay ng mga module ng kernel, kumpara sa kung saan ang BPF ay may ilang mga pakinabang (siyempre, maaari mo lamang ihambing ang mga katulad na aplikasyon, halimbawa, pagsubaybay sa system - hindi ka maaaring magsulat ng isang di-makatwirang driver na may BPF). Maaari mong tandaan ang isang mas mababang entry threshold (ang ilang mga utility na gumagamit ng BPF ay hindi nangangailangan ng gumagamit na magkaroon ng mga kasanayan sa kernel programming, o mga kasanayan sa programming sa pangkalahatan), kaligtasan ng runtime (itaas ang iyong kamay sa mga komento para sa mga hindi nasira ang system kapag nagsusulat o mga module ng pagsubok), atomicity - mayroong downtime kapag nagre-reload ng mga module, at tinitiyak ng subsystem ng BPF na walang mga kaganapan ang napalampas (para maging patas, hindi ito totoo para sa lahat ng uri ng mga programa ng BPF).

Ang pagkakaroon ng gayong mga kakayahan ay ginagawang isang unibersal na tool ang BPF para sa pagpapalawak ng kernel, na nakumpirma sa pagsasanay: parami nang parami ang mga bagong uri ng mga programa ay idinagdag sa BPF, parami nang parami ang malalaking kumpanya na gumagamit ng BPF sa mga server ng labanan 24 Γ— 7, higit pa at higit pa itinatayo ng mga startup ang kanilang negosyo sa mga solusyon batay sa kung saan nakabatay sa BPF. Ginagamit ang BPF saanman: sa pagprotekta laban sa mga pag-atake ng DDoS, paglikha ng SDN (halimbawa, pagpapatupad ng mga network para sa mga kubernetes), bilang pangunahing tool sa pagsubaybay sa system at kolektor ng istatistika, sa mga intrusion detection system at sandbox system, atbp.

Tapusin natin ang pangkalahatang-ideya na bahagi ng artikulo dito at tingnan ang virtual machine at ang BPF ecosystem nang mas detalyado.

Paglihis: mga kagamitan

Upang magawang patakbuhin ang mga halimbawa sa mga sumusunod na seksyon, maaaring kailangan mo ng ilang mga utility, kahit man lang llvm/clang na may suporta sa bpf at bpftool. Sa seksyon Mga tool sa pag-unlad Maaari mong basahin ang mga tagubilin para sa pag-assemble ng mga utility, pati na rin ang iyong kernel. Ang seksyong ito ay inilalagay sa ibaba upang hindi makagambala sa pagkakaisa ng aming presentasyon.

BPF Virtual Machine Registers at Instruction System

Ang arkitektura at sistema ng utos ng BPF ay binuo na isinasaalang-alang ang katotohanan na ang mga programa ay isusulat sa wikang C at, pagkatapos mag-load sa kernel, isinalin sa katutubong code. Samakatuwid, ang bilang ng mga rehistro at ang hanay ng mga utos ay pinili nang may mata sa intersection, sa matematikal na kahulugan, ng mga kakayahan ng mga modernong makina. Bilang karagdagan, ang iba't ibang mga paghihigpit ay ipinataw sa mga programa, halimbawa, hanggang kamakailan ay hindi posible na magsulat ng mga loop at subroutine, at ang bilang ng mga tagubilin ay limitado sa 4096 (ngayon ang mga may pribilehiyong programa ay maaaring mag-load ng hanggang sa isang milyong mga tagubilin).

Ang BPF ay may labing-isang 64-bit na rehistro na naa-access ng gumagamit r0-r10 at isang program counter. Magrehistro r10 naglalaman ng frame pointer at read-only. Ang mga programa ay may access sa isang 512-byte na stack sa runtime at isang walang limitasyong dami ng nakabahaging memorya sa anyo ng mga mapa.

Ang mga programa ng BPF ay pinahihintulutan na magpatakbo ng isang partikular na hanay ng mga program-type na kernel helper at, kamakailan lamang, mga regular na function. Ang bawat tinatawag na function ay maaaring tumagal ng hanggang limang argumento, na ipinasa sa mga rehistro r1-r5, at ang return value ay ipapasa sa r0. Ito ay ginagarantiyahan na pagkatapos bumalik mula sa pag-andar, ang mga nilalaman ng mga rehistro r6-r9 Hindi magbabago.

Para sa mahusay na pagsasalin ng programa, magrehistro r0-r11 para sa lahat ng mga sinusuportahang arkitektura ay natatanging nakamapa sa mga tunay na rehistro, na isinasaalang-alang ang mga tampok ng ABI ng kasalukuyang arkitektura. Halimbawa, para sa x86_64 nagrerehistro r1-r5, na ginagamit upang ipasa ang mga parameter ng function, ay ipinapakita sa rdi, rsi, rdx, rcx, r8, na ginagamit upang ipasa ang mga parameter sa mga function x86_64. Halimbawa, ang code sa kaliwa ay isinasalin sa code sa kanan tulad nito:

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

Magrehistro r0 ginagamit din upang ibalik ang resulta ng pagpapatupad ng programa, at sa rehistro r1 ang programa ay ipinasa ng isang pointer sa konteksto - depende sa uri ng programa, ito ay maaaring, halimbawa, isang istraktura struct xdp_md (para sa XDP) o istraktura struct __sk_buff (para sa iba't ibang mga programa sa network) o istraktura struct pt_regs (para sa iba't ibang uri ng mga programa sa pagsubaybay), atbp.

Kaya, mayroon kaming isang set ng mga rehistro, mga katulong sa kernel, isang stack, isang pointer ng konteksto at nakabahaging memorya sa anyo ng mga mapa. Hindi sa lahat ng ito ay ganap na kinakailangan sa paglalakbay, ngunit...

Ipagpatuloy natin ang paglalarawan at pag-usapan ang command system para sa pagtatrabaho sa mga bagay na ito. lahat (Halos lahat ng) Ang mga tagubilin ng BPF ay may nakapirming 64-bit na laki. Kung titingnan mo ang isang pagtuturo sa isang 64-bit na Big Endian machine makikita mo

BPF para sa maliliit, part one: extended BPF

Dito Code - ito ang pag-encode ng pagtuturo, Dst/Src ay ang mga pag-encode ng receiver at source, ayon sa pagkakabanggit, Off - 16-bit na naka-sign indentation, at Imm ay isang 32-bit signed integer na ginagamit sa ilang mga tagubilin (katulad ng cBPF constant K). Encoding Code ay may isa sa dalawang uri:

BPF para sa maliliit, part one: extended BPF

Ang mga klase ng pagtuturo 0, 1, 2, 3 ay tumutukoy sa mga utos para sa pagtatrabaho sa memorya. sila ay tinawag, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, ayon sa pagkakabanggit. Mga klase 4, 7 (BPF_ALU, BPF_ALU64) ay bumubuo ng isang hanay ng mga tagubilin ng ALU. Mga klase 5, 6 (BPF_JMP, BPF_JMP32) naglalaman ng mga tagubilin sa pagtalon.

Ang karagdagang plano para sa pag-aaral ng sistema ng pagtuturo ng BPF ay ang mga sumusunod: sa halip na masusing ilista ang lahat ng mga tagubilin at ang kanilang mga parameter, titingnan natin ang ilang mga halimbawa sa seksyong ito at mula sa kanila ay magiging malinaw kung paano gumagana ang mga tagubilin at kung paano mano-manong i-disassemble ang anumang binary file para sa BPF. Upang pagsama-samahin ang materyal sa ibang pagkakataon sa artikulo, makakatagpo din kami ng mga indibidwal na tagubilin sa mga seksyon tungkol sa Verifier, JIT compiler, pagsasalin ng klasikong BPF, pati na rin kapag nag-aaral ng mga mapa, mga function ng pagtawag, atbp.

Kapag pinag-uusapan natin ang tungkol sa mga indibidwal na tagubilin, sasangguni tayo sa mga pangunahing file bpf.h ΠΈ bpf_common.h, na tumutukoy sa mga numerical code ng mga tagubilin sa BPF. Kapag nag-aaral ng arkitektura nang mag-isa at/o nag-parse ng mga binary, mahahanap mo ang mga semantika sa mga sumusunod na mapagkukunan, na pinagsunod-sunod ayon sa pagiging kumplikado: Hindi opisyal na eBPF spec, BPF at XDP Reference Guide, Instruction Set, Dokumentasyon/networking/filter.txt at, siyempre, sa Linux source code - verifier, JIT, BPF interpreter.

Halimbawa: pag-disassembling ng BPF sa iyong ulo

Tingnan natin ang isang halimbawa kung saan nag-compile tayo ng isang programa readelf-example.c at tingnan ang resultang binary. Ibubunyag namin ang orihinal na nilalaman readelf-example.c sa ibaba, pagkatapos naming ibalik ang lohika nito mula sa mga binary code:

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

Unang column sa output readelf ay isang indentation at ang aming programa ay binubuo ng apat na command:

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

Ang mga command code ay pantay b7, 15, b7 ΠΈ 95. Alalahanin na ang hindi bababa sa makabuluhang tatlong bit ay ang klase ng pagtuturo. Sa aming kaso, ang ikaapat na bit ng lahat ng mga tagubilin ay walang laman, kaya ang mga klase ng pagtuturo ay 7, 5, 7, 5, ayon sa pagkakabanggit. Ang Class 7 ay BPF_ALU64, at 5 ay BPF_JMP. Para sa parehong mga klase, ang format ng pagtuturo ay pareho (tingnan sa itaas) at maaari naming muling isulat ang aming programa tulad nito (sa parehong oras ay muling isusulat namin ang natitirang mga haligi sa anyong tao):

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

Operasyon b klase ALU64 - Ay BPF_MOV. Nagtatalaga ito ng halaga sa rehistro ng patutunguhan. Kung nakatakda ang bit s (pinagmulan), kung gayon ang halaga ay kinuha mula sa rehistro ng pinagmulan, at kung, tulad ng sa aming kaso, hindi ito nakatakda, kung gayon ang halaga ay kinuha mula sa patlang Imm. Kaya sa una at pangatlong mga tagubilin ginagawa namin ang operasyon r0 = Imm. Dagdag pa, ang JMP class 1 na operasyon ay BPF_JEQ (tumalon kung katumbas). Sa aming kaso, mula noong kaunti S ay zero, inihahambing nito ang halaga ng source register sa field Imm. Kung ang mga halaga ay nag-tutugma, pagkatapos ay ang paglipat ay nangyayari sa PC + OffSaan PC, gaya ng dati, ay naglalaman ng address ng susunod na pagtuturo. Sa wakas, ang JMP Class 9 Operation ay BPF_EXIT. Tinatapos ng pagtuturo na ito ang programa, babalik sa kernel r0. Magdagdag tayo ng bagong column sa ating table:

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

Maaari naming muling isulat ito sa isang mas maginhawang anyo:

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

Kung naaalala natin kung ano ang nasa rehistro r1 ang programa ay ipinasa ng isang pointer sa konteksto mula sa kernel, at sa rehistro r0 ang halaga ay ibinalik sa kernel, pagkatapos ay makikita natin na kung ang pointer sa konteksto ay zero, pagkatapos ay ibabalik natin ang 1, at kung hindi man - 2. Tingnan natin kung tama tayo sa pamamagitan ng pagtingin sa pinagmulan:

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

Oo, isa itong walang kabuluhang programa, ngunit isinasalin ito sa apat na simpleng tagubilin.

Halimbawa ng pagbubukod: 16-byte na pagtuturo

Nabanggit namin kanina na ang ilang mga tagubilin ay tumatagal ng higit sa 64 bits. Nalalapat ito, halimbawa, sa mga tagubilin lddw (Code = 0x18 = BPF_LD | BPF_DW | BPF_IMM) β€” mag-load ng dobleng salita mula sa mga patlang sa rehistro Imm. Ang katotohanan ay iyan Imm ay may sukat na 32, at ang dobleng salita ay 64 bits, kaya ang paglo-load ng 64-bit na agarang halaga sa isang rehistro sa isang 64-bit na pagtuturo ay hindi gagana. Upang gawin ito, dalawang katabing tagubilin ang ginagamit upang iimbak ang pangalawang bahagi ng 64-bit na halaga sa field. Imm... Halimbawa:

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

Mayroon lamang dalawang mga tagubilin sa isang binary program:

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

Magkikita tayong muli na may mga tagubilin lddw, kapag pinag-uusapan natin ang tungkol sa mga relokasyon at pagtatrabaho sa mga mapa.

Halimbawa: pag-disassembling ng BPF gamit ang mga karaniwang tool

Kaya, natutunan naming basahin ang mga binary code ng BPF at handang i-parse ang anumang pagtuturo kung kinakailangan. Gayunpaman, nararapat na sabihin na sa pagsasagawa ito ay mas maginhawa at mas mabilis na i-disassemble ang mga programa gamit ang mga karaniwang tool, halimbawa:

$ 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

Lifecycle ng BPF objects, bpffs file system

(Una kong natutunan ang ilan sa mga detalyeng inilarawan sa subsection na ito mula sa pag-aayuno Alexei Starovoitov sa BPF Blog.)

Ang mga bagay ng BPF - mga programa at mapa - ay nilikha mula sa espasyo ng gumagamit gamit ang mga utos BPF_PROG_LOAD ΠΈ BPF_MAP_CREATE tawag sa sistema bpf(2), pag-uusapan natin nang eksakto kung paano ito nangyayari sa susunod na seksyon. Lumilikha ito ng mga istruktura ng data ng kernel at para sa bawat isa sa kanila refcount (reference count) ay nakatakda sa isa, at isang file descriptor na tumuturo sa object ay ibinalik sa user. Matapos maisara ang hawakan refcount ang bagay ay nabawasan ng isa, at kapag ito ay umabot sa zero, ang bagay ay nawasak.

Kung ang programa ay gumagamit ng mga mapa, kung gayon refcount ang mga mapa na ito ay nadagdagan ng isa pagkatapos i-load ang programa, i.e. ang kanilang mga file descriptor ay maaaring isara mula sa proseso ng user at pa rin refcount hindi magiging zero:

BPF para sa maliliit, part one: extended BPF

Pagkatapos ng matagumpay na paglo-load ng isang programa, karaniwan naming ikinakabit ito sa ilang uri ng generator ng kaganapan. Halimbawa, maaari naming ilagay ito sa isang interface ng network upang iproseso ang mga papasok na packet o ikonekta ito sa ilan tracepoint sa kaibuturan. Sa puntong ito, ang reference counter ay tataas din ng isa at magagawa naming isara ang file descriptor sa loader program.

Ano ang mangyayari kung isasara natin ngayon ang bootloader? Depende ito sa uri ng generator ng kaganapan (hook). Ang lahat ng network hooks ay iiral pagkatapos makumpleto ang loader, ito ang tinatawag na global hooks. At, halimbawa, ang mga trace program ay ilalabas pagkatapos magwakas ang proseso na lumikha sa kanila (at samakatuwid ay tinatawag na lokal, mula sa "lokal hanggang sa proseso"). Sa teknikal, ang mga lokal na kawit ay laging may kaukulang file descriptor sa espasyo ng gumagamit at samakatuwid ay nagsasara kapag ang proseso ay sarado, ngunit ang mga global na kawit ay wala. Sa sumusunod na figure, gamit ang mga pulang krus, sinusubukan kong ipakita kung paano nakakaapekto ang pagwawakas ng programa ng loader sa buhay ng mga bagay sa kaso ng mga lokal at pandaigdigang kawit.

BPF para sa maliliit, part one: extended BPF

Bakit may pagkakaiba sa pagitan ng lokal at pandaigdigang mga kawit? Ang pagpapatakbo ng ilang mga uri ng mga programa sa network ay may katuturan nang walang userspace, halimbawa, isipin ang proteksyon ng DDoS - isinulat ng bootloader ang mga patakaran at ikinokonekta ang programa ng BPF sa interface ng network, pagkatapos kung saan ang bootloader ay maaaring pumunta at pumatay sa sarili nito. Sa kabilang banda, isipin ang isang programa sa pag-debug na bakas na isinulat mo sa iyong mga tuhod sa loob ng sampung minuto - kapag natapos na ito, gusto mong walang natira na basura sa system, at titiyakin iyon ng mga lokal na kawit.

Sa kabilang banda, isipin na gusto mong kumonekta sa isang tracepoint sa kernel at mangolekta ng mga istatistika sa loob ng maraming taon. Sa kasong ito, gugustuhin mong kumpletuhin ang bahagi ng gumagamit at bumalik sa mga istatistika paminsan-minsan. Ang bpf file system ay nagbibigay ng pagkakataong ito. Ito ay isang in-memory-only pseudo-file system na nagbibigay-daan sa paglikha ng mga file na tumutukoy sa mga bagay na BPF at sa gayon ay tumataas refcount mga bagay. Pagkatapos nito, maaaring lumabas ang loader, at mananatiling buhay ang mga bagay na nilikha nito.

BPF para sa maliliit, part one: extended BPF

Ang paggawa ng mga file sa mga bpff na tumutukoy sa mga bagay sa BPF ay tinatawag na "pinning" (tulad ng sa sumusunod na parirala: "ang proseso ay maaaring mag-pin ng BPF program o mapa"). Ang paglikha ng mga file object para sa mga BPF object ay may katuturan hindi lamang para sa pagpapahaba ng buhay ng mga lokal na bagay, kundi para din sa kakayahang magamit ng mga pandaigdigang bagay - babalik sa halimbawa sa pandaigdigang programa ng proteksyon ng DDoS, gusto naming makapunta at tumingin sa mga istatistika paminsan-minsan.

Karaniwang naka-mount ang BPF file system /sys/fs/bpf, ngunit maaari rin itong i-mount nang lokal, halimbawa, tulad nito:

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

Ang mga pangalan ng file system ay nilikha gamit ang command BPF_OBJ_PIN BPF system call. Upang ilarawan, kumuha tayo ng isang programa, i-compile ito, i-upload ito, at i-pin ito sa bpffs. Ang aming programa ay walang ginagawang kapaki-pakinabang, ipinapakita lamang namin ang code upang maaari mong kopyahin ang halimbawa:

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

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

I-compile natin ang program na ito at lumikha ng lokal na kopya ng file system bpffs:

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

Ngayon i-download natin ang aming programa gamit ang utility bpftool at tingnan ang mga kasamang tawag sa system bpf(2) (inalis ang ilang hindi nauugnay na linya mula sa strace output):

$ 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

Dito namin na-load ang program gamit BPF_PROG_LOAD, nakatanggap ng file descriptor mula sa kernel 3 at gamit ang utos BPF_OBJ_PIN na-pin ang descriptor ng file na ito bilang isang file "bpf-mountpoint/test". Pagkatapos nito ang bootloader program bpftool tapos na magtrabaho, ngunit ang aming programa ay nanatili sa kernel, kahit na hindi namin ito ikinakabit sa anumang interface ng network:

$ 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

Maaari naming tanggalin ang file object nang normal unlink(2) at pagkatapos nito ay tatanggalin ang kaukulang programa:

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

Pagtanggal ng mga bagay

Sa pagsasalita tungkol sa pagtanggal ng mga bagay, kinakailangang linawin na pagkatapos nating idiskonekta ang programa mula sa hook (generator ng kaganapan), walang isang bagong kaganapan ang magti-trigger sa paglulunsad nito, gayunpaman, ang lahat ng kasalukuyang mga pagkakataon ng programa ay makukumpleto sa normal na pagkakasunud-sunod .

Ang ilang mga uri ng mga programa ng BPF ay nagpapahintulot sa iyo na palitan ang programa sa mabilisang, i.e. magbigay ng sequence atomicity replace = detach old program, attach new program. Sa kasong ito, ang lahat ng aktibong pagkakataon ng lumang bersyon ng programa ay matatapos sa kanilang trabaho, at ang mga bagong tagapangasiwa ng kaganapan ay gagawin mula sa bagong programa, at ang "atomicity" dito ay nangangahulugan na walang isang kaganapan ang mapalampas.

Pag-attach ng mga programa sa mga mapagkukunan ng kaganapan

Sa artikulong ito, hindi namin hiwalay na ilalarawan ang pagkonekta ng mga programa sa mga pinagmumulan ng kaganapan, dahil makatuwirang pag-aralan ito sa konteksto ng isang partikular na uri ng programa. Cm. halimbawa sa ibaba, kung saan ipinapakita namin kung paano konektado ang mga program tulad ng XDP.

Pagmamanipula ng mga Bagay Gamit ang bpf System Call

mga programa ng BPF

Lahat ng BPF objects ay nilikha at pinamamahalaan mula sa user space gamit ang isang system call bpf, pagkakaroon ng sumusunod na prototype:

#include <linux/bpf.h>

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

Narito ang koponan cmd ay isa sa mga halaga ng uri enum bpf_cmd, attr β€” isang pointer sa mga parameter para sa isang partikular na programa at size β€” laki ng bagay ayon sa pointer, i.e. kadalasan ito sizeof(*attr). Sa kernel 5.8 ang system call bpf sumusuporta sa 34 iba't ibang mga utos, at ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ union bpf_attr sumasakop sa 200 linya. Ngunit hindi tayo dapat matakot dito, dahil magiging pamilyar tayo sa mga utos at parameter sa paglipas ng ilang artikulo.

Magsimula tayo sa koponan BPF_PROG_LOAD, na lumilikha ng mga programang BPF - kumukuha ng isang set ng mga tagubilin sa BPF at nilo-load ito sa kernel. Sa sandali ng paglo-load, ang verifier ay inilunsad, at pagkatapos ay ang JIT compiler at, pagkatapos ng matagumpay na pagpapatupad, ang program file descriptor ay ibinalik sa user. Nakita namin ang susunod na nangyari sa kanya sa nakaraang seksyon tungkol sa ikot ng buhay ng mga bagay na BPF.

Magsusulat kami ngayon ng isang pasadyang programa na maglo-load ng isang simpleng BPF program, ngunit kailangan muna nating magpasya kung anong uri ng programa ang gusto nating i-load - kailangan nating pumili uri ng at sa loob ng balangkas ng ganitong uri, magsulat ng program na papasa sa verifier test. Gayunpaman, upang hindi kumplikado ang proseso, narito ang isang handa na solusyon: kukuha kami ng isang programa tulad ng BPF_PROG_TYPE_XDP, na magbabalik ng halaga XDP_PASS (laktawan ang lahat ng mga pakete). Sa BPF assembler mukhang napaka-simple:

r0 = 2
exit

Pagkatapos naming magdesisyon na mag-a-upload kami, masasabi namin sa iyo kung paano namin ito gagawin:

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

Ang mga kagiliw-giliw na kaganapan sa isang programa ay nagsisimula sa kahulugan ng isang array insns - ang aming BPF program sa machine code. Sa kasong ito, ang bawat pagtuturo ng programa ng BPF ay naka-pack sa istraktura bpf_insn. Unang elemento insns sumusunod sa mga tagubilin r0 = 2, ang ikalawa - exit.

Retreat. Tinutukoy ng kernel ang mga mas maginhawang macro para sa pagsusulat ng mga code ng makina, at gamit ang kernel header file tools/include/linux/filter.h maaari tayong magsulat

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

Ngunit dahil ang pagsusulat ng mga BPF program sa native code ay kailangan lamang para sa pagsusulat ng mga pagsubok sa kernel at mga artikulo tungkol sa BPF, ang kawalan ng mga macro na ito ay hindi talagang kumplikado sa buhay ng developer.

Pagkatapos tukuyin ang BPF program, nagpapatuloy kami sa paglo-load nito sa kernel. Ang aming minimalist na hanay ng mga parameter attr kasama ang uri ng programa, hanay at bilang ng mga tagubilin, kinakailangang lisensya, at pangalan "woo", na ginagamit namin upang mahanap ang aming program sa system pagkatapos mag-download. Ang programa, tulad ng ipinangako, ay na-load sa system gamit ang isang system call bpf.

Sa pagtatapos ng programa napupunta kami sa isang walang katapusang loop na ginagaya ang payload. Kung wala ito, ang program ay papatayin ng kernel kapag ang file descriptor na ibinalik sa amin ng system call ay sarado bpf, at hindi natin ito makikita sa system.

Well, handa na kami para sa pagsubok. Magtipon tayo at patakbuhin ang programa sa ilalim straceupang suriin kung gumagana ang lahat ayon sa nararapat:

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

Maayos ang lahat, bpf(2) ibinalik ang hawakan 3 sa amin at nagpunta kami sa isang walang katapusang loop na may pause(). Subukan nating hanapin ang ating programa sa system. Upang gawin ito, pupunta kami sa isa pang terminal at gagamitin ang utility 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)

Nakita namin na mayroong isang load na programa sa system woo na ang global ID ay 390 at kasalukuyang isinasagawa simple-prog mayroong isang bukas na deskriptor ng file na tumuturo sa programa (at kung simple-prog tatapusin ang trabaho, kung gayon woo mawawala). Gaya ng inaasahan, ang programa woo tumatagal ng 16 bytes - dalawang tagubilin - ng mga binary code sa arkitektura ng BPF, ngunit sa katutubong anyo nito (x86_64) ito ay 40 bytes na. Tingnan natin ang aming programa sa orihinal nitong anyo:

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

walang sorpresa. Ngayon tingnan natin ang code na nabuo ng JIT compiler:

# 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

hindi masyadong epektibo para sa exit(2), ngunit in fairness, ang aming programa ay masyadong simple, at para sa mga non-trivial na programa ang prologue at epilogue na idinagdag ng JIT compiler ay, siyempre, kailangan.

Maps

Ang mga BPF program ay maaaring gumamit ng mga structured na lugar ng memorya na naa-access kapwa sa iba pang BPF program at sa mga program sa user space. Ang mga bagay na ito ay tinatawag na mga mapa at sa seksyong ito ay ipapakita namin kung paano manipulahin ang mga ito gamit ang isang system call bpf.

Sabihin natin kaagad na ang mga kakayahan ng mga mapa ay hindi limitado lamang sa pag-access sa nakabahaging memorya. Mayroong mga mapa na may espesyal na layunin na naglalaman, halimbawa, mga pointer sa mga programa ng BPF o mga pointer sa mga interface ng network, mga mapa para sa pagtatrabaho sa mga kaganapan sa perf, atbp. Hindi natin sila pag-uusapan dito, para hindi malito ang mambabasa. Bukod dito, binabalewala namin ang mga isyu sa pag-synchronize, dahil hindi ito mahalaga para sa aming mga halimbawa. Ang isang kumpletong listahan ng mga magagamit na uri ng mapa ay matatagpuan sa <linux/bpf.h>, at sa seksyong ito ay kukunin natin bilang halimbawa ang makasaysayang unang uri, ang hash table BPF_MAP_TYPE_HASH.

Kung lumikha ka ng hash table sa, sabihin nating, C++, sasabihin mo unordered_map<int,long> woo, na sa Russian ay nangangahulugang β€œKailangan ko ng mesa woo walang limitasyong laki, na ang mga susi ay may uri int, at ang mga halaga ay ang uri long" Upang lumikha ng isang BPF hash table, kailangan nating gawin ang parehong bagay, maliban na kailangan nating tukuyin ang maximum na laki ng talahanayan, at sa halip na tukuyin ang mga uri ng mga key at value, kailangan nating tukuyin ang kanilang mga laki sa mga byte . Upang lumikha ng mga mapa gamitin ang command BPF_MAP_CREATE tawag sa sistema bpf. Tingnan natin ang isang mas kaunting programa na lumilikha ng isang mapa. Pagkatapos ng nakaraang programa na naglo-load ng mga BPF program, ito ay dapat na mukhang simple sa iyo:

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

Dito namin tinukoy ang isang hanay ng mga parameter attr, kung saan sinasabi namin na "Kailangan ko ng hash table na may mga key at value ng laki sizeof(int), kung saan maaari akong maglagay ng maximum na apat na elemento." Kapag lumilikha ng mga mapa ng BPF, maaari mong tukuyin ang iba pang mga parameter, halimbawa, sa parehong paraan tulad ng sa halimbawa sa programa, tinukoy namin ang pangalan ng bagay bilang "woo".

I-compile at patakbuhin natin ang programa:

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

Narito ang system call bpf(2) ibinalik sa amin ang descriptor map number 3 at pagkatapos ang program, gaya ng inaasahan, ay naghihintay para sa karagdagang mga tagubilin sa system call pause(2).

Ngayon ipadala natin ang aming programa sa background o magbukas ng isa pang terminal at tingnan ang aming bagay gamit ang utility bpftool (maaari nating makilala ang ating mapa mula sa iba sa pamamagitan ng pangalan nito):

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

Ang numerong 114 ay ang pandaigdigang ID ng aming object. Ang anumang programa sa system ay maaaring gumamit ng ID na ito upang magbukas ng isang umiiral na mapa gamit ang command BPF_MAP_GET_FD_BY_ID tawag sa sistema bpf.

Ngayon ay maaari na nating paglaruan ang ating hash table. Tingnan natin ang mga nilalaman nito:

$ sudo bpftool map dump id 114
Found 0 elements

Walang laman. Lagyan natin ito ng halaga hash[1] = 1:

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

Tingnan natin muli ang talahanayan:

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

Hooray! Nagawa naming magdagdag ng isang elemento. Tandaan na kailangan nating magtrabaho sa antas ng byte upang magawa ito, dahil bptftool hindi alam kung anong uri ang mga halaga sa hash table. (Ang kaalamang ito ay maaaring ilipat sa kanya gamit ang BTF, ngunit higit pa sa ngayon.)

Paano eksaktong nagbabasa at nagdaragdag ng mga elemento ang bpftool? Tingnan natin sa ilalim ng hood:

$ 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

Una naming binuksan ang mapa sa pamamagitan ng global ID nito gamit ang command BPF_MAP_GET_FD_BY_ID ΠΈ bpf(2) ibinalik sa amin ang descriptor 3. Sa karagdagang gamit ang command BPF_MAP_GET_NEXT_KEY natagpuan namin ang unang susi sa talahanayan sa pamamagitan ng pagpasa NULL bilang isang pointer sa "nakaraang" key. Kung mayroon tayong susi magagawa natin BPF_MAP_LOOKUP_ELEMna nagbabalik ng halaga sa isang pointer value. Ang susunod na hakbang ay sinusubukan naming hanapin ang susunod na elemento sa pamamagitan ng pagpasa ng isang pointer sa kasalukuyang key, ngunit ang aming talahanayan ay naglalaman lamang ng isang elemento at ang command BPF_MAP_GET_NEXT_KEY nagbabalik ENOENT.

Okay, let's change the value by key 1, let's say our business logic requires registering 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

Tulad ng inaasahan, ito ay napaka-simple: ang utos BPF_MAP_GET_FD_BY_ID binubuksan ang aming mapa sa pamamagitan ng ID, at ang command BPF_MAP_UPDATE_ELEM pinapatungan ang elemento.

Kaya, pagkatapos gumawa ng hash table mula sa isang programa, maaari nating basahin at isulat ang mga nilalaman nito mula sa isa pa. Tandaan na kung nagawa namin ito mula sa command line, magagawa ito ng anumang iba pang programa sa system. Bilang karagdagan sa mga utos na inilarawan sa itaas, para sa pagtatrabaho sa mga mapa mula sa espasyo ng gumagamit, Ang mga sumusunod na:

  • BPF_MAP_LOOKUP_ELEM: maghanap ng halaga sa pamamagitan ng susi
  • BPF_MAP_UPDATE_ELEM: i-update/lumikha ng halaga
  • BPF_MAP_DELETE_ELEM: tanggalin ang susi
  • BPF_MAP_GET_NEXT_KEY: hanapin ang susunod (o una) na susi
  • BPF_MAP_GET_NEXT_ID: nagbibigay-daan sa iyo na dumaan sa lahat ng umiiral na mga mapa, ganyan ito gumagana bpftool map
  • BPF_MAP_GET_FD_BY_ID: magbukas ng kasalukuyang mapa sa pamamagitan ng global ID nito
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: atomically i-update ang halaga ng isang bagay at ibalik ang luma
  • BPF_MAP_FREEZE: gawin ang mapa na hindi nababago mula sa userspace (ang operasyong ito ay hindi maaaring bawiin)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: mga operasyong masa. Halimbawa, BPF_MAP_LOOKUP_AND_DELETE_BATCH - ito ang tanging maaasahang paraan upang basahin at i-reset ang lahat ng mga halaga mula sa mapa

Hindi lahat ng mga utos na ito ay gumagana para sa lahat ng mga uri ng mapa, ngunit sa pangkalahatan, ang pagtatrabaho sa iba pang mga uri ng mga mapa mula sa espasyo ng user ay mukhang eksaktong kapareho ng pagtatrabaho sa mga hash table.

Para sa kaayusan, tapusin natin ang mga eksperimento sa hash table. Tandaan na gumawa kami ng table na maaaring maglaman ng hanggang apat na key? Magdagdag pa tayo ng ilang elemento:

$ 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 far so good:

$ 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

Subukan nating magdagdag ng isa pa:

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

Gaya ng inaasahan, hindi kami nagtagumpay. Tingnan natin ang error nang mas detalyado:

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

Maayos ang lahat: tulad ng inaasahan, ang koponan BPF_MAP_UPDATE_ELEM sumusubok na lumikha ng bago, ikalima, susi, ngunit nag-crash E2BIG.

Kaya, maaari tayong lumikha at mag-load ng mga programa ng BPF, gayundin ang lumikha at pamahalaan ang mga mapa mula sa espasyo ng gumagamit. Ngayon ay lohikal na tingnan kung paano namin magagamit ang mga mapa mula sa mga programa ng BPF mismo. Maaari nating pag-usapan ito sa wika ng mga programang mahirap basahin sa mga macro code ng makina, ngunit sa katunayan ay dumating na ang oras upang ipakita kung paano aktwal na isinusulat at pinapanatili ang mga programa ng BPF - gamit libbpf.

(Para sa mga mambabasa na hindi nasisiyahan sa kakulangan ng mababang antas na halimbawa: susuriin namin nang detalyado ang mga programang gumagamit ng mga mapa at mga function ng helper na nilikha gamit ang libbpf at sabihin sa iyo kung ano ang nangyayari sa antas ng pagtuturo. Para sa mga mambabasa na hindi nasisiyahan sobra, dagdag namin halimbawa sa angkop na lugar sa artikulo.)

Pagsusulat ng mga programang BPF gamit ang libbpf

Ang pagsusulat ng mga programa ng BPF gamit ang mga machine code ay maaaring maging kawili-wili sa unang pagkakataon lamang, at pagkatapos ay mabubusog. Sa sandaling ito kailangan mong ibaling ang iyong pansin llvm, na may backend para sa pagbuo ng code para sa arkitektura ng BPF, pati na rin ang isang library libbpf, na nagbibigay-daan sa iyong isulat ang user side ng BPF application at i-load ang code ng BPF program na nabuo gamit ang llvm/clang.

Sa katunayan, tulad ng makikita natin dito at sa mga susunod na artikulo, libbpf medyo maraming trabaho kung wala ito (o mga katulad na tool - iproute2, libbcc, libbpf-go, atbp.) imposibleng mabuhay. Isa sa mga tampok na pamatay ng proyekto libbpf ay BPF CO-RE (Compile Once, Run Everywhere) - isang proyekto na nagbibigay-daan sa iyo na magsulat ng mga BPF program na portable mula sa isang kernel patungo sa isa pa, na may kakayahang tumakbo sa iba't ibang mga API (halimbawa, kapag nagbago ang istraktura ng kernel mula sa bersyon sa bersyon). Upang makapagtrabaho sa CO-RE, ang iyong kernel ay dapat isama sa suporta ng BTF (inilalarawan namin kung paano ito gagawin sa seksyon Mga tool sa pag-unlad. Maaari mong suriin kung ang iyong kernel ay binuo gamit ang BTF o hindi masyadong simple - sa pamamagitan ng pagkakaroon ng sumusunod na file:

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

Ang file na ito ay nag-iimbak ng impormasyon tungkol sa lahat ng uri ng data na ginamit sa kernel at ginagamit sa lahat ng aming mga halimbawa gamit libbpf. Tatalakayin natin nang detalyado ang tungkol sa CO-RE sa susunod na artikulo, ngunit sa isang ito - buuin mo lang ang iyong sarili ng kernel CONFIG_DEBUG_INFO_BTF.

Aklatan libbpf nakatira mismo sa direktoryo tools/lib/bpf kernel at ang pagbuo nito ay isinasagawa sa pamamagitan ng mailing list [email protected]. Gayunpaman, ang isang hiwalay na imbakan ay pinananatili para sa mga pangangailangan ng mga application na naninirahan sa labas ng kernel https://github.com/libbpf/libbpf kung saan ang kernel library ay naka-mirror para sa read access nang higit pa o mas kaunti.

Sa seksyong ito ay titingnan natin kung paano ka makakagawa ng isang proyekto na gumagamit libbpf, sumulat tayo ng ilang (higit pa o mas kaunting walang kahulugan) na mga programa sa pagsubok at pag-aralan nang detalyado kung paano gumagana ang lahat. Ito ay magbibigay-daan sa amin na mas madaling ipaliwanag sa mga sumusunod na seksyon nang eksakto kung paano nakikipag-ugnayan ang mga programa ng BPF sa mga mapa, kernel helper, BTF, atbp.

Karaniwang ginagamit ang mga proyekto libbpf magdagdag ng isang GitHub repository bilang isang git submodule, gagawin namin ang pareho:

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

Papunta sa libbpf napaka-simple:

$ 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

Ang aming susunod na plano sa seksyong ito ay ang mga sumusunod: susulat kami ng isang BPF program tulad ng BPF_PROG_TYPE_XDP, kapareho ng sa nakaraang halimbawa, ngunit sa C, pinagsama-sama namin ito gamit clang, at magsulat ng helper program na maglo-load nito sa kernel. Sa mga sumusunod na seksyon, palalawakin natin ang mga kakayahan ng BPF program at assistant program.

Halimbawa: paglikha ng isang ganap na application gamit ang libbpf

Upang magsimula, ginagamit namin ang file /sys/kernel/btf/vmlinux, na nabanggit sa itaas, at lumikha ng katumbas nito sa anyo ng isang header file:

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

Ang file na ito ay mag-iimbak ng lahat ng mga istruktura ng data na magagamit sa aming kernel, halimbawa, ito ay kung paano tinukoy ang IPv4 header sa kernel:

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

Ngayon ay isusulat namin ang aming BPF program sa 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";

Bagama't naging napakasimple ng aming programa, kailangan pa rin naming bigyang pansin ang maraming detalye. Una, ang unang header file na isinama namin ay vmlinux.h, na kakalikha lang namin gamit bpftool btf dump - ngayon hindi na natin kailangang i-install ang kernel-headers package para malaman kung ano ang hitsura ng mga kernel structures. Ang sumusunod na header file ay dumating sa amin mula sa library libbpf. Ngayon kailangan lang namin ito upang tukuyin ang macro SEC, na nagpapadala ng character sa naaangkop na seksyon ng ELF object file. Ang aming programa ay nakapaloob sa seksyon xdp/simple, kung saan bago ang slash ay tinukoy namin ang uri ng programa na BPF - ito ang kumbensyon na ginamit sa libbpf, batay sa pangalan ng seksyon ay papalitan nito ang tamang uri sa pagsisimula bpf(2). Ang BPF program mismo ay C - napakasimple at binubuo ng isang linya return XDP_PASS. Sa wakas, isang hiwalay na seksyon "license" naglalaman ng pangalan ng lisensya.

Maaari naming i-compile ang aming programa gamit ang llvm/clang, bersyon >= 10.0.0, o mas mabuti pa, mas mataas (tingnan ang seksyon Mga tool sa pag-unlad):

$ 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

Kabilang sa mga kagiliw-giliw na tampok: ipinapahiwatig namin ang target na arkitektura -target bpf at ang landas patungo sa mga header libbpf, na na-install namin kamakailan. Gayundin, huwag kalimutan ang tungkol sa -O2, kung wala ang opsyong ito, maaari kang magkaroon ng mga sorpresa sa hinaharap. Tingnan natin ang aming code, nagawa ba naming isulat ang program na gusto namin?

$ 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

Oo, nagtrabaho ito! Ngayon, mayroon kaming binary file kasama ang program, at gusto naming lumikha ng isang application na maglo-load nito sa kernel. Para sa layuning ito ang aklatan libbpf nag-aalok sa amin ng dalawang pagpipilian - gumamit ng mas mababang antas ng API o mas mataas na antas ng API. Pupunta tayo sa pangalawang paraan, dahil gusto nating matutunan kung paano magsulat, mag-load at magkonekta ng mga programa ng BPF na may kaunting pagsisikap para sa kanilang kasunod na pag-aaral.

Una, kailangan nating bumuo ng "skeleton" ng ating programa mula sa binary nito gamit ang parehong utility bpftool β€” ang Swiss kutsilyo ng mundo ng BPF (na maaaring kunin nang literal, dahil si Daniel Borkman, isa sa mga tagalikha at tagapanatili ng BPF, ay Swiss):

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

Nasa file xdp-simple.skel.h naglalaman ng binary code ng aming programa at mga function para sa pamamahala - paglo-load, pag-attach, pagtanggal ng aming object. Sa aming simpleng kaso, ito ay mukhang overkill, ngunit ito ay gumagana din sa kaso kung saan ang object file ay naglalaman ng maraming BPF programs at mga mapa at upang mai-load ang higanteng ELF na ito kailangan lang naming bumuo ng skeleton at tumawag ng isa o dalawang function mula sa custom na application namin. ay sumusulat Mag-move on na tayo.

Sa mahigpit na pagsasalita, ang aming loader program ay walang halaga:

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

Dito struct xdp_simple_bpf tinukoy sa file xdp-simple.skel.h at inilalarawan ang aming object file:

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

Makakakita tayo ng mga bakas ng mababang antas ng API dito: ang istraktura struct bpf_program *simple ΠΈ struct bpf_link *simple. Ang unang istraktura ay partikular na naglalarawan sa aming programa, na nakasulat sa seksyon xdp/simple, at ang pangalawa ay naglalarawan kung paano kumokonekta ang program sa pinagmulan ng kaganapan.

Tungkulin xdp_simple_bpf__open_and_load, nagbubukas ng ELF object, na-parse ito, lumilikha ng lahat ng mga istruktura at substructure (bukod sa programa, naglalaman din ang ELF ng iba pang mga seksyon - data, readonly data, impormasyon sa pag-debug, lisensya, atbp.), at pagkatapos ay i-load ito sa kernel gamit ang isang system tawag bpf, na maaari nating suriin sa pamamagitan ng pag-compile at pagpapatakbo ng programa:

$ 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

Tingnan natin ngayon ang aming programa gamit bpftool. Hanapin natin ang kanyang 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)

at dump (gumagamit kami ng pinaikling anyo ng command bpftool prog dump xlated):

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

May bago! Ang programa ay nag-print ng mga piraso ng aming C source file. Ito ay ginawa ng library libbpf, na natagpuan ang seksyon ng debug sa binary, pinagsama ito sa isang BTF object, na-load ito sa kernel gamit ang BPF_BTF_LOAD, at pagkatapos ay tinukoy ang nagresultang deskriptor ng file kapag nilo-load ang programa gamit ang utos BPG_PROG_LOAD.

Mga Katulong sa Kernel

Ang mga programa ng BPF ay maaaring magpatakbo ng "panlabas" na mga function - mga katulong sa kernel. Ang mga function ng helper na ito ay nagbibigay-daan sa mga programa ng BPF na ma-access ang mga istruktura ng kernel, pamahalaan ang mga mapa, at makipag-ugnayan din sa "tunay na mundo" - lumikha ng mga kaganapan sa perf, kontrolin ang hardware (halimbawa, mga redirect packet), atbp.

Halimbawa: bpf_get_smp_processor_id

Sa loob ng balangkas ng paradigm na "pag-aaral sa pamamagitan ng halimbawa", isaalang-alang natin ang isa sa mga function ng katulong, bpf_get_smp_processor_id(), tiyak sa file kernel/bpf/helpers.c. Ibinabalik nito ang numero ng processor kung saan tumatakbo ang BPF program na tumawag dito. Ngunit hindi kami interesado sa mga semantika nito tulad ng sa katotohanan na ang pagpapatupad nito ay tumatagal ng isang linya:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Ang mga kahulugan ng function ng helper ng BPF ay katulad ng mga kahulugan ng tawag sa system ng Linux. Dito, halimbawa, ang isang function ay tinukoy na walang mga argumento. (Ang isang function na tumatagal, sabihin, tatlong argumento ay tinukoy gamit ang macro BPF_CALL_3. Ang maximum na bilang ng mga argumento ay lima.) Gayunpaman, ito ay ang unang bahagi lamang ng kahulugan. Ang ikalawang bahagi ay upang tukuyin ang uri ng istraktura struct bpf_func_proto, na naglalaman ng paglalarawan ng function ng helper na nauunawaan ng verifier:

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

Pagrerehistro ng Mga Function ng Helper

Upang magamit ng mga programa ng BPF ng isang partikular na uri ang function na ito, dapat nilang irehistro ito, halimbawa para sa uri BPF_PROG_TYPE_XDP ang isang function ay tinukoy sa kernel xdp_func_proto, na tumutukoy mula sa helper function ID kung sinusuportahan ng XDP ang function na ito o hindi. Ang aming function ay sumusuporta:

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

Ang mga bagong uri ng programa ng BPF ay "tinukoy" sa file include/linux/bpf_types.h gamit ang isang macro BPF_PROG_TYPE. Tinukoy sa mga panipi dahil ito ay isang lohikal na kahulugan, at sa mga termino ng wikang C ang kahulugan ng isang buong hanay ng mga kongkretong istruktura ay nangyayari sa ibang mga lugar. Sa partikular, sa file kernel/bpf/verifier.c lahat ng mga kahulugan mula sa file bpf_types.h ay ginagamit upang lumikha ng isang hanay ng mga istruktura 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
};

Iyon ay, para sa bawat uri ng BPF program, ang isang pointer sa isang istraktura ng data ng uri ay tinukoy struct bpf_verifier_ops, na pinasimulan sa halaga _name ## _verifier_ops, ibig sabihin, xdp_verifier_ops para sa xdp. Istruktura xdp_verifier_ops natutukoy ni sa file net/core/filter.c tulad ng sumusunod:

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

Dito nakikita namin ang aming pamilyar na function xdp_func_proto, na tatakbo sa verifier sa tuwing makakaharap ito ng isang hamon ilang uri function sa loob ng isang BPF program, tingnan verifier.c.

Tingnan natin kung paano ginagamit ng hypothetical BPF program ang function bpf_get_smp_processor_id. Upang gawin ito, muling isinulat namin ang programa mula sa aming nakaraang seksyon tulad ng sumusunod:

#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 natutukoy ni Π² <bpf/bpf_helper_defs.h> mga aklatan libbpf bilang

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

yan ay, bpf_get_smp_processor_id ay isang function pointer na ang value ay 8, kung saan 8 ang value BPF_FUNC_get_smp_processor_id uri enum bpf_fun_id, na tinukoy para sa amin sa file vmlinux.h (file bpf_helper_defs.h sa kernel ay nabuo ng isang script, kaya ang mga "magic" na numero ay ok). Ang function na ito ay hindi tumatagal ng mga argumento at nagbabalik ng isang halaga ng uri __u32. Kapag pinatakbo natin ito sa ating programa, clang bumubuo ng isang pagtuturo BPF_CALL "ang tamang uri" Isama natin ang programa at tingnan ang seksyon 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

Sa unang linya makikita natin ang mga tagubilin call, parameter IMM na katumbas ng 8, at SRC_REG - zero. Ayon sa ABI agreement na ginamit ng verifier, ito ay isang tawag sa helper function number eight. Sa sandaling ito ay inilunsad, ang lohika ay simple. Ibalik ang halaga mula sa rehistro r0 kinopya sa r1 at sa mga linya 2,3 ito ay na-convert sa uri u32 β€” ang itaas na 32 bits ay na-clear. Sa linya 4,5,6,7 ibinabalik namin ang 2 (XDP_PASS) o 1 (XDP_DROP) depende sa kung ang function ng helper mula sa linya 0 ay nagbalik ng zero o non-zero na halaga.

Subukan natin ang ating sarili: i-load ang programa at tingnan ang output 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, nakita ng verifier ang tamang kernel-helper.

Halimbawa: pagpasa ng mga argumento at sa wakas ay patakbuhin ang programa!

Ang lahat ng run-level helper function ay may prototype

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

Ang mga parameter sa mga function ng helper ay ipinapasa sa mga rehistro r1-r5, at ang halaga ay ibinalik sa rehistro r0. Walang mga function na tumatagal ng higit sa limang argumento, at ang suporta para sa mga ito ay hindi inaasahang madaragdag sa hinaharap.

Tingnan natin ang bagong kernel helper at kung paano ipinapasa ng BPF ang mga parameter. Muli nating isulat xdp-simple.bpf.c tulad ng sumusunod (ang natitirang mga linya ay hindi nagbago):

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

Ang aming programa ay nagpi-print ng numero ng CPU kung saan ito tumatakbo. I-compile natin ito at tingnan ang code:

$ 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

Sa mga linya 0-7 isinusulat namin ang string running on CPU%un, at pagkatapos ay sa linya 8 pinapatakbo namin ang pamilyar na isa bpf_get_smp_processor_id. Sa linya 9-12 inihahanda namin ang mga argumento ng katulong bpf_printk - nagrerehistro r1, r2, r3. Bakit tatlo sila at hindi dalawa? kasi bpf_printkito ay isang macro wrapper sa paligid ng tunay na katulong bpf_trace_printk, na kailangang pumasa sa laki ng string ng format.

Magdagdag tayo ngayon ng ilang linya sa xdp-simple.cupang ang aming programa ay kumokonekta sa interface lo at nagsimula na talaga!

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

Dito ginagamit namin ang function bpf_set_link_xdp_fd, na nag-uugnay sa mga XDP-type na BPF program sa mga interface ng network. Na-hardcode namin ang numero ng interface lo, na palaging 1. Pinapatakbo namin ang function nang dalawang beses upang unang tanggalin ang lumang programa kung ito ay naka-attach. Pansinin na ngayon ay hindi na natin kailangan ng hamon pause o isang walang katapusang loop: lalabas ang aming loader program, ngunit hindi papatayin ang BPF program dahil konektado ito sa pinagmulan ng kaganapan. Pagkatapos ng matagumpay na pag-download at koneksyon, ang program ay ilulunsad para sa bawat network packet na darating sa lo.

I-download natin ang programa at tingnan ang interface 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

Ang program na na-download namin ay may ID 669 at nakikita namin ang parehong ID sa interface lo. Magpapadala kami ng ilang pakete sa 127.0.0.1 (hiling + tugon):

$ ping -c1 localhost

at ngayon tingnan natin ang mga nilalaman ng debug virtual file /sys/kernel/debug/tracing/trace_pipe, kung saan bpf_printk nagsusulat ng kanyang mga mensahe:

# 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

Dalawang pakete ang nakita lo at naproseso sa CPU0 - gumana ang aming unang ganap na walang kahulugang BPF program!

Ito ay nagkakahalaga ng pagpuna na bpf_printk Hindi walang kabuluhan ang pagsusulat nito sa debug file: hindi ito ang pinakamatagumpay na katulong para magamit sa produksyon, ngunit ang aming layunin ay magpakita ng isang bagay na simple.

Pag-access sa mga mapa mula sa mga programa ng BPF

Halimbawa: gamit ang isang mapa mula sa BPF program

Sa mga nakaraang seksyon natutunan namin kung paano lumikha at gumamit ng mga mapa mula sa espasyo ng gumagamit, at ngayon tingnan natin ang bahagi ng kernel. Magsimula tayo, gaya ng dati, sa isang halimbawa. Muli nating isulat ang ating programa xdp-simple.bpf.c tulad ng sumusunod:

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

Sa simula ng programa nagdagdag kami ng kahulugan ng mapa woo: Ito ay isang 8-element array na nag-iimbak ng mga value tulad ng u64 (sa C ay tutukuyin natin ang isang array bilang u64 woo[8]). Sa isang programa "xdp/simple" nakukuha namin ang kasalukuyang numero ng processor sa isang variable key at pagkatapos ay gamit ang helper function bpf_map_lookup_element nakakakuha kami ng pointer sa kaukulang entry sa array, na dinadagdagan namin ng isa. Isinalin sa Russian: kinakalkula namin ang mga istatistika kung saan pinoproseso ng CPU ang mga papasok na packet. Subukan nating patakbuhin ang programa:

$ 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

Suriin natin kung na-hook up siya lo at magpadala ng ilang packet:

$ 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

Ngayon tingnan natin ang mga nilalaman ng array:

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

Halos lahat ng proseso ay naproseso sa CPU7. Hindi ito mahalaga sa amin, ang pangunahing bagay ay gumagana ang programa at naiintindihan namin kung paano i-access ang mga mapa mula sa mga programa ng BPF - gamit Ρ…Π΅Π»ΠΏΠ΅Ρ€ΠΎΠ² bpf_mp_*.

Mystical index

Kaya, maaari naming ma-access ang mapa mula sa BPF program gamit ang mga tawag tulad ng

val = bpf_map_lookup_elem(&woo, &key);

kung saan ang hitsura ng helper function

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

ngunit kami ay nagpapasa ng isang pointer &woo sa isang hindi pinangalanang istraktura struct { ... }...

Kung titingnan natin ang assembler ng programa, nakikita natin na ang halaga &woo ay hindi aktwal na tinukoy (linya 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
...

at nakapaloob sa mga relokasyon:

$ 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

Ngunit kung titingnan natin ang na-load na programa, makikita natin ang isang pointer sa tamang mapa (linya 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]
...

Kaya, maaari naming tapusin na sa oras ng paglulunsad ng aming loader program, ang link sa &woo ay pinalitan ng isang bagay na may aklatan libbpf. Una, titingnan natin ang output 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

Nakikita natin yan libbpf nakagawa ng mapa woo at pagkatapos ay nai-download ang aming programa simple. Tingnan natin nang mas malapitan kung paano namin nilo-load ang program:

  • tawag xdp_simple_bpf__open_and_load mula sa file xdp-simple.skel.h
  • na nagiging sanhi ng xdp_simple_bpf__load mula sa file xdp-simple.skel.h
  • na nagiging sanhi ng bpf_object__load_skeleton mula sa file libbpf/src/libbpf.c
  • na nagiging sanhi ng bpf_object__load_xattr ng libbpf/src/libbpf.c

Ang huling function, bukod sa iba pang mga bagay, ay tatawag bpf_object__create_maps, na lumilikha o nagbubukas ng mga umiiral nang mapa, na ginagawang mga deskriptor ng file. (Dito natin nakikita BPF_MAP_CREATE sa output strace.) Susunod na ang function ay tinatawag bpf_object__relocate at siya ang interesado sa amin, dahil naaalala namin ang aming nakita woo sa relocation table. Sa paggalugad nito, sa huli ay makikita natin ang ating sarili sa function bpf_program__relocate, na nakikitungo sa mga paglilipat ng mapa:

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

Kaya kinuha namin ang aming mga tagubilin

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

at palitan ang source register sa loob nito ng BPF_PSEUDO_MAP_FD, at ang unang IMM sa file descriptor ng aming mapa at, kung ito ay katumbas ng, halimbawa, 0xdeadbeef, pagkatapos bilang resulta ay matatanggap namin ang pagtuturo

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

Ito ay kung paano inililipat ang impormasyon ng mapa sa isang partikular na naka-load na BPF program. Sa kasong ito, maaaring malikha ang mapa gamit ang BPF_MAP_CREATE, at binuksan ng ID gamit ang BPF_MAP_GET_FD_BY_ID.

Kabuuan, kapag ginagamit libbpf ang algorithm ay ang mga sumusunod:

  • sa panahon ng compilation, ang mga talaan ay ginawa sa relocation table para sa mga link sa mga mapa
  • libbpf binubuksan ang ELF object book, hinahanap ang lahat ng ginamit na mapa at gumagawa ng mga file descriptor para sa kanila
  • Ang mga deskriptor ng file ay na-load sa kernel bilang bahagi ng pagtuturo LD64

Gaya ng maiisip mo, marami pang darating at kailangan nating tingnan ang core. Sa kabutihang palad, mayroon kaming isang palatandaan - naisulat namin ang kahulugan BPF_PSEUDO_MAP_FD sa rehistro ng pinagmulan at maaari nating ilibing ito, na magdadala sa atin sa banal ng lahat ng mga banal - kernel/bpf/verifier.c, kung saan pinapalitan ng isang function na may natatanging pangalan ang isang file descriptor ng address ng isang istraktura ng uri 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;

(maaaring mahanap ang buong code ΠΏΠΎ ссылкС). Upang mapalawak namin ang aming algorithm:

  • habang nilo-load ang program, sinusuri ng verifier ang tamang paggamit ng mapa at isinusulat ang address ng kaukulang istruktura struct bpf_map

Kapag nagda-download ng ELF binary gamit ang libbpf Marami pang nangyayari, ngunit tatalakayin natin iyon sa ibang mga artikulo.

Naglo-load ng mga programa at mapa nang walang libbpf

Gaya ng ipinangako, narito ang isang halimbawa para sa mga mambabasa na gustong malaman kung paano lumikha at mag-load ng program na gumagamit ng mga mapa, nang walang tulong. libbpf. Maaari itong maging kapaki-pakinabang kapag nagtatrabaho ka sa isang kapaligiran kung saan hindi ka makakagawa ng mga dependency, o nagse-save ng bawat bit, o nagsusulat ng isang programa tulad ng ply, na bumubuo ng BPF binary code sa mabilisang.

Upang gawing mas madaling sundin ang lohika, muling isusulat namin ang aming halimbawa para sa mga layuning ito xdp-simple. Ang kumpleto at bahagyang pinalawak na code ng programang tinalakay sa halimbawang ito ay matatagpuan dito diwa.

Ang lohika ng aming aplikasyon ay ang mga sumusunod:

  • lumikha ng isang uri ng mapa BPF_MAP_TYPE_ARRAY gamit ang utos BPF_MAP_CREATE,
  • lumikha ng isang programa na gumagamit ng mapa na ito,
  • ikonekta ang programa sa interface lo,

na isinasalin sa tao bilang

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

Dito map_create lumilikha ng mapa sa parehong paraan tulad ng ginawa namin sa unang halimbawa tungkol sa system call bpf - "kernel, mangyaring gumawa ako ng isang bagong mapa sa anyo ng isang array ng 8 mga elemento tulad ng __u64 at ibalik sa akin ang file descriptor":

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

Madali ring i-load ang program:

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

Ang nakakalito na bahagi prog_load ay ang kahulugan ng aming BPF program bilang isang hanay ng mga istruktura struct bpf_insn insns[]. Ngunit dahil gumagamit kami ng program na mayroon kami sa C, maaari kaming mandaya nang kaunti:

$ 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

Sa kabuuan, kailangan nating magsulat ng 14 na mga tagubilin sa anyo ng mga istruktura tulad ng struct bpf_insn (payo: kunin ang dump mula sa itaas, basahin muli ang seksyon ng mga tagubilin, buksan linux/bpf.h ΠΈ linux/bpf_common.h at subukan upang matukoy struct bpf_insn insns[] sa sarili):

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

Isang ehersisyo para sa mga hindi sumulat nito sa kanilang sarili - hanapin map_fd.

May isa pang hindi nasabi na bahagi ang natitira sa aming programa - xdp_attach. Sa kasamaang palad, ang mga program tulad ng XDP ay hindi maaaring konektado gamit ang isang system call bpf. Ang mga taong lumikha ng BPF at XDP ay mula sa online na komunidad ng Linux, na nangangahulugang ginamit nila ang pinakapamilyar sa kanila (ngunit hindi para normal people) interface para sa pakikipag-ugnayan sa kernel: mga socket ng netlink, Tingnan din RFC3549. Ang pinakasimpleng paraan upang ipatupad xdp_attach ay kinokopya ang code mula sa libbpf, ibig sabihin, mula sa file netlink.c, na ginawa namin, pinaikli ito ng kaunti:

Maligayang pagdating sa mundo ng mga netlink socket

Magbukas ng uri ng netlink socket 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;
}

Nabasa namin mula sa socket na ito:

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

Sa wakas, narito ang aming function na nagbubukas ng socket at nagpapadala ng isang espesyal na mensahe dito na naglalaman ng isang file descriptor:

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

Kaya, handa na ang lahat para sa pagsubok:

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

Tingnan natin kung nakakonekta ang ating programa 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

Magpadala tayo ng mga ping at tingnan ang mapa:

$ 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

Hurray, gumagana ang lahat. Tandaan, sa pamamagitan ng paraan, na ang aming mapa ay muling ipinapakita sa anyo ng mga byte. Ito ay dahil sa ang katunayan na, hindi katulad libbpf hindi kami nag-load ng type information (BTF). Ngunit pag-uusapan pa natin ito sa susunod.

Mga tool sa pag-unlad

Sa seksyong ito, titingnan natin ang pinakamababang toolkit ng developer ng BPF.

Sa pangkalahatan, hindi mo kailangan ng anumang espesyal upang bumuo ng mga programa ng BPF - tumatakbo ang BPF sa anumang disenteng kernel ng pamamahagi, at ang mga programa ay binuo gamit ang clang, na maaaring ibigay mula sa pakete. Gayunpaman, dahil sa ang katunayan na ang BPF ay nasa ilalim ng pag-unlad, ang kernel at mga tool ay patuloy na nagbabago, kung hindi mo nais na magsulat ng mga programa ng BPF gamit ang mga makalumang pamamaraan mula 2019, pagkatapos ay kailangan mong mag-compile

  • llvm/clang
  • pahole
  • core nito
  • bpftool

(Para sa sanggunian, ang seksyong ito at lahat ng mga halimbawa sa artikulo ay pinatakbo sa Debian 10.)

llvm/clang

Ang BPF ay palakaibigan sa LLVM at, bagama't kamakailan lamang ay maaaring i-compile ang mga programa para sa BPF gamit ang gcc, ang lahat ng kasalukuyang pag-unlad ay isinasagawa para sa LLVM. Samakatuwid, una sa lahat, bubuo kami ng kasalukuyang bersyon clang mula sa 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
... ΠΌΠ½ΠΎΠ³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ спустя
$

Ngayon ay maaari nating suriin kung ang lahat ay pinagsama nang tama:

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

(Mga tagubilin sa pagtitipon clang kinuha ko mula sa bpf_devel_QA.)

Hindi namin ii-install ang mga program na kakagawa lang namin, sa halip ay idagdag lang ang mga ito PATH, halimbawa:

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

(Maaari itong idagdag sa .bashrc o sa isang hiwalay na file. Sa personal, nagdaragdag ako ng mga bagay na tulad nito ~/bin/activate-llvm.sh at kung kinakailangan ay ginagawa ko ito . activate-llvm.sh.)

Pahole at BTF

Kagamitan pahole ginagamit sa pagbuo ng kernel upang lumikha ng impormasyon sa pag-debug sa BTF na format. Hindi namin isasaalang-alang ang artikulong ito tungkol sa mga detalye ng teknolohiya ng BTF, maliban sa katotohanan na ito ay maginhawa at gusto naming gamitin ito. Kaya kung bubuo ka ng iyong kernel, magtayo muna pahole (wala pahole hindi mo magagawang bumuo ng kernel na may opsyon 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

Mga kernel para sa pag-eksperimento sa BPF

Kapag ginalugad ang mga posibilidad ng BPF, gusto kong tipunin ang sarili kong core. Ito, sa pangkalahatan, ay hindi kinakailangan, dahil magagawa mong mag-compile at mag-load ng mga programa ng BPF sa kernel ng pamamahagi, gayunpaman, ang pagkakaroon ng iyong sariling kernel ay nagpapahintulot sa iyo na gamitin ang pinakabagong mga tampok ng BPF, na lilitaw sa iyong pamamahagi sa pinakamainam na buwan. , o, tulad ng sa kaso ng ilang mga tool sa pag-debug ay hindi ipapakete sa lahat sa nakikinita na hinaharap. Gayundin, ang sarili nitong core ay nagpaparamdam na mahalagang mag-eksperimento sa code.

Upang makabuo ng kernel kailangan mo, una, ang kernel mismo, at pangalawa, isang kernel configuration file. Upang mag-eksperimento sa BPF maaari nating gamitin ang karaniwan banilya kernel o isa sa mga development kernels. Sa kasaysayan, ang pag-unlad ng BPF ay nagaganap sa loob ng komunidad ng networking ng Linux at samakatuwid ang lahat ng mga pagbabago maaga o huli ay dumaan kay David Miller, ang tagapagpanatili ng network ng Linux. Depende sa kanilang kalikasan - mga pag-edit o mga bagong tampok - ang mga pagbabago sa network ay nahuhulog sa isa sa dalawang core - net o net-next. Ang mga pagbabago para sa BPF ay ipinamamahagi sa parehong paraan sa pagitan bpf ΠΈ bpf-next, na pagkatapos ay pinagsama-sama sa net at net-next, ayon sa pagkakabanggit. Para sa higit pang mga detalye, tingnan bpf_devel_QA ΠΈ netdev-FAQ. Kaya pumili ng kernel batay sa iyong panlasa at sa mga pangangailangan ng katatagan ng system na iyong sinusubok (*-next ang mga kernel ay ang pinaka-hindi matatag sa mga nakalista).

Lampas sa saklaw ng artikulong ito ang pag-usapan kung paano pamahalaan ang mga file ng pagsasaayos ng kernel - ipinapalagay na alam mo na kung paano ito gawin, o handang matuto sa sarili. Gayunpaman, ang mga sumusunod na tagubilin ay dapat na higit pa o mas kaunti sapat upang mabigyan ka ng gumaganang BPF-enabled system.

I-download ang isa sa mga kernel sa itaas:

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

Bumuo ng isang minimal na gumaganang kernel config:

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

Paganahin ang mga opsyon sa BPF sa file .config sa iyong sariling pagpili (malamang CONFIG_BPF paganahin na dahil ginagamit ito ng systemd). Narito ang isang listahan ng mga opsyon mula sa kernel na ginamit para sa artikulong ito:

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

Pagkatapos ay madali nating mai-assemble at mai-install ang mga module at ang kernel (nga pala, maaari mong tipunin ang kernel gamit ang bagong binuo clangsa pamamagitan ng pagdaragdag CC=clang):

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

at i-reboot gamit ang bagong kernel (ginagamit ko para dito kexec mula sa pakete 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

Ang pinakakaraniwang ginagamit na utility sa artikulo ay ang utility bpftool, na ibinigay bilang bahagi ng Linux kernel. Ito ay isinulat at pinapanatili ng mga developer ng BPF para sa mga developer ng BPF at maaaring gamitin upang pamahalaan ang lahat ng uri ng mga bagay sa BPF - mag-load ng mga programa, lumikha at mag-edit ng mga mapa, galugarin ang buhay ng BPF ecosystem, atbp. Matatagpuan ang dokumentasyon sa anyo ng mga source code para sa mga man page sa kaibuturan o, naipon na, online.

Sa oras ng pagsulat na ito bpftool ay handa lamang para sa RHEL, Fedora at Ubuntu (tingnan, halimbawa, ang thread na ito, na nagsasabi sa hindi natapos na kuwento ng packaging bpftool sa Debian). Ngunit kung naitayo mo na ang iyong kernel, pagkatapos ay bumuo bpftool kasing dali ng pie:

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

$

(dito ${linux} - ito ang iyong kernel directory.) Pagkatapos isagawa ang mga command na ito bpftool ay kokolektahin sa isang direktoryo ${linux}/tools/bpf/bpftool at maaari itong idagdag sa landas (una sa lahat sa user root) o kopyahin lang sa /usr/local/sbin.

Mangolekta bpftool pinakamahusay na gamitin ang huli clang, binuo tulad ng inilarawan sa itaas, at suriin kung ito ay binuo nang tama - gamit, halimbawa, ang utos

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

na magpapakita kung aling mga tampok ng BPF ang pinagana sa iyong kernel.

Sa pamamagitan ng paraan, ang nakaraang utos ay maaaring patakbuhin bilang

# bpftool f p k

Ginagawa ito sa pamamagitan ng pagkakatulad sa mga utility mula sa package iproute2, kung saan maaari nating, halimbawa, sabihin ip a s eth0 sa halip ng ip addr show dev eth0.

Konklusyon

Binibigyang-daan ka ng BPF na magsapatos ng pulgas upang mabisang sukatin at on-the-fly na baguhin ang functionality ng core. Ang sistema ay naging napaka-matagumpay, sa pinakamahusay na mga tradisyon ng UNIX: isang simpleng mekanismo na nagbibigay-daan sa iyo upang (muling i-program ang kernel na nagpapahintulot sa isang malaking bilang ng mga tao at organisasyon na mag-eksperimento. At, kahit na ang mga eksperimento, pati na rin ang pagbuo ng mismong imprastraktura ng BPF, ay malayo sa tapos, ang system ay mayroon nang matatag na ABI na nagbibigay-daan sa iyo upang bumuo ng maaasahan, at higit sa lahat, epektibong lohika ng negosyo.

Gusto kong tandaan na, sa aking opinyon, ang teknolohiya ay naging napakapopular dahil, sa isang banda, maaari upang i-play (ang arkitektura ng isang makina ay maaaring maunawaan nang higit pa o mas kaunti sa isang gabi), at sa kabilang banda, upang malutas ang mga problema na hindi malulutas (maganda) bago ang hitsura nito. Ang dalawang sangkap na ito ay sama-samang pinipilit ang mga tao na mag-eksperimento at mangarap, na humahantong sa paglitaw ng parami nang parami ng mga makabagong solusyon.

Ang artikulong ito, bagama't hindi partikular na maikli, ay isang panimula lamang sa mundo ng BPF at hindi naglalarawan ng "advanced" na mga tampok at mahahalagang bahagi ng arkitektura. Ang plano sa hinaharap ay ganito: ang susunod na artikulo ay isang pangkalahatang-ideya ng mga uri ng programa ng BPF (mayroong 5.8 mga uri ng programa na sinusuportahan sa 30 kernel), pagkatapos ay titingnan natin sa wakas kung paano magsulat ng mga tunay na aplikasyon ng BPF gamit ang mga programa sa pagsubaybay sa kernel bilang isang halimbawa, pagkatapos ay oras na para sa isang mas malalim na kurso sa arkitektura ng BPF, na sinusundan ng mga halimbawa ng BPF networking at mga aplikasyon sa seguridad.

Mga nakaraang artikulo sa seryeng ito

  1. BPF para sa maliliit, part zero: classic BPF

Mga link

  1. Gabay sa Sanggunian ng BPF at XDP β€” dokumentasyon sa BPF mula sa cilium, o mas tiyak mula kay Daniel Borkman, isa sa mga tagalikha at tagapanatili ng BPF. Isa ito sa mga unang seryosong paglalarawan, na naiiba sa iba dahil alam na alam ni Daniel kung ano ang kanyang isinusulat at walang mga pagkakamali doon. Sa partikular, inilalarawan ng dokumentong ito kung paano gumana sa mga programa ng BPF ng mga uri ng XDP at TC gamit ang kilalang utility ip mula sa pakete iproute2.

  2. Dokumentasyon/networking/filter.txt β€” orihinal na file na may dokumentasyon para sa classic at pagkatapos ay pinalawig na BPF. Isang magandang pagbabasa kung gusto mong suriin ang wika ng pagpupulong at mga teknikal na detalye ng arkitektura.

  3. Blog tungkol sa BPF mula sa facebook. Ito ay madalang na na-update, ngunit angkop, tulad ng isinulat doon nina Alexei Starovoitov (may-akda ng eBPF) at Andrii Nakryiko - (tagapangasiwa) libbpf).

  4. Mga lihim ng bpftool. Isang nakakaaliw na twitter thread mula sa Quentin Monnet na may mga halimbawa at lihim ng paggamit ng bpftool.

  5. Sumisid sa BPF: isang listahan ng mga babasahin. Isang higanteng (at pinananatili pa rin) na listahan ng mga link sa dokumentasyon ng BPF mula sa Quentin Monnet.

Pinagmulan: www.habr.com

Magdagdag ng komento