Цей допис не про Core Uefi, а так, дещо типу щоденникового запису в цю історію. По-перше, нарешті до мене вчора приїала сосна64+, а по-друге - трохи хотів записати за ту частину, яку я для себе коротко називаю "стартовий код", тобто та частина, з якої власне починається робота системи, яку можна реально намагатися запускати на голому залізі, bitches! xD
Про сосну. Замовив я її ще в квітні, але в чуваків виникла явна запарка з бізнесом - вони явно не впорувались з напливом охочих спробувати їхню плату. Довелось трохи почекати. Трохи менше двох тижнів тому, прийшло сповіщення на е-пошту, шо нарешті, плата їхатиме. Ну і менше як за два тижні вона прихала. :) Я купив лише плату і USB дріт для живлення - на шось ще, грошей не маю. Купив 2-х гігабайтну версію. Тож, до мого парку додалася ще одна машина, і вона з восьмим армом, SoC Allwinner A64 4x 1.2GHz Cortex-A53. Тепер з першої хвилі, я маю п'ять машин - дві з сьомим армом, дві з восьмим, і одну з 32-бітним міпсом. Серед цих машин, маю два Allwinner'івські камінця - A20 і A64. Це певною мірою добре, бо периферія може бути та сама і програмувати другий буде легше після першого.
Тепер до "стартового коду". Uefi якби має нижню "скриту" (від стандарту) фазу. Получилось сказати якось загадашно, але насправді за цим скривається просто волокіта з найнижньорівневішою ініціалізацією, перед тим, як система могтиме ганяти повну Uefi з усіма її протоколами і сервісами. Так от, ті самі уефішні чуваки рішили стандартизувати і цю частину, але окремо від Uefi. Назвали вони це PI - Platform Initialization. Вийшло дуже плутано і криво. І неповно! Набагато важче, ніж Uefi. І, власне кажучи, - здається вони зробили те ПІ несумісним з Uefi, шо не може не "радувати". Я не хочу переказувати усе своє незадоволення, це зайве мабуть, я просто хотів сказати - воно мені не дуже подобається. Але, оскільки я абсолютний нубас, то мені все ж краще пристати на таку сяку стандартизовану і так сяк описану систему, ніж намагатися з'архітектити шось своє. Це виглядає логічніше. Тож я взявся за ПІ. Але моя мета - Uefi імплементація, і, як наслідок, - сумісність саме з цим стандартом. Щодо сумісности з ПІ, то я не збираюсь відхилятися заради відхиляння, але точно не імплементуватиму шось, шо не є сумісним з Uefi, і, можливо, шось ще, шо видасться мені абсолютною фігнею (це термін). Особливо карявою в плані сумісности з Uefi виглядає Dxe фаза. Це просто таки дуже цікавий підхід - обізвати це одночасно імплементацією Uefi, окремим стандартом, і надмножиною (несумісною ясно) над Uefi. Але, знову, може я шось недочитав, не робімо поспішних висновків. Тим паче, шо до Dxe - як до Києва рачки.
ПІ розбивається на кілька фаз і трохи апендиксів. Основний хребет іде так - SEC->PEI->DXE->BDS.
SEC - це ота сама сама макушка початку, яка отримує керування від ром-коду (в нашому армівсько-міпсівському SBC-шному випадку. Ром-код ми вважатимемо теж SEC фазою, такою собі SEC0 підфазою, на яку ми не маємо ніякого впливу), і яку власне ром-код грузить в SRAM як голий бінарник. безструктурна, неспецифікована, вона має робити най най першу свою ініціалізацію процесора, втілювати (ну а з ром-кодом - перебирати на себе) повноваження кореня довіри, в питаннях безпеки, ініціалізувати стек там, налаштовувати вектори для винятків, може, і, завантажувати-налаштовувати-передавати керування на точку входу в Pei фазу - Pre Efi Initialization - ще одна перед-ініціалізація. Вона має, коротко кажучи, ініціалізувати процесор, чипсет і пам'ять - DRAM, і підготувавшись до цього, викликати точку входу в Dxe. Але там намудрували звичайно більше.
Ось оці дві фази і є те, над чим я парюсь зараз. Як видно, Dxe - Driver Execution Environment, я навіть не чіпав. Виглядає як Dxe є власне самим Uefi. Але з обмовками, шо в Dxe документаціях вона вдає з себе окремий стандарт і явно все норовить "покращувати". Як я вже казав, - це попередні висновки. Поки думатимемо про Dxe як про ранню Uefi фазу, яка нічого не "покращує", а просто робить те, шо від неї треба - приводить систему в стан, гожий для ганяння Uefi сервісів, які тоді використовуватимуться клієнтами - ОС лодарями, і іншими різними інструментами. Але все це діло є для вантаження ОС, - того ОС лодарями. Саме цю фазу, творці PI обізвали BDS - Boot Device Selection, і постійно повторюють, шо це там, де імплементується платформна політика завантаження (platform boot policy), а це власне - Uefi'шний Boot Manager - тож маємо трохи натяк як одне (PI) співвідноситься з іншим (Uefi). Взагалі, виглядає, шо PI - це до певної міри переробка Uefi і явно не в кращий бік. Хоча вона все ж сидить в іншій сфері - передініціалізація, тобто якби не претендує на витіснення Uefi, нема з її боку чітко визначеної межі, де вона закінчується, а де починається Uefi. От шо я хотів сказати, кажучи про "витісняє". Ця PI явно не знає, де вона закінчується, і явно залазить в Uefi, додаючи поверх останньої, ще якісь специфікації (якраз Dxe це особливо стосується). От і виглядає, шо з точки зору PI, UEFI - це всього одна BDS фаза. Не логічно. Позатим, це не так вже принципово, і я ж можу помилятися. Ми ж дивимося на PI саме як на PI - Platfotm Initialization. Саме ініціалізацію, за якою все готове для роботи Uefi.
Саме про це я читаю і саме над цим типу як працюю. Шоб зв'язати імплементацію сервісів, про які йшла мова в попередніх дописах, з стартовим кодом, шоб воно сходилося потихеньку. Це має робитися в паралель.
І наостанок, трохи додам обрисів як я бачу це на БББ - першій машині. Це може змінитися, я вже казав за це. Це лише ранні плани. Отже специфікація Pei постійно попереджає, шо вона - дуже рання фаза, і мовляв, краще робіть пізніше, в Dxe, а тут мовляв чудь чудь. Тим не менш, наварганили вони туди дуже багато вимог і архітектурних свистілок. Ядро Pei - це Pei Foundation, ядром якого є Pei Dispatcher. Ще є Peim'и - модулі, тобто драйвери Pei фази. І Pei сервіси - аналог Uefi Boot і Runtime сервісів - бібліотека для працівників. Якими є Peim'и, вони імплементують PPI - Peim to Peim Interfaces - аналог протоколів, так вони зв'язуються один з одним, використовуючи і надаючи фнкціонал, і так воно все крутиться. Фундація, в особі диспетчера, має витягати Peim'и з фірмварного тому, заносити їх у базу і виконувати їх по порядку. А значить має бути аналог бази даних цих інтерфейсів, з колбеками і нотифікаціями... В середовищі, де навіть пам'ять не ініціалізована (наприклад на міпсі криейторі, розмір tscm - SRAM'а - 16 KB, і ром-код грузе тільки 14KB навантаження)! Порядок виконання Peim'ів забезпечується спеціальною мовою залежностей - depex (dependency expressions). Свій депекс модуль кладе в окрему секцію свого файлу. Цей вираз залежности каже диспетчеру коли можна виконувати Peim'івську точку входу. Модуль повідомляє своїм виразом, які PPI (а значить Peim'и, які їх втілюють) потрібні для його роботи, і так диспетчер взнає, коли його запускати. Специфікатори PI вигадали "дуже" просту систему сховища для фірмварі - на один рівень ієрархії довшу, ніж у звичайних ФС. Бо окрім тому, файлової системи і файла, там виринає ще й секція! І шо більше - секції можуть бути вкладені. Тобто утворюють ієрархію самі по собі. І це тоді, коли вони уже використовують PE32+ формат, який і так уже має механізм секціонування і туди можна було б впхнути все, шо хочеш, навіть бітові карти кнопочок і іконок! Тим паче він би впорався з бінарним представленням депексів, з чим би завгодно впорався. Але нє, їм треба вткнути ще одну сутність в таку обмежену фазу. Ех. До речі, я серйозно розглядаю можливість знехтувати секціями фірмварних томів і використовувати саме PE секції, але ще треба подивитися, як воно відіб'ється на сумісності. Більше треба знати. Далі, в забезпеченні "ще більшої гнучкости", існує apriori файл на кожен фірмварний том, який з вищим пріоритетом, ніж депекси, тобто обчислення залежностей для Peim'ів, дає порядок виконання Peim'ів. Ояк "просто". Звичайно, це зроблено, шоб вгодити усім, на всі випадки. Ми не збираємося ігнорувати цю функціональність ядра Pei, але от виникає питання - чи потрібна у всіх випадках така аж занадто гнучка система? Наприклад на БББ, де є SoC від одного вендора, де всі Peim'и будуть написані однією людиною, чи треба роздрібнювати цю одну з найранніших фаз в таку надскладну систему з купою Peim'ів в кожному з складними залежностями від інших? Мабуть ні. Того я рішив на початках принаймні, оформити на БББ це дещо простіше, і це має виглядати так:
Отже ми маємо SEC фазу, це шматок стартового коду, який отримуватиме керування від ром-коду і передаватиме керування на Pei точку входу. Відповідно, він має розуміти інтерфейси з обох боків. Також "шось" там робитиме з процесором - ще уточнювати, шо саме. Також, можливо постачатиме свою таблицю векторів - обробляти винятки арма. Оскільки на Ситарі, ми НЕ в секуре моде, значить нам нічого робити з Монітором тут. Фізично SEC лежатиме там, де його може бачити ром-код. Спочатку ми беремо тільки SD картки, ані голий Nand, ані eMMC модулі ми не чіпаємо. Ми плануємо вкористовувати GPT партиціонування, отже нам треба змусити Ситарівський ром-код завантажувати в raw режимі, тобто не намагатися читати MBR/FAT. Тож або в кодовому блоці Protective MBR, принаймні частина, або - за PMBR і GPT заголовком. Це перший кілобайт на SD картках (бо сектор - 512 байтів). Тут виникає проблема, і вона переважно від дикої каші в описі того, шо ж насправді треба згодувати ром-коду в TI'шному TRM'і. Попри майже 5 тисяч (!) сторінок, ця надважлива частина задокументована жахливо. Курка лапою краще б задокументувала. Тож невідомо ще, чи взагалі зможемо ми згодувати йому пристрій з protective MBR, так, шоб він зрозумів, шо треба вантажити в сирому режимі. Очевидно, все мало бути просто - є спеціальний формат, який свідчить шо ось це воно, шо ти маєш вантажити в сирому режимі, і не пробувати читати МБР сигнатуру і тд. Він читає 4 місця, спершу 0 сектор. Там має бути TOC структура, а за нею GP заголовок в якому всього нічого - адреса і розмір куди і скільки треба вантажити, далі голий бінарник - наша SEC фаза. Ясно шо це мало якраз лежати в кодовому блоці захисного MBR. І влізло б! SEC фаза знала б, шо внутрі неї наcправді лежить MBR і GPT, і перестрибувала б куди треба трохи далі. Але. Почитайте в ТРМ'і, шо вони пишуть про той TOC. Там нічого не ясно - може чи не може бути він пустий. Тобто коротко кажучи вони в одному абзаці пишуть одне, в іншому протилежне. Але навіть вкупі - ця каша не дає і 10% потрібної інофрмації. Прийдеться питати в мейлинг листах. Там звичайно ніхто нічого не відповість, бо ті дрочуни тіке лінукс конпілірують, а далі виясняти методом тика. Не круто.
А от Pei фазу ми думаємо зробити так. Фізично це BFV (FV0) - Boot Firmware Volume. Лежатиме одразу за GPT заголовком, якшо не обліковуватимемо фірмварні томи як партції в GPT, або за Gpt partiotion array, в протилежному випадку. Це ще не вирішено. Але це релевантно лише для випадків, де системна партиція і фірмварне сховище лежать на одному носієві, тобто для SD карткового варіанту на етапі розробки і на випадки, де тільки так можна (в моїй класифікації - це другий тип плат, тип Raspbery Pi, Pine64, де все сховище - це витягувальний пристрій сховища, нема постійного на платі). Прикольно фірмварні томи (ФТ) мати зарахованими як партиції, це має свої зручності, але має й недолік - в цьому разі ром-код грузитиме в SRAM ще й таблицю партицій (бо вона має йти перед першою партицією, яку вона обліковує). Це не пагано само по собі, адже таблиця всеодно знадобиться пізніше. Пагано тим, шо це може не влізти в SRAM. Згадайте, на mips creator ci20 - це всього 14KB, точно не влізе. На Ситарі, - це 109 KB.
Гаразд, фізично - це наш BFV. Ми взагалі плануємо мати два ФТ - BFV і CFV. Другий - Core Firmware Volume - там лежатиме вся решта фірмварі. Таке розбиття пояснюється саме обмеженням на розмір SRAM і власне пов'язаним з ним - обмеженням на розмір скільки завантажує ром-код. На міпсі криейторі, нам очевидно прийдеться спочатку ініціалізувати пам'ять, і тільки потім завантажуватим CFV. Хоча, і на БББ так само, бо справа не лише в розмірі SRAM. Нам же вигідніше грузити основну фірмавю прямо в динамічну пам'ять. Шоб не копіювати потім. Хай який великий може бути срам, він всеодно маленький. :) Сама ж Pei планується складатися з двох PE32+ файлів. Pei.exe - Pei Foundation, Pei сервіси і інша стандартна логіка, і Pcr.sys, ім'я поки неостаточне - processor, chipset, ram інціалізація - Peim який робе важку роботу ініціалізації, навколо якої, власне вся волокіта. Перший є таким собі двигуном фази і є вельми платформно-незалежним. Наприклад, якшо я його навіть писатиму на асемблері, то можна очікувати, це буде той самий код для усіх сьомих армів. Хоч БББ, хоч КБ2 (муран, аргон). Другий очевидно дуже SoC залежний. Тож розділення вмотивоване і ясне. Тобто ми матимемо Peim'и, ми матимемо диспетчер їхнього виконання, але одразу видно - відпадає потреба в депексах як таких, і апріорі файлі. теж. Власне, специфікація не зобов'язує мати апріорі файл обов'язково, так само депекс може бути нульовим для Peim'а. Ядро має підтримувати ці механізми, але вони можуть бути присутні а можуть не бути як такі на конкртеній інстанціації. Саме так ми і плануємо зробити - наша інстанціація PI на БББ не планує дробити Pei фазу в купу Peim'ів, лише один, і не планує використовувати ані апріорі файл, ані депекси. Хоча можливо апріорі файл можна зробити, в якому тривіально буде прописаний один цей Peim. Так ми зможемо почати з простого, і потім додавати складіншу функціональність в уже робочий двигун, який уже запускався і на скількись перевірений. Є одна обмовка, шо може Peim'ів буде два, а не один. Справді, Pei має завершувати свою роботу підготуванням до передавання керування на Dxe. Це робиться архітектурно-визначено - потрібно мати спеціальний PPI - Dxe IPL PPI - Dxe Initial Program Loader PPI. Це інтерфейс, і ніхто не сказав, шо його неможна імплементувати в Pei.exe, але, можливо, наприклад через обмеження на розмір BFV, його прийдеться виділити в окремий модуль. І це явно "шось" третє з точки зору концептуального розділення на два вищезгаданих компонента. Там перший - це логіка фази, платформно не дуже залежна. Другий - драйвер, модуль, ініціалізації найнижчих низів - дуже залежний платформно - типовий драйвер. А це третє - архітектурна річ, але явно драйверо-подібна - це власне завантажувач всієї решти фірмварі, і йому можливо прийдеться багато працювати для запуску тих дальших фаз. Тобто він як агент від пізніших фаз в цьому Pei середовищі, який возиться з їхньою логікою, породжуючи середовище для них - концептуально якась окремішня, третя річ. Тобто може бути три файли. Але це не вплине на депекси чи апріорі файл - цей Dxe IPL PPI Peim, якшо це Peim, порядок його виконання явно визначений в специфікації і не потребує нішого іншого - він виконується в самому кінці Pei фази і власне вже не вертає керування. Тобто підсумовуючи, наш BFV складатиметься з двох або трьох файлів (якшо апріорі файл використовуватиметься, то на один більше).
BFV: [apriori file], Pei.exe, Pcr.sys, [DxeIpl.sys].
Звичайно, це лише обриси, які треба уточнювати, деталізувати, і наповювати змістом.
четвер, 4 серпня 2016 р.
четвер, 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 тип - це простір імен 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 коду, а це не дуже добре - має ще дивитися чи той шлях часом не є краєм шляху. Це впливає на те, який статус вертати в разі незнайдення/неприєдання жодного драйвера. Коли не знаєш, чим той шлях є, воно нічого подумати, нічого сказати за це - нема розуміння. Почитавши за шляхи, тепер уже набагато ясніше. Це знання. Але, і це мабуть видно з мого опису типів вузлів вище, моє знання за шлях пристрою, ще мале, таке, яке бува від одного прочитання. Але тепер ми маємо уявлення, шо то за "край пристрою", ми навіть знаємо як його перевіряти тут. Як я вже сказав, шляхи ці доведеться вивчити на зубок - вони грають важливу роль і зустрічаються часто. От колись побалакаємо за них і їхню імплементацію.
Загалом, це сервіс дуже складний, і його теперішню мою імплементацію навряд можна вважати остаточною, - вона точно принаймні недороблена. Про неправильна важко шось казати, потрібен час і дальша робота, але перша версія дуже швикдо виявилась неправильна. Сенс ролі сервісу дуже ясний і простий - він знаходить драйвер для контроллеру, для пристрою. Це прив'язування драйверу до прстрою Логіка ж цього знаходження, і, як наслідок, імплементація - складна.
Шо він робить? Це центральна частина моделі драверів 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_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, вона скаже шо нічого такого не знає і не бачила і не чула. А чого, я думаю я трохи пояснив угорі. Може це завалить тест відповідности (але ж до цього як до Києва рачки я вас балагаю), але зробить структуру і двигун БД вільним від штучок ради штучок, зробить простішим яснішим і таким, шо по ділу шось робить.
Балакаючи про мій шлях в цьому, спочатку варто згадати, шо була додана одна зміна в структуру елемента БД, а саме перший елемент з списку агентів тепер сидить в самій структурі протокола, решта все як було - нефрагментована таблиця агентів. Це було зроблено через сементику того прив'язування агентів. Агенти "відкривають" протоколи на гендлах з певними атрибутами. Власне ось вони всі сім на поки:
- 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, вона скаже шо нічого такого не знає і не бачила і не чула. А чого, я думаю я трохи пояснив угорі. Може це завалить тест відповідности (але ж до цього як до Києва рачки я вас балагаю), але зробить структуру і двигун БД вільним від штучок ради штучок, зробить простішим яснішим і таким, шо по ділу шось робить.
вівторок, 17 травня 2016 р.
Завантажувальні сервіси - Сервіси обслуговування протоколів
В попередьному дописі було зроблено огляд БД гендлів і описано пошукові сервіси, які працюють на цій БД. Тепер опишемо сервіси, які вставляють і видаляють елементи.
Коротко нагадаємо структуру БД. Вона складається з таблиць і дерева.
Отже, ми маємо гендли на кожному з яких є таблиця істальованих на нього протоколів, і маємо дерево протоколів, на кожному вузлі якого є таблиця вказівників на гендл, на яких цей протокол істальований. Ця надлишковість дуже пришвидшує пошук за протоколом і не дуже ускладнює сервіси видалення/вставлення.
Які вони? Специфікація встановлює такі:
Функції, які інсталюють або деінсталюють протокольні інтерфейси як вони представлені на рівні специфікації - суть функції, які взагалі будують БД, бо по-перше їхня базова функціональність уже диктує це, а по-друге, специфікація покладає на них неявну роботу з видалення/вставлення всієї ієрархії, наприклад якшо функція InstallProtocolInterface() отримує як значення гендла NULL, то вона має створити гендл, а потім вже інсталювати туди протокол. Так само, - ці функції беруть на себе тягар з побудови імплементцінйо-специфічної структури БД.
Спочатку про словник. Було вже згадано, шо видалення на словнику немає, це насправді дуже вигідно для нашого випадку - фірмваря матиме пам'ять для цього, а от робити зайву роботу не буде. Тож уся складність сервісів видалення - це складність визначена не структурою БД, а семантикою прив'язування агентів до протоколів. Це буде висвітлено окремо.
Переповнення масивів.
Ми вибрали організацію, коли може бути переповнення масивів, і, в разі цього, їх треба розширяти. Це було вибране як кращий варіант, за списки масивів. Розширення робиться спочатку спробою справді розширити виділену ділянку за допомгою внутрішнього сервісу пам'яти, а якшо виділити таке розширення не вдасться, тоді робиться виділення більшого буферу і копіювання старого туди. Це очікується як рідкісна подія. Єдина структура, яка не зможе бути перекопійована - це таблиця гендлів - значення гендлів (як змінних) це адреси структур гендлів в таблиці, якшо ми перенесемо таблицю, зміняться адреси, але клієнти користуються старими гендлами і оповіщати усіх про такі зміни нереалістично. Тобто ми можемо лише спробувати розширити цю таблицю прямо. Внутрішня функція для цього - MiExtendPool(). А взагалі, краще вгадати з кількістю гендлів з самого початку. Це розплата за константний час пошуку гендла.
InstallProtocolInterface(). З усього вище сказаного, витікає, шо більшість роботи пов'язаної з побудовою і перебудовою БД, лягає на цю функцію. Вона інсталює протокол на гендл, якшо треба - створює гендл. А значить, вона має вміти:
UninstallProtocolInterface(). Сервіс видалення, був би дуже складний, з т.з. БД, якби ми мали потребу справді видаляти вузол з словника. Але оскільки ми здогадались, шо цього насправді не треба робити - складність відпадає. Так само нема складности з розширенням навпаки - ми просто не робимо звуження масиву - це просто не потрібно. А от шо потрібно - робити дефраґментацію на видаленні, для нефраґментованих таблиць (а це масиви протоколів, масиви агентів і масиви вказівників на гендли). На видаленні, функція переносить крайній елемент таблиці в новостворену дірку. Також ми налаштовуємо поля в відповідних структурах - зменшуємо лічильник кількости елементів підлеглої таблиці (він же - індекс на першу вільну комірку таблиці). Ця функція робить дефраґментацію на таблицях протоколів і таблицях вказівників на гендли, а CloseProtocol() робить це на таблицях агентів. Декрементація лічильника робиться цією функцією на вузлах словника і гендлах (кількість гендлів і протоколів відповідно). На протоколах декрементація (кількість агентів) робиться CloseProtocol(). Деінсталяція гендла - це позначення поля валідности відповідної структури в стан "невалідний". І ще одна заввага - оскільки протоколи з словника не видаляються, може виникнути питання про стан тих записів протоколи яких насправді ніде не інстальовані (адже протоколи завсіди мають бути інстальвонаі на якийсь гендл). І як такі записи в словнику узгоджуються з консистентністю БД. Відповідь проста - поле кількости гендлів на масиві вказівників на гендл для такого вузла дорівнює нулю. Це і є ознака неістальованости протокола. З т.з. системи він не присутній в системі, - ніде не інстальований, але з т.з. імплементації ми покращили - спростили двигун БД і пришвидчили вставлення - якшо хтось захоче таки інсталювати цей протокол знову - ми вже маємо запис в словнику, і вставляти знову вузол не треба.
ReinstallProtocolInterface() - просто міняє вказівник на інтерфейс на протоколі інстальованому на гендлі. Там це "просто" - дешо складніше, але знову не з т.з БД, а з т.з. синхронізації і звязків між агентами і протоколами. Так же само як сервіси на агентах мають набагато більше мороки з цими зв'язками. Власне саме вони й обслуговують усю кашу з прив'язуванням драйверів і відслідкованим, контрольованим деінсталюванням/реінсталюванням. За це наступного разу.
Шодо виділення пам'яти. На цьому етапі, ми виділяємо пам'ять для структур з системного пулу, використовуючи функцію AllocatePool(). Це стосується всіх структур, крім таблиці гендлів, яка покишо не має імплементованої точки створення - це верхня ініціалізація фірмварі, вона ще не написана. Але там ми або так само використовуватимемо сервіси пулу, або сервіс сторінкового виділення. Невидно поки ніякої потреби тут створювати якийсь спеціальний алокаційний велосипед. Також ми додали внутрішню, вже згадану, функцію MiExtendPool(). Її прототип такий:
Вона розширює виділений за допомогою AllocatePool() буфер, на (UnitSize * (*pUnitCount)) байтів, якшо може, або на N одиніць довжиною UnitSize байтів, де 0 <= N <= UnitCount. І пише виділену кількість одиниць назад в змінну (*pUnitCount). Це дуже вдалий вибір логіки аргументів, бо з ним ми уникаємо проблеми невирівняного виділення. Справді, специфікація вимагає виділення з пулу робити вирівняними на 8 байтів - логічно і розумно. Шоб не створювати проблем, ми й виділювані блоки робитимемо цілим множником 8-ми. От уявімо наша структура протоколу має 24 байти в довжину. Якби ми просили просто розширити на скількись 8-батних блоків, могло б вийти неціле число 24-байтних блоків, тобто - неціле число одиниць протоколів. Тоді на другому розширенні, ми могли б мати хибну нестачу місця. Справді, уявімо, шо на першому розширенні масиву протоколів, функція розширення вернула нам 32 байти розширення. Нам вистачить і одного блоку (24 байти) на раз. Ми ділячи виділене на довжину нашого блоку взнаємо кількість нових місць, а зайве місце не чіпаємо. Але якшо нам знадобиться ще й вдруге доточувати масив, ми наприклад можемо зіткнутись з таким сценарієм і проблемою. Шоразу протяжного місця все менше, його може й не бути, отже вдруге функція може вернути лише 16 байтів. Цього не досить, і ми підемо перевиділяти місце і копіювати - дорога операція. Але насправді, ми мали 12 зайвих байтів уже, плюс 16 додано ще, - цього вистачить на один елемент! Але ми цього не бачимо, бо наша структура не має поля для "зайвого місця" - це було б кривобоко ліпити його туди. Отже, рішення полягає в запитуванні розширювати масив на кількість в блоках. Це не порушує внутрішнього алокаційного закону виділяти все на вирівняних на 8 адресах і виділяти цілими до 8-ми штуками; і робить доточування вільним від тієї кострубатости. Як воно не порушує? Ну, ми маємо просити так, шоб принаймні (UnitSize * UnitCount) ділився націло на 8, а краще й UnitSize мати таким, якшо можливо - нам це і так треба, - шоб не мати внутрішніх проблем з вирівнюванням в структурах. На 32-х платформах, вказівник на гендл - а ми маємо таку таблицю, 4-байтний. Ми отримаємо шонайменше 2 місця замість 1-го з ненульових. От і все. Ми просто не маємо просити дати один 4-байтний блок - функція верне неуспіх. Доточування робиться так:
Ми пишемо в змінну бажану кількість одиниць, на яку б ми хотіли доточити наш масив, і викликаємо функцію, а вона вертає в разі успіху, в цю змінну, - кількість одиниць, яку вона справді виділила, і ця кількість буде від нуля до нашої запитаної кількости. Тож, у випадку з 32-бітними платформами, функція шонайменше верне 2 з ненульвоих варіантів, адже всеодно семантика виділення на пулі диктує 8-байтне вирівнювання, і, як наслідок, - мінімальний блок виділення 8 байтів. В разі неуспіху, функція не чіпатиме масив і залишить все як було.
Цей сервіс дасть нам ще один шанс уникнути копіювання. А у випадку з таблицею гендлів - це єдина надія.
Коротко нагадаємо структуру БД. Вона складається з таблиць і дерева.
- Таблиця (структур) гендлів, одна на всю систему, вона може бути фраґментована і дефраґментація може лише відбуватися на вставленні нових елементів. В разі переповнення можливе лише пряме розширення. В разі неуспіху його - крах системи.
- Таблиця (структур) протоколів. По одній на кожен валідний гендл. Містить масив протоколів, інстальованих на цьому гендлі. Ця таблиця нефраґментована - завсіди на видаленні протокола з гендла, крайній елемент таблиці переноситься в новоутворену від видалення дірку, і масив завсіди лишається суцільним. Може бути розширена прямо, або через перевиділення нового більшого буферу і копіювання старої таблиці туди.
- Таблиця агентів. По одній на кожен протокол (в масиві протоколів на гендлі). Так само - нефраґментована таблиця, розширювана прямо і через копіювання в крайньому разі.
- Дерево протоколів. Одне на всю систему. Червоно-чорне двійкове дерево пошуку. Грає роль словника - містить усі унікальні протоколи, які були інстальовані на систему під час однієї сесії. Це публічна інофрмація за протокол, глобальна для всієї системи - словник. Приватна - це масиви протоколів на гендлах. Ми не видаляємо вузол взагалі, він може бути інстальований знову, і це добра оптимізація. До того ж, нам не треба робити видалення на червоно-чорних деревах, а це дуже марудна справа.
- Масив вказівників на структури гендлів (з таблиці гендлів). По одному на кожен вузол словника протоколів. Нефраґментована, розширювна прямо і через копіювання таблиця.
Отже, ми маємо гендли на кожному з яких є таблиця істальованих на нього протоколів, і маємо дерево протоколів, на кожному вузлі якого є таблиця вказівників на гендл, на яких цей протокол істальований. Ця надлишковість дуже пришвидшує пошук за протоколом і не дуже ускладнює сервіси видалення/вставлення.
Які вони? Специфікація встановлює такі:
- InstallProtocolInterface()
- ReinstallProtocolInterface()
- UninstallProtocolInterface()
- InstallMultipleProtocolInterfaces()
- UninstallMultipleProtocolInterfaces()
Функції, які інсталюють або деінсталюють протокольні інтерфейси як вони представлені на рівні специфікації - суть функції, які взагалі будують БД, бо по-перше їхня базова функціональність уже диктує це, а по-друге, специфікація покладає на них неявну роботу з видалення/вставлення всієї ієрархії, наприклад якшо функція InstallProtocolInterface() отримує як значення гендла NULL, то вона має створити гендл, а потім вже інсталювати туди протокол. Так само, - ці функції беруть на себе тягар з побудови імплементцінйо-специфічної структури БД.
Спочатку про словник. Було вже згадано, шо видалення на словнику немає, це насправді дуже вигідно для нашого випадку - фірмваря матиме пам'ять для цього, а от робити зайву роботу не буде. Тож уся складність сервісів видалення - це складність визначена не структурою БД, а семантикою прив'язування агентів до протоколів. Це буде висвітлено окремо.
Переповнення масивів.
Ми вибрали організацію, коли може бути переповнення масивів, і, в разі цього, їх треба розширяти. Це було вибране як кращий варіант, за списки масивів. Розширення робиться спочатку спробою справді розширити виділену ділянку за допомгою внутрішнього сервісу пам'яти, а якшо виділити таке розширення не вдасться, тоді робиться виділення більшого буферу і копіювання старого туди. Це очікується як рідкісна подія. Єдина структура, яка не зможе бути перекопійована - це таблиця гендлів - значення гендлів (як змінних) це адреси структур гендлів в таблиці, якшо ми перенесемо таблицю, зміняться адреси, але клієнти користуються старими гендлами і оповіщати усіх про такі зміни нереалістично. Тобто ми можемо лише спробувати розширити цю таблицю прямо. Внутрішня функція для цього - MiExtendPool(). А взагалі, краще вгадати з кількістю гендлів з самого початку. Це розплата за константний час пошуку гендла.
InstallProtocolInterface(). З усього вище сказаного, витікає, шо більшість роботи пов'язаної з побудовою і перебудовою БД, лягає на цю функцію. Вона інсталює протокол на гендл, якшо треба - створює гендл. А значить, вона має вміти:
- вставляти протокол в словник (операція на червоно-чорному дереві - не найлегша операція) - "глобальна" інсталяція протоколу,
- вставляти гендл в таблицю - створення гендла,
- виділяти таблицю протоколів для новоствореного гендла,
- вставляти протокол в таблицю протоколів на гендлі - власне інсталяція протокола,
- вставляти вказівник на гендл в масив вказівників на вузлі дерева;
- розширювати прямо таблицю гендлів,
- розширювати прямо або копіюванням таблицю протоколів,
- розширювати прямо або копіюванням масив вказівників на гендл.
UninstallProtocolInterface(). Сервіс видалення, був би дуже складний, з т.з. БД, якби ми мали потребу справді видаляти вузол з словника. Але оскільки ми здогадались, шо цього насправді не треба робити - складність відпадає. Так само нема складности з розширенням навпаки - ми просто не робимо звуження масиву - це просто не потрібно. А от шо потрібно - робити дефраґментацію на видаленні, для нефраґментованих таблиць (а це масиви протоколів, масиви агентів і масиви вказівників на гендли). На видаленні, функція переносить крайній елемент таблиці в новостворену дірку. Також ми налаштовуємо поля в відповідних структурах - зменшуємо лічильник кількости елементів підлеглої таблиці (він же - індекс на першу вільну комірку таблиці). Ця функція робить дефраґментацію на таблицях протоколів і таблицях вказівників на гендли, а CloseProtocol() робить це на таблицях агентів. Декрементація лічильника робиться цією функцією на вузлах словника і гендлах (кількість гендлів і протоколів відповідно). На протоколах декрементація (кількість агентів) робиться CloseProtocol(). Деінсталяція гендла - це позначення поля валідности відповідної структури в стан "невалідний". І ще одна заввага - оскільки протоколи з словника не видаляються, може виникнути питання про стан тих записів протоколи яких насправді ніде не інстальовані (адже протоколи завсіди мають бути інстальвонаі на якийсь гендл). І як такі записи в словнику узгоджуються з консистентністю БД. Відповідь проста - поле кількости гендлів на масиві вказівників на гендл для такого вузла дорівнює нулю. Це і є ознака неістальованости протокола. З т.з. системи він не присутній в системі, - ніде не інстальований, але з т.з. імплементації ми покращили - спростили двигун БД і пришвидчили вставлення - якшо хтось захоче таки інсталювати цей протокол знову - ми вже маємо запис в словнику, і вставляти знову вузол не треба.
ReinstallProtocolInterface() - просто міняє вказівник на інтерфейс на протоколі інстальованому на гендлі. Там це "просто" - дешо складніше, але знову не з т.з БД, а з т.з. синхронізації і звязків між агентами і протоколами. Так же само як сервіси на агентах мають набагато більше мороки з цими зв'язками. Власне саме вони й обслуговують усю кашу з прив'язуванням драйверів і відслідкованим, контрольованим деінсталюванням/реінсталюванням. За це наступного разу.
Шодо виділення пам'яти. На цьому етапі, ми виділяємо пам'ять для структур з системного пулу, використовуючи функцію AllocatePool(). Це стосується всіх структур, крім таблиці гендлів, яка покишо не має імплементованої точки створення - це верхня ініціалізація фірмварі, вона ще не написана. Але там ми або так само використовуватимемо сервіси пулу, або сервіс сторінкового виділення. Невидно поки ніякої потреби тут створювати якийсь спеціальний алокаційний велосипед. Також ми додали внутрішню, вже згадану, функцію MiExtendPool(). Її прототип такий:
EFI_STATUS MiExtendPool( IN PVOID pBuffer, IN UINTN UnitSize, IN OUT PUINTN pUnitCount );
Вона розширює виділений за допомогою AllocatePool() буфер, на (UnitSize * (*pUnitCount)) байтів, якшо може, або на N одиніць довжиною UnitSize байтів, де 0 <= N <= UnitCount. І пише виділену кількість одиниць назад в змінну (*pUnitCount). Це дуже вдалий вибір логіки аргументів, бо з ним ми уникаємо проблеми невирівняного виділення. Справді, специфікація вимагає виділення з пулу робити вирівняними на 8 байтів - логічно і розумно. Шоб не створювати проблем, ми й виділювані блоки робитимемо цілим множником 8-ми. От уявімо наша структура протоколу має 24 байти в довжину. Якби ми просили просто розширити на скількись 8-батних блоків, могло б вийти неціле число 24-байтних блоків, тобто - неціле число одиниць протоколів. Тоді на другому розширенні, ми могли б мати хибну нестачу місця. Справді, уявімо, шо на першому розширенні масиву протоколів, функція розширення вернула нам 32 байти розширення. Нам вистачить і одного блоку (24 байти) на раз. Ми ділячи виділене на довжину нашого блоку взнаємо кількість нових місць, а зайве місце не чіпаємо. Але якшо нам знадобиться ще й вдруге доточувати масив, ми наприклад можемо зіткнутись з таким сценарієм і проблемою. Шоразу протяжного місця все менше, його може й не бути, отже вдруге функція може вернути лише 16 байтів. Цього не досить, і ми підемо перевиділяти місце і копіювати - дорога операція. Але насправді, ми мали 12 зайвих байтів уже, плюс 16 додано ще, - цього вистачить на один елемент! Але ми цього не бачимо, бо наша структура не має поля для "зайвого місця" - це було б кривобоко ліпити його туди. Отже, рішення полягає в запитуванні розширювати масив на кількість в блоках. Це не порушує внутрішнього алокаційного закону виділяти все на вирівняних на 8 адресах і виділяти цілими до 8-ми штуками; і робить доточування вільним від тієї кострубатости. Як воно не порушує? Ну, ми маємо просити так, шоб принаймні (UnitSize * UnitCount) ділився націло на 8, а краще й UnitSize мати таким, якшо можливо - нам це і так треба, - шоб не мати внутрішніх проблем з вирівнюванням в структурах. На 32-х платформах, вказівник на гендл - а ми маємо таку таблицю, 4-байтний. Ми отримаємо шонайменше 2 місця замість 1-го з ненульових. От і все. Ми просто не маємо просити дати один 4-байтний блок - функція верне неуспіх. Доточування робиться так:
Noh = gProtocolArrayLength; Status = MiExtendPool(pHandle->pProtocolArray, sizeof(PROTOCOL), &Noh);
Ми пишемо в змінну бажану кількість одиниць, на яку б ми хотіли доточити наш масив, і викликаємо функцію, а вона вертає в разі успіху, в цю змінну, - кількість одиниць, яку вона справді виділила, і ця кількість буде від нуля до нашої запитаної кількости. Тож, у випадку з 32-бітними платформами, функція шонайменше верне 2 з ненульвоих варіантів, адже всеодно семантика виділення на пулі диктує 8-байтне вирівнювання, і, як наслідок, - мінімальний блок виділення 8 байтів. В разі неуспіху, функція не чіпатиме масив і залишить все як було.
Цей сервіс дасть нам ще один шанс уникнути копіювання. А у випадку з таблицею гендлів - це єдина надія.
четвер, 12 травня 2016 р.
Завантажувальні сервіси - Структура бази даних гендлів
Я згадував, шо працюю зараз над імплементацією UEFI десь так зсередини, тобто я не став вчити спочатку глибочезний апаратний низ якоїсь платформи і, відповідно, - писати такий собі Start.asm (це теж буде, а якже, але пізніше); я сів вчити саму UEFI, читати її специфікацію, і по ходу діла, бачачи, шо тут можна шось писати, почав це робити. Спочатку думав робити це у вигляді C-побідного псевдокоду, але потім виявилось, шо в принципі нічого не заважає намагатися робити повну імплементацію окремих принаймні функцій. Отже, "ядро" UEFI організоване в сервіси - набір функцій, які обслуговують різну функціональність - API, тільки прив'язується клієнт до нього не через статичне чи динамічне лінкування, а через таблицю диспетчеризації, яку ядро надає кожному завантаженому клієнтові. Це драйвери, застосунки, завантажувачі ОС (теж застосунки). Сервіси суть розбиті на дві категорії - Boot Services, базові сервіси доступні в т.зв. "передзавантажувальному" середовищі (pre-boot environment), тобто власне - в час роботи самої UEFI, і Runtime Services - сервіси часу виконання - виконання ОС мається на увазі, тобто це служби, які ОС може в себе викликати, якшо хоче. Вони доступні і під час передзавантажувального періоду, і після перехоплення контролю ОС, і ОС має розуміти і поважати їхню наявність, шоб могти ними користуватися.
Я, читаючи специфікацію, і дійшовши до детального опису завантажувальних сервісів (ЗС), а це глава 6-та в специфікації, вирішив засісти за імплементацію сервісів обслуговування протоколів, СОП, (Protocol Handler Sevices). Всі ЗС діляться на підкатегорії:
З евентами, синхронізацією і тим паче - керуванням пам'яттю все дуже не ясно покишо, тож я засів за СОП (сервіси обслуговування протоколів). От про структури даних, повязані з цими сервісами я сьогодні і розкажу. Як я їх вирішув побудувати.
Ці структури даних організовуються в базу даних гендлів (Handle Data Base). Відповідно існує структура, яка цю базу описує. Це такий собі корінь. Уся база організована з структур двох типів, один з яких, - розбивається на 2 підтипи. Отже які об'єкти складають наші дані в роботі з СОП? Це гендли й протоколи. А також - агенти, які є набором гендлів з атрибутами. Інші об'єкти, які можуть використовуватися, але суть з інших категорій, такі як події і таймери, будуть розглянуті в своїх темах, а те, шо стосується їхнього використання в обслуговуванні протоколів, буде додано пізніше, бо як я вже сказав - синхронізація ще не розроблена взагалі. Я не описуватиму, шо значить кожна сутність визначена в UEFI, це треба читати специфікацію, я ж намагаюсь розказувати за імплементацію. Отже гендл це обєкт, який асоціюється з пристроєм, з образом (драйвера, застосунку або самого UEFI), - це персоналізація базового структруного елементу фірмварі. Протокол - це набір з імени і структури, яка містить вказівники на функції і дані, які цей протокол імплементує. Ім'я робиться у вигляді GUID'а.
Цікавий момент з протоколами полягає в тому, шо ідентифікуються вони саме за іменем, а от інтерфейс може бути різним, або взагалі не бути. Протоколи інсталюються на гендл, і там вони існують. Є функції які можуть переінсталювати той самий протокол помінявши в ньому вказівник на інтерфейс, конткретно, - це сервіс ReinstallProtocolInterface(). З цього й видно - шо хоч і здається, шо конткретний протокол має бути прив'язаний до конктретного його наповнення (інтерфейсної структури, - функцій, які він надає), це не зовсім так, і в системі можуть існувати два (або більше) гендли, на яких інстальовано той самий протокол, але інтерфейсні вказівники - різні. Але, з іншого боку, для системи - це один і той самий протокол. Це видно з опису певних функцій СОП. Наприклад функція LocateHandle() або LocateHandleBuffer(), якшо тип пошуку задано ByProtoocol, має просто вернути масив усіх гендлів "на яких інстальовано цей протокол". А протокол задається лише GUID'ом.
Отже ми маємо гендли. Сам тип, визначений специфікацією, - EFI_HANDLE, визначений як VOID * не зобов'язаний бути вказівником на шось. Тобто його значення має "непрозору" (opaque) семантику. Якась прив'язка цього значення до внутрішньої імплементації, вирішується самою імплементацією. І я вирішив використовувати для значення гендла адресу на структуру, яка його представляє. Це дає велику оптимізацію. Бо пошук гендла стає константним по часу - незалежним від кількости гендлів в системі. Ми просто приводимо гендл до типу вказівника на структуру гендла, і якшо треба, читаємо поле валідности - це дає ґарантію, шо ми не поліземо писати в незрозуміло яку ділянку памяти, і далі вже працюємо з гендловою структурою. Не треба нічого шукати взагалі. З іншого боку, адреси - унікальні, тож якшо ми маємо два гендли з різним значенням, вони представляють два різні гендли. І навпаки - два гендли з одним і тим значенням - це той самий гендл. Це оптимальне рішення звичайно має свою ціну - а саме - ми маємо фраґментацію таблиці гендлів. Гендли організовані в таблицю. Це просто масив структур. Висока локальність, без надмірности зберігання вказівників (як в списках чи деревах). Це перший підтип табличних структур нашої бази. І лише гендли так організовані. Ця таблиця може бути фрагментована, оскільки раз виділивши гендл десь в таблиці, ми не можемо пересовувати його деінде доки він валідний. Це таблиця з непересувними елементами. Дефраґментація відбувається тільки на додаванні - ми вставляємо новий гендл не в край, а спочатку дивимося чи є дірки, якшо є, вставляємо в першу від початку, і тільки якшо нема дірок, вставляємо в край.
Другий табличний тип - це нефраґментовані таблиці. Елементи в них можуть бути пересунуті на видаленні (дивись нижче), тим самим просто підтримуючи протяжність таблиці. В такі таблиці організовані протоколи інстальовані на гендл, і таблиці агентів, які споживають конкретний протокол на конкретному гендлі. Спочатку я думав мати там списки, двонаправлені, закільцьовані, але прийшов до висновку, шо краще динамічні масиви. Знову - масиви дають максимальну локальність і займають менше місця. Вони лінійні по пошуку. Але тут питання кількости. Треба розуміти - ми маємо таблицю гендлів, на кожному гендлі інстальовано протоколи, і кожен протокол має скількись агентів, які його споживають. Просто неймовірно, шоб оці дві останні таблиці були довгими. Важко сказати, нажаль, це не легко оцінити, але, з іншого боку, очевидно - це не може бути великим числом. Тобто питання кількох одиниць на таблицю. Уявляєте якою надмірністю тут було б використовувати дерева! І з огляду на масштабованість - так само неймовірно, шо з часом ця кількість збільшиться, просто по суті - тут не може бути великої кількости елементів. Ну і якшо вже буде - тоді переробимо на дерева.
Як робитиметься дефраґментація. На видаленні якогось елемента (аґента чи протокола), функція, поміщатиме на його місце крайній елемент в списку. Спочатку здавалось, шо дефраґментація буде складним завданням і дорогою операцією. Потім прийшла здогадка про оцю релокацію на видаленні. Таблиця увесь час протяжна. І це досягається за константний час. Ми маємо індекс на перше вільне місце в струткурі, яка має цей масив. Наприклад в гендлі, ми маємо поле NumberOfProtocolInterfaces - це і кількість протоколів інстальованих на цьому гендлі, й індекс першої вільної клітини в масиві. Завдяки цьому полю ми маємо доступ до останнього елемнта константно. І таким чином можемо робити константне переміщення для дефраґментації. Ясно, шо якшо елемент, який видаляється останній, переміщення немає. Нажаль з причин уже вказаних вище, ми не можемо так робити з таблицею гендлів. Але там ми маємо константний доступ, а тут лінійний. Просто лінійність тут не буде повільною - ми маємо невелику кількість елементів. Надто невелику, шоб будувати тут дерева.
І третій тип в нашій базі. Це допоміжна структура. Це словник протоколів в системі. Як вже було відзначено, семантика протоколів взагалі робить їх більше приватними для гендла, згадайте хоча б свободу реінсталювати на протоколі інтерфейсний вказівник, - це має приватний обшир для конкретного гендла. З іншого боку, ми маємо кілька сервісних функцій, сенс яких аж вимагає мати системний словник для протоклів - адже попри волю в виборі інтерфейсу, протокол для системи - це його GUID і всі протоколи інстальовані на різних гендлах з одиним і тим же GUID'ом є одним і тим же протоколом. Саме для оптимізації пошуку за протоколом, ми вводимо третю допоміжну структуру - словник протоколів. Такі функції як LocateHandle() і LocateHandleBuffer(), якшо параметр пошуку є ByProtocol, а також LocateProtocol(), вони мають ключем пошуку саме протокол (його GUID). Очевидно якшо ми шукатимемо через таблицю гендлів, а потім через масив протоколів в кожному гендлі, ми матимемо жахливу залежність O(N*m') - де
N - це кількість гендлів системі (в таблиці), m' - це середня кілкість протоколів в таблиці протоколів кожного гендла. Інші ж функції, а саме ProtocolsPerHandle() , або вищезгадані перші дві, якшо параметр пошуку - AllHandles, виграють сильно в своїх операціях від використання таблиці гендлів. Адже без пошуку вони вже мають гендл, а потім просто видають список протоколів інстальвоаних на гендл, або видають всі гендли в системі - це лінійна робота за будь якої структури організації. Саме набір семантики оцих сервісів і диктував вибір організації підлеглої бази даних. Варто коротко нагадати які саме пошукові сервіси задані UEFI:
Це такі функції:
N - число гендлів в системі,
m' - середнє число протоколів на гендлі
n' - середнє число гендлів на протоколі
M - число протоколів в системі
Очевидно, очікується, шо n' - буде вельми малим, - це кількість гендлів, на яких цей протокол інстальований. Багато протоколів будуть інстальовані взагалі на одному гендлі; ну а M у нас уже під логаритмом. Погодьтесь, різниця є.
Отже підсумовуючи. Структура БД вибрана з огляду на семантику сервісів, які в свою чергу вибрані такими, творцями специфікації з огляду на зручність їхнього використання в клієнтах. БД організована як таблиця гендлів, яка є фрагментований (можливо) динамічний масив. Кожен гендл має таблицю протоколів, які на ньому інстальовані - це нефрагментований масив. І кожен протокол має таблицю агентів, які його споживають на цьому гендлі - ця таблиця теж нефрагментована. Дефрагментація досягається переміщенням крайнього елементу на видаленні як згадано вище. Фрагментованість таблиці гендлів іде як ціна від прискорення через використання адреси структури гендла як його значення, для його ідентифікації. Але фрагментованість очікується низька. До того ж система всеодно має можливість дефраґментувати таблицю на вставленнях.
Нарешті, для прискорення пошукових сервісів, які мають як ключ пошуку протокол, вводиться допоміжна структура - словник протоколів в системі, імплементований як двійкове червоно-чорне дерево пошуку, з високими показниками ефективности пошуку.
Пізніше я додам іллюстрацію, а в наступній статті, проаналізую функції додавання/видалення на описаних структурах, які відбуваються на сервісах інсталяції/деінсталяції гендлів/протоколів/агентів.
Така ось крива й коса ілюстрація бази даних гендлів. може колись я навчусь краще робити ілюстрації, а покишо ось:
Я, читаючи специфікацію, і дійшовши до детального опису завантажувальних сервісів (ЗС), а це глава 6-та в специфікації, вирішив засісти за імплементацію сервісів обслуговування протоколів, СОП, (Protocol Handler Sevices). Всі ЗС діляться на підкатегорії:
- Event and Timer Services - сервіси подій і таймерів
- Task Priority Level Services - сервіси синхронізації за рівнем пріоритету завдання
- Memory Services - сервіси пам'яти (інтерфейс аллокатора пам'яти)
- Protocol Handler Services - сервіси обслуговування протоколів
- Image Services - сервіси завантажувача образів
- Miscelaneous Services - різне, наприклад сервіс підрахунку Crc32
З евентами, синхронізацією і тим паче - керуванням пам'яттю все дуже не ясно покишо, тож я засів за СОП (сервіси обслуговування протоколів). От про структури даних, повязані з цими сервісами я сьогодні і розкажу. Як я їх вирішув побудувати.
Ці структури даних організовуються в базу даних гендлів (Handle Data Base). Відповідно існує структура, яка цю базу описує. Це такий собі корінь. Уся база організована з структур двох типів, один з яких, - розбивається на 2 підтипи. Отже які об'єкти складають наші дані в роботі з СОП? Це гендли й протоколи. А також - агенти, які є набором гендлів з атрибутами. Інші об'єкти, які можуть використовуватися, але суть з інших категорій, такі як події і таймери, будуть розглянуті в своїх темах, а те, шо стосується їхнього використання в обслуговуванні протоколів, буде додано пізніше, бо як я вже сказав - синхронізація ще не розроблена взагалі. Я не описуватиму, шо значить кожна сутність визначена в UEFI, це треба читати специфікацію, я ж намагаюсь розказувати за імплементацію. Отже гендл це обєкт, який асоціюється з пристроєм, з образом (драйвера, застосунку або самого UEFI), - це персоналізація базового структруного елементу фірмварі. Протокол - це набір з імени і структури, яка містить вказівники на функції і дані, які цей протокол імплементує. Ім'я робиться у вигляді GUID'а.
Цікавий момент з протоколами полягає в тому, шо ідентифікуються вони саме за іменем, а от інтерфейс може бути різним, або взагалі не бути. Протоколи інсталюються на гендл, і там вони існують. Є функції які можуть переінсталювати той самий протокол помінявши в ньому вказівник на інтерфейс, конткретно, - це сервіс ReinstallProtocolInterface(). З цього й видно - шо хоч і здається, шо конткретний протокол має бути прив'язаний до конктретного його наповнення (інтерфейсної структури, - функцій, які він надає), це не зовсім так, і в системі можуть існувати два (або більше) гендли, на яких інстальовано той самий протокол, але інтерфейсні вказівники - різні. Але, з іншого боку, для системи - це один і той самий протокол. Це видно з опису певних функцій СОП. Наприклад функція LocateHandle() або LocateHandleBuffer(), якшо тип пошуку задано ByProtoocol, має просто вернути масив усіх гендлів "на яких інстальовано цей протокол". А протокол задається лише GUID'ом.
Отже ми маємо гендли. Сам тип, визначений специфікацією, - EFI_HANDLE, визначений як VOID * не зобов'язаний бути вказівником на шось. Тобто його значення має "непрозору" (opaque) семантику. Якась прив'язка цього значення до внутрішньої імплементації, вирішується самою імплементацією. І я вирішив використовувати для значення гендла адресу на структуру, яка його представляє. Це дає велику оптимізацію. Бо пошук гендла стає константним по часу - незалежним від кількости гендлів в системі. Ми просто приводимо гендл до типу вказівника на структуру гендла, і якшо треба, читаємо поле валідности - це дає ґарантію, шо ми не поліземо писати в незрозуміло яку ділянку памяти, і далі вже працюємо з гендловою структурою. Не треба нічого шукати взагалі. З іншого боку, адреси - унікальні, тож якшо ми маємо два гендли з різним значенням, вони представляють два різні гендли. І навпаки - два гендли з одним і тим значенням - це той самий гендл. Це оптимальне рішення звичайно має свою ціну - а саме - ми маємо фраґментацію таблиці гендлів. Гендли організовані в таблицю. Це просто масив структур. Висока локальність, без надмірности зберігання вказівників (як в списках чи деревах). Це перший підтип табличних структур нашої бази. І лише гендли так організовані. Ця таблиця може бути фрагментована, оскільки раз виділивши гендл десь в таблиці, ми не можемо пересовувати його деінде доки він валідний. Це таблиця з непересувними елементами. Дефраґментація відбувається тільки на додаванні - ми вставляємо новий гендл не в край, а спочатку дивимося чи є дірки, якшо є, вставляємо в першу від початку, і тільки якшо нема дірок, вставляємо в край.
Другий табличний тип - це нефраґментовані таблиці. Елементи в них можуть бути пересунуті на видаленні (дивись нижче), тим самим просто підтримуючи протяжність таблиці. В такі таблиці організовані протоколи інстальовані на гендл, і таблиці агентів, які споживають конкретний протокол на конкретному гендлі. Спочатку я думав мати там списки, двонаправлені, закільцьовані, але прийшов до висновку, шо краще динамічні масиви. Знову - масиви дають максимальну локальність і займають менше місця. Вони лінійні по пошуку. Але тут питання кількости. Треба розуміти - ми маємо таблицю гендлів, на кожному гендлі інстальовано протоколи, і кожен протокол має скількись агентів, які його споживають. Просто неймовірно, шоб оці дві останні таблиці були довгими. Важко сказати, нажаль, це не легко оцінити, але, з іншого боку, очевидно - це не може бути великим числом. Тобто питання кількох одиниць на таблицю. Уявляєте якою надмірністю тут було б використовувати дерева! І з огляду на масштабованість - так само неймовірно, шо з часом ця кількість збільшиться, просто по суті - тут не може бути великої кількости елементів. Ну і якшо вже буде - тоді переробимо на дерева.
Як робитиметься дефраґментація. На видаленні якогось елемента (аґента чи протокола), функція, поміщатиме на його місце крайній елемент в списку. Спочатку здавалось, шо дефраґментація буде складним завданням і дорогою операцією. Потім прийшла здогадка про оцю релокацію на видаленні. Таблиця увесь час протяжна. І це досягається за константний час. Ми маємо індекс на перше вільне місце в струткурі, яка має цей масив. Наприклад в гендлі, ми маємо поле NumberOfProtocolInterfaces - це і кількість протоколів інстальованих на цьому гендлі, й індекс першої вільної клітини в масиві. Завдяки цьому полю ми маємо доступ до останнього елемнта константно. І таким чином можемо робити константне переміщення для дефраґментації. Ясно, шо якшо елемент, який видаляється останній, переміщення немає. Нажаль з причин уже вказаних вище, ми не можемо так робити з таблицею гендлів. Але там ми маємо константний доступ, а тут лінійний. Просто лінійність тут не буде повільною - ми маємо невелику кількість елементів. Надто невелику, шоб будувати тут дерева.
І третій тип в нашій базі. Це допоміжна структура. Це словник протоколів в системі. Як вже було відзначено, семантика протоколів взагалі робить їх більше приватними для гендла, згадайте хоча б свободу реінсталювати на протоколі інтерфейсний вказівник, - це має приватний обшир для конкретного гендла. З іншого боку, ми маємо кілька сервісних функцій, сенс яких аж вимагає мати системний словник для протоклів - адже попри волю в виборі інтерфейсу, протокол для системи - це його GUID і всі протоколи інстальовані на різних гендлах з одиним і тим же GUID'ом є одним і тим же протоколом. Саме для оптимізації пошуку за протоколом, ми вводимо третю допоміжну структуру - словник протоколів. Такі функції як LocateHandle() і LocateHandleBuffer(), якшо параметр пошуку є ByProtocol, а також LocateProtocol(), вони мають ключем пошуку саме протокол (його GUID). Очевидно якшо ми шукатимемо через таблицю гендлів, а потім через масив протоколів в кожному гендлі, ми матимемо жахливу залежність O(N*m') - де
N - це кількість гендлів системі (в таблиці), m' - це середня кілкість протоколів в таблиці протоколів кожного гендла. Інші ж функції, а саме ProtocolsPerHandle() , або вищезгадані перші дві, якшо параметр пошуку - AllHandles, виграють сильно в своїх операціях від використання таблиці гендлів. Адже без пошуку вони вже мають гендл, а потім просто видають список протоколів інстальвоаних на гендл, або видають всі гендли в системі - це лінійна робота за будь якої структури організації. Саме набір семантики оцих сервісів і диктував вибір організації підлеглої бази даних. Варто коротко нагадати які саме пошукові сервіси задані UEFI:
- Група яка має ключем протокол
- Група яка має ключем гендл
Це такі функції:
-
LocateHandle()
,
LocateHandleBuffer()
за пошукового параметра
ByProtocol
- видають всі гендли які мають цей протокол інстальований
- LocateProtocol() - видає перший гендл на якому цей протокол інстальований
- ProtocolsPerHandle() - видає всі протоколи інстальовані на гендлі
- LocateHandle(), LocateHandleBuffer() за пошукового параметру AllHandles - видають всі гендли в системі
N - число гендлів в системі,
m' - середнє число протоколів на гендлі
n' - середнє число гендлів на протоколі
M - число протоколів в системі
Очевидно, очікується, шо n' - буде вельми малим, - це кількість гендлів, на яких цей протокол інстальований. Багато протоколів будуть інстальовані взагалі на одному гендлі; ну а M у нас уже під логаритмом. Погодьтесь, різниця є.
Отже підсумовуючи. Структура БД вибрана з огляду на семантику сервісів, які в свою чергу вибрані такими, творцями специфікації з огляду на зручність їхнього використання в клієнтах. БД організована як таблиця гендлів, яка є фрагментований (можливо) динамічний масив. Кожен гендл має таблицю протоколів, які на ньому інстальовані - це нефрагментований масив. І кожен протокол має таблицю агентів, які його споживають на цьому гендлі - ця таблиця теж нефрагментована. Дефрагментація досягається переміщенням крайнього елементу на видаленні як згадано вище. Фрагментованість таблиці гендлів іде як ціна від прискорення через використання адреси структури гендла як його значення, для його ідентифікації. Але фрагментованість очікується низька. До того ж система всеодно має можливість дефраґментувати таблицю на вставленнях.
Нарешті, для прискорення пошукових сервісів, які мають як ключ пошуку протокол, вводиться допоміжна структура - словник протоколів в системі, імплементований як двійкове червоно-чорне дерево пошуку, з високими показниками ефективности пошуку.
Пізніше я додам іллюстрацію, а в наступній статті, проаналізую функції додавання/видалення на описаних структурах, які відбуваються на сервісах інсталяції/деінсталяції гендлів/протоколів/агентів.
Така ось крива й коса ілюстрація бази даних гендлів. може колись я навчусь краще робити ілюстрації, а покишо ось:
субота, 30 квітня 2016 р.
UEFI - вступ
Боже, як швидко летить час. Більше року тому я почав тут писати за проєкт. Шо ж, мої невидимі читачі, я хочу вас запевнити, - я не закинув нічого, просто от так швидко летить час. Не виправдовуючись, можна тільки сказати - проєкт складний, він потребує багато навчання, багато роботи, за навчання не дуже роскажеш, ну читаєш ти, ну учиш, і шо, кому це цікаво. І тим паче це не "документування" процесу створення ОС. І все ж, відчуттям, здавалось пройшло ну місяців, ну хай 8, але ж не 14! А я ж думав писати сюди десь принаймні допис в місяць. Але є як є. Час летить, і я постійно нию за це...
Залишивши лірічний відступ, шо я хочу сьогодні розказати. Мабуть варто описати поточний стан речей коротко. Це ж таки щоденник, який відображає історію. Нажаль, я поки шо не розібрався зі структурою цього документування, того хай поки це буде пласка череда дописів. Така стадія, шо нема перед очима структури, я ж не книжку пишу.
Отже, я зупинився в самому кінці попереднього допису на згадці "вивчання старту на армі". Саме вивчення таких речей привело мене до ідеї, яка тепер уже є реальністю в цій роботі. Я не знаю чи мав я тоді її, коли писав попередній допис, але зараз маю.
Коли вивчаєш старт платформи, ти неминуче пересікаєшся з концепціями ROM-коду і фірмварі. Вивчаючи за останню, ти бачиш, наскільки вона схожа з ОС в своїх потребах, цілях, внутрішній будові і інструментах - вона фактично робить те саме (і так само), шо й ОС, тільки в міні-варіанті, - забезпечує підтримку машини, поки ОС не перебере контроль на себе. І далі ти взнаєш, як тобі не подобаєтья у-бут, який так тісно запиляний на лінукс, який такий такий, шо тобі він не подобається загалом. А саме він робить вигляд фірмварі на платах, які ти вибрав як першу хвилю на твій проєкт. До речі за це. З часу минулого допису, я надбав ще дві машини - armv8 rk3368 восьмиядерний OTT коробок з відроїдом - це 64-бітний арм, таки маю його. це я купив в серпні. І, о диво, я таки отримав Mips Creator CI20! І отримав в прямому сенсі, оскільки Imagination так і не продає в Україну, я б не міг його купити і досі. Мені його просто подарували. Просто так, і безплатно доставили (при чому з третього разу - попередні довізники морочили голову і повертали назад). І зробила цей шляхетний крок одна добра людина з Imagination - Alex Voica. Безмежна вдячність йому за цей безпрецендентний своєю бескорисливістю чин і постійні безперервні промені добра в його бік. :) Ця подія заслуговує на окремий допис. Десь в майбутніх "спогадах". Отже тепер я маю такі цільові машини для проєкту:
Beagle Bone Black, TI Sitara AM3358, Cortex-A8, armv7.
Cubieboard 2, Allwinner A20, Cortex-A7, armv7.
Mips Creator CI20, Ingenic JZ4780, XBurst2 (?), mips32r2.
CSA-90 OTT Box, Rockchip RK3368, Cortex-A53, armv8.
iMac g5, ppc64.
і x86 новт.
Історично, я завсіди хотів починати з міпса. Але не міг його знайти. І переключився на арм. Але міпс таки прийшов до мене, і так, "перша хвиля" була визначена. З певним відривом в ній іде сьомий арм, оскільки я вже заглибився в непролазні нетрі його специфікацій від арма. Але й підтримка міпса планується іти паралельно, як же я можу не починати з міпса маючи його зараз на руках, навіть коли я збирався починати з нього не маючи його! Я вже казав, шо через його непагану документованість і однопроцесорність, BBB взято як найпершу машину. А Cubieboard і CI20 ідуть одразу за ним. Це перша хвиля.
Повертаючись до фірмварей. Я вирішив написати свою фірмварю в рамках оцього початкового кроку до ОС. І нехай не здаватиметься, шо головним мотиватором було моє незадоволення у-бутом, насправді я мало знаю за нього, те, шо знаю, мені не подобається, але це все, це просто моя суб*єктивна думка, вона не є визначальною в цьому рішенні. Просто вона вдало поєднується з рештою контексту. Мені всеодно треба вміти "піднімати" машину на рівні фірмварі, розуміти деталі на цьому ж рівні, і треба вчитися в цьому ж середовищі, руслі. Я б міг починати з завантажувача ОС на пристойній фірмварі, але хіба це багато міняє? Навпаки, написавши фірмварю, я не тільки впевнюсь, шо я можу писати ОС, не тільки набуду неоціненного досвіду, але ще й матиму корисне на виході для інших. Я передивлявся за різні фірмварі і не міг не помітити UEFI. А дивився за OpenFirmware, дивився за ARC, і UEFI. І якшо на x86 полі UEFI підтримується зараз нормально, і було б логічно саме починати з UEFI OSLoader'а для твоєї ОС, так само - з завантажувача ОС на OpenFirmware-ному iMac'у, то, на цих міні-PC машинах з моєї першої хвилі, ти не маєш нічого такого пристойного. То чого б не спробувати в рамках твого проєкту принести це туди, з усіх отих вищезагаданих причин-мотивацій? Сама UEFI не запиляна під сервери, як хтось може подумати, і може добре служити на мобільному, вбудованому й інших сегментах, просто її ніхто не приніс туди. От це звичайно мотивує. І скажіть, шо це заважатиме ОС проєкту, авжеж ні! Це грандіозний крок вперед і перевірка себе, а заодно пристойна фірмваря для міні-PC користувачів. З усіма її смаколиками. Які саме вони, мабуть не треба тут писати, ті хто зацікавлені будуть це читати, напевно мають своє сформоване уявлення шодо UEFI. А я розказуючи за мою роботу над нею, розказуватиму і про неї, і по ходу це відкриватиме й її вигоди. Я давно знав, шо моя ОС використовуватиме ACPI, було б дивно не заангажувати сюди й UEFI - ці стандарти створені один для одного. Буквально. Восьмий арм, він наче як збирається мати UEFI підтримку, але ті, хто там крутить те все, здається вирішили, шо воно тільки йтиме на їхній і досі тільки мітичний серверний напрямок. А того всі оці ОТТ коробки і розробницькі ОПК з "малими" 53-ми кортексами, випадають з їхнього, тих чуваків, огляду. Вони думають за UEFI на восьмому армі тільки в контексті серверного майбутнього. Міпс взагалі не має UEFI. А це ще більше мотивує. І так от я й вирішив імплементувати UEFI на кілька машин з сьомим армом, восьмим, і міпсом, покишо тільки 32-бітним, бо 64-бітного на горизонті поки не має. Чого "кілька"? Бо це ж фірмваря. Залежність імплементації від платформи ще більша, ніж в "низів" ОС. Але цей підпроєкт, може жити окремим життям в разі успіху, і якшо хтось доєднається, діапазон підтримки може розширитися. Початково ж, я вирішив писати UEFI на БББ і CI20 (ну важко весь час перемикати розкладку). До речі, шоб не писати весь час модельні імена і їхні малозрозумілі скорочення, та ще й в різних абетках, я, згадуючи тут окремі цільові машини, далі називатиму їх за іменем, яке я їм дав. Бо це зручно. А зацікавлений читач легко засвоє це. Та й машин не густо. xD Отже BBB машину я назвав "муран" - на честь собаки (насправді двох) з села.) Ясно, адже емблема BBB - бігль, собака. Свій Cubieboard 2 назвав аргоном, не знаю чого, я хімік, от чого. Ci20 - йодом, - те саме, шо з аргоном. Плюс ci20 - фіолеотовий (пурпуровий), йод теж фіолетовий, і грецьке слово яке дало ім*я йоду, "йодес", означає "фіолетовий". CSA90 OTT коробок я назвав чортиком - через намальованого на коробоку зеленого андроїда-відроїда, а айМак назвав "блондином". xD Здогадайтесь чого!)
Продвинутий, серйозний читач, збайдужіло і роздратовано читаючи попередній абзац за імена, скаже тут - UEFI не декларує підтримки міпса! Це так, але це не проблема. Наш little endian-ний ci20 (Йод) легко може бути туди втесаний - нема технічних перешкод. Сформульовані для включених архітектур вимоги чітко показують, шо визначити за аналогією таке саме для міпса не є проблемою. Я маю на увазі, шо міпс архітектура, не має ніяких внутрішніх властивостей, які б завадили сформулювати за аналогєю з вже підтриманими архітектурами, вимоги і для неї. Ну а далі, нічого не заважає цій майбутній імплементації запалити інтерес власників стандарту вставити підтримку в специфікацію. :)
Тож, надалі якийсь час тут можна очікувати балакання переважно за UEFI. Оскільки це іще й окремий проєкт, то, можливо, він також і окремо буде висвітлений.
Ідучи далі, щодо роботи з "старту платформи". Всеодно і в UEFI, виділяється такий собі низ - це стартовий код, ініціалізація з боку заліза, це дуже специфічні для платформи речі і асемблер того процесора, який ти програмуєш. Це всюди однаково. І якшо ОС вже прийматиме контроль через встановлений UEFI інтерфейс і зроблену її UEFI-завантажувачем роботу, то це просто переносить найнижньорівневіший код в UEFI, ОС стартуватиме більш стандартно визначено. Всеодно, це тільки про старт, а повна підтримка машини? Отже, - HAL і драйвери від лазіння в специфіку апаратури не втечуть. Бажаючи набути досвіду, мені взагалі не варто тікати від важкої роботи з самого початку, якою ініціаліація апаратури безумовно є. Тож мій вибір логічний і узгоджується з моїм баченням, шо я маю робити, шоб "зробити" те, шо хочу. На платформах з цільових машин, які мають UEFI чи OF, можна буде починати з завантажувача ОС, клієнта відповідної фірмварі. Не варто й казати як досвід імплементування самої UEFI допомагатиме тут. Отже це буде корисно на всіх цільових платформах, а не тільки на цільових для UEFI-імплементації платформах - власне більшість з них стане уефішними платофрмами, а це - добре визначений інтерфейс для ОС на більшости її цілей. Це полегшує розробку самої ОС. І нарешті ще раз - користувачі матимуть добрерозроблений промисловий стандарт замість тієї самодіяльної хаотичної каші. В ідеалі, вони зможуть мастурбувати з своїми лінуксовими компіляціями набагато інтенсивніше з UEFI фірмварним інтерфейсом - грузитимуться і з флешки і з USB жорсткого диску, і з мережі, і з будь якої незліченної GPT партиції! І навіть з сексі диску! xD (SCSI диску, я читаю цю абревіатуру саме так, і це до речі - саме так, як один з розробників того стандарту пропонував її читати офіційно, пуританство решти завадило мабуть, але взятий варіант "сказі" має для україномовного користувача не вельми приємні коннотації. "Сексі" ж рулєз для всіх, чи не так? Отже, наскільки я не знайомий натепер з сексі стандартом, і в руках не тримав, і в очі не бачив, настільки я вже впевнено знаю, шо називатиму його саме так). Ці вищезгадані можливості треба ще імплементувати. Але в UEFI, ти маєш грубезний довідник стандарт, який чітко і детально каже тобі, шо ти маєш зробити і як воно все пов'язано. Плюс я належу до людей, які вірять, шо така продумана (спеціалістами з досвідом) впорядкованість дає добрі плоди, якшо ти стаєш на її ґрунт. А у-бут - це антипод цього. Де там до речі документація на нього? Ні, не рецепт, як мені "сконпілірувать" їхню фігню, а те, як воно влаштоване, які його ідеї, принципи, концепції й інші фільозофічні речі, яких вони дотримуються? Отож.
Чи багато я зараз знаю за UEFI? Ну не мало. Чи багато ще взнавати? О так! Чи багато я написав КОДУ? Мало. Але я вже не просто базікаю і мрію. Я вже роблю. І звичайно, ця писанина призвана в тому числі бути звітом за результати роботи. Отже маючи успіхи, я обов'язково напишу про них тут. А зараз я прийшов розказати про поточний напрямок роботи. Задокументувати еволюцію.
Залишивши лірічний відступ, шо я хочу сьогодні розказати. Мабуть варто описати поточний стан речей коротко. Це ж таки щоденник, який відображає історію. Нажаль, я поки шо не розібрався зі структурою цього документування, того хай поки це буде пласка череда дописів. Така стадія, шо нема перед очима структури, я ж не книжку пишу.
Отже, я зупинився в самому кінці попереднього допису на згадці "вивчання старту на армі". Саме вивчення таких речей привело мене до ідеї, яка тепер уже є реальністю в цій роботі. Я не знаю чи мав я тоді її, коли писав попередній допис, але зараз маю.
Коли вивчаєш старт платформи, ти неминуче пересікаєшся з концепціями ROM-коду і фірмварі. Вивчаючи за останню, ти бачиш, наскільки вона схожа з ОС в своїх потребах, цілях, внутрішній будові і інструментах - вона фактично робить те саме (і так само), шо й ОС, тільки в міні-варіанті, - забезпечує підтримку машини, поки ОС не перебере контроль на себе. І далі ти взнаєш, як тобі не подобаєтья у-бут, який так тісно запиляний на лінукс, який такий такий, шо тобі він не подобається загалом. А саме він робить вигляд фірмварі на платах, які ти вибрав як першу хвилю на твій проєкт. До речі за це. З часу минулого допису, я надбав ще дві машини - armv8 rk3368 восьмиядерний OTT коробок з відроїдом - це 64-бітний арм, таки маю його. це я купив в серпні. І, о диво, я таки отримав Mips Creator CI20! І отримав в прямому сенсі, оскільки Imagination так і не продає в Україну, я б не міг його купити і досі. Мені його просто подарували. Просто так, і безплатно доставили (при чому з третього разу - попередні довізники морочили голову і повертали назад). І зробила цей шляхетний крок одна добра людина з Imagination - Alex Voica. Безмежна вдячність йому за цей безпрецендентний своєю бескорисливістю чин і постійні безперервні промені добра в його бік. :) Ця подія заслуговує на окремий допис. Десь в майбутніх "спогадах". Отже тепер я маю такі цільові машини для проєкту:
Beagle Bone Black, TI Sitara AM3358, Cortex-A8, armv7.
Cubieboard 2, Allwinner A20, Cortex-A7, armv7.
Mips Creator CI20, Ingenic JZ4780, XBurst2 (?), mips32r2.
CSA-90 OTT Box, Rockchip RK3368, Cortex-A53, armv8.
iMac g5, ppc64.
і x86 новт.
Історично, я завсіди хотів починати з міпса. Але не міг його знайти. І переключився на арм. Але міпс таки прийшов до мене, і так, "перша хвиля" була визначена. З певним відривом в ній іде сьомий арм, оскільки я вже заглибився в непролазні нетрі його специфікацій від арма. Але й підтримка міпса планується іти паралельно, як же я можу не починати з міпса маючи його зараз на руках, навіть коли я збирався починати з нього не маючи його! Я вже казав, шо через його непагану документованість і однопроцесорність, BBB взято як найпершу машину. А Cubieboard і CI20 ідуть одразу за ним. Це перша хвиля.
Повертаючись до фірмварей. Я вирішив написати свою фірмварю в рамках оцього початкового кроку до ОС. І нехай не здаватиметься, шо головним мотиватором було моє незадоволення у-бутом, насправді я мало знаю за нього, те, шо знаю, мені не подобається, але це все, це просто моя суб*єктивна думка, вона не є визначальною в цьому рішенні. Просто вона вдало поєднується з рештою контексту. Мені всеодно треба вміти "піднімати" машину на рівні фірмварі, розуміти деталі на цьому ж рівні, і треба вчитися в цьому ж середовищі, руслі. Я б міг починати з завантажувача ОС на пристойній фірмварі, але хіба це багато міняє? Навпаки, написавши фірмварю, я не тільки впевнюсь, шо я можу писати ОС, не тільки набуду неоціненного досвіду, але ще й матиму корисне на виході для інших. Я передивлявся за різні фірмварі і не міг не помітити UEFI. А дивився за OpenFirmware, дивився за ARC, і UEFI. І якшо на x86 полі UEFI підтримується зараз нормально, і було б логічно саме починати з UEFI OSLoader'а для твоєї ОС, так само - з завантажувача ОС на OpenFirmware-ному iMac'у, то, на цих міні-PC машинах з моєї першої хвилі, ти не маєш нічого такого пристойного. То чого б не спробувати в рамках твого проєкту принести це туди, з усіх отих вищезагаданих причин-мотивацій? Сама UEFI не запиляна під сервери, як хтось може подумати, і може добре служити на мобільному, вбудованому й інших сегментах, просто її ніхто не приніс туди. От це звичайно мотивує. І скажіть, шо це заважатиме ОС проєкту, авжеж ні! Це грандіозний крок вперед і перевірка себе, а заодно пристойна фірмваря для міні-PC користувачів. З усіма її смаколиками. Які саме вони, мабуть не треба тут писати, ті хто зацікавлені будуть це читати, напевно мають своє сформоване уявлення шодо UEFI. А я розказуючи за мою роботу над нею, розказуватиму і про неї, і по ходу це відкриватиме й її вигоди. Я давно знав, шо моя ОС використовуватиме ACPI, було б дивно не заангажувати сюди й UEFI - ці стандарти створені один для одного. Буквально. Восьмий арм, він наче як збирається мати UEFI підтримку, але ті, хто там крутить те все, здається вирішили, шо воно тільки йтиме на їхній і досі тільки мітичний серверний напрямок. А того всі оці ОТТ коробки і розробницькі ОПК з "малими" 53-ми кортексами, випадають з їхнього, тих чуваків, огляду. Вони думають за UEFI на восьмому армі тільки в контексті серверного майбутнього. Міпс взагалі не має UEFI. А це ще більше мотивує. І так от я й вирішив імплементувати UEFI на кілька машин з сьомим армом, восьмим, і міпсом, покишо тільки 32-бітним, бо 64-бітного на горизонті поки не має. Чого "кілька"? Бо це ж фірмваря. Залежність імплементації від платформи ще більша, ніж в "низів" ОС. Але цей підпроєкт, може жити окремим життям в разі успіху, і якшо хтось доєднається, діапазон підтримки може розширитися. Початково ж, я вирішив писати UEFI на БББ і CI20 (ну важко весь час перемикати розкладку). До речі, шоб не писати весь час модельні імена і їхні малозрозумілі скорочення, та ще й в різних абетках, я, згадуючи тут окремі цільові машини, далі називатиму їх за іменем, яке я їм дав. Бо це зручно. А зацікавлений читач легко засвоє це. Та й машин не густо. xD Отже BBB машину я назвав "муран" - на честь собаки (насправді двох) з села.) Ясно, адже емблема BBB - бігль, собака. Свій Cubieboard 2 назвав аргоном, не знаю чого, я хімік, от чого. Ci20 - йодом, - те саме, шо з аргоном. Плюс ci20 - фіолеотовий (пурпуровий), йод теж фіолетовий, і грецьке слово яке дало ім*я йоду, "йодес", означає "фіолетовий". CSA90 OTT коробок я назвав чортиком - через намальованого на коробоку зеленого андроїда-відроїда, а айМак назвав "блондином". xD Здогадайтесь чого!)
Продвинутий, серйозний читач, збайдужіло і роздратовано читаючи попередній абзац за імена, скаже тут - UEFI не декларує підтримки міпса! Це так, але це не проблема. Наш little endian-ний ci20 (Йод) легко може бути туди втесаний - нема технічних перешкод. Сформульовані для включених архітектур вимоги чітко показують, шо визначити за аналогією таке саме для міпса не є проблемою. Я маю на увазі, шо міпс архітектура, не має ніяких внутрішніх властивостей, які б завадили сформулювати за аналогєю з вже підтриманими архітектурами, вимоги і для неї. Ну а далі, нічого не заважає цій майбутній імплементації запалити інтерес власників стандарту вставити підтримку в специфікацію. :)
Тож, надалі якийсь час тут можна очікувати балакання переважно за UEFI. Оскільки це іще й окремий проєкт, то, можливо, він також і окремо буде висвітлений.
Ідучи далі, щодо роботи з "старту платформи". Всеодно і в UEFI, виділяється такий собі низ - це стартовий код, ініціалізація з боку заліза, це дуже специфічні для платформи речі і асемблер того процесора, який ти програмуєш. Це всюди однаково. І якшо ОС вже прийматиме контроль через встановлений UEFI інтерфейс і зроблену її UEFI-завантажувачем роботу, то це просто переносить найнижньорівневіший код в UEFI, ОС стартуватиме більш стандартно визначено. Всеодно, це тільки про старт, а повна підтримка машини? Отже, - HAL і драйвери від лазіння в специфіку апаратури не втечуть. Бажаючи набути досвіду, мені взагалі не варто тікати від важкої роботи з самого початку, якою ініціаліація апаратури безумовно є. Тож мій вибір логічний і узгоджується з моїм баченням, шо я маю робити, шоб "зробити" те, шо хочу. На платформах з цільових машин, які мають UEFI чи OF, можна буде починати з завантажувача ОС, клієнта відповідної фірмварі. Не варто й казати як досвід імплементування самої UEFI допомагатиме тут. Отже це буде корисно на всіх цільових платформах, а не тільки на цільових для UEFI-імплементації платформах - власне більшість з них стане уефішними платофрмами, а це - добре визначений інтерфейс для ОС на більшости її цілей. Це полегшує розробку самої ОС. І нарешті ще раз - користувачі матимуть добрерозроблений промисловий стандарт замість тієї самодіяльної хаотичної каші. В ідеалі, вони зможуть мастурбувати з своїми лінуксовими компіляціями набагато інтенсивніше з UEFI фірмварним інтерфейсом - грузитимуться і з флешки і з USB жорсткого диску, і з мережі, і з будь якої незліченної GPT партиції! І навіть з сексі диску! xD (SCSI диску, я читаю цю абревіатуру саме так, і це до речі - саме так, як один з розробників того стандарту пропонував її читати офіційно, пуританство решти завадило мабуть, але взятий варіант "сказі" має для україномовного користувача не вельми приємні коннотації. "Сексі" ж рулєз для всіх, чи не так? Отже, наскільки я не знайомий натепер з сексі стандартом, і в руках не тримав, і в очі не бачив, настільки я вже впевнено знаю, шо називатиму його саме так). Ці вищезгадані можливості треба ще імплементувати. Але в UEFI, ти маєш грубезний довідник стандарт, який чітко і детально каже тобі, шо ти маєш зробити і як воно все пов'язано. Плюс я належу до людей, які вірять, шо така продумана (спеціалістами з досвідом) впорядкованість дає добрі плоди, якшо ти стаєш на її ґрунт. А у-бут - це антипод цього. Де там до речі документація на нього? Ні, не рецепт, як мені "сконпілірувать" їхню фігню, а те, як воно влаштоване, які його ідеї, принципи, концепції й інші фільозофічні речі, яких вони дотримуються? Отож.
Чи багато я зараз знаю за UEFI? Ну не мало. Чи багато ще взнавати? О так! Чи багато я написав КОДУ? Мало. Але я вже не просто базікаю і мрію. Я вже роблю. І звичайно, ця писанина призвана в тому числі бути звітом за результати роботи. Отже маючи успіхи, я обов'язково напишу про них тут. А зараз я прийшов розказати про поточний напрямок роботи. Задокументувати еволюцію.
Підписатися на:
Дописи (Atom)