Цей сервіс заслуговує на окрему увагу.
Шо він робить? Це центральна частина моделі драверів UEFI, конкретніше - прив'язування драйверів до пристроїв, які вони мають обслуговувати. Я вже казав, шо розказую тут не за те, чим є та чи інша технологія чи стандарт, а за те, як я роблю його - імплементую конкретний стандарт чи технологію. Того описуватиму я шо ConnectController() робить, стисло.
Я написав її код, а потім виявилось, шо я неправильно зрозумів її семантику, тож, треба переробляти.
Треба також розуміти, шо я далеко не бачу всієї картини - всієї цієї складної системи, якісь нюанси і тд, це ж не книжка з імплеметації UEFI, а щоденник як ту імплементацію намагаються робити. Лише з досвідом вовтуження з ним, я матиму бачення тієї картини. Того, читачу, знову - це переказ шо і як я роблю, тут може бути абсолютно неправильне розуміння.
ConnectController() прив'язує драйвер до контроллера, запускає його, шоб той керував пристроєм. Для цього прив'язування, розробниками стандарту була придумана доволі непроста система правил, яку функція має втілювати. Ця система базується на переважності (або переважанні), - робиться розбиття на 5 груп за переважністю, всі кандидати так чи інашке опиняться в одній з п'яти груп, і ці групи мають пріоритет одна над одною - якась найвища - переважає усіх, наступна переважає всіх крім тієї першої і так далі. Функція має зібрати усіх кандидатів по порядку, і потім дуже довго і нудно викликати на цих кандидатах пару функцій
EFI_DRIVER_BINDING_PROTOCOL зразка на кожному гендлі кандидаті. Плюс, вона має флажок рекурсивности, і якшо він виставлений, вона має зібрати усіх дітей цього контроллера і на кожному так само приєднати його драйвер і так до краю. Так, я думаю - це один з найскладніших сервісів UEFI, хоча хто зна, я більш менш детально знаю покишо не за всі.
Огляд груп.
Перша група називається
Context Override. Викликач має надати масив гендлів, які мають мати інстальовані на них
EFI_DRIVER_BINDING_PROTOCOL (зразки цього протокола). Це перша група, і вона має найвищий пріоритет. І вона може бути пуста. Специфікація каже, шо це "дуже зручно" для налагодження драйверів - таким чином їх під'єднувати. Отже не маючи ніякого досвіду а значить - бачення картини, ми взнаєм
о - ця група для стадії розробки і налагодження. Якшо постачальник цього масиву дає більше одного драйверу, то, як уже сказано, вони трактуватимуться впорядкованими - перший має найвищий пріоритет.
Друга група називається
Platform Driver Override. В системі
може бути інстальований якимсь ще мені невідомим
платформним драйвером, протокол
EFI_PLATFORM_DRIVER_OVERRIDE_PROTOCOL, він має бути тільки один в системі - лише один гендл має мати такий протокол на всю систему. І в нього, цього протоколу, є сервіс
GetDriver(), який вертає по драйверу на виклик, по порядку (переважности). Тобто ти маєш IN/OUT змінну в яку кладеш спочатку нуль, і викликаєш, сервіс вертає найпріоритетніший в групі гендл драйвера, далі ти викликаєш сервіс з цим же значенням гендла в змінній, і сервіс вертає настпуний гендл в ту змінну на виході, і так поки не верне
EFI_NOT_FOUND. Яка мета цього перетирання, я чесно, поки не зрозумів, специфікація каже, шо це шоб перетерти шинноспецифічне прив'язування. Дивись нижче. Тут наочно проглядається брак бачення картини - я ще не розумію, бо не бачу цього, - для чого платформно-специфічне перетирання введене. Але нащастя тут це не надто заважає його втілювати. Ми маємо на руках всю потрібну інофрмацію, шоб наша
ConnectController() обробляла цю групу. Тобто, поки не ясно, хто на практиці може використовувати цей механізм, на прикладах, але його суть з т.з. імплементації ясна.
Третя група.
Driver Family Override. В системі, на гендлах драйверів,
може бути інстальований цей протокол (
EFI_DRIVER_FAMILY_OVERRIDE_PROTOCOL). Він має функцію
GetVersion(), яка вертає версію, яка впливає на пріорітет цього гендла (в цій групі звичайно). Тобто маємо набір гендлів драверів, на яких інстальовано цей протокол, викликаємо сервіс
GetVersion() протокола на гендлах, і таким чином визначаємо порядок за яким ці драйвери слід пробувати для якогось контроллера. Більша версія, вищий пріоритет. Тут треба сортувати гендли самому, перед тим як класти їхні інтерфейсні вказівники в вихідний масив. Ми використовуємо сортування вставками, оскільки очікується, шо кількість таких гендлів мала (якшо взагалі є).
Четверта група.
Bus Specific Driver Override. Така сама за поведінкою як і друга група, але визначає специфічне для шини перетирання. Шоб ця група була задіяна, на гендлі нашого контроллера, має бути інстальований відповідний протокол (
EFI_BUS_SPECIFIC_DRIVER_OVERRIDE_PROTOCOL). Тоді, він вертає один за одним драйвери, які треба пробувати в цій групі, починаючи з найпріоритетнішого.
П'ята група.
Driver Binding Search. Це власне всі драйвери на яких є
EFI_DRIVER_BINDING_PROTOCOL, і які ще не були додані в горішніх групах. Варто сказати, якшо гендл з'являється в якійсь групі, його треба викреслювати з усіх нижчих груп, - тобто пробувати кожен гендл в його найвищий можливій групі. От драйвери які не попали в жодну привілейовану групу, додаються тут. Але перед цим сортуються. Driver Binding протокол має в інтерфейсі таке поле
Version, от по ньому й сортуються гендли кандидати.
Далі, коли всі гендли впорядковані, тобто вказівники на їхні інтерфейси на протокол прив'язування впорядковані,
ConnectController() має викликати на цих інтерфейсах сервіси
Supported() і
Start(). При чому, і тут родзинка, яку я спочатку не зміг з'їсти, - щойно
Supported() якогось інтефейсу вернула успіх, треба обривати цикл, не забувши правда викликати після
Supported() Start() відповідного інтерфейсу,
і починати цикл пробування знову, з найвищого інтерфейсу. Нашо це? Тут треба мати "бачення". Мати більше знання з устрою всієї системи. Це досвід. Я його поки не маю. Мені відповіли, шо це того, шо після успішного Supported/Start можуть з'явитися нові реалії, і треба це враховувати. Які ці реалії саме? Якшо придивитися, шо вимагає специфікація тут робити, виглядає, шо після успішного старту якогось драйвера, на високопріоритетніших гендлах можуть з'явитися протоколи, яких не було, або ще шось, шо зрештою може вплинути на успіх їхньої пари Supported/Start. Саме так можна пояснити вимогу перезапускати цикл. Шо ж, специфікації видніше.
Цей сервіс насправді ще трохи складніший, бо окрім вже переказаного за нього, він, і це вже було згадно до речі, має обробляти рекурсивність - тут найбільша складність полягає в діточках - виглядає, шо динамічність середовища в часи виклику
ConnectController(), робить це збирання не найпростішим - потрібна "якась" синхронізація, а оскілька вона в мене ще не розроблена, мені важко судити, шось казати докладніше, і, зрештою, писати реальний код в цій частині. Тут ясно одне - діти цього контроллера, це всі гендли-агенти, які відкрили якийсь його протокол з атрибутом
BY_CHILD_CONTOLLER. Наче просто, але знову ж - динамізм і синхронізація. Поки туман. Якшо вже казати повно і чесно, цей динамізм робить усю імлементацію функції просякнутоим нерозумінням і тривогами. Ми не бачимо і не знаємо, шо саме може змінитися підчас роботи цієї функції. Хто може, а хто не може перервати її роботу і втрутитися в хід, змінивши якісь базові структури. Специфікація скромно мовчить за це, а от імплементація Tianocore, робить доволі кумедні речі, які мабуть викликані цим динамізмом. Саме, вона будує масив гендлів, це так би мовити вхідний буфер, усі гендли які мають на собі прив'язувальний протокол, їх усі треба розставити за пріоритетом і спробувати, і навпаки - всі кандидати з усіх груп мають мати цей протокол інстальований. Отже, будь усе статичним, цей вхідний буфер, який береться та хоча б викликом LocateHandle() з атрибутом ByProtocol, як власне Tianocore і робить, містив би все необхідне і не треба було б нічого боятися. Але от штука, Tianocore імплементація оце вибудовує вихідний буфер (відсортованих за пріоритетом вказівників на інтерфейс взятих від гендлів), робить усю оту складну роботу, а потім, перед тим як почати пробувати ці інтерфейси,
знову викликає
LocateHandle(), шоб взнати кількість гендлів в системі з цим протоколом інтсальованим (до речі, можна було б і якесь поле кількости зробити вже у внутрішніх структурах, замість того, шоб так по горобцях з гармати лупити), і от якшо кількість
більша, ніж була на початку будівницвта і сортування, вона все викидає і починає спочатку! Це якби натякає, шо поки сервіс морочився з сортуванням, "хтось" такий же крутий, додав протокол прив'язування на гендли. UEFI - система однопроцесорна, але вона обробляє переривання від системного таймера, і таким чином, може переривати виконання сервісів не лише самим обробником переривання, але й завданнями на які він може передати контроль. Отакот. Це все шо треба знати, шоб зрозуміти, шо таки може хтось шось наставити. Звичайно, СОП праюють на рівні
TPL_NOTIFY (правда з обмовкою шо може й нижче), а це наче як неблокований рівень - функція не блокується, а робить до кінця і вертає контроль; але не блокується (сама запитом) і не переривається - це не одне й те саме. Звичайно переривається, і існують вищі за
TPL_NOTIFY рівні. Але, повертаючись до того фокусу в Tianocore, я не імплементував таке саме в своїй функції до речі, я думаю - це таки галімаття. Ми можемо сказати: шо це дасть? Новіший контекст (свіжіша множина гендлів)? Але сервіс ще далеко не закінчив, може тоді і на середині викликів Supported/Start дивитися кількість гендлів? Ні, це не підхід. Тут сам факт наявности таких танців свідчить, шо все набагто складніше з цією синхонізацією. Так само з збиранням дитячих контроллерів на гендлі контроллера - Tianocore "захоплює" якийсь lock, тобто блокує усім доступ до чогось поки шукає дитячі гендли - знову динамізм.
Звичайно, потрібна якась консистентність. Але сервіс не всемогутній і отой танець зі скидання збудованого, не виглядає таким виправданим. Ми можемо сказати: ми працюємо на стані на час, яким він був коли почали будувати наш вхідний буфер. Це виглядає консистентно. Всеодно лигаються проблеми. Розгляньмо третю групу. Специфікація каже, шо треба зібрати гендли, на яких поставлений цей протокол (Family Override протокол). Ясно, шо на них також має бути і прив'язувальний протокол - це ж драйвери, але
як саме нам їх знаходити? Спочатку я думав брати їх так же як гендли з прив'язувальним протоколом, ти можеш лізти у внутрішні структури прямо, внутрі своєї імлементації ядра, а можеш викликати
LocateHandle() - семантка та сама. Так ми матимемо підмножину нашого вхідного буфера (знову - бо всі ці гендли мають також мати прив'язувальний протокол). Але зібрана незалежно (і пізніше за вхідний буфер), ця підмножина "свіжіша" за вхідний буфер! І тут можуть бути гендли, які мали б бути там, але яких нема. Якшо подумати ця неконсистентність не така вже й страшна, але всеодно - неконсистентність. Чого не така страшна? Того шо, ці гендли, всеодно суть кандидати і вони валідні, вони будуть вставлені як треба з т.з. пріоритету, а от технічно, оскільки розмір вихідного буфера пов'язаний з розміром вхідного, і ці гендли там не враховані, вони витіснять (можуть витіснити) найостанніші за пріоритетом гендли так, шо ті не будуть розглянуті, на них не буде запущена їхня Supported/Start пара. Але це ж останні за привілеєм гендли, вони майже напевно не підходять цьому контроллеру. Дрібниця, але дивлячись, розумієш, всеодно неправильно. А шо якшо так сталось шо саме той драйвер, той останній і є той, шо треба. Чи може так бути? Витіснення відбувається (чи не відбувається) на етапі будування вихідного буферу, ми ще нічого не знаємо хто саме приєднається успішно. А ще, гендл
може бутий не витіснений. Адже як додається його інтерфейс? Коли вже він достанутий, робиться перевірка чи такий вказівник уже не вставлений (а так теоретично може бути, і це робить вихідний буфер теоретично меншим за вхідний), якшо його там немає, він вставляяється.
Якшо довжина вихідного буфера ще не вичерпана. Tianocore робить подвійну перевірку і на довжину вхідного і на довжину вихідного буфера і це дивно адже достатньо тоді було б уже перевіряти лише на довжину вхідного, бо саме ця величина визначає довжину вихідного, але це не важливо - важливо додається вказівник якшо є куди. Отже, правильний гендл може не попасти лише якшо довжини не вистачає, а якшо виділити вихідний буфер з запасом? Тоді навіть ця неприємна ситуація з новими неврахованими на початку гендлами має всі шанси завершитися нормально. Але вся оця каша породжує сумніви і невпевненість. Tianocore для третьої групи не бере гендли черз запит до БД гендлів, а перебирає вхідний буфер. Це теж логічно - адже всі гендли кандидати мають мати прив'язувальний протокол, вхідний буфер саме за цим критерієм і побудований. Отже логічно. Але не покидає думка, шо Tianocore робить так через бажання працювати "консистентно" і уникає шоб в її розгляд попали гендли з новішого контексту, неконтрольовано так би мовити. Вихід за межі масиву не може статися і в них, там робиться перевірка. Але ж на довжину вхідного буферу! Отже якби вони викликали на третій групі новий запит і мали б набір гендлів новіших, то, дадачюи до вихідного буферу, але перевіряючи довжину по старому вхідному, вони б мали малий шанс, шо в самому кінці перевірка б зупинила додавання крайніх гендлів. Як уже було сказано. Їхній код додає інтерфейс якшо тільки кількість вихідного буфера менша за незмінну довжину вхідного. Шо смішно, інкрментацію лічилька довжини вихідного вони роблять безумовно, а додають лише на умові меншости за довжину вхідного буфера. От ці сумніви змусили і мене змавпувати їхню поведінку і брати гендли з вхідного буфера а не робити свіжий запит на Family Override протокол. І мені це не подобається. Хоча тут в принципі не все так принципово. Ця проблема можливої неконсистентности, вона тяжіє і тисне, навіть якшо розгляд показує. шо це єрунда, яка ні до яких проблем і відхилень не призведе. Цікаво, шо я все ж зробив вихідний буфер більшим за віхдиний. Аж у двічі (пізніше зменшив до 1.5 довжини вхідного), тобто я майже ґарантував немовжливість витіснення новоприбулими гендлами. Все ж під впливом сумнівів, я таки беру для третьої групи гендли з вхідного буфера. Різниця - хоча мій код уже готовий додати всіх і з старого контексту і з нового, принаймні імовірність витіснення майже обнулюється, адже довживна вихідного буфера просто велика для цього - в два (1.5) рази більша за вхідний, уявіть ці потенційні новоприбулі гендли, та ще й в таких специфічних і необов'язкових групах як третя, вони мають прийти в нереальній кількості, шоб витіснити старих. Але оскільки я беру лише з старого контексту, я не додам новачків. Які могли б виявитися реально тими кого треба приєднати. Але знову, це лише вузенька імовірність в одній лише групі. Це проблема свіжости вхідної множини. Ти міг би обперитися на фіксований стан цієї множдини і працювати на ній - визначеність. Але якшо ти без шкоди можеш хоча б частково обробити і новіші версії, чого б ні, це ж власне не самомета - мати 100% визначений контекст. Приєднати потрібний драйвер - ось справжня мета. І якшо так складеться, шо той єдиний потрібний драйвер, який і варто приєднати, виявиться доданим лише на час коли ти зробиш запит в третій групі і він саме в ній буде, то наче так і треба робити. Одне слово - непевність. Я взяв третю групу, але в решті ситуація непростіша. Наприклад друга або четверта - вони взагалі отримують гендли через виклик до сервісу відповідного протоколу - як той сервіс находить ті драйвери, знає лише він. І ці гендли точно не з вхідного буферу. Але Tianocore робить тут взагалі дивні еквілібристичні речі, і знову нічого в специфікації за це нема, а аналіз залишає стійке чуття, шо це знову робиться шоб брати інфу ультимтивно узгоджену з старим буфером. Але цього разу, з 2-ю і 4-ю групами, і це не пояснює чого все робиться так плутано. Ось шо робить Tianocore тут. Специфікація нічого і не натякає на якісь особливі танці з гендлами вернутими сервісами цих протоколів (2-ї і 4-ї груп). Там це гендли образу драйверу. Але Tianocore, замість додати гендловий протокольний інтерфейс напрям
у, як з рештою груп, розгортає дворівневе додавання. Спочатку воно прокручує всі гендли в вхідному буфері і порівнює поле
ImageHandle зразка протоколу прив'язування кожного гендла з оцим гендом який вернув сервіс 2,4-ї груп. І якшо це поле збігається з цим гендлом, тоді додається гендл як зазвичай, але не вернутий сервісоами 2,4-ї груп, а той чиє поле має це значення, з старого буферу! Шо значить те поле? Специфікація каже, шо то гендл образу драйверу який постачив цю імплементацію цього протоколу, породив це протокол. От так от. Цей фокус явно межує з порушеням специфікації, бо вона про таке нічого не каже. Вона каже додавати ці гендли, а не витягувати через них якісь інші, на які драйвер цього гендлу образу поставив свій інтерфейс протоколу прив'язування. Найголовніше навіть не це, а те, шо робиться різниця між групами. А в специфікації про це нічого нема. Якшо це "швидкий" спосіб витягти усі унікальні вказівники на інтерфейси - адже саме їх ми збираємо в вихідний буфер, і вони не мають дублюватися, то чого тоді так само не робиться з рештою груп. Наприклад в 1-й групі, гендли просто додаються, а не шукається чи є цей гендл в полі
ImageHandle на зразку протокола прив'язування усіх гендлів вхідного буфера.
Всі ідеї, які приходять для пояснення цього дуже туманні, бо це здогадки, як ніяк перевірити. Ну і як вже казав - відповіді на це питання мені не дали. Зрештою, видно, хай навіть не всі фокуси Tianocore можна списати на динамізм, всеодно, він цей динамізм робить мороку. Покишо ми імплементуємо без всякої синхронізації - ми нічого не знаємо за це. Може доведеться переробляти, а може просто шось гарно впишеться. Це ще має вияснитися.
Також,
ConnectController() має аргументом таку цікавезну штуку як Device Path - шлях пристрою - набір вузлів, побудованих навколо генеричної стуктури вузла протокола
EFI_DEVICE_PATH_PROTOCOL. Самому сервісу власне з ним майже нічого робити не треба, - просто передавати його "незмінним" в пару Supported/Start. "Майже" того, шо тут є певні проблеми з вирівнюванням, специфікація каже, шо цю структуру треба розглядати завсіди як потенційно невирівняну десь внутрі, - її структура дуже варіабельна, має купу специфічних додатків залежно від типу, і того майже завсіди може виявитися невирівняною, і, з іншого боку, ті ж сервіси Supported/Start можливо вимагають її вирівяною. Я зараз не пам'ятаю чи вимагають, але от, Tianocore імплементація
ConnectController() вирівнює цей аргумент перед передаванням далі. Я дуже мало дивлюся в інші імплементації, шоб не перетворитися в копіпастера. Тут довелось дивитися, бо я спочатку абсолютно неправильно (і дуже оптимістично) зрозумів семантику
ConnectController().
Але і це ще не все.
Якшо ваша імплементація UEFI підтримує автентифікацію користувачів, безпеку, кажучи загально, треба ще співставляти Device Path на предмет мання користувачем прав на ньому шось запускати, як от контроллер. Ясно, шо це все туман, і поки моя імплементація не підтримує безпеку.
За протокол шляху очевидно треба буде розказати окремо, зараз можна сказати - це UEFI-шний спосіб задавання "шляху" до пристрою, описання пристрою в термінах доступу до нього, його позиції і ролі в системі - всі фірмварі мають механізм для представлення такої властивості компонента як його позиція/роль. Згадаймо ARC-імена, або Дерево Пристроїв OpenFirmware. Шо мені подобається, в уефішній схемі, вона бінарна - це дуже правильно, програмні модулі мають обмінюватися між собою інформацією в форматі, який найбільше придатний для них - найшвидший, найефективніший - двійковий. І лише на інтерфейсі з людиною, треба мати текстові репрезентації, які для машини - шалене марнотратство. Цей протокол має таку. Шлях складається з вузлів. Є звичайно обмеження - які типи вузлів де можуть бути, але загалом - можливі різні варіанти, того система в загальному довільна. Є шість типів вузлів:
- Hardware
- Acpi
- Messaging
- Media
- Bios Specification Boot
- End Of Harware
В кожного є також підтип, от оця дворівнева типізація визначає можливі вузли.
Hardware тип описує зв'язок компонента з системними ресурсами - на яких "адресах" сидить цей пристрій, які відображені в пам'ять чи в просторі В/В ресурси він споживає.
Acpi тип - це простір імен Acpi, шо тут ще скажеш, вчити треба ще. От специфікація каже, шо це місток до Acpi. І наводить приклад, де він потрібен - наприклад кореневий PCI контроллер, каже специфікація, не має ніяких вказівок в PCI специфікаціях як його програмувати, того, його конфігурування покладається на ACPI таблиці. І оцей вузол в шляху буде мати Acpi тип, і описуватиме
де в системі є той кореневий PCI контроллер. Тобто Acpi вузол стоятиме найпершим. Якби ж я вже засвоїв оті скорочені тесктові репрезентації шляхів, я б навів їх тут для прикладу.
Messiging. Він служить для опису взаємодії компонентів
поза системними ресурсами, тобто він периферійний, а не навколо діапазонів ресурсів. Цей тип має найбільше підтипів - тут і USB, і Fibre Channel, і UART, і IPv4, і IPv6 і ще багато чого. Тобто якшо перші два - більше навколо системної пам'яти, кореневих шинних інтерфейсів тощо, - цей іде в периферію. Можна сказати, шо якшо я зрозумів правильно, вузли такого типу описують усе різноманіття периферійних інтерфейсів. Тобто якшо наш пристрій сидить на USB шині, він матиме вузол в своєму шляху, який репрезентуватиме його сидіння там, і це буде вузол такого типу - messaging device path.
Media. А цей тип іде логічно далі, ми пам'ятаємо - UEFI це платформа для завантаження ОС, і її турбують саме ті пристрої, які для цього потрібні. Сховища звичайно сюди попадають. От, коли ми від центральної шини через якийсь периферійний інтерфейс прийшли до сховища, постає питання представлення його структури, його абстраґування. Цей тип цим і займається - він описує партиції, схеми партиціонування, томи, файлові шляхи.
Bios Boot Specification. Це зворотна сумісність. Шоб змогти завантажити ОС, яка не знає нічого за UEFI, але знає за Bios Boot Specification версії 1.01.
End Of Harware. А це вузол термінатор. Він або вказує на край шляху пристрою, або, в рідкісних випадках, служить як "кома" - розмежовує два шляхи в якійсь змінній оточення, куди треба ці два шляхи покласти і які пов'язані якимсь загальним сенсом. Для цього цей тип має два підтипи - один для "крапки", один для "коми".
Власне
ConnectController() окрім вирівнювання цього шляху - про яке нажаль специфікація мовчить в описі
ConnectController(), і хібашо з опису Supported/Start це можна взнати і то, я не пам'ятаю чи воно там є, а так то я взнав з Tianocore коду, а це не дуже добре - має ще дивитися чи той шлях часом не є краєм шляху. Це впливає на те, який статус вертати в разі незнайдення/неприєдання жодного драйвера. Коли не знаєш, чим той шлях є, воно н
ічого подумати, н
ічого сказати за це - нема розуміння. Почитавши за шляхи, тепер уже набагато ясніше. Це знання. Але, і це мабуть видно з мого опису типів вузлів вище, моє знання за шлях пристрою, ще мале, таке, яке бува від одного прочитання. Але тепер ми маємо уявлення, шо то за "край пристрою", ми навіть знаємо як його перевіряти тут. Як я вже сказав, шляхи ці доведеться вивчити на зубок - вони грають важливу роль і зустрічаються часто. От колись побалакаємо за них і їхню імплементацію.
Загалом, це сервіс дуже складний, і його теперішню мою імплементацію навряд можна вважати остаточною, - вона точно принаймні недороблена. Про неправильна важко шось казати, потрібен час і дальша робота, але перша версія дуже швикдо виявилась неправильна. Сенс ролі сервісу дуже ясний і простий - він знаходить драйвер для контроллеру, для пристрою. Це прив'язування драйверу до прстрою Логіка ж цього знаходження, і, як наслідок, імплементація - складна.