BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Սկզբում կար մի տեխնոլոգիա և այն կոչվում էր BPF։ Մենք նայեցինք նրան նախորդ, այս շարքի Հին Կտակարանի հոդվածը։ 2013 թվականին Ալեքսեյ Ստարովոյտովի և Դանիել Բորքմանի ջանքերով մշակվել և Linux միջուկում ներառվել է դրա կատարելագործված տարբերակը՝ օպտիմիզացված ժամանակակից 64-բիթանոց մեքենաների համար։ Այս նոր տեխնոլոգիան համառոտ կոչվում էր Internal BPF, այնուհետև վերանվանվեց Extended BPF, իսկ այժմ, մի քանի տարի անց, բոլորն այն պարզապես անվանում են BPF:

Կոպիտ ասած, BPF-ն թույլ է տալիս գործարկել կամայական օգտատերերի կողմից տրամադրված կոդ Linux միջուկի տարածքում, և նոր ճարտարապետությունն այնքան հաջող է ստացվել, որ մեզ անհրաժեշտ կլինեն ևս մեկ տասնյակ հոդվածներ՝ նկարագրելու դրա բոլոր հավելվածները: (Միակ բանը, որ մշակողները լավ չեն արել, ինչպես տեսնում եք ստորև բերված կատարողական կոդում, արժանապատիվ լոգո ստեղծելն էր:)

Այս հոդվածը նկարագրում է BPF վիրտուալ մեքենայի կառուցվածքը, միջուկի միջերեսները BPF-ի հետ աշխատելու համար, զարգացման գործիքները, ինչպես նաև առկա հնարավորությունների հակիրճ, շատ հակիրճ ակնարկը, այսինքն. այն ամենը, ինչ մեզ անհրաժեշտ կլինի ապագայում՝ BPF-ի գործնական կիրառությունների ավելի խորը ուսումնասիրության համար:
BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Հոդվածի ամփոփում

Ներածություն BPF ճարտարապետությանը: Նախ, մենք թռչնի հայացքով կանդրադառնանք BPF ճարտարապետությանը և կնշենք հիմնական բաղադրիչները:

BPF վիրտուալ մեքենայի ռեգիստրներ և հրամանատարական համակարգ: Արդեն պատկերացում ունենալով ճարտարապետության մասին, որպես ամբողջություն, մենք նկարագրելու ենք BPF վիրտուալ մեքենայի կառուցվածքը:

BPF օբյեկտների կյանքի ցիկլը, bpffs ֆայլային համակարգը: Այս բաժնում մենք ավելի մանրամասն կանդրադառնանք BPF օբյեկտների կյանքի ցիկլին՝ ծրագրերին և քարտեզներին:

Օբյեկտների կառավարում bpf համակարգի զանգի միջոցով: Համակարգի մասին որոշակի պատկերացում ունենալով՝ մենք վերջապես կանդրադառնանք, թե ինչպես ստեղծել և շահարկել օբյեկտները օգտագործողի տարածությունից՝ օգտագործելով հատուկ համակարգային զանգ. bpf(2).

Пишем программы BPF с помощью libbpf. Իհարկե, դուք կարող եք ծրագրեր գրել՝ օգտագործելով համակարգային զանգ: Բայց դժվար է։ Ավելի իրատեսական սցենարի համար միջուկային ծրագրավորողները մշակեցին գրադարան libbpf. Մենք կստեղծենք հիմնական BPF հավելվածի կմախք, որը մենք կօգտագործենք հաջորդ օրինակներում:

Միջուկի օգնականներ. Այստեղ մենք կիմանանք, թե ինչպես BPF ծրագրերը կարող են մուտք գործել միջուկի օգնական գործառույթներ. գործիք, որը քարտեզների հետ միասին հիմնովին ընդլայնում է նոր BPF-ի հնարավորությունները դասականի համեմատ:

Քարտեզների հասանելիություն BPF ծրագրերից: Այս պահին մենք բավականաչափ կիմանանք՝ հստակ հասկանալու համար, թե ինչպես կարող ենք ստեղծել ծրագրեր, որոնք օգտագործում են քարտեզներ: Եվ եկեք նույնիսկ արագ հայացք նետենք մեծ և հզոր ստուգիչին:

Զարգացման գործիքներ. Օգնության բաժինը, թե ինչպես հավաքել անհրաժեշտ կոմունալ ծառայություններն ու միջուկը փորձերի համար:

Եզրակացություն. Հոդվածի վերջում նրանք, ովքեր կարդում են այսքանը, հաջորդ հոդվածներում կգտնեն մոտիվացնող խոսքեր և տեղի ունենալու հակիրճ նկարագրություն։ Մենք նաև կթվարկենք ինքնաուսուցման մի շարք հղումներ նրանց համար, ովքեր ցանկություն կամ հնարավորություն չունեն շարունակությանը սպասելու։

Ներածություն BPF ճարտարապետությանը

Նախքան սկսենք դիտարկել BPF ճարտարապետությունը, մենք կանդրադառնանք վերջին անգամ (oh): դասական BPF, որը մշակվել է որպես RISC մեքենաների առաջացման պատասխան և լուծել փաթեթների արդյունավետ զտման խնդիրը։ Ճարտարապետությունն այնքան հաջող ստացվեց, որ ծնվելով իննսունականներին Բերկլիի UNIX-ում, այն տեղափոխվեց գոյություն ունեցող օպերացիոն համակարգերի մեծ մասում, գոյատևեց մինչև խելահեղ XNUMX-ական թվականները և դեռ նոր հավելվածներ է գտնում:

Նոր BPF-ը մշակվել է որպես պատասխան 64-բիթանոց մեքենաների, ամպային ծառայությունների և SDN-ի ստեղծման համար գործիքների ավելացող անհրաժեշտության պատասխանին (Sծրագրային-dէֆինացված nմշակում): Ստեղծվել է միջուկային ցանցի ինժեներների կողմից՝ որպես դասական BPF-ի բարելավված փոխարինում, նոր BPF-ը բառացիորեն վեց ամիս անց կիրառություն գտավ Linux համակարգերի հետագծման դժվարին գործում, և այժմ, նրա հայտնվելուց վեց տարի անց, մեզ անհրաժեշտ կլինի մի ամբողջ հաջորդ հոդված՝ պարզապես թվարկել տարբեր տեսակի ծրագրեր:

Զվարճալի նկարներ

Իր հիմքում BPF-ն sandbox վիրտուալ մեքենա է, որը թույլ է տալիս գործարկել «կամայական» կոդ միջուկի տարածքում՝ չվնասելով անվտանգությունը: BPF ծրագրերը ստեղծվում են օգտագործողի տարածքում, բեռնվում են միջուկում և միացված են իրադարձության որոշ աղբյուրին: Իրադարձությունը կարող է լինել, օրինակ, փաթեթի առաքումը ցանցային ինտերֆեյս, միջուկի որոշ ֆունկցիայի գործարկում և այլն: Փաթեթի դեպքում BPF ծրագիրը հասանելի կլինի փաթեթի տվյալներին և մետատվյալներին (կարդալու և, հնարավոր է, գրելու համար՝ կախված ծրագրի տեսակից), միջուկի ֆունկցիան գործարկելու դեպքում՝ արգումենտները. գործառույթը, ներառյալ միջուկի հիշողության ցուցիչները և այլն:

Եկեք ավելի սերտ նայենք այս գործընթացին: Սկսենք, եկեք խոսենք դասական BPF-ի առաջին տարբերության մասին, որի ծրագրերը գրվել են assembler-ում: Նոր տարբերակում ճարտարապետությունն ընդլայնվել է այնպես, որ ծրագրերը կարող են գրվել բարձր մակարդակի լեզուներով, առաջին հերթին, իհարկե, C-ով: Դրա համար մշակվել է llvm-ի backend-ը, որը թույլ է տալիս ստեղծել բայթկոդ BPF ճարտարապետության համար:

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

BPF-ի ճարտարապետությունը նախագծված էր մասամբ ժամանակակից մեքենաների վրա արդյունավետ գործելու համար: Որպեսզի դա գործնականում աշխատի, BPF բայթկոդը, երբ բեռնվում է միջուկ, թարգմանվում է բնիկ կոդի՝ օգտագործելով JIT կոմպիլյատոր կոչվող բաղադրիչը (Just In Tես ինձ). Հաջորդը, եթե հիշում եք, դասական BPF-ում ծրագիրը բեռնվում էր միջուկում և կցվում էր իրադարձության աղբյուրին ատոմային եղանակով՝ մեկ համակարգային զանգի համատեքստում: Նոր ճարտարապետության մեջ դա տեղի է ունենում երկու փուլով. նախ՝ կոդը բեռնվում է միջուկ՝ օգտագործելով համակարգային զանգ։ bpf(2)իսկ հետո, ավելի ուշ, այլ մեխանիզմների միջոցով, որոնք տարբերվում են՝ կախված ծրագրի տեսակից, ծրագիրը կցվում է իրադարձության աղբյուրին:

Այստեղ ընթերցողի մոտ կարող է հարց առաջանալ՝ հնարավո՞ր էր դա։ Ինչպե՞ս է երաշխավորվում նման կոդի կատարման անվտանգությունը: Կատարման անվտանգությունը մեզ երաշխավորում է BPF ծրագրերի բեռնման փուլը, որը կոչվում է ստուգիչ (անգլերեն այս փուլը կոչվում է ստուգիչ, և ես կշարունակեմ օգտագործել անգլերեն բառը).

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Ստուգիչը ստատիկ անալիզատոր է, որն ապահովում է, որ ծրագիրը չի խաթարում միջուկի բնականոն աշխատանքը: Սա, ի դեպ, չի նշանակում, որ ծրագիրը չի կարող խանգարել համակարգի աշխատանքին. BPF ծրագրերը, կախված տեսակից, կարող են կարդալ և վերաշարադրել միջուկի հիշողության բաժինները, ֆունկցիաների արժեքները վերադարձնել, կրճատել, ավելացնել, վերագրել: և նույնիսկ փոխանցել ցանցային փաթեթները: Ստուգիչը երաշխավորում է, որ BPF ծրագրի գործարկումը չի խափանի միջուկը, և որ այն ծրագիրը, որը, ըստ կանոնների, ունի գրելու մուտք, օրինակ՝ ելքային փաթեթի տվյալները, չի կարողանա վերագրել միջուկի հիշողությունը փաթեթից դուրս: Ստուգիչին մի փոքր ավելի մանրամասն կանդրադառնանք համապատասխան բաժնում՝ BPF-ի մյուս բոլոր բաղադրիչներին ծանոթանալուց հետո:

Այսպիսով, ի՞նչ ենք մենք սովորել մինչ այժմ: Օգտագործողը գրում է ծրագիր C-ով, այն բեռնում է միջուկ՝ օգտագործելով համակարգային զանգ bpf(2), որտեղ այն ստուգվում է ստուգիչի կողմից և թարգմանվում բնիկ բայթկոդի։ Այնուհետև նույն կամ մեկ այլ օգտվող ծրագիրը միացնում է իրադարձության աղբյուրին և այն սկսում է գործարկել: Բեռնախցիկի և կապի բաժանումը անհրաժեշտ է մի քանի պատճառներով: Նախ, ստուգիչի գործարկումը համեմատաբար թանկ է, և նույն ծրագիրը մի քանի անգամ ներբեռնելով մենք վատնում ենք համակարգչի ժամանակը: Երկրորդ, կոնկրետ ծրագրի միացման եղանակը կախված է դրա տեսակից, և մեկ տարի առաջ ստեղծված «ունիվերսալ» ինտերֆեյսը կարող է հարմար չլինել նոր տեսակի ծրագրերի համար: (Չնայած այժմ, երբ ճարտարապետությունը դառնում է ավելի հասուն, գաղափար կա միավորել այս ինտերֆեյսը մակարդակով libbpf.)

Ուշադիր ընթերցողը կարող է նկատել, որ մենք դեռ չենք ավարտել նկարները։ Իրոք, վերը նշված բոլորը չեն բացատրում, թե ինչու BPF-ն հիմնովին փոխում է պատկերը՝ համեմատած դասական BPF-ի հետ: Երկու նորամուծություններ, որոնք զգալիորեն ընդլայնում են կիրառելիության շրջանակը, ընդհանուր հիշողության և միջուկի օգնական գործառույթներն օգտագործելու ունակությունն է: BPF-ում ընդհանուր հիշողությունն իրականացվում է այսպես կոչված քարտեզների միջոցով՝ տվյալների ընդհանուր կառուցվածքներ հատուկ API-ով: Նրանք հավանաբար ստացել են այս անունը, քանի որ քարտեզի առաջին տեսակը, որը հայտնվեց, հեշ աղյուսակն էր: Այնուհետև հայտնվեցին զանգվածներ, տեղական (մեկ պրոցեսորի) հեշ աղյուսակներ և տեղական զանգվածներ, որոնման ծառեր, BPF ծրագրերի ցուցիչներ պարունակող քարտեզներ և շատ ավելին: Մեզ համար այժմ հետաքրքիրն այն է, որ BPF ծրագրերն այժմ հնարավորություն ունեն պահպանել վիճակը զանգերի միջև և այն կիսել այլ ծրագրերի և օգտագործողի տարածքի հետ:

Քարտեզները հասանելի են օգտատերերի գործընթացներից՝ օգտագործելով համակարգային զանգ bpf(2), և BPF ծրագրերից, որոնք աշխատում են միջուկում՝ օգտագործելով օգնական գործառույթները: Ավելին, օգնականները գոյություն ունեն ոչ միայն քարտեզների հետ աշխատելու, այլ նաև միջուկի այլ հնարավորություններ մուտք գործելու համար: Օրինակ, BPF ծրագրերը կարող են օգտագործել օգնական ֆունկցիաները՝ փաթեթները այլ ինտերֆեյսներ փոխանցելու, perf իրադարձություններ ստեղծելու, միջուկի կառուցվածքներ մուտք գործելու համար և այլն:

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Ամփոփելով, BPF-ն հնարավորություն է տալիս բեռնել կամայական, այսինքն՝ ստուգիչի կողմից փորձարկված օգտագործողի կոդը միջուկի տարածքում: Այս կոդը կարող է պահպանել վիճակը զանգերի միջև և տվյալների փոխանակում օգտատիրոջ տարածքով, ինչպես նաև ունի մուտք դեպի միջուկի ենթահամակարգեր, որոնք թույլատրվում են այս տեսակի ծրագրի կողմից:

Սա արդեն նման է միջուկի մոդուլների տրամադրած հնարավորություններին, որոնց համեմատ BPF-ն ունի որոշ առավելություններ (իհարկե, կարելի է համեմատել միայն նմանատիպ հավելվածները, օրինակ՝ համակարգի հետագծումը. BPF-ով չես կարող կամայական դրայվեր գրել): Դուք կարող եք նշել մուտքի ավելի ցածր շեմ (որոշ կոմունալ ծառայություններ, որոնք օգտագործում են BPF, օգտվողից չեն պահանջում միջուկի ծրագրավորման հմտություններ կամ ընդհանրապես ծրագրավորման հմտություններ), գործարկման ժամանակի անվտանգություն (ձեռքը բարձրացրեք մեկնաբանություններում նրանց համար, ովքեր չեն խախտում համակարգը գրելիս: կամ թեստավորման մոդուլներ), ատոմականություն. մոդուլները վերաբեռնելիս կա խափանում, և BPF ենթահամակարգը երաշխավորում է, որ ոչ մի իրադարձություն բաց չի թողնվի (արդարության համար ասեմ, որ դա ճիշտ չէ բոլոր տեսակի BPF ծրագրերի համար):

Նման հնարավորությունների առկայությունը BPF-ին դարձնում է միջուկը ընդլայնելու ունիվերսալ գործիք, ինչը հաստատվում է պրակտիկայում. BPF-ին ավելանում են նոր տեսակի ծրագրեր, ավելի ու ավելի մեծ ընկերություններ օգտագործում են BPF մարտական ​​սերվերների վրա 24×7, ավելի ու ավելի։ ստարտափները կառուցում են իրենց բիզնեսը լուծումների վրա, որոնց հիման վրա հիմնված են BPF-ի վրա: BPF-ն օգտագործվում է ամենուր՝ DDoS հարձակումներից պաշտպանվելու, SDN-ի ստեղծման (օրինակ՝ ցանցերի ներդրման համար kubernetes-ի համար), որպես համակարգի հետագծման հիմնական գործիք և վիճակագրություն հավաքող, ներխուժման հայտնաբերման համակարգերում և sandbox համակարգերում և այլն:

Եկեք ավարտենք հոդվածի ակնարկային մասը այստեղ և ավելի մանրամասն նայենք վիրտուալ մեքենային և BPF էկոհամակարգին:

Դիգրեսիա՝ կոմունալ

Որպեսզի կարողանաք գործարկել օրինակները հետևյալ բաժիններում, ձեզ կարող են անհրաժեշտ լինել մի շարք կոմունալ ծառայություններ, առնվազն llvm/clang bpf աջակցությամբ և bpftool. Բաժնում Զարգացման գործիքներ Դուք կարող եք կարդալ կոմունալ ծրագրերի հավաքման հրահանգները, ինչպես նաև ձեր միջուկը: Այս բաժինը տեղադրված է ստորև, որպեսզի չխախտի մեր ներկայացման ներդաշնակությունը:

BPF վիրտուալ մեքենաների գրանցումներ և հրահանգների համակարգ

BPF-ի ճարտարապետությունն ու հրամանատարական համակարգը մշակվել են՝ հաշվի առնելով այն փաստը, որ ծրագրերը գրվելու են C լեզվով և միջուկում բեռնվելուց հետո թարգմանվելու են մայրենի կոդի: Հետևաբար, ռեգիստրների քանակն ու հրամանների հավաքածուն ընտրվել է ժամանակակից մեքենաների հնարավորությունների մաթեմատիկական իմաստով խաչմերուկի աչքով: Բացի այդ, ծրագրերի վրա կիրառվեցին տարբեր սահմանափակումներ, օրինակ, մինչև վերջերս հնարավոր չէր գրել հանգույցներ և ենթածրագրեր, իսկ հրահանգների թիվը սահմանափակվում էր 4096-ով (այժմ արտոնյալ ծրագրերը կարող են բեռնել մինչև միլիոն հրահանգներ):

BPF-ն ունի օգտվողի համար հասանելի տասնմեկ 64-բիթանոց ռեգիստր r0-r10 և ծրագրի հաշվիչ: Գրանցվել r10 պարունակում է շրջանակի ցուցիչ և միայն կարդալու համար: Ծրագրերին հասանելի է 512 բայթանոց կույտը գործարկման ժամանակ և անսահմանափակ քանակությամբ ընդհանուր հիշողություն՝ քարտեզների տեսքով:

BPF ծրագրերին թույլատրվում է գործարկել ծրագրի տիպի միջուկի օգնականների հատուկ հավաքածու և, վերջերս, սովորական գործառույթներ: Յուրաքանչյուր կանչված ֆունկցիա կարող է ընդունել մինչև հինգ արգումենտ՝ փոխանցված ռեգիստրներում r1-r5, և վերադարձի արժեքը փոխանցվում է r0. Երաշխավորված է, որ ֆունկցիայից վերադառնալուց հետո ռեգիստրների պարունակությունը r6-r9 Չի փոխվի։

Ծրագրերի արդյունավետ թարգմանության համար գրանցվում է r0-r11 քանի որ բոլոր աջակցվող ճարտարապետությունները եզակի կերպով քարտեզագրվում են իրական ռեգիստրների վրա՝ հաշվի առնելով ներկայիս ճարտարապետության ABI առանձնահատկությունները: Օրինակ, համար x86_64 գրանցամատյաններ r1-r5, որն օգտագործվում է ֆունկցիայի պարամետրերը փոխանցելու համար, ցուցադրվում են rdi, rsi, rdx, rcx, r8, որոնք օգտագործվում են միացված գործառույթներին պարամետրեր փոխանցելու համար x86_64. Օրինակ, ձախ կողմում գտնվող կոդը թարգմանվում է աջ կողմի կոդը այսպես.

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

Գրանցվել r0 օգտագործվում է նաև ծրագրի կատարման արդյունքը վերադարձնելու և ռեգիստրում r1 ծրագիրը փոխանցվում է կոնտեքստի ցուցիչ - կախված ծրագրի տեսակից, սա կարող է լինել, օրինակ, կառուցվածք struct xdp_md (XDP-ի համար) կամ կառուցվածքը struct __sk_buff (տարբեր ցանցային ծրագրերի համար) կամ կառուցվածք struct pt_regs (տարբեր տեսակի հետագծման ծրագրերի համար) և այլն:

Այսպիսով, մենք ունեինք մի շարք ռեգիստրներ, միջուկի օգնականներ, կույտ, համատեքստի ցուցիչ և ընդհանուր հիշողություն՝ քարտեզների տեսքով: Ոչ թե այս ամենը բացարձակապես անհրաժեշտ է ճանապարհորդության ժամանակ, այլ...

Շարունակենք նկարագրությունը և խոսենք այս օբյեկտների հետ աշխատելու հրամանատարական համակարգի մասին։ Բոլորը (համարյա բոլորը) BPF հրահանգներն ունեն ֆիքսված 64 բիթ չափ: Եթե ​​նայեք 64-բիթանոց Big Endian մեքենայի մեկ հրահանգին, կտեսնեք

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Այստեղ Code - սա հրահանգի կոդավորումն է, Dst/Src համապատասխանաբար ստացողի և աղբյուրի կոդավորումներն են, Off - 16-բիթանոց ստորագրված նահանջ և Imm 32-բիթանոց ստորագրված ամբողջ թիվ է, որն օգտագործվում է որոշ հրահանգներում (նման է cBPF K հաստատունին): Կոդավորում Code ունի երկու տեսակներից մեկը.

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Հրահանգների 0, 1, 2, 3 դասերը սահմանում են հիշողության հետ աշխատելու հրամաններ: Նրանք կոչվում են, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, համապատասխանաբար։ Դասարաններ 4, 7 (BPF_ALU, BPF_ALU64) կազմում է ALU հրահանգների մի շարք: Դասարաններ 5, 6 (BPF_JMP, BPF_JMP32) պարունակում է թռիչքի հրահանգներ:

BPF հրահանգների համակարգի ուսումնասիրության հետագա պլանը հետևյալն է. բոլոր հրահանգները և դրանց պարամետրերը մանրակրկիտ թվարկելու փոխարեն, մենք այս բաժնում կանդրադառնանք մի քանի օրինակների, և դրանցից պարզ կդառնա, թե իրականում ինչպես են գործում հրահանգները և ինչպես: ձեռքով ապամոնտաժել ցանկացած երկուական ֆայլ BPF-ի համար: Հոդվածում հետագայում նյութը համախմբելու համար մենք կհանդիպենք նաև առանձին ցուցումների բաժիններում, որոնք վերաբերում են Verifier-ին, JIT կոմպիլյատորին, դասական BPF-ի թարգմանությանը, ինչպես նաև քարտեզներ ուսումնասիրելիս, զանգահարելու գործառույթներ և այլն:

Երբ մենք խոսում ենք առանձին հրահանգների մասին, մենք կանդրադառնանք հիմնական ֆայլերին bpf.h и bpf_common.h, որոնք սահմանում են BPF հրահանգների թվային կոդերը։ Ճարտարապետությունը ինքնուրույն ուսումնասիրելիս և/կամ վերլուծելով երկուականները, կարող եք իմաստաբանություն գտնել հետևյալ աղբյուրներում՝ դասավորված ըստ բարդության. Ոչ պաշտոնական eBPF սպեկտր, BPF և XDP տեղեկատու ուղեցույց, հրահանգների հավաքածու, Documentation/networking/filter.txt և, իհարկե, Linux-ի սկզբնական կոդում՝ ստուգիչ, JIT, BPF թարգմանիչ:

Օրինակ՝ ապամոնտաժել BPF-ը ձեր գլխում

Դիտարկենք մի օրինակ, որտեղ մենք կազմում ենք ծրագիր readelf-example.c և նայեք ստացված երկուականին: Մենք կբացահայտենք բնօրինակ բովանդակությունը readelf-example.c ստորև՝ երկուական կոդերից նրա տրամաբանությունը վերականգնելուց հետո.

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

Առաջին սյունակը ելքի մեջ readelf նահանջ է, և մեր ծրագիրը, հետևաբար, բաղկացած է չորս հրամաններից.

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

Հրամանի կոդերը հավասար են b7, 15, b7 и 95. Հիշեցնենք, որ ամենաքիչ կարևոր երեք բիթերը հրահանգների դասն են: Մեր դեպքում բոլոր հրահանգների չորրորդ բիթը դատարկ է, ուստի հրահանգների դասերը համապատասխանաբար 7, 5, 7, 5 են: 7-րդ դասը. BPF_ALU64, իսկ 5-ն է BPF_JMP. Երկու դասերի համար էլ հրահանգների ձևաչափը նույնն է (տես վերևում), և մենք կարող ենք վերաշարադրել մեր ծրագիրը այսպես (միևնույն ժամանակ մենք կվերագրենք մնացած սյունակները մարդկային տեսքով).

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

Գործողություն b դաս ALU64 - Ից BPF_MOV. Այն արժեք է վերագրում նպատակակետ ռեգիստրին: Եթե ​​բիթը դրված է s (աղբյուր), ապա արժեքը վերցվում է սկզբնաղբյուր գրանցամատյանից, իսկ եթե, ինչպես մեր դեպքում, այն սահմանված չէ, ապա արժեքը վերցվում է դաշտից. Imm. Այսպիսով, առաջին և երրորդ հրահանգներում մենք կատարում ենք գործողությունը r0 = Imm. Ավելին, JMP դասի 1 գործողությունը BPF_JEQ (ցատկել, եթե հավասար է): Մեր դեպքում, քանի որ քիչ S զրո է, այն համեմատում է աղբյուրի ռեգիստրի արժեքը դաշտի հետ Imm. Եթե ​​արժեքները համընկնում են, ապա անցումը տեղի է ունենում PC + OffՈրտեղ PC, ինչպես միշտ, պարունակում է հաջորդ հրահանգի հասցեն։ Վերջապես, JMP Class 9 Operation-ն է BPF_EXIT. Այս հրահանգը դադարեցնում է ծրագիրը՝ վերադառնալով միջուկ r0. Եկեք նոր սյունակ ավելացնենք մեր աղյուսակում.

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

Մենք կարող ենք սա վերաշարադրել ավելի հարմար ձևով.

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

Եթե ​​հիշենք, թե ինչ կա գրանցամատյանում r1 ծրագիրը միջուկից և ռեգիստրում փոխանցվում է կոնտեքստի ցուցիչ r0 արժեքը վերադարձվում է միջուկ, այնուհետև մենք կարող ենք տեսնել, որ եթե կոնտեքստի ցուցիչը զրո է, ապա մենք վերադարձնում ենք 1, իսկ հակառակ դեպքում՝ 2։ Եկեք ստուգենք, որ ճիշտ ենք՝ նայելով աղբյուրը.

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

Այո, դա անիմաստ ծրագիր է, բայց այն թարգմանվում է ընդամենը չորս պարզ հրահանգներով:

Բացառության օրինակ՝ 16 բայթանոց հրահանգ

Մենք ավելի վաղ նշել էինք, որ որոշ հրահանգներ ավելի քան 64 բիթ են զբաղեցնում: Սա վերաբերում է, օրինակ, հրահանգներին lddw (Կոդ = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — բեռնել կրկնակի բառ դաշտերից ռեգիստրում Imm... Փաստն այն է, որ Imm ունի 32 չափ, իսկ կրկնակի բառը 64 բիթ է, ուստի մեկ 64 բիթանոց հրահանգով ռեգիստրում 64-բիթանոց անմիջական արժեքը բեռնելը չի ​​աշխատի: Դա անելու համար օգտագործվում են երկու կից հրահանգներ՝ դաշտում 64-բիթանոց արժեքի երկրորդ մասը պահելու համար Imm. Օրինակ:

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

Երկուական ծրագրում կա ընդամենը երկու հրահանգ.

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

Մենք նորից կհանդիպենք հրահանգներով lddw, երբ խոսում ենք տեղափոխությունների և քարտեզների հետ աշխատելու մասին։

Օրինակ՝ BPF-ի ապամոնտաժում ստանդարտ գործիքների միջոցով

Այսպիսով, մենք սովորել ենք կարդալ BPF երկուական կոդերը և անհրաժեշտության դեպքում պատրաստ ենք վերլուծել ցանկացած հրահանգ: Այնուամենայնիվ, արժե ասել, որ գործնականում ավելի հարմար և արագ է ծրագրերը ապամոնտաժելը ստանդարտ գործիքների միջոցով, օրինակ.

$ llvm-objdump -d x64.o

Disassembly of section .text:

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

BPF օբյեկտների կյանքի ցիկլը, bpffs ֆայլային համակարգը

(Ես առաջին անգամ սովորեցի այս ենթաբաժնում նկարագրված որոշ մանրամասներ ծոմապահություն Ալեքսեյ Ստարովոյտովը BPF բլոգ.)

BPF օբյեկտները՝ ծրագրերը և քարտեզները, ստեղծվում են օգտագործողի տարածությունից՝ հրամանների միջոցով BPF_PROG_LOAD и BPF_MAP_CREATE համակարգային զանգ bpf(2), մենք կխոսենք այն մասին, թե կոնկրետ ինչպես է դա տեղի ունենում հաջորդ բաժնում: Սա ստեղծում է միջուկի տվյալների կառուցվածքներ և դրանցից յուրաքանչյուրի համար refcount (հղումների քանակը) սահմանվում է մեկ, և ֆայլի նկարագրիչը, որը ցույց է տալիս օբյեկտը, վերադարձվում է օգտագործողին: Բռնակը փակվելուց հետո refcount օբյեկտը կրճատվում է մեկով, իսկ երբ այն հասնում է զրոյի, օբյեկտը ոչնչացվում է:

Եթե ​​ծրագիրը օգտագործում է քարտեզներ, ապա refcount այս քարտեզները ծրագիրը բեռնելուց հետո ավելանում են մեկով, այսինքն. նրանց ֆայլերի նկարագրիչները կարող են փակվել օգտագործողի գործընթացից և դեռևս refcount չի դառնա զրո.

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Ծրագիրը հաջողությամբ բեռնելուց հետո մենք այն սովորաբար կցում ենք ինչ-որ իրադարձությունների գեներատորի: Օրինակ՝ մենք կարող ենք այն տեղադրել ցանցային ինտերֆեյսի վրա՝ մուտքային փաթեթները մշակելու կամ որոշներին միացնելու համար tracepoint միջուկում։ Այս պահին հղման հաշվիչը նույնպես կավելանա մեկով, և մենք կկարողանանք փակել ֆայլի նկարագրիչը բեռնիչ ծրագրում:

Ի՞նչ կպատահի, եթե մենք հիմա անջատենք bootloader-ը: Դա կախված է իրադարձությունների գեներատորի (կեռիկի) տեսակից: Բոլոր ցանցային կեռիկները գոյություն կունենան բեռնիչի ավարտից հետո, դրանք այսպես կոչված գլոբալ կեռիկներն են: Եվ, օրինակ, հետագծային ծրագրերը կթողարկվեն դրանց ստեղծած գործընթացի ավարտից հետո (և հետևաբար կոչվում են տեղական՝ «տեղականից դեպի գործընթաց»): Տեխնիկապես տեղական կեռիկները միշտ ունեն համապատասխան ֆայլի նկարագրիչ օգտվողի տարածքում և, հետևաբար, փակվում են, երբ գործընթացը փակ է, իսկ գլոբալ կեռիկները՝ ոչ: Հետևյալ նկարում, օգտագործելով կարմիր խաչերը, ես փորձում եմ ցույց տալ, թե ինչպես է loader ծրագրի դադարեցումը ազդում օբյեկտների կյանքի վրա տեղական և գլոբալ կեռիկների դեպքում:

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Ինչու՞ կա տարբերություն տեղական և գլոբալ կեռիկների միջև: Որոշ տեսակի ցանցային ծրագրերի գործարկումը իմաստ ունի առանց օգտվողների տարածքի, օրինակ, պատկերացրեք DDoS պաշտպանությունը. բեռնիչը գրում է կանոնները և միացնում BPF ծրագիրը ցանցային ինտերֆեյսին, որից հետո բեռնիչը կարող է գնալ և ինքնասպան լինել: Մյուս կողմից, պատկերացրեք վրիպազերծող հետագծային ծրագիր, որը դուք գրել եք ձեր ծնկներին տասը րոպեում. երբ այն ավարտվի, դուք կցանկանայիք, որ համակարգում աղբ չմնա, և տեղական կեռիկները դա կապահովեն:

Մյուս կողմից, պատկերացրեք, որ ցանկանում եք միանալ միջուկի հետագծային կետին և երկար տարիների վիճակագրություն հավաքել: Այս դեպքում դուք կցանկանայիք լրացնել օգտվողի մասը և ժամանակ առ ժամանակ վերադառնալ վիճակագրությանը: Այս հնարավորությունը տալիս է bpf ֆայլային համակարգը: Դա միայն հիշողության մեջ գտնվող կեղծ ֆայլային համակարգ է, որը թույլ է տալիս ստեղծել ֆայլեր, որոնք հղում են կատարում BPF-ի օբյեկտներին և դրանով իսկ մեծացնում refcount առարկաներ. Դրանից հետո բեռնիչը կարող է դուրս գալ, և նրա ստեղծած օբյեկտները կենդանի կմնան:

BPF փոքրերի համար, մաս առաջին. երկարացված BPF

Bpffs-ում ֆայլերի ստեղծումը, որոնք հղում են կատարում BPF-ի օբյեկտներին, կոչվում է «կապում» (ինչպես հետևյալ արտահայտության մեջ. «գործընթացը կարող է ամրացնել BPF ծրագիր կամ քարտեզ»): BPF օբյեկտների համար ֆայլերի օբյեկտների ստեղծումը իմաստ ունի ոչ միայն տեղական օբյեկտների կյանքը երկարացնելու, այլև գլոբալ օբյեկտների օգտագործման համար. վերադառնալով համաշխարհային DDoS պաշտպանության ծրագրի օրինակին, մենք ցանկանում ենք, որ կարողանանք գալ և նայել վիճակագրությունը: ժամանակ առ ժամանակ.

BPF ֆայլային համակարգը սովորաբար տեղադրված է /sys/fs/bpf, բայց այն կարող է տեղադրվել նաև տեղում, օրինակ, այսպես.

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

Ֆայլային համակարգերի անունները ստեղծվում են հրամանի միջոցով BPF_OBJ_PIN BPF համակարգի զանգ. Պատկերացնելու համար եկեք մի ծրագիր վերցնենք, կազմենք, վերբեռնենք և ամրացնենք bpffs. Մեր ծրագիրը ոչ մի օգտակար բան չի անում, մենք միայն ներկայացնում ենք կոդը, որպեսզի կարողանաք վերարտադրել օրինակը.

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

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

Եկեք կազմենք այս ծրագիրը և ստեղծենք ֆայլային համակարգի տեղական պատճենը bpffs:

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

Հիմա եկեք ներբեռնենք մեր ծրագիրը՝ օգտագործելով կոմունալը bpftool և նայեք ուղեկցող համակարգի զանգերին bpf(2) (որոշ անհամապատասխան տողեր հեռացվել են strace output-ից).

$ 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". Դրանից հետո bootloader ծրագիրը bpftool ավարտեց աշխատանքը, բայց մեր ծրագիրը մնաց միջուկում, չնայած մենք այն չենք կցել ցանցային որևէ ինտերֆեյսի.

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

Մենք կարող ենք ջնջել ֆայլի օբյեկտը սովորաբար unlink(2) և դրանից հետո համապատասխան ծրագիրը կջնջվի.

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

Օբյեկտների ջնջում

Խոսելով օբյեկտների ջնջման մասին, անհրաժեշտ է հստակեցնել, որ այն բանից հետո, երբ մենք անջատեցինք ծրագիրը կեռիկից (իրադարձությունների գեներատոր), ոչ մի նոր իրադարձություն չի գործարկի դրա գործարկումը, այնուամենայնիվ, ծրագրի բոլոր ընթացիկ դեպքերը կավարտվեն սովորական կարգով: .

BPF ծրագրերի որոշ տեսակներ թույլ են տալիս փոխարինել ծրագիրը անմիջապես, այսինքն. ապահովել հաջորդականության ատոմականություն replace = detach old program, attach new program. Այս դեպքում ծրագրի հին տարբերակի բոլոր ակտիվ օրինակները կավարտեն իրենց աշխատանքը, և նոր ծրագրից կստեղծվեն նոր իրադարձությունների մշակիչներ, և այստեղ «ատոմիկությունը» նշանակում է, որ ոչ մի իրադարձություն բաց չի թողնվի:

Ծրագրերի կցում միջոցառումների աղբյուրներին

Այս հոդվածում մենք առանձին չենք նկարագրի ծրագրերը իրադարձությունների աղբյուրներին միացնելը, քանի որ իմաստ ունի դա ուսումնասիրել հատուկ տեսակի ծրագրի համատեքստում: Սմ. օրինակ ստորև, որտեղ մենք ցույց ենք տալիս, թե ինչպես են XDP-ի նման ծրագրերը միացված:

Օբյեկտների մանիպուլյացիա՝ օգտագործելով bpf համակարգի կանչը

BPF ծրագրեր

Բոլոր BPF օբյեկտները ստեղծվում և կառավարվում են օգտագործողի տարածությունից՝ օգտագործելով համակարգային զանգ bpf, ունենալով հետևյալ նախատիպը.

#include <linux/bpf.h>

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

Ահա թիմը cmd տիպի արժեքներից մեկն է enum bpf_cmd, attr — որոշակի ծրագրի պարամետրերի ցուցիչ և size - օբյեկտի չափը ըստ ցուցիչի, այսինքն. սովորաբար սա sizeof(*attr). Միջուկում 5.8 համակարգային զանգը bpf աջակցում է 34 տարբեր հրամաններ և որոշումը union bpf_attr զբաղեցնում է 200 տող: Բայց մեզ դա չպետք է վախեցնի, քանի որ մի քանի հոդվածների ընթացքում մենք կծանոթանանք հրամաններին և պարամետրերին:

Սկսենք թիմից BPF_PROG_LOAD, որը ստեղծում է BPF ծրագրեր - վերցնում է BPF հրահանգների մի շարք և բեռնում այն ​​միջուկում: Բեռնման պահին ստուգիչը գործարկվում է, այնուհետև JIT կոմպիլյատորը և հաջող գործարկումից հետո ծրագրի ֆայլի նկարագրիչը վերադարձվում է օգտվողին: Թե ինչ է կատարվում նրա հետ հետո, մենք տեսանք նախորդ բաժնում BPF օբյեկտների կյանքի ցիկլի մասին.

Այժմ մենք կգրենք հատուկ ծրագիր, որը կբեռնի պարզ BPF ծրագիր, բայց նախ պետք է որոշենք, թե ինչպիսի ծրագիր ենք ուզում բեռնել. մենք պետք է ընտրենք Тип և այս տեսակի շրջանակներում գրիր ծրագիր, որը կանցնի ստուգիչի թեստը։ Սակայն, որպեսզի չբարդացնենք գործընթացը, ահա պատրաստի լուծում. մենք կվերցնենք նման ծրագիր BPF_PROG_TYPE_XDP, որը կվերադարձնի արժեքը XDP_PASS (բաց թողնել բոլոր փաթեթները): BPF assembler-ում այն ​​շատ պարզ է թվում.

r0 = 2
exit

Այն բանից հետո, երբ մենք որոշել ենք որ մենք կբեռնենք, կարող ենք ասել, թե ինչպես ենք դա անելու.

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

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

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

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

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

    for ( ;; )
        pause();
}

Ծրագրում հետաքրքիր իրադարձությունները սկսվում են զանգվածի սահմանմամբ insns - մեր BPF ծրագիրը մեքենայի կոդով: Այս դեպքում BPF ծրագրի յուրաքանչյուր հրահանգ փաթեթավորվում է կառուցվածքում bpf_insn. Առաջին տարր insns համապատասխանում է հրահանգներին r0 = 2, երկրորդ - exit.

Նահանջ. Միջուկը սահմանում է ավելի հարմար մակրոներ մեքենայի կոդերը գրելու և միջուկի վերնագրի ֆայլը օգտագործելու համար tools/include/linux/filter.h մենք կարող էինք գրել

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

Բայց քանի որ BPF ծրագրերը հայրենի կոդով գրելն անհրաժեշտ է միայն միջուկում թեստեր գրելու և BPF-ի մասին հոդվածներ գրելու համար, այս մակրոների բացակայությունը իրականում չի բարդացնում մշակողի կյանքը:

BPF ծրագիրը սահմանելուց հետո անցնում ենք այն միջուկում բեռնելուն: Մեր մինիմալիստական ​​պարամետրերի հավաքածուն attr ներառում է ծրագրի տեսակը, հրահանգների հավաքածուն և քանակը, պահանջվող լիցենզիան և անվանումը "woo", որը մենք օգտագործում ենք՝ ներբեռնելուց հետո համակարգում մեր ծրագիրը գտնելու համար։ Ծրագիրը, ինչպես խոստացել էր, բեռնվում է համակարգ՝ օգտագործելով համակարգային զանգ bpf.

Ծրագրի վերջում մենք հայտնվում ենք անսահման օղակում, որը մոդելավորում է օգտակար բեռը: Առանց դրա, ծրագիրը կսպանվի միջուկի կողմից, երբ փակվի ֆայլի նկարագրիչը, որը մեզ վերադարձրել է համակարգի կանչը: bpf, և մենք դա չենք տեսնի համակարգում։

Դե, մենք պատրաստ ենք փորձարկման։ Եկեք հավաքենք և գործարկենք ծրագիրը տակ straceստուգելու համար, որ ամեն ինչ աշխատում է այնպես, ինչպես պետք է.

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

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

Ամեն ինչ լավ է, bpf(2) 3-րդ բռնակը մեզ վերադարձրեց, և մենք մտանք անսահման օղակի մեջ pause(). Փորձենք համակարգում գտնել մեր ծրագիրը։ Դա անելու համար մենք կգնանք մեկ այլ տերմինալ և կօգտվենք կոմունալից bpftool:

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

Մենք տեսնում ենք, որ համակարգում կա բեռնված ծրագիր woo որի գլոբալ ID-ն 390 է և ներկայումս ընթացքի մեջ է simple-prog կա բաց ֆայլի նկարագրիչ, որը ցույց է տալիս ծրագիրը (և եթե simple-prog կավարտի գործը, ուրեմն woo կվերանա): Ինչպես և սպասվում էր, ծրագիրը woo վերցնում է 16 բայթ՝ երկու հրահանգ, երկուական կոդերի BPF ճարտարապետության մեջ, բայց իր բնիկ ձևով (x86_64) այն արդեն 40 բայթ է: Եկեք նայենք մեր ծրագրին իր սկզբնական տեսքով.

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

ոչ մի անակնկալ. Այժմ եկեք նայենք JIT կոմպիլյատորի կողմից ստեղծված կոդը.

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

ոչ այնքան արդյունավետ exit(2), բայց արդարության համար ասեմ, որ մեր ծրագիրը չափազանց պարզ է, և ոչ տրիվիալ ծրագրերի համար անհրաժեշտ է JIT կոմպիլյատորի կողմից ավելացված նախաբանն ու վերջաբանը։

Քարտեզներ

BPF ծրագրերը կարող են օգտագործել կառուցվածքային հիշողության տարածքներ, որոնք հասանելի են ինչպես մյուս BPF ծրագրերին, այնպես էլ օգտագործողի տարածքում գտնվող ծրագրերին: Այս օբյեկտները կոչվում են քարտեզներ, և այս բաժնում մենք ցույց կտանք, թե ինչպես կարելի է դրանք շահարկել՝ օգտագործելով համակարգային զանգը bpf.

Միանգամից ասենք, որ քարտեզների հնարավորությունները չեն սահմանափակվում միայն ընդհանուր հիշողության հասանելիությամբ։ Կան հատուկ նշանակության քարտեզներ, որոնք պարունակում են, օրինակ, BPF ծրագրերի ցուցիչներ կամ ցանցային միջերեսների ցուցիչներ, perf իրադարձությունների հետ աշխատելու քարտեզներ և այլն: Դրանց մասին այստեղ չենք խոսի՝ ընթերցողին շփոթեցնելու համար։ Բացի սրանից, մենք անտեսում ենք համաժամացման խնդիրները, քանի որ դա կարևոր չէ մեր օրինակների համար: Հասանելի քարտեզների տեսակների ամբողջական ցանկը կարելի է գտնել այստեղ <linux/bpf.h>, և այս բաժնում որպես օրինակ կվերցնենք պատմականորեն առաջին տեսակը՝ հեշ աղյուսակը BPF_MAP_TYPE_HASH.

Եթե ​​դուք ստեղծեք հեշ աղյուսակ, ասենք, C++-ում, կասեք unordered_map<int,long> woo, որը ռուսերեն նշանակում է «Ինձ սեղան է պետք woo անսահմանափակ չափս, որի ստեղները տիպի են int, և արժեքները տեսակն են long« BPF հեշ աղյուսակ ստեղծելու համար մենք պետք է անենք մոտավորապես նույն բանը, բացառությամբ, որ մենք պետք է նշենք աղյուսակի առավելագույն չափը, և ստեղների և արժեքների տեսակները նշելու փոխարեն, մենք պետք է նշենք դրանց չափերը բայթերով: . Քարտեզներ ստեղծելու համար օգտագործեք հրամանը BPF_MAP_CREATE համակարգային զանգ bpf. Եկեք նայենք մի քիչ թե շատ նվազագույն ծրագրի, որը ստեղծում է քարտեզ: Նախորդ ծրագրից հետո, որը բեռնում է BPF ծրագրերը, սա ձեզ պետք է պարզ թվա.

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

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

    for ( ;; )
        pause();
}

Այստեղ մենք սահմանում ենք մի շարք պարամետրեր attr, որում ասում ենք «Ինձ հարկավոր է հեշ աղյուսակ՝ ստեղներով և չափի արժեքներով sizeof(int), որի մեջ կարող եմ առավելագույնը չորս տարր դնել»։ BPF քարտեզներ ստեղծելիս կարող եք նշել այլ պարամետրեր, օրինակ, նույն կերպ, ինչպես ծրագրի օրինակում, մենք նշել ենք օբյեկտի անվանումը որպես "woo".

Եկեք կազմենք և գործարկենք ծրագիրը.

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

Ահա համակարգի զանգը bpf(2) մեզ վերադարձրեց նկարագրիչ քարտեզի համարը 3 այնուհետև ծրագիրը, ինչպես և սպասվում էր, սպասում է համակարգային զանգի հետագա հրահանգների pause(2).

Հիմա եկեք ուղարկենք մեր ծրագիրը հետին պլան կամ բացենք մեկ այլ տերմինալ և նայենք մեր օբյեկտին՝ օգտագործելով կոմունալը bpftool (մենք կարող ենք տարբերել մեր քարտեզը մյուսներից իր անունով).

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

114 թիվը մեր օբյեկտի գլոբալ ID-ն է։ Համակարգի ցանկացած ծրագիր կարող է օգտագործել այս ID-ն՝ հրամանի միջոցով գոյություն ունեցող քարտեզ բացելու համար BPF_MAP_GET_FD_BY_ID համակարգային զանգ bpf.

Այժմ մենք կարող ենք խաղալ մեր հեշ սեղանի հետ: Դիտարկենք դրա բովանդակությունը.

$ sudo bpftool map dump id 114
Found 0 elements

Դատարկ. Եկեք դրա մեջ արժեք դնենք hash[1] = 1:

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

Եկեք նորից նայենք աղյուսակին.

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

Ուռա՜ Մեզ հաջողվեց ավելացնել մեկ տարր. Նկատի ունեցեք, որ դա անելու համար մենք պետք է աշխատենք բայթ մակարդակով, քանի որ bptftool չգիտի, թե ինչ տեսակի են հեշ աղյուսակի արժեքները: (Այս գիտելիքը կարող է փոխանցվել նրան BTF-ի միջոցով, բայց հիմա դրա մասին ավելին):

Ինչպե՞ս է bpftool-ը կարդում և ավելացնում տարրեր: Եկեք նայենք գլխարկի տակ.

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

Սկզբում մենք բացեցինք քարտեզը իր գլոբալ 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:

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

Ինչպես և սպասվում էր, դա շատ պարզ է՝ հրամանը BPF_MAP_GET_FD_BY_ID բացում է մեր քարտեզը ID-ով և հրամանով BPF_MAP_UPDATE_ELEM վերագրանցում է տարրը:

Այսպիսով, մի ծրագրից հեշ աղյուսակ ստեղծելուց հետո մենք կարող ենք կարդալ և գրել դրա բովանդակությունը մեկ այլ ծրագրից: Նկատի ունեցեք, որ եթե մենք կարողացանք դա անել հրամանի տողից, ապա համակարգի ցանկացած այլ ծրագիր կարող է դա անել: Բացի վերը նկարագրված հրամաններից, օգտագործողի տարածքից քարտեզների հետ աշխատելու համար, Հետեւյալ:

  • BPF_MAP_LOOKUP_ELEMԳտեք արժեքը ըստ բանալի
  • BPF_MAP_UPDATE_ELEMթարմացնել/ստեղծել արժեք
  • BPF_MAP_DELETE_ELEMհանել բանալին
  • BPF_MAP_GET_NEXT_KEYԳտեք հաջորդ (կամ առաջին) բանալին
  • BPF_MAP_GET_NEXT_IDթույլ է տալիս անցնել բոլոր գոյություն ունեցող քարտեզները, դա այդպես է աշխատում bpftool map
  • BPF_MAP_GET_FD_BY_IDբացեք գոյություն ունեցող քարտեզը իր գլոբալ ID-ով
  • BPF_MAP_LOOKUP_AND_DELETE_ELEMատոմային կերպով թարմացնել օբյեկտի արժեքը և վերադարձնել հինը
  • BPF_MAP_FREEZEՔարտեզը դարձնել անփոփոխ օգտվողների տարածքից (այս գործողությունը հնարավոր չէ հետարկել)
  • BPF_MAP_LOOKUP_BATCH, BPF_MAP_LOOKUP_AND_DELETE_BATCH, BPF_MAP_UPDATE_BATCH, BPF_MAP_DELETE_BATCHԶանգվածային գործողություններ. Օրինակ, BPF_MAP_LOOKUP_AND_DELETE_BATCH - սա քարտեզից բոլոր արժեքները կարդալու և վերակայելու միակ հուսալի միջոցն է

Այս հրամաններից ոչ բոլորն են աշխատում բոլոր քարտեզների տեսակների համար, բայց ընդհանուր առմամբ, օգտագործողի տարածքից այլ տեսակի քարտեզների հետ աշխատելը միանգամայն նույնն է, ինչ հեշ աղյուսակների հետ աշխատելը:

Հանուն կարգի, եկեք ավարտենք մեր հեշ աղյուսակի փորձերը: Հիշո՞ւմ եք, որ մենք ստեղծել ենք աղյուսակ, որը կարող է պարունակել մինչև չորս բանալի: Ավելացնենք ևս մի քանի տարրեր.

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

Առայժմ այնքան լավ.

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

Փորձենք ավելացնել ևս մեկը.

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

Ինչպես և սպասվում էր, մեզ չհաջողվեց։ Եկեք ավելի մանրամասն նայենք սխալին.

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

Ամեն ինչ լավ է. ինչպես և սպասվում էր, թիմը BPF_MAP_UPDATE_ELEM փորձում է ստեղծել նոր, հինգերորդ, բանալին, բայց խափանում է E2BIG.

Այսպիսով, մենք կարող ենք ստեղծել և բեռնել BPF ծրագրեր, ինչպես նաև ստեղծել և կառավարել քարտեզներ օգտագործողի տարածքից: Այժմ տրամաբանական է նայել, թե ինչպես կարող ենք օգտագործել քարտեզները հենց BPF ծրագրերից: Մենք կարող էինք այս մասին խոսել մեքենայական մակրո կոդերում դժվար ընթեռնելի ծրագրերի լեզվով, բայց իրականում եկել է ժամանակը ցույց տալու, թե ինչպես են իրականում գրվում և պահպանվում BPF ծրագրերը. libbpf.

(Ընթերցողների համար, ովքեր դժգոհ են ցածր մակարդակի օրինակի բացակայությունից. մենք մանրամասն կվերլուծենք ծրագրերը, որոնք օգտագործում են քարտեզներ և օգնական գործառույթներ, որոնք ստեղծված են օգտագործելով libbpf և պատմել ձեզ, թե ինչ է տեղի ունենում հրահանգների մակարդակում: Դժգոհ ընթերցողների համար շատ, ավելացրինք մենք օրինակ հոդվածի համապատասխան տեղում։)

BPF ծրագրեր գրելը libbpf-ի միջոցով

Մեքենայի կոդերի միջոցով BPF ծրագրեր գրելը կարող է հետաքրքիր լինել միայն առաջին անգամ, իսկ հետո սկսվում է հագեցվածությունը: Այս պահին դուք պետք է ձեր ուշադրությունը դարձնեք llvm, որն ունի BPF ճարտարապետության համար կոդ ստեղծելու հետնամաս, ինչպես նաև գրադարան libbpf, որը թույլ է տալիս գրել BPF հավելվածների օգտագործողի կողմը և բեռնել BPF ծրագրերի կոդը, որոնք ստեղծվել են օգտագործելով llvm/clang.

Իրականում, ինչպես կտեսնենք այս և հետագա հոդվածներում, libbpf բավականին մեծ աշխատանք է կատարում առանց դրա (կամ նմանատիպ գործիքների. iproute2, libbcc, libbpf-goև այլն) անհնար է ապրել։ Նախագծի սպանիչ հատկանիշներից մեկը libbpf BPF CO-RE-ն է (Compile Once, Run Everywhere) - նախագիծ, որը թույլ է տալիս գրել BPF ծրագրեր, որոնք շարժական են մի միջուկից մյուսը, տարբեր API-ների վրա աշխատելու ունակությամբ (օրինակ, երբ միջուկի կառուցվածքը փոխվում է տարբերակից: տարբերակին): Որպեսզի կարողանաք աշխատել CO-RE-ի հետ, ձեր միջուկը պետք է կազմված լինի BTF աջակցությամբ (մենք նկարագրում ենք, թե ինչպես դա անել բաժնում Զարգացման գործիքներ. Դուք կարող եք ստուգել՝ արդյոք ձեր միջուկը կառուցված է BTF-ով, թե ոչ շատ պարզ՝ հետևյալ ֆայլի առկայությամբ.

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

Այս ֆայլը պահպանում է միջուկում օգտագործվող տվյալների բոլոր տեսակների մասին տեղեկությունները և օգտագործվում է մեր բոլոր օրինակներում՝ օգտագործելով libbpf. Մենք մանրամասն կխոսենք CO-RE-ի մասին հաջորդ հոդվածում, բայց այս մեկում՝ պարզապես ստեղծեք ձեզ միջուկ CONFIG_DEBUG_INFO_BTF.

գրադարան libbpf ապրում է հենց գրացուցակում tools/lib/bpf միջուկը և դրա մշակումն իրականացվում է փոստային ցուցակի միջոցով [email protected]. Այնուամենայնիվ, միջուկից դուրս ապրող հավելվածների կարիքների համար պահպանվում է առանձին պահեստ https://github.com/libbpf/libbpf որում միջուկի գրադարանը արտացոլված է կարդալու հասանելիության համար, ինչպես կա:

Այս բաժնում մենք կանդրադառնանք, թե ինչպես կարող եք ստեղծել նախագիծ, որն օգտագործում է libbpf, գրենք մի քանի (քիչ թե շատ անիմաստ) թեստային ծրագրեր ու մանրամասն վերլուծենք, թե ինչպես է այդ ամենն աշխատում։ Սա թույլ կտա մեզ ավելի հեշտությամբ բացատրել հաջորդ բաժիններում, թե ինչպես են BPF ծրագրերը փոխազդում քարտեզների, միջուկի օգնականների, BTF-ի և այլնի հետ:

Սովորաբար նախագծերն օգտագործում են libbpf ավելացնել GitHub պահեստը որպես git ենթամոդուլ, մենք նույնը կանենք.

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

Պատրաստվում է libbpf Շատ պարզ:

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

Այս բաժնում մեր հաջորդ պլանը հետևյալն է. մենք կգրենք BPF-ի նման ծրագիր BPF_PROG_TYPE_XDP, նույնը, ինչ նախորդ օրինակում, բայց C-ում մենք այն կազմում ենք օգտագործելով clang, և գրեք օգնական ծրագիր, որը կբեռնի այն միջուկում: Հաջորդ բաժիններում մենք կընդլայնենք ինչպես BPF ծրագրի, այնպես էլ օգնական ծրագրի հնարավորությունները:

Օրինակ՝ ստեղծելով լիարժեք հավելված libbpf-ի միջոցով

Սկսելու համար մենք օգտագործում ենք ֆայլը /sys/kernel/btf/vmlinux, որը նշվեց վերևում և ստեղծեք դրա համարժեքը վերնագրի ֆայլի տեսքով.

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

Այս ֆայլը կպահի մեր միջուկում առկա տվյալների բոլոր կառուցվածքները, օրինակ՝ այսպես է սահմանվում IPv4 վերնագիրը միջուկում.

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

Այժմ մենք կգրենք մեր BPF ծրագիրը C-ով.

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

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

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

Չնայած մեր ծրագիրը շատ պարզ է ստացվել, այնուամենայնիվ պետք է ուշադրություն դարձնել շատ մանրամասների։ Նախ, առաջին վերնագրի ֆայլը, որը մենք ներառում ենք vmlinux.h, որը մենք պարզապես ստեղծել ենք օգտագործելով bpftool btf dump - այժմ մենք կարիք չունենք տեղադրել միջուկի վերնագրերի փաթեթը, որպեսզի պարզենք, թե ինչպիսին են միջուկի կառուցվածքները: Հետևյալ վերնագրի ֆայլը գալիս է մեզ գրադարանից libbpf. Այժմ դա մեզ անհրաժեշտ է միայն մակրո սահմանելու համար SEC, որն ուղարկում է նիշը ELF օբյեկտի ֆայլի համապատասխան բաժին։ Մեր ծրագիրը պարունակվում է բաժնում xdp/simple, որտեղ շեղից առաջ մենք սահմանում ենք ծրագրի տեսակը BPF - սա այն կոնվենցիան է, որն օգտագործվում է libbpf, հիմնվելով բաժնի անվան վրա, այն կփոխարինի ճիշտ տեսակը գործարկման ժամանակ bpf(2). BPF ծրագիրը ինքնին է C - շատ պարզ և բաղկացած է մեկ տողից return XDP_PASS. Վերջապես, առանձին բաժին "license" պարունակում է լիցենզիայի անվանումը.

Մենք կարող ենք մեր ծրագիրը կազմել՝ օգտագործելով llvm/clang, տարբերակ >= 10.0.0 կամ ավելի լավ՝ ավելի մեծ (տես բաժինը Զարգացման գործիքներ):

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

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

Հետաքրքիր առանձնահատկությունների շարքում մենք նշում ենք թիրախային ճարտարապետությունը -target bpf և դեպի վերնագրեր տանող ճանապարհը libbpf, որը մենք վերջերս ենք տեղադրել: Բացի այդ, մի մոռացեք -O2, առանց այս տարբերակի ապագայում կարող եք անակնկալներ ունենալ: Եկեք նայենք մեր կոդը, հասցրե՞լ ենք գրել մեր ուզած ծրագիրը։

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

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

Disassembly of section xdp/simple:

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

Այո՛, ստացվեց։ Այժմ մենք ծրագրի հետ ունենք երկուական ֆայլ և ցանկանում ենք ստեղծել ծրագիր, որը կբեռնի այն միջուկում: Այդ նպատակով գրադարանը libbpf առաջարկում է մեզ երկու տարբերակ՝ օգտագործել ավելի ցածր մակարդակի API կամ ավելի բարձր մակարդակի API: Մենք կգնանք երկրորդ ճանապարհով, քանի որ ցանկանում ենք սովորել, թե ինչպես գրել, բեռնել և միացնել BPF ծրագրերը նվազագույն ջանքերով դրանց հետագա ուսումնասիրության համար:

Նախ, մենք պետք է գեներացնենք մեր ծրագրի «կմախքը» դրա երկուականից՝ օգտագործելով նույն օգտակարությունը bpftool — BPF աշխարհի շվեյցարական դանակը (որը կարելի է բառացիորեն ընկալել, քանի որ Դանիել Բորկմանը, BPF-ի ստեղծողներից և սպասարկողներից մեկը, շվեյցարացի է).

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

Ֆայլում xdp-simple.skel.h պարունակում է մեր ծրագրի երկուական կոդը և կառավարելու գործառույթներ՝ բեռնել, կցել, ջնջել մեր օբյեկտը: Մեր պարզ դեպքում սա կարծես թե գերհոգնածություն է, բայց այն նաև գործում է այն դեպքում, երբ օբյեկտի ֆայլը պարունակում է բազմաթիվ BPF ծրագրեր և քարտեզներ, և այս հսկա ELF-ը բեռնելու համար մենք պարզապես պետք է գեներացնենք կմախքը և կանչենք մեկ կամ երկու գործառույթ մեր հատուկ հավելվածից: գրում են Շարունակենք հիմա.

Խստորեն ասած, մեր բեռնիչ ծրագիրը չնչին է.

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

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

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

    pause();

    xdp_simple_bpf__destroy(obj);
}

Այստեղ struct xdp_simple_bpf սահմանված ֆայլում xdp-simple.skel.h և նկարագրում է մեր օբյեկտի ֆայլը՝

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

Այստեղ մենք կարող ենք տեսնել ցածր մակարդակի API-ի հետքեր՝ կառուցվածքը struct bpf_program *simple и struct bpf_link *simple. Առաջին կառուցվածքը կոնկրետ նկարագրում է մեր ծրագիրը՝ գրված բաժնում xdp/simple, իսկ երկրորդը նկարագրում է, թե ինչպես է ծրագիրը միանում իրադարձության աղբյուրին։

Ֆունկցիա xdp_simple_bpf__open_and_load, բացում է ELF օբյեկտը, վերլուծում է այն, ստեղծում է բոլոր կառուցվածքներն ու ենթակառուցվածքները (բացի ծրագրից, ELF-ը պարունակում է նաև այլ բաժիններ՝ տվյալներ, միայն կարդալու տվյալներ, վրիպազերծման տեղեկատվություն, լիցենզիա և այլն), այնուհետև այն բեռնում է միջուկ՝ օգտագործելով համակարգ։ զանգահարել bpf, որը մենք կարող ենք ստուգել՝ կազմելով և գործարկելով ծրագիրը.

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

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

Այժմ նայենք մեր ծրագրին՝ օգտագործելով bpftool. Եկեք գտնենք նրա 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)

և dump (մենք օգտագործում ենք հրամանի կրճատված ձևը bpftool prog dump xlated):

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

Մի նոր բան! Ծրագիրը տպեց մեր C աղբյուրի ֆայլի կտորները: Դա արվեց գրադարանի կողմից libbpf, որը գտել է վրիպազերծման բաժինը երկուականում, այն հավաքել է BTF օբյեկտի մեջ և բեռնել միջուկ՝ օգտագործելով BPF_BTF_LOAD, և այնուհետև նշեք ստացված ֆայլի նկարագրիչը՝ ծրագիրը հրամանով բեռնելիս BPG_PROG_LOAD.

Միջուկի օգնականներ

BPF ծրագրերը կարող են գործարկել «արտաքին» գործառույթներ՝ միջուկի օգնականներ: Այս օգնական գործառույթները թույլ են տալիս BPF ծրագրերին մուտք գործել միջուկի կառուցվածքներ, կառավարել քարտեզները, ինչպես նաև շփվել «իրական աշխարհի» հետ՝ ստեղծել պերֆ իրադարձություններ, կառավարել սարքավորում (օրինակ՝ վերահղման փաթեթներ) և այլն:

Օրինակ՝ bpf_get_smp_processor_id

«Ուսուցում օրինակով» պարադիգմի շրջանակներում դիտարկենք օգնական գործառույթներից մեկը. bpf_get_smp_processor_id(), որոշակի ֆայլում kernel/bpf/helpers.c. Այն վերադարձնում է պրոցեսորի համարը, որի վրա աշխատում է այն զանգահարած BPF ծրագիրը: Բայց մեզ այնքան էլ չի հետաքրքրում դրա իմաստաբանությունը, որքան այն, որ դրա իրականացումը մեկ տող է վերցնում.

BPF_CALL_0(bpf_get_smp_processor_id)
{
    return smp_processor_id();
}

BPF-ի օգնական ֆունկցիայի սահմանումները նման են Linux համակարգի կանչի սահմանումներին: Այստեղ, օրինակ, սահմանված է մի ֆունկցիա, որը չունի փաստարկներ։ (Մակրո օգտագործելով, սահմանվում է մի ֆունկցիա, որը վերցնում է, ասենք, երեք արգումենտ BPF_CALL_3. Փաստարկների առավելագույն թիվը հինգն է:) Այնուամենայնիվ, սա սահմանման միայն առաջին մասն է: Երկրորդ մասը տիպային կառուցվածքի սահմանումն է struct bpf_func_proto, որը պարունակում է օգնականի ֆունկցիայի նկարագրությունը, որը ստուգիչը հասկանում է.

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

Օգնականի գործառույթների գրանցում

Որպեսզի որոշակի տեսակի BPF ծրագրերն օգտագործեն այս գործառույթը, նրանք պետք է գրանցեն այն, օրինակ՝ տեսակի համար BPF_PROG_TYPE_XDP միջուկում սահմանված է ֆունկցիա xdp_func_proto, որը օգնական ֆունկցիայի ID-ից որոշում է՝ արդյոք XDP-ն աջակցում է այս ֆունկցիան, թե ոչ։ Մեր գործառույթն է աջակցում է:

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

BPF ծրագրերի նոր տեսակները «սահմանված են» ֆայլում include/linux/bpf_types.h օգտագործելով մակրո BPF_PROG_TYPE. Սահմանվում է չակերտներով, քանի որ դա տրամաբանական սահմանում է, իսկ C լեզվի տերմիններով կոնկրետ կոնստրուկցիաների մի ամբողջ շարքի սահմանումը տեղի է ունենում այլ վայրերում: Մասնավորապես, ֆայլում kernel/bpf/verifier.c բոլոր սահմանումները ֆայլից bpf_types.h օգտագործվում են կառուցվածքների զանգված ստեղծելու համար bpf_verifier_ops[]:

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

Այսինքն, BPF ծրագրի յուրաքանչյուր տեսակի համար սահմանվում է տվյալ տեսակի տվյալների կառուցվածքի ցուցիչ struct bpf_verifier_ops, որը սկզբնավորվում է արժեքով _name ## _verifier_ops, այսինքն. xdp_verifier_ops համար xdp. Կառուցվածք xdp_verifier_ops որոշվում է ֆայլում net/core/filter.c հետեւյալ կերպ.

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

Այստեղ մենք տեսնում ենք մեր ծանոթ գործառույթը xdp_func_proto, որը կգործարկի ստուգիչը ամեն անգամ, երբ այն հանդիպի մարտահրավերի ոմանք գործառույթներ BPF ծրագրի ներսում, տես verifier.c.

Եկեք նայենք, թե ինչպես է հիպոթետիկ BPF ծրագիրը օգտագործում գործառույթը bpf_get_smp_processor_id. Դա անելու համար մենք վերագրում ենք ծրագիրը մեր նախորդ բաժնից հետևյալ կերպ.

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

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

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

Խորհրդանիշ bpf_get_smp_processor_id որոշվում է в <bpf/bpf_helper_defs.h> գրադարաններ libbpf ինչպես

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

այն է, bpf_get_smp_processor_id ֆունկցիայի ցուցիչ է, որի արժեքը 8 է, որտեղ 8-ը արժեքն է BPF_FUNC_get_smp_processor_id տեսակ enum bpf_fun_id, որը մեզ համար սահմանված է ֆայլում vmlinux.h (ֆայլ bpf_helper_defs.h միջուկում ստեղծվում է սկրիպտով, ուստի «կախարդական» թվերը լավ են): Այս ֆունկցիան չի ընդունում արգումենտներ և վերադարձնում է տեսակի արժեք __u32. Երբ մենք գործարկում ենք այն մեր ծրագրում, clang առաջացնում է հրահանգ BPF_CALL «ճիշտ տեսակ» Եկեք կազմենք ծրագիրը և նայենք բաժինը xdp/simple:

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

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

Disassembly of section xdp/simple:

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

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

Առաջին տողում մենք տեսնում ենք հրահանգներ call, պարամետր IMM որը հավասար է 8-ի և SRC_REG - զրո. Համաձայն ABI-ի համաձայնագրի, որն օգտագործվում է ստուգիչի կողմից, սա զանգ է դեպի օգնականի գործառույթ թիվ ութերորդը: Երբ այն գործարկվի, տրամաբանությունը պարզ է. Վերադարձի արժեքը գրանցումից r0 պատճենված է r1 իսկ 2,3 տողերում այն ​​փոխակերպվում է տիպի u32 — վերին 32 բիթերը մաքրված են: 4,5,6,7 տողերում մենք վերադարձնում ենք 2 (XDP_PASS) կամ 1 (XDP_DROP) կախված նրանից, թե 0 տողից օգնական ֆունկցիան վերադարձրել է զրո կամ ոչ զրոյական արժեք:

Եկեք փորձարկենք ինքներս մեզ. բեռնեք ծրագիրը և նայեք արդյունքին bpftool prog dump xlated:

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

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

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

Լավ, ստուգիչը գտել է միջուկի ճիշտ օգնականը:

Օրինակ՝ փոխանցել փաստարկներ և վերջապես գործարկել ծրագիրը:

Գործարկման մակարդակի բոլոր օգնական գործառույթներն ունեն նախատիպ

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

Օգնական գործառույթների պարամետրերը փոխանցվում են ռեգիստրներում r1-r5, և արժեքը վերադարձվում է գրանցամատյանում r0. Չկան գործառույթներ, որոնք վերցնում են հինգից ավելի փաստարկներ, և դրանց աջակցությունը ապագայում չի նախատեսվում:

Եկեք նայենք միջուկի նոր օգնականին և թե ինչպես է BPF-ն փոխանցում պարամետրերը: Եկեք վերաշարադրենք xdp-simple.bpf.c հետևյալ կերպ (մնացած տողերը չեն փոխվել).

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

Մեր ծրագիրը տպում է պրոցեսորի համարը, որի վրա այն աշխատում է: Եկեք կազմենք այն և նայենք կոդը.

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

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

0-7 տողերում գրում ենք տողը running on CPU%un, իսկ հետո 8-րդ տողում գործարկում ենք ծանոթը bpf_get_smp_processor_id. 9-12 տողերում մենք պատրաստում ենք օգնական փաստարկները bpf_printk - գրանցամատյաններ r1, r2, r3. Ինչու՞ դրանք երեքն են, և ոչ երկուսը: Որովհետեւ bpf_printkսա մակրո փաթաթա է իրական օգնականի շուրջ bpf_trace_printk, որը պետք է փոխանցի ֆորմատի տողի չափը:

Այժմ ավելացնենք մի քանի տող xdp-simple.cորպեսզի մեր ծրագիրը միանա ինտերֆեյսին lo և իսկապես սկսվեց!

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

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

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

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

cleanup:
    xdp_simple_bpf__destroy(obj);
}

Այստեղ մենք օգտագործում ենք գործառույթը bpf_set_link_xdp_fd, որը միացնում է XDP տիպի BPF ծրագրերը ցանցային ինտերֆեյսներին։ Մենք կոշտ կոդավորեցինք ինտերֆեյսի համարը lo, որը միշտ 1 է: Մենք գործարկում ենք ֆունկցիան երկու անգամ, որպեսզի նախ անջատենք հին ծրագիրը, եթե այն կցված էր: Ուշադրություն դարձրեք, որ այժմ մեզ մարտահրավեր պետք չէ pause կամ անսահման հանգույց. մեր բեռնիչ ծրագիրը դուրս կգա, բայց BPF ծրագիրը չի սպանվի, քանի որ այն միացված է իրադարձության աղբյուրին: Հաջող ներբեռնումից և միացումից հետո ծրագիրը կգործարկվի յուրաքանչյուր ցանցի հասնող փաթեթի համար lo.

Եկեք ներբեռնենք ծրագիրը և նայենք ինտերֆեյսին lo:

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

Մեր ներբեռնած ծրագիրը ունի ID 669 և մենք տեսնում ենք նույն ID-ն ինտերֆեյսի վրա lo. Մենք կուղարկենք մի քանի փաթեթ 127.0.0.1 (խնդրանք + պատասխան).

$ ping -c1 localhost

իսկ հիմա եկեք նայենք վրիպազերծման վիրտուալ ֆայլի բովանդակությանը /sys/kernel/debug/tracing/trace_pipe, որի մեջ bpf_printk գրում է իր ուղերձները.

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

Երկու փաթեթի վրա նկատվել է lo և մշակվել CPU0-ով. մեր առաջին լիարժեք անիմաստ BPF ծրագիրը աշխատեց:

Արժե նշել, որ bpf_printk Իզուր չէ, որ այն գրում է վրիպազերծման ֆայլում. սա արտադրության մեջ օգտագործելու ամենահաջող օգնականը չէ, բայց մեր նպատակն էր պարզ բան ցույց տալ:

Քարտեզներ մուտք գործել BPF ծրագրերից

Օրինակ՝ օգտագործելով քարտեզ BPF ծրագրից

Նախորդ բաժիններում մենք սովորեցինք, թե ինչպես ստեղծել և օգտագործել քարտեզներ օգտագործողի տարածությունից, իսկ հիմա նայենք միջուկի հատվածին: Սկսենք, ինչպես միշտ, օրինակով. Եկեք վերաշարադրենք մեր ծրագիրը xdp-simple.bpf.c հետեւյալ կերպ.

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

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

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

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

    *val += 1;

    return XDP_PASS;
}

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

Ծրագրի սկզբում ավելացրինք քարտեզի սահմանումը wooՍա 8 տարրերից բաղկացած զանգված է, որը պահպանում է նման արժեքներ u64 (C-ում մենք կսահմանենք այնպիսի զանգված, ինչպիսին է u64 woo[8]) Ծրագրում "xdp/simple" մենք ստանում ենք ընթացիկ պրոցեսորի համարը փոփոխականի մեջ key այնուհետև օգտագործելով օգնականի գործառույթը bpf_map_lookup_element ստանում ենք զանգվածի համապատասխան մուտքի ցուցիչ, որը մեծացնում ենք մեկով։ Թարգմանված է ռուսերեն. մենք հաշվարկում ենք վիճակագրություն, թե որ պրոցեսորը մշակել է մուտքային փաթեթները: Փորձենք գործարկել ծրագիրը.

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

Եկեք ստուգենք, որ նա կապված է lo և ուղարկեք մի քանի փաթեթ.

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

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

Հիմա եկեք նայենք զանգվածի բովանդակությանը.

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

Գրեթե բոլոր գործընթացները մշակվել են CPU7-ում: Սա մեզ համար կարևոր չէ, գլխավորն այն է, որ ծրագիրն աշխատում է, և մենք հասկանում ենք, թե ինչպես մուտք գործել քարտեզներ BPF ծրագրերից՝ օգտագործելով хелперов bpf_mp_*.

Առեղծվածային ցուցանիշ

Այսպիսով, մենք կարող ենք մուտք գործել քարտեզ BPF ծրագրից՝ օգտագործելով նման զանգեր

val = bpf_map_lookup_elem(&woo, &key);

որտեղ օգնական ֆունկցիան նման է

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

բայց մենք ցուցիչ ենք փոխանցում &woo անանուն կառույցին struct { ... }...

Եթե ​​նայենք ծրագրի assembler-ին, ապա կտեսնենք, որ արժեքը &woo իրականում սահմանված չէ (տող 4).

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

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

Disassembly of section xdp/simple:

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

և պարունակվում է տեղափոխություններում՝

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

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

Բայց եթե նայենք արդեն բեռնված ծրագրին, կտեսնենք ճիշտ քարտեզի ցուցիչ (տող 4).

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

Այսպիսով, մենք կարող ենք եզրակացնել, որ մեր բեռնիչ ծրագիրը գործարկելու պահին հղումը դեպի &woo փոխարինվեց գրադարանով ինչ-որ բանով libbpf. Սկզբում մենք կանդրադառնանք արդյունքին strace:

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

Մենք դա տեսնում ենք libbpf ստեղծել է քարտեզ woo այնուհետև ներբեռնել ենք մեր ծրագիրը simple. Եկեք մանրամասն նայենք, թե ինչպես ենք մենք բեռնում ծրագիրը.

  • զանգահարել xdp_simple_bpf__open_and_load ֆայլից xdp-simple.skel.h
  • որն առաջացնում է xdp_simple_bpf__load ֆայլից xdp-simple.skel.h
  • որն առաջացնում է bpf_object__load_skeleton ֆայլից libbpf/src/libbpf.c
  • որն առաջացնում է bpf_object__load_xattr - ից libbpf/src/libbpf.c

Վերջին գործառույթը, ի թիվս այլ բաների, կկանչի bpf_object__create_maps, որը ստեղծում կամ բացում է գոյություն ունեցող քարտեզները՝ դրանք վերածելով ֆայլերի նկարագրիչների։ (Այստեղ մենք տեսնում ենք BPF_MAP_CREATE ելքի մեջ strace.) Հաջորդը ֆունկցիան է կանչվում bpf_object__relocate և հենց նա է մեզ հետաքրքրում, քանի որ հիշում ենք մեր տեսածը woo տեղափոխման աղյուսակում։ Ուսումնասիրելով այն՝ մենք ի վերջո հայտնվում ենք ֆունկցիայի մեջ bpf_program__relocate, որը զբաղվում է քարտեզների տեղափոխությամբ:

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

Այսպիսով, մենք ընդունում ենք մեր հրահանգները

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

և դրա աղբյուրի ռեգիստրը փոխարինեք դրանով BPF_PSEUDO_MAP_FDև առաջին IMM-ը մեր քարտեզի ֆայլի նկարագրիչին և, եթե այն հավասար է, օրինակ, 0xdeadbeef, ապա արդյունքում մենք կստանանք հրահանգը

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

Այսպես է քարտեզի տեղեկատվությունը փոխանցվում կոնկրետ բեռնված BPF ծրագրին: Այս դեպքում քարտեզը կարող է ստեղծվել՝ օգտագործելով BPF_MAP_CREATE, և բացվեց ID-ի միջոցով՝ օգտագործելով BPF_MAP_GET_FD_BY_ID.

Ընդամենը, երբ օգտագործվում է libbpf ալգորիթմը հետևյալն է.

  • Կազմման ընթացքում գրանցումներ են ստեղծվում տեղափոխման աղյուսակում՝ քարտեզների հղումների համար
  • libbpf բացում է ELF օբյեկտների գիրքը, գտնում է բոլոր օգտագործված քարտեզները և ստեղծում դրանց համար ֆայլերի նկարագրիչներ
  • ֆայլի նկարագրիչները բեռնվում են միջուկում՝ որպես հրահանգի մաս LD64

Ինչպես կարող եք պատկերացնել, դեռ ավելին է սպասվում, և մենք պետք է նայենք առանցքը: Բարեբախտաբար, մենք թել ունենք. մենք գրել ենք իմաստը BPF_PSEUDO_MAP_FD աղբյուրի գրանցամատյանում և մենք կարող ենք թաղել այն, ինչը մեզ կտանի դեպի բոլոր սրբերի սրբությունը. kernel/bpf/verifier.c, որտեղ տարբերակիչ անուն ունեցող ֆունկցիան փոխարինում է ֆայլի նկարագրիչը տիպի կառուցվածքի հասցեով struct bpf_map:

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

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

(ամբողջական կոդը կարելի է գտնել по ссылке) Այսպիսով, մենք կարող ենք ընդլայնել մեր ալգորիթմը.

  • Ծրագիրը բեռնելիս ստուգիչը ստուգում է քարտեզի ճիշտ օգտագործումը և գրում է համապատասխան կառուցվածքի հասցեն struct bpf_map

ELF երկուականը ներբեռնելիս օգտագործելով libbpf Շատ ավելին է կատարվում, բայց մենք կքննարկենք դա այլ հոդվածներում:

Ծրագրերի և քարտեզների բեռնում առանց libbpf

Ինչպես խոստացել էինք, ահա մի օրինակ ընթերցողների համար, ովքեր ցանկանում են իմանալ, թե ինչպես ստեղծել և բեռնել ծրագիր, որն օգտագործում է քարտեզներ՝ առանց օգնության libbpf. Սա կարող է օգտակար լինել, երբ դուք աշխատում եք այնպիսի միջավայրում, որի համար չեք կարող կախվածություն ստեղծել, կամ պահպանել ամեն մի մասնիկը կամ գրել այնպիսի ծրագիր, ինչպիսին է. ply, որը անմիջապես ստեղծում է BPF երկուական կոդը:

Որպեսզի ավելի հեշտ լինի հետևել տրամաբանությանը, մենք կվերագրենք մեր օրինակը այս նպատակների համար xdp-simple. Այս օրինակում քննարկված ծրագրի ամբողջական և մի փոքր ընդլայնված կոդը կարելի է գտնել այստեղ գեղձ.

Մեր դիմումի տրամաբանությունը հետևյալն է.

  • ստեղծել տիպի քարտեզ BPF_MAP_TYPE_ARRAY օգտագործելով հրամանը BPF_MAP_CREATE,
  • ստեղծել ծրագիր, որն օգտագործում է այս քարտեզը,
  • միացրեք ծրագիրը ինտերֆեյսին lo,

որը թարգմանվում է որպես մարդ

int main(void)
{
    int map_fd, prog_fd;

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

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

    xdp_attach(1, prog_fd);
}

Այստեղ map_create ստեղծում է քարտեզ նույն կերպ, ինչպես մենք արեցինք համակարգային կանչի մասին առաջին օրինակում bpf - «միջուկ, խնդրում եմ ինձ նոր քարտեզ պատրաստիր 8 տարրերից բաղկացած զանգվածի տեսքով __u64 և վերադարձրու ինձ ֆայլի նկարագրիչը»:

static int map_create()
{
    union bpf_attr attr;

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

Ծրագիրը նաև հեշտ է բեռնել.

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

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

Բարդ մասը prog_load սա մեր BPF ծրագրի սահմանումն է՝ որպես կառույցների զանգված struct bpf_insn insns[]. Բայց քանի որ մենք օգտագործում ենք մի ծրագիր, որը մենք ունենք C-ում, մենք կարող ենք մի փոքր խաբել.

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

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

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

Ընդհանուր առմամբ, մենք պետք է գրենք 14 հրահանգներ այնպիսի կառույցների տեսքով, ինչպիսիք են struct bpf_insn (խորհուրդ: վերևից վերցրեք աղբանոցը, նորից կարդացեք հրահանգների բաժինը, բացեք linux/bpf.h и linux/bpf_common.h և փորձիր որոշել struct bpf_insn insns[] ինքնուրույն):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Վարժություն նրանց համար, ովքեր իրենք չեն գրել սա՝ գտնել map_fd.

Մեր հաղորդման մեջ մնացել է ևս մեկ չբացահայտված հատված. xdp_attach. Ցավոք, XDP-ի նման ծրագրերը չեն կարող միանալ համակարգային զանգի միջոցով bpf. Մարդիկ, ովքեր ստեղծել են BPF-ն և XDP-ն առցանց Linux համայնքից էին, ինչը նշանակում է, որ նրանք օգտագործել են իրենց համար առավել ծանոթ մեկը (բայց ոչ նորմալ մարդիկ) միջուկի հետ շփվելու ինտերֆեյս. netlink վարդակներ, տես նաեւ RFC3549- ը. Իրականացման ամենապարզ միջոցը xdp_attach պատճենում է կոդը libbpf, մասնավորապես, ֆայլից netlink.c, ինչ որ արեցինք՝ մի փոքր կրճատելով.

Բարի գալուստ netlink վարդակների աշխարհ

Բացեք netlink վարդակի տեսակը NETLINK_ROUTE:

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

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

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

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

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

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

    *nl_pid = sa.nl_pid;
    return sock;
}

Այս վարդակից կարդում ենք.

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

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

        if (len == 0)
            break;

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

Վերջապես, ահա մեր գործառույթը, որը բացում է վարդակից և հատուկ հաղորդագրություն է ուղարկում դրան, որը պարունակում է ֆայլի նկարագրիչ.

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

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

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

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

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

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

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

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

cleanup:
    close(sock);
    return ret;
}

Այսպիսով, ամեն ինչ պատրաստ է փորձարկման.

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

Տեսնենք, արդյոք մեր ծրագիրը միացել է lo:

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

Եկեք ping ուղարկենք և նայենք քարտեզին.

$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
$ sudo bpftool m dump name woo
key: 00 00 00 00  value: 90 01 00 00 00 00 00 00
key: 01 00 00 00  value: 00 00 00 00 00 00 00 00
key: 02 00 00 00  value: 00 00 00 00 00 00 00 00
key: 03 00 00 00  value: 00 00 00 00 00 00 00 00
key: 04 00 00 00  value: 00 00 00 00 00 00 00 00
key: 05 00 00 00  value: 00 00 00 00 00 00 00 00
key: 06 00 00 00  value: 40 b5 00 00 00 00 00 00
key: 07 00 00 00  value: 00 00 00 00 00 00 00 00
Found 8 elements

Ուռա, ամեն ինչ աշխատում է: Նշենք, ի դեպ, որ մեր քարտեզը կրկին ցուցադրվում է բայթերի տեսքով։ Դա պայմանավորված է նրանով, որ, ի տարբերություն libbpf մենք չենք բեռնել տեսակի տեղեկատվությունը (BTF): Բայց այս մասին ավելի շատ կխոսենք հաջորդ անգամ:

Զարգացման գործիքներ

Այս բաժնում մենք կանդրադառնանք BPF մշակողների նվազագույն գործիքակազմին:

Ընդհանուր առմամբ, ձեզ հատուկ բան պետք չէ BPF ծրագրեր մշակելու համար. BPF-ն աշխատում է ցանկացած պատշաճ բաշխման միջուկի վրա, և ծրագրերը կառուցվում են օգտագործելով clang, որը կարելի է մատակարարել փաթեթից։ Այնուամենայնիվ, հաշվի առնելով այն հանգամանքը, որ BPF-ը մշակման փուլում է, միջուկը և գործիքները անընդհատ փոխվում են, եթե դուք չեք ցանկանում գրել 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
... много времени спустя
$

Այժմ մենք կարող ենք ստուգել, ​​թե արդյոք ամեն ինչ ճիշտ է հավաքվել.

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

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

(Հավաքման հրահանգներ clang իմ կողմից վերցված bpf_devel_QA.)

Մենք չենք տեղադրի մեր նոր ստեղծած ծրագրերը, այլ պարզապես դրանք կավելացնենք PATH, օրինակ `

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

(Սա կարելի է ավելացնել .bashrc կամ առանձին ֆայլ: Անձամբ ես նման բաներ եմ ավելացնում ~/bin/activate-llvm.sh և երբ պետք է ես դա անում եմ . activate-llvm.sh.)

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 ցանցային համայնքում և, հետևաբար, բոլոր փոփոխությունները վաղ թե ուշ անցնում են Դեյվիդ Միլլերի միջոցով՝ Linux ցանցի սպասարկողին: Կախված դրանց բնույթից՝ խմբագրումներից կամ նոր հնարավորություններից, ցանցի փոփոխությունները ընկնում են երկու միջուկներից մեկի մեջ. net կամ net-next. BPF-ի փոփոխությունները բաշխվում են նույն կերպ bpf и bpf-next, որոնք այնուհետև միավորվում են համապատասխանաբար ցանցի և net-next-ի մեջ: Լրացուցիչ մանրամասների համար տե՛ս bpf_devel_QA и netdev-ՀՏՀ. Այսպիսով, ընտրեք միջուկ՝ հիմնված ձեր ճաշակի և համակարգի կայունության կարիքների վրա, որի վրա փորձարկում եք (*-next միջուկները թվարկվածներից ամենաանկայունն են):

Այս հոդվածի շրջանակներից դուրս է խոսել այն մասին, թե ինչպես կառավարել միջուկի կազմաձևման ֆայլերը. ենթադրվում է, որ դուք կամ արդեն գիտեք, թե ինչպես դա անել, կամ պատրաստ է սովորել ինքնուրույն: Այնուամենայնիվ, հետևյալ հրահանգները պետք է քիչ թե շատ բավարար լինեն ձեզ BPF-ով աշխատող համակարգ տրամադրելու համար:

Ներբեռնեք վերը նշված միջուկներից մեկը.

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

Կառուցեք նվազագույն աշխատանքային միջուկի կազմաձև՝

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

Միացնել BPF ընտրանքները ֆայլում .config ձեր ընտրությամբ (ամենայն հավանականությամբ CONFIG_BPF արդեն միացված կլինի, քանի որ systemd-ն օգտագործում է այն): Ահա այս հոդվածի համար օգտագործվող միջուկից ընտրանքների ցանկը.

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

Այնուհետև մենք հեշտությամբ կարող ենք հավաքել և տեղադրել մոդուլներն ու միջուկը (ի դեպ, միջուկը կարող եք հավաքել՝ օգտագործելով նոր հավաքվածը clangավելացնելով CC=clang):

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

և վերագործարկեք նոր միջուկով (ես օգտագործում եմ դրա համար kexec փաթեթից kexec-tools):

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

bpftool

Հոդվածում առավել հաճախ օգտագործվող օգտակարը կլինի օգտակարը bpftool, մատակարարվում է որպես Linux միջուկի մաս: Այն գրված և պահպանվում է BPF մշակողների կողմից BPF ծրագրավորողների համար և կարող է օգտագործվել BPF-ի բոլոր տեսակի օբյեկտների կառավարման համար՝ բեռնել ծրագրեր, ստեղծել և խմբագրել քարտեզներ, ուսումնասիրել BPF էկոհամակարգի կյանքը և այլն: Փաստաթղթեր՝ սկզբնական կոդերի տեսքով, man էջերի համար միջուկում կամ արդեն կազմված, ցանցում.

Այս գրելու պահին bpftool պատրաստ է միայն RHEL-ի, Fedora-ի և Ubuntu-ի համար (տե՛ս, օրինակ. այս շարանը, որը պատմում է փաթեթավորման անավարտ պատմությունը bpftool Դեբիանում): Բայց եթե դուք արդեն կառուցել եք ձեր միջուկը, ապա կառուցեք bpftool կարկանդակի պես հեշտ.

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

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

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

$

(այստեղ ${linux} - սա ձեր միջուկի գրացուցակն է:) Այս հրամանները կատարելուց հետո bpftool կհավաքվի գրացուցակում ${linux}/tools/bpf/bpftool և այն կարող է ավելացվել ուղու վրա (առաջին հերթին օգտագործողին root) կամ պարզապես պատճենեք /usr/local/sbin.

Հավաքել bpftool ավելի լավ է օգտագործել վերջինս clang, հավաքված, ինչպես նկարագրված է վերևում, և ստուգեք, թե արդյոք այն ճիշտ է հավաքվել, օգտագործելով, օրինակ, հրամանը

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

որը ցույց կտա, թե BPF-ի որ հատկանիշներն են միացված ձեր միջուկում:

Ի դեպ, նախորդ հրամանը կարող է գործարկվել որպես

# bpftool f p k

Դա արվում է փաթեթից կոմունալ ծառայությունների անալոգիայով iproute2, որտեղ կարող ենք, օրինակ, ասել ip a s eth0 փոխարենը ip addr show dev eth0.

Ամփոփում

BPF-ն թույլ է տալիս կոշիկ դնել լուին՝ միջուկի ֆունկցիոնալությունը արդյունավետորեն չափելու և թռիչքի ժամանակ փոխելու համար: Համակարգը շատ հաջողակ ստացվեց՝ UNIX-ի լավագույն ավանդույթներով. պարզ մեխանիզմը, որը թույլ է տալիս միջուկը (վեր)ծրագրավորել, թույլ տվեց հսկայական թվով մարդկանց և կազմակերպությունների փորձարկումներ կատարել: Եվ, չնայած փորձերը, ինչպես նաև բուն BPF ենթակառուցվածքի զարգացումը հեռու են ավարտվելուց, համակարգն արդեն ունի կայուն ABI, որը թույլ է տալիս կառուցել հուսալի և ամենակարևորը՝ արդյունավետ բիզնես տրամաբանությունը:

Ցանկանում եմ նշել, որ, իմ կարծիքով, տեխնոլոգիան այնքան տարածված է դարձել, քանի որ, մի կողմից, այն կարող է խաղալու համար (մեքենայի ճարտարապետությունը կարելի է քիչ թե շատ հասկանալ մեկ երեկոյան), իսկ մյուս կողմից լուծել խնդիրներ, որոնք հնարավոր չէ լուծել (գեղեցիկ) մինչև դրա հայտնվելը։ Այս երկու բաղադրիչները միասին ստիպում են մարդկանց փորձարկել և երազել, ինչը հանգեցնում է ավելի ու ավելի նորարար լուծումների ի հայտ գալուն։

Այս հոդվածը, թեև առանձնապես կարճ չէ, բայց միայն BPF-ի աշխարհի ներածություն է և չի նկարագրում ճարտարապետության «առաջադեմ» առանձնահատկությունները և կարևոր մասերը: Առաջիկա պլանը մոտավորապես այսպիսին է. հաջորդ հոդվածը կլինի BPF ծրագրերի տեսակների ակնարկ (5.8 միջուկում կա 30 տիպի ծրագիր), այնուհետև մենք վերջապես կնայենք, թե ինչպես գրել իրական BPF հավելվածներ՝ օգտագործելով միջուկի հետագծման ծրագրերը: Որպես օրինակ, ժամանակն է BPF ճարտարապետության վերաբերյալ ավելի խորը դասընթացի, որին հաջորդում են BPF ցանցերի և անվտանգության հավելվածների օրինակները:

Այս շարքի նախորդ հոդվածները

  1. BPF փոքրերի համար, մաս զրոյական՝ դասական BPF

Հղումներ

  1. BPF և XDP տեղեկատու ուղեցույց — BPF-ի վերաբերյալ փաստաթղթեր թիլիումից, ավելի ճիշտ՝ Դանիել Բորքմանից՝ BPF-ի ստեղծողներից և սպասարկողներից: Սա առաջին լուրջ բնութագրումներից է, որը տարբերվում է մյուսներից նրանով, որ Դանիելը հստակ գիտի, թե ինչի մասին է գրում, և այնտեղ սխալներ չկան։ Մասնավորապես, այս փաստաթուղթը նկարագրում է, թե ինչպես աշխատել XDP և TC տիպերի BPF ծրագրերի հետ՝ օգտագործելով հայտնի կոմունալ ծրագիրը: ip փաթեթից iproute2.

  2. Documentation/networking/filter.txt — բնօրինակ ֆայլ՝ փաստաթղթերով դասական և այնուհետև ընդլայնված BPF-ի համար: Լավ ընթերցանություն է, եթե ցանկանում եք խորանալ անսամբլի լեզվի և տեխնիկական ճարտարապետական ​​մանրամասների մեջ:

  3. Բլոգ BPF-ի մասին Facebook-ից. Այն թարմացվում է հազվադեպ, բայց տեղին, ինչպես գրում են այնտեղ Ալեքսեյ Ստարովոյտովը (eBPF-ի հեղինակ) և Անդրեյ Նակրիկոն (պահպանող): libbpf).

  4. bpftool-ի գաղտնիքները. Քվենտին Մոնեի թվիթերյան զվարճալի թեմա՝ bpftool-ի օգտագործման օրինակներով և գաղտնիքներով:

  5. Սուզվեք BPF-ում. ընթերցանության նյութերի ցանկ. Քվենտին Մոնեի կողմից BPF փաստաթղթերի հղումների հսկայական (և դեռ պահպանված) ցուցակ:

Source: www.habr.com

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