BPF untuk si kecil, bahagian satu: BPF lanjutan

Pada mulanya terdapat satu teknologi dan ia dipanggil BPF. Kami memandangnya sebelumnya, artikel Perjanjian Lama siri ini. Pada tahun 2013, melalui usaha Alexei Starovoitov dan Daniel Borkman, versi yang lebih baik daripadanya, dioptimumkan untuk mesin 64-bit moden, telah dibangunkan dan dimasukkan ke dalam kernel Linux. Teknologi baharu ini secara ringkas dipanggil BPF Dalaman, kemudian dinamakan semula BPF Lanjutan, dan kini, selepas beberapa tahun, semua orang hanya memanggilnya BPF.

Secara kasarnya, BPF membenarkan anda menjalankan kod yang dibekalkan pengguna sewenang-wenangnya dalam ruang kernel Linux, dan seni bina baharu ternyata begitu berjaya sehingga kami memerlukan sedozen lagi artikel untuk menerangkan semua aplikasinya. (Satu-satunya perkara yang tidak dilakukan oleh pembangun, seperti yang anda lihat dalam kod prestasi di bawah, ialah mencipta logo yang baik.)

Artikel ini menerangkan struktur mesin maya BPF, antara muka kernel untuk bekerja dengan BPF, alat pembangunan, serta gambaran ringkas yang sangat ringkas tentang keupayaan sedia ada, i.e. segala-galanya yang kami perlukan pada masa hadapan untuk kajian yang lebih mendalam tentang aplikasi praktikal BPF.
BPF untuk si kecil, bahagian satu: BPF lanjutan

Ringkasan artikel

Pengenalan kepada seni bina BPF. Mula-mula, kami akan mengambil pandangan mata terhadap seni bina BPF dan menggariskan komponen utama.

Daftar dan sistem arahan mesin maya BPF. Sudah mempunyai idea tentang seni bina secara keseluruhan, kami akan menerangkan struktur mesin maya BPF.

Kitaran hayat objek BPF, sistem fail bpffs. Dalam bahagian ini, kita akan melihat dengan lebih dekat kitaran hayat objek BPF - program dan peta.

Menguruskan objek menggunakan panggilan sistem bpf. Dengan beberapa pemahaman tentang sistem yang telah sedia ada, kami akhirnya akan melihat cara mencipta dan memanipulasi objek dari ruang pengguna menggunakan panggilan sistem khas βˆ’ bpf(2).

ПишСм ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΡ‹ BPF с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ libbpf. Sudah tentu, anda boleh menulis program menggunakan panggilan sistem. Tetapi ia sukar. Untuk senario yang lebih realistik, pengaturcara nuklear membangunkan perpustakaan libbpf. Kami akan mencipta rangka aplikasi BPF asas yang akan kami gunakan dalam contoh seterusnya.

Pembantu Inti. Di sini kita akan mempelajari cara program BPF boleh mengakses fungsi pembantu kernel - alat yang, bersama-sama dengan peta, secara asasnya mengembangkan keupayaan BPF baharu berbanding dengan yang klasik.

Akses kepada peta daripada program BPF. Pada ketika ini, kita akan cukup tahu untuk memahami dengan tepat cara kita boleh mencipta program yang menggunakan peta. Dan mari kita lihat dengan pantas ke dalam pengesah yang hebat dan hebat.

Alat pembangunan. Bahagian bantuan tentang cara memasang utiliti dan kernel yang diperlukan untuk eksperimen.

Kesimpulannya. Pada akhir artikel, mereka yang membaca sejauh ini akan menemui kata-kata motivasi dan penerangan ringkas tentang apa yang akan berlaku dalam artikel berikut. Kami juga akan menyenaraikan beberapa pautan untuk belajar sendiri bagi mereka yang tidak mempunyai keinginan atau keupayaan untuk menunggu sambungan.

Pengenalan kepada BPF Architecture

Sebelum kita mula mempertimbangkan seni bina BPF, kita akan merujuk untuk kali terakhir (oh). BPF klasik, yang dibangunkan sebagai tindak balas kepada kemunculan mesin RISC dan menyelesaikan masalah penapisan paket yang cekap. Seni bina ternyata begitu berjaya sehingga, setelah dilahirkan pada tahun sembilan puluhan yang mengagumkan di Berkeley UNIX, ia telah dialihkan ke kebanyakan sistem pengendalian sedia ada, bertahan hingga ke usia dua puluhan gila dan masih mencari aplikasi baharu.

BPF baharu telah dibangunkan sebagai tindak balas kepada ubiquity mesin 64-bit, perkhidmatan awan dan peningkatan keperluan untuk alat untuk mencipta SDN (Sofware-dberkesan neworking). Dibangunkan oleh jurutera rangkaian kernel sebagai pengganti yang lebih baik untuk BPF klasik, BPF baharu secara literal enam bulan kemudian menemui aplikasi dalam tugas sukar untuk mengesan sistem Linux, dan kini, enam tahun selepas kemunculannya, kami memerlukan satu artikel seterusnya hanya untuk senaraikan pelbagai jenis program.

Gambar kelakar

Pada terasnya, BPF ialah mesin maya kotak pasir yang membolehkan anda menjalankan kod "sewenang-wenangnya" dalam ruang kernel tanpa menjejaskan keselamatan. Program BPF dicipta dalam ruang pengguna, dimuatkan ke dalam kernel, dan disambungkan ke beberapa sumber acara. Peristiwa boleh, sebagai contoh, penghantaran paket ke antara muka rangkaian, pelancaran beberapa fungsi kernel, dsb. Dalam kes pakej, program BPF akan mempunyai akses kepada data dan metadata pakej (untuk membaca dan, mungkin, menulis, bergantung pada jenis program); dalam kes menjalankan fungsi kernel, hujah-hujah fungsi, termasuk penunjuk ke memori kernel, dsb.

Mari kita lihat lebih dekat proses ini. Sebagai permulaan, mari kita bercakap tentang perbezaan pertama dari BPF klasik, program yang ditulis dalam pemasang. Dalam versi baharu, seni bina telah diperluaskan supaya program boleh ditulis dalam bahasa peringkat tinggi, terutamanya, sudah tentu, dalam C. Untuk ini, bahagian belakang untuk llvm telah dibangunkan, yang membolehkan penjanaan kod bait untuk seni bina BPF.

BPF untuk si kecil, bahagian satu: BPF lanjutan

Seni bina BPF direka, sebahagiannya, untuk berjalan dengan cekap pada mesin moden. Untuk menjadikan ini berfungsi dalam amalan, kod bait BPF, setelah dimuatkan ke dalam kernel, diterjemahkan ke dalam kod asli menggunakan komponen yang dipanggil pengkompil JIT (Just In Time). Seterusnya, jika anda masih ingat, dalam BPF klasik program itu dimuatkan ke dalam kernel dan dilampirkan pada sumber acara secara atom - dalam konteks panggilan sistem tunggal. Dalam seni bina baharu, ini berlaku dalam dua peringkat - pertama, kod dimuatkan ke dalam kernel menggunakan panggilan sistem bpf(2)dan kemudian, kemudian, melalui mekanisme lain yang berbeza-beza bergantung pada jenis program, program itu melekat pada sumber acara.

Di sini pembaca mungkin mempunyai soalan: adakah mungkin? Bagaimanakah keselamatan pelaksanaan kod tersebut dijamin? Keselamatan pelaksanaan dijamin kepada kami dengan peringkat memuatkan program BPF yang dipanggil verifier (dalam bahasa Inggeris peringkat ini dipanggil verifier dan saya akan terus menggunakan perkataan Inggeris):

BPF untuk si kecil, bahagian satu: BPF lanjutan

Pengesah ialah penganalisis statik yang memastikan program tidak mengganggu operasi biasa kernel. Ini, dengan cara ini, tidak bermakna bahawa program tidak boleh mengganggu operasi sistem - program BPF, bergantung pada jenis, boleh membaca dan menulis semula bahagian memori kernel, mengembalikan nilai fungsi, memangkas, menambah, menulis semula dan juga paket rangkaian ke hadapan. Verifier menjamin bahawa menjalankan program BPF tidak akan merosakkan kernel dan program yang, mengikut peraturan, mempunyai akses tulis, contohnya, data paket keluar, tidak akan dapat menulis ganti memori kernel di luar paket. Kami akan melihat pengesah dengan lebih terperinci dalam bahagian yang sepadan, selepas kami membiasakan diri dengan semua komponen BPF yang lain.

Jadi apa yang telah kita pelajari setakat ini? Pengguna menulis program dalam C, memuatkannya ke dalam kernel menggunakan panggilan sistem bpf(2), di mana ia disemak oleh pengesah dan diterjemahkan ke dalam kod bait asli. Kemudian pengguna yang sama atau pengguna lain menyambungkan program kepada sumber acara dan ia mula dilaksanakan. Memisahkan but dan sambungan adalah perlu untuk beberapa sebab. Pertama, menjalankan pengesah agak mahal dan dengan memuat turun program yang sama beberapa kali kita membuang masa komputer. Kedua, cara program disambungkan bergantung pada jenisnya, dan satu antara muka "sejagat" yang dibangunkan setahun yang lalu mungkin tidak sesuai untuk jenis program baharu. (Walaupun kini seni bina menjadi lebih matang, terdapat idea untuk menyatukan antara muka ini di peringkat libbpf.)

Pembaca yang penuh perhatian mungkin menyedari bahawa kami belum selesai dengan gambar-gambar itu. Sesungguhnya, semua perkara di atas tidak menjelaskan mengapa BPF secara asasnya mengubah gambar berbanding BPF klasik. Dua inovasi yang meluaskan skop kebolehgunaan dengan ketara ialah keupayaan untuk menggunakan memori dikongsi dan fungsi pembantu inti. Dalam BPF, memori kongsi dilaksanakan menggunakan apa yang dipanggil peta - struktur data kongsi dengan API tertentu. Mereka mungkin mendapat nama ini kerana jenis peta pertama yang muncul ialah jadual cincang. Kemudian tatasusunan muncul, jadual cincang setempat (per-CPU) dan tatasusunan setempat, pepohon carian, peta yang mengandungi penunjuk kepada program BPF dan banyak lagi. Apa yang menarik kepada kami sekarang ialah program BPF kini mempunyai keupayaan untuk mengekalkan keadaan antara panggilan dan berkongsi dengan program lain dan dengan ruang pengguna.

Peta diakses daripada proses pengguna menggunakan panggilan sistem bpf(2), dan daripada program BPF yang berjalan dalam kernel menggunakan fungsi pembantu. Selain itu, pembantu wujud bukan sahaja untuk bekerja dengan peta, tetapi juga untuk mengakses keupayaan kernel lain. Contohnya, program BPF boleh menggunakan fungsi pembantu untuk memajukan paket ke antara muka lain, menjana peristiwa perf, mengakses struktur kernel dan sebagainya.

BPF untuk si kecil, bahagian satu: BPF lanjutan

Ringkasnya, BPF menyediakan keupayaan untuk memuatkan sewenang-wenangnya, iaitu, kod pengguna yang diuji oleh pengesah ke dalam ruang kernel. Kod ini boleh menyimpan keadaan antara panggilan dan pertukaran data dengan ruang pengguna, dan juga mempunyai akses kepada subsistem kernel yang dibenarkan oleh program jenis ini.

Ini sudah serupa dengan keupayaan yang disediakan oleh modul kernel, berbanding dengan BPF yang mempunyai beberapa kelebihan (sudah tentu, anda hanya boleh membandingkan aplikasi yang serupa, sebagai contoh, pengesanan sistem - anda tidak boleh menulis pemacu sewenang-wenangnya dengan BPF). Anda boleh perhatikan ambang kemasukan yang lebih rendah (sesetengah utiliti yang menggunakan BPF tidak memerlukan pengguna mempunyai kemahiran pengaturcaraan kernel, atau kemahiran pengaturcaraan secara umum), keselamatan masa jalan (angkat tangan anda dalam komen bagi mereka yang tidak memecahkan sistem semasa menulis atau modul ujian), atomicity - terdapat masa henti semasa memuat semula modul, dan subsistem BPF memastikan tiada acara terlepas (untuk bersikap adil, ini tidak benar untuk semua jenis program BPF).

Kehadiran keupayaan sedemikian menjadikan BPF alat universal untuk mengembangkan kernel, yang disahkan dalam amalan: semakin banyak jenis program baru ditambahkan ke BPF, semakin banyak syarikat besar menggunakan BPF pada pelayan pertempuran 24Γ—7, semakin banyak permulaan membina perniagaan mereka berdasarkan penyelesaian berdasarkan BPF. BPF digunakan di mana-mana: dalam melindungi daripada serangan DDoS, mewujudkan SDN (contohnya, melaksanakan rangkaian untuk kubernet), sebagai alat pengesanan sistem utama dan pengumpul statistik, dalam sistem pengesanan pencerobohan dan sistem kotak pasir, dsb.

Mari selesaikan bahagian gambaran keseluruhan artikel di sini dan lihat mesin maya dan ekosistem BPF dengan lebih terperinci.

Penyimpangan: utiliti

Untuk dapat menjalankan contoh dalam bahagian berikut, anda mungkin memerlukan beberapa utiliti, sekurang-kurangnya llvm/clang dengan sokongan bpf dan bpftool. Dalam bahagian ini Alat Pembangunan Anda boleh membaca arahan untuk memasang utiliti, serta kernel anda. Bahagian ini diletakkan di bawah supaya tidak mengganggu keharmonian persembahan kami.

Daftar dan Sistem Arahan Mesin Maya BPF

Seni bina dan sistem arahan BPF telah dibangunkan dengan mengambil kira fakta bahawa program akan ditulis dalam bahasa C dan, selepas dimuatkan ke dalam kernel, diterjemahkan ke dalam kod asli. Oleh itu, bilangan daftar dan set arahan dipilih dengan memperhatikan persilangan, dalam pengertian matematik, keupayaan mesin moden. Di samping itu, pelbagai sekatan telah dikenakan ke atas program, contohnya, sehingga baru-baru ini tidak mungkin untuk menulis gelung dan subrutin, dan bilangan arahan dihadkan kepada 4096 (kini program istimewa boleh memuatkan sehingga satu juta arahan).

BPF mempunyai sebelas daftar 64-bit yang boleh diakses pengguna r0-r10 dan kaunter program. Daftar r10 mengandungi penuding bingkai dan dibaca sahaja. Program mempunyai akses kepada timbunan 512-bait pada masa jalan dan jumlah memori yang dikongsi tanpa had dalam bentuk peta.

Program BPF dibenarkan untuk menjalankan set khusus pembantu kernel jenis program dan, lebih baru-baru ini, fungsi biasa. Setiap fungsi yang dipanggil boleh mengambil sehingga lima argumen, diluluskan dalam daftar r1-r5, dan nilai pulangan dihantar ke r0. Ia dijamin bahawa selepas kembali dari fungsi, kandungan daftar r6-r9 tidak akan berubah.

Untuk terjemahan program yang cekap, daftar r0-r11 untuk semua seni bina yang disokong dipetakan secara unik kepada daftar sebenar, dengan mengambil kira ciri ABI seni bina semasa. Sebagai contoh, untuk x86_64 daftar r1-r5, digunakan untuk lulus parameter fungsi, dipaparkan pada rdi, rsi, rdx, rcx, r8, yang digunakan untuk menghantar parameter kepada fungsi x86_64. Sebagai contoh, kod di sebelah kiri diterjemahkan kepada kod 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 pelaksanaan program, dan dalam daftar r1 atur cara diberikan penunjuk kepada konteks - bergantung pada jenis program, ini boleh menjadi, sebagai contoh, struktur struct xdp_md (untuk XDP) atau struktur struct __sk_buff (untuk program rangkaian yang berbeza) atau struktur struct pt_regs (untuk pelbagai jenis program pengesanan), dsb.

Jadi, kami mempunyai satu set daftar, pembantu kernel, tindanan, penuding konteks dan memori yang dikongsi dalam bentuk peta. Bukan semua ini sangat diperlukan dalam perjalanan, tetapi...

Mari teruskan penerangan dan bercakap tentang sistem arahan untuk bekerja dengan objek ini. Semua (Hampir semua) Arahan BPF mempunyai saiz 64-bit tetap. Jika anda melihat satu arahan pada mesin Big Endian 64-bit, anda akan melihat

BPF untuk si kecil, bahagian satu: BPF lanjutan

ia adalah Code - ini ialah pengekodan arahan, Dst/Src ialah pengekodan penerima dan sumber, masing-masing, Off - Lekukan bertanda 16-bit, dan Imm ialah integer bertanda 32-bit yang digunakan dalam beberapa arahan (serupa dengan pemalar cBPF K). Pengekodan Code mempunyai satu daripada dua jenis:

BPF untuk si kecil, bahagian satu: BPF lanjutan

Kelas arahan 0, 1, 2, 3 mentakrifkan arahan untuk bekerja dengan ingatan. mereka dipanggil, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, masing-masing. Kelas 4, 7 (BPF_ALU, BPF_ALU64) membentuk satu set arahan ALU. Kelas 5, 6 (BPF_JMP, BPF_JMP32) mengandungi arahan lompat.

Pelan lanjut untuk mengkaji sistem arahan BPF adalah seperti berikut: daripada menyenaraikan semua arahan dan parameternya dengan teliti, kami akan melihat beberapa contoh dalam bahagian ini dan daripada mereka ia akan menjadi jelas bagaimana arahan itu sebenarnya berfungsi dan bagaimana untuk buka secara manual mana-mana fail binari untuk BPF. Untuk menyatukan bahan kemudian dalam artikel, kami juga akan bertemu dengan arahan individu dalam bahagian tentang Pengesah, pengkompil JIT, terjemahan BPF klasik, serta semasa mempelajari peta, fungsi panggilan, dll.

Apabila kita bercakap tentang arahan individu, kita akan merujuk kepada fail teras bpf.h ΠΈ bpf_common.h, yang mentakrifkan kod berangka arahan BPF. Apabila mempelajari seni bina sendiri dan/atau menghuraikan binari, anda boleh menemui semantik dalam sumber berikut, diisih mengikut kerumitan: Spesifikasi eBPF tidak rasmi, Panduan Rujukan BPF dan XDP, Set Arahan, Dokumentasi/rangkaian/filter.txt dan, sudah tentu, dalam kod sumber Linux - pengesah, JIT, penterjemah BPF.

Contoh: membuka BPF di kepala anda

Mari kita lihat contoh di mana kita menyusun atur cara readelf-example.c dan lihat binari yang terhasil. Kami akan mendedahkan kandungan asal readelf-example.c di bawah, selepas kami memulihkan logiknya daripada kod binari:

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

Lajur pertama dalam output readelf ialah lekukan dan program kami terdiri daripada empat arahan:

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

Kod arahan adalah sama b7, 15, b7 ΠΈ 95. Ingat bahawa tiga bit yang paling tidak ketara ialah kelas arahan. Dalam kes kami, bit keempat daripada semua arahan adalah kosong, jadi kelas arahan masing-masing adalah 7, 5, 7, 5. Kelas 7 ialah BPF_ALU64, dan 5 ialah BPF_JMP. Untuk kedua-dua kelas, format arahan adalah sama (lihat di atas) dan kami boleh menulis semula program kami seperti ini (pada masa yang sama kami akan menulis semula lajur yang tinggal 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 - Adakah BPF_MOV. Ia memberikan nilai kepada daftar destinasi. Jika bit ditetapkan s (sumber), maka nilai diambil dari daftar sumber, dan jika, seperti dalam kes kami, ia tidak ditetapkan, maka nilai diambil dari medan Imm. Jadi dalam arahan pertama dan ketiga kami melakukan operasi r0 = Imm. Selanjutnya, operasi kelas 1 JMP ialah BPF_JEQ (lompat jika sama). Dalam kes kami, sejak sedikit S ialah sifar, ia membandingkan nilai daftar sumber dengan medan Imm. Jika nilainya bertepatan, maka peralihan berlaku kepada PC + OffJika PC, seperti biasa, mengandungi alamat arahan seterusnya. Akhirnya, Operasi Kelas 9 JMP adalah BPF_EXIT. Arahan ini menamatkan program, kembali ke kernel r0. Mari tambah lajur baharu pada jadual kami:

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 boleh menulis semula ini dalam bentuk yang lebih mudah:

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

Jika kita ingat apa yang ada dalam daftar r1 atur cara diluluskan penunjuk kepada konteks dari kernel, dan dalam daftar r0 nilai dikembalikan kepada kernel, maka kita dapat melihat bahawa jika penunjuk kepada konteks adalah sifar, maka kita kembalikan 1, dan sebaliknya - 2. Mari kita periksa sama ada kita betul dengan melihat sumber:

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

Ya, ia adalah program yang tidak bermakna, tetapi ia diterjemahkan kepada hanya empat arahan mudah.

Contoh pengecualian: Arahan 16-bait

Kami menyebut sebelum ini bahawa beberapa arahan mengambil lebih daripada 64 bit. Ini terpakai, sebagai contoh, untuk arahan lddw (Kod = 0x18 = BPF_LD | BPF_DW | BPF_IMM) β€” memuatkan kata ganda dari medan ke dalam daftar Imm. Hakikatnya ialah Imm mempunyai saiz 32, dan kata ganda ialah 64 bit, jadi memuatkan nilai segera 64-bit ke dalam daftar dalam satu arahan 64-bit tidak akan berfungsi. Untuk melakukan ini, dua arahan bersebelahan digunakan untuk menyimpan bahagian kedua nilai 64-bit dalam medan 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                   ........

Terdapat hanya dua arahan dalam program binari:

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

Kita akan berjumpa lagi dengan arahan lddw, apabila kita bercakap tentang penempatan semula dan bekerja dengan peta.

Contoh: membuka BPF menggunakan alat standard

Jadi, kami telah belajar membaca kod binari BPF dan bersedia untuk menghuraikan sebarang arahan jika perlu. Walau bagaimanapun, patut dikatakan bahawa dalam praktiknya adalah lebih mudah dan lebih cepat untuk membongkar program menggunakan alat standard, sebagai contoh:

$ 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

Kitaran hayat objek BPF, sistem fail bpffs

(Saya mula-mula mempelajari beberapa butiran yang diterangkan dalam subseksyen ini daripada Lent Alexei Starovoitov masuk Blog BPF.)

Objek BPF - program dan peta - dicipta daripada ruang pengguna menggunakan arahan BPF_PROG_LOAD ΠΈ BPF_MAP_CREATE panggilan sistem bpf(2), kita akan bercakap tentang bagaimana ini berlaku dalam bahagian seterusnya. Ini mewujudkan struktur data kernel dan untuk setiap daripadanya refcount (kiraan rujukan) ditetapkan kepada satu, dan deskriptor fail yang menunjuk ke objek dikembalikan kepada pengguna. Selepas pemegang ditutup refcount objek dikurangkan dengan satu, dan apabila ia mencapai sifar, objek itu dimusnahkan.

Jika program menggunakan peta, maka refcount peta ini ditambah satu selepas memuatkan atur cara, i.e. deskriptor fail mereka boleh ditutup daripada proses pengguna dan masih refcount tidak akan menjadi sifar:

BPF untuk si kecil, bahagian satu: BPF lanjutan

Selepas berjaya memuatkan program, kami biasanya melampirkannya pada beberapa jenis penjana acara. Sebagai contoh, kita boleh meletakkannya pada antara muka rangkaian untuk memproses paket masuk atau menyambungkannya kepada beberapa tracepoint dalam inti. Pada ketika ini, kaunter rujukan juga akan meningkat sebanyak satu dan kami akan dapat menutup deskriptor fail dalam program pemuat.

Apa yang berlaku jika kita sekarang menutup pemuat but? Ia bergantung kepada jenis penjana acara (cangkuk). Semua cangkuk rangkaian akan wujud selepas pemuat selesai, ini adalah apa yang dipanggil cangkuk global. Dan, sebagai contoh, program jejak akan dikeluarkan selepas proses yang menciptanya ditamatkan (dan oleh itu dipanggil setempat, daripada "tempatan kepada proses"). Secara teknikal, cangkuk tempatan sentiasa mempunyai deskriptor fail yang sepadan dalam ruang pengguna dan oleh itu ditutup apabila proses ditutup, tetapi cangkuk global tidak. Dalam rajah berikut, menggunakan salib merah, saya cuba menunjukkan bagaimana penamatan program pemuat mempengaruhi jangka hayat objek dalam kes cangkuk tempatan dan global.

BPF untuk si kecil, bahagian satu: BPF lanjutan

Mengapakah terdapat perbezaan antara cangkuk tempatan dan global? Menjalankan beberapa jenis program rangkaian masuk akal tanpa ruang pengguna, sebagai contoh, bayangkan perlindungan DDoS - pemuat but menulis peraturan dan menyambungkan program BPF ke antara muka rangkaian, selepas itu pemuat but boleh pergi dan membunuh dirinya sendiri. Sebaliknya, bayangkan program jejak penyahpepijatan yang anda tulis di atas lutut anda dalam masa sepuluh minit - apabila selesai, anda ingin tiada sisa sampah dalam sistem dan cangkuk tempatan akan memastikannya.

Sebaliknya, bayangkan anda ingin menyambung ke titik jejak dalam kernel dan mengumpul statistik selama bertahun-tahun. Dalam kes ini, anda ingin melengkapkan bahagian pengguna dan kembali ke statistik dari semasa ke semasa. Sistem fail bpf menyediakan peluang ini. Ia adalah sistem fail pseudo dalam memori yang membolehkan penciptaan fail yang merujuk objek BPF dan dengan itu meningkatkan refcount objek. Selepas ini, pemuat boleh keluar, dan objek yang diciptanya akan kekal hidup.

BPF untuk si kecil, bahagian satu: BPF lanjutan

Mencipta fail dalam bpff yang merujuk objek BPF dipanggil "menyemat" (seperti dalam frasa berikut: "proses boleh menyemat program atau peta BPF"). Mencipta objek fail untuk objek BPF masuk akal bukan sahaja untuk memanjangkan hayat objek tempatan, tetapi juga untuk kebolehgunaan objek global - kembali kepada contoh dengan program perlindungan DDoS global, kami ingin dapat datang dan melihat statistik dari semasa ke semasa.

Sistem fail BPF biasanya dipasang /sys/fs/bpf, tetapi ia juga boleh dipasang secara tempatan, contohnya, seperti ini:

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

Nama sistem fail dicipta menggunakan arahan BPF_OBJ_PIN panggilan sistem BPF. Untuk menggambarkan, mari ambil program, susun, muat naik dan sematkannya bpffs. Program kami tidak melakukan apa-apa yang berguna, kami hanya membentangkan kod supaya anda boleh menghasilkan semula contoh:

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

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

Mari kita susun atur cara ini dan buat salinan tempatan sistem fail bpffs:

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

Sekarang mari muat turun program kami menggunakan utiliti bpftool dan lihat panggilan sistem yang disertakan bpf(2) (beberapa baris yang tidak berkaitan dialih keluar daripada output 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 memuatkan program menggunakan BPF_PROG_LOAD, menerima deskriptor fail daripada kernel 3 dan menggunakan arahan BPF_OBJ_PIN menyematkan deskriptor fail ini sebagai fail "bpf-mountpoint/test". Selepas ini program pemuat but bpftool selesai berjalan, tetapi program kami kekal dalam kernel, walaupun kami tidak melampirkannya pada mana-mana antara muka rangkaian:

$ 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

Kami boleh memadamkan objek fail seperti biasa unlink(2) dan selepas itu program yang sepadan akan dipadamkan:

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

Memadam objek

Bercakap tentang memadam objek, adalah perlu untuk menjelaskan bahawa selepas kami memutuskan sambungan program dari cangkuk (penjana acara), tiada satu acara baharu akan mencetuskan pelancarannya, bagaimanapun, semua contoh semasa program akan diselesaikan dalam susunan biasa .

Beberapa jenis program BPF membolehkan anda menggantikan program dengan cepat, i.e. menyediakan keatomitian jujukan replace = detach old program, attach new program. Dalam kes ini, semua contoh aktif versi lama program akan menyelesaikan kerja mereka, dan pengendali acara baharu akan dibuat daripada program baharu, dan "atomicity" di sini bermakna tiada satu acara pun akan terlepas.

Melampirkan program pada sumber acara

Dalam artikel ini, kami tidak akan menerangkan secara berasingan penyambungan program kepada sumber acara, kerana wajar untuk mengkaji perkara ini dalam konteks jenis program tertentu. Cm. contoh di bawah, di mana kami menunjukkan cara program seperti XDP disambungkan.

Memanipulasi Objek Menggunakan Panggilan Sistem bpf

program BPF

Semua objek BPF dicipta dan diuruskan dari ruang pengguna menggunakan panggilan sistem bpf, mempunyai prototaip berikut:

#include <linux/bpf.h>

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

Inilah pasukannya cmd adalah salah satu nilai jenis enum bpf_cmd, attr β€” penunjuk kepada parameter untuk program tertentu dan size β€” saiz objek mengikut penunjuk, i.e. selalunya ini sizeof(*attr). Dalam kernel 5.8 panggilan sistem bpf menyokong 34 arahan yang berbeza, dan ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ union bpf_attr menduduki 200 baris. Tetapi kita tidak seharusnya takut dengan ini, kerana kita akan membiasakan diri dengan arahan dan parameter sepanjang beberapa artikel.

Mari kita mulakan dengan pasukan BPF_PROG_LOAD, yang mencipta program BPF - mengambil satu set arahan BPF dan memuatkannya ke dalam kernel. Pada saat pemuatan, pengesah dilancarkan, dan kemudian pengkompil JIT dan, selepas pelaksanaan berjaya, deskriptor fail program dikembalikan kepada pengguna. Kami melihat apa yang berlaku kepadanya seterusnya dalam bahagian sebelumnya tentang kitaran hidup objek BPF.

Kami kini akan menulis program tersuai yang akan memuatkan program BPF yang mudah, tetapi pertama-tama kami perlu memutuskan jenis program yang ingin kami muatkan - kami perlu memilih Taipkan dan dalam rangka kerja jenis ini, tulis program yang akan lulus ujian pengesah. Walau bagaimanapun, untuk tidak merumitkan proses, berikut adalah penyelesaian siap sedia: kami akan mengambil program seperti BPF_PROG_TYPE_XDP, yang akan mengembalikan nilai XDP_PASS (langkau semua pakej). Dalam pemasang BPF ia kelihatan sangat mudah:

r0 = 2
exit

Selepas kami membuat keputusan bahawa kami akan memuat naik, kami boleh memberitahu 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 program bermula dengan definisi tatasusunan insns - program BPF kami dalam kod mesin. Dalam kes ini, setiap arahan program BPF dimasukkan ke dalam struktur bpf_insn. Elemen pertama insns mematuhi arahan r0 = 2, kedua - exit.

Berundur. Kernel mentakrifkan makro yang lebih mudah untuk menulis kod mesin, dan menggunakan fail pengepala kernel tools/include/linux/filter.h kita boleh menulis

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

Tetapi memandangkan menulis program BPF dalam kod asli hanya diperlukan untuk menulis ujian dalam kernel dan artikel tentang BPF, ketiadaan makro ini tidak benar-benar merumitkan kehidupan pembangun.

Selepas mentakrifkan program BPF, kami meneruskan untuk memuatkannya ke dalam kernel. Set parameter minimalis kami attr termasuk jenis program, set dan bilangan arahan, lesen yang diperlukan dan nama "woo", yang kami gunakan untuk mencari program kami pada sistem selepas memuat turun. Program, seperti yang dijanjikan, dimuatkan ke dalam sistem menggunakan panggilan sistem bpf.

Pada penghujung program kita berakhir dalam gelung tak terhingga yang mensimulasikan muatan. Tanpa itu, program akan dibunuh oleh kernel apabila deskriptor fail yang panggilan sistem dikembalikan kepada kami ditutup bpf, dan kami tidak akan melihatnya dalam sistem.

Nah, kami bersedia untuk ujian. Mari kita kumpulkan dan jalankan program di bawah straceuntuk memastikan 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 sahaja, bpf(2) mengembalikan pemegang 3 kepada kami dan kami pergi ke gelung tak terhingga dengan pause(). Mari cuba cari program kami dalam sistem. Untuk melakukan ini, kami akan pergi ke terminal lain dan menggunakan utiliti 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 bahawa terdapat program yang dimuatkan pada sistem woo ID globalnya ialah 390 dan sedang dalam proses simple-prog terdapat deskriptor fail terbuka yang menunjuk ke program (dan jika simple-prog akan menyelesaikan kerja itu, kemudian woo akan hilang). Seperti yang dijangka, program woo mengambil 16 bait - dua arahan - kod binari dalam seni bina BPF, tetapi dalam bentuk asalnya (x86_64) ia sudah 40 bait. Mari lihat program kami dalam bentuk asalnya:

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

tiada kejutan. Sekarang mari kita lihat kod yang dihasilkan oleh pengkompil 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 begitu berkesan untuk exit(2), tetapi secara adil, program kami terlalu mudah, dan untuk program yang tidak remeh, prolog dan epilog yang ditambahkan oleh pengkompil JIT, sudah tentu, diperlukan.

Peta

Program BPF boleh menggunakan kawasan memori berstruktur yang boleh diakses kedua-dua program BPF lain dan program dalam ruang pengguna. Objek ini dipanggil peta dan dalam bahagian ini kami akan menunjukkan cara untuk memanipulasinya menggunakan panggilan sistem bpf.

Katakan dengan segera bahawa keupayaan peta tidak terhad hanya kepada akses kepada memori yang dikongsi. Terdapat peta tujuan khas yang mengandungi, sebagai contoh, penunjuk kepada program BPF atau penunjuk kepada antara muka rangkaian, peta untuk bekerja dengan acara perf, dsb. Kami tidak akan bercakap tentang mereka di sini, supaya tidak mengelirukan pembaca. Selain daripada ini, kami mengabaikan isu penyegerakan, kerana ini tidak penting untuk contoh kami. Senarai lengkap jenis peta yang tersedia boleh didapati di <linux/bpf.h>, dan dalam bahagian ini kita akan mengambil sebagai contoh jenis pertama dari segi sejarah, jadual cincang BPF_MAP_TYPE_HASH.

Jika anda mencipta jadual hash dalam, katakan, C++, anda akan katakan unordered_map<int,long> woo, yang dalam bahasa Rusia bermaksud β€œSaya perlukan meja woo saiz tidak terhad, yang kuncinya adalah jenis int, dan nilainya ialah jenisnya long" Untuk mencipta jadual cincang BPF, kita perlu melakukan banyak perkara yang sama, kecuali kita perlu menentukan saiz maksimum jadual, dan bukannya menentukan jenis kunci dan nilai, kita perlu menentukan saiznya dalam bait . Untuk membuat peta gunakan arahan BPF_MAP_CREATE panggilan sistem bpf. Mari kita lihat program yang lebih kurang minimum yang mencipta peta. Selepas program sebelumnya yang memuatkan program BPF, program ini sepatutnya kelihatan mudah kepada 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 menentukan satu set parameter attr, di mana kita berkata "Saya memerlukan jadual cincang dengan kunci dan nilai saiz sizeof(int), di mana saya boleh meletakkan maksimum empat elemen." Apabila membuat peta BPF, anda boleh menentukan parameter lain, contohnya, dengan cara yang sama seperti dalam contoh dengan program, kami menentukan nama objek sebagai "woo".

Mari kita susun dan jalankan program:

$ 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 sistem bpf(2) mengembalikan nombor peta deskriptor kepada kami 3 dan kemudian program, seperti yang dijangkakan, menunggu arahan selanjutnya dalam panggilan sistem pause(2).

Sekarang mari hantar program kami ke latar belakang atau buka terminal lain dan lihat objek kami menggunakan utiliti bpftool (kita boleh membezakan peta kita dari yang lain dengan namanya):

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

Nombor 114 ialah ID global objek kami. Mana-mana program pada sistem boleh menggunakan ID ini untuk membuka peta sedia ada menggunakan arahan BPF_MAP_GET_FD_BY_ID panggilan sistem bpf.

Sekarang kita boleh bermain dengan jadual hash kita. Mari kita lihat kandungannya:

$ sudo bpftool map dump id 114
Found 0 elements

kosong. Mari kita meletakkan nilai di dalamnya hash[1] = 1:

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

Mari lihat jadual sekali lagi:

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

Hooray! Kami berjaya menambah satu elemen. Ambil perhatian bahawa kita perlu bekerja pada tahap bait untuk melakukan ini, kerana bptftool tidak tahu jenis nilai dalam jadual hash. (Pengetahuan ini boleh dipindahkan kepadanya menggunakan BTF, tetapi lebih banyak tentang itu sekarang.)

Bagaimanakah sebenarnya bpftool membaca dan menambah elemen? Mari lihat di bawah tudung:

$ 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

Mula-mula kami membuka peta dengan ID globalnya menggunakan arahan BPF_MAP_GET_FD_BY_ID ΠΈ bpf(2) mengembalikan deskriptor 3 kepada kami. Selanjutnya menggunakan arahan BPF_MAP_GET_NEXT_KEY kami menemui kunci pertama dalam jadual dengan lulus NULL sebagai penunjuk kepada kunci "sebelumnya". Kalau kita ada kunci kita boleh buat BPF_MAP_LOOKUP_ELEMyang mengembalikan nilai kepada penunjuk value. Langkah seterusnya ialah kami cuba mencari elemen seterusnya dengan menghantar penunjuk ke kunci semasa, tetapi jadual kami hanya mengandungi satu elemen dan arahan BPF_MAP_GET_NEXT_KEY pulangan ENOENT.

Okay, mari tukar nilai dengan kunci 1, katakan logik perniagaan 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 dijangkakan, ia sangat mudah: arahan BPF_MAP_GET_FD_BY_ID membuka peta kami dengan ID, dan arahan BPF_MAP_UPDATE_ELEM menimpa elemen.

Jadi, selepas mencipta jadual cincang daripada satu program, kita boleh membaca dan menulis kandungannya daripada program lain. Ambil perhatian bahawa jika kami dapat melakukan ini dari baris arahan, maka mana-mana program lain pada sistem boleh melakukannya. Sebagai tambahan kepada arahan yang diterangkan di atas, untuk bekerja dengan peta dari ruang pengguna, Yang berikut:

  • BPF_MAP_LOOKUP_ELEM: cari nilai dengan kunci
  • BPF_MAP_UPDATE_ELEM: kemas kini/cipta nilai
  • BPF_MAP_DELETE_ELEM: keluarkan kunci
  • BPF_MAP_GET_NEXT_KEY: cari kekunci seterusnya (atau pertama).
  • BPF_MAP_GET_NEXT_ID: membolehkan anda melalui semua peta sedia ada, begitulah cara ia berfungsi bpftool map
  • BPF_MAP_GET_FD_BY_ID: membuka peta sedia ada dengan ID globalnya
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: mengemas kini nilai objek secara atom dan mengembalikan yang lama
  • BPF_MAP_FREEZE: jadikan peta tidak berubah daripada ruang pengguna (operasi ini tidak boleh dibuat asal)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: operasi besar-besaran. Sebagai contoh, BPF_MAP_LOOKUP_AND_DELETE_BATCH - ini adalah satu-satunya cara yang boleh dipercayai untuk membaca dan menetapkan semula semua nilai dari peta

Tidak semua arahan ini berfungsi untuk semua jenis peta, tetapi secara umum bekerja dengan jenis peta lain dari ruang pengguna kelihatan sama seperti bekerja dengan jadual cincang.

Demi pesanan, mari selesaikan eksperimen jadual hash kami. Ingat bahawa kami mencipta jadual yang boleh mengandungi sehingga empat kekunci? Mari tambah beberapa lagi elemen:

$ 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

Setakat ini baik:

$ 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 cuba tambah 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 dijangka, kami tidak berjaya. Mari lihat ralat dengan lebih terperinci:

$ 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 dijangkakan, pasukan BPF_MAP_UPDATE_ELEM cuba mencipta kunci baharu, kelima, tetapi ranap E2BIG.

Jadi, kita boleh mencipta dan memuatkan program BPF, serta mencipta dan mengurus peta dari ruang pengguna. Sekarang adalah logik untuk melihat bagaimana kita boleh menggunakan peta daripada program BPF itu sendiri. Kita boleh bercakap tentang ini dalam bahasa program yang sukar dibaca dalam kod makro mesin, tetapi sebenarnya sudah tiba masanya untuk menunjukkan bagaimana program BPF sebenarnya ditulis dan diselenggara - menggunakan libbpf.

(Bagi pembaca yang tidak berpuas hati dengan kekurangan contoh peringkat rendah: kami akan menganalisis secara terperinci program yang menggunakan peta dan fungsi pembantu yang dibuat menggunakan libbpf dan memberitahu anda apa yang berlaku pada peringkat arahan. Bagi pembaca yang tidak berpuas hati sangat banyak, tambah kami contoh di tempat yang sesuai dalam artikel.)

Menulis program BPF menggunakan libbpf

Menulis program BPF menggunakan kod mesin boleh menjadi menarik hanya pada kali pertama, dan kemudian rasa kenyang bermula. Pada masa ini anda perlu mengalihkan perhatian anda llvm, yang mempunyai bahagian belakang untuk menjana kod untuk seni bina BPF, serta perpustakaan libbpf, yang membolehkan anda menulis bahagian pengguna aplikasi BPF dan memuatkan kod program BPF yang dijana menggunakan llvm/clang.

Malah, seperti yang akan kita lihat dalam artikel ini dan seterusnya, libbpf melakukan banyak kerja tanpanya (atau alat serupa - iproute2, libbcc, libbpf-go, dsb.) adalah mustahil untuk hidup. Salah satu ciri pembunuh projek libbpf ialah BPF CO-RE (Compile Once, Run Everywhere) - projek yang membolehkan anda menulis program BPF yang mudah alih dari satu kernel ke yang lain, dengan keupayaan untuk dijalankan pada API yang berbeza (contohnya, apabila struktur kernel berubah daripada versi kepada versi). Untuk dapat bekerja dengan CO-RE, kernel anda mesti disusun dengan sokongan BTF (kami menerangkan cara melakukannya dalam bahagian Alat Pembangunan. Anda boleh menyemak sama ada kernel anda dibina dengan BTF atau tidak begitu mudah - dengan kehadiran fail berikut:

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

Fail ini menyimpan maklumat tentang semua jenis data yang digunakan dalam kernel dan digunakan dalam semua contoh kami menggunakan libbpf. Kami akan bercakap secara terperinci tentang CO-RE dalam artikel seterusnya, tetapi dalam artikel ini - hanya bina kernel dengan anda sendiri CONFIG_DEBUG_INFO_BTF.

Perpustakaan libbpf tinggal betul-betul dalam direktori tools/lib/bpf kernel dan pembangunannya dijalankan melalui senarai mel [email protected]. Walau bagaimanapun, repositori berasingan dikekalkan untuk keperluan aplikasi yang tinggal di luar kernel https://github.com/libbpf/libbpf di mana pustaka kernel dicerminkan untuk akses baca lebih kurang seperti sedia ada.

Dalam bahagian ini kita akan melihat bagaimana anda boleh membuat projek yang menggunakan libbpf, mari tulis beberapa (lebih kurang tidak bermakna) program ujian dan analisis secara terperinci cara semuanya berfungsi. Ini akan membolehkan kami menerangkan dengan lebih mudah dalam bahagian berikut dengan tepat bagaimana program BPF berinteraksi dengan peta, pembantu kernel, BTF, dsb.

Biasanya projek menggunakan libbpf tambah repositori GitHub sebagai submodul git, kami akan melakukan perkara 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 mudah:

$ 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

Pelan seterusnya kami dalam bahagian ini adalah seperti berikut: kami akan menulis program BPF seperti BPF_PROG_TYPE_XDP, sama seperti dalam contoh sebelumnya, tetapi dalam C, kami menyusunnya menggunakan clang, dan tulis program pembantu yang akan memuatkannya ke dalam kernel. Dalam bahagian berikut kami akan mengembangkan keupayaan kedua-dua program BPF dan program pembantu.

Contoh: mencipta aplikasi penuh menggunakan libbpf

Sebagai permulaan, kami menggunakan fail /sys/kernel/btf/vmlinux, yang disebutkan di atas, dan buat yang setara dengannya dalam bentuk fail pengepala:

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

Fail ini akan menyimpan semua struktur data yang tersedia dalam kernel kami, sebagai contoh, ini adalah cara pengepala IPv4 ditakrifkan dalam 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 kami akan menulis program BPF kami dalam 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";

Walaupun program kami ternyata sangat mudah, kami masih perlu memberi perhatian kepada banyak butiran. Pertama, fail pengepala pertama yang kami sertakan ialah vmlinux.h, yang baru kami hasilkan menggunakan bpftool btf dump - kini kita tidak perlu memasang pakej kernel-headers untuk mengetahui rupa struktur kernel. Fail pengepala berikut datang kepada kami daripada pustaka libbpf. Sekarang kita hanya memerlukannya untuk menentukan makro SEC, yang menghantar aksara ke bahagian yang sesuai bagi fail objek ELF. Program kami terkandung dalam bahagian xdp/simple, di mana sebelum garis miring kita menentukan jenis program BPF - ini adalah konvensyen yang digunakan dalam libbpf, berdasarkan nama bahagian ia akan menggantikan jenis yang betul pada permulaan bpf(2). Program BPF itu sendiri ialah C - sangat mudah dan terdiri daripada satu baris return XDP_PASS. Akhirnya, bahagian yang berasingan "license" mengandungi nama lesen.

Kami boleh menyusun atur cara kami menggunakan llvm/clang, versi >= 10.0.0, atau lebih baik lagi, lebih besar (lihat bahagian Alat Pembangunan):

$ 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

Antara ciri menarik: kami menunjukkan seni bina sasaran -target bpf dan laluan ke pengepala libbpf, yang kami pasang baru-baru ini. Juga, jangan lupa tentang -O2, tanpa pilihan ini anda mungkin akan mendapat kejutan pada masa hadapan. Mari lihat kod kami, adakah kami berjaya menulis program yang kami mahukan?

$ 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, ia berjaya! Sekarang, kami mempunyai fail binari dengan program, dan kami ingin mencipta aplikasi yang akan memuatkannya ke dalam kernel. Untuk tujuan ini perpustakaan libbpf menawarkan kami dua pilihan - gunakan API peringkat rendah atau API peringkat lebih tinggi. Kami akan pergi ke cara kedua, kerana kami ingin belajar cara menulis, memuatkan dan menyambungkan program BPF dengan usaha yang minimum untuk kajian seterusnya.

Pertama, kita perlu menjana "rangka" program kami daripada binarinya menggunakan utiliti yang sama bpftool β€” pisau Swiss dunia BPF (yang boleh diambil secara literal, memandangkan Daniel Borkman, salah seorang pencipta dan penyelenggara BPF, adalah Switzerland):

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

Dalam fail xdp-simple.skel.h mengandungi kod binari program kami dan fungsi untuk mengurus - memuatkan, melampirkan, memadam objek kami. Dalam kes mudah kami, ini kelihatan seperti berlebihan, tetapi ia juga berfungsi dalam kes di mana fail objek mengandungi banyak program dan peta BPF dan untuk memuatkan ELF gergasi ini, kami hanya perlu menjana rangka dan memanggil satu atau dua fungsi daripada aplikasi tersuai yang kami sedang menulis Mari kita teruskan sekarang.

Tegasnya, program pemuat kami adalah remeh:

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

ia adalah struct xdp_simple_bpf ditakrifkan dalam fail xdp-simple.skel.h dan menerangkan fail 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 boleh melihat kesan API peringkat rendah di sini: struktur struct bpf_program *simple ΠΈ struct bpf_link *simple. Struktur pertama secara khusus menerangkan program kami, yang ditulis dalam bahagian xdp/simple, dan yang kedua menerangkan cara program menyambung kepada sumber acara.

Fungsi xdp_simple_bpf__open_and_load, membuka objek ELF, menghuraikannya, mencipta semua struktur dan substruktur (selain program, ELF juga mengandungi bahagian lain - data, data baca sahaja, maklumat penyahpepijatan, lesen, dll.), dan kemudian memuatkannya ke dalam kernel menggunakan sistem panggilan bpf, yang boleh kita semak dengan menyusun 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 kami menggunakan bpftool. Jom cari ID dia:

# 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 dipendekkan 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 sebahagian daripada fail sumber C kami. Ini dilakukan oleh perpustakaan libbpf, yang menemui bahagian nyahpepijat dalam binari, menyusunnya ke dalam objek BTF, memuatkannya ke dalam kernel menggunakan BPF_BTF_LOAD, dan kemudian menentukan deskriptor fail yang terhasil apabila memuatkan atur cara dengan arahan BPG_PROG_LOAD.

Pembantu Inti

Program BPF boleh menjalankan fungsi "luaran" - pembantu kernel. Fungsi pembantu ini membolehkan program BPF mengakses struktur kernel, mengurus peta, dan juga berkomunikasi dengan "dunia nyata" - mencipta acara perf, mengawal perkakasan (contohnya, mengubah hala paket), dsb.

Contoh: bpf_get_smp_processor_id

Dalam rangka paradigma "belajar melalui teladan", mari kita pertimbangkan salah satu fungsi pembantu, bpf_get_smp_processor_id(), sesetengah dalam fail kernel/bpf/helpers.c. Ia mengembalikan nombor pemproses yang mana program BPF yang memanggilnya sedang berjalan. Tetapi kami tidak begitu berminat dengan semantiknya seperti dalam fakta bahawa pelaksanaannya mengambil satu baris:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

Takrifan fungsi pembantu BPF adalah serupa dengan takrifan panggilan sistem Linux. Di sini, sebagai contoh, fungsi ditakrifkan yang tidak mempunyai hujah. (Fungsi yang mengambil, katakan, tiga argumen ditakrifkan menggunakan makro BPF_CALL_3. Bilangan maksimum hujah ialah lima.) Walau bagaimanapun, ini hanyalah bahagian pertama definisi. Bahagian kedua adalah untuk menentukan struktur jenis struct bpf_func_proto, yang mengandungi perihalan fungsi pembantu yang difahami oleh pengesah:

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 menggunakan fungsi ini, mereka mesti mendaftarkannya, contohnya untuk jenis BPF_PROG_TYPE_XDP fungsi ditakrifkan dalam kernel xdp_func_proto, yang menentukan daripada ID fungsi pembantu sama ada XDP menyokong fungsi ini atau tidak. Fungsi kami ialah menyokong:

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 baharu "ditakrifkan" dalam fail include/linux/bpf_types.h menggunakan makro BPF_PROG_TYPE. Ditakrifkan dalam petikan kerana ia adalah definisi logik, dan dalam istilah bahasa C definisi keseluruhan set struktur konkrit berlaku di tempat lain. Khususnya, dalam fail kernel/bpf/verifier.c semua definisi dari fail bpf_types.h digunakan untuk mencipta pelbagai 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
};

Iaitu, untuk setiap jenis program BPF, penunjuk kepada struktur data jenis ditakrifkan struct bpf_verifier_ops, yang dimulakan dengan nilai _name ## _verifier_ops, iaitu, xdp_verifier_ops untuk xdp. Struktur xdp_verifier_ops bertekad dalam fail net/core/filter.c seperti 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 biasa kita xdp_func_proto, yang akan menjalankan pengesah setiap kali ia menghadapi cabaran semacam berfungsi dalam program BPF, lihat verifier.c.

Mari kita lihat bagaimana program BPF hipotetikal menggunakan fungsi tersebut bpf_get_smp_processor_id. Untuk melakukan ini, kami menulis semula program dari bahagian sebelumnya seperti 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";

Muzikal bpf_get_smp_processor_id bertekad Π² <bpf/bpf_helper_defs.h> perpustakaan libbpf sebagai

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

itu dia, bpf_get_smp_processor_id ialah penunjuk fungsi yang nilainya ialah 8, dengan 8 ialah nilainya BPF_FUNC_get_smp_processor_id jenis enum bpf_fun_id, yang ditakrifkan untuk kami dalam fail vmlinux.h (fail bpf_helper_defs.h dalam kernel dijana oleh skrip, jadi nombor "ajaib" adalah ok). Fungsi ini tidak mengambil hujah dan mengembalikan nilai jenis __u32. Apabila kami menjalankannya dalam program kami, clang menghasilkan arahan BPF_CALL "jenis yang betul" Mari kita susun atur cara dan lihat bahagiannya 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

Dalam baris pertama kita melihat arahan call, parameter IMM yang bersamaan dengan 8, dan SRC_REG - sifar. Menurut perjanjian ABI yang digunakan oleh pengesah, ini ialah panggilan ke fungsi pembantu nombor lapan. Sebaik sahaja ia dilancarkan, logiknya mudah. Pulangan nilai daripada daftar r0 disalin ke r1 dan pada baris 2,3 ia ditukar kepada menaip u32 β€” 32 bit atas dibersihkan. Pada baris 4,5,6,7 kita kembalikan 2 (XDP_PASS) atau 1 (XDP_DROP) bergantung pada sama ada fungsi pembantu dari baris 0 mengembalikan nilai sifar atau bukan sifar.

Mari kita uji diri kita: muatkan program dan lihat pada output bpftool prog dump xlated:

$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914

$ sudo bpftool p | grep simple
523: xdp  name simple  tag 44c38a10c657e1b0  gpl
        pids xdp-simple(10915)

$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
   0: (85) call bpf_get_smp_processor_id#114128
   1: (bf) r1 = r0
   2: (67) r1 <<= 32
   3: (77) r1 >>= 32
   4: (b7) r0 = 2
; }
   5: (15) if r1 == 0x0 goto pc+1
   6: (b7) r0 = 1
   7: (95) exit

Ok, pengesah menemui kernel-helper yang betul.

Contoh: lulus hujah dan akhirnya menjalankan program!

Semua fungsi pembantu peringkat larian mempunyai prototaip

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

Parameter kepada fungsi pembantu dihantar dalam daftar r1-r5, dan nilai dikembalikan dalam daftar r0. Tiada fungsi yang mengambil lebih daripada lima hujah, dan sokongan untuknya tidak dijangka akan ditambah pada masa hadapan.

Mari kita lihat pembantu kernel baharu dan cara BPF melepasi parameter. Mari kita menulis semula xdp-simple.bpf.c seperti berikut (baris selebihnya 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 nombor CPU yang ia sedang berjalan. Mari kita susun dan lihat kodnya:

$ 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

Dalam baris 0-7 kita menulis rentetan running on CPU%un, dan kemudian pada baris 8 kami menjalankan yang biasa bpf_get_smp_processor_id. Pada baris 9-12 kami menyediakan hujah pembantu bpf_printk - daftar r1, r2, r3. Mengapa terdapat tiga daripadanya dan bukan dua? Kerana bpf_printkini adalah pembalut makro di sekeliling penolong sebenar bpf_trace_printk, yang perlu melepasi saiz rentetan format.

Sekarang mari tambahkan beberapa baris ke xdp-simple.csupaya program kami bersambung ke antara muka lo dan benar-benar bermula!

$ 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 fungsi tersebut bpf_set_link_xdp_fd, yang menghubungkan program BPF jenis XDP kepada antara muka rangkaian. Kami mengekod nombor antara muka lo, yang sentiasa 1. Kami menjalankan fungsi dua kali untuk terlebih dahulu menanggalkan program lama jika ia dilampirkan. Perhatikan bahawa sekarang kita tidak memerlukan cabaran pause atau gelung tak terhingga: program pemuat kami akan keluar, tetapi program BPF tidak akan dimatikan kerana ia disambungkan kepada sumber acara. Selepas muat turun dan sambungan berjaya, program akan dilancarkan untuk setiap paket rangkaian yang tiba di lo.

Mari muat turun program dan lihat antara muka 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 muat turun mempunyai ID 669 dan kami melihat ID yang sama pada antara muka lo. Kami akan menghantar beberapa pakej kepada 127.0.0.1 (permintaan + jawapan):

$ ping -c1 localhost

dan sekarang mari kita lihat kandungan fail maya nyahpepijat /sys/kernel/debug/tracing/trace_pipe, di mana bpf_printk menulis mesejnya:

# 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 bungkusan telah dikesan lo dan diproses pada CPU0 - program BPF pertama kami yang tidak bermakna berjaya!

Perlu diingat bahawa bpf_printk Ia bukan sia-sia ia menulis ke fail nyahpepijat: ini bukan pembantu yang paling berjaya untuk digunakan dalam pengeluaran, tetapi matlamat kami adalah untuk menunjukkan sesuatu yang mudah.

Mengakses peta daripada program BPF

Contoh: menggunakan peta daripada program BPF

Dalam bahagian sebelumnya kita belajar cara mencipta dan menggunakan peta dari ruang pengguna, dan sekarang mari kita lihat bahagian kernel. Mari kita mulakan, seperti biasa, dengan contoh. Mari kita tulis semula program kita xdp-simple.bpf.c seperti 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";

Pada permulaan program kami menambah definisi peta woo: Ini ialah tatasusunan 8 elemen yang menyimpan nilai seperti u64 (dalam C kita akan mentakrifkan tatasusunan seperti u64 woo[8]). Dalam satu program "xdp/simple" kita mendapatkan nombor pemproses semasa menjadi pembolehubah key dan kemudian menggunakan fungsi pembantu bpf_map_lookup_element kita mendapat penunjuk ke entri yang sepadan dalam tatasusunan, yang kita tambahkan satu. Diterjemah ke dalam bahasa Rusia: kami mengira statistik tentang CPU yang memproses paket masuk. Mari cuba jalankan program:

$ 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 sama ada dia telah disambungkan lo dan hantar 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 kandungan array:

$ sudo bpftool map dump name woo
[
    { "key": 0, "value": 0 },
    { "key": 1, "value": 400 },
    { "key": 2, "value": 0 },
    { "key": 3, "value": 0 },
    { "key": 4, "value": 0 },
    { "key": 5, "value": 0 },
    { "key": 6, "value": 0 },
    { "key": 7, "value": 46400 }
]

Hampir semua proses telah diproses pada CPU7. Ini tidak penting kepada kami, perkara utama ialah program ini berfungsi dan kami memahami cara mengakses peta daripada program BPF - menggunakan Ρ…Π΅Π»ΠΏΠ΅Ρ€ΠΎΠ² bpf_mp_*.

Indeks mistik

Jadi, kita boleh mengakses peta daripada program BPF menggunakan panggilan seperti

val = bpf_map_lookup_elem(&woo, &key);

di mana fungsi pembantu kelihatan

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

tetapi kami melepasi penunjuk &woo kepada struktur yang tidak dinamakan struct { ... }...

Jika kita melihat pada penghimpun program, kita melihat bahawa nilai &woo sebenarnya tidak ditakrifkan (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 penempatan semula:

$ 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 telah dimuatkan, kita melihat penunjuk ke peta yang betul (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]
...

Oleh itu, kami boleh membuat kesimpulan bahawa pada masa melancarkan program pemuat kami, pautan ke &woo telah digantikan dengan sesuatu dengan perpustakaan libbpf. Mula-mula kita akan melihat output strace:

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=8, max_entries=8, map_name="woo", ...}, 120) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="simple", ...}, 120) = 5

Kita nampak itu libbpf mencipta peta woo dan kemudian memuat turun program kami simple. Mari kita lihat dengan lebih dekat cara kami memuatkan program:

  • panggilan xdp_simple_bpf__open_and_load daripada fail xdp-simple.skel.h
  • yang menyebabkan xdp_simple_bpf__load daripada fail xdp-simple.skel.h
  • yang menyebabkan bpf_object__load_skeleton daripada fail libbpf/src/libbpf.c
  • yang menyebabkan bpf_object__load_xattr daripada libbpf/src/libbpf.c

Fungsi terakhir, antara lain, akan memanggil bpf_object__create_maps, yang mencipta atau membuka peta sedia ada, mengubahnya menjadi deskriptor fail. (Di sinilah kita lihat BPF_MAP_CREATE dalam keluaran strace.) Seterusnya fungsi dipanggil bpf_object__relocate dan dialah yang menarik minat kita, kerana kita ingat apa yang kita lihat woo dalam jadual penempatan semula. Menerokanya, akhirnya kami mendapati diri kami berada dalam fungsi tersebut bpf_program__relocate, yang berurusan dengan penempatan semula peta:

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

Jadi kami mengambil arahan kami

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

dan gantikan daftar sumber di dalamnya dengan BPF_PSEUDO_MAP_FD, dan IMM pertama kepada deskriptor fail peta kami dan, jika ia sama dengan, sebagai contoh, 0xdeadbeef, maka sebagai hasilnya kami akan menerima arahan

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

Beginilah cara maklumat peta dipindahkan ke program BPF yang dimuatkan khusus. Dalam kes ini, peta boleh dibuat menggunakan BPF_MAP_CREATE, dan dibuka dengan ID menggunakan BPF_MAP_GET_FD_BY_ID.

Jumlah, apabila menggunakan libbpf algoritma adalah seperti berikut:

  • semasa penyusunan, rekod dibuat dalam jadual penempatan semula untuk pautan ke peta
  • libbpf membuka buku objek ELF, mencari semua peta yang digunakan dan mencipta deskriptor fail untuknya
  • deskriptor fail dimuatkan ke dalam kernel sebagai sebahagian daripada arahan LD64

Seperti yang anda boleh bayangkan, masih banyak lagi yang akan datang dan kita perlu melihat ke dalam terasnya. Nasib baik, kami mempunyai petunjuk - kami telah menulis maknanya BPF_PSEUDO_MAP_FD ke dalam daftar sumber dan kita boleh menguburkannya, yang akan membawa kita ke tempat suci semua orang kudus - kernel/bpf/verifier.c, di mana fungsi dengan nama tersendiri menggantikan deskriptor fail dengan alamat struktur jenis 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;

(kod penuh boleh didapati ΠΏΠΎ ссылкС). Jadi kami boleh mengembangkan algoritma kami:

  • semasa memuatkan atur cara, pengesah menyemak penggunaan peta yang betul dan menulis alamat struktur yang sepadan struct bpf_map

Apabila memuat turun binari ELF menggunakan libbpf Terdapat banyak lagi perkara yang berlaku, tetapi kita akan membincangkannya dalam artikel lain.

Memuatkan program dan peta tanpa libbpf

Seperti yang dijanjikan, berikut adalah contoh untuk pembaca yang ingin mengetahui cara membuat dan memuatkan program yang menggunakan peta, tanpa bantuan libbpf. Ini boleh berguna apabila anda bekerja dalam persekitaran yang anda tidak boleh membina kebergantungan, atau menyimpan setiap bit, atau menulis program seperti ply, yang menjana kod binari BPF dengan pantas.

Untuk menjadikannya lebih mudah untuk mengikuti logik, kami akan menulis semula contoh kami untuk tujuan ini xdp-simple. Kod program yang lengkap dan sedikit dikembangkan yang dibincangkan dalam contoh ini boleh didapati dalam ini inti.

Logik aplikasi kami adalah seperti berikut:

  • buat peta jenis BPF_MAP_TYPE_ARRAY menggunakan arahan BPF_MAP_CREATE,
  • buat program yang menggunakan peta ini,
  • sambungkan program ke antara muka lo,

yang diterjemahkan kepada 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);
}

ia adalah map_create mencipta peta dengan cara yang sama seperti yang kita lakukan dalam contoh pertama tentang panggilan sistem bpf - β€œkernel, sila buat saya peta baharu dalam bentuk tatasusunan 8 elemen seperti __u64 dan berikan saya kembali deskriptor fail":

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 untuk dimuatkan:

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

Bahagian yang sukar prog_load ialah takrifan program BPF kami sebagai pelbagai struktur struct bpf_insn insns[]. Tetapi kerana kami menggunakan program yang kami ada dalam C, kami boleh menipu sedikit:

$ 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 keseluruhan, kita perlu menulis 14 arahan dalam bentuk struktur seperti struct bpf_insn (nasihat: ambil tempat pembuangan dari atas, baca semula bahagian arahan, buka linux/bpf.h ΠΈ linux/bpf_common.h dan cuba tentukan struct bpf_insn insns[] atas diri 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 untuk mereka yang tidak menulis ini sendiri - cari map_fd.

Terdapat satu lagi bahagian yang tidak didedahkan dalam program kami - xdp_attach. Malangnya, program seperti XDP tidak boleh disambungkan menggunakan panggilan sistem bpf. Orang yang mencipta BPF dan XDP adalah daripada komuniti Linux dalam talian, yang bermaksud mereka menggunakan yang paling biasa bagi mereka (tetapi bukan untuk biasa orang) antara muka untuk berinteraksi dengan kernel: soket netlink, lihat juga RFC3549. Cara paling mudah untuk dilaksanakan xdp_attach sedang menyalin kod daripada libbpf, iaitu, daripada fail netlink.c, itulah yang kami lakukan, memendekkan sedikit:

Selamat datang ke 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;
}

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

Akhir sekali, berikut ialah fungsi kami yang membuka soket dan menghantar mesej khas kepadanya yang mengandungi deskriptor fail:

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 bersedia untuk ujian:

$ 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 lihat sama ada program kami telah disambungkan ke 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

Mari hantar 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. Perhatikan, dengan cara ini, peta kami sekali lagi dipaparkan dalam bentuk bait. Ini disebabkan oleh fakta bahawa, tidak seperti libbpf kami tidak memuatkan maklumat jenis (BTF). Tetapi kita akan bercakap lebih lanjut mengenai perkara ini lain kali.

Alat Pembangunan

Dalam bahagian ini, kita akan melihat kit alat pembangun BPF minimum.

Secara umumnya, anda tidak memerlukan sesuatu yang istimewa untuk membangunkan program BPF - BPF berjalan pada mana-mana kernel pengedaran yang baik, dan program dibina menggunakan clang, yang boleh dibekalkan daripada pakej. Walau bagaimanapun, disebabkan fakta bahawa BPF sedang dalam pembangunan, kernel dan alat sentiasa berubah, jika anda tidak mahu menulis program BPF menggunakan kaedah lama dari 2019, maka anda perlu menyusun

  • llvm/clang
  • pahole
  • terasnya
  • bpftool

(Untuk rujukan, bahagian ini dan semua contoh dalam artikel dijalankan pada Debian 10.)

llvm/clang

BPF mesra dengan LLVM dan, walaupun baru-baru ini program untuk BPF boleh disusun menggunakan gcc, semua pembangunan semasa dijalankan untuk LLVM. Oleh itu, pertama sekali, kami akan membina versi semasa 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 boleh menyemak sama ada semuanya disatukan dengan betul:

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

(Arahan perhimpunan clang diambil oleh saya daripada bpf_devel_QA.)

Kami tidak akan memasang program yang baru kami bina, sebaliknya hanya menambahnya PATH, sebagai contoh:

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

(Ini boleh ditambah kepada .bashrc atau ke fail yang berasingan. Secara peribadi, saya menambah perkara seperti ini ~/bin/activate-llvm.sh dan bila perlu saya buat . activate-llvm.sh.)

Pahole dan BTF

Utiliti pahole digunakan semasa membina kernel untuk mencipta maklumat penyahpepijatan dalam format BTF. Kami tidak akan menerangkan secara terperinci dalam artikel ini tentang butiran teknologi BTF, selain fakta bahawa ia mudah dan kami mahu menggunakannya. Jadi jika anda akan membina kernel anda, bina dahulu pahole (tanpa pahole anda tidak akan dapat membina kernel dengan pilihan 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

Inti untuk bereksperimen dengan BPF

Apabila meneroka kemungkinan BPF, saya ingin memasang teras saya sendiri. Ini, secara amnya, tidak perlu, kerana anda akan dapat menyusun dan memuatkan program BPF pada kernel pengedaran, namun, mempunyai kernel anda sendiri membolehkan anda menggunakan ciri BPF terkini, yang akan muncul dalam pengedaran anda dalam beberapa bulan paling baik. , atau, seperti dalam kes beberapa alat penyahpepijatan tidak akan dibungkus sama sekali pada masa hadapan yang boleh dijangka. Selain itu, terasnya sendiri membuatkan anda berasa penting untuk mencuba kod tersebut.

Untuk membina kernel yang anda perlukan, pertama, kernel itu sendiri, dan kedua, fail konfigurasi kernel. Untuk bereksperimen dengan BPF kita boleh menggunakan yang biasa vanila kernel atau salah satu kernel pembangunan. Dari segi sejarah, pembangunan BPF berlaku dalam komuniti rangkaian Linux dan oleh itu semua perubahan lambat laun melalui David Miller, penyelenggara rangkaian Linux. Bergantung pada sifatnya - suntingan atau ciri baharu - perubahan rangkaian jatuh ke dalam salah satu daripada dua teras - net atau net-next. Perubahan untuk BPF diagihkan dengan cara yang sama antara bpf ΠΈ bpf-next, yang kemudiannya dikumpulkan ke dalam net dan net-next, masing-masing. Untuk butiran lanjut, lihat bpf_devel_QA ΠΈ netdev-FAQ. Jadi pilih kernel berdasarkan citarasa anda dan keperluan kestabilan sistem yang anda uji (*-next kernel adalah yang paling tidak stabil daripada yang disenaraikan).

Ia adalah di luar skop artikel ini untuk bercakap tentang cara mengurus fail konfigurasi kernel - diandaikan bahawa anda sama ada sudah tahu cara melakukannya, atau bersedia untuk belajar atas diri sendiri. Walau bagaimanapun, arahan berikut sepatutnya lebih kurang cukup untuk memberi anda sistem yang didayakan BPF yang berfungsi.

Muat turun salah satu kernel di atas:

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

Bina konfigurasi kernel yang berfungsi minimum:

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

Dayakan pilihan BPF dalam fail .config pilihan anda sendiri (kemungkinan besar CONFIG_BPF sudah akan didayakan sejak systemd menggunakannya). Berikut ialah senarai pilihan daripada 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 boleh memasang dan memasang modul dan kernel dengan mudah (dengan cara ini, anda boleh memasang kernel menggunakan yang baru dipasang clangdengan menambah CC=clang):

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

dan but semula dengan kernel baru (saya gunakan untuk ini kexec daripada pakej 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

Utiliti yang paling biasa digunakan dalam artikel ialah utiliti bpftool, dibekalkan sebagai sebahagian daripada kernel Linux. Ia ditulis dan diselenggara oleh pembangun BPF untuk pembangun BPF dan boleh digunakan untuk mengurus semua jenis objek BPF - memuatkan program, mencipta dan mengedit peta, meneroka kehidupan ekosistem BPF, dsb. Dokumentasi dalam bentuk kod sumber untuk halaman manual boleh didapati dalam inti atau, sudah disusun, dalam rangkaian.

Pada masa penulisan ini bpftool datang sedia hanya untuk RHEL, Fedora dan Ubuntu (lihat, sebagai contoh, benang ini, yang menceritakan kisah pembungkusan yang belum selesai bpftool dalam Debian). Tetapi jika anda telah membina kernel anda, kemudian bina bpftool semudah pai:

$ 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 ialah direktori kernel anda.) Selepas melaksanakan arahan ini bpftool akan dikumpulkan dalam direktori ${linux}/tools/bpf/bpftool dan ia boleh ditambah pada laluan (pertama sekali kepada pengguna root) atau salin sahaja ke /usr/local/sbin.

Kumpul bpftool lebih baik menggunakan yang terakhir clang, dipasang seperti yang diterangkan di atas, dan semak sama ada ia dipasang dengan betul - menggunakan, sebagai contoh, arahan

$ 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 ciri BPF yang didayakan dalam kernel anda.

Dengan cara ini, arahan sebelumnya boleh dijalankan sebagai

# bpftool f p k

Ini dilakukan dengan analogi dengan utiliti dari pakej iproute2, di mana kita boleh, sebagai contoh, berkata ip a s eth0 bukannya ip addr show dev eth0.

Kesimpulan

BPF membolehkan anda menyarung kutu untuk mengukur dan menukar fungsi teras dengan berkesan. Sistem ini ternyata sangat berjaya, dalam tradisi terbaik UNIX: mekanisme mudah yang membolehkan anda (memprogram semula) kernel membenarkan sejumlah besar orang dan organisasi untuk bereksperimen. Dan, walaupun percubaan, serta pembangunan infrastruktur BPF itu sendiri, masih belum selesai, sistem itu sudah mempunyai ABI yang stabil yang membolehkan anda membina logik perniagaan yang boleh dipercayai, dan yang paling penting, berkesan.

Saya ingin ambil perhatian bahawa, pada pendapat saya, teknologi telah menjadi begitu popular kerana, dalam satu tangan, ia boleh untuk bermain (seni bina mesin boleh difahami lebih kurang dalam satu petang), dan sebaliknya, untuk menyelesaikan masalah yang tidak dapat diselesaikan (dengan cantik) sebelum penampilannya. Kedua-dua komponen ini bersama-sama memaksa orang ramai untuk mencuba dan bermimpi, yang membawa kepada kemunculan penyelesaian yang lebih dan lebih inovatif.

Artikel ini, walaupun tidak terlalu pendek, hanyalah pengenalan kepada dunia BPF dan tidak menerangkan ciri "maju" dan bahagian penting seni bina. Pelan ke hadapan adalah seperti ini: artikel seterusnya akan menjadi gambaran keseluruhan jenis program BPF (terdapat 5.8 jenis program yang disokong dalam kernel 30), kemudian kami akhirnya akan melihat cara menulis aplikasi BPF sebenar menggunakan program pengesanan kernel sebagai contoh, maka sudah tiba masanya untuk kursus yang lebih mendalam tentang seni bina BPF, diikuti dengan contoh rangkaian BPF dan aplikasi keselamatan.

Artikel terdahulu dalam siri ini

  1. BPF untuk si kecil, bahagian sifar: BPF klasik

Pautan

  1. Panduan Rujukan BPF dan XDP β€” dokumentasi mengenai BPF daripada cilium, atau lebih tepat daripada Daniel Borkman, salah seorang pencipta dan penyelenggara BPF. Ini adalah salah satu huraian serius pertama, yang berbeza daripada yang lain kerana Daniel tahu dengan tepat apa yang dia tulis dan tidak ada kesilapan di sana. Khususnya, dokumen ini menerangkan cara bekerja dengan program BPF jenis XDP dan TC menggunakan utiliti yang terkenal ip daripada pakej iproute2.

  2. Dokumentasi/rangkaian/filter.txt β€” fail asal dengan dokumentasi untuk BPF klasik dan kemudian dilanjutkan. Bacaan yang baik jika anda ingin mendalami bahasa perhimpunan dan butiran seni bina teknikal.

  3. Blog tentang BPF dari facebook. Ia jarang dikemas kini, tetapi sesuai, seperti yang ditulis oleh Alexei Starovoitov (pengarang eBPF) dan Andrii Nakryiko - (penyelenggara) libbpf).

  4. Rahsia bpftool. Benang twitter yang menghiburkan daripada Quentin Monnet dengan contoh dan rahsia penggunaan bpftool.

  5. Selami BPF: senarai bahan bacaan. Senarai gergasi (dan masih dikekalkan) pautan ke dokumentasi BPF daripada Quentin Monnet.

Sumber: www.habr.com

Tambah komen