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.
Samenvatting van het artikel
Inleiding tot BPF-architectuur. Eerst bekijken we de BPF-architectuur in vogelvlucht en schetsen we de belangrijkste componenten.
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.
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):
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.
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:
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
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:
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:
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:
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:
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:
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:
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.
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.
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:
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):
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:
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
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:
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:
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:
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:
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".
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:
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:
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
$ 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:
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:
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:
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):
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?
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:
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:
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:
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:
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:
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[]:
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_opsbepaald door in bestand net/core/filter.c следующим обрахом:
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:
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:
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:
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_printk - dit 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!
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 следующим обрахом:
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:
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:
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
$ 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):
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:
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;
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:
(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":
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:
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):
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:
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
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:
(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.
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.
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.
Blog over BPF van Facebook. Het wordt zelden, maar treffend bijgewerkt, zoals Alexei Starovoitov (auteur van eBPF) en Andrii Nakryiko - (onderhouder) daar schrijven libbpf).
Geheimen van bpftool. Een onderhoudende twitterthread van Quentin Monnet met voorbeelden en geheimen van het gebruik van bpftool.