Pada awalnya ada sebuah teknologi yang disebut BPF. Kami memandangnya sebelumnya, artikel Perjanjian Lama dari seri ini. Pada tahun 2013, melalui upaya Alexei Starovoitov dan Daniel Borkman, versi perbaikannya, yang dioptimalkan untuk mesin 64-bit modern, dikembangkan dan dimasukkan ke dalam kernel Linux. Teknologi baru ini sempat disebut Internal BPF, kemudian berganti nama menjadi Extended BPF, dan kini, setelah beberapa tahun, semua orang menyebutnya BPF.
Secara kasar, BPF memungkinkan Anda menjalankan kode sewenang-wenang yang disediakan pengguna di ruang kernel Linux, dan arsitektur baru ini ternyata sangat sukses sehingga kami memerlukan selusin artikel lagi untuk menjelaskan semua aplikasinya. (Satu-satunya hal yang tidak dilakukan dengan baik oleh pengembang, seperti yang Anda lihat pada kode kinerja di bawah, adalah membuat logo yang layak.)
Artikel ini menjelaskan struktur mesin virtual BPF, antarmuka kernel untuk bekerja dengan BPF, alat pengembangan, serta gambaran singkat, sangat singkat tentang kemampuan yang ada, yaitu. segala sesuatu yang kita perlukan di masa depan untuk mempelajari lebih dalam penerapan praktis BPF.
Ringkasan artikel
Pengantar arsitektur BPF. Pertama, kita akan melihat sekilas arsitektur BPF dan menguraikan komponen utamanya.
Mengelola objek menggunakan panggilan sistem bpf. Dengan beberapa pemahaman tentang sistem yang sudah ada, kita akhirnya akan melihat cara membuat dan memanipulasi objek dari ruang pengguna menggunakan panggilan sistem khusus - bpf(2).
ΠΠΈΡΠ΅ΠΌ ΠΏΡΠΎΠ³ΡΠ°ΠΌΠΌΡ BPF Ρ ΠΏΠΎΠΌΠΎΡΡΡ libbpf. Tentu saja, Anda dapat menulis program menggunakan system call. Tapi itu sulit. Untuk skenario yang lebih realistis, pemrogram nuklir mengembangkan perpustakaan libbpf. Kita akan membuat kerangka aplikasi BPF dasar yang akan kita gunakan pada contoh berikutnya.
Pembantu Kernel. Di sini kita akan mempelajari bagaimana program BPF dapat mengakses fungsi pembantu kernel - alat yang, bersama dengan peta, secara mendasar memperluas kemampuan BPF baru dibandingkan dengan BPF klasik.
Akses ke peta dari program BPF. Pada titik ini, kita sudah cukup mengetahui untuk memahami secara pasti bagaimana kita dapat membuat program yang menggunakan peta. Dan mari kita intip sekilas ke dalam verifikator yang hebat dan perkasa.
Alat pengembangan. Bagian bantuan tentang cara merakit utilitas dan kernel yang diperlukan untuk eksperimen.
Kesimpulan. Di akhir artikel, bagi yang membaca sejauh ini akan menemukan kata-kata motivasi dan gambaran singkat tentang apa yang akan terjadi pada artikel-artikel berikut ini. Kami juga akan mencantumkan beberapa link untuk belajar mandiri bagi yang tidak memiliki keinginan atau kemampuan untuk menunggu kelanjutannya.
Pengantar Arsitektur BPF
Sebelum kita mulai mempertimbangkan arsitektur BPF, kita akan merujuknya untuk terakhir kalinya (oh). BPF klasik, yang dikembangkan sebagai respons terhadap munculnya mesin RISC dan memecahkan masalah pemfilteran paket yang efisien. Arsitekturnya ternyata sangat sukses sehingga, lahir pada tahun sembilan puluhan di Berkeley UNIX, ia di-porting ke sebagian besar sistem operasi yang ada, bertahan hingga tahun dua puluhan yang gila dan masih menemukan aplikasi baru.
BPF baru dikembangkan sebagai respons terhadap keberadaan mesin 64-bit, layanan cloud, dan meningkatnya kebutuhan akan alat untuk membuat SDN (Sperangkat lunak-dhalus nkerja sama). Dikembangkan oleh para insinyur jaringan kernel sebagai pengganti yang lebih baik untuk BPF klasik, BPF baru enam bulan kemudian menemukan aplikasi dalam tugas sulit menelusuri sistem Linux, dan sekarang, enam tahun setelah kemunculannya, kita memerlukan artikel berikutnya hanya untuk daftar berbagai jenis program.
Gambar lucu
Pada intinya, BPF adalah mesin virtual sandbox yang memungkinkan Anda menjalankan kode βsewenang-wenangβ di ruang kernel tanpa mengorbankan keamanan. Program BPF dibuat di ruang pengguna, dimuat ke dalam kernel, dan dihubungkan ke beberapa sumber peristiwa. Suatu peristiwa dapat berupa, misalnya, pengiriman paket ke antarmuka jaringan, peluncuran beberapa fungsi kernel, dll. Dalam hal sebuah paket, program BPF akan memiliki akses ke data dan metadata paket tersebut (untuk membaca dan, mungkin, menulis, tergantung pada jenis program); dalam hal menjalankan fungsi kernel, argumen dari fungsinya, termasuk pointer ke memori kernel, dll.
Mari kita lihat lebih dekat proses ini. Untuk memulainya, mari kita bicara tentang perbedaan pertama dari BPF klasik, program yang ditulis dalam assembler. Dalam versi baru, arsitekturnya diperluas sehingga program dapat ditulis dalam bahasa tingkat tinggi, terutama, tentu saja, dalam C. Untuk ini, backend untuk llvm dikembangkan, yang memungkinkan Anda menghasilkan bytecode untuk arsitektur BPF.
Arsitektur BPF dirancang, sebagian, untuk berjalan secara efisien pada mesin modern. Agar hal ini dapat diterapkan, bytecode BPF, setelah dimuat ke dalam kernel, diterjemahkan ke dalam kode asli menggunakan komponen yang disebut kompiler JIT (JUst In Twaktu). Selanjutnya, jika Anda ingat, di BPF klasik, program dimuat ke dalam kernel dan dilampirkan ke sumber peristiwa secara atom - dalam konteks satu panggilan sistem. Dalam arsitektur baru, ini terjadi dalam dua tahap - pertama, kode dimuat ke dalam kernel menggunakan panggilan sistem bpf(2)dan kemudian, melalui mekanisme lain yang berbeda-beda bergantung pada jenis program, program tersebut melekat pada sumber peristiwa.
Di sini pembaca mungkin bertanya-tanya: apakah mungkin? Bagaimana keamanan eksekusi kode tersebut dijamin? Keamanan eksekusi dijamin bagi kami melalui tahap memuat program BPF yang disebut verifier (dalam bahasa Inggris tahap ini disebut verifier dan saya akan terus menggunakan kata bahasa Inggris):
Verifier adalah penganalisis statis yang memastikan bahwa suatu program tidak mengganggu operasi normal kernel. Omong-omong, ini tidak berarti bahwa program tidak dapat mengganggu pengoperasian sistem - program BPF, tergantung pada jenisnya, dapat membaca dan menulis ulang bagian memori kernel, mengembalikan nilai fungsi, memangkas, menambahkan, menulis ulang dan bahkan meneruskan paket jaringan. Verifier menjamin bahwa menjalankan program BPF tidak akan membuat kernel crash dan program yang menurut aturan memiliki akses tulis, misalnya data paket keluar, tidak akan dapat menimpa memori kernel di luar paket. Kita akan melihat verifier lebih detail di bagian terkait, setelah kita mengenal semua komponen BPF lainnya.
Jadi apa yang telah kita pelajari sejauh ini? Pengguna menulis program dalam C, memuatnya ke kernel menggunakan panggilan sistem bpf(2), yang diperiksa oleh verifikator dan diterjemahkan ke dalam bytecode asli. Kemudian pengguna yang sama atau pengguna lain menghubungkan program ke sumber peristiwa dan mulai dijalankan. Memisahkan boot dan koneksi diperlukan karena beberapa alasan. Pertama, menjalankan verifikator relatif mahal dan dengan mendownload program yang sama beberapa kali kita membuang-buang waktu komputer. Kedua, bagaimana tepatnya suatu program dihubungkan bergantung pada jenisnya, dan satu antarmuka βuniversalβ yang dikembangkan setahun yang lalu mungkin tidak cocok untuk jenis program baru. (Meskipun sekarang arsitekturnya menjadi lebih matang, ada ide untuk menyatukan antarmuka ini pada level tersebut libbpf.)
Pembaca yang penuh perhatian mungkin memperhatikan bahwa kita belum selesai dengan gambarnya. Memang, semua hal di atas tidak menjelaskan mengapa BPF mengubah gambaran secara mendasar dibandingkan dengan BPF klasik. Dua inovasi yang secara signifikan memperluas cakupan penerapannya adalah kemampuan untuk menggunakan memori bersama dan fungsi pembantu kernel. Di BPF, memori bersama diimplementasikan menggunakan apa yang disebut peta - struktur data bersama dengan API tertentu. Mereka mungkin mendapat nama ini karena jenis peta pertama yang muncul adalah tabel hash. Kemudian muncul array, tabel hash lokal (per-CPU) dan array lokal, pohon pencarian, peta yang berisi pointer ke program BPF dan banyak lagi. Yang menarik bagi kami sekarang adalah bahwa program BPF kini memiliki kemampuan untuk mempertahankan status di antara panggilan dan membaginya dengan program lain dan dengan ruang pengguna.
Peta diakses dari proses pengguna menggunakan panggilan sistem bpf(2), dan dari program BPF yang berjalan di kernel menggunakan fungsi pembantu. Selain itu, helper ada tidak hanya untuk bekerja dengan peta, tetapi juga untuk mengakses kemampuan kernel lainnya. Misalnya, program BPF dapat menggunakan fungsi pembantu untuk meneruskan paket ke antarmuka lain, menghasilkan peristiwa kinerja, mengakses struktur kernel, dan sebagainya.
Singkatnya, BPF menyediakan kemampuan untuk memuat kode pengguna yang sewenang-wenang, yaitu yang telah diuji oleh verifikator, ke dalam ruang kernel. Kode ini dapat menyimpan status antara panggilan dan pertukaran data dengan ruang pengguna, dan juga memiliki akses ke subsistem kernel yang diizinkan oleh program jenis ini.
Ini sudah mirip dengan kemampuan yang disediakan oleh modul kernel, dibandingkan dengan BPF yang memiliki beberapa keunggulan (tentu saja, Anda hanya dapat membandingkan aplikasi serupa, misalnya, penelusuran sistem - Anda tidak dapat menulis driver sembarangan dengan BPF). Anda dapat mencatat ambang masuk yang lebih rendah (beberapa utilitas yang menggunakan BPF tidak mengharuskan pengguna untuk memiliki keterampilan pemrograman kernel, atau keterampilan pemrograman secara umum), keamanan runtime (angkat tangan Anda di komentar bagi mereka yang tidak merusak sistem saat menulis atau modul pengujian), atomisitas - ada waktu henti saat memuat ulang modul, dan subsistem BPF memastikan tidak ada kejadian yang terlewat (agar adil, hal ini tidak berlaku untuk semua jenis program BPF).
Kehadiran kemampuan tersebut menjadikan BPF alat universal untuk memperluas kernel, yang dikonfirmasi dalam praktik: semakin banyak jenis program baru yang ditambahkan ke BPF, semakin banyak perusahaan besar menggunakan BPF di server tempur 24Γ7, semakin banyak startup membangun bisnis mereka berdasarkan solusi berdasarkan BPF. BPF digunakan di mana-mana: dalam perlindungan terhadap serangan DDoS, pembuatan SDN (misalnya, implementasi jaringan untuk kubernetes), sebagai alat penelusuran sistem utama dan pengumpul statistik, dalam sistem deteksi intrusi dan sistem sandbox, dll.
Mari selesaikan bagian ikhtisar artikel di sini dan lihat mesin virtual dan ekosistem BPF secara lebih detail.
Penyimpangan: utilitas
Agar dapat menjalankan contoh di bagian berikut, Anda mungkin memerlukan setidaknya sejumlah utilitas llvm/clang dengan dukungan bpf dan bpftool. Di bagian Alat pengembangan Anda dapat membaca instruksi untuk merakit utilitas, serta kernel Anda. Bagian ini ditempatkan di bawah agar tidak mengganggu keselarasan presentasi kita.
Register Mesin Virtual BPF dan Sistem Instruksi
Arsitektur dan sistem perintah BPF dikembangkan dengan mempertimbangkan fakta bahwa program akan ditulis dalam bahasa C dan, setelah dimuat ke dalam kernel, diterjemahkan ke dalam kode asli. Oleh karena itu, jumlah register dan kumpulan perintah dipilih dengan memperhatikan perpotongan, dalam pengertian matematis, kemampuan mesin modern. Selain itu, berbagai pembatasan diberlakukan pada program, misalnya, hingga saat ini loop dan subrutin tidak dapat ditulis, dan jumlah instruksi dibatasi hingga 4096 (sekarang program dengan hak istimewa dapat memuat hingga satu juta instruksi).
BPF memiliki sebelas register 64-bit yang dapat diakses pengguna r0-r10 dan penghitung program. Daftar r10 berisi penunjuk bingkai dan bersifat hanya-baca. Program memiliki akses ke tumpukan 512-byte saat runtime dan jumlah memori bersama yang tidak terbatas dalam bentuk peta.
Program BPF diizinkan untuk menjalankan serangkaian pembantu kernel tipe program tertentu dan, yang lebih baru, fungsi reguler. Setiap fungsi yang dipanggil dapat memuat hingga lima argumen, diteruskan dalam register r1-r5, dan nilai kembalian diteruskan ke r0. Dijamin setelah kembali dari fungsinya, isi register r6-r9 Tidak akan berubah.
Untuk terjemahan program yang efisien, register r0-r11 untuk semua arsitektur yang didukung dipetakan secara unik ke register nyata, dengan mempertimbangkan fitur ABI dari arsitektur saat ini. Misalnya untuk x86_64 register r1-r5, digunakan untuk meneruskan parameter fungsi, ditampilkan rdi, rsi, rdx, rcx, r8, yang digunakan untuk meneruskan parameter ke fungsi aktif x86_64. Misalnya kode di sebelah kiri diterjemahkan ke kode di sebelah kanan seperti ini:
Daftar r0 juga digunakan untuk mengembalikan hasil eksekusi program, dan di register r1 program diberikan penunjuk ke konteksnya - tergantung pada jenis programnya, ini bisa berupa, misalnya, sebuah struktur struct xdp_md (untuk XDP) atau struktur struct __sk_buff (untuk program jaringan yang berbeda) atau struktur struct pt_regs (untuk berbagai jenis program penelusuran), dll.
Jadi, kami memiliki satu set register, pembantu kernel, tumpukan, penunjuk konteks, dan memori bersama dalam bentuk peta. Bukan berarti semua ini mutlak diperlukan dalam perjalanan, tapi...
Mari kita lanjutkan uraiannya dan berbicara tentang sistem perintah untuk bekerja dengan objek-objek ini. Semua (Hampir semua) Instruksi BPF memiliki ukuran tetap 64-bit. Jika Anda melihat satu instruksi pada mesin Big Endian 64-bit, Anda akan melihatnya
Di sini Code - ini adalah pengkodean instruksi, Dst/Src adalah pengkodean penerima dan sumber, masing-masing, Off - Lekukan bertanda tangan 16-bit, dan Imm adalah bilangan bulat bertanda 32-bit yang digunakan dalam beberapa instruksi (mirip dengan konstanta cBPF K). Pengkodean Code memiliki salah satu dari dua jenis:
Kelas instruksi 0, 1, 2, 3 mendefinisikan perintah untuk bekerja dengan memori. Mereka disebut, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, masing-masing. Kelas 4, 7 (BPF_ALU, BPF_ALU64) merupakan satu set instruksi ALU. Kelas 5, 6 (BPF_JMP, BPF_JMP32) berisi instruksi lompat.
Rencana selanjutnya untuk mempelajari sistem instruksi BPF adalah sebagai berikut: daripada mendaftar dengan cermat semua instruksi dan parameternya, kita akan melihat beberapa contoh di bagian ini dan dari contoh tersebut akan menjadi jelas bagaimana instruksi sebenarnya bekerja dan bagaimana caranya. secara manual membongkar file biner apa pun untuk BPF. Untuk mengkonsolidasikan materi nanti di artikel, kita juga akan bertemu dengan instruksi individual di bagian tentang Verifier, kompiler JIT, terjemahan BPF klasik, serta saat mempelajari peta, memanggil fungsi, dll.
Ketika kita berbicara tentang instruksi individual, kita akan mengacu pada file inti bpf.h ΠΈ bpf_common.h, yang menentukan kode numerik instruksi BPF. Saat mempelajari arsitektur sendiri dan/atau menguraikan biner, Anda dapat menemukan semantik di sumber berikut, diurutkan berdasarkan kompleksitas: Spesifikasi eBPF tidak resmi, Panduan Referensi BPF dan XDP, Set Instruksi, Dokumentasi/jaringan/filter.txt dan, tentu saja, dalam kode sumber Linux - verifier, JIT, juru bahasa BPF.
Contoh: membongkar BPF di kepala Anda
Mari kita lihat contoh di mana kita mengkompilasi sebuah program readelf-example.c dan lihat biner yang dihasilkan. Kami akan mengungkapkan konten aslinya readelf-example.c di bawah ini, setelah kita mengembalikan logikanya dari kode biner:
Kode perintahnya sama b7, 15, b7 ΠΈ 95. Ingatlah bahwa tiga bit paling tidak signifikan adalah kelas instruksi. Dalam kasus kita, bit keempat dari semua instruksi kosong, sehingga kelas instruksi masing-masing adalah 7, 5, 7, 5. Kelas 7 adalah BPF_ALU64, dan 5 adalah BPF_JMP. Untuk kedua kelas, format instruksinya sama (lihat di atas) dan kita dapat menulis ulang program kita seperti ini (pada saat yang sama kita akan menulis ulang kolom yang tersisa dalam bentuk manusia):
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
Operasi b kelas ALU64 - Apakah BPF_MOV. Ini memberikan nilai ke register tujuan. Jika bit sudah disetel s (sumber), maka nilainya diambil dari register sumber, dan jika, seperti dalam kasus kita, tidak disetel, maka nilainya diambil dari kolom Imm. Jadi pada instruksi pertama dan ketiga kami melakukan operasi r0 = Imm. Selanjutnya, operasi JMP kelas 1 adalah BPF_JEQ (melompat jika sama). Dalam kasus kami, sejak saat itu S adalah nol, maka nilai register sumber akan dibandingkan dengan nilai field Imm. Jika nilainya bertepatan, maka terjadi transisi ke PC + OffDimana PC, seperti biasa, berisi alamat instruksi selanjutnya. Terakhir, Operasi JMP Kelas 9 BPF_EXIT. Instruksi ini mengakhiri program, kembali ke kernel r0. Mari tambahkan kolom baru ke tabel kita:
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
Kita dapat menulis ulang ini dalam bentuk yang lebih mudah:
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exit
Jika kita ingat apa yang ada di register r1 program ini meneruskan pointer ke konteks dari kernel, dan di register r0 nilainya dikembalikan ke kernel, maka kita dapat melihat bahwa jika penunjuk ke konteksnya adalah nol, maka kita mengembalikan 1, dan sebaliknya - 2. Mari kita periksa apakah kita benar dengan melihat sumbernya:
Ya, ini adalah program yang tidak berarti, tetapi ini hanya diterjemahkan ke dalam empat instruksi sederhana.
Contoh pengecualian: instruksi 16-byte
Kami telah menyebutkan sebelumnya bahwa beberapa instruksi memerlukan lebih dari 64 bit. Hal ini berlaku, misalnya, untuk instruksi lddw (Kode = 0x18 = BPF_LD | BPF_DW | BPF_IMM) β memuat kata ganda dari kolom ke dalam register Imm. Titik adalah bahwa Imm memiliki ukuran 32, dan kata ganda berukuran 64 bit, jadi memuat nilai langsung 64-bit ke dalam register dalam satu instruksi 64-bit tidak akan berfungsi. Untuk melakukan ini, dua instruksi yang berdekatan digunakan untuk menyimpan bagian kedua dari nilai 64-bit di lapangan Imm. Contoh:
Kami akan bertemu lagi dengan instruksi lddw, ketika kita berbicara tentang relokasi dan bekerja dengan peta.
Contoh: membongkar BPF menggunakan alat standar
Jadi, kita telah belajar membaca kode biner BPF dan siap mengurai instruksi apa pun jika diperlukan. Namun, perlu dikatakan bahwa dalam praktiknya akan lebih mudah dan cepat untuk membongkar program menggunakan alat standar, misalnya:
(Saya pertama kali mempelajari beberapa detail yang dijelaskan dalam subbagian ini dari pos Alexei Starovoitov masuk Blog BPF.)
Objek BPF - program dan peta - dibuat dari ruang pengguna menggunakan perintah BPF_PROG_LOAD ΠΈ BPF_MAP_CREATE panggilan sistem bpf(2), kita akan membahas bagaimana tepatnya hal ini terjadi di bagian selanjutnya. Ini menciptakan struktur data kernel dan untuk masing-masing struktur tersebut refcount (jumlah referensi) disetel ke satu, dan deskriptor file yang menunjuk ke objek dikembalikan ke pengguna. Setelah pegangan ditutup refcount benda berkurang satu, dan ketika mencapai nol, benda tersebut musnah.
Jika programnya menggunakan peta, maka refcount peta-peta ini bertambah satu setelah memuat program, mis. deskriptor file mereka dapat ditutup dari proses pengguna dan masih refcount tidak akan menjadi nol:
Setelah berhasil memuat suatu program, biasanya kita melampirkannya ke semacam event generator. Misalnya, kita dapat meletakkannya di antarmuka jaringan untuk memproses paket masuk atau menghubungkannya ke beberapa paket tracepoint di inti. Pada titik ini, penghitung referensi juga akan bertambah satu dan kita akan dapat menutup deskriptor file di program pemuat.
Apa yang terjadi jika kita mematikan bootloader sekarang? Itu tergantung pada jenis generator acara (hook). Semua kait jaringan akan ada setelah pemuat selesai, inilah yang disebut kait global. Dan, misalnya, program jejak akan dirilis setelah proses yang membuatnya berakhir (dan oleh karena itu disebut lokal, dari βlokal ke prosesβ). Secara teknis, hook lokal selalu memiliki deskriptor file yang sesuai di ruang pengguna dan oleh karena itu ditutup ketika proses ditutup, tetapi hook global tidak. Pada gambar berikut, dengan menggunakan tanda silang merah, saya mencoba menunjukkan bagaimana penghentian program pemuat memengaruhi masa pakai objek dalam kasus kait lokal dan global.
Mengapa ada perbedaan antara kaitan lokal dan global? Menjalankan beberapa jenis program jaringan masuk akal tanpa ruang pengguna, misalnya, bayangkan perlindungan DDoS - bootloader menulis aturan dan menghubungkan program BPF ke antarmuka jaringan, setelah itu bootloader dapat mati dan mati sendiri. Di sisi lain, bayangkan program pelacakan debug yang Anda tulis dalam sepuluh menit - ketika selesai, Anda ingin tidak ada sampah yang tersisa di sistem, dan kait lokal akan memastikannya.
Di sisi lain, bayangkan Anda ingin terhubung ke tracepoint di kernel dan mengumpulkan statistik selama bertahun-tahun. Dalam hal ini, Anda ingin menyelesaikan bagian pengguna dan kembali ke statistik dari waktu ke waktu. Sistem file bpf memberikan kesempatan ini. Ini adalah sistem file semu dalam memori yang memungkinkan pembuatan file yang mereferensikan objek BPF dan dengan demikian meningkat refcount objek. Setelah ini, pemuat dapat keluar, dan objek yang dibuatnya akan tetap hidup.
Membuat file di bpffs yang mereferensikan objek BPF disebut "menyematkan" (seperti dalam frasa berikut: "proses dapat menyematkan program atau peta BPF"). Membuat objek file untuk objek BPF masuk akal tidak hanya untuk memperpanjang umur objek lokal, tetapi juga untuk kegunaan objek global - kembali ke contoh program perlindungan DDoS global, kami ingin dapat datang dan melihat statistik dari waktu ke waktu.
Sistem file BPF biasanya dipasang di /sys/fs/bpf, tetapi dapat juga dipasang secara lokal, misalnya seperti ini:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
Nama sistem file dibuat menggunakan perintah BPF_OBJ_PIN Panggilan sistem BPF. Sebagai ilustrasi, mari kita ambil sebuah program, kompilasi, unggah, dan sematkan bpffs. Program kami tidak melakukan sesuatu yang berguna, kami hanya menyajikan kode sehingga Anda dapat mereproduksi contohnya:
Sekarang mari kita unduh program kita menggunakan utilitas bpftool dan lihat panggilan sistem yang menyertainya bpf(2) (beberapa baris yang tidak relevan dihapus dari keluaran strace):
Di sini kami telah memuat program menggunakan BPF_PROG_LOAD, menerima deskriptor file dari kernel 3 dan menggunakan perintah BPF_OBJ_PIN menyematkan deskriptor file ini sebagai file "bpf-mountpoint/test". Setelah ini program bootloader bpftool selesai bekerja, tetapi program kami tetap berada di kernel, meskipun kami tidak melampirkannya ke antarmuka jaringan apa pun:
$ 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
Kita dapat menghapus objek file secara normal unlink(2) dan setelah itu program terkait akan dihapus:
$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory
Menghapus objek
Berbicara tentang penghapusan objek, perlu diklarifikasi bahwa setelah kita memutus program dari hook (generator peristiwa), tidak ada satu pun peristiwa baru yang akan memicu peluncurannya, namun, semua contoh program saat ini akan diselesaikan dalam urutan normal. .
Beberapa jenis program BPF memungkinkan Anda mengganti program dengan cepat, mis. memberikan atomisitas urutan replace = detach old program, attach new program. Dalam hal ini, semua instance aktif dari program versi lama akan menyelesaikan pekerjaannya, dan event handler baru akan dibuat dari program baru, dan βatomicityβ di sini berarti tidak ada satu event pun yang terlewat.
Melampirkan program ke sumber acara
Dalam artikel ini, kami tidak akan menjelaskan secara terpisah menghubungkan program ke sumber peristiwa, karena masuk akal untuk mempelajarinya dalam konteks jenis program tertentu. Cm. contoh di bawah ini, kami menunjukkan bagaimana program seperti XDP terhubung.
Memanipulasi Objek Menggunakan System Call bpf
program BPF
Semua objek BPF dibuat dan dikelola dari ruang pengguna menggunakan panggilan sistem bpf, memiliki prototipe berikut:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
Inilah timnya cmd adalah salah satu nilai tipe enum bpf_cmd, attr β penunjuk ke parameter untuk program tertentu dan size β ukuran objek menurut penunjuk, mis. biasanya ini sizeof(*attr). Di kernel 5.8 panggilan sistem bpf mendukung 34 perintah berbeda, dan definisiunion bpf_attr menempati 200 baris. Namun kita tidak boleh terintimidasi oleh hal ini, karena kita akan membiasakan diri dengan perintah dan parameter dalam beberapa artikel.
Mari kita mulai dengan tim BPF_PROG_LOAD, yang membuat program BPF - mengambil serangkaian instruksi BPF dan memuatnya ke dalam kernel. Pada saat memuat, pemverifikasi diluncurkan, dan kemudian kompiler JIT dan, setelah eksekusi berhasil, deskriptor file program dikembalikan ke pengguna. Kita melihat apa yang terjadi padanya selanjutnya di bagian sebelumnya tentang siklus hidup objek BPF.
Sekarang kita akan menulis program khusus yang akan memuat program BPF sederhana, tetapi pertama-tama kita perlu memutuskan jenis program apa yang ingin kita muat - kita harus memilih Ketik dan dalam kerangka jenis ini, tulislah sebuah program yang akan lulus uji verifikator. Namun, agar tidak mempersulit prosesnya, berikut solusi yang sudah jadi: kita akan mengambil program seperti BPF_PROG_TYPE_XDP, yang akan mengembalikan nilainya XDP_PASS (lewati semua paket). Di assembler BPF tampilannya sangat sederhana:
r0 = 2
exit
Setelah kita memutuskan bahwa kami akan mengunggah, kami dapat memberi tahu Anda bagaimana kami akan melakukannya:
Peristiwa menarik dalam suatu program dimulai dengan definisi array insns - program BPF kami dalam kode mesin. Dalam hal ini, setiap instruksi program BPF dikemas ke dalam struktur bpf_insn. Elemen pertama insns mematuhi instruksi r0 = 2, kedua - exit.
Mundur. Kernel mendefinisikan makro yang lebih nyaman untuk menulis kode mesin, dan menggunakan file header kernel tools/include/linux/filter.h kita bisa menulis
Namun karena menulis program BPF dalam kode asli hanya diperlukan untuk menulis tes di kernel dan artikel tentang BPF, tidak adanya makro ini tidak terlalu mempersulit kehidupan pengembang.
Setelah mendefinisikan program BPF, kami melanjutkan untuk memuatnya ke dalam kernel. Kumpulan parameter minimalis kami attr termasuk jenis program, set dan jumlah instruksi, lisensi yang diperlukan, dan nama "woo", yang kami gunakan untuk menemukan program kami di sistem setelah diunduh. Program ini, seperti yang dijanjikan, dimuat ke dalam sistem menggunakan panggilan sistem bpf.
Di akhir program, kita berakhir di loop tak terbatas yang mensimulasikan payload. Tanpanya, program akan dimatikan oleh kernel ketika deskriptor file yang dikembalikan oleh panggilan sistem kepada kita ditutup bpf, dan kami tidak akan melihatnya di sistem.
Baiklah, kami siap untuk pengujian. Mari kita rakit dan jalankan program di bawah ini straceuntuk memeriksa apakah semuanya berfungsi sebagaimana mestinya:
Semuanya baik-baik saja, bpf(2) mengembalikan pegangan 3 kepada kami dan kami memasuki putaran tak terbatas dengan pause(). Mari kita coba mencari program kita di sistem. Untuk melakukan ini kita akan pergi ke terminal lain dan menggunakan utilitas tersebut bpftool:
Kami melihat ada program yang dimuat di sistem woo yang ID globalnya 390 dan sedang dalam proses simple-prog ada deskriptor file terbuka yang menunjuk ke program (dan jika simple-prog akan menyelesaikan pekerjaannya, kalau begitu woo akan hilang). Seperti yang diharapkan, programnya woo membutuhkan 16 byte - dua instruksi - kode biner dalam arsitektur BPF, tetapi dalam bentuk aslinya (x86_64) sudah 40 byte. Mari kita lihat program kita dalam bentuk aslinya:
tidak ada kejutan. Sekarang mari kita lihat kode yang dihasilkan oleh kompiler 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
tidak terlalu efektif untuk exit(2), tetapi sejujurnya, program kami terlalu sederhana, dan untuk program non-sepele, tentu saja diperlukan prolog dan epilog yang ditambahkan oleh kompiler JIT.
Peta
Program BPF dapat menggunakan area memori terstruktur yang dapat diakses oleh program BPF lainnya dan program di ruang pengguna. Objek-objek ini disebut peta dan di bagian ini kami akan menunjukkan cara memanipulasinya menggunakan panggilan sistem bpf.
Katakanlah segera bahwa kemampuan peta tidak terbatas hanya pada akses ke memori bersama. Ada peta tujuan khusus yang berisi, misalnya, penunjuk ke program BPF atau penunjuk ke antarmuka jaringan, peta untuk bekerja dengan acara kinerja, dll. Kami tidak akan membicarakannya di sini agar tidak membingungkan pembaca. Selain itu, kami mengabaikan masalah sinkronisasi, karena ini tidak penting untuk contoh kami. Daftar lengkap jenis peta yang tersedia dapat ditemukan di <linux/bpf.h>, dan di bagian ini kita akan mengambil contoh tipe pertama secara historis, tabel hash BPF_MAP_TYPE_HASH.
Jika Anda membuat tabel hash, katakanlah, C++, Anda akan mengatakannya unordered_map<int,long> woo, yang dalam bahasa Rusia berarti βSaya butuh meja woo ukuran tidak terbatas, yang kuncinya bertipe int, dan nilainya adalah tipenya long" Untuk membuat tabel hash BPF, kita perlu melakukan hal yang hampir sama, kecuali kita harus menentukan ukuran maksimum tabel, dan alih-alih menentukan jenis kunci dan nilai, kita perlu menentukan ukurannya dalam byte. . Untuk membuat peta gunakan perintah BPF_MAP_CREATE panggilan sistem bpf. Mari kita lihat program minimal yang membuat peta. Setelah program sebelumnya yang memuat program BPF, program ini akan tampak sederhana bagi Anda:
Di sini kita mendefinisikan sekumpulan parameter attr, di mana kita mengatakan βSaya memerlukan tabel hash dengan kunci dan nilai ukuran sizeof(int), yang di dalamnya saya dapat memasukkan maksimal empat elemen." Saat membuat peta BPF, Anda dapat menentukan parameter lain, misalnya, dengan cara yang sama seperti pada contoh program, kami menentukan nama objek sebagai "woo".
Inilah panggilan sistemnya bpf(2) mengembalikan kami nomor peta deskriptor 3 dan kemudian program, seperti yang diharapkan, menunggu instruksi lebih lanjut dalam panggilan sistem pause(2).
Sekarang mari kita kirim program kita ke latar belakang atau buka terminal lain dan lihat objek kita menggunakan utilitas bpftool (kita dapat membedakan peta kita dari peta lain berdasarkan namanya):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...
Angka 114 adalah ID global objek kita. Program apa pun di sistem dapat menggunakan ID ini untuk membuka peta yang ada menggunakan perintah BPF_MAP_GET_FD_BY_ID panggilan sistem bpf.
Sekarang kita bisa bermain dengan tabel hash kita. Mari kita lihat isinya:
$ sudo bpftool map dump id 114
Found 0 elements
Kosong. Mari kita beri nilai di dalamnya hash[1] = 1:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0
Mari kita lihat tabelnya lagi:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 element
Hore! Kami berhasil menambahkan satu elemen. Perhatikan bahwa kita harus bekerja pada level byte untuk melakukan ini bptftool tidak tahu apa jenis nilai dalam tabel hash. (Pengetahuan ini dapat ditransfer kepadanya menggunakan BTF, namun lebih dari itu sekarang.)
Bagaimana tepatnya bpftool membaca dan menambahkan elemen? Mari kita lihat di balik terpal:
Pertama kita membuka peta dengan ID globalnya menggunakan perintah BPF_MAP_GET_FD_BY_ID ΠΈ bpf(2) mengembalikan deskriptor 3 kepada kami. Selanjutnya menggunakan perintah BPF_MAP_GET_NEXT_KEY kami menemukan kunci pertama di tabel dengan lewat NULL sebagai penunjuk ke kunci "sebelumnya". Jika kita punya kuncinya, kita bisa melakukannya BPF_MAP_LOOKUP_ELEMyang mengembalikan nilai ke pointer value. Langkah selanjutnya adalah kita mencoba mencari elemen berikutnya dengan meneruskan pointer ke kunci saat ini, tetapi tabel kita hanya berisi satu elemen dan perintah BPF_MAP_GET_NEXT_KEY kembali ENOENT.
Oke, mari kita ubah nilainya dengan kunci 1, misalkan logika bisnis kita memerlukan pendaftaran hash[1] = 2:
Seperti yang diharapkan, ini sangat sederhana: perintah BPF_MAP_GET_FD_BY_ID membuka peta kami berdasarkan ID, dan perintah BPF_MAP_UPDATE_ELEM menimpa elemen tersebut.
Jadi, setelah membuat tabel hash dari satu program, kita bisa membaca dan menulis isinya dari program lain. Perhatikan bahwa jika kami dapat melakukan ini dari baris perintah, maka program lain di sistem dapat melakukannya. Selain perintah yang dijelaskan di atas, untuk bekerja dengan peta dari ruang pengguna, Berikut:
BPF_MAP_LOOKUP_ELEM: temukan nilai berdasarkan kunci
BPF_MAP_GET_NEXT_ID: memungkinkan Anda menelusuri semua peta yang ada, begitulah cara kerjanya bpftool map
BPF_MAP_GET_FD_BY_ID: membuka peta yang ada berdasarkan ID globalnya
BPF_MAP_LOOKUP_AND_DELETE_ELEM: memperbarui nilai suatu objek secara atom dan mengembalikan yang lama
BPF_MAP_FREEZE: membuat peta tidak dapat diubah dari ruang pengguna (operasi ini tidak dapat dibatalkan)
BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: operasi massal. Misalnya, BPF_MAP_LOOKUP_AND_DELETE_BATCH - ini adalah satu-satunya cara yang dapat diandalkan untuk membaca dan mengatur ulang semua nilai dari peta
Tidak semua perintah ini berfungsi untuk semua tipe peta, namun secara umum bekerja dengan tipe peta lain dari ruang pengguna terlihat sama persis dengan bekerja dengan tabel hash.
Demi ketertiban, mari selesaikan eksperimen tabel hash kita. Ingat bahwa kita membuat tabel yang dapat berisi hingga empat kunci? Mari tambahkan beberapa elemen lagi:
$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long
Seperti yang diharapkan, kami tidak berhasil. Mari kita lihat kesalahannya lebih detail:
$ 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 +++
Semuanya baik-baik saja: seperti yang diharapkan, tim BPF_MAP_UPDATE_ELEM mencoba membuat kunci baru, kelima, tetapi macet E2BIG.
Jadi, kita dapat membuat dan memuat program BPF, serta membuat dan mengelola peta dari ruang pengguna. Sekarang logis untuk melihat bagaimana kita dapat menggunakan peta dari program BPF itu sendiri. Kita dapat membicarakan hal ini dalam bahasa program yang sulit dibaca dalam kode makro mesin, namun kenyataannya telah tiba waktunya untuk menunjukkan bagaimana program BPF sebenarnya ditulis dan dipelihara - menggunakan libbpf.
(Bagi pembaca yang tidak puas dengan kurangnya contoh tingkat rendah: kami akan menganalisis secara rinci program yang menggunakan peta dan fungsi pembantu yang dibuat menggunakan libbpf dan memberi tahu Anda apa yang terjadi di tingkat instruksi. Bagi pembaca yang tidak puas sangat banyak, kami menambahkan contoh di tempat yang sesuai dalam artikel.)
Menulis program BPF menggunakan libbpf
Menulis program BPF menggunakan kode mesin mungkin menarik hanya untuk pertama kalinya, dan kemudian rasa kenyang pun muncul. Pada saat ini Anda perlu mengalihkan perhatian Anda llvm, yang memiliki backend untuk menghasilkan kode untuk arsitektur BPF, serta perpustakaan libbpf, yang memungkinkan Anda menulis sisi pengguna aplikasi BPF dan memuat kode program BPF yang dihasilkan menggunakan llvm/clang.
Faktanya, seperti yang akan kita lihat di artikel ini dan artikel selanjutnya, libbpf melakukan cukup banyak pekerjaan tanpanya (atau alat serupa - iproute2, libbcc, libbpf-go, dll.) tidak mungkin untuk hidup. Salah satu fitur mematikan dari proyek ini libbpf adalah BPF CO-RE (Kompilasi Sekali, Jalankan Di Mana Saja) - sebuah proyek yang memungkinkan Anda menulis program BPF yang portabel dari satu kernel ke kernel lainnya, dengan kemampuan untuk dijalankan pada API yang berbeda (misalnya, ketika struktur kernel berubah dari versi ke versi). Agar dapat bekerja dengan CO-RE, kernel Anda harus dikompilasi dengan dukungan BTF (kami menjelaskan cara melakukannya di bagian Alat pengembangan. Anda dapat memeriksa apakah kernel Anda dibangun dengan BTF atau tidak dengan sangat sederhana - dengan adanya file berikut:
File ini menyimpan informasi tentang semua tipe data yang digunakan dalam kernel dan digunakan dalam semua contoh penggunaan kami libbpf. Kami akan berbicara secara rinci tentang CO-RE di artikel berikutnya, tetapi dalam artikel ini - cukup buat sendiri kernelnya CONFIG_DEBUG_INFO_BTF.
perpustakaan libbpf tinggal tepat di direktori tools/lib/bpf kernel dan pengembangannya dilakukan melalui milis [email protected]. Namun, repositori terpisah dipertahankan untuk kebutuhan aplikasi yang berada di luar kernel https://github.com/libbpf/libbpf di mana perpustakaan kernel dicerminkan untuk akses baca kurang lebih apa adanya.
Di bagian ini kita akan melihat bagaimana Anda dapat membuat proyek yang menggunakan libbpf, mari kita menulis beberapa program pengujian (yang kurang lebih tidak berarti) dan menganalisis secara detail cara kerjanya. Hal ini akan memungkinkan kami untuk lebih mudah menjelaskan di bagian berikut bagaimana program BPF berinteraksi dengan peta, pembantu kernel, BTF, dll.
Biasanya proyek menggunakan libbpf tambahkan repositori GitHub sebagai submodul git, kami akan melakukan hal yang sama:
Rencana kita selanjutnya pada bagian ini adalah sebagai berikut: kita akan menulis program BPF seperti BPF_PROG_TYPE_XDP, sama seperti pada contoh sebelumnya, namun di C kita kompilasi menggunakan clang, dan tulis program pembantu yang akan memuatnya ke dalam kernel. Pada bagian berikut ini kami akan memperluas kemampuan program BPF dan program asisten.
Contoh: membuat aplikasi lengkap menggunakan libbpf
Untuk memulainya, kami menggunakan file /sys/kernel/btf/vmlinux, yang telah disebutkan di atas, dan buat padanannya dalam bentuk file header:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
File ini akan menyimpan semua struktur data yang tersedia di kernel kita, misalnya beginilah header IPv4 didefinisikan di kernel:
Meskipun program kami ternyata sangat sederhana, kami tetap perlu memperhatikan banyak detail. Pertama, file header pertama yang kami sertakan adalah vmlinux.h, yang baru saja kita buat menggunakan bpftool btf dump - sekarang kita tidak perlu menginstal paket kernel-headers untuk mengetahui seperti apa struktur kernelnya. File header berikut datang kepada kami dari perpustakaan libbpf. Sekarang kita hanya membutuhkannya untuk mendefinisikan makro SEC, yang mengirimkan karakter ke bagian yang sesuai dari file objek ELF. Program kami terdapat di bagian tersebut xdp/simple, di mana sebelum garis miring kita mendefinisikan jenis program BPF - ini adalah konvensi yang digunakan libbpf, berdasarkan nama bagian itu akan menggantikan tipe yang benar saat startup bpf(2). Program BPF sendiri adalah C - sangat sederhana dan terdiri dari satu baris return XDP_PASS. Terakhir, bagian terpisah "license" berisi nama lisensi.
Kita dapat mengkompilasi program kita menggunakan llvm/clang, versi >= 10.0.0, atau lebih baik lagi, lebih baik lagi (lihat bagian Alat pengembangan):
Di antara fitur-fitur menarik: kami menunjukkan arsitektur target -target bpf dan jalur ke header libbpf, yang baru saja kami instal. Juga, jangan lupakan -O2, tanpa opsi ini Anda mungkin akan mendapat kejutan di masa depan. Mari kita lihat kode kita, apakah kita berhasil menulis program yang kita inginkan?
Ya, itu berhasil! Sekarang, kami memiliki file biner dengan program tersebut, dan kami ingin membuat aplikasi yang akan memuatnya ke dalam kernel. Untuk tujuan ini perpustakaan libbpf menawarkan dua pilihan - gunakan API tingkat rendah atau API tingkat tinggi. Kami akan memilih cara kedua, karena kami ingin mempelajari cara menulis, memuat, dan menghubungkan program BPF dengan sedikit usaha untuk pembelajaran selanjutnya.
Pertama, kita perlu membuat βkerangkaβ program kita dari binernya menggunakan utilitas yang sama bpftool β pisau Swiss dari dunia BPF (yang dapat diartikan secara harfiah, karena Daniel Borkman, salah satu pencipta dan pengelola BPF, adalah orang Swiss):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
Dalam file xdp-simple.skel.h berisi kode biner program kita dan fungsi untuk mengelola - memuat, melampirkan, menghapus objek kita. Dalam kasus sederhana ini sepertinya berlebihan, tetapi ini juga berfungsi ketika file objek berisi banyak program dan peta BPF dan untuk memuat ELF raksasa ini kita hanya perlu membuat kerangka dan memanggil satu atau dua fungsi dari aplikasi khusus yang kita buat. sedang menulis Mari kita lanjutkan sekarang.
Sebenarnya, program pemuat kami adalah hal yang sepele:
#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);
}
Di sini struct xdp_simple_bpf didefinisikan dalam file xdp-simple.skel.h dan menjelaskan file objek kami:
Kita dapat melihat jejak API tingkat rendah di sini: strukturnya struct bpf_program *simple ΠΈ struct bpf_link *simple. Struktur pertama secara khusus menjelaskan program kita, yang ditulis di bagian xdp/simple, dan yang kedua menjelaskan bagaimana program terhubung ke sumber peristiwa.
Fungsi xdp_simple_bpf__open_and_load, membuka objek ELF, menguraikannya, membuat semua struktur dan substruktur (selain program, ELF juga berisi bagian lain - data, data hanya baca, informasi debug, lisensi, dll.), lalu memuatnya ke dalam kernel menggunakan sistem panggilan bpf, yang dapat kita periksa dengan mengkompilasi dan menjalankan program:
Sekarang mari kita lihat program kita menggunakan bpftool. Mari kita temukan ID-nya:
# 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)
dan dump (kami menggunakan bentuk perintah yang disingkat bpftool prog dump xlated):
# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
0: (b7) r0 = 2
1: (95) exit
Sesuatu yang baru! Program ini mencetak potongan file sumber C. Ini dilakukan oleh perpustakaan libbpf, yang menemukan bagian debug dalam biner, mengkompilasinya menjadi objek BTF, memuatnya ke dalam kernel menggunakan BPF_BTF_LOAD, lalu tentukan deskriptor file yang dihasilkan saat memuat program dengan perintah BPG_PROG_LOAD.
Pembantu Kernel
Program BPF dapat menjalankan fungsi "eksternal" - pembantu kernel. Fungsi pembantu ini memungkinkan program BPF mengakses struktur kernel, mengelola peta, dan juga berkomunikasi dengan "dunia nyata" - membuat acara kinerja, mengontrol perangkat keras (misalnya, mengalihkan paket), dll.
Contoh: bpf_get_smp_processor_id
Dalam kerangka paradigma βbelajar melalui contohβ, mari kita pertimbangkan salah satu fungsi pembantu, bpf_get_smp_processor_id(), yakin dalam file kernel/bpf/helpers.c. Ini mengembalikan nomor prosesor yang menjalankan program BPF yang memanggilnya. Namun kami tidak begitu tertarik pada semantiknya, melainkan pada kenyataan bahwa implementasinya mengambil satu baris:
Definisi fungsi pembantu BPF mirip dengan definisi panggilan sistem Linux. Di sini, misalnya, suatu fungsi didefinisikan yang tidak memiliki argumen. (Fungsi yang mengambil, katakanlah, tiga argumen didefinisikan menggunakan makro BPF_CALL_3. Jumlah maksimum argumen adalah lima.) Namun, ini hanyalah bagian pertama dari definisi tersebut. Bagian kedua adalah mendefinisikan struktur tipe struct bpf_func_proto, yang berisi deskripsi fungsi pembantu yang dipahami oleh verifikator:
Agar program BPF jenis tertentu dapat menggunakan fungsi ini, program tersebut harus mendaftarkannya, misalnya untuk jenis tersebut BPF_PROG_TYPE_XDP suatu fungsi didefinisikan di kernel xdp_func_proto, yang menentukan dari ID fungsi pembantu apakah XDP mendukung fungsi ini atau tidak. Fungsi kami adalah mendukung:
Jenis program BPF baru "didefinisikan" di dalam file include/linux/bpf_types.h menggunakan makro BPF_PROG_TYPE. Didefinisikan dalam tanda kutip karena merupakan definisi logis, dan dalam istilah bahasa C definisi seluruh rangkaian struktur beton terjadi di tempat lain. Khususnya pada file kernel/bpf/verifier.c semua definisi dari file bpf_types.h digunakan untuk membuat serangkaian struktur bpf_verifier_ops[]:
Artinya, untuk setiap tipe program BPF, sebuah penunjuk ke struktur data tipe tersebut ditentukan struct bpf_verifier_ops, yang diinisialisasi dengan nilai _name ## _verifier_ops, yaitu, xdp_verifier_ops untuk xdp. Struktur xdp_verifier_opsditentukan oleh dalam file net/core/filter.c sebagai berikut:
Di sini kita melihat fungsi yang kita kenal xdp_func_proto, yang akan menjalankan pemverifikasi setiap kali menghadapi tantangan semacam fungsi di dalam program BPF, lihat verifier.c.
Mari kita lihat bagaimana program BPF hipotetis menggunakan fungsi tersebut bpf_get_smp_processor_id. Untuk melakukan ini, kami menulis ulang program dari bagian sebelumnya sebagai berikut:
yaitu, bpf_get_smp_processor_id adalah penunjuk fungsi yang nilainya 8, dimana 8 adalah nilainya BPF_FUNC_get_smp_processor_id tipe enum bpf_fun_id, yang ditentukan untuk kita di file vmlinux.h (mengajukan bpf_helper_defs.h di kernel dihasilkan oleh skrip, jadi angka "ajaib" tidak masalah). Fungsi ini tidak memerlukan argumen dan mengembalikan nilai bertipe __u32. Saat kami menjalankannya di program kami, clang menghasilkan instruksi BPF_CALL "jenis yang tepat" Mari kita kompilasi programnya dan lihat bagiannya xdp/simple:
Di baris pertama kita melihat instruksi call, parameter IMM yang sama dengan 8, dan SRC_REG - nol. Menurut perjanjian ABI yang digunakan oleh verifikator, ini adalah fungsi panggilan ke pembantu nomor delapan. Setelah diluncurkan, logikanya sederhana. Kembalikan nilai dari register r0 disalin ke r1 dan pada baris 2,3 diubah menjadi tipe u32 β 32 bit teratas dihapus. Pada baris 4,5,6,7 kita mengembalikan 2 (XDP_PASS) atau 1 (XDP_DROP) bergantung pada apakah fungsi pembantu dari baris 0 mengembalikan nilai nol atau bukan nol.
Mari kita uji diri kita sendiri: muat program dan lihat hasilnya 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
Oke, pemverifikasi menemukan pembantu kernel yang benar.
Contoh: menyampaikan argumen dan akhirnya menjalankan program!
Semua fungsi pembantu run-level memiliki prototipe
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
Parameter ke fungsi pembantu diteruskan dalam register r1-r5, dan nilainya dikembalikan dalam register r0. Tidak ada fungsi yang memerlukan lebih dari lima argumen, dan dukungan terhadap fungsi tersebut diperkirakan tidak akan ditambahkan di masa mendatang.
Mari kita lihat helper kernel baru dan bagaimana BPF meneruskan parameter. Mari kita menulis ulang xdp-simple.bpf.c sebagai berikut (baris lainnya tidak berubah):
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}
Program kami mencetak nomor CPU yang menjalankannya. Mari kita kompilasi dan lihat kodenya:
Di baris 0-7 kita menulis stringnya running on CPU%un, dan kemudian pada baris 8 kita menjalankan yang sudah dikenal bpf_get_smp_processor_id. Pada baris 9-12 kita menyiapkan argumen pembantu bpf_printk - mendaftar r1, r2, r3. Mengapa mereka ada tiga dan bukan dua? Karena bpf_printk - ini adalah pembungkus makro di sekitar penolong yang sebenarnya bpf_trace_printk, yang harus meneruskan ukuran string format.
Sekarang mari tambahkan beberapa baris ke dalamnya xdp-simple.csehingga program kita terhubung ke antarmuka lo dan benar-benar dimulai!
Di sini kita menggunakan fungsinya bpf_set_link_xdp_fd, yang menghubungkan program BPF tipe XDP ke antarmuka jaringan. Kami melakukan hardcode pada nomor antarmuka lo, yang selalu 1. Kami menjalankan fungsi dua kali untuk melepaskan program lama terlebih dahulu jika terpasang. Perhatikan bahwa sekarang kita tidak memerlukan tantangan pause atau loop tak terbatas: program pemuat kita akan keluar, tetapi program BPF tidak akan dimatikan karena terhubung ke sumber peristiwa. Setelah pengunduhan dan koneksi berhasil, program akan diluncurkan untuk setiap paket jaringan yang tiba lo.
Mari unduh programnya dan lihat antarmukanya 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
Program yang kami unduh memiliki ID 669 dan kami melihat ID yang sama di antarmuka lo. Kami akan mengirimkan beberapa paket ke 127.0.0.1 (permintaan + balasan):
$ ping -c1 localhost
dan sekarang mari kita lihat isi file virtual debug /sys/kernel/debug/tracing/trace_pipe, di mana bpf_printk menulis pesannya:
# 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
Dua paket terlihat lo dan diproses pada CPU0 - program BPF pertama kami yang tidak berarti dan lengkap berhasil!
Perlu dicatat bahwa bpf_printk Bukan tanpa alasan ia menulis ke file debug: ini bukan penolong yang paling berhasil untuk digunakan dalam produksi, tetapi tujuan kami adalah untuk menunjukkan sesuatu yang sederhana.
Mengakses peta dari program BPF
Contoh: menggunakan peta dari program BPF
Pada bagian sebelumnya kita mempelajari cara membuat dan menggunakan peta dari ruang pengguna, dan sekarang mari kita lihat bagian kernel. Mari kita mulai, seperti biasa, dengan sebuah contoh. Mari kita menulis ulang program kita xdp-simple.bpf.c sebagai berikut:
Di awal program kami menambahkan definisi peta woo: Ini adalah array 8 elemen yang menyimpan nilai-nilai seperti u64 (di C kita akan mendefinisikan array seperti u64 woo[8]). Dalam sebuah program "xdp/simple" kita memasukkan nomor prosesor saat ini ke dalam variabel key dan kemudian menggunakan fungsi pembantu bpf_map_lookup_element kita mendapatkan pointer ke entri yang sesuai dalam array, yang kita tambah satu. Diterjemahkan ke dalam bahasa Rusia: kami menghitung statistik CPU mana yang memproses paket masuk. Mari kita coba jalankan programnya:
Mari kita periksa apakah dia terhubung lo dan mengirim beberapa paket:
$ 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
Hampir semua proses diproses pada CPU7. Ini tidak penting bagi kami, yang utama adalah programnya berfungsi dan kami memahami cara mengakses peta dari program BPF - menggunakan Ρ Π΅Π»ΠΏΠ΅ΡΠΎΠ² bpf_mp_*.
Indeks mistik
Jadi kita bisa mengakses peta dari program BPF menggunakan panggilan seperti
$ 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
Tetapi jika kita melihat program yang sudah dimuat, kita melihat penunjuk ke peta yang benar (baris 4):
Jadi, kami dapat menyimpulkan bahwa pada saat peluncuran program pemuat kami, tautan ke &woo digantikan oleh sesuatu dengan perpustakaan libbpf. Pertama kita akan melihat hasilnya strace:
Kami melihat itu libbpf membuat peta woo dan kemudian mengunduh program kami simple. Mari kita lihat lebih dekat cara kita memuat program:
panggilan xdp_simple_bpf__open_and_load dari file xdp-simple.skel.h
yang menyebabkan xdp_simple_bpf__load dari file xdp-simple.skel.h
yang menyebabkan bpf_object__load_skeleton dari file libbpf/src/libbpf.c
yang menyebabkan bpf_object__load_xattr dari libbpf/src/libbpf.c
Fungsi terakhir antara lain akan memanggil bpf_object__create_maps, yang membuat atau membuka peta yang ada, mengubahnya menjadi deskriptor file. (Di sinilah kita melihat BPF_MAP_CREATE dalam keluaran strace.) Selanjutnya fungsinya dipanggil bpf_object__relocate dan dialah yang menarik minat kita, karena kita mengingat apa yang kita lihat woo dalam tabel relokasi. Menjelajahinya, kita akhirnya menemukan diri kita dalam fungsinya bpf_program__relocate, yang dan berurusan dengan relokasi peta:
case RELO_LD64:
insn[0].src_reg = BPF_PSEUDO_MAP_FD;
insn[0].imm = obj->maps[relo->map_idx].fd;
break;
dan ganti register sumber di dalamnya dengan BPF_PSEUDO_MAP_FD, dan IMM pertama ke deskriptor file peta kita dan, jika sama dengan, misalnya, 0xdeadbeef, maka sebagai hasilnya kita akan menerima instruksi
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll
Ini adalah bagaimana informasi peta ditransfer ke program BPF tertentu yang dimuat. Dalam hal ini, peta dapat dibuat menggunakan BPF_MAP_CREATE, dan dibuka dengan ID menggunakan BPF_MAP_GET_FD_BY_ID.
Total, saat menggunakan libbpf algoritmanya adalah sebagai berikut:
selama kompilasi, catatan dibuat di tabel relokasi untuk tautan ke peta
libbpf membuka buku objek ELF, menemukan semua peta yang digunakan dan membuat deskriptor file untuk peta tersebut
deskriptor file dimuat ke dalam kernel sebagai bagian dari instruksi LD64
Seperti yang dapat Anda bayangkan, masih banyak lagi yang akan datang dan kita harus melihat intinya. Untungnya, kami punya petunjuk - kami telah menuliskan artinya BPF_PSEUDO_MAP_FD ke dalam daftar sumber dan kita dapat menguburnya, yang akan membawa kita ke tempat suci semua orang suci - kernel/bpf/verifier.c, di mana fungsi dengan nama khusus menggantikan deskriptor file dengan alamat tipe struktur struct bpf_map:
(kode lengkap dapat ditemukan ΠΏΠΎ ΡΡΡΠ»ΠΊΠ΅). Jadi kami dapat memperluas algoritme kami:
saat memuat program, pemverifikasi memeriksa kebenaran penggunaan peta dan menulis alamat struktur yang sesuai struct bpf_map
Saat mengunduh biner ELF menggunakan libbpf Masih banyak lagi yang terjadi, tetapi kita akan membahasnya di artikel lain.
Memuat program dan peta tanpa libbpf
Seperti yang dijanjikan, berikut adalah contoh bagi pembaca yang ingin mengetahui cara membuat dan memuat program yang menggunakan peta, tanpa bantuan libbpf. Ini dapat berguna ketika Anda bekerja di lingkungan di mana Anda tidak dapat membangun dependensinya, atau menyimpan setiap bitnya, atau menulis program seperti ply, yang menghasilkan kode biner BPF dengan cepat.
Agar lebih mudah mengikuti logikanya, kami akan menulis ulang contoh kami untuk tujuan ini xdp-simple. Kode program yang dibahas dalam contoh ini secara lengkap dan sedikit diperluas dapat ditemukan di sini inti.
Logika aplikasi kita adalah sebagai berikut:
membuat peta tipe BPF_MAP_TYPE_ARRAY menggunakan perintah BPF_MAP_CREATE,
buat program yang menggunakan peta ini,
menghubungkan program ke antarmuka lo,
yang diterjemahkan menjadi manusia sebagai
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);
}
Di sini map_create membuat peta dengan cara yang sama seperti yang kita lakukan pada contoh pertama tentang panggilan sistem bpf - βkernel, tolong buatkan saya peta baru dalam bentuk array 8 elemen seperti __u64 dan kembalikan deskriptor filenya kepada saya":
Bagian yang sulit prog_load adalah definisi program BPF kami sebagai serangkaian struktur struct bpf_insn insns[]. Tapi karena kita menggunakan program yang kita punya di C, kita bisa sedikit curang:
Secara total, kita perlu menulis 14 instruksi dalam bentuk struktur seperti struct bpf_insn (nasihat: ambil dump dari atas, baca kembali bagian instruksi, buka linux/bpf.h ΠΈ linux/bpf_common.h dan mencoba untuk menentukan struct bpf_insn insns[] sendiri):
Latihan bagi mereka yang tidak menulisnya sendiri - temukan map_fd.
Ada satu bagian lagi yang dirahasiakan dalam program kami - xdp_attach. Sayangnya, program seperti XDP tidak dapat dihubungkan menggunakan system call bpf. Orang-orang yang membuat BPF dan XDP berasal dari komunitas Linux online, yang berarti mereka menggunakan komunitas yang paling mereka kenal (tetapi tidak untuk normal orang) antarmuka untuk berinteraksi dengan kernel: soket netlink, Lihat juga RFC3549. Cara paling sederhana untuk diterapkan xdp_attach sedang menyalin kode dari libbpf, yaitu dari file netlink.c, itulah yang kami lakukan, memperpendeknya sedikit:
Selamat datang di dunia soket netlink
Buka jenis soket 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;
}
Kita membaca dari soket ini:
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;
}
Terakhir, inilah fungsi kami yang membuka soket dan mengirimkan pesan khusus ke dalamnya yang berisi deskriptor file:
Mari kita lihat apakah program kita telah terhubung 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
Hore, semuanya berfungsi. Perlu diperhatikan bahwa peta kita kembali ditampilkan dalam bentuk byte. Hal ini disebabkan oleh fakta bahwa, tidak seperti libbpf kami tidak memuat informasi jenis (BTF). Tapi kita akan membicarakannya lebih lanjut lain kali.
Alat pengembangan
Di bagian ini, kita akan melihat perangkat pengembang BPF minimum.
Secara umum, Anda tidak memerlukan sesuatu yang khusus untuk mengembangkan program BPF - BPF berjalan pada kernel distribusi apa pun yang layak, dan program dibuat menggunakan clang, yang dapat dipasok dari paket. Namun, karena BPF sedang dalam pengembangan, kernel dan alat terus berubah, jika Anda tidak ingin menulis program BPF menggunakan metode kuno mulai tahun 2019, Anda harus mengkompilasi
llvm/clang
pahole
intinya
bpftool
(Sebagai referensi, bagian ini dan semua contoh dalam artikel dijalankan di Debian 10.)
llvm/dentang
BPF bersahabat dengan LLVM dan, meskipun saat ini program untuk BPF dapat dikompilasi menggunakan gcc, semua pengembangan saat ini dilakukan untuk LLVM. Oleh karena itu, pertama-tama, kami akan membuat versi saat ini clang dari git:
(Petunjuk perakitan clang diambil oleh saya dari bpf_devel_QA.)
Kami tidak akan menginstal program yang baru kami buat, melainkan hanya menambahkannya ke dalamnya PATH, misalnya:
export PATH="`pwd`/bin:$PATH"
(Ini dapat ditambahkan ke .bashrc atau ke file terpisah. Secara pribadi, saya menambahkan hal-hal seperti ini ~/bin/activate-llvm.sh dan bila perlu saya melakukannya . activate-llvm.sh.)
Pahole dan BTF
Π° pahole digunakan saat membangun kernel untuk membuat informasi debug dalam format BTF. Kami tidak akan membahas secara detail di artikel ini tentang detail teknologi BTF, selain faktanya nyaman dan kami ingin menggunakannya. Jadi jika Anda ingin membangun kernel, buatlah terlebih dahulu pahole (tanpa pahole Anda tidak akan dapat membangun kernel dengan opsi ini 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
Kernel untuk bereksperimen dengan BPF
Saat menjajaki kemungkinan BPF, saya ingin merakit inti saya sendiri. Secara umum, hal ini tidak diperlukan karena Anda akan dapat mengkompilasi dan memuat program BPF pada kernel distribusi. Namun, memiliki kernel sendiri memungkinkan Anda untuk menggunakan fitur BPF terbaru, yang paling lama akan muncul di distribusi Anda dalam beberapa bulan. , atau, seperti dalam kasus beberapa alat debugging tidak akan dikemas sama sekali di masa mendatang. Selain itu, intinya sendiri membuatnya terasa penting untuk bereksperimen dengan kode tersebut.
Untuk membangun sebuah kernel, Anda memerlukan, pertama, kernel itu sendiri, dan kedua, file konfigurasi kernel. Untuk bereksperimen dengan BPF kita bisa menggunakan yang biasa vanila kernel atau salah satu kernel pengembangan. Secara historis, pengembangan BPF terjadi dalam komunitas jaringan Linux dan oleh karena itu semua perubahan cepat atau lambat akan melalui David Miller, pengelola jaringan Linux. Bergantung pada sifatnya - pengeditan atau fitur baru - perubahan jaringan terbagi dalam salah satu dari dua inti - net ΠΈΠ»ΠΈ net-next. Perubahan untuk BPF didistribusikan dengan cara yang sama bpf ΠΈ bpf-next, yang kemudian dikumpulkan masing-masing menjadi net dan net-next. Untuk lebih jelasnya, lihat bpf_devel_QA ΠΈ netdev-FAQ. Jadi pilihlah kernel berdasarkan selera Anda dan kebutuhan stabilitas sistem yang Anda uji (*-next kernel adalah yang paling tidak stabil dari yang terdaftar).
Pembicaraan tentang cara mengelola file konfigurasi kernel berada di luar cakupan artikel ini - diasumsikan bahwa Anda sudah mengetahui cara melakukannya, atau siap untuk belajar sendiri. Namun, petunjuk berikut ini seharusnya sudah cukup untuk memberi Anda sistem yang mendukung BPF.
Unduh salah satu kernel di atas:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next
Bangun konfigurasi kernel minimal yang berfungsi:
$ cp /boot/config-`uname -r` .config
$ make localmodconfig
Aktifkan opsi BPF di file .config pilihan Anda sendiri (kemungkinan besar CONFIG_BPF sudah diaktifkan sejak systemd menggunakannya). Berikut adalah daftar opsi dari kernel yang digunakan untuk artikel ini:
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
Kemudian kita dapat dengan mudah merakit dan menginstal modul dan kernel (omong-omong, Anda dapat merakit kernel menggunakan yang baru dirakit clangdengan menambahkan CC=clang):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install
dan reboot dengan kernel baru (saya gunakan untuk ini kexec dari paket kexec-tools):
Utilitas yang paling umum digunakan dalam artikel ini adalah utilitas bpftool, disediakan sebagai bagian dari kernel Linux. Ini ditulis dan dikelola oleh pengembang BPF untuk pengembang BPF dan dapat digunakan untuk mengelola semua jenis objek BPF - memuat program, membuat dan mengedit peta, menjelajahi kehidupan ekosistem BPF, dll. Dokumentasi dalam bentuk kode sumber untuk halaman manual dapat ditemukan di inti atau, sudah dikompilasi, jaringan.
Pada saat penulisan ini bpftool sudah siap pakai hanya untuk RHEL, Fedora dan Ubuntu (lihat, misalnya, utas ini, yang menceritakan kisah pengemasan yang belum selesai bpftool di Debian). Tetapi jika Anda sudah membuat kernel, maka buatlah bpftool mudah sekali:
$ 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 ]
$
(di sini ${linux} - ini adalah direktori kernel Anda.) Setelah menjalankan perintah ini bpftool akan dikumpulkan dalam direktori ${linux}/tools/bpf/bpftool dan itu dapat ditambahkan ke jalur (pertama-tama ke pengguna root) atau cukup salin ke /usr/local/sbin.
Mengumpulkan bpftool yang terbaik adalah menggunakan yang terakhir clang, dirakit seperti dijelaskan di atas, dan periksa apakah sudah dirakit dengan benar - menggunakan, misalnya, perintah
$ 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
...
yang akan menunjukkan fitur BPF mana yang diaktifkan di kernel Anda.
Omong-omong, perintah sebelumnya dapat dijalankan sebagai
# bpftool f p k
Hal ini dilakukan dengan analogi dengan utilitas dari paket iproute2, di mana kita dapat, misalnya, mengatakan ip a s eth0 daripada ip addr show dev eth0.
Kesimpulan
BPF memungkinkan Anda memasang kutu untuk mengukur secara efektif dan mengubah fungsi inti dengan cepat. Sistem ini ternyata sangat sukses, dalam tradisi terbaik UNIX: mekanisme sederhana yang memungkinkan Anda memprogram (ulang) kernel memungkinkan banyak orang dan organisasi untuk bereksperimen. Dan meskipun eksperimen serta pengembangan infrastruktur BPF itu sendiri masih jauh dari selesai, sistem tersebut telah memiliki ABI stabil yang memungkinkan Anda membangun logika bisnis yang andal dan yang terpenting, efektif.
Saya ingin mencatat bahwa, menurut saya, teknologi menjadi begitu populer karena, di satu sisi, bisa bermain (arsitektur suatu mesin dapat dipahami kurang lebih dalam satu malam), dan sebaliknya, untuk memecahkan masalah yang tidak dapat diselesaikan (dengan indah) sebelum kemunculannya. Kedua komponen ini bersama-sama memaksa orang untuk bereksperimen dan bermimpi, yang mengarah pada munculnya solusi yang lebih inovatif.
Artikel ini, meskipun tidak terlalu pendek, hanya merupakan pengenalan tentang dunia BPF dan tidak menjelaskan fitur βlanjutanβ dan bagian penting dari arsitektur. Rencana ke depannya kira-kira seperti ini: artikel selanjutnya akan membahas gambaran umum jenis program BPF (ada 5.8 jenis program yang didukung di kernel 30), kemudian kita akhirnya akan melihat cara menulis aplikasi BPF yang sebenarnya menggunakan program penelusuran kernel sebagai contoh, maka saatnya untuk mempelajari lebih dalam tentang arsitektur BPF, diikuti dengan contoh jaringan BPF dan aplikasi keamanan.
Panduan Referensi BPF dan XDP β dokumentasi BPF dari cilium, atau lebih tepatnya dari Daniel Borkman, salah satu pencipta dan pengelola BPF. Ini adalah salah satu uraian serius pertama, yang berbeda dari uraian lainnya karena Daniel tahu persis apa yang ia tulis dan tidak ada kesalahan di sana. Secara khusus, dokumen ini menjelaskan cara bekerja dengan program BPF tipe XDP dan TC menggunakan utilitas terkenal ip dari paket iproute2.
Dokumentasi/jaringan/filter.txt β file asli dengan dokumentasi untuk BPF klasik dan kemudian diperluas. Bacaan yang bagus jika Anda ingin mempelajari bahasa assembly dan detail teknis arsitektur.
Blog tentang BPF dari facebook. Ini jarang diperbarui, tetapi tepat, seperti yang ditulis Alexei Starovoitov (penulis eBPF) dan Andrii Nakryiko - (pengelola) di sana libbpf).
Rahasia bpftool. Untaian twitter yang menghibur dari Quentin Monnet dengan contoh dan rahasia penggunaan bpftool.