Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Басында технология болды және ол BPF деп аталды. Біз оған қарадық алдыңғы, Осы сериядағы Ескі өсиет мақаласы. 2013 жылы Алексей Старовойтов пен Даниэль Боркманның күшімен оның жетілдірілген нұсқасы қазіргі заманғы 64 разрядты машиналар үшін оңтайландырылды және Linux ядросына енгізілді. Бұл жаңа технология қысқаша Ішкі BPF деп аталды, содан кейін кеңейтілген BPF деп өзгертілді, енді бірнеше жылдан кейін барлығы оны жай BPF деп атайды.

Шамамен айтқанда, BPF сізге Linux ядросының кеңістігінде ерікті пайдаланушы беретін кодты іске қосуға мүмкіндік береді және жаңа архитектураның сәтті болғаны сонша, оның барлық қосымшаларын сипаттау үшін бізге тағы ондаған мақала қажет болады. (Төмендегі өнімділік кодында көріп отырғаныңыздай, әзірлеушілер жақсы істемеген жалғыз нәрсе - лайықты логотип жасау болды.)

Бұл мақалада BPF виртуалды машинасының құрылымы, BPF-мен жұмыс істеуге арналған ядро ​​​​интерфейстері, әзірлеу құралдары, сондай-ақ бар мүмкіндіктерге қысқаша, өте қысқаша шолу, яғни. BPF практикалық қолдануларын тереңірек зерттеу үшін болашақта бізге қажет нәрсенің бәрі.
Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Мақаланың қысқаша мазмұны

BPF архитектурасына кіріспе. Біріншіден, біз BPF архитектурасына құс көзімен қарап, негізгі құрамдастарды сипаттаймыз.

BPF виртуалды машинасының регистрлері және командалық жүйесі. Тұтастай архитектура туралы түсінікке ие бола отырып, біз BPF виртуалды машинасының құрылымын сипаттайтын боламыз.

BPF объектілерінің өмірлік циклі, bpffs файлдық жүйесі. Бұл бөлімде біз BPF объектілерінің өмірлік циклін - бағдарламалар мен карталарды егжей-тегжейлі қарастырамыз.

bpf жүйелік шақыру арқылы нысандарды басқару. Жүйе туралы біраз түсінігі бар, біз ақырында арнайы жүйелік шақыру арқылы пайдаланушы кеңістігінен нысандарды қалай құруға және басқаруға болатынын қарастырамыз - bpf(2).

Пишем программы BPF с помощью libbpf. Әрине, жүйелік шақыру арқылы бағдарламаларды жазуға болады. Бірақ қиын. Нақтырақ сценарий үшін ядролық бағдарламашылар кітапхана әзірледі libbpf. Біз келесі мысалдарда қолданатын негізгі BPF қолданбасының қаңқасын жасаймыз.

Ядро көмекшілері. Мұнда біз BPF бағдарламалары ядроның көмекші функцияларына қалай қол жеткізе алатынын үйренеміз - бұл құрал карталармен бірге классикалықпен салыстырғанда жаңа BPF мүмкіндіктерін түбегейлі кеңейтеді.

BPF бағдарламаларынан карталарға қол жеткізу. Осы кезде біз карталарды пайдаланатын бағдарламаларды қалай жасауға болатынын түсіну үшін жеткілікті білеміз. Тіпті керемет және күшті тексерушіге жылдам шолу жасайық.

Әзірлеу құралдары. Эксперименттерге қажетті утилиталар мен ядроларды қалай жинау керектігі туралы анықтама бөлімі.

Қорытынды. Мақаланың соңында осы уақытқа дейін оқығандар келесі мақалаларда мотивациялық сөздер мен не болатынын қысқаша сипаттайды. Сондай-ақ біз жалғастыруды күтуге ниеті немесе мүмкіндігі жоқ адамдар үшін өздігінен оқуға арналған бірқатар сілтемелерді тізімдейміз.

BPF архитектурасына кіріспе

BPF архитектурасын қарастыруды бастамас бұрын, біз соңғы рет (oh) сілтеме жасаймыз классикалық BPF, ол RISC машиналарының пайда болуына жауап ретінде әзірленді және тиімді пакеттерді сүзу мәселесін шешті. Архитектураның сәтті болғаны соншалық, тоқсаныншы жылдары Беркли UNIX-те дүниеге келген ол бар операциялық жүйелердің көпшілігіне ауыстырылды, жиырмасыншы жылдарға дейін аман қалды және әлі де жаңа қосымшаларды табуда.

Жаңа BPF 64-биттік машиналар, бұлттық қызметтер және SDN құру құралдарының қажеттілігінің артуына жауап ретінде әзірленді (Sжиі-dанықталған nжұмыс). Классикалық BPF үшін жетілдірілген ауыстыру ретінде ядролық желі инженерлері әзірлеген, жаңа BPF алты айдан кейін Linux жүйелерін іздеудің қиын тапсырмасында қолданбаларды тапты және енді, пайда болғаннан кейін алты жыл өткен соң, бізге келесі мақала қажет болады: бағдарламалардың әр түрін көрсетіңіз.

Күлкілі суреттер

Негізінде, BPF қауіпсіздікке нұқсан келтірместен ядро ​​кеңістігінде «еркін» кодты іске қосуға мүмкіндік беретін құмсалғыш виртуалды машинасы болып табылады. BPF бағдарламалары пайдаланушы кеңістігінде жасалады, ядроға жүктеледі және кейбір оқиғалар көзіне қосылады. Оқиға, мысалы, желілік интерфейске пакетті жеткізу, кейбір ядро ​​​​функциясын іске қосу және т.б. болуы мүмкін. Бума жағдайында BPF бағдарламасы буманың деректері мен метадеректеріне қол жеткізе алады (бағдарлама түріне байланысты оқу және, мүмкін, жазу үшін); ядро ​​​​функциясын іске қосқан жағдайда, дәлелдер. функция, соның ішінде ядро ​​жадына арналған көрсеткіштер және т.б.

Бұл процесті толығырақ қарастырайық. Алдымен классикалық BPF-тен бірінші айырмашылығы туралы айтайық, олар үшін бағдарламалар ассемблерде жазылған. Жаңа нұсқада архитектура бағдарламаларды жоғары деңгейлі тілдерде, ең алдымен, әрине, С тілінде жазуға болатындай кеңейтілді. Бұл үшін BPF архитектурасы үшін байт-кодты генерациялауға мүмкіндік беретін llvm сервері әзірленді.

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

BPF архитектурасы ішінара заманауи машиналарда тиімді жұмыс істеу үшін жасалған. Бұл тәжірибеде жұмыс істеу үшін ядроға жүктелгеннен кейін BPF байт коды JIT компиляторы деп аталатын құрамдастың көмегімен жергілікті кодқа аударылады (Jжоғары In Tмен). Әрі қарай, есіңізде болса, классикалық BPF-де бағдарлама ядроға жүктеліп, оқиға көзіне атомдық түрде - бір жүйелік шақыру контексінде тіркелген. Жаңа архитектурада бұл екі кезеңде болады - біріншіден, код жүйелік шақыру арқылы ядроға жүктеледі. bpf(2)содан кейін бағдарлама түріне байланысты өзгеретін басқа механизмдер арқылы бағдарлама оқиға көзіне бекітіледі.

Бұл жерде оқырманда сұрақ туындауы мүмкін: мүмкін бе? Мұндай кодты орындау қауіпсіздігіне қалай кепілдік беріледі? Орындау қауіпсіздігі бізге BPF бағдарламаларын жүктеу кезеңі арқылы кепілдік береді verifier (ағылшын тілінде бұл кезең verifier деп аталады және мен ағылшын сөзін пайдалануды жалғастырамын):

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Верификатор – программаның ядроның қалыпты жұмысын бұзбауын қамтамасыз ететін статикалық анализатор. Бұл, айтпақшы, бағдарлама жүйенің жұмысына кедергі келтіре алмайды дегенді білдірмейді - BPF бағдарламалары түріне байланысты ядро ​​жадының бөлімдерін оқи және қайта жаза алады, функциялардың мәндерін қайтара алады, қиюға, қосуға, қайта жазуға болады. және тіпті желілік пакеттерді жіберу. Verifier BPF бағдарламасын іске қосу ядроны бұзбайтынына және ережелерге сәйкес жазуға рұқсаты бар бағдарлама, мысалы, шығыс пакетінің деректері пакеттен тыс ядро ​​​​жадысын қайта жаза алмайтынына кепілдік береді. BPF барлық басқа компоненттерімен танысқаннан кейін біз сәйкес бөлімде верфикаторды толығырақ қарастырамыз.

Сонымен, біз осы уақытқа дейін не білдік? Пайдаланушы бағдарламаны Си тілінде жазады, жүйелік шақыру арқылы ядроға жүктейді bpf(2), мұнда оны тексеруші тексереді және жергілікті байт кодқа аударады. Содан кейін сол немесе басқа пайдаланушы бағдарламаны оқиға көзіне қосады және ол орындала бастайды. Жүктеу мен қосылымды бөлу бірнеше себептерге байланысты қажет. Біріншіден, верфикаторды іске қосу салыстырмалы түрде қымбат және бір бағдарламаны бірнеше рет жүктеп алу арқылы біз компьютер уақытын босқа кетіреміз. Екіншіден, бағдарламаның дәл қалай қосылуы оның түріне байланысты және бір жыл бұрын жасалған бір «әмбебап» интерфейс бағдарламалардың жаңа түрлеріне сәйкес келмеуі мүмкін. (Қазір архитектура жетілген болса да, бұл интерфейсті деңгейде біріктіру идеясы бар. libbpf.)

Мұқият оқырман суреттермен әлі аяқталмағанымызды байқайды. Шынында да, жоғарыда айтылғандардың бәрі BPF классикалық BPF салыстырғанда суретті түбегейлі өзгертетінін түсіндірмейді. Қолдану аясын айтарлықтай кеңейтетін екі жаңалық - ортақ жад пен ядро ​​көмекшісі функцияларын пайдалану мүмкіндігі. BPF-де ортақ жад карталар деп аталатындар арқылы жүзеге асырылады - белгілі бір API бар ортақ деректер құрылымдары. Олар бұл атауды алған болуы мүмкін, себебі картаның бірінші түрі хэш-кесте пайда болды. Содан кейін массивтер пайда болды, жергілікті (әр CPU) хэш кестелері және жергілікті массивтер, іздеу ағаштары, BPF бағдарламаларына көрсеткіштерді қамтитын карталар және т.б. Біз үшін ең қызықтысы, BPF бағдарламаларының енді қоңыраулар арасындағы күйді сақтау және оны басқа бағдарламалармен және пайдаланушы кеңістігімен бөлісу мүмкіндігі бар.

Карталарға жүйелік қоңырау арқылы пайдаланушы процестерінен қатынасады bpf(2), және көмекші функцияларды қолданатын ядрода жұмыс істейтін BPF бағдарламаларынан. Сонымен қатар, көмекшілер карталармен жұмыс істеу үшін ғана емес, сонымен қатар ядроның басқа мүмкіндіктеріне қол жеткізу үшін де бар. Мысалы, BPF бағдарламалары пакеттерді басқа интерфейстерге жіберу, перф оқиғаларын жасау, ядро ​​құрылымдарына қол жеткізу және т.б. үшін көмекші функцияларды пайдалана алады.

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Қорытындылай келе, BPF ядро ​​кеңістігіне ерікті, яғни тексеруші тексерген пайдаланушы кодын жүктеу мүмкіндігін береді. Бұл код қоңыраулар арасындағы күйді сақтай алады және пайдаланушы кеңістігімен деректермен алмасуы мүмкін, сонымен қатар бағдарламаның осы түрі рұқсат еткен ядролық ішкі жүйелерге қол жеткізе алады.

Бұл қазірдің өзінде ядро ​​модульдерімен қамтамасыз етілген мүмкіндіктерге ұқсас, олармен салыстырғанда BPF кейбір артықшылықтары бар (әрине, сіз тек ұқсас қолданбаларды салыстыра аласыз, мысалы, жүйелік бақылау - BPF көмегімен ерікті драйверді жаза алмайсыз). Төменгі кіру шегін (BPF пайдаланатын кейбір утилиталар пайдаланушыдан ядро ​​​​бағдарламалау дағдыларын немесе жалпы бағдарламалау дағдыларын талап етпейді), жұмыс уақытының қауіпсіздігін (жазған кезде жүйені бұзбағандар үшін түсініктемелерде қолыңызды көтеріңіз) атап өтуге болады. немесе тестілеу модульдері), атомдық - модульдерді қайта жүктеу кезінде тоқтау уақыты болады және BPF ішкі жүйесі ешбір оқиғаны өткізіп алмауын қамтамасыз етеді (әділдік үшін, бұл BPF бағдарламаларының барлық түрлеріне қатысты емес).

Мұндай мүмкіндіктердің болуы BPF-ті ядроны кеңейтудің әмбебап құралына айналдырады, бұл іс жүзінде расталды: BPF-ге бағдарламалардың көбірек жаңа түрлері қосылып жатыр, көбірек ірі компаниялар BPF 24 × 7 жауынгерлік серверлерде қолданады. стартаптар өз бизнесін BPF негізіндегі шешімдерге негіздейді. BPF барлық жерде қолданылады: DDoS шабуылдарынан қорғауда, SDN құруда (мысалы, кубернеттерге арналған желілерді жүзеге асыруда), негізгі жүйені бақылау құралы және статистикалық коллектор ретінде, басып кіруді анықтау жүйелерінде және құмсалғыш жүйелерінде және т.б.

Мақаланың шолу бөлігін осында аяқтап, виртуалды машина мен BPF экожүйесін толығырақ қарастырайық.

Шығу: утилиталар

Келесі бөлімдердегі мысалдарды іске қосу үшін сізге кем дегенде бірнеше утилиталар қажет болуы мүмкін. llvm/clang bpf қолдауымен және bpftool. Бөлімде Даму құралдары Сіз утилиталарды, сондай-ақ ядроңызды құрастыру нұсқауларын оқи аласыз. Бұл бөлім презентациямыздың үйлесімділігін бұзбау үшін төменде орналастырылған.

BPF виртуалды машина регистрлері және нұсқаулар жүйесі

BPF архитектурасы мен командалық жүйесі бағдарламалардың Си тілінде жазылатынын және ядроға жүктелгеннен кейін жергілікті кодқа аударылатынын ескере отырып әзірленді. Сондықтан регистрлердің саны мен командалар жиынтығы математикалық мағынада қазіргі заманғы машиналар мүмкіндіктерінің қиылысуын ескере отырып таңдалды. Сонымен қатар, бағдарламаларға әртүрлі шектеулер қойылды, мысалы, соңғы уақытқа дейін циклдар мен ішкі бағдарламаларды жазу мүмкін болмады, ал командалар саны 4096-ға дейін шектелді (қазір артықшылықты бағдарламалар миллионға дейін нұсқауларды жүктей алады).

BPF-де он бір пайдаланушы қол жетімді 64-биттік регистр бар r0-r10 және бағдарлама есептегіші. Тіркелу r10 жақтау көрсеткішін қамтиды және тек оқуға арналған. Бағдарламалар орындалу уақытында 512 байт стекке және карталар түріндегі шектеусіз ортақ жадқа қол жеткізе алады.

BPF бағдарламаларына бағдарлама түріндегі ядро ​​көмекшілерінің белгілі бір жинағын және жақында тұрақты функцияларды іске қосуға рұқсат етілген. Әрбір шақырылатын функция регистрлерде берілген бес аргументке дейін қабылдай алады r1-r5, және қайтарылатын мән жіберіледі r0. Функциядан оралғаннан кейін регистрлердің мазмұнына кепілдік беріледі r6-r9 Өзгермейді.

Бағдарламаны тиімді аудару үшін регистрлер r0-r11 барлық қолдау көрсетілетін архитектуралар үшін ағымдағы архитектураның ABI мүмкіндіктерін ескере отырып, нақты регистрлерге бірегей түрде салыстырылады. Мысалы, үшін x86_64 тіркеледі r1-r5, функция параметрлерін беру үшін пайдаланылады, қосулы болады rdi, rsi, rdx, rcx, r8, олар параметрді функцияларға жіберу үшін пайдаланылады x86_64. Мысалы, сол жақтағы код оң жақтағы кодқа келесідей аударылады:

1:  (b7) r1 = 1                    mov    $0x1,%rdi
2:  (b7) r2 = 2                    mov    $0x2,%rsi
3:  (b7) r3 = 3                    mov    $0x3,%rdx
4:  (b7) r4 = 4                    mov    $0x4,%rcx
5:  (b7) r5 = 5                    mov    $0x5,%r8
6:  (85) call pc+1                 callq  0x0000000000001ee8

Тіркелу r0 сонымен қатар бағдарламаның орындалу нәтижесін қайтару үшін және регистрде қолданылады r1 бағдарлама контекстке көрсеткіш беріледі - бағдарлама түріне байланысты бұл, мысалы, құрылым болуы мүмкін struct xdp_md (XDP үшін) немесе құрылым struct __sk_buff (әртүрлі желілік бағдарламалар үшін) немесе құрылым struct pt_regs (қадағалау бағдарламаларының әртүрлі түрлері үшін) және т.б.

Сонымен, бізде регистрлер жинағы, ядро ​​көмекшілері, стек, контекстік көрсеткіш және карталар түріндегі ортақ жады болды. Мұның бәрі сапарда өте қажет емес, бірақ...

Сипаттаманы жалғастырып, осы объектілермен жұмыс істеуге арналған командалық жүйе туралы сөйлесейік. Барлық (барлығы дерлік) BPF нұсқауларында бекітілген 64 бит өлшемі бар. Егер сіз 64 биттік Big Endian құрылғысындағы бір нұсқауды қарасаңыз, көресіз

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Бұл Code - бұл нұсқаулықтың кодталуы, Dst/Src сәйкесінше қабылдағыш пен көздің кодтаулары болып табылады, Off - 16-биттік қолтаңбалы шегініс, және Imm кейбір нұсқауларда қолданылатын 32 биттік таңбалы бүтін сан (cBPF тұрақты K сияқты). Кодтау Code екі түрінің бірі бар:

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

0, 1, 2, 3 нұсқау кластары жадымен жұмыс істеу командаларын анықтайды. Олар деп аталады, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, тиісінше. 4, 7 сыныптар (BPF_ALU, BPF_ALU64) ALU нұсқауларының жинағын құрайды. 5, 6 сыныптар (BPF_JMP, BPF_JMP32) секіру нұсқауларын қамтиды.

BPF нұсқау жүйесін зерттеудің одан әрі жоспары келесідей: барлық нұсқауларды және олардың параметрлерін мұқият тізбеудің орнына, біз осы бөлімде бірнеше мысалды қарастырамыз және олардан нұсқаулардың қалай жұмыс істейтіні және қалай жұмыс істейтіні белгілі болады. BPF үшін кез келген екілік файлды қолмен бөлшектеңіз. Материалды кейінірек мақалада біріктіру үшін біз Verifier, JIT компиляторы, классикалық BPF аудармасы, сондай-ақ карталарды зерттеу, функцияларды шақыру және т.б. туралы бөлімдерде жеке нұсқаулармен танысамыз.

Жеке нұсқаулар туралы айтатын болсақ, біз негізгі файлдарға сілтеме жасаймыз bpf.h и bpf_common.h, ол BPF нұсқауларының сандық кодтарын анықтайды. Архитектураны өз бетіңізше зерттегенде және/немесе екілік файлдарды талдау кезінде күрделілік ретімен сұрыпталған келесі көздерден семантиканы таба аласыз: Бейресми eBPF спецификациясы, BPF және XDP анықтамалық нұсқаулығы, нұсқаулар жинағы, Documentation/networking/filter.txt және, әрине, Linux бастапқы кодында - тексеруші, JIT, BPF интерпретаторы.

Мысал: сіздің басыңыздағы BPF бөлшектеу

Бағдарламаны құрастыратын мысалды қарастырайық readelf-example.c және алынған екілік жүйені қараңыз. Түпнұсқа мазмұнын ашамыз readelf-example.c төменде, оның логикасын екілік кодтардан қалпына келтіргеннен кейін:

$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Hex dump of section '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................

Шығарылымдағы бірінші баған readelf шегініс болып табылады және біздің бағдарлама осылайша төрт командадан тұрады:

Code Dst Src Off  Imm
b7   0   0   0000 01000000
15   0   1   0100 00000000
b7   0   0   0000 02000000
95   0   0   0000 00000000

Командалық кодтар тең b7, 15, b7 и 95. Еске салайық, ең аз маңызды үш бит нұсқау класы болып табылады. Біздің жағдайда барлық нұсқаулардың төртінші биті бос, сондықтан нұсқау кластары сәйкесінше 7, 5, 7, 5. 7-сынып BPF_ALU64, және 5 BPF_JMP. Екі сынып үшін де нұсқау пішімі бірдей (жоғарыдан қараңыз) және біз өз бағдарламамызды осылай қайта жаза аламыз (бір уақытта біз қалған бағандарды адам түрінде қайта жазамыз):

Op S  Class   Dst Src Off  Imm
b  0  ALU64   0   0   0    1
1  0  JMP     0   1   1    0
b  0  ALU64   0   0   0    2
9  0  JMP     0   0   0    0

Операция b сыныбы ALU64 Мүмкін BPF_MOV. Ол тағайындалған тізілімге мән тағайындайды. Егер бит орнатылған болса s (көз), онда мән бастапқы регистрден алынады, ал егер біздің жағдайымыздағыдай ол орнатылмаса, онда мән өрістен алынады. Imm. Сонымен, бірінші және үшінші нұсқауларда біз операцияны орындаймыз r0 = Imm. Әрі қарай, JMP 1 класс операциясы BPF_JEQ (тең болса секіру). Біздің жағдайда, биттен бері S нөлге тең, ол бастапқы регистрдің мәнін өріспен салыстырады Imm. Егер мәндер сәйкес келсе, онда ауысу орын алады PC + Offқайда PC, әдеттегідей келесі нұсқаудың мекенжайын қамтиды. Соңында, JMP 9-сынып операциясы BPF_EXIT. Бұл нұсқау ядроға оралып, бағдарламаны тоқтатады r0. Кестеге жаңа баған қосамыз:

Op    S  Class   Dst Src Off  Imm    Disassm
MOV   0  ALU64   0   0   0    1      r0 = 1
JEQ   0  JMP     0   1   1    0      if (r1 == 0) goto pc+1
MOV   0  ALU64   0   0   0    2      r0 = 2
EXIT  0  JMP     0   0   0    0      exit

Біз мұны ыңғайлырақ пішінде қайта жаза аламыз:

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

Реестрде не бар екенін еске түсірсек r1 программаға ядродан контекстке көрсеткіш беріледі және регистрде r0 мән ядроға қайтарылады, онда контекстке көрсеткіш нөлге тең болса, онда біз 1 қайтарамыз, ал әйтпесе - 2. Көзге қарап дұрыс екенімізді тексерейік:

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

Иә, бұл мағынасыз бағдарлама, бірақ ол тек төрт қарапайым нұсқауларға аударылады.

Ерекше мысал: 16 байт нұсқау

Кейбір нұсқаулардың 64 биттен көп орын алатынын жоғарыда айттық. Бұл, мысалы, нұсқауларға қатысты lddw (Код = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — өрістерден қос сөзді регистрге жүктеңіз Imm. Істің мәні мұнда Imm өлшемі 32, ал қос сөз 64 бит, сондықтан бір 64 биттік нұсқауда регистрге 64 биттік жедел мәнді жүктеу жұмыс істемейді. Ол үшін өрісте 64-биттік мәннің екінші бөлігін сақтау үшін екі көршілес нұсқаулар қолданылады Imm. Мысал:

$ cat x64.c
long foo(void *ctx)
{
        return 0x11223344aabbccdd;
}
$ clang -target bpf -c x64.c -o x64.o -O2
$ llvm-readelf -x .text x64.o
Hex dump of section '.text':
0x00000000 18000000 ddccbbaa 00000000 44332211 ............D3".
0x00000010 95000000 00000000                   ........

Екілік бағдарламада тек екі нұсқау бар:

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

Нұсқаулармен қайта кездесеміз lddw, біз көшіру және карталармен жұмыс туралы сөйлескенде.

Мысал: стандартты құралдарды пайдаланып BPF бөлшектеу

Сонымен, біз BPF екілік кодтарын оқуды үйрендік және қажет болған жағдайда кез келген нұсқауды талдауға дайынбыз. Дегенмен, іс жүзінде стандартты құралдарды пайдаланып бағдарламаларды бөлшектеу ыңғайлы және жылдамырақ екенін айту керек, мысалы:

$ llvm-objdump -d x64.o

Disassembly of section .text:

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

BPF объектілерінің өмірлік циклі, bpffs файлдық жүйесі

(Мен алдымен осы бөлімшеде сипатталған кейбір мәліметтерді білдім ораза Алексей Старовойтов BPF блогы.)

BPF объектілері – бағдарламалар мен карталар – командалар арқылы пайдаланушы кеңістігінен жасалады BPF_PROG_LOAD и BPF_MAP_CREATE жүйелік қоңырау bpf(2), мұның дәл қалай болатыны туралы келесі бөлімде айтатын боламыз. Бұл ядро ​​деректерінің құрылымдарын және олардың әрқайсысы үшін жасайды refcount (анықтама саны) бір мәнге орнатылады және нысанды көрсететін файл дескрипторы пайдаланушыға қайтарылады. Тұтқаны жабылғаннан кейін refcount объект біреуге азаяды, ал нөлге жеткенде объект жойылады.

Бағдарлама карталарды пайдаланса, онда refcount бұл карталар бағдарламаны жүктегеннен кейін бір есе артады, яғни. олардың файл дескрипторлары пайдаланушы процесінен жабылуы мүмкін және бәрібір refcount нөлге айналмайды:

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Бағдарламаны сәтті жүктегеннен кейін біз оны әдетте оқиға генераторының қандай да бір түріне тіркейміз. Мысалы, біз оны кіріс пакеттерін өңдеу немесе кейбіріне қосу үшін желілік интерфейске қоя аламыз tracepoint өзегінде. Осы кезде анықтамалық есептегіш те бір есе артады және біз жүктеуші бағдарламасындағы файл дескрипторын жаба аламыз.

Жүктегішті қазір өшірсек не болады? Бұл оқиға генераторының (ілмек) түріне байланысты. Барлық желілік ілгектер жүктеуші аяқталғаннан кейін болады, бұл жаһандық ілгектер деп аталады. Және, мысалы, бақылау бағдарламалары оларды жасаған процесс аяқталғаннан кейін шығарылады (сондықтан «жергілікті процесске» жергілікті деп аталады). Техникалық тұрғыдан, жергілікті ілгектер әрқашан пайдаланушы кеңістігінде сәйкес файл дескрипторына ие және сондықтан процесс жабылған кезде жабылады, бірақ жаһандық ілгектер жоқ. Төмендегі суретте қызыл кресттерді пайдалана отырып, мен жергілікті және ғаламдық ілгектер жағдайында жүктеуші бағдарламасының тоқтатылуы нысандардың қызмет ету мерзіміне қалай әсер ететінін көрсетуге тырысамын.

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

Неліктен жергілікті және жаһандық ілгектер арасында айырмашылық бар? Желілік бағдарламалардың кейбір түрлерін іске қосу пайдаланушы кеңістігісіз мағынасы бар, мысалы, DDoS қорғауын елестетіңіз - жүктеуші ережелерді жазады және BPF бағдарламасын желі интерфейсіне қосады, содан кейін жүктеуші барып, өзін өлтіруі мүмкін. Екінші жағынан, сіз он минут ішінде тізеңізге жазған отладтауды бақылау бағдарламасын елестетіп көріңіз - ол аяқталған кезде жүйеде қоқыс қалмағанын қалайсыз және жергілікті ілгектер мұны қамтамасыз етеді.

Екінші жағынан, ядродағы бақылау нүктесіне қосылғыңыз келетінін және көптеген жылдар бойы статистиканы жинағыңыз келетінін елестетіңіз. Бұл жағдайда пайдаланушы бөлігін аяқтап, мезгіл-мезгіл статистикаға оралғыңыз келеді. bpf файлдық жүйесі бұл мүмкіндікті береді. Бұл BPF нысандарына сілтеме жасайтын файлдарды жасауға және осылайша көбейтуге мүмкіндік беретін тек жадтағы псевдофайлдық жүйе. refcount нысандар. Осыдан кейін жүктеуші шыға алады және ол жасаған нысандар тірі қалады.

Кішкентайларға арналған BPF, бірінші бөлім: ұзартылған BPF

BPF нысандарына сілтеме жасайтын bpff файлдарында файлдарды жасау «тіреу» деп аталады (келесі сөз тіркесіндегідей: «процесс BPF бағдарламасын немесе картасын бекітеді»). BPF нысандары үшін файлдық нысандарды жасау тек жергілікті нысандардың қызмет ету мерзімін ұзарту үшін ғана емес, сонымен қатар жаһандық нысандардың ыңғайлылығы үшін де мағынасы бар - жаһандық DDoS қорғау бағдарламасымен мысалға оралсақ, біз келіп, статистиканы көргіміз келеді. кейде.

BPF файлдық жүйесі әдетте орнатылған /sys/fs/bpf, бірақ оны жергілікті түрде де орнатуға болады, мысалы, келесідей:

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

Файлдық жүйе атаулары пәрмен арқылы жасалады BPF_OBJ_PIN BPF жүйесін шақыру. Суреттеу үшін бағдарламаны алайық, оны құрастырайық, жүктеп алайық және оны бекітейік bpffs. Біздің бағдарлама пайдалы ештеңе жасамайды, біз мысалды қайта шығару үшін кодты ғана ұсынып отырмыз:

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

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

Осы бағдарламаны құрастырып, файлдық жүйенің жергілікті көшірмесін жасайық bpffs:

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

Енді утилитаны пайдаланып бағдарламамызды жүктеп алайық bpftool және ілеспе жүйелік қоңырауларды қараңыз bpf(2) (кейбір сәйкес емес жолдар страце шығысынан жойылды):

$ sudo strace -e bpf bpftool prog load ./test.o bpf-mountpoint/test
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="test", ...}, 120) = 3
bpf(BPF_OBJ_PIN, {pathname="bpf-mountpoint/test", bpf_fd=3}, 120) = 0

Мұнда біз бағдарламаны пайдаланып жүктедік BPF_PROG_LOAD, ядродан файл дескрипторын алды 3 және пәрменді пайдалану BPF_OBJ_PIN бұл файл дескрипторын файл ретінде бекітті "bpf-mountpoint/test". Осыдан кейін жүктеуші бағдарламасы bpftool жұмысын аяқтады, бірақ біздің бағдарлама ядрода қалды, бірақ біз оны ешқандай желілік интерфейске тіркемедік:

$ sudo bpftool prog | tail -3
783: xdp  name test  tag 5c8ba0cf164cb46c  gpl
        loaded_at 2020-05-05T13:27:08+0000  uid 0
        xlated 24B  jited 41B  memlock 4096B

Біз файлдық нысанды қалыпты түрде жоя аламыз unlink(2) содан кейін сәйкес бағдарлама жойылады:

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

Объектілерді жою

Нысандарды жою туралы айтатын болсақ, бағдарламаны ілмектен (оқиға генераторы) ажыратқаннан кейін бірде-бір жаңа оқиға оның іске қосылуын тудырмайтынын түсіндіру керек, бірақ бағдарламаның барлық ағымдағы даналары қалыпты тәртіпте аяқталады. .

BPF бағдарламаларының кейбір түрлері бағдарламаны жылдам ауыстыруға мүмкіндік береді, яғни. реттілік атомдылығын қамтамасыз етеді replace = detach old program, attach new program. Бұл жағдайда бағдарламаның ескі нұсқасының барлық белсенді даналары өз жұмысын аяқтайды және жаңа бағдарламадан жаңа оқиғалар өңдегіштері жасалады және мұндағы «атомдық» бірде-бір оқиға өткізілмейтінін білдіреді.

Оқиға көздеріне бағдарламаларды тіркеу

Бұл мақалада біз бағдарламаларды оқиға көздеріне қосуды бөлек сипаттамаймыз, өйткені оны бағдарламаның белгілі бір түрі контекстінде зерттеу мағынасы бар. См. мысал төменде, біз XDP сияқты бағдарламалардың қалай қосылғанын көрсетеміз.

bpf жүйелік шақыруын пайдалану арқылы нысандарды басқару

BPF бағдарламалары

Барлық BPF нысандары жүйелік шақыру арқылы пайдаланушы кеңістігінен жасалады және басқарылады bpf, келесі прототипі бар:

#include <linux/bpf.h>

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

Міне, команда cmd тип мәндерінің бірі болып табылады enum bpf_cmd, attr — белгілі бір бағдарламаның параметрлеріне көрсеткіш және size — көрсеткішке сәйкес нысан өлшемі, яғни. әдетте бұл sizeof(*attr). 5.8 ядросында жүйелік шақыру bpf 34 түрлі пәрмендерді қолдайды және анықтау union bpf_attr 200 жолды алады. Бірақ бұл бізді қорқытпау керек, өйткені біз бірнеше мақалалар барысында командалар мен параметрлермен танысатын боламыз.

Командадан бастайық BPF_PROG_LOAD, ол BPF бағдарламаларын жасайды - BPF нұсқауларының жиынтығын алады және оны ядроға жүктейді. Жүктеу сәтінде тексеруші іске қосылады, содан кейін JIT компиляторы және сәтті орындалған соң бағдарлама файлының дескрипторы пайдаланушыға қайтарылады. Онымен не болғанын біз алдыңғы бөлімде көрдік BPF объектілерінің өмірлік циклі туралы.

Біз енді қарапайым BPF бағдарламасын жүктейтін теңшелетін бағдарламаны жазамыз, бірақ алдымен қандай бағдарламаны жүктегіміз келетінін шешуіміз керек - таңдау керек. Түрі және осы типтің шеңберінде тексеруші тесттен өтетін бағдарлама жазыңыз. Дегенмен, процесті қиындатпау үшін, мұнда дайын шешім: біз сияқты бағдарламаны аламыз BPF_PROG_TYPE_XDP, ол мәнді қайтарады XDP_PASS (барлық пакеттерді өткізіп жіберіңіз). BPF ассемблерінде бұл өте қарапайым көрінеді:

r0 = 2
exit

Біз шешкеннен кейін сол жүктеп саламыз, мұны қалай жасайтынымызды айта аламыз:

#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

static inline __u64 ptr_to_u64(const void *ptr)
{
        return (__u64) (unsigned long) ptr;
}

int main(void)
{
    struct bpf_insn insns[] = {
        {
            .code = BPF_ALU64 | BPF_MOV | BPF_K,
            .dst_reg = BPF_REG_0,
            .imm = XDP_PASS
        },
        {
            .code = BPF_JMP | BPF_EXIT
        },
    };

    union bpf_attr attr = {
        .prog_type = BPF_PROG_TYPE_XDP,
        .insns     = ptr_to_u64(insns),
        .insn_cnt  = sizeof(insns)/sizeof(insns[0]),
        .license   = ptr_to_u64("GPL"),
    };

    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

Бағдарламадағы қызықты оқиғалар массивтің анықтамасынан басталады insns - машиналық кодтағы біздің BPF бағдарламамыз. Бұл жағдайда BPF бағдарламасының әрбір нұсқауы құрылымға оралады bpf_insn. Бірінші элемент insns нұсқауларға сәйкес келеді r0 = 2, екінші - exit.

Шегіну. Ядро машина кодтарын жазу және ядро ​​тақырыбы файлын пайдалану үшін ыңғайлырақ макростарды анықтайды tools/include/linux/filter.h жаза алар едік

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

Бірақ BPF бағдарламаларын жергілікті кодта жазу тек ядродағы сынақтар мен BPF туралы мақалаларды жазу үшін қажет болғандықтан, бұл макростардың болмауы әзірлеушінің өмірін қиындатпайды.

BPF бағдарламасын анықтағаннан кейін оны ядроға жүктеуге көшеміз. Біздің минималистік параметрлер жиынтығы attr бағдарлама түрін, нұсқаулар жиынтығы мен санын, қажетті лицензияны және атауын қамтиды "woo", оны жүктеп алғаннан кейін жүйеде бағдарламамызды табу үшін қолданамыз. Бағдарлама, уәде етілгендей, жүйеге қоңырау шалу арқылы жүктеледі bpf.

Бағдарламаның соңында біз пайдалы жүктемені имитациялайтын шексіз циклмен аяқталамыз. Онсыз, жүйелік шақыру бізге қайтарылған файл дескрипторы жабылған кезде бағдарлама ядромен жойылады. bpf, және біз оны жүйеде көрмейміз.

Ал, біз сынаққа дайынбыз. Төменде бағдарламаны жинап, іске қосайық straceбәрі дұрыс жұмыс істеп тұрғанын тексеру үшін:

$ clang -g -O2 simple-prog.c -o simple-prog

$ sudo strace ./simple-prog
execve("./simple-prog", ["./simple-prog"], 0x7ffc7b553480 /* 13 vars */) = 0
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0x7ffe03c4ed50, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(

Бәрі жақсы, bpf(2) бізге 3 тұтқасын қайтарды және біз шексіз циклге кірдік pause(). Жүйеде бағдарламамызды табуға тырысайық. Мұны істеу үшін біз басқа терминалға өтіп, қызметтік бағдарламаны қолданамыз bpftool:

# bpftool prog | grep -A3 woo
390: xdp  name woo  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-31T24:66:44+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        pids simple-prog(10381)

Жүйеде жүктелген бағдарлама бар екенін көреміз woo жаһандық идентификаторы 390 және қазір орындалуда simple-prog бағдарламаға нұсқайтын ашық файл дескрипторы бар (және егер simple-prog содан кейін жұмысты аяқтайды woo жоғалады). Күткендей, бағдарлама woo BPF архитектурасында екілік кодтардың 16 байт - екі нұсқаулығын алады, бірақ оның түпнұсқа түрінде (x86_64) ол қазірдің өзінде 40 байт. Бағдарламамызды бастапқы түрінде қарастырайық:

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

тосынсыйлар жоқ. Енді JIT компиляторы жасаған кодты қарастырайық:

# bpftool prog dump jited id 390
bpf_prog_3b185187f1855c4c_woo:
   0:   nopl   0x0(%rax,%rax,1)
   5:   push   %rbp
   6:   mov    %rsp,%rbp
   9:   sub    $0x0,%rsp
  10:   push   %rbx
  11:   push   %r13
  13:   push   %r14
  15:   push   %r15
  17:   pushq  $0x0
  19:   mov    $0x2,%eax
  1e:   pop    %rbx
  1f:   pop    %r15
  21:   pop    %r14
  23:   pop    %r13
  25:   pop    %rbx
  26:   leaveq
  27:   retq

үшін өте тиімді емес exit(2), бірақ әділдік үшін, біздің бағдарламамыз тым қарапайым және тривиальды емес бағдарламалар үшін JIT компиляторы қосқан пролог пен эпилог, әрине, қажет.

Карталар

BPF бағдарламалары басқа BPF бағдарламаларына да, пайдаланушы кеңістігіндегі бағдарламаларға да қол жетімді құрылымдық жад аймақтарын пайдалана алады. Бұл нысандар карталар деп аталады және бұл бөлімде жүйелік шақыру арқылы оларды қалай басқару керектігін көрсетеміз bpf.

Бірден айта кетейік, карталардың мүмкіндіктері тек ортақ жадқа қол жеткізумен шектелмейді. Арнайы мақсаттағы карталар бар, мысалы, BPF бағдарламаларына көрсеткіштер немесе желілік интерфейстерге көрсеткіштер, перф оқиғаларымен жұмыс істеу карталары және т.б. Оқырманды шатастырмау үшін біз бұл жерде олар туралы айтпаймыз. Бұдан басқа, біз синхрондау мәселелерін елемейміз, өйткені бұл біздің мысалдарымыз үшін маңызды емес. Қолжетімді карта түрлерінің толық тізімін мына жерден табуға болады <linux/bpf.h>, және бұл бөлімде мысал ретінде тарихи бірінші типті хэш кестесін аламыз BPF_MAP_TYPE_HASH.

Егер сіз C++ тілінде хэш кестесін жасасаңыз, айтасыз unordered_map<int,long> woo, бұл орыс тілінде «Маған үстел керек woo шектеусіз өлшем, оның кілттері типті int, ал мәндер түрі болып табылады long" BPF хэш кестесін жасау үшін біз кестенің максималды өлшемін көрсетуіміз керек, ал кілттер мен мәндердің түрлерін көрсетудің орнына олардың өлшемдерін байтпен көрсетуіміз керек болса, дәл солай істеуіміз керек. . Карталарды жасау үшін пәрменді пайдаланыңыз BPF_MAP_CREATE жүйелік қоңырау bpf. Картаны жасайтын азды-көпті минималды бағдарламаны қарастырайық. BPF бағдарламаларын жүктейтін алдыңғы бағдарламадан кейін бұл сізге қарапайым болып көрінуі керек:

$ cat simple-map.c
#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>

int main(void)
{
    union bpf_attr attr = {
        .map_type = BPF_MAP_TYPE_HASH,
        .key_size = sizeof(int),
        .value_size = sizeof(int),
        .max_entries = 4,
    };
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));

    for ( ;; )
        pause();
}

Мұнда біз параметрлер жинағын анықтаймыз attr, онда біз «Маған кілттері мен өлшем мәндері бар хэш кестесі қажет sizeof(int), онда мен ең көбі төрт элементті қоя аламын». BPF карталарын жасау кезінде сіз басқа параметрлерді көрсете аласыз, мысалы, бағдарламамен мысалдағыдай, біз нысанның атын келесідей көрсеттік. "woo".

Бағдарламаны құрастырып, іске қосайық:

$ clang -g -O2 simple-map.c -o simple-map
$ sudo strace ./simple-map
execve("./simple-map", ["./simple-map"], 0x7ffd40a27070 /* 14 vars */) = 0
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=4, max_entries=4, map_name="woo", ...}, 72) = 3
pause(

Міне, жүйелік қоңырау bpf(2) бізге дескриптор картасының нөмірін қайтарды 3 содан кейін бағдарлама, күткендей, жүйелік қоңырауда қосымша нұсқауларды күтеді pause(2).

Енді бағдарламамызды фондық режимге жіберейік немесе басқа терминалды ашып, қызметтік бағдарлама арқылы объектімізді қарастырайық bpftool (біздің картаны басқалардан оның аты бойынша ажырата аламыз):

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

114 саны біздің нысанның ғаламдық идентификаторы болып табылады. Жүйедегі кез келген бағдарлама осы идентификаторды пәрмен арқылы бар картаны ашу үшін пайдалана алады BPF_MAP_GET_FD_BY_ID жүйелік қоңырау bpf.

Енді біз хэш кестемізбен ойнай аламыз. Оның мазмұнын қарастырайық:

$ sudo bpftool map dump id 114
Found 0 elements

Бос. Оған мән берейік hash[1] = 1:

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

Кестеге тағы да назар аударайық:

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

Ура! Біз бір элементті қоса алдық. Мұны істеу үшін байт деңгейінде жұмыс істеу керек екенін ескеріңіз, өйткені bptftool хэш кестесіндегі мәндердің қандай типті екенін білмейді. (Бұл білімді оған BTF арқылы беруге болады, бірақ қазір бұл туралы көбірек.)

bpftool қалай дәл оқиды және элементтерді қосады? Сорғыштың астына қарайық:

$ sudo strace -e bpf bpftool map dump id 114
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=NULL, next_key=0x55856ab65280}, 120) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=3, key=0x55856ab65280, value=0x55856ab652a0}, 120) = 0
key: 01 00 00 00  value: 01 00 00 00
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=0x55856ab65280, next_key=0x55856ab65280}, 120) = -1 ENOENT

Алдымен пәрмен арқылы картаны оның ғаламдық идентификаторы бойынша аштық BPF_MAP_GET_FD_BY_ID и bpf(2) бізге 3 дескрипторды қайтарды.Одан әрі пәрменді қолдану BPF_MAP_GET_NEXT_KEY өту арқылы кестедегі бірінші кілтті таптық NULL «алдыңғы» кілтке көрсеткіш ретінде. Егер бізде кілт болса, біз істей аламыз BPF_MAP_LOOKUP_ELEMол көрсеткішке мәнді қайтарады value. Келесі қадам - ​​ағымдағы кілтке көрсеткішті беру арқылы келесі элементті табуға тырысамыз, бірақ біздің кестеде тек бір элемент пен пәрмен бар. BPF_MAP_GET_NEXT_KEY қайтарады ENOENT.

Жарайды, мәнді 1 перне арқылы өзгертейік, бизнес логика тіркелуді қажет етеді делік hash[1] = 2:

$ sudo strace -e bpf bpftool map update id 114 key 1 0 0 0 value 2 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x55dcd72be260, value=0x55dcd72be280, flags=BPF_ANY}, 120) = 0

Күткендей, бұл өте қарапайым: пәрмен BPF_MAP_GET_FD_BY_ID ID және пәрмен арқылы картамызды ашады BPF_MAP_UPDATE_ELEM элементті қайта жазады.

Сонымен, бір бағдарламадан хэш кестесін жасағаннан кейін оның мазмұнын екіншісінен оқып, жаза аламыз. Назар аударыңыз, егер біз мұны пәрмен жолынан жасай алсақ, жүйедегі кез келген басқа бағдарлама мұны істей алады. Пайдаланушы кеңістігіндегі карталармен жұмыс істеу үшін жоғарыда сипатталған пәрмендерге қосымша, төменде келтірілген:

  • BPF_MAP_LOOKUP_ELEM: кілт бойынша мәнді табыңыз
  • BPF_MAP_UPDATE_ELEM: жаңарту/мән жасау
  • BPF_MAP_DELETE_ELEM: кілтті алып тастаңыз
  • BPF_MAP_GET_NEXT_KEY: келесі (немесе бірінші) пернені табыңыз
  • BPF_MAP_GET_NEXT_ID: барлық бар карталарды қарап шығуға мүмкіндік береді, ол осылай жұмыс істейді bpftool map
  • BPF_MAP_GET_FD_BY_ID: бар картаны оның ғаламдық идентификаторы бойынша ашыңыз
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM: нысанның мәнін атомдық түрде жаңартып, ескісін қайтарыңыз
  • BPF_MAP_FREEZE: пайдаланушы кеңістігінен картаны өзгермейтін ету (бұл әрекетті қайтару мүмкін емес)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: жаппай операциялар. Мысалы, BPF_MAP_LOOKUP_AND_DELETE_BATCH - бұл картадан барлық мәндерді оқудың және қалпына келтірудің жалғыз сенімді жолы

Бұл пәрмендердің барлығы барлық карта түрлері үшін жұмыс істемейді, бірақ жалпы пайдаланушы кеңістігінен карталардың басқа түрлерімен жұмыс істеу хэш кестелерімен жұмыс істеумен бірдей көрінеді.

Тапсырыс үшін хэш-кесте тәжірибелерімізді аяқтайық. Есіңізде болсын, біз төрт кілтті қамтитын кестені жасағанымызды есте сақтаңыз ба? Тағы бірнеше элементтерді қосамыз:

$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0

Барлығы ойдығыдай:

$ sudo bpftool map dump id 114
key: 01 00 00 00  value: 01 00 00 00
key: 02 00 00 00  value: 01 00 00 00
key: 04 00 00 00  value: 01 00 00 00
key: 03 00 00 00  value: 01 00 00 00
Found 4 elements

Тағы біреуін қосуға тырысайық:

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

Күткеніміздей, біз сәтті болмадық. Қатені толығырақ қарастырайық:

$ sudo strace -e bpf bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=80, info=0x7ffe6c626da0}}, 120) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x56049ded5260, value=0x56049ded5280, flags=BPF_ANY}, 120) = -1 E2BIG (Argument list too long)
Error: update failed: Argument list too long
+++ exited with 255 +++

Барлығы жақсы: команда күткендей BPF_MAP_UPDATE_ELEM жаңа, бесінші, кілт жасауға тырысады, бірақ бұзылады E2BIG.

Осылайша, біз BPF бағдарламаларын жасай және жүктей аламыз, сонымен қатар пайдаланушы кеңістігінен карталарды жасай және басқара аламыз. Енді BPF бағдарламаларының карталарын қалай пайдалануға болатынын қарастыру қисынды. Біз бұл туралы машина макрокодтарындағы оқу қиын бағдарламалар тілінде айта аламыз, бірақ шын мәнінде BPF бағдарламаларының қалай жазылатынын және қалай жұмыс істейтінін көрсететін уақыт келді - пайдалану libbpf.

(Төмен деңгейлі мысалдың жоқтығына қанағаттанбайтын оқырмандар үшін: біз карталарды пайдаланатын бағдарламаларды және көмегімен жасалған көмекші функцияларды егжей-тегжейлі талдаймыз. libbpf және нұсқау деңгейінде не болатынын айтыңыз. Қанағаттанбаған оқырмандар үшін өте күшті, қостық мысал мақаланың тиісті орнында.)

libbpf көмегімен BPF бағдарламаларын жазу

Машиналық кодтарды пайдаланып BPF бағдарламаларын жазу бірінші рет қызықты болуы мүмкін, содан кейін қанықтылық басталады. Осы сәтте сіз назарыңызды аударуыңыз керек llvm, оның BPF архитектурасына арналған кодты генерациялауға арналған сервері және кітапханасы бар libbpf, ол BPF қолданбаларының пайдаланушы жағын жазуға және көмегімен жасалған BPF бағдарламаларының кодын жүктеуге мүмкіндік береді. llvm/clang.

Шын мәнінде, біз осы және кейінгі мақалаларда көретініміздей, libbpf онсыз өте көп жұмыс істейді (немесе ұқсас құралдар - iproute2, libbcc, libbpf-goжәне т.б.) өмір сүру мүмкін емес. Жобаның киллер ерекшеліктерінің бірі libbpf бұл BPF CO-RE (Бір рет құрастыру, барлық жерде іске қосу) – әртүрлі API интерфейстерінде жұмыс істеу мүмкіндігі бар (мысалы, ядро ​​құрылымы нұсқадан өзгерген кезде) бір ядродан екіншісіне тасымалданатын BPF бағдарламаларын жазуға мүмкіндік беретін жоба. нұсқасына). CO-RE-мен жұмыс істеу үшін сіздің ядроңыз BTF қолдауымен құрастырылуы керек (біз мұны қалай істеу керектігін бөлімде сипаттаймыз. Даму құралдары. Сіз ядроңыздың BTF көмегімен жасалғанын немесе өте қарапайым емес екенін келесі файлдың болуы арқылы тексере аласыз:

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

Бұл файл ядрода пайдаланылатын барлық деректер түрлері туралы ақпаратты сақтайды және біздің қолданатын барлық мысалдарымызда қолданылады libbpf. Біз CO-RE туралы келесі мақалада егжей-тегжейлі айтатын боламыз, бірақ бұл мақалада - өзіңізге ядро ​​​​құрыңыз. CONFIG_DEBUG_INFO_BTF.

Кітапхана libbpf каталогта тұрады tools/lib/bpf ядро және оның дамуы пошталық тізім арқылы жүзеге асырылады [email protected]. Дегенмен, ядродан тыс тұратын қолданбалардың қажеттіліктері үшін бөлек репозиторий сақталады https://github.com/libbpf/libbpf онда ядро ​​кітапханасы азды-көпті оқуға қол жеткізу үшін шағылыстырылған.

Бұл бөлімде біз пайдаланатын жобаны қалай жасауға болатынын қарастырамыз libbpf, бірнеше (көп немесе аз мағынасыз) сынақ бағдарламаларын жазып, оның барлығы қалай жұмыс істейтінін егжей-тегжейлі талдап көрейік. Бұл келесі бөлімдерде BPF бағдарламаларының карталармен, ядро ​​көмекшілерімен, BTF және т.

Әдетте жобаларды пайдаланады libbpf GitHub репозиторийін git ішкі модулі ретінде қосыңыз, біз де солай істейміз:

$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Initialized empty Git repository in /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Cloning into '/tmp/libbpf-example/libbpf'...
remote: Enumerating objects: 200, done.
remote: Counting objects: 100% (200/200), done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 3354 (delta 101), reused 118 (delta 79), pack-reused 3154
Receiving objects: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, done.
Resolving deltas: 100% (2176/2176), done.

Барады libbpf өте оңай:

$ cd libbpf/src
$ mkdir build
$ OBJDIR=build DESTDIR=root make -s install
$ find root
root
root/usr
root/usr/include
root/usr/include/bpf
root/usr/include/bpf/bpf_tracing.h
root/usr/include/bpf/xsk.h
root/usr/include/bpf/libbpf_common.h
root/usr/include/bpf/bpf_endian.h
root/usr/include/bpf/bpf_helpers.h
root/usr/include/bpf/btf.h
root/usr/include/bpf/bpf_helper_defs.h
root/usr/include/bpf/bpf.h
root/usr/include/bpf/libbpf_util.h
root/usr/include/bpf/libbpf.h
root/usr/include/bpf/bpf_core_read.h
root/usr/lib64
root/usr/lib64/libbpf.so.0.1.0
root/usr/lib64/libbpf.so.0
root/usr/lib64/libbpf.a
root/usr/lib64/libbpf.so
root/usr/lib64/pkgconfig
root/usr/lib64/pkgconfig/libbpf.pc

Бұл бөлімдегі келесі жоспарымыз келесідей: біз BPF бағдарламасын жазамыз BPF_PROG_TYPE_XDP, алдыңғы мысалдағыдай, бірақ C тілінде біз оны пайдалана отырып құрастырамыз clang, және оны ядроға жүктейтін көмекші бағдарламаны жазыңыз. Келесі бөлімдерде біз BPF бағдарламасының да, көмекші бағдарламасының да мүмкіндіктерін кеңейтеміз.

Мысал: libbpf көмегімен толыққанды қосымшаны жасау

Бастау үшін біз файлды қолданамыз /sys/kernel/btf/vmlinux, жоғарыда аталған және оның баламасын тақырып файлы түрінде жасаңыз:

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

Бұл файл ядрода қол жетімді барлық деректер құрылымдарын сақтайды, мысалы, ядрода IPv4 тақырыбы осылай анықталады:

$ grep -A 12 'struct iphdr {' vmlinux.h
struct iphdr {
    __u8 ihl: 4;
    __u8 version: 4;
    __u8 tos;
    __be16 tot_len;
    __be16 id;
    __be16 frag_off;
    __u8 ttl;
    __u8 protocol;
    __sum16 check;
    __be32 saddr;
    __be32 daddr;
};

Енді біз BPF бағдарламамызды C тілінде жазамыз:

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

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

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

Бағдарламамыз өте қарапайым болып шыққанымен, біз әлі де көптеген бөлшектерге назар аударуымыз керек. Біріншіден, біз енгізетін бірінші тақырып файлы vmlinux.h, біз оны жаңа ғана пайдаланып шығардық bpftool btf dump - енді ядро ​​құрылымдарының қандай екенін білу үшін ядро-тақырыптар бумасын орнатудың қажеті жоқ. Келесі тақырып файлы бізге кітапханадан келеді libbpf. Енді ол макросты анықтау үшін ғана қажет SEC, ол таңбаны ELF нысан файлының сәйкес бөліміне жібереді. Біздің бағдарлама бөлімде қамтылған xdp/simple, мұнда қиғаш сызықтың алдында BPF программа түрін анықтаймыз - бұл қолданылатын шарт libbpf, бөлім атауына негізделген ол іске қосу кезінде дұрыс түрді ауыстырады bpf(2). BPF бағдарламасының өзі C - өте қарапайым және бір жолдан тұрады return XDP_PASS. Соңында, бөлек бөлім "license" лицензияның атауын қамтиды.

Біз бағдарламамызды llvm/clang, >= 10.0.0 нұсқасы немесе одан да жақсырақ (бөлімді қараңыз) арқылы құрастыра аламыз. Даму құралдары):

$ clang --version
clang version 11.0.0 (https://github.com/llvm/llvm-project.git afc287e0abec710398465ee1f86237513f2b5091)
...

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o

Қызықты ерекшеліктер арасында: біз мақсатты архитектураны көрсетеміз -target bpf және тақырыптарға жол libbpf, біз жақында орнатқан. Сондай-ақ, бұл туралы ұмытпаңыз -O2, бұл опциясыз сіз болашақта тосын сыйларға тап болуыңыз мүмкін. Біздің кодты қарастырайық, біз қалаған бағдарламаны жаза алдық па?

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

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       r0 = 2
       1:       exit

Иә, жұмыс істеді! Енді бізде бағдарлама бар екілік файл бар және біз оны ядроға жүктейтін қолданба жасағымыз келеді. Осы мақсатта кітапхана libbpf бізге екі опцияны ұсынады - төменгі деңгейлі API немесе жоғары деңгейлі API пайдаланыңыз. Біз екінші жолмен жүреміз, өйткені біз BPF бағдарламаларын жазуды, жүктеуді және оларды кейінгі оқу үшін аз күш жұмсауды үйренгіміз келеді.

Біріншіден, біз сол утилитаны пайдаланып, оның екілік жүйесінен бағдарламамыздың «қаңқасын» жасауымыз керек bpftool — BPF әлемінің швейцариялық пышағы (оны сөзбе-сөз қабылдауға болады, өйткені BPF құрушылардың және қолдаушылардың бірі Даниэль Боркман швейцариялық):

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

Файлда xdp-simple.skel.h біздің бағдарламамыздың екілік кодын және басқару функцияларын қамтиды - жүктеу, тіркеу, объектімізді жою. Біздің қарапайым жағдайда бұл шамадан тыс көрінеді, бірақ ол сонымен қатар объектілік файлда көптеген BPF бағдарламалары мен карталары болған жағдайда жұмыс істейді және осы алып ELF жүктеу үшін бізге тек қаңқаны жасап, пайдаланушы қолданбасынан бір немесе екі функцияны шақыру керек. жазып жатыр Енді әрі қарай жүрейік.

Қатаң айтқанда, біздің жүктеуші бағдарламамыз тривиальды:

#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    pause();

    xdp_simple_bpf__destroy(obj);
}

Бұл struct xdp_simple_bpf файлда анықталған xdp-simple.skel.h және біздің нысан файлымызды сипаттайды:

struct xdp_simple_bpf {
    struct bpf_object_skeleton *skeleton;
    struct bpf_object *obj;
    struct {
        struct bpf_program *simple;
    } progs;
    struct {
        struct bpf_link *simple;
    } links;
};

Төмен деңгейлі API іздерін мына жерден көре аламыз: құрылым struct bpf_program *simple и struct bpf_link *simple. Бірінші құрылым бөлімде жазылған бағдарламамызды арнайы сипаттайды xdp/simple, ал екіншісі бағдарламаның оқиға көзіне қосылу жолын сипаттайды.

функция xdp_simple_bpf__open_and_load, ELF объектісін ашады, оны талдайды, барлық құрылымдар мен ішкі құрылымдарды жасайды (бағдарламадан басқа ELF-де басқа бөлімдер де бар - деректер, тек оқуға арналған деректер, жөндеу ақпараты, лицензия және т.б.), содан кейін жүйені пайдаланып ядроға жүктейді. қоңырау шалу bpf, біз оны бағдарламаны құрастыру және іске қосу арқылы тексере аламыз:

$ clang -O2 -I ./libbpf/src/root/usr/include/ xdp-simple.c -o xdp-simple ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz

$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_BTF_LOAD, 0x7ffdb8fd9670, 120)  = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0xdfd580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 8, 0), prog_flags=0, prog_name="simple", prog_ifindex=0, expected_attach_type=0x25 /* BPF_??? */, ...}, 120) = 4

Енді қолданатын бағдарламамызды қарастырайық bpftool. Оның жеке куәлігін табайық:

# bpftool p | grep -A4 simple
463: xdp  name simple  tag 3b185187f1855c4c  gpl
        loaded_at 2020-08-01T01:59:49+0000  uid 0
        xlated 16B  jited 40B  memlock 4096B
        btf_id 185
        pids xdp-simple(16498)

және демп (біз команданың қысқартылған түрін қолданамыз bpftool prog dump xlated):

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

Жаңа нәрсе! Бағдарлама біздің C бастапқы файлымыздың бөліктерін басып шығарды.Мұны кітапхана жасады libbpf, ол екілік жүйеде жөндеу бөлімін тапты, оны BTF нысанына құрастырды, оны пайдаланып ядроға жүктеді. BPF_BTF_LOAD, содан кейін пәрменмен бағдарламаны жүктеген кезде алынған файл дескрипторын көрсетеді BPG_PROG_LOAD.

Ядро көмекшілері

BPF бағдарламалары «сыртқы» функцияларды – ядро ​​көмекшілерін іске қоса алады. Бұл көмекші функциялар BPF бағдарламаларына ядро ​​құрылымдарына қол жеткізуге, карталарды басқаруға, сондай-ақ «нақты әлеммен» байланысуға мүмкіндік береді - тамаша оқиғаларды жасауға, аппараттық құралдарды басқаруға (мысалы, пакеттерді қайта бағыттау) және т.б.

Мысал: bpf_get_smp_processor_id

«Үлгі бойынша оқыту» парадигмасы аясында көмекші функциялардың бірін қарастырайық, bpf_get_smp_processor_id(), белгілі файлда kernel/bpf/helpers.c. Ол оны шақырған BPF бағдарламасы жұмыс істеп тұрған процессордың нөмірін қайтарады. Бірақ оның семантикасы бізді қызықтырмайды, өйткені оның орындалуы бір жолды алады:

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF көмекшісі функциясының анықтамалары Linux жүйесінің қоңырау анықтамаларына ұқсас. Мұнда, мысалы, аргументтері жоқ функция анықталған. (Айталық, үш аргумент алатын функция макрос арқылы анықталады BPF_CALL_3. Аргументтердің максималды саны – бес.) Дегенмен, бұл анықтаманың бірінші бөлігі ғана. Екінші бөлім тип құрылымын анықтау болып табылады struct bpf_func_proto, онда тексеруші түсінетін көмекші функцияның сипаттамасы бар:

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

Көмекші функцияларын тіркеу

Белгілі бір түрдегі BPF бағдарламалары бұл функцияны пайдалануы үшін оны, мысалы, түрі үшін тіркеуі керек BPF_PROG_TYPE_XDP функция ядрода анықталған xdp_func_proto, ол көмекші функция идентификаторынан XDP бұл функцияны қолдайтынын немесе қолдамайтынын анықтайды. Біздің функциямыз қолдайды:

static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
    switch (func_id) {
    ...
    case BPF_FUNC_get_smp_processor_id:
        return &bpf_get_smp_processor_id_proto;
    ...
    }
}

Жаңа BPF бағдарламасының түрлері файлда "анықталған" include/linux/bpf_types.h макросты пайдалану BPF_PROG_TYPE. Тырнақшаларда анықталған, себебі бұл логикалық анықтама, ал Си тілінің терминдерінде нақты құрылымдардың тұтас жиынтығының анықтамасы басқа жерлерде кездеседі. Атап айтқанда, файлда kernel/bpf/verifier.c файлдағы барлық анықтамалар bpf_types.h құрылымдар массивін жасау үшін қолданылады bpf_verifier_ops[]:

static const struct bpf_verifier_ops *const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) 
    [_id] = & _name ## _verifier_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
};

Яғни, BPF бағдарламасының әрбір түрі үшін типтің деректер құрылымына көрсеткіш анықталады struct bpf_verifier_ops, ол мәнмен инициализацияланған _name ## _verifier_ops, яғни, xdp_verifier_ops үшін xdp. Құрылым xdp_verifier_ops анықтаған файлда net/core/filter.c келесідей:

const struct bpf_verifier_ops xdp_verifier_ops = {
    .get_func_proto     = xdp_func_proto,
    .is_valid_access    = xdp_is_valid_access,
    .convert_ctx_access = xdp_convert_ctx_access,
    .gen_prologue       = bpf_noop_prologue,
};

Мұнда біз таныс функциямызды көреміз xdp_func_proto, ол тексерушіні қиындыққа тап болған сайын іске қосады кейбіреулері BPF бағдарламасындағы функцияларды қараңыз verifier.c.

Гипотетикалық BPF бағдарламасы функцияны қалай пайдаланатынын қарастырайық bpf_get_smp_processor_id. Ол үшін біз алдыңғы бөлімдегі бағдарламаны келесідей қайта жазамыз:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

SEC("xdp/simple")
int simple(void *ctx)
{
    if (bpf_get_smp_processor_id() != 0)
        return XDP_DROP;
    return XDP_PASS;
}

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

Таңба bpf_get_smp_processor_id анықтаған в <bpf/bpf_helper_defs.h> кітапханалар libbpf қалай

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

яғни, bpf_get_smp_processor_id мәні 8 болатын функция көрсеткіші, мұндағы 8 - мән BPF_FUNC_get_smp_processor_id түрі enum bpf_fun_id, ол біз үшін файлда анықталған vmlinux.h (файл bpf_helper_defs.h ядрода сценарий арқылы жасалады, сондықтан «сиқырлы» сандар жақсы). Бұл функция аргументтерді қабылдамайды және түрдің мәнін қайтарады __u32. Біз оны бағдарламамызда іске қосқан кезде, clang нұсқауды қалыптастырады BPF_CALL «дұрыс түрі» Бағдарламаны құрастырып, бөлімді қарастырайық xdp/simple:

$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ llvm-objdump -D --section=xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       bf 01 00 00 00 00 00 00 r1 = r0
       2:       67 01 00 00 20 00 00 00 r1 <<= 32
       3:       77 01 00 00 20 00 00 00 r1 >>= 32
       4:       b7 00 00 00 02 00 00 00 r0 = 2
       5:       15 01 01 00 00 00 00 00 if r1 == 0 goto +1 <LBB0_2>
       6:       b7 00 00 00 01 00 00 00 r0 = 1

0000000000000038 <LBB0_2>:
       7:       95 00 00 00 00 00 00 00 exit

Бірінші жолда біз нұсқауларды көреміз call, параметр IMM ол 8-ге тең және SRC_REG - нөл. Тексеруші пайдаланатын ABI келісіміне сәйкес, бұл сегізінші көмекші функциясына шақыру. Ол іске қосылғаннан кейін логика қарапайым. Регистрден мәнді қайтару r0 көшірілді r1 ал 2,3-жолдарда типке түрлендіріледі u32 — жоғарғы 32 бит тазаланады. 4,5,6,7-жолдарда 2 қайтарамыз (XDP_PASS) немесе 1 (XDP_DROP) 0 жолындағы көмекші функция нөлдік немесе нөлдік емес мәнді қайтарғанына байланысты.

Өзімізді сынап көрейік: бағдарламаны жүктеп, нәтижені қараңыз bpftool prog dump xlated:

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

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

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

Жарайды, тексеруші дұрыс ядро ​​көмекшісін тапты.

Мысал: дәлелдерді беру және соңында бағдарламаны іске қосу!

Барлық іске қосу деңгейіндегі көмекші функциялардың прототипі бар

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

Көмекші функцияларға параметрлер регистрлерде беріледі r1-r5, және мән тізілімде қайтарылады r0. Бес артық аргумент қабылдайтын функциялар жоқ және оларға қолдау болашақта қосылмайды деп күтілмейді.

Жаңа ядро ​​көмекшісін және BPF параметрлерді қалай беретінін қарастырайық. Қайта жазайық xdp-simple.bpf.c келесідей (қалған жолдар өзгерген жоқ):

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

Біздің бағдарлама жұмыс істеп тұрған процессордың нөмірін басып шығарады. Оны құрастырып, кодты қарастырайық:

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

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

0-7 жолдарда жолды жазамыз running on CPU%un, содан кейін 8-жолда біз таныс жолды іске қосамыз bpf_get_smp_processor_id. 9-12 жолдарда көмекші аргументтерді дайындаймыз bpf_printk - тіркеледі r1, r2, r3. Неліктен олардың екеуі емес, үшеуі бар? Себебі bpf_printkбұл макрос орауыш нағыз көмекшінің айналасында bpf_trace_printk, ол пішім жолының өлшемін беруі керек.

Енді бір-екі жолды қосайық xdp-simple.cбіздің бағдарлама интерфейске қосылуы үшін lo және шынымен басталды!

$ cat xdp-simple.c
#include <linux/if_link.h>
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"

int main(int argc, char **argv)
{
    __u32 flags = XDP_FLAGS_SKB_MODE;
    struct xdp_simple_bpf *obj;

    obj = xdp_simple_bpf__open_and_load();
    if (!obj)
        err(1, "failed to open and/or load BPF objectn");

    bpf_set_link_xdp_fd(1, -1, flags);
    bpf_set_link_xdp_fd(1, bpf_program__fd(obj->progs.simple), flags);

cleanup:
    xdp_simple_bpf__destroy(obj);
}

Мұнда біз функцияны қолданамыз bpf_set_link_xdp_fd, ол XDP типті BPF бағдарламаларын желі интерфейстеріне қосады. Біз интерфейс нөмірін қатты кодтадық lo, ол әрқашан 1. Ескі бағдарлама тіркелген болса, алдымен оны ажырату үшін функцияны екі рет іске қосамыз. Назар аударыңыз, қазір бізге сынақ қажет емес pause немесе шексіз цикл: біздің жүктеуші бағдарламамыз шығады, бірақ BPF бағдарламасы оқиға көзіне қосылғандықтан жойылмайды. Сәтті жүктеу және қосылудан кейін бағдарлама келген әрбір желі пакеті үшін іске қосылады lo.

Бағдарламаны жүктеп алып, интерфейсті қарастырайық lo:

$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp  name simple  tag 4fca62e77ccb43d6  gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 669

Біз жүктеп алған бағдарламада ID 669 бар және біз интерфейсте бірдей идентификаторды көреміз lo. Біз бірнеше пакетті жібереміз 127.0.0.1 (сұрау + жауап):

$ ping -c1 localhost

енді жөндеу виртуалды файлының мазмұнын қарастырайық /sys/kernel/debug/tracing/trace_pipe, онда bpf_printk хабарламаларын жазады:

# cat /sys/kernel/debug/tracing/trace_pipe
ping-13937 [000] d.s1 442015.377014: bpf_trace_printk: running on CPU0
ping-13937 [000] d.s1 442015.377027: bpf_trace_printk: running on CPU0

Екі пакет байқалды lo және CPU0-де өңделеді - біздің бірінші толыққанды мағынасыз BPF бағдарлама жұмыс істеді!

Мұны айта кеткен жөн bpf_printk Оның отладка файлына жазуы бекер емес: бұл өндірісте пайдалану үшін ең сәтті көмекші емес, бірақ біздің мақсатымыз қарапайым нәрсені көрсету болды.

BPF бағдарламаларынан карталарға қол жеткізу

Мысалы: BPF бағдарламасындағы картаны пайдалану

Алдыңғы бөлімдерде пайдаланушы кеңістігінен карталарды құру және пайдалану жолын үйрендік, ал енді ядро ​​бөлігін қарастырайық. Әдеттегідей мысалмен бастайық. Бағдарламамызды қайта жазайық xdp-simple.bpf.c келесідей:

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 8);
    __type(key, u32);
    __type(value, u64);
} woo SEC(".maps");

SEC("xdp/simple")
int simple(void *ctx)
{
    u32 key = bpf_get_smp_processor_id();
    u32 *val;

    val = bpf_map_lookup_elem(&woo, &key);
    if (!val)
        return XDP_ABORTED;

    *val += 1;

    return XDP_PASS;
}

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

Бағдарламаның басында біз карта анықтамасын қостық woo: Бұл сияқты мәндерді сақтайтын 8 элементті массив u64 (С тілінде біз осындай массивті анықтайтын едік u64 woo[8]). Бағдарламада "xdp/simple" ағымдағы процессор нөмірін айнымалыға аламыз key содан кейін көмекші функциясын пайдаланыңыз bpf_map_lookup_element біз массивтегі сәйкес жазбаға көрсеткіш аламыз, оны бір көбейтеміз. Орыс тіліне аударылған: біз кіріс пакеттерін өңдеген процессордың статистикасын есептейміз. Бағдарламаны іске қосып көрейік:

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

Оның қосылғанын тексерейік lo және бірнеше пакеттерді жіберіңіз:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 108

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

Енді массивтің мазмұнын қарастырайық:

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

Барлық дерлік процестер CPU7-де өңделді. Бұл біз үшін маңызды емес, бастысы бағдарлама жұмыс істейді және біз BPF бағдарламаларынан карталарға қалай қол жеткізуге болатындығын түсінеміз - пайдалану хелперов bpf_mp_*.

Мистикалық көрсеткіш

Осылайша, біз картаға BPF бағдарламасынан қоңыраулар арқылы қол жеткізе аламыз

val = bpf_map_lookup_elem(&woo, &key);

көмекші функциясы қайда ұқсайды

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

бірақ біз көрсеткіштен өтіп жатырмыз &woo атаусыз құрылымға struct { ... }...

Бағдарлама ассемблеріне қарасақ, мәнді көреміз &woo нақты анықталмаған (4-жол):

llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

xdp-simple.bpf.o:       file format elf64-bpf

Disassembly of section xdp/simple:

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
...

және қоныс аударуда қамтылған:

$ llvm-readelf -r xdp-simple.bpf.o | head -4

Relocation section '.relxdp/simple' at offset 0xe18 contains 1 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name
0000000000000020  0000002700000001 R_BPF_64_64            0000000000000000 woo

Бірақ егер біз жүктелген бағдарламаны қарасақ, біз дұрыс картаға көрсеткішті көреміз (4-жол):

$ sudo bpftool prog dump x name simple
int simple(void *ctx):
   0: (85) call bpf_get_smp_processor_id#114128
   1: (63) *(u32 *)(r10 -4) = r0
   2: (bf) r2 = r10
   3: (07) r2 += -4
   4: (18) r1 = map[id:64]
...

Осылайша, жүктеуші бағдарламаны іске қосу кезінде сілтеме бар деп қорытынды жасауға болады &woo кітапханасы бар нәрсемен ауыстырылды libbpf. Алдымен біз шығуды қарастырамыз strace:

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

Біз мұны көреміз libbpf картасын жасады woo содан кейін біздің бағдарламаны жүктеп алдық simple. Бағдарламаны қалай жүктейтінімізді толығырақ қарастырайық:

  • қоңырау шалу xdp_simple_bpf__open_and_load файлдан xdp-simple.skel.h
  • бұл тудырады xdp_simple_bpf__load файлдан xdp-simple.skel.h
  • бұл тудырады bpf_object__load_skeleton файлдан libbpf/src/libbpf.c
  • бұл тудырады bpf_object__load_xattr -дан libbpf/src/libbpf.c

Соңғы функция, басқа нәрселермен қатар, шақырады bpf_object__create_maps, ол бар карталарды жасайды немесе ашады, оларды файл дескрипторларына айналдырады. (Бұл жерде біз көреміз BPF_MAP_CREATE шығысында strace.) Әрі қарай функция шақырылады bpf_object__relocate және ол бізді қызықтырады, өйткені біз көргенімізді есте сақтаймыз woo орын ауыстыру кестесінде. Оны зерттей отырып, біз өзімізді функцияда табамыз bpf_program__relocate, қай картаны ауыстырумен айналысады:

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

Сондықтан біз нұсқауларымызды қабылдаймыз

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

және ондағы бастапқы регистрді ауыстырыңыз BPF_PSEUDO_MAP_FD, және картамыздың файл дескрипторына бірінші IMM және, егер ол тең болса, мысалы, 0xdeadbeef, содан кейін біз нұсқаулықты аламыз

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

Карта туралы ақпарат белгілі бір жүктелген BPF бағдарламасына осылай тасымалданады. Бұл жағдайда картаны қолдану арқылы жасауға болады BPF_MAP_CREATE, және ID арқылы ашылады BPF_MAP_GET_FD_BY_ID.

Барлығы, пайдаланған кезде libbpf алгоритмі келесідей:

  • құрастыру кезінде карталарға сілтемелер үшін орын ауыстыру кестесінде жазбалар жасалады
  • libbpf ELF нысан кітабын ашады, барлық пайдаланылған карталарды табады және олар үшін файл дескрипторларын жасайды
  • файл дескрипторлары ядроға нұсқаулықтың бөлігі ретінде жүктеледі LD64

Өздеріңіз ойлағандай, алда әлі көп нәрсе бар және біз өзегіне қарауымыз керек. Бақытымызға орай, бізде түсінік бар - біз мағынасын жазып алдық BPF_PSEUDO_MAP_FD бастапқы тізілімге енгізіңіз және біз оны жерлей аламыз, бұл бізді барлық әулиелердің киелісіне апарады - kernel/bpf/verifier.c, мұнда ерекше аты бар функция файл дескрипторын тип құрылымының адресімен ауыстырады struct bpf_map:

static int replace_map_fd_with_map_ptr(struct bpf_verifier_env *env) {
    ...

    f = fdget(insn[0].imm);
    map = __bpf_map_get(f);
    if (insn->src_reg == BPF_PSEUDO_MAP_FD) {
        addr = (unsigned long)map;
    }
    insn[0].imm = (u32)addr;
    insn[1].imm = addr >> 32;

(толық кодты табуға болады байланыс). Сонымен, біз алгоритмімізді кеңейте аламыз:

  • Бағдарламаны жүктеу кезінде верфикатор картаның дұрыс пайдаланылуын тексереді және сәйкес құрылымның мекенжайын жазады struct bpf_map

ELF екілік нұсқасын жүктеп алу кезінде libbpf Тағы да көп нәрсе бар, бірақ біз бұл туралы басқа мақалаларда талқылаймыз.

libbpf жоқ бағдарламалар мен карталарды жүктеу

Уәде етілгендей, карталарды пайдаланатын бағдарламаны көмексіз қалай жасау және жүктеу керектігін білгісі келетін оқырмандар үшін мысал. libbpf. Бұл тәуелділіктерді құра алмайтын ортада жұмыс істегенде немесе әрбір битті сақтайтын немесе осындай бағдарлама жазғанда пайдалы болуы мүмкін. ply, ол жылдам BPF екілік кодын жасайды.

Логиканы ұстануды жеңілдету үшін біз мысалды осы мақсаттар үшін қайта жазамыз xdp-simple. Осы мысалда қарастырылған бағдарламаның толық және сәл кеңейтілген кодын мына жерден табуға болады гист.

Біздің қолданбаның логикасы келесідей:

  • тип картасын құру BPF_MAP_TYPE_ARRAY пәрменін пайдалану BPF_MAP_CREATE,
  • осы картаны пайдаланатын бағдарлама құру,
  • бағдарламаны интерфейске қосыңыз lo,

ол адам деп аударылады

int main(void)
{
    int map_fd, prog_fd;

    map_fd = map_create();
    if (map_fd < 0)
        err(1, "bpf: BPF_MAP_CREATE");

    prog_fd = prog_load(map_fd);
    if (prog_fd < 0)
        err(1, "bpf: BPF_PROG_LOAD");

    xdp_attach(1, prog_fd);
}

Бұл map_create жүйелік шақыру туралы бірінші мысалдағыдай картаны жасайды bpf - «ядро, маған 8 элементтен тұратын массив түріндегі жаңа карта жасаңыз __u64 және маған файл дескрипторын қайтарыңыз»:

static int map_create()
{
    union bpf_attr attr;

    memset(&attr, 0, sizeof(attr));
    attr.map_type = BPF_MAP_TYPE_ARRAY,
    attr.key_size = sizeof(__u32),
    attr.value_size = sizeof(__u64),
    attr.max_entries = 8,
    strncpy(attr.map_name, "woo", sizeof(attr.map_name));
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}

Бағдарламаны жүктеу де оңай:

static int prog_load(int map_fd)
{
    union bpf_attr attr;
    struct bpf_insn insns[] = {
        ...
    };

    memset(&attr, 0, sizeof(attr));
    attr.prog_type = BPF_PROG_TYPE_XDP;
    attr.insns     = ptr_to_u64(insns);
    attr.insn_cnt  = sizeof(insns)/sizeof(insns[0]);
    attr.license   = ptr_to_u64("GPL");
    strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
    return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
}

Күрделі бөлік prog_load құрылымдар массиві ретінде біздің BPF бағдарламамыздың анықтамасы болып табылады struct bpf_insn insns[]. Бірақ бізде C тілінде бар бағдарламаны пайдаланып жатқандықтан, біз аздап алдап аламыз:

$ llvm-objdump -D --section xdp/simple xdp-simple.bpf.o

0000000000000000 <simple>:
       0:       85 00 00 00 08 00 00 00 call 8
       1:       63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
       2:       bf a2 00 00 00 00 00 00 r2 = r10
       3:       07 02 00 00 fc ff ff ff r2 += -4
       4:       18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
       6:       85 00 00 00 01 00 00 00 call 1
       7:       b7 01 00 00 00 00 00 00 r1 = 0
       8:       15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2>
       9:       61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0)
      10:       07 01 00 00 01 00 00 00 r1 += 1
      11:       63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1
      12:       b7 01 00 00 02 00 00 00 r1 = 2

0000000000000068 <LBB0_2>:
      13:       bf 10 00 00 00 00 00 00 r0 = r1
      14:       95 00 00 00 00 00 00 00 exit

Барлығы сияқты құрылымдар түрінде 14 нұсқаулық жазуымыз керек struct bpf_insn (кеңес: үйіндіні жоғарыдан алыңыз, нұсқаулар бөлімін қайта оқып шығыңыз, ашыңыз linux/bpf.h и linux/bpf_common.h және анықтауға тырысыңыз struct bpf_insn insns[] өз бетінше):

struct bpf_insn insns[] = {
    /* 85 00 00 00 08 00 00 00 call 8 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 8,
    },

    /* 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 */
    {
        .code = BPF_MEM | BPF_STX,
        .off = -4,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_10,
    },

    /* bf a2 00 00 00 00 00 00 r2 = r10 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_10,
        .dst_reg = BPF_REG_2,
    },

    /* 07 02 00 00 fc ff ff ff r2 += -4 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_2,
        .imm = -4,
    },

    /* 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll */
    {
        .code = BPF_LD | BPF_DW | BPF_IMM,
        .src_reg = BPF_PSEUDO_MAP_FD,
        .dst_reg = BPF_REG_1,
        .imm = map_fd,
    },
    { }, /* placeholder */

    /* 85 00 00 00 01 00 00 00 call 1 */
    {
        .code = BPF_JMP | BPF_CALL,
        .imm = 1,
    },

    /* b7 01 00 00 00 00 00 00 r1 = 0 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 0,
    },

    /* 15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2> */
    {
        .code = BPF_JMP | BPF_JEQ | BPF_K,
        .off = 4,
        .src_reg = BPF_REG_0,
        .imm = 0,
    },

    /* 61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0) */
    {
        .code = BPF_MEM | BPF_LDX,
        .off = 0,
        .src_reg = BPF_REG_0,
        .dst_reg = BPF_REG_1,
    },

    /* 07 01 00 00 01 00 00 00 r1 += 1 */
    {
        .code = BPF_ALU64 | BPF_ADD | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 1,
    },

    /* 63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1 */
    {
        .code = BPF_MEM | BPF_STX,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* b7 01 00 00 02 00 00 00 r1 = 2 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_K,
        .dst_reg = BPF_REG_1,
        .imm = 2,
    },

    /* <LBB0_2>: bf 10 00 00 00 00 00 00 r0 = r1 */
    {
        .code = BPF_ALU64 | BPF_MOV | BPF_X,
        .src_reg = BPF_REG_1,
        .dst_reg = BPF_REG_0,
    },

    /* 95 00 00 00 00 00 00 00 exit */
    {
        .code = BPF_JMP | BPF_EXIT
    },
};

Мұны өздері жазбағандарға арналған жаттығу - табыңыз map_fd.

Біздің бағдарламада тағы бір ашылмаған бөлік қалды - xdp_attach. Өкінішке орай, XDP сияқты бағдарламаларды жүйелік қоңырау арқылы қосу мүмкін емес bpf. BPF және XDP жасаған адамдар желідегі Linux қауымдастығының өкілдері болды, яғни олар өздеріне ең танысын пайдаланды (бірақ қалыпты адамдар) ядромен әрекеттесу интерфейсі: netlink розеткалары, сондай-ақ қараңыз RFC3549. Іске асырудың ең қарапайым жолы xdp_attach кодты көшіріп жатыр libbpf, атап айтқанда, файлдан netlink.c, бұл біз мұны аздап қысқартып:

Netlink ұяшықтары әлеміне қош келдіңіз

Netlink ұяшығының түрін ашыңыз NETLINK_ROUTE:

int netlink_open(__u32 *nl_pid)
{
    struct sockaddr_nl sa;
    socklen_t addrlen;
    int one = 1, ret;
    int sock;

    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;

    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (sock < 0)
        err(1, "socket");

    if (setsockopt(sock, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)) < 0)
        warnx("netlink error reporting not supported");

    if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
        err(1, "bind");

    addrlen = sizeof(sa);
    if (getsockname(sock, (struct sockaddr *)&sa, &addrlen) < 0)
        err(1, "getsockname");

    *nl_pid = sa.nl_pid;
    return sock;
}

Біз осы розеткадан оқимыз:

static int bpf_netlink_recv(int sock, __u32 nl_pid, int seq)
{
    bool multipart = true;
    struct nlmsgerr *errm;
    struct nlmsghdr *nh;
    char buf[4096];
    int len, ret;

    while (multipart) {
        multipart = false;
        len = recv(sock, buf, sizeof(buf), 0);
        if (len < 0)
            err(1, "recv");

        if (len == 0)
            break;

        for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);
                nh = NLMSG_NEXT(nh, len)) {
            if (nh->nlmsg_pid != nl_pid)
                errx(1, "wrong pid");
            if (nh->nlmsg_seq != seq)
                errx(1, "INVSEQ");
            if (nh->nlmsg_flags & NLM_F_MULTI)
                multipart = true;
            switch (nh->nlmsg_type) {
                case NLMSG_ERROR:
                    errm = (struct nlmsgerr *)NLMSG_DATA(nh);
                    if (!errm->error)
                        continue;
                    ret = errm->error;
                    // libbpf_nla_dump_errormsg(nh); too many code to copy...
                    goto done;
                case NLMSG_DONE:
                    return 0;
                default:
                    break;
            }
        }
    }
    ret = 0;
done:
    return ret;
}

Соңында, розетканы ашатын және оған файл дескрипторы бар арнайы хабарлама жіберетін функциямыз:

static int xdp_attach(int ifindex, int prog_fd)
{
    int sock, seq = 0, ret;
    struct nlattr *nla, *nla_xdp;
    struct {
        struct nlmsghdr  nh;
        struct ifinfomsg ifinfo;
        char             attrbuf[64];
    } req;
    __u32 nl_pid = 0;

    sock = netlink_open(&nl_pid);
    if (sock < 0)
        return sock;

    memset(&req, 0, sizeof(req));
    req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
    req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
    req.nh.nlmsg_type = RTM_SETLINK;
    req.nh.nlmsg_pid = 0;
    req.nh.nlmsg_seq = ++seq;
    req.ifinfo.ifi_family = AF_UNSPEC;
    req.ifinfo.ifi_index = ifindex;

    /* started nested attribute for XDP */
    nla = (struct nlattr *)(((char *)&req)
            + NLMSG_ALIGN(req.nh.nlmsg_len));
    nla->nla_type = NLA_F_NESTED | IFLA_XDP;
    nla->nla_len = NLA_HDRLEN;

    /* add XDP fd */
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FD;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(int);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &prog_fd, sizeof(prog_fd));
    nla->nla_len += nla_xdp->nla_len;

    /* if user passed in any flags, add those too */
    __u32 flags = XDP_FLAGS_SKB_MODE;
    nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
    nla_xdp->nla_type = IFLA_XDP_FLAGS;
    nla_xdp->nla_len = NLA_HDRLEN + sizeof(flags);
    memcpy((char *)nla_xdp + NLA_HDRLEN, &flags, sizeof(flags));
    nla->nla_len += nla_xdp->nla_len;

    req.nh.nlmsg_len += NLA_ALIGN(nla->nla_len);

    if (send(sock, &req, req.nh.nlmsg_len, 0) < 0)
        err(1, "send");
    ret = bpf_netlink_recv(sock, nl_pid, seq);

cleanup:
    close(sock);
    return ret;
}

Сонымен, барлығы тестілеуге дайын:

$ cc nolibbpf.c -o nolibbpf
$ sudo strace -e bpf ./nolibbpf
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, map_name="woo", ...}, 72) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=15, prog_name="woo", ...}, 72) = 4
+++ exited with 0 +++

Бағдарламамыздың қосылғанын көрейік lo:

$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    prog/xdp id 160

Пингтер жіберіп, картаға қарайық:

$ 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

Ура, бәрі жұмыс істейді. Айтпақшы, біздің карта қайтадан байт түрінде көрсетілетінін ескеріңіз. Бұл айырмашылығы, бұл фактімен байланысты libbpf біз түрі туралы ақпаратты (BTF) жүктеген жоқпыз. Бірақ бұл туралы келесі жолы көбірек айтатын боламыз.

Даму құралдары

Бұл бөлімде біз ең аз BPF әзірлеуші ​​құралдар жинағын қарастырамыз.

Жалпы айтқанда, BPF бағдарламаларын әзірлеу үшін сізге арнайы ештеңе қажет емес - BPF кез келген лайықты тарату ядросында жұмыс істейді және бағдарламалар clang, ол пакеттен жеткізілуі мүмкін. Дегенмен, BPF әзірленуде болғандықтан, ядро ​​мен құралдар үнемі өзгеріп отырады, егер сіз 2019 жылдан бастап ескі әдістерді пайдаланып BPF бағдарламаларын жазғыңыз келмесе, онда сізге компиляция қажет болады.

  • llvm/clang
  • pahole
  • оның өзегі
  • bpftool

(Анықтама үшін, бұл бөлім және мақаладағы барлық мысалдар Debian 10 жүйесінде орындалған.)

llvm/clang

BPF LLVM-мен достас және жақында BPF бағдарламаларын gcc арқылы құрастыруға болатынына қарамастан, барлық ағымдағы әзірлеу LLVM үшін жүзеге асырылады. Сондықтан, ең алдымен, біз қазіргі нұсқаны құрастырамыз clang git сайтынан:

$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86" 
                      -DLLVM_ENABLE_PROJECTS="clang" 
                      -DBUILD_SHARED_LIBS=OFF 
                      -DCMAKE_BUILD_TYPE=Release 
                      -DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$

Енді біз бәрі дұрыс біріктірілгенін тексере аламыз:

$ ./bin/llc --version
LLVM (http://llvm.org/):
  LLVM version 11.0.0git
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: znver1

  Registered Targets:
    bpf    - BPF (host endian)
    bpfeb  - BPF (big endian)
    bpfel  - BPF (little endian)
    x86    - 32-bit X86: Pentium-Pro and above
    x86-64 - 64-bit X86: EM64T and AMD64

(Құрастыру нұсқаулары clang меннен алынған bpf_devel_QA.)

Біз жаңа ғана құрастырған бағдарламаларды орнатпаймыз, оның орнына оларды жай ғана қосамыз PATH, мысалы:

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

(Мұны қосуға болады .bashrc немесе бөлек файлға. Жеке өзім осындай нәрселерді қосамын ~/bin/activate-llvm.sh және қажет кезде мен мұны істеймін . activate-llvm.sh.)

Пахоле және BTF

Утилита pahole BTF пішімінде жөндеу ақпаратын жасау үшін ядроны құру кезінде пайдаланылады. Біз бұл мақалада BTF технологиясының егжей-тегжейлері туралы егжей-тегжейлі тоқталмаймыз, тек бұл ыңғайлы және біз оны қолданғымыз келеді. Сондықтан ядроңызды құрастырғыңыз келсе, алдымен құрастырыңыз pahole (жоқ pahole опциясы бар ядроны құра алмайсыз CONFIG_DEBUG_INFO_BTF:

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

BPF-мен тәжірибе жасауға арналған ядролар

BPF мүмкіндіктерін зерттегенде, мен өз өзегімді жинағым келеді. Бұл, жалпы айтқанда, қажет емес, өйткені сіз тарату ядросында BPF бағдарламаларын құрастыра және жүктей аласыз, дегенмен, өзіңіздің ядроңыздың болуы сізге дистрибуцияңызда ең жақсы айларда пайда болатын соңғы BPF мүмкіндіктерін пайдалануға мүмкіндік береді. , немесе, кейбір отладка құралдары жағдайындағыдай, жақын болашақта мүлде пакеттелмейді. Сондай-ақ, оның өзегі кодпен тәжірибе жасауды маңызды сезінеді.

Ядро құру үшін, біріншіден, ядроның өзі, екіншіден, ядро ​​конфигурациясының файлы қажет. BPF-мен тәжірибе жасау үшін біз әдеттегідей пайдалана аламыз ванильді ядро немесе әзірлеу ядроларының бірі. Тарихи түрде, BPF дамуы Linux желілік қауымдастығында орын алады, сондықтан барлық өзгерістер ерте ме, кеш пе, Linux желісін қолдаушы Дэвид Миллер арқылы өтеді. Олардың сипатына қарай - өңдеулер немесе жаңа мүмкіндіктер - желілік өзгерістер екі ядроның біріне жатады - net немесе net-next. BPF үшін өзгерістер арасында бірдей жолмен бөлінеді bpf и bpf-next, олар кейін сәйкесінше net және net-next ішіне біріктіріледі. Қосымша мәліметтер алу үшін қараңыз bpf_devel_QA и netdev-ЖҚС. Сондықтан өз талғамыңызға және сынап жатқан жүйенің тұрақтылық қажеттіліктеріне қарай ядроны таңдаңыз (*-next ядролар тізімделгендердің ең тұрақсызы).

Ядро конфигурациясының файлдарын қалай басқаруға болатыны туралы айту осы мақаланың ауқымынан тыс - сіз мұны қалай жасау керектігін әлдеқашан білетін шығарсыз немесе үйренуге дайын өз бетінше. Дегенмен, келесі нұсқаулар сізге жұмыс істейтін BPF қосылған жүйені беру үшін жеткілікті немесе аз болуы керек.

Жоғарыдағы ядролардың бірін жүктеп алыңыз:

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

Ең аз жұмыс істейтін ядро ​​конфигурациясын жасаңыз:

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

Файлдағы BPF опцияларын қосыңыз .config өз таңдауыңыз бойынша (ең алдымен CONFIG_BPF systemd оны пайдаланатындықтан әлдеқашан қосылған болады). Міне, осы мақала үшін пайдаланылған ядродағы опциялардың тізімі:

CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_LSM=y
CONFIG_BPF_SYSCALL=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_IPV6_SEG6_BPF=y
# CONFIG_NETFILTER_XT_MATCH_BPF is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y

Содан кейін біз модульдерді және ядроны оңай жинап, орната аламыз (айтпақшы, сіз жаңадан жиналған ядроны пайдаланып ядроны жинай аласыз. clangқосу арқылы CC=clang):

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

және жаңа ядромен қайта жүктеңіз (бұл үшін қолданамын kexec пакеттен kexec-tools):

v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e

bpftool

Мақалада ең жиі қолданылатын утилита утилита болады bpftool, Linux ядросының бөлігі ретінде жеткізіледі. Оны BPF әзірлеушілері үшін BPF әзірлеушілері жазып, жүргізеді және BPF нысандарының барлық түрлерін басқару үшін пайдаланылуы мүмкін - бағдарламаларды жүктеу, карталарды жасау және өңдеу, BPF экожүйесінің өмірін зерттеу және т.б. Адам беттеріне арналған бастапқы кодтар түріндегі құжаттаманы табуға болады өзегінде немесе қазірдің өзінде құрастырылған, желіде.

Осы жазу кезінде bpftool тек RHEL, Fedora және Ubuntu үшін дайын келеді (мысалы, қараңыз). бұл жіп, ол қаптаманың аяқталмаған тарихын айтады bpftool Debian-да). Бірақ егер сіз өз ядроңызды құрып қойған болсаңыз, онда құрастырыңыз bpftool пирог сияқты оңай:

$ cd ${linux}/tools/bpf/bpftool
# ... пропишите пути к последнему clang, как рассказано выше
$ make -s

Auto-detecting system features:
...                        libbfd: [ on  ]
...        disassembler-four-args: [ on  ]
...                          zlib: [ on  ]
...                        libcap: [ on  ]
...               clang-bpf-co-re: [ on  ]

Auto-detecting system features:
...                        libelf: [ on  ]
...                          zlib: [ on  ]
...                           bpf: [ on  ]

$

(Мұнда ${linux} - бұл сіздің ядро ​​каталогыңыз.) Осы командаларды орындағаннан кейін bpftool каталогта жиналады ${linux}/tools/bpf/bpftool және оны жолға қосуға болады (ең алдымен пайдаланушыға root) немесе жай ғана көшіріңіз /usr/local/sbin.

Жинау bpftool соңғысын қолданған дұрыс clang, жоғарыда сипатталғандай құрастырылған және оның дұрыс жиналғанын тексеріңіз - мысалы, пәрменді пайдаланып

$ sudo bpftool feature probe kernel
Scanning system configuration...
bpf() syscall for unprivileged users is enabled
JIT compiler is enabled
JIT compiler hardening is disabled
JIT compiler kallsyms exports are enabled for root
...

ол ядрода қандай BPF мүмкіндіктері қосылғанын көрсетеді.

Айтпақшы, алдыңғы пәрменді келесідей орындауға болады

# bpftool f p k

Бұл пакеттегі утилиталарға ұқсастық арқылы жасалады iproute2, біз, мысалы, айта аламыз ip a s eth0 орнына ip addr show dev eth0.

қорытынды

BPF өзегінің функционалдығын тиімді өлшеу және бірден өзгерту үшін бүргеге аяқ киюге мүмкіндік береді. Жүйе UNIX-тің ең жақсы дәстүрлерінде өте сәтті болды: ядроны (қайта) бағдарламалауға мүмкіндік беретін қарапайым механизм көптеген адамдар мен ұйымдарға тәжірибе жасауға мүмкіндік берді. Тәжірибелер, сондай-ақ BPF инфрақұрылымының дамуы әлі аяқталмағанымен, жүйеде сенімді және ең бастысы тиімді бизнес логикасын құруға мүмкіндік беретін тұрақты ABI бар.

Менің ойымша, технология соншалықты танымал болғанын атап өткім келеді, өйткені, бір жағынан, мүмкін ойнаңыз (машинаның архитектурасын бір кеште азды-көпті түсінуге болады), ал екінші жағынан, оның пайда болуына дейін шешілмейтін (әдемі) мәселелерді шешу. Бұл екі құрамдас бірігіп адамдарды тәжірибе жасауға және армандауға мәжбүрлейді, бұл барған сайын инновациялық шешімдердің пайда болуына әкеледі.

Бұл мақала, әсіресе қысқа болмаса да, тек BPF әлеміне кіріспе болып табылады және архитектураның «жетілдірілген» мүмкіндіктері мен маңызды бөліктерін сипаттамайды. Алдағы жоспар келесідей: келесі мақала BPF бағдарламасының түрлеріне шолу болады (5.8 ядросында қолдау көрсетілетін 30 бағдарлама түрі бар), содан кейін біз ядроны бақылау бағдарламаларын пайдаланып нақты BPF қосымшаларын қалай жазу керектігін қарастырамыз. мысал ретінде, BPF архитектурасы бойынша тереңдетілген курстың уақыты келді, содан кейін BPF желісі және қауіпсіздік қолданбаларының мысалдары.

Осы сериядағы алдыңғы мақалалар

  1. Кішкентайларға арналған BPF, нөлдік бөлік: классикалық BPF

Сілтемелер

  1. BPF және XDP анықтамалық нұсқаулығы — цилиумнан алынған BPF туралы құжаттама, дәлірек айтқанда, BPF жасаушылар мен қолдаушылардың бірі Дэниел Боркманнан. Бұл бірінші маңызды сипаттамалардың бірі, оның басқалардан ерекшелігі Даниел не туралы жазып жатқанын жақсы біледі және онда ешқандай қате жоқ. Атап айтқанда, бұл құжат белгілі утилитаны пайдаланып XDP және TC типті BPF бағдарламаларымен қалай жұмыс істеу керектігін сипаттайды. ip пакеттен iproute2.

  2. Documentation/networking/filter.txt — классикалық, содан кейін кеңейтілген BPF құжаттамасы бар түпнұсқа файл. Ассемблер тілі мен техникалық архитектуралық бөлшектерді зерттегіңіз келсе, жақсы оқу.

  3. Facebook-тен BPF туралы блог. Ол сирек жаңартылады, бірақ Алексей Старовойтов (eBPF авторы) және Андрей Накрыйко - (жұмысшы) жазғандай. libbpf).

  4. bpftool құпиялары. Квентин Моннеден bpftool қолдану мысалдары мен құпиялары бар қызықты твиттер желісі.

  5. BPF-ге ену: оқу материалдарының тізімі. Квентин Моннеден BPF құжаттамасына сілтемелердің үлкен (және әлі де сақталған) тізімі.

Ақпарат көзі: www.habr.com

пікір қалдыру