BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Στην αρχή υπήρχε μια τεχνολογία και λεγόταν BPF. Την κοιτάξαμε προηγούμενος, άρθρο της Παλαιάς Διαθήκης αυτής της σειράς. Το 2013, με τις προσπάθειες των Alexei Starovoitov και Daniel Borkman, αναπτύχθηκε και συμπεριλήφθηκε στον πυρήνα του Linux μια βελτιωμένη έκδοση, βελτιστοποιημένη για σύγχρονες μηχανές 64-bit. Αυτή η νέα τεχνολογία ονομάστηκε εν συντομία Internal BPF, στη συνέχεια μετονομάστηκε σε Extended BPF και τώρα, μετά από αρκετά χρόνια, όλοι την αποκαλούν απλά BPF.

Σε γενικές γραμμές, το BPF σάς επιτρέπει να εκτελείτε αυθαίρετο κώδικα που παρέχεται από τον χρήστη στον χώρο του πυρήνα του Linux και η νέα αρχιτεκτονική αποδείχθηκε τόσο επιτυχημένη που θα χρειαστούμε μια ντουζίνα ακόμα άρθρα για να περιγράψουμε όλες τις εφαρμογές της. (Το μόνο πράγμα που οι προγραμματιστές δεν έκαναν καλά, όπως μπορείτε να δείτε στον κώδικα απόδοσης παρακάτω, ήταν η δημιουργία ενός αξιοπρεπούς λογότυπου.)

Αυτό το άρθρο περιγράφει τη δομή της εικονικής μηχανής BPF, τις διεπαφές πυρήνα για εργασία με το BPF, τα εργαλεία ανάπτυξης, καθώς και μια σύντομη, πολύ σύντομη επισκόπηση των υπαρχουσών δυνατοτήτων, π.χ. όλα όσα θα χρειαστούμε στο μέλλον για μια βαθύτερη μελέτη των πρακτικών εφαρμογών του BPF.
BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Περίληψη του άρθρου

Εισαγωγή στην αρχιτεκτονική BPF. Αρχικά, θα πάρουμε μια πανοραμική άποψη της αρχιτεκτονικής BPF και θα περιγράψουμε τα κύρια στοιχεία.

Μητρώα και σύστημα εντολών της εικονικής μηχανής BPF. Έχοντας ήδη μια ιδέα της αρχιτεκτονικής στο σύνολό της, θα περιγράψουμε τη δομή της εικονικής μηχανής BPF.

Κύκλος ζωής αντικειμένων BPF, σύστημα αρχείων bpffs. Σε αυτή την ενότητα, θα ρίξουμε μια πιο προσεκτική ματιά στον κύκλο ζωής των αντικειμένων BPF - προγραμμάτων και χαρτών.

Διαχείριση αντικειμένων χρησιμοποιώντας την κλήση συστήματος bpf. Έχοντας κάποια κατανόηση του συστήματος ήδη σε εφαρμογή, θα εξετάσουμε επιτέλους πώς να δημιουργήσουμε και να χειριστούμε αντικείμενα από το χώρο του χρήστη χρησιμοποιώντας μια ειδική κλήση συστήματος − bpf(2).

Пишем программы BPF с помощью libbpf. Φυσικά, μπορείτε να γράψετε προγράμματα χρησιμοποιώντας μια κλήση συστήματος. Αλλά είναι δύσκολο. Για ένα πιο ρεαλιστικό σενάριο, οι πυρηνικοί προγραμματιστές ανέπτυξαν μια βιβλιοθήκη libbpf. Θα δημιουργήσουμε έναν βασικό σκελετό εφαρμογής BPF που θα χρησιμοποιήσουμε σε επόμενα παραδείγματα.

Βοηθοί πυρήνα. Εδώ θα μάθουμε πώς τα προγράμματα BPF μπορούν να έχουν πρόσβαση στις βοηθητικές λειτουργίες του πυρήνα - ένα εργαλείο που, μαζί με τους χάρτες, επεκτείνει θεμελιωδώς τις δυνατότητες του νέου BPF σε σύγκριση με το κλασικό.

Πρόσβαση σε χάρτες από προγράμματα BPF. Σε αυτό το σημείο, θα γνωρίζουμε αρκετά για να καταλάβουμε ακριβώς πώς μπορούμε να δημιουργήσουμε προγράμματα που χρησιμοποιούν χάρτες. Και ας ρίξουμε μια γρήγορη ματιά στον μεγάλο και πανίσχυρο επαληθευτή.

Εργαλεία ανάπτυξης. Ενότητα βοήθειας σχετικά με τον τρόπο συναρμολόγησης των απαιτούμενων βοηθητικών προγραμμάτων και του πυρήνα για πειράματα.

Συμπέρασμα. Στο τέλος του άρθρου, όσοι διαβάσουν ως εδώ, θα βρουν λόγια παρακίνησης και μια σύντομη περιγραφή του τι θα συμβεί στα επόμενα άρθρα. Θα παραθέσουμε επίσης μια σειρά από συνδέσμους για αυτοδιδασκαλία για όσους δεν έχουν την επιθυμία ή τη δυνατότητα να περιμένουν τη συνέχεια.

Εισαγωγή στην Αρχιτεκτονική BPF

Πριν αρχίσουμε να εξετάζουμε την αρχιτεκτονική BPF, θα αναφερθούμε για μια τελευταία φορά (ω). κλασικό BPF, το οποίο αναπτύχθηκε ως απάντηση στην εμφάνιση των μηχανών RISC και έλυσε το πρόβλημα του αποτελεσματικού φιλτραρίσματος πακέτων. Η αρχιτεκτονική αποδείχθηκε τόσο επιτυχημένη που, έχοντας γεννηθεί τη ραγδαία δεκαετία του 'XNUMX στο Berkeley UNIX, μεταφέρθηκε στα περισσότερα υπάρχοντα λειτουργικά συστήματα, επέζησε στη δεκαετία του 'XNUMX και εξακολουθεί να βρίσκει νέες εφαρμογές.

Το νέο BPF αναπτύχθηκε ως απάντηση στην πανταχού παρουσία των μηχανών 64-bit, των υπηρεσιών cloud και της αυξημένης ανάγκης για εργαλεία για τη δημιουργία SDN (Soffware-dφινιρισμένος networking). Αναπτύχθηκε από μηχανικούς δικτύων πυρήνα ως βελτιωμένη αντικατάσταση του κλασικού BPF, το νέο BPF κυριολεκτικά έξι μήνες αργότερα βρήκε εφαρμογές στο δύσκολο έργο της ανίχνευσης συστημάτων Linux και τώρα, έξι χρόνια μετά την εμφάνισή του, θα χρειαστούμε ένα ολόκληρο επόμενο άρθρο για να απαριθμήστε τους διαφορετικούς τύπους προγραμμάτων.

ΑΣΤΕΙΕΣ ΕΙΚΟΝΕΣ

Στον πυρήνα του, το BPF είναι μια εικονική μηχανή sandbox που σας επιτρέπει να εκτελείτε «αυθαίρετο» κώδικα στο χώρο του πυρήνα χωρίς να διακυβεύεται η ασφάλεια. Τα προγράμματα BPF δημιουργούνται στο χώρο χρήστη, φορτώνονται στον πυρήνα και συνδέονται με κάποια πηγή συμβάντων. Ένα συμβάν θα μπορούσε να είναι, για παράδειγμα, η παράδοση ενός πακέτου σε μια διεπαφή δικτύου, η εκκίνηση κάποιας λειτουργίας πυρήνα κ.λπ. Στην περίπτωση ενός πακέτου, το πρόγραμμα BPF θα έχει πρόσβαση στα δεδομένα και τα μεταδεδομένα του πακέτου (για ανάγνωση και, πιθανώς, εγγραφή, ανάλογα με τον τύπο του προγράμματος)· στην περίπτωση εκτέλεσης μιας συνάρτησης πυρήνα, τα ορίσματα του τη συνάρτηση, συμπεριλαμβανομένων των δεικτών στη μνήμη του πυρήνα, κ.λπ.

Ας ρίξουμε μια πιο προσεκτική ματιά σε αυτή τη διαδικασία. Αρχικά, ας μιλήσουμε για την πρώτη διαφορά από το κλασικό BPF, τα προγράμματα για τα οποία γράφτηκαν σε assembler. Στη νέα έκδοση, η αρχιτεκτονική επεκτάθηκε έτσι ώστε τα προγράμματα να μπορούν να γράφονται σε γλώσσες υψηλού επιπέδου, κυρίως, φυσικά, σε C. Για αυτό, αναπτύχθηκε ένα backend για το llvm, το οποίο επιτρέπει τη δημιουργία bytecode για την αρχιτεκτονική BPF.

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Η αρχιτεκτονική BPF σχεδιάστηκε, εν μέρει, για να λειτουργεί αποτελεσματικά σε σύγχρονα μηχανήματα. Για να λειτουργήσει αυτό στην πράξη, ο bytecode BPF, μόλις φορτωθεί στον πυρήνα, μεταφράζεται σε εγγενή κώδικα χρησιμοποιώντας ένα στοιχείο που ονομάζεται μεταγλωττιστής JIT (Jανώτερος In Tώρα). Στη συνέχεια, αν θυμάστε, στο κλασικό BPF το πρόγραμμα φορτώθηκε στον πυρήνα και προσαρτήθηκε στην πηγή συμβάντος ατομικά - στο πλαίσιο μιας μεμονωμένης κλήσης συστήματος. Στη νέα αρχιτεκτονική, αυτό συμβαίνει σε δύο στάδια - πρώτον, ο κώδικας φορτώνεται στον πυρήνα χρησιμοποιώντας μια κλήση συστήματος bpf(2), а затем, позднее, при помощи других механизмов, разных в зависимости от типа программы, программа подсоединяется (attaches) к источнику событий.

Εδώ ο αναγνώστης μπορεί να έχει μια ερώτηση: ήταν δυνατόν; Πώς είναι εγγυημένη η ασφάλεια εκτέλεσης τέτοιου κώδικα; Η ασφάλεια εκτέλεσης μας είναι εγγυημένη από το στάδιο φόρτωσης των προγραμμάτων BPF που ονομάζεται verfier (στα αγγλικά αυτό το στάδιο ονομάζεται verfier και θα συνεχίσω να χρησιμοποιώ την αγγλική λέξη):

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Ο Verifier είναι ένας στατικός αναλυτής που διασφαλίζει ότι ένα πρόγραμμα δεν διακόπτει την κανονική λειτουργία του πυρήνα. Αυτό, παρεμπιπτόντως, δεν σημαίνει ότι το πρόγραμμα δεν μπορεί να παρεμβαίνει στη λειτουργία του συστήματος - τα προγράμματα BPF, ανάλογα με τον τύπο, μπορούν να διαβάσουν και να ξαναγράψουν τμήματα της μνήμης του πυρήνα, να επιστρέψουν τιμές συναρτήσεων, περικοπή, προσθήκη, επανεγγραφή και ακόμη και προώθηση πακέτων δικτύου. Ο Verifier εγγυάται ότι η εκτέλεση ενός προγράμματος BPF δεν θα διακόψει τη λειτουργία του πυρήνα και ότι ένα πρόγραμμα που, σύμφωνα με τους κανόνες, έχει πρόσβαση εγγραφής, για παράδειγμα, τα δεδομένα ενός εξερχόμενου πακέτου, δεν θα μπορεί να αντικαταστήσει τη μνήμη του πυρήνα εκτός του πακέτου. Θα εξετάσουμε τον επαληθευτή λίγο πιο αναλυτικά στην αντίστοιχη ενότητα, αφού εξοικειωθούμε με όλα τα άλλα συστατικά του BPF.

Τι έχουμε μάθει λοιπόν μέχρι τώρα; Ο χρήστης γράφει ένα πρόγραμμα σε C, το φορτώνει στον πυρήνα χρησιμοποιώντας μια κλήση συστήματος bpf(2), όπου ελέγχεται από έναν επαληθευτή και μεταφράζεται σε εγγενή bytecode. Στη συνέχεια, ο ίδιος ή άλλος χρήστης συνδέει το πρόγραμμα με την πηγή συμβάντος και αρχίζει να εκτελείται. Ο διαχωρισμός της εκκίνησης και της σύνδεσης είναι απαραίτητος για διάφορους λόγους. Πρώτον, η εκτέλεση ενός επαληθευτή είναι σχετικά ακριβή και με τη λήψη του ίδιου προγράμματος πολλές φορές χάνουμε χρόνο στον υπολογιστή. Δεύτερον, το πώς ακριβώς συνδέεται ένα πρόγραμμα εξαρτάται από τον τύπο του και μια «καθολική» διεπαφή που αναπτύχθηκε πριν από ένα χρόνο μπορεί να μην είναι κατάλληλη για νέους τύπους προγραμμάτων. (Αν και τώρα που η αρχιτεκτονική γίνεται πιο ώριμη, υπάρχει μια ιδέα να ενοποιηθεί αυτή η διεπαφή σε επίπεδο libbpf.)

Ο προσεκτικός αναγνώστης μπορεί να παρατηρήσει ότι δεν έχουμε τελειώσει ακόμα με τις εικόνες. Πράγματι, όλα τα παραπάνω δεν εξηγούν γιατί το BPF αλλάζει ριζικά την εικόνα σε σύγκριση με το κλασικό BPF. Δύο καινοτομίες που διευρύνουν σημαντικά το πεδίο εφαρμογής είναι η δυνατότητα χρήσης κοινόχρηστης μνήμης και βοηθητικών λειτουργιών πυρήνα. Στο BPF, η κοινή μνήμη υλοποιείται χρησιμοποιώντας τους λεγόμενους χάρτες - κοινόχρηστες δομές δεδομένων με ένα συγκεκριμένο API. Πιθανότατα πήραν αυτό το όνομα επειδή ο πρώτος τύπος χάρτη που εμφανίστηκε ήταν ένας πίνακας κατακερματισμού. Στη συνέχεια εμφανίστηκαν πίνακες, πίνακες κατακερματισμού τοπικών (ανά CPU) και τοπικοί πίνακες, δέντρα αναζήτησης, χάρτες που περιέχουν δείκτες σε προγράμματα BPF και πολλά άλλα. Αυτό που μας ενδιαφέρει τώρα είναι ότι τα προγράμματα BPF έχουν πλέον τη δυνατότητα να διατηρούν την κατάσταση μεταξύ των κλήσεων και να την μοιράζονται με άλλα προγράμματα και με το χώρο χρήστη.

Η πρόσβαση στους Χάρτες γίνεται από διεργασίες χρήστη χρησιμοποιώντας μια κλήση συστήματος bpf(2), а из программ BPF, работающих в ядре — при помощи функций-помощников. Более того, helpers существуют не только для работы с мапами, но и для доступа к другим возможностям ядра. Например, программы BPF могут использовать функции-помощники для перенаправления пакетов на другие интерфейсы, для генерации событий подсистемы perf, доступа к структурам ядра и т.п.

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Συνοπτικά, το BPF παρέχει τη δυνατότητα φόρτωσης αυθαίρετου, δηλαδή, ελεγμένου από επαληθευτή, κώδικα χρήστη στο χώρο του πυρήνα. Αυτός ο κώδικας μπορεί να αποθηκεύει κατάσταση μεταξύ των κλήσεων και να ανταλλάσσει δεδομένα με το χώρο χρήστη, ενώ έχει επίσης πρόσβαση σε υποσυστήματα πυρήνα που επιτρέπεται από αυτόν τον τύπο προγράμματος.

Αυτό είναι ήδη παρόμοιο με τις δυνατότητες που παρέχουν οι μονάδες πυρήνα, σε σύγκριση με τις οποίες το BPF έχει ορισμένα πλεονεκτήματα (φυσικά, μπορείτε να συγκρίνετε μόνο παρόμοιες εφαρμογές, για παράδειγμα, ανίχνευση συστήματος - δεν μπορείτε να γράψετε ένα αυθαίρετο πρόγραμμα οδήγησης με το BPF). Μπορείτε να σημειώσετε χαμηλότερο όριο εισόδου (ορισμένα βοηθητικά προγράμματα που χρησιμοποιούν BPF δεν απαιτούν από τον χρήστη να έχει δεξιότητες προγραμματισμού πυρήνα ή δεξιότητες προγραμματισμού γενικά), ασφάλεια χρόνου εκτέλεσης (σηκώστε το χέρι σας στα σχόλια για όσους δεν έσπασαν το σύστημα κατά τη σύνταξη ή δοκιμαστικές μονάδες), ατομικότητα - υπάρχει χρόνος διακοπής κατά την επαναφόρτωση μονάδων και το υποσύστημα BPF διασφαλίζει ότι δεν χάνονται συμβάντα (για να είμαστε δίκαιοι, αυτό δεν ισχύει για όλους τους τύπους προγραμμάτων BPF).

Η παρουσία τέτοιων δυνατοτήτων καθιστά το BPF ένα καθολικό εργαλείο για την επέκταση του πυρήνα, κάτι που επιβεβαιώνεται στην πράξη: όλο και περισσότεροι νέοι τύποι προγραμμάτων προστίθενται στο BPF, όλο και περισσότερες μεγάλες εταιρείες χρησιμοποιούν το BPF σε διακομιστές μάχης 24×7, όλο και περισσότερα οι νεοφυείς επιχειρήσεις χτίζουν την επιχείρησή τους σε λύσεις που βασίζονται στο BPF. Το BPF χρησιμοποιείται παντού: για προστασία από επιθέσεις DDoS, δημιουργία SDN (για παράδειγμα, υλοποίηση δικτύων για kubernetes), ως το κύριο εργαλείο ανίχνευσης συστήματος και συλλέκτης στατιστικών στοιχείων, σε συστήματα ανίχνευσης εισβολής και συστήματα sandbox κ.λπ.

Ας ολοκληρώσουμε το επισκόπηση του άρθρου εδώ και ας δούμε την εικονική μηχανή και το οικοσύστημα BPF με περισσότερες λεπτομέρειες.

Παρέκβαση: βοηθητικά προγράμματα

Για να μπορέσετε να εκτελέσετε τα παραδείγματα στις ακόλουθες ενότητες, ίσως χρειαστείτε μια σειρά από βοηθητικά προγράμματα, τουλάχιστον llvm/clang με υποστήριξη bpf και bpftool. Στο τμήμα Εργαλεία ανάπτυξης Μπορείτε να διαβάσετε τις οδηγίες για τη συναρμολόγηση των βοηθητικών προγραμμάτων, καθώς και τον πυρήνα σας. Αυτή η ενότητα τοποθετείται παρακάτω για να μην διαταραχθεί η αρμονία της παρουσίασής μας.

Μητρώα και σύστημα οδηγιών εικονικής μηχανής BPF

Η αρχιτεκτονική και το σύστημα εντολών του BPF αναπτύχθηκαν λαμβάνοντας υπόψη το γεγονός ότι τα προγράμματα θα γραφτούν στη γλώσσα C και, μετά τη φόρτωση στον πυρήνα, θα μεταφραστούν σε εγγενή κώδικα. Επομένως, ο αριθμός των καταχωρητών και το σύνολο των εντολών επιλέχθηκαν λαμβάνοντας υπόψη τη διασταύρωση, με τη μαθηματική έννοια, των δυνατοτήτων των σύγχρονων μηχανών. Επιπλέον, επιβλήθηκαν διάφοροι περιορισμοί σε προγράμματα, για παράδειγμα, μέχρι πρόσφατα δεν ήταν δυνατή η εγγραφή βρόχων και υπορουτίνων και ο αριθμός των εντολών περιορίστηκε σε 4096 (τώρα τα προνομιακά προγράμματα μπορούν να φορτώσουν έως και ένα εκατομμύριο οδηγίες).

Το BPF διαθέτει έντεκα καταχωρητές 64-bit προσβάσιμους από το χρήστη r0-r10 και έναν μετρητή προγράμματος. Κανω ΕΓΓΡΑΦΗ r10 περιέχει δείκτη πλαισίου και είναι μόνο για ανάγνωση. Τα προγράμματα έχουν πρόσβαση σε μια στοίβα 512 byte κατά το χρόνο εκτέλεσης και απεριόριστη ποσότητα κοινόχρηστης μνήμης με τη μορφή χαρτών.

Τα προγράμματα BPF επιτρέπεται να εκτελούν ένα συγκεκριμένο σύνολο βοηθημάτων πυρήνα τύπου προγράμματος και, πιο πρόσφατα, κανονικές λειτουργίες. Κάθε καλούμενη συνάρτηση μπορεί να λάβει έως και πέντε ορίσματα, μεταβιβασμένα σε καταχωρητές r1-r5, а возвращаемое значение передается в r0. Είναι εγγυημένο ότι μετά την επιστροφή από τη λειτουργία, τα περιεχόμενα των μητρώων r6-r9 Δεν θα αλλάξει.

Για αποτελεσματική μετάφραση προγράμματος, εγγραφείτε r0-r11 για όλες τις υποστηριζόμενες αρχιτεκτονικές αντιστοιχίζονται μοναδικά σε πραγματικούς καταχωρητές, λαμβάνοντας υπόψη τα χαρακτηριστικά ABI της τρέχουσας αρχιτεκτονικής. Για παράδειγμα, για x86_64 μητρώα r1-r5, που χρησιμοποιείται για τη μετάδοση παραμέτρων συνάρτησης, εμφανίζονται στο rdi, rsi, rdx, rcx, r8, τα οποία χρησιμοποιούνται για τη μεταβίβαση παραμέτρων σε συναρτήσεις που είναι ενεργοποιημένες x86_64. Για παράδειγμα, ο κώδικας στα αριστερά μεταφράζεται στον κώδικα στα δεξιά ως εξής:

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

Κανω ΕΓΓΡΑΦΗ r0 χρησιμοποιείται επίσης για την επιστροφή του αποτελέσματος της εκτέλεσης του προγράμματος και στον καταχωρητή r1 το πρόγραμμα μεταβιβάζεται ένας δείκτης στο περιβάλλον - ανάλογα με τον τύπο του προγράμματος, αυτό θα μπορούσε να είναι, για παράδειγμα, μια δομή struct xdp_md (для XDP) или структура struct __sk_buff (για διαφορετικά προγράμματα δικτύου) ή δομή struct pt_regs (για διαφορετικούς τύπους προγραμμάτων εντοπισμού) κ.λπ.

Έτσι, είχαμε ένα σύνολο από καταχωρητές, βοηθούς πυρήνα, μια στοίβα, έναν δείκτη περιβάλλοντος και κοινή μνήμη με τη μορφή χαρτών. Όχι ότι όλα αυτά είναι απολύτως απαραίτητα στο ταξίδι, αλλά...

Ας συνεχίσουμε την περιγραφή και ας μιλήσουμε για το σύστημα εντολών για την εργασία με αυτά τα αντικείμενα. Ολα (Σχεδόν όλοι) Οι οδηγίες BPF έχουν σταθερό μέγεθος 64 bit. Αν κοιτάξετε μια οδηγία σε μια μηχανή Big Endian 64-bit, θα δείτε

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Εδώ Code - αυτή είναι η κωδικοποίηση της εντολής, Dst/Src είναι οι κωδικοποιήσεις του δέκτη και της πηγής, αντίστοιχα, Off - Υπογεγραμμένη εσοχή 16 bit και Imm είναι ένας ακέραιος αριθμός 32-bit που χρησιμοποιείται σε ορισμένες εντολές (παρόμοιος με τη σταθερά K cBPF). Κωδικοποίηση Code έχει έναν από τους δύο τύπους:

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Οι κλάσεις εντολών 0, 1, 2, 3 ορίζουν εντολές για εργασία με μνήμη. Αυτοί λέγονται, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, αντίστοιχα. Τάξεις 4, 7 (BPF_ALU, BPF_ALU64) αποτελούν ένα σύνολο οδηγιών ALU. Τάξεις 5, 6 (BPF_JMP, BPF_JMP32) περιέχει οδηγίες μετάβασης.

Το περαιτέρω σχέδιο για τη μελέτη του συστήματος διδασκαλίας BPF έχει ως εξής: αντί να απαριθμήσουμε σχολαστικά όλες τις οδηγίες και τις παραμέτρους τους, θα δούμε μερικά παραδείγματα σε αυτήν την ενότητα και από αυτά θα καταστεί σαφές πώς λειτουργούν πραγματικά οι οδηγίες και πώς να αποσυναρμολογήστε με μη αυτόματο τρόπο οποιοδήποτε δυαδικό αρχείο για BPF. Για να ενοποιήσουμε το υλικό αργότερα στο άρθρο, θα συναντήσουμε επίσης μεμονωμένες οδηγίες στις ενότητες σχετικά με το Verifier, τον μεταγλωττιστή JIT, τη μετάφραση του κλασικού BPF, καθώς και κατά τη μελέτη χαρτών, την κλήση συναρτήσεων κ.λπ.

Когда мы будем говорить об индивидуальных инструкциях, мы будем ссылаться на файлы ядра bpf.h и bpf_common.h, που ορίζουν τους αριθμητικούς κωδικούς των εντολών BPF. Όταν μελετάτε την αρχιτεκτονική μόνοι σας ή/και αναλύετε δυαδικά αρχεία, μπορείτε να βρείτε τη σημασιολογία στις ακόλουθες πηγές, ταξινομημένες κατά σειρά πολυπλοκότητας: Ανεπίσημη προδιαγραφή eBPF, Οδηγός αναφοράς BPF και XDP, σετ οδηγιών, Τεκμηρίωση/δικτύωση/φίλτρο.txt και, φυσικά, στον πηγαίο κώδικα Linux - επαληθευτής, JIT, διερμηνέας BPF.

Παράδειγμα: αποσυναρμολόγηση του BPF στο κεφάλι σας

Ας δούμε ένα παράδειγμα στο οποίο μεταγλωττίζουμε ένα πρόγραμμα readelf-example.c και κοιτάξτε το δυαδικό που προκύπτει. Θα αποκαλύψουμε το αρχικό περιεχόμενο readelf-example.c παρακάτω, αφού επαναφέρουμε τη λογική του από δυαδικούς κώδικες:

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

Πρώτη στήλη στην έξοδο readelf είναι μια εσοχή και έτσι το πρόγραμμά μας αποτελείται από τέσσερις εντολές:

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

Οι κωδικοί εντολών είναι ίσοι b7, 15, b7 и 95. Θυμηθείτε ότι τα λιγότερο σημαντικά τρία bit είναι η κλάση εντολών. Στην περίπτωσή μας, το τέταρτο bit όλων των εντολών είναι κενό, επομένως οι κλάσεις εντολών είναι 7, 5, 7, 5, αντίστοιχα. Η κλάση 7 είναι BPF_ALU64και το 5 είναι BPF_JMP. Και για τις δύο κατηγορίες, η μορφή εντολών είναι η ίδια (δείτε παραπάνω) και μπορούμε να ξαναγράψουμε το πρόγραμμά μας έτσι (ταυτόχρονα θα ξαναγράψουμε τις υπόλοιπες στήλες σε ανθρώπινη μορφή):

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

Λειτουργία b κατηγορία ALU64 - Είναι BPF_MOV. Εκχωρεί μια τιμή στον καταχωρητή προορισμού. Εάν το bit έχει ρυθμιστεί s (πηγή), τότε η τιμή λαμβάνεται από τον καταχωρητή πηγής και εάν, όπως στην περίπτωσή μας, δεν έχει οριστεί, τότε η τιμή λαμβάνεται από το πεδίο Imm. Έτσι στην πρώτη και τρίτη οδηγία εκτελούμε την επέμβαση r0 = Imm. Επιπλέον, JMP class 1 λειτουργία είναι BPF_JEQ (άλμα αν είναι ίσο). Στην περίπτωσή μας, από το λίγο S είναι μηδέν, συγκρίνει την τιμή του καταχωρητή πηγής με το πεδίο Imm. Εάν οι τιμές συμπίπτουν, τότε γίνεται η μετάβαση σε PC + OffΌπου PC, ως συνήθως, περιέχει τη διεύθυνση της επόμενης εντολής. Τέλος, JMP Class 9 Operation είναι BPF_EXIT. Αυτή η οδηγία τερματίζει το πρόγραμμα, επιστρέφοντας στον πυρήνα r0. Ας προσθέσουμε μια νέα στήλη στον πίνακά μας:

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

Μπορούμε να το ξαναγράψουμε σε μια πιο βολική μορφή:

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

Αν θυμηθούμε τι υπάρχει στο μητρώο r1 το πρόγραμμα περνάει έναν δείκτη στο περιβάλλον από τον πυρήνα και στον καταχωρητή r0 в ядро возвращается значение, то мы можем увидеть, что если указатель на контекст равен нулю, то мы возвращаем 1, а в противном случае — 2. Проверим, что мы правы, посмотрев на исходник:

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

Ναι, είναι ένα πρόγραμμα χωρίς νόημα, αλλά μεταφράζεται σε τέσσερις απλές οδηγίες.

Παράδειγμα εξαίρεσης: Οδηγίες 16 byte

Αναφέραμε νωρίτερα ότι ορισμένες οδηγίες καταλαμβάνουν περισσότερα από 64 bit. Αυτό ισχύει, για παράδειγμα, για οδηγίες lddw (Κωδικός = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — φορτώστε μια διπλή λέξη από τα πεδία στον καταχωρητή Imm. Το γεγονός είναι ότι Imm έχει μέγεθος 32 και μια διπλή λέξη είναι 64 bit, επομένως η φόρτωση μιας άμεσης τιμής 64 bit σε έναν καταχωρητή σε μία εντολή 64 bit δεν θα λειτουργήσει. Για να γίνει αυτό, χρησιμοποιούνται δύο γειτονικές οδηγίες για την αποθήκευση του δεύτερου μέρους της τιμής των 64 bit στο πεδίο Imm. Ένα παράδειγμα:

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

Υπάρχουν μόνο δύο οδηγίες σε ένα δυαδικό πρόγραμμα:

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

Θα ξανασυναντηθούμε με οδηγίες lddw, όταν μιλάμε για μετακομίσεις και εργασία με χάρτες.

Παράδειγμα: αποσυναρμολόγηση BPF με χρήση τυπικών εργαλείων

Итак, мы научились читать бинарные коды BPF и готовы разобрать любую инструкцию, если потребуется. Однако, стоит сказать, что на практике удобнее и быстрее дизассемблировать программы при помощи стандартных средств, например:

$ 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

Κύκλος ζωής αντικειμένων BPF, σύστημα αρχείων bpffs

(Πρώτα έμαθα μερικές από τις λεπτομέρειες που περιγράφονται σε αυτήν την υποενότητα από Θέση Ο Αλεξέι Σταροβοΐτοφ μέσα BPF Blog.)

Τα αντικείμενα BPF - προγράμματα και χάρτες - δημιουργούνται από το χώρο χρήστη χρησιμοποιώντας εντολές BPF_PROG_LOAD и BPF_MAP_CREATE κλήση συστήματος bpf(2), θα μιλήσουμε για το πώς ακριβώς συμβαίνει αυτό στην επόμενη ενότητα. Αυτό δημιουργεί δομές δεδομένων πυρήνα και για καθεμία από αυτές refcount (αριθμός αναφοράς) ορίζεται σε ένα και ένας περιγραφέας αρχείου που δείχνει το αντικείμενο επιστρέφεται στο χρήστη. Αφού κλείσει η λαβή refcount το αντικείμενο μειώνεται κατά ένα και όταν φτάσει στο μηδέν, το αντικείμενο καταστρέφεται.

Εάν το πρόγραμμα χρησιμοποιεί χάρτες, τότε refcount αυτοί οι χάρτες αυξάνονται κατά ένα μετά τη φόρτωση του προγράμματος, δηλ. οι περιγραφείς των αρχείων τους μπορούν να κλείσουν από τη διαδικασία χρήστη και ακόμα refcount δεν θα γίνει μηδέν:

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Μετά την επιτυχή φόρτωση ενός προγράμματος, συνήθως το επισυνάπτουμε σε κάποιο είδος δημιουργίας συμβάντων. Για παράδειγμα, μπορούμε να το τοποθετήσουμε σε μια διεπαφή δικτύου για να επεξεργαστούμε τα εισερχόμενα πακέτα ή να το συνδέσουμε με κάποια tracepoint στον πυρήνα. Σε αυτό το σημείο, ο μετρητής αναφοράς θα αυξηθεί επίσης κατά ένα και θα μπορέσουμε να κλείσουμε τον περιγραφέα αρχείου στο πρόγραμμα φόρτωσης.

Τι θα συμβεί αν τώρα τερματίσουμε τη λειτουργία του bootloader; Εξαρτάται από τον τύπο της γεννήτριας συμβάντων (άγκιστρο). Όλα τα άγκιστρα δικτύου θα υπάρχουν μετά την ολοκλήρωση του φορτωτή, αυτά είναι τα λεγόμενα καθολικά άγκιστρα. Και, για παράδειγμα, τα προγράμματα ανίχνευσης θα κυκλοφορήσουν μετά τον τερματισμό της διαδικασίας που τα δημιούργησε (και επομένως ονομάζονται τοπικά, από "τοπικό στη διαδικασία"). Τεχνικά, τα τοπικά άγκιστρα έχουν πάντα έναν αντίστοιχο περιγραφέα αρχείου στο χώρο χρήστη και επομένως κλείνουν όταν η διαδικασία είναι κλειστή, αλλά τα καθολικά άγκιστρα δεν έχουν. Στο παρακάτω σχήμα, χρησιμοποιώντας κόκκινους σταυρούς, προσπαθώ να δείξω πώς ο τερματισμός του προγράμματος φόρτωσης επηρεάζει τη διάρκεια ζωής των αντικειμένων στην περίπτωση τοπικών και καθολικών αγκίστρων.

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Γιατί υπάρχει διάκριση μεταξύ τοπικών και παγκόσμιων αγκίστρων; Η εκτέλεση ορισμένων τύπων προγραμμάτων δικτύου έχει νόημα χωρίς χώρο χρήστη, για παράδειγμα, φανταστείτε την προστασία DDoS - ο bootloader γράφει τους κανόνες και συνδέει το πρόγραμμα BPF στη διεπαφή δικτύου, μετά από το οποίο ο bootloader μπορεί να πάει και να αυτοκτονήσει. Από την άλλη πλευρά, φανταστείτε ένα πρόγραμμα εντοπισμού σφαλμάτων που γράψατε στα γόνατά σας σε δέκα λεπτά - όταν τελειώσει, θα θέλατε να μην υπάρχουν σκουπίδια στο σύστημα και τα τοπικά άγκιστρα θα το εξασφαλίσουν.

Από την άλλη πλευρά, φανταστείτε ότι θέλετε να συνδεθείτε σε ένα σημείο εντοπισμού στον πυρήνα και να συλλέξετε στατιστικά στοιχεία για πολλά χρόνια. Σε αυτήν την περίπτωση, θα θέλατε να ολοκληρώσετε το τμήμα χρήστη και να επιστρέφετε στα στατιστικά από καιρό σε καιρό. Το σύστημα αρχείων bpf παρέχει αυτήν την ευκαιρία. Είναι ένα σύστημα ψευδο-αρχείων μόνο στη μνήμη που επιτρέπει τη δημιουργία αρχείων που αναφέρονται σε αντικείμενα BPF και ως εκ τούτου αυξάνουν refcount αντικείμενα. Μετά από αυτό, ο φορτωτής μπορεί να βγει και τα αντικείμενα που δημιούργησε θα παραμείνουν ζωντανά.

BPF για τα μικρά, μέρος πρώτο: εκτεταμένο BPF

Η δημιουργία αρχείων σε bpffs που αναφέρονται σε αντικείμενα BPF ονομάζεται "καρφίτσωμα" (όπως στην ακόλουθη φράση: "η διαδικασία μπορεί να καρφιτσώσει ένα πρόγραμμα ή έναν χάρτη BPF"). Η δημιουργία αντικειμένων αρχείων για αντικείμενα BPF έχει νόημα όχι μόνο για την παράταση της διάρκειας ζωής των τοπικών αντικειμένων, αλλά και για τη χρηστικότητα των καθολικών αντικειμένων - επιστρέφοντας στο παράδειγμα με το παγκόσμιο πρόγραμμα προστασίας DDoS, θέλουμε να μπορούμε να έρθουμε και να δούμε στατιστικά πότε-πότε.

Το σύστημα αρχείων BPF είναι συνήθως προσαρτημένο /sys/fs/bpf, αλλά μπορεί επίσης να τοποθετηθεί τοπικά, για παράδειγμα, ως εξής:

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

Τα ονόματα των συστημάτων αρχείων δημιουργούνται χρησιμοποιώντας την εντολή BPF_OBJ_PIN Κλήση συστήματος BPF. Για παράδειγμα, ας πάρουμε ένα πρόγραμμα, ας το μεταγλωττίσουμε, το ανεβάσουμε και ας το καρφιτσώσουμε bpffs. Το πρόγραμμά μας δεν κάνει τίποτα χρήσιμο, παρουσιάζουμε μόνο τον κώδικα για να μπορείτε να αναπαράγετε το παράδειγμα:

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

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

Ας μεταγλωττίσουμε αυτό το πρόγραμμα και ας δημιουργήσουμε ένα τοπικό αντίγραφο του συστήματος αρχείων bpffs:

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

Τώρα ας κατεβάσουμε το πρόγραμμά μας χρησιμοποιώντας το βοηθητικό πρόγραμμα bpftool και δείτε τις συνοδευτικές κλήσεις συστήματος bpf(2) (ορισμένες άσχετες γραμμές αφαιρέθηκαν από την έξοδο γραμμών):

$ 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

Εδώ έχουμε φορτώσει το πρόγραμμα χρησιμοποιώντας BPF_PROG_LOAD, έλαβε μια περιγραφή αρχείου από τον πυρήνα 3 και χρησιμοποιώντας την εντολή BPF_OBJ_PIN καρφιτσώθηκε αυτό το περιγραφικό αρχείου ως αρχείο "bpf-mountpoint/test". Μετά από αυτό το πρόγραμμα bootloader bpftool τελείωσε να λειτουργεί, αλλά το πρόγραμμά μας παρέμεινε στον πυρήνα, αν και δεν το επισυνάψαμε σε καμία διεπαφή δικτύου:

$ 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

Μπορούμε να διαγράψουμε το αντικείμενο αρχείου κανονικά unlink(2) και μετά θα διαγραφεί το αντίστοιχο πρόγραμμα:

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

Διαγραφή αντικειμένων

Μιλώντας για τη διαγραφή αντικειμένων, είναι απαραίτητο να διευκρινιστεί ότι αφού αποσυνδέσουμε το πρόγραμμα από το άγκιστρο (γεννήτρια συμβάντων), κανένα νέο συμβάν δεν θα ενεργοποιήσει την εκκίνησή του, ωστόσο, όλες οι τρέχουσες παρουσίες του προγράμματος θα ολοκληρωθούν με την κανονική σειρά .

Ορισμένοι τύποι προγραμμάτων BPF σας επιτρέπουν να αντικαταστήσετε το πρόγραμμα εν κινήσει, π.χ. παρέχουν ατομικότητα αλληλουχίας replace = detach old program, attach new program. Σε αυτήν την περίπτωση, όλα τα ενεργά στιγμιότυπα της παλιάς έκδοσης του προγράμματος θα ολοκληρώσουν την εργασία τους και θα δημιουργηθούν νέοι χειριστές συμβάντων από το νέο πρόγραμμα και η "ατομικότητα" εδώ σημαίνει ότι δεν θα χαθεί ούτε ένα συμβάν.

Επισύναψη προγραμμάτων σε πηγές συμβάντων

Σε αυτό το άρθρο, δεν θα περιγράψουμε χωριστά τη σύνδεση προγραμμάτων με πηγές συμβάντων, καθώς είναι λογικό να το μελετήσουμε στο πλαίσιο ενός συγκεκριμένου τύπου προγράμματος. Εκ. παράδειγμα παρακάτω, στο οποίο δείχνουμε πώς συνδέονται προγράμματα όπως το XDP.

Χειρισμός αντικειμένων με χρήση της κλήσης συστήματος bpf

Προγράμματα BPF

Все объекты BPF создаются и управляются из пространства пользователя при помощи системного вызова bpf, έχοντας το ακόλουθο πρωτότυπο:

#include <linux/bpf.h>

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

Εδώ είναι η ομάδα cmd είναι μία από τις τιμές του τύπου enum bpf_cmd, attr — δείκτη για παραμέτρους για ένα συγκεκριμένο πρόγραμμα και size — μέγεθος αντικειμένου σύμφωνα με τον δείκτη, δηλ. συνήθως αυτό sizeof(*attr). Στον πυρήνα 5.8 η κλήση συστήματος bpf υποστηρίζει 34 διαφορετικές εντολές και προσδιορισμός του union bpf_attr καταλαμβάνει 200 ​​γραμμές. Αλλά δεν πρέπει να μας τρομάζει αυτό, καθώς θα εξοικειωθούμε με τις εντολές και τις παραμέτρους κατά τη διάρκεια πολλών άρθρων.

Ας ξεκινήσουμε με την ομάδα BPF_PROG_LOAD, το οποίο δημιουργεί προγράμματα BPF - παίρνει ένα σύνολο εντολών BPF και το φορτώνει στον πυρήνα. Τη στιγμή της φόρτωσης, εκκινείται ο επαληθευτής και στη συνέχεια ο μεταγλωττιστής JIT και, μετά την επιτυχή εκτέλεση, ο περιγραφέας αρχείου προγράμματος επιστρέφεται στον χρήστη. Είδαμε τι του συμβαίνει μετά στην προηγούμενη ενότητα σχετικά με τον κύκλο ζωής των αντικειμένων BPF.

Τώρα θα γράψουμε ένα προσαρμοσμένο πρόγραμμα που θα φορτώνει ένα απλό πρόγραμμα BPF, αλλά πρώτα πρέπει να αποφασίσουμε τι είδους πρόγραμμα θέλουμε να φορτώσουμε - θα πρέπει να επιλέξουμε τύπου και στα πλαίσια αυτού του τύπου, γράψτε ένα πρόγραμμα που θα περάσει το τεστ επαληθευτή. Ωστόσο, για να μην περιπλέκουμε τη διαδικασία, εδώ είναι μια έτοιμη λύση: θα πάρουμε ένα πρόγραμμα όπως BPF_PROG_TYPE_XDP, которая будет возвращать значение XDP_PASS (παράβλεψη όλων των πακέτων). Στον συναρμολογητή BPF φαίνεται πολύ απλό:

r0 = 2
exit

Αφού το αποφασίσουμε ότι θα ανεβάσουμε, μπορούμε να σας πούμε πώς θα το κάνουμε:

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

Τα ενδιαφέροντα γεγονότα σε ένα πρόγραμμα ξεκινούν με τον ορισμό ενός πίνακα insns - το πρόγραμμά μας BPF σε κώδικα μηχανής. Σε αυτήν την περίπτωση, κάθε εντολή του προγράμματος BPF συσκευάζεται στη δομή bpf_insn. Πρώτο στοιχείο insns συμμορφώνεται με τις οδηγίες r0 = 2, δεύτερο - exit.

Υποχώρηση. Ο πυρήνας ορίζει πιο βολικές μακροεντολές για τη σύνταξη κωδικών μηχανής και τη χρήση του αρχείου κεφαλίδας του πυρήνα tools/include/linux/filter.h θα μπορούσαμε να γράψουμε

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

Αλλά επειδή η εγγραφή προγραμμάτων BPF σε εγγενή κώδικα είναι απαραίτητη μόνο για τη σύνταξη δοκιμών στον πυρήνα και άρθρων σχετικά με το BPF, η απουσία αυτών των μακροεντολών δεν περιπλέκει πραγματικά τη ζωή του προγραμματιστή.

Αφού ορίσουμε το πρόγραμμα BPF, προχωράμε στη φόρτωσή του στον πυρήνα. Το μινιμαλιστικό μας σύνολο παραμέτρων attr περιλαμβάνει τον τύπο προγράμματος, το σύνολο και τον αριθμό των οδηγιών, την απαιτούμενη άδεια χρήσης και το όνομα "woo", το οποίο χρησιμοποιούμε για να βρούμε το πρόγραμμά μας στο σύστημα μετά τη λήψη. Το πρόγραμμα, όπως υποσχέθηκε, φορτώνεται στο σύστημα χρησιμοποιώντας μια κλήση συστήματος bpf.

Στο τέλος του προγράμματος καταλήγουμε σε έναν άπειρο βρόχο που προσομοιώνει το ωφέλιμο φορτίο. Χωρίς αυτό, το πρόγραμμα θα σκοτωθεί από τον πυρήνα όταν κλείσει ο περιγραφέας αρχείου που μας επέστρεψε η κλήση συστήματος bpf, και δεν θα το δούμε στο σύστημα.

Λοιπόν, είμαστε έτοιμοι για δοκιμή. Ας συναρμολογήσουμε και εκτελέσουμε το πρόγραμμα κάτω από straceγια να ελέγξετε ότι όλα λειτουργούν όπως θα έπρεπε:

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

Ολα ειναι καλά, bpf(2) επέστρεψε τη λαβή 3 σε εμάς και μπήκαμε σε έναν άπειρο βρόχο με pause(). Ας προσπαθήσουμε να βρούμε το πρόγραμμά μας στο σύστημα. Για να το κάνουμε αυτό θα πάμε σε άλλο τερματικό και θα χρησιμοποιήσουμε το βοηθητικό πρόγραμμα 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)

Βλέπουμε ότι υπάρχει ένα φορτωμένο πρόγραμμα στο σύστημα woo του οποίου το παγκόσμιο αναγνωριστικό είναι 390 και βρίσκεται αυτή τη στιγμή σε εξέλιξη simple-prog υπάρχει ένας ανοιχτός περιγραφέας αρχείου που δείχνει προς το πρόγραμμα (και αν simple-prog θα τελειώσει τη δουλειά λοιπόν woo θα εξαφανιστεί). Όπως ήταν αναμενόμενο, το πρόγραμμα woo παίρνει 16 byte - δύο εντολές - δυαδικών κωδικών στην αρχιτεκτονική BPF, αλλά στην εγγενή της μορφή (x86_64) είναι ήδη 40 byte. Ας δούμε το πρόγραμμά μας στην αρχική του μορφή:

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

όχι εκπλήξεις. Τώρα ας δούμε τον κώδικα που δημιουργείται από τον μεταγλωττιστή JIT:

# 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

όχι πολύ αποτελεσματικό για exit(2), αλλά για να είμαστε δίκαιοι, το πρόγραμμά μας είναι πολύ απλό και για μη τετριμμένα προγράμματα ο πρόλογος και ο επίλογος που προστέθηκαν από τον μεταγλωττιστή JIT, φυσικά, χρειάζονται.

χάρτες

Τα προγράμματα BPF μπορούν να χρησιμοποιούν περιοχές δομημένης μνήμης που είναι προσβάσιμες τόσο σε άλλα προγράμματα BPF όσο και σε προγράμματα στο χώρο χρήστη. Αυτά τα αντικείμενα ονομάζονται χάρτες και σε αυτή την ενότητα θα δείξουμε πώς να τα χειριστείτε χρησιμοποιώντας μια κλήση συστήματος bpf.

Ας πούμε αμέσως ότι οι δυνατότητες των χαρτών δεν περιορίζονται μόνο στην πρόσβαση στην κοινόχρηστη μνήμη. Υπάρχουν χάρτες ειδικού σκοπού που περιέχουν, για παράδειγμα, δείκτες σε προγράμματα BPF ή δείκτες σε διεπαφές δικτύου, χάρτες για εργασία με συμβάντα perf κ.λπ. Δεν θα μιλήσουμε για αυτά εδώ, για να μην μπερδέψουμε τον αναγνώστη. Εκτός από αυτό, αγνοούμε τα θέματα συγχρονισμού, καθώς αυτό δεν είναι σημαντικό για τα παραδείγματά μας. Μπορείτε να βρείτε μια πλήρη λίστα με τους διαθέσιμους τύπους χαρτών <linux/bpf.h>, και σε αυτή την ενότητα θα πάρουμε ως παράδειγμα τον ιστορικά πρώτο τύπο, τον πίνακα κατακερματισμού BPF_MAP_TYPE_HASH.

Εάν δημιουργήσετε έναν πίνακα κατακερματισμού, ας πούμε, σε C++, θα λέγατε unordered_map<int,long> woo, που στα ρωσικά σημαίνει «Χρειάζομαι ένα τραπέζι woo απεριόριστο μέγεθος, τα κλειδιά του οποίου είναι τύπου int, και οι τιμές είναι ο τύπος long" Για να δημιουργήσουμε έναν πίνακα κατακερματισμού BPF, πρέπει να κάνουμε το ίδιο πράγμα, εκτός από το ότι πρέπει να καθορίσουμε το μέγιστο μέγεθος του πίνακα και αντί να προσδιορίσουμε τους τύπους κλειδιών και τιμών, πρέπει να καθορίσουμε τα μεγέθη τους σε byte . Για να δημιουργήσετε χάρτες χρησιμοποιήστε την εντολή BPF_MAP_CREATE κλήση συστήματος bpf. Ας δούμε ένα λίγο πολύ μίνιμαλ πρόγραμμα που δημιουργεί έναν χάρτη. Μετά το προηγούμενο πρόγραμμα που φορτώνει προγράμματα BPF, αυτό θα πρέπει να σας φαίνεται απλό:

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

Εδώ ορίζουμε ένα σύνολο παραμέτρων attr, в котором говорим «мне нужна хэш-таблица с ключами и значениями размера sizeof(int), στο οποίο μπορώ να βάλω το πολύ τέσσερα στοιχεία». Κατά τη δημιουργία χαρτών BPF, μπορείτε να καθορίσετε άλλες παραμέτρους, για παράδειγμα, με τον ίδιο τρόπο όπως στο παράδειγμα με το πρόγραμμα, καθορίσαμε το όνομα του αντικειμένου ως "woo".

Ας κάνουμε μεταγλώττιση και εκτέλεση του προγράμματος:

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

Εδώ είναι η κλήση συστήματος bpf(2) μας επέστρεψε τον αριθμό του χάρτη περιγραφής 3 и дальше программа, как и ожидалось, ждет дальнейших указаний в системном вызове pause(2).

Τώρα ας στείλουμε το πρόγραμμά μας στο παρασκήνιο ή ας ανοίξουμε ένα άλλο τερματικό και ας δούμε το αντικείμενο μας χρησιμοποιώντας το βοηθητικό πρόγραμμα bpftool (μπορούμε να ξεχωρίσουμε τον χάρτη μας από άλλους με το όνομά του):

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

Ο αριθμός 114 είναι το καθολικό αναγνωριστικό του αντικειμένου μας. Οποιοδήποτε πρόγραμμα στο σύστημα μπορεί να χρησιμοποιήσει αυτό το αναγνωριστικό για να ανοίξει έναν υπάρχοντα χάρτη χρησιμοποιώντας την εντολή BPF_MAP_GET_FD_BY_ID κλήση συστήματος bpf.

Τώρα μπορούμε να παίξουμε με το hash τραπέζι μας. Ας δούμε το περιεχόμενό του:

$ sudo bpftool map dump id 114
Found 0 elements

Αδειάζω. Ας δώσουμε μια αξία σε αυτό hash[1] = 1:

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

Ας δούμε ξανά τον πίνακα:

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

Ζήτω! Καταφέραμε να προσθέσουμε ένα στοιχείο. Σημειώστε ότι πρέπει να εργαστούμε σε επίπεδο byte για να το κάνουμε αυτό, αφού bptftool δεν γνωρίζει τι είδους είναι οι τιμές στον πίνακα κατακερματισμού. (Αυτή η γνώση μπορεί να της μεταφερθεί χρησιμοποιώντας το BTF, αλλά περισσότερα για αυτό τώρα.)

Πώς ακριβώς διαβάζει και προσθέτει στοιχεία το bpftool; Ας ρίξουμε μια ματιά κάτω από την κουκούλα:

$ 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

Αρχικά ανοίξαμε τον χάρτη με το καθολικό του αναγνωριστικό χρησιμοποιώντας την εντολή BPF_MAP_GET_FD_BY_ID и bpf(2) μας επέστρεψε τον περιγραφέα 3. Χρησιμοποιώντας περαιτέρω την εντολή BPF_MAP_GET_NEXT_KEY βρήκαμε το πρώτο κλειδί στον πίνακα περνώντας NULL в качестве указателя на «предыдущий» ключ. При наличии ключа мы можем сделать BPF_MAP_LOOKUP_ELEMπου επιστρέφει μια τιμή σε έναν δείκτη value. Следующий шаг — мы пытаемся найти следующий элемент, передавая указатель на текущий ключ, но наша таблица содержит только один элемент и команда BPF_MAP_GET_NEXT_KEY επιστρέφει ENOENT.

Εντάξει, ας αλλάξουμε την τιμή με το κλειδί 1, ας υποθέσουμε ότι η επιχειρηματική μας λογική απαιτεί εγγραφή 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

Όπως ήταν αναμενόμενο, είναι πολύ απλό: η εντολή BPF_MAP_GET_FD_BY_ID ανοίγει τον χάρτη μας κατά ID, και την εντολή BPF_MAP_UPDATE_ELEM αντικαθιστά το στοιχείο.

Έτσι, αφού δημιουργήσουμε έναν πίνακα κατακερματισμού από ένα πρόγραμμα, μπορούμε να διαβάσουμε και να γράψουμε τα περιεχόμενά του από ένα άλλο. Σημειώστε ότι εάν μπορούσαμε να το κάνουμε αυτό από τη γραμμή εντολών, τότε οποιοδήποτε άλλο πρόγραμμα στο σύστημα μπορεί να το κάνει. Εκτός από τις εντολές που περιγράφονται παραπάνω, για εργασία με χάρτες από το χώρο χρήστη, Οι ακόλουθες:

  • BPF_MAP_LOOKUP_ELEM: найти значение по ключу
  • BPF_MAP_UPDATE_ELEM: ενημέρωση/δημιουργία αξίας
  • BPF_MAP_DELETE_ELEM: αφαίρεση κλειδιού
  • BPF_MAP_GET_NEXT_KEY: βρείτε το επόμενο (ή το πρώτο) κλειδί
  • BPF_MAP_GET_NEXT_ID: σας επιτρέπει να περάσετε από όλους τους υπάρχοντες χάρτες, έτσι λειτουργεί bpftool map
  • BPF_MAP_GET_FD_BY_ID: ανοίξτε έναν υπάρχοντα χάρτη με το καθολικό του αναγνωριστικό
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: ενημερώστε ατομικά την τιμή ενός αντικειμένου και επιστρέψτε την παλιά
  • BPF_MAP_FREEZE: κάνει τον χάρτη αμετάβλητο από το userspace (αυτή η λειτουργία δεν μπορεί να αναιρεθεί)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: μαζικές επιχειρήσεις. Για παράδειγμα, BPF_MAP_LOOKUP_AND_DELETE_BATCH - αυτός είναι ο μόνος αξιόπιστος τρόπος ανάγνωσης και επαναφοράς όλων των τιμών από τον χάρτη

Δεν λειτουργούν όλες αυτές οι εντολές για όλους τους τύπους χαρτών, αλλά γενικά η εργασία με άλλους τύπους χαρτών από το χώρο χρήστη μοιάζει ακριβώς με την εργασία με πίνακες κατακερματισμού.

Για λόγους τάξης, ας ολοκληρώσουμε τα πειράματά μας στον πίνακα κατακερματισμού. Θυμάστε ότι δημιουργήσαμε έναν πίνακα που μπορεί να περιέχει έως και τέσσερα κλειδιά; Ας προσθέσουμε μερικά ακόμη στοιχεία:

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

Ας προσπαθήσουμε να προσθέσουμε ένα ακόμη:

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

Όπως ήταν αναμενόμενο, δεν τα καταφέραμε. Ας δούμε το σφάλμα πιο αναλυτικά:

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

Όλα είναι καλά: όπως ήταν αναμενόμενο, η ομάδα BPF_MAP_UPDATE_ELEM προσπαθεί να δημιουργήσει ένα νέο, πέμπτο, κλειδί, αλλά κολλάει E2BIG.

Έτσι, μπορούμε να δημιουργήσουμε και να φορτώσουμε προγράμματα BPF, καθώς και να δημιουργήσουμε και να διαχειριστούμε χάρτες από το χώρο χρήστη. Τώρα είναι λογικό να δούμε πώς μπορούμε να χρησιμοποιήσουμε χάρτες από τα ίδια τα προγράμματα BPF. Θα μπορούσαμε να μιλήσουμε για αυτό στη γλώσσα των δυσανάγνωστων προγραμμάτων σε κώδικες μακροεντολών μηχανής, αλλά στην πραγματικότητα ήρθε η ώρα να δείξουμε πώς γράφονται και συντηρούνται πραγματικά τα προγράμματα BPF - χρησιμοποιώντας libbpf.

(Για τους αναγνώστες που δεν είναι ικανοποιημένοι με την έλλειψη παραδείγματος χαμηλού επιπέδου: θα αναλύσουμε λεπτομερώς προγράμματα που χρησιμοποιούν χάρτες και βοηθητικές συναρτήσεις που δημιουργήθηκαν με τη χρήση libbpf και να σας πω τι συμβαίνει σε επίπεδο διδασκαλίας. Για τους αναγνώστες που είναι δυσαρεστημένοι πάρα πολύ, προσθέσαμε παράδειγμα στην κατάλληλη θέση στο άρθρο.)

Γράφοντας προγράμματα BPF χρησιμοποιώντας libbpf

Η σύνταξη προγραμμάτων BPF με χρήση κωδικών μηχανής μπορεί να είναι ενδιαφέρουσα μόνο την πρώτη φορά και μετά αρχίζει ο κορεσμός. Αυτή τη στιγμή πρέπει να στρέψεις την προσοχή σου llvm, το οποίο διαθέτει ένα backend για τη δημιουργία κώδικα για την αρχιτεκτονική BPF, καθώς και μια βιβλιοθήκη libbpf, που σας επιτρέπει να γράψετε την πλευρά χρήστη των εφαρμογών BPF και να φορτώσετε τον κώδικα των προγραμμάτων BPF που δημιουργούνται χρησιμοποιώντας llvm/clang.

Στην πραγματικότητα, όπως θα δούμε σε αυτό και σε επόμενα άρθρα, libbpf κάνει πολλή δουλειά χωρίς αυτό (ή παρόμοια εργαλεία - iproute2, libbcc, libbpf-goκ.λπ.) είναι αδύνατο να ζήσεις. Ένα από τα δολοφονικά χαρακτηριστικά του έργου libbpf είναι το BPF CO-RE (Compile Once, Run Everywhere) - ένα έργο που σας επιτρέπει να γράψετε προγράμματα BPF που είναι φορητά από τον έναν πυρήνα στον άλλο, με τη δυνατότητα να εκτελούνται σε διαφορετικά API (για παράδειγμα, όταν η δομή του πυρήνα αλλάζει από την έκδοση στην έκδοση). Για να μπορέσετε να εργαστείτε με το CO-RE, ο πυρήνας σας πρέπει να έχει μεταγλωττιστεί με υποστήριξη BTF (περιγράφουμε πώς να το κάνετε αυτό στην ενότητα Εργαλεία ανάπτυξης. Μπορείτε να ελέγξετε εάν ο πυρήνας σας είναι κατασκευασμένος με BTF ή όχι πολύ απλά - με την παρουσία του παρακάτω αρχείου:

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

Αυτό το αρχείο αποθηκεύει πληροφορίες σχετικά με όλους τους τύπους δεδομένων που χρησιμοποιούνται στον πυρήνα και χρησιμοποιείται σε όλα τα παραδείγματα που χρησιμοποιούμε libbpf. Θα μιλήσουμε λεπτομερώς για το CO-RE στο επόμενο άρθρο, αλλά σε αυτό - απλώς δημιουργήστε έναν πυρήνα με CONFIG_DEBUG_INFO_BTF.

Βιβλιοθήκη libbpf ζει ακριβώς στον κατάλογο tools/lib/bpf πυρήνα και η ανάπτυξή του πραγματοποιείται μέσω της λίστας αλληλογραφίας [email protected]. Однако для нужд приложений, живущих за пределами ядра, поддерживается отдельный репозиторий https://github.com/libbpf/libbpf στην οποία η βιβλιοθήκη του πυρήνα αντικατοπτρίζεται για πρόσβαση ανάγνωσης λίγο πολύ ως έχει.

В данном разделе мы посмотрим на то, как можно создать проект, использующий libbpf, ας γράψουμε αρκετά (λίγο πολύ χωρίς νόημα) δοκιμαστικά προγράμματα και ας αναλύσουμε λεπτομερώς πώς λειτουργούν όλα. Αυτό θα μας επιτρέψει να εξηγήσουμε πιο εύκολα στις επόμενες ενότητες πώς ακριβώς αλληλεπιδρούν τα προγράμματα BPF με χάρτες, βοηθούς πυρήνα, BTF κ.λπ.

Τυπικά έργα που χρησιμοποιούν libbpf προσθέστε ένα αποθετήριο GitHub ως υπομονάδα git, θα κάνουμε το ίδιο:

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

Πηγαίνω σε libbpf πολύ απλό:

$ 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

Το επόμενο σχέδιό μας σε αυτήν την ενότητα είναι το εξής: θα γράψουμε ένα πρόγραμμα BPF όπως BPF_PROG_TYPE_XDP, το ίδιο όπως στο προηγούμενο παράδειγμα, αλλά στο C, το μεταγλωττίζουμε χρησιμοποιώντας clangκαι γράψτε ένα βοηθητικό πρόγραμμα που θα το φορτώσει στον πυρήνα. Στις επόμενες ενότητες θα επεκτείνουμε τις δυνατότητες τόσο του προγράμματος BPF όσο και του προγράμματος βοηθού.

Παράδειγμα: δημιουργία μιας πλήρους εφαρμογής χρησιμοποιώντας το libbpf

Αρχικά, χρησιμοποιούμε το αρχείο /sys/kernel/btf/vmlinux, που αναφέρθηκε παραπάνω, και δημιουργήστε το αντίστοιχο με τη μορφή αρχείου κεφαλίδας:

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

Αυτό το αρχείο θα αποθηκεύσει όλες τις δομές δεδομένων που είναι διαθέσιμες στον πυρήνα μας, για παράδειγμα, έτσι ορίζεται η κεφαλίδα IPv4 στον πυρήνα:

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

Τώρα θα γράψουμε το πρόγραμμα BPF σε C:

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

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

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

Αν και το πρόγραμμά μας αποδείχθηκε πολύ απλό, πρέπει να προσέχουμε πολλές λεπτομέρειες. Πρώτον, το πρώτο αρχείο κεφαλίδας που συμπεριλαμβάνουμε είναι vmlinux.h, το οποίο μόλις δημιουργήσαμε χρησιμοποιώντας bpftool btf dump - τώρα δεν χρειάζεται να εγκαταστήσουμε το πακέτο kernel-headers για να μάθουμε πώς μοιάζουν οι δομές του πυρήνα. Το παρακάτω αρχείο κεφαλίδας έρχεται σε εμάς από τη βιβλιοθήκη libbpf. Τώρα το χρειαζόμαστε μόνο για να ορίσουμε τη μακροεντολή SEC, το οποίο στέλνει τον χαρακτήρα στην κατάλληλη ενότητα του αρχείου αντικειμένου ELF. Το πρόγραμμά μας περιέχεται στην ενότητα xdp/simple, όπου πριν από την κάθετο ορίζουμε τον τύπο προγράμματος BPF - αυτή είναι η σύμβαση που χρησιμοποιείται σε libbpf, με βάση το όνομα της ενότητας θα αντικαταστήσει τον σωστό τύπο κατά την εκκίνηση bpf(2). Το ίδιο το πρόγραμμα BPF είναι C - πολύ απλό και αποτελείται από μία γραμμή return XDP_PASS. Τέλος, μια ξεχωριστή ενότητα "license" περιέχει το όνομα της άδειας.

Μπορούμε να μεταγλωττίσουμε το πρόγραμμά μας χρησιμοποιώντας llvm/clang, έκδοση >= 10.0.0 ή ακόμα καλύτερα, μεγαλύτερη (βλ. ενότητα Εργαλεία ανάπτυξης):

$ 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

Ανάμεσα στα ενδιαφέροντα χαρακτηριστικά: υποδεικνύουμε την αρχιτεκτονική στόχο -target bpf и путь к заголовкам libbpf, το οποίο εγκαταστήσαμε πρόσφατα. Επίσης, μην ξεχνάτε -O2, χωρίς αυτήν την επιλογή μπορεί να βρεθείτε μπροστά σε εκπλήξεις στο μέλλον. Ας δούμε τον κώδικα μας, καταφέραμε να γράψουμε το πρόγραμμα που θέλαμε;

$ 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

Ναι, δούλεψε! Τώρα, έχουμε ένα δυαδικό αρχείο με το πρόγραμμα και θέλουμε να δημιουργήσουμε μια εφαρμογή που θα το φορτώσει στον πυρήνα. Για το σκοπό αυτό η βιβλιοθήκη libbpf μας προσφέρει δύο επιλογές - χρησιμοποιήστε ένα API χαμηλότερου επιπέδου ή ένα API υψηλότερου επιπέδου. Θα πάμε στον δεύτερο δρόμο, αφού θέλουμε να μάθουμε πώς να γράφουμε, να φορτώνουμε και να συνδέουμε προγράμματα BPF με ελάχιστη προσπάθεια για την μετέπειτα μελέτη τους.

Αρχικά, πρέπει να δημιουργήσουμε τον «σκελετό» του προγράμματός μας από το δυαδικό του αρχείο χρησιμοποιώντας το ίδιο βοηθητικό πρόγραμμα bpftool — το ελβετικό μαχαίρι του κόσμου της BPF (το οποίο μπορεί να ληφθεί κυριολεκτικά, αφού ο Daniel Borkman, ένας από τους δημιουργούς και συντηρητές του BPF, είναι Ελβετός):

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

Στο αρχείο xdp-simple.skel.h содержится бинарный код нашей программы и функции для управления — загрузки, присоединения, удаления нашего объекта. В нашем простом случае это выглядит как overkill, но это работает и в случае, когда объектный файл содержит множество программ BPF и мапов и для загрузки этого гигантского ELF нам достаточно лишь сгенерировать скелет и вызвать одну-две функции из пользовательского приложения, к написанию которого мы сейчас и перейдем.

Αυστηρά μιλώντας, το πρόγραμμα φόρτωσης μας είναι ασήμαντο:

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

Εδώ struct xdp_simple_bpf ορίζεται στο αρχείο xdp-simple.skel.h и описывает наш объектный файл:

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

Μπορούμε να δούμε ίχνη ενός API χαμηλού επιπέδου εδώ: τη δομή struct bpf_program *simple и struct bpf_link *simple. Η πρώτη δομή περιγράφει συγκεκριμένα το πρόγραμμά μας, γραμμένο στην ενότητα xdp/simple, και το δεύτερο περιγράφει πώς το πρόγραμμα συνδέεται με την πηγή συμβάντος.

Λειτουργία xdp_simple_bpf__open_and_load, ανοίγει ένα αντικείμενο ELF, το αναλύει, δημιουργεί όλες τις δομές και τις υποδομές (εκτός από το πρόγραμμα, το ELF περιέχει και άλλες ενότητες - δεδομένα, δεδομένα μόνο για ανάγνωση, πληροφορίες εντοπισμού σφαλμάτων, άδεια χρήσης κ.λπ.) και στη συνέχεια το φορτώνει στον πυρήνα χρησιμοποιώντας ένα σύστημα κλήση bpf, το οποίο μπορούμε να ελέγξουμε με τη μεταγλώττιση και εκτέλεση του προγράμματος:

$ 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

Ας δούμε τώρα το πρόγραμμά μας χρησιμοποιώντας bpftool. Ας βρούμε την ταυτότητά της:

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

και dump (χρησιμοποιούμε μια συντομευμένη μορφή της εντολής bpftool prog dump xlated):

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

Κάτι νέο! Το πρόγραμμα εκτύπωσε κομμάτια του αρχείου πηγής C. Αυτό έγινε από τη βιβλιοθήκη libbpf, που βρήκε το τμήμα εντοπισμού σφαλμάτων στο δυαδικό αρχείο, το μεταγλωττίστηκε σε ένα αντικείμενο BTF, το φόρτωσε στον πυρήνα χρησιμοποιώντας BPF_BTF_LOAD, και, στη συνέχεια, καθορίσατε τον περιγραφέα αρχείου που προκύπτει κατά τη φόρτωση του προγράμματος με την εντολή BPG_PROG_LOAD.

Βοηθοί πυρήνα

Τα προγράμματα BPF μπορούν να εκτελέσουν "εξωτερικές" λειτουργίες - βοηθοί πυρήνα. Αυτές οι βοηθητικές λειτουργίες επιτρέπουν στα προγράμματα BPF να έχουν πρόσβαση σε δομές πυρήνα, να διαχειρίζονται χάρτες και επίσης να επικοινωνούν με τον "πραγματικό κόσμο" - να δημιουργούν συμβάντα perf, να ελέγχουν το υλικό (για παράδειγμα, να ανακατευθύνουν πακέτα) κ.λπ.

Παράδειγμα: bpf_get_smp_processor_id

Στο πλαίσιο του παραδείγματος «μάθηση μέσω παραδείγματος», ας εξετάσουμε μια από τις βοηθητικές λειτουργίες, bpf_get_smp_processor_id(), βέβαιος στο αρχείο kernel/bpf/helpers.c. Επιστρέφει τον αριθμό του επεξεργαστή στον οποίο εκτελείται το πρόγραμμα BPF που τον κάλεσε. Αλλά δεν μας ενδιαφέρει τόσο η σημασιολογία του όσο το γεγονός ότι η εφαρμογή του παίρνει μια γραμμή:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Οι ορισμοί της βοηθητικής συνάρτησης BPF είναι παρόμοιοι με τους ορισμούς κλήσεων συστήματος Linux. Εδώ, για παράδειγμα, ορίζεται μια συνάρτηση που δεν έχει ορίσματα. (Μια συνάρτηση που παίρνει, ας πούμε, τρία ορίσματα ορίζεται χρησιμοποιώντας τη μακροεντολή BPF_CALL_3. Ο μέγιστος αριθμός ορισμάτων είναι πέντε.) Ωστόσο, αυτό είναι μόνο το πρώτο μέρος του ορισμού. Το δεύτερο μέρος είναι να ορίσουμε τη δομή τύπου struct bpf_func_proto, το οποίο περιέχει μια περιγραφή της βοηθητικής συνάρτησης που κατανοεί ο επαληθευτής:

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

Καταχώρηση βοηθητικών λειτουργιών

Προκειμένου τα προγράμματα BPF ενός συγκεκριμένου τύπου να χρησιμοποιούν αυτήν τη λειτουργία, πρέπει να την καταχωρήσουν, για παράδειγμα για τον τύπο BPF_PROG_TYPE_XDP μια συνάρτηση ορίζεται στον πυρήνα xdp_func_proto, το οποίο καθορίζει από το αναγνωριστικό της βοηθητικής συνάρτησης εάν το XDP υποστηρίζει αυτήν τη λειτουργία ή όχι. Η λειτουργία μας είναι υποστηρίζει:

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

Νέοι τύποι προγραμμάτων BPF "ορίζονται" στο αρχείο include/linux/bpf_types.h χρησιμοποιώντας μια μακροεντολή BPF_PROG_TYPE. Ορίζεται σε εισαγωγικά επειδή είναι ένας λογικός ορισμός, και με όρους γλώσσας C, ο ορισμός ενός ολόκληρου συνόλου κατασκευών από σκυρόδεμα εμφανίζεται σε άλλα μέρη. Ειδικότερα, στο φάκελο kernel/bpf/verifier.c όλους τους ορισμούς από το αρχείο bpf_types.h χρησιμοποιούνται για τη δημιουργία μιας σειράς δομών 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
};

Δηλαδή, για κάθε τύπο προγράμματος BPF, ορίζεται ένας δείκτης σε μια δομή δεδομένων του τύπου struct bpf_verifier_ops, το οποίο αρχικοποιείται με την τιμή _name ## _verifier_ops, δηλ. xdp_verifier_ops για xdp. Δομή xdp_verifier_ops καθορίζεται από στο αρχείο 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,
};

Εδώ βλέπουμε τη γνωστή μας λειτουργία xdp_func_proto, το οποίο θα εκτελεί τον επαληθευτή κάθε φορά που αντιμετωπίζει μια πρόκληση κάποιο είδος λειτουργίες μέσα σε ένα πρόγραμμα BPF, βλ verifier.c.

Ας δούμε πώς ένα υποθετικό πρόγραμμα BPF χρησιμοποιεί τη συνάρτηση bpf_get_smp_processor_id. Για να γίνει αυτό, ξαναγράφουμε το πρόγραμμα από την προηγούμενη ενότητα ως εξής:

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

Σύμβολο bpf_get_smp_processor_id καθορίζεται από в <bpf/bpf_helper_defs.h> βιβλιοθήκη libbpf как

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

αυτό είναι, bpf_get_smp_processor_id είναι ένας δείκτης συνάρτησης του οποίου η τιμή είναι 8, όπου 8 είναι η τιμή BPF_FUNC_get_smp_processor_id Τύπος enum bpf_fun_id, το οποίο ορίζεται για εμάς στο αρχείο vmlinux.h (αρχείο bpf_helper_defs.h στον πυρήνα δημιουργείται από ένα σενάριο, οπότε οι "μαγικοί" αριθμοί είναι εντάξει). Αυτή η συνάρτηση δεν δέχεται ορίσματα και επιστρέφει μια τιμή τύπου __u32. Όταν το τρέχουμε στο πρόγραμμά μας, clang δημιουργεί μια οδηγία BPF_CALL "το σωστό είδος" Ας μεταγλωττίσουμε το πρόγραμμα και ας δούμε την ενότητα 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

Στην πρώτη γραμμή βλέπουμε οδηγίες call, παράμετρος IMM που ισούται με 8, και SRC_REG - μηδέν. Σύμφωνα με τη συμφωνία ABI που χρησιμοποιείται από τον επαληθευτή, αυτή είναι η λειτουργία κλήσης βοηθού αριθμός οκτώ. Μόλις ξεκινήσει, η λογική είναι απλή. Επιστρεφόμενη τιμή από το μητρώο r0 αντιγράφηκε σε r1 και στις γραμμές 2,3 μετατρέπεται σε τύπο u32 — τα ανώτερα 32 bit διαγράφονται. Στις γραμμές 4,5,6,7 επιστρέφουμε 2 (XDP_PASS) ή 1 (XDP_DROP) ανάλογα με το αν η βοηθητική συνάρτηση από τη γραμμή 0 επέστρεψε μηδενική ή μη μηδενική τιμή.

Проверим себя: загрузим программу и посмотрим на вывод 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

Εντάξει, ο επαληθευτής βρήκε τον σωστό βοηθό πυρήνα.

Παράδειγμα: μεταβίβαση ορισμάτων και τέλος εκτέλεση του προγράμματος!

Όλες οι βοηθητικές συναρτήσεις σε επίπεδο εκτέλεσης έχουν ένα πρωτότυπο

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

Οι παράμετροι στις βοηθητικές συναρτήσεις μεταβιβάζονται σε καταχωρητές r1-r5, και η τιμή επιστρέφεται στο μητρώο r0. Δεν υπάρχουν συναρτήσεις που λαμβάνουν περισσότερα από πέντε ορίσματα και δεν αναμένεται να προστεθεί υποστήριξη για αυτά στο μέλλον.

Ας ρίξουμε μια ματιά στον νέο βοηθό πυρήνα και πώς το BPF μεταβιβάζει παραμέτρους. Ας ξαναγράψουμε xdp-simple.bpf.c ως εξής (οι υπόλοιπες γραμμές δεν έχουν αλλάξει):

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

Το πρόγραμμά μας εκτυπώνει τον αριθμό της CPU στην οποία εκτελείται. Ας το μεταγλωττίσουμε και ας δούμε τον κώδικα:

$ 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

В строках 0-7 мы записываем на стек строку running on CPU%un, και στη συνέχεια στη γραμμή 8 τρέχουμε το γνωστό bpf_get_smp_processor_id. Στις γραμμές 9-12 ετοιμάζουμε τα βοηθητικά ορίσματα bpf_printk - μητρώα r1, r2, r3. Γιατί είναι τρία από αυτά και όχι δύο; Επειδή bpf_printkαυτό είναι ένα περιτύλιγμα μακροεντολών γύρω από τον πραγματικό βοηθό bpf_trace_printk, το οποίο πρέπει να περάσει το μέγεθος της συμβολοσειράς μορφής.

Ας προσθέσουμε τώρα μερικές γραμμές xdp-simple.cώστε το πρόγραμμά μας να συνδεθεί στη διεπαφή lo και πραγματικά ξεκίνησε!

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

Εδώ χρησιμοποιούμε τη συνάρτηση bpf_set_link_xdp_fd, το οποίο συνδέει προγράμματα BPF τύπου XDP με διεπαφές δικτύου. Κωδικοποιήσαμε τον αριθμό διεπαφής lo, που είναι πάντα 1. Εκτελούμε τη συνάρτηση δύο φορές για να αφαιρέσουμε πρώτα το παλιό πρόγραμμα, αν ήταν συνδεδεμένο. Προσέξτε ότι τώρα δεν χρειαζόμαστε πρόκληση pause ή ένας άπειρος βρόχος: το πρόγραμμα φόρτωσης θα βγει, αλλά το πρόγραμμα BPF δεν θα σκοτωθεί αφού είναι συνδεδεμένο με την πηγή συμβάντος. Μετά την επιτυχή λήψη και σύνδεση, το πρόγραμμα θα ξεκινήσει για κάθε πακέτο δικτύου που φθάνει lo.

Ας κατεβάσουμε το πρόγραμμα και ας δούμε τη διεπαφή 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

Το πρόγραμμα που κατεβάσαμε έχει ID 669 και βλέπουμε το ίδιο ID στη διεπαφή lo. Θα στείλουμε μερικά πακέτα στο 127.0.0.1 (αίτημα + απάντηση):

$ ping -c1 localhost

και τώρα ας δούμε τα περιεχόμενα του εικονικού αρχείου εντοπισμού σφαλμάτων /sys/kernel/debug/tracing/trace_pipe, στο οποίο bpf_printk γράφει τα μηνύματά του:

# 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

Εντοπίστηκαν δύο πακέτα lo και υποβλήθηκε σε επεξεργασία σε CPU0 - το πρώτο μας πλήρες πρόγραμμα BPF χωρίς νόημα λειτούργησε!

Αξίζει να σημειωθεί ότι bpf_printk Δεν είναι τυχαίο που γράφει στο αρχείο εντοπισμού σφαλμάτων: αυτός δεν είναι ο πιο επιτυχημένος βοηθός για χρήση στην παραγωγή, αλλά ο στόχος μας ήταν να δείξουμε κάτι απλό.

Πρόσβαση σε χάρτες από προγράμματα BPF

Παράδειγμα: χρήση χάρτη από το πρόγραμμα BPF

Στις προηγούμενες ενότητες μάθαμε πώς να δημιουργούμε και να χρησιμοποιούμε χάρτες από το χώρο χρήστη και τώρα ας δούμε το τμήμα του πυρήνα. Ας ξεκινήσουμε, ως συνήθως, με ένα παράδειγμα. Ας ξαναγράψουμε το πρόγραμμά μας 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";

Στην αρχή του προγράμματος προσθέσαμε έναν ορισμό χάρτη woo: Αυτός είναι ένας πίνακας 8 στοιχείων που αποθηκεύει τιμές όπως u64 (στο C θα ορίζαμε έναν τέτοιο πίνακα ως u64 woo[8]). Σε ένα πρόγραμμα "xdp/simple" παίρνουμε τον τρέχοντα αριθμό επεξεργαστή σε μια μεταβλητή key και στη συνέχεια χρησιμοποιώντας τη συνάρτηση βοηθού bpf_map_lookup_element παίρνουμε δείκτη στην αντίστοιχη καταχώρηση στον πίνακα, τον οποίο αυξάνουμε κατά ένα. Μετάφραση στα ρωσικά: υπολογίζουμε στατιστικά στοιχεία για τα οποία η CPU επεξεργάστηκε τα εισερχόμενα πακέτα. Ας προσπαθήσουμε να εκτελέσουμε το πρόγραμμα:

$ 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

Ας ελέγξουμε ότι είναι συνδεδεμένη 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 108

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done

Τώρα ας δούμε τα περιεχόμενα του πίνακα:

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

Почти все процессы были обработаны на CPU7. Нам это не важно, главное, что программа работает и мы поняли как получить доступ к мапам из программ BPF — при помощи хелперов bpf_mp_*.

Μυστικός ευρετήριο

Έτσι, μπορούμε να έχουμε πρόσβαση στον χάρτη από το πρόγραμμα BPF χρησιμοποιώντας κλήσεις όπως

val = bpf_map_lookup_elem(&woo, &key);

όπου φαίνεται η συνάρτηση βοηθού

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

αλλά περνάμε έναν δείκτη &woo σε μια ανώνυμη δομή struct { ... }...

Αν κοιτάξουμε το πρόγραμμα συναρμολόγησης, βλέπουμε ότι η τιμή &woo δεν ορίζεται στην πραγματικότητα (γραμμή 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
...

και περιέχεται στις μετεγκαταστάσεις:

$ 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

Αλλά αν κοιτάξουμε το ήδη φορτωμένο πρόγραμμα, βλέπουμε έναν δείκτη προς τον σωστό χάρτη (γραμμή 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]
...

Έτσι, μπορούμε να συμπεράνουμε ότι τη στιγμή της εκκίνησης του προγράμματος φόρτωσης, ο σύνδεσμος προς &woo αντικαταστάθηκε από κάτι με βιβλιοθήκη libbpf. Πρώτα θα δούμε την έξοδο 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

Το βλέπουμε αυτό libbpf δημιούργησε έναν χάρτη woo και στη συνέχεια κατέβασα το πρόγραμμά μας simple. Ας ρίξουμε μια πιο προσεκτική ματιά στον τρόπο φόρτωσης του προγράμματος:

  • κλήση xdp_simple_bpf__open_and_load από αρχείο xdp-simple.skel.h
  • που προκαλεί xdp_simple_bpf__load από αρχείο xdp-simple.skel.h
  • που προκαλεί bpf_object__load_skeleton από αρχείο libbpf/src/libbpf.c
  • που προκαλεί bpf_object__load_xattr του libbpf/src/libbpf.c

Η τελευταία συνάρτηση, μεταξύ άλλων, θα καλέσει bpf_object__create_maps, το οποίο δημιουργεί ή ανοίγει υπάρχοντες χάρτες, μετατρέποντάς τους σε περιγραφείς αρχείων. (Εδώ βλέπουμε BPF_MAP_CREATE στην έξοδο strace.) Στη συνέχεια καλείται η συνάρτηση bpf_object__relocate και είναι αυτή που μας ενδιαφέρει, αφού θυμόμαστε τι είδαμε woo в таблице релокаций. Исследуя ее, мы, в конце-концов попадаем в функцию bpf_program__relocate, οι οποίες ασχολείται με τις μετακινήσεις χαρτών:

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

Παίρνουμε λοιπόν τις οδηγίες μας

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

και αντικαταστήστε τον καταχωρητή πηγής σε αυτό με BPF_PSEUDO_MAP_FD, και το πρώτο IMM στον περιγραφέα αρχείου του χάρτη μας και, αν είναι ίσο, για παράδειγμα, με 0xdeadbeef, τότε ως αποτέλεσμα θα λάβουμε την οδηγία

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

Αυτός είναι ο τρόπος με τον οποίο μεταφέρονται οι πληροφορίες χάρτη σε ένα συγκεκριμένο φορτωμένο πρόγραμμα BPF. Σε αυτήν την περίπτωση, ο χάρτης μπορεί να δημιουργηθεί χρησιμοποιώντας BPF_MAP_CREATE, και άνοιξε με ID χρησιμοποιώντας BPF_MAP_GET_FD_BY_ID.

Σύνολο, κατά τη χρήση libbpf ο αλγόριθμος είναι ο εξής:

  • κατά τη μεταγλώττιση, δημιουργούνται εγγραφές στον πίνακα μετεγκατάστασης για συνδέσμους προς χάρτες
  • libbpf ανοίγει το βιβλίο αντικειμένων ELF, βρίσκει όλους τους χρησιμοποιημένους χάρτες και δημιουργεί περιγραφές αρχείων για αυτούς
  • Οι περιγραφείς αρχείων φορτώνονται στον πυρήνα ως μέρος της εντολής LD64

Όπως μπορείτε να φανταστείτε, έρχονται περισσότερα και θα πρέπει να εξετάσουμε τον πυρήνα. Ευτυχώς, έχουμε μια ιδέα - έχουμε γράψει το νόημα BPF_PSEUDO_MAP_FD στο μητρώο πηγών και μπορούμε να το θάψουμε, το οποίο θα μας οδηγήσει στα άγια των αγίων - kernel/bpf/verifier.c, όπου μια συνάρτηση με διακριτικό όνομα αντικαθιστά έναν περιγραφέα αρχείου με τη διεύθυνση μιας δομής τύπου 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;

(μπορείτε να βρείτε τον πλήρη κωδικό по ссылке). Μπορούμε λοιπόν να επεκτείνουμε τον αλγόριθμό μας:

  • κατά τη φόρτωση του προγράμματος, ο επαληθευτής ελέγχει τη σωστή χρήση του χάρτη και γράφει τη διεύθυνση της αντίστοιχης δομής struct bpf_map

Κατά τη λήψη του δυαδικού ELF χρησιμοποιώντας libbpf Συμβαίνουν πολλά περισσότερα, αλλά θα το συζητήσουμε σε άλλα άρθρα.

Φόρτωση προγραμμάτων και χαρτών χωρίς libbpf

Όπως υποσχέθηκε, εδώ είναι ένα παράδειγμα για τους αναγνώστες που θέλουν να μάθουν πώς να δημιουργούν και να φορτώνουν ένα πρόγραμμα που χρησιμοποιεί χάρτες, χωρίς βοήθεια libbpf. Αυτό μπορεί να είναι χρήσιμο όταν εργάζεστε σε ένα περιβάλλον για το οποίο δεν μπορείτε να δημιουργήσετε εξαρτήσεις ή να αποθηκεύσετε κάθε κομμάτι ή να γράφετε ένα πρόγραμμα όπως ply, το οποίο δημιουργεί εν κινήσει δυαδικό κώδικα BPF.

Για να είναι πιο εύκολο να ακολουθήσουμε τη λογική, θα ξαναγράψουμε το παράδειγμά μας για αυτούς τους σκοπούς xdp-simple. Ο πλήρης και ελαφρώς διευρυμένος κώδικας του προγράμματος που συζητήθηκε σε αυτό το παράδειγμα βρίσκεται σε αυτό ουσία.

Η λογική της εφαρμογής μας είναι η εξής:

  • δημιουργήστε έναν χάρτη τύπου BPF_MAP_TYPE_ARRAY χρησιμοποιώντας την εντολή BPF_MAP_CREATE,
  • δημιουργήστε ένα πρόγραμμα που χρησιμοποιεί αυτόν τον χάρτη,
  • συνδέστε το πρόγραμμα στη διεπαφή lo,

που μεταφράζεται σε ανθρώπινο ως

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

Εδώ map_create δημιουργεί έναν χάρτη με τον ίδιο τρόπο όπως κάναμε στο πρώτο παράδειγμα σχετικά με την κλήση συστήματος bpf - «πυρήνας, σε παρακαλώ φτιάξε μου έναν νέο χάρτη με τη μορφή ενός πίνακα 8 στοιχείων όπως __u64 και δώστε μου πίσω τον περιγραφέα αρχείου":

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

Το πρόγραμμα είναι επίσης εύκολο στη φόρτωση:

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

Το δύσκολο κομμάτι prog_load είναι ο ορισμός του προγράμματος BPF ως μια σειρά δομών struct bpf_insn insns[]. Αλλά επειδή χρησιμοποιούμε ένα πρόγραμμα που έχουμε στο C, μπορούμε να εξαπατήσουμε λίγο:

$ 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

Итого, нам нужно написать 14 инструкций в виде структур типа struct bpf_insn (συμβουλή: πάρτε τη χωματερή από πάνω, διαβάστε ξανά την ενότητα οδηγιών, ανοίξτε linux/bpf.h и linux/bpf_common.h και προσπαθήστε να προσδιορίσετε struct bpf_insn insns[] μόνος του):

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

Μια άσκηση για όσους δεν το έγραψαν οι ίδιοι - βρείτε map_fd.

Έχει απομείνει ένα ακόμη αδιευκρίνιστο μέρος στο πρόγραμμά μας - xdp_attach. Δυστυχώς, προγράμματα όπως το XDP δεν μπορούν να συνδεθούν χρησιμοποιώντας κλήση συστήματος bpf. Τα άτομα που δημιούργησαν το BPF και το XDP ήταν από την διαδικτυακή κοινότητα Linux, πράγμα που σημαίνει ότι χρησιμοποίησαν αυτό που τους είναι πιο οικείο (αλλά όχι κανονικός άνθρωποι) διεπαφή για αλληλεπίδραση με τον πυρήνα: υποδοχές netlink, δείτε επίσης RFC3549. Ο απλούστερος τρόπος υλοποίησης xdp_attach αντιγράφει κώδικα από libbpf, δηλαδή, από το αρχείο netlink.c, το οποίο κάναμε, συντομεύοντάς το λίγο:

Καλώς ήρθατε στον κόσμο των υποδοχών netlink

Ανοίξτε έναν τύπο υποδοχής σύνδεσης δικτύου 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;
}

Διαβάζουμε από αυτή την πρίζα:

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

Τέλος, εδώ είναι η συνάρτησή μας που ανοίγει μια υποδοχή και της στέλνει ένα ειδικό μήνυμα που περιέχει έναν περιγραφέα αρχείου:

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

Έτσι, όλα είναι έτοιμα για δοκιμή:

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

Ας δούμε αν έχει συνδεθεί το πρόγραμμά μας 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

Ας στείλουμε ping και ας δούμε τον χάρτη:

$ 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

Ούρα, όλα λειτουργούν. Σημειώστε, παρεμπιπτόντως, ότι ο χάρτης μας εμφανίζεται και πάλι σε μορφή byte. Αυτό οφείλεται στο γεγονός ότι, σε αντίθεση libbpf δεν φορτώσαμε πληροφορίες τύπου (BTF). Αλλά θα μιλήσουμε περισσότερο για αυτό την επόμενη φορά.

Εργαλεία ανάπτυξης

Σε αυτήν την ενότητα, θα εξετάσουμε την ελάχιστη εργαλειοθήκη προγραμματιστών BPF.

Σε γενικές γραμμές, δεν χρειάζεστε κάτι ιδιαίτερο για την ανάπτυξη προγραμμάτων BPF - το BPF εκτελείται σε οποιονδήποτε αξιοπρεπή πυρήνα διανομής και τα προγράμματα δημιουργούνται χρησιμοποιώντας clang, το οποίο μπορεί να προμηθευτεί από τη συσκευασία. Ωστόσο, λόγω του γεγονότος ότι το BPF βρίσκεται υπό ανάπτυξη, ο πυρήνας και τα εργαλεία αλλάζουν συνεχώς, εάν δεν θέλετε να γράψετε προγράμματα BPF χρησιμοποιώντας παλιομοδίτιες μεθόδους από το 2019, τότε θα πρέπει να μεταγλωττίσετε

  • llvm/clang
  • pahole
  • свое ядро
  • bpftool

(Для справки: этот раздел и все примеры в статье запускались на Debian 10.)

llvm/clang

Το BPF είναι φιλικό με το LLVM και, αν και πρόσφατα τα προγράμματα για το BPF μπορούν να μεταγλωττιστούν χρησιμοποιώντας gcc, όλη η τρέχουσα ανάπτυξη πραγματοποιείται για το LLVM. Επομένως, πρώτα απ 'όλα, θα δημιουργήσουμε την τρέχουσα έκδοση clang από το 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
... много времени спустя
$

Τώρα μπορούμε να ελέγξουμε αν όλα συνδυάστηκαν σωστά:

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

(Οδηγίες συναρμολόγησης clang λήφθηκε από εμένα από bpf_devel_QA.)

Δεν θα εγκαταστήσουμε τα προγράμματα που μόλις δημιουργήσαμε, αλλά απλώς θα τα προσθέσουμε PATH, για παράδειγμα:

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

(Αυτό μπορεί να προστεθεί σε .bashrc ή σε ξεχωριστό αρχείο. Προσωπικά, προσθέτω τέτοια πράγματα ~/bin/activate-llvm.sh και όταν χρειάζεται το κάνω . activate-llvm.sh.)

Pahole и BTF

Χρησιμότητα pahole χρησιμοποιείται κατά την κατασκευή του πυρήνα για τη δημιουργία πληροφοριών εντοπισμού σφαλμάτων σε μορφή BTF. Δεν θα αναφερθούμε σε λεπτομέρειες σε αυτό το άρθρο σχετικά με τις λεπτομέρειες της τεχνολογίας BTF, εκτός από το γεγονός ότι είναι βολικό και θέλουμε να τη χρησιμοποιήσουμε. Επομένως, εάν πρόκειται να δημιουργήσετε τον πυρήνα σας, δημιουργήστε πρώτα pahole (χωρίς pahole δεν θα μπορείτε να δημιουργήσετε τον πυρήνα με την επιλογή 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

Πυρήνες για πειραματισμό με BPF

Όταν διερευνώ τις δυνατότητες του BPF, θέλω να συναρμολογήσω τον δικό μου πυρήνα. Αυτό, σε γενικές γραμμές, δεν είναι απαραίτητο, καθώς θα μπορείτε να μεταγλωττίζετε και να φορτώνετε προγράμματα BPF στον πυρήνα διανομής, ωστόσο, έχοντας τον δικό σας πυρήνα σας επιτρέπει να χρησιμοποιείτε τις πιο πρόσφατες δυνατότητες BPF, οι οποίες θα εμφανίζονται στη διανομή σας σε μήνες στην καλύτερη περίπτωση , ή, όπως στην περίπτωση ορισμένων εργαλείων εντοπισμού σφαλμάτων, δεν θα συσκευαστούν καθόλου στο άμεσο μέλλον. Επίσης, ο δικός του πυρήνας καθιστά σημαντικό να πειραματιστείτε με τον κώδικα.

Για να δημιουργήσετε έναν πυρήνα χρειάζεστε, πρώτον, τον ίδιο τον πυρήνα και δεύτερον, ένα αρχείο διαμόρφωσης πυρήνα. Για να πειραματιστούμε με το BPF μπορούμε να χρησιμοποιήσουμε το συνηθισμένο βανίλια πυρήνα ή έναν από τους πυρήνες ανάπτυξης. Ιστορικά, η ανάπτυξη του BPF λαμβάνει χώρα εντός της κοινότητας δικτύων Linux και επομένως όλες οι αλλαγές αργά ή γρήγορα περνούν από τον David Miller, τον συντηρητή δικτύων Linux. Ανάλογα με τη φύση τους - τροποποιήσεις ή νέες δυνατότητες - οι αλλαγές δικτύου εμπίπτουν σε έναν από τους δύο πυρήνες - net ή net-next. Οι αλλαγές για το BPF κατανέμονται με τον ίδιο τρόπο μεταξύ bpf и bpf-next, τα οποία στη συνέχεια συγκεντρώνονται σε net και net-next, αντίστοιχα. Για περισσότερες λεπτομέρειες, βλ bpf_devel_QA и netdev-FAQ. Επιλέξτε λοιπόν έναν πυρήνα με βάση το γούστο σας και τις ανάγκες σταθερότητας του συστήματος στο οποίο δοκιμάζετε (*-next οι πυρήνες είναι οι πιο ασταθείς από αυτούς που αναφέρονται).

Είναι πέρα ​​από το πεδίο αυτού του άρθρου να μιλήσουμε για τον τρόπο διαχείρισης των αρχείων διαμόρφωσης πυρήνα - υποτίθεται ότι είτε γνωρίζετε ήδη πώς να το κάνετε αυτό, είτε έτοιμο να μάθει μόνος του. Ωστόσο, οι παρακάτω οδηγίες θα πρέπει να είναι λίγο πολύ αρκετές για να σας δώσουν ένα λειτουργικό σύστημα με δυνατότητα BPF.

Κατεβάστε έναν από τους παραπάνω πυρήνες:

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

Δημιουργήστε μια ελάχιστη λειτουργική διαμόρφωση πυρήνα:

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

Ενεργοποιήστε τις επιλογές BPF στο αρχείο .config της επιλογής σας (πιθανότατα CONFIG_BPF θα είναι ήδη ενεργοποιημένο αφού το systemd το χρησιμοποιεί). Ακολουθεί μια λίστα επιλογών από τον πυρήνα που χρησιμοποιείται για αυτό το άρθρο:

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

Στη συνέχεια, μπορούμε εύκολα να συναρμολογήσουμε και να εγκαταστήσουμε τις μονάδες και τον πυρήνα (παρεμπιπτόντως, μπορείτε να συναρμολογήσετε τον πυρήνα χρησιμοποιώντας το πρόσφατα συναρμολογημένο clangπροσθέτοντας CC=clang):

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

και επανεκκινήστε με τον νέο πυρήνα (χρησιμοποιώ για αυτό kexec από τη συσκευασία 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

Το βοηθητικό πρόγραμμα που χρησιμοποιείται πιο συχνά στο άρθρο θα είναι το βοηθητικό πρόγραμμα bpftool, που παρέχεται ως μέρος του πυρήνα Linux. Είναι γραμμένο και συντηρείται από προγραμματιστές BPF για προγραμματιστές BPF και μπορεί να χρησιμοποιηθεί για τη διαχείριση όλων των τύπων αντικειμένων BPF - φόρτωση προγραμμάτων, δημιουργία και επεξεργασία χαρτών, εξερεύνηση της ζωής του οικοσυστήματος BPF κ.λπ. Μπορείτε να βρείτε τεκμηρίωση με τη μορφή πηγαίου κώδικα για σελίδες man στον πυρήνα ή, ήδη μεταγλωττισμένο, δίκτυο.

Τη στιγμή της συγγραφής αυτής bpftool διατίθεται έτοιμο μόνο για RHEL, Fedora και Ubuntu (δείτε, για παράδειγμα, этот тред, που αφηγείται την ημιτελή ιστορία της συσκευασίας bpftool στο Debian). Αλλά αν έχετε ήδη δημιουργήσει τον πυρήνα σας, τότε κάντε build bpftool πανεύκολος:

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

$

(εδώ ${linux} - αυτός είναι ο κατάλογος του πυρήνα σας.) Μετά την εκτέλεση αυτών των εντολών bpftool будет собрана в директории ${linux}/tools/bpf/bpftool και μπορεί να προστεθεί στη διαδρομή (πρώτα από όλα στον χρήστη root) ή απλώς αντιγράψτε σε /usr/local/sbin.

Συλλέγω bpftool είναι καλύτερο να χρησιμοποιήσετε το τελευταίο clang, συναρμολογήθηκε όπως περιγράφεται παραπάνω και ελέγξτε εάν έχει συναρμολογηθεί σωστά - χρησιμοποιώντας, για παράδειγμα, την εντολή

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

που θα δείξει ποιες δυνατότητες BPF είναι ενεργοποιημένες στον πυρήνα σας.

Παρεμπιπτόντως, η προηγούμενη εντολή μπορεί να εκτελεστεί ως

# bpftool f p k

Это сделано по аналогии с утилитами из пакета iproute2, όπου μπορούμε, για παράδειγμα, να πούμε ip a s eth0 αντί για ip addr show dev eth0.

Συμπέρασμα

BPF позволяет подковать блоху эффективно измерять и налету изменять функциональность ядра. Система получилась очень удачной, в лучших традициях UNIX: простой механизм, позволяющий (пере)программировать ядро, позволил огромному количеству людей и организаций экспериментировать. И, хотя эксперименты, так же как и развитие самой инфраструктуры BPF, еще далеко не закончены, система уже имеет стабильное ABI, позволяющее выстраивать надежную, а главное, эффективную бизнес-логику.

Θα ήθελα να σημειώσω ότι, κατά τη γνώμη μου, η τεχνολογία έχει γίνει τόσο δημοφιλής επειδή, αφενός, μπορεί για να παίξει (η αρχιτεκτονική μιας μηχανής μπορεί να γίνει κατανοητή λίγο πολύ σε ένα βράδυ), και από την άλλη, να λύνει προβλήματα που δεν μπορούσαν να λυθούν (όμορφα) πριν από την εμφάνισή της. Αυτά τα δύο στοιχεία μαζί αναγκάζουν τους ανθρώπους να πειραματιστούν και να ονειρεύονται, γεγονός που οδηγεί στην εμφάνιση ολοένα και περισσότερων καινοτόμων λύσεων.

Αυτό το άρθρο, αν και δεν είναι ιδιαίτερα σύντομο, είναι μόνο μια εισαγωγή στον κόσμο του BPF και δεν περιγράφει «προηγμένα» χαρακτηριστικά και σημαντικά μέρη της αρχιτεκτονικής. Το σχέδιο που θα ακολουθήσει είναι κάπως έτσι: το επόμενο άρθρο θα είναι μια επισκόπηση των τύπων προγραμμάτων BPF (υπάρχουν 5.8 τύποι προγραμμάτων που υποστηρίζονται στον πυρήνα 30), και στη συνέχεια θα δούμε τελικά πώς να γράψουμε πραγματικές εφαρμογές BPF χρησιμοποιώντας προγράμματα ανίχνευσης πυρήνα Ως παράδειγμα, τότε ήρθε η ώρα για ένα πιο εμπεριστατωμένο μάθημα σχετικά με την αρχιτεκτονική BPF, ακολουθούμενο από παραδείγματα εφαρμογών δικτύωσης και ασφάλειας BPF.

Προηγούμενα άρθρα αυτής της σειράς

  1. BPF για τα μικρά, μέρος μηδέν: κλασικό BPF

Συνδέσεις

  1. Οδηγός αναφοράς BPF και XDP — τεκμηρίωση για το BPF από cilium, ή ακριβέστερα από τον Daniel Borkman, έναν από τους δημιουργούς και συντηρητές του BPF. Αυτή είναι μια από τις πρώτες σοβαρές περιγραφές, που διαφέρει από τις άλλες στο ότι ο Ντάνιελ ξέρει ακριβώς τι γράφει και δεν υπάρχουν λάθη εκεί. Συγκεκριμένα, αυτό το έγγραφο περιγράφει τον τρόπο εργασίας με προγράμματα BPF των τύπων XDP και TC χρησιμοποιώντας το γνωστό βοηθητικό πρόγραμμα ip από τη συσκευασία iproute2.

  2. Τεκμηρίωση/δικτύωση/φίλτρο.txt — πρωτότυπο αρχείο με τεκμηρίωση για το κλασικό και στη συνέχεια εκτεταμένο BPF. Μια καλή ανάγνωση αν θέλετε να εμβαθύνετε στη γλώσσα συναρμολόγησης και στις τεχνικές αρχιτεκτονικές λεπτομέρειες.

  3. Blog για το BPF από το facebook. Ενημερώνεται σπάνια, αλλά εύστοχα, όπως γράφουν εκεί ο Alexei Starovoitov (συγγραφέας του eBPF) και ο Andrii Nakryiko - (συντηρητής) libbpf).

  4. Τα μυστικά του bpftool. Ένα διασκεδαστικό νήμα στο twitter από τον Quentin Monnet με παραδείγματα και μυστικά χρήσης του bpftool.

  5. Dive into BPF: a list of reading material. Μια τεράστια (και διατηρείται ακόμα) λίστα με συνδέσμους προς την τεκμηρίωση του BPF από τον Quentin Monnet.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο