Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

Башында технология болгон жана ал BPF деп аталган. Биз ага карадык мурунку, Бул сериядагы Эски Келишимдеги макала. 2013-жылы Алексей Старовойтов менен Даниел Боркмандын күч-аракети менен анын заманбап 64-бит машиналары үчүн оптималдаштырылган жакшыртылган версиясы иштелип чыгып, Linux ядросуна киргизилген. Бул жаңы технология кыскача Ички 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 архитектурасын карап баштоодон мурун, биз акыркы жолу кайрылабыз (оо). классикалык BPF, ал RISC машиналарынын пайда болушуна жооп катары иштелип чыккан жана пакеттерди эффективдүү чыпкалоо маселесин чечкен. Архитектура ушунчалык ийгиликтүү болуп, XNUMX-жылдары Беркли UNIXте жаралып, ал учурдагы операциялык системалардын көпчүлүгүнө көчүрүлүп, жыйырманчы жылдарга чейин аман калган жана дагы эле жаңы тиркемелерди табууда.

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

Күлкүлүү сүрөттөр

Негизи, BPF бул коопсуздукту бузбастан ядро ​​мейкиндигинде “эркин” кодду иштетүүгө мүмкүндүк берген кумдук виртуалдык машина. BPF программалары колдонуучу мейкиндигинде түзүлүп, ядрого жүктөлөт жана кандайдыр бир окуя булагына туташтырылат. Окуя, мисалы, пакетти тармак интерфейсине жеткирүү, ядронун кандайдыр бир функцияларын ишке киргизүү ж. Пакет болгон учурда, BPF программасы пакеттин маалыматтарына жана метаберилиштерине (окуу жана, балким, программанын түрүнө жараша жазуу үчүн) жетүү мүмкүнчүлүгүнө ээ болот; ядро ​​​​функциясы иштетилген учурда, аргументтери функция, анын ичинде ядро ​​эстутумунун көрсөткүчтөрү ж.б.

Келгиле, бул процессти кененирээк карап чыгалы. Баштоо үчүн, классикалык BPFден биринчи айырмачылык жөнүндө сүйлөшөлү, алар үчүн программалар ассемблерде жазылган. Жаңы версияда программаларды жогорку деңгээлдеги тилдерде, биринчи кезекте, албетте, С тилинде жазуу үчүн архитектура кеңейтилген. Бул үчүн BPF архитектурасы үчүн байт кодду түзүүгө мүмкүндүк берүүчү llvm үчүн бэкэнд иштелип чыккан.

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

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

Бул жерде окурманда суроо пайда болушу мүмкүн: бул мүмкүнбү? Мындай коддун аткарылышынын коопсуздугу кандай кепилденет? Аткаруу коопсуздугу бизге BPF программаларын жүктөө этабы менен кепилдикке алынат (англис тилинде бул этап verifier деп аталат жана мен англисче сөздү колдоно берем):

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

Verifier - бул программа ядронун нормалдуу иштешин бузбоосун камсыз кылган статикалык анализатор. Бул, демек, программа системанын иштешине тоскоол боло албайт дегенди билдирбейт - BPF программалары түрүнө жараша ядро ​​эс тутумунун бөлүмдөрүн окуй жана кайра жаза алат, функциялардын маанилерин кайтара алат, кыркып, кошуп, кайра жаза алат. жана ал тургай, тармак пакеттерин алдыга. Verifier BPF программасын иштетүү өзөктү бузуп албастыгын жана эрежелерге ылайык жазууга мүмкүнчүлүгү бар программа, мисалы, чыгуучу пакеттин маалыматтары, пакеттен тышкары өзөк эстутумун кайра жаза албасына кепилдик берет. BPFтин башка бардык компоненттери менен таанышкандан кийин, биз тиешелүү бөлүмдө текшергичти бир аз кененирээк карап чыгабыз.

Ошентип, биз буга чейин эмнени үйрөндүк? Колдонуучу программаны C тилинде жазат, аны системалык чалуу аркылуу ядрого жүктөйт bpf(2), бул жерде ал текшерүүчү тарабынан текшерилет жана жергиликтүү байткодго которулат. Андан кийин ошол эле же башка колдонуучу программаны окуя булагына туташтырат жана ал аткарыла баштайт. Жүктөө жана туташууну бөлүү бир нече себептерден улам зарыл. Биринчиден, текшергичти иштетүү салыштырмалуу кымбат жана бир эле программаны бир нече жолу жүктөө менен биз компьютердин убактысын текке кетиребиз. Экинчиден, программанын так кантип туташтырылганы анын түрүнөн көз каранды жана бир жыл мурун иштелип чыккан бир “универсалдуу” интерфейс программалардын жаңы түрлөрүнө ылайыктуу болбой калышы мүмкүн. (Азыр архитектура жетилип баратканына карабастан, бул интерфейсти өз деңгээлинде бириктирүү идеясы бар. libbpf.)

Кунт коюп окурман сүрөттөр менен бүтө электигибизди байкашы мүмкүн. Чынында эле, жогоруда айтылгандардын бардыгы эмне үчүн BPF классикалык BPFге салыштырмалуу сүрөттү түп-тамырынан бери өзгөрткөндүгүн түшүндүрбөйт. Колдонуу чөйрөсүн кыйла кеңейткен эки жаңылык – бул жалпы эс тутумдун жана ядронун жардамчы функцияларын колдонуу мүмкүнчүлүгү. BPFде жалпы эс тутум карталар деп аталган нерсени колдонуу менен ишке ашырылат - белгилүү бир API менен бөлүшүлгөн маалымат структуралары. Картанын биринчи түрү хэш таблицасы болгондуктан, алар бул атка ээ болушу мүмкүн. Андан кийин массивдер пайда болду, жергиликтүү (бир CPU) хэш таблицалары жана локалдык массивдер, издөө дарактары, BPF программаларына көрсөткүчтөрдү камтыган карталар жана башкалар. Азыр биз үчүн кызыктуу нерсе, BPF программалары азыр чалуулар ортосундагы абалды сактап, аны башка программалар жана колдонуучу мейкиндиги менен бөлүшүү мүмкүнчүлүгүнө ээ.

Карталарга системалык чалуу аркылуу колдонуучу процесстеринен кирүүгө болот bpf(2), жана жардамчы функцияларды колдонуу менен ядродо иштеген BPF программаларынан. Мындан тышкары, жардамчылар карталар менен иштөө үчүн гана эмес, ядронун башка мүмкүнчүлүктөрүнө жетүү үчүн да бар. Мисалы, BPF программалары пакеттерди башка интерфейстерге жөнөтүү, perf окуяларын түзүү, ядро ​​структураларына жетүү ж.б. үчүн жардамчы функцияларды колдоно алат.

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

Жыйынтыктап айтканда, BPF өз алдынча, башкача айтканда, текшерүүчү тарабынан текшерилген колдонуучу кодун ядро ​​мейкиндигине жүктөө мүмкүнчүлүгүн берет. Бул код чалуулар ортосундагы абалды сактай алат жана колдонуучу мейкиндиги менен маалымат алмаша алат, ошондой эле программанын бул түрү уруксат берген ядронун подсистемаларына кире алат.

Бул ядро ​​модулдары тарабынан берилген мүмкүнчүлүктөргө окшош, аларга салыштырмалуу BPF кээ бир артыкчылыктарга ээ (албетте, сиз окшош тиркемелерди гана салыштыра аласыз, мисалы, системаны издөө - BPF менен ыктыярдуу драйверди жаза албайсыз). Сиз төмөнкү кирүү босогосун белгилей аласыз (BPF колдонгон кээ бир утилиталар колдонуучудан ядро ​​​​программалоо жөндөмүнө ээ болушун талап кылбайт, же жалпысынан программалоо көндүмдөрүн талап кылбайт), иштөө убактысынын коопсуздугу (жазуу учурунда системаны бузбагандар үчүн комментарийлерде колуңузду көтөрүңүз) же тестирлөө модулдары), атомдук - модулдарды кайра жүктөөдө токтоп калуулар болот жана BPF подсистемасы эч кандай окуяларды өткөрүп жибербөөнү камсыздайт (адилеттүүлүк үчүн, бул BPF программаларынын бардык түрлөрү үчүн туура эмес).

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

Келгиле, макаланын серептөө бөлүгүн бул жерден бүтүрүп, виртуалдык машинаны жана BPF экосистемасын кененирээк карап көрөлү.

Дигрессия: коммуналдык кызматтар

Төмөнкү бөлүмдөрдөгү мисалдарды иштетүү үчүн сизге бир катар утилиталар керек болушу мүмкүн, жок дегенде llvm/clang bpf колдоосу менен жана bpftool. Бөлүмүндө Иштеп чыгуу куралдары Сиз утилиталарды, ошондой эле ядроңузду чогултуу боюнча нускамаларды окуй аласыз. Бул бөлүм презентациябыздын гармониясын бузбоо үчүн төмөндө жайгаштырылды.

BPF виртуалдык машина регистрлери жана нускама системасы

BPFтин архитектурасы жана командалык системасы программалар Си тилинде жазыларын жана ядрого жүктөлгөндөн кийин жергиликтүү кодго которуларын эске алуу менен иштелип чыккан. Демек, регистрлердин саны жана командалардын жыйындысы математикалык мааниде заманбап машиналардын мүмкүнчүлүктөрүнүн кесилишин эске алуу менен тандалып алынган. Мындан тышкары, программаларга ар кандай чектөөлөр киргизилген, мисалы, акыркы убакка чейин циклдерди жана подпрограммаларды жазуу мүмкүн эмес болчу, ал эми инструкциялардын саны 4096га чейин чектелген (азыр артыкчылыктуу программалар миллионго чейин нускамаларды жүктөй алат).

BPF он бир колдонуучуга жеткиликтүү 64-бит регистрге ээ r0-r10 жана программалык эсептегич. Каттоо r10 кадр көрсөткүчүн камтыйт жана окуу үчүн гана. Программалар иштөө учурунда 512 байт стекке жана карталар түрүндө чексиз көлөмдөгү жалпы эстутумга кирүү мүмкүнчүлүгүнө ээ.

BPF программаларына программа тибиндеги ядро ​​жардамчыларынын белгилүү бир топтомун жана жакында эле кадимки функцияларды иштетүүгө уруксат берилет. Ар бир чакырылган функция регистрлерде өткөн беш аргументке чейин кабыл алат r1-r5, жана кайтаруу мааниси өткөрүлүп берилет r0. Функциядан кайтып келгенден кийин регистрлердин мазмуну кепилденет r6-r9 Өзгөрбөйт.

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

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

Каттоо r0 ошондой эле программанын аткарылышынын натыйжасын кайтаруу үчүн жана реестрде колдонулат r1 программа контекстке көрсөткүч өткөрүлөт - программанын түрүнө жараша, бул, мисалы, структура болушу мүмкүн struct xdp_md (XDP үчүн) же түзүм struct __sk_buff (ар кандай тармактык программалар үчүн) же түзүмү struct pt_regs (көзөмөл программаларынын ар кандай түрлөрү үчүн) ж.б.

Ошентип, бизде регистрлердин, ядро ​​жардамчыларынын, стектин, контексттик көрсөткүчтүн жана карталар түрүндөгү жалпы эс тутумдун топтому бар болчу. Мунун баары саякатта өтө зарыл эмес, бирок...

Келгиле, сүрөттөөнү уланталы жана бул объекттер менен иштөө үчүн буйрук системасы жөнүндө сүйлөшөлү. баары (дээрлик баары) BPF нускамаларында белгиленген 64 биттик өлчөмү бар. Эгер сиз 64 биттик Big Endian машинасындагы бир нускаманы карасаңыз, көрө аласыз

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

бул Code - бул инструкциянын коддолушу, Dst/Src тиешелүүлүгүнө жараша кабыл алуучунун жана булактын коддору болуп саналат, Off - 16-бит кол чегинүү, жана Imm кээ бир инструкцияларда колдонулган 32 биттик бүтүн сан (cBPF константасы К сыяктуу). Коддоо Code эки түрү бар:

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

0, 1, 2, 3 нускама класстары эс менен иштөө үчүн буйруктарды аныктайт. Алар деп аталат, BPF_LD, BPF_LDX, BPF_ST, BPF_STX, тиешелүүлүгүнө жараша. 4, 7-класстар (BPF_ALU, BPF_ALU64) ALU көрсөтмөлөрүнүн жыйындысын түзөт. 5, 6-класстар (BPF_JMP, BPF_JMP32) секирүү нускамаларын камтыйт.

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

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

Мисал: башыңыздагы BPFди демонтаждоо

Келгиле, программаны түзгөн мисалды карап көрөлү readelf-example.c жана натыйжада бинардык карагыла. Биз оригиналдуу мазмунду ачып беребиз readelf-example.c төмөндө, биз анын логикасын экилик коддордон калыбына келтиргенден кийин:

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

Чыгуудагы биринчи тилке readelf чегинүү болуп саналат жана биздин программа Ошентип, төрт буйруктан турат:

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

Буйрук коддору бирдей b7, 15, b7 и 95. Эсиңизде болсун, эң аз маанилүү үч бит инструкция классы болуп саналат. Биздин учурда, бардык инструкциялардын төртүнчү бити бош, андыктан нускама класстары тиешелүүлүгүнө жараша 7, 5, 7, 5. 7-класс BPF_ALU64, жана 5 болуп саналат BPF_JMP. Эки класс үчүн тең нускама форматы бирдей (жогорудан караңыз) жана биз программабызды ушул сыяктуу кайра жаза алабыз (ошол эле учурда биз адам формасында калган тилкелерди кайра жазабыз):

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

операция b тап ALU64 - аны BPF_MOV. Ал көздөгөн реестрге маани берет. Эгерде бит коюлган болсо s (булак), анда маани баштапкы регистрден алынат, ал эми биздин жагдайдагыдай ал коюлбаса, анда маани талаадан алынат. Imm. Ошентип, биринчи жана үчүнчү нускамаларда биз операцияны жасайбыз r0 = Imm. Андан ары, JMP класс 1 операция болуп саналат BPF_JEQ (бар болсо секирүү). Биздин учурда, биттен бери S нөлгө барабар, ал булак регистринин маанисин талаа менен салыштырат Imm. Эгерде баалуулуктар дал келсе, анда өтүү башталат PC + Offкайда PC, адаттагыдай эле, кийинки нускаманын дарегин камтыйт. Акыр-аягы, JMP класс 9 операция болуп саналат BPF_EXIT. Бул нускама программаны токтотуп, ядрого кайтып келет r0. Биздин таблицага жаңы тилке кошолу:

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

Биз муну ыңгайлуураак формада кайра жаза алабыз:

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

Реестрде эмне бар экенин эстесек r1 программа өзөктөн жана реестрден контекстке көрсөткүч өткөрүлөт r0 маани ядрого кайтарылган болсо, анда биз контексттин көрсөткүчү нөл болсо, анда биз 1, ал эми башка учурда - 2 кайтараарын көрө алабыз. Булакты карап, туура экенибизди текшерели:

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

Ооба, бул маанисиз программа, бирок ал төрт эле жөнөкөй көрсөтмөгө которулат.

Өзгөчө мисал: 16 байт нускама

Кээ бир инструкциялар 64 биттен ашыкты ээлей турганын жогоруда айтканбыз. Бул, мисалы, көрсөтмөлөргө тиешелүү lddw (Код = 0x18 = BPF_LD | BPF_DW | BPF_IMM) — талаалардан кош сөздү реестрге жүктөңүз Imm... Чындыгында ошол Imm өлчөмү 32 жана кош сөз 64 бит, ошондуктан 64 биттик дароо маанини регистрге бир 64 биттик нускамада жүктөө иштебейт. Бул үчүн, талаада 64 биттик маанинин экинчи бөлүгүн сактоо үчүн эки чектеш нускама колдонулат Imm... Мисалы:

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

Бинардык программада эки гана нускама бар:

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

Инструкция менен дагы жолугабыз lddw, биз көчүрүү жана карталар менен иштөө жөнүндө сөз болгондо.

Мисал: стандарттык шаймандарды колдонуу менен BPFди демонтаждоо

Ошентип, биз BPF экилик коддорун окууну үйрөндүк жана зарыл болсо, ар кандай көрсөтмөлөрдү талдап чыгууга даярбыз. Бирок, иш жүзүндө стандарттуу куралдарды колдонуу менен программаларды демонтаждоо ыңгайлуу жана тезирээк экенин айтуу керек, мисалы:

$ llvm-objdump -d x64.o

Disassembly of section .text:

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

BPF объекттеринин жашоо цикли, bpffs файл системасы

(Мен биринчи бул бөлүмдө сүрөттөлгөн кээ бир майда-чүйдөсүнө чейин билдим орозо Алексей Старовойтов BPF блогу.)

BPF объекттери - программалар жана карталар - буйруктарды колдонуу менен колдонуучу мейкиндигинен түзүлөт BPF_PROG_LOAD и BPF_MAP_CREATE системалык чалуу bpf(2), мунун так кандай болору тууралуу кийинки бөлүмдө сүйлөшөбүз. Бул ядро ​​маалымат структураларын жана алардын ар бири үчүн түзөт refcount (маалымат саны) бирге коюлуп, объектти көрсөткөн файлдын дескриптору колдонуучуга кайтарылат. туткасы жабылгандан кийин refcount объект бир азаят, ал эми нөлгө жеткенде объект бузулат.

Эгерде программа карталарды колдонсо, анда refcount бул карталар программа жүктөлгөндөн кийин бир көбөйөт, б.а. алардын файл дескрипторлору колдонуучу процессинен жабылышы мүмкүн жана дагы refcount нөл болбойт:

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

Программаны ийгиликтүү жүктөгөндөн кийин, биз аны адатта кандайдыр бир окуя генераторуна тиркөөбүз. Мисалы, биз келген пакеттерди иштеп чыгуу же айрымдарына туташтыруу үчүн аны тармак интерфейсине коё алабыз tracepoint өзөгүндө. Бул учурда, маалымдама эсептегичи дагы бир көбөйөт жана биз жүктөөчү программасында файлдын дескрипторун жаба алабыз.

Азыр жүктөгүчтү өчүрсөк эмне болот? Бул окуя генераторунун түрүнө жараша болот (илмек). Бардык тармак илгичтери жүктөгүч аяктагандан кийин болот, булар глобалдык илгичтер деп аталат. Жана, мисалы, трек программалары аларды жараткан процесс аяктагандан кийин чыгарылат (ошондуктан "жергиликтүү процесстен процесске" жергиликтүү деп аталат). Техникалык жактан алганда, жергиликтүү илгичтерде ар дайым колдонуучу мейкиндигинде тиешелүү файл дескриптору болот, ошондуктан процесс жабылганда жабылат, бирок глобалдык илгичтер андай эмес. Төмөнкү сүрөттө, кызыл кресттерди колдонуу менен, мен жүктөгүч программасынын токтотулушу локалдык жана глобалдык илгичтер болгон учурда объекттердин иштөө мөөнөтүнө кандай таасир тийгизерин көрсөтүүгө аракет кылам.

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

Эмне үчүн жергиликтүү жана глобалдык илгичтердин ортосунда айырма бар? Тармактык программалардын кээ бир түрлөрүн иштетүү колдонуучу мейкиндигисиз эле акылга сыярлык, мисалы, DDoS коргоону элестетиңиз - жүктөгүч эрежелерди жазат жана BPF программасын тармак интерфейсине туташтырат, андан кийин жүктөгүч барып өзүн өлтүрө алат. Башка жагынан алганда, он мүнөттө тизеңизге жазган мүчүлүштүктөрдү аныктоо программасын элестетиңиз - ал аяктагандан кийин, системада таштанды калбасын каалайсыз жана жергиликтүү илгичтер муну камсыздайт.

Башка жагынан алып караганда, сиз ядродогу траспорттук пунктка туташып, көп жылдар бою статистикалык маалыматтарды чогултууну каалап жатканыңызды элестетиңиз. Бул учурда, сиз колдонуучу бөлүгүн бүтүрүп, мезгил-мезгили менен статистикага кайтып келүүнү каалайсыз. bpf файл системасы бул мүмкүнчүлүк берет. Бул BPF объектилерине шилтеме берген файлдарды түзүүгө жана ошону менен көбөйтүүгө мүмкүндүк берген эс тутумдагы гана псевдо-файл системасы. refcount объектилер. Андан кийин жүктөгүч чыга алат жана ал жараткан объекттер тирүү бойдон кала берет.

Кичинекейлер үчүн BPF, биринчи бөлүк: кеңейтилген BPF

BPF объекттерине шилтеме кылган bpff файлдарын түзүү "кадоо" деп аталат (төмөнкү сөз айкашындагыдай: "процесс BPF программасын же картаны кадап алат"). BPF объектилери үчүн файлдык объекттерди түзүү локалдык объекттердин иштөө мөөнөтүн узартуу үчүн гана эмес, ошондой эле глобалдык объекттердин колдонууга ыңгайлуулугу үчүн мааниси бар - глобалдык DDoS коргоо программасы менен мисалга кайрылып, биз келип, статистиканы карап көргүбүз келет. анда санда.

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

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

Файл системасынын аттары буйрукту колдонуу менен түзүлөт BPF_OBJ_PIN BPF системасы чалуу. Мисал үчүн, келгиле, программаны алып, аны компиляциялап, жүктөп, кадап көрөлү bpffs. Биздин программа пайдалуу эч нерсе кылбайт, биз кодду гана сунуштап жатабыз, ошондуктан сиз мисалды кайра чыгара аласыз:

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

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

Келгиле, бул программаны компиляциялап, файл тутумунун жергиликтүү көчүрмөсүн түзөлү bpffs:

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

Эми утилитаны колдонуп программабызды жүктөп алалы bpftool жана коштолгон системалык чалууларды караңыз bpf(2) (кээ бир тиешеси жок сызыктар strace чыгаруудан алынып салынды):

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

Бул жерде биз программаны колдонуп жүктөгөнбүз BPF_PROG_LOAD, ядродон файлдын дескрипторун алды 3 жана буйрукту колдонуу BPF_OBJ_PIN бул файлдын дескрипторун файл катары кадап койду "bpf-mountpoint/test". Андан кийин, жүктөөчү программа bpftool иштеп бүттү, бирок программабыз ядродо калды, бирок биз аны эч кандай тармак интерфейсине тиркөө элек:

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

Биз файл объектисин кадимкидей өчүрө алабыз unlink(2) жана андан кийин тиешелүү программа жок кылынат:

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

Объекттерди жок кылуу

Объекттерди жок кылуу жөнүндө айта турган болсок, биз программаны илгичтен (окуянын генератору) ажыраткандан кийин бир дагы жаңы окуя анын ишке киришине түрткү бербестигин тактоо керек, бирок программанын бардык учурдагы инстанциялары кадимки тартипте аяктайт. .

BPF программаларынын кээ бир түрлөрү программаны тез арада алмаштырууга мүмкүндүк берет, б.а. ырааттуу атомдуулугун камсыз кылуу replace = detach old program, attach new program. Бул учурда, программанын эски версиясынын бардык активдүү инстанциялары өз ишин бүтүрөт жана жаңы программадан жаңы окуяларды иштеткичтер түзүлөт жана бул жерде "атомдук" бир дагы окуя өткөрүлбөйт дегенди билдирет.

Окуя булактарына программаларды тиркөө

Бул макалада биз программаларды окуя булактарына туташтырууну өзүнчө сүрөттөбөйбүз, анткени муну белгилүү бир программанын контекстинде изилдөө мааниси бар. См. мисал төмөндө, анда биз XDP сыяктуу программалар кантип туташтырылганын көрсөтөбүз.

bpf тутумунун чалуусунун жардамы менен объекттерди башкаруу

BPF программалары

Бардык BPF объекттери системалык чалуу аркылуу колдонуучу мейкиндигинен түзүлөт жана башкарылат bpf, төмөнкү прототиби бар:

#include <linux/bpf.h>

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

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

Келиңиз, командадан баштайлы BPF_PROG_LOAD, BPF программаларын түзүүчү - BPF инструкцияларынын топтомун алат жана аны ядрого жүктөйт. Жүктөө учурунда текшерүүчү ишке киргизилет, андан кийин JIT компилятору жана ийгиликтүү аткарылгандан кийин программа файлынын дескриптору колдонуучуга кайтарылат. Андан кийин эмне болорун мурунку бөлүмдө көрдүк BPF объекттеринин жашоо цикли жөнүндө.

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

r0 = 2
exit

Биз чечкенден кийин ошол биз жүктөйбүз, муну кантип кыла аларыбызды айта алабыз:

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

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

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

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

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

    for ( ;; )
        pause();
}

Программадагы кызыктуу окуялар массивди аныктоодон башталат insns - машина кодундагы биздин BPF программабыз. Бул учурда, BPF программасынын ар бир инструкциясы түзүмгө камтылган bpf_insn. Биринчи элемент insns көрсөтмөлөргө ылайык келет r0 = 2, экинчи - exit.

Артка чегинүү. Ядро машина коддорун жазуу жана ядронун баш файлын колдонуу үчүн ыңгайлуу макросторду аныктайт tools/include/linux/filter.h жаза алабыз

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

Бирок BPF программаларын жергиликтүү коддо жазуу ядродогу тесттерди жана BPF жөнүндө макалаларды жазуу үчүн гана керек болгондуктан, бул макростордун жоктугу иштеп чыгуучунун жашоосун чындап эле татаалдаштырбайт.

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

Программанын аягында биз пайдалуу жүктү окшоштурган чексиз циклге туш болобуз. Ансыз, системалык чалуу бизге кайтарылган файлдын дескриптору жабылганда программа ядро ​​тарабынан өлтүрүлөт. bpf, жана биз аны системада көрбөйбүз.

Ооба, биз сыноого даярбыз. Келгиле, программаны чогултуп, иштетели straceбаары талаптагыдай иштеп жатканын текшерүү үчүн:

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

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

Баары жайында, bpf(2) 3 тутканы бизге кайтарып берди жана биз чексиз циклге кирдик pause(). Биздин программаны системадан табууга аракет кылалы. Бул үчүн биз башка терминалга барып, утилитаны колдонобуз bpftool:

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

Системада жүктөлгөн программа бар экенин көрүп жатабыз woo анын глобалдык ID 390 жана учурда аткарылып жатат simple-prog программаны көрсөткөн ачык файл дескриптору бар (жана эгерде simple-prog анда ишти бүтүрөт woo жок болот). Күтүлгөндөй, программа woo BPF архитектурасында бинардык коддордун 16 байт - эки нускамасын алат, бирок анын түпкү түрүндө (x86_64) ал 40 байт. Келгиле, программабызды баштапкы түрүндө карап көрөлү:

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

сюрприз жок. Эми JIT компилятору тарабынан түзүлгөн кодду карап көрөлү:

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

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

Карталар

BPF программалары башка BPF программаларына да, колдонуучу мейкиндигиндеги программаларга да жеткиликтүү болгон структураланган эс аймактарын колдоно алат. Бул объекттер карталар деп аталат жана бул бөлүмдө биз аларды системалык чакыруу аркылуу кантип башкарууну көрсөтөбүз bpf.

Карталардын мүмкүнчүлүктөрү жалпы эстутумга жетүү менен гана чектелбейт деп дароо айталы. Атайын максаттагы карталар бар, мисалы, BPF программаларына көрсөткүчтөрдү же тармактык интерфейстерге көрсөткүчтөрдү, перф окуялары менен иштөө үчүн карталарды ж.б. Окурмандын башын айлантпоо үчүн бул жерде алар тууралуу сөз кылбайбыз. Мындан тышкары, биз синхрондоштуруу маселелерине көңүл бурбайбыз, анткени бул биздин мисалдар үчүн маанилүү эмес. Жеткиликтүү карта түрлөрүнүн толук тизмеси менен тапса болот <linux/bpf.h>, жана бул бөлүмдө биз мисал катары тарыхый биринчи түрүн, хэш таблицасын алабыз BPF_MAP_TYPE_HASH.

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

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

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

    for ( ;; )
        pause();
}

Бул жерде биз параметрлердин топтомун аныктайбыз attr, анда биз "Мага ачкычтары жана өлчөмдөрү бар хэш таблицасы керек sizeof(int), анда мен эң көп төрт элементти сала алам." BPF карталарын түзүүдө сиз башка параметрлерди көрсөтсөңүз болот, мисалы, программадагы мисалдагыдай эле, биз объекттин атын төмөнкүдөй көрсөттүк. "woo".

Келгиле, программаны компиляциялап, иштетели:

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

Бул жерде системалык чалуу bpf(2) бизге дескриптор картасынын номерин кайтарды 3 жана андан кийин программа, күтүлгөндөй, тутумдук чакырууда кошумча көрсөтмөлөрдү күтөт pause(2).

Эми программабызды фонго жөнөтөлү же башка терминалды ачып, утилитаны колдонуп объектибизди карап көрөлү bpftool (биздин картаны аты менен башкалардан айырмалай алабыз):

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

114 саны биздин объекттин глобалдык 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

Адегенде биз картаны анын глобалдык идентификатору боюнча буйрукту колдонуп ачтык BPF_MAP_GET_FD_BY_ID и bpf(2) бизге 3 дескрипторду кайтарды. Андан ары команданы колдонуу BPF_MAP_GET_NEXT_KEY өтүү менен таблицадагы биринчи ачкычты таптык NULL "мурунку" ачкыч үчүн көрсөткүч катары. Ачкыч бизде болсо, жасай алабыз BPF_MAP_LOOKUP_ELEMкөрсөткүчкө маани кайтарат value. Кийинки кадам - ​​учурдагы ачкычка көрсөткүчтү өткөрүү менен кийинки элементти табууга аракет кылуу, бирок биздин таблицада бир гана элемент жана буйрук бар BPF_MAP_GET_NEXT_KEY кайтып келет ENOENT.

Макул, келгиле, маанини 1-ачкыч менен өзгөртөлү, бизнес логикабыз каттоодон өтүүнү талап кылат дейли hash[1] = 2:

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

Күтүлгөндөй, бул абдан жөнөкөй: буйрук BPF_MAP_GET_FD_BY_ID ID жана буйрук менен картабызды ачат BPF_MAP_UPDATE_ELEM элементтин үстүнөн жазат.

Ошентип, бир программадан хэш таблицасын түзгөндөн кийин, биз анын мазмунун башка программадан окуп жана жаза алабыз. Эскертүү, эгерде биз муну буйрук сабынан жасай алсак, анда системадагы башка программа муну жасай алат. Жогоруда сүрөттөлгөн буйруктардан тышкары, колдонуучу мейкиндигинен карталар менен иштөө үчүн, кийинки:

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

Бул буйруктардын баары картанын бардык түрлөрү үчүн иштебейт, бирок жалпысынан колдонуучу мейкиндигинен карталардын башка түрлөрү менен иштөө хэш таблицалары менен иштөөгө окшош.

Тартип үчүн, хэш таблицасы боюнча эксперименттерибизди бүтүрөлү. Биз төрт ачкычка чейин камтыган таблицаны түзгөнүбүздү унутпайсызбы? Дагы бир нече элементтерди кошолу:

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

Азырынча жакшы:

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

Дагы бирөөнү кошууга аракет кылалы:

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

Күтүлгөндөй, биз ийгиликке жеткен жокпуз. Келгиле, катаны кененирээк карап көрөлү:

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

Баары жакшы: күтүлгөндөй, команда BPF_MAP_UPDATE_ELEM жаңы, бешинчи, ачкыч түзүүгө аракет кылат, бирок бузулат E2BIG.

Ошентип, биз BPF программаларын түзүп, жүктөй алабыз, ошондой эле колдонуучу мейкиндигинен карталарды түзүп, башкара алабыз. Эми BPF программаларынын карталарын кантип колдоно аларыбызды карап чыгуу логикалык. Биз бул тууралуу машина макрокоддорундагы окууга кыйын программалардын тилинде айтсак болот, бирок чындыгында BPF программалары чындыгында кантип жазыларын жана сакталып жатканын көрсөтүүгө убакыт келди. libbpf.

(Төмөнкү деңгээлдеги мисалдын жоктугуна нааразы болгон окурмандар үчүн: биз карталарды колдонгон программаларды жана жардамчы функцияларды кеңири талдап чыгабыз. libbpf жана нускама деңгээлинде эмне болуп жатканын айтып бериңиз. Нааразы болгон окурмандар үчүн абдан катуу, коштук мисал макаланын тиешелүү жеринде.)

libbpf аркылуу BPF программаларын жазуу

Машина коддору менен BPF программаларын жазуу биринчи жолу гана кызыктуу болушу мүмкүн, андан кийин токчулук башталат. Бул учурда сиз көңүлүңүздү бурушуңуз керек llvm, ал BPF архитектурасы үчүн кодду түзүү үчүн серверге, ошондой эле китепканага ээ libbpf, бул сизге BPF тиркемелеринин колдонуучу тарабын жазууга жана BPF программаларынын кодун жүктөөгө мүмкүндүк берет. llvm/clang.

Чындыгында, биз ушул жана кийинки макалаларда көрөбүз. libbpf ансыз көп иштерди аткарат (же ушуга окшош куралдар - iproute2, libbcc, libbpf-goж.б.) жашоого мүмкүн эмес. Долбоордун коркунучтуу өзгөчөлүктөрүнүн бири libbpf бул BPF CO-RE (Бир жолу Compile, Run Everywhere) - ар кандай API'лерде иштөө мүмкүнчүлүгү менен (мисалы, ядронун структурасы версиядан өзгөргөндө) бир ядродон экинчисине көчмө BPF программаларын жазууга мүмкүндүк берген долбоор. версияга). CO-RE менен иштей алуу үчүн, ядроңуз BTF колдоосу менен түзүлүшү керек (биз муну кантип жасоону бөлүмдө сүрөттөп беребиз. Иштеп чыгуу куралдары. Сиз өзүңүздүн ядроңуз BTF менен курулганын же жөнөкөй эмес экендигин төмөнкү файлдын болушу менен текшере аласыз:

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

Бул файл ядродо колдонулган бардык маалымат түрлөрү жөнүндө маалыматты сактайт жана биздин бардык мисалдарыбызда колдонулат libbpf. Кийинки макалада CO-RE жөнүндө кененирээк сүйлөшөбүз, бирок бул макалада - жөн гана өзүңүзгө ядро ​​куруңуз. CONFIG_DEBUG_INFO_BTF.

китепкана libbpf каталогдо жашайт tools/lib/bpf ядро жана аны иштеп чыгуу почта тизмеси аркылуу ишке ашырылат [email protected]. Бирок, ядродон тышкары жашаган тиркемелердин муктаждыктары үчүн өзүнчө репозиторий сакталат https://github.com/libbpf/libbpf анда ядро ​​китепканасы аздыр-көптүр окуу мүмкүнчүлүгү үчүн чагылдырылган.

Бул бөлүмдө биз сиз колдонгон долбоорду кантип түзө аларыңызды карап чыгабыз libbpf, келгиле, бир нече (аздыр-көптүр маанисиз) тест программаларын жазып, анын баары кантип иштээрин деталдуу талдап көрөлү. Бул бизге кийинки бөлүмдөрдө BPF программалары карталар, ядро ​​жардамчылары, BTF ж.б.

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

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

Бара жатам libbpf өтө жөнөкөй:

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

Бул бөлүмдөгү кийинки планыбыз төмөнкүдөй: биз BPF программасын жазабыз BPF_PROG_TYPE_XDP, мурунку мисалдагыдай эле, бирок С тилинде биз аны колдонуп түзөбүз 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 программаларына ядро ​​түзүмдөрүнө кирүүгө, карталарды башкарууга, ошондой эле "чыныгы дүйнө" менен байланышууга мүмкүндүк берет - perf окуяларын түзүүгө, аппараттык каражаттарды башкарууга (мисалы, пакеттерди кайра багыттоо) ж.б.

Мисал: 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_protoXDP бул функцияны колдойбу же жокпу, жардамчы функциянын идентификаторунан аныктайт. Биздин функция тирөөчү:

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

BPF программасынын жаңы түрлөрү файлда "аныкталган" include/linux/bpf_types.h макро колдонуу BPF_PROG_TYPE. Бул логикалык аныктама болгондуктан, тырмакчада аныкталган, ал эми Си тилинин терминдеринде конкреттүү конструкциялардын бүтүндөй жыйындысынын аныктамасы башка жерлерде кездешет. Атап айтканда, файлда kernel/bpf/verifier.c файлдан бардык аныктамалар bpf_types.h структуралардын массивдерин түзүү үчүн колдонулат bpf_verifier_ops[]:

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

Башкача айтканда, BPF программасынын ар бир түрү үчүн типтеги маалымат структурасына көрсөткүч аныкталат struct bpf_verifier_ops, мааниси менен инициализацияланган _name ## _verifier_ops, б.а., xdp_verifier_ops үчүн xdp. Структура xdp_verifier_ops аныкталган файлда net/core/filter.c төмөнкүдөй:

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

Бул жерде биз өзүбүздүн тааныш функциябызды көрөбүз xdp_func_proto, ал ар бир кыйынчылыкка туш болгондо текшерүүчүнү иштетет кээ бир BPF программасынын ичиндеги функциялар, караңыз verifier.c.

Келгиле, гипотетикалык BPF программасы функцияны кантип колдонорун карап көрөлү bpf_get_smp_processor_id. Бул үчүн, биз мурунку бөлүмүбүздөгү программаны төмөнкүдөй кайра жазабыз:

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

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

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

белги bpf_get_smp_processor_id аныкталган в <bpf/bpf_helper_defs.h> китепкана libbpf кантип

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

башкача айтканда, bpf_get_smp_processor_id мааниси 8 болгон функция көрсөткүчү, мында 8 мааниси BPF_FUNC_get_smp_processor_id түрү enum bpf_fun_id, файлда биз үчүн аныкталган vmlinux.h (файл bpf_helper_defs.h ядродо скрипт тарабынан түзүлгөн, ошондуктан "сыйкырдуу" сандар жакшы). Бул функция эч кандай аргумент албайт жана типтин маанисин кайтарат __u32. Биз аны программабызда иштеткенде, clang инструкцияны жаратат BPF_CALL "туура түрү" Программаны түзүп, бөлүмдү карап көрөлү xdp/simple:

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

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

Disassembly of section xdp/simple:

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

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

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

Келгиле, өзүбүздү сынап көрөлү: программаны жүктөп, натыйжаны карап көрөлү bpftool prog dump xlated:

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

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

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

Макул, текшерүүчү туура ядро-жардамчыны тапты.

Мисал: аргументтерди өткөрүү жана акырында программаны иштетүү!

Бардык иштетүү деңгээлиндеги жардамчы функциялардын прототиби бар

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

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

Келгиле, жаңы ядро ​​жардамчысын жана BPF параметрлерди кантип өткөрөрүн карап көрөлү. Кайра жазалы xdp-simple.bpf.c төмөнкүдөй (калган саптар өзгөргөн жок):

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

Биздин программа ал иштеп жаткан CPU номерин басып чыгарат. Келгиле, аны компиляциялап, кодду карап көрөлү:

$ 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 (С тилинде биз мындай массивди аныктайбыз u64 woo[8]). Программада "xdp/simple" биз учурдагы процессордун номерин өзгөрмөгө алабыз key анан жардамчы функциясын колдонуңуз bpf_map_lookup_element биз массивдеги тиешелүү жазууга көрсөткүчтү алабыз, аны бирге көбөйтөбүз. Орус тилине которулган: биз CPU келген пакеттерди иштеткен статистикасын эсептейбиз. Программаны иштетүүгө аракет кылалы:

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

Кел, анын кошулганын текшерип көрөлү lo жана бир нече пакеттерди жөнөтүү:

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

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

Эми массивдин мазмунун карап көрөлү:

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

Дээрлик бардык процесстер CPU7де иштетилген. Бул биз үчүн маанилүү эмес, эң негизгиси программа иштейт жана биз BPF программаларынан карталарга кантип кирүүнү түшүнөбүз - колдонуу хелперов bpf_mp_*.

Мистикалык көрсөткүч

Ошентип, биз картага BPF программасынан сыяктуу чалууларды колдоно алабыз

val = bpf_map_lookup_elem(&woo, &key);

жардамчы функциясы кайда көрүнөт

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

бирок биз көрсөткүчтү өткөрүп жатабыз &woo аты аталбаган түзүлүшкө struct { ... }...

Программанын ассемблерин карасак, анда биз маанини көрөбүз &woo иш жүзүндө аныкталган эмес (4-сап):

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

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

Disassembly of section xdp/simple:

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

жана көчүрүү камтылган:

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

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

Бирок биз буга чейин жүктөлгөн программаны карасак, туура картага көрсөткүчтү көрөбүз (4-сап):

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

Ошентип, биз жүктөөчү программабызды ишке киргизүү учурунда, шилтеме деген тыянак чыгарууга болот &woo китепкана менен бир нерсе менен алмаштырылган libbpf. Алгач биз чыгууну карайбыз strace:

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

Биз муну көрүп жатабыз libbpf карта түздү woo анан биздин программаны жүктөгөн simple. Келгиле, программаны кантип жүктөшүбүздү кененирээк карап көрөлү:

  • чалуу xdp_simple_bpf__open_and_load файлдан xdp-simple.skel.h
  • эмне себеп болот xdp_simple_bpf__load файлдан xdp-simple.skel.h
  • эмне себеп болот bpf_object__load_skeleton файлдан libbpf/src/libbpf.c
  • эмне себеп болот bpf_object__load_xattr чейин libbpf/src/libbpf.c

Акыркы функция, башка нерселер менен бирге, чакырат bpf_object__create_maps, ал бар карталарды түзүп же ачат, аларды файл дескрипторлоруна айлантат. (Бул жерде биз көрүп жатабыз BPF_MAP_CREATE чыгарууда strace.) Андан кийин функция чакырылат bpf_object__relocate Ал бизди кызыктырат, анткени биз көргөнүбүздү эстейбиз woo көчүрүү столунда. Аны изилдеп отуруп, акыры функцияда өзүбүздү табабыз bpf_program__relocate, кайсы карта көчүрүү менен алектенет:

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

Ошентип, биз көрсөтмөлөрдү кабыл алабыз

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

жана андагы булак реестрин алмаштыруу BPF_PSEUDO_MAP_FD, жана биздин картанын файл дескрипторуна биринчи IMM жана, эгерде ал барабар болсо, мисалы, 0xdeadbeef, анда биз инструкцияны алабыз

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

Бул карта маалыматы белгилүү бир жүктөлгөн BPF программасына өткөрүлүп берилет. Бул учурда, карта колдонуу менен түзүлүшү мүмкүн BPF_MAP_CREATE, жана ID аркылуу ачылган BPF_MAP_GET_FD_BY_ID.

Бардыгы, колдонууда libbpf алгоритм төмөнкүдөй:

  • компиляция учурунда карталарга шилтемелер үчүн көчүрүү таблицасында жазуулар түзүлөт
  • libbpf ELF объект китебин ачат, бардык колдонулган карталарды табат жана алар үчүн файл дескрипторлорун түзөт
  • файлдын дескрипторлору инструкциянын бир бөлүгү катары ядрого жүктөлөт LD64

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

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

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

(толук кодун тапса болот байланыш). Ошентип, биз алгоритмибизди кеңейте алабыз:

  • программаны жүктөөдө, текшерүүчү картанын туура колдонулушун текшерет жана тиешелүү түзүмдүн дарегин жазат struct bpf_map

колдонуп ELF бинардык жүктөп жатканда libbpf Дагы көп нерселер бар, бирок биз муну башка макалаларда талкуулайбыз.

libbpf жок программаларды жана карталарды жүктөө

Убада кылынгандай, бул жерде жардамсыз карталарды колдонгон программаны кантип түзүү жана жүктөө керектигин билгиси келген окурмандар үчүн мисал libbpf. Бул көз карандылыкты түзө албаган чөйрөдө иштеп жатканыңызда же ар бир битти үнөмдөөдө же ушул сыяктуу программаны жазганыңызда пайдалуу болушу мүмкүн. ply, ал учууда BPF экилик кодун жаратат.

Логиканы карманууну жеңилдетүү үчүн, биз ушул максаттар үчүн үлгүбүздү кайра жазабыз xdp-simple. Бул мисалда талкууланган программанын толук жана бир аз кеңейтилген кодун ушундан тапса болот мааниси.

Биздин колдонмонун логикасы төмөнкүдөй:

  • түрү картасын түзүү BPF_MAP_TYPE_ARRAY буйругун колдонуу BPF_MAP_CREATE,
  • бул картаны колдонгон программаны түзүү,
  • программаны интерфейске туташтырыңыз lo,

адам деп которулат

int main(void)
{
    int map_fd, prog_fd;

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

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

    xdp_attach(1, prog_fd);
}

бул map_create системалык чалуу жөнүндө биринчи мисалдагыдай эле картаны түзөт bpf - “ядро, сураныч мага 8 элементтен турган массив түрүндөгү жаңы карта жасап бериңизчи __u64 жана мага файлдын дескрипторун кайтарып бер":

static int map_create()
{
    union bpf_attr attr;

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

Программаны жүктөө да оңой:

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

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

Татаал бөлүгү prog_load структуралардын массиви катары биздин BPF программабыздын аныктамасы struct bpf_insn insns[]. Бирок биз C тилиндеги программаны колдонуп жаткандыктан, биз бир аз алдап алабыз:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Муну өздөрү жазбагандар үчүн көнүгүү - табыңыз map_fd.

Биздин программанын дагы бир ачыла элек бөлүгү калды - xdp_attach. Тилекке каршы, XDP сыяктуу программаларды тутумдук чалуу аркылуу туташтыруу мүмкүн эмес bpf. BPF жана XDP түзгөн адамдар Linux онлайн коомчулугунан болушкан, демек, алар өздөрүнө эң таанышын колдонушкан (бирок нормалдуу адамдар) ядро ​​менен өз ара аракеттенүү үчүн интерфейс: netlink розеткалары, ошондой эле караңыз RFC3549. Ишке ашыруунун эң жөнөкөй жолу xdp_attach кодду көчүрүп жатат libbpf, тактап айтканда, файлдан netlink.c, биз эмне кылдык, аны бир аз кыскартып:

Netlink розеткаларынын дүйнөсүнө кош келиңиз

Netlink розетка түрүн ачыңыз NETLINK_ROUTE:

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

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

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

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

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

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

    *nl_pid = sa.nl_pid;
    return sock;
}

Биз бул розеткадан окуйбуз:

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

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

        if (len == 0)
            break;

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

Акыр-аягы, бул жерде биздин функциябыз розетка ачып, ага файлдын дескрипторун камтыган атайын билдирүү жөнөтөт:

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

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

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

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

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

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

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

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

cleanup:
    close(sock);
    return ret;
}

Ошентип, баары сыноого даяр:

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

Келгиле, биздин программа туташканбы, көрөлү lo:

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

Келгиле, пингдерди жөнөтүп, картаны карап көрөлү:

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

Уррай, баары иштейт. Баса, биздин карта дагы байт түрүндө көрсөтүлөөрүнө көңүл буруңуз. Бул айырмаланып жаткандыгы менен шартталган libbpf биз түр маалыматын (BTF) жүктөй алган жокпуз. Бирок кийинки жолу бул тууралуу көбүрөөк сүйлөшөбүз.

Иштеп чыгуу куралдары

Бул бөлүмдө биз минималдуу BPF иштеп чыгуучу куралдар топтомун карап чыгабыз.

Жалпысынан алганда, BPF программаларын иштеп чыгуу үчүн атайын эч нерсе кереги жок - BPF ар кандай татыктуу бөлүштүрүү ядросунда иштейт жана программалар clang, пакеттен берилиши мүмкүн. Бирок, BPF иштеп жаткандыктан, ядро ​​жана куралдар тынымсыз өзгөрүп турат, эгерде сиз 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 менен тажрыйба жүргүзүү үчүн биз кадимкидей колдоно алабыз Vanilla ядро же иштеп чыгуу ядролорунун бири. Тарыхый жактан алганда, BPF өнүгүүсү Linux тармактык коомчулугунда ишке ашат, ошондуктан бардык өзгөрүүлөр эртеби-кечпи Дэвид Миллер аркылуу, Linux тармагын тейлөөчүсү аркылуу өтөт. Алардын мүнөзүнө жараша - түзөтүүлөр же жаңы функциялар - тармактык өзгөрүүлөр эки өзөктүн бирине кирет - net же net-next. BPF үчүн өзгөртүүлөр ортосунда бирдей жол менен бөлүштүрүлөт bpf и bpf-next, алар андан кийин тиешелүүлүгүнө жараша нет жана нет-кийинкиге бириктирилет. Көбүрөөк маалымат алуу үчүн караңыз bpf_devel_QA и netdev-FAQ. Андыктан өзүңүздүн табитиңизге жана сиз сынап жаткан системанын туруктуулук муктаждыктарына жараша ядрону тандаңыз (*-next ядролор саналгандардын эң туруксузу).

Ядро конфигурациясынын файлдарын кантип башкаруу керектиги жөнүндө сөз кылуу бул макаланын алкагына кирбейт - сиз муну кантип жасоону мурунтан эле билесиз деп болжолдонууда, же үйрөнүүгө даяр өз алдынча. Бирок, төмөнкү көрсөтмөлөр сизге BPF иштетилген системаны берүү үчүн жетиштүү болушу керек.

Жогорудагы ядролордун бирин жүктөп алыңыз:

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

Минималдуу жумушчу ядро ​​конфигурациясын түзүңүз:

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

Файлдагы BPF параметрлерин иштетүү .config өз тандооңуз боюнча (кыязы CONFIG_BPF systemd аны колдонгондуктан, буга чейин иштетилген болот). Бул макалада колдонулган ядронун варианттарынын тизмеси:

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

Андан кийин биз модулдарды жана ядрону оңой чогултуп, орното алабыз (айтмакчы, сиз жаңы чогулган ядрону колдонуп өзөктү чогулта аласыз. clangкошуу менен CC=clang):

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

жана жаңы ядро ​​менен кайра жүктөө (мен бул үчүн колдоном kexec пакеттен kexec-tools):

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

bpftool

Макалада эң көп колдонулган утилита бул утилита болот bpftool, Linux ядросунун бир бөлүгү катары берилген. Ал BPF иштеп чыгуучулары үчүн BPF иштеп чыгуучулары тарабынан жазылган жана сакталган жана BPF объекттеринин бардык түрлөрүн башкаруу үчүн колдонулушу мүмкүн - программаларды жүктөө, карталарды түзүү жана түзөтүү, BPF экосистемасынын жашоосун изилдөө ж.б. Man баракчалары үчүн баштапкы коддор түрүндөгү документтерди тапса болот өзөгүндө же буга чейин түзүлгөн, тармакта.

Бул жазуу учурунда bpftool RHEL, Fedora жана Ubuntu үчүн гана даяр болот (мисалы, караңыз, бул жип, анда кутулоонун бүтпөгөн окуясы баяндалат bpftool Debianда). Бирок, эгер сиз өзүңүздүн ядроңузду буга чейин курган болсоңуз, анда куруңуз bpftool пирог сыяктуу жеңил:

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

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

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

$

(бул жерде ${linux} - бул сиздин ядро ​​каталогуңуз.) Бул буйруктарды аткаргандан кийин bpftool каталогуна чогултулат ${linux}/tools/bpf/bpftool жана аны жолго кошууга болот (биринчи кезекте колдонуучуга root) же жөн эле көчүрүңүз /usr/local/sbin.

чогултуу bpftool акыркысын колдонуу жакшы clang, жогоруда сүрөттөлгөндөй чогулуп, анын туура чогулганын текшериңиз - мисалы, буйрукту колдонуу

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

бул сиздин ядроңузда кайсы BPF функциялары иштетилгенин көрсөтөт.

Баса, мурунку буйрук катары иштетсе болот

# bpftool f p k

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

жыйынтыктоо

BPF эффективдүү өлчөө жана өзөктүн иштөөсүн өзгөртүү үчүн бүргөгө бут кийим кийүүгө мүмкүндүк берет. Система UNIXтин эң мыкты салттарында абдан ийгиликтүү болуп чыкты: ядрону (кайра) программалоого мүмкүндүк берүүчү жөнөкөй механизм көптөгөн адамдарга жана уюмдарга эксперимент жүргүзүүгө мүмкүндүк берди. Жана эксперименттер, ошондой эле BPF инфраструктурасын өнүктүрүү аягына чыга элек болсо да, тутумда ишенимдүү жана эң негизгиси натыйжалуу бизнес логикасын курууга мүмкүндүк берген туруктуу ABI бар.

Мен белгилеп кетким келет, менин оюмча, технология абдан популярдуу болуп калды, анткени, бир жагынан, ал мүмкүн ойноо (машинанын архитектурасын бир кечте аздыр-көптүр түшүнүүгө болот), экинчи жагынан, анын пайда болушуна чейин чечилбей калган (кооз) маселелерди чечүү. Бул эки компонент чогуу адамдарды экспериментке жана кыялданууга мажбурлайт, бул уламдан-улам инновациялык чечимдердин пайда болушуна алып келет.

Бул макала, өзгөчө кыска болбосо да, BPF дүйнөсүнө киришүү гана болуп саналат жана архитектуранын "өнүккөн" өзгөчөлүктөрүн жана маанилүү бөлүктөрүн сүрөттөбөйт. Алдыдагы план төмөнкүдөй: кийинки макалада BPF программасынын түрлөрүнө сереп салуу болот (5.8 ядросунда 30 программанын түрү колдоого алынат), андан кийин биз ядрону издөө программаларын колдонуп, чыныгы BPF тиркемелерин кантип жазууну карап чыгабыз. Мисалы, анда BPF архитектурасы боюнча тереңдетилген курска, андан кийин BPF тармагынын жана коопсуздук колдонмолорунун мисалдарына убакыт келди.

Бул сериядагы мурунку макалалар

  1. Кичинекейлер үчүн BPF, нөл бөлүгү: классикалык BPF

Шилтемелер

  1. BPF жана XDP маалымдама колдонмосу — кирпиликтен алынган BPF боюнча документтер, тагыраак айтканда, BPFди түзүүчүлөрдүн жана колдоочулардын бири Даниел Боркмандан. Бул биринчи олуттуу сүрөттөмөлөрдүн бири, Даниел эмне жөнүндө жазып жатканын так билет жана ал жерде эч кандай ката жок. Атап айтканда, бул документ белгилүү утилитаны колдонуу менен XDP жана TC типтеги BPF программалары менен кантип иштөөнү сүрөттөйт. ip пакеттен iproute2.

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

  3. Фейсбуктан BPF жөнүндө блог. Ал сейрек жаңыртылат, бирок Алексей Старовойтов (eBPFтин автору) жана Андрей Накрыйко - (текшерүүчү) ошол жерде жазгандай. libbpf).

  4. bpftool сырлары. Квентин Моннеттен bpftool колдонуунун мисалдары жана сырлары менен көңүл ачуучу Твиттер темасы.

  5. BPFге чөмүлүү: окуу материалдарынын тизмеси. Квентин Моннеден BPF документтерине шилтемелердин ири (жана дагы деле сакталып турган) тизмеси.

Source: www.habr.com

Комментарий кошуу