середа, 22 березня 2017 р.

Нарешті стартовий код

Колись я тут згадував за "стартовий код". Коли, рік тому? Не пам'ятаю точно. І лише зараз можу хоч шось сказати про зроблене в цьому плані.

Іронія, перший стартовий код, код який насправді можна закинути на реальну машину і запускати і дивитись, шо він робить, він у мене вийшов таки на MIPS архітектурі. :) Так склалось, знайшов схожий проєкт (клікни мене), він допоміг зрозуміти як починати програмувати апаратуру, бо знаєте, та купа зваленої інформації під назвою TRM від TI, не дуже цьому допомагала, того хоч Beagle Bone Black і стояв першим в черзі, Mips Creator CI20 вийшов першим.

По-перше, ми відкинули імплементацію Pei фази. Бо тут на йоді, дуже мало TCSM, а по-друге - з часом стало ясно, шо Pei і Dxe дуже сильно дублюють одне одного, а все ж мають трохи різні внутрішні організації. Вийшла б зайва робота, більше помилок. Обов'язок ініціалізувати SDRAM перейшов на SEC. А всю решту робитиме Dxe. Зміна насправді не дуже істотна.

Почитавши і подивившись на схожий проєкт, ми вирішили взятися за те, шо завсіди називали "стартовий код". Шоб збудити проєкт, який до цього писався лише в стіл так би мовити - бо код же Boot Service'ів, наприклад, сам по собі не поганяєш. Ця частина - стартовий код - дуже особлива, вона і має емоційний ефект від запускання на реальній машині, і, також, - дає великий поштовх, впевненість в собі, бо якшо це вдалося, думаєш, значить, ти можеш написати і ФВ і ОС. :)

От ми й узялися за нього. І так вийшло, шо попри більшу ознайомленість на той момент з ARM'ом, ніж з MIPS'ом, саме останній став першою архітектурою, де це сталося. Тепер я можу сказати, шо знаю обидві архітектури однаково. Однаково пагано, :D але однаково.

Перший запуск ясно шо не працював. Виявилось не працював він через неправильну лінкувальну адресу образу, і переплутане описання багатьох речей в мануалі, в убутових вихідних кодах... Далі наш код нарешті показав ознаки керування машиною - першим, шо він зробив було перемикання наплатного LED з червоного на синій колір. Далі ми надрукували в послідовний порт перше слово - воно вийшло "UefiUefi". Скриншот цього я виклав в твітер, і цей допис навіть ретвітнули люди з Imagination. Отако. Потім я нарешті виявив помилку з адресою, і зміг викликати власні функції - це дало можливість друкувати більше, ми привітали світ і Гаврилівну! і надрукували трохи потрібних регістрів. Потім взялися за PLL. І тут були помилки, прикрі, але зважаючи на те яке це складне діло ті PLL, можна сказати, з цим вийшло все не так важко. І ми тепер ініціалізували дві з чотирьох PLL - APLL (для процесора і його кешу 2-го рівня) і MPLL (для пам'яти і AHB шин (а значить і для периферії)). Ми виставили вихідну частоту APLL на 1200 МГц, це якраз максимальна рекомендована частота процесора на йоді, MPLL виставили на 400 МГц, знову ж з прицілом на 400 МГц для DDR3 і AHB0 шини. Але ділителі для цих кореневих компонентів ми зробили такими, шо принаймні поки вони бігають на значно менших частотах. Далі буде видно, але це ж фірмваря. Конкретніше частоти такі:
КомпонентДжерело тактівДілительЧастота, МГц
APLLEXTCLK(50/2/1)1200
MPLLEXTCLK(50/6/1)400
CPUAPLL (SCLK_A)6200
L2APLL (SCLK_A)12100
AHB0MPLL4100
AHB2MPLL4100
PCLKMPLL850
От. А тепер ми хочемо налаштувати таймери - Watchdog і OS таймери, перший, шоб уникнути безкінечного зависання, ми думаємо виставити його ресетити плату після 3 хвилин  нічогонеробіння - так наприклад ром-код мурана робить. Ясно, шо нам в нормальному плині, треба незадовго до краю цього терміну, скидати лічильник, ну сторожовий собака. Другий, це серце системного коду, фірмварі, ОС. Тут він нам потрібен практично для ефективного чекання на довгих інтервалах, де busy wait виглядає неприйнятним, а такі інтервали будуть в ініціалізації SDRAM, і нам там потрібно чекати коло 400 мс. От для цього. Але шоб це справді було ефективне нічогонеробіння, одних таймерів мало. Треба ще налаштувати контроллер переривань, і, звичайно, надати відповідні обробники. Це не буде "занадто" для цієї фази. Навіть специфікація UEFI зобов'язує підтримувати обробку переривань від таймера, і використовувати це. Не кажучи вже за ОС. А оскільки все шо треба робити у нас уже перед очима, нема ніяких причин відкладати це на потім. Тим паче, як ми вже казали, нам це потрібно вже тут і зараз.

От на цьому ми поки зупинилися - на таймерах і контроллері переривань. Багато ще треба
робити, шоб мати шось юзабельне, але все ж уже неможна сказати, шо ми лише теоретизуємо,
стартовий код уже є!
А це один з скриншотів його роботи. Тут ми вже налаштували PLL. Та це вчора було. :)

пʼятниця, 19 серпня 2016 р.

Структура сховища і потік виконання перших фаз. перші спроби й обриси.

В продовження попереднього допису, опишемо трохи фізичну конструкцію нашого стартового коду, як він планується бути оформлений і яка послідовність виконання. Це все дуже поверхнево і неостаточно і це саме для BBB.
Вивчаючи інтерфейс ром коду на Sitar'і, як і слід було очікувати, виникли суперечності між документацією і реальною поведінкою. А також вияснилося, шо принаймні в частині опису ініціалізації, цей 5000-но сторінковий TRM - це каша каша каша. Шо ж, довелося таки питати в мейлинг-листах, я наче вже писав за це, шо прийдеться, ну а там мене перенаправили на питайте-можливо-відповімо сайт самого TI. Ну от чекаю тепер. Просто як нотатка, прикол такий - ситарин ром-код каже, шо на вбудованій пам'яті, тобто eMMC в нашому випадку, а також може бути NAND, ну і як зовсім екзотичний і мною ніколи не бачений варіант - eSD, він не робитиме MBR/FAT завантаження, а робитиме сирий режим - просто читатиме перший сектор, оцінюватиме чи є там образ і вантажитиме той образ прямо, і прямо на нього передаватиме керування. На картках же, він ще може робити MBR/FAT. Виявилось, шо на мурановому eMMC, нема нічого крім нулів в першій половині MBR (а саме - в BootCode блоку), де тільки й міг би лежати той голий образ для сирого режиму. Зато є файл MLO в корені FAT16 партиції, шо натякає, шо ром код на мурані таки робе файловосистемне завантаження. Точно навпаки як написано в документації. Круто!
Гаразд. Думаннями, ми надумали нарешті, як виглядатиме наш пристрій сховища структурно. Розкрій так би мовити. Той, шо англійською - layout. Розкрій мені подобається. Це первинно так. Далі уточнюватимемо (згадки за певні уточнення сюди додані, оскільки цей блог дає можливість редаґувати статтю).
Отже спочатку словесно, а потім мабуть ще картинку потворну намалюємо для іллюстрації. Розмір сектора 512 байтів тут.
Захисна MBR (Protective MBR)
Розміром в один сектор. Ця структура служить для зворотньої сумісности і для захисту GPT структур і партицій від нетямущих старих утиліт. Вона описує увесь наявний простір за собою (включно з рештою GPT метаданих) як одну неактивну партицію. Оті перші 440 байтів MBR, BootCode блок не використовуються UEFI, але нашою Sec фазою таки будуть використовуватися (не будуть, дивись уточнення нижче), по-перше цьому нема альтернативи, а по-друге, тут, на армівських і міпсівських ОПК, це не створить ніяких проблем (сумісности), того а чого власне не використаовувати цей простір? Ром код не розуміє GPT партиціонування, того ми можемо лише використовувати сирий режим. А в ньому, ром код диктує, де і як має лежати навантаження. І це майже завсіди - найперший сектор (а також якісь наперед визначені дуплікативні місця, як наприклад сектор #256, #512, #768 у випадку з ситариним ром кодом, але це майже ніким і ніколи не використовується (додане пізніше: так, майже "ніким", яка іронія, саме ці місця і доведеться використовувати див. нижче)). Тобто очевидно, ром код очікує GP заголовок (а це два 32-бітні поля для розміру і адреси куди вантажити), і сам образ в секторі #0 і далі (важливе уточнення: в секторі #0 він очікує лише маркер сирого режиму - CH, а сам образ - очікує в наступному секторі). Ми умисно оминаємо проблему з CH, бо як я писав вище - треба почекати на відповідь, але суть шанси, шо він не потрібен, і от з таким оптимістичним варіантом ми і йдемо - у нас GP заголовок і сам образ1. Така схема не конфліктує з PMBR і GPTH - вони маленькі і можуть бути якби включені в Sec шматок. Вони будуть завантажені, вони нам всеодно потрібні, все нормально. Навіть на міпсі криейторі (йод), такі об'єми влізуть (там 14КБ макс. розмір навантаження). Отже наша Sec фаза (в нашій термінології це Sec1) фізично розбивається на дві частини - Sec1.0 та, шо лежить в BootCode блоку, і потім перестрибує через решту PMBR і GPTH на Sec1.1 - решту, шо не влізло в BootCode блок. Цю другу частину я вирішив поки шо зробити 2 сектори в розмірі - 1024 байти. Тобто первинно ми вибрали Sec1 фазу в розмірі максимум 1456 байтів:
(1024 (Sec1.1) + 440 (BootCode) - 8 (GPH)). Це 364 армівські (і міпсівські теж) інструкції. Не мало, якшо не роздувати чортівні.
GPT Заголовок (GPTH)
Ну заголовок як заголовок. Все як написано в специфікації на GPT. Не переказуватиму. Він має фіксовану позицію - сектор #1, і розмір в секторах - один сектор, тобто 512 байтів в нашому випадку.
Sec1.1+BFV
Далі іде вже згадана нами друга частина Sec фази, визначена в два сектори розміром. І - BFV - Boot Firmware Volume. Фірмварний завантажувальний том. Той, де лежить Pei з Peim'ами. Ми визначили його розмір в 16КБ. Керувались тим, шо рівнятися треба на той випадок, який обмежує в розмірах. Шоб ця фаза була однакового набору функціональности на всіх наших машинах. А обмежує в розмірах нас міпс криейтор. В якого максимум навантаження - 14КБ. У мурана 109КБ. А в аргона точно не знаю, але виглядає, шо десь посередині - шось типу як 48КБ орієнтир. Також з метою ефективности. Нам не треба розганяти апетити. В цих 16КБ буде лежати код, який зрештою ініціалізує центральну логіку і пам'ять. І запускає завантажувач решти ФВ. Як я вже казав, ми маємо два ФТ - BFV і CFV. Не пам'ятаю чи казав я за облік їх в GPT, наче казав. Так от. Я якось спочатку узівав, шо можна ж якісь обліковувати, а якісь ні. Спершу я думав, за принципом - або обліковувати всі або ніякі. Але потім стало ясно, шо нам лише BFV треба покласти найближче до початку, тобто перед масивом партицій, який має йти перед будь якою облікованою ним партицією і має бути шонайменше 16КБ в розмірі. Ясно, шо ми не можемо його вантажити в SRAM - не влізе. Значить BFV не попадає в GPT облікування, але CFV нічого не заважає туди попасти. Отже так і буде - CFV йтиме як перша партиція в GPT. А вертаючись до нашої сув'язи SEC+BFV, ми маємо 17КБ в розмірі.
Масив партицій GPT (GPTPA)
Знову як і заголовком - все як у стандарті. Ми вибираємо йому 16КБ того мінімуму, який вимагається. Бо цей мінімум і так величезний для сховищ нашого діапазону. Справді, маючи, шо кожен запис в цій таблиці бере 128 байтів (може більше, але ми не робитимемо цього, це просто не треба), ми маємо потенційно 128 партицій! Першою карткою я обрав 16GB-ну, - це більш, ніж досить для неї. Насправді 128 партицій - це більш, ніж досить для будь якого нині сущого пристрою сховища.
А які ж партиції я вирішив створити?
CFV
Я писав уже, шо це таке. Це власне вся фірмваря крім, BFV і системної партиції з ОС лодарями і картинками. Тут таке - чим далі в ліс, тим гладкіші партизани. менше визначености тобто. Який розмір? Я вибрав 128КБ. Думаю цього має вистачити. Ми хочемо робити ефективно. Без отого роздування. Для коду всієї фірмварі, цього має вистачити.
Системна партиція FAT тип
Далі іде архітектурна штука - системна партиція. Куди кластимуться певні речі від фірмварі і головно, - куди сторонні розробники драйверів і лодарів кластимуть свої поробки. Не знаю який саме FAT вибрати. Але напевно або FAT16 або FAT32. Розмір. А хто його зна? Якшо тут кластимуть якісь бітові карти для графічного інтерфейса і логотипів, то це вже мегабайти. Тож це ще треба уточнювати.
А далі ідуть просто користувацькі партиції. В рамках тренування, і для цікавости, я планую створити кілька FAT партицій, шоб же подивитися, як Віндовс (та й решта) сприйме SD картку з наприклад п'ятьма партиціями і GPT схемою! Це до біса цікаво. Але треба вже зараз вивчити FAT будову. Воно того варте.
Звичайно, в кінці диску, мають іти дзеркальні копії GPTH GPTPA - але знову, - це стандратно. Треба ще навчитися запитувати точний розмір пристрою, шоб не губити жодного сектора, а поки я не знаю як це робити, ми кладемо логічну 2-х гігабайтну картку в фізичну 16-ти гігабайтну. Взнаємо як читати розмір картки, розширимо схему, і пересунемо дзеркальні структури в справжній край. (додано пізніше: я вже знаю як взнавати розмір пристрою, дякуючи простій і ясній кодовій базі відомої утиліти Win32DiskImager, і всемогутньому MSDN'у. Ключова функція в цій справі, - DeviceIoControl(), ну й іо контроли звичайно).
Далі ще треба покласти ілюстрацію вищеописаного (ілюстрація нижче).
А в кінці, - невеликий підсумок кодової послідовности на цьому етапі. Отже ром код вантажить нашу sec1 фазу. Вона в SRAM'і має відображений BFV (mmbfv), вона налаштовує потрібні структури, встановлює стек. Шось може налаштовує в процесорі. Наприклад, ситарин ромкод лишає вимкненими армівські предиктор галуження, кеші даних і коду, першого рівня, шо там уже казати за другий, вимкнена ясно й MMU. Але я не знаю, де я це налаштовуватиму - тут, в sec1, чи вже в Pcr.sys. А от ще там годинники (в смислі не таймери, чи ГРЧ а саме тактові сигнали, clock, всі оті APPLS і APLLLJ) можливо треба тут буде налаiтовувати. Watchdog таймер також. Ну і обробники винятків архітектурні армівські. Навіть якшо спочатку можна покластися на ром код, так же як і з тактовою логікою, це лише на перших кроках - адже багато чого не налаштовоано ром кодом як з PLL - не всі вони налаштовані і не всі так як треба саме на цьому етапі, а лише ті, які йому потрібні. По мінімуму. І з векторами - вони трівіальні, в повнофункціональній системі, фірмваря має перебирати на себе цю роль і налаштовувати ці речі так, як треба їй і подальшим фазам. Тож є шо робити. А ще скільки всього не відомо.
Потім, sec1, з mmbfv, завантажує Pei.exe (завантажує як виконуваний образ), якому як ми казали, не треба робити релокацію (в нормі, єдиний варіант відхилення від якої, який я зараз можу вигадати, - це коли сторінка за адресою бази Pei.exe може виявитися "паганою" апаратно, для цього ми лишаємо релокаційну інформацію в образі Pei.exe і маємо вміти в sec1 робити релокацію. Але це не важко в PE.). І передає на нього керування і аргументи визначені в специфікації. Адреси в SRAM'і, де Pei може розгорнутися. Адреси стека тощо. Далі Pei робитиме так само свою роботу, серед якої завантажить з mmbfv Pcr.sys, який ініціалізує процесор, чипсет і па'мять (DRAM). Це останнє - значна подія. Мало я ще уявляю, шо там за робота, але уявляю, шо її багато. Нарешті в кінці запуститься Dxe Ipl, чи то як окремий драйвер, чи як частина Pei.exe. І він робитиме, шо там специфікація йому велить. А також завантажуватиме з сховища CFV, тобто робитиме shadowing тому, роблячи його відображеним в пам'ять - породжуватиме mmcfv. Шоб усі подальші фази, звертались саме сюди, коли їм треба прочитати з фірмварного сховища. Це ж вам і кешування. Лише оновлення фірмварі писатиме в том на пристрій. А також збереження всяких змінних і конфігурацій в нелетке сховище. Після роботи цього модуля (Dxe Ipl), ми маємо мати розгорнуту повну Uefi Core, готову до виконання. Це вже інші етапи, які ще не вивчені і не пророблені зовсім. З точки зору їхньої ініціалізації і детального розуміння їхньої роботи, бо я маю навіть певні сервіси написані. :) Але це одне й друге - ще дуууже далеко одне від одного. От треба наближати їх.
Додано 24.08.2016
Круто, написав вчора сюди трохи уточнень, оскільки вже не було часу перевіряти описки, повернув допис в чернетки, приходю сьогодні доробити, додати малюнок, а редаґування як вітром здуло. Круто! Зато оновлює автоматично текст разів десять за хвилину. Обожнюю таке.
Гаразд, напишемо знову.
Я писав вище, шо спитав на форумі TI і чекаю на уточнення. Ну от мені відповіли і відповіли змістовно. І, тепер, треба вносити поправки в структуру. Але я вирішив не змінювати вже написане, а дописати, - у нас не посібник, а щоденник. Це відображає хід роботи.
Отже, на eMMC БББ, ром код таки робить MBR/FAT читання. Просто TRM каже неправду, шо він не робить такого на вбудованій пам'яті. Далі, ром код таки вимагає CH в першому секторі (на чотирьох позиціях, дивись нижче) якшо ти хочеш сирий режим. Ті структури, які він вимагає, влазять в BootCode блок, вони десь 288 байтів (так, 288 бісових байтів, шоб просигналити, шо тут є валідний образ). Але хоч той CH і влазе в BootCode блок і ще трохи місця лишає, нам це не допоможе поєднати це все з GPT, бо ром код очікує образ на початку наступного сектору, бо він, бач, хоче, шоб образ був вирівняним на сектор (нашось). Шо найцікавіше, про це ні слова в TRM'і. Дійсно, така дрібниця. Того, BootCode блок ми не можемо використовувати. Більше того, в наступному секторі у нас іде фіксована структура - GPT заголовок, і ми ніяк не можемо сумістити це з кладінням сюди образу (ромкод хоче там GP заголовок і передає керування на перше слово за заголовокм). Халепа. Але вихід є. І він полягає в тому, і це продовження нових знань отриманих з відповідей, шо ромкод для сирого режиму впевнено робить всі чотири спроби читання в секторах 0, 256, 512, 768, і лише тоді, коли не знаходить в жодному вказівки на гожий образ, іде до MBR/FAT завантаження. Це важливо, бо я не мав такої впевнености (наприклад я думав, шо хто зна, а може він не знайшовши валідного сирого образу в першому місці, але побачивши там MBR сигнатуру, стрибне в MBR/FAT режим. Дійсно, а шо його думати, коли все так паскудно задокументовано, і ще й явно поведінка розходиться з тим, шо написано). Тобто, ми можемо покласти все майже як збирались спочатку, але в сектор #256, пропустивши 256-34=222 сектори (34 сектори - це PMBR, GPTH, GPTPA). Це марнує 111КБ місця, проте дає можливість не розбивати sec1 на два шматки і також - обліковувати BFV як партицію (без необхідности завантажувати GPTPA в SRAM). Але головне - це не наша вина, це вимога ром коду. Суть і інші варіанти, але вони гірші. Наприклад ми можемо покласти в це пусте місце якусь іншу партицію, а далі BFV. Але це і небезпечно і незручно. Дані, які вантажаться на таких ранніх стадіях і які містять фірмварю, - це особливі дані, і спокійніше, коли вони лежать десь на переді, або взагалі на своєму пристрої. Мати якусь фіксовану точку, десь всередині сховища для фірмварі, між довільними даними - це не те. Або є так звана гібридна MBR - найсправжнісінькі костилі. Вона поєднує в собі захисну MBR і також дублює облік кількох GPT партицій (згадаймо, PMBR з чотирьох записів в таблиці записів, використовує лише один - для захисної партиції - якою покривається увесь простір за PMBR, решта записів мають мати нулі, але гібридна схема має там записи, шо дублюють інформацію про якісь вже обліковані GPT партиції. В цьому і є весь трюк - потім код, який не розуміє GPT (як от наш ром код), може використовувати інформацію за партиції з цих записів, для якого вони суть звичайні MBR записи). Але це взагалі ахтунг. Це добре коли такий код лише читає. Як ром код. Можна лише уявити, чим це все може закінчитись, якшо там влізе хтось модифікувати ті партиції. Ані одна ані інша сторона не поважатимуть один одного. Дві хазяйки на одній кухні детектед. І це звичайно суперечить GPT специфікації, це вже не GPT. Отже, просто зсовуємось на 256 секторів вперед. А облишене місце ми можемо використовувати для чогось ще - таке навіть GPT специфікація дозволяє. На відміну від гібридної MBR. Шо саме ми там можемо зберігати? Поки не знаю. Але от варіанти - логотипові картинки, користувацького інтерфейсу картинки. Або сховище для змінних і іншої конфігурації, яка вимагає нелеткого сховища.
Ці новини змінюють наш розкрій дещо. Як вже сказано, ми не розбиваємо sec1, і тепер вона суцільна, один бінарник, який лежатиме на LBA #257 одразу за сектором, який показує ром коду, шо тут лежить потрібний образ. Все як він хоче. Який розмір sec1 фази? Це ще не відомо, але тоді в нас було коло 1.5 КБ, тож можемо вибрати 4 сектори, закруглючи так би мовити. Це може змінитися. Але як перший варіант хай буде 4 сектори. Далі BFV в 16КБ (32 сектори), далі CFV 128КБ (256 секторів), далі системна партиція (SYSP) неясно ще в скільки КБ. Попри те, шо тут BFV і CFV лежать бік о бік і не рознесені як в першому варіанті, ми не поєднуємо їх в один том. Бо для цього суть купа причин. Головна - BFV відображається і його вміст працює в SRAM, CFV - в DRAM. Різним кодом і в різні фази, з різними можливостями. Набагато більше вигід в таких реаліях мати два томи, ніж об'єднувати їх. А додаткові дані на облік, які це розбиття забере - всього пара сотень байтів. Ну і це лише на БББ так. Наприклад на аргоні (кб2), там свої будуть причуди у ромкода (бром він у них називається), я ще мало знаю за нього, але там можливо підійде та первинна схема, з розбитою надвоє sec1 фазою, BFV поза обліком і CFV як партиція. Головне - таке розділення обумовлене архітектурою сучасних машин, коли спочатку в доступі є лише маленький SRAM, а потім значно більший DRAM. Код в BFV працює в першому, код CFV - вся фірмваря, працює в другому. Це насправді оптимізація мати це розбиття. От наприклад, уявіть, Pei ядру треба завантажити Pcr.sys. Шоб ініціалізувати ту ж динамічну пам'ять. Так вона шукатиме його в маленькому BFV томі, там всього кілька файлів, а з одним томом їй би прийшлось перевертати значно більший том з значно більшою кількістю файлів. Враховуючи просту пласку структуру фірмварних томів, без папок, з файлами які ідуть один за одним - це важливий фактор. Мати маленький том в обмеженій фазі, це вигідно. І по часі і по простору.
На хід виконання ранніх фаз ці зміни майже не вплинуть. sec1 фаза тепер нікуди не перестрибуватиме. Також не буде в SRAM'і GPTH. І нам треба його самим завантажувати, коли він знадобиться. Це ясно шо не страшно. Власне так навіть трохи чіткіше виходить. Шкода лише, шо місце марнується.
А це іллюстрація з врахуванням змін.
Додано 24.10.2016: На малюнку все правильно, крім кількох моментів. Створюючи першу картку з цим розкроєм, ми зсунули SYSP на LBA #512, таким чином CFV став трохи менше за 128 КБ (на розмір CH+SEC+BFV, тобто на 37 секторів, 18.5 КБ). Розмір SYSP теж вибрано було так, шоб юзерська партиція лежала на рівній адресі (LBA #40000h), SYSP відтак іде трохи менше за 128МБ.

четвер, 4 серпня 2016 р.

Не дуже категоризоване - про сосну і PI.

Цей допис не про 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].
Звичайно, це лише обриси, які треба уточнювати, деталізувати, і наповювати змістом.

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

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

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

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

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

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

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

вівторок, 17 травня 2016 р.

Завантажувальні сервіси - Сервіси обслуговування протоколів

В попередьному дописі було зроблено огляд БД гендлів і описано пошукові сервіси, які працюють на цій БД. Тепер опишемо сервіси, які вставляють і видаляють елементи.
Коротко нагадаємо структуру БД. Вона складається з таблиць і дерева.
  • Таблиця (структур) гендлів, одна на всю систему, вона може бути фраґментована і дефраґментація може лише відбуватися на вставленні нових елементів. В разі переповнення можливе лише пряме розширення. В разі неуспіху його - крах системи.
  • Таблиця (структур) протоколів. По одній на кожен валідний гендл. Містить масив протоколів, інстальованих на цьому гендлі. Ця таблиця нефраґментована - завсіди на видаленні протокола з гендла, крайній елемент таблиці переноситься в новоутворену від видалення дірку, і масив завсіди лишається суцільним. Може бути розширена прямо, або через перевиділення нового більшого буферу і копіювання старої таблиці туди.
  • Таблиця агентів. По одній на кожен протокол (в масиві протоколів на гендлі). Так само - нефраґментована таблиця, розширювана прямо і через копіювання в крайньому разі.
  • Дерево протоколів. Одне на всю систему. Червоно-чорне двійкове дерево пошуку. Грає роль словника - містить усі унікальні протоколи, які були інстальовані на систему під час однієї сесії. Це публічна інофрмація за протокол, глобальна для всієї системи - словник. Приватна - це масиви протоколів на гендлах. Ми не видаляємо вузол взагалі, він може бути інстальований знову, і це добра оптимізація. До того ж, нам не треба робити видалення на червоно-чорних деревах, а це дуже марудна справа.
  • Масив вказівників на структури гендлів (з таблиці гендлів). По одному на кожен вузол словника протоколів. Нефраґментована, розширювна прямо і через копіювання таблиця.
Ці два останні елементи - словник з дерева і масиви вказівників на гендл на вузлах дерева - дуже важлива частина БД покликана пришвидшувати пошукові сервіси, які шукають через протокол. Ще одна оптимізація - це відсутність видалення на вузлах цього словника. Якшо протокол видаляється з системи загалом, ми, замість робити страшну процедуру видалення на червоно-чорному дереві, просто занулюємо довжину таблиці вказівників на вузлі, ми навіть не звільняємо сам масив вказівників. Якшо драйвер спробує інсталювати цей протокол знову, все шо нам треба зробити на вузлі, це інкрементувати згадану довжину, і додати в масив вказівник на гендл. А не вставляти вузол знову, який насправді забирає всього пару десятків байтів. Дуже добре. Усе дерево не забере багато навіть разом з масивами вказівників, але пршивдшення пошуку очевидне.
Отже, ми маємо гендли на кожному з яких є таблиця істальованих на нього протоколів, і маємо дерево протоколів, на кожному вузлі якого є таблиця вказівників на гендл, на яких цей протокол істальований. Ця надлишковість дуже пришвидшує пошук за протоколом і не дуже ускладнює сервіси видалення/вставлення.
Які вони? Специфікація встановлює такі:
  • InstallProtocolInterface()
  • ReinstallProtocolInterface()
  • UninstallProtocolInterface()
  • InstallMultipleProtocolInterfaces()
  • UninstallMultipleProtocolInterfaces()
Такі сервіси як OpenProtocol(), OpenProtocolInformation(), CloseProtocol(), ConnectController(), DisconnectController() будуть розглянуті окремо там, де це показуватиме якісь їхні особливості, - з точки зору операцій на БД, вони мають аналогічні ефекти, шо й згадані, лише на своїх таблицях - таблицях агентів. Наприклад OpenProtocol() додає агента, якшо все йде успішно, до таблиці агентів на протоколі, інстальованому на гендлі. CloseProtocol() очевидно робить зворотню роботу. OpenProtocolInformation() читає таблицю агентів. З цим відстежуванням агентів в БД складність полягає не в тому як організована БД за шо ми зараз балакаємо, а з самою семантикою того відстежування - воно заслуговує окремого розгляду. Так як і синхронізація на БД в тому числі.
Функції, які інсталюють або деінсталюють протокольні інтерфейси як вони представлені на рівні специфікації - суть функції, які взагалі будують БД, бо по-перше їхня базова функціональність уже диктує це, а по-друге, специфікація покладає на них неявну роботу з видалення/вставлення всієї ієрархії, наприклад якшо функція InstallProtocolInterface() отримує як значення гендла NULL, то вона має створити гендл, а потім вже інсталювати туди протокол. Так само, - ці функції беруть на себе тягар з побудови імплементцінйо-специфічної структури БД.
Спочатку про словник. Було вже згадано, шо видалення на словнику немає, це насправді дуже вигідно для нашого випадку - фірмваря матиме пам'ять для цього, а от робити зайву роботу не буде. Тож уся складність сервісів видалення - це складність визначена не структурою БД, а семантикою прив'язування агентів до протоколів. Це буде висвітлено окремо.
Переповнення масивів.
Ми вибрали організацію, коли може бути переповнення масивів, і, в разі цього, їх треба розширяти. Це було вибране як кращий варіант, за списки масивів. Розширення робиться спочатку спробою справді розширити виділену ділянку за допомгою внутрішнього сервісу пам'яти, а якшо виділити таке розширення не вдасться, тоді робиться виділення більшого буферу і копіювання старого туди. Це очікується як рідкісна подія. Єдина структура, яка не зможе бути перекопійована - це таблиця гендлів - значення гендлів (як змінних) це адреси структур гендлів в таблиці, якшо ми перенесемо таблицю, зміняться адреси, але клієнти користуються старими гендлами і оповіщати усіх про такі зміни нереалістично. Тобто ми можемо лише спробувати розширити цю таблицю прямо. Внутрішня функція для цього - MiExtendPool(). А взагалі, краще вгадати з кількістю гендлів з самого початку. Це розплата за константний час пошуку гендла.
InstallProtocolInterface(). З усього вище сказаного, витікає, шо більшість роботи пов'язаної з побудовою і перебудовою БД, лягає на цю функцію. Вона інсталює протокол на гендл, якшо треба - створює гендл. А значить, вона має вміти:
  • вставляти протокол в словник (операція на червоно-чорному дереві - не найлегша операція) - "глобальна" інсталяція протоколу,
  • вставляти гендл в таблицю - створення гендла,
  • виділяти таблицю протоколів для новоствореного гендла,
  • вставляти протокол в таблицю протоколів на гендлі - власне інсталяція протокола,
  • вставляти вказівник на гендл в масив вказівників на вузлі дерева;
 а також:
  • розширювати прямо таблицю гендлів,
  • розширювати прямо або копіюванням таблицю протоколів,
  • розширювати прямо або копіюванням масив вказівників на гендл.
"Multiple" варіанти сервісів (останні два в списку), які обробляють багато протоколів одразу, всередині викликають "одиничні" функції. Вони введені для "зручности" написання драйверів. З точки зору обслуговування БД, вони мають ту саму семантику. Варто також зазначити, шо OpenProtocol() робить те саме на таблицях агентів - створює її, додає туди елементи, розширює її прямо або копіюванням.
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 байтів. В разі неуспіху, функція не чіпатиме масив і залишить все як було.
Цей сервіс дасть нам ще один шанс уникнути копіювання. А у випадку з таблицею гендлів - це єдина надія.