Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Başlanğıcda texnologiya var idi və onun adı BPF idi. Biz ona baxdıq əvvəlki, Bu seriyadakı Əhdi-Ətiq məqaləsi. 2013-cü ildə Aleksey Starovoitov və Daniel Borkmanın səyləri ilə onun müasir 64 bitlik maşınlar üçün optimallaşdırılmış təkmilləşdirilmiş versiyası hazırlanmış və Linux nüvəsinə daxil edilmişdir. Bu yeni texnologiya qısaca Daxili BPF adlanırdı, sonra Genişləndirilmiş BPF adlandırıldı və indi, bir neçə il sonra hamı onu sadəcə olaraq BPF adlandırır.

Təxminən desək, BPF sizə Linux nüvə məkanında ixtiyari istifadəçi tərəfindən təmin edilən kodu işlətməyə imkan verir və yeni arxitektura o qədər uğurlu oldu ki, onun bütün tətbiqlərini təsvir etmək üçün bizə daha onlarla məqalə lazım olacaq. (Aşağıdakı performans kodunda gördüyünüz kimi, tərtibatçıların yaxşı etmədiyi yeganə şey, layiqli bir loqo yaratmaq idi.)

Bu məqalədə BPF virtual maşınının strukturu, BPF ilə işləmək üçün nüvə interfeysləri, inkişaf alətləri, həmçinin mövcud imkanların qısa, çox qısa icmalı, yəni. BPF-nin praktik tətbiqlərini daha dərindən öyrənmək üçün gələcəkdə ehtiyac duyacağımız hər şey.
Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Məqalənin xülasəsi

BPF arxitekturasına giriş. Birincisi, biz BPF arxitekturasına quş baxışı ilə baxacağıq və əsas komponentləri təsvir edəcəyik.

BPF virtual maşınının registrləri və komanda sistemi. Artıq bütövlükdə arxitektura haqqında təsəvvürümüzə malik olmaqla, biz BPF virtual maşınının strukturunu təsvir edəcəyik.

BPF obyektlərinin həyat dövrü, bpffs fayl sistemi. Bu bölmədə biz BPF obyektlərinin - proqramların və xəritələrin həyat dövrünə daha yaxından nəzər salacağıq.

Bpf sistem çağırışından istifadə edərək obyektlərin idarə edilməsi. Artıq mövcud olan sistem haqqında bir qədər anlayışla, nəhayət, xüsusi sistem çağırışından istifadə edərək istifadəçi məkanından obyektlərin necə yaradılması və manipulyasiya ediləcəyinə baxacağıq - bpf(2).

Пишем программы BPF с помощью libbpf. Əlbəttə ki, sistem çağırışından istifadə edərək proqramlar yaza bilərsiniz. Amma çətindir. Daha real ssenari üçün nüvə proqramçıları kitabxana hazırladılar libbpf. Biz sonrakı nümunələrdə istifadə edəcəyimiz əsas BPF tətbiqi skeletini yaradacağıq.

Kernel Köməkçiləri. Burada biz BPF proqramlarının kernel köməkçi funksiyalarına necə daxil ola biləcəyini öyrənəcəyik - bu alət xəritələrlə birlikdə klassik proqramla müqayisədə yeni BPF-nin imkanlarını əsaslı şəkildə genişləndirir.

BPF proqramlarından xəritələrə giriş. Bu nöqtəyə qədər xəritələrdən istifadə edən proqramları necə yarada biləcəyimizi dəqiq başa düşmək üçün kifayət qədər məlumat əldə edəcəyik. Və hətta böyük və qüdrətli yoxlayıcıya tez nəzər salaq.

İnkişaf vasitələri. Təcrübələr üçün tələb olunan utilitləri və nüvəni necə yığmaq barədə kömək bölməsi.

Nəticə. Məqalənin sonunda bura qədər oxuyanlar növbəti yazılarda həvəsləndirici sözlər və baş verəcəklərin qısa təsvirini tapacaqlar. Davamını gözləmək arzusu və ya imkanı olmayanlar üçün öz-özünə təhsil almaq üçün bir sıra bağlantıları da sadalayacağıq.

BPF Arxitekturasına giriş

BPF arxitekturasını nəzərdən keçirməyə başlamazdan əvvəl sonuncu dəfə (oh) müraciət edəcəyik klassik BPFRISC maşınlarının meydana gəlməsinə cavab olaraq hazırlanmış və effektiv paket filtrləmə problemini həll etmişdir. Arxitektura o qədər uğurlu oldu ki, XNUMX-cı illərdə Berkeley UNIX-də doğulduqdan sonra o, mövcud əməliyyat sistemlərinin əksəriyyətinə köçürüldü, çılğın iyirminci illərə qədər sağ qaldı və hələ də yeni tətbiqlər tapmaqdadır.

Yeni BPF 64 bitlik maşınların, bulud xidmətlərinin və SDN yaratmaq üçün alətlərə artan ehtiyacın hər yerdə olmasına cavab olaraq hazırlanmışdır (Stez-tezddəqiqləşdirilib nişləmə). Klassik BPF-nin təkmilləşdirilmiş əvəzedicisi kimi nüvə şəbəkəsi mühəndisləri tərəfindən hazırlanmış yeni BPF, sözün əsl mənasında, altı aydan sonra Linux sistemlərini izləmək kimi çətin tapşırıqda tətbiqlər tapdı və indi, ortaya çıxdıqdan altı il sonra, bizə sadəcə olaraq bütün növbəti məqaləyə ehtiyacımız olacaq. müxtəlif proqram növlərini sadalayın.

Gülməli şəkillər

Özündə, BPF təhlükəsizliyə xələl gətirmədən nüvə məkanında “ixtiyari” kodu işlətməyə imkan verən bir sandbox virtual maşınıdır. BPF proqramları istifadəçi məkanında yaradılır, nüvəyə yüklənir və bəzi hadisə mənbəyinə qoşulur. Hadisə, məsələn, bir paketin şəbəkə interfeysinə çatdırılması, bəzi nüvə funksiyasının işə salınması və s. ola bilər. Paket halında, BPF proqramı paketin məlumatlarına və metaməlumatlarına (proqramın növündən asılı olaraq oxumaq və bəlkə də yazmaq üçün) çıxış əldə edəcək; nüvə funksiyasını işə saldıqda, arqumentlər funksiya, o cümlədən nüvə yaddaşına göstəricilər və s.

Gəlin bu prosesə daha yaxından nəzər salaq. Başlamaq üçün, proqramları assemblerdə yazılmış klassik BPF-dən ilk fərq haqqında danışaq. Yeni versiyada proqramların yüksək səviyyəli dillərdə, ilk növbədə, əlbəttə ki, C dilində yazılması üçün arxitektura genişləndirilmişdir. Bunun üçün BPF arxitekturası üçün bayt kodu yaratmağa imkan verən llvm üçün backend hazırlanmışdır.

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

BPF arxitekturası qismən müasir maşınlarda səmərəli işləmək üçün nəzərdə tutulmuşdur. Bunun praktikada işləməsi üçün nüvəyə yükləndikdən sonra BPF bayt kodu JIT kompilyatoru adlanan komponentdən istifadə edərək yerli koda çevrilir (Just In Time). Sonra, xatırlayırsınızsa, klassik BPF-də proqram nüvəyə yükləndi və hadisə mənbəyinə atomik şəkildə - vahid sistem çağırışı kontekstində əlavə edildi. Yeni arxitekturada bu, iki mərhələdə baş verir - birincisi, kod sistem çağırışından istifadə edərək nüvəyə yüklənir. bpf(2)sonra isə proqramın növündən asılı olaraq dəyişən digər mexanizmlər vasitəsilə proqram hadisə mənbəyinə qoşulur.

Burada oxucunun sualı ola bilər: mümkün idimi? Belə kodun icra təhlükəsizliyinə necə təminat verilir? İcra təhlükəsizliyi bizə verifier adlı BPF proqramlarının yüklənməsi mərhələsi ilə təmin edilir (ingilis dilində bu mərhələ verifier adlanır və mən ingilis sözündən istifadə etməyə davam edəcəyəm):

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Verifier proqramın nüvənin normal işini pozmamasını təmin edən statik analizatordur. Bu, yeri gəlmişkən, proqramın sistemin işinə mane ola bilməyəcəyi anlamına gəlmir - BPF proqramları, növündən asılı olaraq, nüvə yaddaşının bölmələrini oxuya və yenidən yaza, funksiyaların dəyərlərini qaytara, kəsə, əlavə edə, yenidən yaza bilər. və hətta şəbəkə paketlərini yönləndirir. Verifier zəmanət verir ki, BPF proqramının işlədilməsi kernelin qəzaya uğramayacağına və qaydalara uyğun olaraq yazma imkanı olan proqramın, məsələn, gedən paketin məlumatlarının paketdən kənar nüvə yaddaşının üzərinə yaza bilməyəcək. BPF-nin bütün digər komponentləri ilə tanış olduqdan sonra, müvafiq bölmədə doğrulayıcıya bir az daha ətraflı baxacağıq.

Beləliklə, indiyə qədər nə öyrəndik? İstifadəçi C dilində proqram yazır, sistem çağırışından istifadə edərək onu nüvəyə yükləyir bpf(2), burada o, yoxlayıcı tərəfindən yoxlanılır və yerli bayt koduna çevrilir. Sonra eyni və ya digər istifadəçi proqramı hadisə mənbəyinə qoşur və o icra etməyə başlayır. Yükləmə və əlaqəni ayırmaq bir neçə səbəbə görə lazımdır. Birincisi, doğrulayıcı işlətmək nisbətən bahalıdır və eyni proqramı bir neçə dəfə yükləməklə biz kompüter vaxtını itirmiş oluruq. İkincisi, proqramın tam olaraq necə bağlanması onun növündən asılıdır və bir il əvvəl hazırlanmış bir "universal" interfeys yeni proqram növləri üçün uyğun olmaya bilər. (Baxmayaraq ki, indi memarlıq daha yetkinləşir, bu interfeysi səviyyədə birləşdirmək ideyası var. libbpf.)

Diqqətli oxucu görə bilər ki, şəkillərlə hələ bitməmişik. Həqiqətən, yuxarıda göstərilənlərin hamısı BPF-nin klassik BPF ilə müqayisədə mənzərəni niyə əsaslı şəkildə dəyişdirdiyini izah etmir. Tətbiq sahəsini əhəmiyyətli dərəcədə genişləndirən iki yenilik, paylaşılan yaddaş və nüvə köməkçi funksiyalarından istifadə etmək imkanıdır. BPF-də paylaşılan yaddaş sözdə xəritələrdən - müəyyən bir API ilə paylaşılan məlumat strukturlarından istifadə etməklə həyata keçirilir. Yəqin ki, onlar bu adı alıblar, çünki ilk görünən xəritə növü hash cədvəli olub. Sonra massivlər meydana çıxdı, yerli (her CPU) hash cədvəlləri və yerli massivlər, axtarış ağacları, BPF proqramlarına göstəriciləri ehtiva edən xəritələr və daha çox. İndi bizim üçün maraqlı olan odur ki, BPF proqramları indi zənglər arasında vəziyyəti saxlamaq və onu digər proqramlarla və istifadəçi sahəsi ilə bölüşmək imkanına malikdir.

Xəritələrə sistem zəngi vasitəsilə istifadəçi proseslərindən daxil olur bpf(2), və köməkçi funksiyalardan istifadə edərək nüvədə işləyən BPF proqramlarından. Üstəlik, köməkçilər yalnız xəritələrlə işləmək üçün deyil, həm də digər nüvə imkanlarına daxil olmaq üçün mövcuddur. Məsələn, BPF proqramları paketləri digər interfeyslərə yönləndirmək, mükəmməl hadisələr yaratmaq, nüvə strukturlarına daxil olmaq və s. üçün köməkçi funksiyalardan istifadə edə bilər.

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Xülasə, BPF ixtiyari, yəni yoxlayıcı tərəfindən sınaqdan keçirilmiş istifadəçi kodunu nüvə sahəsinə yükləmək imkanı verir. Bu kod zənglər arasında vəziyyəti saxlaya və istifadəçi sahəsi ilə məlumat mübadiləsi edə bilər, həmçinin bu tip proqramlar tərəfindən icazə verilən kernel alt sistemlərinə çıxışı var.

Bu, artıq kernel modullarının təmin etdiyi imkanlara bənzəyir, bununla müqayisədə BPF bəzi üstünlüklərə malikdir (əlbəttə ki, siz yalnız oxşar tətbiqləri müqayisə edə bilərsiniz, məsələn, sistem izləmə - BPF ilə ixtiyari bir sürücü yaza bilməzsiniz). Daha aşağı giriş həddini qeyd edə bilərsiniz (BPF-dən istifadə edən bəzi utilitlər istifadəçidən nüvə proqramlaşdırma bacarıqlarına və ya ümumiyyətlə proqramlaşdırma bacarıqlarına malik olmasını tələb etmir), iş vaxtının təhlükəsizliyini (yazarkən sistemi pozmayanlar üçün şərhlərdə əlinizi qaldırın) və ya sınaq modulları), atomiklik - modulları yenidən yükləyərkən fasilələr olur və BPF alt sistemi heç bir hadisənin qaçırılmamasını təmin edir (ədalətli olmaq üçün bu, bütün növ BPF proqramları üçün doğru deyil).

Bu cür imkanların olması BPF-ni nüvəni genişləndirmək üçün universal bir vasitə halına gətirir ki, bu da praktikada təsdiqlənir: BPF-yə getdikcə daha çox yeni proqram növləri əlavə olunur, getdikcə daha çox böyük şirkətlər BPF-ni 24×7 döyüş serverlərində istifadə edir, getdikcə daha çox. startaplar öz bizneslərini BPF-ə əsaslanan həllər üzərində qururlar. BPF hər yerdə istifadə olunur: DDoS hücumlarından qorunmaqda, SDN yaratmaqda (məsələn, kubernetlər üçün şəbəkələrin həyata keçirilməsində), əsas sistem izləmə aləti və statistika kollektoru kimi, müdaxilənin aşkarlanması sistemlərində və sandbox sistemlərində və s.

Məqalənin icmal hissəsini burada bitirək və virtual maşın və BPF ekosisteminə daha ətraflı baxaq.

Diqressiya: kommunal xidmətlər

Aşağıdakı bölmələrdəki nümunələri işlədə bilmək üçün sizə ən azı bir sıra kommunal proqramlar lazım ola bilər. llvm/clang bpf dəstəyi ilə və bpftool. Bölmədə İnkişaf Alətləri Utilitləri, eləcə də nüvənizi yığmaq üçün təlimatları oxuya bilərsiniz. Təqdimatımızın harmoniyasını pozmamaq üçün bu bölmə aşağıda yerləşdirilmişdir.

BPF Virtual Maşın Registrləri və Təlimat Sistemi

BPF-nin arxitekturası və əmr sistemi proqramların C dilində yazılacağını və nüvəyə yükləndikdən sonra yerli koda çevriləcəyini nəzərə alaraq hazırlanmışdır. Buna görə də registrlərin sayı və əmrlər dəsti müasir maşınların imkanlarının riyazi mənada kəsişməsinə nəzər salmaqla seçilmişdir. Bundan əlavə, proqramlara müxtəlif məhdudiyyətlər qoyuldu, məsələn, son vaxtlara qədər dövrələr və alt proqramlar yazmaq mümkün deyildi və təlimatların sayı 4096 ilə məhdudlaşdırıldı (indi imtiyazlı proqramlar bir milyona qədər təlimat yükləyə bilər).

BPF on bir istifadəçinin 64 bitlik registrinə malikdir r0-r10 və proqram sayğacı. Qeydiyyatdan keçin r10 çərçivə göstəricisini ehtiva edir və yalnız oxunur. Proqramların işləmə zamanı 512 baytlıq yığına və xəritələr şəklində qeyri-məhdud paylaşılan yaddaşa çıxışı var.

BPF proqramlarına proqram tipli kernel köməkçilərinin xüsusi dəstini və daha yaxınlarda müntəzəm funksiyaları işə salmağa icazə verilir. Hər çağırılan funksiya registrlərdə ötürülən beşə qədər arqument qəbul edə bilər r1-r5, və qaytarılan dəyər ötürülür r0. Funksiyadan qayıtdıqdan sonra registrlərin məzmununa zəmanət verilir r6-r9 Dəyişməyəcək.

Proqramın effektiv tərcüməsi üçün qeydiyyatdan keçir r0-r11 bütün dəstəklənən arxitekturalar üçün cari arxitekturanın ABI xüsusiyyətləri nəzərə alınmaqla real registrlərlə unikal şəkildə əlaqələndirilir. Məsələn, üçün x86_64 qeydiyyatdan keçir r1-r5, funksiya parametrlərini ötürmək üçün istifadə olunur, üzərində göstərilir rdi, rsi, rdx, rcx, r8, parametrləri funksiyalara ötürmək üçün istifadə olunur x86_64. Məsələn, soldakı kod sağdakı koda belə çevrilir:

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

Qeydiyyatdan keçin r0 proqramın icrasının nəticəsini qaytarmaq üçün də istifadə olunur və registrdə r1 proqram kontekst üçün göstərici ötürülür - proqramın növündən asılı olaraq bu, məsələn, struktur ola bilər. struct xdp_md (XDP üçün) və ya struktur struct __sk_buff (müxtəlif şəbəkə proqramları üçün) və ya struktur struct pt_regs (müxtəlif növ izləmə proqramları üçün) və s.

Beləliklə, bir sıra registrlər, nüvə köməkçiləri, yığın, kontekst göstəricisi və xəritələr şəklində paylaşılan yaddaşımız var idi. Bütün bunlar səfərdə mütləq lazım deyil, amma...

Təsviri davam etdirək və bu obyektlərlə işləmək üçün komanda sistemi haqqında danışaq. Hamısı (demək olar ki, hamısı) BPF təlimatları sabit 64 bitlik ölçüyə malikdir. 64 bitlik Big Endian maşınında bir təlimata baxsanız, görəcəksiniz

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Burada Code - bu təlimatın kodlaşdırılmasıdır, Dst/Src müvafiq olaraq qəbuledicinin və mənbənin kodlaşdırmalarıdır, Off - 16 bitlik imzalı abzas və Imm bəzi təlimatlarda istifadə olunan 32 bitlik işarəli tam ədəddir (cBPF sabiti K ilə oxşar). Kodlaşdırma Code iki növdən birinə malikdir:

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

0, 1, 2, 3 təlimat sinifləri yaddaşla işləmək üçün əmrləri müəyyənləşdirir. Onlar çağırılır, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, müvafiq olaraq. Sinif 4, 7 (BPF_ALU, BPF_ALU64) ALU təlimatları toplusunu təşkil edir. Sinif 5, 6 (BPF_JMP, BPF_JMP32) atlama təlimatlarını ehtiva edir.

BPF təlimat sistemini öyrənmək üçün sonrakı plan belədir: bütün təlimatları və onların parametrlərini diqqətlə sadalamaq əvəzinə, bu bölmədə bir neçə nümunəyə baxacağıq və onlardan təlimatların əslində necə işlədiyi və necə işlədiyi aydın olacaq. BPF üçün hər hansı ikili faylı əl ilə sökün. Məqalənin sonrakı hissəsində materialı birləşdirmək üçün Verifier, JIT kompilyatoru, klassik BPF-nin tərcüməsi, həmçinin xəritələri öyrənərkən, funksiyaları çağırarkən və s. haqqında bölmələrdə fərdi təlimatlarla da görüşəcəyik.

Fərdi təlimatlar haqqında danışarkən, əsas fayllara müraciət edəcəyik bpf.h и bpf_common.hBPF təlimatlarının ədədi kodlarını təyin edən . Arxitekturanı müstəqil öyrənərkən və/və ya ikili faylları təhlil edərkən, mürəkkəblik sırasına görə sıralanmış aşağıdakı mənbələrdə semantika tapa bilərsiniz: Qeyri-rəsmi eBPF spesifikasiyası, BPF və XDP İstinad Bələdçisi, Təlimatlar Dəsti, Documentation/şəbəkə/filter.txt və əlbəttə ki, Linux mənbə kodunda - verifier, JIT, BPF interpreter.

Misal: başınızdakı BPF-nin sökülməsi

Proqramı tərtib etdiyimiz bir nümunəyə baxaq readelf-example.c və nəticədə ikiliyə baxın. Orijinal məzmunu açıqlayacağıq readelf-example.c aşağıda, onun məntiqini ikili kodlardan bərpa etdikdən sonra:

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

Çıxışda ilk sütun readelf girintidir və proqramımız dörd əmrdən ibarətdir:

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

Komanda kodları bərabərdir b7, 15, b7 и 95. Xatırladaq ki, ən az əhəmiyyətli üç bit təlimat sinfidir. Bizim vəziyyətimizdə bütün təlimatların dördüncü biti boşdur, ona görə də təlimat sinifləri müvafiq olaraq 7, 5, 7, 5-dir. BPF_ALU64, və 5-dir BPF_JMP. Hər iki sinif üçün təlimat formatı eynidir (yuxarıya bax) və proqramımızı bu şəkildə yenidən yaza bilərik (eyni zamanda qalan sütunları insan şəklində yenidən yazacağıq):

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

Əməliyyat b sinif ALU64 - Mi BPF_MOV. O, təyinat reyestrinə dəyər təyin edir. Bit quraşdırılıbsa s (mənbə), onda dəyər mənbə registrindən götürülür və əgər bizim vəziyyətimizdə olduğu kimi təyin olunmayıbsa, onda dəyər sahədən götürülür. Imm. Beləliklə, birinci və üçüncü təlimatlarda əməliyyatı yerinə yetiririk r0 = Imm. Bundan əlavə, JMP sinif 1 əməliyyatıdır BPF_JEQ (əgər bərabərdirsə, tullanmaq). Bizim vəziyyətimizdə, bitdən bəri S sıfırdır, mənbə registrinin qiymətini sahə ilə müqayisə edir Imm. Dəyərlər üst-üstə düşürsə, keçid baş verir PC + OffHara PC, həmişə olduğu kimi, növbəti təlimatın ünvanını ehtiva edir. Nəhayət, JMP Class 9 Əməliyyatı BPF_EXIT. Bu təlimat nüvəyə qayıdaraq proqramı dayandırır r0. Gəlin cədvəlimizə yeni sütun əlavə edək:

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

Bunu daha rahat formada yenidən yaza bilərik:

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

Reyestrdə nə olduğunu xatırlasaq r1 proqram nüvədən və registrdən kontekstə göstərici ötürülür r0 dəyər kernelə qaytarılır, onda görə bilərik ki, əgər kontekstin göstəricisi sıfırdırsa, onda biz 1-i, əks halda isə - 2-ni qaytarırıq. Mənbəyə baxaraq haqlı olduğumuzu yoxlayaq:

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

Bəli, bu mənasız proqramdır, lakin o, sadəcə dörd sadə təlimata çevrilir.

İstisna nümunəsi: 16 baytlıq təlimat

Daha əvvəl qeyd etdik ki, bəzi təlimatlar 64 bitdən çox yer tutur. Bu, məsələn, təlimatlara aiddir lddw (Kod = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — sahələrdən qoşa sözü registrə yükləyin Imm. Fakt Imm ölçüsü 32, qoşa söz isə 64 bitdir, ona görə də bir 64 bitlik təlimatda 64 bitlik ani dəyərin registrə yüklənməsi işləməyəcək. Bunun üçün sahədə 64 bitlik dəyərin ikinci hissəsini saxlamaq üçün iki bitişik təlimat istifadə olunur Imm. Nümunə:

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

Binar proqramda yalnız iki təlimat var:

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

Biz göstərişlərlə yenidən görüşəcəyik lddw, köçürmələr və xəritələrlə işləmək haqqında danışarkən.

Misal: standart alətlərdən istifadə edərək BPF-nin sökülməsi

Beləliklə, biz BPF ikili kodları oxumağı öyrəndik və lazım gələrsə, istənilən təlimatı təhlil etməyə hazırıq. Bununla belə, praktikada standart alətlərdən istifadə edərək proqramları sökmək daha rahat və daha sürətli olduğunu söyləməyə dəyər, məsələn:

$ llvm-objdump -d x64.o

Disassembly of section .text:

0000000000000000 <foo>:
 0: 18 00 00 00 dd cc bb aa 00 00 00 00 44 33 22 11 r0 = 1234605617868164317 ll
 2: 95 00 00 00 00 00 00 00 exit

BPF obyektlərinin həyat dövrü, bpffs fayl sistemi

(Bu yarımbölmədə təsvir edilən bəzi təfərrüatları əvvəlcə buradan öyrəndim oruc tutmaq Aleksey Starovoitov BPF Blogu.)

BPF obyektləri - proqramlar və xəritələr - əmrlərdən istifadə edərək istifadəçi sahəsindən yaradılır BPF_PROG_LOAD и BPF_MAP_CREATE sistem çağırışı bpf(2), bunun tam olaraq necə baş verdiyi haqqında növbəti hissədə danışacağıq. Bu, nüvə məlumat strukturlarını və onların hər biri üçün yaradır refcount (referans sayı) birinə təyin edilir və obyektə işarə edən fayl deskriptoru istifadəçiyə qaytarılır. Tutacaq bağlandıqdan sonra refcount obyekt bir azaldılır, sıfıra çatdıqda isə obyekt məhv olur.

Proqram xəritələrdən istifadə edirsə, o zaman refcount bu xəritələr proqramı yüklədikdən sonra bir artır, yəni. onların fayl deskriptorları istifadəçi prosesindən bağlana bilər və hələ də refcount sıfır olmayacaq:

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Proqramı uğurla yüklədikdən sonra biz onu adətən bir növ hadisə generatoruna əlavə edirik. Məsələn, daxil olan paketləri emal etmək və ya bəzilərinə qoşmaq üçün onu şəbəkə interfeysinə qoya bilərik tracepoint əsasda. Bu zaman istinad sayğacı da bir artacaq və yükləyici proqramda fayl deskriptorunu bağlaya biləcəyik.

İndi yükləyicini bağlasaq nə olar? Bu, hadisə generatorunun (qarmaq) növündən asılıdır. Bütün şəbəkə qarmaqları yükləyici tamamlandıqdan sonra mövcud olacaq, bunlar qlobal qarmaqlar adlananlardır. Və, məsələn, izləmə proqramları onları yaradan proses başa çatdıqdan sonra buraxılacaq (və buna görə də “yerlidən prosesə” yerli adlanır). Texniki olaraq, yerli qarmaqlar həmişə istifadəçi məkanında müvafiq fayl deskriptoruna malikdir və buna görə də proses bağlandıqda bağlanır, lakin qlobal qarmaqlar yoxdur. Aşağıdakı şəkildə, qırmızı xaçlardan istifadə edərək, yükləyici proqramının dayandırılmasının yerli və qlobal qarmaqlar vəziyyətində obyektlərin ömrünə necə təsir etdiyini göstərməyə çalışıram.

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

Niyə yerli və qlobal qarmaqlar arasında fərq var? Bəzi növ şəbəkə proqramlarını işə salmaq istifadəçi sahəsi olmadan məna kəsb edir, məsələn, DDoS qorunmasını təsəvvür edin - yükləyici qaydaları yazır və BPF proqramını şəbəkə interfeysinə qoşur, bundan sonra yükləyici gedib özünü öldürə bilər. Digər tərəfdən, on dəqiqə ərzində dizlərinizə yazdığınız bir ayıklama izi proqramını təsəvvür edin - bu, başa çatdıqdan sonra sistemdə heç bir zibil qalmamasını istərdiniz və yerli qarmaqlar bunu təmin edəcəkdir.

Digər tərəfdən, nüvədəki izləmə nöqtəsinə qoşulmaq və uzun illər ərzində statistika toplamaq istədiyinizi təsəvvür edin. Bu halda, siz istifadəçi hissəsini tamamlamaq və vaxtaşırı statistikaya qayıtmaq istərdiniz. bpf fayl sistemi bu imkanı təmin edir. Bu, BPF obyektlərinə istinad edən faylların yaradılmasına və bununla da onların həcmini artırmağa imkan verən yalnız yaddaşda olan psevdofayl sistemidir. refcount obyektlər. Bundan sonra yükləyici çıxa bilər və onun yaratdığı obyektlər canlı qalacaq.

Kiçiklər üçün BPF, birinci hissə: uzadılmış BPF

BPF obyektlərinə istinad edən bpff-lərdə faylların yaradılması "pinning" adlanır (aşağıdakı ifadədə olduğu kimi: "proses BPF proqramını və ya xəritəsini bağlaya bilər"). BPF obyektləri üçün fayl obyektlərinin yaradılması təkcə yerli obyektlərin ömrünü uzatmaq üçün deyil, həm də qlobal obyektlərin istifadəyə yararlılığı baxımından məna kəsb edir - qlobal DDoS mühafizə proqramı ilə nümunəyə qayıdaraq, gəlib statistikaya baxmaq istəyirik. zaman-zaman.

BPF fayl sistemi adətən quraşdırılır /sys/fs/bpf, lakin o, yerli olaraq da quraşdırıla bilər, məsələn, bu kimi:

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

Fayl sistemi adları əmrdən istifadə etməklə yaradılır BPF_OBJ_PIN BPF sistem çağırışı. Nümunə etmək üçün bir proqramı götürək, onu tərtib edək, yükləyək və onu bağlayaq bpffs. Proqramımız faydalı heç nə etmir, biz sadəcə kodu təqdim edirik ki, nümunəni təkrarlayasınız:

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

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

Gəlin bu proqramı tərtib edək və fayl sisteminin lokal surətini yaradaq bpffs:

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

İndi isə yardımçı proqramdan istifadə edərək proqramımızı yükləyək bpftool və müşayiət olunan sistem zənglərinə baxın bpf(2) (bəzi aidiyyatı olmayan xətlər strace çıxışından çıxarılıb):

$ 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

Burada istifadə edərək proqramı yüklədik BPF_PROG_LOAD, nüvədən fayl deskriptoru aldı 3 və əmrdən istifadə etməklə BPF_OBJ_PIN bu fayl deskriptorunu fayl kimi bağladı "bpf-mountpoint/test". Bundan sonra bootloader proqramı bpftool işləməyi başa çatdırdı, lakin proqramı heç bir şəbəkə interfeysinə əlavə etməsək də, nüvədə qaldı:

$ 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

Fayl obyektini normal şəkildə silə bilərik unlink(2) və bundan sonra müvafiq proqram silinəcək:

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

Obyektlərin silinməsi

Obyektlərin silinməsindən danışarkən aydınlaşdırmaq lazımdır ki, proqramı çəngəldən (hadisə generatoru) ayırdıqdan sonra heç bir yeni hadisə onun işə salınmasına səbəb olmayacaq, lakin proqramın bütün cari nümunələri normal qaydada tamamlanacaq. .

BPF proqramlarının bəzi növləri proqramı tez bir zamanda əvəz etməyə imkan verir, yəni. ardıcıl atomluluğu təmin edir replace = detach old program, attach new program. Bu halda, proqramın köhnə versiyasının bütün aktiv nümunələri öz işini bitirəcək və yeni proqramdan yeni hadisə idarəçiləri yaradılacaq və burada "atomluq" heç bir hadisənin qaçırılmaması deməkdir.

Proqramların hadisə mənbələrinə əlavə edilməsi

Bu yazıda proqramların hadisə mənbələrinə qoşulmasını ayrıca təsvir etməyəcəyik, çünki bunu müəyyən bir proqram növü kontekstində araşdırmağın mənası var. Santimetr. misal aşağıda, XDP kimi proqramların necə bağlandığını göstəririk.

bpf Sistem Zəngindən istifadə edərək Obyektlərin Manipulyasiyası

BPF proqramları

Bütün BPF obyektləri sistem çağırışından istifadə edərək istifadəçi məkanından yaradılır və idarə olunur bpf, aşağıdakı prototipə malikdir:

#include <linux/bpf.h>

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

Budur komanda cmd tip dəyərlərindən biridir enum bpf_cmd, attr — müəyyən bir proqram üçün parametrlər üçün göstərici və size — göstəriciyə görə obyekt ölçüsü, yəni. adətən bu sizeof(*attr). kernel 5.8-də sistem çağırışı bpf 34 müxtəlif əmrləri dəstəkləyir və müəyyənləşdirilməsi union bpf_attr 200 sətir tutur. Ancaq bundan qorxmamalıyıq, çünki bir neçə məqalə ərzində əmrlər və parametrlərlə tanış olacağıq.

Komandadan başlayaq BPF_PROG_LOAD, BPF proqramlarını yaradan - BPF təlimatları toplusunu götürür və onu nüvəyə yükləyir. Yükləmə anında yoxlayıcı işə salınır, sonra JIT kompilyatoru və müvəffəqiyyətlə icra edildikdən sonra proqram faylı deskriptoru istifadəçiyə qaytarılır. Bundan sonra onun başına gələnləri əvvəlki hissədə gördük BPF obyektlərinin həyat dövrü haqqında.

İndi biz sadə bir BPF proqramını yükləyən xüsusi proqram yazacağıq, lakin əvvəlcə hansı proqramı yükləmək istədiyimizə qərar verməliyik - seçməliyik. Tipi və bu tip çərçivəsində yoxlayıcı testdən keçəcək proqram yazın. Ancaq prosesi çətinləşdirməmək üçün burada hazır bir həll var: kimi bir proqramı alacağıq BPF_PROG_TYPE_XDP, dəyəri qaytaracaq XDP_PASS (bütün paketləri atlayın). BPF assembler-də çox sadə görünür:

r0 = 2
exit

Qərar verdikdən sonra o yükləyəcəyik, bunu necə edəcəyimizi sizə deyə bilərik:

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

Proqramda maraqlı hadisələr massivin tərifindən başlayır insns - maşın kodunda BPF proqramımız. Bu halda, BPF proqramının hər bir təlimatı struktura yığılır bpf_insn. Birinci element insns göstərişlərə uyğundur r0 = 2, ikinci - exit.

Geri çəkilmək. Kernel maşın kodlarının yazılması və nüvə başlıq faylının istifadəsi üçün daha rahat makroları müəyyən edir tools/include/linux/filter.h yaza bildik

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

Lakin BPF proqramlarını yerli kodda yazmaq yalnız nüvədə testlər və BPF haqqında məqalələr yazmaq üçün lazım olduğundan, bu makroların olmaması tərtibatçının həyatını həqiqətən çətinləşdirmir.

BPF proqramını müəyyən etdikdən sonra onun nüvəyə yüklənməsinə keçirik. Bizim minimalist parametrlər dəstimiz attr proqram növü, təlimatların dəsti və sayı, tələb olunan lisenziya və ad daxildir "woo", yüklədikdən sonra proqramımızı sistemdə tapmaq üçün istifadə edirik. Proqram, söz verildiyi kimi, sistem çağırışı ilə sistemə yüklənir bpf.

Proqramın sonunda biz faydalı yükü simulyasiya edən sonsuz bir döngəyə düşürük. Onsuz, sistem çağırışının bizə qaytardığı fayl deskriptoru bağlandıqda proqram nüvə tərəfindən öldürüləcək. bpf, və biz bunu sistemdə görməyəcəyik.

Yaxşı, sınaq üçün hazırıq. Proqramı yığıb işə salaq stracehər şeyin lazım olduğu kimi işlədiyini yoxlamaq üçün:

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

Hər şey yaxşıdır, bpf(2) bizə qolu 3 qaytardı və sonsuz bir döngəyə girdik pause(). Proqramımızı sistemdə tapmağa çalışaq. Bunu etmək üçün başqa bir terminala gedəcəyik və köməkçi proqramdan istifadə edəcəyik 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)

Sistemdə yüklənmiş proqramın olduğunu görürük woo qlobal ID-si 390-dır və hazırda davam edir simple-prog proqrama işarə edən açıq fayl deskriptoru var (və əgər simple-prog sonra işi bitirəcək woo yox olacaq). Gözlənildiyi kimi, proqram woo BPF arxitekturasında ikili kodların 16 baytını - iki təlimatı alır, lakin yerli formada (x86_64) artıq 40 baytdır. Proqramımıza orijinal formada baxaq:

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

sürprizlər yoxdur. İndi JIT kompilyatoru tərəfindən yaradılan koda baxaq:

# 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

üçün çox təsirli deyil exit(2), amma insaf naminə desək, proqramımız çox sadədir və qeyri-trivial proqramlar üçün JIT kompilyatorunun əlavə etdiyi proloq və epiloq, əlbəttə ki, lazımdır.

Maps

BPF proqramları həm digər BPF proqramları, həm də istifadəçi məkanındakı proqramlar üçün əlçatan olan strukturlaşdırılmış yaddaş sahələrindən istifadə edə bilər. Bu obyektlər xəritələr adlanır və bu bölmədə sistem çağırışından istifadə edərək onları necə idarə edəcəyimizi göstərəcəyik bpf.

Dərhal deyək ki, xəritələrin imkanları yalnız ümumi yaddaşa daxil olmaq ilə məhdudlaşmır. Xüsusi təyinatlı xəritələr var, məsələn, BPF proqramlarına göstəricilər və ya şəbəkə interfeyslərinə göstəricilər, mükəmməl hadisələrlə işləmək üçün xəritələr və s. Oxucunu çaşdırmamaq üçün burada onlar haqqında danışmayacağıq. Bundan əlavə, biz sinxronizasiya məsələlərinə məhəl qoymuruq, çünki bu, nümunələrimiz üçün vacib deyil. Mövcud xəritə növlərinin tam siyahısını burada tapa bilərsiniz <linux/bpf.h>, və bu bölmədə tarixən birinci tip olan hash cədvəlini nümunə götürəcəyik BPF_MAP_TYPE_HASH.

C++-da hash cədvəli yaratsanız, deyəcəksiniz unordered_map<int,long> woo, rus dilində “Mənə masa lazımdır woo açarları tipli olan limitsiz ölçü int, və dəyərlər növüdür long" BPF hash cədvəlini yaratmaq üçün biz eyni şeyi etməliyik, istisna olmaqla, cədvəlin maksimum ölçüsünü təyin etməliyik və açarların və dəyərlərin növlərini təyin etmək əvəzinə, onların ölçülərini baytlarda göstərməliyik. . Xəritələr yaratmaq üçün əmrdən istifadə edin BPF_MAP_CREATE sistem çağırışı bpf. Xəritə yaradan az-çox minimal proqrama baxaq. BPF proqramlarını yükləyən əvvəlki proqramdan sonra bu sizə sadə görünməlidir:

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

Burada bir sıra parametrlər təyin edirik attr, biz deyirik ki, “Mənə açarlar və ölçü dəyərləri olan bir hash cədvəli lazımdır sizeof(int), mən maksimum dörd element qoya bilərəm." BPF xəritələrini yaradarkən, digər parametrləri təyin edə bilərsiniz, məsələn, proqramla nümunədə olduğu kimi, biz obyektin adını aşağıdakı kimi göstərdik. "woo".

Proqramı tərtib edib işə salaq:

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

Budur sistem çağırışı bpf(2) bizə təsviri xəritə nömrəsini qaytardı 3 və sonra proqram, gözlənildiyi kimi, sistem çağırışında əlavə təlimatları gözləyir pause(2).

İndi proqramımızı arxa plana göndərək və ya başqa bir terminal açaq və yardımçı proqramdan istifadə edərək obyektimizə baxaq bpftool (xəritəmizi adı ilə başqalarından fərqləndirə bilərik):

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

114 rəqəmi obyektimizin qlobal identifikatorudur. Sistemdəki istənilən proqram bu identifikatordan əmrdən istifadə edərək mövcud xəritəni aça bilər BPF_MAP_GET_FD_BY_ID sistem çağırışı bpf.

İndi hash cədvəlimizlə oynaya bilərik. Onun məzmununa nəzər salaq:

$ sudo bpftool map dump id 114
Found 0 elements

Boş. Gəlin buna dəyər verək hash[1] = 1:

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

Cədvələ yenidən baxaq:

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

Yaşasın! Bir element əlavə edə bildik. Qeyd edək ki, bunu etmək üçün bayt səviyyəsində işləməliyik, çünki bptftool hash cədvəlindəki dəyərlərin hansı tipdə olduğunu bilmir. (Bu bilik BTF istifadə edərək ona ötürülə bilər, lakin indi daha çox.)

Bpftool elementləri tam olaraq necə oxuyur və əlavə edir? Başlıq altına nəzər salaq:

$ 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

Əvvəlcə komandanı istifadə edərək xəritəni qlobal ID ilə açdıq BPF_MAP_GET_FD_BY_ID и bpf(2) bizə deskriptor 3-ü qaytardı.Daha sonra əmrdən istifadə etməklə BPF_MAP_GET_NEXT_KEY ötürərək cədvəldə birinci açarı tapdıq NULL "əvvəlki" açarın göstəricisi kimi. Əgər açarımız varsa, edə bilərik BPF_MAP_LOOKUP_ELEMgöstəriciyə dəyər qaytarır value. Növbəti addım, cari açara göstərici ötürməklə növbəti elementi tapmağa çalışacağıq, lakin cədvəlimizdə yalnız bir element və əmr var. BPF_MAP_GET_NEXT_KEY qayıdır ENOENT.

Yaxşı, gəlin dəyəri 1 açarı ilə dəyişək, tutaq ki, biznes məntiqimiz qeydiyyatdan keçməyi tələb edir 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

Gözlənildiyi kimi, çox sadədir: əmr BPF_MAP_GET_FD_BY_ID xəritəmizi ID və əmrlə açır BPF_MAP_UPDATE_ELEM elementin üzərinə yazır.

Beləliklə, bir proqramdan hash cədvəli yaratdıqdan sonra onun məzmununu digər proqramdan oxuya və yaza bilərik. Qeyd edək ki, əgər biz bunu komanda xəttindən edə bilsək, sistemdəki hər hansı digər proqram bunu edə bilər. İstifadəçi məkanından xəritələrlə işləmək üçün yuxarıda təsvir edilən əmrlərə əlavə olaraq, Aşağıdakı:

  • BPF_MAP_LOOKUP_ELEM: açarla dəyəri tapın
  • BPF_MAP_UPDATE_ELEM: yeniləyin/dəyər yaradın
  • BPF_MAP_DELETE_ELEM: açarı çıxarın
  • BPF_MAP_GET_NEXT_KEY: növbəti (və ya birinci) açarı tapın
  • BPF_MAP_GET_NEXT_ID: bütün mövcud xəritələrdən keçməyə imkan verir, bu belə işləyir bpftool map
  • BPF_MAP_GET_FD_BY_ID: qlobal ID ilə mövcud xəritəni açın
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: obyektin dəyərini atomik olaraq yeniləyin və köhnəsini qaytarın
  • BPF_MAP_FREEZE: xəritəni istifadəçi sahəsindən dəyişməz etmək (bu əməliyyat geri qaytarıla bilməz)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: kütləvi əməliyyatlar. Misal üçün, BPF_MAP_LOOKUP_AND_DELETE_BATCH - bu, xəritədən bütün dəyərləri oxumaq və sıfırlamaq üçün yeganə etibarlı yoldur

Bu əmrlərin hamısı bütün xəritə növləri üçün işləmir, lakin ümumiyyətlə istifadəçi məkanından digər xəritə növləri ilə işləmək hash cədvəlləri ilə işləmək kimi görünür.

Sifariş naminə hash cədvəli təcrübələrimizi bitirək. Yadda saxlayın ki, biz dörd açarı ehtiva edə bilən bir cədvəl yaratdıq? Daha bir neçə element əlavə edək:

$ 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

İndiyə qədər yaxşı:

$ 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

Daha birini əlavə etməyə çalışaq:

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

Gözlənildiyi kimi, uğur qazana bilmədik. Gəlin xətaya daha ətraflı baxaq:

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

Hər şey qaydasındadır: gözlənildiyi kimi, komanda BPF_MAP_UPDATE_ELEM yeni, beşinci, açar yaratmağa çalışır, lakin çökür E2BIG.

Beləliklə, biz BPF proqramlarını yarada və yükləyə, həmçinin istifadəçi məkanından xəritələr yarada və idarə edə bilərik. İndi BPF proqramlarının xəritələrindən necə istifadə edə biləcəyimizə baxmaq məntiqlidir. Biz bu barədə maşın makro kodlarında çətin oxunan proqramların dilində danışa bilərdik, lakin əslində BPF proqramlarının əslində necə yazıldığını və saxlandığını göstərməyin vaxtı gəldi - istifadə edərək. libbpf.

(Aşağı səviyyəli nümunənin olmamasından narazı olan oxucular üçün: xəritələrdən istifadə edən proqramları və istifadə edərək yaradılan köməkçi funksiyaları ətraflı təhlil edəcəyik. libbpf və təlimat səviyyəsində nə baş verdiyini sizə xəbər verin. Narazı olan oxucular üçün çox, əlavə etdik misal məqalənin müvafiq yerində.)

libbpf istifadə edərək BPF proqramlarının yazılması

Maşın kodlarından istifadə edərək BPF proqramlarının yazılması yalnız ilk dəfə maraqlı ola bilər və sonra toxluq başlayır. Bu anda diqqətinizi yönəltməlisiniz llvm, BPF arxitekturası üçün kod yaratmaq üçün arxa plana, eləcə də kitabxanaya malikdir libbpf, bu, BPF tətbiqlərinin istifadəçi tərəfini yazmağa və istifadə edərək yaradılan BPF proqramlarının kodunu yükləməyə imkan verir. llvm/clang.

Əslində, bu və sonrakı məqalələrdə görəcəyimiz kimi, libbpf onsuz kifayət qədər çox iş görür (və ya oxşar alətlər - iproute2, libbcc, libbpf-govə s.) yaşamaq mümkün deyil. Layihənin öldürücü xüsusiyyətlərindən biri libbpf BPF CO-RE (Bir dəfə tərtib et, hər yerdə işlə) - müxtəlif API-lərdə işləmək imkanı ilə bir nüvədən digərinə daşınan BPF proqramlarını yazmağa imkan verən layihədir (məsələn, kernel strukturu versiyadan dəyişdikdə). versiyaya). CO-RE ilə işləyə bilmək üçün nüvəniz BTF dəstəyi ilə tərtib edilməlidir (bunun necə ediləcəyini bölmədə təsvir edirik. İnkişaf Alətləri. Nüvənizin BTF ilə qurulduğunu və ya çox sadə olmadığını aşağıdakı faylın olması ilə yoxlaya bilərsiniz:

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

Bu fayl nüvədə istifadə olunan bütün məlumat növləri haqqında məlumatları saxlayır və istifadə etdiyimiz bütün nümunələrdə istifadə olunur libbpf. Növbəti məqalədə CO-RE haqqında ətraflı danışacağıq, lakin bu məqalədə özünüzə bir nüvə qurmaq kifayətdir. CONFIG_DEBUG_INFO_BTF.

kitabxana libbpf kataloqda yaşayır tools/lib/bpf kernel və onun inkişafı poçt siyahısı vasitəsilə həyata keçirilir [email protected]. Bununla belə, nüvədən kənarda yaşayan tətbiqlərin ehtiyacları üçün ayrıca bir depo saxlanılır https://github.com/libbpf/libbpf nüvə kitabxanasının daha çox və ya daha az olduğu kimi oxumaq üçün əks olunduğu.

Bu bölmədə istifadə edən bir layihəni necə yarada biləcəyinizi nəzərdən keçirəcəyik libbpf, gəlin bir neçə (az-çox mənasız) test proqramı yazaq və hamısının necə işlədiyini ətraflı təhlil edək. Bu, BPF proqramlarının xəritələr, nüvə köməkçiləri, BTF və s. ilə necə qarşılıqlı əlaqədə olduğunu aşağıdakı bölmələrdə daha asan izah etməyə imkan verəcək.

Adətən layihələrdən istifadə edir libbpf git alt modulu kimi GitHub repozitoriyası əlavə edin, biz də eyni şeyi edəcəyik:

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

Getmək libbpf çox sadədir:

$ 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

Bu bölmədə növbəti planımız belədir: kimi bir BPF proqramı yazacağıq BPF_PROG_TYPE_XDP, əvvəlki nümunədə olduğu kimi, lakin C dilində biz onu istifadə edərək tərtib edirik clang, və onu nüvəyə yükləyən köməkçi proqram yazın. Növbəti bölmələrdə biz həm BPF proqramının, həm də köməkçi proqramının imkanlarını genişləndirəcəyik.

Misal: libbpf istifadə edərək tam hüquqlu proqram yaratmaq

Başlamaq üçün fayldan istifadə edirik /sys/kernel/btf/vmlinuxyuxarıda qeyd olunan , və onun ekvivalentini başlıq faylı şəklində yaradın:

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

Bu fayl nüvəmizdə mövcud olan bütün məlumat strukturlarını saxlayacaq, məsələn, nüvədə IPv4 başlığı belə müəyyən edilir:

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

İndi BPF proqramımızı C dilində yazacağıq:

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

Proqramımızın çox sadə olduğu ortaya çıxsa da, hələ də bir çox detallara diqqət yetirməliyik. Birincisi, daxil etdiyimiz ilk başlıq faylıdır vmlinux.h, biz indicə istifadə edərək yaratdıq bpftool btf dump - indi nüvə strukturlarının necə göründüyünü öyrənmək üçün kernel-headers paketini quraşdırmağa ehtiyacımız yoxdur. Aşağıdakı başlıq faylı bizə kitabxanadan gəlir libbpf. İndi bizə yalnız makronu təyin etmək lazımdır SEC, bu simvolu ELF obyekt faylının müvafiq bölməsinə göndərir. Proqramımız bölmədə yer alır xdp/simple, burada kəsikdən əvvəl proqram növünü BPF təyin edirik - bu, istifadə olunan konvensiyadır libbpf, bölmə adına əsaslanaraq başlanğıcda düzgün növü əvəz edəcək bpf(2). BPF proqramının özüdür C - çox sadə və bir sətirdən ibarətdir return XDP_PASS. Nəhayət, ayrı bir bölmə "license" lisenziyanın adını ehtiva edir.

Biz proqramımızı llvm/clang, >= 10.0.0 və ya daha yaxşısı, daha böyük versiyadan istifadə edərək tərtib edə bilərik (bölməyə bax). İnkişaf Alətləri):

$ 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

Maraqlı xüsusiyyətlər arasında: hədəf arxitekturasını göstəririk -target bpf və başlıqlara gedən yol libbpf, bu yaxınlarda quraşdırdığımız. Həmçinin, haqqında unutmayın -O2, bu seçim olmadan gələcəkdə sürprizlərlə qarşılaşa bilərsiniz. Gəlin kodumuza baxaq, istədiyimiz proqramı yaza bildikmi?

$ 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

Bəli, işlədi! İndi proqramla ikili faylımız var və onu nüvəyə yükləyən proqram yaratmaq istəyirik. Bu məqsədlə kitabxana libbpf bizə iki seçim təklif edir - aşağı səviyyəli API və ya daha yüksək səviyyəli API istifadə edin. Biz ikinci yolla gedəcəyik, çünki BPF proqramlarını sonrakı öyrənilməsi üçün minimum səylə yazmağı, yükləməyi və birləşdirməyi öyrənmək istəyirik.

Birincisi, eyni yardım proqramından istifadə edərək, proqramımızın ikili sistemindən "skeletini" yaratmalıyıq bpftool — BPF dünyasının İsveçrə bıçağı (bunu sözün əsl mənasında qəbul etmək olar, çünki BPF-nin yaradıcılarından və baxıcılarından biri olan Daniel Borkman isveçrəlidir):

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

Faylda xdp-simple.skel.h proqramımızın ikili kodunu və idarəetmə funksiyalarını ehtiva edir - obyektimizi yükləmək, əlavə etmək, silmək. Sadə vəziyyətimizdə bu, həddindən artıq yük kimi görünür, lakin obyekt faylında çoxlu BPF proqramları və xəritələr olduğu halda da işləyir və bu nəhəng ELF-i yükləmək üçün sadəcə skelet yaratmaq və xüsusi proqramdan bir və ya iki funksiyanı çağırmaq lazımdır. yazır, indi davam edək.

Düzünü desək, yükləyici proqramımız mənasızdır:

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

Burada struct xdp_simple_bpf faylda müəyyən edilir xdp-simple.skel.h və obyekt faylımızı təsvir edir:

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

Aşağı səviyyəli API-nin izlərini burada görə bilərik: struktur struct bpf_program *simple и struct bpf_link *simple. Birinci struktur bölmədə yazılmış proqramımızı xüsusi olaraq təsvir edir xdp/simple, ikincisi isə proqramın hadisə mənbəyinə necə qoşulduğunu təsvir edir.

Function xdp_simple_bpf__open_and_load, ELF obyektini açır, onu təhlil edir, bütün strukturları və alt strukturları yaradır (proqramdan başqa ELF-də digər bölmələr də var - verilənlər, yalnız oxunan məlumatlar, sazlama məlumatları, lisenziya və s.) və sonra sistemdən istifadə edərək onu nüvəyə yükləyir. zəng edin bpf, proqramı tərtib edib işlətməklə yoxlaya bilərik:

$ 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

İndi istifadə etdiyimiz proqramı nəzərdən keçirək bpftool. Gəlin onun şəxsiyyət vəsiqəsini tapaq:

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

və dump (biz komandanın qısaldılmış formasından istifadə edirik bpftool prog dump xlated):

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

Yeni bir şey! Proqram C mənbə faylımızın hissələrini çap etdi.Bunu kitabxana həyata keçirdi libbpf, binar sistemdə debug bölməsini tapmış, onu BTF obyektinə tərtib etmiş və istifadə edərək nüvəyə yükləmişdir. BPF_BTF_LOAD, sonra isə proqramı əmrlə yükləyərkən yaranan fayl deskriptorunu təyin edin BPG_PROG_LOAD.

Kernel Köməkçiləri

BPF proqramları "xarici" funksiyaları - nüvə köməkçilərini işlədə bilər. Bu köməkçi funksiyalar BPF proqramlarına nüvə strukturlarına daxil olmaq, xəritələri idarə etmək, həmçinin “real dünya” ilə əlaqə saxlamaq imkanı verir - mükəmməl hadisələr yaratmaq, aparatları idarə etmək (məsələn, paketləri yönləndirmək) və s.

Misal: bpf_get_smp_processor_id

“Nümunə ilə öyrənmə” paradiqması çərçivəsində köməkçi funksiyalardan birini nəzərdən keçirək, bpf_get_smp_processor_id(), müəyyən faylda kernel/bpf/helpers.c. Onu çağıran BPF proqramının işlədiyi prosessorun nömrəsini qaytarır. Lakin biz onun semantikası ilə o qədər də maraqlanmırıq, çünki onun həyata keçirilməsi bir xətt çəkir:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF köməkçi funksiyasının tərifləri Linux sistem çağırış təriflərinə bənzəyir. Burada, məsələn, heç bir arqumenti olmayan funksiya müəyyən edilir. (Məsələn, üç arqument götürən funksiya makrodan istifadə etməklə müəyyən edilir BPF_CALL_3. Arqumentlərin maksimum sayı beşdir.) Lakin bu, tərifin yalnız birinci hissəsidir. İkinci hissə tip strukturunu müəyyən etməkdir struct bpf_func_proto, yoxlayıcının başa düşdüyü köməkçi funksiyanın təsvirini ehtiva edir:

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

Köməkçi funksiyaların qeydiyyatı

Müəyyən tipli BPF proqramlarının bu funksiyadan istifadə etməsi üçün onlar onu, məsələn, növ üçün qeydiyyatdan keçirməlidirlər BPF_PROG_TYPE_XDP nüvədə funksiya müəyyən edilir xdp_func_protoXDP-nin bu funksiyanı dəstəklədiyini və ya dəstəkləmədiyini köməkçi funksiya identifikatorundan müəyyən edir. Bizim funksiyamızdır dəstəkləyir:

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

Yeni BPF proqram növləri faylda "müəyyən edilmişdir" include/linux/bpf_types.h makrodan istifadə etməklə BPF_PROG_TYPE. Məntiqi tərif olduğu üçün dırnaqlarla müəyyən edilir və C dili terminlərində konkret konstruksiyaların bütün toplusunun tərifi başqa yerlərdə də olur. Xüsusilə, faylda kernel/bpf/verifier.c fayldan bütün təriflər bpf_types.h strukturlar massivi yaratmaq üçün istifadə olunur 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
};

Yəni, BPF proqramının hər bir növü üçün növün məlumat strukturuna göstərici müəyyən edilir struct bpf_verifier_ops, dəyəri ilə başlatılmışdır _name ## _verifier_ops, yəni, xdp_verifier_ops uğrunda xdp. Struktur xdp_verifier_ops tərəfindən müəyyənləşdirilmişdir faylda net/core/filter.c belədir:

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

Burada tanış funksiyamızı görürük xdp_func_proto, bu, hər dəfə problemlə qarşılaşdıqda doğrulayıcını işə salacaq bəziləri BPF proqramı daxilində funksiyalar, bax verifier.c.

Gəlin hipotetik BPF proqramının funksiyadan necə istifadə etdiyinə baxaq bpf_get_smp_processor_id. Bunun üçün əvvəlki bölməmizdəki proqramı aşağıdakı kimi yenidən yazırıq:

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

Rəmz bpf_get_smp_processor_id tərəfindən müəyyənləşdirilmişdir в <bpf/bpf_helper_defs.h> kitabxanalar libbpf kimi

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

yəni, bpf_get_smp_processor_id dəyəri 8 olan funksiya göstəricisidir, burada 8 dəyərdir BPF_FUNC_get_smp_processor_id növü enum bpf_fun_id, faylda bizim üçün müəyyən edilmişdir vmlinux.h (fayl bpf_helper_defs.h nüvədə bir skript yaradılır, buna görə də "sehrli" nömrələr yaxşıdır). Bu funksiya heç bir arqument qəbul etmir və növün dəyərini qaytarır __u32. Proqramımızda işlətdiyimiz zaman, clang göstəriş yaradır BPF_CALL "düzgün növ" Proqramı tərtib edək və bölməyə baxaq 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

Birinci sətirdə təlimatları görürük call, parametr IMM 8-ə bərabərdir və SRC_REG - sıfır. Doğrulayıcı tərəfindən istifadə edilən ABI razılaşmasına əsasən, bu, səkkiz nömrəli köməkçi funksiyaya zəngdir. O işə salındıqdan sonra məntiq sadədir. Reyestrdən dəyəri qaytarın r0 -ə köçürdü r1 2,3-cü sətirlərdə isə tipə çevrilir u32 — yuxarı 32 bit təmizlənir. 4,5,6,7-ci sətirlərdə 2 qaytarırıq (XDP_PASS) və ya 1 (XDP_DROP) 0-cı sətirdəki köməkçi funksiyanın sıfır və ya sıfırdan fərqli qiymət qaytarmasından asılı olaraq.

Gəlin özümüzü sınayaq: proqramı yükləyin və nəticəyə baxaq 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, doğrulayıcı düzgün kernel-köməkçini tapdı.

Nümunə: arqumentləri ötürmək və nəhayət proqramı işə salmaq!

Bütün run səviyyəli köməkçi funksiyaların prototipi var

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

Köməkçi funksiyalara parametrlər registrlərə ötürülür r1-r5, və dəyər registrdə qaytarılır r0. Beşdən çox arqument götürən funksiyalar yoxdur və gələcəkdə onlara dəstəyin əlavə edilməsi gözlənilmir.

Gəlin yeni nüvə köməkçisinə və BPF-nin parametrləri necə ötürməsinə nəzər salaq. Yenidən yazaq xdp-simple.bpf.c aşağıdakı kimi (sətirlərin qalan hissəsi dəyişməyib):

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

Proqramımız işlədiyi CPU-nun nömrəsini çap edir. Gəlin onu tərtib edək və koda baxaq:

$ llvm-objdump -D --section=xdp/simple --no-show-raw-insn xdp-simple.bpf.o

0000000000000000 <simple>:
       0:       r1 = 10
       1:       *(u16 *)(r10 - 8) = r1
       2:       r1 = 8441246879787806319 ll
       4:       *(u64 *)(r10 - 16) = r1
       5:       r1 = 2334956330918245746 ll
       7:       *(u64 *)(r10 - 24) = r1
       8:       call 8
       9:       r1 = r10
      10:       r1 += -24
      11:       r2 = 18
      12:       r3 = r0
      13:       call 6
      14:       r0 = 2
      15:       exit

0-7 sətirlərdə sətir yazırıq running on CPU%un, və sonra 8-ci sətirdə tanış olanı işə salırıq bpf_get_smp_processor_id. 9-12-ci sətirlərdə köməkçi arqumentlər hazırlayırıq bpf_printk - qeydiyyatdan keçir r1, r2, r3. Niyə ikisi yox, üçü var? Çünki bpf_printkbu makro sarğıdır əsl köməkçi ətrafında bpf_trace_printk, format sətirinin ölçüsünü keçməsi lazımdır.

İndi bir neçə sətir əlavə edək xdp-simple.cproqramımızın interfeysə qoşulması üçün lo və həqiqətən başladı!

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

Burada funksiyadan istifadə edirik bpf_set_link_xdp_fd, XDP tipli BPF proqramlarını şəbəkə interfeyslərinə birləşdirən. İnterfeys nömrəsini kodlaşdırdıq lo, bu həmişə 1-dir. Biz funksiyanı iki dəfə işlədirik ki, əgər köhnə proqram əlavə olunubsa, əvvəlcə onu ayırmaq lazımdır. Diqqət yetirin ki, indi bizim çağırışa ehtiyacımız yoxdur pause və ya sonsuz döngə: yükləyici proqramımız çıxacaq, lakin hadisə mənbəyinə qoşulduğu üçün BPF proqramı öldürülməyəcək. Uğurlu yükləmə və əlaqədən sonra proqram hər gələn şəbəkə paketi üçün işə salınacaq lo.

Proqramı yükləyək və interfeysə baxaq 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

Yüklədiyimiz proqram ID 669-a malikdir və interfeysdə eyni ID-ni görürük lo. Bir neçə paket göndərəcəyik 127.0.0.1 (sorğu + cavab):

$ ping -c1 localhost

və indi debug virtual faylının məzmununa baxaq /sys/kernel/debug/tracing/trace_pipe, hansında bpf_printk mesajlarını yazır:

# 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

Üzərində iki paket göründü lo və CPU0-da işlənmişdir - ilk tam hüquqlu mənasız BPF proqramımız işləmişdir!

Bunu qeyd etməyə dəyər bpf_printk Sazlama faylına yazması boş yerə deyil: bu istehsalda istifadə üçün ən uğurlu köməkçi deyil, amma məqsədimiz sadə bir şey göstərmək idi.

BPF proqramlarından xəritələrə daxil olmaq

Misal: BPF proqramından xəritədən istifadə etmək

Əvvəlki bölmələrdə istifadəçi məkanından xəritələrin necə yaradılmasını və istifadəsini öyrəndik, indi isə nüvə hissəsinə baxaq. Həmişə olduğu kimi, bir nümunə ilə başlayaq. Proqramımızı yenidən yazaq xdp-simple.bpf.c belədir:

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

Proqramın əvvəlində xəritə tərifini əlavə etdik woo: Bu kimi dəyərləri saxlayan 8 elementli massivdir u64 (C dilində biz belə bir massivi təyin edərdik u64 woo[8]). Bir proqramda "xdp/simple" cari prosessor nömrəsini dəyişənə alırıq key və sonra köməkçi funksiyadan istifadə edin bpf_map_lookup_element massivdəki müvafiq girişə bir göstərici alırıq, onu bir artırırıq. Rus dilinə tərcümə: gələn paketləri hansı CPU-nun emal etdiyinə dair statistikanı hesablayırıq. Proqramı işə salmağa çalışaq:

$ 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

Gəlin onun bağlı olduğunu yoxlayaq lo və bir neçə paket göndərin:

$ 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

İndi isə serialın məzmununa baxaq:

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

Demək olar ki, bütün proseslər CPU7-də işlənmişdir. Bu bizim üçün vacib deyil, əsas odur ki, proqram işləyir və biz BPF proqramlarından xəritələrə necə daxil olmağı başa düşürük - istifadə edərək хелперов bpf_mp_*.

Mistik göstərici

Beləliklə, kimi zənglərdən istifadə edərək BPF proqramından xəritəyə daxil ola bilərik

val = bpf_map_lookup_elem(&woo, &key);

köməkçi funksiyası harada görünür

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

lakin biz bir göstərici keçirik &woo adsız bir quruluşa struct { ... }...

Proqram assemblerinə baxsaq dəyərin olduğunu görərik &woo faktiki olaraq müəyyən edilməyib (sətir 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
...

və köçürmələrə daxildir:

$ 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

Ancaq artıq yüklənmiş proqrama baxsaq, düzgün xəritəyə bir göstərici görürük (sətir 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]
...

Beləliklə, belə bir nəticəyə gələ bilərik ki, yükləyici proqramımızı işə salarkən, link &woo kitabxanası olan bir şeylə əvəz olundu libbpf. Əvvəlcə çıxışa baxacağıq 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

Biz bunu görürük libbpf xəritə yaratdı woo və sonra proqramımızı yüklədik simple. Proqramı necə yükləməyimizə daha yaxından nəzər salaq:

  • zəng edin xdp_simple_bpf__open_and_load fayldan xdp-simple.skel.h
  • səbəb olur xdp_simple_bpf__load fayldan xdp-simple.skel.h
  • səbəb olur bpf_object__load_skeleton fayldan libbpf/src/libbpf.c
  • səbəb olur bpf_object__load_xattr haqqında libbpf/src/libbpf.c

Son funksiya, digər şeylər arasında, zəng edəcək bpf_object__create_maps, mövcud xəritələri yaradan və ya açan, onları fayl deskriptorlarına çevirən. (Gördüyümüz yer budur BPF_MAP_CREATE çıxışda strace.) Sonra funksiya çağırılır bpf_object__relocate və gördüklərimizi xatırladığımız üçün bizi maraqlandıran odur woo köçürmə cədvəlində. Onu araşdıraraq, nəticədə özümüzü funksiyada tapırıq bpf_program__relocate, hansı xəritələrin köçürülməsi ilə məşğul olur:

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

Beləliklə, biz təlimatlarımızı alırıq

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

və içindəki mənbə registrini ilə əvəz edin BPF_PSEUDO_MAP_FD, və xəritəmizin fayl deskriptoruna ilk IMM və bərabərdirsə, məsələn, 0xdeadbeef, nəticədə biz təlimat alacağıq

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

Xəritə məlumatı xüsusi yüklənmiş BPF proqramına belə ötürülür. Bu halda, xəritə istifadə edərək yaradıla bilər BPF_MAP_CREATE, və istifadə edərək ID ilə açılır BPF_MAP_GET_FD_BY_ID.

Ümumi, istifadə edərkən libbpf alqoritm aşağıdakı kimidir:

  • tərtib zamanı xəritələrə keçidlər üçün yerdəyişmə cədvəlində qeydlər yaradılır
  • libbpf ELF obyekt kitabını açır, bütün istifadə olunan xəritələri tapır və onlar üçün fayl deskriptorları yaradır
  • fayl deskriptorları təlimatın bir hissəsi kimi nüvəyə yüklənir LD64

Təsəvvür edə bildiyiniz kimi, qarşıda daha çox şey var və biz özümüzə baxmalı olacağıq. Xoşbəxtlikdən, bir ipucumuz var - mənasını yazdıq BPF_PSEUDO_MAP_FD mənbə reyestrinə daxil edin və biz onu basdıra bilərik, bu da bizi bütün müqəddəslərin müqəddəsliyinə aparacaq - kernel/bpf/verifier.c, burada fərqli ada malik funksiya fayl deskriptorunu tipli strukturun ünvanı ilə əvəz edir 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;

(tam kodu tapa bilərsiniz по ссылке). Beləliklə, alqoritmimizi genişləndirə bilərik:

  • Proqramı yükləyərkən yoxlayıcı xəritədən düzgün istifadəni yoxlayır və müvafiq strukturun ünvanını yazır struct bpf_map

ELF binar faylını yükləyərkən istifadə edərək libbpf Daha çox şey var, amma biz bunu digər məqalələrdə müzakirə edəcəyik.

Proqramların və xəritələrin libbpf olmadan yüklənməsi

Söz verdiyimiz kimi, xəritələrdən istifadə edən proqramı köməksiz necə yaratmağı və yükləməyi öyrənmək istəyən oxucular üçün bir nümunədir. libbpf. Bu, asılılıq qura bilməyəcəyiniz bir mühitdə işləyərkən və ya hər biti saxlaya bildiyiniz zaman və ya bu kimi bir proqram yazarkən faydalı ola bilər. ply, tez BPF ikili kodu yaradır.

Məntiqə əməl etməyi asanlaşdırmaq üçün nümunəmizi bu məqsədlər üçün yenidən yazacağıq xdp-simple. Bu nümunədə müzakirə olunan proqramın tam və bir qədər genişləndirilmiş kodunu burada tapa bilərsiniz əsas.

Tətbiqimizin məntiqi belədir:

  • tip xəritəsi yaradın BPF_MAP_TYPE_ARRAY əmrindən istifadə edərək BPF_MAP_CREATE,
  • bu xəritədən istifadə edən proqram yaradın,
  • proqramı interfeysə qoşun lo,

kimi insan dilinə tərcümə olunur

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

Burada map_create sistem çağırışı ilə bağlı ilk nümunədə etdiyimiz kimi bir xəritə yaradır bpf - “kernel, xahiş edirəm mənə 8 elementdən ibarət massiv şəklində yeni xəritə yaradın __u64 və mənə fayl deskriptorunu geri verin":

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

Proqramı yükləmək də asandır:

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

Çətin hissə prog_load BPF proqramımızın strukturlar massivi kimi tərifidir struct bpf_insn insns[]. Ancaq biz C dilində olan bir proqramdan istifadə etdiyimiz üçün bir az aldada bilərik:

$ 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

Ümumilikdə, kimi strukturlar şəklində 14 təlimat yazmalıyıq struct bpf_insn (məsləhət: yuxarıdan zibil götürün, təlimatlar bölməsini yenidən oxuyun, açın linux/bpf.h и linux/bpf_common.h və müəyyən etməyə çalışın struct bpf_insn insns[] öz başına):

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

Bunu özləri yazmayanlar üçün bir məşq - tapın map_fd.

Proqramımızda daha bir açıqlanmayan hissə qalıb - xdp_attach. Təəssüf ki, XDP kimi proqramlar sistem çağırışı ilə qoşula bilməz bpf. BPF və XDP-ni yaradan insanlar onlayn Linux icmasından idilər, yəni onlara ən tanış olanı istifadə etdilər (lakin bunu etmək üçün yox) normal insanlar) nüvə ilə qarşılıqlı əlaqə üçün interfeys: netlink yuvaları, həmçinin bax RFC3549. Həyata keçirməyin ən sadə yolu xdp_attach kodunu kopyalayır libbpf, yəni fayldan netlink.c, biz bunu bir az qısaldaraq etdik:

Netlink rozetkaları dünyasına xoş gəlmisiniz

Netlink yuvası növünü açın 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;
}

Bu yuvadan oxuyuruq:

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

Nəhayət, rozetkanı açan və ona fayl deskriptoru olan xüsusi mesaj göndərən funksiyamız budur:

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

Beləliklə, hər şey sınaq üçün hazırdır:

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

Proqramımızın qoşulub-qoşulmadığına baxaq 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

Pinglər göndərək və xəritəyə baxaq:

$ 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

Hörmətli, hər şey işləyir. Yeri gəlmişkən qeyd edək ki, xəritəmiz yenidən bayt şəklində göstərilir. Bu, fərqli olaraq olması ilə əlaqədardır libbpf tip məlumatını (BTF) yükləmədik. Ancaq bu barədə növbəti dəfə daha çox danışacağıq.

İnkişaf Alətləri

Bu bölmədə biz minimum BPF tərtibatçı alət dəstinə baxacağıq.

Ümumiyyətlə, BPF proqramlarını inkişaf etdirmək üçün xüsusi bir şeyə ehtiyacınız yoxdur - BPF hər hansı layiqli paylama nüvəsi üzərində işləyir və proqramlar aşağıdakı proqramlardan istifadə etməklə qurulur. clang, paketdən təmin edilə bilər. Bununla belə, BPF inkişaf mərhələsində olduğu üçün nüvə və alətlər daim dəyişir, əgər 2019-cu ildən köhnə metodlardan istifadə edərək BPF proqramları yazmaq istəmirsinizsə, onda siz tərtib etməli olacaqsınız.

  • llvm/clang
  • pahole
  • onun əsası
  • bpftool

(İstinad üçün, bu bölmə və məqalədəki bütün nümunələr Debian 10-da işlədilib.)

llvm/clang

BPF LLVM ilə dostdur və bu yaxınlarda BPF üçün proqramlar gcc istifadə edərək tərtib oluna bilsə də, bütün cari inkişaflar LLVM üçün həyata keçirilir. Buna görə də, ilk növbədə, hazırkı versiyanı quracağıq clang git-dən:

$ 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
... много времени спустя
$

İndi hər şeyin düzgün olub olmadığını yoxlaya bilərik:

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

(Quraşdırma təlimatları clang məndən götürülmüşdür bpf_devel_QA.)

Biz indicə qurduğumuz proqramları quraşdırmayacağıq, əksinə onları əlavə edəcəyik PATH, məsələn:

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

(Buna əlavə etmək olar .bashrc və ya ayrıca fayla. Şəxsən mən bu kimi şeyləri əlavə edirəm ~/bin/activate-llvm.sh və lazım olanda bunu edirəm . activate-llvm.sh.)

Pahole və BTF

Kommunal pahole BTF formatında sazlama məlumatı yaratmaq üçün nüvəni qurarkən istifadə olunur. BTF texnologiyasının təfərrüatları haqqında bu yazıda onun rahat olması və ondan istifadə etmək istədiyimizdən başqa, ətraflı məlumat verməyəcəyik. Beləliklə, nüvənizi qurmaq niyyətindəsinizsə, əvvəlcə qurun pahole (olmadan pahole seçimi ilə kernel qura bilməyəcəksiniz CONFIG_DEBUG_INFO_BTF:

$ git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
$ cd pahole/
$ sudo apt install cmake
$ mkdir build
$ cd build/
$ cmake -D__LIB=lib ..
$ make
$ sudo make install
$ which pahole
/usr/local/bin/pahole

BPF ilə təcrübə aparmaq üçün ləpələr

BPF-nin imkanlarını araşdırarkən mən öz nüvəmi yığmaq istəyirəm. Bu, ümumiyyətlə, lazım deyil, çünki siz paylama kernelində BPF proqramlarını tərtib edə və yükləyə biləcəksiniz, lakin öz nüvənizin olması sizə ən son BPF xüsusiyyətlərindən istifadə etməyə imkan verir ki, bu da paylamada ən yaxşı halda aylar ərzində görünəcək. , və ya bəzi sazlama alətlərində olduğu kimi yaxın gələcəkdə paketlənməyəcək. Həmçinin, öz nüvəsi kodla sınaq keçirməyi vacib hiss edir.

Bir nüvə qurmaq üçün, birincisi, nüvənin özü, ikincisi, nüvənin konfiqurasiya faylı lazımdır. BPF ilə təcrübə etmək üçün adi üsuldan istifadə edə bilərik vanil kernel və ya inkişaf ləpələrindən biri. Tarixən, BPF inkişafı Linux şəbəkə icması daxilində baş verir və buna görə də bütün dəyişikliklər gec-tez Linux şəbəkəsinin təminatçısı David Millerdən keçir. Təbiətindən asılı olaraq - redaktələr və ya yeni funksiyalar - şəbəkə dəyişiklikləri iki nüvədən birinə düşür - net və ya net-next. BPF üçün dəyişikliklər arasında eyni şəkildə paylanır bpf и bpf-next, sonra müvafiq olaraq net və net-next-ə birləşdirilir. Ətraflı məlumat üçün bax bpf_devel_QA и netdev-FAQ. Beləliklə, zövqünüzə və sınaqdan keçirdiyiniz sistemin sabitlik ehtiyaclarına əsaslanaraq nüvəni seçin (*-next ləpələr sadalananlardan ən qeyri-sabitdir).

Kernel konfiqurasiya fayllarını necə idarə etmək barədə danışmaq bu məqalənin əhatə dairəsi xaricindədir - güman edilir ki, ya bunu necə edəcəyinizi bilirsiniz, ya da öyrənməyə hazırdır tək başına. Bununla belə, aşağıdakı təlimatlar sizə işlək BPF-i dəstəkləyən sistem vermək üçün kifayət qədər az və ya çox olmalıdır.

Yuxarıdakı ləpələrdən birini endirin:

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

Minimum işləyən nüvə konfiqurasiyası qurun:

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

Faylda BPF seçimlərini aktivləşdirin .config öz seçiminizlə (çox güman ki CONFIG_BPF systemd istifadə etdiyi üçün artıq aktiv olacaq). Bu məqalə üçün istifadə olunan nüvədən seçimlərin siyahısı:

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

Sonra modulları və kerneli asanlıqla yığıb quraşdıra bilərik (yeri gəlmişkən, siz yeni yığılmış nüvədən istifadə edərək nüvəni yığa bilərsiniz. clangəlavə etməklə CC=clang):

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

və yeni nüvə ilə yenidən başladın (bunun üçün istifadə edirəm kexec paketdən 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

Məqalədə ən çox istifadə olunan yardım proqramı köməkçi proqram olacaqdır bpftool, Linux nüvəsinin bir hissəsi kimi təchiz edilmişdir. O, BPF tərtibatçıları üçün BPF tərtibatçıları tərəfindən yazılır və saxlanılır və bütün növ BPF obyektlərini idarə etmək üçün istifadə edilə bilər - proqramları yükləmək, xəritələr yaratmaq və redaktə etmək, BPF ekosisteminin həyatını araşdırmaq və s. İnsan səhifələri üçün mənbə kodları şəklində sənədlər tapıla bilər əsasda və ya artıq tərtib edilmiş, şəbəkədə.

Bu yazı zamanı bpftool yalnız RHEL, Fedora və Ubuntu üçün hazırdır (bax, məsələn, bu ip, qablaşdırmanın yarımçıq hekayəsindən bəhs edir bpftool Debian-da). Ancaq nüvənizi artıq qurmusunuzsa, onda qurun bpftool pasta kimi asan:

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

$

(Burada ${linux} - bu sizin nüvə kataloqunuzdur.) Bu əmrləri yerinə yetirdikdən sonra bpftool kataloqda toplanacaq ${linux}/tools/bpf/bpftool və yola əlavə edilə bilər (ilk növbədə istifadəçi üçün root) və ya sadəcə kopyalayın /usr/local/sbin.

Toplayın bpftool sonuncudan istifadə etmək daha yaxşıdır clang, yuxarıda göstərildiyi kimi yığılır və düzgün yığılıb- yığılmadığını yoxlayın - məsələn, əmrdən istifadə edərək

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

nüvənizdə hansı BPF xüsusiyyətlərinin aktiv olduğunu göstərəcək.

Yeri gəlmişkən, əvvəlki əmr kimi işlədilə bilər

# bpftool f p k

Bu, paketdəki kommunallarla bənzətmə yolu ilə edilir iproute2, məsələn, deyə bilərik ip a s eth0 əvəzinə ip addr show dev eth0.

Nəticə

BPF, nüvənin funksionallığını effektiv şəkildə ölçmək və anında dəyişmək üçün birə çəkməyə imkan verir. Sistem UNIX-in ən yaxşı ənənələrində çox uğurlu oldu: ləpəni (yenidən) proqramlaşdırmağa imkan verən sadə mexanizm çoxlu sayda insan və təşkilata təcrübə keçirməyə imkan verdi. Təcrübələr, eləcə də BPF infrastrukturunun özünün inkişafı başa çatmaqdan çox uzaq olsa da, sistem artıq etibarlı və ən əsası effektiv biznes məntiqi qurmağa imkan verən sabit ABI-yə malikdir.

Qeyd etmək istərdim ki, mənim fikrimcə, texnologiya bu qədər populyarlaşdı, çünki bir tərəfdən bunu edə bilər oyun (bir maşının arxitekturasını bir axşama az-çox başa düşmək olar), digər tərəfdən isə üzə çıxmazdan əvvəl (gözəl) həlli mümkün olmayan məsələləri həll etmək. Bu iki komponent birlikdə insanları təcrübə və xəyal qurmağa məcbur edir ki, bu da getdikcə daha çox innovativ həllərin yaranmasına səbəb olur.

Bu məqalə, xüsusilə qısa olmasa da, yalnız BPF dünyasına girişdir və memarlığın "qabaqcıl" xüsusiyyətlərini və vacib hissələrini təsvir etmir. Gələcək plan belədir: növbəti məqalədə BPF proqram növlərinin icmalı olacaq (5.8 kerneldə dəstəklənən 30 proqram növü var), sonra biz nəhayət kernel izləmə proqramlarından istifadə edərək real BPF tətbiqlərinin necə yazılacağına baxacağıq. misal olaraq, BPF arxitekturasına dair daha dərin kursun, ardınca BPF şəbəkəsi və təhlükəsizlik proqramlarının nümunələrinin vaxtıdır.

Bu seriyanın əvvəlki məqalələri

  1. Kiçiklər üçün BPF, sıfır hissə: klassik BPF

Bağlantılar

  1. BPF və XDP İstinad Bələdçisi — ciliumdan, daha doğrusu, BPF-nin yaradıcılarından və baxıcılarından biri olan Daniel Borkmandan BPF haqqında sənədlər. Bu, digərlərindən fərqlənən ilk ciddi təsvirlərdən biridir ki, Daniel nə haqqında yazdığını dəqiq bilir və orada heç bir səhv yoxdur. Xüsusilə, bu sənəd tanınmış yardım proqramından istifadə edərək XDP və TC tipli BPF proqramları ilə necə işləməyi təsvir edir. ip paketdən iproute2.

  2. Documentation/şəbəkə/filter.txt — klassik və sonra genişləndirilmiş BPF üçün sənədləri olan orijinal fayl. Montaj dilini və texniki memarlıq detallarını araşdırmaq istəyirsinizsə yaxşı oxuyun.

  3. Facebook-dan BPF haqqında blog. Nadir hallarda yenilənir, lakin Aleksey Starovoitov (eBPF-nin müəllifi) və Andrii Nakryiko - (baxıcı) orada yazdıqları kimi uyğundur. libbpf).

  4. bpftool sirləri. Quentin Monnet-dən bpftool istifadəsinin nümunələri və sirləri ilə əyləncəli twitter mövzusu.

  5. BPF-ə dalın: oxu materiallarının siyahısı. Quentin Monnet-dən BPF sənədlərinə keçidlərin nəhəng (və hələ də saxlanılan) siyahısı.

Mənbə: www.habr.com

Добавить комментарий