четвер, 9 червня 2016 р.

Завантажувальні сервіси - ConnectController() сервіс

Цей сервіс заслуговує на окрему увагу.
Шо він робить? Це центральна частина моделі драверів 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 коду, а це не дуже добре - має ще дивитися чи той шлях часом не є краєм шляху. Це впливає на те, який статус вертати в разі незнайдення/неприєдання жодного драйвера. Коли не знаєш, чим той шлях є, воно нічого подумати, нічого сказати за це - нема розуміння. Почитавши за шляхи, тепер уже набагато ясніше. Це знання. Але, і це мабуть видно з мого опису типів вузлів вище, моє знання за шлях пристрою, ще мале, таке, яке бува від одного прочитання. Але тепер ми маємо уявлення, шо то за "край пристрою", ми навіть знаємо як його перевіряти тут. Як я вже сказав, шляхи ці доведеться вивчити на зубок - вони грають важливу роль і зустрічаються часто. От колись побалакаємо за них і їхню імплементацію.
Загалом, це сервіс дуже складний, і його теперішню мою імплементацію навряд можна вважати остаточною, - вона точно принаймні недороблена. Про неправильна важко шось казати, потрібен час і дальша робота, але перша версія дуже швикдо виявилась неправильна. Сенс ролі сервісу дуже ясний і простий - він знаходить драйвер для контроллеру, для пристрою. Це прив'язування драйверу до прстрою Логіка ж цього знаходження, і, як наслідок, імплементація - складна.

пʼятниця, 3 червня 2016 р.

Завантажувальні сервіси - Аґенти, споживання протоколів на гендлі

Такі функції як OpenProtocol(), CloseProtocol(), OpenProtocolInformation() були додані в UEFI пізніше, і були призвані розширити можливості фірмварі і прибрати небезпеки, які таїла старіша модель, в якій була лише функція HandleProtocol(), а деінстлаяціні й реінсталяційні сервіси могли нареінсталювати все дуже необачливо - без огляду на клієнтів, які покладались на ті протоколи, ясно, шо реінсталяція чи деінсталяція протоколу на гендлі, який використовується ще кимсь - це пагана ідея. Ну, от вони, ці сервіси й додали, мабуть. Більше безпеки. Але разом з тим, додали також мороки для втілення всього цього. Не в останню чергу через не найкраще подання в специфікації приправлене кумедними обдруківками, шо вилилось в доволі непростий шлях для розуміння шо ж мається на увазі.
Балакаючи про мій шлях в цьому, спочатку варто згадати, шо була додана одна зміна в структуру елемента БД, а саме перший елемент з списку агентів тепер сидить в самій структурі протокола, решта все як було - нефрагментована таблиця агентів. Це було зроблено через сементику того прив'язування агентів. Агенти "відкривають" протоколи на гендлах з певними атрибутами. Власне ось вони всі сім на поки:
  • BY_DRIVER
  • BY_DRIVER_EXCLUSIVE
  • EXCLUSIVE
  • BY_CHILD_CONTROLLER
  • GET_PROTOCOL
  • TEST_PROTOCOL
  • BY_HANDLE_PROTOCOL

Специфікація визначає, шо в гендла на якомусь конкретному протоколі завсіди може бути лише один агент з одним з трьох атрибутів - BY_DRIVER, BY_DRIVER_EXCLUSIVE, EXCLUSIVE.
Може й не бути жодного з цих трьох, як я розумію, але якшо є з тих трьох, то тільки якийсь один з них. Перший - це коли драйвер споживає протокол, другий це коли драйвер споживає з виключним правом, третій це коли з виключним правом споживає, якийсь застосунок (не драйвер). З назв ясно, шо "виключне" споживання має привілей, а саме - якшо вже є споживач з атрибутом BY_DRIVER, виключний споживач може його витурити. При чому зробити це детерміністично, а саме функція OpenProtocol() викличе в цьому випадку DisconnectController() для драйвери, який треба від'єднати, а та викличе Stop() сервіс в DRIVER_BINDING_PROTOCOL'і який має бути інстальований на гендлі від'єднуваного драйвера. От так робити правильно.
Не можу похвалитися, шо я вже чітко бачу всю цю модель, але от в цьому аспекті нащастя видно один момент - ці три типи споживання стоять окремо від решти, і наче як просяться на особливу роль і в організації підлеглих даних. Не просто так, а для оптимізації звичайно. Дійсно, а шо, якшо для цієї трійці, ми виділимо першу комірку в таблиці, і там тільки такі атрибути лежатимуть? А потім виникає продовження цієї ідеї - покласти цю комірку в саму структуру протоколу - це підвищує локальність - майже напевно саме ці агенти суть "найголовніші" і саме з ними буде найбільше волокіти - отже мати їх уже в протоколі, робить пошук їх константним замість лінійного. Отже зміна - голова таблиці агентів тепер всунута в структуру протокола. Для пришвидчення - це відображення на структурі даних особливої ролі таких агентів.
Варто також додати, шо чим більше роздумів і вияснення іде шодо агентів, тим більше вимальовується картина, в якій стійко звучить питання - їх взагалі всі оті атрибути будуть використовувати? Мається на увазі не взагалі використовувати, а так, шо дійсно треба тримати інформацію за всіх агентів, які відкрили протокол для тесту наприклад, або з іншим атрибутом. Адже навіть специфікація каже, шо такі агенти навіть не мають викликати CloseProtocol() до пари. Виникає питання - нашо тоді БД триматиме цю зайву інформацію? Шоб собі додати мороки? І того, думається, частина БД шо стосується агентів, може зазнати певних змін. В бік редукції - нам може бути не треба аж так багато записувати як думалося з першого прочитання.
Розгляньмо поштучно решту атрибутів.
TEST_PROTOCOL
Це для тесту чи підтримує цей гендл цей протокол. Чи він присутній. Клієнт не має ані викликати CloseProtocol(), ані постачати змінну куди буде вернутий вказівник на структуру інтерфейсу.
Питання: нашо тримати інформацію в БД, шо якийсь клієнт так відкривав протокол? Шоб зробити "безпечну" деінсталяцію/реінсталяцію протокола? Але жодних дій оповіщення такого клієнта не передбачено. Йому типу як всеодно. Єдина причина тримати інформацію за таке відкривання - це того, шо функція OpenProtocolInformation(), має вернути масив усіх клієнтів споживачів. Ага. Але це причина заради причини. Знову, отримавши такий масив, нікому - ані фірмварі, ані тому, хто викликав цю функцію, ані самому клієнтові який відкрив протокол для тесту, не буде від цього ніякої користи. Нема наприклад вимоги, оповіщувати якось клієнта, шо от протокол який ти тестував уже скоро буде невалідний. Висновок який напрошується - `нічого тут тримати в БД.
Специфікація так вельми довільно описуючи як цей "безпечний" варіант робитиме, в функції UninstallProtocolInterface(), каже, шо от від'єднавши (безпечно) драйвери на цьому протоколі, можна тоді "від'єднувати" тестувальниікв і решту. Але шо це значить для самих тестувальників. Звичайно, для БД формально, ми змінюємо - ми самі викликаємо CloseProtocol() для тестувальників, викреслюємо їх з БД, але шо це дасть? Кому це треба? Хто цю роботу оцінить, для кого це матиме якісь важливі наслідки? Ні для кого.
GET_PROTOCOL
Усе сказане в попередньому абзаці стосується так само і цієї опції. Різниця в тому, шо семантика тут уже не тестувальна зовсім, а куди серйозніше - специфікація каже, шо це для драйверів, шоб отримати доступ до протоколу. І тут же попереджає бути пильним, бо при цьому методі "драйвер не буде оповіщений про деінсталяцію або реінсталяцію протокольного інтерфейсу на гендлі". Просто фантастика! Нате ось вам інструмент для обвалювання системи, користуйтесь і не кажіть шо ми вас не попереджали... Але облишивши критику, можна сказати, імплементатор має миритися з вимогами стандарту. Тобто таки треба втілювати оце ось засовування пальця в розетку. Але знову - підтримка інформацї про таких агентів не має ніякого сенсу крім видати її незрозуміло для чого на спеціальному запиті. Ніякої функціональности поза цим немає.
[Додано пізніше] Звичайно, тут я трохи погарячкував, цей метод таки потрібен. Це не означає. шо він стає безпечним, або шо шось міняється в плані підтримки інформації за цей тип під'єднання в БД, не міняє, - просто цей метод виявляється потрібним. Коли я писав ConnectContoller(), стало ясно, шо GET_PROTOCOL потрібен -  нам потрібен саме вказівник на інтерфейс протоколу, але навряд нам потрібно відкривати його BY_DRIVER - це зробить пізніше Supported() або Start() самого протоколу. Намагаючись під'єнати драйвер до контроллера, ми перебираємо купу гендлів драйверів потенційних кандидатів. На рівні ConnectController(), ми маємо саме мати доступ до Supported() і Start(), цього протоколу. Як? Правильно, треба відкрити протокол. Не те шо б це було очевидно, але здається відкривати його BY_DRIVER було б тут паганою ідеєю. От ми й використовуємо той "ненадійний" GET_PROTOCOL. Мабуть я правильно відслідкував логіку введення його в специфікацію. Так же само, мабуть існує купа подібних ситуацій для драйверів і зстосунків, де їм треба інтерфейс протоколу, але їм не треба або вони не можуть відкривати його BY_DRIVER. Було б тільки краще, якби вони чіткіше роз'яснили це в описі цього атрибуту. Чого він ненадійний? Бо якшо хтось деінсталює/реінсталює цей протокол, клієнт, який відкрив так протокол, нічого не знатиме за це, і намагатиметься використовувати його й далі, а там уже все не так - халепа. Якшо ж ти відкриваєш BY_DRIVER, тебе принаймні буде оповіщено шо ти маєш від'єднатися. Або взагалі "конкуренти" отримають ACCESS_DENIED. Але це володіння протоколом не підходить для ConnectController(), яка насправді тільки намагається вияснити хто ж має під'єднуватися. Якшо вона відкриє BY_DRIVER, а далі передасть до Supported(), та можливо, не змігши відкрити цей протокол верне неуспіх. Все дуже складно, і пишучи це я навіть не розумію, чи я правильно розумію. Ясно одне - специфікація дуже плутана і складна в цьому місці. Хай там як, поки моя імплементація OpenProtocol() отримавши GET_PROTOCOL і впевнившись, шо цей протокол є присутній на гендлі, радісно вертає вказівник на його інтерфейс, нічого не змінюючи в БД. Стосовно ж небезпечности використання ConnectController()'ом вказівника отриманого через GET_PROTOCOL, думається, тут не має бути проблеми - СОП працюють на рівні TPL_NOTIFY, блокування заборонене. Якшо ж Supported() викличе UninstalProtocolInterface() або родичів, ну, це буде як молотком по компутеру - не встоє. Вона не має цього робити. Проти всіх можливих помилок нічого не зробиш. Звичайно, можна навіть проти цього протистояти - відкривати після Supported() і перед Start() OpenProtocol() на цьому інтерфейсі, але це було б уже геть тупо. Зрештою, семантично неправильно: ланцюжок успіху: OpenProtocol->Supported->Start мається на увазі на одному й тому самому протокольному зразку. З тим самим вказівником на інтерфейс. А не переставленим. Ми вважаємо, шо той, хто використовує GET_PROTOCOL, знає, шо вказівником на інтерфейс, який з цим атрибутом відкривається, можна користуватися "недовго". Бо теоретично, він може зіпсуватися.
BY_HANDLE_PROTOCOL
Ще один їжачок в тумані. Єдине, шо тут ясно - це зворотня сумісність. Тут таки вимагають я так зрозумів викликати CloseProtocol(), отже зроблений механізм споживачеві самому визначати точку інвалідизації інформації яку він споживає - він отримує інтерфейс, використовує його, сміливо, а потім, коли воно йому не треба - повідомляє про це систему, і вже не використовуватиме цей ресурс. Круто.
[Додано пізніше] Ні, тут я зрозумів неправильно. Ніхто нічого не вимагає - просто не може. Як? Це для сумісности з HandlePrototcol(), вона ні про яке CloseProtocol() не знає, просто вони забули згадати, шо цей варіант теж не вимагає викликати CloseProtocol(). Але головне, - взагалі, якшо просто деінде викликати CloseProtocol(), то ніякої безпеки не вийде - цю функцію має викликати саме клієнт який відкрив той протокол. Тільки тоді це має сенс. Якшо наприклад я, драйверописець, напишу драйвер, який намагатиметься відкрити протокол через BY_DRIVER, а в разі ACCESS_DENIED, викликатиму CloseProtocol() на агенті, який уже відкрив цей протокол і мені "заважає", (а це не секретна інформація, мій драйвер може викликати OpenProtocolInformation()), то уся система безпеки піде коту під хвіст - той агент не матиме й гадки, шо його від'єднали. Тільки якшо він сам викликає CloseProtocol(), це має сенс. CloseProtocol() просто "закриває" шо просиш, це "прибиральна", не "перевіряльна" функція, там нема ACCESS_DENIED. Знову - груба сила, шо його робити.
Далі за BY_HANDLE_PROTOCOL. З т.з. системи у цього варіанту можлива лише напередвизначений профіль самого агента - в якості агента виступає гендл самої фірмварі, а в якості гендла контроллера виступає NULL. Отже, на виході ми маємо тут фіксований атрибут, фіксовану пару агент/контроллер. Тільки змінна OpenCount тут може мати яксь варіації. Про цю змінну взагалі важко шо небудь сказати - шо вона має значити в контексті? Для чого вона? Шо ми знову бачимо - максимум, шо треба підтримувати в базі це Opencount для цього варіанту. З тим сенсом, шо цей лічильник даватиме нам можливість визначати чи є ще споживачі такого кшталту. Але от тут проблема виникає. Якшо у випадку за драйверами, тобто першою категорією, ми робимо реальне від'єднання з викликом DisconnectController() і дальшим ланцюжком, і клієнт справді змінює свій стан внаслідок цього від'єднання, в оповіщений щодо змін на протоколі, то тут, клієнт сам просто формально викликає CloseProtocol(), засвідчуючи свою дальшу нецікавість в протоколі. Якшо ми замість нього викликаємо цю функцію, як описано в UninstallProtooclInterface(), це ніяк не скаже клієнту про зміни, ніяк. Виходить, як і у випадку з GET_PROTOCOL - це небезпечна опція яка не має механізму себе убезпечити. Єдине її виправдання - це зворотня сумісність. Використовувати її в новому коді не треба взагалі. Користь від змінної OpenCount - суто статистична, ми просто знатимемо скільки таких клієнтів. І все. "Взула і забула". Того, подумавши, я взагалі нічого не додаю в БД з цим атрибутом. Знову - просто вертаю вказівник на інтерфейс якшо протокол інстальований на гендлі.
BY_CHILD_CONTROLLER
Єдина надія виправдати мання масиву агентів - це ця остання опція, яка очікується юзабельною, і дуже важливою для моделі драйверів UEFI. Я мало ще усвідомив за неї. Єдине це скоріше за все шинний драйвер, такий собі бонза в системі, відкриватиме так протокол для своїх "дітей". Отже це має бути треба. Має бути важливо. А не такий пустий формалізм і відбувайлівка як у вапидку з попередніми трьома опціями.
Отже, якшо я все правильно зрозумів, то наш масив протоколів трохи змінюється семантично.
Перше - в ньому виділяється "особливе місце" для відкривання драйвером (виключно теж) або застосунком виключно. Це перша категорія. Ми переносимо це в саму структуру протоколу. Тут ясно.
Друге - ніякої інформації про тестувальні відкривання а також відкривання через GET_PROTOCOL, BY_HANDLE_PROTOCOL, ми не тримаємо. Інформація в БД за це, нічого не робить нікому. Лише валятиметься в базі без діла і обслуговувальні функції, лише матимуть мороку возитися з цим.
Знову - ніякого убезпечення деінсталяції або реінсталяції для таких агентів ми зробити не зможемо - сама природа цього типу (BY_HANDLE_PROTOCOL), не дає це зробити - це зворотна сумісність - старий код просто викликає HandleProtocol() нічого не знаючи за оці всі агентські справи. І так усе тут і лишається з безпечністю як і було в часи коли той код був молодий. Але ми не маємо старого коду. Юху! Писатимемо наші драйвери по-новому, по-модньому. Ну і як сказав вище, OpenCount теж редуковано.
Четверте. Весь масив зводиться до елементів відкритих як BY_CHILD_CONTROLLER. Це полегшує. Але робить поле атрибуту в структурі баластом. Ну'.
Шодо виклику OpenProtocolInformation(), вона власне нічого не казатиме за BY_HANDLE_PROTOCOL, TEST_PROTOCOL і за GET_PROTOCOL, вона скаже шо нічого такого не знає і не бачила і не чула. А чого, я думаю я трохи пояснив угорі. Може це завалить тест відповідности (але ж до цього як до Києва рачки я вас балагаю), але зробить структуру і двигун БД вільним від штучок ради штучок, зробить простішим яснішим і таким, шо по ділу шось робить.