BPF voor de kleintjes, deel één: uitgebreide BPF

In het begin was er een technologie en die heette BPF. Wij keken naar haar vorig, Oudtestamentisch artikel uit deze serie. In 2013 werd, dankzij de inspanningen van Alexei Starovoitov en Daniel Borkman, een verbeterde versie ervan, geoptimaliseerd voor moderne 64-bit machines, ontwikkeld en opgenomen in de Linux-kernel. Deze nieuwe technologie heette kortstondig Internal BPF en werd vervolgens omgedoopt tot Extended BPF, en nu, na een aantal jaren, noemt iedereen het gewoon BPF.

Grof gezegd staat BPF je toe willekeurige door de gebruiker aangeleverde code uit te voeren in de Linux-kernelruimte, en de nieuwe architectuur bleek zo succesvol dat we nog een tiental artikelen nodig zullen hebben om al zijn toepassingen te beschrijven. (Het enige dat de ontwikkelaars niet goed hebben gedaan, zoals je kunt zien in de onderstaande prestatiecode, was het maken van een fatsoenlijk logo.)

Dit artikel beschrijft de structuur van de virtuele BPF-machine, kernelinterfaces voor het werken met BPF, ontwikkelingstools, evenals een kort, zeer kort overzicht van bestaande mogelijkheden, d.w.z. alles wat we in de toekomst nodig zullen hebben voor een diepere studie van de praktische toepassingen van BPF.
BPF voor de kleintjes, deel één: uitgebreide BPF

Samenvatting van het artikel

Inleiding tot BPF-architectuur. Eerst bekijken we de BPF-architectuur in vogelvlucht en schetsen we de belangrijkste componenten.

Registers en commandosysteem van de virtuele BPF-machine. Omdat we al een idee hebben van de architectuur als geheel, zullen we de structuur van de virtuele BPF-machine beschrijven.

Levenscyclus van BPF-objecten, bpffs-bestandssysteem. In deze sectie zullen we de levenscyclus van BPF-objecten - programma's en kaarten - nader bekijken.

Objecten beheren met behulp van de bpf-systeemaanroep. Nu er al enig begrip is van het systeem, zullen we eindelijk kijken hoe we objecten vanuit de gebruikersruimte kunnen maken en manipuleren met behulp van een speciale systeemaanroep − bpf(2).

Пишем программы BPF с помощью libbpf. Natuurlijk kunt u programma's schrijven met behulp van een systeemaanroep. Maar het is moeilijk. Voor een realistischer scenario ontwikkelden nucleaire programmeurs een bibliotheek libbpf. We zullen een basis BPF-applicatieskelet maken dat we in de volgende voorbeelden zullen gebruiken.

Kernelhelpers. Hier zullen we leren hoe BPF-programma's toegang kunnen krijgen tot kernelhelperfuncties - een tool die, samen met kaarten, de mogelijkheden van de nieuwe BPF fundamenteel uitbreidt in vergelijking met de klassieke.

Toegang tot kaarten van BPF-programma's. Op dit punt zullen we genoeg weten om precies te begrijpen hoe we programma's kunnen maken die kaarten gebruiken. En laten we zelfs even een kijkje nemen in de grote en machtige verificateur.

Ontwikkelingshulpmiddelen. Help-sectie over het samenstellen van de vereiste hulpprogramma's en kernel voor experimenten.

Conclusie. Aan het einde van het artikel zullen degenen die tot hier hebben gelezen motiverende woorden vinden en een korte beschrijving van wat er in de volgende artikelen zal gebeuren. Ook zetten we een aantal links voor zelfstudie op een rij, voor wie niet wil of kan wachten op het vervolg.

Inleiding tot BPF-architectuur

Voordat we de BPF-architectuur gaan beschouwen, zullen we er nog een laatste keer (oh) naar verwijzen klassieke BPF, dat werd ontwikkeld als reactie op de komst van RISC-machines en het probleem van efficiënte pakketfiltering oploste. De architectuur bleek zo succesvol dat ze, geboren in de onstuimige jaren negentig in Berkeley UNIX, werd geporteerd naar de meeste bestaande besturingssystemen, overleefde tot in de gekke jaren twintig en nog steeds nieuwe toepassingen vindt.

De nieuwe BPF is ontwikkeld als reactie op de alomtegenwoordigheid van 64-bits machines, clouddiensten en de toegenomen behoefte aan tools voor het creëren van SDN (Svaak-dverfijnd netwerken). Ontwikkeld door kernelnetwerkingenieurs als een verbeterde vervanging voor de klassieke BPF, vond de nieuwe BPF letterlijk zes maanden later toepassingen in de moeilijke taak om Linux-systemen te traceren, en nu, zes jaar na zijn verschijning, hebben we een heel volgend artikel nodig om noem de verschillende soorten programma's.

Grappige foto's

In de kern is BPF een virtuele sandbox-machine waarmee u “willekeurige” code in de kernelruimte kunt uitvoeren zonder de veiligheid in gevaar te brengen. BPF-programma's worden in de gebruikersruimte gemaakt, in de kernel geladen en verbonden met een gebeurtenisbron. Een gebeurtenis kan bijvoorbeeld de levering van een pakket aan een netwerkinterface zijn, de lancering van een bepaalde kernelfunctie, enz. In het geval van een pakket heeft het BPF-programma toegang tot de gegevens en metagegevens van het pakket (voor lezen en mogelijk schrijven, afhankelijk van het type programma); in het geval van het uitvoeren van een kernelfunctie, de argumenten van de functie, inclusief verwijzingen naar kernelgeheugen, enz.

Laten we dit proces eens nader bekijken. Laten we om te beginnen praten over het eerste verschil met de klassieke BPF, programma's waarvoor in assembler zijn geschreven. In de nieuwe versie werd de architectuur uitgebreid zodat programma's in talen op hoog niveau konden worden geschreven, uiteraard voornamelijk in C. Voor dit doel werd een backend voor llvm ontwikkeld, waarmee bytecode voor de BPF-architectuur kan worden gegenereerd.

BPF voor de kleintjes, deel één: uitgebreide BPF

De BPF-architectuur is gedeeltelijk ontworpen om efficiënt te werken op moderne machines. Om dit in de praktijk te laten werken, wordt de BPF-bytecode, zodra deze in de kernel is geladen, vertaald naar native code met behulp van een component die een JIT-compiler wordt genoemd (Just In Tik mij). Vervolgens, als je het je herinnert, werd het programma in de klassieke BPF in de kernel geladen en atomair aan de gebeurtenisbron gekoppeld - in de context van een enkele systeemaanroep. In de nieuwe architectuur gebeurt dit in twee fasen: eerst wordt de code in de kernel geladen met behulp van een systeemaanroep bpf(2)en later, via andere mechanismen die variëren afhankelijk van het type programma, hecht het programma zich aan de gebeurtenisbron.

Hier heeft de lezer misschien een vraag: was het mogelijk? Hoe wordt de uitvoeringsveiligheid van dergelijke code gegarandeerd? De veiligheid van de uitvoering wordt ons gegarandeerd door de fase van het laden van BPF-programma's, genaamd verifier (in het Engels heet deze fase verifier en ik zal het Engelse woord blijven gebruiken):

BPF voor de kleintjes, deel één: uitgebreide BPF

Verifier is een statische analysator die ervoor zorgt dat een programma de normale werking van de kernel niet verstoort. Dit betekent overigens niet dat het programma de werking van het systeem niet kan verstoren - BPF-programma's kunnen, afhankelijk van het type, delen van het kernelgeheugen lezen en herschrijven, waarden van functies retourneren, trimmen, toevoegen, herschrijven en zelfs netwerkpakketten doorsturen. Verifier garandeert dat het uitvoeren van een BPF-programma de kernel niet zal laten crashen en dat een programma dat volgens de regels schrijftoegang heeft, bijvoorbeeld de gegevens van een uitgaand pakket, het kernelgeheugen buiten het pakket niet zal kunnen overschrijven. We zullen de verificateur wat gedetailleerder bekijken in de overeenkomstige sectie, nadat we kennis hebben gemaakt met alle andere componenten van BPF.

Dus wat hebben we tot nu toe geleerd? De gebruiker schrijft een programma in C en laadt het in de kernel met behulp van een systeemaanroep bpf(2), waar het wordt gecontroleerd door een verificateur en wordt vertaald naar native bytecode. Vervolgens verbindt dezelfde of een andere gebruiker het programma met de gebeurtenisbron en begint het uit te voeren. Het scheiden van opstarten en verbinden is om verschillende redenen noodzakelijk. Ten eerste is het uitvoeren van een verifier relatief duur en door hetzelfde programma meerdere keren te downloaden, verspillen we computertijd. Ten tweede hangt de manier waarop een programma precies is aangesloten af ​​van het type ervan, en een ‘universele’ interface die een jaar geleden is ontwikkeld, is misschien niet geschikt voor nieuwe soorten programma’s. (Hoewel nu de architectuur volwassener wordt, is er een idee om deze interface op niveau te verenigen libbpf.)

De oplettende lezer zal wellicht merken dat we nog niet klaar zijn met de foto's. Al het bovenstaande verklaart niet waarom BPF het beeld fundamenteel verandert in vergelijking met klassieke BPF. Twee innovaties die de reikwijdte van de toepasbaarheid aanzienlijk vergroten, zijn de mogelijkheid om gedeeld geheugen en kernelhelperfuncties te gebruiken. In BPF wordt gedeeld geheugen geïmplementeerd met behulp van zogenaamde kaarten: gedeelde datastructuren met een specifieke API. Ze hebben deze naam waarschijnlijk gekregen omdat het eerste type kaart dat verscheen een hashtabel was. Toen verschenen er arrays, lokale (per CPU) hashtabellen en lokale arrays, zoekbomen, kaarten met verwijzingen naar BPF-programma's en nog veel meer. Wat voor ons nu interessant is, is dat BPF-programma's nu de mogelijkheid hebben om de status tussen oproepen door te behouden en deze te delen met andere programma's en met gebruikersruimte.

Maps is toegankelijk vanuit gebruikersprocessen met behulp van een systeemaanroep bpf(2), en van BPF-programma's die in de kernel draaien en helperfuncties gebruiken. Bovendien zijn er niet alleen helpers om met kaarten te werken, maar ook om toegang te krijgen tot andere kernelmogelijkheden. BPF-programma's kunnen bijvoorbeeld helperfuncties gebruiken om pakketten door te sturen naar andere interfaces, perf-gebeurtenissen te genereren, toegang te krijgen tot kernelstructuren, enzovoort.

BPF voor de kleintjes, deel één: uitgebreide BPF

Samenvattend biedt BPF de mogelijkheid om willekeurige, d.w.z. door verificateurs geteste, gebruikerscode in de kernelruimte te laden. Deze code kan de status tussen oproepen opslaan en gegevens uitwisselen met gebruikersruimte, en heeft ook toegang tot kernelsubsystemen die door dit type programma worden toegestaan.

Dit is al vergelijkbaar met de mogelijkheden van kernelmodules, vergeleken met welke BPF enkele voordelen heeft (je kunt natuurlijk alleen vergelijkbare applicaties vergelijken, bijvoorbeeld systeemtracering - je kunt geen willekeurige driver in BPF schrijven). U kunt een lagere instapdrempel opmerken (voor sommige hulpprogramma's die BPF gebruiken, hoeft de gebruiker geen kernelprogrammeervaardigheden te hebben, of programmeervaardigheden in het algemeen), runtimeveiligheid (steek uw hand op in de opmerkingen voor degenen die het systeem niet hebben kapot gemaakt tijdens het schrijven of het testen van modules), atomiciteit - er is downtime bij het herladen van modules, en het BPF-subsysteem zorgt ervoor dat er geen gebeurtenissen worden gemist (om eerlijk te zijn, dit geldt niet voor alle soorten BPF-programma's).

De aanwezigheid van dergelijke mogelijkheden maakt BPF tot een universeel hulpmiddel voor het uitbreiden van de kernel, wat in de praktijk wordt bevestigd: er worden steeds meer nieuwe soorten programma's aan BPF toegevoegd, steeds meer grote bedrijven gebruiken BPF 24×7 op gevechtsservers, steeds meer startups bouwen hun bedrijf op oplossingen die zijn gebaseerd op BPF. BPF wordt overal gebruikt: bij de bescherming tegen DDoS-aanvallen, bij het creëren van SDN (bijvoorbeeld bij het implementeren van netwerken voor kubernetes), als de belangrijkste tool voor het traceren van systemen en het verzamelen van statistieken, in inbraakdetectiesystemen en sandbox-systemen, enz.

Laten we het overzichtsgedeelte van het artikel hier afronden en de virtuele machine en het BPF-ecosysteem in meer detail bekijken.

Uitweiding: nutsvoorzieningen

Om de voorbeelden in de volgende secties te kunnen uitvoeren, heeft u mogelijk op zijn minst een aantal hulpprogramma's nodig llvm/clang met bpf-ondersteuning en bpftool. In sectie Ontwikkelingshulpmiddelen U kunt de instructies lezen voor het samenstellen van de hulpprogramma's, evenals uw kernel. Dit gedeelte is hieronder geplaatst om de harmonie van onze presentatie niet te verstoren.

BPF virtuele machineregisters en instructiesysteem

De architectuur en het commandosysteem van BPF zijn ontwikkeld rekening houdend met het feit dat programma's in de C-taal zullen worden geschreven en, nadat ze in de kernel zijn geladen, in native code worden vertaald. Daarom werden het aantal registers en de reeks commando's gekozen met het oog op het snijvlak, in wiskundige zin, van de mogelijkheden van moderne machines. Bovendien werden er verschillende beperkingen opgelegd aan programma's, tot voor kort was het bijvoorbeeld niet mogelijk om lussen en subroutines te schrijven, en was het aantal instructies beperkt tot 4096 (nu kunnen bevoorrechte programma's maximaal een miljoen instructies laden).

BPF heeft elf voor de gebruiker toegankelijke 64-bits registers r0-r10 en een programmateller. Register r10 bevat een frameaanwijzer en is alleen-lezen. Programma's hebben tijdens runtime toegang tot een stapel van 512 bytes en een onbeperkte hoeveelheid gedeeld geheugen in de vorm van kaarten.

BPF-programma's mogen een specifieke set kernelhelpers van het programmatype uitvoeren en, meer recentelijk, reguliere functies. Elke aangeroepen functie kan maximaal vijf argumenten bevatten, doorgegeven in registers r1-r5, en de geretourneerde waarde wordt doorgegeven aan r0. Het is gegarandeerd dat na terugkeer uit de functie de inhoud van de registers wordt weergegeven r6-r9 Zal niet veranderen.

Voor een efficiënte programmavertaling, registers r0-r11 want alle ondersteunde architecturen worden op unieke wijze toegewezen aan echte registers, waarbij rekening wordt gehouden met de ABI-kenmerken van de huidige architectuur. Bijvoorbeeld voor x86_64 registreert r1-r5, gebruikt om functieparameters door te geven, worden weergegeven op rdi, rsi, rdx, rcx, r8, die worden gebruikt om parameters door te geven aan functies x86_64. De code aan de linkerkant vertaalt zich bijvoorbeeld als volgt naar de code aan de rechterkant:

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

Registreren r0 ook gebruikt om het resultaat van de programma-uitvoering terug te geven, en in het register r1 aan het programma wordt een verwijzing naar de context doorgegeven - afhankelijk van het type programma kan dit bijvoorbeeld een structuur zijn struct xdp_md (voor XDP) of structuur struct __sk_buff (voor verschillende netwerkprogramma's) of structuur struct pt_regs (voor verschillende soorten traceringsprogramma's), enz.

We hadden dus een set registers, kernelhelpers, een stapel, een contextaanwijzer en gedeeld geheugen in de vorm van kaarten. Niet dat dit alles absoluut noodzakelijk is tijdens de reis, maar...

Laten we doorgaan met de beschrijving en praten over het commandosysteem voor het werken met deze objecten. Alle (Bijna alle) BPF-instructies hebben een vaste grootte van 64 bits. Als je naar één instructie op een 64-bits Big Endian-machine kijkt, zul je het zien

BPF voor de kleintjes, deel één: uitgebreide BPF

Hier Code - dit is de codering van de instructie, Dst/Src zijn de coderingen van respectievelijk de ontvanger en de bron, Off - 16-bit ondertekende inspringing, en Imm is een 32-bits geheel getal met teken dat in sommige instructies wordt gebruikt (vergelijkbaar met de cBPF-constante K). Codering Code heeft een van de twee typen:

BPF voor de kleintjes, deel één: uitgebreide BPF

Instructieklassen 0, 1, 2, 3 definiëren commando's voor het werken met geheugen. Zij worden genoemd, BPF_LD, BPF_LDX, BPF_ST, BPF_STXrespectievelijk. Klassen 4, 7 (BPF_ALU, BPF_ALU64) vormen een reeks ALU-instructies. Klassen 5, 6 (BPF_JMP, BPF_JMP32) bevatten spronginstructies.

Het verdere plan voor het bestuderen van het BPF-instructiesysteem is als volgt: in plaats van alle instructies en hun parameters nauwgezet op te sommen, zullen we in deze sectie naar een paar voorbeelden kijken en daaruit zal duidelijk worden hoe de instructies feitelijk werken en hoe demonteer handmatig elk binair bestand voor BPF. Om het materiaal later in het artikel te consolideren, zullen we ook individuele instructies vinden in de secties over Verifier, JIT-compiler, vertaling van klassieke BPF, maar ook bij het bestuderen van kaarten, het aanroepen van functies, enz.

Als we het hebben over individuele instructies, verwijzen we naar de kernbestanden bpf.h и bpf_common.h, die de numerieke codes van BPF-instructies definiëren. Wanneer u zelf architectuur bestudeert en/of binaire bestanden analyseert, kunt u semantiek vinden in de volgende bronnen, gesorteerd op complexiteit: Onofficiële eBPF-specificatie, BPF- en XDP-referentiegids, instructieset, Documentatie/netwerken/filter.txt en natuurlijk in de Linux-broncode - verifier, JIT, BPF-interpreter.

Voorbeeld: BPF in je hoofd demonteren

Laten we eens kijken naar een voorbeeld waarin we een programma compileren readelf-example.c en kijk naar het resulterende binaire getal. We zullen de originele inhoud onthullen readelf-example.c hieronder, nadat we de logica van binaire codes hebben hersteld:

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

Eerste kolom in uitvoer readelf is een inspringing en ons programma bestaat dus uit vier opdrachten:

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

Commandocodes zijn gelijk b7, 15, b7 и 95. Bedenk dat de minst significante drie bits de instructieklasse zijn. In ons geval is het vierde bit van alle instructies leeg, dus de instructieklassen zijn respectievelijk 7, 5, 7, 5. Klasse 7 is BPF_ALU64, en 5 is BPF_JMP. Voor beide klassen is het instructieformaat hetzelfde (zie hierboven) en we kunnen ons programma als volgt herschrijven (tegelijkertijd zullen we de resterende kolommen in menselijke vorm herschrijven):

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

Operatie b klasse ALU64 - Is BPF_MOV. Het wijst een waarde toe aan het bestemmingsregister. Als de bit is ingesteld s (bron), dan wordt de waarde uit het bronregister gehaald en als deze, zoals in ons geval, niet is ingesteld, dan wordt de waarde uit het veld gehaald Imm. Dus in de eerste en derde instructies voeren we de bewerking uit r0 = Imm. Verder is JMP klasse 1 werking BPF_JEQ (spring indien gelijk). In ons geval sinds het bit S nul is, vergelijkt het de waarde van het bronregister met het veld Imm. Als de waarden samenvallen, vindt de overgang plaats naar PC + OffWaar PC, zoals gebruikelijk, bevat het adres van de volgende instructie. Ten slotte is JMP Class 9 Operation BPF_EXIT. Deze instructie beëindigt het programma en keert terug naar de kernel r0. Laten we een nieuwe kolom aan onze tabel toevoegen:

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

We kunnen dit in een handiger vorm herschrijven:

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

Als we ons herinneren wat er in het register staat r1 het programma krijgt een verwijzing naar de context vanuit de kernel en in het register doorgegeven r0 de waarde wordt teruggegeven aan de kernel, dan kunnen we zien dat als de verwijzing naar de context nul is, we 1 retourneren, en anders - 2. Laten we controleren of we gelijk hebben door naar de bron te kijken:

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

Ja, het is een betekenisloos programma, maar het vertaalt zich in slechts vier eenvoudige instructies.

Uitzonderingsvoorbeeld: instructie van 16 bytes

We hebben eerder vermeld dat sommige instructies meer dan 64 bits in beslag nemen. Dit geldt bijvoorbeeld voor instructies lddw (Code= 0x18 = BPF_LD | BPF_DW | BPF_IMM) — laad een dubbel woord uit de velden in het register Imm. Het feit is dat Imm heeft een grootte van 32 en een dubbel woord is 64 bits, dus het laden van een onmiddellijke waarde van 64 bits in een register in één 64-bits instructie zal niet werken. Om dit te doen worden twee aangrenzende instructies gebruikt om het tweede deel van de 64-bits waarde in het veld op te slaan Imm. Voorbeeld:

$ cat x64.c
long foo(void *ctx)
{
        return 0x11223344aabbccdd;
}
$ clang -target bpf -c x64.c -o x64.o -O2
$ llvm-readelf -x .text x64.o
Hex dump of section '.text':
0x00000000 18000000 ddccbbaa 00000000 44332211 ............D3".
0x00000010 95000000 00000000                   ........

Er zijn slechts twee instructies in een binair programma:

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

We ontmoeten elkaar weer met instructies lddw, als we het hebben over verhuizingen en het werken met kaarten.

Voorbeeld: BPF demonteren met standaard gereedschap

We hebben dus geleerd om binaire BPF-codes te lezen en zijn klaar om indien nodig elke instructie te ontleden. Het is echter de moeite waard om te zeggen dat het in de praktijk handiger en sneller is om programma's te demonteren met behulp van standaardtools, bijvoorbeeld:

$ 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

Levenscyclus van BPF-objecten, bpffs-bestandssysteem

(Ik hoorde voor het eerst enkele details die in deze subsectie worden beschreven van vasten Alexei Starovoitov in BPF-blog.)

BPF-objecten - programma's en kaarten - worden met behulp van opdrachten vanuit de gebruikersruimte gemaakt BPF_PROG_LOAD и BPF_MAP_CREATE systeem oproep bpf(2), we zullen in de volgende sectie bespreken hoe dit precies gebeurt. Dit creëert kerneldatastructuren voor elk ervan refcount (aantal referenties) wordt ingesteld op één, en een bestandsdescriptor die naar het object verwijst, wordt teruggestuurd naar de gebruiker. Nadat de hendel is gesloten refcount het object wordt met één verkleind, en wanneer het nul bereikt, wordt het vernietigd.

Als het programma kaarten gebruikt, dan refcount deze kaarten worden na het laden van het programma met één vergroot, d.w.z. hun bestandsdescriptors kunnen worden gesloten voor het gebruikersproces en toch refcount wordt niet nul:

BPF voor de kleintjes, deel één: uitgebreide BPF

Nadat we een programma succesvol hebben geladen, koppelen we het meestal aan een soort gebeurtenisgenerator. We kunnen het bijvoorbeeld op een netwerkinterface plaatsen om inkomende pakketten te verwerken of er verbinding mee te maken tracepoint in de kern. Op dit punt zal de referentieteller ook met één toenemen en kunnen we de bestandsdescriptor in het laadprogramma sluiten.

Wat gebeurt er als we nu de bootloader afsluiten? Het hangt af van het type gebeurtenisgenerator (hook). Alle netwerkhaken zullen bestaan ​​nadat de lader is voltooid, dit zijn de zogenaamde globale haken. En traceerprogramma's worden bijvoorbeeld vrijgegeven nadat het proces dat ze heeft gemaakt, is beëindigd (en worden daarom lokaal genoemd, van "lokaal tot het proces"). Technisch gezien hebben lokale hooks altijd een corresponderende bestandsdescriptor in de gebruikersruimte en sluiten ze daarom wanneer het proces wordt gesloten, maar globale hooks niet. In de volgende afbeelding probeer ik met behulp van rode kruisen te laten zien hoe de beëindiging van het laadprogramma de levensduur van objecten beïnvloedt in het geval van lokale en globale hooks.

BPF voor de kleintjes, deel één: uitgebreide BPF

Waarom is er een onderscheid tussen lokale en mondiale hooks? Het uitvoeren van sommige soorten netwerkprogramma's is zinvol zonder een gebruikersruimte. Stel je bijvoorbeeld DDoS-bescherming voor: de bootloader schrijft de regels en verbindt het BPF-programma met de netwerkinterface, waarna de bootloader zelfmoord kan plegen. Aan de andere kant, stel je een debug-trace-programma voor dat je in tien minuten op je knieën hebt geschreven. Als het klaar is, wil je dat er geen rommel meer in het systeem achterblijft, en lokale hooks zullen daarvoor zorgen.

Stel je aan de andere kant voor dat je verbinding wilt maken met een tracepoint in de kernel en over vele jaren statistieken wilt verzamelen. In dit geval wilt u het gebruikersgedeelte voltooien en van tijd tot tijd terugkeren naar de statistieken. Het bpf-bestandssysteem biedt deze mogelijkheid. Het is een pseudo-bestandssysteem dat uitsluitend in het geheugen wordt opgeslagen en waarmee bestanden kunnen worden gemaakt die verwijzen naar BPF-objecten en daardoor toenemen refcount voorwerpen. Hierna kan de lader afsluiten en blijven de objecten die hij heeft gemaakt in leven.

BPF voor de kleintjes, deel één: uitgebreide BPF

Het maken van bestanden in bpffs die verwijzen naar BPF-objecten wordt "vastzetten" genoemd (zoals in de volgende zin: "proces kan een BPF-programma of kaart vastzetten"). Het maken van bestandsobjecten voor BPF-objecten is niet alleen zinvol voor het verlengen van de levensduur van lokale objecten, maar ook voor de bruikbaarheid van globale objecten - als we teruggaan naar het voorbeeld met het wereldwijde DDoS-beveiligingsprogramma, willen we naar de statistieken kunnen komen kijken van tijd tot tijd.

Het BPF-bestandssysteem wordt meestal gemount in /sys/fs/bpf, maar het kan ook lokaal worden gemonteerd, bijvoorbeeld als volgt:

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

Bestandssysteemnamen worden gemaakt met behulp van de opdracht BPF_OBJ_PIN BPF-systeemoproep. Laten we ter illustratie een programma nemen, het compileren, uploaden en erop vastzetten bpffs. Ons programma doet niets nuttigs, we presenteren alleen de code zodat u het voorbeeld kunt reproduceren:

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

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

Laten we dit programma compileren en een lokale kopie van het bestandssysteem maken bpffs:

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

Laten we nu ons programma downloaden met behulp van het hulpprogramma bpftool en bekijk de bijbehorende systeemoproepen bpf(2) (enkele irrelevante regels verwijderd uit de strace-uitvoer):

$ sudo strace -e bpf bpftool prog load ./test.o bpf-mountpoint/test
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="test", ...}, 120) = 3
bpf(BPF_OBJ_PIN, {pathname="bpf-mountpoint/test", bpf_fd=3}, 120) = 0

Hier hebben we het programma geladen met behulp van BPF_PROG_LOAD, heeft een bestandsdescriptor van de kernel ontvangen 3 en het commando gebruiken BPF_OBJ_PIN heeft deze bestandsdescriptor vastgezet als een bestand "bpf-mountpoint/test". Hierna het bootloaderprogramma bpftool klaar met werken, maar ons programma bleef in de kernel, hoewel we het niet aan een netwerkinterface hadden gekoppeld:

$ 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

We kunnen het bestandsobject normaal verwijderen unlink(2) en daarna wordt het bijbehorende programma verwijderd:

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

Objecten verwijderen

Over het verwijderen van objecten gesproken, het is noodzakelijk om te verduidelijken dat nadat we het programma hebben losgekoppeld van de hook (gebeurtenisgenerator), geen enkele nieuwe gebeurtenis de lancering ervan zal activeren, maar dat alle huidige exemplaren van het programma in de normale volgorde zullen worden voltooid. .

Bij sommige soorten BPF-programma's kunt u het programma direct vervangen, d.w.z. zorgen voor sequentie-atomiciteit replace = detach old program, attach new program. In dit geval zullen alle actieve exemplaren van de oude versie van het programma hun werk voltooien en zullen er nieuwe gebeurtenishandlers worden gemaakt op basis van het nieuwe programma, en "atomiciteit" betekent hier dat geen enkele gebeurtenis zal worden gemist.

Programma's koppelen aan gebeurtenisbronnen

In dit artikel zullen we het verbinden van programma's met gebeurtenisbronnen niet afzonderlijk beschrijven, omdat het zinvol is om dit te bestuderen in de context van een specifiek type programma. Cm. voorbeeld hieronder, waarin we laten zien hoe programma's als XDP zijn aangesloten.

Objecten manipuleren met behulp van de bpf-systeemaanroep

BPF-programma's

Alle BPF-objecten worden gemaakt en beheerd vanuit de gebruikersruimte met behulp van een systeemaanroep bpf, met het volgende prototype:

#include <linux/bpf.h>

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

Hier is het team cmd is een van de waarden van type enum bpf_cmd, attr — een verwijzing naar parameters voor een specifiek programma en size — objectgrootte volgens de aanwijzer, d.w.z. meestal dit sizeof(*attr). In kernel 5.8 de systeemaanroep bpf ondersteunt 34 verschillende opdrachten, en определение union bpf_attr beslaat 200 regels. Maar we moeten ons hierdoor niet laten intimideren, aangezien we in de loop van verschillende artikelen vertrouwd zullen raken met de commando's en parameters.

Laten we beginnen met het team BPF_PROG_LOAD, dat BPF-programma's maakt - neemt een set BPF-instructies en laadt deze in de kernel. Op het moment van laden wordt de verifier gestart en vervolgens wordt de JIT-compiler en, na succesvolle uitvoering, de programmabestandsdescriptor teruggestuurd naar de gebruiker. Wat er vervolgens met hem gebeurt, hebben we in de vorige sectie gezien over de levenscyclus van BPF-objecten.

We gaan nu een aangepast programma schrijven dat een eenvoudig BPF-programma laadt, maar eerst moeten we beslissen wat voor soort programma we willen laden - we zullen moeten selecteren тип en schrijf binnen het raamwerk van dit type een programma dat de verificatietest zal doorstaan. Om het proces echter niet ingewikkelder te maken, is hier een kant-en-klare oplossing: we nemen een programma als BPF_PROG_TYPE_XDP, waarmee de waarde wordt geretourneerd XDP_PASS (sla alle pakketten over). In BPF assembler ziet het er heel eenvoudig uit:

r0 = 2
exit

Nadat we besloten hebben dat we zullen uploaden, we kunnen u vertellen hoe we het zullen doen:

#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

static inline __u64 ptr_to_u64(const void *ptr)
{
        return (__u64) (unsigned long) ptr;
}

int main(void)
{
    struct bpf_insn insns[] = {
        {
            .code = BPF_ALU64 | BPF_MOV | BPF_K,
            .dst_reg = BPF_REG_0,
            .imm = XDP_PASS
        },
        {
            .code = BPF_JMP | BPF_EXIT
        },
    };

    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_XDP,
        .insns     = ptr_to_u64(insns),
        .insn_cnt  = sizeof(insns)/sizeof(insns[0]),
        .license   = ptr_to_u64("GPL"),
    };

    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

Interessante gebeurtenissen in een programma beginnen met de definitie van een array insns - ons BPF-programma in machinecode. In dit geval wordt elke instructie van het BPF-programma in de structuur verpakt bpf_insn. Eerste element insns voldoet aan de instructies r0 = 2, de seconde - exit.

Toevluchtsoord. De kernel definieert handiger macro's voor het schrijven van machinecodes en het gebruik van het kernelheaderbestand tools/include/linux/filter.h wij zouden kunnen schrijven

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

Maar aangezien het schrijven van BPF-programma's in native code alleen nodig is voor het schrijven van tests in de kernel en artikelen over BPF, maakt de afwezigheid van deze macro's het leven van de ontwikkelaar niet echt ingewikkeld.

Nadat we het BPF-programma hebben gedefinieerd, gaan we verder met het laden ervan in de kernel. Onze minimalistische set parameters attr omvat het programmatype, de set en het aantal instructies, de vereiste licentie en de naam "woo", die we gebruiken om ons programma na het downloaden op het systeem te vinden. Het programma wordt, zoals beloofd, in het systeem geladen met behulp van een systeemaanroep bpf.

Aan het einde van het programma komen we terecht in een oneindige lus die de payload simuleert. Zonder dit zal het programma door de kernel worden gedood wanneer de bestandsdescriptor die de systeemaanroep naar ons heeft geretourneerd, wordt gesloten bpf, en we zullen het niet in het systeem zien.

Nou, we zijn klaar om te testen. Laten we het programma samenstellen en uitvoeren onder straceom te controleren of alles naar behoren werkt:

$ clang -g -O2 simple-prog.c -o simple-prog

$ sudo strace ./simple-prog
execve("./simple-prog", ["./simple-prog"], 0x7ffc7b553480 /* 13 vars */) = 0
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0x7ffe03c4ed50, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

Alles is in orde, bpf(2) gaf handvat 3 terug aan ons en we gingen in een oneindige lus met pause(). Laten we proberen ons programma in het systeem te vinden. Om dit te doen, gaan we naar een andere terminal en gebruiken we het hulpprogramma 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)

We zien dat er een geladen programma op het systeem staat woo wiens globale ID 390 is en momenteel aan de gang is simple-prog er is een open bestandsdescriptor die naar het programma verwijst (en if simple-prog zal dan de klus afmaken woo zal verdwijnen). Zoals verwacht, het programma woo neemt 16 bytes - twee instructies - aan binaire codes in de BPF-architectuur in beslag, maar in zijn oorspronkelijke vorm (x86_64) is dit al 40 bytes. Laten we ons programma in zijn oorspronkelijke vorm bekijken:

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

geen verrassingen. Laten we nu eens kijken naar de code die door de JIT-compiler is gegenereerd:

# 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

niet erg effectief voor exit(2), maar eerlijk gezegd is ons programma te simpel, en voor niet-triviale programma's zijn de proloog en epiloog toegevoegd door de JIT-compiler natuurlijk nodig.

Maps

BPF-programma's kunnen gestructureerde geheugengebieden gebruiken die toegankelijk zijn voor zowel andere BPF-programma's als voor programma's in de gebruikersruimte. Deze objecten worden kaarten genoemd en in deze sectie laten we zien hoe u ze kunt manipuleren met behulp van een systeemaanroep bpf.

Laten we meteen zeggen dat de mogelijkheden van kaarten niet alleen beperkt zijn tot toegang tot gedeeld geheugen. Er zijn kaarten voor speciale doeleinden die bijvoorbeeld verwijzingen naar BPF-programma's of verwijzingen naar netwerkinterfaces bevatten, kaarten voor het werken met perf-gebeurtenissen, enz. We zullen er hier niet over praten, om de lezer niet in verwarring te brengen. Daarnaast negeren we synchronisatieproblemen, omdat dit voor onze voorbeelden niet belangrijk is. Een volledige lijst met beschikbare kaarttypen vindt u in <linux/bpf.h>, en in deze sectie nemen we als voorbeeld het historisch eerste type, de hashtabel BPF_MAP_TYPE_HASH.

Als je een hashtabel maakt in bijvoorbeeld C++, zou je zeggen unordered_map<int,long> woo, wat in het Russisch betekent: 'Ik heb een tafel nodig woo onbeperkte grootte, waarvan de toetsen van type zijn int, en de waarden zijn van het type long" Om een ​​BPF-hashtabel te maken, moeten we vrijwel hetzelfde doen, behalve dat we de maximale grootte van de tabel moeten specificeren, en in plaats van de typen sleutels en waarden te specificeren, moeten we hun grootte in bytes specificeren. . Gebruik de opdracht om kaarten te maken BPF_MAP_CREATE systeem oproep bpf. Laten we eens kijken naar een min of meer minimaal programma dat een kaart maakt. Na het vorige programma dat BPF-programma's laadt, zou dit voor jou eenvoudig moeten lijken:

$ cat simple-map.c
#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

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

    for ( ;; )
        pause();
}

Hier definiëren we een reeks parameters attr, waarin we zeggen: 'Ik heb een hashtabel nodig met sleutels en groottewaarden sizeof(int), waarin ik maximaal vier elementen kan plaatsen." Bij het maken van BPF-kaarten kunt u andere parameters opgeven, bijvoorbeeld op dezelfde manier als in het voorbeeld met het programma. We hebben de naam van het object opgegeven als "woo".

Laten we het programma compileren en uitvoeren:

$ clang -g -O2 simple-map.c -o simple-map
$ sudo strace ./simple-map
execve("./simple-map", ["./simple-map"], 0x7ffd40a27070 /* 14 vars */) = 0
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=4, max_entries=4, map_name="woo", ...}, 72) = 3
pause(

Hier is de systeemoproep bpf(2) heeft ons het descriptorkaartnummer geretourneerd 3 en vervolgens wacht het programma, zoals verwacht, op verdere instructies in de systeemaanroep pause(2).

Laten we nu ons programma naar de achtergrond sturen of een andere terminal openen en ons object bekijken met behulp van het hulpprogramma bpftool (we kunnen onze kaart van anderen onderscheiden door zijn naam):

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

Het getal 114 is de globale ID van ons object. Elk programma op het systeem kan deze ID gebruiken om een ​​bestaande kaart te openen met behulp van de opdracht BPF_MAP_GET_FD_BY_ID systeem oproep bpf.

Nu kunnen we spelen met onze hashtafel. Laten we eens kijken naar de inhoud ervan:

$ sudo bpftool map dump id 114
Found 0 elements

Leeg. Laten we er een waarde in stoppen hash[1] = 1:

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

Laten we nog eens naar de tabel kijken:

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

Hoera! We zijn erin geslaagd één element toe te voegen. Merk op dat we hiervoor op byteniveau moeten werken, aangezien bptftool weet niet welk type de waarden in de hashtabel zijn. (Deze kennis kan via BTF aan haar worden overgedragen, maar daarover nu meer.)

Hoe leest en voegt bpftool precies elementen toe? Laten we eens onder de motorkap kijken:

$ 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

Eerst hebben we de kaart geopend op basis van de globale ID met behulp van de opdracht BPF_MAP_GET_FD_BY_ID и bpf(2) heeft descriptor 3 aan ons geretourneerd, verder met behulp van de opdracht BPF_MAP_GET_NEXT_KEY we vonden de eerste sleutel in de tafel door te passeren NULL als verwijzing naar de "vorige" sleutel. Als we de sleutel hebben, kunnen we het doen BPF_MAP_LOOKUP_ELEMdie een waarde retourneert aan een pointer value. De volgende stap is dat we proberen het volgende element te vinden door een verwijzing naar de huidige sleutel door te geven, maar onze tabel bevat slechts één element en het commando BPF_MAP_GET_NEXT_KEY geeft terug ENOENT.

Oké, laten we de waarde wijzigen met sleutel 1, laten we zeggen dat onze bedrijfslogica registratie vereist 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

Zoals verwacht is het heel eenvoudig: het commando BPF_MAP_GET_FD_BY_ID opent onze kaart op ID en de opdracht BPF_MAP_UPDATE_ELEM overschrijft het element.

Dus nadat we een hashtabel van het ene programma hebben gemaakt, kunnen we de inhoud ervan vanuit een ander programma lezen en schrijven. Merk op dat als we dit vanaf de opdrachtregel konden doen, elk ander programma op het systeem dit ook kan doen. Naast de hierboven beschreven opdrachten, voor het werken met kaarten vanuit de gebruikersruimte, De volgende:

  • BPF_MAP_LOOKUP_ELEM: zoek waarde op sleutel
  • BPF_MAP_UPDATE_ELEM: waarde bijwerken/creëren
  • BPF_MAP_DELETE_ELEM: sleutel verwijderen
  • BPF_MAP_GET_NEXT_KEY: zoek de volgende (of eerste) sleutel
  • BPF_MAP_GET_NEXT_ID: hiermee kun je door alle bestaande kaarten bladeren, zo werkt het bpftool map
  • BPF_MAP_GET_FD_BY_ID: open een bestaande kaart op basis van zijn globale ID
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: de waarde van een object atomair bijwerken en de oude retourneren
  • BPF_MAP_FREEZE: maak de kaart onveranderlijk vanuit de gebruikersruimte (deze bewerking kan niet ongedaan worden gemaakt)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: massa-operaties. Bijvoorbeeld, BPF_MAP_LOOKUP_AND_DELETE_BATCH - dit is de enige betrouwbare manier om alle waarden van de kaart te lezen en opnieuw in te stellen

Niet al deze opdrachten werken voor alle kaarttypen, maar over het algemeen ziet het werken met andere typen kaarten vanuit de gebruikersruimte er precies hetzelfde uit als het werken met hashtabellen.

Laten we voor de orde onze hashtabelexperimenten afronden. Weet je nog dat we een tabel hebben gemaakt die maximaal vier sleutels kan bevatten? Laten we nog een paar elementen toevoegen:

$ 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

Tot nu toe gaat het goed:

$ sudo bpftool map dump id 114
key: 01 00 00 00  value: 01 00 00 00
key: 02 00 00 00  value: 01 00 00 00
key: 04 00 00 00  value: 01 00 00 00
key: 03 00 00 00  value: 01 00 00 00
Found 4 elements

Laten we proberen er nog één toe te voegen:

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

Zoals verwacht is het ons niet gelukt. Laten we de fout in meer detail bekijken:

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

Alles is in orde: zoals verwacht, het team BPF_MAP_UPDATE_ELEM probeert een nieuwe, vijfde sleutel te maken, maar crasht E2BIG.

We kunnen dus BPF-programma's maken en laden, en ook kaarten maken en beheren vanuit de gebruikersruimte. Nu is het logisch om te kijken hoe we kaarten uit de BPF-programma’s zelf kunnen gebruiken. We zouden hierover kunnen praten in de taal van moeilijk leesbare programma's in machinemacrocodes, maar in feite is de tijd gekomen om te laten zien hoe BPF-programma's feitelijk worden geschreven en onderhouden - met behulp van libbpf.

(Voor lezers die ontevreden zijn over het ontbreken van een voorbeeld op laag niveau: we zullen in detail programma's analyseren die kaarten en hulpfuncties gebruiken die zijn gemaakt met behulp van libbpf en vertellen wat er op instructieniveau gebeurt. Voor lezers die ontevreden zijn heel veel, we hebben toegevoegd voorbeeld op de juiste plaats in het artikel.)

BPF-programma's schrijven met libbpf

Het schrijven van BPF-programma's met behulp van machinecodes kan alleen de eerste keer interessant zijn, en dan begint de verzadiging. Op dit moment moet u uw aandacht richten op llvm, dat een backend heeft voor het genereren van code voor de BPF-architectuur, evenals een bibliotheek libbpf, waarmee u de gebruikerskant van BPF-applicaties kunt schrijven en de code kunt laden van BPF-programma's die zijn gegenereerd met llvm/clang.

Zoals we in dit en volgende artikelen zullen zien, libbpf doet behoorlijk wat werk zonder (of soortgelijke hulpmiddelen - iproute2, libbcc, libbpf-go, enz.) het is onmogelijk om te leven. Een van de killer features van het project libbpf is BPF CO-RE (Compile Once, Run Everywhere) - een project waarmee je BPF-programma's kunt schrijven die overdraagbaar zijn van de ene kernel naar de andere, met de mogelijkheid om op verschillende API's te draaien (bijvoorbeeld wanneer de kernelstructuur verandert van versie naar versie). Om met CO-RE te kunnen werken, moet je kernel gecompileerd zijn met BTF-ondersteuning (hoe je dit doet, beschrijven we in de sectie Ontwikkelingshulpmiddelen. Je kunt heel eenvoudig controleren of je kernel met BTF is gebouwd of niet - door de aanwezigheid van het volgende bestand:

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

Dit bestand slaat informatie op over alle gegevenstypen die in de kernel worden gebruikt en wordt in al onze voorbeelden gebruikt met behulp van libbpf. We zullen in het volgende artikel in detail over CO-RE praten, maar in dit artikel bouw je gewoon een kernel mee CONFIG_DEBUG_INFO_BTF.

bibliotheek libbpf woont precies in de directory tools/lib/bpf kernel en de ontwikkeling ervan wordt uitgevoerd via de mailinglijst [email protected]. Er wordt echter een aparte repository onderhouden voor de behoeften van applicaties die buiten de kernel leven https://github.com/libbpf/libbpf waarin de kernelbibliotheek min of meer wordt gespiegeld voor leestoegang.

In deze sectie bekijken we hoe u een project kunt maken dat gebruikmaakt van libbpfLaten we verschillende (min of meer betekenisloze) testprogramma's schrijven en in detail analyseren hoe het allemaal werkt. Dit zal ons in staat stellen om in de volgende secties gemakkelijker uit te leggen hoe BPF-programma's precies omgaan met kaarten, kernelhelpers, BTF, enz.

Meestal gebruiken projecten libbpf voeg een GitHub-repository toe als een git-submodule, we zullen hetzelfde doen:

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

gaan naar libbpf heel simpel:

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

Ons volgende plan in deze sectie is als volgt: we zullen een BPF-programma schrijven zoals BPF_PROG_TYPE_XDP, hetzelfde als in het vorige voorbeeld, maar in C compileren we het met behulp van clangen schrijf een helperprogramma dat het in de kernel laadt. In de volgende paragrafen zullen we de mogelijkheden van zowel het BPF-programma als het assistent-programma uitbreiden.

Voorbeeld: een volwaardige applicatie maken met libbpf

Om te beginnen gebruiken we het bestand /sys/kernel/btf/vmlinux, die hierboven werd vermeld, en maak het equivalent ervan in de vorm van een headerbestand:

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

Dit bestand slaat alle datastructuren op die beschikbaar zijn in onze kernel. Dit is bijvoorbeeld hoe de IPv4-header in de kernel wordt gedefinieerd:

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

Nu zullen we ons BPF-programma in C schrijven:

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

Hoewel ons programma heel eenvoudig bleek te zijn, moeten we toch op veel details letten. Ten eerste is het eerste headerbestand dat we opnemen vmlinux.h, die we zojuist hebben gegenereerd met behulp van bpftool btf dump - nu hoeven we het kernel-headers-pakket niet te installeren om erachter te komen hoe de kernelstructuren eruit zien. Het volgende headerbestand komt vanuit de bibliotheek naar ons toe libbpf. Nu hebben we het alleen nodig om de macro te definiëren SEC, die het teken naar de juiste sectie van het ELF-objectbestand stuurt. Ons programma vindt u in de sectie xdp/simple, waar we vóór de schuine streep het programmatype BPF definiëren - dit is de conventie die wordt gebruikt libbpf, op basis van de sectienaam zal het bij het opstarten het juiste type vervangen bpf(2). Het BPF-programma zelf is dat wel C - heel eenvoudig en bestaat uit één regel return XDP_PASS. Tenslotte nog een apart hoofdstuk "license" bevat de naam van de licentie.

We kunnen ons programma compileren met llvm/clang, versie >= 10.0.0, of beter nog, hoger (zie sectie Ontwikkelingshulpmiddelen):

$ 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

Een van de interessante kenmerken: we geven de doelarchitectuur aan -target bpf en het pad naar de headers libbpf, die we onlangs hebben geïnstalleerd. Vergeet ook niet -O2Zonder deze optie kunt u in de toekomst voor verrassingen komen te staan. Laten we eens naar onze code kijken: zijn we erin geslaagd het programma te schrijven dat we wilden?

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

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

Disassembly of section xdp/simple:

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

Ja, het werkte! Nu hebben we een binair bestand bij het programma en we willen een applicatie maken die het in de kernel laadt. Voor dit doel de bibliotheek libbpf biedt ons twee opties: gebruik een API op een lager niveau of een API op een hoger niveau. We gaan de tweede kant op, omdat we willen leren hoe we met minimale inspanning BPF-programma's kunnen schrijven, laden en verbinden voor hun latere studie.

Eerst moeten we het ‘skelet’ van ons programma genereren vanuit het binaire bestand met hetzelfde hulpprogramma bpftool – het Zwitserse mes van de BPF-wereld (dat letterlijk kan worden opgevat, aangezien Daniel Borkman, een van de makers en beheerders van BPF, Zwitsers is):

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

In bestand xdp-simple.skel.h bevat de binaire code van ons programma en functies voor het beheren - laden, koppelen en verwijderen van ons object. In ons eenvoudige geval lijkt dit overdreven, maar het werkt ook in het geval waarin het objectbestand veel BPF-programma's en kaarten bevat en om deze gigantische ELF te laden hoeven we alleen maar het skelet te genereren en een of twee functies aan te roepen vanuit de aangepaste applicatie die we hebben gebruikt. schrijven Laten we nu verder gaan.

Strikt genomen is ons loaderprogramma triviaal:

#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    struct xdp_simple_bpf *obj;

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

    pause();

    xdp_simple_bpf__destroy(obj);
}

Hier struct xdp_simple_bpf gedefinieerd in het bestand xdp-simple.skel.h en beschrijft ons objectbestand:

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

We kunnen hier sporen zien van een API op laag niveau: de structuur struct bpf_program *simple и struct bpf_link *simple. De eerste structuur beschrijft specifiek ons ​​programma, geschreven in de sectie xdp/simple, en de tweede beschrijft hoe het programma verbinding maakt met de gebeurtenisbron.

Functie xdp_simple_bpf__open_and_load, opent een ELF-object, parseert het, creëert alle structuren en substructuren (naast het programma bevat ELF ook andere secties - gegevens, alleen-lezen gegevens, foutopsporingsinformatie, licentie, enz.), en laadt het vervolgens in de kernel met behulp van een systeem telefoongesprek bpf, wat we kunnen controleren door het programma te compileren en uit te voeren:

$ 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

Laten we nu eens kijken naar ons programma met behulp van bpftool. Laten we haar ID zoeken:

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

en dump (we gebruiken een verkorte vorm van het commando bpftool prog dump xlated):

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

Iets nieuws! Het programma drukte stukjes van ons bronbestand in C af. Dit werd gedaan door de bibliotheek libbpf, die de debug-sectie in het binaire bestand vond, het in een BTF-object compileerde en het in de kernel laadde met behulp van BPF_BTF_LOADen specificeerde vervolgens de resulterende bestandsdescriptor bij het laden van het programma met de opdracht BPG_PROG_LOAD.

Kernelhelpers

BPF-programma's kunnen "externe" functies uitvoeren - kernelhelpers. Met deze helperfuncties kunnen BPF-programma's toegang krijgen tot kernelstructuren, kaarten beheren en ook communiceren met de "echte wereld" - perf-gebeurtenissen creëren, hardware besturen (bijvoorbeeld pakketten omleiden), enz.

Voorbeeld: bpf_get_smp_processor_id

Laten we, binnen het raamwerk van het ‘leren door het goede voorbeeld te geven’-paradigma, een van de helperfuncties bekijken: bpf_get_smp_processor_id(), zeker in bestand kernel/bpf/helpers.c. Het retourneert het nummer van de processor waarop het BPF-programma dat het heeft aangeroepen, draait. Maar we zijn niet zo geïnteresseerd in de semantiek ervan als in het feit dat de implementatie ervan één lijn volgt:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

De definities van de BPF-helperfunctie zijn vergelijkbaar met de definities van de Linux-systeemaanroepen. Hier wordt bijvoorbeeld een functie gedefinieerd die geen argumenten heeft. (Een functie die bijvoorbeeld drie argumenten nodig heeft, wordt gedefinieerd met behulp van de macro BPF_CALL_3. Het maximale aantal argumenten is vijf.) Dit is echter slechts het eerste deel van de definitie. Het tweede deel is het definiëren van de typestructuur struct bpf_func_proto, die een beschrijving bevat van de helperfunctie die de verificateur begrijpt:

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

Helperfuncties registreren

Om ervoor te zorgen dat BPF-programma's van een bepaald type deze functie kunnen gebruiken, moeten ze deze bijvoorbeeld voor dat type registreren BPF_PROG_TYPE_XDP een functie is gedefinieerd in de kernel xdp_func_proto, die aan de hand van de helperfunctie-ID bepaalt of XDP deze functie ondersteunt of niet. Onze functie is ondersteunt de:

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

Nieuwe BPF-programmatypen worden in het bestand "gedefinieerd". include/linux/bpf_types.h met behulp van een macro BPF_PROG_TYPE. Gedefinieerd tussen aanhalingstekens omdat het een logische definitie is, en in C-taaltermen komt de definitie van een hele reeks concrete structuren op andere plaatsen voor. Met name in het bestand kernel/bpf/verifier.c alle definities uit bestand bpf_types.h worden gebruikt om een ​​reeks structuren te creëren 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
};

Dat wil zeggen dat voor elk type BPF-programma een verwijzing naar een datastructuur van dat type wordt gedefinieerd struct bpf_verifier_ops, die wordt geïnitialiseerd met de waarde _name ## _verifier_ops, dat wil zeggen, xdp_verifier_ops voor xdp. Structuur xdp_verifier_ops bepaald door in bestand net/core/filter.c следующим обрахом:

const struct bpf_verifier_ops xdp_verifier_ops = {
    .get_func_proto     = xdp_func_proto,
    .is_valid_access    = xdp_is_valid_access,
    .convert_ctx_access = xdp_convert_ctx_access,
    .gen_prologue       = bpf_noop_prologue,
};

Hier zien we onze vertrouwde functie xdp_func_proto, waarmee de verifier wordt uitgevoerd telkens wanneer er een uitdaging wordt tegengekomen sommige functies binnen een BPF-programma, zie verifier.c.

Laten we eens kijken hoe een hypothetisch BPF-programma de functie gebruikt bpf_get_smp_processor_id. Om dit te doen, herschrijven we het programma uit onze vorige sectie als volgt:

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

symbool bpf_get_smp_processor_id bepaald door в <bpf/bpf_helper_defs.h> Bibliotheek libbpf hoe

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

dat is, bpf_get_smp_processor_id is een functieaanwijzer waarvan de waarde 8 is, waarbij 8 de waarde is BPF_FUNC_get_smp_processor_id типа enum bpf_fun_id, die voor ons in het bestand is gedefinieerd vmlinux.h (bestand bpf_helper_defs.h in de kernel wordt gegenereerd door een script, dus de “magische” cijfers zijn oké). Deze functie accepteert geen argumenten en retourneert een waarde van het type __u32. Wanneer we het in ons programma uitvoeren, clang genereert een instructie BPF_CALL "de juiste soort" Laten we het programma compileren en naar de sectie kijken xdp/simple:

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ llvm-objdump -D --section=xdp/simple xdp-simple.bpf.o

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

Disassembly of section xdp/simple:

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

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

In de eerste regel zien we instructies call, parameter IMM wat gelijk is aan 8, en SRC_REG - nul. Volgens de ABI-overeenkomst die door de verificateur wordt gebruikt, is dit een oproep naar helperfunctie nummer acht. Zodra het is gelanceerd, is de logica eenvoudig. Retourwaarde uit register r0 gekopieerd naar r1 en op regels 2,3 wordt het omgezet naar type u32 — de bovenste 32 bits worden gewist. Op regels 4,5,6,7 retourneren we 2 (XDP_PASS) of 1 (XDP_DROP) afhankelijk van of de helperfunctie van regel 0 een nul- of niet-nulwaarde retourneerde.

Laten we onszelf testen: laad het programma en bekijk de uitvoer 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é, de verifier heeft de juiste kernelhelper gevonden.

Voorbeeld: argumenten doorgeven en uiteindelijk het programma uitvoeren!

Alle run-level helperfuncties hebben een prototype

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

Parameters voor helperfuncties worden in registers doorgegeven r1-r5en de waarde wordt geretourneerd in het register r0. Er zijn geen functies die meer dan vijf argumenten nodig hebben, en er wordt niet verwacht dat er in de toekomst ondersteuning voor zal worden toegevoegd.

Laten we eens kijken naar de nieuwe kernelhelper en hoe BPF parameters doorgeeft. Laten we herschrijven xdp-simple.bpf.c als volgt (de rest van de regels is niet veranderd):

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

Ons programma drukt het nummer af van de CPU waarop het draait. Laten we het compileren en naar de code kijken:

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

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

In regels 0-7 schrijven we de string running on CPU%un, en dan voeren we op regel 8 de bekende uit bpf_get_smp_processor_id. Op de regels 9-12 bereiden we de helperargumenten voor bpf_printk - registreert r1, r2, r3. Waarom zijn het er drie en niet twee? Omdat bpf_printkdit is een macro-wrapper rond de echte helper bpf_trace_printk, die de grootte van de formatstring moet doorgeven.

Laten we nu een paar regels toevoegen xdp-simple.czodat ons programma verbinding maakt met de interface lo en echt begonnen!

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

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

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

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

cleanup:
    xdp_simple_bpf__destroy(obj);
}

Hier gebruiken we de functie bpf_set_link_xdp_fd, dat BPF-programma's van het XDP-type verbindt met netwerkinterfaces. We hebben het interfacenummer hardgecodeerd lo, wat altijd 1 is. We voeren de functie twee keer uit om eerst het oude programma los te koppelen als dit was gekoppeld. Merk op dat we nu geen uitdaging meer nodig hebben pause of een oneindige lus: ons laadprogramma wordt afgesloten, maar het BPF-programma wordt niet afgesloten omdat het is verbonden met de gebeurtenisbron. Na een succesvolle download en verbinding wordt het programma gestart voor elk aankomend netwerkpakket lo.

Laten we het programma downloaden en naar de interface kijken 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

Het programma dat we hebben gedownload heeft ID 669 en we zien dezelfde ID op de interface lo. We sturen een paar pakketten naar 127.0.0.1 (verzoek + antwoord):

$ ping -c1 localhost

en laten we nu eens kijken naar de inhoud van het virtuele debug-bestand /sys/kernel/debug/tracing/trace_pipe, waarin bpf_printk schrijft zijn berichten:

# 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

Er werden twee pakketten gespot lo en verwerkt op CPU0 - ons eerste volwaardige betekenisloze BPF-programma werkte!

Het is vermeldenswaard dat bpf_printk Het is niet voor niets dat het naar het debug-bestand schrijft: dit is niet de meest succesvolle helper voor gebruik in de productie, maar ons doel was om iets eenvoudigs te laten zien.

Toegang tot kaarten vanuit BPF-programma's

Voorbeeld: gebruik van een kaart uit het BPF-programma

In de vorige secties hebben we geleerd hoe we kaarten vanuit de gebruikersruimte kunnen maken en gebruiken, en laten we nu naar het kernelgedeelte kijken. Laten we, zoals gewoonlijk, beginnen met een voorbeeld. Laten we ons programma herschrijven xdp-simple.bpf.c следующим обрахом:

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

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

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

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

    *val += 1;

    return XDP_PASS;
}

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

Aan het begin van het programma hebben we een kaartdefinitie toegevoegd woo: Dit is een array met 8 elementen die waarden opslaat zoals u64 (in C zouden we zo'n array definiëren als u64 woo[8]). In een programma "xdp/simple" we krijgen het huidige processornummer in een variabele key en vervolgens de helperfunctie gebruiken bpf_map_lookup_element we krijgen een verwijzing naar de overeenkomstige vermelding in de array, die we met één verhogen. Vertaald in het Russisch: we berekenen statistieken over welke CPU inkomende pakketten heeft verwerkt. Laten we proberen het programma uit te voeren:

$ 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

Laten we eens kijken of ze aangesloten is lo en stuur wat pakketjes:

$ 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

Laten we nu eens kijken naar de inhoud van de 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 }
]

Bijna alle processen werden verwerkt op CPU7. Dit is niet belangrijk voor ons, het belangrijkste is dat het programma werkt en dat we begrijpen hoe we toegang kunnen krijgen tot kaarten uit BPF-programma's - met behulp van хелперов bpf_mp_*.

Mystieke index

We hebben dus toegang tot de kaart vanuit het BPF-programma met behulp van oproepen zoals

val = bpf_map_lookup_elem(&woo, &key);

waar de helperfunctie eruit ziet

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

maar we passeren een aanwijzer &woo naar een naamloze structuur struct { ... }...

Als we naar de programma-assembler kijken, zien we dat de waarde &woo is niet daadwerkelijk gedefinieerd (regel 4):

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

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

Disassembly of section xdp/simple:

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

en is opgenomen in verhuizingen:

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

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

Maar als we naar het reeds geladen programma kijken, zien we een verwijzing naar de juiste kaart (regel 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]
...

We kunnen dus concluderen dat op het moment dat ons loader-programma werd gelanceerd, de link naar &woo werd vervangen door iets met een bibliotheek libbpf. Eerst kijken we naar de 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

We zien dat libbpf een kaart gemaakt woo en downloadde vervolgens ons programma simple. Laten we eens nader bekijken hoe we het programma laden:

  • telefoongesprek xdp_simple_bpf__open_and_load van bestand xdp-simple.skel.h
  • wat veroorzaakt xdp_simple_bpf__load van bestand xdp-simple.skel.h
  • wat veroorzaakt bpf_object__load_skeleton van bestand libbpf/src/libbpf.c
  • wat veroorzaakt bpf_object__load_xattr van libbpf/src/libbpf.c

De laatste functie zal onder andere aanroepen bpf_object__create_maps, waarmee bestaande kaarten worden gemaakt of geopend en deze in bestandsbeschrijvingen worden omgezet. (Dit is waar we het zien BPF_MAP_CREATE in de uitvoer strace.) Vervolgens wordt de functie aangeroepen bpf_object__relocate en zij is het die ons interesseert, omdat we ons herinneren wat we zagen woo in de verhuistabel. Als we het verkennen, komen we uiteindelijk in de functie terecht bpf_program__relocate, welke en houdt zich bezig met kaartverplaatsingen:

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

Dus we volgen onze instructies

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

en vervang het bronregister daarin door BPF_PSEUDO_MAP_FD, en de eerste IMM naar de bestandsdescriptor van onze kaart en, als deze bijvoorbeeld gelijk is aan, 0xdeadbeef, dan zullen we als resultaat de instructie ontvangen

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

Op deze manier wordt kaartinformatie overgebracht naar een specifiek geladen BPF-programma. In dit geval kan de kaart worden gemaakt met behulp van BPF_MAP_CREATE, en geopend door ID met behulp van BPF_MAP_GET_FD_BY_ID.

Totaal, bij gebruik libbpf het algoritme is als volgt:

  • tijdens het compileren worden er records aangemaakt in de verhuistabel voor koppelingen naar kaarten
  • libbpf opent het ELF-objectenboek, vindt alle gebruikte kaarten en maakt er bestandsbeschrijvingen voor
  • bestandsdescriptors worden als onderdeel van de instructie in de kernel geladen LD64

Zoals u zich kunt voorstellen, komt er nog meer en zullen we naar de kern moeten kijken. Gelukkig hebben we een idee: we hebben de betekenis opgeschreven BPF_PSEUDO_MAP_FD in het bronregister en we kunnen het begraven, wat ons naar het heilige van alle heiligen zal leiden - kernel/bpf/verifier.c, waarbij een functie met een onderscheidende naam een ​​bestandsdescriptor vervangt door het adres van een structuur of type struct bpf_map:

static int replace_map_fd_with_map_ptr(struct bpf_verifier_env *env) {
    ...

    f = fdget(insn[0].imm);
    map = __bpf_map_get(f);
    if (insn->src_reg == BPF_PSEUDO_MAP_FD) {
        addr = (unsigned long)map;
    }
    insn[0].imm = (u32)addr;
    insn[1].imm = addr >> 32;

(volledige code is te vinden link). We kunnen ons algoritme dus uitbreiden:

  • tijdens het laden van het programma controleert de verifier het juiste gebruik van de kaart en schrijft het adres van de overeenkomstige structuur struct bpf_map

Bij het downloaden van het ELF-binaire bestand met behulp van libbpf Er is nog veel meer aan de hand, maar dat bespreken we in andere artikelen.

Programma's en kaarten laden zonder libbpf

Zoals beloofd is hier een voorbeeld voor lezers die willen weten hoe ze zonder hulp een programma kunnen maken en laden dat kaarten gebruikt libbpf. Dit kan handig zijn als u in een omgeving werkt waarvoor u geen afhankelijkheden kunt bouwen, of niet alles kunt opslaan, of een programma kunt schrijven zoals ply, dat direct binaire BPF-code genereert.

Om het gemakkelijker te maken de logica te volgen, zullen we ons voorbeeld voor deze doeleinden herschrijven xdp-simple. De volledige en enigszins uitgebreide code van het programma dat in dit voorbeeld wordt besproken, vindt u hierin kern.

De logica van onze applicatie is als volgt:

  • maak een typekaart BPF_MAP_TYPE_ARRAY met behulp van de opdracht BPF_MAP_CREATE,
  • maak een programma dat deze kaart gebruikt,
  • Verbind het programma met de interface lo,

wat zich vertaalt in menselijk als

int main(void)
{
    int map_fd, prog_fd;

    map_fd = map_create();
    if (map_fd < 0)
        err(1, "bpf: BPF_MAP_CREATE");

    prog_fd = prog_load(map_fd);
    if (prog_fd < 0)
        err(1, "bpf: BPF_PROG_LOAD");

    xdp_attach(1, prog_fd);
}

Hier map_create maakt een kaart op dezelfde manier als in het eerste voorbeeld over de systeemaanroep bpf - “kernel, maak me alsjeblieft een nieuwe kaart in de vorm van een reeks van 8 elementen zoals __u64 en geef me de bestandsdescriptor terug":

static int map_create()
{
    union bpf_attr attr;

    memset(&attr, 0, sizeof(attr));
    attr.map_type = BPF_MAP_TYPE_ARRAY,
    attr.key_size = sizeof(__u32),
    attr.value_size = sizeof(__u64),
    attr.max_entries = 8,
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}

Het programma is ook eenvoudig te laden:

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

Het lastige deel prog_load is de definitie van ons BPF-programma als een reeks structuren struct bpf_insn insns[]. Maar omdat we een programma gebruiken dat we in C hebben, kunnen we een beetje vals spelen:

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

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

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

In totaal moeten we 14 instructies schrijven in de vorm van structuren zoals struct bpf_insn (advies: neem de stortplaats van bovenaf, lees de instructies opnieuw, open linux/bpf.h и linux/bpf_common.h en proberen te bepalen struct bpf_insn insns[] op zichzelf):

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

Een oefening voor degenen die dit niet zelf hebben geschreven: zoek map_fd.

Er is nog een niet bekendgemaakt onderdeel over in ons programma: xdp_attach. Helaas kunnen programma's als XDP niet worden verbonden via een systeemoproep bpf. De mensen die BPF en XDP hebben gemaakt, kwamen uit de online Linux-gemeenschap, wat betekent dat ze degene gebruikten die hen het meest bekend was (maar niet de meest bekende). normaal people) interface voor interactie met de kernel: netlink-aansluitingen, zie ook RFC3549. De eenvoudigste manier om te implementeren xdp_attach kopieert code van libbpf, namelijk uit het bestand netlink.c, en dat is wat we deden, door het een beetje in te korten:

Welkom in de wereld van netlink-sockets

Open een netlink-sockettype 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;
}

We lezen uit deze socket:

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

Tenslotte is hier onze functie die een socket opent en er een speciaal bericht naartoe stuurt met daarin een bestandsdescriptor:

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

Dus alles is klaar om te testen:

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

Laten we eens kijken of ons programma verbinding heeft gemaakt met 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

Laten we pings verzenden en naar de kaart kijken:

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

Hoera, alles werkt. Merk trouwens op dat onze kaart opnieuw wordt weergegeven in de vorm van bytes. Dit komt door het feit dat, in tegenstelling tot libbpf we hebben geen type-informatie (BTF) geladen. Maar de volgende keer zullen we hier meer over praten.

Ontwikkelingshulpmiddelen

In deze sectie bekijken we de minimale BPF-ontwikkelaarstoolkit.

Over het algemeen heb je niets speciaals nodig om BPF-programma's te ontwikkelen - BPF draait op elke fatsoenlijke distributiekernel, en programma's worden gebouwd met behulp van clang, die uit de verpakking kan worden geleverd. Echter, vanwege het feit dat BPF in ontwikkeling is, veranderen de kernel en tools voortdurend, als je vanaf 2019 geen BPF-programma's met ouderwetse methoden wilt schrijven, dan zul je moeten compileren

  • llvm/clang
  • pahole
  • zijn kern
  • bpftool

(Ter referentie: deze sectie en alle voorbeelden in het artikel zijn uitgevoerd op Debian 10.)

llvm/clang

BPF is vriendelijk met LLVM en hoewel recentelijk programma's voor BPF kunnen worden gecompileerd met behulp van gcc, wordt alle huidige ontwikkeling uitgevoerd voor LLVM. Daarom zullen we eerst de huidige versie bouwen clang van git:

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

Nu kunnen we controleren of alles correct is samengevoegd:

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

(Montage-instructies clang door mij overgenomen bpf_devel_QA.)

We installeren de programma's die we zojuist hebben gebouwd niet, maar voegen ze gewoon toe PATH, bijvoorbeeld:

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

(Dit kan worden toegevoegd .bashrc of naar een apart bestand. Persoonlijk voeg ik dit soort dingen toe ~/bin/activate-llvm.sh en als het nodig is, doe ik dat . activate-llvm.sh.)

Pahole en BTF

Nut pahole gebruikt bij het bouwen van de kernel om foutopsporingsinformatie in BTF-formaat te creëren. We zullen in dit artikel niet in detail treden over de details van de BTF-technologie, behalve dat het handig is en we er gebruik van willen maken. Dus als je je kernel gaat bouwen, bouw dan eerst pahole (zonder pahole met deze optie kun je de kernel niet bouwen 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

Kernels voor het experimenteren met BPF

Bij het verkennen van de mogelijkheden van BPF wil ik mijn eigen kern samenstellen. Dit is over het algemeen niet nodig, omdat je BPF-programma's op de distributiekernel kunt compileren en laden. Als je echter een eigen kernel hebt, kun je de nieuwste BPF-functies gebruiken, die op zijn best binnen enkele maanden in je distributie zullen verschijnen. , of, zoals in het geval van sommige debugging-tools, in de nabije toekomst helemaal niet zullen worden verpakt. Bovendien zorgt de eigen kern ervoor dat het belangrijk voelt om met de code te experimenteren.

Om een ​​kernel te bouwen heb je ten eerste de kernel zelf nodig, en ten tweede een kernelconfiguratiebestand. Om met BPF te experimenteren kunnen we het gebruikelijke gebruiken vanille kernel of een van de ontwikkelingskernels. Historisch gezien vindt de ontwikkeling van BPF plaats binnen de Linux-netwerkgemeenschap en daarom gaan alle veranderingen vroeg of laat via David Miller, de beheerder van het Linux-netwerk. Afhankelijk van hun aard – bewerkingen of nieuwe functies – vallen netwerkveranderingen in een van de twee kernen: net of net-next. Veranderingen voor BPF worden op dezelfde manier verdeeld tussen bpf и bpf-next, die vervolgens worden samengevoegd in respectievelijk net en net-next. Voor meer details, zie bpf_devel_QA и netdev-FAQ. Kies dus een kernel op basis van uw smaak en de stabiliteitsbehoeften van het systeem waarop u test (*-next kernels zijn de meest onstabiele van de genoemde kernels).

Het valt buiten het bestek van dit artikel om te praten over het beheren van kernelconfiguratiebestanden - er wordt aangenomen dat u al weet hoe u dit moet doen, of klaar om te leren op zichzelf. De volgende instructies zouden echter min of meer voldoende moeten zijn om u een werkend systeem te bieden dat geschikt is voor BPF.

Download een van de bovenstaande kernels:

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

Bouw een minimaal werkende kernelconfiguratie:

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

Schakel BPF-opties in het bestand in .config van uw eigen keuze (hoogstwaarschijnlijk CONFIG_BPF zal al ingeschakeld zijn omdat systemd het gebruikt). Hier is een lijst met opties uit de kernel die voor dit artikel wordt gebruikt:

CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_LSM=y
CONFIG_BPF_SYSCALL=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_IPV6_SEG6_BPF=y
# CONFIG_NETFILTER_XT_MATCH_BPF is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y

Dan kunnen we de modules en de kernel eenvoudig assembleren en installeren (je kunt de kernel trouwens assembleren met behulp van de nieuw geassembleerde clangdoor toe te voegen CC=clang):

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

en herstart met de nieuwe kernel (ik gebruik hiervoor kexec uit het pakket 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

Het meest gebruikte hulpprogramma in het artikel is het hulpprogramma bpftool, geleverd als onderdeel van de Linux-kernel. Het is geschreven en onderhouden door BPF-ontwikkelaars voor BPF-ontwikkelaars en kan worden gebruikt om alle soorten BPF-objecten te beheren: programma's laden, kaarten maken en bewerken, de levensduur van het BPF-ecosysteem verkennen, enz. Documentatie in de vorm van broncodes voor manpagina's is te vinden in de kern of, al samengesteld, Netwerk.

РќР ° РјРѕРјРµРЅС ‚РЅР ° РїРёСЃР ° РЅРёСЏ стР° тьи bpftool wordt alleen kant-en-klaar geleverd voor RHEL, Fedora en Ubuntu (zie bijvoorbeeld deze draad, dat het onvoltooide verhaal van verpakkingen vertelt bpftool bij Debian). Maar als je je kernel al hebt gebouwd, bouw dan bpftool zo eenvoudig als taart:

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

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

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

$

(Hier ${linux} - dit is je kernelmap.) Na het uitvoeren van deze opdrachten bpftool worden verzameld in een directory ${linux}/tools/bpf/bpftool en het kan aan het pad worden toegevoegd (in de eerste plaats aan de gebruiker root) of kopieer gewoon naar /usr/local/sbin.

Verzamelen bpftool het is het beste om de laatste te gebruiken clang, geassembleerd zoals hierboven beschreven, en controleer of het correct is geassembleerd, bijvoorbeeld met behulp van het commando

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

die zal laten zien welke BPF-functies in uw kernel zijn ingeschakeld.

Overigens kan het vorige commando worden uitgevoerd als

# bpftool f p k

Dit gebeurt naar analogie met de hulpprogramma's uit het pakket iproute2, waar we bijvoorbeeld kunnen zeggen ip a s eth0 in plaats van ip addr show dev eth0.

Conclusie

Met BPF kunt u een vlo beslaan om de functionaliteit van de kern effectief te meten en on-the-fly te veranderen. Het systeem bleek zeer succesvol, in de beste tradities van UNIX: een eenvoudig mechanisme waarmee je de kernel kunt (her)programmeren, waardoor een groot aantal mensen en organisaties kon experimenteren. En hoewel de experimenten, evenals de ontwikkeling van de BPF-infrastructuur zelf, nog lang niet zijn voltooid, beschikt het systeem al over een stabiele ABI waarmee u betrouwbare en vooral effectieve bedrijfslogica kunt bouwen.

Ik zou willen opmerken dat de technologie naar mijn mening zo populair is geworden omdat het enerzijds mogelijk is играть (de architectuur van een machine kan min of meer in één avond worden begrepen), en aan de andere kant om problemen op te lossen die niet (mooi) konden worden opgelost voordat deze verscheen. Deze twee componenten samen dwingen mensen om te experimenteren en te dromen, wat leidt tot de opkomst van steeds meer innovatieve oplossingen.

Dit artikel, hoewel niet bijzonder kort, is slechts een introductie in de wereld van BPF en beschrijft geen “geavanceerde” kenmerken en belangrijke delen van de architectuur. Het plan voor de toekomst is ongeveer als volgt: het volgende artikel zal een overzicht zijn van BPF-programmatypen (er worden 5.8 programmatypen ondersteund in de 30-kernel), daarna zullen we eindelijk bekijken hoe we echte BPF-toepassingen kunnen schrijven met behulp van kerneltracingprogramma's Als voorbeeld, dan is het tijd voor een meer diepgaande cursus over BPF-architectuur, gevolgd door voorbeelden van BPF-netwerk- en beveiligingstoepassingen.

Eerdere artikelen in deze serie

  1. BPF voor de kleintjes, deel nul: klassieke BPF

Koppelingen

  1. BPF- en XDP-referentiegids — documentatie over BPF van cilium, of beter gezegd van Daniel Borkman, een van de makers en beheerders van BPF. Dit is een van de eerste serieuze beschrijvingen, die verschilt van de andere doordat Daniel precies weet waar hij over schrijft en er geen fouten in staan. In het bijzonder beschrijft dit document hoe u kunt werken met BPF-programma's van het type XDP en TC met behulp van het bekende hulpprogramma ip uit het pakket iproute2.

  2. Documentatie/netwerken/filter.txt — origineel bestand met documentatie voor klassieke en vervolgens uitgebreide BPF. Een goed boek als je je wilt verdiepen in assembleertaal en technische architectonische details.

  3. Blog over BPF van Facebook. Het wordt zelden, maar treffend bijgewerkt, zoals Alexei Starovoitov (auteur van eBPF) en Andrii Nakryiko - (onderhouder) daar schrijven libbpf).

  4. Geheimen van bpftool. Een onderhoudende twitterthread van Quentin Monnet met voorbeelden en geheimen van het gebruik van bpftool.

  5. Duik in BPF: een lijst met leesmateriaal. Een gigantische (en nog steeds onderhouden) lijst met links naar BPF-documentatie van Quentin Monnet.

Bron: www.habr.com

Voeg een reactie