У почетку је постојала технологија и звала се БПФ. Погледали смо је , Стари завет, чланак у овој серији. Године 2013, захваљујући напорима Алексеја Старовоитова и Данијела Боркмана, развијен је и укључен у основни Linux Побољшана верзија оптимизована за модерне 64-битне машине. Ова нова технологија је кратко названа Интернал БПФ, затим преименована у Екстендед БПФ, а сада, неколико година касније, сви је једноставно зову БПФ.
У основи, BPF дозвољава произвољном коду који је обезбедио корисник да се покреће у простору језгра. Linux И нова архитектура се показала толико успешном да би нам било потребно још десетак чланака да опишемо све њене примене. (Једино што програмери нису успели да ураде, као што можете видети на графикону ефикасности испод, јесте да направе пристојан лого.)
У овом чланку је описана структура БПФ виртуелне машине, интерфејси кернела за рад са БПФ-ом, алати за развој, као и кратак, врло кратак преглед постојећих могућности, тј. све што ће нам у будућности бити потребно за дубље проучавање практичне примене БПФ-а.
Резиме чланка
Прво ћемо погледати БПФ архитектуру из птичје перспективе и навести главне компоненте.
Већ имамо идеју о архитектури у целини, описали ћемо структуру виртуелне машине БПФ.
У овом одељку ћемо детаљније погледати животни циклус БПФ објеката – програма и мапа.
Са већ одређеним разумевањем система, коначно ћемо погледати како да креирамо и манипулишемо објектима из корисничког простора користећи посебан системски позив – bpf(2).
Наравно, можете писати програме користећи системски позив. Али тешко је. За реалнији сценарио, нуклеарни програмери су развили библиотеку libbpf. Направићемо основни скелет БПФ апликације који ћемо користити у наредним примерима.
Овде ћемо научити како БПФ програми могу да приступе помоћним функцијама кернела – алату који, заједно са мапама, суштински проширује могућности новог БПФ-а у поређењу са класичним.
До овог тренутка ћемо знати довољно да разумемо како тачно можемо да креирамо програме који користе мапе. И хајде да чак на брзину завиримо у велики и моћни верификатор.
Одељак помоћи о томе како да саставите потребне услужне програме и кернел за експерименте.
На крају чланка, они који читају до сада наћи ће мотивационе речи и кратак опис онога што ће се десити у наредним чланцима. Навешћемо и низ линкова за самостално учење за оне који немају жељу или могућност да чекају наставак.
Увод у БПФ архитектуру
Пре него што почнемо да разматрамо БПФ архитектуру, осврнућемо се последњи пут (ох). , који је развијен као одговор на појаву РИСЦ машина и решио је проблем ефикасног филтрирања пакета. Испоставило се да је архитектура била толико успешна да је, рођена у полетним деведесетим у Беркли УНИКС-у, портована на већину постојећих оперативних система, преживела луде двадесете и још увек проналази нове апликације.
Нови БПФ је развијен као одговор на свеприсутност 64-битних машина, услуга у облаку и повећану потребу за алатима за креирање СДН (Sофваре-dефинед nРазвијен од стране инжењера основне мреже као побољшана замена за класични BPF, нови BPF је нашао примену у тешком задатку праћења само шест месеци касније. Linux системи, а сада, шест година након њиховог појављивања, био би нам потребан потпуно нови чланак само да бисмо навели различите врсте програма.
Смесне слике
У својој сржи, БПФ је виртуелна машина са сандбоком која вам омогућава да покренете „произвољни“ код у простору кернела без угрожавања безбедности. БПФ програми се креирају у корисничком простору, учитавају се у кернел и повезују са неким извором догађаја. Догађај може бити, на пример, испорука пакета мрежном интерфејсу, покретање неке функције кернела итд. У случају пакета, БПФ програм ће имати приступ подацима и метаподацима пакета (за читање и, евентуално, писање, у зависности од типа програма, у случају покретања функције кернела, аргумената). функција, укључујући показиваче на меморију кернела, итд.
Хајде да ближе погледамо овај процес. За почетак, хајде да причамо о првој разлици од класичног БПФ-а, програми за који су написани на асемблеру. У новој верзији, архитектура је проширена тако да се програми могу писати на језицима високог нивоа, првенствено, наравно, на Ц. За ово је развијен бацкенд за ллвм, који вам омогућава да генеришете бајт код за БПФ архитектуру.

Архитектура БПФ-а је делимично дизајнирана да ефикасно ради на модерним машинама. Да би ово функционисало у пракси, БПФ бајт код, када се једном учита у кернел, преводи се у изворни код помоћу компоненте која се зове ЈИТ компајлер (Jуст In Tиме). Следеће, ако се сећате, у класичном БПФ-у програм је учитан у језгро и атомски прикључен извору догађаја - у контексту једног системског позива. У новој архитектури, ово се дешава у две фазе - прво, код се учитава у језгро помоћу системског позива bpf(2)а затим, касније, кроз друге механизме који се разликују у зависности од типа програма, програм се везује за извор догађаја.
Овде читалац може имати питање: да ли је то било могуће? Како се гарантује сигурност извршавања таквог кода? Безбедност извршења нам гарантује фаза учитавања БПФ програма која се зове верифиер (на енглеском се ова фаза зове верифиер и наставићу да користим енглеску реч):

Верифиер је статички анализатор који осигурава да програм не омета нормалан рад кернела. То, иначе, не значи да програм не може да омета рад система - БПФ програми, у зависности од типа, могу да читају и преписују делове меморије кернела, враћају вредности функција, урежу, додају, преписују па чак и проследи мрежне пакете. Верифиер гарантује да покретање БПФ програма неће срушити кернел и да програм који, према правилима, има приступ писању, на пример, подацима одлазног пакета, неће моћи да препише меморију кернела ван пакета. Верификатор ћемо погледати мало детаљније у одговарајућем одељку, након што се упознамо са свим осталим компонентама БПФ-а.
Дакле, шта смо до сада научили? Корисник пише програм у Ц, учитава га у језгро користећи системски позив bpf(2), где га проверава верификатор и преводи у изворни бајткод. Затим исти или други корисник повезује програм са извором догађаја и он почиње да се извршава. Раздвајање покретања и везе је неопходно из неколико разлога. Прво, покретање верификатора је релативно скупо и преузимањем истог програма неколико пута губимо време на рачунару. Друго, тачно како је програм повезан зависи од његовог типа, а један „универзални“ интерфејс развијен пре годину дана можда неће бити погодан за нове типове програма. (Иако сада када архитектура постаје зрелија, постоји идеја да се овај интерфејс обједини на нивоу libbpf.)
Пажљиви читалац може приметити да још нисмо завршили са сликама. Заиста, све наведено не објашњава зашто БПФ суштински мења слику у поређењу са класичним БПФ-ом. Две иновације које значајно проширују обим применљивости су могућност коришћења дељене меморије и помоћне функције кернела. У БПФ-у, заједничка меморија се имплементира коришћењем такозваних мапа - дељених структура података са одређеним АПИ-јем. Вероватно су добили ово име јер је прва врста мапе која се појавила била хеш табела. Затим су се појавили низови, локалне (по ЦПУ) хеш табеле и локални низови, стабла претраге, мапе које садрже показиваче на БПФ програме и још много тога. Оно што нам је сада интересантно је да БПФ програми сада имају могућност да истрају стање између позива и деле га са другим програмима и са корисничким простором.
Мапама се приступа из корисничких процеса помоћу системског позива bpf(2), и из БПФ програма који раде у језгру користећи помоћне функције. Штавише, помоћници постоје не само за рад са мапама, већ и за приступ другим могућностима кернела. На пример, БПФ програми могу да користе помоћне функције за прослеђивање пакета другим интерфејсима, генерисање перф догађаја, приступ структурама језгра итд.

Укратко, БПФ пружа могућност учитавања произвољног, тј. верификаторског тестираног корисничког кода у простор кернела. Овај код може да сачува стање између позива и размењује податке са корисничким простором, а такође има приступ подсистемима језгра које дозвољава ова врста програма.
Ово је већ слично могућностима које пружају модули кернела, у поређењу са којима БПФ има неке предности (наравно, можете упоређивати само сличне апликације, на пример, праћење система - не можете написати произвољан драјвер са БПФ-ом). Можете приметити нижи улазни праг (неки услужни програми који користе БПФ не захтевају од корисника да има вештине програмирања кернела или вештине програмирања уопште), безбедност током извршавања (подигните руку у коментарима за оне који нису разбили систем приликом писања или тестирања модула), атомичност – постоји застој приликом поновног учитавања модула, а БПФ подсистем осигурава да се ниједан догађај не пропусти (да будемо поштени, ово није тачно за све типове БПФ програма).
Присуство таквих могућности чини БПФ универзалним алатом за проширење кернела, што се потврђује у пракси: све више и више нових типова програма се додаје у БПФ, све више великих компанија користи БПФ на борбеним серверима 24×7, све више и више стартапи граде своје пословање на решењима на основу којих су засновани на БПФ-у. БПФ се користи свуда: у заштити од ДДоС напада, креирању СДН-а (на пример, имплементацији мрежа за кубернетес), као главни алат за праћење система и сакупљач статистике, у системима за откривање упада и сандбок системима, итд.
Хајде да завршимо прегледни део чланка овде и погледамо виртуелну машину и БПФ екосистем детаљније.
Дигресија: комуналне услуге
Да бисте могли да покренете примере у следећим одељцима, можда ће вам требати одређени број услужних програма, најмање llvm/clang уз бпф подршку и bpftool. У одељку Можете прочитати упутства за састављање услужних програма, као и своје језгро. Овај одељак је постављен испод како не би нарушио хармонију нашег излагања.
БПФ регистри виртуелних машина и систем инструкција
Архитектура и командни систем БПФ-а су развијени узимајући у обзир чињеницу да ће програми бити написани на Ц-у и, након учитавања у кернел, преведени у изворни код. Дакле, број регистара и скуп команди су изабрани с обзиром на пресек, у математичком смислу, могућности савремених машина. Поред тога, програмима су наметнута разна ограничења, на пример, донедавно није било могуће писати петље и потпрограме, а број инструкција је био ограничен на 4096 (сада привилеговани програми могу учитати до милион инструкција).
БПФ има једанаест корисничких 64-битних регистара r0-r10 и програмски бројач. Регистровати r10 садржи показивач оквира и само је за читање. Програми имају приступ стеку од 512 бајта у току рада и неограниченој количини дељене меморије у облику мапа.
БПФ програмима је дозвољено да покрећу одређени скуп помоћника језгра програмског типа и, у скорије време, регуларне функције. Свака позвана функција може узети до пет аргумената, прослеђених у регистрима r1-r5, а повратна вредност се прослеђује r0. Гарантовано је да по повратку из функције садржај регистара r6-r9 Неће се променити.
За ефикасно превођење програма, регистри r0-r11 за све подржане архитектуре су јединствено мапиране у стварне регистре, узимајући у обзир карактеристике АБИ тренутне архитектуре. На пример, за 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 програму се прослеђује показивач на контекст - у зависности од типа програма, то може бити, на пример, структура (за КСДП) или структуру (за различите мрежне програме) или структуру (за различите врсте програма за праћење) итд.
Дакле, имали смо скуп регистара, помоћнике кернела, стек, показивач контекста и заједничку меморију у облику мапа. Није да је све ово неопходно на путовању, али...
Наставимо опис и причамо о командном систему за рад са овим објектима. Све () БПФ инструкције имају фиксну 64-битну величину. Ако погледате једну инструкцију на 64-битној Биг Ендиан машини видећете
![]()
Овде Code - ово је кодирање инструкције, Dst/Src су кодирања пријемника и извора, респективно, Off - 16-битно увлачење са знаком, и Imm је 32-битни предписани цео број који се користи у неким упутствима (слично цБПФ константи К). Енцодинг Code има један од два типа:

Класе инструкција 0, 1, 2, 3 дефинишу команде за рад са меморијом. Они , BPF_LD, BPF_LDX, BPF_ST, BPF_STX, редом. Одељења 4, 7 (BPF_ALU, BPF_ALU64) чине скуп АЛУ инструкција. Одељења 5, 6 (BPF_JMP, BPF_JMP32) садрже упутства за прескакање.
Даљи план за проучавање БПФ система инструкција је следећи: уместо педантно набрајања свих инструкција и њихових параметара, погледаћемо неколико примера у овом одељку и из њих ће постати јасно како упутства заправо функционишу и како се ручно раставите било коју бинарну датотеку за БПФ. Да бисмо консолидовали материјал касније у чланку, сусрећемо се и са појединачним упутствима у одељцима о верификатору, ЈИТ компајлеру, преводу класичног БПФ-а, као и приликом проучавања мапа, позивања функција итд.
Када говоримо о појединачним упутствима, осврнућемо се на основне датотеке и , који дефинишу нумеричке кодове БПФ инструкција. Када самостално проучавате архитектуру и/или анализирате бинарне датотеке, можете пронаћи семантику у следећим изворима, поређану по сложености: , , и, наравно, у изворним кодовима Linux — верификатор, JIT, 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, респективно 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 - Је . Он додељује вредност одредишном регистру. Ако је бит постављен s (извор), онда се вредност узима из изворног регистра, а ако, као у нашем случају, није подешена, онда се вредност узима из поља Imm. Дакле, у првом и трећем упутству изводимо операцију r0 = Imm. Даље, операција ЈМП класе 1 је (скок ако је једнако). У нашем случају, од бит S је нула, упоређује вредност изворног регистра са пољем Imm. Ако се вредности поклапају, онда долази до преласка на PC + OffГде PC, као и обично, садржи адресу следеће инструкције. Коначно, операција ЈМП класе 9 је . Ово упутство прекида програм, враћајући се на језгро 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 = | | ) — учитајте двоструку реч из поља у регистар 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, када говоримо о селидбама и раду са мапама.
Пример: растављање БПФ-а помоћу стандардних алата
Дакле, научили смо да читамо БПФ бинарне кодове и спремни смо да рашчланимо било које упутство ако је потребно. Међутим, вреди рећи да је у пракси практичније и брже растављати програме помоћу стандардних алата, на пример:
$ 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_PROG_LOAD и BPF_MAP_CREATE системски позив bpf(2), говорићемо о томе како се то тачно дешава у следећем одељку. Ово ствара структуре података кернела и за сваку од њих refcount (број референци) је постављен на један, а дескриптор датотеке који указује на објекат се враћа кориснику. Након што је ручка затворена refcount објекат се смањује за један, а када достигне нулу, објекат се уништава.
Ако програм користи мапе, онда refcount ове карте се повећавају за један након учитавања програма, тј. њихови дескриптори датотека могу бити затворени из корисничког процеса и даље refcount неће постати нула:

Након успешног учитавања програма, обично га повезујемо са неком врстом генератора догађаја. На пример, можемо га ставити на мрежни интерфејс за обраду долазних пакета или га повезати са неким tracepoint у језгру. У овом тренутку, референтни бројач ће се такође повећати за један и моћи ћемо да затворимо дескриптор датотеке у програму за учитавање.
Шта се дешава ако сада искључимо покретач? Зависи од типа генератора догађаја (хоок). Све мрежне закачице ће постојати након што се учитавач заврши, то су такозване глобалне куке. И, на пример, програми праћења ће бити пуштени након што се процес који их је креирао заврши (и стога се називају локалним, од „локалног до процеса“). Технички, локалне закачице увек имају одговарајући дескриптор датотеке у корисничком простору и стога се затварају када се процес затвори, али глобалне закачице немају. На следећој слици, користећи црвене крстове, покушавам да покажем како завршетак програма за учитавање утиче на животни век објеката у случају локалних и глобалних кукица.

Зашто постоји разлика између локалних и глобалних кукица? Покретање неких врста мрежних програма има смисла без корисничког простора, на пример, замислите ДДоС заштиту – покретач записује правила и повезује БПФ програм са мрежним интерфејсом, након чега покретач може да оде и да се убије. С друге стране, замислите програм за праћење грешака који сте написали на коленима за десет минута – када се заврши, желели бисте да у систему не остане смеће, а локалне куке ће то обезбедити.
С друге стране, замислите да желите да се повежете са тачком праћења у језгру и прикупите статистику током много година. У овом случају, желели бисте да завршите кориснички део и да се с времена на време враћате на статистику. Бпф систем датотека пружа ову могућност. То је систем псеудо-датотека само у меморији који омогућава креирање датотека које упућују на БПФ објекте и на тај начин повећавају refcount објеката. Након тога, учитавач може изаћи, а објекти које је креирао ће остати живи.

Креирање датотека у бпффс које упућују на БПФ објекте назива се „прикачење“ (као у следећој фрази: „процес може закачити БПФ програм или мапу“). Креирање фајл објеката за БПФ објекте има смисла не само за продужење животног века локалних објеката, већ и за употребљивост глобалних објеката - враћајући се на пример са глобалним ДДоС заштитним програмом, желимо да можемо да дођемо и погледамо статистику с времена на време.
БПФ систем датотека се обично монтира /sys/fs/bpf, али се може монтирати и локално, на пример, овако:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpointНазиви система датотека се креирају помоћу команде BPF_OBJ_PIN БПФ системски позив. За илустрацију, узмимо програм, компајлирамо га, отпремимо и закачимо 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Брисање објеката
Говорећи о брисању објеката, потребно је разјаснити да након што смо искључили програм са куке (генератора догађаја), ниједан нови догађај неће покренути његово покретање, међутим, све тренутне инстанце програма ће бити завршене нормалним редоследом .
Неке врсте БПФ програма омогућавају вам да замените програм у ходу, тј. обезбедити атомичност секвенце replace = detach old program, attach new program. У овом случају, све активне инстанце старе верзије програма ће завршити свој рад, а нови руковаоци догађаја ће бити креирани из новог програма, а „атомичност“ овде значи да ниједан догађај неће бити пропуштен.
Повезивање програма са изворима догађаја
У овом чланку нећемо посебно описивати повезивање програма са изворима догађаја, јер има смисла ово проучавати у контексту одређене врсте програма. Центиметар. испод, у којем показујемо како су повезани програми попут КСДП-а.
Манипулисање објектима помоћу бпф системског позива
БПФ програми
Сви БПФ објекти се креирају и управљају из корисничког простора помоћу системског позива bpf, који има следећи прототип:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);Ево тима cmd је једна од вредности типа , attr — показивач на параметре за одређени програм и size — величина објекта према показивачу, тј. обично ово sizeof(*attr). У кернелу 5.8 системски позив bpf подржава 34 различите команде, и union bpf_attr заузима 200 редова. Али не треба да се плашимо овога, пошто ћемо се упознати са командама и параметрима током неколико чланака.
Почнимо са тимом BPF_PROG_LOAD, који креира БПФ програме - узима скуп БПФ инструкција и учитава га у језгро. У тренутку учитавања покреће се верификатор, а затим се кориснику враћа ЈИТ компајлер и, након успешног извршења, дескриптор програмске датотеке. Видели смо шта му се даље дешава у претходном одељку .
Сада ћемо написати прилагођени програм који ће учитати једноставан БПФ програм, али прво треба да одлучимо коју врсту програма желимо да учитамо - мораћемо да изаберете и у оквиру овог типа написати програм који ће проћи верификациони тест. Међутим, да не бисмо компликовали процес, ево готовог решења: узећемо програм као BPF_PROG_TYPE_XDP, који ће вратити вредност XDP_PASS (прескочите све пакете). У БПФ асемблеру то изгледа веома једноставно:
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 - наш БПФ програм у машинском коду. У овом случају, свака инструкција БПФ програма је упакована у структуру . Први елемент insns у складу са упутствима r0 = 2, други - exit.
Повлачење. Кернел дефинише погодније макрое за писање машинских кодова и коришћење заглавља кернела tools/include/linux/filter.h могли бисмо писати
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
BPF_EXIT_INSN()
};Али пошто је писање БПФ програма у изворном коду неопходно само за писање тестова у језгру и чланака о БПФ-у, одсуство ових макроа не компликује живот програмера.
Након дефинисања БПФ програма, прелазимо на његово учитавање у кернел. Наш минималистички скуп параметара 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 заузима 16 бајтова - две инструкције - бинарних кодова у БПФ архитектури, али у свом изворном облику (к86_64) то је већ 40 бајтова. Погледајмо наш програм у оригиналном облику:
# bpftool prog dump xlated id 390
0: (b7) r0 = 2
1: (95) exitбез изненађења. Сада погледајмо код који генерише ЈИТ компајлер:
# 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), али поштено речено, наш програм је превише једноставан, а за нетривијалне програме су, наравно, потребни пролог и епилог које је додао ЈИТ компајлер.
Карте
БПФ програми могу да користе структуриране меморијске области које су доступне и другим БПФ програмима и програмима у корисничком простору. Ови објекти се називају мапе и у овом одељку ћемо показати како се њима манипулише коришћењем системског позива bpf.
Рецимо одмах да могућности мапа нису ограничене само на приступ заједничкој меморији. Постоје мапе посебне намене које садрже, на пример, показиваче на БПФ програме или показиваче на мрежне интерфејсе, мапе за рад са перф догађајима итд. Овде нећемо говорити о њима, да не бисмо збунили читаоца. Осим тога, игноришемо проблеме синхронизације, јер то није важно за наше примере. Комплетна листа доступних типова мапа може се наћи у , а у овом одељку ћемо узети као пример историјски први тип, хеш табелу BPF_MAP_TYPE_HASH.
Ако направите хеш табелу у, рецимо, Ц++, рекли бисте unordered_map<int,long> woo, што на руском значи „Треба ми сто woo неограничене величине, чији су кључеви типа int, а вредности су тип long" Да бисмо креирали БПФ хеш табелу, морамо да урадимо скоро исту ствар, осим што морамо да наведемо максималну величину табеле и уместо да специфицирамо типове кључева и вредности, треба да наведемо њихове величине у бајтовима . За креирање мапа користите команду BPF_MAP_CREATE системски позив 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), у који могу да ставим највише четири елемента“. Приликом креирања БПФ мапа, можете одредити друге параметре, на пример, на исти начин као у примеру са програмом, навели смо име објекта као "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 не зна који су тип вредности у хеш табели. (Ово знање јој се може пренети помоћу БТФ-а, али више о томе сада.)
Како тачно бпфтоол чита и додаје елементе? Хајде да погледамо испод хаубе:
$ 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 отвара нашу мапу по ИД-у и команди 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 mapBPF_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.
Дакле, можемо креирати и учитавати БПФ програме, као и креирати и управљати мапама из корисничког простора. Сада је логично да погледамо како можемо да користимо мапе из самих БПФ програма. О томе бисмо могли говорити језиком тешко читљивих програма у машинским макро кодовима, али је у ствари дошло време да се покаже како се БПФ програми заправо пишу и одржавају – користећи libbpf.
(За читаоце који су незадовољни недостатком примера ниског нивоа: детаљно ћемо анализирати програме који користе мапе и помоћне функције креиране помоћу libbpf и рећи вам шта се дешава на нивоу инструкција. За читаоце који су незадовољни пуно, додали смо на одговарајућем месту у чланку.)
Писање БПФ програма користећи либбпф
Писање БПФ програма коришћењем машинских кодова може бити занимљиво само први пут, а онда долази до ситости. У овом тренутку морате скренути пажњу на llvm, који има позадину за генерисање кода за БПФ архитектуру, као и библиотеку libbpf, који вам омогућава да напишете корисничку страну БПФ апликација и учитате код БПФ програма генерисан помоћу llvm/clang.
У ствари, као што ћемо видети у овом и наредним чланцима, libbpf ради доста посла без њега (или сличних алата - iproute2, libbcc, libbpf-go, итд.) немогуће је живети. Једна од убитачних карактеристика пројекта libbpf је БПФ ЦО-РЕ (Цомпиле Онце, Рун Еверивхере) – пројекат који вам омогућава да пишете БПФ програме који су преносиви са једног језгра на друго, са могућношћу покретања на различитим АПИ-јима (на пример, када се структура кернела промени из верзије до верзије). Да бисте могли да радите са ЦО-РЕ, ваше језгро мора бити преведено са БТФ подршком (описујемо како се то ради у одељку . Можете проверити да ли је ваше језгро изграђено са БТФ-ом или не врло једноставно - присуством следеће датотеке:
$ ls -lh /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 2.6M Jul 29 15:30 /sys/kernel/btf/vmlinuxОва датотека чува информације о свим типовима података који се користе у кернелу и користи се у свим нашим примерима коришћења libbpf. У следећем чланку ћемо детаљно говорити о ЦО-РЕ, али у овом - само направите себи кернел са CONFIG_DEBUG_INFO_BTF.
библиотека libbpf живи тачно у именику tools/lib/bpf кернел и његов развој се одвија преко маилинг листе bpf@vger.kernel.org. Међутим, одвојено спремиште се одржава за потребе апликација које живе изван кернела у којој је библиотека кернела пресликана за приступ читању мање-више онаква каква јесте.
У овом одељку ћемо погледати како можете креирати пројекат који користи libbpf, хајде да напишемо неколико (мање-више бесмислених) тест програма и детаљно анализирамо како све то функционише. Ово ће нам омогућити да у следећим одељцима лакше објаснимо како тачно БПФ програми комуницирају са мапама, помоћницима кернела, БТФ-ом итд.
Обично пројекти користе libbpf додајте ГитХуб спремиште као гит подмодул, урадићемо исто:
$ 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_PROG_TYPE_XDP, исто као у претходном примеру, али у Ц-у га компајлирамо користећи clang, и напишите помоћни програм који ће га учитати у кернел. У наредним одељцима ћемо проширити могућности и програма БПФ и програма асистента.
Пример: креирање пуноправне апликације користећи либбпф
За почетак користимо датотеку /sys/kernel/btf/vmlinux, који је горе поменут, и креирајте његов еквивалент у облику датотеке заглавља:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.hОва датотека ће чувати све структуре података доступне у нашем кернелу, на пример, овако је дефинисано ИПв4 заглавље у кернелу:
$ 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;
};Сада ћемо написати наш БПФ програм на Ц:
$ 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, који шаље знак у одговарајући одељак ЕЛФ објектне датотеке. Наш програм је садржан у одељку xdp/simple, где пре косе црте дефинишемо тип програма БПФ - ово је конвенција која се користи у libbpf, на основу назива секције ће заменити исправан тип при покретању bpf(2). Сам програм БПФ је C - веома једноставан и састоји се од једног реда return XDP_PASS. На крају, посебан одељак "license" садржи назив лиценце.
Наш програм можемо компајлирати користећи ллвм/цланг, верзија >= 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 нуди нам две опције - користите АПИ нижег нивоа или АПИ вишег нивоа. Ићи ћемо другим путем, пошто желимо да научимо како да пишемо, учитавамо и повезујемо БПФ програме уз минималан напор за њихово накнадно проучавање.
Прво, морамо да генеришемо „костур“ нашег програма из његовог бинарног система користећи исти услужни програм bpftool — швајцарски нож БПФ света (што се може схватити буквално, пошто је Даниел Боркман, један од твораца и одржавалаца БПФ-а, Швајцарац):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.hУ фајлу xdp-simple.skel.h садржи бинарни код нашег програма и функције за управљање - учитавање, причвршћивање, брисање нашег објекта. У нашем једноставном случају ово изгледа као претерано, али такође функционише у случају када објектна датотека садржи много БПФ програма и мапа и да бисмо учитали овај џиновски ЕЛФ, само треба да генеришемо костур и позовемо једну или две функције из прилагођене апликације коју пишу Хајдемо сада.
Строго говорећи, наш програм за учитавање је тривијалан:
#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;
};Овде можемо видети трагове АПИ ниског нивоа: структура struct bpf_program *simple и struct bpf_link *simple. Прва структура посебно описује наш програм, написан у одељку xdp/simple, а други описује како се програм повезује са извором догађаја.
Функција xdp_simple_bpf__open_and_load, отвара ЕЛФ објекат, анализира га, креира све структуре и подструктуре (осим програма, ЕЛФ садржи и друге секције - податке, податке само за читање, информације за отклањање грешака, лиценцу, итд.), а затим га учитава у језгро помоћу система позив 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Нешто ново! Програм је одштампао делове нашег изворног фајла Ц. Ово је урадила библиотека libbpf, који је пронашао одељак за отклањање грешака у бинарном систему, превео га у БТФ објекат, учитао га у кернел користећи BPF_BTF_LOAD, а затим одредио резултујући дескриптор датотеке приликом учитавања програма помоћу команде BPG_PROG_LOAD.
Кернел Хелперс
БПФ програми могу да покрећу „спољне“ функције – помоћнике кернела. Ове помоћне функције омогућавају БПФ програмима да приступе структурама језгра, управљају мапама, а такође и комуницирају са „стварним светом“ – креирају перф догађаје, контролишу хардвер (на пример, преусмеравају пакете) итд.
Пример: бпф_гет_смп_процессор_ид
У оквиру парадигме „учење на примеру примера“, размотримо једну од помоћних функција, bpf_get_smp_processor_id(), у фајлу kernel/bpf/helpers.c. Враћа број процесора на којем је покренут БПФ програм који га је позвао. Али нас не занима толико његова семантика колико чињеница да његова имплементација заузима једну линију:
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_PROG_TYPE_XDP функција је дефинисана у језгру xdp_func_proto, који користи ИД помоћне функције да одреди да ли КСДП подржава ову функцију или не. Наша функција је :
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_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
};То јест, за сваки тип БПФ програма дефинисан је показивач на структуру података тог типа 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_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 - нула. Према АБИ споразуму који користи верификатор, ово је позив помоћној функцији број осам. Једном када се покрене, логика је једноставна. Врати вредност из регистра 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. Не постоје функције које узимају више од пет аргумената, а подршка за њих се не очекује у будућности.
Хајде да погледамо нови помоћник кернела и како БПФ преноси параметре. Хајде да препишемо 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, који повезује БПФ програме типа КСДП са мрежним интерфејсима. Чврсто смо кодирали број интерфејса lo, што је увек 1. Покрећемо функцију два пута да бисмо прво одвојили стари програм ако је био прикључен. Приметите да нам сада није потребан изазов pause или бесконачна петља: наш програм за учитавање ће изаћи, али БПФ програм неће бити уништен пошто је повезан са извором догађаја. Након успешног преузимања и повезивања, програм ће бити покренут за сваки мрежни пакет који стигне 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Програм који смо преузели има ИД 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 и обрађен на ЦПУ0 - наш први пуноправни бесмислени БПФ програм је радио!
Вреди напоменути да bpf_printk Није узалуд што уписује у датотеку за отклањање грешака: ово није најуспешнији помоћник за употребу у производњи, али наш циљ је био да покажемо нешто једноставно.
Приступ мапама из БПФ програма
Пример: коришћење карте из БПФ програма
У претходним одељцима научили смо како да креирамо и користимо мапе из корисничког простора, а сада погледајмо део кернела. Почнимо, као и обично, са примером. Хајде да препишемо наш програм 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 }
]Скоро сви процеси су обрађени на ЦПУ7. Ово нам није важно, главна ствар је да програм ради и да разумемо како да приступимо мапама из БПФ програма - користећи .
Мистични индекс
Дакле, можемо приступити мапи из БПФ програма користећи позиве попут
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, и први ИММ у дескриптор датотеке наше мапе и, ако је једнак, нпр. 0xdeadbeef, онда ћемо као резултат добити инструкцију
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 llОвако се информације о мапи преносе у одређени учитани БПФ програм. У овом случају, мапа се може креирати помоћу BPF_MAP_CREATE, а отвара се помоћу ИД-а BPF_MAP_GET_FD_BY_ID.
Укупно, када се користи libbpf алгоритам је следећи:
- током компилације креирају се записи у табели премештања за везе ка мапама
libbpfотвара књигу ЕЛФ објеката, проналази све коришћене мапе и креира дескрипторе датотека за њих- дескриптори датотека се учитавају у језгро као део инструкције
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
Приликом преузимања ЕЛФ бинарног фајла користећи libbpf Има још много тога, али о томе ћемо разговарати у другим чланцима.
Учитавање програма и мапа без либбпф-а
Као што је обећано, ево примера за читаоце који желе да знају како да креирају и учитају програм који користи мапе, без помоћи libbpf. Ово може бити корисно када радите у окружењу за које не можете да изградите зависности, или уштедите сваки део, или пишете програм као што је , који генерише БПФ бинарни код у ходу.
Да бисмо лакше пратили логику, преписаћемо наш пример у ове сврхе 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 је дефиниција нашег БПФ програма као низа структура struct bpf_insn insns[]. Али пошто користимо програм који имамо у Ц-у, можемо мало варати:
$ 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 (савет: узмите депонију одозго, поново прочитајте одељак са упутствима, отворите и и покушајте да одредите 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. Нажалост, програми као што је КСДП не могу се повезати помоћу системског позива bpfЉуди који су креирали BPF и XDP били су из онлајн заједнице. Linux, што значи да су користили онај који им је био најпознатији (али не за нормалан људи) интерфејс за интеракцију са кернелом: , такође видети . Најједноставнији начин за имплементацију xdp_attach копира код из libbpf, наиме, из датотеке , што смо и урадили, скративши га мало:
Добродошли у свет нетлинк сокета
Отворите тип нетлинк утичнице 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 нисмо учитали информације о типу (БТФ). Али о овоме ћемо више разговарати следећи пут.
Развојни алати
У овом одељку ћемо погледати минимални алат БПФ програмера.
Уопштено говорећи, не треба вам ништа посебно да бисте развили БПФ програме – БПФ ради на било ком пристојном дистрибутивном кернелу, а програми се праве помоћу clang, који се може испоручити из пакета. Међутим, због чињенице да је БПФ у развоју, кернел и алати се стално мењају, ако не желите да пишете БПФ програме користећи старомодне методе од 2019, онда ћете морати да компајлирате
llvm/clangpahole- њено језгро
bpftool
(За референцу: овај одељак и сви примери у чланку су покренути на Debian КСНУМКС).
ллвм/цланг
БПФ је пријатељски са ЛЛВМ-ом и, иако се недавно програми за БПФ могу компајлирати помоћу гцц-а, сав тренутни развој се обавља за ЛЛВМ. Стога, пре свега, направићемо тренутну верзију clang из гит-а:
$ 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 преузето од мене из .)
Нећемо инсталирати програме које смо управо направили, већ ћемо их само додати у њих PATH, на пример:
export PATH="`pwd`/bin:$PATH"(Ово се може додати на .bashrc или у посебну датотеку. Ја лично додајем овакве ствари ~/bin/activate-llvm.sh а кад је потребно то радим . activate-llvm.sh.)
Пахоле и БТФ
Корисност pahole користи се приликом изградње кернела за креирање информација за отклањање грешака у БТФ формату. У овом чланку нећемо улазити у детаље о детаљима БТФ технологије, осим чињенице да је згодна и желимо да је користимо. Дакле, ако намеравате да направите своје језгро, прво направите 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-а се одвијао унутар мрежне заједнице. Linux и стога све промене пре или касније пролазе преко Дејвида Милера, одржаваоца мреже LinuxУ зависности од њихове природе – исправке или нове функције – промене мреже завршавају у једној од две језгре: или . Промене за БПФ се дистрибуирају на исти начин између и , који се затим обједињују у нет и нет-нект, респективно. За више детаља, погледајте и . Зато изаберите кернел на основу вашег укуса и потреба за стабилношћу система на којем тестирате (*-next језгра су најнестабилнија од наведених).
Изнад оквира овог чланка је говорити о томе како управљати конфигурационим датотекама језгра - претпоставља се да или већ знате како то да урадите, или на властитим. Међутим, следећа упутства би требало да буду мање-више довољна да вам дају радни систем који подржава БПФ.
Преузмите један од горњих кернела:
$ 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Омогућите БПФ опције у датотеци .config по сопственом избору (највероватније CONFIG_BPF ће већ бити омогућено пошто га системд користи). Ево листе опција из кернела који се користи за овај чланак:
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, испоручује се као део језгра 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.
Collect 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
...који ће показати које су БПФ функције омогућене у вашем кернелу.
Иначе, претходна команда се може покренути као
# bpftool f p kОво се ради по аналогији са услужним програмима из пакета iproute2, где можемо нпр ip a s eth0 уместо ip addr show dev eth0.
Закључак
БПФ вам омогућава да потујете буву како бисте ефикасно измерили и у ходу променили функционалност језгра. Систем се показао веома успешним, у најбољим традицијама УНИКС-а: једноставан механизам који вам омогућава да (ре)програмирате кернел омогућио је огромном броју људи и организација да експериментишу. И, иако су експерименти, као и развој саме БПФ инфраструктуре, далеко од завршетка, систем већ има стабилан АБИ који вам омогућава да изградите поуздану, и што је најважније, ефикасну пословну логику.
Желео бих да приметим да је, по мом мишљењу, технологија постала толико популарна јер, с једне стране, може играј (архитектура машине се може разумети мање-више за једно вече), а са друге стране решити проблеме који се нису могли (лепо) решити пре њеног појављивања. Ове две компоненте заједно терају људе да експериментишу и сањају, што доводи до појаве све иновативнијих решења.
Овај чланак, иако није нарочито кратак, само је увод у свет БПФ-а и не описује „напредне“ карактеристике и важне делове архитектуре. План за будућност је отприлике овакав: следећи чланак ће бити преглед типова БПФ програма (постоји 5.8 типова програма подржаних у језгру 30), затим ћемо коначно погледати како да напишемо праве БПФ апликације користећи програме за праћење кернела као пример, онда је време за детаљнији курс о БПФ архитектури, праћен примерима БПФ умрежавања и безбедносних апликација.
Претходни чланци у овој серији
Линкови
— документација о БПФ-у од цилија, тачније од Даниела Боркмана, једног од твораца и одржавалаца БПФ-а. Ово је један од првих озбиљних описа, који се разликује од осталих по томе што Данијел тачно зна о чему пише и ту нема грешака. Конкретно, овај документ описује како се ради са БПФ програмима типа КСДП и ТЦ користећи добро познати услужни програм
ipиз пакетаiproute2.— оригинални фајл са документацијом за класични, а затим проширени БПФ. Добро читање ако желите да се удубите у асемблерски језик и техничке архитектонске детаље.
. Ажурира се ретко, али прикладно, како тамо пишу Алексеј Старовоитов (аутор еБПФ) и Андрии Накриико - (одржавалац)
libbpf).. Забавна твитер тема Квентина Монеа са примерима и тајнама коришћења бпфтоол-а.
. Огромна (и још увек одржавана) листа веза са документацијом БПФ-а од Куентин Моннет-а.
Извор: ввв.хабр.цом
