BPF для самых маленькіх, частка першая: extended BPF
Спачатку была тэхналогія і называлася яна BPF. Мы паглядзелі на яе ў папярэдняй, старазапаветнай, артыкуле гэтага цыклу. У 2013 годзе намаганнямі Аляксея Старавойтава (Alexei Starovoitov) і Даніэля Боркмана (Daniel Borkman) была распрацавана і ўключана ў ядро Linux яе ўдасканаленая версія, аптымізаваная пад сучасныя 64-бітныя машыны. Гэтая новая тэхналогія нядоўгі час насіла назву Internal BPF, затым была пераназваная ў Extended BPF, а зараз, па сканчэнні некалькіх гадоў, усё яе завуць проста BPF.
Грубіянска кажучы, BPF дазваляе запускаць адвольны код, які прадстаўляецца карыстачом, у прасторы ядра Linux і новая архітэктура апынулася настолькі ўдалай, што нам запатрабуецца яшчэ з дзясятак артыкулаў, каб апісаць усе яе ўжыванні. (Адзінае з чым не справіліся распрацоўшчыкі, як вы можаце бачыць на ккдв ніжэй, гэта са стварэннем прыстойнага лагатыпа.)
У гэтым артыкуле апісваецца будынак віртуальнай машыны BPF, інтэрфейсы ядра для працы з BPF, сродкі распрацоўкі, а таксама кароткі, вельмі кароткі, агляд наяўных магчымасцяў, г.зн. усё тое, што нам спатрэбіцца ў далейшым для больш глыбокага вывучэння практычных ужыванняў BPF.
Кароткі змест артыкула
Увядзенне ў архітэктуру BPF. Спачатку мы паглядзім на архітэктуру BPF з вышыні птушынага палёту і абазначым асноўныя кампаненты.
Упраўленне аб'ектамі пры дапамозе сістэмнага выкліку bpf. Маючы ўжо некаторае ўяўленне пра сістэму мы, нарэшце, паглядзім на тое, як ствараць і кіраваць аб'ектамі з прасторы карыстача пры дапамозе адмысловага сістэмнага выкліку. bpf(2).
Пишем программы BPF с помощью libbpf. Пісаць праграмы пры дапамозе сістэмнага выкліку, вядома, можна. Але складана. Для больш рэалістычнага сцэнарыя ядзернымі праграмістамі была распрацавана бібліятэка libbpf. Мы створым найпросты шкілет прыкладання BPF, які мы будзем выкарыстоўваць у наступных прыкладах.
Kernel Helpers. Тут мы даведаемся як праграмы BPF могуць звяртацца да функцый-памочнікам ядра – прыладзе, які, нараўне з мапамі, прынцыпова пашырае магчымасці новага BPF у параўнанні з класічным.
Доступ да maps з праграм BPF. Да гэтага моманту мы будзем ведаць дастаткова, каб зразумець як менавіта можна ствараць праграмы, якія выкарыстоўваюць карты. І нават адным вочкам зазірнем у вялікі і магутны verifier.
Сродкі распрацоўкі. Даведачны раздзел аб тым, як сабраць патрабаваныя ўтыліты і ядро для эксперыментаў.
Зняволенне. У канцы артыкула тыя, хто датуль дачытае, знойдуць матывавальныя словы і кароткае апісанне таго, што будзе ў наступных артыкулах. Мы таксама пералічым некаторую колькасць спасылак для самастойнага вывучэння для тых, у каго няма жадання ці магчымасці чакаць працягі.
Увядзенне ў архітэктуру BPF
Перад тым як пачаць разглядаць архітэктуру BPF мы ў апошні раз (ой ці) спашлемся на класічны BPF, Які быў распрацаваны як адказ на з'яўленне RISC машын і вырашаў праблему эфектыўнай фільтрацыі пакетаў. Архітэктура атрымалася настолькі ўдалай, што, нарадзіўшыся ў хвацкія дзевяностыя ў Berkeley UNIX, яна была партаваная на большасць існых аперацыйных сістэм, дажыла да вар'яцкіх дваццатых і дагэтуль знаходзіць новыя ўжыванні.
Новы BPF быў распрацаваны як адказ на паўсюднае распаўсюджванне 64-бітных машын, хмарных сэрвісаў і ўзрослых запатрабаванняў у прыладах для стварэння SDN (Software-defined networking). Распрацаваны сеткавымі інжынерамі ядра як удасканаленая замена класічнага BPF, новы BPF літаральна праз паўгода знайшоў ужыванні ў нялёгкай справе трасіроўкі Linux сістэм, а цяпер, праз шэсць гадоў пасля з'яўлення, нам запатрабуецца цэлы, наступны, артыкул толькі для таго, каб пералічыць розныя тыпы праграм.
ВЯСЁЛЫЯ КАРТЫНКІ
У сваёй аснове BPF - гэта віртуальная машына-пясочніца, якая дазваляе запускаць "адвольны" код у прасторы ядра без шкоды для бяспекі. Праграмы BPF ствараюцца ў прасторы карыстальніка, загружаюцца ў ядро і падлучаюцца да якой-небудзь крыніцы падзей. Падзеяй можа быць, напрыклад, дастаўка пакета на сеткавы інтэрфейс, запуск якой-небудзь функцыі ядра, і да т.п. У выпадку пакета праграме BPF будуць даступныя дадзеныя і метададзеныя пакета (на чытанне і, можа, на запіс, у залежнасці ад тыпу праграмы), у выпадку запуску функцыі ядра - аргументы функцыі, уключаючы паказальнікі на памяць ядра, і да т.п.
Давайце паглядзім на гэты працэс падрабязней. Для пачатку раскажам пра першае адрозненне ад класічнага BPF, праграмы для якога пісаліся на асэмблеры. У новай версіі архітэктура была дапоўнена так, што праграмы стала можна пісаць на мовах высокага ўзроўня, у першую чаргу, вядома, на C. Для гэтага быў распрацаваны бакенд для llvm, які дазваляе генераваць байт-код для архітэктуры BPF.
Архітэктура BPF распрацоўвалася, у прыватнасці, для таго, каб эфектыўна выконвацца на сучасных машынах. Для таго, каб гэта працавала на практыку, байт-код BPF, пасля загрузкі ў ядро транслюецца ў натыўны код пры дапамозе кампанента пад назовам JIT compiler (Jусть In Time). Далей, калі вы падушыце, у класічным BPF праграма загружалася ў ядро і далучалася да крыніцы падзей атамарна — у кантэксце аднаго сістэмнага выкліку. У новай архітэктуры гэта адбываецца ў два этапы - спачатку код загружаецца ў ядро пры дапамозе сістэмнага выкліку bpf(2), а затым, пазней, пры дапамозе іншых механізмаў, розных у залежнасці ад тыпу праграмы, праграма падлучаецца (attaches) да крыніцы падзей.
Тут у чытача можа ўзнікнуць пытанне: а што, так можна было? Якім чынам гарантуецца бяспека выканання такога кода? Бяспека выканання гарантуецца нам этапам загрузкі праграм BPF пад назвай верыфікатар (па-ангельску гэты этап называецца verifier і я далей буду выкарыстоўваць ангельскае слова):
Verifier - гэта статычны аналізатар, які гарантуе, што праграма не парушыць нармальны ход працы ядра. Гэта, дарэчы, не азначае, што праграма не можа ўмяшацца ў працу сістэмы – праграмы BPF, у залежнасці ад тыпу, могуць чытаць і перапісваць участкі памяці ядра, якія вяртаюцца значэнні функцый, абразаць, дапаўняць, перапісваць і нават перасылаць сеткавыя пакеты. Verifier гарантуе, што ад запуску праграмы BPF ядро не зваліцца і што праграма, якой па правілах даступныя на запіс, напрыклад, дадзеныя выходнага пакета, не зможа перапісаць памяць ядра па-за пакетам. Крыху больш падрабязна мы паглядзім на verifier у адпаведным раздзеле, пасля таго, як пазнаёмімся з усімі астатнімі кампанентамі BPF.
Такім чынам, што мы даведаліся да гэтага моманту? Карыстальнік піша праграму на мове C, загружае яе ў ядро пры дапамозе сістэмнага выкліку bpf(2), дзе яна праходзіць праверку на verifier і транслюецца ў натыўны байткод. Затым той жа ці іншы карыстач падлучае праграму да крыніцы падзей і яна пачынае выконвацца. Падзел загрузкі і падлучэнні трэба па некалькіх чыннікам. Па-першае, запуск verifier - гэта адносна дорага і, загружаючы адну і тую ж праграму некалькі разоў, мы марнуем кампутарны час марна. Па-другое, тое, як менавіта падлучаецца праграма, залежыць ад яе тыпу і адзін "універсальны" інтэрфейс, распрацаваны год таму можа не падысці для новых тыпаў праграм. (Хоць зараз, калі архітэктура становіцца больш сталай, ёсць ідэя уніфікаваць гэты інтэрфейс на ўзроўні libbpf.)
Уважлівы чытач можа заўважыць, што мы яшчэ не скончылі з карцінкамі. І праўда, усё сказанае вышэй не тлумачыць, чым BPF прынцыпова мяняе карціну ў параўнанні з класічным BPF. Дзве новаўвядзенні, якія істотна пашыраюць межы дастасавальнасці - гэта наяўнасць магчымасці выкарыстоўваць падзяляную памяць і функцыі-памочнікі ядра (kernel helpers). У BPF падзяляная памяць рэалізаваная пры дапамозе так званых maps - падзяляных структур дадзеных з вызначаным API. Такую назву яны атрымалі, мусіць, таму, што першым які з'явіўся тыпам map была хэш-табліца. Далей з'явіліся масівы, лакальныя (per-CPU) хэш-табліцы і лакальныя масівы, дрэвы пошуку, карты, якія змяшчаюць паказальнікі на праграмы BPF і шматлікае іншае. Нам зараз цікавы той факт, што праграмы BPF атрымалі магчымасць захоўваць стан паміж выклікамі і падзяляць яго з іншымі праграмамі і з прасторай карыстальніка.
Доступ да maps ажыццяўляецца з карыстацкіх працэсаў пры дапамозе сістэмнага выкліку. bpf(2), а з праграм BPF, якія працуюць у ядры - пры дапамозе функцый-памочнікаў. Больш за тое, helpers існуюць не толькі для працы з мапамі, але і для доступу да іншых магчымасцяў ядра. Напрыклад, праграмы BPF могуць выкарыстоўваць функцыі-памочнікі для перанакіравання пакетаў на іншыя інтэрфейсы, для генерацыі падзей падсістэмы perf, доступу да структур ядра і да т.п.
Разам, BPF дае магчымасць загружаць адвольны, г.зн., які прайшоў праверку на verifier, код карыстальніка ў прастору ядра. Гэты код можа захоўваць стан паміж выклікамі і абменьвацца дадзенымі з прасторай карыстальніка, а таксама мае доступ да дазволеных дадзенаму тыпу праграм падсістэм ядра.
Гэта ўжо падобна на магчымасці, якія прадстаўляюцца модулямі ядра, у параўнанні з якімі ў BPF ёсць некаторыя перавагі (вядома, параўноўваць можна толькі падобныя прыкладанні, напрыклад, трасіроўку сістэмы - на BPF нельга напісаць адвольны драйвер). Можна адзначыць ніжэйшы парог уваходу (некаторыя ўтыліты, якія выкарыстоўваюць BPF не мяркуюць у карыстача наяўнасць навыкаў праграмавання ядра, ды і наогул навыкаў праграмавання), бяспека часу выканання (падніміце руку ў каментарах тыя, хто не ламаў сістэму пры напісанні ці тэставанні модуляў), атамарнасць - Пры перазагрузцы модуляў ёсць час прастою, а падсістэма BPF гарантуе, што ні адна падзея не будзе прапушчана (дзеля справядлівасці, гэта дакладна не для ўсіх тыпаў праграм BPF).
Наяўнасць такіх магчымасцяў і робіць BPF універсальнай прыладай для пашырэння ядра, што пацвярджаецца на практыцы: усё новыя і новыя тыпы праграм дадаюцца ў BPF, усё больш буйных кампаній выкарыстоўваюць BPF на баявых серверах 24×7, усё больш стартапаў будуюць свой бізнэс на рашэннях, у аснове якіх ляжыць BPF. BPF выкарыстоўваецца ўсюды: у абароне ад DDoS нападаў, стварэнні SDN (напрыклад, рэалізацыі сетак для kubernetes), у якасці асноўнай прылады трасіроўкі сістэм і зборшчыка статыстыкі, у сістэмах выяўлення ўварвання і ў сістэмах-пясочніцах і т.п.
Давайце на гэтым скончым аглядную частку артыкула і паглядзім на віртуальную машыну і на экасістэму BPF больш падрабязна.
Адступленне: утыліты
Для таго, каб мець магчымасць запускаць прыклады з наступных раздзелаў, вам можа спатрэбіцца некаторая колькасць утыліт, мінімум llvm/clang з падтрымкай bpf і bpftool. У раздзеле Сродкі распрацоўкі можна прачытаць інструкцыі па зборцы ўтыліт, а таксама свайго ядра. Гэты раздзел змешчаны ніжэй, каб не парушаць складнасць нашага выкладу.
Рэгістры і сістэма каманд віртуальнай машыны BPF
Архітэктура і сістэма каманд BPF распрацоўвалася з улікам таго, што праграмы будуць пісацца на мове C і пасля загрузкі ў ядро транслявацца ў натыўны код. Таму колькасць рэгістраў і мноства каманд выбіралася з аглядкай на скрыжаванне, у матэматычным сэнсе, магчымасцяў сучасных машын. Акрамя гэтага, на праграмы накладаліся рознага роду абмежаванні, напрыклад, да нядаўняга часу не было магчымасці пісаць цыклы і падпраграмы, а колькасць інструкцый было абмежавана 4096 (цяпер прывілеяваным праграмам можна загружаць да мільёна інструкцый).
У BPF маецца адзінаццаць даступных карыстачу 64-бітных рэгістраў r0-r10 і лічыльнік каманд (program counter). Рэгістр r10 змяшчае паказальнік на стэк (frame pointer) і даступны толькі для чытання. Праграмам падчас выканання даступны стэк у 512 байт і неабмежаваную колькасць падзялянай памяці ў выглядзе maps.
Праграмам BPF дазваляецца запускаць вызначаны ў залежнасці ад тыпу праграмы набор функцый-памагатых (kernel helpers) і, з нядаўніх сітавін, і звычайныя функцыі. Кожная выкліканая функцыя можа прымаць да пяці аргументаў, якія перадаюцца ў рэгістрах r1-r5, а якое вяртаецца значэнне перадаецца ў r0. Гарантуецца, што пасля вяртання з функцыі змест рэгістраў r6-r9 не зменіцца.
Для эфектыўнай трансляцыі праграм рэгістры r0-r11 для ўсіх падтрымліваемых архітэктур адназначна адлюстроўваюцца на сапраўдныя рэгістры з улікам асаблівасцяў ABI бягучай архітэктуры. Напрыклад, для x86_64 рэгістры r1-r5, якія выкарыстоўваюцца для перадачы параметраў функцый, адлюстроўваюцца на rdi, rsi, rdx, rcx, r8, якія выкарыстоўваюцца для перадачы параметраў у функцыі на x86_64. Напрыклад, код злева транслюецца ў код справа вось так:
рэгістр r0 таксама выкарыстоўваецца для вяртання выніку выканання праграмы, а ў рэгістры r1 праграме перадаецца паказальнік на кантэкст - у залежнасці ад тыпу праграмы гэта можа быць, напрыклад, структура struct xdp_md (для XDP) або структура struct __sk_buff (для розных сеткавых праграм) ці структура struct pt_regs (для розных тыпаў tracing праграм) і да т.п.
Такім чынам, у нас быў набор рэгістраў, kernel helpers, стэк, паказальнік на кантэкст і падзяляемая памяць у выглядзе maps. Не тое, каб усё гэта было катэгарычна неабходна ў паездцы, але...
Давайце працягнем апісанне і раскажам пра сістэму каманд для працы з гэтымі аб'ектамі. Усе (амаль усе) інструкцыі BPF маюць фіксаваны 64-бітны памер. Калі вы паглядзіце на адну інструкцыю на 64-бітнай Big Endian машыне, то вы ўбачыце
Тут Code - гэта кадоўка інструкцыі, Dst/Src - гэта кадоўкі прымача і крыніцы, адпаведна, Off - 16-бітны знакавы водступ, а Imm - Гэта 32-бітнае знакавы цэлы лік, выкарыстоўванае ў некаторых камандах (аналаг канстанты K з cBPF). Кадзіроўка Code мае адзін з двух відаў:
Класы інструкцый 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, а таксама пры вывучэнні maps, выкліку функцый і да т.п.
Калі мы будзем казаць аб індывідуальных інструкцыях, мы будзем спасылацца на файлы ядра bpf.h и bpf_common.h, у якіх вызначаюцца лікавыя коды інструкцый BPF. Пры самастойным вывучэнні архітэктуры і/або разборы бінарнікаў, семантыку вы можаце знайсці ў наступных, адсартаваных у парадку складанасці, крыніцах: Unofficial eBPF spec, BPF and XDP Reference Guide, Instruction Set, Documentation/networking/filter.txt і, вядома, у зыходных кодах Linux - verifier, JIT, інтэрпрэтатар BPF.
Прыклад: дызасэмбуем BPF у розуме
Давайце разбяром прыклад, у якім мы скампілюем праграму readelf-example.c і паглядзім на атрыманы бінарнік. Мы раскрыем арыгінальнае змесціва readelf-example.c ніжэй, пасля таго як адновім яго логіку з бінарных кодаў:
Коды каманд роўныя 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 (source), тое значэнне бярэцца з рэгістра-крыніцы, а калі, як у нашым выпадку, ён не ўсталяваны, тое значэнне бярэцца з поля Imm. Такім чынам, у першай і трэцяй інструкцыях мы выконваем аперацыю r0 = Imm. Далей, аперацыя 1 класа JMP - гэта BPF_JEQ (jump if equal). У нашым выпадку, бо біт S роўны нулю, яна параўноўвае значэнне рэгістра-крыніцы з полем Imm. Калі значэнні супадаюць, то пераход адбываецца на PC + Off, Дзе PC, як водзіцца, змяшчае адрас наступнай інструкцыі. Нарэшце, аперацыя 9 класа JMP - гэта 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. Праверым, што мы маем рацыю, паглядзеўшы на зыходнік:
Так, гэта бессэнсоўная праграма, але затое яна транслюецца ўсяго ў чатыры простыя інструкцыі.
Прыклад-выключэнне: 16-байтная інструкцыя
Раней мы згадалі, што некаторыя інструкцыі займаюць больш за 64 біта. Гэта адносіцца, напрыклад, да інструкцыі lddw (Code = 0x18 = BPF_LD | BPF_DW | BPF_IMM) - загрузіць у рэгістр падвойнае слова з палёў Imm. Справа ў тым што Imm мае памер 32, а падвойнае слова - 64 біта, таму загрузіць у рэгістр 64-бітнае непасрэднае значэнне ў адной 64-бітнай інструкцыі не атрымаецца. Для гэтага дзве суседнія інструкцыі выкарыстоўваюцца для захоўвання другой часткі 64-бітнага значэння ў полі Imm. прыклад:
Мы яшчэ сустрэнемся з інструкцыяй lddw, калі пагаворым аб рэлакацыях і працы з maps.
Прыклад: дызасэмбуем BPF стандартнымі сродкамі
Такім чынам, мы навучыліся чытаць бінарныя коды BPF і гатовы разабраць любую інструкцыю, калі спатрэбіцца. Аднак, варта сказаць, што на практыцы зручней і хутчэй дызасэмбляваць праграмы пры дапамозе стандартных сродкаў, напрыклад:
(Некаторыя падрабязнасці, якія апісваюцца ў гэтым падраздзеле, я ўпершыню даведаўся з паста Alexei Starovoitov ў BPF Blog.)
Аб'екты BPF - праграмы і карты - ствараюцца з прасторы карыстальніка пры дапамозе каманд BPF_PROG_LOAD и BPF_MAP_CREATE сістэмнага выкліку bpf(2), мы пагаворым пра тое, як менавіта гэта адбываецца ў наступным раздзеле. Пры гэтым ствараюцца структуры дадзеных ядра і для кожнай з іх refcount (лічыльнік спасылак) усталёўваецца роўным адзінцы, а карыстачу вяртаецца файлавы дэскрыптар, які паказвае на аб'ект. Пасля закрыцця дэскрыптара refcount аб'екта памяншаецца на адзінку, і пры дасягненні ім за нуль аб'ект знішчаецца.
Калі праграма выкарыстоўвае карты, то refcount гэтых карт павялічваецца на адзінку пасля загрузкі праграмы, г.зн. іх файлавыя дэскрыптары можна зачыніць з карыстацкага працэсу і пры гэтым. refcount не стане банкрутам:
Пасля паспяховай загрузкі праграмы мы звычайна далучаем яе да якога-небудзь генератара падзей. Напрыклад, мы можам пасадзіць яе на сеткавы інтэрфейс для апрацоўкі ўваходзяць пакетаў або падключыць яе да якой-небудзь tracepoint у ядры. У гэты момант лічыльнік спасылак таксама павялічыцца на адзінку і мы зможам зачыніць файлавы дэскрыптар у праграме-загрузніку.
Што здарыцца калі мы зараз завершым працу загрузніка? Гэта залежыць ад тыпу генератара падзей (hook). Усе сеткавыя хукі будуць існаваць пасля завяршэння загрузніка, гэта, так званыя, глабальныя хукі. А, напрыклад, праграмы трасіроўкі будуць вызваленыя пасля завяршэння працэсу, які стварыў іх (і таму завуцца лакальнымі, ад "local to the process"). Тэхнічна, лакальныя хукі заўсёды маюць які адпавядае файлавы дэскрыптар у прасторы карыстача і таму зачыняюцца з зачыненнем працэсу, а глабальныя — не. На наступным малюнку я пры дапамозе чырвоных крыжыкаў імкнуся паказаць як завяршэнне праграмы-загрузніка ўплывае на час жыцця аб'ектаў у выпадку лакальных і глабальных хукаў.
Навошта існуе падзел на лакальныя і глабальныя хукі? Запуск некаторых тыпаў сеткавых праграм мае сэнс і без userspace, напрыклад, прадстаўце абарону ад DDoS - загрузнік прапісвае правілы і падлучае BPF праграму да сеткавага інтэрфейсу, пасля чаго загрузнік можа пайсці і забіцца. З іншага боку, уявіце сабе адладкавую праграму трасіроўкі, якую вы напісалі на каленцы за дзесяць хвілін - пасля яе завяршэння вам бы хацелася, каб у сістэме не заставалася смецця, і лакальныя хукі гэта гарантуюць.
З іншага боку, уявіце, што вы хочаце далучыцца да tracepoint у ядры і збіраць статыстыку на працягу многіх гадоў. У гэтым выпадку вам бы жадалася завяршыць карыстацкую частку і вяртацца да статыстыкі час ад часу. Такую магчымасць дае файлавая сістэма bpf. Гэта псеўда-файлавая сістэма, якая існуе толькі ў памяці, якая дазваляе ствараць файлы, якія спасылаюцца на аб'екты BPF і, тым самым, якія павялічваюць refcount аб'ектаў. Пасля гэтага загрузнік можа завяршыць працу, а створаныя ім аб'екты застануцца жывыя.
Стварэнне файлаў у bpffs, якія спасылаюцца на аб'екты BPF завецца "замацаванне" ("pin", як у наступнай фразе: "працэс can pin a BPF праграма або map"). Стварэнне файлавых аб'ектаў для аб'ектаў BPF мае сэнс не толькі для падаўжэння жыцця лакальных аб'ектаў, але і для выгоды выкарыстання глабальных аб'ектаў – вяртаючыся да прыкладу з глабальнай праграмай для абароны ад DDoS, мы жадаем мець магчымасць час ад часу прыходзіць і глядзець на статыстыку.
Файлавая сістэма BPF звычайна мантуецца ў /sys/fs/bpf, але яе можна змантаваць і лакальна, напрыклад, так:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
Імёны ў файлавай сістэме ствараюцца пры дапамозе каманды BPF_OBJ_PIN сістэмнага выкліку BPF. У якасці ілюстрацыі давайце возьмем якую-небудзь праграму, скампілюем, загрузім і замацуем яе ў bpffs. Наша праграма не робіць нічога карыснага, мы прыводзім яе код толькі для таго, каб вы маглі прайграць прыклад:
Цяпер загрузім нашу праграму пры дапамозе ўтыліты. bpftool і паглядзім на спадарожныя сістэмныя выклікі bpf(2) (з высновы strace выдаленыя некаторыя не якія адносяцца да справы радка):
Тут мы загрузілі праграму пры дапамозе 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/XNUMX сістэмны выклік bpf падтрымлівае 34 розных каманды, а вызначэннеunion bpf_attr займае 200 радкоў. Але нас не павінна гэта палохаць, бо мы будзем знаёміцца з камандамі і параметрамі на працягу некалькіх артыкулаў.
Пачнём мы з каманды BPF_PROG_LOAD, якая стварае праграмы BPF - бярэ набор інструкцый BPF і загружае яго ў ядро. У момант загрузкі запускаецца verifier, а потым JIT compiler і, пасля паспяховага выканання, карыстачу вяртаецца файлавы дэскрыптар праграмы. Мы бачылі што з ім адбываецца далей у папярэднім раздзеле пра жыццёвы цыкл аб'ектаў BPF.
Цяпер мы напішам карыстацкую праграму, якая будзе загружаць простую праграму BPF, але спачатку нам трэба вырашыць, што менавіта за праграму мы хочам загрузіць — нам давядзецца выбраць тып і ў рамках гэтага тыпу пісаць праграму, якая пройдзе праверку на verifier. Аднак, каб не ўскладняць працэс, вось гатовае рашэнне: мы возьмем праграму тыпу BPF_PROG_TYPE_XDP, якая будзе вяртаць значэнне XDP_PASS (прапусціць усе пакеты). На асэмблеры BPF гэта выглядае вельмі проста:
r0 = 2
exit
Пасля таго, як мы вызначыліся з тым, што мы будзем загружаць, мы можам расказаць як мы гэта зробім:
Цікавыя падзеі ў праграме пачынаюцца з вызначэння масіва insns - Нашай праграмы BPF у машынных кодах. Пры гэтым кожная інструкцыя праграмы BPF пакуецца ў структуру. bpf_insn. Першы элемент insns адпавядае інструкцыі r0 = 2, другі - exit.
Адступленне. У ядры вызначаны зручнейшыя макрасы для напісання машынных кодаў, і, выкарыстаючы ядзерны загалоўкавых файлаў tools/include/linux/filter.h мы маглі б напісаць
Але бо напісанне праграм BPF у машынных кодах трэба толькі для напісання тэстаў у ядры і артыкулаў пра BPF, адсутнасць гэтых макрасаў насамрэч не ўскладняе жыццё распрацоўніка.
Пасля вызначэння праграмы BPF мы пераходзім да яе загрузкі ў ядро. Наш мінімалісцкі набор параметраў attr уключае ў сябе тып праграмы, набор і колькасць інструкцый, абавязковую ліцэнзію, а таксама імя "woo", Якое мы выкарыстоўваем, каб знайсці нашу праграму ў сістэме пасля загрузкі. Праграма, як і было абяцана, загружаецца ў сістэму пры дапамозе сістэмнага выкліку bpf.
У канцы праграмы мы пападаем у бясконцы цыкл, які імітуе карысную нагрузку. Без яго праграма будзе знішчана ядром пры зачыненні файлавага дэскрыптара, які вярнуў нам сістэмны выклік. bpf, і мы не ўбачым яе ў сістэме.
Ну што ж, мы гатовы да тэсціравання. Збяром і запусцім праграму пад strace, каб праверыць, што ўсё працуе як трэба:
Усё ў парадку, bpf(2) вярнуў нам дэскрыптар 3 і мы адправіліся ў бясконцы цыкл з pause(). Давайце паспрабуем знайсці нашу праграму ў сістэме. Для гэтага мы пойдзем у іншы тэрмінал і выкарыстоўваем утыліту bpftool:
Мы бачым, што ў сістэме ёсць загружаная праграма woo чый глабальны ID роўны 390, і што ў дадзены момант у працэсе simple-prog маецца адчынены файлавы дэскрыптар, які паказвае на праграму (і калі simple-prog завершыць працу, то woo знікне). Як і чакалася, праграма woo займае 16 байт - дзве інструкцыі - бінарных кодаў у архітэктуры BPF, але ў натыўным выглядзе (x86_64) - гэта ўжо 40 байт. Давайце паглядзім на нашу праграму ў арыгінальным выглядзе:
без сюрпрызаў. Цяпер паглядзім на код, створаны 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, так і праграмам з прасторы карыстальніка. Гэтыя аб'екты называюцца maps і ў гэтым раздзеле мы пакажам як кіраваць імі пры дапамозе сістэмнага выкліку. bpf.
Адразу скажам, што магчымасці maps не абмяжоўваюцца толькі доступам да агульнай памяці. Існуюць карты спецыяльнага прызначэння, якія змяшчаюць, напрыклад, паказальнікі на праграмы BPF або паказальнікі на сеткавыя інтэрфейсы, карты для працы з perf events і да т.п. Тут мы аб іх казаць не будзем, каб не блытаць чытача. Акрамя гэтага, мы ігнаруем праблемы сінхранізацыі, бо гэта не важна для нашых прыкладаў. Поўны спіс даступных тыпаў карт можна знайсці ў <linux/bpf.h>, а ў гэтым раздзеле мы ў якасці прыкладу возьмем гістарычна першы тып, хэш-табліцу BPF_MAP_TYPE_HASH.
Калі вы ствараеце хэш-табліцу, скажам, у C++, вы скажаце unordered_map<int,long> woo, што па-руску азначае «мне патрэбна табліца woo неабмежаванага памеру, у якой ключы маюць тып int, а значэння - тып long». Для таго, каб стварыць хэш-табліцу BPF мы павінны зрабіць прыкладна тое ж самае, з папраўкай на тое, што нам давядзецца паказаць максімальны памер табліцы, а замест тыпаў ключоў і значэнняў нам трэба паказаць іх памеры ў байтах. Для стварэння карт выкарыстоўваецца каманда BPF_MAP_CREATE сістэмнага выкліку bpf. Давайце паглядзім на больш-менш мінімальную праграму, якая стварае map. Пасля папярэдняй праграмы, якая загружае праграмы BPF, гэтая павінна вам здацца просты:
Тут мы вызначаем набор параметраў attr, у якім гаворым «мне патрэбна хэш-табліца з ключамі і значэннямі памеру sizeof(int), у якую я магу пакласці максімум чатыры элементы». Пры стварэнні BPF карт можна паказваць і іншыя параметры, напрыклад, гэтак жа, як і ў прыкладзе з праграмай, мы паказалі назву аб'екта як "woo".
Тут сістэмны выклік bpf(2) вярнуў нам дэскрыптар карта нумар 3 і далей праграма, як і чакалася, чакае далейшых указанняў у сістэмным выкліку pause(2).
Цяпер адправім нашу праграму ў background або адкрыем іншы тэрмінал і паглядзім на наш аб'ект пры дапамозе ўтыліты. bpftool (мы можам адрозніць наш map ад іншых па ім імя):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...
Лік 114 - гэта глабальны ID нашага аб'екта. Любая праграма ў сістэме можа выкарыстоўваць гэты ID, каб адкрыць ужо існуючы map пры дапамозе каманды 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 чытае і дадае элементы? Давайце зазірнем пад капот:
Спачатку мы адкрылі карт па яго глабальным ID пры дапамозе каманды 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:
Як і чакалася, гэта вельмі проста: каманда 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: адкрыць існуючы карт па яго глабальным ID
BPF_MAP_LOOKUP_AND_DELETE_ELEM: атамарна абнавіць значэнне аб'екта і вярнуць старое
BPF_MAP_FREEZE: зрабіць карт нязменным з userspace (гэтую аперацыю нельга адмяніць)
BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCH: масавыя аперацыі. Напрыклад, BPF_MAP_LOOKUP_AND_DELETE_BATCH - гэта адзіны надзейны спосаб прачытаць і абнуліць усе значэнні з карта
Не ўсе з гэтых каманд працуюць для ўсіх тыпаў карт, але наогул праца з іншымі тыпамі maps з прасторы карыстача выглядае сапраўды гэтак жа, як і праца з хэш-табліцамі.
Для парадку, давайце завершым нашы эксперыменты з хэш-табліцай. Памятайце, што мы стварылі табліцу, у якой можа змяшчацца да чатырох ключоў? Дадамо яшчэ некалькі элементаў:
$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long
Як і чакалася, у нас не атрымалася. Паглядзім на памылку падрабязней:
$ 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 і раскажам, што адбываецца на ўзроўні інструкцый. Для чытачоў, незадаволеных вельмі моцна, мы дадалі прыклад у адпаведным месцы артыкула.)
Пішам праграмы BPF з дапамогай libbpf
Пісаць праграмы BPF пры дапамозе машынных кодаў можа быць цікава толькі першы час, а потым надыходзіць перасычэнне. У гэты момант трэба звярнуць свой погляд на llvm, у якім ёсць бакенд для генерацыі кода для архітэктуры BPF, а таксама на бібліятэку libbpf, якая дазваляе пісаць карыстацкую частку прыкладанняў BPF і загружаць код праграм BPF, згенераваных пры дапамозе llvm/clang.
Насамрэч, як мы ўбачым у гэтым і наступных артыкулах, libbpf робіць даволі шмат працы і без яе (або аналагічных інструментаў - iproute2, libbcc, libbpf-go, і да т.п.) жыць немагчыма. Адной з killer-фіч праекта libbpf з'яўляецца BPF CO-RE (Compile Once, Run Everywhere) – праект, які дазваляе пісаць праграмы BPF, якія пераносяцца з аднаго ядра на іншае, з магчымасцю запуску на розных API (напрыклад, калі структура ядра змяняецца ад версіі да версіі). Для таго, каб мець магчымасць працаваць з CO-RE, ваша ядро павінна быць скампілявана з падтрымкай BTF (як гэта зрабіць мы расказваем у раздзеле Сродкі распрацоўкі. Праверыць, ці сабрана ваша ядро з BTF ці не, можна вельмі проста - па наяўнасці наступнага файла:
Гэты файл захоўвае ў сабе інфармацыю аб усіх тыпах дадзеных, выкарыстоўваных у ядры і выкарыстоўваецца ва ўсіх нашых прыкладах, выкарыстоўвалых libbpf. Мы будзем падрабязна казаць пра CO-RE у наступным артыкуле, а ў гэтым – проста пабудуйце сабе ядро з CONFIG_DEBUG_INFO_BTF.
Бібліятэка libbpf жыве прама ў дырэкторыі tools/lib/bpf ядра і яе распрацоўка вядзецца праз спіс рассылкі [email protected]. Аднак для патрэб прыкладанняў, якія жывуць за межамі ядра, падтрымліваецца асобны рэпазітар. https://github.com/libbpf/libbpf у якім ядзерная бібліятэка люстэркуецца для доступу на чытанне больш-менш як ёсць.
У дадзеным раздзеле мы паглядзім на тое, як можна стварыць праект, які выкарыстоўвае libbpf, напішам некалькі (больш-менш бессэнсоўных) тэставых праграм і падрабязна разбяром як усё гэта працуе. Гэта дазволіць нам у наступных раздзелах прасцей растлумачыць, як менавіта праграмы BPF узаемадзейнічаюць з maps, kernel helpers, BTF, і да т.п.
Звычайна праекты, якія выкарыстоўваюць libbpf дадаюць гітхабаўскі рэпазітар у якасці git submodule, зробім гэта і мы:
Наш далейшы план у гэтым раздзеле заключаецца ў наступным: мы напішам праграму 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:
Хоць праграма ў нас атрымалася вельмі простая, але ўсё ж мы мусім звярнуць увагу на мноства дэталяў. Па-першае, першым загалоўкавых файлаў, які мы ўключаем з'яўляецца vmlinux.h, які мы толькі што згенеравалі пры дапамозе bpftool btf dump - зараз нам не трэба ўсталёўваць пакет kernel-headers, каб даведацца, як выглядаюць структуры ядра. Наступны загалоўкавыя файл прыходзіць да нас з бібліятэкі libbpf. Цяпер ён нам патрэбен толькі для таго, каб вызначыўся макрас. SEC, які адпраўляе сімвал у адпаведную секцыю аб'ектнага файла ELF. Наша праграма змяшчаецца ў секцыі xdp/simple, дзе перад слэшам мы вызначаем тып праграмы BPF - гэта пагадненне, якое выкарыстоўваецца ў libbpf, на аснове назвы секцыі яна падставіць правільны тып пры запуску bpf(2). Сама праграма BPF на C - Вельмі простая і складаецца з аднаго радка return XDP_PASS. Нарэшце, асобная секцыя "license" змяшчае назву ліцэнзіі.
Мы можам скампіляваць нашу праграму пры дапамозе llvm/clang, версіі >= 10.0.0, а лепш - больш (гл. раздзел Сродкі распрацоўкі):
З цікавых асаблівасцяў: мы паказваем мэтавую архітэктуру -target bpf і шлях да загалоўкаў libbpf, якія мы нядаўна ўстанавілі. Таксама, не забывайце пра -O2, без гэтай опцыі вас могуць чакаць сюрпрызы надалей. Паглядзім на наш код, ці атрымалася ў нас напісаць праграму, якую мы хацелі?
Так, атрымалася! Цяпер, у нас ёсць бінарны файл з праграмай, і мы хочам стварыць дадатак, якое будзе яго загружаць у ядро. Для гэтага бібліятэка libbpf прапануе нам два варыянты - выкарыстоўваць больш нізкаўзроўневыя API або больш высокаўзроўневыя API. Мы пойдзем другім шляхам, бо нам хочацца навучыцца пісаць, загружаць і падлучаць праграмы BPF мінімальнымі намаганнямі для іх наступнага вывучэння.
Для пачатку, нам трэба згенераваць "шкілет" нашай праграмы з яе бінарніка пры дапамозе ўсё той жа ўтыліты bpftool - швейцарскага нажа свету BPF (што можна разумець і літаральна, бо Daniel Borkman - адзін са стваральнікаў і мантэйнераў BPF - швейцарац):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
У файле xdp-simple.skel.h змяшчаецца бінарны код нашай праграмы і функцыі для кіравання - загрузкі, далучэнні, выдаленні нашага аб'екта. У нашым простым выпадку гэта выглядае як overkill, але гэта працуе і ў выпадку, калі аб'ектны файл утрымоўвае мноства праграм BPF і карт і для загрузкі гэтага гіганцкага ELF нам досыць толькі згенераваць шкілет і выклікаць адну-дзве функцыі з карыстацкага прыкладання, да напісання якога мы зараз і пяройдзем.
Мы можам заўважыць тут сляды нізкаўзроўняга API: структуру struct bpf_program *simple и struct bpf_link *simple. Першая структура апісвае менавіта нашу праграму, запісаную ў секцыі. xdp/simple, а другая - апісвае тое, як праграма падлучаецца да крыніцы падзей.
Функцыя xdp_simple_bpf__open_and_load, адкрывае аб'ект ELF, парсіць яго, стварае ўсе структуры і падструктуры (акрамя праграмы ў ELF знаходзяцца і іншыя секцыі – data, readonly data, адладкавая інфармацыя, ліцэнзія і да т.п.), а потым загружае ў ядро пасродкам сістэмнага выкліку bpf, што мы можам праверыць, скампіляваўшы і запусціўшы праграму:
Давайце зараз паглядзім на нашу праграму пры дапамозе bpftool. Знойдзем яе ID:
# 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.
Kernel Helpers
Праграмы BPF могуць запускаць "вонкавыя" функцыі - kernel helpers. Гэтыя функцыі-памочнікі дазваляюць праграмам BPF атрымліваць доступ да структур ядра, кіраваць maps, а таксама мець зносіны з "рэальным светам" - ствараць perf events, кіраваць абсталяваннем (напрыклад, перанакіроўваць пакеты) і да т.п.
Прыклад: bpf_get_smp_processor_id
У рамках парадыгмы "вучымся на прыкладах", разгледзім адну з функцый-памагатых, bpf_get_smp_processor_id(), пэўную у файле kernel/bpf/helpers.c. Яна вяртае нумар працэсара, на якім запускаецца праграма BPF, якая выклікала яе. Але нас не так цікавіць яе семантыка, як тое, што яе рэалізацыя займае адзін радок:
Азначэнні функцый-памагатых BPF падобныя на азначэнні сістэмных выклікаў Linux. Тут, напрыклад, вызначаецца функцыя, якая не мае аргументаў. (Функцыя, якая прымае, скажам, тры аргументы, вызначаецца пры дапамозе макраса BPF_CALL_3. Максімальная колькасць аргументаў роўна пяці.) Аднак, гэта толькі першая частка вызначэння. Другая частка заключаецца ў вызначэнні структуры тыпу struct bpf_func_proto, якая змяшчае апісанне функцыі-памочніка, зразумелае verifier:
Для таго, каб праграмы BPF вызначанага тыпу маглі выкарыстоўваць гэтую функцыю, яны павінны зарэгістраваць яе, напрыклад, для тыпу BPF_PROG_TYPE_XDP у ядры вызначаецца функцыя xdp_func_proto, якая па ID функцыі-памочніка вызначае, ці падтрымлівае XDP гэтую функцыю ці не. Нашу функцыю яна падтрымлівае:
Новыя тыпы праграм BPF "вызначаюцца" у файле include/linux/bpf_types.h пры дапамозе макраса BPF_PROG_TYPE. Вызначаюцца ўзята ў двукоссі, бо гэтае лагічнае азначэнне, а ў тэрмінах мовы C азначэнне цэлага набору пэўных структур адбываецца ў іншых месцах. У прыватнасці, у файле kernel/bpf/verifier.c усе азначэнні з файла bpf_types.h выкарыстоўваюцца, каб стварыць масіў структур bpf_verifier_ops[]:
Гэта значыць, для кожнага тыпу праграм BPF вызначаецца паказальнік на структуру дадзеных тыпу. struct bpf_verifier_ops, які ініцыялізуецца значэннем _name ## _verifier_ops, г.зн., xdp_verifier_ops для xdp. Структура xdp_verifier_opsвызначаецца у файле net/core/filter.c наступным чынам:
Тут мы і бачым нашу знаёмую функцыю xdp_func_proto, Якая будзе запускацца verifier кожны раз, як ён сустрэне выклік нейкі функцыі ўнутры праграмы BPF, гл. verifier.c.
Паглядзім на тое, як гіпатэтычная праграма BPF выкарыстоўвае функцыю bpf_get_smp_processor_id. Для гэтага перапішам праграму з нашай папярэдняй часткі наступным чынам:
гэта значыць, bpf_get_smp_processor_id - Гэта паказальнік на функцыю, значэнне якога роўна 8, дзе 8 - гэта значэнне BPF_FUNC_get_smp_processor_id тыпу enum bpf_fun_id, якое вызначаецца для нас у файле vmlinux.h (файл bpf_helper_defs.h у ядры генеруецца скрыптам, таму "магічныя" лікі - гэта ok). Гэтая функцыя не прымае аргументаў і вяртае значэнне тыпу __u32. Калі мы запускаем яе ў нашай праграме, clang генеруе інструкцыю BPF_CALL "правільнага выгляду". Давайце скампілюем праграму і паглядзім на секцыю xdp/simple:
У першым жа радку мы бачым інструкцыю call, параметр IMM якой роўны 8, а SRC_REG - нулю. Па ABI-дамове, выкарыстоўваным verifier, гэта і ёсць выклік функцыі-памочніка пад нумарам восем. Пасля яе запуску логіка простая. Вярнутае значэнне з рэгістра 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
Добра, verifier знайшоў правільны kernel-helper.
Прыклад: перадаем аргументы і, нарэшце, запускаем праграму!
Усе функцыі-памочнікі на ўзроўні выканання маюць прататып
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
Параметры функцый-памочнікам перадаюцца ў рэгістрах r1-r5, а значэнне вяртаецца ў рэгістры r0. Функцый, якія прымаюць больш за пяць аргументаў - няма і дадаваць іх падтрымку ў будучыні не мяркуецца.
Давайце паглядзім на новы kernel helper і тое, як 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;
}
Наша праграма друкуе нумар CPU, на якім яна запушчана. Скампілюем яе і паглядзім на код:
У радках 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 і па-сапраўднаму запускалася!
Тут мы выкарыстоўваем функцыю bpf_set_link_xdp_fd, якая падлучае праграмы BPF тыпу XDP да сеткавым інтэрфейсам. Мы захардкадзілі нумар інтэрфейсу 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 і той жа ID мы бачым на інтэрфейсе lo. Пашлем пару пакетаў на 127.0.0.1 (request + reply):
$ 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 не дарма піша ў адладкавы файл: гэта не самы ўдалы хелпер для выкарыстання ў production, але наша мэта была паказаць нешта простае.
Доступ да maps з праграм BPF
Прыклад: выкарыстоўваем карт з праграмы BPF
У папярэдніх раздзелах мы навучыліся ствараць і выкарыстоўваць карты з прасторы карыстальніка, а зараз паглядзім на ядзерную частку. Пачнём, як водзіцца, з прыкладу. Перапішам нашу праграму xdp-simple.bpf.c наступным чынам:
У пачатак праграмы мы дадалі вызначэнне карта woo: гэта масіў з 8 элементаў, у якім захоўваюцца значэнні тыпу u64 (на C мы вызначылі б такі масіў як u64 woo[8]). У праграме "xdp/simple" мы атрымліваем нумар бягучага працэсара ў зменную key і затым пры дапамозе функцыі-памочніка bpf_map_lookup_element атрымліваем паказальнік на адпаведны запіс у масіве, які павялічваем на адзінку. У перакладзе на рускую: мы падлічваем статыстыку таго, на якім CPU былі апрацаваны ўваходныя пакеты. Паспрабуем запусціць праграму:
Праверым, што яна падчапілася да 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
Амаль усе працэсы былі апрацаваны на CPU7. Нам гэта не важна, галоўнае, што праграма працуе і мы зразумелі як атрымаць доступ да карт з праграм BPF – пры дапамозе хелперов bpf_mp_*.
Містычны паказальнік
Такім чынам, мы можам атрымліваць доступ з праграмы BPF да карты пры дапамозе выклікаў выгляду.
$ 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
Але калі мы паглядзім на ўжо загружаную праграму, то ўбачым паказальнік на правільны map (радок 4):
Такім чынам, мы можам зрабіць выснову, што ў момант запуску нашай праграмы-загрузніка спасылка на &woo была на нешта заменена бібліятэкай libbpf. Для пачатку мы паглядзім на выснову strace:
Мы бачым, што 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, якая стварае або адкрывае існуючыя 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;
і заменны ў ёй рэгістр-крыніца на 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:
(поўны код можна знайсці па спасылцы). Так што мы можам дапоўніць наш алгарытм:
падчас загрузкі праграмы verifier правярае карэктнасць выкарыстання карта і прапісвае адрас адпаведнай структуры. 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 і вярні мне файлавы дэскрыптар»:
Складаная частка prog_load - гэта вызначэнне нашай праграмы BPF у выглядзе масіва структур struct bpf_insn insns[]. Але так як мы выкарыстоўваем праграму, якая ў нас ёсць на C, то мы можам крыху схітрыць:
Разам нам трэба напісаць 14 інструкцый у выглядзе структур тыпу. struct bpf_insn (парада: вазьміце дамп зверху, перачытайце раздзел пра інструкцыі, адкрыйце linux/bpf.h и linux/bpf_common.h і паспрабуйце вызначыць struct bpf_insn insns[] самастойна):
Практыкаванне для тых, хто не стаў пісаць гэта сам - знайдзіце map_fd.
У нашай праграме засталася яшчэ адна нераскрытая частка - xdp_attach. Нажаль, праграмы тыпу XDP нельга падлучыць пры дапамозе сістэмнага выкліку bpf. Людзі, якія стваралі BPF і XDP былі з сеткавай супольнасці Linux, а значыць, яны выкарыстоўвалі самы звыклы для іх (але не для нармальных людзей) інтэрфейс узаемадзеяння з ядром: netlink sockets, гл. таксама 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;
}
Нарэшце, вось наша функцыя, якая адчыняе сокет і пасылае ў яго адмысловае паведамленне, якое змяшчае файлавы дэскрыптар:
$ 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
Ура, усё працуе. Заўважце, дарэчы, што наш map ізноў адлюстроўваецца ў выглядзе байцікаў. Гэта адбываецца з-за таго, што, у адрозненне ад libbpf мы не загружалі інфармацыю аб тыпах (BTF). Але падрабязней пра гэта мы пагаворым наступным разам.
Сродкі распрацоўкі
У гэтай частцы мы паглядзім на мінімальны інструментальны набор распрацоўніка BPF.
Наогул кажучы, для распрацоўкі праграм BPF не трэба нічога асаблівага - BPF працуе на любым прыстойным дыстрыбутыўным ядры, а праграмы збіраюцца пры дапамозе clang, Які можна паставіць з пакета. Аднак, з-за таго, што BPF знаходзіцца ў працэсе распрацоўкі, ядро і інструменты пастаянна мяняюцца, калі вы не хочаце пісаць праграмы BPF дзедаўскімі метадамі з 2019-х, то вам давядзецца сабраць
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
... много времени спустя
$
Цяпер мы можам праверыць, ці правільна ўсё сабралася:
(Інструкцыя па зборцы clang узята мной з bpf_devel_QA.)
Мы не будзем усталёўваць толькі што сабраныя праграмы, а замест гэтага проста дадамо іх у PATH, Напрыклад:
export PATH="`pwd`/bin:$PATH"
(Гэта можна дадаць у .bashrc ці ў асобны файл. Асабіста я дадаю такія рэчы ў ~/bin/activate-llvm.sh і калі трэба раблю . activate-llvm.sh.)
Pahole і 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 і таму ўсе змены рана ці позна праходзяць праз Дэвіда Мілера (David Miller) – мантэйнера сеткавай часткі Linux. У залежнасці ад сваёй прыроды - праўка або новыя фічы - сеткавыя змены трапляюць у адно з двух ядраў - net або net-next. Змены для BPF такім жа чынам размяркоўваюцца паміж bpf и bpf-next, якія потым куляцца ў net і net-next, адпаведна. Больш падрабязна гл. у bpf_devel_QA и netdev-FAQ. Так што выбірайце ядро зыходзячы з вашага густу і запатрабаванняў у стабільнасці сістэмы, на якой вы тэстуеце (*-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 - загружаць праграмы, ствараць і змяняць maps, даследаваць жыццядзейнасць экасістэмы BPF, і да т.п. Дакументацыю ў выглядзе зыходнікаў да man pages можна знайсці у ядры ці, ужо скампіляваную, у сетцы.
На момант напісання артыкула 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, а затым - для прыкладаў сеткавых і security прыкладанняў BPF.
BPF and XDP Reference Guide - Дакументацыя па BPF ад cilium, а дакладней ад Daniel Borkman, аднаго са стваральнікаў і мантэйнераў BPF. Гэта адно з першых сур'ёзных апісанняў, якое выдатна ад астатніх тым, што Daniel сапраўды ведае пра што піша і ляпаў там не назіраецца. У прыватнасці, у гэтым дакуменце расказваецца як працаваць з праграмамі BPF тыпаў XDP і TC пры дапамозе вядомай утыліты. ip з пакета iproute2.
Documentation/networking/filter.txt - арыгінальны файл з дакументацыяй па класічным, а затым і па extended BPF. Карысна чытаць, калі вы хочаце пакапацца ў асэмблеры і тэхнічных дэталях архітэктуры.
Блог пра BPF ад facebook. Абнаўляецца рэдка, але трапна, бо пішуць туды Alexei Starovoitov (аўтар eBPF) і Andrii Nakryiko – (мантэйнер libbpf).
Сакрэты bpftool. Займальны twitter-трэд ад Quentin Monnet з прыкладамі і сакрэтамі выкарыстання bpftool.
Dive into BPF: апісанне матэрыялу. Гіганцкі (і да гэтага часу які падтрымліваецца) спіс спасылак на дакументацыю па BPF ад Quentin Monnet.