BPF untuk si kecil, bagian satu: BPF diperpanjang

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.
BPF untuk si kecil, bagian satu: BPF diperpanjang

Ringkasan artikel

Pengantar arsitektur BPF. Pertama, kita akan melihat sekilas arsitektur BPF dan menguraikan komponen utamanya.

Register dan sistem perintah mesin virtual BPF. Sudah mempunyai gambaran tentang arsitektur secara keseluruhan, kami akan menjelaskan struktur mesin virtual BPF.

Siklus hidup objek BPF, sistem file bpffs. Di bagian ini, kita akan melihat lebih dekat siklus hidup objek BPF - program dan peta.

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.

BPF untuk si kecil, bagian satu: BPF diperpanjang

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

BPF untuk si kecil, bagian satu: BPF diperpanjang

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.

BPF untuk si kecil, bagian satu: BPF diperpanjang

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:

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

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

BPF untuk si kecil, bagian satu: BPF diperpanjang

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:

BPF untuk si kecil, bagian satu: BPF diperpanjang

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:

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

Kolom pertama dalam keluaran readelf adalah lekukan dan program kami terdiri dari empat perintah:

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

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:

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

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:

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

Hanya ada dua instruksi dalam program biner:

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

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:

$ 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

Siklus hidup objek BPF, sistem file bpffs

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

BPF untuk si kecil, bagian satu: BPF diperpanjang

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.

BPF untuk si kecil, bagian satu: BPF diperpanjang

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.

BPF untuk si kecil, bagian satu: BPF diperpanjang

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:

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

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

Mari kompilasi program ini dan buat salinan lokal dari sistem file bpffs:

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

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

$ 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

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 definisi union 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:

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

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

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

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:

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

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:

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

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:

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

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:

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

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

Mari kompilasi dan jalankan programnya:

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

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:

$ 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

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:

$ 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

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_UPDATE_ELEM: memperbarui/membuat nilai
  • BPF_MAP_DELETE_ELEM: hapus kunci
  • BPF_MAP_GET_NEXT_KEY: temukan kunci berikutnya (atau pertama).
  • 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

Sejauh ini bagus:

$ 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

Mari kita coba menambahkan satu lagi:

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

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

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:

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

Pergi ke libbpf sangat sederhana:

$ 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

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:

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

Sekarang kita akan menulis program BPF kita di 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";

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

$ 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

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?

$ 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

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:

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

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:

$ 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

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:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

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:

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

Mendaftarkan Fungsi Pembantu

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:

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

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[]:

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

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_ops ditentukan oleh dalam file net/core/filter.c sebagai berikut:

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

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:

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

simbol bpf_get_smp_processor_id ditentukan oleh Π² <bpf/bpf_helper_defs.h> perpustakaan libbpf sebagai

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

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:

$ 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

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:

$ 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

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_printkini 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!

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

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:

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

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:

$ 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

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

Sekarang mari kita lihat isi arraynya:

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

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

val = bpf_map_lookup_elem(&woo, &key);

di mana fungsi pembantu terlihat seperti

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

tapi kami memberikan sebuah pointer &woo ke struktur yang tidak disebutkan namanya struct { ... }...

Jika kita melihat program assembler, kita melihat nilainya &woo sebenarnya tidak ditentukan (baris 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
...

dan terkandung dalam relokasi:

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

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

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:

$ 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

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;

Jadi kami mengikuti instruksi kami

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

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:

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;

(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":

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

Program ini juga mudah dimuat:

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

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:

$ 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

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

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

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:

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

Jadi, semuanya siap untuk diuji:

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

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

Ayo kirim ping dan lihat peta:

$ 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

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:

$ 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
... ΠΌΠ½ΠΎΠ³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ спустя
$

Sekarang kita dapat memeriksa apakah semuanya berjalan dengan benar:

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

(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):

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

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.

Artikel sebelumnya dalam seri ini

  1. BPF untuk si kecil, bagian nol: BPF klasik

Tautan

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

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

  3. Blog tentang BPF dari facebook. Ini jarang diperbarui, tetapi tepat, seperti yang ditulis Alexei Starovoitov (penulis eBPF) dan Andrii Nakryiko - (pengelola) di sana libbpf).

  4. Rahasia bpftool. Untaian twitter yang menghibur dari Quentin Monnet dengan contoh dan rahasia penggunaan bpftool.

  5. Selami BPF: daftar bahan bacaan. Daftar tautan raksasa (dan masih dipertahankan) ke dokumentasi BPF dari Quentin Monnet.

Sumber: www.habr.com

Tambah komentar